scrolly 0.2.4__tar.gz → 0.3.0__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 (126) hide show
  1. {scrolly-0.2.4 → scrolly-0.3.0}/PKG-INFO +1 -1
  2. {scrolly-0.2.4 → scrolly-0.3.0}/pyproject.toml +2 -2
  3. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/_cli/_introspect/__init__.py +7 -7
  4. scrolly-0.2.4/scrolly/_cli/_introspect/_assets.py → scrolly-0.3.0/scrolly/_cli/_introspect/assets.py +1 -1
  5. scrolly-0.2.4/scrolly/_cli/_introspect/_common.py → scrolly-0.3.0/scrolly/_cli/_introspect/common.py +25 -34
  6. scrolly-0.2.4/scrolly/_cli/_introspect/_dom.py → scrolly-0.3.0/scrolly/_cli/_introspect/dom.py +1 -1
  7. scrolly-0.2.4/scrolly/_cli/_introspect/_elements.py → scrolly-0.3.0/scrolly/_cli/_introspect/elements.py +1 -1
  8. scrolly-0.2.4/scrolly/_cli/_introspect/_slides.py → scrolly-0.3.0/scrolly/_cli/_introspect/slides.py +1 -1
  9. scrolly-0.2.4/scrolly/_cli/_introspect/_snaps.py → scrolly-0.3.0/scrolly/_cli/_introspect/snaps.py +3 -3
  10. scrolly-0.2.4/scrolly/_cli/_introspect/_snapshot.py → scrolly-0.3.0/scrolly/_cli/_introspect/snapshot.py +1 -1
  11. scrolly-0.2.4/scrolly/_cli/_introspect/_timeline.py → scrolly-0.3.0/scrolly/_cli/_introspect/timeline.py +1 -1
  12. scrolly-0.3.0/scrolly/_cli/ai_help.py +124 -0
  13. scrolly-0.2.4/scrolly/_cli/_cli.py → scrolly-0.3.0/scrolly/_cli/cli.py +28 -13
  14. scrolly-0.3.0/scrolly/_cli/console.py +28 -0
  15. scrolly-0.2.4/scrolly/_cli/_errors.py → scrolly-0.3.0/scrolly/_cli/errors.py +2 -7
  16. scrolly-0.2.4/scrolly/_cli/_schema.py → scrolly-0.3.0/scrolly/_cli/schema.py +31 -18
  17. scrolly-0.3.0/scrolly/_shared/__init__.py +1 -0
  18. scrolly-0.3.0/scrolly/_shared/mime.py +53 -0
  19. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/model.py +1 -1
  20. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/parser.py +3 -1
  21. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/schema.py +1 -1
  22. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/__init__.py +1 -1
  23. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/_report.py +1 -7
  24. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/_validation_error.py +1 -1
  25. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E001.md +1 -1
  26. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E403.md +1 -1
  27. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E504.md +1 -1
  28. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E601.md +1 -1
  29. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/__init__.py +2 -2
  30. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/_bundler.py +10 -10
  31. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/assets.py +9 -15
  32. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/introspect.py +5 -4
  33. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/lint.py +17 -56
  34. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/assembler.py +9 -15
  35. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/assets/canvas.css +11 -11
  36. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/assets/canvas.js +95 -90
  37. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/fan.py +11 -10
  38. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/nav_data.py +5 -4
  39. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/zoom_control.py +1 -1
  40. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/_shared.py +37 -32
  41. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/markdown.py +1 -1
  42. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/_framework/animated_values.py +30 -28
  43. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/_framework/element.py +43 -34
  44. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/_framework/utils.py +3 -2
  45. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/slide.py +2 -2
  46. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/renderers/slide.py +11 -11
  47. {scrolly-0.2.4 → scrolly-0.3.0}/.gitignore +0 -0
  48. {scrolly-0.2.4 → scrolly-0.3.0}/LICENSE +0 -0
  49. {scrolly-0.2.4 → scrolly-0.3.0}/README.md +0 -0
  50. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/__init__.py +0 -0
  51. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/_cli/__init__.py +0 -0
  52. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/__init__.py +0 -0
  53. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/inference.py +0 -0
  54. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/introspect.py +0 -0
  55. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/validator.py +0 -0
  56. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/_catalog.py +0 -0
  57. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/_codes.py +0 -0
  58. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E002.md +0 -0
  59. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E003.md +0 -0
  60. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E004.md +0 -0
  61. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E005.md +0 -0
  62. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E006.md +0 -0
  63. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E007.md +0 -0
  64. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E008.md +0 -0
  65. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E009.md +0 -0
  66. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E010.md +0 -0
  67. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E011.md +0 -0
  68. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E012.md +0 -0
  69. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E101.md +0 -0
  70. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E102.md +0 -0
  71. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E103.md +0 -0
  72. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E201.md +0 -0
  73. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E202.md +0 -0
  74. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E203.md +0 -0
  75. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E204.md +0 -0
  76. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E205.md +0 -0
  77. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E206.md +0 -0
  78. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E207.md +0 -0
  79. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E299.md +0 -0
  80. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E301.md +0 -0
  81. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E302.md +0 -0
  82. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E303.md +0 -0
  83. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E304.md +0 -0
  84. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E305.md +0 -0
  85. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E306.md +0 -0
  86. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E307.md +0 -0
  87. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E308.md +0 -0
  88. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E401.md +0 -0
  89. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E402.md +0 -0
  90. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E501.md +0 -0
  91. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E502.md +0 -0
  92. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E503.md +0 -0
  93. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E505.md +0 -0
  94. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E602.md +0 -0
  95. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E603.md +0 -0
  96. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E701.md +0 -0
  97. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E702.md +0 -0
  98. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/__init__.py +0 -0
  99. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/loader.py +0 -0
  100. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/orchestrator.py +0 -0
  101. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/writer.py +0 -0
  102. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/__init__.py +0 -0
  103. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/assets/mermaid-LICENSE +0 -0
  104. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/assets/mermaid.min.js +0 -0
  105. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/bundled_assets.py +0 -0
  106. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/color.py +0 -0
  107. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/templates/index.html.j2 +0 -0
  108. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/__init__.py +0 -0
  109. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/__init__.py +0 -0
  110. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/ir.py +0 -0
  111. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/processor.py +0 -0
  112. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/registry.py +0 -0
  113. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/rendered.py +0 -0
  114. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/__init__.py +0 -0
  115. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/html.py +0 -0
  116. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/iframe.py +0 -0
  117. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/image.py +0 -0
  118. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/image_sequence.py +0 -0
  119. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/mermaid.py +0 -0
  120. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/html.py +0 -0
  121. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/introspect.py +0 -0
  122. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/__init__.py +0 -0
  123. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/_framework/__init__.py +0 -0
  124. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/processor.py +0 -0
  125. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/registry.py +0 -0
  126. {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/renderers/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrolly
3
- Version: 0.2.4
3
+ Version: 0.3.0
4
4
  Summary: CLI that compiles a JSON5 deck + slide files into a self-contained 2D-canvas HTML presentation.
5
5
  Project-URL: Homepage, https://scrolly.readthedocs.io/en/stable/
6
6
  Project-URL: Documentation, https://scrolly.readthedocs.io/en/stable/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrolly"
3
- version = "0.2.4"
3
+ version = "0.3.0"
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"
@@ -51,7 +51,7 @@ capture = [
51
51
  ]
52
52
 
53
53
  [project.scripts]
54
- scrolly = "scrolly._cli._cli:cli"
54
+ scrolly = "scrolly._cli.cli:cli"
55
55
 
56
56
  [project.urls]
57
57
  Homepage = "https://scrolly.readthedocs.io/en/stable/"
@@ -23,13 +23,13 @@ from __future__ import annotations
23
23
 
24
24
  import click
25
25
 
26
- from scrolly._cli._introspect._assets import assets_command
27
- from scrolly._cli._introspect._dom import dom_command
28
- from scrolly._cli._introspect._elements import elements_command
29
- from scrolly._cli._introspect._slides import slides_command
30
- from scrolly._cli._introspect._snaps import snaps_command
31
- from scrolly._cli._introspect._snapshot import snapshot_command
32
- from scrolly._cli._introspect._timeline import timeline_command
26
+ from scrolly._cli._introspect.assets import assets_command
27
+ from scrolly._cli._introspect.dom import dom_command
28
+ from scrolly._cli._introspect.elements import elements_command
29
+ from scrolly._cli._introspect.slides import slides_command
30
+ from scrolly._cli._introspect.snaps import snaps_command
31
+ from scrolly._cli._introspect.snapshot import snapshot_command
32
+ from scrolly._cli._introspect.timeline import timeline_command
33
33
 
34
34
 
35
35
  @click.group(name="introspect")
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import click
8
8
 
9
- from scrolly._cli._introspect._common import run_introspect_command
9
+ from scrolly._cli._introspect.common import run_introspect_command
10
10
  from scrolly.pipeline.introspect import assets_to_json
11
11
 
12
12
 
@@ -13,23 +13,37 @@ Every subcommand goes through ``run_introspect_command``, which:
13
13
  from __future__ import annotations
14
14
 
15
15
  import json
16
- import sys
17
16
  from collections.abc import Callable
18
17
  from pathlib import Path
19
18
 
20
19
  import click
21
- from rich.console import Console
22
20
 
21
+ from scrolly._cli.console import error_exit
23
22
  from scrolly.deck.model import Deck
24
23
  from scrolly.errors import ScrollyError
25
24
  from scrolly.pipeline import load_deck
26
25
  from scrolly.slide.ir import SlideIR
27
26
 
28
- _err_console = Console(stderr=True, highlight=False)
29
-
30
27
  ToJsonFn = Callable[[Deck, dict[str, SlideIR], tuple[str, ...] | None], dict]
31
28
 
32
29
 
30
+ def _load_deck_or_exit(deck_path: Path) -> tuple[Deck, dict[str, SlideIR]]:
31
+ """Load and validate a deck, or print the error to stderr and exit non-zero."""
32
+ try:
33
+ return load_deck(deck_path)
34
+ except ScrollyError as e:
35
+ error_exit(str(e))
36
+
37
+
38
+ def _emit_json(payload: dict, output_path: Path | None) -> None:
39
+ """Write an indented JSON payload to ``output_path``, or stdout when ``None``."""
40
+ rendered = json.dumps(payload, indent=2)
41
+ if output_path is not None:
42
+ output_path.write_text(rendered, encoding="utf-8")
43
+ else:
44
+ click.echo(rendered)
45
+
46
+
33
47
  def run_introspect_command(
34
48
  deck_path: Path,
35
49
  slide_ids: tuple[str, ...],
@@ -49,28 +63,16 @@ def run_introspect_command(
49
63
  SystemExit: Non-zero exit on validation gate failure or unknown
50
64
  ``--slide`` ids; the error message goes to stderr.
51
65
  """
52
- try:
53
- deck, slide_irs = load_deck(deck_path)
54
- except ScrollyError as e:
55
- _err_console.print(f"[red]error:[/red] {e}")
56
- sys.exit(1)
66
+ deck, slide_irs = _load_deck_or_exit(deck_path)
57
67
 
58
68
  if slide_ids:
59
69
  known = {s.id for s in deck.slides}
60
70
  unknown = [sid for sid in slide_ids if sid not in known]
61
71
  if unknown:
62
- _err_console.print(
63
- f"[red]error:[/red] unknown slide id(s): {', '.join(unknown)}. Known: {', '.join(sorted(known))}"
64
- )
65
- sys.exit(1)
72
+ error_exit(f"unknown slide id(s): {', '.join(unknown)}. Known: {', '.join(sorted(known))}")
66
73
 
67
74
  payload = to_json_fn(deck, slide_irs, slide_ids or None)
68
- rendered = json.dumps(payload, indent=2)
69
-
70
- if output_path is not None:
71
- output_path.write_text(rendered, encoding="utf-8")
72
- else:
73
- click.echo(rendered)
75
+ _emit_json(payload, output_path)
74
76
 
75
77
 
76
78
  def run_snapshot_command(
@@ -103,16 +105,11 @@ def run_snapshot_command(
103
105
  """
104
106
  from scrolly.slide.introspect import snapshot_to_json
105
107
 
106
- try:
107
- deck, slide_irs = load_deck(deck_path)
108
- except ScrollyError as e:
109
- _err_console.print(f"[red]error:[/red] {e}")
110
- sys.exit(1)
108
+ deck, slide_irs = _load_deck_or_exit(deck_path)
111
109
 
112
110
  known = {s.id for s in deck.slides}
113
111
  if slide_id not in known:
114
- _err_console.print(f"[red]error:[/red] unknown slide id: '{slide_id}'. Known: {', '.join(sorted(known))}")
115
- sys.exit(1)
112
+ error_exit(f"unknown slide id: '{slide_id}'. Known: {', '.join(sorted(known))}")
116
113
 
117
114
  ir = slide_irs[slide_id]
118
115
  scroll_range = ir.scroll_range
@@ -124,13 +121,7 @@ def run_snapshot_command(
124
121
  invalid.append((scroll, f"exceeds slide's scroll_range ({scroll_range})"))
125
122
  if invalid:
126
123
  lines = "\n".join(f" scroll={s}: {reason}" for s, reason in invalid)
127
- _err_console.print(f"[red]error:[/red] --scroll out-of-range for slide '{slide_id}':\n{lines}")
128
- sys.exit(1)
124
+ error_exit(f"--scroll out-of-range for slide '{slide_id}':\n{lines}")
129
125
 
130
126
  payload = snapshot_to_json(deck, slide_irs, slide_id, scrolls)
131
- rendered = json.dumps(payload, indent=2)
132
-
133
- if output_path is not None:
134
- output_path.write_text(rendered, encoding="utf-8")
135
- else:
136
- click.echo(rendered)
127
+ _emit_json(payload, output_path)
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import click
8
8
 
9
- from scrolly._cli._introspect._common import run_introspect_command
9
+ from scrolly._cli._introspect.common import run_introspect_command
10
10
  from scrolly.slide.introspect import dom_to_json
11
11
 
12
12
 
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import click
8
8
 
9
- from scrolly._cli._introspect._common import run_introspect_command
9
+ from scrolly._cli._introspect.common import run_introspect_command
10
10
  from scrolly.slide.introspect import element_tree_to_json
11
11
 
12
12
 
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import click
8
8
 
9
- from scrolly._cli._introspect._common import run_introspect_command
9
+ from scrolly._cli._introspect.common import run_introspect_command
10
10
  from scrolly.deck.introspect import slides_to_json
11
11
 
12
12
 
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import click
8
8
 
9
- from scrolly._cli._introspect._common import run_introspect_command
9
+ from scrolly._cli._introspect.common import run_introspect_command
10
10
  from scrolly.slide.introspect import snaps_to_json
11
11
 
12
12
 
@@ -26,10 +26,10 @@ from scrolly.slide.introspect import snaps_to_json
26
26
  help="Write JSON to this file instead of stdout.",
27
27
  )
28
28
  def snaps_command(deck_path: Path, slide_ids: tuple[str, ...], output_path: Path | None) -> None:
29
- """Per-slide snap positions: author-supplied + element-derived (image_sequence hold-centres).
29
+ """Per-slide snap positions: author-supplied + element-derived (image_sequence hold-centers).
30
30
 
31
31
  Author entries come from each slide's ``snap_positions`` field;
32
- derived entries come from ``ImageSequenceElement`` hold-centres
32
+ derived entries come from ``ImageSequenceElement`` hold-centers
33
33
  (one per frame). The ``merged`` list is the deduplicated + sorted
34
34
  union — what the canvas runtime actually uses.
35
35
 
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import click
8
8
 
9
- from scrolly._cli._introspect._common import run_snapshot_command
9
+ from scrolly._cli._introspect.common import run_snapshot_command
10
10
 
11
11
 
12
12
  @click.command(name="snapshot")
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
 
7
7
  import click
8
8
 
9
- from scrolly._cli._introspect._common import run_introspect_command
9
+ from scrolly._cli._introspect.common import run_introspect_command
10
10
  from scrolly.slide.introspect import timeline_to_json
11
11
 
12
12
 
@@ -0,0 +1,124 @@
1
+ """Builds the ``scrolly --help-for-ai-tools`` document.
2
+
3
+ One markdown document covering the entire CLI surface in a single read —
4
+ the command tree, every source-file and element schema, and every error
5
+ code — so an LLM agent gets the whole picture without round-tripping
6
+ through ``scrolly schema`` / ``scrolly errors`` per type and code. It is
7
+ a pure aggregator: every section is the output the individual commands
8
+ already produce, merged under markdown headers, never re-rendered.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Iterator
14
+
15
+ import click
16
+
17
+ from scrolly._cli.schema import element_schema_json, file_schema_json, file_type_names
18
+ from scrolly.errors import registered_codes
19
+ from scrolly.errors._catalog import load_body
20
+ from scrolly.slide import element_source_types
21
+
22
+
23
+ # ==================================================================================================
24
+ # Document assembly
25
+ # ==================================================================================================
26
+ def build_ai_help(root_command: click.Command, version: str) -> str:
27
+ """Render the whole CLI reference as one self-contained markdown document.
28
+
29
+ Args:
30
+ root_command: The top-level ``scrolly`` Click group, walked for the command tree.
31
+ version: The scrolly version string, shown in the document header.
32
+
33
+ Returns:
34
+ A single markdown document: header, command tree, file schemas,
35
+ element schemas, and error codes — heading levels nested so the
36
+ whole document forms one consistent hierarchy.
37
+ """
38
+ sections = [
39
+ _header(version),
40
+ _commands_section(root_command),
41
+ _file_schemas_section(),
42
+ _element_schemas_section(),
43
+ _error_codes_section(),
44
+ ]
45
+ return "\n\n".join(sections) + "\n"
46
+
47
+
48
+ def _header(version: str) -> str:
49
+ """Render the document title and one-paragraph orientation."""
50
+ return (
51
+ f"# scrolly {version} — CLI reference for AI tools\n\n"
52
+ "The complete scrolly command-line surface in one document: every command, "
53
+ "every source-file and element schema, and every error code. Generated from "
54
+ "the installed scrolly, so it matches this version exactly."
55
+ )
56
+
57
+
58
+ # ==================================================================================================
59
+ # Sections
60
+ # ==================================================================================================
61
+ def _commands_section(root_command: click.Command) -> str:
62
+ """Render every command's help text, walking the full command tree."""
63
+ blocks = ["## Commands"]
64
+ for path, help_text in _walk_commands(root_command, "scrolly", None):
65
+ blocks.append(f"### `{path}`\n\n```\n{help_text.rstrip()}\n```")
66
+ return "\n\n".join(blocks)
67
+
68
+
69
+ def _file_schemas_section() -> str:
70
+ """Render the JSON Schema for every source-file type (deck, slide)."""
71
+ blocks = ["## File schemas"]
72
+ for name in file_type_names():
73
+ blocks.append(f"### `{name}`\n\n```json\n{file_schema_json(name)}\n```")
74
+ return "\n\n".join(blocks)
75
+
76
+
77
+ def _element_schemas_section() -> str:
78
+ """Render the JSON Schema for every slide-element type."""
79
+ blocks = ["## Element schemas"]
80
+ for key in element_source_types():
81
+ blocks.append(f"### `{key}`\n\n```json\n{element_schema_json(key)}\n```")
82
+ return "\n\n".join(blocks)
83
+
84
+
85
+ def _error_codes_section() -> str:
86
+ """Render the catalog entry for every registered error code.
87
+
88
+ Each catalog body is verbatim markdown starting at its own ``# E…``
89
+ heading; demoting by two levels nests every entry under this section.
90
+ """
91
+ blocks = ["## Error codes"]
92
+ for code in sorted(registered_codes()):
93
+ blocks.append(_demote_headings(load_body(code).rstrip(), 2))
94
+ return "\n\n".join(blocks)
95
+
96
+
97
+ # ==================================================================================================
98
+ # Helpers
99
+ # ==================================================================================================
100
+ def _walk_commands(
101
+ command: click.Command, info_name: str, parent_ctx: click.Context | None
102
+ ) -> Iterator[tuple[str, str]]:
103
+ """Yield ``(command_path, help_text)`` for a command and all its subcommands, depth-first."""
104
+ ctx = click.Context(command, info_name=info_name, parent=parent_ctx)
105
+ yield ctx.command_path, command.get_help(ctx)
106
+ if isinstance(command, click.Group):
107
+ for name in command.list_commands(ctx):
108
+ sub = command.get_command(ctx, name)
109
+ if sub is None or sub.hidden:
110
+ continue
111
+ yield from _walk_commands(sub, name, ctx)
112
+
113
+
114
+ def _demote_headings(markdown: str, levels: int) -> str:
115
+ """Deepen every ATX heading by ``levels`` ``#``, leaving fenced code blocks untouched."""
116
+ out = []
117
+ in_fence = False
118
+ for line in markdown.splitlines():
119
+ if line.lstrip().startswith("```"):
120
+ in_fence = not in_fence
121
+ if not in_fence and line.startswith("#"):
122
+ line = "#" * levels + line
123
+ out.append(line)
124
+ return "\n".join(out)
@@ -3,21 +3,38 @@ import sys
3
3
  from pathlib import Path
4
4
 
5
5
  import click
6
- from rich.console import Console
7
6
 
8
7
  from scrolly import __version__
9
- from scrolly._cli._errors import errors_command
10
8
  from scrolly._cli._introspect import introspect
11
- from scrolly._cli._schema import schema
9
+ from scrolly._cli.console import err_console, error_exit, print_error
10
+ from scrolly._cli.errors import errors_command
11
+ from scrolly._cli.schema import schema
12
+ from scrolly.deck import Deck
12
13
  from scrolly.errors import ScrollyError, ValidationError
13
14
  from scrolly.pipeline import build_deck, load_deck
14
15
  from scrolly.pipeline.lint import lint_deck
15
16
 
16
- _err_console = Console(stderr=True, highlight=False)
17
+
18
+ def _emit_ai_help(ctx: click.Context, param: click.Parameter, value: bool) -> None:
19
+ """Eager ``--help-for-ai-tools`` callback: print the full CLI reference and exit."""
20
+ if not value or ctx.resilient_parsing:
21
+ return
22
+ from scrolly._cli.ai_help import build_ai_help
23
+
24
+ click.echo(build_ai_help(ctx.find_root().command, __version__))
25
+ ctx.exit()
17
26
 
18
27
 
19
28
  @click.group()
20
29
  @click.version_option(__version__, prog_name="scrolly")
30
+ @click.option(
31
+ "--help-for-ai-tools",
32
+ is_flag=True,
33
+ is_eager=True,
34
+ expose_value=False,
35
+ callback=_emit_ai_help,
36
+ help="Print the entire CLI reference (commands, schemas, error codes) as one markdown document for AI agents.",
37
+ )
21
38
  def cli() -> None:
22
39
  """scrolly — compile a JSON5 deck into a self-contained 2D-canvas HTML presentation."""
23
40
 
@@ -74,8 +91,7 @@ def build(
74
91
  offline=offline,
75
92
  )
76
93
  except ScrollyError as e:
77
- _err_console.print(f"[red]error:[/red] {e}")
78
- sys.exit(1)
94
+ error_exit(str(e))
79
95
 
80
96
  if strict:
81
97
  _report_diagnostics(deck)
@@ -100,7 +116,7 @@ def validate(deck_path: Path, strict: bool, as_json: bool) -> None:
100
116
  if as_json:
101
117
  click.echo(json.dumps({"ok": False, "errors": [_error_to_dict(e)]}, indent=2))
102
118
  else:
103
- _err_console.print(f"[red]error:[/red] {e}")
119
+ print_error(str(e))
104
120
  sys.exit(1)
105
121
 
106
122
  if strict:
@@ -113,7 +129,7 @@ def validate(deck_path: Path, strict: bool, as_json: bool) -> None:
113
129
 
114
130
 
115
131
  def _error_to_dict(err: ScrollyError) -> dict:
116
- """Serialise a ``ScrollyError`` for JSON output."""
132
+ """Serialize a ``ScrollyError`` for JSON output."""
117
133
  if isinstance(err, ValidationError):
118
134
  return {
119
135
  "code": err.code,
@@ -126,11 +142,11 @@ def _error_to_dict(err: ScrollyError) -> dict:
126
142
  return {"code": None, "message": str(err)}
127
143
 
128
144
 
129
- def _report_diagnostics(deck) -> None:
145
+ def _report_diagnostics(deck: Deck) -> None:
130
146
  """Run lint checks and print any diagnostics to stderr."""
131
147
  diagnostics = lint_deck(deck)
132
148
  for d in diagnostics:
133
- _err_console.print(f"[yellow]{d.level}:[/yellow] {d.location}: {d.message}")
149
+ err_console.print(f"[yellow]{d.level}:[/yellow] {d.location}: {d.message}")
134
150
 
135
151
 
136
152
  _INIT_DECK = """\
@@ -145,7 +161,7 @@ _INIT_DECK = """\
145
161
 
146
162
  _INIT_SLIDE = """\
147
163
  {
148
- title: "My Deck",
164
+ title: "Intro",
149
165
  elements: [
150
166
  { markdown: "# My Deck\\n\\nWelcome to your new presentation." },
151
167
  ],
@@ -158,8 +174,7 @@ _INIT_SLIDE = """\
158
174
  def init(dir_path: Path) -> None:
159
175
  """Scaffold a minimal deck in DIR_PATH."""
160
176
  if dir_path.exists() and any(dir_path.iterdir()):
161
- _err_console.print(f"[red]error:[/red] directory is not empty: {dir_path}")
162
- sys.exit(1)
177
+ error_exit(f"directory is not empty: {dir_path}")
163
178
 
164
179
  slides_dir = dir_path / "slides"
165
180
  slides_dir.mkdir(parents=True, exist_ok=True)
@@ -0,0 +1,28 @@
1
+ """Shared stderr console and error-reporting helpers for the CLI commands.
2
+
3
+ Every command surfaces failures the same way — a red ``error:`` line on a
4
+ stderr-only Rich console, followed (for terminal failures) by a non-zero
5
+ exit. Centralizing the console instance and that idiom here keeps the
6
+ formatting identical across ``build``, ``validate``, ``schema``,
7
+ ``errors``, and the ``introspect`` subcommands.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from typing import NoReturn
14
+
15
+ from rich.console import Console
16
+
17
+ err_console = Console(stderr=True, highlight=False)
18
+
19
+
20
+ def print_error(message: str) -> None:
21
+ """Print a red ``error:`` line to stderr without exiting."""
22
+ err_console.print(f"[red]error:[/red] {message}")
23
+
24
+
25
+ def error_exit(message: str) -> NoReturn:
26
+ """Print a red ``error:`` line to stderr and exit non-zero."""
27
+ print_error(message)
28
+ sys.exit(1)
@@ -10,16 +10,12 @@ Three forms, mirroring the progressive-disclosure pattern used by
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
- import sys
14
-
15
13
  import click
16
- from rich.console import Console
17
14
 
15
+ from scrolly._cli.console import error_exit
18
16
  from scrolly.errors import is_registered_code, registered_codes
19
17
  from scrolly.errors._catalog import load_body, load_summary
20
18
 
21
- _err_console = Console(stderr=True, highlight=False)
22
-
23
19
 
24
20
  @click.command(name="errors")
25
21
  @click.argument("code", required=False)
@@ -48,7 +44,6 @@ def errors_command(code: str | None, list_codes: bool) -> None:
48
44
  return
49
45
 
50
46
  if not is_registered_code(code):
51
- _err_console.print(f"[red]error:[/red] unknown error code '{code}'")
52
- sys.exit(1)
47
+ error_exit(f"unknown error code '{code}'")
53
48
 
54
49
  click.echo(load_body(code))
@@ -18,12 +18,10 @@ specific *resolved deck instance* — different surfaces, kept distinct.
18
18
  from __future__ import annotations
19
19
 
20
20
  import json
21
- import sys
22
21
 
23
22
  import click
24
- from rich.console import Console
25
23
 
26
- _err_console = Console(stderr=True, highlight=False)
24
+ from scrolly._cli.console import error_exit
27
25
 
28
26
  # Width of the name / suffix columns in the human-readable index, matching
29
27
  # the alignment used by ``scrolly errors``.
@@ -53,9 +51,9 @@ def schema(ctx: click.Context) -> None:
53
51
  _print_element_index()
54
52
 
55
53
 
56
- # --------------------------------------------------------------------------
54
+ # ==================================================================================================
57
55
  # Subcommands
58
- # --------------------------------------------------------------------------
56
+ # ==================================================================================================
59
57
  @schema.command(name="file")
60
58
  @click.argument("type_name", required=False)
61
59
  @click.option(
@@ -72,7 +70,7 @@ def schema_file(type_name: str | None, list_types: bool) -> None:
72
70
  scrolly schema file <type> → JSON Schema for <type>
73
71
  scrolly schema file --list-types → bare type names, one per line (agent / scripting)
74
72
  """
75
- names = _file_type_names()
73
+ names = file_type_names()
76
74
 
77
75
  if list_types:
78
76
  for name in names:
@@ -83,11 +81,10 @@ def schema_file(type_name: str | None, list_types: bool) -> None:
83
81
  _print_file_index()
84
82
  return
85
83
 
86
- schema_dict = _file_schema(type_name)
87
- if schema_dict is None:
88
- _err_console.print(f"[red]error:[/red] unknown file type '{type_name}' (known: {', '.join(names)})")
89
- sys.exit(1)
90
- click.echo(json.dumps(schema_dict, indent=2))
84
+ schema_text = file_schema_json(type_name)
85
+ if schema_text is None:
86
+ error_exit(f"unknown file type '{type_name}' (known: {', '.join(names)})")
87
+ click.echo(schema_text)
91
88
 
92
89
 
93
90
  @schema.command(name="element")
@@ -119,16 +116,16 @@ def schema_element(type_name: str | None, list_types: bool) -> None:
119
116
  _print_element_index()
120
117
  return
121
118
 
122
- if type_name not in elements:
123
- _err_console.print(f"[red]error:[/red] unknown element type '{type_name}' (known: {', '.join(elements)})")
124
- sys.exit(1)
125
- click.echo(json.dumps(elements[type_name].source_schema(), indent=2))
119
+ schema_text = element_schema_json(type_name)
120
+ if schema_text is None:
121
+ error_exit(f"unknown element type '{type_name}' (known: {', '.join(elements)})")
122
+ click.echo(schema_text)
126
123
 
127
124
 
128
- # --------------------------------------------------------------------------
125
+ # ==================================================================================================
129
126
  # Schema lookup + index rendering
130
- # --------------------------------------------------------------------------
131
- def _file_type_names() -> list[str]:
127
+ # ==================================================================================================
128
+ def file_type_names() -> list[str]:
132
129
  """Return the sorted source-file type names (deck + registered slide types)."""
133
130
  from scrolly.slide import registered_ir_types
134
131
 
@@ -148,6 +145,22 @@ def _file_schema(type_name: str) -> dict | None:
148
145
  return None
149
146
 
150
147
 
148
+ def file_schema_json(type_name: str) -> str | None:
149
+ """Render a source-file type's JSON Schema as indented JSON text, or ``None`` if unknown."""
150
+ schema_dict = _file_schema(type_name)
151
+ return None if schema_dict is None else json.dumps(schema_dict, indent=2)
152
+
153
+
154
+ def element_schema_json(type_name: str) -> str | None:
155
+ """Render an element type's JSON Schema as indented JSON text, or ``None`` if unknown."""
156
+ from scrolly.slide import element_source_types
157
+
158
+ elements = element_source_types()
159
+ if type_name not in elements:
160
+ return None
161
+ return json.dumps(elements[type_name].source_schema(), indent=2)
162
+
163
+
151
164
  def _print_file_index() -> None:
152
165
  """Print the human-readable index of source-file schemas."""
153
166
  from scrolly.slide import registered_ir_types
@@ -0,0 +1 @@
1
+ """Layer-neutral primitives shared across the deck / slide / render / pipeline layers."""