Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/src/man/man1/linuxcncrsh.1.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/src/man/man1/mesambccc.1.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):

Expand All @@ -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 '<command>' 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
Expand Down Expand Up @@ -387,6 +390,7 @@ Recognized '<initlist>/<command>/<data>' 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 '<command>' in the '<commands>' section maps to one or more HAL pins with
specific type using the 'haltype' attribute. Recognized are:
Expand Down
84 changes: 84 additions & 0 deletions scripts/docs-anchor-check.py
Original file line number Diff line number Diff line change
@@ -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())
Loading