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("
  • root: x
  • ", html) + self.assertNotIn("**", html) + def test_a_list_under_a_key_is_a_sublist(self): html = self._html({"roms": [{"name": "a.bin", "size": 2}, "b.bin"]}) self.assertNotIn("
    ", 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