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())