Skip to content

Commit 98c0e22

Browse files
authored
docs: fix RST double-backtick notation breaking API cross-reference links (#658)
RST-style ``Symbol`` in docstrings caused add_cross_references to generate malformed link syntax: `[`Symbol`](url)` renders as inline code rather than a clickable link in Mintlify. - Add normalize_rst_backticks() pass in decorate_api_mdx.py (runs before add_cross_references) to convert ``x`` to `x` in MDX prose - Add validate_rst_docstrings() in validate.py to scan source files and report occurrences as a warning (does not fail the build) 91 source files / 992 occurrences detected; fix is applied at build time. Source cleanup tracked separately.
1 parent 3c0cfa4 commit 98c0e22

2 files changed

Lines changed: 111 additions & 3 deletions

File tree

tooling/docs-autogen/decorate_api_mdx.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,39 @@ def replace_md(match):
117117
return content
118118

119119

120+
# =========================
121+
# RST double-backtick normalisation
122+
# =========================
123+
124+
_RST_DOUBLE_BACKTICK_RE = re.compile(r"``([^`]+)``")
125+
126+
127+
def normalize_rst_backticks(content: str) -> str:
128+
"""Convert RST-style double-backtick literals to single-backtick Markdown.
129+
130+
Replaces ``Symbol`` with `Symbol` in MDX prose (outside fenced code blocks).
131+
This prevents add_cross_references from generating malformed link syntax such
132+
as `[`Backend`](url)` where the link is wrapped in an extra code span and
133+
renders as raw text rather than a clickable link.
134+
135+
Args:
136+
content: MDX file content
137+
138+
Returns:
139+
Content with ``x`` replaced by `x` outside code fences
140+
"""
141+
lines = content.splitlines(keepends=True)
142+
result = []
143+
in_fence = False
144+
for line in lines:
145+
if line.lstrip().startswith("```"):
146+
in_fence = not in_fence
147+
if not in_fence:
148+
line = _RST_DOUBLE_BACKTICK_RE.sub(r"`\1`", line)
149+
result.append(line)
150+
return "".join(result)
151+
152+
120153
# =========================
121154
# MDX escaping
122155
# =========================
@@ -801,11 +834,17 @@ def process_mdx_file(
801834
else:
802835
module_path = path.stem
803836

837+
# Step 0.5: Normalise RST double-backtick notation → single backtick
838+
# Must run before add_cross_references so ``Symbol`` doesn't generate `[`Symbol`](url)`
839+
text = normalize_rst_backticks(original)
840+
804841
# Step 1: Fix GitHub source links
805-
text = fix_source_links(original, version)
842+
text = fix_source_links(text, version)
806843

807-
# Step 2: Inject preamble
844+
# Step 2: Inject preamble (docstring cache text may also contain RST notation;
845+
# inject_preamble runs after normalize so the injected text needs a second pass)
808846
text = inject_preamble(text, module_path, docstring_cache)
847+
text = normalize_rst_backticks(text)
809848

810849
# Step 3: inject SidebarFix
811850
text = inject_sidebar_fix(text)

tooling/docs-autogen/validate.py

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,13 +253,51 @@ def mintlify_anchor(heading: str) -> str:
253253
return len(errors), errors
254254

255255

256+
def validate_rst_docstrings(source_dir: Path) -> tuple[int, list[str]]:
257+
"""Scan Python source files for RST double-backtick notation in docstrings.
258+
259+
RST-style ``Symbol`` double-backtick markup interacts badly with the
260+
add_cross_references step: the regex matches the inner single-backtick
261+
boundary and generates a broken link wrapped in an extra code span, e.g.
262+
``Backend`` → `[`Backend`](url)` which Mintlify renders as raw text
263+
rather than a clickable link.
264+
265+
Args:
266+
source_dir: Root of the Python source tree to scan (e.g. repo/mellea)
267+
268+
Returns:
269+
Tuple of (error_count, error_messages)
270+
"""
271+
errors = []
272+
# Match ``Word`` where Word starts with a letter (RST inline literal)
273+
pattern = re.compile(r"``([A-Za-z][^`]*)``")
274+
275+
for py_file in source_dir.rglob("*.py"):
276+
try:
277+
content = py_file.read_text(encoding="utf-8")
278+
except Exception:
279+
continue
280+
281+
for line_num, line in enumerate(content.splitlines(), 1):
282+
if pattern.search(line):
283+
rel = py_file.relative_to(source_dir.parent)
284+
errors.append(
285+
f"{rel}:{line_num}: RST double-backtick notation — "
286+
f"use single backticks for Markdown/MDX compatibility\n"
287+
f" {line.strip()[:100]}"
288+
)
289+
290+
return len(errors), errors
291+
292+
256293
def generate_report(
257294
source_link_errors: list[str],
258295
coverage_passed: bool,
259296
coverage_report: dict,
260297
mdx_errors: list[str],
261298
link_errors: list[str],
262299
anchor_errors: list[str],
300+
rst_docstring_errors: list[str] | None = None,
263301
) -> dict:
264302
"""Generate validation report.
265303
@@ -294,12 +332,18 @@ def generate_report(
294332
"error_count": len(anchor_errors),
295333
"errors": anchor_errors,
296334
},
335+
"rst_docstrings": {
336+
"passed": len(rst_docstring_errors or []) == 0,
337+
"error_count": len(rst_docstring_errors or []),
338+
"errors": rst_docstring_errors or [],
339+
},
297340
"overall_passed": (
298341
len(source_link_errors) == 0
299342
and coverage_passed
300343
and len(mdx_errors) == 0
301344
and len(link_errors) == 0
302345
and len(anchor_errors) == 0
346+
# rst_docstrings is a warning only — does not fail the build
303347
),
304348
}
305349

@@ -320,6 +364,12 @@ def main():
320364
parser.add_argument(
321365
"--skip-coverage", action="store_true", help="Skip coverage validation"
322366
)
367+
parser.add_argument(
368+
"--source-dir",
369+
type=Path,
370+
default=None,
371+
help="Python source root to scan for RST double-backtick notation (e.g. mellea/)",
372+
)
323373
args = parser.parse_args()
324374

325375
docs_dir = Path(args.docs_dir)
@@ -356,6 +406,11 @@ def main():
356406
print("Checking anchor collisions...")
357407
_, anchor_errors = validate_anchor_collisions(docs_dir)
358408

409+
rst_docstring_errors: list[str] = []
410+
if args.source_dir:
411+
print("Checking source docstrings for RST double-backtick notation...")
412+
_, rst_docstring_errors = validate_rst_docstrings(args.source_dir)
413+
359414
# Generate report
360415
report = generate_report(
361416
source_link_errors,
@@ -364,6 +419,7 @@ def main():
364419
mdx_errors,
365420
link_errors,
366421
anchor_errors,
422+
rst_docstring_errors,
367423
)
368424

369425
# Print results
@@ -398,14 +454,27 @@ def main():
398454
if not report["anchor_collisions"]["passed"]:
399455
print(f" {report['anchor_collisions']['error_count']} errors found")
400456

457+
if args.source_dir:
458+
print(
459+
f"✅ RST docstrings: {'PASS' if report['rst_docstrings']['passed'] else 'FAIL'}"
460+
)
461+
if not report["rst_docstrings"]["passed"]:
462+
print(f" {report['rst_docstrings']['error_count']} occurrences found")
463+
401464
print("\n" + "=" * 60)
402465
print(f"Overall: {'✅ PASS' if report['overall_passed'] else '❌ FAIL'}")
403466
print("=" * 60)
404467

405468
# Print detailed errors
406469
if not report["overall_passed"]:
407470
print("\nDetailed Errors:")
408-
for error in source_link_errors + mdx_errors + link_errors + anchor_errors:
471+
for error in (
472+
source_link_errors
473+
+ mdx_errors
474+
+ link_errors
475+
+ anchor_errors
476+
+ rst_docstring_errors
477+
):
409478
print(f" • {error}")
410479

411480
# Save report

0 commit comments

Comments
 (0)