From e7d33caa3f45e3cb1712eaa9da134e5a5e831b89 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:41:00 +0100 Subject: [PATCH] feat: platform file rows that open full width Each file is a details whose summary is the row: name, folder when it is not the bare name, status chip. The short hash sits on one copy button beside it, since a summary is itself a control. Opened, the evidence runs across the list. A list with no folder drops the column. Code spans in the evidence are plain text. The lists split by the share of a page they render to (elements against 10,000, bytes against 600,000) instead of Markdown length. With its parts, Batocera goes from 12 pages, 18,854 elements and 1.21 MB to 10 pages, 13,671 and 1.11 MB. A part that folds back into its platform page redirects there, from the record in website/src/data/platform-parts.json. --- scripts/generate_site.py | 270 ++++++++++++++++----------- scripts/sitestarlight.py | 14 +- tests/test_site_exports.py | 6 +- tests/test_site_platform_density.py | 110 ++++++++--- tests/test_site_r2_reference.py | 17 +- tests/test_site_r3_reference.py | 2 +- tests/test_site_weight.py | 2 +- website/src/data/platform-parts.json | 82 ++++++++ website/src/scripts/site.ts | 33 ++++ website/src/styles/retrobios.css | 96 ++++++---- 10 files changed, 440 insertions(+), 192 deletions(-) create mode 100644 website/src/data/platform-parts.json diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 0174f9035..540f64ed5 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -3238,44 +3238,35 @@ def with_setup_table(page: str, coverages: dict, registry: dict | None = None) - return f"{page.rstrip()}\n\n{table}" -# A platform system whose file list passes this much Markdown gets a page -# of its own: RetroDECK's arcade list was 750 KB of a 1.1 MB page. -PLATFORM_SECTION_BUDGET = 200_000 -# A system's own page in runs of at most this much Markdown: RetroDECK's -# arcade list, each file with its served copy and pinned readers, rendered -# 1.4 MB of HTML on one page. -PLATFORM_PART_BUDGET = 350_000 -# The Markdown of a platform page's file lists, past which the largest +# A platform's file lists are split by the share of a page they render to, +# in thousandths: their elements against validate_site's MAX_PAGE_ELEMENTS, +# their bytes against the 600,000 of test_site_r2_gate, whichever is +# larger. Measured in Markdown characters, the lists split anew whenever +# their markup changed: rows written as tables moved 38 more systems to +# pages of their own. +PAGE_ELEMENTS = 10_000 +PAGE_BYTES = 600_000 +# A system whose list passes this share gets a page of its own. +PLATFORM_SECTION_BUDGET = 400 +# A system's own page in runs of at most this share. +PLATFORM_PART_BUDGET = 700 +# The share of a platform page's file lists, past which the largest # systems move to pages of their own. -PLATFORM_REFERENCE_BUDGET = 250_000 +PLATFORM_REFERENCE_BUDGET = 600 -def _file_runs(body: list[str], budget: int) -> list[list[str]]: - """A system's file list in runs of at most ``budget`` characters. +# Every part page a platform list has had, with the system it held: +# platforms/batocera-atari-st.md -> platforms/batocera.md#atari-st. When a +# budget or a list changes, a part can fold back into its platform page, +# and its address then redirects there instead of ending on the 404 page. +PLATFORM_PARTS_FILE = (Path(__file__).resolve().parent.parent + / "website" / "src" / "data" / "platform-parts.json") - The lines before the first file (counts, emulators, a table's header) - open every run. A file is never cut from its hash and evidence lines, - and a table row is one line. - """ - rows = next((i + 1 for i, line in enumerate(body) if line.startswith("|--")), None) - first = rows if rows is not None else next( - (i for i, line in enumerate(body) if line.startswith("- ")), len(body)) - head, items = body[:first], [] - for line in body[first:]: - if line.startswith(("- ", "| ")) or not items: - items.append([line]) - else: - items[-1].append(line) - runs: list[list[str]] = [[]] - size = 0 - for item in items: - weight = sum(len(line) + 1 for line in item) - if runs[-1] and size + weight > budget: - runs.append([]) - size = 0 - runs[-1].extend(item) - size += weight - return [head + run for run in runs] + +def _published_platform_parts() -> dict[str, str]: + if not PLATFORM_PARTS_FILE.is_file(): + return {} + return json.loads(PLATFORM_PARTS_FILE.read_text(encoding="utf-8")) # A list moved off a reference page (gaps, provenance) is cut in parts of @@ -3283,13 +3274,18 @@ def _file_runs(body: list[str], budget: int) -> list[list[str]]: LIST_PART_BUDGET = 300_000 -def _blocks_in_runs(blocks: list[list[str]], budget: int) -> list[list[str]]: - """Blocks (table rows, folded lists) in runs of at most ``budget`` - characters, a block never cut.""" +def _characters(lines: list[str]) -> int: + return sum(len(line) + 1 for line in lines) + + +def _blocks_in_runs(blocks: list[list[str]], budget: int, + weigh: Callable[[list[str]], int] = _characters) -> list[list[str]]: + """Blocks (table rows, folded lists) in runs of at most ``budget``, by + default in characters, a block never cut.""" runs: list[list[str]] = [[]] size = 0 for block in blocks: - weight = sum(len(line) + 1 for line in block) + weight = weigh(block) if runs[-1] and size + weight > budget: runs.append([]) size = 0 @@ -3426,16 +3422,15 @@ def _launcher_refs(platform: str, config: dict) -> dict[str, list[str]]: return found -PLATFORM_FILE_HEADER = [ - "| File | Path | Status | Hash | Details |", - "|---|---|---|---|---|", -] # The characters of a hash a row shows, as many as the served copy's sha1. SHORT_HASH = 8 # The hash a row shows first, when the list does not say which it checks. _ROW_HASHES = ("sha1", "md5", "sha256", "crc32") # An untested entry whose reason is a hash the bytes do not match. _WRONG_HASH = re.compile(r"hash|mismatch|expected|must match", re.IGNORECASE) +# A list this long gets a filter box above it, as a table of RECORD_ROWS +# rows does (website/src/plugins/rehype-stack-tables.mjs). +FILTER_ROWS = 20 def _status_chip(f: dict) -> str: @@ -3450,12 +3445,13 @@ def _status_chip(f: dict) -> str: tone = "warning" else: word, tone = str(status), "muted" - return f'{word}' + return f'{html.escape(word)}' def _copy_hash(algorithm: str, value: str | list) -> str: - """A declared hash cut to SHORT_HASH characters on a button that copies it whole. - A list of accepted values (Recalbox) shows its first and how many more.""" + """A declared hash cut to SHORT_HASH characters on a button that copies + it whole. A list of accepted values (Recalbox) copies its first. Outside + a form a button submits nothing, so it carries no type.""" from hashing import parse_hash_list # noqa: PLC0415 values = parse_hash_list(value) @@ -3463,43 +3459,68 @@ def _copy_hash(algorithm: str, value: str | list) -> str: return "" first = html.escape(values[0]) more = f" +{len(values) - 1}" if len(values) > 1 else "" - return (f'{more}') + return (f'') -def _cell(text: str) -> str: - """Text for one cell of a Markdown table: a pipe would end the cell.""" - return text.replace("|", "\\|") +# Characters Markdown or MDX would read inside the raw HTML of a row: an +# MDX page parses the text of its HTML as Markdown, a .md page does not. +_INERT = str.maketrans({"*": "*", "_": "_", "`": "`", "[": "[", "]": "]"}) +_INLINE_MD = re.compile( + r"\[(?P