diff options
| author | Yuriy Andamasov <yuriy@vyos.io> | 2026-05-10 19:11:24 +0300 |
|---|---|---|
| committer | Yuriy Andamasov <yuriy@vyos.io> | 2026-05-10 19:11:24 +0300 |
| commit | 7a3aa29fc010c2604b84d6437ed87053947f3f27 (patch) | |
| tree | c241c2ba91c2dba370d915102405fb24e2c39178 /scripts | |
| parent | 266acf2829fdcf809fe851203d8e34167075dd3f (diff) | |
| download | vyos-documentation-7a3aa29fc010c2604b84d6437ed87053947f3f27.tar.gz vyos-documentation-7a3aa29fc010c2604b84d6437ed87053947f3f27.zip | |
ci: extend doc-linter to MyST Markdown
Active docs are now MyST `.md`; the linter previously only inspected
`.rst` and `.txt`, so ~250 active pages were unchecked for IP usage and
line length on every PR.
scripts/doc-linter.py:
- Add `.md` to the extension filter (use `endswith` for correctness;
the prior 4-char slice silently skipped `.md` files).
- Track MyST/Markdown fenced code blocks (```` ``` ```` and `:::`) for
line-length exemption — same semantics as `.. code-block::` for RST.
- Recognize both suppression marker forms: `.. stop_vyoslinter` /
`.. start_vyoslinter` (RST and `.txt` includes) and `% stop_vyoslinter`
/ `% start_vyoslinter` (MyST). Both work in either context; pick the
form that matches the surrounding parser.
- Replace the brittle `try/finally: fp.close()` with a `with` block —
the previous form raised `UnboundLocalError` if `open()` itself
failed.
- Fix typo `forgett` → `forget`.
.github/instructions/rst-linter.instructions.md → doc-linter.instructions.md:
- Broaden `applyTo` from `**/*.rst` to `**/*.md,**/*.rst,**/*.txt`.
- Document MyST suppression syntax and fenced-code line-length
exemption.
- Note the parser-form rule for `{eval-rst}` blocks.
No regression on `.txt` includes: identical lint output verified
against the origin/rolling baseline on a sample of files.
Pre-existing IP violations exist in 14 `.md` files (e.g.
`configexamples/lac-lns.md` line 95 — a `8.8.8.8` already wrapped in
`% stop_vyoslinter`/`% start_vyoslinter`, correctly suppressed). PRs
touching unsuppressed violations will start failing CI; this is the
intent of enabling the check.
🤖 Generated by [robots](https://vyos.io)
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/doc-linter.py | 133 |
1 files changed, 73 insertions, 60 deletions
diff --git a/scripts/doc-linter.py b/scripts/doc-linter.py index 3dc7c2fc..ec00472c 100644 --- a/scripts/doc-linter.py +++ b/scripts/doc-linter.py @@ -27,6 +27,20 @@ MAC = r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})' NUMBER = r"([\s']\d+[\s'])" +SUPPORTED_EXTS = ('.md', '.rst', '.txt') + +# MyST / Markdown fenced code block: leading whitespace + 3+ backticks or 3+ colons. +# Same character and length-or-greater closes. +MD_FENCE_RE = re.compile(r'^(\s*)(`{3,}|:{3,})') + + +def is_suppression_marker(line, kind): + """Detect `.. stop_vyoslinter` (RST/txt) or `% stop_vyoslinter` (MyST).""" + token = f"{kind}_vyoslinter" + if token not in line: + return False + return f".. {token}" in line or f"% {token}" in line + def lint_mac(cnt, line): mac = re.search(MAC, line, re.I) @@ -79,65 +93,64 @@ def lint_linelen(cnt, line): def handle_file_action(filepath): errors = [] - try: - with open(filepath) as fp: - line = fp.readline() - cnt = 1 - test_line_lenght = True - start_vyoslinter = True - indentation = 0 - while line: - # search for ignore linter comments in lines - if ".. stop_vyoslinter" in line: - start_vyoslinter = False - if ".. start_vyoslinter" in line: - start_vyoslinter = True - if start_vyoslinter: - # ignore every '.. code-block::' for line lenght - # rst code-block have its own style in html the format in rst - # and the build page must be the same - if test_line_lenght is False: - if len(line) > indentation: - #print(f"'{line}'") - #print(indentation) - if line[indentation].isspace() is False: - test_line_lenght = True - - if ".. code-block::" in line: - test_line_lenght = False - indentation = 0 - for i in line: - if i.isspace(): - indentation = indentation + 1 - else: - break - - err_mac = lint_mac(cnt, line.strip()) - # disable mac detection for the moment, too many false positives - err_mac = None - err_ip4 = lint_ipv4(cnt, line.strip()) - err_ip6 = lint_ipv6(cnt, line.strip()) - if test_line_lenght: - err_len = lint_linelen(cnt, line) + in_md_fence = False + md_fence_char = None + md_fence_min_len = 0 + in_rst_codeblock = False + rst_codeblock_indent = 0 + start_vyoslinter = True + cnt = 0 + + with open(filepath) as fp: + for cnt, line in enumerate(fp, start=1): + if is_suppression_marker(line, 'stop'): + start_vyoslinter = False + if is_suppression_marker(line, 'start'): + start_vyoslinter = True + + if not start_vyoslinter: + continue + + # MD/MyST fenced code block tracking (``` or :::) + fence_match = MD_FENCE_RE.match(line) + if fence_match: + fence = fence_match.group(2) + fence_char = fence[0] + fence_len = len(fence) + if not in_md_fence: + in_md_fence = True + md_fence_char = fence_char + md_fence_min_len = fence_len + elif fence_char == md_fence_char and fence_len >= md_fence_min_len: + in_md_fence = False + + # RST `.. code-block::` tracking (existing semantics for .rst/.txt). + if in_rst_codeblock: + if len(line) > rst_codeblock_indent and not line[rst_codeblock_indent].isspace(): + in_rst_codeblock = False + if not in_rst_codeblock and ".. code-block::" in line: + in_rst_codeblock = True + rst_codeblock_indent = 0 + for ch in line: + if ch.isspace(): + rst_codeblock_indent += 1 else: - err_len = None - if err_mac: - errors.append(err_mac) - if err_ip4: - errors.append(err_ip4) - if err_ip6: - errors.append(err_ip6) - if err_len: - errors.append(err_len) - - line = fp.readline() - cnt += 1 - - # ensure linter was not stop on top and forgot to tun on again - if start_vyoslinter == False: - errors.append((f"Don't forgett to turn linter back on", cnt, 'error')) - finally: - fp.close() + break + + test_line_length = not (in_md_fence or in_rst_codeblock) + + err_mac = lint_mac(cnt, line.strip()) + # disable mac detection for the moment, too many false positives + err_mac = None + err_ip4 = lint_ipv4(cnt, line.strip()) + err_ip6 = lint_ipv6(cnt, line.strip()) + err_len = lint_linelen(cnt, line) if test_line_length else None + for e in (err_mac, err_ip4, err_ip6, err_len): + if e: + errors.append(e) + + if not start_vyoslinter: + errors.append(("Don't forget to turn linter back on", cnt, 'error')) if len(errors) > 0: ''' @@ -156,14 +169,14 @@ def main(): try: files = ast.literal_eval(sys.argv[1]) for file in files: - if file[-4:] in [".rst", ".txt"] and "_build" not in file: + if file.endswith(SUPPORTED_EXTS) and "_build" not in file: if handle_file_action(file) is False: bool_error = False except Exception as e: for root, dirs, files in os.walk("docs"): path = root.split(os.sep) for file in files: - if file[-4:] in [".rst", ".txt"] and "_build" not in path: + if file.endswith(SUPPORTED_EXTS) and "_build" not in path: fpath = '/'.join(path) filepath = f"{fpath}/{file}" if handle_file_action(filepath) is False: |
