Coverage for src/rhiza_task/tasks/presentation.py: 100%
37 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:13 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:13 +0000
1"""The Marp tasks: presentation.mk, as tasks.
3The fragment's ``require-marp`` does not check for Marp, it *installs* it:
5 if ! command -v marp; then npm install -g @marp-team/marp-cli; fi
7-- a global npm install, triggered by typing ``make presentation``, changing a machine
8outside the repository. :func:`marp_argv` keeps the property that made that acceptable (a
9consumer with Node but no Marp can still build slides) without that side effect:
10``npx --yes`` runs the CLI from npm's cache instead. The precedence is Marp on PATH first,
11so a deliberately installed or pinned Marp still wins.
13:attr:`~rhiza_task.config.Config.marp_package` is what npx is given, unpinned by default
14because ``npm install -g @marp-team/marp-cli`` was unpinned too. Pin it to
15``@marp-team/marp-cli@4.2.3`` when reproducible slides matter more than current ones.
17``PRESENTATION.md`` becomes a setting rather than a constant, and the output name is
18derived from it -- lower-cased, so the default still produces ``presentation.html`` and
19``presentation.pdf`` exactly as the fragment does.
20"""
22from __future__ import annotations
24from pathlib import Path
26from ..config import Config
27from ..spec import Skip, have, task
28from ..uv import tool
30SECTION = "Presentation"
32NODE_URL = "https://nodejs.org/"
35def marp_argv(cfg: Config) -> tuple[str, tuple[str, ...]]:
36 """Resolve how to reach the Marp CLI on this machine.
38 Args:
39 cfg: The resolved config.
41 Returns:
42 The executable to run and the arguments that must precede Marp's own.
44 Raises:
45 Skip: When neither marp nor npx is available.
46 """
47 if have("marp"):
48 return "marp", ()
49 if have("npx"):
50 return "npx", ("--yes", cfg.marp_package)
51 raise Skip(f"neither marp nor npx found; install Node.js ({NODE_URL})")
54def source(cfg: Config) -> Path:
55 """Return the slide deck's source file.
57 Args:
58 cfg: The resolved config.
60 Returns:
61 The absolute path to the configured Markdown file.
63 Raises:
64 Skip: When the file does not exist.
65 """
66 path = cfg.root / cfg.presentation_file
67 if not path.is_file():
68 raise Skip(f"no {cfg.presentation_file}")
69 return path
72def output(cfg: Config, suffix: str) -> str:
73 """Return the output filename for a format.
75 Lower-cased so that the default ``PRESENTATION.md`` yields ``presentation.html``,
76 which is the name presentation.mk hard-codes and the one a consumer's ``.gitignore``
77 and links already point at.
79 Args:
80 cfg: The resolved config.
81 suffix: The output extension, with its dot.
83 Returns:
84 A repository-relative filename.
85 """
86 return Path(cfg.presentation_file).with_suffix(suffix).name.lower()
89@task("presentation", "generate the HTML slides with Marp", section=SECTION)
90def presentation(cfg: Config) -> None:
91 """Export the deck to a single HTML file.
93 Args:
94 cfg: The resolved config.
95 """
96 binary, prefix = marp_argv(cfg)
97 target = output(cfg, ".html")
98 tool(binary, *prefix, source(cfg).name, "-o", target, cwd=cfg.root)
99 print(f"[SUCCESS] {target} — open it in a browser to view the slides")
102@task("presentation-pdf", "generate the PDF slides with Marp", section=SECTION)
103def presentation_pdf(cfg: Config) -> None:
104 """Export the deck to PDF.
106 ``--allow-local-files`` is presentation.mk's and is required rather than optional:
107 Marp renders the PDF through headless Chrome, which refuses ``file://`` images
108 without it, so a deck with a local logo silently loses it.
110 Args:
111 cfg: The resolved config.
112 """
113 binary, prefix = marp_argv(cfg)
114 target = output(cfg, ".pdf")
115 tool(binary, *prefix, source(cfg).name, "-o", target, "--allow-local-files", cwd=cfg.root)
116 print(f"[SUCCESS] {target}")
119@task("presentation-serve", "serve the slides with Marp's live preview", section=SECTION)
120def presentation_serve(cfg: Config) -> None:
121 """Start Marp's watching server over the repository.
123 Args:
124 cfg: The resolved config.
125 """
126 binary, prefix = marp_argv(cfg)
127 print("[INFO] starting the Marp server (Ctrl-C to stop)")
128 tool(binary, *prefix, "-s", ".", cwd=cfg.root)