v / cmd / tools / vdoc / vdoc.v
465 lines · 440 sloc · 12.04 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1module main
2
3import markdown
4import os
5import time
6import strings
7import runtime
8import document as doc
9import v.vmod
10import v.util
11import json
12import term
13
14struct Readme {
15 frontmatter map[string]string
16 content string
17 path string
18}
19
20enum OutputType {
21 unset
22 none
23 html
24 markdown
25 json
26 ansi // text with ANSI color escapes
27 plaintext
28}
29
30@[heap]
31struct VDoc {
32 cfg Config @[required]
33mut:
34 docs []doc.Doc
35 assets map[string]string
36 manifest vmod.Manifest
37 search_index []string
38 search_data []SearchResult
39 search_module_index []string // search results are split into a module part and the rest
40 search_module_data []SearchModuleResult
41 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
42 example_oks int // how many ok examples were found when `-run-examples` was passed, that compiled and finished with 0 exit code.
43}
44
45//
46struct Output {
47mut:
48 path string
49 typ OutputType = .unset
50}
51
52struct ParallelDoc {
53 d doc.Doc
54 out Output
55}
56
57fn (vd &VDoc) gen_json(d doc.Doc) string {
58 cfg := vd.cfg
59 mut jw := strings.new_builder(200)
60 jw.write_string('{"module_name":"${d.head.name}",')
61 if d.head.comments.len > 0 && cfg.include_comments {
62 comments := if cfg.include_examples {
63 d.head.merge_comments()
64 } else {
65 d.head.merge_comments_without_examples()
66 }
67 jw.write_string('"description":"${escape(comments)}",')
68 }
69 jw.write_string('"contents":')
70 jw.write_string(json.encode(d.contents.keys().map(d.contents[it])))
71 jw.write_string(',"generator":"vdoc","time_generated":"${d.time_generated.str()}"}')
72 return jw.str()
73}
74
75fn (mut vd VDoc) gen_plaintext(d doc.Doc) string {
76 cfg := vd.cfg
77 mut pw := strings.new_builder(200)
78 if cfg.is_color {
79 content_arr := d.head.content.split(' ')
80 pw.writeln('${term.bright_blue(content_arr[0])} ${term.green(content_arr[1])}')
81 } else {
82 pw.writeln('${d.head.content}')
83 }
84 if cfg.include_comments {
85 comments := if cfg.include_examples {
86 d.head.merge_comments()
87 } else {
88 d.head.merge_comments_without_examples()
89 }
90 if comments.trim_space().len > 0 {
91 pw.writeln(indent(comments))
92 }
93 }
94 pw.writeln('')
95 vd.write_plaintext_content(d.contents.arr(), mut pw)
96 return pw.str()
97}
98
99fn indent(s string) string {
100 return ' ' + s.replace('\n', '\n ')
101}
102
103fn dn_to_location(cn doc.DocNode) string {
104 location := '${util.path_styled_for_error_messages(cn.file_path)}:${cn.pos.line_nr + 1:-4}'
105 if location.len > 24 {
106 return '${location:-38s} '
107 }
108 return '${location:-24s} '
109}
110
111fn write_location(cn doc.DocNode, mut pw strings.Builder) {
112 pw.write_string(dn_to_location(cn))
113}
114
115fn (mut vd VDoc) write_plaintext_content(contents []doc.DocNode, mut pw strings.Builder) {
116 cfg := vd.cfg
117 for cn in contents {
118 if cn.content.len > 0 {
119 if cfg.show_loc {
120 write_location(cn, mut pw)
121 }
122 if cfg.is_color {
123 pw.writeln(color_highlight(cn.content, vd.docs[0].table))
124 } else {
125 pw.writeln(cn.content)
126 }
127 if cn.comments.len > 0 && cfg.include_comments {
128 comments := cn.merge_comments_without_examples()
129 pw.writeln(indent(comments.trim_space()))
130 if cfg.include_examples {
131 examples := cn.examples()
132 for ex in examples {
133 pw.write_string(' Example: ')
134 mut fex := ex
135 if ex.index_u8(`\n`) >= 0 {
136 // multi-line example
137 pw.write_u8(`\n`)
138 fex = indent(ex)
139 }
140 if cfg.is_color {
141 fex = color_highlight(fex, vd.docs[0].table)
142 }
143 pw.writeln(fex)
144 }
145 }
146 }
147 }
148 vd.write_plaintext_content(cn.children, mut pw)
149 }
150}
151
152fn (mut vd VDoc) render_doc(d doc.Doc, out Output) (string, string) {
153 name := vd.get_file_name(d.head.name, out)
154 output := match out.typ {
155 .none { '' }
156 .html { vd.gen_html(d) }
157 .markdown { vd.gen_markdown(d, true) }
158 .json { vd.gen_json(d) }
159 else { vd.gen_plaintext(d) }
160 }
161
162 contents := d.contents.arr()
163 vd.process_all_examples(contents)
164 return name, output
165}
166
167// get_file_name returns the final file name from a module name
168fn (vd &VDoc) get_file_name(mod string, out Output) string {
169 cfg := vd.cfg
170 mut name := mod
171 if mod == 'README' {
172 name = 'index'
173 } else if !cfg.is_multi && !os.is_dir(out.path) {
174 name = os.file_name(out.path)
175 }
176 if name == '' {
177 name = 'index'
178 }
179 name = name + match out.typ {
180 .html { '.html' }
181 .markdown { '.md' }
182 .json { '.json' }
183 else { '.txt' }
184 }
185
186 return name
187}
188
189fn (mut vd VDoc) work_processor(work chan ParallelDoc) {
190 for {
191 pdoc := <-work or { break }
192 vd.vprintln('> start processing ${pdoc.d.base_path} ...')
193 file_name, content := vd.render_doc(pdoc.d, pdoc.out)
194 if vd.cfg.output_type != .none {
195 output_path := os.join_path(pdoc.out.path, file_name)
196 println('Generating ${content.len:8} bytes of ${pdoc.out.typ} in `${output_path}` ...')
197 os.write_file(output_path, content) or { panic(err) }
198 }
199 }
200}
201
202fn (mut vd VDoc) render_parallel(out Output) {
203 mut work := chan ParallelDoc{cap: vd.docs.len}
204 for i in 0 .. vd.docs.len {
205 work <- ParallelDoc{vd.docs[i], out}
206 }
207 work.close()
208
209 vjobs := runtime.nr_jobs()
210 mut worker_threads := []thread{cap: vjobs}
211 for _ in 0 .. vjobs {
212 worker_threads << spawn vd.work_processor(work)
213 }
214 worker_threads.wait()
215}
216
217fn (mut vd VDoc) render(out Output) map[string]string {
218 mut docs := map[string]string{}
219 for doc in vd.docs {
220 name, output := vd.render_doc(doc, out)
221 docs[name] = output.trim_space()
222 }
223 vd.vprintln('Rendered: ' + docs.keys().str())
224 return docs
225}
226
227fn (vd &VDoc) get_readme(path string) Readme {
228 mut fname := ''
229 for name in ['readme.md', 'README.md'] {
230 if os.exists(os.join_path(path, name)) {
231 fname = name
232 break
233 }
234 }
235 if fname == '' {
236 if path.all_after_last(os.path_separator) == 'src' {
237 next_path := path.all_before_last(os.path_separator)
238 if next_path != '' && path != next_path && os.is_dir(next_path) {
239 return vd.get_readme(next_path)
240 }
241 }
242 return Readme{}
243 }
244 readme_path := os.join_path(path, fname)
245 vd.vprintln('Reading README file from ${readme_path}')
246 mut readme_contents := os.read_file(readme_path) or { '' }
247 mut readme_frontmatter := map[string]string{}
248 if readme_contents.starts_with('---\n') {
249 if frontmatter_lines_end_idx := readme_contents.index('\n---\n') {
250 front_matter_lines :=
251 readme_contents#[4..frontmatter_lines_end_idx].trim_space().split_into_lines()
252 for line in front_matter_lines {
253 x := line.split(': ')
254 if x.len == 2 {
255 readme_frontmatter[x[0]] = x[1]
256 }
257 }
258 readme_contents = readme_contents#[5 + frontmatter_lines_end_idx..]
259 }
260 }
261 return Readme{
262 frontmatter: readme_frontmatter
263 content: readme_contents
264 path: readme_path
265 }
266}
267
268fn (vd &VDoc) emit_generate_err(err IError) {
269 cfg := vd.cfg
270 mut err_msg := err.msg()
271 if err.code() == 1 {
272 mod_list := get_modules(cfg.input_path)
273 println('Available modules:\n==================')
274 for mod in mod_list {
275 println(mod.all_after('vlib/').all_after('modules/').replace('/', '.'))
276 }
277 err_msg += ' Use the `-m` flag when generating docs from a directory that has multiple modules.'
278 }
279 eprintln(err_msg)
280}
281
282fn (mut vd VDoc) generate_docs_from_file() {
283 sw := time.new_stopwatch()
284 defer {
285 if vd.cfg.show_time {
286 println('Generation took: ${sw.elapsed().milliseconds()} ms.')
287 }
288 }
289
290 cfg := vd.cfg
291 mut out := Output{
292 path: cfg.output_path
293 typ: cfg.output_type
294 }
295 if out.path == '' {
296 if cfg.output_type == .unset {
297 out.typ = .ansi
298 } else {
299 vd.vprintln('No output path has detected. Using input path instead.')
300 out.path = cfg.input_path
301 }
302 } else if out.typ == .unset {
303 vd.vprintln('Output path detected. Identifying output type..')
304 ext := os.file_ext(out.path)
305 out.typ = set_output_type_from_str(ext.all_after('.'))
306 }
307 if cfg.include_readme && out.typ !in [.html, .ansi, .plaintext, .none] {
308 eprintln('vdoc: Including README.md for doc generation is supported on HTML output, or when running directly in the terminal.')
309 exit(1)
310 }
311 dir_path := if cfg.is_vlib {
312 os.join_path(vroot, 'vlib')
313 } else if os.is_dir(cfg.input_path) {
314 cfg.input_path
315 } else {
316 os.dir(cfg.input_path)
317 }
318 manifest_path := if cfg.is_vlib {
319 os.join_path(vroot, 'v.mod')
320 } else {
321 os.join_path(dir_path, 'v.mod')
322 }
323 if os.exists(manifest_path) {
324 vd.vprintln('Reading v.mod info from ${manifest_path}')
325 if manifest := vmod.from_file(manifest_path) {
326 vd.manifest = manifest
327 }
328 } else if cfg.is_vlib {
329 assert false, 'vdoc: manifest does not exist for vlib'
330 }
331 if cfg.include_readme || cfg.is_vlib {
332 mut readme_name := 'README'
333 readme := vd.get_readme(dir_path)
334 if page := readme.frontmatter['page'] {
335 readme_name = page
336 }
337 comment := doc.DocComment{
338 is_readme: true
339 frontmatter: readme.frontmatter
340 text: readme.content
341 }
342 if out.typ == .ansi {
343 println(markdown.to_plain(readme.content))
344 } else if out.typ == .html && cfg.is_multi {
345 vd.docs << doc.Doc{
346 head: doc.DocNode{
347 is_readme: true
348 name: readme_name
349 file_path: readme.path
350 frontmatter: readme.frontmatter
351 comments: [comment]
352 }
353 time_generated: time.now()
354 }
355 }
356 }
357 dirs := if cfg.is_multi { get_modules(cfg.input_path) } else { [cfg.input_path] }
358 for dirpath in dirs {
359 vd.vprintln('Generating ${out.typ} docs for "${dirpath}"')
360 mut dcs := doc.generate(dirpath, cfg.pub_only, true, cfg.platform, cfg.symbol_name) or {
361 // TODO: use a variable like `src_path := os.join_path(dirpath, 'src')` after `https://github.com/vlang/v/issues/21504`
362 if os.exists(os.join_path(dirpath, 'src')) {
363 doc.generate(os.join_path(dirpath, 'src'), cfg.pub_only, true, cfg.platform,
364 cfg.symbol_name) or {
365 vd.emit_generate_err(err)
366 exit(1)
367 }
368 } else {
369 vd.emit_generate_err(err)
370 exit(1)
371 }
372 }
373 if cfg.is_multi || (!cfg.is_multi && cfg.include_readme) {
374 readme := vd.get_readme(dirpath)
375 if readme.path != '' {
376 comment := doc.DocComment{
377 is_readme: true
378 frontmatter: readme.frontmatter
379 text: readme.content
380 }
381 dcs.head.comments = [comment]
382 dcs.head.file_path = readme.path
383 }
384 }
385 if cfg.pub_only {
386 for name, dc in dcs.contents {
387 dcs.contents[name].content = dc.content.all_after('pub ')
388 for i, cc in dc.children {
389 dcs.contents[name].children[i].content = cc.content.all_after('pub ')
390 }
391 }
392 }
393 vd.docs << dcs
394 }
395 if dirs.len == 0 && cfg.is_multi {
396 eprintln('vdoc: -m requires at least 1 module folder')
397 exit(1)
398 }
399 vd.vprintln('Rendering docs...')
400 if out.path == '' || out.path == 'stdout' || out.path == '-' {
401 if out.typ == .html {
402 vd.render_static_html(out)
403 }
404 outputs := vd.render(out)
405 if outputs.len == 0 {
406 if dirs.len == 0 {
407 eprintln('vdoc: No documentation found')
408 } else {
409 eprintln('vdoc: No documentation found for ${dirs[0]}')
410 }
411 exit(1)
412 } else {
413 first := outputs.keys()[0]
414 println(outputs[first])
415 }
416 } else {
417 if !os.exists(out.path) {
418 os.mkdir_all(out.path) or { panic(err) }
419 } else if !os.is_dir(out.path) {
420 out.path = os.real_path('.')
421 }
422 if cfg.is_multi {
423 out.path = if cfg.input_path == out.path {
424 os.join_path(out.path, '_docs')
425 } else {
426 out.path
427 }
428 if !os.exists(out.path) {
429 os.mkdir(out.path) or { panic(err) }
430 } else {
431 for fname in css_js_assets {
432 existing_asset_path := os.join_path(out.path, fname)
433 if os.exists(existing_asset_path) {
434 os.rm(existing_asset_path) or { panic(err) }
435 }
436 }
437 }
438 }
439 if out.typ == .html {
440 vd.render_static_html(out)
441 }
442 vd.render_parallel(out)
443 if out.typ == .html {
444 println('Creating search index...')
445 vd.collect_search_index(out)
446 vd.render_search_index(out)
447 // move favicons to target directory
448 println('Copying favicons...')
449 favicons_path := os.join_path(cfg.theme_dir, 'favicons')
450 favicons := os.ls(favicons_path) or { panic(err) }
451 for favicon in favicons {
452 favicon_path := os.join_path(favicons_path, favicon)
453 destination_path := os.join_path(out.path, favicon)
454 os.cp(favicon_path, destination_path) or { panic(err) }
455 }
456 println('Copied ${favicons.len} icons to `${out.path}` .')
457 }
458 }
459}
460
461fn (vd &VDoc) vprintln(str string) {
462 if vd.cfg.is_verbose {
463 println('vdoc: ${str}')
464 }
465}
466