mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-07 08:35:54 +00:00
fix(docs-governance): read HTML comments and code spans in one pass, as CommonMark does
Code spans were blanked before comments, so a code span could swallow a comment's closer and the comment then hid a real link after it. Comments and code spans are now read left to right, whichever opens first. An escaped \<!-- is text, and a comment becomes an opaque run of NUL characters rather than spaces, so a reference definition can no longer start across it.
This commit is contained in:
5 files changed
+102
-73
No files matched your search
@@ -49,10 +49,36 @@ def without_fenced_code(text: str) -> str:
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def without_inline_code(text: str) -> str:
|
||||
def _is_escaped(text: str, index: int) -> bool:
|
||||
backslashes = 0
|
||||
while index - backslashes > 0 and text[index - backslashes - 1] == "\\":
|
||||
backslashes += 1
|
||||
return backslashes % 2 == 1
|
||||
|
||||
|
||||
def without_inline_code_and_html_comments(text: str) -> str:
|
||||
"""Blank out inline code spans and HTML comments, whichever opens first.
|
||||
|
||||
A comment runs from `<!--` to the first `-->` after it (`<!-->` and `<!--->` included). It
|
||||
becomes an opaque run of NUL characters, so a link or a reference definition cannot continue
|
||||
through it, as none can through raw HTML. An escaped `\\<!--`, or an opener with no closer,
|
||||
stays literal text, as in CommonMark.
|
||||
"""
|
||||
output: list[str] = []
|
||||
cursor = 0
|
||||
comments_can_close = True
|
||||
while cursor < len(text):
|
||||
if comments_can_close and text.startswith("<!--", cursor) and not _is_escaped(text, cursor):
|
||||
closing = text.find("-->", cursor + 2)
|
||||
if closing != -1:
|
||||
output.extend(
|
||||
"\n" if character == "\n" else "\0"
|
||||
for character in text[cursor : closing + 3]
|
||||
)
|
||||
cursor = closing + 3
|
||||
continue
|
||||
# -- no closer here means none for any later opener either --
|
||||
comments_can_close = False
|
||||
if text[cursor] != "`":
|
||||
output.append(text[cursor])
|
||||
cursor += 1
|
||||
@@ -74,28 +100,6 @@ def without_inline_code(text: str) -> str:
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def without_html_comments(text: str) -> str:
|
||||
"""Blank out HTML comments, where link-shaped text is not a link.
|
||||
|
||||
A comment runs from `<!--` to the first `-->` after it (`<!-->` and `<!--->` included). An
|
||||
opener with no closer is left alone, as CommonMark leaves it literal text inline.
|
||||
"""
|
||||
output: list[str] = []
|
||||
cursor = 0
|
||||
while True:
|
||||
start = text.find("<!--", cursor)
|
||||
if start == -1:
|
||||
break
|
||||
end = text.find("-->", start + 2)
|
||||
if end == -1:
|
||||
break
|
||||
output.append(text[cursor:start])
|
||||
output.append(re.sub(r"[^\n]", " ", text[start : end + 3]))
|
||||
cursor = end + 3
|
||||
output.append(text[cursor:])
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def inline_link_targets(text: str) -> list[str]:
|
||||
targets: list[str] = []
|
||||
matches = list(INLINE_LINK_START_RE.finditer(text))
|
||||
@@ -195,7 +199,7 @@ def reference_link_targets(text: str) -> list[str]:
|
||||
|
||||
|
||||
def markdown_link_targets(text: str) -> list[str]:
|
||||
text = without_html_comments(without_inline_code(without_fenced_code(text)))
|
||||
text = without_inline_code_and_html_comments(without_fenced_code(text))
|
||||
return inline_link_targets(text) + reference_link_targets(text)
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"id": "hand-seeded",
|
||||
"source": "manual-curation-pending-regeneration"
|
||||
},
|
||||
"registryDigest": "26e3471f7d97e474b311e1cc45c3d967f5be98fca09f264a8cfd8de33ac1158d",
|
||||
"registryDigest": "f2a5daa835fd6ed2974e14b3f6118ac640115fd8caf10fd8568f1b8adc9a432b",
|
||||
"schemaVersion": 1,
|
||||
"triggers": {
|
||||
"skill:agent-runtime-gateway-smoke-test": [
|
||||
|
||||
@@ -49,10 +49,36 @@ def without_fenced_code(text: str) -> str:
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def without_inline_code(text: str) -> str:
|
||||
def _is_escaped(text: str, index: int) -> bool:
|
||||
backslashes = 0
|
||||
while index - backslashes > 0 and text[index - backslashes - 1] == "\\":
|
||||
backslashes += 1
|
||||
return backslashes % 2 == 1
|
||||
|
||||
|
||||
def without_inline_code_and_html_comments(text: str) -> str:
|
||||
"""Blank out inline code spans and HTML comments, whichever opens first.
|
||||
|
||||
A comment runs from `<!--` to the first `-->` after it (`<!-->` and `<!--->` included). It
|
||||
becomes an opaque run of NUL characters, so a link or a reference definition cannot continue
|
||||
through it, as none can through raw HTML. An escaped `\\<!--`, or an opener with no closer,
|
||||
stays literal text, as in CommonMark.
|
||||
"""
|
||||
output: list[str] = []
|
||||
cursor = 0
|
||||
comments_can_close = True
|
||||
while cursor < len(text):
|
||||
if comments_can_close and text.startswith("<!--", cursor) and not _is_escaped(text, cursor):
|
||||
closing = text.find("-->", cursor + 2)
|
||||
if closing != -1:
|
||||
output.extend(
|
||||
"\n" if character == "\n" else "\0"
|
||||
for character in text[cursor : closing + 3]
|
||||
)
|
||||
cursor = closing + 3
|
||||
continue
|
||||
# -- no closer here means none for any later opener either --
|
||||
comments_can_close = False
|
||||
if text[cursor] != "`":
|
||||
output.append(text[cursor])
|
||||
cursor += 1
|
||||
@@ -74,28 +100,6 @@ def without_inline_code(text: str) -> str:
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def without_html_comments(text: str) -> str:
|
||||
"""Blank out HTML comments, where link-shaped text is not a link.
|
||||
|
||||
A comment runs from `<!--` to the first `-->` after it (`<!-->` and `<!--->` included). An
|
||||
opener with no closer is left alone, as CommonMark leaves it literal text inline.
|
||||
"""
|
||||
output: list[str] = []
|
||||
cursor = 0
|
||||
while True:
|
||||
start = text.find("<!--", cursor)
|
||||
if start == -1:
|
||||
break
|
||||
end = text.find("-->", start + 2)
|
||||
if end == -1:
|
||||
break
|
||||
output.append(text[cursor:start])
|
||||
output.append(re.sub(r"[^\n]", " ", text[start : end + 3]))
|
||||
cursor = end + 3
|
||||
output.append(text[cursor:])
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def inline_link_targets(text: str) -> list[str]:
|
||||
targets: list[str] = []
|
||||
matches = list(INLINE_LINK_START_RE.finditer(text))
|
||||
@@ -195,7 +199,7 @@ def reference_link_targets(text: str) -> list[str]:
|
||||
|
||||
|
||||
def markdown_link_targets(text: str) -> list[str]:
|
||||
text = without_html_comments(without_inline_code(without_fenced_code(text)))
|
||||
text = without_inline_code_and_html_comments(without_fenced_code(text))
|
||||
return inline_link_targets(text) + reference_link_targets(text)
|
||||
|
||||
|
||||
|
||||
@@ -49,10 +49,36 @@ def without_fenced_code(text: str) -> str:
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def without_inline_code(text: str) -> str:
|
||||
def _is_escaped(text: str, index: int) -> bool:
|
||||
backslashes = 0
|
||||
while index - backslashes > 0 and text[index - backslashes - 1] == "\\":
|
||||
backslashes += 1
|
||||
return backslashes % 2 == 1
|
||||
|
||||
|
||||
def without_inline_code_and_html_comments(text: str) -> str:
|
||||
"""Blank out inline code spans and HTML comments, whichever opens first.
|
||||
|
||||
A comment runs from `<!--` to the first `-->` after it (`<!-->` and `<!--->` included). It
|
||||
becomes an opaque run of NUL characters, so a link or a reference definition cannot continue
|
||||
through it, as none can through raw HTML. An escaped `\\<!--`, or an opener with no closer,
|
||||
stays literal text, as in CommonMark.
|
||||
"""
|
||||
output: list[str] = []
|
||||
cursor = 0
|
||||
comments_can_close = True
|
||||
while cursor < len(text):
|
||||
if comments_can_close and text.startswith("<!--", cursor) and not _is_escaped(text, cursor):
|
||||
closing = text.find("-->", cursor + 2)
|
||||
if closing != -1:
|
||||
output.extend(
|
||||
"\n" if character == "\n" else "\0"
|
||||
for character in text[cursor : closing + 3]
|
||||
)
|
||||
cursor = closing + 3
|
||||
continue
|
||||
# -- no closer here means none for any later opener either --
|
||||
comments_can_close = False
|
||||
if text[cursor] != "`":
|
||||
output.append(text[cursor])
|
||||
cursor += 1
|
||||
@@ -74,28 +100,6 @@ def without_inline_code(text: str) -> str:
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def without_html_comments(text: str) -> str:
|
||||
"""Blank out HTML comments, where link-shaped text is not a link.
|
||||
|
||||
A comment runs from `<!--` to the first `-->` after it (`<!-->` and `<!--->` included). An
|
||||
opener with no closer is left alone, as CommonMark leaves it literal text inline.
|
||||
"""
|
||||
output: list[str] = []
|
||||
cursor = 0
|
||||
while True:
|
||||
start = text.find("<!--", cursor)
|
||||
if start == -1:
|
||||
break
|
||||
end = text.find("-->", start + 2)
|
||||
if end == -1:
|
||||
break
|
||||
output.append(text[cursor:start])
|
||||
output.append(re.sub(r"[^\n]", " ", text[start : end + 3]))
|
||||
cursor = end + 3
|
||||
output.append(text[cursor:])
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def inline_link_targets(text: str) -> list[str]:
|
||||
targets: list[str] = []
|
||||
matches = list(INLINE_LINK_START_RE.finditer(text))
|
||||
@@ -195,7 +199,7 @@ def reference_link_targets(text: str) -> list[str]:
|
||||
|
||||
|
||||
def markdown_link_targets(text: str) -> list[str]:
|
||||
text = without_html_comments(without_inline_code(without_fenced_code(text)))
|
||||
text = without_inline_code_and_html_comments(without_fenced_code(text))
|
||||
return inline_link_targets(text) + reference_link_targets(text)
|
||||
|
||||
|
||||
|
||||
@@ -280,6 +280,23 @@ def test_artifact_scope_ignores_links_inside_html_comments(project: Path) -> Non
|
||||
assert "Broken Markdown link: 'guide.md' -> 'missing-after.md'" in result.stdout
|
||||
|
||||
|
||||
def test_artifact_scope_reads_html_comments_as_commonmark_does(project: Path) -> None:
|
||||
(project / "guide.md").write_text(
|
||||
# -- an escaped opener is text, so the link after it is real --
|
||||
"Intro \\<!-- [escaped](missing-escaped.md) -->\n\n"
|
||||
# -- a comment that opens before a code span ends at its own closer --
|
||||
"<!-- ` --> `sample` [after-code](missing-after-code.md)\n\n"
|
||||
# -- a comment where a destination must begin does not make a definition --
|
||||
"[foo]: <!-- note --> missing-definition.md\n\n[foo]\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = run_audit(project, "artifacts")
|
||||
assert result.returncode == 1
|
||||
assert "Broken Markdown link: 'guide.md' -> 'missing-escaped.md'" in result.stdout
|
||||
assert "Broken Markdown link: 'guide.md' -> 'missing-after-code.md'" in result.stdout
|
||||
assert "missing-definition.md" not in result.stdout
|
||||
|
||||
|
||||
def test_artifact_scope_ignores_title_with_escaped_quotes(project: Path) -> None:
|
||||
(project / "guide.md").write_text("# Guide\n", encoding="utf-8")
|
||||
(project / "index.md").write_text(
|
||||
|
||||
Reference in new issue
Block a user