', ""]
- # 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
```