fix(wiki): protect inline-code spans from cross-ref injection

_inject_cross_refs temporarily replaces backtick spans with null-byte
placeholders before injecting [[slug]] cross-refs, then restores them.
This prevents concept titles inside `code` from being rewritten.
Add tests for inline-code protection and full-list cross-ref injection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-06-14 03:49:06 +02:00
parent 0202a2266e
commit ef46d14aed
2 changed files with 54 additions and 1 deletions

View File

@@ -284,6 +284,9 @@ def _run_grounding_guard(
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_INLINE_CODE_RE = re.compile(r"`[^`]+`")
def _inject_cross_refs( def _inject_cross_refs(
markdown: str, markdown: str,
all_concepts: list[Concept], all_concepts: list[Concept],
@@ -292,8 +295,21 @@ def _inject_cross_refs(
"""Replace occurrences of other concept titles/aliases with ``[[slug]]`` links. """Replace occurrences of other concept titles/aliases with ``[[slug]]`` links.
Only exact case-insensitive whole-word matches outside of existing Only exact case-insensitive whole-word matches outside of existing
``[[…]]`` blocks or inline code are replaced. ``[[…]]`` blocks or inline code spans are replaced.
Inline-code spans (`` `…` ``) are temporarily protected by null-byte
placeholders and restored after injection.
""" """
# Step 1: protect inline-code spans from replacement
placeholders: dict[str, str] = {}
def _protect(m: re.Match[str]) -> str:
key = f"\x00{len(placeholders)}\x00"
placeholders[key] = m.group(0)
return key
markdown = _INLINE_CODE_RE.sub(_protect, markdown)
# Step 2: inject cross-refs on unprotected text
for concept in all_concepts: for concept in all_concepts:
if concept.slug == current_slug: if concept.slug == current_slug:
continue continue
@@ -304,6 +320,11 @@ def _inject_cross_refs(
pattern = re.compile(rf"(?<!\[\[)\b{escaped}\b(?!\]\])", re.IGNORECASE) pattern = re.compile(rf"(?<!\[\[)\b{escaped}\b(?!\]\])", re.IGNORECASE)
replacement = f"[[{concept.slug}]]" replacement = f"[[{concept.slug}]]"
markdown = pattern.sub(replacement, markdown) markdown = pattern.sub(replacement, markdown)
# Step 3: restore inline-code spans
for key, val in placeholders.items():
markdown = markdown.replace(key, val)
return markdown return markdown

View File

@@ -212,6 +212,38 @@ def test_inject_cross_refs_replaces_alias() -> None:
assert "[[circle-packing]]" in result assert "[[circle-packing]]" in result
def test_inject_cross_refs_skips_inline_code() -> None:
"""Concept titles inside backtick inline-code spans are NOT replaced."""
concepts = [
Concept(
slug="circle-packing",
title="circle-packing",
aliases=[],
),
FIXTURE_CONCEPT,
]
# The term "circle-packing" appears both bare (should be replaced)
# and inside backticks (should NOT be replaced).
text = "Use `circle-packing` for this. See also circle-packing theory."
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
assert "`circle-packing`" in result # inline-code preserved
assert "[[circle-packing]]" in result # bare occurrence replaced
def test_inject_cross_refs_full_list_even_when_single_concept() -> None:
"""Cross-ref injection uses the full concept list, not just the compiled concept."""
concepts = [
Concept(slug="circle-packing", title="Circle Packing", aliases=[]),
Concept(slug="discrete-conformal-map", title="Discrete Conformal Map", aliases=[]),
FIXTURE_CONCEPT,
]
# Compiling lobachevsky-function only; text mentions the other two concepts
text = "Discrete Conformal Map is used with Circle Packing."
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
assert "[[circle-packing]]" in result
assert "[[discrete-conformal-map]]" in result
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# compile_concept (full mock) # compile_concept (full mock)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------