diff --git a/scripts/generate_site.py b/scripts/generate_site.py index db91b02d7..4fc19c664 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -3242,41 +3242,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) open every run. A - file is never cut from its hash and evidence lines. - """ - first = 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 @@ -3284,13 +3278,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 @@ -3427,6 +3426,188 @@ def _launcher_refs(platform: str, config: dict) -> dict[str, list[str]]: return found +# 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: + """The row's status as a chip, its word and its colour saying the same.""" + status = f.get("status") + if status == "ok": + word, tone = "OK", "success" + elif status == "missing": + word, tone = "Missing", "danger" + elif status == "untested": + word = "Wrong hash" if _WRONG_HASH.search(str(f.get("reason") or "")) else "Untested" + tone = "warning" + else: + word, tone = str(status), "muted" + 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) 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) + if not values: + return "" + first = html.escape(values[0]) + more = f" +{len(values) - 1}" if len(values) > 1 else "" + return (f'') + + +# 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