apexfile 1.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 +94 -0
- package/README.md +415 -0
- package/bin/cli.js +334 -0
- package/package.json +59 -0
- package/src/ast/index.js +260 -0
- package/src/index.js +438 -0
- package/src/parser/index.js +594 -0
- package/src/renderer/html.js +983 -0
- package/src/resolver/index.js +442 -0
- package/src/tokenizer/index.js +518 -0
- package/src/tokenizer/tokens.js +75 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ApexDoc Parser
|
|
5
|
+
* Converts a flat token array (from Tokenizer) into a full AST.
|
|
6
|
+
*
|
|
7
|
+
* Parsing strategy:
|
|
8
|
+
* 1. Partition tokens into META / STYLE / BODY groups
|
|
9
|
+
* 2. Parse each group into its subtree
|
|
10
|
+
* 3. Merge into a Document root node
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const T = require('../tokenizer/tokens');
|
|
14
|
+
const Node = require('../ast');
|
|
15
|
+
|
|
16
|
+
class Parser {
|
|
17
|
+
constructor(tokens) {
|
|
18
|
+
this.tokens = tokens;
|
|
19
|
+
this.pos = 0;
|
|
20
|
+
this.errors = [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Entry Point ────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
parse() {
|
|
26
|
+
const groups = this._partition();
|
|
27
|
+
|
|
28
|
+
const meta = this._parseMeta(groups.meta);
|
|
29
|
+
const style = this._parseStyle(groups.style);
|
|
30
|
+
const body = this._parseBody(groups.body);
|
|
31
|
+
|
|
32
|
+
return Node.document(meta, style, body);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Partition tokens into sections ────────────────────────────
|
|
36
|
+
|
|
37
|
+
_partition() {
|
|
38
|
+
const groups = { meta: [], style: [], body: [] };
|
|
39
|
+
|
|
40
|
+
let current = null;
|
|
41
|
+
let depth = 0;
|
|
42
|
+
|
|
43
|
+
for (const tok of this.tokens) {
|
|
44
|
+
if (tok.type === T.SECTION_OPEN) {
|
|
45
|
+
current = tok.value; // 'meta' | 'style' | 'body'
|
|
46
|
+
depth = 0;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (tok.type === T.SECTION_CLOSE) {
|
|
51
|
+
current = null;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (tok.type === T.EOF) break;
|
|
56
|
+
|
|
57
|
+
// If no section is open, assume body (bare file)
|
|
58
|
+
const target = current || 'body';
|
|
59
|
+
if (groups[target]) {
|
|
60
|
+
groups[target].push(tok);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return groups;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── META Parser ────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
_parseMeta(tokens) {
|
|
70
|
+
const fields = {};
|
|
71
|
+
|
|
72
|
+
for (const tok of tokens) {
|
|
73
|
+
if (tok.type === T.META_FIELD) {
|
|
74
|
+
fields[tok.value] = tok.props.value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Node.meta(fields);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── STYLE Parser ───────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
_parseStyle(tokens) {
|
|
84
|
+
const variables = {};
|
|
85
|
+
const themes = {};
|
|
86
|
+
const directives = [];
|
|
87
|
+
|
|
88
|
+
let i = 0;
|
|
89
|
+
let currentBlock = null; // current @theme / @media name
|
|
90
|
+
|
|
91
|
+
while (i < tokens.length) {
|
|
92
|
+
const tok = tokens[i];
|
|
93
|
+
|
|
94
|
+
if (tok.type === T.STYLE_BLOCK_OPEN) {
|
|
95
|
+
// e.g. @theme dark-ocean or @media (prefers-color-scheme: dark)
|
|
96
|
+
currentBlock = { directive: tok.value, name: tok.props.name, vars: {} };
|
|
97
|
+
i++;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (tok.type === T.STYLE_BLOCK_CLOSE) {
|
|
102
|
+
if (currentBlock) {
|
|
103
|
+
if (currentBlock.directive === '@theme') {
|
|
104
|
+
themes[currentBlock.name] = currentBlock.vars;
|
|
105
|
+
}
|
|
106
|
+
// @media / @breakpoint blocks stored in directives with their vars
|
|
107
|
+
else {
|
|
108
|
+
directives.push({
|
|
109
|
+
directive: currentBlock.directive,
|
|
110
|
+
name: currentBlock.name,
|
|
111
|
+
variables: currentBlock.vars,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
currentBlock = null;
|
|
115
|
+
}
|
|
116
|
+
i++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (tok.type === T.STYLE_VAR) {
|
|
121
|
+
if (currentBlock) {
|
|
122
|
+
currentBlock.vars[tok.value] = tok.props.value;
|
|
123
|
+
} else {
|
|
124
|
+
variables[tok.value] = tok.props.value;
|
|
125
|
+
}
|
|
126
|
+
i++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (tok.type === T.STYLE_DIRECTIVE) {
|
|
131
|
+
directives.push(Node.styleDirective(tok.value, tok.props));
|
|
132
|
+
i++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return Node.style(variables, themes, directives);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── BODY Parser ────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
_parseBody(tokens) {
|
|
145
|
+
this._bodyTokens = tokens;
|
|
146
|
+
this._bodyPos = 0;
|
|
147
|
+
return this._parseNodes();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parse a sequence of nodes until we hit EOF or a matching BLOCK_CLOSE.
|
|
152
|
+
* This is called recursively for nested blocks.
|
|
153
|
+
*/
|
|
154
|
+
_parseNodes(stopAtBlockClose = false) {
|
|
155
|
+
const nodes = [];
|
|
156
|
+
|
|
157
|
+
while (this._bodyPos < this._bodyTokens.length) {
|
|
158
|
+
const tok = this._bodyTokens[this._bodyPos];
|
|
159
|
+
|
|
160
|
+
if (!tok || tok.type === T.EOF) break;
|
|
161
|
+
|
|
162
|
+
// Stop when we hit a block close (used for nested block parsing)
|
|
163
|
+
if (stopAtBlockClose && tok.type === T.BLOCK_CLOSE) break;
|
|
164
|
+
|
|
165
|
+
const node = this._parseNode();
|
|
166
|
+
if (node) {
|
|
167
|
+
// Flatten arrays (inline parsers return arrays)
|
|
168
|
+
if (Array.isArray(node)) {
|
|
169
|
+
nodes.push(...node.filter(Boolean));
|
|
170
|
+
} else {
|
|
171
|
+
nodes.push(node);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return nodes;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
_parseNode() {
|
|
180
|
+
const tok = this._current();
|
|
181
|
+
if (!tok) return null;
|
|
182
|
+
|
|
183
|
+
switch (tok.type) {
|
|
184
|
+
|
|
185
|
+
// ── Headings ──────────────────────────────────────────────
|
|
186
|
+
case T.HEADING: {
|
|
187
|
+
this._advance();
|
|
188
|
+
// Parse heading text as inline content
|
|
189
|
+
const children = this._parseInlineText(tok.value, tok.line);
|
|
190
|
+
return Node.heading(tok.props.level, children, tok.props, tok.line);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── Block (%%name {props} ... %%end) ──────────────────────
|
|
194
|
+
case T.BLOCK_OPEN: {
|
|
195
|
+
return this._parseBlock();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Self-Closing Block ─────────────────────────────────────
|
|
199
|
+
case T.BLOCK_SELF_CLOSE: {
|
|
200
|
+
this._advance();
|
|
201
|
+
return Node.selfCloseBlock(tok.value, tok.props, tok.line);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Logic ─────────────────────────────────────────────────
|
|
205
|
+
case T.SET: {
|
|
206
|
+
this._advance();
|
|
207
|
+
return Node.setVar(tok.value, tok.props.value, tok.line);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
case T.IF: {
|
|
211
|
+
return this._parseConditional();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case T.EACH: {
|
|
215
|
+
return this._parseLoop();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
case T.KEYFRAME: {
|
|
219
|
+
return this._parseKeyframe();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case T.MATH_BLOCK: {
|
|
223
|
+
this._advance();
|
|
224
|
+
return Node.mathBlock(tok.value, tok.props, tok.line);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Lists ─────────────────────────────────────────────────
|
|
228
|
+
case T.LIST_ITEM:
|
|
229
|
+
case T.LIST_ITEM_ORDERED:
|
|
230
|
+
case T.LIST_ITEM_TASK: {
|
|
231
|
+
return this._parseList();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Blockquote ────────────────────────────────────────────
|
|
235
|
+
case T.BLOCKQUOTE: {
|
|
236
|
+
return this._parseBlockquote();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── HR ────────────────────────────────────────────────────
|
|
240
|
+
case T.HR: {
|
|
241
|
+
this._advance();
|
|
242
|
+
return Node.hr(tok.value, tok.line);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Footnotes ─────────────────────────────────────────────
|
|
246
|
+
case T.FOOTNOTE_DEF: {
|
|
247
|
+
this._advance();
|
|
248
|
+
return Node.footnoteDef(tok.value, tok.props.content, tok.line);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Inline text tokens → paragraph ────────────────────────
|
|
252
|
+
case T.TEXT:
|
|
253
|
+
case T.BOLD:
|
|
254
|
+
case T.ITALIC:
|
|
255
|
+
case T.UNDERLINE:
|
|
256
|
+
case T.STRIKETHROUGH:
|
|
257
|
+
case T.SUPERSCRIPT:
|
|
258
|
+
case T.SUBSCRIPT:
|
|
259
|
+
case T.CODE_INLINE:
|
|
260
|
+
case T.HIGHLIGHT:
|
|
261
|
+
case T.INLINE_STYLED:
|
|
262
|
+
case T.LINK:
|
|
263
|
+
case T.FOOTNOTE_REF:
|
|
264
|
+
case T.MATH_INLINE:
|
|
265
|
+
case T.EXPRESSION: {
|
|
266
|
+
return this._parseParagraph();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Newline ───────────────────────────────────────────────
|
|
270
|
+
case T.NEWLINE: {
|
|
271
|
+
this._advance();
|
|
272
|
+
return null; // Skip bare newlines — paragraphs handle spacing
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── Unknown / skip ────────────────────────────────────────
|
|
276
|
+
default: {
|
|
277
|
+
this._advance();
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Block Parser ───────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
_parseBlock() {
|
|
286
|
+
const openTok = this._current();
|
|
287
|
+
this._advance(); // consume BLOCK_OPEN
|
|
288
|
+
|
|
289
|
+
const name = openTok.value;
|
|
290
|
+
const props = openTok.props;
|
|
291
|
+
const children = [];
|
|
292
|
+
|
|
293
|
+
// Special handling for blocks with raw content (code, math, etc.)
|
|
294
|
+
const RAW_BLOCKS = new Set(['code', 'math', 'chem', 'svg', 'uml',
|
|
295
|
+
'flowchart', 'sequence', 'terminal',
|
|
296
|
+
'diff', 'keyframe']);
|
|
297
|
+
|
|
298
|
+
if (RAW_BLOCKS.has(name)) {
|
|
299
|
+
// Collect raw text lines until BLOCK_CLOSE
|
|
300
|
+
const rawLines = [];
|
|
301
|
+
while (this._bodyPos < this._bodyTokens.length) {
|
|
302
|
+
const tok = this._current();
|
|
303
|
+
if (!tok || tok.type === T.BLOCK_CLOSE) break;
|
|
304
|
+
if (tok.type === T.TEXT) rawLines.push(tok.value);
|
|
305
|
+
if (tok.type === T.NEWLINE) rawLines.push('');
|
|
306
|
+
this._advance();
|
|
307
|
+
}
|
|
308
|
+
this._advance(); // consume BLOCK_CLOSE
|
|
309
|
+
return Node.block(name, props, [Node.text(rawLines.join('\n'), openTok.line)], openTok.line);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// For special nesting-aware blocks, parse children recursively
|
|
313
|
+
while (this._bodyPos < this._bodyTokens.length) {
|
|
314
|
+
const tok = this._current();
|
|
315
|
+
if (!tok || tok.type === T.EOF) break;
|
|
316
|
+
if (tok.type === T.BLOCK_CLOSE) {
|
|
317
|
+
this._advance(); // consume %%end
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
const child = this._parseNode();
|
|
321
|
+
if (child) {
|
|
322
|
+
if (Array.isArray(child)) children.push(...child.filter(Boolean));
|
|
323
|
+
else children.push(child);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return Node.block(name, props, children, openTok.line);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── List Parser ────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
_parseList() {
|
|
333
|
+
const items = [];
|
|
334
|
+
const isTask = this._current().type === T.LIST_ITEM_TASK;
|
|
335
|
+
const ordered = this._current().type === T.LIST_ITEM_ORDERED;
|
|
336
|
+
|
|
337
|
+
while (this._bodyPos < this._bodyTokens.length) {
|
|
338
|
+
const tok = this._current();
|
|
339
|
+
if (!tok) break;
|
|
340
|
+
|
|
341
|
+
// Stop if we see something that's not a list item
|
|
342
|
+
const isListTok = tok.type === T.LIST_ITEM ||
|
|
343
|
+
tok.type === T.LIST_ITEM_ORDERED ||
|
|
344
|
+
tok.type === T.LIST_ITEM_TASK;
|
|
345
|
+
if (!isListTok) break;
|
|
346
|
+
|
|
347
|
+
this._advance();
|
|
348
|
+
const children = this._parseInlineText(tok.value, tok.line);
|
|
349
|
+
|
|
350
|
+
if (tok.type === T.LIST_ITEM_TASK) {
|
|
351
|
+
items.push(Node.taskItem(children, tok.props.state, tok.props.indent, tok.line));
|
|
352
|
+
} else {
|
|
353
|
+
items.push(Node.listItem(children, tok.props.indent, tok.line));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return Node.list(items, ordered, items[0]?.line);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Blockquote Parser ──────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
_parseBlockquote() {
|
|
363
|
+
const lines = [];
|
|
364
|
+
const level = this._current().props.level;
|
|
365
|
+
|
|
366
|
+
while (this._bodyPos < this._bodyTokens.length) {
|
|
367
|
+
const tok = this._current();
|
|
368
|
+
if (!tok || tok.type !== T.BLOCKQUOTE) break;
|
|
369
|
+
if (tok.props.level !== level) break;
|
|
370
|
+
this._advance();
|
|
371
|
+
lines.push(tok.value);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const children = this._parseInlineText(lines.join(' '), lines[0]?.line);
|
|
375
|
+
return Node.blockquote(children, level, lines[0]?.line);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ── Paragraph Parser ───────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
_parseParagraph() {
|
|
381
|
+
const INLINE_TYPES = new Set([
|
|
382
|
+
T.TEXT, T.BOLD, T.ITALIC, T.UNDERLINE, T.STRIKETHROUGH,
|
|
383
|
+
T.SUPERSCRIPT, T.SUBSCRIPT, T.CODE_INLINE, T.HIGHLIGHT,
|
|
384
|
+
T.INLINE_STYLED, T.LINK, T.FOOTNOTE_REF, T.EXPRESSION,
|
|
385
|
+
T.MATH_INLINE,
|
|
386
|
+
]);
|
|
387
|
+
|
|
388
|
+
const children = [];
|
|
389
|
+
const lineNum = this._current()?.line;
|
|
390
|
+
|
|
391
|
+
// Collect all consecutive inline tokens
|
|
392
|
+
while (this._bodyPos < this._bodyTokens.length) {
|
|
393
|
+
const tok = this._current();
|
|
394
|
+
if (!tok) break;
|
|
395
|
+
if (tok.type === T.NEWLINE) {
|
|
396
|
+
// Two newlines in a row = paragraph break
|
|
397
|
+
const next = this._peek(1);
|
|
398
|
+
if (!next || next.type === T.NEWLINE) break;
|
|
399
|
+
// Single newline — soft break, continue paragraph
|
|
400
|
+
this._advance();
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (!INLINE_TYPES.has(tok.type)) break;
|
|
404
|
+
|
|
405
|
+
children.push(this._parseInlineNode(tok));
|
|
406
|
+
this._advance();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (children.length === 0) return null;
|
|
410
|
+
return Node.paragraph(children, lineNum);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Conditional Parser ─────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
_parseConditional() {
|
|
416
|
+
const tok = this._current();
|
|
417
|
+
const condition = tok.value;
|
|
418
|
+
this._advance(); // consume @if
|
|
419
|
+
|
|
420
|
+
const consequent = [];
|
|
421
|
+
const alternates = [];
|
|
422
|
+
let fallback = [];
|
|
423
|
+
let branch = 'if'; // 'if' | 'elseif' | 'else'
|
|
424
|
+
let currentAlt = null;
|
|
425
|
+
|
|
426
|
+
while (this._bodyPos < this._bodyTokens.length) {
|
|
427
|
+
const t = this._current();
|
|
428
|
+
if (!t || t.type === T.EOF) break;
|
|
429
|
+
|
|
430
|
+
if (t.type === T.END_LOGIC) {
|
|
431
|
+
this._advance();
|
|
432
|
+
if (currentAlt) {
|
|
433
|
+
alternates.push(currentAlt);
|
|
434
|
+
currentAlt = null;
|
|
435
|
+
}
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (t.type === T.ELSEIF) {
|
|
440
|
+
if (currentAlt) alternates.push(currentAlt);
|
|
441
|
+
currentAlt = { condition: t.value, body: [] };
|
|
442
|
+
branch = 'elseif';
|
|
443
|
+
this._advance();
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (t.type === T.ELSE) {
|
|
448
|
+
if (currentAlt) { alternates.push(currentAlt); currentAlt = null; }
|
|
449
|
+
branch = 'else';
|
|
450
|
+
this._advance();
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const node = this._parseNode();
|
|
455
|
+
if (!node) continue;
|
|
456
|
+
|
|
457
|
+
if (branch === 'if') consequent.push(node);
|
|
458
|
+
else if (branch === 'elseif' && currentAlt) currentAlt.body.push(node);
|
|
459
|
+
else if (branch === 'else') fallback.push(node);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return Node.conditional(condition, consequent, alternates, fallback, tok.line);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ── Loop Parser ────────────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
_parseLoop() {
|
|
468
|
+
const tok = this._current();
|
|
469
|
+
this._advance(); // consume @each
|
|
470
|
+
|
|
471
|
+
const variable = tok.value;
|
|
472
|
+
const source = tok.props.source;
|
|
473
|
+
const body = [];
|
|
474
|
+
|
|
475
|
+
while (this._bodyPos < this._bodyTokens.length) {
|
|
476
|
+
const t = this._current();
|
|
477
|
+
if (!t || t.type === T.EOF) break;
|
|
478
|
+
if (t.type === T.END_LOGIC) { this._advance(); break; }
|
|
479
|
+
|
|
480
|
+
const node = this._parseNode();
|
|
481
|
+
if (node) body.push(node);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return Node.loop(variable, source, body, tok.line);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── Keyframe Parser ────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
_parseKeyframe() {
|
|
490
|
+
const tok = this._current();
|
|
491
|
+
this._advance();
|
|
492
|
+
const name = tok.value;
|
|
493
|
+
const steps = {};
|
|
494
|
+
|
|
495
|
+
// Collect text lines and parse as keyframe steps
|
|
496
|
+
while (this._bodyPos < this._bodyTokens.length) {
|
|
497
|
+
const t = this._current();
|
|
498
|
+
if (!t || t.type === T.EOF || t.type === T.BLOCK_CLOSE) break;
|
|
499
|
+
if (t.type === T.TEXT) {
|
|
500
|
+
// e.g. "0% { transform: scale(1) }"
|
|
501
|
+
const stepMatch = t.value.match(/^(\d+%|from|to)\s*\{([^}]*)\}/);
|
|
502
|
+
if (stepMatch) {
|
|
503
|
+
steps[stepMatch[1]] = stepMatch[2].trim();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
this._advance();
|
|
507
|
+
}
|
|
508
|
+
if (this._current()?.type === T.BLOCK_CLOSE) this._advance();
|
|
509
|
+
|
|
510
|
+
return Node.keyframe(name, steps, tok.line);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── Inline Node Builder ────────────────────────────────────────
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Build an inline AST node from a single token.
|
|
517
|
+
* Called during paragraph parsing.
|
|
518
|
+
*/
|
|
519
|
+
_parseInlineNode(tok) {
|
|
520
|
+
switch (tok.type) {
|
|
521
|
+
case T.TEXT: return Node.text(tok.value, tok.line);
|
|
522
|
+
case T.BOLD: return Node.bold(this._parseInlineText(tok.value, tok.line), tok.line);
|
|
523
|
+
case T.ITALIC: return Node.italic(this._parseInlineText(tok.value, tok.line), tok.line);
|
|
524
|
+
case T.UNDERLINE: return Node.underline(this._parseInlineText(tok.value, tok.line), tok.line);
|
|
525
|
+
case T.STRIKETHROUGH: return Node.strikethrough(this._parseInlineText(tok.value, tok.line), tok.line);
|
|
526
|
+
case T.SUPERSCRIPT: return Node.superscript(this._parseInlineText(tok.value, tok.line), tok.line);
|
|
527
|
+
case T.SUBSCRIPT: return Node.subscript(this._parseInlineText(tok.value, tok.line), tok.line);
|
|
528
|
+
case T.CODE_INLINE: return Node.codeInline(tok.value, tok.line);
|
|
529
|
+
case T.HIGHLIGHT: return Node.highlight(this._parseInlineText(tok.value, tok.line), tok.line);
|
|
530
|
+
case T.EXPRESSION: return Node.expression(tok.value, tok.line);
|
|
531
|
+
case T.MATH_INLINE: return Node.mathInline(tok.value, tok.line);
|
|
532
|
+
case T.FOOTNOTE_REF: return Node.footnoteRef(tok.value, tok.line);
|
|
533
|
+
|
|
534
|
+
case T.INLINE_STYLED:
|
|
535
|
+
return Node.inlineStyled(
|
|
536
|
+
this._parseInlineText(tok.value, tok.line),
|
|
537
|
+
tok.props.props || {},
|
|
538
|
+
tok.line
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
case T.LINK:
|
|
542
|
+
return Node.link(
|
|
543
|
+
this._parseInlineText(tok.value, tok.line),
|
|
544
|
+
tok.props.href || '',
|
|
545
|
+
tok.line
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
default:
|
|
549
|
+
return Node.text(tok.value || '', tok.line);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Re-tokenize a plain string as inline nodes.
|
|
555
|
+
* Used for heading text, list items, and blockquote content.
|
|
556
|
+
*/
|
|
557
|
+
_parseInlineText(text, lineNum) {
|
|
558
|
+
if (!text || text.trim() === '') return [];
|
|
559
|
+
|
|
560
|
+
// We re-use the tokenizer's inline method
|
|
561
|
+
const Tokenizer = require('../tokenizer');
|
|
562
|
+
const tmpTok = new Tokenizer('');
|
|
563
|
+
const inlineToks = tmpTok._tokenizeInline(text, lineNum || 0);
|
|
564
|
+
|
|
565
|
+
return inlineToks.map(tok => this._parseInlineNode(tok)).filter(Boolean);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ── Cursor Helpers ─────────────────────────────────────────────
|
|
569
|
+
|
|
570
|
+
_current() {
|
|
571
|
+
return this._bodyTokens[this._bodyPos] || null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
_peek(offset = 1) {
|
|
575
|
+
return this._bodyTokens[this._bodyPos + offset] || null;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
_advance() {
|
|
579
|
+
this._bodyPos++;
|
|
580
|
+
return this._bodyTokens[this._bodyPos - 1];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
_error(msg, tok) {
|
|
584
|
+
const err = {
|
|
585
|
+
code: 'APX-PARSE',
|
|
586
|
+
message: msg,
|
|
587
|
+
line: tok?.line || 0,
|
|
588
|
+
};
|
|
589
|
+
this.errors.push(err);
|
|
590
|
+
return err;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
module.exports = Parser;
|