summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/instructions/doc-linter.instructions.md (renamed from .github/instructions/rst-linter.instructions.md)46
-rw-r--r--scripts/doc-linter.py133
2 files changed, 106 insertions, 73 deletions
diff --git a/.github/instructions/rst-linter.instructions.md b/.github/instructions/doc-linter.instructions.md
index 419f094a..5e7d359e 100644
--- a/.github/instructions/rst-linter.instructions.md
+++ b/.github/instructions/doc-linter.instructions.md
@@ -1,10 +1,10 @@
---
-applyTo: "**/*.rst"
+applyTo: "**/*.md,**/*.rst,**/*.txt"
---
-## RST Linter Rules
+## Doc Linter Rules
-Every pull request is automatically linted by `doc-linter.py` (from the `vyos/.github` repository). It checks **only changed files** for two things: IP address usage and line length.
+Every pull request is automatically linted by `scripts/doc-linter.py`. It checks **only changed files** for two things: IP address usage and line length. MyST Markdown (`.md`), legacy RST (`.rst`), and `.txt` includes are all linted.
### IP Address Rules
@@ -39,11 +39,28 @@ The linter rejects public IP addresses that are not reserved for documentation.
### Line Length Rules
-Maximum 80 characters per line. Lines inside `.. code-block::` directives are **exempt** from the line length limit (they render with `<pre>` tags and preserve source formatting).
+Maximum 80 characters per line. The line-length check is **skipped** inside fenced code:
+
+- MyST / Markdown: ` ```...``` ` and `:::...:::` fenced blocks (any info string, including ` ```{eval-rst} `, ` ```{cfgcmd} `, etc.).
+- RST: lines indented under `.. code-block::` directives.
### Suppression Syntax
-When real public IPs or long lines are unavoidable, wrap the block:
+When real public IPs or long lines are unavoidable, wrap the block. The marker form depends on the surrounding parser:
+
+**MyST Markdown (`.md`)** — use the MyST comment form:
+
+````md
+% stop_vyoslinter
+
+```
+set system name-server '8.8.8.8'
+```
+
+% start_vyoslinter
+````
+
+**RST (`.rst`) and `.txt` includes** — use the RST comment form:
```rst
.. stop_vyoslinter
@@ -55,16 +72,19 @@ When real public IPs or long lines are unavoidable, wrap the block:
.. start_vyoslinter
```
+**Inside a `{eval-rst}` block in MyST** — the embedded content is parsed as RST, so the RST form (`.. stop_vyoslinter`) matches the surrounding parser. The linter recognizes both forms regardless of context, but use the form that fits the parser of the enclosing block.
+
**Rules for suppression markers:**
-- Place `.. stop_vyoslinter` on its own line, immediately before the content
-- Place `.. start_vyoslinter` on its own line, immediately after the content
-- Always re-enable the linter — never leave it stopped for the rest of the file
-- Suppress the smallest possible region
-- Do NOT use suppression for content that can use documentation-reserved addresses instead
+- Place the stop marker on its own line, immediately before the content.
+- Place the start marker on its own line, immediately after the content.
+- Always re-enable the linter — never leave it stopped for the rest of the file.
+- Suppress the smallest possible region.
+- Do NOT use suppression for content that can use documentation-reserved addresses instead.
### Common Mistakes
-- Removing `stop/start_vyoslinter` markers without fixing the underlying issue (exposes the line to the linter and fails CI)
-- Using real public IPs when documentation addresses would work just as well
-- Adding suppression markers around content that only has private (RFC 1918) addresses or `0.0.0.0/0` — these are allowed and don't need suppression
+- Removing `stop/start_vyoslinter` markers without fixing the underlying issue (exposes the line to the linter and fails CI).
+- Using real public IPs when documentation addresses would work just as well.
+- Adding suppression markers around content that only has private (RFC 1918) addresses or `0.0.0.0/0` — these are allowed and don't need suppression.
+- Mixing marker forms (using `% stop_vyoslinter` inside an `{eval-rst}` block, or `.. stop_vyoslinter` in plain MyST prose).
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: