diff --git a/scripts/generate_site.py b/scripts/generate_site.py index b947ea0d..673dc00a 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -3055,6 +3055,22 @@ def _plain_value(value: object) -> str: return _escape_tags(str(value)) +def _structured_field_text(profile: dict) -> Callable[[object, object], str]: + """Scalars of a structured block, citations linked at the profile's pin. + + The file entries and platform details linked their source_ref, and the + analysis and firmware blocks printed 186 of them as plain code. + """ + def text(key: object, value: object) -> str: + if key == "source_ref" and isinstance(value, str): + return _source_ref_markdown(profile, value) + if isinstance(value, str): + return link_prose_citations(profile, _field_text(key, value)) + return _field_text(key, value) + + return text + + def _render_structured_blocks(profile: dict) -> list[str]: """Every remaining structured field, as a collapsible section.""" lines: list[str] = [] @@ -3090,7 +3106,8 @@ def _render_structured_blocks(profile: dict) -> list[str]: lines.append(f'???+ abstract "{label}"') lines.append("") start = len(lines) - _render_yaml_value(lines, val, indent=4) + _render_yaml_value(lines, val, indent=4, + field_text=_structured_field_text(profile)) lines[start:] = [_escape_tags(line) for line in lines[start:]] lines.append("") @@ -5168,6 +5185,7 @@ from sitesources import ( # noqa: E402,F401 from siterender import ( # noqa: E402,F401 _by_mode, _count, + _field_text, _render_yaml_value, _slugify_anchor, _system_page_map, diff --git a/scripts/siterender.py b/scripts/siterender.py index 81d3a04b..3f769501 100644 --- a/scripts/siterender.py +++ b/scripts/siterender.py @@ -8,6 +8,7 @@ import sys import urllib.error import urllib.parse import urllib.request +from collections.abc import Callable from concurrent.futures import ThreadPoolExecutor from pathlib import Path @@ -212,9 +213,10 @@ def _field_text(key: object, value: object) -> str: elif value is None: text = "none" else: - text = str(value) - if len(text) > 200: - text = text[:200] + "..." + # One line: a folded note's second line started at column 0 and + # left the admonition it was written in. Never cut: 34 notes lost + # their end, and a cut citation links to another line. + text = " ".join(str(value).split()) return f"`{text}`" if key == "source_ref" else text @@ -223,12 +225,19 @@ def _field_label(key: object) -> str: return str(key).replace("_", " ") -def _render_yaml_value(lines: list[str], val: object, indent: int = 4) -> None: +FieldText = Callable[[object, object], str] + + +def _render_yaml_value( + lines: list[str], val: object, indent: int = 4, + field_text: FieldText = _field_text, +) -> None: """Render any YAML value as a nested markdown list. Every level is a list item and its children a sublist four spaces in. A key written as a paragraph with its children indented below it read as an indented code block: the clownmdemu page showed the raw asterisks. + ``field_text`` renders each scalar, so a page can link a source_ref. """ pad = " " * indent if isinstance(val, dict): @@ -236,27 +245,32 @@ def _render_yaml_value(lines: list[str], val: object, indent: int = 4) -> None: if isinstance(v, (dict, list)) and v: lines.append(f"{pad}- **{_field_label(k)}:**") lines.append("") - _render_yaml_value(lines, v, indent + 4) + _render_yaml_value(lines, v, indent + 4, field_text) 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: if isinstance(item, dict): - parts = [ - f"{_field_label(ik)}: {_field_text(ik, iv)}" - for ik, iv in item.items() - if not isinstance(iv, (dict, list)) - ] - lines.append(f"{pad}- {', '.join(parts)}") + # Scalars on the item's line, lists and maps as its children: + # cemu's firmware titles lost the files each title carries. + scalars = {ik: iv for ik, iv in item.items() + if not (isinstance(iv, (dict, list)) and iv)} + nested = {ik: iv for ik, iv in item.items() if ik not in scalars} + parts = [f"{_field_label(ik)}: {field_text(ik, iv)}" + for ik, iv in scalars.items()] + lines.append(f"{pad}- {', '.join(parts)}".rstrip()) + if nested: + lines.append("") + _render_yaml_value(lines, nested, indent + 4, field_text) else: - lines.append(f"{pad}- {_field_text(None, item)}") + lines.append(f"{pad}- {field_text(None, item)}") lines.append("") elif isinstance(val, str) and "\n" in val: for line in val.split("\n"): lines.append(f"{pad}{line}") else: - lines.append(f"{pad}{_field_text(None, val)}") + lines.append(f"{pad}{field_text(None, val)}") def _platform_link(name: str, display: str, prefix: str = "") -> str: diff --git a/tests/test_site_render.py b/tests/test_site_render.py index d551ad1b..1228703a 100644 --- a/tests/test_site_render.py +++ b/tests/test_site_render.py @@ -139,5 +139,47 @@ class BundledFilesAreNotToSource(unittest.TestCase): '1 |', page) +class StructuredBlocks(unittest.TestCase): + PIN = "1e0de94dc7e669c0925a22c0fccf6cdc837af0a0" + + def profile(self, block: str, value: object) -> dict: + return {"source": "https://github.com/libretro/blastem", + "source_commit": self.PIN, block: value} + + def render(self, profile: dict) -> str: + import generate_site + + return "\n".join(generate_site._render_structured_blocks(profile)) + + def test_a_source_ref_in_a_block_is_a_pinned_link(self): + """186 citations in analysis and firmware blocks were plain code.""" + page = self.render(self.profile("analysis", {"tmss": { + "source_ref": "libblastem.c:355-359"}})) + self.assertIn( + f"[`libblastem.c:355-359`](https://github.com/libretro/blastem/blob/" + f"{self.PIN}/libblastem.c#L355-L359)", page) + + def test_a_list_inside_a_list_item_is_shown(self): + """cemu's firmware titles lost the files each title carries.""" + page = self.render(self.profile("firmware_titles", [ + {"id": "0005001b-10042400", "name": "Shared fonts", + "content": ["CafeCn.ttf", "CafeKr.ttf"]}])) + self.assertIn(" - id: 0005001b-10042400, name: Shared fonts\n", page) + self.assertIn(" - **content:**", page) + self.assertIn(" - CafeKr.ttf", page) + + def test_a_folded_note_stays_inside_its_block(self): + page = self.render(self.profile("analysis", {"tmss": { + "note": "The core points\nsystem/tmss_path at the system directory."}})) + self.assertIn("- **note:** The core points system/tmss_path at the", page) + body = page.split("\n")[1:] + self.assertEqual([line for line in body if line and not line.startswith(" ")], []) + + def test_a_long_note_is_not_cut(self): + note = "word " * 80 + "end." + page = self.render(self.profile("analysis", {"x": {"note": note}})) + self.assertIn("end.", page) + + if __name__ == "__main__": unittest.main()