From 28b8909e911dbeae325cde1cff87e77b228f21bc Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:08:23 +0800 Subject: [PATCH] docs: add derived-id section reference lint scripts/docs-anchor-check.py, wired into the htmldocs CI job with --enforce, flags <<_derived>> xrefs and link:...html#_derived URLs whose target has no explicit [[anchor]] in docs/src. Derived ids come from section titles, so such references break when a title is translated or retitled. Pin the target with an explicit anchor instead. The lint reads only the English sources, so translations cannot trip it. Also pins the remaining lint hits, all same-page links in untranslated man pages (_subcommands, _mbccs_file_format, _modbus_functions, _modbus_types, _hal_types). Explicit ids match the auto-generated ones, so output is unchanged. With the five anchors from #4339 merged, the lint reports the tree clean. --- .github/workflows/ci.yml | 2 + docs/src/man/man1/linuxcncrsh.1.adoc | 1 + docs/src/man/man1/mesambccc.1.adoc | 4 ++ scripts/docs-anchor-check.py | 84 ++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100755 scripts/docs-anchor-check.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1d9c9a1430..518315a9d67 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -187,6 +187,8 @@ jobs: .github/scripts/build-doc.sh - name: Check manpage names match filenames run: scripts/manpage-name-check.py --enforce docs/build/man + - name: Check for derived-id section references + run: scripts/docs-anchor-check.py --enforce - name: Verify no untracked or modified files after build run: | #*.po and documentation.pot are modifyed by build. Ignore them for now. diff --git a/docs/src/man/man1/linuxcncrsh.1.adoc b/docs/src/man/man1/linuxcncrsh.1.adoc index bff8aa543e7..4a3e5ec4f0c 100644 --- a/docs/src/man/man1/linuxcncrsh.1.adoc +++ b/docs/src/man/man1/linuxcncrsh.1.adoc @@ -178,6 +178,7 @@ The supported commands are as follows: Help will respond regardless of whether a 'HELLO' has been successfully negotiated. +[[_subcommands]] == SUBCOMMANDS Commands and parameters are not case sensitive, except for the diff --git a/docs/src/man/man1/mesambccc.1.adoc b/docs/src/man/man1/mesambccc.1.adoc index cafddf82b11..04790753bbf 100644 --- a/docs/src/man/man1/mesambccc.1.adoc +++ b/docs/src/man/man1/mesambccc.1.adoc @@ -28,6 +28,7 @@ below. Output a verbose list of configuration parameters, devices, Modbus messages and HAL pins. +[[_mbccs_file_format]] == MBCCS FILE FORMAT The Modbus command control source file (mbccs file) is an XML formatted document describing the communication parameters, connected devices, HAL pins @@ -59,6 +60,7 @@ All values are checked to be within an acceptable range. Errors and warnings are emitted when values are out of bounds. In case of a warning they may be clamped to the acceptable range. +[[_modbus_functions]] === MODBUS FUNCTIONS A subset of the Modbus functions is supported (with function number in parentheses): @@ -82,6 +84,7 @@ A subset of the Modbus functions is supported (with function number in parenthes You can use the function's symbolic name or numerical value in '' in the 'function' attribute (as in `function="W_REGISTERS"` or `function="16"`). +[[_modbus_types]] === MODBUS TYPES The 'modbustype' attribute declares the interpretation of values to or from a Modbus device's registers. They can be a signed integer (S), unsigned integer @@ -387,6 +390,7 @@ Recognized '//' attributes: 16-bit integer but depends on the 'modbustype' attribute and the range of acceptable values depends on the Modbus function. +[[_hal_types]] === HAL TYPES A '' in the '' section maps to one or more HAL pins with specific type using the 'haltype' attribute. Recognized are: diff --git a/scripts/docs-anchor-check.py b/scripts/docs-anchor-check.py new file mode 100755 index 00000000000..3a60a66821c --- /dev/null +++ b/scripts/docs-anchor-check.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# Flag section references relying on auto-generated AsciiDoc ids: +# <<_derived_id>> xrefs and link:...html#_derived_id URLs. Derived ids come +# from the section title, so the reference breaks when the title is +# translated or retitled. Pin the target with an explicit [[anchor]] instead. +# Pass 1 collects explicit anchor definitions in docs/src, pass 2 reports +# derived-id references with no explicit target. +# Warn-only unless --enforce or DOCS_ANCHOR_CHECK_ENFORCE is set. + +import os +import re +import sys +import glob + +# The script lives in scripts/, the docs are one level up at ../docs, so it runs from anywhere. +HERE = os.path.dirname(os.path.realpath(__file__, strict=True)) +DOCS = os.path.normpath(os.path.join(HERE, '..', 'docs')) +SRC = os.path.join(DOCS, 'src') + +# Explicit anchor definition forms, mirroring ANCHOR_DEF in +# docs/src/extensions/xref_resolver.rb, plus the anchor: macro. +ANCHOR_DEF = re.compile(r""" + \[\[ ([A-Za-z_][\w:.-]*) (?:,[^\]]*)? \]\] | # [[id]] or [[id,reftext]] + \[\# ([A-Za-z_][\w:.-]*) (?:[.%][^\]]*)? \] | # [#id] + \[ (?:[^,\]]*,\s*)* id\s*=\s*["']? ([A-Za-z_][\w:.-]*) ["']? [,\]] | # [id="foo"] + ^anchor: ([A-Za-z_][\w:.-]*) \[\] | # anchor:id[] + ^:id:\s* ([A-Za-z_][\w:.-]*) # :id: foo +""", re.X | re.M) + +# Derived-id references: <<_foo>> / <<_foo,Title>> xrefs and +# link:...html#_foo[...] URL fragments. Namespaced anchors carry a ':', so a +# target that starts with '_' and has no ':' is a derived id by convention. +XREF = re.compile(r'<<(_[A-Za-z0-9][\w.-]*)(?:,.*?)?>>') +LINKURL = re.compile(r'link:[^\s\[]*#(_[A-Za-z0-9][\w.-]*)\[') + +def collect(adoc_files): + defined = {} # anchor -> file + for path in adoc_files: + text = open(path, encoding='utf-8', errors='replace').read() + for m in ANCHOR_DEF.finditer(text): + anchor = next(g for g in m.groups() if g) + defined.setdefault(anchor, path) + return defined + +def find_derived_refs(adoc_files, defined): + problems = [] + for path in adoc_files: + lines = open(path, encoding='utf-8', errors='replace').read().splitlines() + for lineno, line in enumerate(lines, 1): + for regex in (XREF, LINKURL): + for m in regex.finditer(line): + target = m.group(1) + if target not in defined: + rel = os.path.relpath(path, DOCS) + problems.append((rel, lineno, m.group(0)[:60], target)) + return problems + +def main(): + enforce = '--enforce' in sys.argv or os.environ.get('DOCS_ANCHOR_CHECK_ENFORCE') + adoc_files = sorted(glob.glob(f'{SRC}/**/*.adoc', recursive=True)) + defined = collect(adoc_files) + problems = find_derived_refs(adoc_files, defined) + if not problems: + return 0 + out = ['Derived-id section references with no explicit anchor target', + '(these break in translated docs when the target title is translated;', + 'pin the target section with an explicit [[anchor]] and reference that):', + ''] + for rel, lineno, ref, target in problems: + out.append(f'{rel}:{lineno}: {ref}') + out.append(f' target `[[{target}]]` is not explicitly defined anywhere in docs/src') + text = '\n'.join(out) + print(text) + if os.environ.get('GITHUB_ACTIONS'): + summary = os.environ.get('GITHUB_STEP_SUMMARY') + if summary: + with open(summary, 'a', encoding='utf-8') as f: + f.write('## Derived-id anchor check\n\n```\n' + text + '\n```\n') + level = 'error' if enforce else 'warning' + print(f'::{level} title=Derived-id anchor check::{len(problems)} reference(s) rely on auto-generated section ids, see job summary') + return 1 if enforce else 0 + +if __name__ == '__main__': + sys.exit(main())