@honeydeck/honeydeck 0.4.0 → 0.5.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.
Files changed (64) hide show
  1. package/DEVELOPMENT.md +4 -1
  2. package/Readme.md +2 -2
  3. package/SPEC.md +3 -3
  4. package/docs/components-browser-frame.md +34 -0
  5. package/docs/components-keyboard.md +31 -0
  6. package/docs/components-list-style.md +49 -0
  7. package/docs/components-notes.md +36 -0
  8. package/docs/components-reveal-group.md +58 -0
  9. package/docs/components-reveal-with.md +37 -0
  10. package/docs/components-reveal.md +33 -0
  11. package/docs/components-timeline-steps.md +48 -0
  12. package/docs/components.md +13 -54
  13. package/docs/configuration.md +11 -0
  14. package/docs/deeper-dive.md +30 -7
  15. package/docs/getting-started.md +2 -2
  16. package/docs/navigation.md +1 -1
  17. package/docs/pdf-export.md +4 -2
  18. package/docs/presenter-mode.md +6 -3
  19. package/docs/skills.md +3 -3
  20. package/docs/slidev-migration.md +3 -0
  21. package/docs/steps-and-reveals.md +143 -8
  22. package/package.json +4 -1
  23. package/skills/SPEC.md +2 -2
  24. package/skills/honeydeck/SKILL.md +2 -2
  25. package/skills/slidev-migration/SKILL.md +1 -0
  26. package/src/SPEC.md +8 -3
  27. package/src/cli/SPEC.md +3 -2
  28. package/src/cli/pdf.ts +11 -4
  29. package/src/remark/SPEC.md +102 -2
  30. package/src/remark/code-utils.ts +151 -0
  31. package/src/remark/shiki-code-blocks.ts +329 -136
  32. package/src/remark/step-numbering.ts +408 -103
  33. package/src/runtime/Deck.tsx +133 -116
  34. package/src/runtime/EffectiveColorModeContext.tsx +37 -0
  35. package/src/runtime/SPEC.md +21 -8
  36. package/src/runtime/SlideCanvas.tsx +19 -16
  37. package/src/runtime/SlideScaleContext.tsx +23 -0
  38. package/src/runtime/components/CodeBlock.tsx +19 -202
  39. package/src/runtime/components/CodeBlockCopyButton.tsx +64 -0
  40. package/src/runtime/components/CodeBlockShared.ts +17 -0
  41. package/src/runtime/components/Fade.tsx +51 -0
  42. package/src/runtime/components/FadeGroup.tsx +175 -0
  43. package/src/runtime/components/FadeWith.tsx +54 -0
  44. package/src/runtime/components/MagicCodeBlock.tsx +223 -0
  45. package/src/runtime/components/NavBar.tsx +1 -1
  46. package/src/runtime/components/NormalCodeBlock.tsx +128 -0
  47. package/src/runtime/components/Reveal.tsx +27 -27
  48. package/src/runtime/components/RevealGroup.tsx +143 -41
  49. package/src/runtime/components/RevealWith.tsx +63 -0
  50. package/src/runtime/components/SPEC.md +112 -7
  51. package/src/runtime/components/TimelineReveal.tsx +81 -0
  52. package/src/runtime/components/index.ts +13 -5
  53. package/src/runtime/components/timelineVisibility.ts +45 -0
  54. package/src/runtime/index.ts +9 -1
  55. package/src/runtime/navigation.ts +6 -4
  56. package/src/runtime/presentationApi.ts +449 -0
  57. package/src/runtime/views/PresenterCastButton.tsx +39 -0
  58. package/src/runtime/views/PresenterView.tsx +21 -4
  59. package/src/runtime/views/SPEC.md +7 -5
  60. package/src/theme/base.css +67 -2
  61. package/src/vite-plugin/SPEC.md +20 -2
  62. package/src/vite-plugin/index.ts +16 -2
  63. package/src/vite-plugin/splitter.ts +1 -0
  64. package/src/vite-plugin/virtual-modules.ts +16 -6
@@ -1,36 +1,45 @@
1
1
  /**
2
- * Remark plugin: assign `at` props to `<Reveal>` and `<RevealGroup>` elements
3
- * and count total timeline steps per slide.
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) and
8
- * injects `as="div"`/`as="span"` from the MDX flow/text context.
9
- * 2. Assigns `at={n}` to each `<RevealGroup>` element using the starting
10
- * index of the group and adds internal per-target step numbers for group
11
- * children/list items.
12
- * 3. Assigns `at={n}` to each `<TimelineSteps steps={n}>` element and
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
- * 4. Counts timeline steps from code fence `|`-separated groups after the
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
- * 5. Writes the total step count to `vfile.data.stepCount`.
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
- * ### Existing `at` props
24
- * If a `<Reveal>` or `<RevealGroup>` already has an explicit `at` prop, this
25
- * plugin leaves it untouched. Future V2 may expose `<Reveal at={2}>` as a
26
- * public API for out-of-order reveals. `<Reveal>` still receives a compiler
27
- * `as` prop when missing, so manually numbered inline reveals remain valid HTML.
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 reveals, groups,
32
- * and code walkthrough steps after the first baseline highlight consume later
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 injectRevealWrapperElement(el: MdxJsxElement): void {
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
- function countListItems(node: unknown): number | null {
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 getRevealGroupTargets(el: MdxJsxElement): unknown[] {
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
- const listItemCount = countListItems(child);
166
- const c = child as { type?: string; children?: unknown[] };
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 === "Reveal" ||
273
- node.name === "RevealGroup" ||
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 `<Reveal>` and `<RevealGroup>` elements with
296
- * sequential `at` props and stores the total step count in `vfile.data`.
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
- if (!hasAtProp(el)) {
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 === "Reveal") {
368
- injectRevealWrapperElement(el);
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 (!hasAtProp(el)) {
371
- el.attributes.push(makeAtAttribute(counter));
372
- counter++;
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 === "RevealGroup") {
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
- return;
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 targets = getRevealGroupTargets(el);
638
+ const listRevealMode =
639
+ el.name === "RevealGroup"
640
+ ? readRevealGroupListRevealMode(el)
641
+ : "direct";
384
642
  const targetSteps: number[] = [];
385
643
 
386
- el.attributes.push(makeAtAttribute(counter));
387
- if (targets.length === 0) {
388
- counter++;
389
- return;
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
- for (const target of targets) {
393
- targetSteps.push(counter);
679
+ if (targetSteps.length === 0) {
680
+ removeAttribute(el, "targetStepsJson");
394
681
  counter++;
395
- visitNode(target);
682
+ return;
396
683
  }
397
684
 
398
- el.attributes.push(
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 = counter - 1; // counter started at 1
716
+ vfile.data.stepCount = stepCount;
412
717
  };