@markdy/core 0.6.0 → 0.7.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/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@ type AssetDef = {
7
7
  value: string;
8
8
  };
9
9
  type ActorDef = {
10
- type: "sprite" | "text" | "box" | "figure";
11
- /** Constructor arguments: asset name for sprite, display text for text actors. */
10
+ type: "sprite" | "text" | "box" | "figure" | "caption";
11
+ /** Constructor arguments: asset name for sprite, display text for text/caption actors. */
12
12
  args: string[];
13
13
  x: number;
14
14
  y: number;
@@ -19,6 +19,12 @@ type ActorDef = {
19
19
  size?: number;
20
20
  /** Z-index for layering control (via the `z` modifier). */
21
21
  z?: number;
22
+ /**
23
+ * Semantic anchor for captions (`top`, `bottom`, `center`). Absent for
24
+ * non-caption actors. The parser also fills `x` and `y` from the anchor
25
+ * so renderers that don't understand the field still place the caption.
26
+ */
27
+ anchor?: "top" | "bottom" | "center";
22
28
  };
23
29
  type TimelineEvent = {
24
30
  time: number;
@@ -26,6 +32,11 @@ type TimelineEvent = {
26
32
  action: string;
27
33
  params: Record<string, unknown>;
28
34
  line: number;
35
+ /**
36
+ * The `scene "title" { ... }` block this event belongs to, if any.
37
+ * Undefined for events in the top-level scope.
38
+ */
39
+ chapter?: string;
29
40
  };
30
41
  type SceneMeta = {
31
42
  width: number;
@@ -61,6 +72,37 @@ type SequenceDef = {
61
72
  paramsRaw: string;
62
73
  }>;
63
74
  };
75
+ /**
76
+ * A `scene "title" { ... }` block — a named grouping of timeline events.
77
+ * Start/end times are inclusive wall-clock seconds so renderers and
78
+ * tooling can highlight the active chapter without re-walking events.
79
+ */
80
+ type Chapter = {
81
+ name: string;
82
+ startTime: number;
83
+ endTime: number;
84
+ startLine: number;
85
+ };
86
+ /**
87
+ * A non-fatal parse issue. Renderers should surface these via
88
+ * `onWarning` so the author can fix the underlying cause; the
89
+ * renderer otherwise no-ops the offending statement.
90
+ */
91
+ type ParseWarning = {
92
+ kind: "unknown-action" | "unknown-modifier" | "unknown-scene-key" | "unknown-camera-action" | "unknown-preset" | "import-unresolved" | "preset-mixed";
93
+ message: string;
94
+ line: number;
95
+ };
96
+ /**
97
+ * An `import "path.markdy" as ns` declaration. Parsing records the
98
+ * intent; the host (CLI, bundler) resolves the path and may supply
99
+ * pre-parsed ASTs via the `parse(..., { imports })` option.
100
+ */
101
+ type ImportDecl = {
102
+ path: string;
103
+ namespace: string;
104
+ line: number;
105
+ };
64
106
  type SceneAST = {
65
107
  meta: SceneMeta;
66
108
  assets: Record<string, AssetDef>;
@@ -72,12 +114,57 @@ type SceneAST = {
72
114
  seqs: Record<string, SequenceDef>;
73
115
  /** User-defined variables — kept in AST for tooling/inspection. */
74
116
  vars: Record<string, string>;
117
+ /** Named chapter blocks in author order. Empty when no chapters were used. */
118
+ chapters: Chapter[];
119
+ /** Soft parse issues. Always present; empty in the happy path. */
120
+ warnings: ParseWarning[];
121
+ /** `import` declarations in author order. Always present; empty when none were used. */
122
+ imports: ImportDecl[];
75
123
  };
76
124
 
77
125
  declare class ParseError extends Error {
78
126
  readonly line: number;
79
127
  constructor(message: string, line: number);
80
128
  }
81
- declare function parse(source: string): SceneAST;
129
+ interface ParseOptions {
130
+ /**
131
+ * Pre-parsed ASTs for `import "path" as ns` declarations. The host
132
+ * (CLI, bundler) is responsible for reading files from disk and
133
+ * parsing them; the parser itself is pure.
134
+ *
135
+ * When an import's namespace is present here, its `vars`, `defs`,
136
+ * and `seqs` are merged into the importing AST under the
137
+ * `<ns>.<name>` prefix. Missing namespaces produce a soft warning.
138
+ */
139
+ imports?: Record<string, SceneAST>;
140
+ /**
141
+ * Internal flag — distinguishes a top-level call from a recursive
142
+ * call used to expand a `preset`. Preset expansion bypasses the
143
+ * "mixed preset and other statements" warning because the expanded
144
+ * source is the only content.
145
+ */
146
+ _fromPreset?: boolean;
147
+ }
148
+ declare function parse(source: string, opts?: ParseOptions): SceneAST;
149
+
150
+ /**
151
+ * MarkdyScript built-in presets.
152
+ *
153
+ * A preset is a string template that expands at parse time into canonical
154
+ * MarkdyScript. The renderer never sees preset statements — by the time
155
+ * parsing reaches the actor/event stage, the preset has been replaced
156
+ * with its expansion.
157
+ *
158
+ * Design rules:
159
+ * 1. Each preset is self-contained — it declares its own `scene`,
160
+ * actors, and timeline.
161
+ * 2. Presets are short. The value is in `preset <name>` being a
162
+ * one-liner that expands to a scaffold the user can then tweak.
163
+ * 3. Presets share a common visual grammar so a feed of them feels
164
+ * like a family, not a collage.
165
+ */
166
+ type PresetFn = (args: string[]) => string;
167
+ declare const PRESETS: Record<string, PresetFn>;
168
+ declare const PRESET_NAMES: readonly string[];
82
169
 
83
- export { type ActorDef, type AssetDef, ParseError, type SceneAST, type SceneMeta, type SequenceDef, type TemplateDef, type TimelineEvent, parse };
170
+ export { type ActorDef, type AssetDef, type Chapter, type ImportDecl, PRESETS, PRESET_NAMES, ParseError, type ParseOptions, type ParseWarning, type PresetFn, type SceneAST, type SceneMeta, type SequenceDef, type TemplateDef, type TimelineEvent, parse };
package/dist/index.js CHANGED
@@ -1,3 +1,229 @@
1
+ // src/presets.ts
2
+ function quote(s) {
3
+ const stripped = s.replace(/^"|"$/g, "");
4
+ return `"${stripped.replace(/"/g, '\\"')}"`;
5
+ }
6
+ var PRESETS = {
7
+ meme: (args) => {
8
+ const top = quote(args[0] ?? "when the code works");
9
+ const bottom = quote(args[1] ?? "and you don't know why");
10
+ return `scene width=720 height=720 bg=#111
11
+
12
+ actor top = caption(${top}) at top
13
+ actor hero = figure(#c68642, m, \u{1F60E}) at (360, 430)
14
+ actor bottom = caption(${bottom}) at bottom
15
+
16
+ @0.0: top.fade_in(dur=0.4)
17
+ @0.4: hero.enter(from=bottom, dur=0.6)
18
+ @1.2: hero.face("\u{1F602}")
19
+ @1.8: bottom.fade_in(dur=0.4)
20
+ @3.2: hero.bounce(intensity=20, count=2, dur=0.8)
21
+ `;
22
+ },
23
+ explainer: (args) => {
24
+ const title = quote(args[0] ?? "how it works");
25
+ return `scene width=960 height=540 bg=#0d1117
26
+
27
+ actor title = caption(${title}) at top
28
+ actor hero = figure(#c68642, m, \u{1F60E}) at (480, 360)
29
+
30
+ scene "intro" {
31
+ @+0.0: title.fade_in(dur=0.5)
32
+ @+1.0: hero.enter(from=bottom, dur=0.5)
33
+ }
34
+
35
+ scene "body" {
36
+ @+0.0: camera.zoom(to=1.2, dur=0.8)
37
+ @+1.0: hero.wave(side=right, dur=0.5)
38
+ }
39
+ `;
40
+ },
41
+ reaction: (args) => {
42
+ const line = quote(args[0] ?? "wait, what?");
43
+ return `scene width=720 height=720 bg=#fff5f9
44
+
45
+ actor hero = figure(#c68642, m, \u{1F642}) at (360, 380)
46
+
47
+ @0.0: hero.enter(from=left, dur=0.4)
48
+ @0.5: hero.face("\u{1F633}")
49
+ @0.5: hero.say(${line}, dur=1.4)
50
+ @2.0: hero.face("\u{1F602}")
51
+ @2.0: hero.shake(intensity=6, dur=0.5)
52
+ `;
53
+ },
54
+ pov: (args) => {
55
+ const pov = quote(args[0] ?? "POV: you hit ship");
56
+ return `scene width=720 height=960 bg=#0d1117
57
+
58
+ actor label = caption(${pov}) at top
59
+ actor hero = figure(#c68642, m, \u{1F60E}) at (360, 600)
60
+
61
+ @0.0: label.fade_in(dur=0.4)
62
+ @0.4: hero.enter(from=bottom, dur=0.6)
63
+ @1.2: hero.pose(arm_left=70, arm_right=-70, dur=0.3)
64
+ @1.2: hero.face("\u{1F525}")
65
+ `;
66
+ },
67
+ typing: (args) => {
68
+ const text = quote(args[0] ?? "hello world");
69
+ return `scene width=800 height=300 bg=#0f1115
70
+
71
+ actor cursor = text("|") at (120, 150) size 40 opacity 0
72
+ actor line = text(${text}) at (140, 150) size 32 opacity 0
73
+
74
+ @0.0: cursor.fade_in(dur=0.15)
75
+ @+0.1: cursor.fade_out(dur=0.15)
76
+ @+0.0: cursor.fade_in(dur=0.15)
77
+ @+0.1: cursor.fade_out(dur=0.15)
78
+ @+0.0: cursor.fade_in(dur=0.15)
79
+ @+0.0: line.fade_in(dur=0.8)
80
+ @+0.3: cursor.fade_out(dur=0.3)
81
+ `;
82
+ },
83
+ terminal: (args) => {
84
+ const cmd = quote(args[0] ?? "$ npx markdy");
85
+ const output = quote(args[1] ?? "playground ready at http://localhost:4242");
86
+ return `scene width=960 height=420 bg=#0d1117
87
+
88
+ actor prompt = text(${cmd}) at (60, 140) size 24 opacity 0
89
+ actor result = text(${output}) at (60, 200) size 22 opacity 0
90
+
91
+ @0.0: prompt.fade_in(dur=0.4)
92
+ @1.0: result.fade_in(dur=0.4)
93
+ `;
94
+ },
95
+ chat_bubble: (args) => {
96
+ const a = quote(args[0] ?? "how do I animate this?");
97
+ const b = quote(args[1] ?? "ask markdy.");
98
+ return `scene width=800 height=400 bg=#f6f7fb
99
+
100
+ actor alice = figure(#fad4c0, f, \u{1F642}) at (180, 240)
101
+ actor bob = figure(#c68642, m, \u{1F642}) at (620, 240)
102
+
103
+ @0.0: alice.enter(from=left, dur=0.4)
104
+ @0.2: bob.enter(from=right, dur=0.4)
105
+ @0.8: alice.say(${a}, dur=1.8)
106
+ @2.6: bob.face("\u{1F60E}")
107
+ @2.6: bob.say(${b}, dur=1.6)
108
+ `;
109
+ },
110
+ vs: (args) => {
111
+ const left = quote(args[0] ?? "team a");
112
+ const right = quote(args[1] ?? "team b");
113
+ return `scene width=960 height=540 bg=#111
114
+
115
+ actor a = caption(${left}) at top
116
+ actor b = caption(${right}) at bottom
117
+ actor lf = figure(#c68642, m, \u{1F624}) at (260, 320)
118
+ actor rf = figure(#8d5524, m, \u{1F60F}) at (700, 320)
119
+
120
+ @0.0: a.fade_in(dur=0.3)
121
+ @0.0: b.fade_in(dur=0.3)
122
+ @0.3: lf.enter(from=left, dur=0.5)
123
+ @0.3: rf.enter(from=right, dur=0.5)
124
+ @1.2: lf.punch(side=right, dur=0.3)
125
+ @1.3: rf.shake(intensity=8, dur=0.4)
126
+ `;
127
+ },
128
+ tutorial_step: (args) => {
129
+ const n = quote(args[0] ?? "Step 1");
130
+ const body = quote(args[1] ?? "open your editor");
131
+ return `scene width=960 height=420 bg=white
132
+
133
+ actor step = caption(${n}) at top
134
+ actor descr = text(${body}) at (100, 220) size 32 opacity 0
135
+
136
+ @0.0: step.fade_in(dur=0.4)
137
+ @0.5: descr.fade_in(dur=0.4)
138
+ @1.2: descr.move(to=(120, 220), dur=0.5, ease=out)
139
+ `;
140
+ },
141
+ countdown: (args) => {
142
+ const to = quote(args[0] ?? "launch");
143
+ return `scene width=600 height=600 bg=#0d1117
144
+
145
+ actor three = text("3") at (270, 280) size 120 opacity 0
146
+ actor two = text("2") at (270, 280) size 120 opacity 0
147
+ actor one = text("1") at (270, 280) size 120 opacity 0
148
+ actor go = caption(${to}) at center
149
+
150
+ @0.0: three.fade_in(dur=0.2)
151
+ @0.9: three.fade_out(dur=0.2)
152
+ @0.9: two.fade_in(dur=0.2)
153
+ @1.8: two.fade_out(dur=0.2)
154
+ @1.8: one.fade_in(dur=0.2)
155
+ @2.7: one.fade_out(dur=0.2)
156
+ @2.7: go.fade_in(dur=0.4)
157
+ `;
158
+ },
159
+ reveal: (args) => {
160
+ const secret = quote(args[0] ?? "and that's the trick");
161
+ return `scene width=800 height=450 bg=#0d1117
162
+
163
+ actor cover = box() at (350, 175) scale 10 opacity 1
164
+ actor reveal = caption(${secret}) at center
165
+
166
+ @0.4: cover.fade_out(dur=0.5)
167
+ @0.4: reveal.fade_in(dur=0.5)
168
+ `;
169
+ },
170
+ glitch: (_args) => {
171
+ const txt = quote("GLITCH");
172
+ return `scene width=800 height=400 bg=#000
173
+
174
+ actor t = text(${txt}) at (260, 170) size 72
175
+
176
+ @0.0: t.fade_in(dur=0.2)
177
+ @0.4: t.shake(intensity=10, dur=0.3)
178
+ @0.8: t.shake(intensity=6, dur=0.3)
179
+ @1.3: t.fade_out(dur=0.3)
180
+ `;
181
+ },
182
+ zoom_punchline: (args) => {
183
+ const line = quote(args[0] ?? "the punchline");
184
+ return `scene width=800 height=450 bg=#0d1117
185
+
186
+ actor line = caption(${line}) at center
187
+
188
+ @0.0: line.fade_in(dur=0.4)
189
+ @0.6: camera.zoom(to=1.4, dur=0.8, ease=out)
190
+ @1.6: camera.shake(intensity=6, dur=0.4)
191
+ `;
192
+ },
193
+ before_after: (args) => {
194
+ const before = quote(args[0] ?? "before");
195
+ const after = quote(args[1] ?? "after");
196
+ return `scene width=960 height=420 bg=#f6f7fb
197
+
198
+ actor a = caption(${before}) at top
199
+ actor b = caption(${after}) at bottom
200
+ actor dude = figure(#c68642, m, \u{1F635}) at (480, 240)
201
+
202
+ @0.0: a.fade_in(dur=0.3)
203
+ @0.0: dude.enter(from=left, dur=0.5)
204
+ @1.4: dude.face("\u{1F60E}")
205
+ @1.4: b.fade_in(dur=0.3)
206
+ `;
207
+ },
208
+ tier_list: (_args) => {
209
+ return `scene width=960 height=540 bg=#111
210
+
211
+ actor s = text("S") at (60, 90) size 72 opacity 0
212
+ actor a = text("A") at (60, 200) size 72 opacity 0
213
+ actor b = text("B") at (60, 310) size 72 opacity 0
214
+ actor c = text("C") at (60, 420) size 72 opacity 0
215
+ actor title = caption("tier list") at top
216
+
217
+ @0.0: title.fade_in(dur=0.3)
218
+ @0.3: s.fade_in(dur=0.2)
219
+ @0.6: a.fade_in(dur=0.2)
220
+ @0.9: b.fade_in(dur=0.2)
221
+ @1.2: c.fade_in(dur=0.2)
222
+ `;
223
+ }
224
+ };
225
+ var PRESET_NAMES = Object.keys(PRESETS);
226
+
1
227
  // src/parser.ts
2
228
  var ParseError = class extends Error {
3
229
  constructor(message, line) {
@@ -37,9 +263,15 @@ function stripComment(line) {
37
263
  function splitByComma(s) {
38
264
  const parts = [];
39
265
  let depth = 0;
266
+ let inString = false;
40
267
  let start = 0;
41
268
  for (let i = 0; i < s.length; i++) {
42
269
  const ch = s[i];
270
+ if (ch === '"') {
271
+ inString = !inString;
272
+ continue;
273
+ }
274
+ if (inString) continue;
43
275
  if (ch === "(") depth++;
44
276
  else if (ch === ")") depth--;
45
277
  else if (ch === "," && depth === 0) {
@@ -93,32 +325,124 @@ function parseActionParams(action, raw) {
93
325
  }
94
326
  return params;
95
327
  }
96
- function parseModifiers(raw) {
328
+ var UNIVERSAL_ACTIONS = /* @__PURE__ */ new Set([
329
+ "enter",
330
+ "exit",
331
+ "move",
332
+ "fade_in",
333
+ "fade_out",
334
+ "scale",
335
+ "rotate",
336
+ "shake",
337
+ "say",
338
+ "throw",
339
+ "play"
340
+ ]);
341
+ var FIGURE_ONLY_ACTIONS = /* @__PURE__ */ new Set([
342
+ "punch",
343
+ "kick",
344
+ "wave",
345
+ "nod",
346
+ "jump",
347
+ "bounce",
348
+ "face",
349
+ "rotate_part",
350
+ "pose"
351
+ ]);
352
+ var CAMERA_ACTIONS = /* @__PURE__ */ new Set(["pan", "zoom", "shake"]);
353
+ function isKnownAction(actorType, action) {
354
+ if (actorType === "camera") return CAMERA_ACTIONS.has(action);
355
+ if (UNIVERSAL_ACTIONS.has(action)) return true;
356
+ return FIGURE_ONLY_ACTIONS.has(action);
357
+ }
358
+ var MODIFIER_KEYS = /* @__PURE__ */ new Set(["scale", "rotate", "opacity", "size", "z"]);
359
+ function parseSpaceModifiers(raw, lineNum, warnings) {
97
360
  const result = {};
98
361
  const tokens = raw.trim().split(/\s+/).filter(Boolean);
99
362
  for (let i = 0; i + 1 < tokens.length; i += 2) {
100
363
  const key = tokens[i];
101
364
  const val = Number(tokens[i + 1]);
102
365
  if (Number.isNaN(val)) continue;
103
- if (key === "scale") result.scale = val;
104
- else if (key === "rotate") result.rotate = val;
105
- else if (key === "opacity") result.opacity = val;
106
- else if (key === "size") result.size = val;
107
- else if (key === "z") result.z = val;
366
+ if (MODIFIER_KEYS.has(key)) {
367
+ result[key] = val;
368
+ } else {
369
+ warnings.push({
370
+ kind: "unknown-modifier",
371
+ message: `unknown modifier "${key}" \u2014 ignored`,
372
+ line: lineNum
373
+ });
374
+ }
375
+ }
376
+ return result;
377
+ }
378
+ function parseWithModifiers(raw, lineNum, warnings) {
379
+ const result = {};
380
+ for (const token of splitByComma(raw)) {
381
+ const t = token.trim();
382
+ if (!t) continue;
383
+ const eqIdx = t.indexOf("=");
384
+ if (eqIdx === -1) {
385
+ warnings.push({
386
+ kind: "unknown-modifier",
387
+ message: `expected "key=value" in with-clause, got "${t}"`,
388
+ line: lineNum
389
+ });
390
+ continue;
391
+ }
392
+ const key = t.slice(0, eqIdx).trim();
393
+ const val = Number(t.slice(eqIdx + 1).trim());
394
+ if (Number.isNaN(val)) {
395
+ warnings.push({
396
+ kind: "unknown-modifier",
397
+ message: `modifier "${key}" needs a numeric value \u2014 ignored`,
398
+ line: lineNum
399
+ });
400
+ continue;
401
+ }
402
+ if (MODIFIER_KEYS.has(key)) {
403
+ result[key] = val;
404
+ } else {
405
+ warnings.push({
406
+ kind: "unknown-modifier",
407
+ message: `unknown modifier "${key}" \u2014 ignored`,
408
+ line: lineNum
409
+ });
410
+ }
108
411
  }
109
412
  return result;
110
413
  }
414
+ function parseActorTrailer(trailerRaw, lineNum, warnings) {
415
+ const trimmed = trailerRaw.trim();
416
+ if (!trimmed) return {};
417
+ const withMatch = /(^|\s)with(\s|$)/.exec(trimmed);
418
+ if (!withMatch) {
419
+ return parseSpaceModifiers(trimmed, lineNum, warnings);
420
+ }
421
+ const before = trimmed.slice(0, withMatch.index).trim();
422
+ const after = trimmed.slice(withMatch.index + withMatch[0].length).trim();
423
+ const fromSpace = before ? parseSpaceModifiers(before, lineNum, warnings) : {};
424
+ const fromWith = after ? parseWithModifiers(after, lineNum, warnings) : {};
425
+ return { ...fromSpace, ...fromWith };
426
+ }
111
427
  var ASSET_RE = /^asset\s+(\w+)\s*=\s*(image|icon)\("([^"]+)"\)$/;
112
- var BUILTIN_ACTOR_TYPES = /* @__PURE__ */ new Set(["sprite", "text", "box", "figure"]);
113
- var ACTOR_RE = /^actor\s+(\w+)\s*=\s*(\w+)\(([^)]*)\)\s+at\s+\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)(.*)$/;
114
- var EVENT_RE = /^@([\d.]+):\s+(\w+)\.(\w+)\((.*)\)$/;
428
+ var BUILTIN_ACTOR_TYPES = /* @__PURE__ */ new Set(["sprite", "text", "box", "figure", "caption"]);
429
+ var ACTOR_NUM_POS_RE = /^actor\s+(\w+)\s*=\s*([\w.]+)\(([^)]*)\)\s+at\s+\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)(.*)$/;
430
+ var ACTOR_ANCHOR_POS_RE = /^actor\s+(\w+)\s*=\s*([\w.]+)\(([^)]*)\)\s+at\s+(top|bottom|center)\b(.*)$/;
431
+ var EVENT_RE = /^@([\d.]+):\s+(\w+)\.(!?\w+)\((.*)\)$/;
432
+ var REL_EVENT_RE = /^@\+([\d.]+):\s+(\w+)\.(!?\w+)\((.*)\)$/;
115
433
  var VAR_RE = /^var\s+(\w+)\s*=\s*(.+)$/;
116
434
  var DEF_HEADER_RE = /^def\s+(\w+)\(([^)]*)\)\s*\{$/;
117
- var DEF_BODY_RE = /^\s*(sprite|text|box|figure)\(([^)]*)\)\s*$/;
435
+ var DEF_BODY_RE = /^\s*(sprite|text|box|figure|caption)\(([^)]*)\)\s*$/;
118
436
  var SEQ_HEADER_RE = /^seq\s+(\w+)(?:\(([^)]*)\))?\s*\{$/;
119
- var SEQ_EVENT_RE = /^@\+([\d.]+):\s+\$\.(\w+)\((.*)\)$/;
437
+ var SEQ_EVENT_RE = /^@\+([\d.]+):\s+\$\.(!?\w+)\((.*)\)$/;
438
+ var CHAPTER_HEADER_RE = /^scene\s+"([^"]+)"\s*\{$/;
439
+ var IMPORT_RE = /^import\s+"([^"]+)"\s+as\s+(\w+)\s*$/;
440
+ var PRESET_RE = /^preset\s+(\w+)(?:\s*\((.*)\))?\s*$/;
120
441
  function interpolate(s, vars) {
121
- return s.replace(/\\?\$\{(\w+)\}/g, (_, name) => vars[name] ?? `\${${name}}`);
442
+ return s.replace(
443
+ /\\?\$\{([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\}/g,
444
+ (_, name) => vars[name] ?? `\${${name}}`
445
+ );
122
446
  }
123
447
  var DEFAULTS = {
124
448
  width: 800,
@@ -126,6 +450,10 @@ var DEFAULTS = {
126
450
  fps: 30,
127
451
  bg: "white"
128
452
  };
453
+ var CAPTION_TOP_Y_FRAC = 0.12;
454
+ var CAPTION_BOTTOM_Y_FRAC = 0.88;
455
+ var CAPTION_CENTER_Y_FRAC = 0.5;
456
+ var KNOWN_SCENE_KEYS = /* @__PURE__ */ new Set(["width", "height", "fps", "bg", "duration"]);
129
457
  function validateMoveTarget(action, params, meta, actor, line) {
130
458
  if (action !== "move") return;
131
459
  const to = params.to;
@@ -138,7 +466,11 @@ function validateMoveTarget(action, params, meta, actor, line) {
138
466
  );
139
467
  }
140
468
  }
141
- function parse(source) {
469
+ function parse(source, opts = {}) {
470
+ if (!opts._fromPreset) {
471
+ const expansion = tryExpandSolePreset(source);
472
+ if (expansion) return parse(expansion, { ...opts, _fromPreset: true });
473
+ }
142
474
  const ast = {
143
475
  meta: { ...DEFAULTS },
144
476
  assets: {},
@@ -146,13 +478,18 @@ function parse(source) {
146
478
  events: [],
147
479
  defs: {},
148
480
  seqs: {},
149
- vars: {}
481
+ vars: {},
482
+ chapters: [],
483
+ warnings: [],
484
+ imports: []
150
485
  };
151
486
  let sceneFound = false;
152
487
  const lines = source.split(/\r?\n/);
153
488
  let inDef = null;
154
489
  let defNeedsClose = false;
155
490
  let inSeq = null;
491
+ let inChapter = null;
492
+ const topScope = { name: "", prevEnd: 0 };
156
493
  for (let i = 0; i < lines.length; i++) {
157
494
  const lineNum = i + 1;
158
495
  const rawUntouched = lines[i].trim();
@@ -182,6 +519,19 @@ function parse(source) {
182
519
  inSeq = null;
183
520
  continue;
184
521
  }
522
+ if (inChapter) {
523
+ const endTime = inChapter.scope.prevEnd;
524
+ const startTime = inChapter.earliestEventTime === Infinity ? inChapter.openedAt : Math.min(inChapter.openedAt, inChapter.earliestEventTime);
525
+ ast.chapters.push({
526
+ name: inChapter.name,
527
+ startLine: inChapter.startLine,
528
+ startTime,
529
+ endTime
530
+ });
531
+ if (endTime > topScope.prevEnd) topScope.prevEnd = endTime;
532
+ inChapter = null;
533
+ continue;
534
+ }
185
535
  throw new ParseError("Unexpected '}'", lineNum);
186
536
  }
187
537
  if (inDef) {
@@ -207,11 +557,35 @@ function parse(source) {
207
557
  throw new ParseError(`Invalid seq event (expected "@+offset: $.action(params)"): ${raw}`, lineNum);
208
558
  }
209
559
  const [, offsetStr, action, paramsRaw] = sm;
210
- inSeq.events.push({
211
- offset: Number(offsetStr),
212
- action,
213
- paramsRaw
214
- });
560
+ inSeq.events.push({ offset: Number(offsetStr), action, paramsRaw });
561
+ continue;
562
+ }
563
+ if (raw.startsWith("import ")) {
564
+ const im = IMPORT_RE.exec(raw);
565
+ if (!im) {
566
+ throw new ParseError(`Invalid import declaration: ${raw}`, lineNum);
567
+ }
568
+ const [, path, namespace] = im;
569
+ const decl = { path, namespace, line: lineNum };
570
+ ast.imports.push(decl);
571
+ const resolved = opts.imports?.[namespace];
572
+ if (!resolved) {
573
+ ast.warnings.push({
574
+ kind: "import-unresolved",
575
+ message: `import "${path}" as ${namespace} \u2014 no pre-parsed AST supplied by host`,
576
+ line: lineNum
577
+ });
578
+ } else {
579
+ for (const [k, v] of Object.entries(resolved.vars)) {
580
+ ast.vars[`${namespace}.${k}`] = v;
581
+ }
582
+ for (const [k, v] of Object.entries(resolved.defs)) {
583
+ ast.defs[`${namespace}.${k}`] = v;
584
+ }
585
+ for (const [k, v] of Object.entries(resolved.seqs)) {
586
+ ast.seqs[`${namespace}.${k}`] = v;
587
+ }
588
+ }
215
589
  continue;
216
590
  }
217
591
  if (raw.startsWith("def ")) {
@@ -235,6 +609,30 @@ function parse(source) {
235
609
  continue;
236
610
  }
237
611
  if (/^scene(\s|$)/.test(raw)) {
612
+ const chm = CHAPTER_HEADER_RE.exec(raw);
613
+ if (chm) {
614
+ if (inChapter) {
615
+ throw new ParseError(
616
+ `Nested chapters are not supported; close chapter "${inChapter.name}" first`,
617
+ lineNum
618
+ );
619
+ }
620
+ const chapterName = chm[1];
621
+ inChapter = {
622
+ name: chapterName,
623
+ startLine: lineNum,
624
+ openedAt: topScope.prevEnd,
625
+ earliestEventTime: Infinity,
626
+ scope: { name: chapterName, prevEnd: topScope.prevEnd }
627
+ };
628
+ continue;
629
+ }
630
+ if (inChapter) {
631
+ throw new ParseError(
632
+ `scene header inside chapter "${inChapter.name}" is not allowed; close the chapter first or use 'scene "title" { ... }' for a nested section`,
633
+ lineNum
634
+ );
635
+ }
238
636
  if (sceneFound) {
239
637
  throw new ParseError("Duplicate scene declaration", lineNum);
240
638
  }
@@ -257,7 +655,12 @@ function parse(source) {
257
655
  ast.meta.duration = Number(val);
258
656
  break;
259
657
  default:
260
- throw new ParseError(`Unknown scene property: ${key}`, lineNum);
658
+ if (KNOWN_SCENE_KEYS.has(key)) break;
659
+ ast.warnings.push({
660
+ kind: "unknown-scene-key",
661
+ message: `unknown scene property "${key}" \u2014 ignored`,
662
+ line: lineNum
663
+ });
261
664
  }
262
665
  }
263
666
  continue;
@@ -271,107 +674,31 @@ function parse(source) {
271
674
  ast.assets[name] = { type, value };
272
675
  continue;
273
676
  }
274
- if (raw.startsWith("actor")) {
275
- const m = ACTOR_RE.exec(raw);
276
- if (!m) {
277
- throw new ParseError(`Invalid actor declaration: ${raw}`, lineNum);
278
- }
279
- const [, name, typeName, argsRaw, xStr, yStr, modifiersRaw] = m;
280
- let resolvedType;
281
- let resolvedArgs;
282
- if (BUILTIN_ACTOR_TYPES.has(typeName)) {
283
- resolvedType = typeName;
284
- resolvedArgs = argsRaw.trim() ? splitByComma(argsRaw).map((a) => {
285
- const t = a.trim();
286
- return t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t;
287
- }) : [];
288
- } else if (ast.defs[typeName]) {
289
- const tmpl = ast.defs[typeName];
290
- const callArgs = argsRaw.trim() ? splitByComma(argsRaw).map((a) => {
291
- const t = a.trim();
292
- return t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t;
293
- }) : [];
294
- const localVars = {};
295
- for (let pi = 0; pi < tmpl.params.length; pi++) {
296
- localVars[tmpl.params[pi]] = callArgs[pi] ?? "";
297
- }
298
- resolvedType = tmpl.actorType;
299
- resolvedArgs = tmpl.bodyArgs.map((a) => interpolate(a, localVars));
677
+ if (raw.startsWith("preset ")) {
678
+ const pm = PRESET_RE.exec(raw);
679
+ const maybeName = pm?.[1];
680
+ if (maybeName && !PRESETS[maybeName]) {
681
+ const nameList = Object.keys(PRESETS).sort().join(", ");
682
+ ast.warnings.push({
683
+ kind: "unknown-preset",
684
+ message: `unknown preset "${maybeName}" \u2014 available: ${nameList}`,
685
+ line: lineNum
686
+ });
300
687
  } else {
301
- throw new ParseError(`Unknown actor type or template: "${typeName}"`, lineNum);
688
+ ast.warnings.push({
689
+ kind: "preset-mixed",
690
+ message: "`preset` is a whole-file shorthand; mid-file presets are ignored",
691
+ line: lineNum
692
+ });
302
693
  }
303
- const modifiers = parseModifiers(modifiersRaw);
304
- const x = Number(xStr);
305
- const y = Number(yStr);
306
- if (x < 0 || x > ast.meta.width || y < 0 || y > ast.meta.height) {
307
- throw new ParseError(
308
- `Actor "${name}" position (${x}, ${y}) is outside scene bounds (0\u2013${ast.meta.width}, 0\u2013${ast.meta.height})`,
309
- lineNum
310
- );
311
- }
312
- ast.actors[name] = {
313
- type: resolvedType,
314
- args: resolvedArgs,
315
- x,
316
- y,
317
- ...modifiers
318
- };
694
+ continue;
695
+ }
696
+ if (raw.startsWith("actor ")) {
697
+ parseActorLine(raw, lineNum, ast);
319
698
  continue;
320
699
  }
321
700
  if (raw.startsWith("@")) {
322
- const m = EVENT_RE.exec(raw);
323
- if (!m) {
324
- throw new ParseError(`Invalid event: ${raw}`, lineNum);
325
- }
326
- const [, timeStr, actor, action, paramsRaw] = m;
327
- const time = Number(timeStr);
328
- if (Number.isNaN(time)) {
329
- throw new ParseError(`Invalid time value: ${timeStr}`, lineNum);
330
- }
331
- if (!ast.actors[actor]) {
332
- throw new ParseError(`Unknown actor: "${actor}"`, lineNum);
333
- }
334
- if (action === "play") {
335
- const playParts = splitByComma(paramsRaw);
336
- const seqName = playParts[0]?.trim();
337
- if (!seqName || !ast.seqs[seqName]) {
338
- throw new ParseError(`Unknown sequence: "${seqName}"`, lineNum);
339
- }
340
- const seq = ast.seqs[seqName];
341
- const playVars = {};
342
- for (let pi = 1; pi < playParts.length; pi++) {
343
- const eqIdx = playParts[pi].indexOf("=");
344
- if (eqIdx !== -1) {
345
- const k = playParts[pi].slice(0, eqIdx).trim();
346
- const v = playParts[pi].slice(eqIdx + 1).trim();
347
- playVars[k] = v;
348
- }
349
- }
350
- let posIdx = 0;
351
- for (let pi = 1; pi < playParts.length; pi++) {
352
- if (!playParts[pi].includes("=") && posIdx < seq.params.length) {
353
- playVars[seq.params[posIdx]] = playParts[pi].trim();
354
- posIdx++;
355
- }
356
- }
357
- for (const sev of seq.events) {
358
- const expandedParams = interpolate(sev.paramsRaw, playVars);
359
- const absTime = Math.round((time + sev.offset) * 1e3) / 1e3;
360
- const params2 = parseActionParams(sev.action, expandedParams);
361
- validateMoveTarget(sev.action, params2, ast.meta, actor, lineNum);
362
- ast.events.push({
363
- time: absTime,
364
- actor,
365
- action: sev.action,
366
- params: params2,
367
- line: lineNum
368
- });
369
- }
370
- continue;
371
- }
372
- const params = parseActionParams(action, paramsRaw);
373
- validateMoveTarget(action, params, ast.meta, actor, lineNum);
374
- ast.events.push({ time, actor, action, params, line: lineNum });
701
+ parseEventLine(raw, lineNum, ast, inChapter, topScope);
375
702
  continue;
376
703
  }
377
704
  throw new ParseError(`Unrecognized statement: ${raw}`, lineNum);
@@ -385,17 +712,297 @@ function parse(source) {
385
712
  if (inSeq) {
386
713
  throw new ParseError(`Unclosed seq block "${inSeq.name}"`, inSeq.startLine);
387
714
  }
715
+ if (inChapter) {
716
+ throw new ParseError(`Unclosed chapter "${inChapter.name}"`, inChapter.startLine);
717
+ }
388
718
  if (ast.meta.duration === void 0) {
389
719
  let maxEnd = 0;
390
720
  for (const ev of ast.events) {
391
721
  const dur = typeof ev.params.dur === "number" ? ev.params.dur : 0;
392
722
  maxEnd = Math.max(maxEnd, ev.time + dur);
393
723
  }
394
- if (maxEnd > 0) ast.meta.duration = maxEnd;
724
+ if (maxEnd > 0) ast.meta.duration = round3(maxEnd);
395
725
  }
396
726
  return ast;
397
727
  }
728
+ function parseActorCallArgs(argsRaw) {
729
+ if (!argsRaw.trim()) return [];
730
+ return splitByComma(argsRaw).map((a) => {
731
+ const t = a.trim();
732
+ return t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t;
733
+ });
734
+ }
735
+ function parseActorLine(raw, lineNum, ast) {
736
+ const amAnchor = ACTOR_ANCHOR_POS_RE.exec(raw);
737
+ const amNum = amAnchor ? null : ACTOR_NUM_POS_RE.exec(raw);
738
+ if (!amAnchor && !amNum) {
739
+ throw new ParseError(`Invalid actor declaration: ${raw}`, lineNum);
740
+ }
741
+ let name;
742
+ let typeName;
743
+ let argsRaw;
744
+ let x;
745
+ let y;
746
+ let anchor;
747
+ let trailer;
748
+ if (amAnchor) {
749
+ const [, nm, tn, ar, an, tr] = amAnchor;
750
+ name = nm;
751
+ typeName = tn;
752
+ argsRaw = ar;
753
+ anchor = an;
754
+ trailer = tr;
755
+ x = ast.meta.width / 2;
756
+ switch (anchor) {
757
+ case "top":
758
+ y = Math.round(ast.meta.height * CAPTION_TOP_Y_FRAC);
759
+ break;
760
+ case "bottom":
761
+ y = Math.round(ast.meta.height * CAPTION_BOTTOM_Y_FRAC);
762
+ break;
763
+ default:
764
+ y = Math.round(ast.meta.height * CAPTION_CENTER_Y_FRAC);
765
+ break;
766
+ }
767
+ } else {
768
+ const [, nm, tn, ar, xs, ys, tr] = amNum;
769
+ name = nm;
770
+ typeName = tn;
771
+ argsRaw = ar;
772
+ x = Number(xs);
773
+ y = Number(ys);
774
+ trailer = tr;
775
+ }
776
+ if (name === "camera") {
777
+ throw new ParseError(
778
+ `"camera" is a reserved actor name; drop this declaration and use camera.pan/zoom/shake directly`,
779
+ lineNum
780
+ );
781
+ }
782
+ const rawArgs = parseActorCallArgs(argsRaw);
783
+ let resolvedType;
784
+ let resolvedArgs;
785
+ if (BUILTIN_ACTOR_TYPES.has(typeName)) {
786
+ resolvedType = typeName;
787
+ resolvedArgs = rawArgs;
788
+ } else if (ast.defs[typeName]) {
789
+ const tmpl = ast.defs[typeName];
790
+ const localVars = {};
791
+ for (let pi = 0; pi < tmpl.params.length; pi++) {
792
+ localVars[tmpl.params[pi]] = rawArgs[pi] ?? "";
793
+ }
794
+ resolvedType = tmpl.actorType;
795
+ resolvedArgs = tmpl.bodyArgs.map((a) => interpolate(a, localVars));
796
+ } else {
797
+ throw new ParseError(`Unknown actor type or template: "${typeName}"`, lineNum);
798
+ }
799
+ const modifiers = parseActorTrailer(trailer, lineNum, ast.warnings);
800
+ if (anchor && resolvedType !== "caption") {
801
+ throw new ParseError(
802
+ `anchor syntax "at ${anchor}" only applies to caption actors; got ${typeName}`,
803
+ lineNum
804
+ );
805
+ }
806
+ if (resolvedType === "caption" && !anchor) {
807
+ throw new ParseError(
808
+ `Caption actors require anchor syntax (\`at top | bottom | center\`); got numeric position (${x}, ${y}) for "${name}"`,
809
+ lineNum
810
+ );
811
+ }
812
+ if (!anchor && (x < 0 || x > ast.meta.width || y < 0 || y > ast.meta.height)) {
813
+ throw new ParseError(
814
+ `Actor "${name}" position (${x}, ${y}) is outside scene bounds (0\u2013${ast.meta.width}, 0\u2013${ast.meta.height})`,
815
+ lineNum
816
+ );
817
+ }
818
+ ast.actors[name] = {
819
+ type: resolvedType,
820
+ args: resolvedArgs,
821
+ x,
822
+ y,
823
+ ...modifiers,
824
+ ...anchor ? { anchor } : {}
825
+ };
826
+ }
827
+ function parseEventLine(raw, lineNum, ast, inChapter, topScope) {
828
+ const scope = inChapter?.scope ?? topScope;
829
+ const recordEventTime = (t) => {
830
+ if (inChapter && t < inChapter.earliestEventTime) {
831
+ inChapter.earliestEventTime = t;
832
+ }
833
+ };
834
+ const rel = REL_EVENT_RE.exec(raw);
835
+ const abs = rel ? null : EVENT_RE.exec(raw);
836
+ if (!rel && !abs) {
837
+ throw new ParseError(`Invalid event: ${raw}`, lineNum);
838
+ }
839
+ let time;
840
+ let actor;
841
+ let actionToken;
842
+ let paramsRaw;
843
+ if (rel) {
844
+ const [, offsetStr, act, action2, pr] = rel;
845
+ const offset = Number(offsetStr);
846
+ if (Number.isNaN(offset)) {
847
+ throw new ParseError(`Invalid @+offset value: ${offsetStr}`, lineNum);
848
+ }
849
+ time = round3(scope.prevEnd + offset);
850
+ actor = act;
851
+ actionToken = action2;
852
+ paramsRaw = pr;
853
+ } else {
854
+ const [, timeStr, act, action2, pr] = abs;
855
+ const t = Number(timeStr);
856
+ if (Number.isNaN(t)) {
857
+ throw new ParseError(`Invalid time value: ${timeStr}`, lineNum);
858
+ }
859
+ time = t;
860
+ actor = act;
861
+ actionToken = action2;
862
+ paramsRaw = pr;
863
+ }
864
+ const mustUnderstand = actionToken.startsWith("!");
865
+ const action = mustUnderstand ? actionToken.slice(1) : actionToken;
866
+ if (actor === "camera") {
867
+ if (!CAMERA_ACTIONS.has(action)) {
868
+ if (mustUnderstand) {
869
+ throw new ParseError(`Unknown camera action "${action}"`, lineNum);
870
+ }
871
+ ast.warnings.push({
872
+ kind: "unknown-camera-action",
873
+ message: `unknown camera action "${action}" \u2014 renderer will no-op`,
874
+ line: lineNum
875
+ });
876
+ }
877
+ const params2 = parseActionParams(action, paramsRaw);
878
+ recordEventTime(time);
879
+ pushEvent(ast, scope, {
880
+ time,
881
+ actor: "camera",
882
+ action,
883
+ params: params2,
884
+ line: lineNum,
885
+ ...inChapter ? { chapter: inChapter.name } : {}
886
+ });
887
+ return;
888
+ }
889
+ const actorDef = ast.actors[actor];
890
+ if (!actorDef) {
891
+ throw new ParseError(`Unknown actor: "${actor}"`, lineNum);
892
+ }
893
+ if (action === "play") {
894
+ const playParts = splitByComma(paramsRaw);
895
+ const seqName = playParts[0]?.trim();
896
+ if (!seqName || !ast.seqs[seqName]) {
897
+ throw new ParseError(`Unknown sequence: "${seqName}"`, lineNum);
898
+ }
899
+ const seq = ast.seqs[seqName];
900
+ const playVars = {};
901
+ for (let pi = 1; pi < playParts.length; pi++) {
902
+ const eqIdx = playParts[pi].indexOf("=");
903
+ if (eqIdx !== -1) {
904
+ const k = playParts[pi].slice(0, eqIdx).trim();
905
+ const v = playParts[pi].slice(eqIdx + 1).trim();
906
+ playVars[k] = v;
907
+ }
908
+ }
909
+ let posIdx = 0;
910
+ for (let pi = 1; pi < playParts.length; pi++) {
911
+ if (!playParts[pi].includes("=") && posIdx < seq.params.length) {
912
+ playVars[seq.params[posIdx]] = playParts[pi].trim();
913
+ posIdx++;
914
+ }
915
+ }
916
+ for (const sev of seq.events) {
917
+ const expandedParams = interpolate(sev.paramsRaw, playVars);
918
+ const absTime = round3(time + sev.offset);
919
+ const sevMustUnderstand = sev.action.startsWith("!");
920
+ const sevAction = sevMustUnderstand ? sev.action.slice(1) : sev.action;
921
+ validateActionForActor(sevAction, actorDef, sevMustUnderstand, lineNum, ast.warnings);
922
+ const params2 = parseActionParams(sevAction, expandedParams);
923
+ validateMoveTarget(sevAction, params2, ast.meta, actor, lineNum);
924
+ recordEventTime(absTime);
925
+ pushEvent(ast, scope, {
926
+ time: absTime,
927
+ actor,
928
+ action: sevAction,
929
+ params: params2,
930
+ line: lineNum,
931
+ ...inChapter ? { chapter: inChapter.name } : {}
932
+ });
933
+ }
934
+ return;
935
+ }
936
+ validateActionForActor(action, actorDef, mustUnderstand, lineNum, ast.warnings);
937
+ const params = parseActionParams(action, paramsRaw);
938
+ validateMoveTarget(action, params, ast.meta, actor, lineNum);
939
+ recordEventTime(time);
940
+ pushEvent(ast, scope, {
941
+ time,
942
+ actor,
943
+ action,
944
+ params,
945
+ line: lineNum,
946
+ ...inChapter ? { chapter: inChapter.name } : {}
947
+ });
948
+ }
949
+ function pushEvent(ast, scope, ev) {
950
+ ast.events.push(ev);
951
+ const dur = typeof ev.params.dur === "number" ? ev.params.dur : 0;
952
+ const endTime = round3(ev.time + dur);
953
+ if (endTime > scope.prevEnd) scope.prevEnd = endTime;
954
+ }
955
+ function validateActionForActor(action, actorDef, mustUnderstand, lineNum, warnings) {
956
+ if (FIGURE_ONLY_ACTIONS.has(action)) {
957
+ if (actorDef.type !== "figure") {
958
+ throw new ParseError(
959
+ `action "${action}" is figure-only; actor type is "${actorDef.type}"`,
960
+ lineNum
961
+ );
962
+ }
963
+ return;
964
+ }
965
+ if (isKnownAction(actorDef.type, action)) return;
966
+ if (mustUnderstand) {
967
+ throw new ParseError(`Unknown action "${action}" (must-understand form)`, lineNum);
968
+ }
969
+ warnings.push({
970
+ kind: "unknown-action",
971
+ message: `unknown action "${action}" on ${actorDef.type} actor \u2014 renderer will no-op`,
972
+ line: lineNum
973
+ });
974
+ }
975
+ function tryExpandSolePreset(source) {
976
+ const lines = source.split(/\r?\n/);
977
+ let presetLine = null;
978
+ for (const raw of lines) {
979
+ const stripped = stripComment(raw).trim();
980
+ if (!stripped) continue;
981
+ if (stripped.startsWith("preset ")) {
982
+ if (presetLine) return null;
983
+ presetLine = stripped;
984
+ } else {
985
+ return null;
986
+ }
987
+ }
988
+ if (!presetLine) return null;
989
+ const pm = PRESET_RE.exec(presetLine);
990
+ if (!pm) return null;
991
+ const [, name, argsRaw] = pm;
992
+ const fn = PRESETS[name];
993
+ if (!fn) return null;
994
+ const args = argsRaw ? splitByComma(argsRaw).map((a) => {
995
+ const t = a.trim();
996
+ return t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t;
997
+ }) : [];
998
+ return fn(args);
999
+ }
1000
+ function round3(n) {
1001
+ return Math.round(n * 1e3) / 1e3;
1002
+ }
398
1003
  export {
1004
+ PRESETS,
1005
+ PRESET_NAMES,
399
1006
  ParseError,
400
1007
  parse
401
1008
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdy/core",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "MarkdyScript parser and AST types — zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",