1 | // Copyright (c) 2019-2023 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. |
4 | module main |
5 | |
6 | import os |
7 | import os.cmdline |
8 | import rand |
9 | import term |
10 | import v.help |
11 | import regex |
12 | |
13 | const ( |
14 | too_long_line_length_example = 120 |
15 | too_long_line_length_codeblock = 120 |
16 | too_long_line_length_table = 120 |
17 | too_long_line_length_link = 150 |
18 | too_long_line_length_other = 100 |
19 | term_colors = term.can_show_color_on_stderr() |
20 | hide_warnings = '-hide-warnings' in os.args || '-w' in os.args |
21 | show_progress = os.getenv('GITHUB_JOB') == '' && '-silent' !in os.args |
22 | non_option_args = cmdline.only_non_options(os.args[2..]) |
23 | is_verbose = os.getenv('VERBOSE') != '' |
24 | vcheckfolder = os.join_path(os.vtmp_dir(), 'v', 'vcheck_${os.getuid()}') |
25 | should_autofix = os.getenv('VAUTOFIX') != '' |
26 | vexe = @VEXE |
27 | ) |
28 | |
29 | struct CheckResult { |
30 | pub mut: |
31 | warnings int |
32 | errors int |
33 | oks int |
34 | } |
35 | |
36 | fn (v1 CheckResult) + (v2 CheckResult) CheckResult { |
37 | return CheckResult{ |
38 | warnings: v1.warnings + v2.warnings |
39 | errors: v1.errors + v2.errors |
40 | oks: v1.oks + v2.oks |
41 | } |
42 | } |
43 | |
44 | fn main() { |
45 | if non_option_args.len == 0 || '-help' in os.args { |
46 | help.print_and_exit('check-md') |
47 | exit(0) |
48 | } |
49 | if '-all' in os.args { |
50 | println('´-all´ flag is deprecated. Please use ´v check-md .´ instead.') |
51 | exit(1) |
52 | } |
53 | mut skip_line_length_check := '-skip-line-length-check' in os.args |
54 | if show_progress { |
55 | // this is intended to be replaced by the progress lines |
56 | println('') |
57 | } |
58 | mut files_paths := non_option_args.clone() |
59 | mut res := CheckResult{} |
60 | if term_colors { |
61 | os.setenv('VCOLORS', 'always', true) |
62 | } |
63 | os.mkdir_all(vcheckfolder, mode: 0o700) or {} // keep directory private |
64 | defer { |
65 | os.rmdir_all(vcheckfolder) or {} |
66 | } |
67 | for i := 0; i < files_paths.len; i++ { |
68 | file_path := files_paths[i] |
69 | if os.is_dir(file_path) { |
70 | files_paths << md_file_paths(file_path) |
71 | continue |
72 | } |
73 | real_path := os.real_path(file_path) |
74 | lines := os.read_lines(real_path) or { |
75 | println('"${file_path}" does not exist') |
76 | res.warnings++ |
77 | continue |
78 | } |
79 | mut mdfile := MDFile{ |
80 | skip_line_length_check: skip_line_length_check |
81 | path: file_path |
82 | lines: lines |
83 | } |
84 | res += mdfile.check() |
85 | } |
86 | if res.errors == 0 && show_progress { |
87 | clear_previous_line() |
88 | } |
89 | if res.warnings > 0 || res.errors > 0 || res.oks > 0 { |
90 | println('\nWarnings: ${res.warnings} | Errors: ${res.errors} | OKs: ${res.oks}') |
91 | } |
92 | if res.errors > 0 { |
93 | exit(1) |
94 | } |
95 | } |
96 | |
97 | fn md_file_paths(dir string) []string { |
98 | mut files_to_check := []string{} |
99 | md_files := os.walk_ext(dir, '.md') |
100 | for file in md_files { |
101 | nfile := file.replace('\\', '/') |
102 | if nfile.contains_any_substr(['/thirdparty/', 'CHANGELOG']) { |
103 | continue |
104 | } |
105 | files_to_check << file |
106 | } |
107 | return files_to_check |
108 | } |
109 | |
110 | fn wprintln(s string) { |
111 | if !hide_warnings { |
112 | println(s) |
113 | } |
114 | } |
115 | |
116 | fn ftext(s string, cb fn (string) string) string { |
117 | if term_colors { |
118 | return cb(s) |
119 | } |
120 | return s |
121 | } |
122 | |
123 | fn btext(s string) string { |
124 | return ftext(s, term.bold) |
125 | } |
126 | |
127 | fn mtext(s string) string { |
128 | return ftext(s, term.magenta) |
129 | } |
130 | |
131 | fn rtext(s string) string { |
132 | return ftext(s, term.red) |
133 | } |
134 | |
135 | fn wline(file_path string, lnumber int, column int, message string) string { |
136 | return btext('${file_path}:${lnumber + 1}:${column + 1}:') + btext(mtext(' warn:')) + |
137 | rtext(' ${message}') |
138 | } |
139 | |
140 | fn eline(file_path string, lnumber int, column int, message string) string { |
141 | return btext('${file_path}:${lnumber + 1}:${column + 1}:') + btext(rtext(' error: ${message}')) |
142 | } |
143 | |
144 | const default_command = 'compile' |
145 | |
146 | struct VCodeExample { |
147 | mut: |
148 | text []string |
149 | command string |
150 | sline int |
151 | eline int |
152 | } |
153 | |
154 | enum MDFileParserState { |
155 | markdown |
156 | vexample |
157 | codeblock |
158 | } |
159 | |
160 | struct MDFile { |
161 | path string |
162 | skip_line_length_check bool |
163 | mut: |
164 | lines []string |
165 | examples []VCodeExample |
166 | current VCodeExample |
167 | state MDFileParserState = .markdown |
168 | } |
169 | |
170 | fn (mut f MDFile) progress(message string) { |
171 | if show_progress { |
172 | clear_previous_line() |
173 | println('File: ${f.path:-30s}, Lines: ${f.lines.len:5}, ${message}') |
174 | } |
175 | } |
176 | |
177 | fn (mut f MDFile) check() CheckResult { |
178 | mut res := CheckResult{} |
179 | mut anchor_data := AnchorData{} |
180 | for j, line in f.lines { |
181 | // f.progress('line: $j') |
182 | if !f.skip_line_length_check { |
183 | if f.state == .vexample { |
184 | if line.len > too_long_line_length_example { |
185 | wprintln(wline(f.path, j, line.len, 'example lines must be less than ${too_long_line_length_example} characters')) |
186 | wprintln(line) |
187 | res.warnings++ |
188 | } |
189 | } else if f.state == .codeblock { |
190 | if line.len > too_long_line_length_codeblock { |
191 | wprintln(wline(f.path, j, line.len, 'code lines must be less than ${too_long_line_length_codeblock} characters')) |
192 | wprintln(line) |
193 | res.warnings++ |
194 | } |
195 | } else if line.starts_with('|') { |
196 | if line.len > too_long_line_length_table { |
197 | wprintln(wline(f.path, j, line.len, 'table lines must be less than ${too_long_line_length_table} characters')) |
198 | wprintln(line) |
199 | res.warnings++ |
200 | } |
201 | } else if line.contains('http') { |
202 | if line.all_after('https').len > too_long_line_length_link { |
203 | wprintln(wline(f.path, j, line.len, 'link lines must be less than ${too_long_line_length_link} characters')) |
204 | wprintln(line) |
205 | res.warnings++ |
206 | } |
207 | } else if line.len > too_long_line_length_other { |
208 | eprintln(eline(f.path, j, line.len, 'must be less than ${too_long_line_length_other} characters')) |
209 | eprintln(line) |
210 | res.errors++ |
211 | } |
212 | } |
213 | if f.state == .markdown { |
214 | anchor_data.add_links(j, line) |
215 | anchor_data.add_link_targets(j, line) |
216 | } |
217 | |
218 | f.parse_line(j, line) |
219 | } |
220 | anchor_data.check_link_target_match(f.path, mut res) |
221 | res += f.check_examples() |
222 | return res |
223 | } |
224 | |
225 | fn (mut f MDFile) parse_line(lnumber int, line string) { |
226 | if line.starts_with('```v') { |
227 | if f.state == .markdown { |
228 | f.state = .vexample |
229 | mut command := line.replace('```v', '').trim_space() |
230 | if command == '' { |
231 | command = default_command |
232 | } else if command == 'nofmt' { |
233 | command += ' ${default_command}' |
234 | } |
235 | f.current = VCodeExample{ |
236 | sline: lnumber |
237 | command: command |
238 | } |
239 | } |
240 | return |
241 | } |
242 | if line.starts_with('```') { |
243 | match f.state { |
244 | .vexample { |
245 | f.state = .markdown |
246 | f.current.eline = lnumber |
247 | f.examples << f.current |
248 | f.current = VCodeExample{} |
249 | return |
250 | } |
251 | .codeblock { |
252 | f.state = .markdown |
253 | return |
254 | } |
255 | .markdown { |
256 | f.state = .codeblock |
257 | return |
258 | } |
259 | } |
260 | } |
261 | if f.state == .vexample { |
262 | f.current.text << line |
263 | } |
264 | } |
265 | |
266 | struct Headline { |
267 | line int |
268 | lable string |
269 | level int |
270 | } |
271 | |
272 | struct Anchor { |
273 | line int |
274 | } |
275 | |
276 | type AnchorTarget = Anchor | Headline |
277 | |
278 | struct AnchorLink { |
279 | line int |
280 | lable string |
281 | } |
282 | |
283 | struct AnchorData { |
284 | mut: |
285 | links map[string][]AnchorLink |
286 | anchors map[string][]AnchorTarget |
287 | } |
288 | |
289 | fn (mut ad AnchorData) add_links(line_number int, line string) { |
290 | query := r'\[(?P<lable>[^\]]+)\]\(\s*#(?P<link>[a-z0-9\-\_\x7f-\uffff]+)\)' |
291 | mut re := regex.regex_opt(query) or { panic(err) } |
292 | res := re.find_all_str(line) |
293 | |
294 | for elem in res { |
295 | re.match_string(elem) |
296 | link := re.get_group_by_name(elem, 'link') |
297 | ad.links[link] << AnchorLink{ |
298 | line: line_number |
299 | lable: re.get_group_by_name(elem, 'lable') |
300 | } |
301 | } |
302 | } |
303 | |
304 | fn (mut ad AnchorData) add_link_targets(line_number int, line string) { |
305 | if line.trim_space().starts_with('#') { |
306 | if headline_start_pos := line.index(' ') { |
307 | headline := line.substr(headline_start_pos + 1, line.len) |
308 | link := create_ref_link(headline) |
309 | ad.anchors[link] << Headline{ |
310 | line: line_number |
311 | lable: headline |
312 | level: headline_start_pos |
313 | } |
314 | } |
315 | } else { |
316 | query := '<a\\s*id=["\'](?P<link>[a-z0-9\\-\\_\\x7f-\\uffff]+)["\']\\s*/>' |
317 | mut re := regex.regex_opt(query) or { panic(err) } |
318 | res := re.find_all_str(line) |
319 | |
320 | for elem in res { |
321 | re.match_string(elem) |
322 | link := re.get_group_by_name(elem, 'link') |
323 | ad.anchors[link] << Anchor{ |
324 | line: line_number |
325 | } |
326 | } |
327 | } |
328 | } |
329 | |
330 | fn (mut ad AnchorData) check_link_target_match(fpath string, mut res CheckResult) { |
331 | mut checked_headlines := []string{} |
332 | mut found_error_warning := false |
333 | for link, linkdata in ad.links { |
334 | if link in ad.anchors { |
335 | checked_headlines << link |
336 | if ad.anchors[link].len > 1 { |
337 | found_error_warning = true |
338 | res.errors++ |
339 | for anchordata in ad.anchors[link] { |
340 | eprintln(eline(fpath, anchordata.line, 0, 'multiple link targets of existing link (#${link})')) |
341 | } |
342 | } |
343 | } else { |
344 | found_error_warning = true |
345 | res.errors++ |
346 | for brokenlink in linkdata { |
347 | eprintln(eline(fpath, brokenlink.line, 0, 'no link target found for existing link [${brokenlink.lable}](#${link})')) |
348 | } |
349 | } |
350 | } |
351 | for link, anchor_lists in ad.anchors { |
352 | if link !in checked_headlines { |
353 | if anchor_lists.len > 1 { |
354 | for anchor in anchor_lists { |
355 | line := match anchor { |
356 | Headline { |
357 | anchor.line |
358 | } |
359 | Anchor { |
360 | anchor.line |
361 | } |
362 | } |
363 | wprintln(wline(fpath, line, 0, 'multiple link target for non existing link (#${link})')) |
364 | found_error_warning = true |
365 | res.warnings++ |
366 | } |
367 | } |
368 | } |
369 | } |
370 | if found_error_warning { |
371 | eprintln('') // fix suppressed last error output |
372 | } |
373 | } |
374 | |
375 | // based on a reference sample md doc |
376 | // https://github.com/aheissenberger/vlang-markdown-module/blob/master/test.md |
377 | fn create_ref_link(s string) string { |
378 | mut result := '' |
379 | for c in s.trim_space() { |
380 | result += match c { |
381 | `a`...`z`, `0`...`9` { |
382 | c.ascii_str() |
383 | } |
384 | `A`...`Z` { |
385 | c.ascii_str().to_lower() |
386 | } |
387 | ` `, `-` { |
388 | '-' |
389 | } |
390 | `_` { |
391 | '_' |
392 | } |
393 | else { |
394 | if c > 127 { c.ascii_str() } else { '' } |
395 | } |
396 | } |
397 | } |
398 | return result |
399 | } |
400 | |
401 | fn (mut f MDFile) debug() { |
402 | for e in f.examples { |
403 | eprintln('f.path: ${f.path} | example: ${e}') |
404 | } |
405 | } |
406 | |
407 | fn cmdexecute(cmd string) int { |
408 | verbose_println(cmd) |
409 | res := os.execute(cmd) |
410 | if res.exit_code < 0 { |
411 | return 1 |
412 | } |
413 | if res.exit_code != 0 { |
414 | eprint(res.output) |
415 | } |
416 | return res.exit_code |
417 | } |
418 | |
419 | fn silent_cmdexecute(cmd string) int { |
420 | verbose_println(cmd) |
421 | res := os.execute(cmd) |
422 | return res.exit_code |
423 | } |
424 | |
425 | fn get_fmt_exit_code(vfile string, vexe string) int { |
426 | return silent_cmdexecute('${os.quoted_path(vexe)} fmt -verify ${os.quoted_path(vfile)}') |
427 | } |
428 | |
429 | fn (mut f MDFile) check_examples() CheckResult { |
430 | mut errors := 0 |
431 | mut oks := 0 |
432 | recheck_all_examples: for e in f.examples { |
433 | if e.command == 'ignore' { |
434 | continue |
435 | } |
436 | if e.command == 'wip' { |
437 | continue |
438 | } |
439 | fname := os.base(f.path).replace('.md', '_md') |
440 | uid := rand.ulid() |
441 | cfile := os.join_path(vcheckfolder, '${uid}.c') |
442 | vfile := os.join_path(vcheckfolder, 'check_${fname}_example_${e.sline}__${e.eline}__${uid}.v') |
443 | efile := os.join_path(vcheckfolder, 'check_${fname}_example_${e.sline}__${e.eline}__${uid}.exe') |
444 | mut should_cleanup_vfile := true |
445 | // eprintln('>>> checking example $vfile ...') |
446 | vcontent := e.text.join('\n') + '\n' |
447 | os.write_file(vfile, vcontent) or { panic(err) } |
448 | mut acommands := e.command.split(' ') |
449 | nofmt := 'nofmt' in acommands |
450 | for command in acommands { |
451 | f.progress('example from ${e.sline} to ${e.eline}, command: ${command}') |
452 | fmt_res := if nofmt { 0 } else { get_fmt_exit_code(vfile, vexe) } |
453 | match command { |
454 | 'compile' { |
455 | res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(efile)} ${os.quoted_path(vfile)}') |
456 | if res != 0 || fmt_res != 0 { |
457 | if res != 0 { |
458 | eprintln(eline(f.path, e.sline, 0, 'example failed to compile')) |
459 | } |
460 | f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or { |
461 | unsafe { |
462 | goto recheck_all_examples |
463 | } |
464 | } |
465 | eprintln(vcontent) |
466 | should_cleanup_vfile = false |
467 | errors++ |
468 | continue |
469 | } |
470 | oks++ |
471 | } |
472 | 'cgen' { |
473 | res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}') |
474 | if res != 0 || fmt_res != 0 { |
475 | if res != 0 { |
476 | eprintln(eline(f.path, e.sline, 0, 'example failed to generate C code')) |
477 | } |
478 | f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or { |
479 | unsafe { |
480 | goto recheck_all_examples |
481 | } |
482 | } |
483 | eprintln(vcontent) |
484 | should_cleanup_vfile = false |
485 | errors++ |
486 | continue |
487 | } |
488 | oks++ |
489 | } |
490 | 'globals' { |
491 | res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -enable-globals -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}') |
492 | if res != 0 || fmt_res != 0 { |
493 | if res != 0 { |
494 | eprintln(eline(f.path, e.sline, 0, '`example failed to compile with -enable-globals')) |
495 | } |
496 | f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or { |
497 | unsafe { |
498 | goto recheck_all_examples |
499 | } |
500 | } |
501 | eprintln(vcontent) |
502 | should_cleanup_vfile = false |
503 | errors++ |
504 | continue |
505 | } |
506 | oks++ |
507 | } |
508 | 'live' { |
509 | res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -live -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}') |
510 | if res != 0 || fmt_res != 0 { |
511 | if res != 0 { |
512 | eprintln(eline(f.path, e.sline, 0, 'example failed to compile with -live')) |
513 | } |
514 | f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or { |
515 | unsafe { |
516 | goto recheck_all_examples |
517 | } |
518 | } |
519 | eprintln(vcontent) |
520 | should_cleanup_vfile = false |
521 | errors++ |
522 | continue |
523 | } |
524 | oks++ |
525 | } |
526 | 'shared' { |
527 | res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -shared -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}') |
528 | if res != 0 || fmt_res != 0 { |
529 | if res != 0 { |
530 | eprintln(eline(f.path, e.sline, 0, 'module example failed to compile with -shared')) |
531 | } |
532 | f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or { |
533 | unsafe { |
534 | goto recheck_all_examples |
535 | } |
536 | } |
537 | eprintln(vcontent) |
538 | should_cleanup_vfile = false |
539 | errors++ |
540 | continue |
541 | } |
542 | oks++ |
543 | } |
544 | 'failcompile' { |
545 | res := silent_cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -o ${os.quoted_path(cfile)} ${os.quoted_path(vfile)}') |
546 | if res == 0 || fmt_res != 0 { |
547 | if res == 0 { |
548 | eprintln(eline(f.path, e.sline, 0, '`failcompile` example compiled')) |
549 | } |
550 | f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or { |
551 | unsafe { |
552 | goto recheck_all_examples |
553 | } |
554 | } |
555 | eprintln(vcontent) |
556 | should_cleanup_vfile = false |
557 | errors++ |
558 | continue |
559 | } |
560 | oks++ |
561 | } |
562 | 'oksyntax' { |
563 | res := cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -check-syntax ${os.quoted_path(vfile)}') |
564 | if res != 0 || fmt_res != 0 { |
565 | if res != 0 { |
566 | eprintln(eline(f.path, e.sline, 0, '`oksyntax` example with invalid syntax')) |
567 | } |
568 | f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or { |
569 | unsafe { |
570 | goto recheck_all_examples |
571 | } |
572 | } |
573 | eprintln(vcontent) |
574 | should_cleanup_vfile = false |
575 | errors++ |
576 | continue |
577 | } |
578 | oks++ |
579 | } |
580 | 'okfmt' { |
581 | if fmt_res != 0 { |
582 | f.report_not_formatted_example_if_needed(e, fmt_res, vfile) or { |
583 | unsafe { |
584 | goto recheck_all_examples |
585 | } |
586 | } |
587 | eprintln(vcontent) |
588 | should_cleanup_vfile = false |
589 | errors++ |
590 | continue |
591 | } |
592 | oks++ |
593 | } |
594 | 'badsyntax' { |
595 | res := silent_cmdexecute('${os.quoted_path(vexe)} -w -Wfatal-errors -check-syntax ${os.quoted_path(vfile)}') |
596 | if res == 0 { |
597 | eprintln(eline(f.path, e.sline, 0, '`badsyntax` example can be parsed fine')) |
598 | eprintln(vcontent) |
599 | should_cleanup_vfile = false |
600 | errors++ |
601 | continue |
602 | } |
603 | oks++ |
604 | } |
605 | 'nofmt' {} |
606 | // mark the example as playable inside docs |
607 | 'play' {} |
608 | // same as play, but run example as a test |
609 | 'play-test' {} |
610 | // when ```vmod |
611 | 'mod' {} |
612 | else { |
613 | eprintln(eline(f.path, e.sline, 0, 'unrecognized command: "${command}", use one of: wip/ignore/compile/failcompile/okfmt/nofmt/oksyntax/badsyntax/cgen/globals/live/shared')) |
614 | should_cleanup_vfile = false |
615 | errors++ |
616 | } |
617 | } |
618 | } |
619 | os.rm(cfile) or {} |
620 | os.rm(efile) or {} |
621 | if should_cleanup_vfile { |
622 | os.rm(vfile) or { panic(err) } |
623 | } |
624 | } |
625 | return CheckResult{ |
626 | errors: errors |
627 | oks: oks |
628 | } |
629 | } |
630 | |
631 | fn verbose_println(message string) { |
632 | if is_verbose { |
633 | println(message) |
634 | } |
635 | } |
636 | |
637 | fn clear_previous_line() { |
638 | if is_verbose { |
639 | return |
640 | } |
641 | term.clear_previous_line() |
642 | } |
643 | |
644 | fn (mut f MDFile) report_not_formatted_example_if_needed(e VCodeExample, fmt_res int, vfile string) ! { |
645 | if fmt_res == 0 { |
646 | return |
647 | } |
648 | eprintln(eline(f.path, e.sline, 0, 'example is not formatted')) |
649 | if !should_autofix { |
650 | return |
651 | } |
652 | f.autofix_example(e, vfile) or { |
653 | if err is ExampleWasRewritten { |
654 | eprintln('>> f.path: ${f.path} | example from ${e.sline} to ${e.eline} was re-formated by vfmt') |
655 | return err |
656 | } |
657 | eprintln('>> f.path: ${f.path} | encountered error while autofixing the example: ${err}') |
658 | } |
659 | } |
660 | |
661 | struct ExampleWasRewritten { |
662 | Error |
663 | } |
664 | |
665 | fn (mut f MDFile) autofix_example(e VCodeExample, vfile string) ! { |
666 | eprintln('>>> AUTOFIXING f.path: ${f.path} | e.sline: ${e.sline} | vfile: ${vfile}') |
667 | res := cmdexecute('${os.quoted_path(vexe)} fmt -w ${os.quoted_path(vfile)}') |
668 | if res != 0 { |
669 | return error('could not autoformat the example') |
670 | } |
671 | formatted_content_lines := os.read_lines(vfile) or { return } |
672 | mut new_lines := []string{} |
673 | new_lines << f.lines#[0..e.sline + 1] |
674 | new_lines << formatted_content_lines |
675 | new_lines << f.lines#[e.eline..] |
676 | f.update_examples(new_lines)! |
677 | os.rm(vfile) or {} |
678 | f.examples = f.examples.filter(it.sline >= e.sline) |
679 | return ExampleWasRewritten{} |
680 | } |
681 | |
682 | fn (mut f MDFile) update_examples(new_lines []string) ! { |
683 | os.write_file(f.path, new_lines.join('\n'))! |
684 | f.lines = new_lines |
685 | f.examples = [] |
686 | f.current = VCodeExample{} |
687 | f.state = .markdown |
688 | for j, line in f.lines { |
689 | f.parse_line(j, line) |
690 | } |
691 | } |