From a2c2c5494e82ebb34ee8116bb7001fc3388c170f Mon Sep 17 00:00:00 2001 From: Fangliding Date: Mon, 25 May 2026 21:06:14 +0800 Subject: [PATCH] Check json focus in CI --- .github/check-scripts/check_code_focus.py | 105 ++++++++++++++++++++++ .github/workflows/postcheck.yml | 23 +++++ docs/config/fakedns.md | 1 - docs/en/config/fakedns.md | 1 - docs/ru/config/fakedns.md | 1 - docs/ru/config/inbounds/vless.md | 2 +- docs/ru/config/outbounds/vmess.md | 2 +- 7 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 .github/check-scripts/check_code_focus.py create mode 100644 .github/workflows/postcheck.yml diff --git a/.github/check-scripts/check_code_focus.py b/.github/check-scripts/check_code_focus.py new file mode 100644 index 00000000..32bfd189 --- /dev/null +++ b/.github/check-scripts/check_code_focus.py @@ -0,0 +1,105 @@ +# Validate that lines covered by // [!code focus:N] form valid JSONC when wrapped in {}. +import re +import sys +from pathlib import Path + +FOCUS_RE = re.compile(r"//\s*\[!code focus:(\d+)\]") +CODEBLOCK_START = re.compile(r"^```json\w*") +CODEBLOCK_END = re.compile(r"^```\s*$") + + +def strip_jsonc_comments(text: str) -> str: + result = [] + for line in text.splitlines(): + stripped = line.lstrip() + if stripped.startswith("//"): + continue + if "//" in line: + in_str = False + escape = False + cut = -1 + for i, ch in enumerate(line): + if escape: + escape = False + continue + if ch == "\\": + escape = True + continue + if ch == '"': + in_str = not in_str + if not in_str and line[i : i + 2] == "//": + cut = i + break + if cut >= 0: + line = line[:cut] + result.append(line) + return "\n".join(result) + + +def validate_jsonc(fragment: str) -> str | None: + import json + + cleaned = strip_jsonc_comments(fragment) + wrapped = "{\n" + cleaned + "\n}" + try: + json.loads(wrapped) + return None + except json.JSONDecodeError as e: + return str(e) + + +def check_file(path: Path) -> list[str]: + errors = [] + lines = path.read_text(encoding="utf-8").splitlines() + in_json_block = False + + for i, line in enumerate(lines): + stripped = line.strip() + if CODEBLOCK_START.match(stripped): + in_json_block = True + continue + if CODEBLOCK_END.match(stripped): + in_json_block = False + continue + if not in_json_block: + continue + + m = FOCUS_RE.search(line) + if not m: + continue + + n = int(m.group(1)) + start = i + 1 + end = min(start + n, len(lines)) + fragment = "\n".join(lines[start:end]) + err = validate_jsonc(fragment) + if err: + rel = path.as_posix() + errors.append(f" {rel}:{i + 1} focus:{n} {err}") + + return errors + + +def main() -> int: + docs_root = Path.cwd() + all_errors: list[str] = [] + + for md in sorted(docs_root.rglob("*.md")): + text = md.read_text(encoding="utf-8") + if "[!code focus:" not in text: + continue + errs = check_file(md) + all_errors.extend(errs) + + if all_errors: + print(f"Found {len(all_errors)} invalid focus block(s):\n") + for e in all_errors: + print(e) + return 1 + + print("All focus blocks are valid JSONC.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/postcheck.yml b/.github/workflows/postcheck.yml new file mode 100644 index 00000000..48d886e6 --- /dev/null +++ b/.github/workflows/postcheck.yml @@ -0,0 +1,23 @@ +name: Post Check + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Check code focus blocks + run: python .github/check-scripts/check_code_focus.py diff --git a/docs/config/fakedns.md b/docs/config/fakedns.md index 5bc9041e..72f9bdbf 100644 --- a/docs/config/fakedns.md +++ b/docs/config/fakedns.md @@ -158,7 +158,6 @@ FakeDNS 本质上是一个 [DNS 服务器](./dns.md#serverobject),能够与任 { "dns": { "servers": [ - // [!code focus:13] { "address": "fakedns", "domains": [ diff --git a/docs/en/config/fakedns.md b/docs/en/config/fakedns.md index 6e164a98..4222c36c 100644 --- a/docs/en/config/fakedns.md +++ b/docs/en/config/fakedns.md @@ -158,7 +158,6 @@ When using DNS routing (traffic splitting), to ensure `fakedns` has high priorit { "dns": { "servers": [ - // [!code focus:13] { "address": "fakedns", "domains": [ diff --git a/docs/ru/config/fakedns.md b/docs/ru/config/fakedns.md index bb830343..4cf7cc6a 100644 --- a/docs/ru/config/fakedns.md +++ b/docs/ru/config/fakedns.md @@ -161,7 +161,6 @@ FakeDNS будет использовать этот блок IP-адресов { "dns": { "servers": [ - // [!code focus:13] { "address": "fakedns", "domains": [ diff --git a/docs/ru/config/inbounds/vless.md b/docs/ru/config/inbounds/vless.md index beb6b00b..c6c13640 100644 --- a/docs/ru/config/inbounds/vless.md +++ b/docs/ru/config/inbounds/vless.md @@ -14,7 +14,7 @@ VLESS - это легкий транспортный протокол без с { // ... "protocol": "vless", - // [!code focus:17] + // [!code focus:18] "settings": { "flow": "xtls-rprx-vision", "users": [ diff --git a/docs/ru/config/outbounds/vmess.md b/docs/ru/config/outbounds/vmess.md index 757d8a08..18f16525 100644 --- a/docs/ru/config/outbounds/vmess.md +++ b/docs/ru/config/outbounds/vmess.md @@ -16,7 +16,7 @@ VMess полагается на системное время. Убедитес { // ... "protocol": "vmess", - // [!code focus:8] + // [!code focus:6] "settings": { "id": "5783a3e7-e373-51cd-8642-c83782b807c5", "security": "auto",