ci(validate): detect and deny HTML in README

This commit is contained in:
Lemmy
2026-07-13 23:03:30 -04:00
parent 31fccb72ba
commit a82af31452
+65
View File
@@ -93,6 +93,19 @@ SETTING_FIELDS = {
OPTION_FIELDS = {"value", "label_key"}
VISIBLE_WHEN_FIELDS = {"key", "values"}
# Raw HTML is not supported on plugin pages. Markdown autolinks such as
# <https://example.com> do not match this expression.
HTML_RE = re.compile(
r"<!--|<\?|<!\[CDATA\[|<![A-Z]|"
r"</[A-Za-z][A-Za-z0-9-]*\s*>|"
r"<[A-Za-z][A-Za-z0-9-]*"
r"(?:\s+[A-Za-z_:][A-Za-z0-9_.:-]*"
r"(?:\s*=\s*(?:[^\s\"'=<>`]+|'[^']*'|\"[^\"]*\"))?)*\s*/?>",
re.DOTALL,
)
INLINE_CODE_RE = re.compile(r"(?<!`)(`+)(?!`)(.*?)(?<!`)\1(?!`)", re.DOTALL)
FENCE_OPEN_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})")
def is_non_empty_string(value: Any) -> bool:
return isinstance(value, str) and value.strip() != ""
@@ -113,6 +126,42 @@ def rel(root: Path, path: Path) -> str:
return path.as_posix()
def raw_html_line(markdown: str) -> int | None:
"""Return the first line containing raw HTML outside Markdown code, if any."""
visible: list[str] = []
fence_char = ""
fence_length = 0
for line in markdown.splitlines(keepends=True):
if fence_char:
closing = rf"^ {{0,3}}{re.escape(fence_char)}{{{fence_length},}}\s*$"
if re.match(closing, line.rstrip("\r\n")):
fence_char = ""
fence_length = 0
visible.append("\n" if line.endswith(("\n", "\r")) else "")
continue
opening = FENCE_OPEN_RE.match(line)
if opening:
fence = opening.group(1)
fence_char = fence[0]
fence_length = len(fence)
visible.append("\n" if line.endswith(("\n", "\r")) else "")
continue
visible.append(line)
text = "".join(visible)
text = INLINE_CODE_RE.sub(
lambda match: "".join("\n" if char == "\n" else " " for char in match.group(0)),
text,
)
match = HTML_RE.search(text)
if match is None:
return None
return text.count("\n", 0, match.start()) + 1
def webp_dimensions(header: bytes) -> tuple[int, int] | None:
"""Width and height from a WebP header, or None if it is not one we can read.
@@ -749,6 +798,21 @@ class Validator:
f"Export one with {THUMBNAIL_GENERATOR_URL}",
)
def validate_readme(self, plugin_dir: Path) -> None:
readme = plugin_dir / "README.md"
if not readme.is_file():
return
try:
contents = readme.read_text(encoding="utf-8")
except UnicodeDecodeError:
self.add_error(readme, "must be UTF-8 text")
return
line = raw_html_line(contents)
if line is not None:
self.add_error(readme, f"raw HTML on line {line} is not allowed; use Markdown instead")
def validate_no_symlinks(self, manifest_path: Path, plugin_dir: Path) -> None:
for path in plugin_dir.rglob("*"):
if path.is_symlink():
@@ -765,6 +829,7 @@ class Validator:
self.validate_root_fields(manifest_path, manifest)
self.validate_required_files(manifest_path, plugin_dir)
self.validate_thumbnail(manifest_path, plugin_dir)
self.validate_readme(plugin_dir)
self.validate_no_symlinks(manifest_path, plugin_dir)
if "setting" in manifest: