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.
@@ -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;