mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-17 05:35:36 +02:00
Treat non-2xx activity responses as feed errors, preserve non-Markdown Copilot agent assets, and apply the reviewed reference escape parity correction with regenerated bindings. Allow the test runner to drain piped output on its existing success or failure exit status. Validate installed workflows and bounded real Node20/24 pipe regressions.
203 lines
6.4 KiB
Python
203 lines
6.4 KiB
Python
"""Small Markdown link parser used by the documentation-governance audit."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from urllib.parse import unquote
|
|
|
|
INLINE_LINK_START_RE = re.compile(r"\[[^\]\n]*\]\(")
|
|
REFERENCE_DEFINITION_RE = re.compile(
|
|
r"""(?mx)
|
|
^\ {0,3}\[(?P<label>[^\]\n]+)\]:[\t\ ]*
|
|
(?:\r?\n[\t\ ]*)?
|
|
(?P<target><[^>\n]+>|[^\t\ \n]+)
|
|
(?:[\t\ ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\([^()]*\)))?
|
|
[\t\ ]*$
|
|
"""
|
|
)
|
|
REFERENCE_USAGE_RE = re.compile(
|
|
r"(?<![\\])(?:\\\\)*\[(?P<text>[^\]\n]+)\](?:\[(?P<label>[^\]\n]*)\])?"
|
|
)
|
|
EXTERNAL_URI_RE = re.compile(
|
|
r"^(?:[A-Za-z][A-Za-z0-9+.-]*://|(?:data|doi|geo|irc|magnet|mailto|news|sms|tel|urn):)",
|
|
re.IGNORECASE,
|
|
)
|
|
|
|
|
|
def without_fenced_code(text: str) -> str:
|
|
output: list[str] = []
|
|
fence_character: str | None = None
|
|
fence_length = 0
|
|
for line in text.splitlines(keepends=True):
|
|
marker = re.match(r"^ {0,3}(`{3,}|~{3,})(.*)$", line)
|
|
if fence_character is None and marker:
|
|
fence_character = marker.group(1)[0]
|
|
fence_length = len(marker.group(1))
|
|
output.append("\n" if line.endswith("\n") else "")
|
|
continue
|
|
if fence_character is not None:
|
|
closing = re.match(
|
|
rf"^ {{0,3}}{re.escape(fence_character)}{{{fence_length},}}[ \t]*(?:\r?\n)?$",
|
|
line,
|
|
)
|
|
if closing:
|
|
fence_character = None
|
|
fence_length = 0
|
|
output.append("\n" if line.endswith("\n") else "")
|
|
continue
|
|
output.append(line)
|
|
return "".join(output)
|
|
|
|
|
|
def without_inline_code(text: str) -> str:
|
|
output: list[str] = []
|
|
cursor = 0
|
|
while cursor < len(text):
|
|
if text[cursor] != "`":
|
|
output.append(text[cursor])
|
|
cursor += 1
|
|
continue
|
|
end_of_marker = cursor
|
|
while end_of_marker < len(text) and text[end_of_marker] == "`":
|
|
end_of_marker += 1
|
|
marker = text[cursor:end_of_marker]
|
|
closing = text.find(marker, end_of_marker)
|
|
if closing == -1:
|
|
output.append(marker)
|
|
cursor = end_of_marker
|
|
continue
|
|
output.extend(
|
|
"\n" if character == "\n" else " "
|
|
for character in text[cursor : closing + len(marker)]
|
|
)
|
|
cursor = closing + len(marker)
|
|
return "".join(output)
|
|
|
|
|
|
def inline_link_targets(text: str) -> list[str]:
|
|
targets: list[str] = []
|
|
matches = list(INLINE_LINK_START_RE.finditer(text))
|
|
index = 0
|
|
cursor = 0
|
|
while index < len(matches):
|
|
match = matches[index]
|
|
if match.start() < cursor:
|
|
index += 1
|
|
continue
|
|
start = match.end()
|
|
if start < len(text) and text[start] == "<":
|
|
end = text.find(">", start + 1)
|
|
if end != -1:
|
|
targets.append(text[start : end + 1])
|
|
cursor = end + 1
|
|
index += 1
|
|
continue
|
|
index += 1
|
|
if index < len(matches):
|
|
continue
|
|
break
|
|
depth = 0
|
|
quote: str | None = None
|
|
escaped = False
|
|
closed = False
|
|
saw_closing = False
|
|
for end in range(start, len(text)):
|
|
character = text[end]
|
|
if quote is not None:
|
|
if escaped:
|
|
escaped = False
|
|
elif character == "\\":
|
|
escaped = True
|
|
elif character == quote:
|
|
quote = None
|
|
continue
|
|
if escaped:
|
|
escaped = False
|
|
continue
|
|
if character == "\\":
|
|
escaped = True
|
|
continue
|
|
if (
|
|
character in {"'", '"'}
|
|
and depth == 0
|
|
and end > start
|
|
and text[end - 1].isspace()
|
|
):
|
|
quote = character
|
|
continue
|
|
if character == "(":
|
|
depth += 1
|
|
elif character == ")":
|
|
saw_closing = True
|
|
if depth == 0:
|
|
targets.append(text[start:end])
|
|
cursor = end + 1
|
|
closed = True
|
|
break
|
|
depth -= 1
|
|
if closed:
|
|
index += 1
|
|
continue
|
|
index += 1
|
|
if not saw_closing or index >= len(matches):
|
|
break
|
|
return targets
|
|
|
|
|
|
def normalize_reference_label(label: str) -> str:
|
|
unescaped = re.sub(r"\\([\\\[\]])", r"\1", label)
|
|
return re.sub(r"\s+", " ", unescaped.strip()).casefold()
|
|
|
|
|
|
def reference_link_targets(text: str) -> list[str]:
|
|
definition_matches = list(REFERENCE_DEFINITION_RE.finditer(text))
|
|
definitions = {
|
|
normalize_reference_label(match.group("label")): match.group("target")
|
|
for match in definition_matches
|
|
}
|
|
targets: list[str] = []
|
|
for match in REFERENCE_USAGE_RE.finditer(text):
|
|
if any(
|
|
definition.start() <= match.start() < definition.end()
|
|
for definition in definition_matches
|
|
):
|
|
continue
|
|
following = text[match.end() : match.end() + 1]
|
|
if following == "(":
|
|
continue
|
|
label = match.group("label")
|
|
key = normalize_reference_label(label or match.group("text"))
|
|
if key in definitions:
|
|
targets.append(definitions[key])
|
|
return targets
|
|
|
|
|
|
def markdown_link_targets(text: str) -> list[str]:
|
|
text = without_inline_code(without_fenced_code(text))
|
|
return inline_link_targets(text) + reference_link_targets(text)
|
|
|
|
|
|
def normalize_link_target(raw: str) -> str | None:
|
|
target = raw.strip()
|
|
is_unc_path = target.startswith("\\\\")
|
|
if target.startswith("<"):
|
|
closing = target.find(">")
|
|
if closing == -1:
|
|
return None
|
|
target = target[1:closing]
|
|
else:
|
|
title = re.search(
|
|
r'\s+(?:"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'|\([^()]*\))\s*$',
|
|
target,
|
|
)
|
|
if title:
|
|
target = target[: title.start()]
|
|
# Unescape supported Markdown punctuation before Windows path separators.
|
|
target = re.sub(r"\\([()\[\]'])", r"\1", target)
|
|
target = unquote(target.split("#", 1)[0].split("?", 1)[0]).replace("\\", "/")
|
|
if not target or (target.startswith("//") and not is_unc_path):
|
|
return None
|
|
if EXTERNAL_URI_RE.match(target):
|
|
return None
|
|
return target
|