Skip to content

Commit 42b738e

Browse files
sbryngelsonclaude
andcommitted
Strengthen doc linter with new Doxygen rendering checks
Add three new checks to lint_docs.py: - check_unpaired_math: catches unpaired \f$ delimiters and unbalanced \f[/\f] display math blocks that break all subsequent rendering - check_doxygen_commands_in_backticks: catches Doxygen @ and \ block commands inside backtick code spans (known Doxygen bug #6054) - check_single_quote_in_backtick: catches single quotes in single- backtick spans which Doxygen treats as ending the span Fix 7 pre-existing single-quote issues found by the new check in case.md, contributing.md, and testing.md (use double backticks). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 00c2485 commit 42b738e

4 files changed

Lines changed: 168 additions & 6 deletions

File tree

docs/documentation/case.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ These physical parameters must be consistent with fluid material's parameters de
298298
#### Elliptic Smoothing
299299

300300
Initial conditions in which not all patches support the `patch_icpp(j)%%smoothen` parameter can still be smoothed by applying iterations of the heat equation to the initial condition.
301-
This is enabled by adding `'elliptic_smoothing': "T",` and `'elliptic_smoothing_iters': N,` to the case dictionary, where `N` is the number of smoothing iterations to apply.
301+
This is enabled by adding ``'elliptic_smoothing': "T",`` and ``'elliptic_smoothing_iters': N,`` to the case dictionary, where `N` is the number of smoothing iterations to apply.
302302

303303
### 4. Immersed Boundary Patches {#sec-immersed-boundary-patches}
304304

@@ -658,7 +658,7 @@ If `file_per_process` is true, then pre_process, simulation, and post_process mu
658658

659659
- `cons_vars_wrt` and `prim_vars_wrt` activate the output of conservative and primitive state variables into the database.
660660

661-
- `[variable's name]_wrt` activates the output of each specified variable into the database.
661+
- ``[variable's name]_wrt`` activates the output of each specified variable into the database.
662662

663663
- `schlieren_alpha(i)` specifies the intensity of the numerical Schlieren of $i$-th component.
664664

@@ -898,11 +898,11 @@ The parameters are optionally used to define initial velocity profiles and pertu
898898

899899
- `mixlayer_vel_profile` activates setting the mean streamwise velocity to a hyperbolic tangent profile. This option works only for `n > 0`.
900900

901-
- `mixlayer_vel_coef` is a parameter for the hyperbolic tangent profile of a mixing layer when `mixlayer_vel_profile = 'T'`. The mean streamwise velocity profile is given as:
901+
- `mixlayer_vel_coef` is a parameter for the hyperbolic tangent profile of a mixing layer when ``mixlayer_vel_profile = 'T'``. The mean streamwise velocity profile is given as:
902902

903903
\f[ u = \text{patch\_icpp(1)\%vel(1)} \cdot \tanh( y_{cc} \cdot \text{mixlayer\_vel\_coef}) \f]
904904

905-
- `mixlayer_perturb` activates the velocity perturbation for a temporal mixing layer with hyperbolic tangent mean streamwise velocity profile, using an inverter version of the spectrum-based synthetic turbulence generation method proposed by \cite Guo23. This option only works for `p > 0` and `mixlayer_vel_profile = 'T'`.
905+
- `mixlayer_perturb` activates the velocity perturbation for a temporal mixing layer with hyperbolic tangent mean streamwise velocity profile, using an inverter version of the spectrum-based synthetic turbulence generation method proposed by \cite Guo23. This option only works for `p > 0` and ``mixlayer_vel_profile = 'T'``.
906906

907907
### 11. Phase Change Model {#sec-phase-change}
908908
| Parameter | Type | Description |

docs/documentation/contributing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ $:END_GPU_PARALLEL_LOOP()
380380
Key rules:
381381
- Always pair `$:GPU_PARALLEL_LOOP(...)` with `$:END_GPU_PARALLEL_LOOP()`
382382
- Use `collapse(n)` to fuse nested loops when the loop bounds are independent
383-
- Declare all loop-local temporaries in `private='[...]'`
383+
- Declare all loop-local temporaries in ``private='[...]'``
384384
- Never use `stop` or `error stop` inside a GPU loop
385385

386386
### How to Allocate and Manage GPU Arrays

docs/documentation/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ To test the post-processing code, append the `-a` or `--test-all` option:
9999
./mfc.sh test -a -j 8
100100
```
101101

102-
This argument will re-run the test stack with `parallel_io='T'`, which generates silo_hdf5 files.
102+
This argument will re-run the test stack with ``parallel_io='T'``, which generates silo_hdf5 files.
103103
It will also turn most write parameters (`*_wrt`) on.
104104
Then, it searches through the silo files using `h5dump` to ensure that there are no `NaN`s or `Infinity`s.
105105
Although adding this option does not guarantee that accurate `.silo` files are generated, it does ensure that the post-process code does not fail or produce malformed data.

toolchain/mfc/lint_docs.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,165 @@ def check_cli_refs(repo_root: Path) -> list[str]:
549549
return errors
550550

551551

552+
def check_unpaired_math(repo_root: Path) -> list[str]:
553+
"""Check for unpaired \\f$ inline math and unbalanced \\f[/\\f] display math."""
554+
doc_dir = repo_root / "docs" / "documentation"
555+
if not doc_dir.exists():
556+
return []
557+
558+
ignored = _gitignored_docs(repo_root)
559+
errors = []
560+
561+
for md_file in sorted(doc_dir.glob("*.md")):
562+
if md_file.name in ignored:
563+
continue
564+
text = md_file.read_text(encoding="utf-8")
565+
rel = md_file.relative_to(repo_root)
566+
in_code = False
567+
display_math_open = 0 # line number where \f[ was opened, 0 = closed
568+
569+
for i, line in enumerate(text.split("\n"), 1):
570+
if line.strip().startswith("```"):
571+
in_code = not in_code
572+
continue
573+
if in_code:
574+
continue
575+
576+
# Count \f$ occurrences (should be even per line for inline math)
577+
inline_count = len(re.findall(r"\\f\$", line))
578+
if inline_count % 2 != 0:
579+
errors.append(
580+
f" {rel}:{i} has {inline_count} \\f$ delimiter(s) (odd)."
581+
" Fix: ensure every \\f$ has a matching closing \\f$"
582+
)
583+
584+
# Track \f[ / \f] balance
585+
opens = len(re.findall(r"\\f\[", line))
586+
closes = len(re.findall(r"\\f\]", line))
587+
for _ in range(opens):
588+
if display_math_open:
589+
errors.append(
590+
f" {rel}:{i} opens \\f[ but previous \\f["
591+
f" from line {display_math_open} is still open."
592+
" Fix: add missing \\f]"
593+
)
594+
display_math_open = i
595+
for _ in range(closes):
596+
if not display_math_open:
597+
errors.append(
598+
f" {rel}:{i} has \\f] without a preceding \\f[."
599+
" Fix: add missing \\f[ or remove extra \\f]"
600+
)
601+
else:
602+
display_math_open = 0
603+
604+
if display_math_open:
605+
errors.append(
606+
f" {rel}:{display_math_open} opens \\f[ that is never closed."
607+
" Fix: add \\f] to close the display math block"
608+
)
609+
610+
return errors
611+
612+
613+
# Doxygen block commands that are incorrectly processed inside backtick
614+
# code spans (known Doxygen bug, see github.com/doxygen/doxygen/issues/6054).
615+
_DOXYGEN_BLOCK_CMDS = {
616+
"code", "endcode", "verbatim", "endverbatim",
617+
"dot", "enddot", "msc", "endmsc",
618+
"startuml", "enduml",
619+
"latexonly", "endlatexonly", "htmlonly", "endhtmlonly",
620+
"xmlonly", "endxmlonly", "rtfonly", "endrtfonly",
621+
"manonly", "endmanonly", "docbookonly", "enddocbookonly",
622+
"todo", "deprecated", "bug", "test",
623+
"note", "warning", "attention", "remark",
624+
"brief", "details", "param", "return", "returns",
625+
}
626+
627+
628+
def check_doxygen_commands_in_backticks(repo_root: Path) -> list[str]:
629+
"""Check for Doxygen @/\\ commands inside backtick code spans.
630+
631+
Doxygen processes certain block commands even inside backtick code
632+
spans, which can break rendering. Flag these so authors can
633+
restructure or use fenced code blocks instead.
634+
"""
635+
doc_dir = repo_root / "docs" / "documentation"
636+
if not doc_dir.exists():
637+
return []
638+
639+
ignored = _gitignored_docs(repo_root)
640+
code_span_re = re.compile(r"``([^`\n]+)``|`([^`\n]+)`")
641+
# Match @cmd or \cmd where cmd is a known Doxygen block command
642+
cmd_pattern = "|".join(re.escape(c) for c in sorted(_DOXYGEN_BLOCK_CMDS))
643+
doxy_cmd_re = re.compile(rf"(?:@|\\)({cmd_pattern})\b")
644+
645+
errors = []
646+
for md_file in sorted(doc_dir.glob("*.md")):
647+
if md_file.name in ignored:
648+
continue
649+
text = md_file.read_text(encoding="utf-8")
650+
rel = md_file.relative_to(repo_root)
651+
in_code = False
652+
for i, line in enumerate(text.split("\n"), 1):
653+
if line.strip().startswith("```"):
654+
in_code = not in_code
655+
continue
656+
if in_code:
657+
continue
658+
for m in code_span_re.finditer(line):
659+
span = m.group(1) or m.group(2)
660+
cmd_match = doxy_cmd_re.search(span)
661+
if cmd_match:
662+
cmd = cmd_match.group(0)
663+
errors.append(
664+
f" {rel}:{i} backtick span contains Doxygen"
665+
f" command '{cmd}' which may be processed."
666+
" Fix: use a fenced code block or rephrase"
667+
)
668+
669+
return errors
670+
671+
672+
def check_single_quote_in_backtick(repo_root: Path) -> list[str]:
673+
"""Check for single quotes inside single-backtick code spans.
674+
675+
Doxygen treats a single quote inside a single-backtick code span as
676+
ending the span (backward-compat quirk). Use double backticks instead.
677+
"""
678+
doc_dir = repo_root / "docs" / "documentation"
679+
if not doc_dir.exists():
680+
return []
681+
682+
ignored = _gitignored_docs(repo_root)
683+
# Match single-backtick spans (not double-backtick)
684+
single_bt_re = re.compile(r"(?<!`)`([^`\n]+)`(?!`)")
685+
686+
errors = []
687+
for md_file in sorted(doc_dir.glob("*.md")):
688+
if md_file.name in ignored:
689+
continue
690+
text = md_file.read_text(encoding="utf-8")
691+
rel = md_file.relative_to(repo_root)
692+
in_code = False
693+
for i, line in enumerate(text.split("\n"), 1):
694+
if line.strip().startswith("```"):
695+
in_code = not in_code
696+
continue
697+
if in_code:
698+
continue
699+
for m in single_bt_re.finditer(line):
700+
span = m.group(1)
701+
if "'" in span:
702+
errors.append(
703+
f" {rel}:{i} single-backtick span `{span}` contains"
704+
" a single quote, which Doxygen treats as ending the"
705+
f" span. Fix: use double backticks ``{span}``"
706+
)
707+
708+
return errors
709+
710+
552711
def main():
553712
repo_root = Path(__file__).resolve().parents[2]
554713

@@ -558,7 +717,10 @@ def main():
558717
all_errors.extend(check_param_refs(repo_root))
559718
all_errors.extend(check_page_refs(repo_root))
560719
all_errors.extend(check_math_syntax(repo_root))
720+
all_errors.extend(check_unpaired_math(repo_root))
561721
all_errors.extend(check_doxygen_percent(repo_root))
722+
all_errors.extend(check_doxygen_commands_in_backticks(repo_root))
723+
all_errors.extend(check_single_quote_in_backtick(repo_root))
562724
all_errors.extend(check_section_anchors(repo_root))
563725
all_errors.extend(check_physics_docs_coverage(repo_root))
564726
all_errors.extend(check_identifier_refs(repo_root))

0 commit comments

Comments
 (0)