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

1"""The Marp tasks: presentation.mk, as tasks. 

2 

3The fragment's ``require-marp`` does not check for Marp, it *installs* it: 

4 

5 if ! command -v marp; then npm install -g @marp-team/marp-cli; fi 

6 

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. 

12 

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. 

16 

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""" 

21 

22from __future__ import annotations 

23 

24from pathlib import Path 

25 

26from ..config import Config 

27from ..spec import Skip, have, task 

28from ..uv import tool 

29 

30SECTION = "Presentation" 

31 

32NODE_URL = "https://nodejs.org/" 

33 

34 

35def marp_argv(cfg: Config) -> tuple[str, tuple[str, ...]]: 

36 """Resolve how to reach the Marp CLI on this machine. 

37 

38 Args: 

39 cfg: The resolved config. 

40 

41 Returns: 

42 The executable to run and the arguments that must precede Marp's own. 

43 

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})") 

52 

53 

54def source(cfg: Config) -> Path: 

55 """Return the slide deck's source file. 

56 

57 Args: 

58 cfg: The resolved config. 

59 

60 Returns: 

61 The absolute path to the configured Markdown file. 

62 

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 

70 

71 

72def output(cfg: Config, suffix: str) -> str: 

73 """Return the output filename for a format. 

74 

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. 

78 

79 Args: 

80 cfg: The resolved config. 

81 suffix: The output extension, with its dot. 

82 

83 Returns: 

84 A repository-relative filename. 

85 """ 

86 return Path(cfg.presentation_file).with_suffix(suffix).name.lower() 

87 

88 

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. 

92 

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") 

100 

101 

102@task("presentation-pdf", "generate the PDF slides with Marp", section=SECTION) 

103def presentation_pdf(cfg: Config) -> None: 

104 """Export the deck to PDF. 

105 

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. 

109 

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}") 

117 

118 

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. 

122 

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)