@markdy/core 0.1.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/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +373 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hoang Yell (https://hoangyell.com)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @markdy/core
|
|
2
|
+
|
|
3
|
+
The parser and AST types for [MarkdyScript](../../docs/SYNTAX.md) — a DSL for describing 2D animated scenes.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Zero runtime dependencies** — pure TypeScript, no DOM or platform APIs
|
|
8
|
+
- **Single-pass parser** — line-by-line state machine with strict `ParseError` diagnostics
|
|
9
|
+
- **Rich type system** — `var`, `def`, `seq` expanded at parse time for composable scene authoring
|
|
10
|
+
- **Isomorphic** — runs in Node.js, Deno, Bun, edge runtimes, and the browser
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add @markdy/core
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { parse, ParseError } from "@markdy/core";
|
|
22
|
+
import type { SceneAST } from "@markdy/core";
|
|
23
|
+
|
|
24
|
+
const source = `
|
|
25
|
+
scene width=600 height=300 bg=white
|
|
26
|
+
actor label = text("Hello") at (50, 130) size 40 opacity 0
|
|
27
|
+
@0.3: label.fade_in(dur=0.6)
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const ast: SceneAST = parse(source);
|
|
32
|
+
|
|
33
|
+
console.log(ast.meta); // { width: 600, height: 300, fps: 30, bg: "white", duration: 0.9 }
|
|
34
|
+
console.log(ast.actors); // { label: { type: "text", args: ["Hello"], x: 50, y: 130, ... } }
|
|
35
|
+
console.log(ast.events); // [{ time: 0.3, actor: "label", action: "fade_in", ... }]
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (e instanceof ParseError) {
|
|
38
|
+
console.error(`Line ${e.line}: ${e.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Exports
|
|
44
|
+
|
|
45
|
+
| Export | Type | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `parse` | `(source: string) => SceneAST` | Parse MarkdyScript source into an AST |
|
|
48
|
+
| `ParseError` | class | Error with `.line` number for diagnostics |
|
|
49
|
+
| `SceneAST` | type | Complete scene representation |
|
|
50
|
+
| `SceneMeta` | type | Scene configuration (width, height, bg, etc.) |
|
|
51
|
+
| `AssetDef` | type | Asset declaration (image or icon) |
|
|
52
|
+
| `ActorDef` | type | Actor declaration (type, position, modifiers) |
|
|
53
|
+
| `TimelineEvent` | type | Timeline event (time, actor, action, params) |
|
|
54
|
+
| `TemplateDef` | type | User-defined actor template |
|
|
55
|
+
| `SequenceDef` | type | User-defined animation sequence |
|
|
56
|
+
|
|
57
|
+
## Documentation
|
|
58
|
+
|
|
59
|
+
- **[Syntax Reference](../../docs/SYNTAX.md)** — complete DSL language spec
|
|
60
|
+
- **[Tutorial](../../docs/TUTORIAL.md)** — step-by-step guide
|
|
61
|
+
- **[Agent Guide](../../docs/AGENT.md)** — structured reference for AI/LLM code generation
|
|
62
|
+
- **[Architecture](../../docs/ARCHITECTURE.md)** — parser internals and design decisions
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
[MIT](../../LICENSE)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST types for MarkdyScript — the complete output of the parser.
|
|
3
|
+
* Zero runtime dependencies.
|
|
4
|
+
*/
|
|
5
|
+
type AssetDef = {
|
|
6
|
+
type: "image" | "icon";
|
|
7
|
+
value: string;
|
|
8
|
+
};
|
|
9
|
+
type ActorDef = {
|
|
10
|
+
type: "sprite" | "text" | "box" | "figure";
|
|
11
|
+
/** Constructor arguments: asset name for sprite, display text for text actors. */
|
|
12
|
+
args: string[];
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
scale?: number;
|
|
16
|
+
rotate?: number;
|
|
17
|
+
opacity?: number;
|
|
18
|
+
/** Font size in pixels; applies to text actors (via the `size` modifier). */
|
|
19
|
+
size?: number;
|
|
20
|
+
};
|
|
21
|
+
type TimelineEvent = {
|
|
22
|
+
time: number;
|
|
23
|
+
actor: string;
|
|
24
|
+
action: string;
|
|
25
|
+
params: Record<string, unknown>;
|
|
26
|
+
line: number;
|
|
27
|
+
};
|
|
28
|
+
type SceneMeta = {
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
fps: number;
|
|
32
|
+
bg: string;
|
|
33
|
+
/** Auto-computed from the last event + its dur param when not explicitly set. */
|
|
34
|
+
duration?: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A user-defined actor template (`def`).
|
|
38
|
+
* Expands to an actor type + args at parse time — the renderer never sees it.
|
|
39
|
+
*/
|
|
40
|
+
type TemplateDef = {
|
|
41
|
+
/** Parameter names declared on the def line. */
|
|
42
|
+
params: string[];
|
|
43
|
+
/** The actor type this def expands to (e.g. "figure", "sprite"). */
|
|
44
|
+
actorType: ActorDef["type"];
|
|
45
|
+
/** Raw constructor arg tokens (may contain `${param}` references). */
|
|
46
|
+
bodyArgs: string[];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* A user-defined reusable animation sequence (`seq`).
|
|
50
|
+
* Expanded inline wherever `actor.play(seqName)` appears.
|
|
51
|
+
*/
|
|
52
|
+
type SequenceDef = {
|
|
53
|
+
/** Parameter names (excluding the implicit `$` target actor). */
|
|
54
|
+
params: string[];
|
|
55
|
+
/** Raw event lines with `@+offset` and `$` actor placeholder. */
|
|
56
|
+
events: Array<{
|
|
57
|
+
offset: number;
|
|
58
|
+
action: string;
|
|
59
|
+
paramsRaw: string;
|
|
60
|
+
}>;
|
|
61
|
+
};
|
|
62
|
+
type SceneAST = {
|
|
63
|
+
meta: SceneMeta;
|
|
64
|
+
assets: Record<string, AssetDef>;
|
|
65
|
+
actors: Record<string, ActorDef>;
|
|
66
|
+
events: TimelineEvent[];
|
|
67
|
+
/** User-defined actor templates — kept in AST for tooling/inspection. */
|
|
68
|
+
defs: Record<string, TemplateDef>;
|
|
69
|
+
/** User-defined sequences — kept in AST for tooling/inspection. */
|
|
70
|
+
seqs: Record<string, SequenceDef>;
|
|
71
|
+
/** User-defined variables — kept in AST for tooling/inspection. */
|
|
72
|
+
vars: Record<string, string>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
declare class ParseError extends Error {
|
|
76
|
+
readonly line: number;
|
|
77
|
+
constructor(message: string, line: number);
|
|
78
|
+
}
|
|
79
|
+
declare function parse(source: string): SceneAST;
|
|
80
|
+
|
|
81
|
+
export { type ActorDef, type AssetDef, ParseError, type SceneAST, type SceneMeta, type SequenceDef, type TemplateDef, type TimelineEvent, parse };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// src/parser.ts
|
|
2
|
+
var ParseError = class extends Error {
|
|
3
|
+
constructor(message, line) {
|
|
4
|
+
super(`Line ${line}: ${message}`);
|
|
5
|
+
this.line = line;
|
|
6
|
+
this.name = "ParseError";
|
|
7
|
+
}
|
|
8
|
+
line;
|
|
9
|
+
};
|
|
10
|
+
function stripComment(line) {
|
|
11
|
+
let depth = 0;
|
|
12
|
+
let inString = false;
|
|
13
|
+
for (let i = 0; i < line.length; i++) {
|
|
14
|
+
const ch = line[i];
|
|
15
|
+
if (ch === '"') {
|
|
16
|
+
inString = !inString;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (inString) continue;
|
|
20
|
+
if (ch === "(") {
|
|
21
|
+
depth++;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (ch === ")") {
|
|
25
|
+
depth--;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (ch === "#" && depth === 0) return line.slice(0, i);
|
|
29
|
+
}
|
|
30
|
+
return line;
|
|
31
|
+
}
|
|
32
|
+
function splitByComma(s) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
let depth = 0;
|
|
35
|
+
let start = 0;
|
|
36
|
+
for (let i = 0; i < s.length; i++) {
|
|
37
|
+
const ch = s[i];
|
|
38
|
+
if (ch === "(") depth++;
|
|
39
|
+
else if (ch === ")") depth--;
|
|
40
|
+
else if (ch === "," && depth === 0) {
|
|
41
|
+
parts.push(s.slice(start, i));
|
|
42
|
+
start = i + 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
parts.push(s.slice(start));
|
|
46
|
+
return parts;
|
|
47
|
+
}
|
|
48
|
+
function parseValue(s) {
|
|
49
|
+
const t = s.trim();
|
|
50
|
+
if (t.startsWith('"') && t.endsWith('"')) {
|
|
51
|
+
return t.slice(1, -1);
|
|
52
|
+
}
|
|
53
|
+
if (t.startsWith("(") && t.endsWith(")")) {
|
|
54
|
+
const inner = t.slice(1, -1);
|
|
55
|
+
return inner.split(",").map((p) => {
|
|
56
|
+
const v = p.trim();
|
|
57
|
+
const n2 = Number(v);
|
|
58
|
+
return Number.isNaN(n2) ? v : n2;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const n = Number(t);
|
|
62
|
+
if (!Number.isNaN(n) && t !== "") return n;
|
|
63
|
+
return t;
|
|
64
|
+
}
|
|
65
|
+
var POSITIONAL_KEYS = {
|
|
66
|
+
say: ["text"],
|
|
67
|
+
throw: ["asset"]
|
|
68
|
+
};
|
|
69
|
+
function parseActionParams(action, raw) {
|
|
70
|
+
const params = {};
|
|
71
|
+
const trimmed = raw.trim();
|
|
72
|
+
if (!trimmed) return params;
|
|
73
|
+
const positionalKeys = POSITIONAL_KEYS[action] ?? [];
|
|
74
|
+
let positionalIndex = 0;
|
|
75
|
+
for (const token of splitByComma(trimmed)) {
|
|
76
|
+
const t = token.trim();
|
|
77
|
+
if (!t) continue;
|
|
78
|
+
const eqIdx = t.indexOf("=");
|
|
79
|
+
if (eqIdx === -1) {
|
|
80
|
+
const key = positionalKeys[positionalIndex] ?? `_${positionalIndex}`;
|
|
81
|
+
positionalIndex++;
|
|
82
|
+
params[key] = parseValue(t);
|
|
83
|
+
} else {
|
|
84
|
+
const key = t.slice(0, eqIdx).trim();
|
|
85
|
+
const val = t.slice(eqIdx + 1).trim();
|
|
86
|
+
params[key] = parseValue(val);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return params;
|
|
90
|
+
}
|
|
91
|
+
function parseModifiers(raw) {
|
|
92
|
+
const result = {};
|
|
93
|
+
const tokens = raw.trim().split(/\s+/).filter(Boolean);
|
|
94
|
+
for (let i = 0; i + 1 < tokens.length; i += 2) {
|
|
95
|
+
const key = tokens[i];
|
|
96
|
+
const val = Number(tokens[i + 1]);
|
|
97
|
+
if (Number.isNaN(val)) continue;
|
|
98
|
+
if (key === "scale") result.scale = val;
|
|
99
|
+
else if (key === "rotate") result.rotate = val;
|
|
100
|
+
else if (key === "opacity") result.opacity = val;
|
|
101
|
+
else if (key === "size") result.size = val;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
var ASSET_RE = /^asset\s+(\w+)\s*=\s*(image|icon)\("([^"]+)"\)$/;
|
|
106
|
+
var BUILTIN_ACTOR_TYPES = /* @__PURE__ */ new Set(["sprite", "text", "box", "figure"]);
|
|
107
|
+
var ACTOR_RE = /^actor\s+(\w+)\s*=\s*(\w+)\(([^)]*)\)\s+at\s+\(\s*(-?[\d.]+)\s*,\s*(-?[\d.]+)\s*\)(.*)$/;
|
|
108
|
+
var EVENT_RE = /^@([\d.]+):\s+(\w+)\.(\w+)\((.*)\)$/;
|
|
109
|
+
var VAR_RE = /^var\s+(\w+)\s*=\s*(.+)$/;
|
|
110
|
+
var DEF_HEADER_RE = /^def\s+(\w+)\(([^)]*)\)\s*\{$/;
|
|
111
|
+
var DEF_BODY_RE = /^\s*(sprite|text|box|figure)\(([^)]*)\)\s*$/;
|
|
112
|
+
var SEQ_HEADER_RE = /^seq\s+(\w+)(?:\(([^)]*)\))?\s*\{$/;
|
|
113
|
+
var SEQ_EVENT_RE = /^@\+([\d.]+):\s+\$\.(\w+)\((.*)\)$/;
|
|
114
|
+
function interpolate(s, vars) {
|
|
115
|
+
return s.replace(/\$\{(\w+)\}/g, (_, name) => vars[name] ?? `\${${name}}`);
|
|
116
|
+
}
|
|
117
|
+
var DEFAULTS = {
|
|
118
|
+
width: 800,
|
|
119
|
+
height: 400,
|
|
120
|
+
fps: 30,
|
|
121
|
+
bg: "white"
|
|
122
|
+
};
|
|
123
|
+
function parse(source) {
|
|
124
|
+
const ast = {
|
|
125
|
+
meta: { ...DEFAULTS },
|
|
126
|
+
assets: {},
|
|
127
|
+
actors: {},
|
|
128
|
+
events: [],
|
|
129
|
+
defs: {},
|
|
130
|
+
seqs: {},
|
|
131
|
+
vars: {}
|
|
132
|
+
};
|
|
133
|
+
let sceneFound = false;
|
|
134
|
+
const lines = source.split(/\r?\n/);
|
|
135
|
+
let inDef = null;
|
|
136
|
+
let defNeedsClose = false;
|
|
137
|
+
let inSeq = null;
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
const lineNum = i + 1;
|
|
140
|
+
const rawUntouched = lines[i].trim();
|
|
141
|
+
if (!inDef && !defNeedsClose && !inSeq && rawUntouched.startsWith("var ")) {
|
|
142
|
+
const varLine = interpolate(rawUntouched, ast.vars);
|
|
143
|
+
const vm = VAR_RE.exec(varLine);
|
|
144
|
+
if (!vm) {
|
|
145
|
+
throw new ParseError(`Invalid var declaration: ${varLine}`, lineNum);
|
|
146
|
+
}
|
|
147
|
+
const [, name, value] = vm;
|
|
148
|
+
ast.vars[name] = value.trim();
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
let raw = stripComment(lines[i]).trim();
|
|
152
|
+
if (!raw) continue;
|
|
153
|
+
raw = interpolate(raw, ast.vars);
|
|
154
|
+
if (raw === "}") {
|
|
155
|
+
if (defNeedsClose) {
|
|
156
|
+
defNeedsClose = false;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (inDef) {
|
|
160
|
+
throw new ParseError(`Empty def body for "${inDef.name}"`, lineNum);
|
|
161
|
+
}
|
|
162
|
+
if (inSeq) {
|
|
163
|
+
ast.seqs[inSeq.name] = { params: inSeq.params, events: inSeq.events };
|
|
164
|
+
inSeq = null;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
throw new ParseError("Unexpected '}'", lineNum);
|
|
168
|
+
}
|
|
169
|
+
if (inDef) {
|
|
170
|
+
const bm = DEF_BODY_RE.exec(raw);
|
|
171
|
+
if (!bm) {
|
|
172
|
+
throw new ParseError(`Invalid def body (expected "type(args)"): ${raw}`, lineNum);
|
|
173
|
+
}
|
|
174
|
+
const [, actorType, bodyArgsRaw] = bm;
|
|
175
|
+
const bodyArgs = bodyArgsRaw.trim() ? splitByComma(bodyArgsRaw).map((a) => a.trim()) : [];
|
|
176
|
+
ast.defs[inDef.name] = {
|
|
177
|
+
params: inDef.params,
|
|
178
|
+
actorType,
|
|
179
|
+
bodyArgs
|
|
180
|
+
};
|
|
181
|
+
inDef = null;
|
|
182
|
+
defNeedsClose = true;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (inSeq) {
|
|
186
|
+
const interpolatedSeq = interpolate(raw, ast.vars);
|
|
187
|
+
const sm = SEQ_EVENT_RE.exec(interpolatedSeq);
|
|
188
|
+
if (!sm) {
|
|
189
|
+
throw new ParseError(`Invalid seq event (expected "@+offset: $.action(params)"): ${raw}`, lineNum);
|
|
190
|
+
}
|
|
191
|
+
const [, offsetStr, action, paramsRaw] = sm;
|
|
192
|
+
inSeq.events.push({
|
|
193
|
+
offset: Number(offsetStr),
|
|
194
|
+
action,
|
|
195
|
+
paramsRaw
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (raw.startsWith("def ")) {
|
|
200
|
+
const dm = DEF_HEADER_RE.exec(raw);
|
|
201
|
+
if (!dm) {
|
|
202
|
+
throw new ParseError(`Invalid def declaration: ${raw}`, lineNum);
|
|
203
|
+
}
|
|
204
|
+
const [, name, paramsRaw] = dm;
|
|
205
|
+
const params = paramsRaw.split(",").map((p) => p.trim()).filter(Boolean);
|
|
206
|
+
inDef = { name, params, startLine: lineNum };
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (raw.startsWith("seq ")) {
|
|
210
|
+
const sm = SEQ_HEADER_RE.exec(raw);
|
|
211
|
+
if (!sm) {
|
|
212
|
+
throw new ParseError(`Invalid seq declaration: ${raw}`, lineNum);
|
|
213
|
+
}
|
|
214
|
+
const [, name, paramsRaw] = sm;
|
|
215
|
+
const params = paramsRaw ? paramsRaw.split(",").map((p) => p.trim()).filter(Boolean) : [];
|
|
216
|
+
inSeq = { name, params, events: [], startLine: lineNum };
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (/^scene(\s|$)/.test(raw)) {
|
|
220
|
+
if (sceneFound) {
|
|
221
|
+
throw new ParseError("Duplicate scene declaration", lineNum);
|
|
222
|
+
}
|
|
223
|
+
sceneFound = true;
|
|
224
|
+
for (const [, key, val] of raw.matchAll(/(\w+)=([\S]+)/g)) {
|
|
225
|
+
switch (key) {
|
|
226
|
+
case "width":
|
|
227
|
+
ast.meta.width = Number(val);
|
|
228
|
+
break;
|
|
229
|
+
case "height":
|
|
230
|
+
ast.meta.height = Number(val);
|
|
231
|
+
break;
|
|
232
|
+
case "fps":
|
|
233
|
+
ast.meta.fps = Number(val);
|
|
234
|
+
break;
|
|
235
|
+
case "bg":
|
|
236
|
+
ast.meta.bg = val;
|
|
237
|
+
break;
|
|
238
|
+
case "duration":
|
|
239
|
+
ast.meta.duration = Number(val);
|
|
240
|
+
break;
|
|
241
|
+
default:
|
|
242
|
+
throw new ParseError(`Unknown scene property: ${key}`, lineNum);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (raw.startsWith("asset")) {
|
|
248
|
+
const m = ASSET_RE.exec(raw);
|
|
249
|
+
if (!m) {
|
|
250
|
+
throw new ParseError(`Invalid asset declaration: ${raw}`, lineNum);
|
|
251
|
+
}
|
|
252
|
+
const [, name, type, value] = m;
|
|
253
|
+
ast.assets[name] = { type, value };
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (raw.startsWith("actor")) {
|
|
257
|
+
const m = ACTOR_RE.exec(raw);
|
|
258
|
+
if (!m) {
|
|
259
|
+
throw new ParseError(`Invalid actor declaration: ${raw}`, lineNum);
|
|
260
|
+
}
|
|
261
|
+
const [, name, typeName, argsRaw, xStr, yStr, modifiersRaw] = m;
|
|
262
|
+
let resolvedType;
|
|
263
|
+
let resolvedArgs;
|
|
264
|
+
if (BUILTIN_ACTOR_TYPES.has(typeName)) {
|
|
265
|
+
resolvedType = typeName;
|
|
266
|
+
resolvedArgs = argsRaw.trim() ? splitByComma(argsRaw).map((a) => {
|
|
267
|
+
const t = a.trim();
|
|
268
|
+
return t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t;
|
|
269
|
+
}) : [];
|
|
270
|
+
} else if (ast.defs[typeName]) {
|
|
271
|
+
const tmpl = ast.defs[typeName];
|
|
272
|
+
const callArgs = argsRaw.trim() ? splitByComma(argsRaw).map((a) => {
|
|
273
|
+
const t = a.trim();
|
|
274
|
+
return t.startsWith('"') && t.endsWith('"') ? t.slice(1, -1) : t;
|
|
275
|
+
}) : [];
|
|
276
|
+
const localVars = {};
|
|
277
|
+
for (let pi = 0; pi < tmpl.params.length; pi++) {
|
|
278
|
+
localVars[tmpl.params[pi]] = callArgs[pi] ?? "";
|
|
279
|
+
}
|
|
280
|
+
resolvedType = tmpl.actorType;
|
|
281
|
+
resolvedArgs = tmpl.bodyArgs.map((a) => interpolate(a, localVars));
|
|
282
|
+
} else {
|
|
283
|
+
throw new ParseError(`Unknown actor type or template: "${typeName}"`, lineNum);
|
|
284
|
+
}
|
|
285
|
+
const modifiers = parseModifiers(modifiersRaw);
|
|
286
|
+
ast.actors[name] = {
|
|
287
|
+
type: resolvedType,
|
|
288
|
+
args: resolvedArgs,
|
|
289
|
+
x: Number(xStr),
|
|
290
|
+
y: Number(yStr),
|
|
291
|
+
...modifiers
|
|
292
|
+
};
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (raw.startsWith("@")) {
|
|
296
|
+
const m = EVENT_RE.exec(raw);
|
|
297
|
+
if (!m) {
|
|
298
|
+
throw new ParseError(`Invalid event: ${raw}`, lineNum);
|
|
299
|
+
}
|
|
300
|
+
const [, timeStr, actor, action, paramsRaw] = m;
|
|
301
|
+
const time = Number(timeStr);
|
|
302
|
+
if (Number.isNaN(time)) {
|
|
303
|
+
throw new ParseError(`Invalid time value: ${timeStr}`, lineNum);
|
|
304
|
+
}
|
|
305
|
+
if (!ast.actors[actor]) {
|
|
306
|
+
throw new ParseError(`Unknown actor: "${actor}"`, lineNum);
|
|
307
|
+
}
|
|
308
|
+
if (action === "play") {
|
|
309
|
+
const playParts = splitByComma(paramsRaw);
|
|
310
|
+
const seqName = playParts[0]?.trim();
|
|
311
|
+
if (!seqName || !ast.seqs[seqName]) {
|
|
312
|
+
throw new ParseError(`Unknown sequence: "${seqName}"`, lineNum);
|
|
313
|
+
}
|
|
314
|
+
const seq = ast.seqs[seqName];
|
|
315
|
+
const playVars = {};
|
|
316
|
+
for (let pi = 1; pi < playParts.length; pi++) {
|
|
317
|
+
const eqIdx = playParts[pi].indexOf("=");
|
|
318
|
+
if (eqIdx !== -1) {
|
|
319
|
+
const k = playParts[pi].slice(0, eqIdx).trim();
|
|
320
|
+
const v = playParts[pi].slice(eqIdx + 1).trim();
|
|
321
|
+
playVars[k] = v;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
let posIdx = 0;
|
|
325
|
+
for (let pi = 1; pi < playParts.length; pi++) {
|
|
326
|
+
if (!playParts[pi].includes("=") && posIdx < seq.params.length) {
|
|
327
|
+
playVars[seq.params[posIdx]] = playParts[pi].trim();
|
|
328
|
+
posIdx++;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const sev of seq.events) {
|
|
332
|
+
const expandedParams = interpolate(sev.paramsRaw, playVars);
|
|
333
|
+
const absTime = Math.round((time + sev.offset) * 1e3) / 1e3;
|
|
334
|
+
const params2 = parseActionParams(sev.action, expandedParams);
|
|
335
|
+
ast.events.push({
|
|
336
|
+
time: absTime,
|
|
337
|
+
actor,
|
|
338
|
+
action: sev.action,
|
|
339
|
+
params: params2,
|
|
340
|
+
line: lineNum
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const params = parseActionParams(action, paramsRaw);
|
|
346
|
+
ast.events.push({ time, actor, action, params, line: lineNum });
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
throw new ParseError(`Unrecognized statement: ${raw}`, lineNum);
|
|
350
|
+
}
|
|
351
|
+
if (inDef) {
|
|
352
|
+
throw new ParseError(`Unclosed def block "${inDef.name}"`, inDef.startLine);
|
|
353
|
+
}
|
|
354
|
+
if (defNeedsClose) {
|
|
355
|
+
throw new ParseError("Unclosed def block (missing '}')", lines.length);
|
|
356
|
+
}
|
|
357
|
+
if (inSeq) {
|
|
358
|
+
throw new ParseError(`Unclosed seq block "${inSeq.name}"`, inSeq.startLine);
|
|
359
|
+
}
|
|
360
|
+
if (ast.meta.duration === void 0) {
|
|
361
|
+
let maxEnd = 0;
|
|
362
|
+
for (const ev of ast.events) {
|
|
363
|
+
const dur = typeof ev.params.dur === "number" ? ev.params.dur : 0;
|
|
364
|
+
maxEnd = Math.max(maxEnd, ev.time + dur);
|
|
365
|
+
}
|
|
366
|
+
if (maxEnd > 0) ast.meta.duration = maxEnd;
|
|
367
|
+
}
|
|
368
|
+
return ast;
|
|
369
|
+
}
|
|
370
|
+
export {
|
|
371
|
+
ParseError,
|
|
372
|
+
parse
|
|
373
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@markdy/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MarkdyScript parser and AST types — zero runtime dependencies.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"markdy",
|
|
23
|
+
"markdyscript",
|
|
24
|
+
"animation",
|
|
25
|
+
"dsl",
|
|
26
|
+
"parser",
|
|
27
|
+
"ast"
|
|
28
|
+
],
|
|
29
|
+
"author": "Hoang Yell <hoangyell@gmail.com> (https://hoangyell.com)",
|
|
30
|
+
"homepage": "https://markdy.com",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/HoangYell/markdy-com.git",
|
|
34
|
+
"directory": "packages/core"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/HoangYell/markdy-com/issues"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"tsup": "^8.3.0",
|
|
44
|
+
"typescript": "^5.4.0",
|
|
45
|
+
"vitest": "^1.6.0"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"lint": "tsc --noEmit"
|
|
52
|
+
}
|
|
53
|
+
}
|