mirror of
https://github.com/vlang/v.git
synced 2025-08-04 02:07:28 -04:00
454 lines
12 KiB
V
454 lines
12 KiB
V
module main
|
|
|
|
import markdown
|
|
import os
|
|
import time
|
|
import strings
|
|
import sync
|
|
import runtime
|
|
import document as doc
|
|
import v.vmod
|
|
import v.util
|
|
import json
|
|
import term
|
|
|
|
struct Readme {
|
|
frontmatter map[string]string
|
|
content string
|
|
}
|
|
|
|
enum OutputType {
|
|
unset
|
|
html
|
|
markdown
|
|
json
|
|
ansi // text with ANSI color escapes
|
|
plaintext
|
|
}
|
|
|
|
@[heap]
|
|
struct VDoc {
|
|
cfg Config @[required]
|
|
mut:
|
|
docs []doc.Doc
|
|
assets map[string]string
|
|
manifest vmod.Manifest
|
|
search_index []string
|
|
search_data []SearchResult
|
|
search_module_index []string // search results are split into a module part and the rest
|
|
search_module_data []SearchModuleResult
|
|
example_failures int // how many times an example failed to compile or run with non 0 exit code; when positive, finish with exit code 1
|
|
example_oks int // how many ok examples were found when `-run-examples` was passed, that compiled and finished with 0 exit code.
|
|
}
|
|
|
|
//
|
|
struct Output {
|
|
mut:
|
|
path string
|
|
typ OutputType = .unset
|
|
}
|
|
|
|
struct ParallelDoc {
|
|
d doc.Doc
|
|
out Output
|
|
}
|
|
|
|
fn (vd &VDoc) gen_json(d doc.Doc) string {
|
|
cfg := vd.cfg
|
|
mut jw := strings.new_builder(200)
|
|
jw.write_string('{"module_name":"${d.head.name}",')
|
|
if d.head.comments.len > 0 && cfg.include_comments {
|
|
comments := if cfg.include_examples {
|
|
d.head.merge_comments()
|
|
} else {
|
|
d.head.merge_comments_without_examples()
|
|
}
|
|
jw.write_string('"description":"${escape(comments)}",')
|
|
}
|
|
jw.write_string('"contents":')
|
|
jw.write_string(json.encode(d.contents.keys().map(d.contents[it])))
|
|
jw.write_string(',"generator":"vdoc","time_generated":"${d.time_generated.str()}"}')
|
|
return jw.str()
|
|
}
|
|
|
|
fn (mut vd VDoc) gen_plaintext(d doc.Doc) string {
|
|
cfg := vd.cfg
|
|
mut pw := strings.new_builder(200)
|
|
if cfg.is_color {
|
|
content_arr := d.head.content.split(' ')
|
|
pw.writeln('${term.bright_blue(content_arr[0])} ${term.green(content_arr[1])}')
|
|
} else {
|
|
pw.writeln('${d.head.content}')
|
|
}
|
|
if cfg.include_comments {
|
|
comments := if cfg.include_examples {
|
|
d.head.merge_comments()
|
|
} else {
|
|
d.head.merge_comments_without_examples()
|
|
}
|
|
if comments.trim_space().len > 0 {
|
|
pw.writeln(indent(comments))
|
|
}
|
|
}
|
|
pw.writeln('')
|
|
vd.write_plaintext_content(d.contents.arr(), mut pw)
|
|
return pw.str()
|
|
}
|
|
|
|
fn indent(s string) string {
|
|
return ' ' + s.replace('\n', '\n ')
|
|
}
|
|
|
|
fn dn_to_location(cn doc.DocNode) string {
|
|
location := '${util.path_styled_for_error_messages(cn.file_path)}:${cn.pos.line_nr + 1:-4}'
|
|
if location.len > 24 {
|
|
return '${location:-38s} '
|
|
}
|
|
return '${location:-24s} '
|
|
}
|
|
|
|
fn write_location(cn doc.DocNode, mut pw strings.Builder) {
|
|
pw.write_string(dn_to_location(cn))
|
|
}
|
|
|
|
fn (mut vd VDoc) write_plaintext_content(contents []doc.DocNode, mut pw strings.Builder) {
|
|
cfg := vd.cfg
|
|
for cn in contents {
|
|
if cn.content.len > 0 {
|
|
if cfg.show_loc {
|
|
write_location(cn, mut pw)
|
|
}
|
|
if cfg.is_color {
|
|
pw.writeln(color_highlight(cn.content, vd.docs[0].table))
|
|
} else {
|
|
pw.writeln(cn.content)
|
|
}
|
|
if cn.comments.len > 0 && cfg.include_comments {
|
|
comments := cn.merge_comments_without_examples()
|
|
pw.writeln(indent(comments.trim_space()))
|
|
if cfg.include_examples {
|
|
examples := cn.examples()
|
|
for ex in examples {
|
|
pw.write_string(' Example: ')
|
|
mut fex := ex
|
|
if ex.index_u8(`\n`) >= 0 {
|
|
// multi-line example
|
|
pw.write_u8(`\n`)
|
|
fex = indent(ex)
|
|
}
|
|
if cfg.is_color {
|
|
fex = color_highlight(fex, vd.docs[0].table)
|
|
}
|
|
pw.writeln(fex)
|
|
}
|
|
}
|
|
}
|
|
vd.run_examples(cn, mut pw)
|
|
}
|
|
vd.write_plaintext_content(cn.children, mut pw)
|
|
}
|
|
}
|
|
|
|
fn (mut vd VDoc) render_doc(d doc.Doc, out Output) (string, string) {
|
|
name := vd.get_file_name(d.head.name, out)
|
|
output := match out.typ {
|
|
.html { vd.gen_html(d) }
|
|
.markdown { vd.gen_markdown(d, true) }
|
|
.json { vd.gen_json(d) }
|
|
else { vd.gen_plaintext(d) }
|
|
}
|
|
return name, output
|
|
}
|
|
|
|
// get_file_name returns the final file name from a module name
|
|
fn (vd &VDoc) get_file_name(mod string, out Output) string {
|
|
cfg := vd.cfg
|
|
mut name := mod
|
|
if mod == 'README' {
|
|
name = 'index'
|
|
} else if !cfg.is_multi && !os.is_dir(out.path) {
|
|
name = os.file_name(out.path)
|
|
}
|
|
if name == '' {
|
|
name = 'index'
|
|
}
|
|
name = name + match out.typ {
|
|
.html { '.html' }
|
|
.markdown { '.md' }
|
|
.json { '.json' }
|
|
else { '.txt' }
|
|
}
|
|
return name
|
|
}
|
|
|
|
fn (mut vd VDoc) work_processor(mut work sync.Channel, mut wg sync.WaitGroup) {
|
|
for {
|
|
mut pdoc := ParallelDoc{}
|
|
if !work.pop(&pdoc) {
|
|
break
|
|
}
|
|
vd.vprintln('> start processing ${pdoc.d.base_path} ...')
|
|
flush_stdout()
|
|
file_name, content := vd.render_doc(pdoc.d, pdoc.out)
|
|
output_path := os.join_path(pdoc.out.path, file_name)
|
|
println('Generating ${pdoc.out.typ} in "${output_path}"')
|
|
flush_stdout()
|
|
os.write_file(output_path, content) or { panic(err) }
|
|
}
|
|
wg.done()
|
|
}
|
|
|
|
fn (mut vd VDoc) render_parallel(out Output) {
|
|
vjobs := runtime.nr_jobs()
|
|
mut work := sync.new_channel[ParallelDoc](u32(vd.docs.len))
|
|
mut wg := sync.new_waitgroup()
|
|
for i in 0 .. vd.docs.len {
|
|
p_doc := ParallelDoc{vd.docs[i], out}
|
|
work.push(&p_doc)
|
|
}
|
|
work.close()
|
|
wg.add(vjobs)
|
|
for _ in 0 .. vjobs {
|
|
spawn vd.work_processor(mut work, mut wg)
|
|
}
|
|
wg.wait()
|
|
}
|
|
|
|
fn (mut vd VDoc) render(out Output) map[string]string {
|
|
mut docs := map[string]string{}
|
|
for doc in vd.docs {
|
|
name, output := vd.render_doc(doc, out)
|
|
docs[name] = output.trim_space()
|
|
}
|
|
vd.vprintln('Rendered: ' + docs.keys().str())
|
|
return docs
|
|
}
|
|
|
|
fn (vd &VDoc) get_readme(path string) Readme {
|
|
mut fname := ''
|
|
for name in ['readme.md', 'README.md'] {
|
|
if os.exists(os.join_path(path, name)) {
|
|
fname = name
|
|
break
|
|
}
|
|
}
|
|
if fname == '' {
|
|
if path.all_after_last(os.path_separator) == 'src' {
|
|
return vd.get_readme(path.all_before_last(os.path_separator))
|
|
}
|
|
return Readme{}
|
|
}
|
|
readme_path := os.join_path(path, fname)
|
|
vd.vprintln('Reading README file from ${readme_path}')
|
|
mut readme_contents := os.read_file(readme_path) or { '' }
|
|
mut readme_frontmatter := map[string]string{}
|
|
if readme_contents.starts_with('---\n') {
|
|
if frontmatter_lines_end_idx := readme_contents.index('\n---\n') {
|
|
front_matter_lines := readme_contents#[4..frontmatter_lines_end_idx].trim_space().split_into_lines()
|
|
for line in front_matter_lines {
|
|
x := line.split(': ')
|
|
if x.len == 2 {
|
|
readme_frontmatter[x[0]] = x[1]
|
|
}
|
|
}
|
|
readme_contents = readme_contents#[5 + frontmatter_lines_end_idx..]
|
|
}
|
|
}
|
|
return Readme{
|
|
frontmatter: readme_frontmatter
|
|
content: readme_contents
|
|
}
|
|
}
|
|
|
|
fn (vd &VDoc) emit_generate_err(err IError) {
|
|
cfg := vd.cfg
|
|
mut err_msg := err.msg()
|
|
if err.code() == 1 {
|
|
mod_list := get_modules(cfg.input_path)
|
|
println('Available modules:\n==================')
|
|
for mod in mod_list {
|
|
println(mod.all_after('vlib/').all_after('modules/').replace('/', '.'))
|
|
}
|
|
err_msg += ' Use the `-m` flag when generating docs from a directory that has multiple modules.'
|
|
}
|
|
eprintln(err_msg)
|
|
}
|
|
|
|
fn (mut vd VDoc) generate_docs_from_file() {
|
|
cfg := vd.cfg
|
|
mut out := Output{
|
|
path: cfg.output_path
|
|
typ: cfg.output_type
|
|
}
|
|
if out.path == '' {
|
|
if cfg.output_type == .unset {
|
|
out.typ = .ansi
|
|
} else {
|
|
vd.vprintln('No output path has detected. Using input path instead.')
|
|
out.path = cfg.input_path
|
|
}
|
|
} else if out.typ == .unset {
|
|
vd.vprintln('Output path detected. Identifying output type..')
|
|
ext := os.file_ext(out.path)
|
|
out.typ = set_output_type_from_str(ext.all_after('.'))
|
|
}
|
|
if cfg.include_readme && out.typ !in [.html, .ansi, .plaintext] {
|
|
eprintln('vdoc: Including README.md for doc generation is supported on HTML output, or when running directly in the terminal.')
|
|
exit(1)
|
|
}
|
|
dir_path := if cfg.is_vlib {
|
|
os.join_path(vroot, 'vlib')
|
|
} else if os.is_dir(cfg.input_path) {
|
|
cfg.input_path
|
|
} else {
|
|
os.dir(cfg.input_path)
|
|
}
|
|
manifest_path := if cfg.is_vlib {
|
|
os.join_path(vroot, 'v.mod')
|
|
} else {
|
|
os.join_path(dir_path, 'v.mod')
|
|
}
|
|
if os.exists(manifest_path) {
|
|
vd.vprintln('Reading v.mod info from ${manifest_path}')
|
|
if manifest := vmod.from_file(manifest_path) {
|
|
vd.manifest = manifest
|
|
}
|
|
} else if cfg.is_vlib {
|
|
assert false, 'vdoc: manifest does not exist for vlib'
|
|
}
|
|
if cfg.include_readme || cfg.is_vlib {
|
|
mut readme_name := 'README'
|
|
readme := vd.get_readme(dir_path)
|
|
if page := readme.frontmatter['page'] {
|
|
readme_name = page
|
|
}
|
|
comment := doc.DocComment{
|
|
is_readme: true
|
|
frontmatter: readme.frontmatter
|
|
text: readme.content
|
|
}
|
|
if out.typ == .ansi {
|
|
println(markdown.to_plain(readme.content))
|
|
} else if out.typ == .html && cfg.is_multi {
|
|
vd.docs << doc.Doc{
|
|
head: doc.DocNode{
|
|
is_readme: true
|
|
name: readme_name
|
|
frontmatter: readme.frontmatter
|
|
comments: [comment]
|
|
}
|
|
time_generated: time.now()
|
|
}
|
|
}
|
|
}
|
|
dirs := if cfg.is_multi { get_modules(cfg.input_path) } else { [cfg.input_path] }
|
|
for dirpath in dirs {
|
|
vd.vprintln('Generating ${out.typ} docs for "${dirpath}"')
|
|
mut dcs := doc.generate(dirpath, cfg.pub_only, true, cfg.platform, cfg.symbol_name) or {
|
|
// TODO: use a variable like `src_path := os.join_path(dirpath, 'src')` after `https://github.com/vlang/v/issues/21504`
|
|
if os.exists(os.join_path(dirpath, 'src')) {
|
|
doc.generate(os.join_path(dirpath, 'src'), cfg.pub_only, true, cfg.platform,
|
|
cfg.symbol_name) or {
|
|
vd.emit_generate_err(err)
|
|
exit(1)
|
|
}
|
|
} else {
|
|
vd.emit_generate_err(err)
|
|
exit(1)
|
|
}
|
|
}
|
|
if dcs.contents.len == 0 {
|
|
continue
|
|
}
|
|
if cfg.is_multi || (!cfg.is_multi && cfg.include_readme) {
|
|
readme := vd.get_readme(dirpath)
|
|
comment := doc.DocComment{
|
|
is_readme: true
|
|
frontmatter: readme.frontmatter
|
|
text: readme.content
|
|
}
|
|
dcs.head.comments = [comment]
|
|
}
|
|
if cfg.pub_only {
|
|
for name, dc in dcs.contents {
|
|
dcs.contents[name].content = dc.content.all_after('pub ')
|
|
for i, cc in dc.children {
|
|
dcs.contents[name].children[i].content = cc.content.all_after('pub ')
|
|
}
|
|
}
|
|
}
|
|
vd.docs << dcs
|
|
}
|
|
if dirs.len == 0 && cfg.is_multi {
|
|
eprintln('vdoc: -m requires at least 1 module folder')
|
|
exit(1)
|
|
}
|
|
vd.vprintln('Rendering docs...')
|
|
if out.path == '' || out.path == 'stdout' || out.path == '-' {
|
|
if out.typ == .html {
|
|
vd.render_static_html(out)
|
|
}
|
|
outputs := vd.render(out)
|
|
if outputs.len == 0 {
|
|
if dirs.len == 0 {
|
|
eprintln('vdoc: No documentation found')
|
|
} else {
|
|
eprintln('vdoc: No documentation found for ${dirs[0]}')
|
|
}
|
|
exit(1)
|
|
} else {
|
|
first := outputs.keys()[0]
|
|
println(outputs[first])
|
|
}
|
|
} else {
|
|
if !os.exists(out.path) {
|
|
os.mkdir_all(out.path) or { panic(err) }
|
|
} else if !os.is_dir(out.path) {
|
|
out.path = os.real_path('.')
|
|
}
|
|
if cfg.is_multi {
|
|
out.path = if cfg.input_path == out.path {
|
|
os.join_path(out.path, '_docs')
|
|
} else {
|
|
out.path
|
|
}
|
|
if !os.exists(out.path) {
|
|
os.mkdir(out.path) or { panic(err) }
|
|
} else {
|
|
for fname in css_js_assets {
|
|
existing_asset_path := os.join_path(out.path, fname)
|
|
if os.exists(existing_asset_path) {
|
|
os.rm(existing_asset_path) or { panic(err) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if out.typ == .html {
|
|
vd.render_static_html(out)
|
|
}
|
|
vd.render_parallel(out)
|
|
if out.typ == .html {
|
|
println('Creating search index...')
|
|
vd.collect_search_index(out)
|
|
vd.render_search_index(out)
|
|
// move favicons to target directory
|
|
println('Copying favicons...')
|
|
|
|
favicons_path := os.join_path(cfg.theme_dir, 'favicons')
|
|
|
|
favicons := os.ls(favicons_path) or { panic(err) }
|
|
for favicon in favicons {
|
|
favicon_path := os.join_path(favicons_path, favicon)
|
|
destination_path := os.join_path(out.path, favicon)
|
|
os.cp(favicon_path, destination_path) or { panic(err) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn (vd &VDoc) vprintln(str string) {
|
|
if vd.cfg.is_verbose {
|
|
println('vdoc: ${str}')
|
|
}
|
|
}
|