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 {