Coverage for src/rhiza_task/tasks/presentation.py: 100%

39 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-10-13 02:27 +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 

89def has_presentation(cfg: Config) -> bool: 

90 """Return whether the repository has a slide deck, for ``list``. 

91 

92 Args: 

93 cfg: The resolved config. 

94 

95 Returns: 

96 True when ``presentation_file`` exists -- the file :func:`source` skips without. 

97 """ 

98 return (cfg.root / cfg.presentation_file).is_file() 

99 

100 

101@task("presentation", "generate the HTML slides with Marp", section=SECTION, applies=has_presentation) 

102def presentation(cfg: Config) -> None: 

103 """Export the deck to a single HTML file. 

104 

105 Args: 

106 cfg: The resolved config. 

107 """ 

108 binary, prefix = marp_argv(cfg) 

109 target = output(cfg, ".html") 

110 tool(binary, *prefix, source(cfg).name, "-o", target, cwd=cfg.root) 

111 print(f"[SUCCESS] {target} — open it in a browser to view the slides") 

112 

113 

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

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

116 """Export the deck to PDF. 

117 

118 ``--allow-local-files`` is presentation.mk's and is required rather than optional: 

119 Marp renders the PDF through headless Chrome, which refuses ``file://`` images 

120 without it, so a deck with a local logo silently loses it. 

121 

122 Args: 

123 cfg: The resolved config. 

124 """ 

125 binary, prefix = marp_argv(cfg) 

126 target = output(cfg, ".pdf") 

127 tool(binary, *prefix, source(cfg).name, "-o", target, "--allow-local-files", cwd=cfg.root) 

128 print(f"[SUCCESS] {target}") 

129 

130 

131@task("presentation-serve", "serve the slides with Marp's live preview", section=SECTION, applies=has_presentation) 

132def presentation_serve(cfg: Config) -> None: 

133 """Start Marp's watching server over the repository. 

134 

135 Args: 

136 cfg: The resolved config. 

137 """ 

138 binary, prefix = marp_argv(cfg) 

139 print("[INFO] starting the Marp server (Ctrl-C to stop)") 

140 tool(binary, *prefix, "-s", ".", cwd=cfg.root)