fix: let the site search land on cards

The search splits a page at its headings. Emulator cards were titled in
bold and platform systems folded into blocks under one heading, so a
pasted hash opened the top of a page or the systems overview of a
100-block platform page. Each file card is now a level 4 heading kept
out of the table of contents, and each platform system a heading.

Card lines that list other pages (platforms, packs, source citations,
collection paths) leave the search index: 24.7 MB down to 18.1 MB, the
names, hashes and notes still searchable.
This commit is contained in:
Abdessamad Derraz committed 2026-10-21 05:47:31 +02:00
1 parent 32dcfcf85a
commit a67265e903
7 files changed
+189 -34

No files matched your search

+20 -3
View File
@@ -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;
}
+3
View File
@@ -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
+77 -27
View File
@@ -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'<a id="{anchor}"></a>')
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'<a id="{anchor}"></a>')
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'<div class="rb-sys-file"{id_attr} markdown>', ""]
# The anchor is this page's, the second link is the file's own address,
# resolved by content wherever its card moves.
lines = ['<div class="rb-sys-file" markdown>', ""]
# 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' <a class="rb-permalink" href="#{anchor}" title="Link to this card">#</a>'
f' <a class="rb-permalink" href="../../files/?sha1={sha1_full}" '
f'title="Permanent link, by content">permalink</a>'
) if anchor else ""
lines.append(f'**`{name}`** <span class="rb-badge rb-badge-muted">{size}</span>{permalink}')
lines.append(_card_title(
f'{_code(name)} <span class="rb-badge rb-badge-muted">{size}</span>{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}: <span class="rb-hash">{_code(value)}</span>')
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(["", "</div>", ""])
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(["", '<div class="rb-search-skip" data-search-exclude markdown>', ""])
elif skipping and not skip:
out.extend(["", "</div>", ""])
skipping = skip
out.append(line)
if skipping:
out.extend(["", "</div>", ""])
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'<div class="rb-file-entry"{id_attr} markdown>')
lines.append('<div class="rb-file-entry" markdown>')
lines.append("")
lines.append(_card_title(f"{_code(fname)} {badge_str}", anchor))
lines.append("")
permalink = f' <a class="rb-permalink" href="#{anchor}" title="Link to this file">#</a>' if anchor else ""
lines.append(f"**`{fname}`** {badge_str}{permalink}")
if desc:
lines.append(f"<br>{_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("</div>")
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
+4 -3
View File
@@ -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"}
+74
View File
@@ -337,5 +337,79 @@ class TheDatHoldsEveryCopy(unittest.TestCase):
self.assertEqual(dat.count("<rom "), 2)
class TheSearchLandsOnTheCard(unittest.TestCase):
"""The site search splits a page at its headings. Emulator cards were
titled in bold and platform systems folded into blocks under one
heading, so a pasted hash led to the page top or to the systems
overview of a 100-block page."""
def test_a_platform_system_is_a_heading(self):
import generate_site
by_system = {f"system-{i:02d}": [{"name": f"f{i}.bin", "status": "ok"}]
for i in range(11)}
lines = generate_site._render_platform_systems(by_system, {}, {})
headings = [line for line in lines if line.startswith("### ")]
self.assertEqual(len(headings), 11)
self.assertTrue(headings[0].endswith("{ #system-00 }"))
self.assertNotIn('<a id="system-00"></a>', 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('<div class="rb-search-skip" data-search-exclude markdown>')
self.assertIn("- MD5:", searched)
self.assertIn("- Emulators:", searched)
self.assertIn("- Platforms:", skipped)
self.assertIn("- Also installed by:", skipped)
self.assertEqual(text.count("</div>"), 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",
'<div class="rb-search-skip" data-search-exclude markdown>',
"- Source: `a.c:1`", "- Platforms: p", "</div>",
"- Destination: `d`", "- Contents (1 members):", " - `m`",
])
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -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)
+10
View File
@@ -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
```