@honeydeck/honeydeck 0.4.0 → 0.6.0
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.
- package/AGENTS.md +4 -4
- package/DEVELOPMENT.md +6 -4
- package/Readme.md +15 -15
- package/SPEC.md +5 -4
- package/docs/browser-frame.md +38 -0
- package/docs/components.md +16 -57
- package/docs/configuration.md +13 -0
- package/docs/customization.md +2 -0
- package/docs/deeper-dive.md +32 -7
- package/docs/getting-started.md +4 -2
- package/docs/index.json +258 -0
- package/docs/keyboard.md +35 -0
- package/docs/list-style.md +53 -0
- package/docs/local-development.md +3 -1
- package/docs/mermaid.md +2 -0
- package/docs/mobile.md +2 -0
- package/docs/navigation.md +3 -1
- package/docs/notes.md +40 -0
- package/docs/pdf-export.md +6 -2
- package/docs/presenter-mode.md +8 -3
- package/docs/reveal-group.md +60 -0
- package/docs/reveal-with.md +39 -0
- package/docs/reveal.md +35 -0
- package/docs/skills.md +5 -3
- package/docs/slides.md +2 -0
- package/docs/slidev-migration.md +5 -0
- package/docs/steps-and-reveals.md +145 -8
- package/docs/timeline-steps.md +50 -0
- package/package.json +6 -2
- package/skills/SPEC.md +6 -6
- package/skills/honeydeck/SKILL.md +9 -9
- package/skills/slidev-migration/SKILL.md +7 -6
- package/src/SPEC.md +8 -3
- package/src/cli/SPEC.md +3 -2
- package/src/cli/pdf.ts +11 -4
- package/src/remark/SPEC.md +102 -2
- package/src/remark/code-utils.ts +151 -0
- package/src/remark/shiki-code-blocks.ts +329 -136
- package/src/remark/step-numbering.ts +408 -103
- package/src/runtime/Deck.tsx +133 -116
- package/src/runtime/EffectiveColorModeContext.tsx +37 -0
- package/src/runtime/SPEC.md +21 -8
- package/src/runtime/SlideCanvas.tsx +19 -16
- package/src/runtime/SlideScaleContext.tsx +23 -0
- package/src/runtime/components/CodeBlock.tsx +19 -202
- package/src/runtime/components/CodeBlockCopyButton.tsx +64 -0
- package/src/runtime/components/CodeBlockShared.ts +17 -0
- package/src/runtime/components/Fade.tsx +51 -0
- package/src/runtime/components/FadeGroup.tsx +175 -0
- package/src/runtime/components/FadeWith.tsx +54 -0
- package/src/runtime/components/MagicCodeBlock.tsx +223 -0
- package/src/runtime/components/NavBar.tsx +1 -1
- package/src/runtime/components/NormalCodeBlock.tsx +128 -0
- package/src/runtime/components/Reveal.tsx +27 -27
- package/src/runtime/components/RevealGroup.tsx +143 -41
- package/src/runtime/components/RevealWith.tsx +63 -0
- package/src/runtime/components/SPEC.md +115 -10
- package/src/runtime/components/TimelineReveal.tsx +81 -0
- package/src/runtime/components/index.ts +13 -5
- package/src/runtime/components/timelineVisibility.ts +45 -0
- package/src/runtime/index.ts +9 -1
- package/src/runtime/navigation.ts +6 -4
- package/src/runtime/presentationApi.ts +449 -0
- package/src/runtime/views/PresenterCastButton.tsx +39 -0
- package/src/runtime/views/PresenterView.tsx +21 -4
- package/src/runtime/views/SPEC.md +7 -5
- package/src/theme/base.css +67 -2
- package/src/vite-plugin/SPEC.md +20 -2
- package/src/vite-plugin/index.ts +16 -2
- package/src/vite-plugin/splitter.ts +1 -0
- package/src/vite-plugin/virtual-modules.ts +16 -6
|
@@ -1,36 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Remark plugin: assign `at` props to
|
|
3
|
-
*
|
|
2
|
+
* Remark plugin: assign `at` props to timeline-aware MDX elements and count
|
|
3
|
+
* total timeline steps per slide.
|
|
4
4
|
*
|
|
5
5
|
* ### What it does
|
|
6
6
|
* Walks the MDAST (including MDX JSX nodes) in document order and:
|
|
7
|
-
* 1. Assigns `at={n}` to each `<Reveal>` element (starting at 1)
|
|
8
|
-
* injects `as="div"`/`as="span"` from the MDX flow/text context.
|
|
9
|
-
* 2. Assigns `at={n}` to each `<RevealGroup>` element using
|
|
10
|
-
* index of the group and adds internal per-target step numbers
|
|
11
|
-
* children/list items.
|
|
12
|
-
*
|
|
7
|
+
* 1. Assigns `at={n}` to each `<Reveal>`/`<Fade>` element (starting at 1)
|
|
8
|
+
* and injects `as="div"`/`as="span"` from the MDX flow/text context.
|
|
9
|
+
* 2. Assigns `at={n}` to each `<RevealGroup>`/`<FadeGroup>` element using
|
|
10
|
+
* the starting index of the group and adds internal per-target step numbers
|
|
11
|
+
* for group children/list items. `listRevealMode="nested"` makes nested
|
|
12
|
+
* RevealGroup list items step targets too.
|
|
13
|
+
* 3. Collects literal `<Reveal name="...">` targets and resolves
|
|
14
|
+
* `<RevealWith target="...">`, numeric `target={n}`, and `at={n}`
|
|
15
|
+
* sync props for `<RevealWith>`/`<FadeWith>` without adding steps.
|
|
16
|
+
* 4. Assigns `at={n}` to each `<TimelineSteps steps={n}>` element and
|
|
13
17
|
* advances the timeline by its literal static step count.
|
|
14
|
-
*
|
|
18
|
+
* 5. Counts timeline steps from code fence `|`-separated groups after the
|
|
15
19
|
* first baseline group (counted here so `stepCount` is already accurate).
|
|
16
|
-
*
|
|
20
|
+
* 6. Writes the total step count to `vfile.data.stepCount`.
|
|
17
21
|
*
|
|
18
22
|
* ### `at` prop injection
|
|
19
23
|
* Numeric JSX props require an ESTree expression node. We construct a minimal
|
|
20
24
|
* `Program` → `ExpressionStatement` → `Literal` subtree so that `@mdx-js/mdx`
|
|
21
25
|
* generates `at={<number>}` (not `at="<string>"`).
|
|
22
26
|
*
|
|
23
|
-
* ###
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
27
|
+
* ### Internal `at` props
|
|
28
|
+
* Honeydeck injects `at` props during compilation to connect timeline-driven
|
|
29
|
+
* components to their assigned slide-local steps. `at` is internal compiler
|
|
30
|
+
* plumbing for `<Reveal>`, `<Fade>`, `<RevealGroup>`, and `<FadeGroup>`, and a
|
|
31
|
+
* validated sync target for `<RevealWith>`/`<FadeWith>`. Timeline-aware
|
|
32
|
+
* elements still receive compiler `as` props when missing, so inline usages
|
|
33
|
+
* remain valid HTML.
|
|
28
34
|
*
|
|
29
35
|
* ### Recursion
|
|
30
|
-
* Nested step producers are flattened into the same slide-local timeline
|
|
31
|
-
* Parent reveal targets consume their step first, then nested
|
|
32
|
-
* and code walkthrough steps after the
|
|
33
|
-
* steps before the next sibling target.
|
|
36
|
+
* Nested step producers are flattened into the same slide-local timeline for
|
|
37
|
+
* reveal targets. Parent reveal targets consume their step first, then nested
|
|
38
|
+
* reveal/fade elements, reveal groups, and code walkthrough steps after the
|
|
39
|
+
* first baseline highlight consume later steps before the next sibling target.
|
|
40
|
+
* With components cannot contain nested timeline producers because they do not
|
|
41
|
+
* create timeline steps. Fade targets cannot contain nested timeline producers
|
|
42
|
+
* because a faded parent would hide later nested steps.
|
|
34
43
|
*/
|
|
35
44
|
|
|
36
45
|
import type { Program } from "estree";
|
|
@@ -42,6 +51,12 @@ import type {
|
|
|
42
51
|
MdxJsxTextElement,
|
|
43
52
|
} from "mdast-util-mdx-jsx";
|
|
44
53
|
import type { Plugin } from "unified";
|
|
54
|
+
import {
|
|
55
|
+
countCodeFenceGroups,
|
|
56
|
+
countCodeFenceSteps,
|
|
57
|
+
countMagicCodeStates,
|
|
58
|
+
isMagicCodeFence,
|
|
59
|
+
} from "./code-utils.ts";
|
|
45
60
|
|
|
46
61
|
// ---------------------------------------------------------------------------
|
|
47
62
|
// Helper: build an `at={n}` attribute node
|
|
@@ -103,6 +118,28 @@ function hasAtProp(el: MdxJsxElement): boolean {
|
|
|
103
118
|
);
|
|
104
119
|
}
|
|
105
120
|
|
|
121
|
+
function hasAttribute(el: MdxJsxElement, name: string): boolean {
|
|
122
|
+
return el.attributes.some(
|
|
123
|
+
(a) => a.type === "mdxJsxAttribute" && a.name === name,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isStepElementName(name: string | null | undefined): boolean {
|
|
128
|
+
return name === "Reveal" || name === "Fade";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isWithElementName(name: string | null | undefined): boolean {
|
|
132
|
+
return name === "RevealWith" || name === "FadeWith";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isGroupElementName(name: string | null | undefined): boolean {
|
|
136
|
+
return name === "RevealGroup" || name === "FadeGroup";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isFadeElementName(name: string | null | undefined): boolean {
|
|
140
|
+
return name === "Fade";
|
|
141
|
+
}
|
|
142
|
+
|
|
106
143
|
function getAttribute(
|
|
107
144
|
el: MdxJsxElement,
|
|
108
145
|
name: string,
|
|
@@ -113,7 +150,27 @@ function getAttribute(
|
|
|
113
150
|
);
|
|
114
151
|
}
|
|
115
152
|
|
|
116
|
-
function
|
|
153
|
+
function removeAttribute(el: MdxJsxElement, name: string): void {
|
|
154
|
+
el.attributes = el.attributes.filter(
|
|
155
|
+
(a) => !(a.type === "mdxJsxAttribute" && a.name === name),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function setAtAttribute(el: MdxJsxElement, at: number): void {
|
|
160
|
+
removeAttribute(el, "at");
|
|
161
|
+
el.attributes.push(makeAtAttribute(at));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setStringAttribute(
|
|
165
|
+
el: MdxJsxElement,
|
|
166
|
+
name: string,
|
|
167
|
+
value: string,
|
|
168
|
+
): void {
|
|
169
|
+
removeAttribute(el, name);
|
|
170
|
+
el.attributes.push(makeStringAttribute(name, value));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function injectTimelineWrapperElement(el: MdxJsxElement): void {
|
|
117
174
|
if (getAttribute(el, "as")) return;
|
|
118
175
|
|
|
119
176
|
el.attributes.push(
|
|
@@ -125,28 +182,7 @@ function injectRevealWrapperElement(el: MdxJsxElement): void {
|
|
|
125
182
|
// Helper: count reveal steps produced by a RevealGroup child
|
|
126
183
|
// ---------------------------------------------------------------------------
|
|
127
184
|
|
|
128
|
-
|
|
129
|
-
const n = node as { type?: string; children?: unknown[] };
|
|
130
|
-
|
|
131
|
-
if (n.type === "list") {
|
|
132
|
-
return (n.children ?? []).filter((child) => {
|
|
133
|
-
const c = child as { type?: string };
|
|
134
|
-
return c.type === "listItem";
|
|
135
|
-
}).length;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (isJsxElement(node) && (node.name === "ul" || node.name === "ol")) {
|
|
139
|
-
return node.children.filter((child) => {
|
|
140
|
-
const c = child as { type?: string; value?: string };
|
|
141
|
-
if (c.type === "text") {
|
|
142
|
-
return (c.value ?? "").trim().length > 0;
|
|
143
|
-
}
|
|
144
|
-
return true;
|
|
145
|
-
}).length;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
185
|
+
type RevealGroupListRevealMode = "direct" | "nested";
|
|
150
186
|
|
|
151
187
|
function getMeaningfulChildren(children: unknown[]): unknown[] {
|
|
152
188
|
return children.filter((child) => {
|
|
@@ -158,24 +194,54 @@ function getMeaningfulChildren(children: unknown[]): unknown[] {
|
|
|
158
194
|
});
|
|
159
195
|
}
|
|
160
196
|
|
|
161
|
-
function
|
|
197
|
+
function isMarkdownList(node: unknown): boolean {
|
|
198
|
+
return (node as { type?: string }).type === "list";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isMarkdownListItem(node: unknown): boolean {
|
|
202
|
+
return (node as { type?: string }).type === "listItem";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isJsxListElement(node: unknown): node is MdxJsxElement {
|
|
206
|
+
return isJsxElement(node) && (node.name === "ul" || node.name === "ol");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isJsxListItemElement(node: unknown): node is MdxJsxElement {
|
|
210
|
+
return isJsxElement(node) && node.name === "li";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function isHtmlJsxElement(node: unknown): node is MdxJsxElement {
|
|
214
|
+
return isJsxElement(node) && /^[a-z]/.test(node.name ?? "");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function isJsxFragmentElement(node: unknown): node is MdxJsxElement {
|
|
218
|
+
return isJsxElement(node) && !node.name;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function isListElement(node: unknown): boolean {
|
|
222
|
+
return isMarkdownList(node) || isJsxListElement(node);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getListChildren(node: unknown): unknown[] {
|
|
226
|
+
const n = node as { type?: string; children?: unknown[] };
|
|
227
|
+
|
|
228
|
+
if (n.type === "list") {
|
|
229
|
+
return (n.children ?? []).filter(isMarkdownListItem);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (isJsxListElement(node)) {
|
|
233
|
+
return getMeaningfulChildren(node.children);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getDirectRevealGroupTargets(el: MdxJsxElement): unknown[] {
|
|
162
240
|
const targets: unknown[] = [];
|
|
163
241
|
|
|
164
242
|
for (const child of getMeaningfulChildren(el.children)) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (listItemCount !== null && Array.isArray(c.children)) {
|
|
169
|
-
if (c.type === "list") {
|
|
170
|
-
targets.push(
|
|
171
|
-
...c.children.filter((item) => {
|
|
172
|
-
const i = item as { type?: string };
|
|
173
|
-
return i.type === "listItem";
|
|
174
|
-
}),
|
|
175
|
-
);
|
|
176
|
-
} else {
|
|
177
|
-
targets.push(...getMeaningfulChildren(c.children));
|
|
178
|
-
}
|
|
243
|
+
if (isListElement(child)) {
|
|
244
|
+
targets.push(...getListChildren(child));
|
|
179
245
|
continue;
|
|
180
246
|
}
|
|
181
247
|
|
|
@@ -185,25 +251,6 @@ function getRevealGroupTargets(el: MdxJsxElement): unknown[] {
|
|
|
185
251
|
return targets;
|
|
186
252
|
}
|
|
187
253
|
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
|
-
// Helper: count groups/steps encoded in a code fence meta string
|
|
190
|
-
//
|
|
191
|
-
// Pattern: `{2|4-5|all}` → 3 groups, 2 timeline steps.
|
|
192
|
-
// The first group is the baseline active highlight. Each later group consumes
|
|
193
|
-
// one timeline step.
|
|
194
|
-
// ---------------------------------------------------------------------------
|
|
195
|
-
|
|
196
|
-
function countCodeFenceGroups(meta: string | null | undefined): number {
|
|
197
|
-
if (!meta) return 0;
|
|
198
|
-
const match = meta.match(/\{([^}]+)\}/);
|
|
199
|
-
if (!match?.[1]) return 0;
|
|
200
|
-
return match[1].split("|").filter(Boolean).length;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function countCodeFenceSteps(meta: string | null | undefined): number {
|
|
204
|
-
return Math.max(0, countCodeFenceGroups(meta) - 1);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
254
|
function expressionValue(attr: MdxJsxAttribute): unknown {
|
|
208
255
|
const value = attr.value;
|
|
209
256
|
if (
|
|
@@ -223,6 +270,29 @@ function expressionValue(attr: MdxJsxAttribute): unknown {
|
|
|
223
270
|
return expression.value;
|
|
224
271
|
}
|
|
225
272
|
|
|
273
|
+
function stringLiteralValue(attr: MdxJsxAttribute): string | null {
|
|
274
|
+
if (typeof attr.value === "string") return attr.value;
|
|
275
|
+
|
|
276
|
+
const value = expressionValue(attr);
|
|
277
|
+
return typeof value === "string" ? value : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function readNonEmptyStringLiteral(
|
|
281
|
+
attr: MdxJsxAttribute,
|
|
282
|
+
description: string,
|
|
283
|
+
): string {
|
|
284
|
+
const value = stringLiteralValue(attr);
|
|
285
|
+
if (value === null) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`Honeydeck ${description} must be a literal string. Dynamic expressions are not supported because RevealWith targets are resolved at build time.`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (value.length === 0) {
|
|
291
|
+
throw new Error(`Honeydeck ${description} must not be empty.`);
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
|
|
226
296
|
function parsePositiveIntegerLiteral(attr: MdxJsxAttribute): number | null {
|
|
227
297
|
if (typeof attr.value === "string") {
|
|
228
298
|
if (!/^[1-9]\d*$/.test(attr.value)) return null;
|
|
@@ -241,6 +311,24 @@ function parsePositiveIntegerLiteral(attr: MdxJsxAttribute): number | null {
|
|
|
241
311
|
return null;
|
|
242
312
|
}
|
|
243
313
|
|
|
314
|
+
function readRevealGroupListRevealMode(
|
|
315
|
+
el: MdxJsxElement,
|
|
316
|
+
): RevealGroupListRevealMode {
|
|
317
|
+
const attr = getAttribute(el, "listRevealMode");
|
|
318
|
+
if (!attr) return "direct";
|
|
319
|
+
|
|
320
|
+
const value =
|
|
321
|
+
typeof attr.value === "string" ? attr.value : expressionValue(attr);
|
|
322
|
+
|
|
323
|
+
if (value === "direct" || value === "nested") {
|
|
324
|
+
return value;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
throw new Error(
|
|
328
|
+
'Honeydeck <RevealGroup> `listRevealMode` must be the literal string "direct" or "nested" because timeline steps are counted at build time.',
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
244
332
|
function readTimelineSteps(el: MdxJsxElement): number {
|
|
245
333
|
const attr = getAttribute(el, "steps");
|
|
246
334
|
if (!attr || attr.value === null) {
|
|
@@ -264,13 +352,18 @@ function findNestedStepProducer(node: unknown): string | null {
|
|
|
264
352
|
|
|
265
353
|
if (n.type === "code") {
|
|
266
354
|
const codeNode = node as Code;
|
|
355
|
+
if (isMagicCodeFence(codeNode)) {
|
|
356
|
+
return countMagicCodeStates(codeNode.value) > 1
|
|
357
|
+
? "Magic Code block"
|
|
358
|
+
: null;
|
|
359
|
+
}
|
|
267
360
|
return countCodeFenceSteps(codeNode.meta) > 0 ? "stepped code fence" : null;
|
|
268
361
|
}
|
|
269
362
|
|
|
270
363
|
if (isJsxElement(node)) {
|
|
271
364
|
if (
|
|
272
|
-
node.name
|
|
273
|
-
node.name
|
|
365
|
+
isStepElementName(node.name) ||
|
|
366
|
+
isGroupElementName(node.name) ||
|
|
274
367
|
node.name === "TimelineSteps"
|
|
275
368
|
) {
|
|
276
369
|
return `<${node.name}>`;
|
|
@@ -292,8 +385,8 @@ function findNestedStepProducer(node: unknown): string | null {
|
|
|
292
385
|
// ---------------------------------------------------------------------------
|
|
293
386
|
|
|
294
387
|
/**
|
|
295
|
-
* Remark plugin that numbers
|
|
296
|
-
*
|
|
388
|
+
* Remark plugin that numbers timeline-aware MDX elements with sequential `at`
|
|
389
|
+
* props and stores the total step count in `vfile.data`.
|
|
297
390
|
*
|
|
298
391
|
* Usage (in Vite plugin config):
|
|
299
392
|
* ```ts
|
|
@@ -302,6 +395,10 @@ function findNestedStepProducer(node: unknown): string | null {
|
|
|
302
395
|
*/
|
|
303
396
|
export const remarkStepNumbering: Plugin<[], Root> = () => (tree, vfile) => {
|
|
304
397
|
let counter = 1; // next `at` value to assign; 1-based
|
|
398
|
+
let activeRevealGroupTargetSteps: number[] = [];
|
|
399
|
+
const namedReveals = new Map<string, number>();
|
|
400
|
+
const withStringTargets: Array<{ el: MdxJsxElement; target: string }> = [];
|
|
401
|
+
const withNumericTargets: Array<{ el: MdxJsxElement; at: number }> = [];
|
|
305
402
|
|
|
306
403
|
function visitChildren(node: unknown): void {
|
|
307
404
|
const n = node as { children?: unknown[] };
|
|
@@ -312,12 +409,92 @@ export const remarkStepNumbering: Plugin<[], Root> = () => (tree, vfile) => {
|
|
|
312
409
|
}
|
|
313
410
|
}
|
|
314
411
|
|
|
412
|
+
function pushRevealGroupTarget(
|
|
413
|
+
targetSteps: number[],
|
|
414
|
+
target: unknown,
|
|
415
|
+
visitContent: boolean,
|
|
416
|
+
): void {
|
|
417
|
+
targetSteps.push(counter);
|
|
418
|
+
counter++;
|
|
419
|
+
if (visitContent) visitNode(target);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function visitRevealGroupNestedListContent(node: unknown): void {
|
|
423
|
+
if (isListElement(node)) {
|
|
424
|
+
visitRevealGroupNestedList(node);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (isHtmlJsxElement(node) || isJsxFragmentElement(node)) {
|
|
429
|
+
for (const child of getMeaningfulChildren(node.children)) {
|
|
430
|
+
visitRevealGroupNestedListContent(child);
|
|
431
|
+
}
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
visitNode(node);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function visitRevealGroupNestedListItemContent(item: unknown): void {
|
|
439
|
+
if (!isMarkdownListItem(item) && !isJsxListItemElement(item)) {
|
|
440
|
+
visitNode(item);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const i = item as { children?: unknown[] };
|
|
445
|
+
for (const child of getMeaningfulChildren(i.children ?? [])) {
|
|
446
|
+
visitRevealGroupNestedListContent(child);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function visitRevealGroupNestedList(
|
|
451
|
+
list: unknown,
|
|
452
|
+
targetSteps: number[] = activeRevealGroupTargetSteps,
|
|
453
|
+
): void {
|
|
454
|
+
for (const item of getListChildren(list)) {
|
|
455
|
+
pushRevealGroupTarget(targetSteps, item, false);
|
|
456
|
+
visitRevealGroupNestedListItemContent(item);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function recordRevealName(el: MdxJsxElement, at: number): void {
|
|
461
|
+
const nameAttr = getAttribute(el, "name");
|
|
462
|
+
if (!nameAttr) return;
|
|
463
|
+
|
|
464
|
+
const name = readNonEmptyStringLiteral(nameAttr, "<Reveal> `name`");
|
|
465
|
+
if (namedReveals.has(name)) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`Honeydeck <Reveal> name "${name}" is duplicated on this slide. Reveal names must be unique per slide for <RevealWith target="..."> resolution.`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
namedReveals.set(name, at);
|
|
471
|
+
}
|
|
472
|
+
|
|
315
473
|
function visitNode(node: unknown): void {
|
|
316
474
|
const n = node as { type?: string };
|
|
317
475
|
|
|
318
476
|
// ── Code blocks with step-through meta ─────────────────────────────
|
|
319
477
|
if (n.type === "code") {
|
|
320
478
|
const codeNode = node as Code;
|
|
479
|
+
|
|
480
|
+
if (isMagicCodeFence(codeNode)) {
|
|
481
|
+
const stateCount = countMagicCodeStates(codeNode.value);
|
|
482
|
+
const steps = Math.max(0, stateCount - 1);
|
|
483
|
+
if (stateCount > 0) {
|
|
484
|
+
if (!(codeNode as unknown as Record<string, unknown>).data) {
|
|
485
|
+
(codeNode as unknown as Record<string, unknown>).data = {};
|
|
486
|
+
}
|
|
487
|
+
(
|
|
488
|
+
(codeNode as unknown as Record<string, unknown>).data as Record<
|
|
489
|
+
string,
|
|
490
|
+
unknown
|
|
491
|
+
>
|
|
492
|
+
).honeydeckStartAt = counter;
|
|
493
|
+
}
|
|
494
|
+
counter += steps;
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
321
498
|
const groupCount = countCodeFenceGroups(codeNode.meta);
|
|
322
499
|
const steps = Math.max(0, groupCount - 1);
|
|
323
500
|
if (groupCount > 0) {
|
|
@@ -356,48 +533,156 @@ export const remarkStepNumbering: Plugin<[], Root> = () => (tree, vfile) => {
|
|
|
356
533
|
);
|
|
357
534
|
}
|
|
358
535
|
|
|
359
|
-
|
|
360
|
-
el.attributes.push(makeAtAttribute(counter));
|
|
361
|
-
}
|
|
362
|
-
|
|
536
|
+
setAtAttribute(el, counter);
|
|
363
537
|
counter += steps;
|
|
538
|
+
visitChildren(el);
|
|
364
539
|
return;
|
|
365
540
|
}
|
|
366
541
|
|
|
367
|
-
if (el.name
|
|
368
|
-
|
|
542
|
+
if (isStepElementName(el.name)) {
|
|
543
|
+
if (hasAtProp(el)) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Honeydeck <${el.name}> \`at\` is internal compiler plumbing and cannot be authored. Use <RevealWith at={n}> or <FadeWith at={n}> to sync content with an existing step.`,
|
|
546
|
+
);
|
|
547
|
+
}
|
|
369
548
|
|
|
370
|
-
if (
|
|
371
|
-
|
|
372
|
-
|
|
549
|
+
if (isFadeElementName(el.name)) {
|
|
550
|
+
const nestedProducer = findNestedStepProducer({
|
|
551
|
+
type: "honeydeckFadeChildren",
|
|
552
|
+
children: el.children,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
if (nestedProducer) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
`Honeydeck <${el.name}> cannot contain nested timeline producers (${nestedProducer}). Put fade components inside a Reveal instead of putting reveal/timeline components inside a Fade.`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const stepAt = counter;
|
|
563
|
+
injectTimelineWrapperElement(el);
|
|
564
|
+
setAtAttribute(el, stepAt);
|
|
565
|
+
if (el.name === "Reveal") {
|
|
566
|
+
recordRevealName(el, stepAt);
|
|
567
|
+
}
|
|
568
|
+
counter++;
|
|
569
|
+
if (!isFadeElementName(el.name)) {
|
|
373
570
|
visitChildren(el);
|
|
374
571
|
}
|
|
375
572
|
return;
|
|
376
573
|
}
|
|
377
574
|
|
|
378
|
-
if (el.name
|
|
575
|
+
if (isWithElementName(el.name)) {
|
|
576
|
+
injectTimelineWrapperElement(el);
|
|
577
|
+
|
|
578
|
+
const nestedProducer = findNestedStepProducer({
|
|
579
|
+
type: "honeydeckWithChildren",
|
|
580
|
+
children: el.children,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
if (nestedProducer) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`Honeydeck <${el.name}> cannot contain nested timeline producers (${nestedProducer}). With components do not create timeline steps; target them at sibling timeline steps instead.`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const hasTarget = hasAttribute(el, "target");
|
|
590
|
+
const hasAt = hasAtProp(el);
|
|
591
|
+
if (hasTarget === hasAt) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`Honeydeck <${el.name}> requires exactly one of \`target\` or \`at\`.`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (hasTarget) {
|
|
598
|
+
const targetAttr = getAttribute(el, "target");
|
|
599
|
+
if (!targetAttr) return;
|
|
600
|
+
const targetValue = expressionValue(targetAttr);
|
|
601
|
+
if (typeof targetValue === "number") {
|
|
602
|
+
const at = parsePositiveIntegerLiteral(targetAttr);
|
|
603
|
+
if (at === null) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`Honeydeck <${el.name}> \`target\` must be a literal positive integer or non-empty string.`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
withNumericTargets.push({ el, at });
|
|
609
|
+
} else {
|
|
610
|
+
const target = readNonEmptyStringLiteral(
|
|
611
|
+
targetAttr,
|
|
612
|
+
`<${el.name}> \`target\``,
|
|
613
|
+
);
|
|
614
|
+
withStringTargets.push({ el, target });
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
const atAttr = getAttribute(el, "at");
|
|
618
|
+
const at = atAttr ? parsePositiveIntegerLiteral(atAttr) : null;
|
|
619
|
+
if (at === null) {
|
|
620
|
+
throw new Error(
|
|
621
|
+
`Honeydeck <${el.name}> \`at\` must be a literal positive integer, for example at={2}. Dynamic expressions are not supported because With steps are resolved at build time.`,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
setAtAttribute(el, at);
|
|
625
|
+
withNumericTargets.push({ el, at });
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (isGroupElementName(el.name)) {
|
|
379
632
|
if (hasAtProp(el)) {
|
|
380
|
-
|
|
633
|
+
throw new Error(
|
|
634
|
+
`Honeydeck <${el.name}> \`at\` is internal compiler plumbing and cannot be authored. Use <RevealWith at={n}> or <FadeWith at={n}> to sync content with an existing group step.`,
|
|
635
|
+
);
|
|
381
636
|
}
|
|
382
637
|
|
|
383
|
-
const
|
|
638
|
+
const listRevealMode =
|
|
639
|
+
el.name === "RevealGroup"
|
|
640
|
+
? readRevealGroupListRevealMode(el)
|
|
641
|
+
: "direct";
|
|
384
642
|
const targetSteps: number[] = [];
|
|
385
643
|
|
|
386
|
-
el
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
644
|
+
setAtAttribute(el, counter);
|
|
645
|
+
|
|
646
|
+
if (listRevealMode === "nested") {
|
|
647
|
+
const previousTargetSteps = activeRevealGroupTargetSteps;
|
|
648
|
+
activeRevealGroupTargetSteps = targetSteps;
|
|
649
|
+
|
|
650
|
+
for (const child of getMeaningfulChildren(el.children)) {
|
|
651
|
+
if (isListElement(child)) {
|
|
652
|
+
visitRevealGroupNestedList(child);
|
|
653
|
+
} else {
|
|
654
|
+
pushRevealGroupTarget(targetSteps, child, true);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
activeRevealGroupTargetSteps = previousTargetSteps;
|
|
659
|
+
} else {
|
|
660
|
+
const targets = getDirectRevealGroupTargets(el);
|
|
661
|
+
|
|
662
|
+
if (el.name === "FadeGroup") {
|
|
663
|
+
for (const target of targets) {
|
|
664
|
+
const nestedProducer = findNestedStepProducer(target);
|
|
665
|
+
|
|
666
|
+
if (nestedProducer) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
`Honeydeck <FadeGroup> targets cannot contain nested timeline producers (${nestedProducer}). Put fade components inside a RevealGroup/Reveal target instead of nesting timeline components inside a FadeGroup target.`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
for (const target of targets) {
|
|
675
|
+
pushRevealGroupTarget(targetSteps, target, el.name !== "FadeGroup");
|
|
676
|
+
}
|
|
390
677
|
}
|
|
391
678
|
|
|
392
|
-
|
|
393
|
-
|
|
679
|
+
if (targetSteps.length === 0) {
|
|
680
|
+
removeAttribute(el, "targetStepsJson");
|
|
394
681
|
counter++;
|
|
395
|
-
|
|
682
|
+
return;
|
|
396
683
|
}
|
|
397
684
|
|
|
398
|
-
el.
|
|
399
|
-
makeStringAttribute("targetStepsJson", JSON.stringify(targetSteps)),
|
|
400
|
-
);
|
|
685
|
+
setStringAttribute(el, "targetStepsJson", JSON.stringify(targetSteps));
|
|
401
686
|
return;
|
|
402
687
|
}
|
|
403
688
|
}
|
|
@@ -407,6 +692,26 @@ export const remarkStepNumbering: Plugin<[], Root> = () => (tree, vfile) => {
|
|
|
407
692
|
|
|
408
693
|
visitChildren(tree);
|
|
409
694
|
|
|
695
|
+
const stepCount = counter - 1; // counter started at 1
|
|
696
|
+
|
|
697
|
+
for (const { el, target } of withStringTargets) {
|
|
698
|
+
const at = namedReveals.get(target);
|
|
699
|
+
if (at === undefined) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
`Honeydeck <${el.name} target="${target}"> could not find a same-slide <Reveal name="${target}"> target.`,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
setAtAttribute(el, at);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
for (const { el, at } of withNumericTargets) {
|
|
708
|
+
if (at > stepCount) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
`Honeydeck <${el.name} at={${at}}> targets step ${at}, but this slide only has ${stepCount} timeline step${stepCount === 1 ? "" : "s"}.`,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
410
715
|
// Store total step count on the vfile for the virtual modules plugin to read.
|
|
411
|
-
vfile.data.stepCount =
|
|
716
|
+
vfile.data.stepCount = stepCount;
|
|
412
717
|
};
|