diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 22b6a062..c3f20048 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -3021,11 +3021,12 @@ def _render_platform_details(profile: dict) -> list[str]: lines.extend(['???+ info "Platform details"', ""]) for pk, pv in platform_details.items(): if isinstance(pv, dict): - lines.append(f" **{pk}:**") + lines.extend([f" - **{pk}:**", ""]) for sk, sv in pv.items(): - lines.append(f" - {sk}: {_detail_value(profile, sk, sv)}") + lines.append(f" - {sk}: {_detail_value(profile, sk, sv)}") + lines.append("") else: - lines.append(f" **{pk}:** {_detail_value(profile, pk, pv)}") + lines.append(f" - **{pk}:** {_detail_value(profile, pk, pv)}") lines.append("") return lines diff --git a/scripts/siterender.py b/scripts/siterender.py index 619058a7..33f3afc6 100644 --- a/scripts/siterender.py +++ b/scripts/siterender.py @@ -197,11 +197,11 @@ def _render_yaml_value(lines: list[str], val: object, indent: int = 4) -> None: if isinstance(val, dict): for k, v in val.items(): if isinstance(v, (dict, list)) and v: - lines.append(f"{pad}- {_field_label(k)}:") + lines.append(f"{pad}- **{_field_label(k)}:**") lines.append("") _render_yaml_value(lines, v, indent + 4) else: - lines.append(f"{pad}- {_field_label(k)}: {_field_text(k, v)}") + lines.append(f"{pad}- **{_field_label(k)}:** {_field_text(k, v)}") lines.append("") elif isinstance(val, list): for item in val: diff --git a/scripts/validate_site.py b/scripts/validate_site.py index 72740561..0e892ce4 100644 --- a/scripts/validate_site.py +++ b/scripts/validate_site.py @@ -32,6 +32,11 @@ class Page: links: list[str] = field(default_factory=list) missing_alt: list[str] = field(default_factory=list) jsonld: list[str] = field(default_factory=list) + unrendered: list[str] = field(default_factory=list) + + +# Text inside these is shown as written, so markup there is not a leftover. +_LITERAL_TAGS = frozenset({"pre", "code", "script", "style", "textarea"}) class _PageParser(HTMLParser): @@ -42,9 +47,12 @@ class _PageParser(HTMLParser): self._title_parts: list[str] = [] self._jsonld_depth = 0 self._jsonld_parts: list[str] = [] + self._literal_depth = 0 def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: data = dict(attrs) + if tag in _LITERAL_TAGS: + self._literal_depth += 1 if tag == "html": self.page.lang = data.get("lang", "") elif tag == "title": @@ -73,6 +81,8 @@ class _PageParser(HTMLParser): self.page.canonical.append(data.get("href", "")) def handle_endtag(self, tag: str) -> None: + if tag in _LITERAL_TAGS and self._literal_depth: + self._literal_depth -= 1 if tag == "title": self._title_depth -= 1 elif tag == "script" and self._jsonld_depth: @@ -85,6 +95,8 @@ class _PageParser(HTMLParser): self._title_parts.append(data) if self._jsonld_depth: self._jsonld_parts.append(data) + if not self._literal_depth and "**" in data: + self.page.unrendered.append(data.strip()[:60]) def close(self) -> None: super().close() @@ -192,6 +204,10 @@ def _check_pages( report(path, f"duplicate IDs: {duplicate_ids[:5]}") if page.missing_alt: report(path, f"images missing alt: {page.missing_alt[:5]}") + if page.unrendered: + # Bold that reached the reader as asterisks: a block indented + # one level too deep is read as code (the clownmdemu analysis). + report(path, f"markdown left unrendered: {page.unrendered[:3]}") if not is_404 and len(page.jsonld) != 1: report(path, f"expected one JSON-LD block, got {len(page.jsonld)}") diff --git a/tests/test_site_exports.py b/tests/test_site_exports.py index 3c44f936..43693d08 100644 --- a/tests/test_site_exports.py +++ b/tests/test_site_exports.py @@ -858,9 +858,24 @@ class NestedProfileBlocksRenderAsLists(unittest.TestCase): "tmss": {"supported": True}}) self.assertNotIn("
", html)
self.assertNotIn("**", html)
- self.assertIn("external required: no", html)
+ self.assertIn("external required: no", html)
self.assertIn("a.c:1", html)
+ def test_platform_details_are_a_list(self):
+ import generate_site
+
+ lines = generate_site._render_platform_details(
+ {"platform_details": {"megacd": {"root": "x"}, "note": "y"}}
+ )
+ lines[0] = lines[0].replace("???+", "!!!")
+ try:
+ import markdown
+ except ImportError:
+ self.skipTest("markdown is not installed")
+ html = markdown.markdown("\n".join(lines), extensions=["admonition"])
+ self.assertIn("", html)
diff --git a/tests/test_site_validation.py b/tests/test_site_validation.py
index 65733f59..75472aad 100644
--- a/tests/test_site_validation.py
+++ b/tests/test_site_validation.py
@@ -100,9 +100,9 @@ class PlatformDetails(unittest.TestCase):
"root": "/x",
}}}
)
- self.assertIn(" - extensions_tried: .bin, .zip", lines)
- self.assertIn(" - hle_available: no", lines)
- self.assertIn(" - root: <system_dir>/x", lines)
+ self.assertIn(" - extensions_tried: .bin, .zip", lines)
+ self.assertIn(" - hle_available: no", lines)
+ self.assertIn(" - root: <system_dir>/x", lines)
class RenderedSiteValidation(unittest.TestCase):
@@ -165,6 +165,28 @@ class RenderedSiteValidation(unittest.TestCase):
+class UnrenderedMarkdown(unittest.TestCase):
+ """The clownmdemu analysis block reached readers as "- **bios_hle:**
+ True": a list indented one level too deep, read as code."""
+
+ setUp = RenderedSiteValidation.setUp
+ tearDown = RenderedSiteValidation.tearDown
+
+ def test_asterisks_in_prose_fail_and_in_code_pass(self):
+ (self.site / "index.html").write_text(
+ _page("Home", "Unique home description.", "- **bios_hle:** True
"),
+ encoding="utf-8",
+ )
+ issues = validate_site(self.site, self.config)
+ self.assertTrue(any("markdown left unrendered" in issue for issue in issues))
+ (self.site / "index.html").write_text(
+ _page("Home", "Unique home description.",
+ "a = 2 ** 8
**/*.py"),
+ encoding="utf-8",
+ )
+ self.assertEqual(validate_site(self.site, self.config), [])
+
+
class EveryTestDependencyIsInstalledInCi(unittest.TestCase):
"""The markdown rendering test skipped on every CI run: the test job
installed pyyaml and jsonschema only. A test that skips where it is