ci(validate): detect and deny HTML in README
This commit is contained in:
@@ -93,6 +93,19 @@ SETTING_FIELDS = {
|
|||||||
OPTION_FIELDS = {"value", "label_key"}
|
OPTION_FIELDS = {"value", "label_key"}
|
||||||
VISIBLE_WHEN_FIELDS = {"key", "values"}
|
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:
|
def is_non_empty_string(value: Any) -> bool:
|
||||||
return isinstance(value, str) and value.strip() != ""
|
return isinstance(value, str) and value.strip() != ""
|
||||||
@@ -113,6 +126,42 @@ def rel(root: Path, path: Path) -> str:
|
|||||||
return path.as_posix()
|
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:
|
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.
|
"""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}",
|
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:
|
def validate_no_symlinks(self, manifest_path: Path, plugin_dir: Path) -> None:
|
||||||
for path in plugin_dir.rglob("*"):
|
for path in plugin_dir.rglob("*"):
|
||||||
if path.is_symlink():
|
if path.is_symlink():
|
||||||
@@ -765,6 +829,7 @@ class Validator:
|
|||||||
self.validate_root_fields(manifest_path, manifest)
|
self.validate_root_fields(manifest_path, manifest)
|
||||||
self.validate_required_files(manifest_path, plugin_dir)
|
self.validate_required_files(manifest_path, plugin_dir)
|
||||||
self.validate_thumbnail(manifest_path, plugin_dir)
|
self.validate_thumbnail(manifest_path, plugin_dir)
|
||||||
|
self.validate_readme(plugin_dir)
|
||||||
self.validate_no_symlinks(manifest_path, plugin_dir)
|
self.validate_no_symlinks(manifest_path, plugin_dir)
|
||||||
|
|
||||||
if "setting" in manifest:
|
if "setting" in manifest:
|
||||||
|
|||||||
Reference in New Issue
Block a user