From fda684528997df1a725aa129ea6e10dce7dc04c2 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 10 Nov 2026 14:11:47 +0100 Subject: [PATCH] refactor: show diagrams as themed SVG images An inline SVG put every label of both layouts, its title and its desc into the page text, next to the prose that already says the steps: a text dump of the Steam Deck tab read the steps three times, glued. Each drawing is now one SVG file per layout and theme under assets/diagrams, shown through with alt="". The figure says its words once, in one screen-reader line and its caption. --- scripts/generate_site.py | 4 + scripts/sitediagrams.py | 132 +++++++++++++++++++++++++++---- scripts/sitesteps.py | 50 +++++++----- tests/test_site_steps.py | 20 +++-- tests/test_site_wiki.py | 2 +- website/src/styles/retrobios.css | 24 +++--- 6 files changed, 170 insertions(+), 62 deletions(-) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 8272392a7..e1428de88 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -95,6 +95,7 @@ import siteentries import siteinputs import retired_files import siteagents +import sitediagrams import sitestarlight import sitesteps from siteexports import generate_exports_page, load_export_reports @@ -12497,6 +12498,9 @@ def _generate(args: argparse.Namespace) -> None: source = Path("docs_assets") / folder if source.is_dir(): shutil.copytree(source, public / "assets" / dest, dirs_exist_ok=True) + # The diagrams and the drawn install steps, one SVG per layout and theme. + sitediagrams.write_diagram_files(public / sitediagrams.DIAGRAM_DIR, + {**sitediagrams.figure_files(), **sitesteps.steps_files()}) demo_dest = public / "assets" / "demo" demo_dest.mkdir(parents=True, exist_ok=True) for name in DEMO_ASSETS: diff --git a/scripts/sitediagrams.py b/scripts/sitediagrams.py index 09e78ddfd..aa192d101 100644 --- a/scripts/sitediagrams.py +++ b/scripts/sitediagrams.py @@ -9,14 +9,20 @@ A page asks for a diagram with an HTML comment on a line of its own:: -``resolve_images`` replaces the comment with the figure. The SVG is inline -so its text takes the page font and its colours come from the theme -variables in ``docs_assets/extra.css``: it follows the palette toggle, which -an ```` cannot. Presentation attributes give the same drawing a -readable default when the SVG is opened on its own. +``resolve_images`` replaces the comment with the figure. Each drawing is +an SVG file shown through ````, so the page text holds its words once: +an inline SVG put every label of both layouts, its title and its desc into +what a screen reader, a copy, a text dump or a crawler reads, next to the +prose that already says them. A file reads no stylesheet of the page, so it +is written once per theme with the colours retrobios.css gives the +``rb-d-*`` classes (``THEMES``), and the stylesheet shows the one of the +current theme. Presentation attributes give the same drawing a readable +default when the SVG is opened on its own. -Every figure carries two layouts, wide and narrow, and the stylesheet shows -one: a 720-unit drawing scaled to a phone would print 9 px text. +Every figure carries two layouts, wide and narrow, and ```` picks +one: a 720-unit drawing scaled to a phone would print 9 px text. The +drawings are ``alt=""``: the figure says what they show once, in a +screen-reader line, and the caption under them. """ from __future__ import annotations @@ -296,22 +302,112 @@ def grid(boxes: list[Box], per_row: int, total: float) -> tuple[float, float, li return total, y - gap_y + 1, out -def svg(figure: Figure, layout: str, suffix: str) -> str: - """One layout of *figure* as a standalone, accessible SVG element.""" - width, height, body = (figure.wide if layout == "wide" else figure.narrow)() - base = f"rb-fig-{figure.image_id}{suffix}-{layout}" +def svg_document(width: float, height: float, title: str, desc: str, body: list[str], + base: str) -> str: + """A drawing as a standalone, accessible SVG element.""" return ( f'' - f'{escape(figure.title)}' - f'{escape(figure.desc)}' + f'aria-labelledby="{base}-t {base}-d" font-family="{FONT}">' + f'{escape(title)}' + f'{escape(desc)}' + "".join(body) + "" ) +def svg(figure: Figure, layout: str, suffix: str = "") -> str: + """One layout of *figure* as a standalone, accessible SVG element.""" + width, height, body = (figure.wide if layout == "wide" else figure.narrow)() + return svg_document(width, height, figure.title, figure.desc, body, + f"rb-fig-{figure.image_id}{suffix}-{layout}") + + +# The colours retrobios.css gives a diagram, per theme: --rb-diagram-* and +# --rb-text-secondary, then the text and background Starlight resolves +# --sl-color-text and --sl-color-bg to (props.css: gray-2 and black, whose +# values swap with the theme). An SVG shown through reads none of them. +THEMES: dict[str, dict[str, str]] = { + "dark": {"text": "#c1c3c8", "bg": "#17181c", "fill": "#252838", "stroke": "#7c82a3", + "accent": "#aebcff", "muted": "#a6adc8"}, + "light": {"text": "#353841", "bg": "#ffffff", "fill": "#f5f6fa", "stroke": "#8c92ac", + "accent": "#3b3f7a", "muted": "#546e7a"}, +} +# Where the files are served, under the site's public folder. +DIAGRAM_DIR = "assets/diagrams" +# retrobios.css shows the narrow layout below this width. +NARROW_MEDIA = "(max-width: 37.5em)" + + +def themed(document: str, theme: str) -> str: + """*document* with the theme's colours, the rules retrobios.css gives + the .rb-diagram classes on the page.""" + c = THEMES[theme] + style = ( + f"" + ) + head = document.index(">") + 1 + return document[:head] + style + document[head:] + + +def _size(document: str) -> tuple[str, str]: + match = re.match(r']*?width="([\d.]+)" height="([\d.]+)"', document) + if not match: + raise ValueError("an SVG document without its size") + return match.group(1), match.group(2) + + +def diagram_files(stem: str, layouts: dict[str, str]) -> dict[str, str]: + """The files of one drawing: *layouts* maps wide and narrow to its SVG, + each written once per theme.""" + return {f"{stem}-{layout}-{theme}.svg": themed(document, theme) + "\n" + for layout, document in layouts.items() for theme in THEMES} + + +def pictures(stem: str, layouts: dict[str, str]) -> str: + """The drawing on the page: one per theme, the narrow layout + under NARROW_MEDIA. Each image is alt="", the figure says the words.""" + (wide_w, wide_h), (narrow_w, narrow_h) = _size(layouts["wide"]), _size(layouts["narrow"]) + out = [] + for theme in THEMES: + url = markup.site_href(f"{DIAGRAM_DIR}/{stem}") + out.append( + f'' + f'' + f'' + ) + return "".join(out) + + +def _layouts(figure: Figure) -> dict[str, str]: + return {layout: svg(figure, layout) for layout in ("wide", "narrow")} + + +def figure_files() -> dict[str, str]: + """Every figure's files, by name under DIAGRAM_DIR.""" + return {name: text for image_id in IMAGE_IDS + for name, text in diagram_files(image_id, _layouts(figures()[image_id])).items()} + + +def write_diagram_files(directory: Path, files: dict[str, str]) -> None: + """*files* under *directory*, and no other SVG: a drawing renamed or + dropped leaves no file behind.""" + directory.mkdir(parents=True, exist_ok=True) + for stale in directory.glob("*.svg"): + if stale.name not in files: + stale.unlink() + for name, text in sorted(files.items()): + path = directory / name + if not path.is_file() or path.read_text(encoding="utf-8") != text: + path.write_text(text, encoding="utf-8") + + # Data @@ -667,18 +763,20 @@ _INLINE_ESCAPES = str.maketrans({"_": "_", "*": "*", "`": "`", "[": def figure_html(image_id: str, occurrence: int = 1, inline: bool = False) -> str: figure = figures()[image_id] suffix = "" if occurrence == 1 else f"-{occurrence}" - drawings = svg(figure, "wide", suffix) + svg(figure, "narrow", suffix) + drawings = pictures(image_id, _layouts(figure)) + said = f'{escape(figure.desc)}' caption = escape(figure.caption) anchor = f"rb-fig-{image_id}{suffix}" if inline: html = ( f'' - f'{drawings}{caption}' + f'{drawings} {said} {caption}' ) return html.translate(_INLINE_ESCAPES) return ( f'
\n' f"{drawings}\n" + f"{said}\n" f"
{caption}
\n" "
" ) diff --git a/scripts/sitesteps.py b/scripts/sitesteps.py index c0c75a9dd..f79f437c5 100644 --- a/scripts/sitesteps.py +++ b/scripts/sitesteps.py @@ -3,12 +3,14 @@ Two things per tab, both drawn from data: - The steps that differ by system, opening a terminal and pasting the line, - as an inline SVG: one card per step, a pictogram and a short caption, - drawn with sitediagrams' primitives so it takes the theme's colours in - light and dark. The text steps above it (platforms/_guides.yml, - terminals) stay the accessible source. The SVG says the same in its title - and desc. The pictograms are generic shapes (a menu, a search field, a - window, keys, a mouse, the power symbol), no vendor's interface or logo. + as a drawing: one card per step, a pictogram and a short caption, drawn + with sitediagrams' primitives and shown as its SVG files, one per layout + and theme (sitediagrams.pictures). The text steps above it + (platforms/_guides.yml, terminals) are the one text source: the images + are alt="", so the page reads the steps once. The files say the same in + their title and desc, for one opened on its own. The pictograms are + generic shapes (a menu, a search field, a window, keys, a mouse, the + power symbol), no vendor's interface or logo. - The installer demo recorded for that place (scripts/make_demo.py): the full-screen interface on a handheld under "On the device", the terminal recording under the others but Windows. No Windows recording is @@ -23,7 +25,6 @@ from xml.sax.saxutils import escape import sitemarkup as markup from sitediagrams import ( - FONT, MONO_STYLE, NARROW, STROKE, @@ -35,6 +36,9 @@ from sitediagrams import ( _n, _rect, _text, + diagram_files, + pictures, + svg_document, wrap, ) @@ -92,7 +96,7 @@ def demo_variant(tab: str) -> str: def description(tab: str) -> str: - """The steps in words: the SVG's desc.""" + """The steps in words: the desc of the SVG files.""" return " ".join(f"{index}. {step.caption}." for index, step in enumerate(STEPS[tab][1], start=1)) @@ -234,25 +238,29 @@ def _narrow(steps: tuple[Step, ...]) -> tuple[float, float, list[str]]: def _svg(tab: str, layout: str) -> str: title, steps = STEPS[tab] width, height, body = (_wide if layout == "wide" else _narrow)(steps) - base = f"rb-steps-{tab}-{layout}" - return ( - f'' - f'{escape(title)}' - f'{escape(description(tab))}' - + "".join(body) + "" - ) + return svg_document(width, height, title, description(tab), body, f"rb-steps-{tab}-{layout}") + + +def _layouts(tab: str) -> dict[str, str]: + return {layout: _svg(tab, layout) for layout in ("wide", "narrow")} + + +def stem(tab: str) -> str: + return f"steps-{tab}" + + +def steps_files() -> dict[str, str]: + """Every tab's drawing files, by name under sitediagrams.DIAGRAM_DIR.""" + return {name: text for tab in STEPS for name, text in diagram_files(stem(tab), _layouts(tab)).items()} def steps_figure(tab: str) -> list[str]: - """The figure of a tab's steps, both layouts, or nothing for a tab with - no drawing.""" + """The figure of a tab's steps, both layouts in both themes, or nothing + for a tab with no drawing. The steps above it say its words.""" if tab not in STEPS: return [] return [f'
', - _svg(tab, "wide") + _svg(tab, "narrow"), "
", ""] + pictures(stem(tab), _layouts(tab)), "", ""] def tab_demo(tab: str) -> list[str]: diff --git a/tests/test_site_steps.py b/tests/test_site_steps.py index 77e4319ab..0e5f6715b 100644 --- a/tests/test_site_steps.py +++ b/tests/test_site_steps.py @@ -1,9 +1,10 @@ """The drawn steps of the install box (scripts/sitesteps.py). -Each drawing says in words what it shows, to a screen reader and to a -reader whose images do not load: a title and a desc the SVG names. It tells -the same steps as the text above it, the keys included, so the picture and -the words never disagree. And it draws in the theme's colours. +Each drawing file says in words what it shows, for one opened on its own: +a title and a desc the SVG names. On the page the images are alt="" and the +text above them is the one reading (tests/test_site_page_text.py). The +drawing tells the same steps as that text, the keys included, so the picture +and the words never disagree. And it draws in the theme's colours. """ from __future__ import annotations @@ -19,6 +20,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT / "scripts")) +import sitediagrams # noqa: E402 import sitesteps # noqa: E402 from siteguides import TERMINALS # noqa: E402 @@ -84,10 +86,12 @@ class TheBuiltPageHoldsThem(unittest.TestCase): self.skipTest("no built site") html = page.read_text(encoding="utf-8") for tab in sitesteps.STEPS: - with self.subTest(tab=tab): - self.assertIn(f'aria-labelledby="rb-steps-{tab}-wide-t rb-steps-{tab}-wide-d"', - html) - self.assertIn(f'', html) + for layout in ("wide", "narrow"): + for theme in sitediagrams.THEMES: + name = f"{sitesteps.stem(tab)}-{layout}-{theme}.svg" + with self.subTest(file=name): + self.assertIn(f"/retrobios/{sitediagrams.DIAGRAM_DIR}/{name}", html) + self.assertTrue((DIST / sitediagrams.DIAGRAM_DIR / name).is_file()) if __name__ == "__main__": diff --git a/tests/test_site_wiki.py b/tests/test_site_wiki.py index 57684cde9..f1106bbc1 100644 --- a/tests/test_site_wiki.py +++ b/tests/test_site_wiki.py @@ -352,7 +352,7 @@ class DiagramText(unittest.TestCase): sitediagrams.MAX_BOX_LINES) def test_a_path_is_mono_on_its_own_line(self): - html = sitediagrams.figure_html("bios-chain") + html = sitediagrams.svg(sitediagrams.figures()["bios-chain"], "wide") self.assertRegex(html, r']*style="font-family: var\(--sl-font-mono[^>]*>/userdata/bios/') def test_boxes_of_a_row_share_one_height(self): diff --git a/website/src/styles/retrobios.css b/website/src/styles/retrobios.css index fc89ca2e2..61e36df4c 100644 --- a/website/src/styles/retrobios.css +++ b/website/src/styles/retrobios.css @@ -756,9 +756,16 @@ body:has(table.rb-entries) { --sl-content-width: var(--rb-grid-width); } /* Terminal output: what the screen shows, not something to type */ .sl-markdown-content .rb-terminal .expressive-code pre { background: var(--rb-terminal-bg); color: var(--rb-terminal-fg); } -/* Diagrams written by scripts/sitediagrams.py follow the theme */ +/* Diagrams written by scripts/sitediagrams.py follow the theme: each is an + image per theme (its colours written in the file, sitediagrams.THEMES), + and the one of the other theme is not shown, so not fetched either. + Starlight's dark theme is the one without data-theme='light'. */ .sl-markdown-content .rb-figure { margin: 1.2rem 0 1.6rem; } -.sl-markdown-content .rb-figure svg { display: block; width: 100%; height: auto; max-width: 52rem; color: var(--sl-color-text); } +.sl-markdown-content .rb-figure .rb-diagram { display: block; max-width: 52rem; } +.sl-markdown-content .rb-figure .rb-diagram img { display: block; width: 100%; height: auto; } +.sl-markdown-content .rb-figure .rb-diagram-light { display: none; } +:root[data-theme='light'] .sl-markdown-content .rb-figure .rb-diagram-light { display: block; } +:root[data-theme='light'] .sl-markdown-content .rb-figure .rb-diagram-dark { display: none; } .sl-markdown-content .rb-figure :is(figcaption, .rb-figure-caption) { display: block; max-width: 44em; @@ -766,19 +773,6 @@ body:has(table.rb-entries) { --sl-content-width: var(--rb-grid-width); } font-size: 0.8rem; color: var(--rb-text-secondary); } -.rb-diagram text { font-family: var(--sl-font); fill: currentColor; } -.rb-diagram .rb-d-box { fill: var(--rb-diagram-fill); stroke: var(--rb-diagram-stroke); } -.rb-diagram .rb-d-accent { stroke: var(--rb-diagram-accent); } -.rb-diagram .rb-d-line { stroke: var(--rb-diagram-stroke); } -.rb-diagram .rb-d-head { fill: var(--rb-diagram-stroke); } -.rb-diagram .rb-d-num { fill: var(--rb-diagram-accent); } -.rb-diagram .rb-d-numtext { fill: var(--sl-color-bg); } -.rb-diagram .rb-d-muted { fill: var(--rb-text-secondary); } -.rb-diagram-narrow { display: none !important; } -@media (max-width: 37.5em) { - .rb-diagram-wide { display: none !important; } - .rb-diagram-narrow { display: block !important; } -} /* Glossary terms */ abbr.rb-term {