scrolly 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. scrolly-0.2.2/LICENSE +28 -0
  2. {scrolly-0.2.1 → scrolly-0.2.2}/PKG-INFO +7 -3
  3. {scrolly-0.2.1 → scrolly-0.2.2}/README.md +6 -2
  4. {scrolly-0.2.1 → scrolly-0.2.2}/pyproject.toml +16 -1
  5. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/_cli.py +10 -0
  6. scrolly-0.2.2/scrolly/errors/catalog/E306.md +20 -0
  7. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/pipeline/lint.py +2 -3
  8. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/pipeline/orchestrator.py +31 -3
  9. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/pipeline/writer.py +18 -7
  10. scrolly-0.2.2/scrolly/render/__init__.py +6 -0
  11. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/render/assembler.py +25 -6
  12. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/render/assets/canvas.css +42 -0
  13. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/render/assets/canvas.js +236 -123
  14. scrolly-0.2.1/LICENSE → scrolly-0.2.2/scrolly/render/assets/mermaid-LICENSE +2 -2
  15. scrolly-0.2.2/scrolly/render/assets/mermaid.min.js +3405 -0
  16. scrolly-0.2.2/scrolly/render/bundled_assets.py +147 -0
  17. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/rendered.py +1 -1
  18. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/renderers/image_sequence.py +65 -51
  19. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/html.py +1 -1
  20. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/introspect.py +6 -5
  21. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/ir/_framework/element.py +41 -29
  22. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/renderers/slide.py +18 -1
  23. scrolly-0.2.1/scrolly/errors/catalog/E306.md +0 -20
  24. scrolly-0.2.1/scrolly/render/__init__.py +0 -6
  25. scrolly-0.2.1/scrolly/render/bundled_assets.py +0 -55
  26. {scrolly-0.2.1 → scrolly-0.2.2}/.gitignore +0 -0
  27. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/__init__.py +0 -0
  28. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/__init__.py +0 -0
  29. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/_errors.py +0 -0
  30. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/_introspect/__init__.py +0 -0
  31. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/_introspect/_assets.py +0 -0
  32. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/_introspect/_common.py +0 -0
  33. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/_introspect/_dom.py +0 -0
  34. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/_introspect/_elements.py +0 -0
  35. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/_introspect/_slides.py +0 -0
  36. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/_introspect/_snaps.py +0 -0
  37. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/_introspect/_snapshot.py +0 -0
  38. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/_cli/_introspect/_timeline.py +0 -0
  39. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/deck/__init__.py +0 -0
  40. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/deck/inference.py +0 -0
  41. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/deck/introspect.py +0 -0
  42. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/deck/model.py +0 -0
  43. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/deck/parser.py +0 -0
  44. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/deck/schema.py +0 -0
  45. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/deck/validator.py +0 -0
  46. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/__init__.py +0 -0
  47. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/_catalog.py +0 -0
  48. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/_codes.py +0 -0
  49. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/_report.py +0 -0
  50. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/_validation_error.py +0 -0
  51. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E001.md +0 -0
  52. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E002.md +0 -0
  53. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E003.md +0 -0
  54. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E004.md +0 -0
  55. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E005.md +0 -0
  56. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E006.md +0 -0
  57. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E007.md +0 -0
  58. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E008.md +0 -0
  59. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E009.md +0 -0
  60. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E010.md +0 -0
  61. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E011.md +0 -0
  62. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E012.md +0 -0
  63. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E101.md +0 -0
  64. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E102.md +0 -0
  65. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E103.md +0 -0
  66. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E201.md +0 -0
  67. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E202.md +0 -0
  68. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E203.md +0 -0
  69. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E204.md +0 -0
  70. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E205.md +0 -0
  71. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E206.md +0 -0
  72. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E207.md +0 -0
  73. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E299.md +0 -0
  74. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E301.md +0 -0
  75. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E302.md +0 -0
  76. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E303.md +0 -0
  77. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E304.md +0 -0
  78. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E305.md +0 -0
  79. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E307.md +0 -0
  80. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E308.md +0 -0
  81. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E401.md +0 -0
  82. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E402.md +0 -0
  83. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E403.md +0 -0
  84. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E501.md +0 -0
  85. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E502.md +0 -0
  86. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E503.md +0 -0
  87. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E504.md +0 -0
  88. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E505.md +0 -0
  89. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E601.md +0 -0
  90. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E602.md +0 -0
  91. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E603.md +0 -0
  92. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E701.md +0 -0
  93. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/E702.md +0 -0
  94. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/errors/catalog/__init__.py +0 -0
  95. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/pipeline/__init__.py +0 -0
  96. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/pipeline/_bundler.py +0 -0
  97. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/pipeline/assets.py +0 -0
  98. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/pipeline/introspect.py +0 -0
  99. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/pipeline/loader.py +0 -0
  100. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/render/fan.py +0 -0
  101. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/render/nav_data.py +0 -0
  102. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/render/templates/index.html.j2 +0 -0
  103. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/render/zoom_control.py +0 -0
  104. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/__init__.py +0 -0
  105. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/__init__.py +0 -0
  106. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/ir.py +0 -0
  107. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/processor.py +0 -0
  108. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/registry.py +0 -0
  109. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/renderers/__init__.py +0 -0
  110. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/renderers/_shared.py +0 -0
  111. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/renderers/html.py +0 -0
  112. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/renderers/iframe.py +0 -0
  113. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/renderers/image.py +0 -0
  114. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/renderers/markdown.py +0 -0
  115. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/element_ir/renderers/mermaid.py +0 -0
  116. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/ir/__init__.py +0 -0
  117. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/ir/_framework/__init__.py +0 -0
  118. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/ir/_framework/animated_values.py +0 -0
  119. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/ir/_framework/utils.py +0 -0
  120. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/ir/slide.py +0 -0
  121. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/processor.py +0 -0
  122. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/registry.py +0 -0
  123. {scrolly-0.2.1 → scrolly-0.2.2}/scrolly/slide/renderers/__init__.py +0 -0
scrolly-0.2.2/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026, Bert Pluymers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ----
24
+
25
+ scrolly bundles third-party assets that ship under their own licenses,
26
+ located alongside the asset in the package tree. See for example
27
+ `scrolly/render/assets/mermaid-LICENSE` (MIT — Copyright (c) 2014-2022
28
+ Knut Sveidqvist).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrolly
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: CLI that compiles a JSON5 deck + slide files into a self-contained 2D-canvas HTML presentation.
5
5
  Project-URL: Source, https://github.com/bertpl/scrolly
6
6
  Project-URL: Changelog, https://github.com/bertpl/scrolly/blob/main/CHANGELOG.md
@@ -33,6 +33,10 @@ Description-Content-Type: text/markdown
33
33
 
34
34
  Compile a JSON5 deck into a self-contained, scrollable 2D-canvas HTML presentation.
35
35
 
36
+ *Liked by humans; understood by agents.*
37
+
38
+ [![scrolly — scroll-driven 2D-canvas presentations](https://raw.githubusercontent.com/bertpl/scrolly/main/docs/assets/hero-v1.gif)](https://github.com/bertpl/scrolly)
39
+
36
40
  [![CI](https://img.shields.io/github/actions/workflow/status/bertpl/scrolly/push_to_main.yml?branch=main&label=CI)](https://github.com/bertpl/scrolly/actions/workflows/push_to_main.yml)
37
41
  [![PyPI](https://img.shields.io/pypi/v/scrolly.svg)](https://pypi.org/project/scrolly/)
38
42
  [![Python](https://img.shields.io/pypi/pyversions/scrolly.svg)](https://pypi.org/project/scrolly/)
@@ -53,11 +57,11 @@ uv tool install scrolly
53
57
  ## Quickstart
54
58
 
55
59
  ```bash
56
- scrolly build examples/worked-example/deck.deck.json --out /tmp/scrolly-out --force
60
+ scrolly build examples/stacked-diffs/deck.deck.json --out /tmp/scrolly-out --force
57
61
  open /tmp/scrolly-out/index.html
58
62
  ```
59
63
 
60
- See [`examples/worked-example/`](examples/worked-example/) for a reference deck.
64
+ See [`examples/stacked-diffs/`](examples/stacked-diffs/) for a complete example deck.
61
65
 
62
66
  ## Source format
63
67
 
@@ -2,6 +2,10 @@
2
2
 
3
3
  Compile a JSON5 deck into a self-contained, scrollable 2D-canvas HTML presentation.
4
4
 
5
+ *Liked by humans; understood by agents.*
6
+
7
+ [![scrolly — scroll-driven 2D-canvas presentations](https://raw.githubusercontent.com/bertpl/scrolly/main/docs/assets/hero-v1.gif)](https://github.com/bertpl/scrolly)
8
+
5
9
  [![CI](https://img.shields.io/github/actions/workflow/status/bertpl/scrolly/push_to_main.yml?branch=main&label=CI)](https://github.com/bertpl/scrolly/actions/workflows/push_to_main.yml)
6
10
  [![PyPI](https://img.shields.io/pypi/v/scrolly.svg)](https://pypi.org/project/scrolly/)
7
11
  [![Python](https://img.shields.io/pypi/pyversions/scrolly.svg)](https://pypi.org/project/scrolly/)
@@ -22,11 +26,11 @@ uv tool install scrolly
22
26
  ## Quickstart
23
27
 
24
28
  ```bash
25
- scrolly build examples/worked-example/deck.deck.json --out /tmp/scrolly-out --force
29
+ scrolly build examples/stacked-diffs/deck.deck.json --out /tmp/scrolly-out --force
26
30
  open /tmp/scrolly-out/index.html
27
31
  ```
28
32
 
29
- See [`examples/worked-example/`](examples/worked-example/) for a reference deck.
33
+ See [`examples/stacked-diffs/`](examples/stacked-diffs/) for a complete example deck.
30
34
 
31
35
  ## Source format
32
36
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrolly"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "CLI that compiles a JSON5 deck + slide files into a self-contained 2D-canvas HTML presentation."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -37,6 +37,17 @@ dev = [
37
37
  "pytest-cov>=6.0",
38
38
  "ruff>=0.14.0",
39
39
  "pre-commit>=4.0",
40
+ # Exact pin: gitsvg is pre-1.0 and its output schema can shift
41
+ # between releases. Used to regenerate the stacked-diffs example's
42
+ # gitsvg frames.
43
+ "gitsvg==0.2.2",
44
+ ]
45
+ # Optional group for the hero-animation capture pipeline (docs/_gen).
46
+ # Heavy (Playwright ships browser binaries via `make capture-setup`),
47
+ # so it's separate from `dev` — casual contributors don't pay the cost.
48
+ capture = [
49
+ "playwright>=1.40",
50
+ "pillow>=10.0",
40
51
  ]
41
52
 
42
53
  [project.scripts]
@@ -66,6 +77,10 @@ select = ["I"]
66
77
 
67
78
  [tool.pytest.ini_options]
68
79
  testpaths = ["tests/python"]
80
+ # The hero-animation engine lives under docs/_gen (dev-only tooling, not
81
+ # shipped in the wheel); put it on the path so its pure-logic unit tests
82
+ # can import it without the optional `capture` group installed.
83
+ pythonpath = ["docs/_gen"]
69
84
 
70
85
  [tool.coverage.run]
71
86
  source = ["scrolly"]
@@ -43,6 +43,14 @@ def cli() -> None:
43
43
  is_flag=True,
44
44
  help="Disable gzip compression of inlined assets.",
45
45
  )
46
+ @click.option(
47
+ "--offline",
48
+ is_flag=True,
49
+ help=(
50
+ "Skip the mermaid CDN download and use the wheel-bundled mermaid for "
51
+ "byte-reproducibility. SCROLLY_OFFLINE=1 in the environment is equivalent."
52
+ ),
53
+ )
46
54
  def build(
47
55
  deck_path: Path,
48
56
  out_dir: Path,
@@ -51,6 +59,7 @@ def build(
51
59
  strict: bool,
52
60
  simplified_zoom_control: bool,
53
61
  no_compress: bool,
62
+ offline: bool,
54
63
  ) -> None:
55
64
  """Build a deck into a self-contained HTML presentation."""
56
65
  try:
@@ -61,6 +70,7 @@ def build(
61
70
  inline=not no_inline,
62
71
  simplified_zoom_control=simplified_zoom_control,
63
72
  compress=not no_compress,
73
+ offline=offline,
64
74
  )
65
75
  except ScrollyError as e:
66
76
  _err_console.print(f"[red]error:[/red] {e}")
@@ -0,0 +1,20 @@
1
+ # E306 - image_sequence timing constraint violated
2
+
3
+ ## Cause
4
+
5
+ One of the timing constraints on `image_sequence` is violated:
6
+ `frame_distance` must be `> 0`; `hold_fraction` must be in `[0, 1)` (so the
7
+ crossfade `frame_distance * (1 - hold_fraction)` stays positive); `fade_in`
8
+ and `fade_out` must be `>= 0`.
9
+
10
+ ## Example
11
+
12
+ ```json5
13
+ { image_sequence: ["a.png", "b.png"], frame_distance: 400, hold_fraction: 1 }
14
+ // hold_fraction = 1 leaves no room to crossfade
15
+ ```
16
+
17
+ ## How to fix
18
+
19
+ Set `frame_distance > 0`, `0 <= hold_fraction < 1`, and `fade_in,
20
+ fade_out >= 0`. `hold_fraction` defaults to `0.2`, so it can be omitted.
@@ -145,9 +145,8 @@ def _check_image_sequence(
145
145
  diagnostics: list[Diagnostic],
146
146
  ) -> None:
147
147
  """Check the auto-generated opacity keyframes for an image sequence element."""
148
- n = len(el.image_sequence)
149
- timeline_start = el.scroll_offset - el.fade_in
150
- timeline_end = el.scroll_offset + (n - 1) * el.frame_distance + el.hold + el.fade_out
148
+ timeline_start = el.timeline_start()
149
+ timeline_end = el.timeline_end()
151
150
  if timeline_start < 0:
152
151
  diagnostics.append(
153
152
  Diagnostic(
@@ -12,6 +12,7 @@ from scrolly.pipeline.assets import copy_assets, rewrite_asset_refs
12
12
  from scrolly.pipeline.loader import load_deck
13
13
  from scrolly.pipeline.writer import write_output
14
14
  from scrolly.render.assembler import assemble
15
+ from scrolly.render.bundled_assets import MermaidAsset, mermaid_asset
15
16
  from scrolly.slide.html import SlideHTML
16
17
  from scrolly.slide.ir import SlideIR
17
18
  from scrolly.slide.registry import find_renderer
@@ -25,8 +26,28 @@ def build_deck(
25
26
  inline: bool = True,
26
27
  simplified_zoom_control: bool = False,
27
28
  compress: bool = True,
29
+ offline: bool = False,
28
30
  ) -> Deck:
29
- """Build a deck from `deck_path` into `out_dir`. Returns the fully-resolved `Deck`."""
31
+ """Build a deck from `deck_path` into `out_dir`. Returns the fully-resolved `Deck`.
32
+
33
+ Args:
34
+ deck_path: Path to the ``.deck.json`` source.
35
+ out_dir: Destination directory for ``index.html`` (and bundled
36
+ assets when ``inline=False``).
37
+ force: Allow overwriting a non-empty ``out_dir``.
38
+ inline: Inline CSS/JS/mermaid into ``index.html`` (default) vs.
39
+ emit separate files.
40
+ simplified_zoom_control: Use the legacy single-icon zoom-out
41
+ control instead of the default deck mini-map.
42
+ compress: Emit the combined-payload gzip+base64 bundle when the
43
+ ≥5% savings gate passes.
44
+ offline: Skip the mermaid CDN download and use the
45
+ wheel-bundled mermaid instead. Honored together with the
46
+ ``SCROLLY_OFFLINE`` environment variable.
47
+
48
+ Returns:
49
+ The fully-resolved ``Deck``.
50
+ """
30
51
  deck, slide_irs = load_deck(deck_path)
31
52
 
32
53
  # Bundler is the canonical "compressible payload tracker" whenever
@@ -56,6 +77,13 @@ def build_deck(
56
77
  if fallback:
57
78
  chunks = _substitute_fallback(chunks, fallback)
58
79
 
80
+ # Resolve mermaid once when any chunk needs it — threaded into both
81
+ # assemble (for inlined content + help-screen version) and write_output
82
+ # (for the standalone-file emission under `inline=False`).
83
+ mermaid: MermaidAsset | None = None
84
+ if any(chunk.has_mermaid for chunk in chunks.values()):
85
+ mermaid = mermaid_asset(offline=offline)
86
+
59
87
  html = assemble(
60
88
  deck,
61
89
  chunks,
@@ -63,10 +91,10 @@ def build_deck(
63
91
  simplified_zoom_control=simplified_zoom_control,
64
92
  compressed_payload_json=compressed_payload_json,
65
93
  bundle_stats=bundle_stats,
94
+ mermaid=mermaid,
66
95
  )
67
- has_mermaid = any(chunk.has_mermaid for chunk in chunks.values())
68
96
 
69
- write_output(out_dir, html, force=force, has_mermaid=has_mermaid, inline=inline)
97
+ write_output(out_dir, html, force=force, mermaid=mermaid, inline=inline)
70
98
  if not inline:
71
99
  copy_assets(chunks, out_dir)
72
100
 
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from pathlib import Path
6
6
 
7
7
  from scrolly.errors import OutputError
8
- from scrolly.render import iter_assets, mermaid_asset
8
+ from scrolly.render import MermaidAsset, iter_assets
9
9
 
10
10
 
11
11
  def write_output(
@@ -13,13 +13,25 @@ def write_output(
13
13
  html: str,
14
14
  *,
15
15
  force: bool = False,
16
- has_mermaid: bool = False,
16
+ mermaid: MermaidAsset | None = None,
17
17
  inline: bool = True,
18
18
  ) -> None:
19
19
  """Write `html` as `out_dir/index.html` and optionally copy bundled assets.
20
20
 
21
- If `out_dir` exists and is non-empty, `force=True` is required to overwrite.
22
- In inline mode, only `index.html` is written (CSS/JS are embedded in the HTML).
21
+ Args:
22
+ out_dir: Destination directory.
23
+ html: Assembled page HTML.
24
+ force: Allow overwriting a non-empty `out_dir`.
25
+ mermaid: Resolved mermaid asset (passed through from
26
+ :func:`build_deck`). When ``inline=False`` and ``mermaid``
27
+ is non-None, the mermaid JS file is written alongside the
28
+ other bundled assets.
29
+ inline: When ``True``, only ``index.html`` is written (CSS, JS,
30
+ and mermaid are embedded in the HTML).
31
+
32
+ Raises:
33
+ OutputError: ``out_dir`` exists but is not a directory, or is
34
+ non-empty without ``force=True``.
23
35
  """
24
36
  if out_dir.exists():
25
37
  if not out_dir.is_dir():
@@ -36,6 +48,5 @@ def write_output(
36
48
  if not inline:
37
49
  for name, content in iter_assets():
38
50
  (out_dir / name).write_bytes(content)
39
- if has_mermaid:
40
- name, content = mermaid_asset()
41
- (out_dir / name).write_bytes(content)
51
+ if mermaid is not None:
52
+ (out_dir / mermaid.name).write_bytes(mermaid.content)
@@ -0,0 +1,6 @@
1
+ """Final HTML assembly — canvas template + bundled static assets."""
2
+
3
+ from scrolly.render.assembler import assemble
4
+ from scrolly.render.bundled_assets import MermaidAsset, bundled_css, bundled_js, iter_assets, mermaid_asset
5
+
6
+ __all__ = ["MermaidAsset", "assemble", "bundled_css", "bundled_js", "iter_assets", "mermaid_asset"]
@@ -15,7 +15,7 @@ from jinja2 import Environment, PackageLoader, StrictUndefined, select_autoescap
15
15
  from scrolly import __version__
16
16
  from scrolly.deck import Deck
17
17
  from scrolly.pipeline._bundler import BundleStats
18
- from scrolly.render.bundled_assets import bundled_css, bundled_js, mermaid_js
18
+ from scrolly.render.bundled_assets import MermaidAsset, bundled_css, bundled_js
19
19
  from scrolly.render.nav_data import build_nav_data
20
20
  from scrolly.render.zoom_control import MinimapGeometry, compute_minimap_geometry
21
21
  from scrolly.slide import SlideHTML
@@ -43,6 +43,7 @@ def assemble(
43
43
  simplified_zoom_control: bool = False,
44
44
  compressed_payload_json: str | None = None,
45
45
  bundle_stats: BundleStats | None = None,
46
+ mermaid: MermaidAsset | None = None,
46
47
  ) -> str:
47
48
  """Render the deck and its chunks into a single HTML string.
48
49
 
@@ -59,6 +60,10 @@ def assemble(
59
60
  bundle_stats: Stats from the bundler's ``build()``, used to
60
61
  populate the help-screen statistics. ``None`` when no bundle
61
62
  was emitted.
63
+ mermaid: Resolved mermaid asset (content + version + source
64
+ tier), or ``None`` when the deck has no mermaid elements.
65
+ Inlined into the page when ``inline=True`` and present;
66
+ its version always lands in the help-screen meta.
62
67
 
63
68
  Returns:
64
69
  The rendered HTML page as a single string.
@@ -66,16 +71,16 @@ def assemble(
66
71
  template = _env().get_template("index.html.j2")
67
72
  nav_data = build_nav_data(deck, chunks)
68
73
  scoped_css_blocks = _collect_scoped_css(deck, chunks)
69
- has_mermaid = any(chunk.has_mermaid for chunk in chunks.values())
74
+ has_mermaid = mermaid is not None
70
75
  minimap: MinimapGeometry | None = None if simplified_zoom_control else compute_minimap_geometry(deck)
71
- meta = _build_meta(deck, chunks, bundle_stats=bundle_stats)
76
+ meta = _build_meta(deck, chunks, bundle_stats=bundle_stats, mermaid=mermaid)
72
77
 
73
78
  inline_vars = {}
74
79
  if inline:
75
80
  inline_vars["bundled_css"] = bundled_css()
76
81
  inline_vars["bundled_js"] = bundled_js()
77
- if has_mermaid:
78
- inline_vars["mermaid_js_content"] = mermaid_js()
82
+ if mermaid is not None:
83
+ inline_vars["mermaid_js_content"] = mermaid.content.decode("utf-8")
79
84
 
80
85
  html = template.render(
81
86
  title=deck.title or "scrolly",
@@ -100,8 +105,21 @@ def _build_meta(
100
105
  chunks: dict[str, SlideHTML],
101
106
  *,
102
107
  bundle_stats: BundleStats | None = None,
108
+ mermaid: MermaidAsset | None = None,
103
109
  ) -> dict[str, Any]:
104
- """Build the metadata dict injected into the HTML for the help screen."""
110
+ """Build the metadata dict injected into the HTML for the help screen.
111
+
112
+ Args:
113
+ deck: The fully-resolved deck.
114
+ chunks: Rendered per-slide HTML chunks.
115
+ bundle_stats: Bundler snapshot for the payload-stats section.
116
+ mermaid: Resolved mermaid asset, whose version is surfaced in
117
+ the help-screen statistics. ``None`` for decks without
118
+ mermaid elements.
119
+
120
+ Returns:
121
+ Help-screen metadata dict.
122
+ """
105
123
  return {
106
124
  "version": __version__,
107
125
  "author": "Bert Pluymers",
@@ -110,6 +128,7 @@ def _build_meta(
110
128
  "slides": len(deck.slides),
111
129
  "edges": len(deck.edges),
112
130
  "payloads": _payload_stats(bundle_stats),
131
+ "mermaid_version": mermaid.version if mermaid is not None else None,
113
132
  "file_size": "__FILE_SIZE_PLACEHOLDER__",
114
133
  },
115
134
  }
@@ -314,6 +314,48 @@ body {
314
314
  overflow: hidden;
315
315
  }
316
316
 
317
+ /* ---- Mixed-font typography inside slide content ------------------------
318
+ *
319
+ * Slides routinely mix the body sans-serif with inline `<code>`, `<kbd>`,
320
+ * `<samp>` and `<pre>` blocks. By default the browser pairs `system-ui`
321
+ * (SF Pro / Segoe UI / Roboto) with the user's preferred generic
322
+ * `monospace` (Menlo / Consolas / Courier) — fonts not designed to share
323
+ * metrics — which makes inline `<code>` glyphs sit visibly higher or
324
+ * lower than adjacent sans-serif text whenever a layout depends on
325
+ * vertical alignment between lines.
326
+ *
327
+ * Two corrections, narrow scope (slide content only — UI chrome keeps
328
+ * its explicit per-component fonts):
329
+ *
330
+ * 1. Pin monospace to `ui-monospace` and OS-native pairs. `ui-monospace`
331
+ * picks the platform partner of `system-ui` (SF Mono on macOS,
332
+ * Cascadia Mono on Windows), drawn by the same designer to share
333
+ * metrics with the sans-serif.
334
+ *
335
+ * 2. Set `font-size-adjust: from-font` on the slide-element wrapper.
336
+ * The browser computes the body sans-serif's x-height ratio and
337
+ * inherits it down; when a `<code>` descendant falls back to its
338
+ * monospace family, the monospace font is rescaled so its x-height
339
+ * matches the sans-serif's. The visible "weight" of glyphs aligns
340
+ * across fonts even though their natural metrics differ.
341
+ *
342
+ * Together these reduce the residual mismatch to where it's no longer
343
+ * visible in normal slide layouts. The structural fix — a layout
344
+ * primitive that owns vertical pitch independently of font metrics —
345
+ * would supersede this CSS defaulting.
346
+ */
347
+ .slide-type-slide-json .slide-element {
348
+ font-size-adjust: from-font;
349
+ }
350
+
351
+ .slide-type-slide-json .slide-element code,
352
+ .slide-type-slide-json .slide-element kbd,
353
+ .slide-type-slide-json .slide-element samp,
354
+ .slide-type-slide-json .slide-element pre {
355
+ font-family: ui-monospace, SFMono-Regular, "SF Mono",
356
+ Menlo, Consolas, monospace;
357
+ }
358
+
317
359
  /* Animation-driven slides (numeric scroll_range) counter-translate the
318
360
  * chunk's scroll-driven translateY so content stays visually stationary
319
361
  * while --scroll-position drives the calc()-based keyframe expressions