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,518 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ApexDoc Tokenizer
|
|
5
|
+
* Converts raw .apx source text into a flat array of tokens.
|
|
6
|
+
* Each token = { type, value, props, line, col }
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const T = require('./tokens');
|
|
10
|
+
|
|
11
|
+
// Top-level sections (%%meta, %%style, %%body)
|
|
12
|
+
const TOP_SECTIONS = new Set(['meta', 'style', 'body']);
|
|
13
|
+
|
|
14
|
+
// Blocks that self-close (no %%end needed when written as %%name {props}%%end inline)
|
|
15
|
+
const SELF_CLOSE_BLOCKS = new Set([
|
|
16
|
+
'chart', 'image', 'video', 'audio', 'clock', 'countdown', 'divider',
|
|
17
|
+
'spacer', 'progress', 'qr', 'map', 'lottie', 'icon', 'rating',
|
|
18
|
+
'back-to-top', 'breadcrumbs', 'sidenav', 'toc', 'progress-bar',
|
|
19
|
+
'theme', 'theme-switcher', 'stat', 'signature', 'feed',
|
|
20
|
+
'slider', 'toggle', 'unit',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
class Tokenizer {
|
|
24
|
+
constructor(source) {
|
|
25
|
+
this.source = source;
|
|
26
|
+
this.pos = 0;
|
|
27
|
+
this.line = 1;
|
|
28
|
+
this.col = 1;
|
|
29
|
+
this.tokens = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Main Entry ─────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
tokenize() {
|
|
35
|
+
const lines = this.source.split('\n');
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
this.line = i + 1;
|
|
39
|
+
const raw = lines[i];
|
|
40
|
+
const line = raw.trimEnd();
|
|
41
|
+
|
|
42
|
+
// Empty line
|
|
43
|
+
if (line.trim() === '') {
|
|
44
|
+
this.push(T.NEWLINE, '', {}, i + 1);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Block directives %%name {props} ──────────────────────
|
|
49
|
+
if (line.trimStart().startsWith('%%')) {
|
|
50
|
+
this._tokenizeBlock(line.trim(), i + 1);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── Context-dependent: inside %%meta or %%style ──────────
|
|
55
|
+
// MUST come before @ check so @theme/@import inside %%style
|
|
56
|
+
// are handled by the style tokenizer, not the logic tokenizer.
|
|
57
|
+
const ctx = this._currentContext();
|
|
58
|
+
|
|
59
|
+
if (ctx === 'meta') {
|
|
60
|
+
this._tokenizeMetaField(line, i + 1);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (ctx === 'style') {
|
|
65
|
+
this._tokenizeStyleLine(line, i + 1);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Logic directives @set / @if / @each / @end ───────────
|
|
70
|
+
if (line.trimStart().startsWith('@')) {
|
|
71
|
+
this._tokenizeLogic(line.trim(), i + 1);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Body content ─────────────────────────────────────────
|
|
76
|
+
this._tokenizeBodyLine(line, i + 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
this.push(T.EOF, '', {}, this.line);
|
|
80
|
+
return this.tokens;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Block Tokenizer ────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
_tokenizeBlock(line, lineNum) {
|
|
86
|
+
// %%end — closes current block or section
|
|
87
|
+
if (line === '%%end') {
|
|
88
|
+
const ctx = this._currentContext();
|
|
89
|
+
if (TOP_SECTIONS.has(ctx)) {
|
|
90
|
+
this.push(T.SECTION_CLOSE, ctx, {}, lineNum);
|
|
91
|
+
} else {
|
|
92
|
+
this.push(T.BLOCK_CLOSE, ctx, {}, lineNum);
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// %%name {props}%%end — self-closing inline
|
|
98
|
+
const selfCloseMatch = line.match(/^%%([a-z0-9-]+)(?:\s*(\{[^}]*\}))?%%end$/i);
|
|
99
|
+
if (selfCloseMatch) {
|
|
100
|
+
const name = selfCloseMatch[1].toLowerCase();
|
|
101
|
+
const props = selfCloseMatch[2] ? this._parseProps(selfCloseMatch[2]) : {};
|
|
102
|
+
this.push(T.BLOCK_SELF_CLOSE, name, props, lineNum);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// %%name {props} or %%name
|
|
107
|
+
const blockMatch = line.match(/^%%([a-z0-9-]+)(?:\s+(\{[\s\S]*\}))?$/i);
|
|
108
|
+
if (blockMatch) {
|
|
109
|
+
const name = blockMatch[1].toLowerCase();
|
|
110
|
+
const props = blockMatch[2] ? this._parseProps(blockMatch[2]) : {};
|
|
111
|
+
|
|
112
|
+
if (TOP_SECTIONS.has(name)) {
|
|
113
|
+
this.push(T.SECTION_OPEN, name, props, lineNum);
|
|
114
|
+
} else {
|
|
115
|
+
this.push(T.BLOCK_OPEN, name, props, lineNum);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Fallback — treat as text
|
|
121
|
+
this.push(T.TEXT, line, {}, lineNum);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Logic Tokenizer ────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
_tokenizeLogic(line, lineNum) {
|
|
127
|
+
// @set name = value
|
|
128
|
+
const setMatch = line.match(/^@set\s+(\w+)\s*=\s*(.+)$/);
|
|
129
|
+
if (setMatch) {
|
|
130
|
+
this.push(T.SET, setMatch[1], { value: setMatch[2].trim() }, lineNum);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// @if condition
|
|
135
|
+
const ifMatch = line.match(/^@if\s+(.+)$/);
|
|
136
|
+
if (ifMatch) {
|
|
137
|
+
this.push(T.IF, ifMatch[1].trim(), {}, lineNum);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// @elseif condition
|
|
142
|
+
const elseifMatch = line.match(/^@elseif\s+(.+)$/);
|
|
143
|
+
if (elseifMatch) {
|
|
144
|
+
this.push(T.ELSEIF, elseifMatch[1].trim(), {}, lineNum);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// @else
|
|
149
|
+
if (line === '@else') {
|
|
150
|
+
this.push(T.ELSE, '', {}, lineNum);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// @end
|
|
155
|
+
if (line === '@end') {
|
|
156
|
+
this.push(T.END_LOGIC, '', {}, lineNum);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// @each item in list
|
|
161
|
+
const eachMatch = line.match(/^@each\s+(\w+)\s+in\s+(.+)$/);
|
|
162
|
+
if (eachMatch) {
|
|
163
|
+
this.push(T.EACH, eachMatch[1], { source: eachMatch[2].trim() }, lineNum);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// @keyframe name { ... } (single-line — multi-line handled in parser)
|
|
168
|
+
const kfMatch = line.match(/^@keyframe\s+(\w+)/);
|
|
169
|
+
if (kfMatch) {
|
|
170
|
+
this.push(T.KEYFRAME, kfMatch[1], {}, lineNum);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Unknown @ directive — treat as text
|
|
175
|
+
this.push(T.TEXT, line, {}, lineNum);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Meta Field Tokenizer ───────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
_tokenizeMetaField(line, lineNum) {
|
|
181
|
+
// key: value or key: "value"
|
|
182
|
+
const fieldMatch = line.match(/^\s*([\w-]+)\s*:\s*(.*)$/);
|
|
183
|
+
if (fieldMatch) {
|
|
184
|
+
const key = fieldMatch[1].trim();
|
|
185
|
+
const value = this._parseMetaValue(fieldMatch[2].trim());
|
|
186
|
+
this.push(T.META_FIELD, key, { value }, lineNum);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Style Line Tokenizer ───────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
_tokenizeStyleLine(line, lineNum) {
|
|
193
|
+
const trimmed = line.trim();
|
|
194
|
+
|
|
195
|
+
// @import / @media / @breakpoint (without a block {)
|
|
196
|
+
if (trimmed.startsWith('@') && !trimmed.includes('{')) {
|
|
197
|
+
this.push(T.STYLE_DIRECTIVE, trimmed, {}, lineNum);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// @theme name { / @media (...) { / @breakpoint name {
|
|
202
|
+
if (trimmed.startsWith('@') && trimmed.endsWith('{')) {
|
|
203
|
+
const directiveMatch = trimmed.match(/^(@\w+)\s+([^{]+)\s*\{$/);
|
|
204
|
+
if (directiveMatch) {
|
|
205
|
+
this.push(T.STYLE_BLOCK_OPEN, directiveMatch[1], { name: directiveMatch[2].trim() }, lineNum);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Closing brace of a style block
|
|
211
|
+
if (trimmed === '}') {
|
|
212
|
+
this.push(T.STYLE_BLOCK_CLOSE, '', {}, lineNum);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --variable: value
|
|
217
|
+
const varMatch = trimmed.match(/^(--[\w-]+)\s*:\s*(.+)$/);
|
|
218
|
+
if (varMatch) {
|
|
219
|
+
this.push(T.STYLE_VAR, varMatch[1], { value: varMatch[2].trim() }, lineNum);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Regular CSS property inside a style block
|
|
224
|
+
const propMatch = trimmed.match(/^([\w-]+)\s*:\s*(.+)$/);
|
|
225
|
+
if (propMatch) {
|
|
226
|
+
this.push(T.STYLE_VAR, propMatch[1], { value: propMatch[2].trim() }, lineNum);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Body Line Tokenizer ────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
_tokenizeBodyLine(line, lineNum) {
|
|
233
|
+
// Heading # ## ### ####
|
|
234
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
235
|
+
if (headingMatch) {
|
|
236
|
+
const level = headingMatch[1].length;
|
|
237
|
+
const rest = headingMatch[2];
|
|
238
|
+
// Extract inline props from {props} at end of heading
|
|
239
|
+
const { text, props } = this._extractInlineProps(rest);
|
|
240
|
+
this.push(T.HEADING, text, { level, ...props }, lineNum);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Horizontal rule
|
|
245
|
+
if (/^(---|===|~~~)$/.test(line.trim())) {
|
|
246
|
+
this.push(T.HR, line.trim(), {}, lineNum);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Blockquote > text
|
|
251
|
+
if (line.trimStart().startsWith('>')) {
|
|
252
|
+
const level = (line.match(/^(>+)/)?.[1] || '>').length;
|
|
253
|
+
const content = line.replace(/^>+\s?/, '');
|
|
254
|
+
this.push(T.BLOCKQUOTE, content, { level }, lineNum);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Unordered list item
|
|
259
|
+
const ulMatch = line.match(/^(\s*)-\s+(.+)$/);
|
|
260
|
+
if (ulMatch) {
|
|
261
|
+
const indent = ulMatch[1].length;
|
|
262
|
+
const content = ulMatch[2];
|
|
263
|
+
// Task list - [x] / - [ ] / - [~]
|
|
264
|
+
const taskMatch = content.match(/^\[([x ~])\]\s+(.+)$/i);
|
|
265
|
+
if (taskMatch) {
|
|
266
|
+
const state = taskMatch[1] === 'x' ? 'done' : taskMatch[1] === '~' ? 'progress' : 'todo';
|
|
267
|
+
this.push(T.LIST_ITEM_TASK, taskMatch[2], { indent, state }, lineNum);
|
|
268
|
+
} else {
|
|
269
|
+
this.push(T.LIST_ITEM, content, { indent }, lineNum);
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Ordered list item 1. text
|
|
275
|
+
const olMatch = line.match(/^(\s*)(\d+)\.\s+(.+)$/);
|
|
276
|
+
if (olMatch) {
|
|
277
|
+
const indent = olMatch[1].length;
|
|
278
|
+
const num = parseInt(olMatch[2], 10);
|
|
279
|
+
this.push(T.LIST_ITEM_ORDERED, olMatch[3], { indent, num }, lineNum);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Footnote definition [^1]: content
|
|
284
|
+
const fnDefMatch = line.match(/^\[\^(\w+)\]:\s+(.+)$/);
|
|
285
|
+
if (fnDefMatch) {
|
|
286
|
+
this.push(T.FOOTNOTE_DEF, fnDefMatch[1], { content: fnDefMatch[2] }, lineNum);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Regular paragraph — tokenize inline syntax
|
|
291
|
+
const inlineTokens = this._tokenizeInline(line, lineNum);
|
|
292
|
+
for (const tok of inlineTokens) {
|
|
293
|
+
this.tokens.push(tok);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ── Inline Tokenizer ───────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
_tokenizeInline(text, lineNum) {
|
|
300
|
+
const tokens = [];
|
|
301
|
+
let remaining = text;
|
|
302
|
+
let buffer = '';
|
|
303
|
+
|
|
304
|
+
const flush = () => {
|
|
305
|
+
if (buffer.length > 0) {
|
|
306
|
+
tokens.push(this._tok(T.TEXT, buffer, {}, lineNum));
|
|
307
|
+
buffer = '';
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const patterns = [
|
|
312
|
+
// Math inline $...$
|
|
313
|
+
{ re: /^\$([^$]+)\$/, type: T.MATH_INLINE, val: m => m[1] },
|
|
314
|
+
|
|
315
|
+
// Inline styled [text]{props}
|
|
316
|
+
{ re: /^\[([^\]]+)\]\{([^}]*)\}/, type: T.INLINE_STYLED,
|
|
317
|
+
val: m => m[1], extra: m => ({ props: this._parseInlineProps(m[2]) }) },
|
|
318
|
+
|
|
319
|
+
// Link [text](url)
|
|
320
|
+
{ re: /^\[([^\]]+)\]\(([^)]+)\)/, type: T.LINK,
|
|
321
|
+
val: m => m[1], extra: m => ({ href: m[2] }) },
|
|
322
|
+
|
|
323
|
+
// Footnote ref [^1]
|
|
324
|
+
{ re: /^\[\^(\w+)\]/, type: T.FOOTNOTE_REF, val: m => m[1] },
|
|
325
|
+
|
|
326
|
+
// Bold **text**
|
|
327
|
+
{ re: /^\*\*([^*]+)\*\*/, type: T.BOLD, val: m => m[1] },
|
|
328
|
+
|
|
329
|
+
// Italic _text_
|
|
330
|
+
{ re: /^_([^_]+)_/, type: T.ITALIC, val: m => m[1] },
|
|
331
|
+
|
|
332
|
+
// Underline __text__
|
|
333
|
+
{ re: /^__([^_]+)__/, type: T.UNDERLINE, val: m => m[1] },
|
|
334
|
+
|
|
335
|
+
// Strikethrough ~~text~~
|
|
336
|
+
{ re: /^~~([^~]+)~~/, type: T.STRIKETHROUGH, val: m => m[1] },
|
|
337
|
+
|
|
338
|
+
// Superscript ^^text^^
|
|
339
|
+
{ re: /^\^\^([^^]+)\^\^/, type: T.SUPERSCRIPT, val: m => m[1] },
|
|
340
|
+
|
|
341
|
+
// Subscript ,,text,,
|
|
342
|
+
{ re: /^,,([^,]+),,/, type: T.SUBSCRIPT, val: m => m[1] },
|
|
343
|
+
|
|
344
|
+
// Code inline `text`
|
|
345
|
+
{ re: /^`([^`]+)`/, type: T.CODE_INLINE, val: m => m[1] },
|
|
346
|
+
|
|
347
|
+
// Highlight ==text==
|
|
348
|
+
{ re: /^==([^=]+)==/, type: T.HIGHLIGHT, val: m => m[1] },
|
|
349
|
+
|
|
350
|
+
// Expression {expr}
|
|
351
|
+
{ re: /^\{([^}]+)\}/, type: T.EXPRESSION, val: m => m[1] },
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
while (remaining.length > 0) {
|
|
355
|
+
let matched = false;
|
|
356
|
+
|
|
357
|
+
for (const pat of patterns) {
|
|
358
|
+
const m = remaining.match(pat.re);
|
|
359
|
+
if (m) {
|
|
360
|
+
flush();
|
|
361
|
+
const extra = pat.extra ? pat.extra(m) : {};
|
|
362
|
+
tokens.push(this._tok(pat.type, pat.val(m), extra, lineNum));
|
|
363
|
+
remaining = remaining.slice(m[0].length);
|
|
364
|
+
matched = true;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!matched) {
|
|
370
|
+
buffer += remaining[0];
|
|
371
|
+
remaining = remaining.slice(1);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
flush();
|
|
376
|
+
return tokens;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ── Helpers ────────────────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Parse a props string like {type: bar, data: sales, animated: true}
|
|
383
|
+
* into a plain object.
|
|
384
|
+
*/
|
|
385
|
+
_parseProps(str) {
|
|
386
|
+
const result = {};
|
|
387
|
+
// Remove outer braces
|
|
388
|
+
const inner = str.replace(/^\{|\}$/g, '').trim();
|
|
389
|
+
if (!inner) return result;
|
|
390
|
+
|
|
391
|
+
// Split on commas that are NOT inside quotes or brackets
|
|
392
|
+
const parts = this._splitProps(inner);
|
|
393
|
+
|
|
394
|
+
for (const part of parts) {
|
|
395
|
+
const colonIdx = part.indexOf(':');
|
|
396
|
+
if (colonIdx === -1) continue;
|
|
397
|
+
const key = part.slice(0, colonIdx).trim();
|
|
398
|
+
const value = part.slice(colonIdx + 1).trim();
|
|
399
|
+
result[key] = this._parseMetaValue(value);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_parseInlineProps(str) {
|
|
406
|
+
return this._parseProps(`{${str}}`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Split a props string on commas, respecting nested brackets and quotes.
|
|
411
|
+
*/
|
|
412
|
+
_splitProps(str) {
|
|
413
|
+
const parts = [];
|
|
414
|
+
let depth = 0;
|
|
415
|
+
let inQuote = false;
|
|
416
|
+
let quoteChar = '';
|
|
417
|
+
let current = '';
|
|
418
|
+
|
|
419
|
+
for (let i = 0; i < str.length; i++) {
|
|
420
|
+
const ch = str[i];
|
|
421
|
+
|
|
422
|
+
if (!inQuote && (ch === '"' || ch === "'")) {
|
|
423
|
+
inQuote = true;
|
|
424
|
+
quoteChar = ch;
|
|
425
|
+
current += ch;
|
|
426
|
+
} else if (inQuote && ch === quoteChar) {
|
|
427
|
+
inQuote = false;
|
|
428
|
+
current += ch;
|
|
429
|
+
} else if (!inQuote && (ch === '(' || ch === '[' || ch === '{')) {
|
|
430
|
+
depth++;
|
|
431
|
+
current += ch;
|
|
432
|
+
} else if (!inQuote && (ch === ')' || ch === ']' || ch === '}')) {
|
|
433
|
+
depth--;
|
|
434
|
+
current += ch;
|
|
435
|
+
} else if (!inQuote && depth === 0 && ch === ',') {
|
|
436
|
+
parts.push(current.trim());
|
|
437
|
+
current = '';
|
|
438
|
+
} else {
|
|
439
|
+
current += ch;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (current.trim()) parts.push(current.trim());
|
|
444
|
+
return parts;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Parse a meta value — handles strings, booleans, numbers, arrays
|
|
449
|
+
*/
|
|
450
|
+
_parseMetaValue(raw) {
|
|
451
|
+
if (!raw) return '';
|
|
452
|
+
|
|
453
|
+
// String with quotes
|
|
454
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) ||
|
|
455
|
+
(raw.startsWith("'") && raw.endsWith("'"))) {
|
|
456
|
+
return raw.slice(1, -1);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Array [a, b, c]
|
|
460
|
+
if (raw.startsWith('[') && raw.endsWith(']')) {
|
|
461
|
+
const inner = raw.slice(1, -1);
|
|
462
|
+
return this._splitProps(inner).map(v => this._parseMetaValue(v.trim()));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Boolean
|
|
466
|
+
if (raw === 'true') return true;
|
|
467
|
+
if (raw === 'false') return false;
|
|
468
|
+
|
|
469
|
+
// Null / undefined
|
|
470
|
+
if (raw === 'null' || raw === 'never' || raw === '') return null;
|
|
471
|
+
|
|
472
|
+
// Number
|
|
473
|
+
if (!isNaN(raw) && raw !== '') return parseFloat(raw);
|
|
474
|
+
|
|
475
|
+
// Semver / date / plain string
|
|
476
|
+
return raw;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Extract {props} from end of a heading line
|
|
481
|
+
* "My Heading {color: red}" → { text: "My Heading", props: { color: "red" } }
|
|
482
|
+
*/
|
|
483
|
+
_extractInlineProps(text) {
|
|
484
|
+
const match = text.match(/^(.*?)\s*(\{[^}]*\})\s*$/);
|
|
485
|
+
if (match) {
|
|
486
|
+
return {
|
|
487
|
+
text: match[1].trim(),
|
|
488
|
+
props: this._parseProps(match[2]),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return { text, props: {} };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Returns the current open context (meta/style/body/blockname)
|
|
496
|
+
*/
|
|
497
|
+
_currentContext() {
|
|
498
|
+
// Walk backwards through tokens to find last open section/block
|
|
499
|
+
for (let i = this.tokens.length - 1; i >= 0; i--) {
|
|
500
|
+
const tok = this.tokens[i];
|
|
501
|
+
if (tok.type === T.SECTION_OPEN) return tok.value;
|
|
502
|
+
if (tok.type === T.SECTION_CLOSE) return 'root';
|
|
503
|
+
if (tok.type === T.BLOCK_OPEN) return tok.value;
|
|
504
|
+
if (tok.type === T.BLOCK_CLOSE) continue;
|
|
505
|
+
}
|
|
506
|
+
return 'root';
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
_tok(type, value, props, line) {
|
|
510
|
+
return { type, value, props: props || {}, line };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
push(type, value, props, line) {
|
|
514
|
+
this.tokens.push(this._tok(type, value, props, line));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
module.exports = Tokenizer;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ApexDoc Token Types
|
|
3
|
+
* Every piece of an .apx file becomes one of these tokens
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const T = {
|
|
7
|
+
// ── Document Structure ──────────────────────────────────────────
|
|
8
|
+
SECTION_OPEN: 'SECTION_OPEN', // %%meta %%style %%body
|
|
9
|
+
SECTION_CLOSE: 'SECTION_CLOSE', // %%end (closing a top-level section)
|
|
10
|
+
BLOCK_OPEN: 'BLOCK_OPEN', // %%box {props}
|
|
11
|
+
BLOCK_CLOSE: 'BLOCK_CLOSE', // %%end (closing a block)
|
|
12
|
+
BLOCK_SELF_CLOSE: 'BLOCK_SELF_CLOSE', // %%chart {props}%%end
|
|
13
|
+
|
|
14
|
+
// ── Headings ────────────────────────────────────────────────────
|
|
15
|
+
HEADING: 'HEADING', // # ## ### ####
|
|
16
|
+
|
|
17
|
+
// ── Inline Text ─────────────────────────────────────────────────
|
|
18
|
+
TEXT: 'TEXT', // plain text
|
|
19
|
+
BOLD: 'BOLD', // **text**
|
|
20
|
+
ITALIC: 'ITALIC', // _text_
|
|
21
|
+
UNDERLINE: 'UNDERLINE', // __text__
|
|
22
|
+
STRIKETHROUGH: 'STRIKETHROUGH', // ~~text~~
|
|
23
|
+
SUPERSCRIPT: 'SUPERSCRIPT', // ^^text^^
|
|
24
|
+
SUBSCRIPT: 'SUBSCRIPT', // ,,text,,
|
|
25
|
+
CODE_INLINE: 'CODE_INLINE', // `text`
|
|
26
|
+
HIGHLIGHT: 'HIGHLIGHT', // ==text==
|
|
27
|
+
INLINE_STYLED: 'INLINE_STYLED', // [text]{props}
|
|
28
|
+
LINK: 'LINK', // [text](url)
|
|
29
|
+
|
|
30
|
+
// ── Math ────────────────────────────────────────────────────────
|
|
31
|
+
MATH_INLINE: 'MATH_INLINE', // $...$
|
|
32
|
+
MATH_BLOCK: 'MATH_BLOCK', // %%math...%%end
|
|
33
|
+
|
|
34
|
+
// ── Lists ───────────────────────────────────────────────────────
|
|
35
|
+
LIST_ITEM: 'LIST_ITEM', // - item
|
|
36
|
+
LIST_ITEM_ORDERED: 'LIST_ITEM_ORDERED', // 1. item
|
|
37
|
+
LIST_ITEM_TASK: 'LIST_ITEM_TASK', // - [x] / - [ ] / - [~]
|
|
38
|
+
|
|
39
|
+
// ── Logic ───────────────────────────────────────────────────────
|
|
40
|
+
SET: 'SET', // @set x = value
|
|
41
|
+
IF: 'IF', // @if condition
|
|
42
|
+
ELSEIF: 'ELSEIF', // @elseif condition
|
|
43
|
+
ELSE: 'ELSE', // @else
|
|
44
|
+
END_LOGIC: 'END_LOGIC', // @end
|
|
45
|
+
EACH: 'EACH', // @each item in list
|
|
46
|
+
KEYFRAME: 'KEYFRAME', // @keyframe name { }
|
|
47
|
+
|
|
48
|
+
// ── Variables & Expressions ─────────────────────────────────────
|
|
49
|
+
EXPRESSION: 'EXPRESSION', // {expr} {name} {fn()}
|
|
50
|
+
|
|
51
|
+
// ── Metadata ────────────────────────────────────────────────────
|
|
52
|
+
META_FIELD: 'META_FIELD', // key: value inside %%meta
|
|
53
|
+
|
|
54
|
+
// ── Style ───────────────────────────────────────────────────────
|
|
55
|
+
STYLE_VAR: 'STYLE_VAR', // --primary: #00f5d4
|
|
56
|
+
STYLE_DIRECTIVE: 'STYLE_DIRECTIVE', // @theme / @import / @media / @breakpoint
|
|
57
|
+
STYLE_BLOCK_OPEN: 'STYLE_BLOCK_OPEN', // @theme name {
|
|
58
|
+
STYLE_BLOCK_CLOSE: 'STYLE_BLOCK_CLOSE', // }
|
|
59
|
+
|
|
60
|
+
// ── Blockquote ──────────────────────────────────────────────────
|
|
61
|
+
BLOCKQUOTE: 'BLOCKQUOTE', // > text
|
|
62
|
+
|
|
63
|
+
// ── Horizontal Rule ─────────────────────────────────────────────
|
|
64
|
+
HR: 'HR', // --- === ~~~
|
|
65
|
+
|
|
66
|
+
// ── Footnote ────────────────────────────────────────────────────
|
|
67
|
+
FOOTNOTE_REF: 'FOOTNOTE_REF', // [^1]
|
|
68
|
+
FOOTNOTE_DEF: 'FOOTNOTE_DEF', // [^1]: content
|
|
69
|
+
|
|
70
|
+
// ── Special ─────────────────────────────────────────────────────
|
|
71
|
+
NEWLINE: 'NEWLINE',
|
|
72
|
+
EOF: 'EOF',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
module.exports = T;
|