Import LinuxCNC source tree
This commit is contained in:
84
scripts/docs-anchor-check.py
Executable file
84
scripts/docs-anchor-check.py
Executable file
@@ -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())
|
||||
Reference in New Issue
Block a user