mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-23 12:19:13 -05:00
fix: link a system to the section of its name
A system link followed the folder its files sit in: emerson-arcadia-2001 reached the Saturn and sega-naomi the Dreamcast. It now goes to the section system_name() reads, or stays a plain name. Console headings carry an explicit id, so Sega's Mega-CD no longer shares mega-cd.
This commit is contained in:
1 parent
a9181f9d04
commit
bb13e7b5f9
4 files changed
+153
-27
No files matched your search
+13
-12
@@ -2355,13 +2355,13 @@ def _system_heading_link(sys_id: str, prefix: str = "") -> str:
|
||||
writes. A system with no section of its own is named, never sent to the
|
||||
page a storage folder chose.
|
||||
"""
|
||||
from siterender import _own_section
|
||||
from siterender import system_section
|
||||
|
||||
name = _escape_tags(system_name(sys_id))
|
||||
section = _own_section(sys_id)
|
||||
section = system_section(sys_id)
|
||||
if section is not None:
|
||||
mfr, console = section
|
||||
name = f"[{name}]({console_href(_slug(mfr), console, prefix)})"
|
||||
slug, console = section
|
||||
name = f"[{name}]({console_href(slug, console, prefix)})"
|
||||
return f"{name} <small>{_escape_tags(sys_id)}</small>"
|
||||
|
||||
|
||||
@@ -3436,8 +3436,7 @@ def generate_systems_index(manufacturers: dict) -> str:
|
||||
]
|
||||
for name, mfr, console, count in rows:
|
||||
cell = name.replace("|", "-")
|
||||
anchor = _slugify_anchor(console)
|
||||
href = f"{_slug(mfr)}.md" + (f"#{anchor}" if anchor else "")
|
||||
href = console_href(_slug(mfr), console).removeprefix("systems/")
|
||||
lines.append(f"| [{cell}]({href}) | {count:,} | [{mfr}]({_slug(mfr)}.md) |")
|
||||
lines.append("")
|
||||
|
||||
@@ -4291,11 +4290,12 @@ def _console_heading(manufacturer: str, console: str) -> str:
|
||||
"""A console's heading: its icon hidden from screen readers, then the
|
||||
name with no space before it. The space read as "Section titled
|
||||
“ PlayStation”" on five of seven Sony headings."""
|
||||
anchor = f" {{ #{console_anchor(manufacturer, console)} }}"
|
||||
icon = _icon_name(manufacturer, console)
|
||||
if not _icon_available.get(icon):
|
||||
return f"## {console}"
|
||||
return f"## {console}{anchor}"
|
||||
image = markup.image("", _icon_url(icon), width="24", **{"aria-hidden": "true"})
|
||||
return f"## {image}{console}"
|
||||
return f"## {image}{console}{anchor}"
|
||||
|
||||
|
||||
_NEXT_FROM_SYSTEMS = [
|
||||
@@ -4500,13 +4500,13 @@ def _named_system_link(sys_id: str, prefix: str = "") -> str:
|
||||
The emulator pages printed the id ("sony-playstation") where every
|
||||
other page says "Sony PlayStation". The id stays in the address only.
|
||||
"""
|
||||
from siterender import _page_by_norm # noqa: PLC0415
|
||||
from siterender import system_section # noqa: PLC0415
|
||||
|
||||
name = system_name(sys_id).replace("[", r"\[").replace("]", r"\]")
|
||||
page = _system_page_map.get(sys_id) or _page_by_norm(sys_id)
|
||||
if not page:
|
||||
section = system_section(sys_id)
|
||||
if not section:
|
||||
return name
|
||||
slug, console = page
|
||||
slug, console = section
|
||||
return f"[{name}]({console_href(slug, console, prefix)})"
|
||||
|
||||
|
||||
@@ -7934,6 +7934,7 @@ from siterender import ( # noqa: E402,F401
|
||||
system_name,
|
||||
card_page,
|
||||
console_href,
|
||||
console_anchor,
|
||||
record_console_pages,
|
||||
record_emulator_cards,
|
||||
emulator_card_page,
|
||||
|
||||
+61
-5
@@ -170,13 +170,31 @@ def split_system_stems() -> set[str]:
|
||||
return {stem for stems in _console_pages.values() for stem in stems}
|
||||
|
||||
|
||||
# (manufacturer slug, console) -> the id of the console's heading. Two
|
||||
# folders spelled alike (Sega's "Mega CD" and "Mega-CD") share a slug, and
|
||||
# the second heading took "mega-cd_1" while every link said "mega-cd".
|
||||
_console_anchors: dict[tuple[str, str], str] = {}
|
||||
|
||||
|
||||
def _manufacturer_slug(manufacturer: str) -> str:
|
||||
return manufacturer.lower().replace(" ", "-")
|
||||
|
||||
|
||||
def console_anchor(manufacturer: str, console: str) -> str:
|
||||
"""The id of a console's heading on its manufacturer's page."""
|
||||
from sitestarlight import toc_slug
|
||||
|
||||
key = (_manufacturer_slug(manufacturer), console)
|
||||
return _console_anchors.get(key) or toc_slug(console)
|
||||
|
||||
|
||||
def console_href(slug: str, console: str, prefix: str = "") -> str:
|
||||
"""Where a console's files are: its own page, or its section of the
|
||||
manufacturer's page (also where a console cut into parts lists them)."""
|
||||
stems = _console_pages.get((slug, console))
|
||||
if stems and len(stems) == 1:
|
||||
return f"{prefix}systems/{urllib.parse.quote(stems[0])}.md"
|
||||
return f"{prefix}systems/{urllib.parse.quote(slug)}.md#{_slugify_anchor(console)}"
|
||||
return f"{prefix}systems/{urllib.parse.quote(slug)}.md#{console_anchor(slug, console)}"
|
||||
|
||||
|
||||
def card_page(path: str, sha1: str) -> str:
|
||||
@@ -204,11 +222,33 @@ def emulator_card_page(emulator: str, anchor: str) -> str:
|
||||
return _emulator_card_pages.get((emulator, anchor), emulator)
|
||||
|
||||
|
||||
def system_section(sys_id: str) -> tuple[str, str] | None:
|
||||
"""The console section a system id is named after, as (manufacturer
|
||||
slug, console): its own section, else the one section whose heading
|
||||
reads system_name(). The folder a system's files sit in is no answer:
|
||||
Emerson's Arcadia 2001 led to the Saturn and Sega's NAOMI to the
|
||||
Dreamcast."""
|
||||
own = _own_section(sys_id)
|
||||
if own is not None:
|
||||
return _manufacturer_slug(own[0]), own[1]
|
||||
try:
|
||||
name = system_name(sys_id)
|
||||
except KeyError:
|
||||
return None
|
||||
places = _sections_by_name.get(name.casefold(), set())
|
||||
if len(places) != 1:
|
||||
return None
|
||||
mfr, console = next(iter(places))
|
||||
if mfr in _PROGRAM_TOPS:
|
||||
return None
|
||||
return _manufacturer_slug(mfr), console
|
||||
|
||||
|
||||
def _system_link(sys_id: str, prefix: str = "") -> str:
|
||||
"""Generate a markdown link to a system page with anchor."""
|
||||
page = _system_page_map.get(sys_id) or _page_by_norm(sys_id)
|
||||
if page:
|
||||
slug, console = page
|
||||
"""A system id linked to the section named after it, or the bare id."""
|
||||
section = system_section(sys_id)
|
||||
if section:
|
||||
slug, console = section
|
||||
return f"[{sys_id}]({console_href(slug, console, prefix)})"
|
||||
return sys_id
|
||||
|
||||
@@ -322,6 +362,8 @@ _PROGRAM_TOPS = frozenset({"Other", "Arcade"})
|
||||
|
||||
# Normalized id -> the console sections spelled that way.
|
||||
_console_sections: dict[str, set[tuple[str, str]]] = {}
|
||||
# A console section by the name its heading reads (Bandai WonderSwan).
|
||||
_sections_by_name: dict[str, set[tuple[str, str]]] = {}
|
||||
# Emulator and engine names. A heading spelled like one (Game Engines >
|
||||
# Solarus, ZC > ZC210) files that program's copies and names no system.
|
||||
_program_names: set[str] = set()
|
||||
@@ -355,7 +397,21 @@ def set_console_sections(
|
||||
{(mfr, console) for mfr, consoles in sections.items() for console in consoles}
|
||||
if isinstance(sections, dict) else set(sections)
|
||||
)
|
||||
from sitestarlight import _unique, toc_slug
|
||||
|
||||
_console_sections.clear()
|
||||
_sections_by_name.clear()
|
||||
_console_anchors.clear()
|
||||
by_maker: dict[str, list[str]] = {}
|
||||
for mfr, console in pairs:
|
||||
by_maker.setdefault(mfr, []).append(console)
|
||||
for name in {_heading_name(mfr, console).casefold(), console.casefold()}:
|
||||
_sections_by_name.setdefault(name, set()).add((mfr, console))
|
||||
# In the order the page lists them, after the page title's own id.
|
||||
for mfr, consoles in by_maker.items():
|
||||
used = {toc_slug(mfr)}
|
||||
for console in sorted(consoles):
|
||||
_console_anchors[(_manufacturer_slug(mfr), console)] = _unique(toc_slug(console), used)
|
||||
for mfr, console in pairs:
|
||||
spelled = console.lower().replace(" ", "-")
|
||||
slug = mfr.lower().replace(" ", "-")
|
||||
|
||||
@@ -219,13 +219,13 @@ class ConsoleHeadingsStartWithTheName(unittest.TestCase):
|
||||
icon = siterender._icon_name("Sony", "PlayStation")
|
||||
with mock.patch.dict(siterender._icon_available, {icon: True}):
|
||||
heading = gs._console_heading("Sony", "PlayStation")
|
||||
self.assertRegex(heading, r'^## <img [^>]*aria-hidden="true"[^>]*>PlayStation$')
|
||||
self.assertRegex(heading, r'^## <img [^>]*aria-hidden="true"[^>]*>PlayStation \{ #playstation \}$')
|
||||
self.assertIn('alt=""', heading)
|
||||
|
||||
def test_no_icon_no_image(self):
|
||||
icon = siterender._icon_name("Nobody", "Nothing")
|
||||
with mock.patch.dict(siterender._icon_available, {icon: False}):
|
||||
self.assertEqual(gs._console_heading("Nobody", "Nothing"), "## Nothing")
|
||||
self.assertEqual(gs._console_heading("Nobody", "Nothing"), "## Nothing { #nothing }")
|
||||
|
||||
|
||||
class TheSystemsIndexListsConsoles(unittest.TestCase):
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""A platform system links to the page of the folder its files live in.
|
||||
"""A system is placed by the folder its files live in, and a link to it
|
||||
goes to the section named after it.
|
||||
|
||||
The site mapped each system by the bare name of its first file, the last
|
||||
console written winning: MiSTer's boot.rom sits under a dozen consoles, so
|
||||
wonderswan and tiger-game-com linked to VideoBrain, ngpc and tandy-coco to
|
||||
TI-99. Reads the repository: skips when the database is absent.
|
||||
TI-99. A link then followed that folder, not the name it printed:
|
||||
emerson-arcadia-2001 led to the Saturn and sega-naomi to the Dreamcast.
|
||||
Reads the repository: skips when the database is absent.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -76,14 +79,14 @@ class OtherSpellingsOfOneSystem(unittest.TestCase):
|
||||
self.addCleanup(siterender.set_system_page_map, saved)
|
||||
siterender.set_system_page_map(mapping)
|
||||
|
||||
def test_a_profile_spelling_links_to_the_platform_page(self):
|
||||
def test_a_profile_spelling_finds_the_platform_page(self):
|
||||
import siterender
|
||||
|
||||
self._install({"sega-mega-drive": ("sega", "Mega Drive")})
|
||||
self.assertIn("systems/sega.md#mega-drive", siterender._system_link("sega-megadrive"))
|
||||
self.assertEqual(siterender._page_by_norm("sega-megadrive"), ("sega", "Mega Drive"))
|
||||
self._install({"sega-mega-drive": ("sega", "Mega Drive"),
|
||||
"megadrive": ("other", "Elsewhere")})
|
||||
self.assertEqual(siterender._system_link("sega-megadrive"), "sega-megadrive")
|
||||
self.assertIsNone(siterender._page_by_norm("sega-megadrive"))
|
||||
|
||||
def test_a_table_of_the_same_size_replaces_the_other_spellings(self):
|
||||
"""The index of other spellings was kept while the table kept its
|
||||
@@ -91,10 +94,9 @@ class OtherSpellingsOfOneSystem(unittest.TestCase):
|
||||
import siterender
|
||||
|
||||
self._install({"sega-mega-drive": ("sega", "Mega Drive")})
|
||||
self.assertIn("systems/sega.md#", siterender._system_link("sega-megadrive"))
|
||||
self.assertEqual(siterender._page_by_norm("sega-megadrive"), ("sega", "Mega Drive"))
|
||||
self._install({"sega-mega-drive": ("other", "Elsewhere")})
|
||||
self.assertEqual(siterender._system_link("sega-megadrive"),
|
||||
"[sega-megadrive](systems/other.md#elsewhere)")
|
||||
self.assertEqual(siterender._page_by_norm("sega-megadrive"), ("other", "Elsewhere"))
|
||||
|
||||
def test_a_system_only_profiles_name_is_placed_by_their_files(self):
|
||||
manufacturers = {"Commodore": {"PET": [{"name": "basic4.bin"}]}}
|
||||
@@ -261,5 +263,72 @@ class SystemEmulatorsWhenNoIdMatches(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
DOCS = REPO_ROOT / "website" / "src" / "content" / "docs"
|
||||
|
||||
|
||||
@unittest.skipUnless((REPO_ROOT / "database.json").is_file()
|
||||
and (DOCS / "systems" / "sony.md").is_file(), "site not generated")
|
||||
class EverySystemLinkLandsOnItsSection(unittest.TestCase):
|
||||
"""Every system id of every platform and profile, linked as the pages
|
||||
link it, against the generated pages: the anchor exists on its target
|
||||
page, and the heading there is the one system_name() reads."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
import re
|
||||
|
||||
import siterender
|
||||
import sitestarlight
|
||||
from common import (
|
||||
list_registered_platforms, load_database, load_emulator_profiles, load_platform_config,
|
||||
)
|
||||
|
||||
db = load_database(str(REPO_ROOT / "database.json"))
|
||||
profiles = load_emulator_profiles(str(REPO_ROOT / "emulators"), skip_aliases=False)
|
||||
siterender.set_console_sections(gs._group_by_manufacturer(db), siterender.program_names(profiles))
|
||||
platforms = REPO_ROOT / "platforms"
|
||||
ids = {str(sid) for profile in profiles.values() for sid in profile.get("systems") or []}
|
||||
for name in list_registered_platforms(str(platforms), include_archived=True):
|
||||
ids |= {str(sid) for sid in load_platform_config(name, str(platforms)).get("systems") or {}}
|
||||
cls.ids = sorted(ids)
|
||||
cls.siterender = siterender
|
||||
heading = re.compile(r"^##\s+(?:<img [^>]*>)?(?P<text>.+?)\s*\{\s*#(?P<id>[^\s}]+)[^}]*\}\s*$", re.M)
|
||||
cls.headings: dict[str, dict[str, str]] = {}
|
||||
for page in (DOCS / "systems").glob("*.md*"):
|
||||
text = page.read_text(encoding="utf-8")
|
||||
cls.headings[page.name.rsplit(".", 1)[0]] = {
|
||||
m["id"]: m["text"] for m in heading.finditer(text)}
|
||||
cls.headings[page.name.rsplit(".", 1)[0]].update(
|
||||
dict.fromkeys(sitestarlight.heading_ids(text) - cls.headings[page.name.rsplit(".", 1)[0]].keys(), ""))
|
||||
|
||||
def test_every_anchor_exists_and_names_the_system(self):
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
linked = 0
|
||||
for sys_id in self.ids:
|
||||
link = self.siterender._system_link(sys_id)
|
||||
match = re.fullmatch(r"\[[^\]]*\]\(systems/(?P<page>[^)#]+)\.md(?:#(?P<anchor>[^)]+))?\)", link)
|
||||
if not match:
|
||||
self.assertEqual(link, sys_id)
|
||||
continue
|
||||
linked += 1
|
||||
page = urllib.parse.unquote(match["page"])
|
||||
with self.subTest(system=sys_id, link=link):
|
||||
self.assertIn(page, self.headings)
|
||||
if match["anchor"]:
|
||||
self.assertIn(match["anchor"], self.headings[page])
|
||||
_slug, console = self.siterender.system_section(sys_id)
|
||||
self.assertEqual(self.headings[page][match["anchor"]], console)
|
||||
self.assertGreater(linked, 200)
|
||||
|
||||
def test_a_system_without_a_section_of_its_name_is_not_sent_elsewhere(self):
|
||||
for sys_id in ("emerson-arcadia-2001", "sega-naomi"):
|
||||
with self.subTest(system=sys_id):
|
||||
link = self.siterender._system_link(sys_id)
|
||||
self.assertNotIn("#saturn", link)
|
||||
self.assertNotIn("#dreamcast", link)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user