slidesync 0.1.0__tar.gz → 0.2.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.
- {slidesync-0.1.0/slidesync.egg-info → slidesync-0.2.0}/PKG-INFO +17 -4
- {slidesync-0.1.0 → slidesync-0.2.0}/README.md +16 -3
- {slidesync-0.1.0 → slidesync-0.2.0}/pyproject.toml +2 -2
- {slidesync-0.1.0 → slidesync-0.2.0}/slidesync/__init__.py +1 -1
- {slidesync-0.1.0 → slidesync-0.2.0}/slidesync/_sync.py +41 -3
- {slidesync-0.1.0 → slidesync-0.2.0/slidesync.egg-info}/PKG-INFO +17 -4
- {slidesync-0.1.0 → slidesync-0.2.0}/slidesync.egg-info/SOURCES.txt +1 -0
- slidesync-0.2.0/tests/test_comment_preservation.py +132 -0
- {slidesync-0.1.0 → slidesync-0.2.0}/LICENSE +0 -0
- {slidesync-0.1.0 → slidesync-0.2.0}/requirements.txt +0 -0
- {slidesync-0.1.0 → slidesync-0.2.0}/setup.cfg +0 -0
- {slidesync-0.1.0 → slidesync-0.2.0}/slidesync.egg-info/dependency_links.txt +0 -0
- {slidesync-0.1.0 → slidesync-0.2.0}/slidesync.egg-info/entry_points.txt +0 -0
- {slidesync-0.1.0 → slidesync-0.2.0}/slidesync.egg-info/requires.txt +0 -0
- {slidesync-0.1.0 → slidesync-0.2.0}/slidesync.egg-info/top_level.txt +0 -0
- {slidesync-0.1.0 → slidesync-0.2.0}/tests/test_markdown.py +0 -0
- {slidesync-0.1.0 → slidesync-0.2.0}/tests/test_pull.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slidesync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Bidirectional sync between a Slidev markdown deck and Google Slides as native, editable objects
|
|
5
5
|
Author-email: Daniel Hails <slidesync@hails.info>
|
|
6
6
|
License: MIT
|
|
@@ -22,7 +22,7 @@ Bidirectional sync between a [Slidev](https://sli.dev) markdown deck and **Googl
|
|
|
22
22
|
Slides** — as native, editable objects (title/body/bullets/tables/positioned
|
|
23
23
|
images, brand-styled text boxes), not pasted screenshots.
|
|
24
24
|
|
|
25
|
-
Version: 0.
|
|
25
|
+
Version: 0.2.0
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
uvx slidesync --help # run without installing
|
|
@@ -84,7 +84,8 @@ is a no-op. Diff per run: identical hash → skip; same key, new content → rep
|
|
|
84
84
|
new key → create. Removed slides are **kept** unless `--prune`. **Only `s2g_`
|
|
85
85
|
slides are ever touched** — hand-authored slides are invisible to the sync. A
|
|
86
86
|
hidden `<!-- s2g {...} -->` marker in speaker notes carries the human id, image
|
|
87
|
-
path, and template
|
|
87
|
+
path, template vars — and, for template slides, the authored body markdown
|
|
88
|
+
(base64) — so `pull` recovers the source verbatim.
|
|
88
89
|
|
|
89
90
|
## Markdown dialect
|
|
90
91
|
|
|
@@ -96,7 +97,11 @@ may have its own frontmatter (`id:`, `template:`, `layout:`).
|
|
|
96
97
|
`**bold**` / `*italic*` / `` `code` `` / `[link](url)`. GFM tables.
|
|
97
98
|
`` images (uploaded to Drive; `alt` becomes the accessibility
|
|
98
99
|
description, round-tripped on pull). Blank lines preserved as spacing.
|
|
99
|
-
`<!-- notes -->` become speaker notes
|
|
100
|
+
`<!-- notes -->` become speaker notes — and round-trip as **comments, in
|
|
101
|
+
place**: template slides carry their authored source in the marker, so `pull`
|
|
102
|
+
re-emits each comment where it was written instead of one merged trailing
|
|
103
|
+
blob. Speaker notes edited live in Slides come back as one extra trailing
|
|
104
|
+
comment.
|
|
100
105
|
- **Internal links:** `[text](#slide-id)` becomes a native Slides link to the
|
|
101
106
|
slide whose `id:` (or title slug) is `slide-id`.
|
|
102
107
|
|
|
@@ -105,6 +110,9 @@ may have its own frontmatter (`id:`, `template:`, `layout:`).
|
|
|
105
110
|
Select per slide via `template:` — native styled boxes, no in-deck templates:
|
|
106
111
|
`dark`/`title`, `appendix`, `question`/`label`, `topic`, `content`,
|
|
107
112
|
`graph`/`full` (single full-bleed image), `prompt`/`code` (verbatim monospace).
|
|
113
|
+
Title cards (`dark`/`title`/`appendix`) render body lines as a small dimmed
|
|
114
|
+
**byline** beneath the headline (e.g. `Project · Presenter`) — they still have
|
|
115
|
+
no linkable body region.
|
|
108
116
|
Slides with no `template:` fall back to a generative path (section /
|
|
109
117
|
title+body / table / image) that also brands the background + IBM Plex.
|
|
110
118
|
|
|
@@ -131,6 +139,11 @@ Releases publish to PyPI via Trusted Publishing (OIDC) on a `v*.*.*` tag — see
|
|
|
131
139
|
— this is a content mapper, not a CSS renderer.
|
|
132
140
|
- On `pull`, the slide model holds a single image, so a slide with multiple
|
|
133
141
|
images keeps the first; image `contentUrl`s from foreign decks are ephemeral.
|
|
142
|
+
- Verbatim-source markers are seeded at push time, so comment preservation
|
|
143
|
+
applies from the first push with v0.2+ (older slides re-render once: the
|
|
144
|
+
content hash is now over the authored source). Generative-path slides (no
|
|
145
|
+
`template:`) still merge comments into a single trailing comment on pull,
|
|
146
|
+
since their live Slides edits — not the marker — are the source of truth.
|
|
134
147
|
|
|
135
148
|
## License
|
|
136
149
|
|
|
@@ -4,7 +4,7 @@ Bidirectional sync between a [Slidev](https://sli.dev) markdown deck and **Googl
|
|
|
4
4
|
Slides** — as native, editable objects (title/body/bullets/tables/positioned
|
|
5
5
|
images, brand-styled text boxes), not pasted screenshots.
|
|
6
6
|
|
|
7
|
-
Version: 0.
|
|
7
|
+
Version: 0.2.0
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
uvx slidesync --help # run without installing
|
|
@@ -66,7 +66,8 @@ is a no-op. Diff per run: identical hash → skip; same key, new content → rep
|
|
|
66
66
|
new key → create. Removed slides are **kept** unless `--prune`. **Only `s2g_`
|
|
67
67
|
slides are ever touched** — hand-authored slides are invisible to the sync. A
|
|
68
68
|
hidden `<!-- s2g {...} -->` marker in speaker notes carries the human id, image
|
|
69
|
-
path, and template
|
|
69
|
+
path, template vars — and, for template slides, the authored body markdown
|
|
70
|
+
(base64) — so `pull` recovers the source verbatim.
|
|
70
71
|
|
|
71
72
|
## Markdown dialect
|
|
72
73
|
|
|
@@ -78,7 +79,11 @@ may have its own frontmatter (`id:`, `template:`, `layout:`).
|
|
|
78
79
|
`**bold**` / `*italic*` / `` `code` `` / `[link](url)`. GFM tables.
|
|
79
80
|
`` images (uploaded to Drive; `alt` becomes the accessibility
|
|
80
81
|
description, round-tripped on pull). Blank lines preserved as spacing.
|
|
81
|
-
`<!-- notes -->` become speaker notes
|
|
82
|
+
`<!-- notes -->` become speaker notes — and round-trip as **comments, in
|
|
83
|
+
place**: template slides carry their authored source in the marker, so `pull`
|
|
84
|
+
re-emits each comment where it was written instead of one merged trailing
|
|
85
|
+
blob. Speaker notes edited live in Slides come back as one extra trailing
|
|
86
|
+
comment.
|
|
82
87
|
- **Internal links:** `[text](#slide-id)` becomes a native Slides link to the
|
|
83
88
|
slide whose `id:` (or title slug) is `slide-id`.
|
|
84
89
|
|
|
@@ -87,6 +92,9 @@ may have its own frontmatter (`id:`, `template:`, `layout:`).
|
|
|
87
92
|
Select per slide via `template:` — native styled boxes, no in-deck templates:
|
|
88
93
|
`dark`/`title`, `appendix`, `question`/`label`, `topic`, `content`,
|
|
89
94
|
`graph`/`full` (single full-bleed image), `prompt`/`code` (verbatim monospace).
|
|
95
|
+
Title cards (`dark`/`title`/`appendix`) render body lines as a small dimmed
|
|
96
|
+
**byline** beneath the headline (e.g. `Project · Presenter`) — they still have
|
|
97
|
+
no linkable body region.
|
|
90
98
|
Slides with no `template:` fall back to a generative path (section /
|
|
91
99
|
title+body / table / image) that also brands the background + IBM Plex.
|
|
92
100
|
|
|
@@ -113,6 +121,11 @@ Releases publish to PyPI via Trusted Publishing (OIDC) on a `v*.*.*` tag — see
|
|
|
113
121
|
— this is a content mapper, not a CSS renderer.
|
|
114
122
|
- On `pull`, the slide model holds a single image, so a slide with multiple
|
|
115
123
|
images keeps the first; image `contentUrl`s from foreign decks are ephemeral.
|
|
124
|
+
- Verbatim-source markers are seeded at push time, so comment preservation
|
|
125
|
+
applies from the first push with v0.2+ (older slides re-render once: the
|
|
126
|
+
content hash is now over the authored source). Generative-path slides (no
|
|
127
|
+
`template:`) still merge comments into a single trailing comment on pull,
|
|
128
|
+
since their live Slides edits — not the marker — are the source of truth.
|
|
116
129
|
|
|
117
130
|
## License
|
|
118
131
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "slidesync"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
authors = [
|
|
5
5
|
{ name = "Daniel Hails", email = "slidesync@hails.info" },
|
|
6
6
|
]
|
|
@@ -32,7 +32,7 @@ include = ["slidesync*"]
|
|
|
32
32
|
dev = ["pytest>=8"]
|
|
33
33
|
|
|
34
34
|
[tool.bumpver]
|
|
35
|
-
current_version = "0.
|
|
35
|
+
current_version = "0.2.0"
|
|
36
36
|
version_pattern = "MAJOR.MINOR.PATCH"
|
|
37
37
|
commit_message = "bump version {old_version} -> {new_version}"
|
|
38
38
|
commit = true
|
|
@@ -21,7 +21,9 @@ Idempotent sync (upsert), never a blind append:
|
|
|
21
21
|
create. Removed slides are kept by default (`--prune` to delete).
|
|
22
22
|
- Only `s2g_`-prefixed slides are ever touched; hand-authored slides are
|
|
23
23
|
invisible to the sync. A tiny `<!-- s2g {...} -->` marker in speaker notes
|
|
24
|
-
carries the human id + image path
|
|
24
|
+
carries the human id + image path — and, for template slides, the authored
|
|
25
|
+
body markdown (base64) — so `pull` recovers the source verbatim: comments
|
|
26
|
+
stay comments, in place, instead of collapsing into one speaker-notes blob.
|
|
25
27
|
|
|
26
28
|
Usage:
|
|
27
29
|
bin/slidesync.py push deck.slidev.md --deck <id> [--anchor <slideId>] [--prune]
|
|
@@ -33,6 +35,7 @@ Usage:
|
|
|
33
35
|
from __future__ import annotations
|
|
34
36
|
|
|
35
37
|
import argparse
|
|
38
|
+
import base64
|
|
36
39
|
import hashlib
|
|
37
40
|
import html
|
|
38
41
|
import json
|
|
@@ -81,6 +84,7 @@ INK = {"red": 0.011764706, "green": 0.02745098, "blue": 0.07058824} # #03070F
|
|
|
81
84
|
BODY_INK = {"red": 0.11764706, "green": 0.1254902, "blue": 0.14117648} # #1E2024
|
|
82
85
|
PAPER = {"red": 0.98039216, "green": 0.98039216, "blue": 0.98039216} # #FAFAFA
|
|
83
86
|
WHITE = {"red": 1.0, "green": 1.0, "blue": 1.0} # #FFFFFF
|
|
87
|
+
MUTED = {"red": 0.62, "green": 0.65, "blue": 0.69} # dimmed byline on dark cards
|
|
84
88
|
LIGHT_BG, DARK_BG = PAPER, BODY_INK
|
|
85
89
|
|
|
86
90
|
# ---------------------------------------------------------------------------
|
|
@@ -187,6 +191,7 @@ class Slide:
|
|
|
187
191
|
vars: dict = field(default_factory=dict) # extra frontmatter -> {{token}} values
|
|
188
192
|
custom: str | None = None # ```gslides``` literal Slides API requests (JSON)
|
|
189
193
|
verbatim: str | None = None # ``` ``` fenced body for prompt/code slides
|
|
194
|
+
src: str | None = None # body markdown as authored (comments in place)
|
|
190
195
|
key_hash: str = ""
|
|
191
196
|
content_hash: str = ""
|
|
192
197
|
object_id: str = ""
|
|
@@ -282,6 +287,7 @@ def build_slides(chunks: list[tuple[dict, str]]) -> list[Slide]:
|
|
|
282
287
|
|
|
283
288
|
|
|
284
289
|
def build_slide(meta: dict, body: str, index: int) -> Slide:
|
|
290
|
+
authored = body.strip("\n")
|
|
285
291
|
custom, body = _extract_custom(body)
|
|
286
292
|
verbatim = None
|
|
287
293
|
if (meta.get("template") or "").lower() in ("prompt", "code"):
|
|
@@ -299,6 +305,8 @@ def build_slide(meta: dict, body: str, index: int) -> Slide:
|
|
|
299
305
|
slide.vars = {k: v for k, v in meta.items() if k not in RESERVED_KEYS}
|
|
300
306
|
slide.custom = custom
|
|
301
307
|
slide.verbatim = verbatim
|
|
308
|
+
if custom is None: # custom slides are pull-authoritative; their source goes stale
|
|
309
|
+
slide.src = authored
|
|
302
310
|
return _finalize(slide)
|
|
303
311
|
|
|
304
312
|
|
|
@@ -467,6 +475,18 @@ def to_slidev(slide: Slide, include_id: bool = True) -> str:
|
|
|
467
475
|
out = []
|
|
468
476
|
if fm:
|
|
469
477
|
out += ["---"] + [f"{k}: {v}" for k, v in fm.items()] + ["---"]
|
|
478
|
+
if slide.src is not None:
|
|
479
|
+
# Authored source is the canonical render: comments stay comments, in
|
|
480
|
+
# place. Speaker notes are emitted only when they no longer match the
|
|
481
|
+
# authored comments (i.e. someone edited the notes pane in Slides) —
|
|
482
|
+
# compared whitespace-normalised, since the notes shape flattens
|
|
483
|
+
# paragraphs when read back.
|
|
484
|
+
out.append(slide.src)
|
|
485
|
+
extra = slide.notes.strip()
|
|
486
|
+
if extra and " ".join(extra.split()) != \
|
|
487
|
+
" ".join(_extract_notes(slide.src).split()):
|
|
488
|
+
out.append(f"<!-- {extra} -->")
|
|
489
|
+
return "\n".join(out).strip() + "\n"
|
|
470
490
|
if slide.custom is not None:
|
|
471
491
|
out += ["```gslides", slide.custom, "```"]
|
|
472
492
|
if slide.notes:
|
|
@@ -553,6 +573,11 @@ def _marker(slide: Slide) -> str:
|
|
|
553
573
|
data["body"] = body_md
|
|
554
574
|
if slide.vars:
|
|
555
575
|
data["vars"] = slide.vars
|
|
576
|
+
if slide.src is not None:
|
|
577
|
+
# base64: MARKER_RE is delimiter-based, so a `}` + `-->` sequence
|
|
578
|
+
# in raw authored markdown would truncate the JSON mid-string;
|
|
579
|
+
# encoding also keeps the visible notes pane free of a giant blob.
|
|
580
|
+
data["src"] = base64.b64encode(slide.src.encode()).decode()
|
|
556
581
|
elif slide.layout_name and slide.layout_name not in SECTION_LAYOUTS:
|
|
557
582
|
data["tpl"] = slide.layout_name
|
|
558
583
|
return f"<!-- s2g {json.dumps(data, separators=(',', ':'))} -->"
|
|
@@ -664,9 +689,15 @@ def _styled_requests(slide: Slide, style: Style, image_url, image_px) -> list[di
|
|
|
664
689
|
head_pt = _fit_headline_pt(headline_text, style.headline_pt,
|
|
665
690
|
max_lines=style.head_lines)
|
|
666
691
|
head_h = _est_lines(headline_text, head_pt) * head_pt * 1.25 / 72 + 0.1
|
|
667
|
-
# Title cards
|
|
692
|
+
# Title cards have no body region; body lines render as a small dimmed
|
|
693
|
+
# byline beneath the headline (e.g. "Project · Presenter" on a title slide).
|
|
694
|
+
byline = ""
|
|
695
|
+
if style.body_align is None and slide.paras:
|
|
696
|
+
byline = "\n".join(p.text for p in slide.paras if p.text)
|
|
697
|
+
by_h = (byline.count("\n") + 1) * 0.32 if byline else 0.0
|
|
698
|
+
# Title cards (no body, no image) vertically centre kicker+headline+byline.
|
|
668
699
|
if (style.body_align is None or not slide.paras) and not slide.image:
|
|
669
|
-
block = (0.36 if kicker_text else 0) + head_h
|
|
700
|
+
block = (0.36 if kicker_text else 0) + head_h + (by_h + 0.1 if byline else 0)
|
|
670
701
|
y = max(0.4, (5.63 - block) / 2)
|
|
671
702
|
else:
|
|
672
703
|
y = style.top
|
|
@@ -682,6 +713,11 @@ def _styled_requests(slide: Slide, style: Style, image_url, image_px) -> list[di
|
|
|
682
713
|
headline_text, head_pt, style.headline_rgb, True,
|
|
683
714
|
halign=head_align)
|
|
684
715
|
y += head_h + 0.15
|
|
716
|
+
if byline:
|
|
717
|
+
rgb = MUTED if style.bg == DARK_BG else BODY_INK
|
|
718
|
+
reqs += _text_box(sid, sid + "_by", (0.34, y, 9.32, by_h),
|
|
719
|
+
byline, 14, rgb, False)
|
|
720
|
+
y += by_h + 0.1
|
|
685
721
|
if style.body_align and slide.paras:
|
|
686
722
|
bid = sid + "_b"
|
|
687
723
|
reqs.append({"createShape": {"objectId": bid, "shapeType": "TEXT_BOX",
|
|
@@ -1503,6 +1539,8 @@ def _slide_from_marker(marker: dict, notes: str) -> Slide:
|
|
|
1503
1539
|
slide.kicker = marker.get("h2", "")
|
|
1504
1540
|
slide.template_name = marker["template"]
|
|
1505
1541
|
slide.vars = marker.get("vars", {})
|
|
1542
|
+
if "src" in marker:
|
|
1543
|
+
slide.src = base64.b64decode(marker["src"]).decode()
|
|
1506
1544
|
return slide
|
|
1507
1545
|
|
|
1508
1546
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: slidesync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Bidirectional sync between a Slidev markdown deck and Google Slides as native, editable objects
|
|
5
5
|
Author-email: Daniel Hails <slidesync@hails.info>
|
|
6
6
|
License: MIT
|
|
@@ -22,7 +22,7 @@ Bidirectional sync between a [Slidev](https://sli.dev) markdown deck and **Googl
|
|
|
22
22
|
Slides** — as native, editable objects (title/body/bullets/tables/positioned
|
|
23
23
|
images, brand-styled text boxes), not pasted screenshots.
|
|
24
24
|
|
|
25
|
-
Version: 0.
|
|
25
|
+
Version: 0.2.0
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
28
|
uvx slidesync --help # run without installing
|
|
@@ -84,7 +84,8 @@ is a no-op. Diff per run: identical hash → skip; same key, new content → rep
|
|
|
84
84
|
new key → create. Removed slides are **kept** unless `--prune`. **Only `s2g_`
|
|
85
85
|
slides are ever touched** — hand-authored slides are invisible to the sync. A
|
|
86
86
|
hidden `<!-- s2g {...} -->` marker in speaker notes carries the human id, image
|
|
87
|
-
path, and template
|
|
87
|
+
path, template vars — and, for template slides, the authored body markdown
|
|
88
|
+
(base64) — so `pull` recovers the source verbatim.
|
|
88
89
|
|
|
89
90
|
## Markdown dialect
|
|
90
91
|
|
|
@@ -96,7 +97,11 @@ may have its own frontmatter (`id:`, `template:`, `layout:`).
|
|
|
96
97
|
`**bold**` / `*italic*` / `` `code` `` / `[link](url)`. GFM tables.
|
|
97
98
|
`` images (uploaded to Drive; `alt` becomes the accessibility
|
|
98
99
|
description, round-tripped on pull). Blank lines preserved as spacing.
|
|
99
|
-
`<!-- notes -->` become speaker notes
|
|
100
|
+
`<!-- notes -->` become speaker notes — and round-trip as **comments, in
|
|
101
|
+
place**: template slides carry their authored source in the marker, so `pull`
|
|
102
|
+
re-emits each comment where it was written instead of one merged trailing
|
|
103
|
+
blob. Speaker notes edited live in Slides come back as one extra trailing
|
|
104
|
+
comment.
|
|
100
105
|
- **Internal links:** `[text](#slide-id)` becomes a native Slides link to the
|
|
101
106
|
slide whose `id:` (or title slug) is `slide-id`.
|
|
102
107
|
|
|
@@ -105,6 +110,9 @@ may have its own frontmatter (`id:`, `template:`, `layout:`).
|
|
|
105
110
|
Select per slide via `template:` — native styled boxes, no in-deck templates:
|
|
106
111
|
`dark`/`title`, `appendix`, `question`/`label`, `topic`, `content`,
|
|
107
112
|
`graph`/`full` (single full-bleed image), `prompt`/`code` (verbatim monospace).
|
|
113
|
+
Title cards (`dark`/`title`/`appendix`) render body lines as a small dimmed
|
|
114
|
+
**byline** beneath the headline (e.g. `Project · Presenter`) — they still have
|
|
115
|
+
no linkable body region.
|
|
108
116
|
Slides with no `template:` fall back to a generative path (section /
|
|
109
117
|
title+body / table / image) that also brands the background + IBM Plex.
|
|
110
118
|
|
|
@@ -131,6 +139,11 @@ Releases publish to PyPI via Trusted Publishing (OIDC) on a `v*.*.*` tag — see
|
|
|
131
139
|
— this is a content mapper, not a CSS renderer.
|
|
132
140
|
- On `pull`, the slide model holds a single image, so a slide with multiple
|
|
133
141
|
images keeps the first; image `contentUrl`s from foreign decks are ephemeral.
|
|
142
|
+
- Verbatim-source markers are seeded at push time, so comment preservation
|
|
143
|
+
applies from the first push with v0.2+ (older slides re-render once: the
|
|
144
|
+
content hash is now over the authored source). Generative-path slides (no
|
|
145
|
+
`template:`) still merge comments into a single trailing comment on pull,
|
|
146
|
+
since their live Slides edits — not the marker — are the source of truth.
|
|
134
147
|
|
|
135
148
|
## License
|
|
136
149
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Comments round-trip as comments, in place — not one merged speaker-notes blob.
|
|
2
|
+
|
|
3
|
+
The authored body of every non-custom slide is stored (base64) in the `s2g`
|
|
4
|
+
marker; `pull` re-emits it verbatim, so comment positions, prompt fences, and
|
|
5
|
+
formatting quirks all survive. Speaker notes edited live in Slides surface as
|
|
6
|
+
one extra trailing comment instead of silently replacing the authored ones.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from slidesync._sync import (
|
|
10
|
+
STYLES,
|
|
11
|
+
_marker,
|
|
12
|
+
_read_marker,
|
|
13
|
+
_slide_from_marker,
|
|
14
|
+
build_slides,
|
|
15
|
+
split_slides,
|
|
16
|
+
to_slidev,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
DECK = """---
|
|
20
|
+
theme: seriph
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
template: topic
|
|
25
|
+
id: thread-finding
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# Compression hurts recall
|
|
29
|
+
## FINDING
|
|
30
|
+
|
|
31
|
+
<!-- framing: say this slowly -->
|
|
32
|
+
|
|
33
|
+
- one solid point
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
<!-- data-source: scripts/fig.py -->
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
template: prompt
|
|
41
|
+
id: appendix-prompt-monitor
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## MONITOR PROMPT
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
You are a monitor. Score 0-100 in <verdict></verdict>.
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
template: dark
|
|
52
|
+
id: weekly
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
# 2026/06/12
|
|
56
|
+
## WEEKLY UPDATE
|
|
57
|
+
|
|
58
|
+
Touchstone · Daniel Hails
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _slide(key):
|
|
63
|
+
return next(s for s in build_slides(split_slides(DECK)) if s.key == key)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _roundtrip(slide, notes=None):
|
|
67
|
+
marker = _read_marker(_marker(slide))
|
|
68
|
+
return _slide_from_marker(marker, slide.notes if notes is None else notes)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_marker_parses_despite_comment_terminators_in_src():
|
|
72
|
+
# The authored body contains comments (`-->`) and braces; base64 keeps the
|
|
73
|
+
# marker JSON immune to `}` + `-->` sequences that would truncate the
|
|
74
|
+
# delimiter-based MARKER_RE match mid-string.
|
|
75
|
+
slide = _slide("thread-finding")
|
|
76
|
+
slide.src += "\n<!-- edge: {brace} -->"
|
|
77
|
+
marker = _read_marker(_marker(slide))
|
|
78
|
+
assert marker.get("src"), "marker should carry the authored source"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_comments_survive_in_place_not_merged():
|
|
82
|
+
md = to_slidev(_roundtrip(_slide("thread-finding")))
|
|
83
|
+
assert md.count("<!--") == 2, "each comment stays its own comment"
|
|
84
|
+
assert md.index("framing: say this slowly") < md.index("one solid point")
|
|
85
|
+
assert md.index("data-source: scripts/fig.py") > md.index("../figures/fig.png")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_prompt_fence_survives_pull():
|
|
89
|
+
md = to_slidev(_roundtrip(_slide("appendix-prompt-monitor")))
|
|
90
|
+
assert "You are a monitor." in md # previously lost: marker had no body
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_unchanged_notes_are_not_duplicated():
|
|
94
|
+
slide = _slide("thread-finding")
|
|
95
|
+
# The notes shape flattens paragraphs to spaces when read back.
|
|
96
|
+
flattened = " ".join(slide.notes.split())
|
|
97
|
+
md = to_slidev(_roundtrip(slide, notes=flattened))
|
|
98
|
+
assert md.count("<!--") == 2
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_live_notes_edit_appends_trailing_comment():
|
|
102
|
+
slide = _slide("thread-finding")
|
|
103
|
+
md = to_slidev(_roundtrip(slide, notes=slide.notes + "\nadded in slides"))
|
|
104
|
+
assert md.count("<!--") == 3
|
|
105
|
+
assert md.rstrip().endswith("-->") and "added in slides" in md
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _rendered(slide):
|
|
109
|
+
slide.object_id = "s2g_aaaaaaaaaa_bbbbbbbbbb"
|
|
110
|
+
return _styled(slide)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _styled(slide):
|
|
114
|
+
from slidesync._sync import _styled_requests
|
|
115
|
+
|
|
116
|
+
return _styled_requests(slide, STYLES["dark"], None, None)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_dark_template_renders_body_as_byline():
|
|
120
|
+
reqs = _rendered(_slide("weekly"))
|
|
121
|
+
boxes = [r["createShape"]["objectId"] for r in reqs if "createShape" in r]
|
|
122
|
+
assert "s2g_aaaaaaaaaa_bbbbbbbbbb_by" in boxes
|
|
123
|
+
texts = [r["insertText"]["text"] for r in reqs if "insertText" in r]
|
|
124
|
+
assert "Touchstone · Daniel Hails" in texts
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_dark_template_without_body_has_no_byline():
|
|
128
|
+
slide = _slide("weekly")
|
|
129
|
+
slide.paras = []
|
|
130
|
+
reqs = _rendered(slide)
|
|
131
|
+
boxes = [r["createShape"]["objectId"] for r in reqs if "createShape" in r]
|
|
132
|
+
assert not [b for b in boxes if b.endswith("_by")]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|