diff --git a/docs_assets/extra.css b/docs_assets/extra.css index d302614c..dbc36106 100644 --- a/docs_assets/extra.css +++ b/docs_assets/extra.css @@ -109,6 +109,24 @@ font-weight: 700; } +/* A card's title is a heading so the search lands on it, set as body text */ +.md-typeset .rb-file-title { + margin: 0 0 0.3rem; + font-size: inherit; + font-weight: 400; + letter-spacing: normal; + scroll-margin-top: 4rem; +} + +.md-typeset .rb-file-title code { + font-weight: 700; +} + +/* Lines the search skips continue the card's list */ +.rb-search-skip > ul { + margin-top: 0.1rem; +} + .rb-file-entry ul, .rb-sys-file ul { margin: 0.3rem 0 0; @@ -265,8 +283,7 @@ .rb-permalink:focus { opacity: 1; } -.rb-file-entry:target, -.rb-sys-file:target { +.rb-file-entry:has(> .rb-file-title:target), +.rb-sys-file:has(> .rb-file-title:target) { background: var(--rb-surface); - scroll-margin-top: 4rem; } diff --git a/mkdocs.yml b/mkdocs.yml index 9c521968..87fdda72 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,6 +82,9 @@ markdown_extensions: - tables - toc: permalink: true + # File cards are level 4 headings: the search lands on them, and a + # page of a hundred cards keeps a readable table of contents. + toc_depth: 3 - pymdownx.details - pymdownx.highlight: anchor_linenums: true diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 1a6ef12c..d551e497 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -1728,16 +1728,19 @@ def _render_platform_systems( sys_link = _system_link(sys_id, "../") anchor = sys_id.replace(" ", "-") + # Every system is a heading with the same anchor in both layouts: + # emulator cards link here, and the site search splits a page at its + # headings. Folded into blocks under one heading, every hash pasted + # in the search led to the systems overview of a 100-block page. if use_collapsible: status_tag = "OK" if ok_count == total else _count(total - ok_count, "issue") - lines.append(f'') - lines.append(f'??? note "{sys_id} ({ok_count}/{total} - {status_tag})"') + lines.append(f"### {sys_link} {{ #{anchor} }}") + lines.append("") + lines.append(f'??? note "{_count(total, "file")} ({status_tag})"') lines.append("") pad = " " else: - # The same anchor in both layouts: emulator cards link here. - lines.append(f'') - lines.append(f"## {sys_link}") + lines.append(f"## {sys_link} {{ #{anchor} }}") lines.append("") pad = "" @@ -2528,22 +2531,18 @@ def _system_file_card( anchor = "" else: anchored.add(anchor) - id_attr = f' id="{anchor}"' if anchor else "" - lines = [f'
', ""] - # The anchor is this page's, the second link is the file's own address, - # resolved by content wherever its card moves. + lines = ['
', ""] + # The heading's own link is this page's, the second link is the file's + # own address, resolved by content wherever its card moves. permalink = ( - f' #' f' permalink' ) if anchor else "" - lines.append(f'**`{name}`** {size}{permalink}') + lines.append(_card_title( + f'{_code(name)} {size}{permalink}', + anchor, + )) lines.append("") - if f.get("path"): - # The card named a file, its hashes and provenance, but not where to - # get it: a user missing scph5501.bin found the reference and no way - # to the bytes. - lines.append(f"- Collection path: {_code(f['path'])} ([download]({_fetch_url(f['path'])}))") # Every hash in full: the site search indexes text, not a title # attribute, so a pasted md5 or crc32 (what clrmamepro, RomVault or an # emulator log prints) found nothing behind a 12-character prefix. @@ -2551,6 +2550,16 @@ def _system_file_card( ("CRC32", f.get("crc32")), ("SHA256", f.get("sha256"))): if value and value != "unknown": lines.append(f'- {label}: {_code(value)}') + if emus: + lines.append(f"- Emulators: {', '.join(_emulator_links(sha1_full, consumers))}") + provenance = f.get("provenance", {}) + if provenance: + lines.extend(_provenance_lines(provenance, "../")) + if f.get("path"): + # The card named a file, its hashes and provenance, but not where to + # get it: a user missing scph5501.bin found the reference and no way + # to the bytes. + lines.append(f"- Collection path: {_code(f['path'])} ([download]({_fetch_url(f['path'])}))") if plats: # Each link lands on the platform's system section, and says where # that platform puts the file: from the card, retrodeck sent the @@ -2562,15 +2571,53 @@ def _system_file_card( # looking up scph5501.bin found every platform but RetroBat, whose # pack installs it for six cores. lines.append(f"- Also installed by: {', '.join(installed)}") - if emus: - lines.append(f"- Emulators: {', '.join(_emulator_links(sha1_full, consumers))}") - provenance = f.get("provenance", {}) - if provenance: - lines.extend(_provenance_lines(provenance, "../")) + lines[2:] = _search_skipped_apart(lines[2:]) lines.extend(["", "
", ""]) return lines +# Card lines the site search leaves out. They list where a file goes and +# who cites it, links to other pages that index the same names, and they +# were 6.6 of the 24.7 MB the browser downloads and indexes before the first +# result. The name, the hashes, the entry's path and the notes stay +# searchable. +SEARCH_SKIPPED = ( + "- Platforms:", "- Also installed by:", "- Source:", "- Asked for by:", + "- Collection path:", "- Collection copy:", "- Collection copies:", + "- Installed by the platform's own package on:", + "- Not in the platform's package", "- Never opened on these platforms", +) + + +def _search_skipped_apart(lines: list[str]) -> list[str]: + """Card list lines, each run the search skips in a block of its own.""" + out: list[str] = [] + skipping = False + for line in lines: + skip = line.startswith(SEARCH_SKIPPED) or (skipping and line.startswith(" ")) + if skip and not skipping: + out.extend(["", '
', ""]) + elif skipping and not skip: + out.extend(["", "
", ""]) + skipping = skip + out.append(line) + if skipping: + out.extend(["", "
", ""]) + return out + + +def _card_title(text: str, anchor: str) -> str: + """A file card's heading, so the site search lands on the card. + + The search splits a page at its headings: a card titled in bold sent a + pasted md5 to the top of a page of a hundred cards. Level 4 stays out + of the table of contents (toc_depth 3), and a card without an anchor + keeps the id the toc gives it. + """ + attributes = f"#{anchor} .rb-file-title" if anchor else ".rb-file-title" + return f"#### {text} {{ {attributes} }}" + + def generate_system_page( manufacturer: str, consoles: dict[str, list], @@ -2863,14 +2910,13 @@ def _render_emulator_file( badges = _file_badges(f, in_repo) badge_str = " ".join(badges) - id_attr = f' id="{anchor}"' if anchor else "" - lines.append(f'
') + lines.append('
') + lines.append("") + lines.append(_card_title(f"{_code(fname)} {badge_str}", anchor)) lines.append("") - permalink = f' #' if anchor else "" - lines.append(f"**`{fname}`** {badge_str}{permalink}") if desc: - lines.append(f"
{_escape_tags(str(desc))}") - lines.append("") + lines.extend([_escape_tags(str(desc)), ""]) + body_start = len(lines) details = [] if fpath and fpath != fname: @@ -3022,6 +3068,7 @@ def _render_emulator_file( lines.append(f" - `{c.get('name', '')}`{extra}") else: lines.append(f" - {c}") + lines[body_start:] = _search_skipped_apart(lines[body_start:]) lines.append("") lines.append("
") lines.append("") @@ -5224,6 +5271,9 @@ markdown_extensions: - tables - toc: permalink: true + # File cards are level 4 headings: the search lands on them, and a + # page of a hundred cards keeps a readable table of contents. + toc_depth: 3 - pymdownx.details - pymdownx.highlight: anchor_linenums: true diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index 2c500274..7e632e89 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -4098,8 +4098,9 @@ class AFileCardSaysAllItKnows(unittest.TestCase): text = "\n".join(_render_emulator_file( entry, {"files": [entry]}, None, [entry], lambda f: True, "file-neogeo-zip", )) - self.assertIn('id="file-neogeo-zip"', text) - self.assertIn('href="#file-neogeo-zip"', text) + # The card is a heading with this id, linked by the heading's own + # permalink. + self.assertIn("{ #file-neogeo-zip .rb-file-title }", text) self.assertIn("Standalone path: `Firmware/neogeo.zip`", text) self.assertIn("`m11.bin`", text) self.assertIn("CRC32 `0000000b`", text) @@ -4819,7 +4820,7 @@ class VersionAnchorsFollowContent(unittest.TestCase): "raze", {"emulator": "Raze", "type": "standalone", "files": files}, {"files": {}, "indexes": {}}, ) - return re.findall(r'id="(file-[^"]+)"', page) + return re.findall(r"\{ #(file-[^ ]+) \.rb-file-title \}", page) def test_inserting_a_version_keeps_the_others(self): a = {"name": "DUKE3D.GRP", "path": "DUKE3D.GRP", "crc32": "bbc9ce44"} diff --git a/tests/test_site_render.py b/tests/test_site_render.py index 13630dbe..8f7dd3b1 100644 --- a/tests/test_site_render.py +++ b/tests/test_site_render.py @@ -337,5 +337,79 @@ class TheDatHoldsEveryCopy(unittest.TestCase): self.assertEqual(dat.count("', lines) + self.assertIn('??? note "1 file (OK)"', lines) + + def test_an_emulator_card_is_a_heading_with_its_anchor(self): + import generate_site + + entry = {"name": "scph5501.bin", "required": True} + lines = generate_site._render_emulator_file( + entry, {"files": [entry]}, None, [entry], lambda f: True, "file-scph5501-bin") + self.assertTrue(lines[2].startswith("#### `scph5501.bin` ")) + self.assertTrue(lines[2].endswith("{ #file-scph5501-bin .rb-file-title }")) + + def test_a_system_card_is_a_heading_with_its_anchor(self): + import generate_site + from cross_reference import FileConsumers + + entry = {"name": "a.bin", "sha1": "a" * 40, "md5": "b" * 32, "size": 3} + lines = generate_site._system_file_card(entry, FileConsumers({"files": {}})) + self.assertTrue(lines[2].startswith("#### `a.bin` ")) + self.assertTrue(lines[2].endswith("{ #sha1-aaaaaaaaaaaa .rb-file-title }")) + + +class TheSearchSkipsLinkLists(unittest.TestCase): + """The search index was 24.7 MB, read and indexed before the first + result. Lists of platforms, copies and source citations on every card + made 6.6 MB of it, names already indexed on their own pages.""" + + def test_a_system_card_keeps_its_hashes_and_skips_its_platforms(self): + import generate_site + from cross_reference import FileConsumers + + consumers = FileConsumers({"files": {}}) + sha1 = "a" * 40 + consumers.platforms = {sha1: {"retroarch"}} + consumers.installed = {sha1: {"batocera": {"bios/a.bin"}}} + consumers.emulators = {sha1: {"duckstation"}} + entry = {"name": "a.bin", "sha1": sha1, "md5": "b" * 32, "size": 3} + text = "\n".join(generate_site._system_file_card(entry, consumers)) + searched, skipped = text.split('
') + self.assertIn("- MD5:", searched) + self.assertIn("- Emulators:", searched) + self.assertIn("- Platforms:", skipped) + self.assertIn("- Also installed by:", skipped) + self.assertEqual(text.count("
"), 2) + + def test_runs_alternate_in_card_order(self): + import generate_site + + lines = ["- SHA1: x", "- Source: `a.c:1`", "- Platforms: p", + "- Destination: `d`", "- Contents (1 members):", " - `m`"] + out = [line for line in generate_site._search_skipped_apart(lines) if line] + self.assertEqual(out, [ + "- SHA1: x", + '
', + "- Source: `a.c:1`", "- Platforms: p", "
", + "- Destination: `d`", "- Contents (1 members):", " - `m`", + ]) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_site_system_links.py b/tests/test_site_system_links.py index 61e9d4be..17f05e3d 100644 --- a/tests/test_site_system_links.py +++ b/tests/test_site_system_links.py @@ -143,7 +143,7 @@ class AnIdNamesOneCardPerPage(unittest.TestCase): "path": "bios/Nintendo/DS/a.bin"} page = gs.generate_system_page( "Nintendo", {"DS": [entry], "DSi": [entry]}, FileConsumers({"files": {}})) - self.assertEqual(page.count('id="sha1-aaaaaaaaaaaa"'), 1) + self.assertEqual(page.count("#sha1-aaaaaaaaaaaa .rb-file-title"), 1) self.assertEqual(page.count("rb-sys-file"), 2) diff --git a/wiki/architecture.md b/wiki/architecture.md index 14289f14..e467fd95 100644 --- a/wiki/architecture.md +++ b/wiki/architecture.md @@ -66,6 +66,16 @@ dist/ generated packs (gitignored) `platforms/`, `emulators/`, `database.json`, and the `wiki/` sources. Documentation changes belong in `wiki/` or in the generator. +The site search splits each page at its headings and sends a result to the +heading above the match. Every file card is therefore a level 4 heading with +the card's anchor, and every platform system a heading of its own: a hash +pasted in the search opens that card or that system, not the top of the page. +Level 4 stays out of the table of contents (`toc_depth: 3`). The lines that +list other pages (platforms, packs, source citations, collection paths) sit +in a `data-search-exclude` block, since the browser downloads and indexes the +whole search index before it shows a first result. File names, hashes, the +paths an emulator reads and the notes stay searchable. + ## Data flow ```