vdoc: heavy refactor, immutable config (#7945)

pull/7960/head
Larpon 2021-01-08 11:25:22 +01:00 committed by GitHub
parent ab8fd063b6
commit b299fb1e92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1055 additions and 971 deletions

View File

@ -143,6 +143,10 @@ pub fn (mut ts TestSession) init() {
ts.benchmark = benchmark.new_benchmark_no_cstep() ts.benchmark = benchmark.new_benchmark_no_cstep()
} }
pub fn (mut ts TestSession) add(file string) {
ts.files << file
}
pub fn (mut ts TestSession) test() { pub fn (mut ts TestSession) test() {
// Ensure that .tmp.c files generated from compiling _test.v files, // Ensure that .tmp.c files generated from compiling _test.v files,
// are easy to delete at the end, *without* affecting the existing ones. // are easy to delete at the end, *without* affecting the existing ones.
@ -307,32 +311,38 @@ pub fn prepare_test_session(zargs string, folder string, oskipped []string, main
files := os.walk_ext(os.join_path(parent_dir, folder), '.v') files := os.walk_ext(os.join_path(parent_dir, folder), '.v')
mut mains := []string{} mut mains := []string{}
mut skipped := oskipped.clone() mut skipped := oskipped.clone()
for f in files { next_file: for f in files {
if !f.contains('modules') && !f.contains('preludes') { if f.contains('modules') || f.contains('preludes') {
// $if !linux { continue
// run pg example only on linux
if f.contains('/pg/') {
continue
}
// }
if f.contains('life_gg') || f.contains('/graph.v') || f.contains('rune.v') {
continue
}
$if windows {
// skip pico example on windows
if f.ends_with('examples\\pico\\pico.v') {
continue
}
}
c := os.read_file(f) or { panic(err) }
maxc := if c.len > 300 { 300 } else { c.len }
start := c[0..maxc]
if start.contains('module ') && !start.contains('module main') {
skipped_f := f.replace(os.join_path(parent_dir, ''), '')
skipped << skipped_f
}
mains << f
} }
// $if !linux {
// run pg example only on linux
if f.contains('/pg/') {
continue
}
// }
if f.contains('life_gg') || f.contains('/graph.v') || f.contains('rune.v') {
continue
}
$if windows {
// skip pico example on windows
if f.ends_with('examples\\pico\\pico.v') {
continue
}
}
c := os.read_file(f) or { panic(err) }
maxc := if c.len > 300 { 300 } else { c.len }
start := c[0..maxc]
if start.contains('module ') && !start.contains('module main') {
skipped_f := f.replace(os.join_path(parent_dir, ''), '')
skipped << skipped_f
}
for skip_prefix in oskipped {
if f.starts_with(skip_prefix) {
continue next_file
}
}
mains << f
} }
session.files << mains session.files << mains
session.skip_files << skipped session.skip_files << skipped

View File

@ -4,23 +4,41 @@ import os
import testing import testing
import v.util import v.util
fn p(s string) string { // NB: tools like vdoc are compiled in their own subfolder
println(s) // => cmd/tools/vdoc/vdoc.exe
return s // Usually, they have several top level .v files in the subfolder,
} // that cannot be compiled separately, but instead, the whole folder,
// should be compiled (v folder).
// To implement that, these folders are initially skipped, then added
// as a whole *after the testing.prepare_test_session call*.
const tools_in_subfolders = ['vdoc']
// non_packaged_tools are tools that should not be packaged with
// prebuild versions of V, to keep the size smaller.
// They are mainly usefull for the V project itself, not to end users.
const non_packaged_tools = ['gen1m', 'gen_vc', 'fast', 'wyhash']
fn main() { fn main() {
util.ensure_modules_for_all_tools_are_installed('-v' in os.args)
args_string := os.args[1..].join(' ') args_string := os.args[1..].join(' ')
skips := []string{}
vexe := os.getenv('VEXE') vexe := os.getenv('VEXE')
vroot := os.dir(vexe) vroot := os.dir(vexe)
util.ensure_modules_for_all_tools_are_installed('-v' in os.args) os.chdir(vroot)
folder := 'cmd/tools' folder := 'cmd/tools'
tfolder := os.join_path(vroot, 'cmd', 'tools')
main_label := 'Building $folder ...' main_label := 'Building $folder ...'
finish_label := 'building $folder' finish_label := 'building $folder'
mut session := testing.prepare_test_session(args_string.all_before('build-tools'), //
folder, skips, main_label) mut skips := []string{}
for stool in tools_in_subfolders {
skips << os.join_path(tfolder, stool)
}
buildopts := args_string.all_before('build-tools')
mut session := testing.prepare_test_session(buildopts, folder, skips, main_label)
session.rm_binaries = false session.rm_binaries = false
for stool in tools_in_subfolders {
session.add(os.join_path(tfolder, stool))
}
session.test() session.test()
eprintln(session.benchmark.total_message(finish_label)) eprintln(session.benchmark.total_message(finish_label))
if session.failed { if session.failed {
@ -29,9 +47,17 @@ fn main() {
// //
mut executables := os.ls(session.vtmp_dir) ? mut executables := os.ls(session.vtmp_dir) ?
executables.sort() executables.sort()
executables = executables.filter(it !in ['gen1m', 'gen_vc', 'fast', 'wyhash']) for texe in executables {
for exe in executables { tname := texe.replace(os.file_ext(texe), '')
os.mv_by_cp(os.join_path(session.vtmp_dir, exe), os.join_path(vroot, 'cmd', 'tools', if tname in non_packaged_tools {
exe)) continue
}
//
tpath := os.join_path(session.vtmp_dir, texe)
if tname in tools_in_subfolders {
os.mv_by_cp(tpath, os.join_path(tfolder, tname, texe))
continue
}
os.mv_by_cp(tpath, os.join_path(tfolder, texe))
} }
} }

View File

@ -0,0 +1,522 @@
module main
import os
import net.urllib
import strings
import markdown
import v.scanner
import v.table
import v.token
import v.doc
import v.pref
const (
css_js_assets = ['doc.css', 'normalize.css', 'doc.js', 'dark-mode.js']
res_path = os.resource_abs_path('resources')
favicons_path = os.join_path(res_path, 'favicons')
html_content = '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="x-ua-compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} | vdoc</title>
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Work+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
<link rel="manifest" href="site.webmanifest">
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="theme-color" content="#ffffff">
{{ head_assets }}
</head>
<body>
<div id="page">
<header class="doc-nav hidden">
<div class="heading-container">
<div class="heading">
<input type="text" id="search" placeholder="Search... (beta)" autocomplete="off">
<div class="module">{{ head_name }}</div>
<div class="toggle-version-container">
<span>{{ version }}</span>
<div id="dark-mode-toggle" role="switch" aria-checked="false" aria-label="Toggle dark mode">{{ light_icon }}{{ dark_icon }}</div>
</div>
{{ menu_icon }}
</div>
</div>
<nav class="search"></nav>
<nav class="content hidden">
<ul>
{{ toc_links }}
</ul>
</nav>
</header>
<div class="doc-scrollview">
<div class="doc-container">
<div class="doc-content">
{{ contents }}
<div class="footer">
{{ footer_content }}
</div>
</div>
{{ right_content }}
</div>
</div>
</div>
{{ footer_assets }}
<script async src="search_index.js" type="text/javascript"></script>
</body>
</html>'
)
enum HighlightTokenTyp {
unone
boolean
builtin
char
comment
function
keyword
name
number
operator
punctuation
string
symbol
}
struct SearchModuleResult {
description string
link string
}
struct SearchResult {
prefix string
badge string
description string
link string
}
fn (vd VDoc) render_search_index(out Output) {
mut js_search_index := strings.new_builder(200)
mut js_search_data := strings.new_builder(200)
js_search_index.write('var searchModuleIndex = [')
js_search_data.write('var searchModuleData = [')
for i, title in vd.search_module_index {
data := vd.search_module_data[i]
js_search_index.write('"$title",')
js_search_data.write('["$data.description","$data.link"],')
}
js_search_index.writeln('];')
js_search_index.write('var searchIndex = [')
js_search_data.writeln('];')
js_search_data.write('var searchData = [')
for i, title in vd.search_index {
data := vd.search_data[i]
js_search_index.write('"$title",')
// array instead of object to reduce file size
js_search_data.write('["$data.badge","$data.description","$data.link","$data.prefix"],')
}
js_search_index.writeln('];')
js_search_data.writeln('];')
out_file_path := os.join_path(out.path, 'search_index.js')
os.write_file(out_file_path, js_search_index.str() + js_search_data.str())
}
fn (mut vd VDoc) render_static_html(serve_via_http bool, out Output) {
vd.assets = {
'doc_css': vd.get_resource(css_js_assets[0], true, out)
'normalize_css': vd.get_resource(css_js_assets[1], true, out)
'doc_js': vd.get_resource(css_js_assets[2], !serve_via_http, out)
'dark_mode_js': vd.get_resource(css_js_assets[3], !serve_via_http, out)
'light_icon': vd.get_resource('light.svg', true, out)
'dark_icon': vd.get_resource('dark.svg', true, out)
'menu_icon': vd.get_resource('menu.svg', true, out)
'arrow_icon': vd.get_resource('arrow.svg', true, out)
}
}
fn (vd VDoc) get_resource(name string, minify bool, out Output) string {
cfg := vd.cfg
path := os.join_path(res_path, name)
mut res := os.read_file(path) or { panic('vdoc: could not read $path') }
if minify {
if name.ends_with('.js') {
res = js_compress(res)
} else {
res = res.split_into_lines().map(it.trim_space()).join('')
}
}
// TODO: Make SVG inline for now
if cfg.inline_assets || path.ends_with('.svg') {
return res
} else {
output_path := os.join_path(out.path, name)
if !os.exists(output_path) {
println('Generating $out.typ in "$output_path"')
os.write_file(output_path, res)
}
return name
}
}
fn (mut vd VDoc) collect_search_index(out Output) {
cfg := vd.cfg
for doc in vd.docs {
mod := doc.head.name
vd.search_module_index << mod
comments := if cfg.include_examples {
doc.head.merge_comments()
} else {
doc.head.merge_comments_without_examples()
}
vd.search_module_data << SearchModuleResult{
description: trim_doc_node_description(comments)
link: vd.get_file_name(mod, out)
}
for _, dn in doc.contents {
vd.create_search_results(mod, dn, out)
}
}
}
fn (mut vd VDoc) create_search_results(mod string, dn doc.DocNode, out Output) {
cfg := vd.cfg
if dn.kind == .const_group {
return
}
comments := if cfg.include_examples {
dn.merge_comments()
} else {
dn.merge_comments_without_examples()
}
dn_description := trim_doc_node_description(comments)
vd.search_index << dn.name
vd.search_data << SearchResult{
prefix: if dn.parent_name != '' {
'$dn.kind ($dn.parent_name)'
} else {
'$dn.kind '
}
description: dn_description
badge: mod
link: vd.get_file_name(mod, out) + '#' + get_node_id(dn)
}
for child in dn.children {
vd.create_search_results(mod, child, out)
}
}
fn (vd VDoc) write_content(cn &doc.DocNode, d &doc.Doc, mut hw strings.Builder) {
cfg := vd.cfg
base_dir := os.dir(os.real_path(cfg.input_path))
file_path_name := if cfg.is_multi {
cn.file_path.replace('$base_dir/', '')
} else {
os.file_name(cn.file_path)
}
src_link := get_src_link(vd.manifest.repo_url, file_path_name, cn.pos.line)
if cn.content.len != 0 || (cn.name == 'Constants') {
hw.write(doc_node_html(cn, src_link, false, cfg.include_examples, d.table))
}
for child in cn.children {
child_file_path_name := child.file_path.replace('$base_dir/', '')
child_src_link := get_src_link(vd.manifest.repo_url, child_file_path_name, child.pos.line)
hw.write(doc_node_html(child, child_src_link, false, cfg.include_examples, d.table))
}
}
fn (vd VDoc) gen_html(d doc.Doc) string {
cfg := vd.cfg
mut symbols_toc := strings.new_builder(200)
mut modules_toc := strings.new_builder(200)
mut contents := strings.new_builder(200)
dcs_contents := d.contents.arr()
// generate toc first
contents.writeln(doc_node_html(d.head, '', true, cfg.include_examples, d.table))
if is_module_readme(d.head) {
write_toc(d.head, mut symbols_toc)
}
for cn in dcs_contents {
vd.write_content(&cn, &d, mut contents)
write_toc(cn, mut symbols_toc)
} // write head
// write css
version := if vd.manifest.version.len != 0 { vd.manifest.version } else { '' }
header_name := if cfg.is_multi && vd.docs.len > 1 {
os.file_name(os.real_path(cfg.input_path))
} else {
d.head.name
}
// write nav1
if cfg.is_multi || vd.docs.len > 1 {
mut submod_prefix := ''
for i, dc in vd.docs {
if i - 1 >= 0 && dc.head.name.starts_with(submod_prefix + '.') {
continue
}
names := dc.head.name.split('.')
submod_prefix = if names.len > 1 { names[0] } else { dc.head.name }
mut href_name := './${dc.head.name}.html'
if (cfg.is_vlib && dc.head.name == 'builtin' && !cfg.include_readme) ||
dc.head.name == 'README' {
href_name = './index.html'
} else if submod_prefix !in vd.docs.map(it.head.name) {
href_name = '#'
}
submodules := vd.docs.filter(it.head.name.starts_with(submod_prefix + '.'))
dropdown := if submodules.len > 0 { vd.assets['arrow_icon'] } else { '' }
active_class := if dc.head.name == d.head.name { ' active' } else { '' }
modules_toc.write('<li class="open$active_class"><div class="menu-row">$dropdown<a href="$href_name">$submod_prefix</a></div>')
for j, cdoc in submodules {
if j == 0 {
modules_toc.write('<ul>')
}
submod_name := cdoc.head.name.all_after(submod_prefix + '.')
sub_selected_classes := if cdoc.head.name == d.head.name {
' class="active"'
} else {
''
}
modules_toc.write('<li$sub_selected_classes><a href="./${cdoc.head.name}.html">$submod_name</a></li>')
if j == submodules.len - 1 {
modules_toc.write('</ul>')
}
}
modules_toc.write('</li>')
}
}
modules_toc_str := modules_toc.str()
symbols_toc_str := symbols_toc.str()
modules_toc.free()
symbols_toc.free()
return html_content.replace('{{ title }}', d.head.name).replace('{{ head_name }}',
header_name).replace('{{ version }}', version).replace('{{ light_icon }}', vd.assets['light_icon']).replace('{{ dark_icon }}',
vd.assets['dark_icon']).replace('{{ menu_icon }}', vd.assets['menu_icon']).replace('{{ head_assets }}',
if cfg.inline_assets {
'\n${tabs[0]}<style>' + vd.assets['doc_css'] + '</style>\n${tabs[0]}<style>' + vd.assets['normalize_css'] +
'</style>\n${tabs[0]}<script>' + vd.assets['dark_mode_js'] + '</script>'
} else {
'\n${tabs[0]}<link rel="stylesheet" href="' + vd.assets['doc_css'] + '" />\n${tabs[0]}<link rel="stylesheet" href="' +
vd.assets['normalize_css'] + '" />\n${tabs[0]}<script src="' + vd.assets['dark_mode_js'] + '"></script>'
}).replace('{{ toc_links }}', if cfg.is_multi || vd.docs.len > 1 {
modules_toc_str
} else {
symbols_toc_str
}).replace('{{ contents }}', contents.str()).replace('{{ right_content }}', if cfg.is_multi &&
vd.docs.len > 1 && d.head.name != 'README' {
'<div class="doc-toc"><ul>' + symbols_toc_str + '</ul></div>'
} else {
''
}).replace('{{ footer_content }}', gen_footer_text(d, !cfg.no_timestamp)).replace('{{ footer_assets }}',
if cfg.inline_assets {
'<script>' + vd.assets['doc_js'] + '</script>'
} else {
'<script src="' + vd.assets['doc_js'] + '"></script>'
})
}
fn get_src_link(repo_url string, file_name string, line_nr int) string {
mut url := urllib.parse(repo_url) or { return '' }
if url.path.len <= 1 || file_name.len == 0 {
return ''
}
url.path = url.path.trim_right('/') + match url.host {
'github.com' { '/blob/master/$file_name' }
'gitlab.com' { '/-/blob/master/$file_name' }
'git.sir.ht' { '/tree/master/$file_name' }
else { '' }
}
if url.path == '/' {
return ''
}
url.fragment = 'L$line_nr'
return url.str()
}
fn html_highlight(code string, tb &table.Table) string {
builtin := ['bool', 'string', 'i8', 'i16', 'int', 'i64', 'i128', 'byte', 'u16', 'u32', 'u64',
'u128', 'rune', 'f32', 'f64', 'any_int', 'any_float', 'byteptr', 'voidptr', 'any']
highlight_code := fn (tok token.Token, typ HighlightTokenTyp) string {
lit := if typ in [.unone, .operator, .punctuation] {
tok.kind.str()
} else if typ == .string {
"'$tok.lit'"
} else if typ == .char {
'`$tok.lit`'
} else {
tok.lit
}
return if typ in [.unone, .name] {
lit
} else {
'<span class="token $typ">$lit</span>'
}
}
mut s := scanner.new_scanner(code, .parse_comments, &pref.Preferences{})
mut tok := s.scan()
mut next_tok := s.scan()
mut buf := strings.new_builder(200)
mut i := 0
for i < code.len {
if i == tok.pos {
mut tok_typ := HighlightTokenTyp.unone
match tok.kind {
.name {
if tok.lit in builtin || tb.known_type(tok.lit) {
tok_typ = .builtin
} else if next_tok.kind == .lcbr {
tok_typ = .symbol
} else if next_tok.kind == .lpar {
tok_typ = .function
} else {
tok_typ = .name
}
}
.comment {
tok_typ = .comment
}
.chartoken {
tok_typ = .char
}
.string {
tok_typ = .string
}
.number {
tok_typ = .number
}
.key_true, .key_false {
tok_typ = .boolean
}
.lpar, .lcbr, .rpar, .rcbr, .lsbr, .rsbr, .semicolon, .colon, .comma, .dot {
tok_typ = .punctuation
}
else {
if token.is_key(tok.lit) || token.is_decl(tok.kind) {
tok_typ = .keyword
} else if tok.kind == .decl_assign || tok.kind.is_assign() || tok.is_unary() ||
tok.kind.is_relational() || tok.kind.is_infix() {
tok_typ = .operator
}
}
}
buf.write(highlight_code(tok, tok_typ))
if next_tok.kind != .eof {
i = tok.pos + tok.len
tok = next_tok
next_tok = s.scan()
} else {
break
}
} else {
buf.write_b(code[i])
i++
}
}
return buf.str()
}
fn doc_node_html(dn doc.DocNode, link string, head bool, include_examples bool, tb &table.Table) string {
mut dnw := strings.new_builder(200)
link_svg := '<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>'
head_tag := if head { 'h1' } else { 'h2' }
comments := dn.merge_comments_without_examples()
md_content := markdown.to_html(html_tag_escape(comments))
hlighted_code := html_highlight(dn.content, tb)
node_class := if dn.kind == .const_group { ' const' } else { '' }
sym_name := get_sym_name(dn)
mut node_id := get_node_id(dn)
mut hash_link := if !head { ' <a href="#$node_id">#</a>' } else { '' }
if head && is_module_readme(dn) {
node_id = 'readme_$node_id'
hash_link = ' <a href="#$node_id">#</a>'
}
dnw.writeln('${tabs[1]}<section id="$node_id" class="doc-node$node_class">')
if dn.name.len > 0 {
if dn.kind == .const_group {
dnw.write('${tabs[2]}<div class="title"><$head_tag>$sym_name$hash_link</$head_tag>')
} else {
dnw.write('${tabs[2]}<div class="title"><$head_tag>$dn.kind $sym_name$hash_link</$head_tag>')
}
if link.len != 0 {
dnw.write('<a class="link" rel="noreferrer" target="_blank" href="$link">$link_svg</a>')
}
dnw.write('</div>')
}
if !head && dn.content.len > 0 {
dnw.writeln('<pre class="signature"><code>$hlighted_code</code></pre>')
}
// do not mess with md_content further, its formatting is important, just output it 1:1 !
dnw.writeln('$md_content\n')
// Write examples if any found
examples := dn.examples()
if include_examples && examples.len > 0 {
example_title := if examples.len > 1 { 'Examples' } else { 'Example' }
dnw.writeln('<section class="doc-node examples"><h4>$example_title</h4>')
for example in examples {
// hl_example := html_highlight(example, tb)
dnw.writeln('<pre><code class="language-v">$example</code></pre>')
}
dnw.writeln('</section>')
}
dnw.writeln('</section>')
dnw_str := dnw.str()
defer {
dnw.free()
}
return dnw_str
}
fn html_tag_escape(str string) string {
return str.replace_each(['<', '&lt;', '>', '&gt;'])
}
fn js_compress(str string) string {
mut js := strings.new_builder(200)
lines := str.split_into_lines()
rules := [') {', ' = ', ', ', '{ ', ' }', ' (', '; ', ' + ', ' < ', ' - ', ' || ', ' var',
': ', ' >= ', ' && ', ' else if', ' === ', ' !== ', ' else ']
clean := ['){', '=', ',', '{', '}', '(', ';', '+', '<', '-', '||', 'var', ':', '>=', '&&',
'else if', '===', '!==', 'else']
for line in lines {
mut trimmed := line.trim_space()
if trimmed.starts_with('//') || (trimmed.starts_with('/*') && trimmed.ends_with('*/')) {
continue
}
for i in 0 .. rules.len - 1 {
trimmed = trimmed.replace(rules[i], clean[i])
}
js.write(trimmed)
}
js_str := js.str()
js.free()
return js_str
}
fn write_toc(dn doc.DocNode, mut toc strings.Builder) {
mut toc_slug := if dn.name.len == 0 || dn.content.len == 0 { '' } else { slug(dn.name) }
if toc_slug == '' && dn.children.len > 0 {
if dn.children[0].name == '' {
toc_slug = slug(dn.name)
} else {
toc_slug = slug(dn.name + '.' + dn.children[0].name)
}
}
if is_module_readme(dn) {
toc.write('<li class="open"><a href="#readme_$toc_slug">README</a>')
} else if dn.name != 'Constants' {
toc.write('<li class="open"><a href="#$toc_slug">$dn.kind $dn.name</a>')
toc.writeln(' <ul>')
for child in dn.children {
cname := dn.name + '.' + child.name
toc.writeln('<li><a href="#${slug(cname)}">$child.kind $child.name</a></li>')
}
toc.writeln('</ul>')
} else {
toc.write('<li class="open"><a href="#$toc_slug">$dn.name</a>')
}
toc.writeln('</li>')
}

View File

@ -0,0 +1,95 @@
module main
import io
import net
import strings
fn (mut vd VDoc) serve_html(out Output) {
cfg := vd.cfg
if out.typ == .html {
vd.render_static_html(true, out)
}
docs := vd.render(out)
dkeys := docs.keys()
if dkeys.len < 1 {
eprintln('no documentation created, the module has no `pub` functions')
exit(1)
}
def_name := docs.keys()[0]
server_url := 'http://localhost:' + cfg.server_port.str()
server := net.listen_tcp(cfg.server_port) or { panic(err) }
println('Serving docs on: $server_url')
if cfg.open_docs {
open_url(server_url)
}
content_type := match out.typ {
.html { 'text/html' }
.markdown { 'text/markdown' }
.json { 'application/json' }
else { 'text/plain' }
}
server_context := VdocHttpServerContext{
docs: docs
content_type: content_type
default_filename: def_name
}
for {
mut conn := server.accept() or {
server.close() or { }
panic(err)
}
handle_http_connection(mut conn, server_context)
conn.close() or { eprintln('error closing the connection: $err') }
}
}
struct VdocHttpServerContext {
docs map[string]string
content_type string
default_filename string
}
fn handle_http_connection(mut con net.TcpConn, ctx &VdocHttpServerContext) {
mut reader := io.new_buffered_reader(reader: io.make_reader(con))
first_line := reader.read_line() or {
send_http_response(mut con, 501, ctx.content_type, 'bad request')
return
}
request_parts := first_line.split(' ')
if request_parts.len != 3 {
send_http_response(mut con, 501, ctx.content_type, 'bad request')
return
}
urlpath := request_parts[1]
filename := if urlpath == '/' {
ctx.default_filename.trim_left('/')
} else {
urlpath.trim_left('/')
}
if ctx.docs[filename].len == 0 {
send_http_response(mut con, 404, ctx.content_type, 'file not found')
return
}
send_http_response(mut con, 200, ctx.content_type, ctx.docs[filename])
}
fn send_http_response(mut con net.TcpConn, http_code int, content_type string, html string) {
content_length := html.len.str()
shttp_code := http_code.str()
mut http_response := strings.new_builder(20000)
http_response.write('HTTP/1.1 ')
http_response.write(shttp_code)
http_response.write(' OK\r\n')
http_response.write('Server: VDoc\r\n')
http_response.write('Content-Type: ')
http_response.write(content_type)
http_response.write('\r\n')
http_response.write('Content-Length: ')
http_response.write(content_length)
http_response.write('\r\n')
http_response.write('Connection: close\r\n')
http_response.write('\r\n')
http_response.write(html)
sresponse := http_response.str()
con.write_str(sresponse) or { eprintln('error sending http response: $err') }
}

View File

@ -0,0 +1,50 @@
module main
import strings
import v.doc
fn (vd VDoc) gen_markdown(d doc.Doc, with_toc bool) string {
mut hw := strings.new_builder(200)
mut cw := strings.new_builder(200)
hw.writeln('# $d.head.content\n')
if d.head.comments.len > 0 {
comments := if vd.cfg.include_examples {
d.head.merge_comments()
} else {
d.head.merge_comments_without_examples()
}
hw.writeln('$comments\n')
}
if with_toc {
hw.writeln('## Contents')
}
vd.write_markdown_content(d.contents.arr(), mut cw, mut hw, 0, with_toc)
footer_text := gen_footer_text(d, !vd.cfg.no_timestamp)
cw.writeln('#### $footer_text')
return hw.str() + '\n' + cw.str()
}
fn (vd VDoc) write_markdown_content(contents []doc.DocNode, mut cw strings.Builder, mut hw strings.Builder, indent int, with_toc bool) {
for cn in contents {
if with_toc && cn.name.len > 0 {
hw.writeln(' '.repeat(2 * indent) + '- [#$cn.name](${slug(cn.name)})')
cw.writeln('## $cn.name')
}
if cn.content.len > 0 {
comments := cn.merge_comments_without_examples()
cw.writeln('```v\n$cn.content\n```\n$comments\n')
// Write examples if any found
examples := cn.examples()
if vd.cfg.include_examples && examples.len > 0 {
example_title := if examples.len > 1 { 'Examples' } else { 'Example' }
cw.writeln('$example_title\n```v\n')
for example in examples {
cw.writeln('$example\n')
}
cw.writeln('```\n')
}
cw.writeln('[\[Return to contents\]](#Contents)\n')
}
vd.write_markdown_content(cn.children, mut cw, mut hw, indent + 1, with_toc)
}
}

View File

@ -0,0 +1,145 @@
module main
import os
import v.doc
[inline]
fn slug(title string) string {
return title.replace(' ', '-')
}
[inline]
fn open_url(url string) {
$if windows {
os.system('start $url')
}
$if macos {
os.system('open $url')
}
$if linux {
os.system('xdg-open $url')
}
}
fn escape(str string) string {
return str.replace_each(['"', '\\"', '\r\n', '\\n', '\n', '\\n', '\t', '\\t'])
}
fn get_sym_name(dn doc.DocNode) string {
sym_name := if dn.parent_name.len > 0 && dn.parent_name != 'void' {
'($dn.parent_name) $dn.name'
} else {
dn.name
}
return sym_name
}
fn get_node_id(dn doc.DocNode) string {
tag := if dn.parent_name.len > 0 && dn.parent_name != 'void' {
'${dn.parent_name}.$dn.name'
} else {
dn.name
}
return slug(tag)
}
fn is_module_readme(dn doc.DocNode) bool {
if dn.comments.len > 0 && dn.content == 'module $dn.name' {
return true
}
return false
}
fn trim_doc_node_description(description string) string {
mut dn_description := description.replace_each(['\r\n', '\n', '"', '\\"'])
// 80 is enough to fill one line
if dn_description.len > 80 {
dn_description = dn_description[..80]
}
if '\n' in dn_description {
dn_description = dn_description.split('\n')[0]
}
// if \ is last character, it ends with \" which leads to a JS error
if dn_description.ends_with('\\') {
dn_description = dn_description.trim_right('\\')
}
return dn_description
}
fn set_output_type_from_str(format string) OutputType {
output_type := match format {
'htm', 'html' { OutputType.html }
'md', 'markdown' { OutputType.markdown }
'json' { OutputType.json }
'stdout' { OutputType.stdout }
else { OutputType.plaintext }
}
return output_type
}
fn get_ignore_paths(path string) ?[]string {
ignore_file_path := os.join_path(path, '.vdocignore')
ignore_content := os.read_file(ignore_file_path) or {
return error_with_code('ignore file not found.', 1)
}
mut res := []string{}
if ignore_content.trim_space().len > 0 {
rules := ignore_content.split_into_lines().map(it.trim_space())
mut final := []string{}
for rule in rules {
if rule.contains('*.') || rule.contains('**') {
println('vdoc: Wildcards in ignore rules are not allowed for now.')
continue
}
final << rule
}
res = final.map(os.join_path(path, it.trim_right('/')))
} else {
mut dirs := os.ls(path) or { return []string{} }
res = dirs.map(os.join_path(path, it)).filter(os.is_dir(it))
}
return res.map(it.replace('/', os.path_separator))
}
fn is_included(path string, ignore_paths []string) bool {
if path.len == 0 {
return true
}
for ignore_path in ignore_paths {
if ignore_path !in path {
continue
}
return false
}
return true
}
fn get_modules_list(path string, ignore_paths2 []string) []string {
files := os.ls(path) or { return []string{} }
mut ignore_paths := get_ignore_paths(path) or { []string{} }
ignore_paths << ignore_paths2
mut dirs := []string{}
for file in files {
fpath := os.join_path(path, file)
if os.is_dir(fpath) && is_included(fpath, ignore_paths) && !os.is_link(path) {
dirs << get_modules_list(fpath, ignore_paths.filter(it.starts_with(fpath)))
} else if fpath.ends_with('.v') && !fpath.ends_with('_test.v') {
if path in dirs {
continue
}
dirs << path
}
}
dirs.sort()
return dirs
}
fn gen_footer_text(d &doc.Doc, include_timestamp bool) string {
footer_text := 'Powered by vdoc.'
if !include_timestamp {
return footer_text
}
generated_time := d.time_generated
time_str := '$generated_time.day $generated_time.smonth() $generated_time.year $generated_time.hhmmss()'
return '$footer_text Generated on: $time_str'
}

File diff suppressed because it is too large Load Diff

View File

@ -18,8 +18,8 @@ const (
'nonexistant', 'nonexistant',
] ]
vfmt_verify_list = [ vfmt_verify_list = [
'cmd/tools/vdoc/vdoc.v',
'cmd/v/v.v', 'cmd/v/v.v',
'cmd/tools/vdoc/',
'vlib/arrays/', 'vlib/arrays/',
'vlib/benchmark/', 'vlib/benchmark/',
'vlib/bitfield/', 'vlib/bitfield/',

View File

@ -34,7 +34,7 @@ fn v_test_compiler(vargs string) {
eprintln('v.c can be compiled without warnings. This is good :)') eprintln('v.c can be compiled without warnings. This is good :)')
} }
} }
building_tools_failed := testing.v_build_failing(vargs, 'cmd/tools') building_tools_failed := os.system('"$vexe" build-tools') != 0
eprintln('') eprintln('')
testing.eheader('Testing all _test.v files...') testing.eheader('Testing all _test.v files...')
mut compiler_test_session := testing.new_test_session(vargs) mut compiler_test_session := testing.new_test_session(vargs)