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.
- {scrolly-0.2.4 → scrolly-0.3.0}/PKG-INFO +1 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/pyproject.toml +2 -2
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/_cli/_introspect/__init__.py +7 -7
- scrolly-0.2.4/scrolly/_cli/_introspect/_assets.py → scrolly-0.3.0/scrolly/_cli/_introspect/assets.py +1 -1
- scrolly-0.2.4/scrolly/_cli/_introspect/_common.py → scrolly-0.3.0/scrolly/_cli/_introspect/common.py +25 -34
- scrolly-0.2.4/scrolly/_cli/_introspect/_dom.py → scrolly-0.3.0/scrolly/_cli/_introspect/dom.py +1 -1
- scrolly-0.2.4/scrolly/_cli/_introspect/_elements.py → scrolly-0.3.0/scrolly/_cli/_introspect/elements.py +1 -1
- scrolly-0.2.4/scrolly/_cli/_introspect/_slides.py → scrolly-0.3.0/scrolly/_cli/_introspect/slides.py +1 -1
- scrolly-0.2.4/scrolly/_cli/_introspect/_snaps.py → scrolly-0.3.0/scrolly/_cli/_introspect/snaps.py +3 -3
- scrolly-0.2.4/scrolly/_cli/_introspect/_snapshot.py → scrolly-0.3.0/scrolly/_cli/_introspect/snapshot.py +1 -1
- scrolly-0.2.4/scrolly/_cli/_introspect/_timeline.py → scrolly-0.3.0/scrolly/_cli/_introspect/timeline.py +1 -1
- scrolly-0.3.0/scrolly/_cli/ai_help.py +124 -0
- scrolly-0.2.4/scrolly/_cli/_cli.py → scrolly-0.3.0/scrolly/_cli/cli.py +28 -13
- scrolly-0.3.0/scrolly/_cli/console.py +28 -0
- scrolly-0.2.4/scrolly/_cli/_errors.py → scrolly-0.3.0/scrolly/_cli/errors.py +2 -7
- scrolly-0.2.4/scrolly/_cli/_schema.py → scrolly-0.3.0/scrolly/_cli/schema.py +31 -18
- scrolly-0.3.0/scrolly/_shared/__init__.py +1 -0
- scrolly-0.3.0/scrolly/_shared/mime.py +53 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/model.py +1 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/parser.py +3 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/schema.py +1 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/__init__.py +1 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/_report.py +1 -7
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/_validation_error.py +1 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E001.md +1 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E403.md +1 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E504.md +1 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E601.md +1 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/__init__.py +2 -2
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/_bundler.py +10 -10
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/assets.py +9 -15
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/introspect.py +5 -4
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/lint.py +17 -56
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/assembler.py +9 -15
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/assets/canvas.css +11 -11
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/assets/canvas.js +95 -90
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/fan.py +11 -10
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/nav_data.py +5 -4
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/zoom_control.py +1 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/_shared.py +37 -32
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/markdown.py +1 -1
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/_framework/animated_values.py +30 -28
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/_framework/element.py +43 -34
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/_framework/utils.py +3 -2
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/slide.py +2 -2
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/renderers/slide.py +11 -11
- {scrolly-0.2.4 → scrolly-0.3.0}/.gitignore +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/LICENSE +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/README.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/__init__.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/_cli/__init__.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/__init__.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/inference.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/introspect.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/deck/validator.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/_catalog.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/_codes.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E002.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E003.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E004.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E005.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E006.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E007.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E008.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E009.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E010.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E011.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E012.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E101.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E102.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E103.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E201.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E202.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E203.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E204.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E205.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E206.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E207.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E299.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E301.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E302.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E303.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E304.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E305.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E306.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E307.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E308.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E401.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E402.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E501.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E502.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E503.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E505.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E602.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E603.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E701.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/errors/catalog/E702.md +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/__init__.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/loader.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/orchestrator.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/pipeline/writer.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/__init__.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/assets/mermaid-LICENSE +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/assets/mermaid.min.js +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/bundled_assets.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/color.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/render/templates/index.html.j2 +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/__init__.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/__init__.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/ir.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/processor.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/registry.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/rendered.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/__init__.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/html.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/iframe.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/image.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/image_sequence.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/element_ir/renderers/mermaid.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/html.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/introspect.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/__init__.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/ir/_framework/__init__.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/processor.py +0 -0
- {scrolly-0.2.4 → scrolly-0.3.0}/scrolly/slide/registry.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
27
|
-
from scrolly._cli._introspect.
|
|
28
|
-
from scrolly._cli._introspect.
|
|
29
|
-
from scrolly._cli._introspect.
|
|
30
|
-
from scrolly._cli._introspect.
|
|
31
|
-
from scrolly._cli._introspect.
|
|
32
|
-
from scrolly._cli._introspect.
|
|
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")
|
scrolly-0.2.4/scrolly/_cli/_introspect/_common.py → scrolly-0.3.0/scrolly/_cli/_introspect/common.py
RENAMED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
scrolly-0.2.4/scrolly/_cli/_introspect/_snaps.py → scrolly-0.3.0/scrolly/_cli/_introspect/snaps.py
RENAMED
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
import click
|
|
8
8
|
|
|
9
|
-
from scrolly._cli._introspect.
|
|
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-
|
|
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-
|
|
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
|
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
87
|
-
if
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
click.echo(
|
|
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
|
|
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."""
|