v / cmd / tools / vcheck-md.v
930 lines · 871 sloc · 23.15 KB · 8e35f4d9848f7ad35d857a187dddbfd2eca5e19d
Raw
1// Copyright (c) 2019-2024 Alexander Medvednikov. All rights reserved.
2// Use of this source code is governed by an MIT license
3// that can be found in the LICENSE file.
4module main
5
6import os
7import os.cmdline
8import rand
9import term
10import v.help
11import regex
12
13const too_long_line_length_example = 120
14const too_long_line_length_codeblock = 120
15const too_long_line_length_table = 160
16const too_long_line_length_link = 250
17const too_long_line_length_other = 100
18const term_colors = term.can_show_color_on_stderr()
19const hide_warnings = '-hide-warnings' in os.args || '-w' in os.args
20const show_progress = os.getenv('GITHUB_JOB') == '' && '-silent' !in os.args
21const non_option_args = cmdline.only_non_options(os.args[2..])
22const is_verbose = os.getenv('VERBOSE') != ''
23const vcheckfolder = os.join_path(os.vtmp_dir(), 'vcheck_${os.getpid()}')
24const should_autofix = os.getenv('VAUTOFIX') != '' || '-fix' in os.args
25const vexe = @VEXE
26
27struct CheckResult {
28pub mut:
29 files int
30 lines int
31 examples int
32 oks int
33 warnings int
34 ferrors int
35 errors int
36}
37
38struct VCheckIgnoreRule {
39 base_dir string
40 pattern string
41}
42
43struct VCheckIgnoreContext {
44 repo_root string
45}
46
47struct VCheckIgnoreMatch {
48 ignore_file string
49 pattern string
50}
51
52struct MDPathScanResult {
53 files []string
54 skipped int
55}
56
57fn (v1 CheckResult) + (v2 CheckResult) CheckResult {
58 return CheckResult{
59 files: v1.files + v2.files
60 lines: v1.lines + v2.lines
61 examples: v1.examples + v2.examples
62 oks: v1.oks + v2.oks
63 warnings: v1.warnings + v2.warnings
64 ferrors: v1.ferrors + v2.ferrors
65 errors: v1.errors + v2.errors
66 }
67}
68
69fn main() {
70 unbuffer_stdout()
71 if non_option_args.len == 0 || '-help' in os.args {
72 help.print_and_exit('check-md')
73 }
74 if '-all' in os.args {
75 println('´-all´ flag is deprecated. Please use ´v check-md .´ instead.')
76 exit(1)
77 }
78 mut skip_line_length_check := '-skip-line-length-check' in os.args
79 mut files_paths := non_option_args.clone()
80 mut res := CheckResult{}
81 if term_colors {
82 os.setenv('VCOLORS', 'always', true)
83 }
84 os.mkdir_all(vcheckfolder, mode: 0o700) or {} // keep directory private
85 defer {
86 os.rmdir_all(vcheckfolder) or {}
87 }
88 mut all_mdfiles := []MDFile{}
89 mut skipped_mdfiles := 0
90 for i := 0; i < files_paths.len; i++ {
91 file_path := files_paths[i]
92 if os.is_dir(file_path) {
93 scan_result := md_file_paths(file_path)
94 files_paths << scan_result.files
95 skipped_mdfiles += scan_result.skipped
96 continue
97 }
98 real_path := os.real_path(file_path)
99 lines := os.read_lines(real_path) or {
100 println('"${file_path}" does not exist')
101 res.warnings++
102 continue
103 }
104 all_mdfiles << MDFile{
105 skip_line_length_check: skip_line_length_check
106 path: file_path
107 lines: lines
108 }
109 }
110 println('> Found: ${all_mdfiles.len} .md files. Skipped by .vcheckignore: ${skipped_mdfiles}.')
111 if is_verbose {
112 for idx, mdfile in all_mdfiles {
113 println('> file ${idx + 1} is ${mdfile.path}')
114 }
115 }
116 if show_progress {
117 // this is intended to be replaced by the progress lines
118 println('')
119 }
120 for idx, mut mdfile in all_mdfiles {
121 mdfile.idx = idx
122 mdfile.nfiles = all_mdfiles.len
123 res += mdfile.check()
124 }
125 if res.errors == 0 && show_progress {
126 clear_previous_line()
127 }
128 println('Checked .md files: ${res.files} | Ex.: ${res.examples} | Lines: ${res.lines} | OKs: ${res.oks} | Warnings: ${res.warnings} | Errors: ${res.errors} | Fmt errors: ${res.ferrors}')
129 if res.ferrors > 0 && !should_autofix {
130 println('Note: you can use `VAUTOFIX=1 v check-md file.md`, or `v check-md -fix file.md`,')
131 println(' to fix the V formatting errors in the markdown code blocks, when possible.')
132 println(' Run the command 2 times, to verify that all formatting errors were fixed.')
133 println('Note: `v help check-md` shows a list of ```v fence keywords (for partial code).')
134 }
135 if res.errors > 0 {
136 exit(1)
137 }
138}
139
140fn md_file_paths(dir string) MDPathScanResult {
141 mut files_to_check := []string{}
142 mut skipped := 0
143 vcheckignore := collect_vcheckignore_context(dir)
144 md_files := os.walk_ext(dir, '.md')
145 for file in md_files {
146 nfile := file.replace('\\', '/')
147 if nfile.contains_any_substr(['/thirdparty/', 'CHANGELOG', '/testdata/']) {
148 continue
149 }
150 if skip_match := vcheckignore.skip_match(file) {
151 if is_verbose {
152 println('SKIP: ${vcheckignore.repo_relative_path(file)} (from ${vcheckignore.repo_relative_path(skip_match.ignore_file)}: ${skip_match.pattern})')
153 }
154 skipped++
155 continue
156 }
157 files_to_check << file
158 }
159 return MDPathScanResult{
160 files: files_to_check
161 skipped: skipped
162 }
163}
164
165fn collect_vcheckignore_context(cwd string) VCheckIgnoreContext {
166 repo_root := find_repo_root(cwd)
167 return VCheckIgnoreContext{
168 repo_root: repo_root
169 }
170}
171
172fn find_repo_root(cwd string) string {
173 mut dir := os.real_path(cwd)
174 for {
175 if os.exists(os.join_path(dir, '.git')) {
176 return dir
177 }
178 parent := os.dir(dir)
179 if parent == dir || parent == '' {
180 return dir
181 }
182 dir = parent
183 }
184 return dir
185}
186
187fn (ctx VCheckIgnoreContext) skip_match(file_path string) ?VCheckIgnoreMatch {
188 file := os.real_path(file_path).replace('\\', '/')
189 mut dir := os.dir(file)
190 repo_root := ctx.repo_root.replace('\\', '/')
191 for {
192 ignore_path := os.join_path(dir, '.vcheckignore')
193 if os.is_file(ignore_path) {
194 lines := os.read_lines(ignore_path) or { []string{} }
195 for line in lines {
196 pattern := normalize_vcheckignore_line(line)
197 if pattern == '' || pattern.starts_with('#') {
198 continue
199 }
200 if matches_vcheckignore_rule(file, VCheckIgnoreRule{
201 base_dir: dir
202 pattern: pattern
203 })
204 {
205 return VCheckIgnoreMatch{
206 ignore_file: ignore_path
207 pattern: pattern
208 }
209 }
210 }
211 }
212 if dir.replace('\\', '/') == repo_root {
213 break
214 }
215 parent := os.dir(dir)
216 if parent == dir || parent == '' {
217 break
218 }
219 dir = parent
220 }
221 return none
222}
223
224fn normalize_vcheckignore_line(line string) string {
225 trimmed := line.trim_space()
226 if trimmed == '' {
227 return ''
228 }
229 if comment_idx := trimmed.index('#') {
230 return trimmed[..comment_idx].trim_space()
231 }
232 return trimmed
233}
234
235fn (ctx VCheckIgnoreContext) repo_relative_path(file_path string) string {
236 file := os.real_path(file_path).replace('\\', '/')
237 root := ctx.repo_root.replace('\\', '/')
238 root_prefix := root + '/'
239 if file.starts_with(root_prefix) {
240 return file.all_after(root_prefix)
241 }
242 return file
243}
244
245fn matches_vcheckignore_rule(file string, rule VCheckIgnoreRule) bool {
246 base := rule.base_dir.replace('\\', '/')
247 base_prefix := base + '/'
248 if !file.starts_with(base_prefix) {
249 return false
250 }
251 relative_file := file.all_after(base_prefix)
252 mut pattern := rule.pattern.replace('\\', '/')
253 if pattern.starts_with('!') {
254 return false
255 }
256 mut anchored := false
257 if pattern.starts_with('/') {
258 anchored = true
259 pattern = pattern.trim_left('/')
260 }
261 if pattern.ends_with('/') {
262 pattern = pattern.trim_right('/')
263 return matches_vcheckignore_directory_pattern(relative_file, pattern, anchored)
264 }
265 if anchored {
266 return relative_file.match_glob(pattern)
267 }
268 if pattern.contains('/') {
269 return relative_file.match_glob(pattern)
270 }
271 return os.file_name(relative_file).match_glob(pattern)
272}
273
274fn matches_vcheckignore_directory_pattern(relative_file string, pattern string, anchored bool) bool {
275 mut relative_dir := os.dir(relative_file).replace('\\', '/')
276 if relative_dir == '.' || relative_dir == '' {
277 return false
278 }
279 if anchored {
280 return relative_dir.match_glob(pattern) || relative_dir.match_glob(pattern + '/*')
281 }
282 mut candidate := relative_dir
283 for {
284 if candidate.match_glob(pattern) || candidate.match_glob(pattern + '/*') {
285 return true
286 }
287 if slash_idx := candidate.index('/') {
288 candidate = candidate[slash_idx + 1..]
289 continue
290 }
291 break
292 }
293 return false
294}
295
296fn wprintln(s string) {
297 if !hide_warnings {
298 println(s)
299 }
300}
301
302fn ftext(s string, cb fn (string) string) string {
303 if term_colors {
304 return cb(s)
305 }
306 return s
307}
308
309fn btext(s string) string {
310 return ftext(s, term.bold)
311}
312
313fn mtext(s string) string {
314 return ftext(s, term.magenta)
315}
316
317fn rtext(s string) string {
318 return ftext(s, term.red)
319}
320
321fn wline(file_path string, lnumber int, column int, message string) string {
322 return btext('${file_path}:${lnumber + 1}:${column + 1}:') + btext(mtext(' warn:')) +
323 rtext(' ${message}')
324}
325
326fn eline(file_path string, lnumber int, column int, message string) string {
327 return btext('${file_path}:${lnumber + 1}:${column + 1}:') + btext(rtext(' error: ${message}'))
328}
329
330const default_command = 'compile'
331
332struct VCodeExample {
333mut:
334 text []string
335 command string
336 sline int
337 eline int
338}
339
340enum MDFileParserState {
341 markdown
342 vexample
343 codeblock
344}
345
346struct MDFile {
347 path string
348 skip_line_length_check bool
349mut:
350 idx int
351 nfiles int
352 lines []string
353 examples []VCodeExample
354 current VCodeExample
355 state MDFileParserState = .markdown
356
357 oks int
358 warnings int
359 errors int // compilation errors + formatting errors
360 ferrors int // purely formatting errors
361}
362
363fn (mut f MDFile) progress(message string) {
364 if show_progress {
365 clear_previous_line()
366 println('${message} | File ${f.idx + 1:3}/${f.nfiles:-3}: ${f.path}')
367 }
368}
369
370struct CheckResultContext {
371 path string
372 line_number int
373 line string
374}
375
376fn (mut f MDFile) wcheck(actual int, limit int, ctx CheckResultContext, msg_template string) {
377 if actual > limit {
378 final := msg_template.replace('@', limit.str()) + ', actual: ' + actual.str()
379 wprintln(wline(ctx.path, ctx.line_number, ctx.line.len, final))
380 wprintln(ctx.line)
381 wprintln(ftext('-'.repeat(limit) + '^', term.gray))
382 f.warnings++
383 }
384}
385
386fn (mut f MDFile) echeck(actual int, limit int, ctx CheckResultContext, msg_template string) {
387 if actual > limit {
388 final := msg_template.replace('@', limit.str()) + ', actual: ' + actual.str()
389 eprintln(eline(ctx.path, ctx.line_number, ctx.line.len, final))
390 eprintln(ctx.line)
391 eprintln(ftext('-'.repeat(limit) + '^', term.gray))
392 f.errors++
393 }
394}
395
396fn (mut f MDFile) check() CheckResult {
397 mut anchor_data := AnchorData{}
398 for j, line in f.lines {
399 // f.progress('line: ${j}')
400 if !f.skip_line_length_check {
401 ctx := CheckResultContext{f.path, j, line}
402 if f.state == .vexample {
403 f.wcheck(line.len, too_long_line_length_example, ctx,
404 'example lines must be less than @ characters')
405 } else if f.state == .codeblock {
406 f.wcheck(line.len, too_long_line_length_codeblock, ctx,
407 'code lines must be less than @ characters')
408 } else if line.starts_with('|') {
409 f.wcheck(line.len, too_long_line_length_table, ctx,
410 'table lines must be less than @ characters')
411 } else if line.contains('http') {
412 // vfmt off
413 f.wcheck(line.all_after('https').len, too_long_line_length_link, ctx, 'link lines must be less than @ characters')
414 // vfmt on
415 } else {
416 f.echeck(line.len, too_long_line_length_other, ctx,
417 'must be less than @ characters')
418 }
419 }
420 if f.state == .markdown {
421 anchor_data.add_links(j, line)
422 anchor_data.add_link_targets(j, line)
423 }
424
425 f.parse_line(j, line)
426 }
427 f.check_link_target_match(anchor_data)
428 f.check_examples()
429 return CheckResult{
430 files: 1
431 lines: f.lines.len
432 examples: f.examples.len
433 oks: f.oks
434 warnings: f.warnings
435 errors: f.errors
436 ferrors: f.ferrors
437 }
438}
439
440fn (mut f MDFile) parse_line(lnumber int, line string) {
441 if line.starts_with('```v') {
442 if f.state == .markdown {
443 f.state = .vexample
444 mut command := line.replace('```v', '').trim_space()
445 if command == '' {
446 command = default_command
447 } else if command == 'nofmt' {
448 command += ' ${default_command}'
449 }
450 f.current = VCodeExample{
451 sline: lnumber
452 command: command
453 }
454 }
455 return
456 }
457 if line.starts_with('```') {
458 match f.state {
459 .vexample {
460 f.state = .markdown
461 f.current.eline = lnumber
462 f.examples << f.current
463 f.current = VCodeExample{}
464 return
465 }
466 .codeblock {
467 f.state = .markdown
468 return
469 }
470 .markdown {
471 f.state = .codeblock
472 return
473 }
474 }
475 }
476 if f.state == .vexample {
477 f.current.text << line
478 }
479}
480
481struct Headline {
482 line int
483 label string
484 level int
485}
486
487struct Anchor {
488 line int
489}
490
491type AnchorTarget = Anchor | Headline
492
493struct AnchorLink {
494 line int
495 label string
496}
497
498struct AnchorData {
499mut:
500 links map[string][]AnchorLink
501 anchors map[string][]AnchorTarget
502}
503
504fn (mut ad AnchorData) add_links(line_number int, line string) {
505 query := r'\[(?P<label>[^\]]+)\]\(\s*#(?P<link>[a-z0-9\-\_\x7f-\uffff]+)\)'
506 mut re := regex.regex_opt(query) or { panic(err) }
507 res := re.find_all_str(line)
508
509 for elem in res {
510 re.match_string(elem)
511 link := re.get_group_by_name(elem, 'link')
512 if link !in ad.links {
513 ad.links[link] = []AnchorLink{}
514 }
515 ad.links[link] << AnchorLink{
516 line: line_number
517 label: re.get_group_by_name(elem, 'label')
518 }
519 }
520}
521
522fn (mut ad AnchorData) add_link_targets(line_number int, line string) {
523 if line.trim_space().starts_with('#') {
524 if headline_start_pos := line.index(' ') {
525 headline := line.substr(headline_start_pos + 1, line.len)
526 link := create_ref_link(headline)
527 if link !in ad.anchors {
528 ad.anchors[link] = []AnchorTarget{}
529 }
530 ad.anchors[link] << Headline{
531 line: line_number
532 label: headline
533 level: headline_start_pos
534 }
535 }
536 } else {
537 query := '<a\\s*id=["\'](?P<link>[a-z0-9\\-\\_\\x7f-\\uffff]+)["\']\\s*/>'
538 mut re := regex.regex_opt(query) or { panic(err) }
539 res := re.find_all_str(line)
540
541 for elem in res {
542 re.match_string(elem)
543 link := re.get_group_by_name(elem, 'link')
544 if link !in ad.anchors {
545 ad.anchors[link] = []AnchorTarget{}
546 }
547 ad.anchors[link] << Anchor{
548 line: line_number
549 }
550 }
551 }
552}
553
554fn (mut f MDFile) check_link_target_match(ad AnchorData) {
555 mut checked_headlines := []string{}
556 mut found_error_warning := false
557 for link, linkdata in ad.links {
558 if link in ad.anchors {
559 checked_headlines << link
560 if ad.anchors[link].len > 1 {
561 found_error_warning = true
562 f.errors++
563 for anchordata in ad.anchors[link] {
564 eprintln(eline(f.path, anchordata.line, 0,
565 'multiple link targets of existing link (#${link})'))
566 }
567 }
568 } else {
569 found_error_warning = true
570 f.errors++
571 for brokenlink in linkdata {
572 eprintln(eline(f.path, brokenlink.line, 0,
573 'no link target found for existing link [${brokenlink.label}](#${link})'))
574 }
575 }
576 }
577 for link, anchor_lists in ad.anchors {
578 if link !in checked_headlines {
579 if anchor_lists.len > 1 {
580 for anchor in anchor_lists {
581 line := match anchor {
582 Headline {
583 anchor.line
584 }
585 Anchor {
586 anchor.line
587 }
588 }
589
590 wprintln(wline(f.path, line, 0,
591 'multiple link target for non existing link (#${link})'))
592 found_error_warning = true
593 f.warnings++
594 }
595 }
596 }
597 }
598 if found_error_warning {
599 eprintln('') // fix suppressed last error output
600 }
601}
602
603// based on a reference sample md doc
604// https://github.com/aheissenberger/vlang-markdown-module/blob/master/test.md
605fn create_ref_link(s string) string {
606 mut result := ''
607 for c in s.trim_space() {
608 result += match c {
609 `a`...`z`, `0`...`9` {
610 c.ascii_str()
611 }
612 `A`...`Z` {
613 c.ascii_str().to_lower()
614 }
615 ` `, `-` {
616 '-'
617 }
618 `_` {
619 '_'
620 }
621 else {
622 if c > 127 { c.ascii_str() } else { '' }
623 }
624 }
625 }
626 return result
627}
628
629fn (mut f MDFile) debug() {
630 for e in f.examples {
631 eprintln('f.path: ${f.path} | example: ${e}')
632 }
633}
634
635fn cmdexecute(cmd string) int {
636 verbose_println(cmd)
637 res := os.execute(cmd)
638 if res.exit_code < 0 {
639 return 1
640 }
641 if res.exit_code != 0 {
642 eprint(res.output)
643 }
644 return res.exit_code
645}
646
647fn silent_cmdexecute(cmd string) int {
648 verbose_println(cmd)
649 res := os.execute(cmd)
650 return res.exit_code
651}
652
653fn get_fmt_exit_code(vfile string, vexe string) int {
654 return silent_cmdexecute('${os.quoted_path(vexe)} fmt -verify ${os.quoted_path(vfile)}')
655}
656
657fn (mut f MDFile) check_examples() {
658 recheck_all_examples: for eidx, e in f.examples {
659 if e.command == 'ignore' {
660 continue
661 }
662 if e.command == 'wip' {
663 continue
664 }
665 fname := os.base(f.path).replace('.md', '_md')
666 uid := rand.ulid()
667 cfile := os.join_path(vcheckfolder, '${uid}.c')
668 vfile := os.join_path(vcheckfolder,
669 'check_${fname}_example_${e.sline}__${e.eline}__${uid}.v')
670 efile := os.join_path(vcheckfolder,
671 'check_${fname}_example_${e.sline}__${e.eline}__${uid}.exe')
672 mut should_cleanup_vfile := true
673 // eprintln('>>> checking example ${vfile} ...')
674 vcontent := e.text.join('\n') + '\n'
675 os.write_file(vfile, vcontent) or { panic(err) }
676 mut acommands := e.command.split(' ')
677 nofmt := 'nofmt' in acommands
678 for command in acommands {
679 f.progress('OK: ${f.oks:3}, W: ${f.warnings:2}, E: ${f.errors:2}, F: ${f.ferrors:2}, ex. ${
680 eidx + 1:3}/${f.examples.len:-3}, from line ${e.sline:4} to line ${e.eline:-4} of ${f.lines.len:-4}, command: ${command:12s}')
681 fmt_res := if nofmt { 0 } else { get_fmt_exit_code(vfile, vexe) }
682 f.ferrors += fmt_res
683 match command {
684 'compile' {
685 res :=
686 cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(efile)} ${os.quoted_path(vfile)}')
687 if res != 0 || fmt_res != 0 {
688 if res != 0 {
689 eprintln(eline(f.path, e.sline, 0, 'example failed to compile'))
690 }
691 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
692 unsafe {
693 goto recheck_all_examples
694 }
695 }
696 eprintln(vcontent)
697 should_cleanup_vfile = false
698 f.errors++
699 continue
700 }
701 f.oks++
702 }
703 'cgen' {
704 res :=
705 cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
706 if res != 0 || fmt_res != 0 {
707 if res != 0 {
708 eprintln(eline(f.path, e.sline, 0, 'example failed to generate C code'))
709 }
710 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
711 unsafe {
712 goto recheck_all_examples
713 }
714 }
715 eprintln(vcontent)
716 should_cleanup_vfile = false
717 f.errors++
718 continue
719 }
720 f.oks++
721 }
722 'globals' {
723 res :=
724 cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -enable-globals -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
725 if res != 0 || fmt_res != 0 {
726 if res != 0 {
727 eprintln(eline(f.path, e.sline, 0,
728 '`example failed to compile with -enable-globals'))
729 }
730 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
731 unsafe {
732 goto recheck_all_examples
733 }
734 }
735 eprintln(vcontent)
736 should_cleanup_vfile = false
737 f.errors++
738 continue
739 }
740 f.oks++
741 }
742 'live' {
743 res :=
744 cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -live -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
745 if res != 0 || fmt_res != 0 {
746 if res != 0 {
747 eprintln(eline(f.path, e.sline, 0,
748 'example failed to compile with -live'))
749 }
750 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
751 unsafe {
752 goto recheck_all_examples
753 }
754 }
755 eprintln(vcontent)
756 should_cleanup_vfile = false
757 f.errors++
758 continue
759 }
760 f.oks++
761 }
762 'shared' {
763 res :=
764 cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -shared -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
765 if res != 0 || fmt_res != 0 {
766 if res != 0 {
767 eprintln(eline(f.path, e.sline, 0,
768 'module example failed to compile with -shared'))
769 }
770 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
771 unsafe {
772 goto recheck_all_examples
773 }
774 }
775 eprintln(vcontent)
776 should_cleanup_vfile = false
777 f.errors++
778 continue
779 }
780 f.oks++
781 }
782 'failcompile' {
783 res :=
784 silent_cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}')
785 if res == 0 || fmt_res != 0 {
786 if res == 0 {
787 eprintln(eline(f.path, e.sline, 0, '`failcompile` example compiled'))
788 }
789 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
790 unsafe {
791 goto recheck_all_examples
792 }
793 }
794 eprintln(vcontent)
795 should_cleanup_vfile = false
796 f.errors++
797 continue
798 }
799 f.oks++
800 }
801 'oksyntax' {
802 res :=
803 cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -check-syntax ${os.quoted_path(vfile)}')
804 if res != 0 || fmt_res != 0 {
805 if res != 0 {
806 eprintln(eline(f.path, e.sline, 0,
807 '`oksyntax` example with invalid syntax'))
808 }
809 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
810 unsafe {
811 goto recheck_all_examples
812 }
813 }
814 eprintln(vcontent)
815 should_cleanup_vfile = false
816 f.errors++
817 continue
818 }
819 f.oks++
820 }
821 'okfmt' {
822 if fmt_res != 0 {
823 f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or {
824 unsafe {
825 goto recheck_all_examples
826 }
827 }
828 eprintln(vcontent)
829 should_cleanup_vfile = false
830 f.errors++
831 continue
832 }
833 f.oks++
834 }
835 'badsyntax' {
836 res :=
837 silent_cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -check-syntax ${os.quoted_path(vfile)}')
838 if res == 0 {
839 eprintln(eline(f.path, e.sline, 0, '`badsyntax` example can be parsed fine'))
840 eprintln(vcontent)
841 should_cleanup_vfile = false
842 f.errors++
843 continue
844 }
845 f.oks++
846 }
847 'nofmt' {}
848 // mark the example as playable inside docs
849 'play' {}
850 // same as play, but run example as a test
851 'play-test' {}
852 // when ```vmod
853 'mod' {}
854 else {
855 eprintln(eline(f.path, e.sline, 0,
856 'unrecognized command: "${command}", use one of: wip/ignore/compile/failcompile/okfmt/nofmt/oksyntax/badsyntax/cgen/globals/live/shared'))
857 should_cleanup_vfile = false
858 f.errors++
859 }
860 }
861 }
862 os.rm(cfile) or {}
863 os.rm(efile) or {}
864 if should_cleanup_vfile {
865 os.rm(vfile) or { panic(err) }
866 }
867 }
868}
869
870fn verbose_println(message string) {
871 if is_verbose {
872 println(message)
873 }
874}
875
876fn clear_previous_line() {
877 if is_verbose {
878 return
879 }
880 term.clear_previous_line()
881}
882
883fn (mut f MDFile) report_not_formatted_example_if_needed(e VCodeExample, fmt_res int, vfile string) ! {
884 if fmt_res == 0 {
885 return
886 }
887 eprintln(eline(f.path, e.sline, 0, 'example is not formatted'))
888 if !should_autofix {
889 return
890 }
891 f.autofix_example(e, vfile) or {
892 if err is ExampleWasRewritten {
893 eprintln('>> f.path: ${f.path} | example from ${e.sline} to ${e.eline} was re-formatted by vfmt')
894 return err
895 }
896 eprintln('>> f.path: ${f.path} | encountered error while autofixing the example: ${err}')
897 }
898}
899
900struct ExampleWasRewritten {
901 Error
902}
903
904fn (mut f MDFile) autofix_example(e VCodeExample, vfile string) ! {
905 eprintln('>>> AUTOFIXING f.path: ${f.path} | e.sline: ${e.sline} | vfile: ${vfile}')
906 res := cmdexecute('${os.quoted_path(vexe)} fmt -w ${os.quoted_path(vfile)}')
907 if res != 0 {
908 return error('could not autoformat the example')
909 }
910 formatted_content_lines := os.read_lines(vfile) or { return }
911 mut new_lines := []string{}
912 new_lines << f.lines#[0..e.sline + 1]
913 new_lines << formatted_content_lines
914 new_lines << f.lines#[e.eline..]
915 f.update_examples(new_lines)!
916 os.rm(vfile) or {}
917 f.examples = f.examples.filter(it.sline >= e.sline)
918 return ExampleWasRewritten{}
919}
920
921fn (mut f MDFile) update_examples(new_lines []string) ! {
922 os.write_file(f.path, new_lines.join('\n'))!
923 f.lines = new_lines
924 f.examples = []
925 f.current = VCodeExample{}
926 f.state = .markdown
927 for j, line in f.lines {
928 f.parse_line(j, line)
929 }
930}
931