v/cmd/tools/vdoc/vdoc.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}')
}
}