@regmisatyam/retex 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/dist/index.cjs ADDED
@@ -0,0 +1,3444 @@
1
+ 'use strict';
2
+
3
+ // src/types/source.ts
4
+ function pos(offset, line, column) {
5
+ return { offset, line, column };
6
+ }
7
+ function range(start, end) {
8
+ return { start, end };
9
+ }
10
+ function mergeRanges(a, b) {
11
+ return {
12
+ start: a.start.offset <= b.start.offset ? a.start : b.start,
13
+ end: a.end.offset >= b.end.offset ? a.end : b.end
14
+ };
15
+ }
16
+ function rangeContains(r, offset) {
17
+ return offset >= r.start.offset && offset <= r.end.offset;
18
+ }
19
+
20
+ // src/types/tokens.ts
21
+ var TokenType = /* @__PURE__ */ ((TokenType2) => {
22
+ TokenType2["Command"] = "Command";
23
+ TokenType2["LBrace"] = "LBrace";
24
+ TokenType2["RBrace"] = "RBrace";
25
+ TokenType2["LBracket"] = "LBracket";
26
+ TokenType2["RBracket"] = "RBracket";
27
+ TokenType2["Text"] = "Text";
28
+ TokenType2["Whitespace"] = "Whitespace";
29
+ TokenType2["ParBreak"] = "ParBreak";
30
+ TokenType2["LineBreak"] = "LineBreak";
31
+ TokenType2["Comment"] = "Comment";
32
+ TokenType2["EOF"] = "EOF";
33
+ return TokenType2;
34
+ })(TokenType || {});
35
+ var SPECIAL_CHARS = /* @__PURE__ */ new Set(["\\", "{", "}", "[", "]"]);
36
+
37
+ // src/types/ast.ts
38
+ function isParent(node) {
39
+ return Array.isArray(node.children);
40
+ }
41
+ function isNode(node, type) {
42
+ return node.type === type;
43
+ }
44
+
45
+ // src/types/diagnostics.ts
46
+ var DiagnosticSeverity = /* @__PURE__ */ ((DiagnosticSeverity2) => {
47
+ DiagnosticSeverity2["Error"] = "error";
48
+ DiagnosticSeverity2["Warning"] = "warning";
49
+ DiagnosticSeverity2["Info"] = "info";
50
+ DiagnosticSeverity2["Hint"] = "hint";
51
+ return DiagnosticSeverity2;
52
+ })(DiagnosticSeverity || {});
53
+ var DiagnosticCode = /* @__PURE__ */ ((DiagnosticCode2) => {
54
+ DiagnosticCode2["UnexpectedToken"] = "RTX1001";
55
+ DiagnosticCode2["UnterminatedGroup"] = "RTX1002";
56
+ DiagnosticCode2["UnterminatedArgument"] = "RTX1003";
57
+ DiagnosticCode2["UnexpectedEOF"] = "RTX1004";
58
+ DiagnosticCode2["MismatchedBrace"] = "RTX1005";
59
+ DiagnosticCode2["UnknownCommand"] = "RTX2001";
60
+ DiagnosticCode2["UnknownEnvironment"] = "RTX2002";
61
+ DiagnosticCode2["MissingRequiredArgument"] = "RTX2003";
62
+ DiagnosticCode2["TooManyArguments"] = "RTX2004";
63
+ DiagnosticCode2["MissingEnvironmentEnd"] = "RTX2005";
64
+ DiagnosticCode2["MismatchedEnvironment"] = "RTX2006";
65
+ DiagnosticCode2["InvalidColor"] = "RTX3001";
66
+ DiagnosticCode2["InvalidUrl"] = "RTX3002";
67
+ DiagnosticCode2["InvalidDimension"] = "RTX3003";
68
+ DiagnosticCode2["MissingRequiredField"] = "RTX3004";
69
+ DiagnosticCode2["UnknownField"] = "RTX3005";
70
+ DiagnosticCode2["EmptyArgument"] = "RTX3006";
71
+ DiagnosticCode2["CommandOutsideContext"] = "RTX4001";
72
+ DiagnosticCode2["UnknownIcon"] = "RTX4002";
73
+ DiagnosticCode2["UnknownThemeColor"] = "RTX4003";
74
+ DiagnosticCode2["UnsafeUrlBlocked"] = "RTX5001";
75
+ return DiagnosticCode2;
76
+ })(DiagnosticCode || {});
77
+
78
+ // src/tokenizer/tokenizer.ts
79
+ var isLetter = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z";
80
+ var isInlineSpace = (c) => c === " " || c === " " || c === "\r";
81
+ var Tokenizer = class _Tokenizer {
82
+ constructor(source) {
83
+ this.offset = 0;
84
+ this.line = 1;
85
+ this.column = 1;
86
+ this.tokens = [];
87
+ this.diagnostics = [];
88
+ this.src = source.replace(/\r\n?/g, "\n");
89
+ }
90
+ static tokenize(source) {
91
+ return new _Tokenizer(source).run();
92
+ }
93
+ run() {
94
+ while (!this.atEnd()) {
95
+ this.scanToken();
96
+ }
97
+ this.push("EOF" /* EOF */, "", this.position());
98
+ return { tokens: this.tokens, diagnostics: this.diagnostics };
99
+ }
100
+ /* --------------------------- scanners --------------------------- */
101
+ scanToken() {
102
+ const start = this.position();
103
+ const c = this.peek();
104
+ switch (c) {
105
+ case "\\":
106
+ this.scanBackslash(start);
107
+ return;
108
+ case "{":
109
+ this.read();
110
+ this.push("LBrace" /* LBrace */, "{", start);
111
+ return;
112
+ case "}":
113
+ this.read();
114
+ this.push("RBrace" /* RBrace */, "}", start);
115
+ return;
116
+ case "[":
117
+ this.read();
118
+ this.push("LBracket" /* LBracket */, "[", start);
119
+ return;
120
+ case "]":
121
+ this.read();
122
+ this.push("RBracket" /* RBracket */, "]", start);
123
+ return;
124
+ case "%":
125
+ if (this.peek(1) === "%") {
126
+ this.scanComment(start);
127
+ return;
128
+ }
129
+ this.scanText(start);
130
+ return;
131
+ default:
132
+ if (isInlineSpace(c) || c === "\n") {
133
+ this.scanWhitespace(start);
134
+ return;
135
+ }
136
+ this.scanText(start);
137
+ }
138
+ }
139
+ scanBackslash(start) {
140
+ this.read();
141
+ const next = this.peek();
142
+ if (next === "\\") {
143
+ this.read();
144
+ this.push("LineBreak" /* LineBreak */, "\\\\", start);
145
+ return;
146
+ }
147
+ if (isLetter(next)) {
148
+ let name = "";
149
+ while (isLetter(this.peek())) name += this.read();
150
+ if (this.peek() === "*") name += this.read();
151
+ this.push("Command" /* Command */, name, start);
152
+ return;
153
+ }
154
+ if (next !== "" && next !== "\n") {
155
+ const ch = this.read();
156
+ this.push("Text" /* Text */, ch, start);
157
+ return;
158
+ }
159
+ this.push("Text" /* Text */, "\\", start);
160
+ }
161
+ scanComment(start) {
162
+ let value = "";
163
+ while (!this.atEnd() && this.peek() !== "\n") value += this.read();
164
+ this.push("Comment" /* Comment */, value, start);
165
+ }
166
+ scanWhitespace(start) {
167
+ let value = "";
168
+ let newlines = 0;
169
+ while (!this.atEnd()) {
170
+ const c = this.peek();
171
+ if (c === "\n") {
172
+ newlines++;
173
+ value += this.read();
174
+ } else if (isInlineSpace(c)) {
175
+ value += this.read();
176
+ } else {
177
+ break;
178
+ }
179
+ }
180
+ this.push(newlines >= 2 ? "ParBreak" /* ParBreak */ : "Whitespace" /* Whitespace */, value, start);
181
+ }
182
+ scanText(start) {
183
+ let value = "";
184
+ while (!this.atEnd()) {
185
+ const c = this.peek();
186
+ if (c === "\\" || c === "{" || c === "}" || c === "[" || c === "]" || c === "\n" || isInlineSpace(c)) {
187
+ break;
188
+ }
189
+ if (c === "%" && this.peek(1) === "%") break;
190
+ value += this.read();
191
+ }
192
+ if (value === "") value = this.read();
193
+ this.push("Text" /* Text */, value, start);
194
+ }
195
+ /* --------------------------- cursor ----------------------------- */
196
+ atEnd() {
197
+ return this.offset >= this.src.length;
198
+ }
199
+ peek(k = 0) {
200
+ return this.src[this.offset + k] ?? "";
201
+ }
202
+ read() {
203
+ const c = this.src[this.offset] ?? "";
204
+ this.offset++;
205
+ if (c === "\n") {
206
+ this.line++;
207
+ this.column = 1;
208
+ } else {
209
+ this.column++;
210
+ }
211
+ return c;
212
+ }
213
+ position() {
214
+ return { offset: this.offset, line: this.line, column: this.column };
215
+ }
216
+ push(type, value, start) {
217
+ this.tokens.push({ type, value, range: { start, end: this.position() } });
218
+ }
219
+ };
220
+ function tokenize(source) {
221
+ return Tokenizer.tokenize(source);
222
+ }
223
+
224
+ // src/parser/registry.ts
225
+ var CommandRegistry = class _CommandRegistry {
226
+ constructor() {
227
+ this.commands = /* @__PURE__ */ new Map();
228
+ this.environments = /* @__PURE__ */ new Map();
229
+ }
230
+ registerCommand(def) {
231
+ this.commands.set(def.name, def);
232
+ return this;
233
+ }
234
+ registerEnvironment(def) {
235
+ this.environments.set(def.name, def);
236
+ return this;
237
+ }
238
+ getCommand(name) {
239
+ return this.commands.get(name);
240
+ }
241
+ getEnvironment(name) {
242
+ return this.environments.get(name);
243
+ }
244
+ hasCommand(name) {
245
+ return this.commands.has(name);
246
+ }
247
+ hasEnvironment(name) {
248
+ return this.environments.has(name);
249
+ }
250
+ commandNames() {
251
+ return [...this.commands.keys()];
252
+ }
253
+ environmentNames() {
254
+ return [...this.environments.keys()];
255
+ }
256
+ allCommands() {
257
+ return [...this.commands.values()];
258
+ }
259
+ allEnvironments() {
260
+ return [...this.environments.values()];
261
+ }
262
+ /** Shallow-clone the registry (definitions are shared, maps are copied). */
263
+ clone() {
264
+ const next = new _CommandRegistry();
265
+ next.commands = new Map(this.commands);
266
+ next.environments = new Map(this.environments);
267
+ return next;
268
+ }
269
+ };
270
+
271
+ // src/parser/args.ts
272
+ function splitTopLevel(raw, sep = ",") {
273
+ const parts = [];
274
+ let depth = 0;
275
+ let current = "";
276
+ for (let i = 0; i < raw.length; i++) {
277
+ const c = raw[i];
278
+ if (c === "{" || c === "[") depth++;
279
+ else if (c === "}" || c === "]") depth = Math.max(0, depth - 1);
280
+ if (c === sep && depth === 0) {
281
+ parts.push(current);
282
+ current = "";
283
+ } else {
284
+ current += c;
285
+ }
286
+ }
287
+ parts.push(current);
288
+ return parts;
289
+ }
290
+ function parseListArg(raw) {
291
+ return splitTopLevel(raw).map((s) => s.trim()).filter((s) => s.length > 0);
292
+ }
293
+ function parseKeyValArg(raw) {
294
+ const segments = splitTopLevel(raw);
295
+ const merged = [];
296
+ for (const seg of segments) {
297
+ if (!seg.includes("=") && merged.length > 0) {
298
+ merged[merged.length - 1] += "," + seg;
299
+ } else {
300
+ merged.push(seg);
301
+ }
302
+ }
303
+ const fields = {};
304
+ for (const seg of merged) {
305
+ const trimmed = seg.trim();
306
+ if (trimmed === "") continue;
307
+ const eq = trimmed.indexOf("=");
308
+ if (eq === -1) {
309
+ fields[trimmed] = "";
310
+ continue;
311
+ }
312
+ const key = trimmed.slice(0, eq).trim();
313
+ const value = trimmed.slice(eq + 1).trim();
314
+ if (key) fields[key] = value;
315
+ }
316
+ return fields;
317
+ }
318
+
319
+ // src/security/sanitize.ts
320
+ function escapeHtml(input) {
321
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
322
+ }
323
+ function escapeAttribute(input) {
324
+ return escapeHtml(input);
325
+ }
326
+ var SAFE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:", "ftp:", "sms:"]);
327
+ var SCHEME_RE = /^([a-z][a-z0-9+.-]*):/i;
328
+ var CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
329
+ function sanitizeUrl(input) {
330
+ const raw = String(input ?? "").trim();
331
+ const cleaned = raw.replace(CONTROL_CHARS_RE, "");
332
+ const match = SCHEME_RE.exec(cleaned);
333
+ if (!match) {
334
+ return { safe: cleaned, blocked: false };
335
+ }
336
+ const scheme = match[1].toLowerCase() + ":";
337
+ if (SAFE_PROTOCOLS.has(scheme)) {
338
+ return { safe: cleaned, blocked: false, scheme };
339
+ }
340
+ return { safe: "#", blocked: true, scheme };
341
+ }
342
+ function isSafeColor(value) {
343
+ const v = value.trim();
344
+ if (/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(v)) return true;
345
+ if (/^(rgb|rgba|hsl|hsla)\(\s*[0-9.,%\s/]+\)$/i.test(v)) return true;
346
+ return CSS_NAMED_COLORS.has(v.toLowerCase());
347
+ }
348
+ function isSafeDimension(value) {
349
+ return /^-?\d*\.?\d+(px|pt|em|rem|ex|ch|vw|vh|vmin|vmax|cm|mm|in|pc|%|fr)?$/.test(
350
+ value.trim()
351
+ );
352
+ }
353
+ function sanitizeStyleValue(value) {
354
+ const v = value.trim();
355
+ if (/[<>;{}]/.test(v) || /expression|url\s*\(|javascript:/i.test(v)) {
356
+ return void 0;
357
+ }
358
+ return v;
359
+ }
360
+ var CSS_NAMED_COLORS = /* @__PURE__ */ new Set([
361
+ "black",
362
+ "silver",
363
+ "gray",
364
+ "grey",
365
+ "white",
366
+ "maroon",
367
+ "red",
368
+ "purple",
369
+ "fuchsia",
370
+ "green",
371
+ "lime",
372
+ "olive",
373
+ "yellow",
374
+ "navy",
375
+ "blue",
376
+ "teal",
377
+ "aqua",
378
+ "cyan",
379
+ "magenta",
380
+ "orange",
381
+ "pink",
382
+ "brown",
383
+ "gold",
384
+ "indigo",
385
+ "violet",
386
+ "tan",
387
+ "beige",
388
+ "ivory",
389
+ "coral",
390
+ "salmon",
391
+ "khaki",
392
+ "crimson",
393
+ "turquoise",
394
+ "lavender",
395
+ "plum",
396
+ "orchid",
397
+ "slateblue",
398
+ "slategray",
399
+ "steelblue",
400
+ "skyblue",
401
+ "royalblue",
402
+ "midnightblue",
403
+ "darkblue",
404
+ "darkgreen",
405
+ "darkred",
406
+ "darkgray",
407
+ "darkgrey",
408
+ "lightgray",
409
+ "lightgrey",
410
+ "lightblue",
411
+ "transparent",
412
+ "currentcolor",
413
+ "inherit"
414
+ ]);
415
+
416
+ // src/icons/icons.ts
417
+ var ICONS = {
418
+ github: {
419
+ body: '<path d="M12 1.5A10.5 10.5 0 0 0 8.7 22c.5.1.7-.2.7-.5v-2c-2.9.6-3.5-1.3-3.5-1.3-.5-1.2-1.2-1.5-1.2-1.5-.9-.6.1-.6.1-.6 1 .1 1.6 1 1.6 1 .9 1.6 2.4 1.1 3 .9.1-.7.4-1.1.7-1.4-2.3-.3-4.8-1.2-4.8-5.2 0-1.2.4-2.1 1-2.9-.1-.3-.5-1.3.1-2.7 0 0 .9-.3 2.8 1.1a9.6 9.6 0 0 1 5 0c1.9-1.4 2.8-1.1 2.8-1.1.6 1.4.2 2.4.1 2.7.7.8 1 1.7 1 2.9 0 4-2.5 4.9-4.8 5.2.4.3.7 1 .7 2v3c0 .3.2.6.7.5A10.5 10.5 0 0 0 12 1.5Z"/>'
420
+ },
421
+ linkedin: {
422
+ body: '<path d="M20.5 2h-17A1.5 1.5 0 0 0 2 3.5v17A1.5 1.5 0 0 0 3.5 22h17a1.5 1.5 0 0 0 1.5-1.5v-17A1.5 1.5 0 0 0 20.5 2ZM8 19H5v-9h3v9ZM6.5 8.7A1.7 1.7 0 1 1 6.5 5.3a1.7 1.7 0 0 1 0 3.4ZM19 19h-3v-4.7c0-1.1 0-2.6-1.6-2.6s-1.8 1.2-1.8 2.5V19h-3v-9h2.9v1.2h.04a3.2 3.2 0 0 1 2.9-1.6c3.1 0 3.7 2 3.7 4.7V19Z"/>'
423
+ },
424
+ email: {
425
+ body: '<path d="M3 5h18a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Zm.4 2 8.6 6 8.6-6H3.4ZM20 8.2l-7.4 5.2a1 1 0 0 1-1.2 0L4 8.2V17h16V8.2Z"/>',
426
+ aliases: ["mail", "envelope"]
427
+ },
428
+ phone: {
429
+ body: '<path d="M6.6 10.8a15.5 15.5 0 0 0 6.6 6.6l2.2-2.2a1 1 0 0 1 1-.25 11.4 11.4 0 0 0 3.6.58 1 1 0 0 1 1 1V20a1 1 0 0 1-1 1A17 17 0 0 1 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1 11.4 11.4 0 0 0 .57 3.6 1 1 0 0 1-.25 1l-2.2 2.2Z"/>',
430
+ aliases: ["tel", "telephone"]
431
+ },
432
+ location: {
433
+ body: '<path d="M12 2a7 7 0 0 0-7 7c0 5 7 13 7 13s7-8 7-13a7 7 0 0 0-7-7Zm0 9.5A2.5 2.5 0 1 1 12 6.5a2.5 2.5 0 0 1 0 5Z"/>',
434
+ aliases: ["map", "pin", "marker", "geo"]
435
+ },
436
+ website: {
437
+ body: '<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm6.9 6h-2.9a15.7 15.7 0 0 0-1.3-3.4A8 8 0 0 1 18.9 8ZM12 4c.8 1.1 1.5 2.5 1.9 4h-3.8c.4-1.5 1.1-2.9 1.9-4ZM4.3 14a8 8 0 0 1 0-4h3.3a17.6 17.6 0 0 0 0 4H4.3Zm.8 2h2.9a15.7 15.7 0 0 0 1.3 3.4A8 8 0 0 1 5.1 16Zm2.9-8H5.1a8 8 0 0 1 4.2-3.4A15.7 15.7 0 0 0 8 8Zm4 12c-.8-1.1-1.5-2.5-1.9-4h3.8c-.4 1.5-1.1 2.9-1.9 4Zm2.3-6h-4.6a15.3 15.3 0 0 1 0-4h4.6a15.3 15.3 0 0 1 0 4Zm.4 5.4a15.7 15.7 0 0 0 1.3-3.4h2.9a8 8 0 0 1-4.2 3.4ZM16.4 14a17.6 17.6 0 0 0 0-4h3.3a8 8 0 0 1 0 4h-3.3Z"/>',
438
+ aliases: ["globe", "web", "link", "url"]
439
+ },
440
+ twitter: {
441
+ body: '<path d="M22 5.9c-.7.3-1.5.6-2.3.7a4 4 0 0 0 1.8-2.2c-.8.5-1.7.8-2.6 1a4 4 0 0 0-6.8 3.6A11.3 11.3 0 0 1 3.9 4.8a4 4 0 0 0 1.2 5.3c-.6 0-1.2-.2-1.8-.5a4 4 0 0 0 3.2 3.9c-.6.1-1.2.2-1.8.1a4 4 0 0 0 3.7 2.8A8 8 0 0 1 2 18.1 11.3 11.3 0 0 0 8.1 20c7.4 0 11.5-6.2 11.5-11.5v-.5c.8-.6 1.5-1.3 2-2.1Z"/>',
442
+ aliases: ["x"]
443
+ },
444
+ gitlab: {
445
+ body: '<path d="m21.9 13.1-1.1-3.4-2.2-6.8a.6.6 0 0 0-1.1 0l-2.2 6.8H8.7L6.5 2.9a.6.6 0 0 0-1.1 0L3.2 9.7l-1.1 3.4a1.2 1.2 0 0 0 .4 1.3l9.5 6.9 9.5-6.9a1.2 1.2 0 0 0 .4-1.3Z"/>'
446
+ },
447
+ stackoverflow: {
448
+ body: '<path d="M17 21v-6h2v8H4v-8h2v6h11ZM7 17h9v-2H7v2Zm.3-4.2 8.8 1.8.4-2-8.8-1.8-.4 2Zm1.2-4.4 8.1 3.8.8-1.8-8.1-3.8-.8 1.8Zm2.5-4 6.9 5.7 1.3-1.5-6.9-5.7-1.3 1.5ZM15.6 1l-1.6 1.2 5.3 7.2 1.6-1.2L15.6 1Z"/>',
449
+ aliases: ["stack-overflow", "so"]
450
+ },
451
+ scholar: {
452
+ body: '<path d="M12 2 1 9l11 7 9-5.7V17h2V9L12 2ZM5 14.2V18c0 1.7 3.1 3 7 3s7-1.3 7-3v-3.8l-7 4.4-7-4.4Z"/>',
453
+ aliases: ["google-scholar", "academic"]
454
+ },
455
+ orcid: {
456
+ body: '<path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20ZM8.5 7.6a1.1 1.1 0 1 1 0 2.2 1.1 1.1 0 0 1 0-2.2ZM9.5 17h-2v-6.2h2V17Zm2-6.2h3.3c2.2 0 3.6 1.5 3.6 3.1 0 1.8-1.4 3.1-3.6 3.1H11.5v-6.2Zm2 1.7v2.8h1.1c1.2 0 1.8-.6 1.8-1.4s-.6-1.4-1.8-1.4h-1.1Z"/>'
457
+ },
458
+ calendar: {
459
+ body: '<path d="M7 2v2H5a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-2V2h-2v2H9V2H7ZM5 9h14v10H5V9Z"/>',
460
+ aliases: ["date"]
461
+ },
462
+ briefcase: {
463
+ body: '<path d="M9 3a2 2 0 0 0-2 2v1H4a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-3V5a2 2 0 0 0-2-2H9Zm0 3V5h6v1H9Z"/>',
464
+ aliases: ["work", "job"]
465
+ }
466
+ };
467
+ var ALIASES = {};
468
+ for (const [name, def] of Object.entries(ICONS)) {
469
+ for (const alias of def.aliases ?? []) ALIASES[alias] = name;
470
+ }
471
+ function resolveIconName(name) {
472
+ const key = name.trim().toLowerCase();
473
+ if (ICONS[key]) return key;
474
+ if (ALIASES[key]) return ALIASES[key];
475
+ return void 0;
476
+ }
477
+ function hasIcon(name) {
478
+ return resolveIconName(name) !== void 0;
479
+ }
480
+ function getIcon(name) {
481
+ const key = resolveIconName(name);
482
+ return key ? ICONS[key] : void 0;
483
+ }
484
+ function registerIcon(name, def) {
485
+ const key = name.trim().toLowerCase();
486
+ ICONS[key] = def;
487
+ for (const alias of def.aliases ?? []) ALIASES[alias] = key;
488
+ }
489
+ function iconNames() {
490
+ return Object.keys(ICONS);
491
+ }
492
+ function iconToSvg(name, opts = {}) {
493
+ const def = getIcon(name);
494
+ if (!def) return null;
495
+ const size = opts.size ?? "1em";
496
+ const dim = typeof size === "number" ? `${size}` : size;
497
+ const cls = opts.className ? ` class="${opts.className}"` : "";
498
+ const fillOrStroke = def.stroked ? 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"' : 'fill="currentColor"';
499
+ return `<svg${cls} width="${dim}" height="${dim}" viewBox="0 0 24 24" ${fillOrStroke} role="img" aria-hidden="true" focusable="false">${def.body}</svg>`;
500
+ }
501
+
502
+ // src/parser/builtins.ts
503
+ var mark = (name, type, summary) => ({
504
+ name,
505
+ category: "inline",
506
+ args: [{ kind: "content", name: "content" }],
507
+ summary,
508
+ example: `\\${name}{...}`,
509
+ build: ({ args }) => ({ type, children: args[0]?.children ?? [] })
510
+ });
511
+ var scaleSwitch = (name, scale) => ({
512
+ name,
513
+ category: "switch",
514
+ scoped: true,
515
+ summary: `Set the font size to "${name}" for the rest of the group.`,
516
+ example: `{\\${name} ...}`,
517
+ build: ({ scope }) => ({ type: "fontscale", scale, children: scope })
518
+ });
519
+ var TYPOGRAPHY = [
520
+ mark("textbf", "bold", "Bold text."),
521
+ mark("textit", "italic", "Italic text."),
522
+ mark("emph", "italic", "Emphasized (italic) text."),
523
+ mark("underline", "underline", "Underlined text."),
524
+ mark("sout", "strike", "Struck-through text."),
525
+ {
526
+ name: "textcolor",
527
+ category: "inline",
528
+ args: [
529
+ { kind: "string", name: "color", format: "color" },
530
+ { kind: "content", name: "content" }
531
+ ],
532
+ summary: "Color text with a hex/named color.",
533
+ example: "\\textcolor{#2563eb}{OpenAI}",
534
+ build: ({ args, utils, report }) => {
535
+ const color = utils.textOf(args[0]).trim();
536
+ if (color && !isSafeColor(color)) {
537
+ report({
538
+ severity: "warning" /* Warning */,
539
+ code: "RTX3001" /* InvalidColor */,
540
+ message: `"${color}" is not a recognized color; it will be ignored.`,
541
+ range: args[0]?.range ?? ZERO_RANGE
542
+ });
543
+ }
544
+ return {
545
+ type: "color",
546
+ color: isSafeColor(color) ? color : "",
547
+ children: args[1]?.children ?? []
548
+ };
549
+ }
550
+ },
551
+ {
552
+ name: "fontsize",
553
+ category: "inline",
554
+ args: [
555
+ { kind: "string", name: "size", format: "dimension" },
556
+ { kind: "content", name: "content" }
557
+ ],
558
+ summary: "Set an explicit font size for the wrapped content.",
559
+ example: "\\fontsize{14pt}{Custom Size}",
560
+ build: ({ args, utils, report }) => {
561
+ const size = utils.textOf(args[0]).trim();
562
+ if (size && !isSafeDimension(size)) {
563
+ report({
564
+ severity: "warning" /* Warning */,
565
+ code: "RTX3003" /* InvalidDimension */,
566
+ message: `"${size}" is not a valid dimension.`,
567
+ range: args[0]?.range ?? ZERO_RANGE
568
+ });
569
+ }
570
+ return {
571
+ type: "fontsize",
572
+ size: isSafeDimension(size) ? size : "1em",
573
+ children: args[1]?.children ?? []
574
+ };
575
+ }
576
+ },
577
+ {
578
+ name: "fontfamily",
579
+ category: "inline",
580
+ args: [
581
+ { kind: "string", name: "family" },
582
+ { kind: "content", name: "content" }
583
+ ],
584
+ summary: "Set the font family for the wrapped content.",
585
+ example: "\\fontfamily{Georgia}{Serif text}",
586
+ build: ({ args, utils }) => ({
587
+ type: "fontfamily",
588
+ family: utils.textOf(args[0]).trim() || "inherit",
589
+ children: args[1]?.children ?? []
590
+ })
591
+ },
592
+ {
593
+ name: "themecolor",
594
+ category: "inline",
595
+ args: [
596
+ { kind: "string", name: "token" },
597
+ { kind: "content", name: "content" }
598
+ ],
599
+ summary: "Color text using a token from the active theme.",
600
+ example: "\\themecolor{primary}{Highlighted}",
601
+ build: ({ args, utils }) => ({
602
+ type: "themecolor",
603
+ token: utils.textOf(args[0]).trim() || "primary",
604
+ children: args[1]?.children ?? []
605
+ })
606
+ },
607
+ scaleSwitch("small", "small"),
608
+ scaleSwitch("large", "large"),
609
+ scaleSwitch("Large", "Large"),
610
+ scaleSwitch("Huge", "Huge"),
611
+ {
612
+ name: "normalsize",
613
+ category: "switch",
614
+ scoped: true,
615
+ summary: "Reset the font size to the document base size.",
616
+ build: ({ scope }) => ({ type: "fontscale", scale: "normal", children: scope })
617
+ },
618
+ {
619
+ name: "bfseries",
620
+ category: "switch",
621
+ scoped: true,
622
+ summary: "Switch to bold for the rest of the group.",
623
+ build: ({ scope }) => ({ type: "bold", children: scope })
624
+ },
625
+ {
626
+ name: "itshape",
627
+ category: "switch",
628
+ scoped: true,
629
+ summary: "Switch to italic for the rest of the group.",
630
+ build: ({ scope }) => ({ type: "italic", children: scope })
631
+ },
632
+ {
633
+ name: "ttfamily",
634
+ category: "switch",
635
+ scoped: true,
636
+ summary: "Switch to a monospace font for the rest of the group.",
637
+ build: ({ scope }) => ({
638
+ type: "fontfamily",
639
+ family: "monospace",
640
+ children: scope
641
+ })
642
+ }
643
+ ];
644
+ var LINKS = [
645
+ {
646
+ name: "href",
647
+ category: "inline",
648
+ args: [
649
+ { kind: "string", name: "url", format: "url" },
650
+ { kind: "content", name: "label" }
651
+ ],
652
+ summary: "A hyperlink with custom label text.",
653
+ example: "\\href{https://github.com/you}{GitHub}",
654
+ build: ({ args, utils }) => {
655
+ const raw = utils.textOf(args[0]).trim();
656
+ return {
657
+ type: "link",
658
+ href: utils.safeUrl(raw),
659
+ rawHref: raw,
660
+ children: args[1]?.children ?? []
661
+ };
662
+ }
663
+ },
664
+ {
665
+ name: "url",
666
+ category: "inline",
667
+ args: [{ kind: "string", name: "url", format: "url" }],
668
+ summary: "A hyperlink that displays its own URL.",
669
+ example: "\\url{https://linkedin.com/in/you}",
670
+ build: ({ args, utils }) => {
671
+ const raw = utils.textOf(args[0]).trim();
672
+ return { type: "url", href: utils.safeUrl(raw), rawHref: raw };
673
+ }
674
+ }
675
+ ];
676
+ var SECTIONS = [
677
+ {
678
+ name: "section",
679
+ category: "block",
680
+ args: [{ kind: "string", name: "title" }],
681
+ summary: "A top-level resume section.",
682
+ example: "\\section{Experience}",
683
+ build: ({ args, utils }) => ({
684
+ type: "section",
685
+ title: utils.textOf(args[0]).trim(),
686
+ level: 1
687
+ })
688
+ },
689
+ {
690
+ name: "subsection",
691
+ category: "block",
692
+ args: [{ kind: "string", name: "title" }],
693
+ summary: "A second-level section heading.",
694
+ example: "\\subsection{Open Source}",
695
+ build: ({ args, utils }) => ({
696
+ type: "section",
697
+ title: utils.textOf(args[0]).trim(),
698
+ level: 2
699
+ })
700
+ }
701
+ ];
702
+ var CONTACT_FIELDS = [
703
+ { cmd: "name", field: "name", summary: "Your full name (document header)." },
704
+ { cmd: "title", field: "title", summary: "Professional headline / role." },
705
+ { cmd: "email", field: "email", summary: "Contact email (rendered as a mailto link)." },
706
+ { cmd: "phone", field: "phone", summary: "Contact phone number." },
707
+ { cmd: "location", field: "location", summary: "City / location." },
708
+ { cmd: "website", field: "website", summary: "Personal website (rendered as a link)." }
709
+ ];
710
+ var CONTACT = CONTACT_FIELDS.map(({ cmd, field, summary }) => ({
711
+ name: cmd,
712
+ category: "meta",
713
+ args: [{ kind: "string", name: cmd }],
714
+ summary,
715
+ example: `\\${cmd}{...}`,
716
+ build: ({ args, utils }) => ({ type: "contact", field, value: utils.textOf(args[0]).trim() })
717
+ }));
718
+ var zero = { offset: 0, line: 1, column: 1 };
719
+ var ZERO_RANGE = { start: zero, end: zero };
720
+ var requireFields = (report, fields, required, range2) => {
721
+ for (const key of required) {
722
+ if (!fields[key]) {
723
+ report({
724
+ severity: "warning" /* Warning */,
725
+ code: "RTX3004" /* MissingRequiredField */,
726
+ message: `Missing recommended field "${key}".`,
727
+ range: range2
728
+ });
729
+ }
730
+ }
731
+ };
732
+ var ENTRIES = [
733
+ {
734
+ name: "job",
735
+ category: "block",
736
+ args: [
737
+ { kind: "keyval", name: "fields" },
738
+ { kind: "content", name: "body", optional: true }
739
+ ],
740
+ summary: "A work-experience entry.",
741
+ example: "\\job{title=Senior Engineer, company=OpenAI, start=2023, end=Present}{Built AI systems}",
742
+ build: ({ args, report }) => {
743
+ const fields = parseKeyValArg(args[0]?.raw ?? "");
744
+ const r = args[0]?.range ?? ZERO_RANGE;
745
+ requireFields(report, fields, ["title", "company"], r);
746
+ return { type: "job", fields, children: args[1]?.children ?? [] };
747
+ }
748
+ },
749
+ {
750
+ name: "education",
751
+ category: "block",
752
+ args: [
753
+ { kind: "keyval", name: "fields" },
754
+ { kind: "content", name: "body", optional: true }
755
+ ],
756
+ summary: "An education entry.",
757
+ example: "\\education{school=MIT, degree=BS Computer Science, start=2018, end=2022}",
758
+ build: ({ args, report }) => {
759
+ const fields = parseKeyValArg(args[0]?.raw ?? "");
760
+ const r = args[0]?.range ?? ZERO_RANGE;
761
+ requireFields(report, fields, ["school", "degree"], r);
762
+ return { type: "education", fields, children: args[1]?.children ?? [] };
763
+ }
764
+ },
765
+ {
766
+ name: "project",
767
+ category: "block",
768
+ args: [
769
+ { kind: "keyval", name: "fields" },
770
+ { kind: "content", name: "body", optional: true }
771
+ ],
772
+ summary: "A project entry.",
773
+ example: "\\project{name=ReTeX, url=https://github.com/you/retex}{A resume engine}",
774
+ build: ({ args, report }) => {
775
+ const fields = parseKeyValArg(args[0]?.raw ?? "");
776
+ const r = args[0]?.range ?? ZERO_RANGE;
777
+ requireFields(report, fields, ["name"], r);
778
+ return { type: "project", fields, children: args[1]?.children ?? [] };
779
+ }
780
+ },
781
+ {
782
+ name: "skills",
783
+ category: "block",
784
+ args: [{ kind: "list", name: "skills" }],
785
+ summary: "A comma-separated list of skills.",
786
+ example: "\\skills{JavaScript, TypeScript, React, Node.js, AWS}",
787
+ build: ({ args }) => ({ type: "skills", items: parseListArg(args[0]?.raw ?? "") })
788
+ }
789
+ ];
790
+ var ICONS2 = [
791
+ {
792
+ name: "icon",
793
+ category: "inline",
794
+ args: [{ kind: "string", name: "name" }],
795
+ summary: "Inline SVG icon (github, linkedin, email, phone, \u2026).",
796
+ example: "\\icon{github}",
797
+ build: ({ args, utils, report }) => {
798
+ const name = utils.textOf(args[0]).trim().toLowerCase();
799
+ if (name && !hasIcon(name)) {
800
+ report({
801
+ severity: "info" /* Info */,
802
+ code: "RTX4002" /* UnknownIcon */,
803
+ message: `Unknown icon "${name}".`,
804
+ range: args[0]?.range ?? ZERO_RANGE
805
+ });
806
+ }
807
+ return { type: "icon", name };
808
+ }
809
+ }
810
+ ];
811
+ var PRIMITIVES = [
812
+ {
813
+ name: "newline",
814
+ category: "inline",
815
+ summary: "An explicit line break.",
816
+ build: () => ({ type: "linebreak" })
817
+ },
818
+ {
819
+ name: "linebreak",
820
+ category: "inline",
821
+ summary: "An explicit line break.",
822
+ build: () => ({ type: "linebreak" })
823
+ },
824
+ {
825
+ name: "hrule",
826
+ category: "block",
827
+ summary: "A horizontal rule / divider.",
828
+ build: () => ({ type: "rule" })
829
+ },
830
+ {
831
+ name: "divider",
832
+ category: "block",
833
+ summary: "A horizontal rule / divider.",
834
+ build: () => ({ type: "rule" })
835
+ },
836
+ {
837
+ name: "vspace",
838
+ category: "block",
839
+ args: [{ kind: "string", name: "size", format: "dimension" }],
840
+ summary: "Vertical space.",
841
+ example: "\\vspace{1em}",
842
+ build: ({ args, utils }) => ({
843
+ type: "space",
844
+ axis: "vertical",
845
+ size: utils.textOf(args[0]).trim() || "1em"
846
+ })
847
+ },
848
+ {
849
+ name: "hspace",
850
+ category: "inline",
851
+ args: [{ kind: "string", name: "size", format: "dimension" }],
852
+ summary: "Horizontal space.",
853
+ example: "\\hspace{1em}",
854
+ build: ({ args, utils }) => ({
855
+ type: "space",
856
+ axis: "horizontal",
857
+ size: utils.textOf(args[0]).trim() || "1em"
858
+ })
859
+ },
860
+ // `\item` and `\column` are consumed by their environments; they are
861
+ // registered so they are recognized (and so the validator can flag misuse
862
+ // outside the right environment) but have no standalone builder.
863
+ {
864
+ name: "item",
865
+ category: "block",
866
+ summary: "A list item (inside itemize/enumerate).",
867
+ example: "\\item Built distributed systems"
868
+ },
869
+ {
870
+ name: "column",
871
+ category: "block",
872
+ args: [{ kind: "string", name: "width", format: "dimension" }],
873
+ summary: "A column (inside the columns environment).",
874
+ example: "\\column{40%}"
875
+ }
876
+ ];
877
+ var ENVIRONMENTS = [
878
+ {
879
+ name: "itemize",
880
+ itemCommand: "item",
881
+ allowedChildren: ["item"],
882
+ summary: "A bulleted list.",
883
+ example: "\\begin{itemize}\n \\item ...\n\\end{itemize}",
884
+ build: ({ entries }) => ({
885
+ type: "list",
886
+ kind: "itemize",
887
+ items: (entries ?? []).map(
888
+ (e) => ({ type: "item", children: e.content })
889
+ )
890
+ })
891
+ },
892
+ {
893
+ name: "enumerate",
894
+ itemCommand: "item",
895
+ allowedChildren: ["item"],
896
+ summary: "A numbered list.",
897
+ example: "\\begin{enumerate}\n \\item ...\n\\end{enumerate}",
898
+ build: ({ entries }) => ({
899
+ type: "list",
900
+ kind: "enumerate",
901
+ items: (entries ?? []).map(
902
+ (e) => ({ type: "item", children: e.content })
903
+ )
904
+ })
905
+ },
906
+ {
907
+ name: "columns",
908
+ itemCommand: "column",
909
+ allowedChildren: ["column"],
910
+ summary: "A multi-column layout.",
911
+ example: "\\begin{columns}\n \\column{40%} Left\n \\column{60%} Right\n\\end{columns}",
912
+ build: ({ entries, utils }) => ({
913
+ type: "columns",
914
+ columns: (entries ?? []).map((e) => {
915
+ const width = utils.textOf(e.marker.args[0]).trim() || "auto";
916
+ return { type: "column", width, children: e.content };
917
+ })
918
+ })
919
+ },
920
+ {
921
+ name: "center",
922
+ summary: "Center-align the contained block content.",
923
+ example: "\\begin{center} ... \\end{center}",
924
+ build: ({ body }) => ({
925
+ type: "command",
926
+ name: "center",
927
+ args: [{ kind: "required", children: body }]
928
+ })
929
+ }
930
+ ];
931
+ function createDefaultRegistry() {
932
+ const registry = new CommandRegistry();
933
+ for (const def of [
934
+ ...TYPOGRAPHY,
935
+ ...LINKS,
936
+ ...SECTIONS,
937
+ ...CONTACT,
938
+ ...ENTRIES,
939
+ ...ICONS2,
940
+ ...PRIMITIVES
941
+ ]) {
942
+ registry.registerCommand(def);
943
+ }
944
+ for (const env of ENVIRONMENTS) registry.registerEnvironment(env);
945
+ return registry;
946
+ }
947
+
948
+ // src/ast/walk.ts
949
+ function childrenOf(node) {
950
+ if (node.type === "list") return node.items;
951
+ if (node.type === "columns") return node.columns;
952
+ if (isParent(node)) return node.children;
953
+ return [];
954
+ }
955
+ function walk(root, opts) {
956
+ const visit = (node, parent) => {
957
+ const descend = opts.enter ? opts.enter(node, parent) : void 0;
958
+ if (descend !== false) {
959
+ for (const child of childrenOf(node)) visit(child, node);
960
+ }
961
+ opts.exit?.(node, parent);
962
+ };
963
+ visit(root, null);
964
+ }
965
+ function collect(root, pred) {
966
+ const out = [];
967
+ walk(root, {
968
+ enter(n) {
969
+ if (pred(n)) out.push(n);
970
+ }
971
+ });
972
+ return out;
973
+ }
974
+ function nodePathAt(root, offset) {
975
+ const path = [];
976
+ const visit = (node) => {
977
+ const r = node.range;
978
+ if (r && (offset < r.start.offset || offset > r.end.offset)) return false;
979
+ path.push(node);
980
+ for (const child of childrenOf(node)) {
981
+ if (visit(child)) break;
982
+ }
983
+ return true;
984
+ };
985
+ visit(root);
986
+ return path;
987
+ }
988
+
989
+ // src/ast/text.ts
990
+ function flattenText(input) {
991
+ const nodes = Array.isArray(input) ? input : [input];
992
+ let out = "";
993
+ for (const node of nodes) {
994
+ switch (node.type) {
995
+ case "text":
996
+ out += node.value;
997
+ break;
998
+ case "linebreak":
999
+ out += " ";
1000
+ break;
1001
+ case "parbreak":
1002
+ out += "\n\n";
1003
+ break;
1004
+ case "url":
1005
+ out += node.href;
1006
+ break;
1007
+ case "icon":
1008
+ out += "";
1009
+ break;
1010
+ default:
1011
+ out += flattenText(childrenOf(node));
1012
+ }
1013
+ }
1014
+ return out;
1015
+ }
1016
+ function normalizeWhitespace(s) {
1017
+ return s.replace(/\s+/g, " ").trim();
1018
+ }
1019
+
1020
+ // src/parser/suggest.ts
1021
+ function levenshtein(a, b) {
1022
+ const m = a.length;
1023
+ const n = b.length;
1024
+ if (m === 0) return n;
1025
+ if (n === 0) return m;
1026
+ let prev = new Array(n + 1);
1027
+ let curr = new Array(n + 1);
1028
+ for (let j = 0; j <= n; j++) prev[j] = j;
1029
+ for (let i = 1; i <= m; i++) {
1030
+ curr[0] = i;
1031
+ for (let j = 1; j <= n; j++) {
1032
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1033
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
1034
+ }
1035
+ [prev, curr] = [curr, prev];
1036
+ }
1037
+ return prev[n];
1038
+ }
1039
+ function closestMatch(name, candidates) {
1040
+ let best;
1041
+ let bestDist = Infinity;
1042
+ const threshold = Math.max(2, Math.floor(name.length / 3) + 1);
1043
+ for (const c of candidates) {
1044
+ const d = levenshtein(name.toLowerCase(), c.toLowerCase());
1045
+ if (d < bestDist) {
1046
+ bestDist = d;
1047
+ best = c;
1048
+ }
1049
+ }
1050
+ return best !== void 0 && bestDist <= threshold ? best : void 0;
1051
+ }
1052
+
1053
+ // src/parser/parser.ts
1054
+ var ZERO = { offset: 0, line: 1, column: 1 };
1055
+ var ZERO_RANGE2 = { start: ZERO, end: ZERO };
1056
+ var Parser = class _Parser {
1057
+ constructor(tokens, options = {}) {
1058
+ this.diagnostics = [];
1059
+ this.pos = 0;
1060
+ this.depth = 0;
1061
+ this.reportedMaxDepth = false;
1062
+ /** Range of the command currently being built, for builder diagnostics. */
1063
+ this.currentRange = ZERO_RANGE2;
1064
+ this.tokens = tokens;
1065
+ this.registry = options.registry ?? createDefaultRegistry();
1066
+ this.maxDepth = options.maxDepth ?? 300;
1067
+ this.utils = {
1068
+ textOf: (arg) => {
1069
+ if (!arg) return "";
1070
+ if (arg.raw !== void 0) return arg.raw;
1071
+ return flattenText(arg.children);
1072
+ },
1073
+ safeUrl: (url) => {
1074
+ const res = sanitizeUrl(url);
1075
+ if (res.blocked) {
1076
+ this.report({
1077
+ severity: "warning" /* Warning */,
1078
+ code: "RTX5001" /* UnsafeUrlBlocked */,
1079
+ message: `Blocked unsafe URL with "${res.scheme ?? "unknown"}" scheme.`,
1080
+ range: this.currentRange
1081
+ });
1082
+ }
1083
+ return res.safe;
1084
+ }
1085
+ };
1086
+ }
1087
+ static parse(tokens, options) {
1088
+ return new _Parser(tokens, options).parse();
1089
+ }
1090
+ parse() {
1091
+ const start = this.peek().range.start;
1092
+ const children = this.parseContent(() => this.at("EOF" /* EOF */));
1093
+ const ast = {
1094
+ type: "document",
1095
+ children,
1096
+ range: { start, end: this.previousEnd() }
1097
+ };
1098
+ return { ast, diagnostics: this.diagnostics };
1099
+ }
1100
+ /* --------------------------- content loop --------------------------- */
1101
+ parseContent(stop) {
1102
+ if (++this.depth > this.maxDepth) {
1103
+ this.depth--;
1104
+ return this.skipFlat(stop);
1105
+ }
1106
+ try {
1107
+ return this.parseContentInner(stop);
1108
+ } finally {
1109
+ this.depth--;
1110
+ }
1111
+ }
1112
+ parseContentInner(stop) {
1113
+ const nodes = [];
1114
+ while (!this.at("EOF" /* EOF */) && !stop()) {
1115
+ const tok = this.peek();
1116
+ if (tok.type === "Command" /* Command */) {
1117
+ const def = this.registry.getCommand(tok.value);
1118
+ if (def?.scoped) {
1119
+ const built = this.parseScopedSwitch(def, stop);
1120
+ if (Array.isArray(built)) nodes.push(...built);
1121
+ else nodes.push(built);
1122
+ break;
1123
+ }
1124
+ }
1125
+ const node = this.parseNode();
1126
+ if (Array.isArray(node)) nodes.push(...node);
1127
+ else if (node) nodes.push(node);
1128
+ }
1129
+ return nodes;
1130
+ }
1131
+ parseNode() {
1132
+ const tok = this.peek();
1133
+ switch (tok.type) {
1134
+ case "Command" /* Command */:
1135
+ return this.parseCommandApplication();
1136
+ case "LBrace" /* LBrace */:
1137
+ return this.parseGroup();
1138
+ case "RBrace" /* RBrace */:
1139
+ this.advance();
1140
+ this.report({
1141
+ severity: "warning" /* Warning */,
1142
+ code: "RTX1005" /* MismatchedBrace */,
1143
+ message: "Unexpected '}' with no matching '{'.",
1144
+ range: tok.range
1145
+ });
1146
+ return null;
1147
+ case "Text" /* Text */:
1148
+ this.advance();
1149
+ return { type: "text", value: tok.value, range: tok.range };
1150
+ case "Whitespace" /* Whitespace */:
1151
+ this.advance();
1152
+ return { type: "text", value: " ", range: tok.range };
1153
+ case "ParBreak" /* ParBreak */:
1154
+ this.advance();
1155
+ return { type: "parbreak", range: tok.range };
1156
+ case "LineBreak" /* LineBreak */:
1157
+ this.advance();
1158
+ return { type: "linebreak", range: tok.range };
1159
+ case "LBracket" /* LBracket */:
1160
+ this.advance();
1161
+ return { type: "text", value: "[", range: tok.range };
1162
+ case "RBracket" /* RBracket */:
1163
+ this.advance();
1164
+ return { type: "text", value: "]", range: tok.range };
1165
+ case "Comment" /* Comment */:
1166
+ default:
1167
+ this.advance();
1168
+ return null;
1169
+ }
1170
+ }
1171
+ parseGroup() {
1172
+ const open = this.advance();
1173
+ const children = this.parseContent(() => this.at("RBrace" /* RBrace */));
1174
+ const end = this.consumeClose(
1175
+ "RBrace" /* RBrace */,
1176
+ open,
1177
+ "RTX1002" /* UnterminatedGroup */
1178
+ );
1179
+ return { type: "group", children, range: { start: open.range.start, end } };
1180
+ }
1181
+ /* --------------------------- commands ------------------------------- */
1182
+ parseScopedSwitch(def, stop) {
1183
+ const start = this.advance();
1184
+ const args = this.parseArgs(def.args ?? []);
1185
+ const scope = this.parseContent(stop);
1186
+ return this.buildCommand(def, args, scope, start);
1187
+ }
1188
+ parseCommandApplication() {
1189
+ const start = this.advance();
1190
+ const name = start.value;
1191
+ if (name === "begin") return this.parseEnvironment(start);
1192
+ if (name === "end") {
1193
+ this.skipInlineTrivia();
1194
+ const got = this.tryReadGroupRaw();
1195
+ this.report({
1196
+ severity: "error" /* Error */,
1197
+ code: "RTX2006" /* MismatchedEnvironment */,
1198
+ message: got ? `\\end{${got}} has no matching \\begin{${got}}.` : "\\end has no matching \\begin.",
1199
+ range: { start: start.range.start, end: this.previousEnd() }
1200
+ });
1201
+ return null;
1202
+ }
1203
+ const def = this.registry.getCommand(name);
1204
+ if (!def) {
1205
+ const args2 = this.parseGreedyArgs();
1206
+ const range2 = { start: start.range.start, end: this.previousEnd() };
1207
+ const suggestion = closestMatch(name, this.registry.commandNames());
1208
+ this.report({
1209
+ severity: "warning" /* Warning */,
1210
+ code: "RTX2001" /* UnknownCommand */,
1211
+ message: suggestion ? `Unknown command "\\${name}". Did you mean "\\${suggestion}"?` : `Unknown command "\\${name}".`,
1212
+ range: range2,
1213
+ ...suggestion ? {
1214
+ fixes: [
1215
+ {
1216
+ title: `Replace with \\${suggestion}`,
1217
+ replacement: `\\${suggestion}`,
1218
+ range: start.range
1219
+ }
1220
+ ]
1221
+ } : {}
1222
+ });
1223
+ return { type: "command", name, args: args2, range: range2 };
1224
+ }
1225
+ const args = this.parseArgs(def.args ?? []);
1226
+ return this.buildCommand(def, args, [], start);
1227
+ }
1228
+ buildCommand(def, args, scope, start) {
1229
+ const range2 = { start: start.range.start, end: this.previousEnd() };
1230
+ this.currentRange = range2;
1231
+ const built = def.build ? def.build({
1232
+ name: def.name,
1233
+ args,
1234
+ scope,
1235
+ report: (d) => this.report(d),
1236
+ utils: this.utils
1237
+ }) : null;
1238
+ if (built == null) {
1239
+ return { type: "command", name: def.name, args, range: range2 };
1240
+ }
1241
+ if (Array.isArray(built)) {
1242
+ for (const n of built) if (!n.range) n.range = range2;
1243
+ return built;
1244
+ }
1245
+ if (!built.range) built.range = range2;
1246
+ return built;
1247
+ }
1248
+ /* --------------------------- arguments ------------------------------ */
1249
+ parseArgs(specs) {
1250
+ const args = [];
1251
+ for (const spec of specs) {
1252
+ const arg = this.parseArg(spec);
1253
+ if (arg) {
1254
+ args.push(arg);
1255
+ } else if (!spec.optional) {
1256
+ this.report({
1257
+ severity: "error" /* Error */,
1258
+ code: "RTX2003" /* MissingRequiredArgument */,
1259
+ message: `Missing required argument${spec.name ? ` "${spec.name}"` : ""}.`,
1260
+ range: { start: this.previousEnd(), end: this.previousEnd() }
1261
+ });
1262
+ args.push({ kind: "required", children: [], raw: "", range: ZERO_RANGE2 });
1263
+ }
1264
+ }
1265
+ return args;
1266
+ }
1267
+ parseArg(spec) {
1268
+ this.skipInlineTrivia();
1269
+ const bracket = spec.delimiter === "bracket";
1270
+ const open = bracket ? "LBracket" /* LBracket */ : "LBrace" /* LBrace */;
1271
+ const close = bracket ? "RBracket" /* RBracket */ : "RBrace" /* RBrace */;
1272
+ if (!this.at(open)) return null;
1273
+ const openTok = this.advance();
1274
+ const kind = spec.optional ? "optional" : "required";
1275
+ if (spec.kind === "content") {
1276
+ const children = this.parseContent(() => this.at(close));
1277
+ const end2 = this.consumeClose(close, openTok, "RTX1003" /* UnterminatedArgument */);
1278
+ return { kind, children, range: { start: openTok.range.start, end: end2 } };
1279
+ }
1280
+ const raw = this.readRawUntil(close);
1281
+ const end = this.consumeClose(close, openTok, "RTX1003" /* UnterminatedArgument */);
1282
+ return { kind, children: [], raw, range: { start: openTok.range.start, end } };
1283
+ }
1284
+ parseGreedyArgs() {
1285
+ const args = [];
1286
+ for (; ; ) {
1287
+ this.skipInlineTrivia();
1288
+ if (!this.at("LBrace" /* LBrace */)) break;
1289
+ const openTok = this.advance();
1290
+ const children = this.parseContent(() => this.at("RBrace" /* RBrace */));
1291
+ const end = this.consumeClose(
1292
+ "RBrace" /* RBrace */,
1293
+ openTok,
1294
+ "RTX1003" /* UnterminatedArgument */
1295
+ );
1296
+ args.push({
1297
+ kind: "required",
1298
+ children,
1299
+ range: { start: openTok.range.start, end }
1300
+ });
1301
+ }
1302
+ return args;
1303
+ }
1304
+ /** Concatenate the raw source of tokens until the matching close at depth 0. */
1305
+ readRawUntil(close) {
1306
+ let raw = "";
1307
+ let depth = 0;
1308
+ while (!this.at("EOF" /* EOF */)) {
1309
+ const t = this.peek();
1310
+ if (depth === 0 && t.type === close) break;
1311
+ if (t.type === "LBrace" /* LBrace */ || t.type === "LBracket" /* LBracket */) {
1312
+ depth++;
1313
+ } else if (t.type === "RBrace" /* RBrace */ || t.type === "RBracket" /* RBracket */) {
1314
+ if (depth === 0) break;
1315
+ depth--;
1316
+ }
1317
+ raw += t.type === "Command" /* Command */ ? `\\${t.value}` : t.value;
1318
+ this.advance();
1319
+ }
1320
+ return raw;
1321
+ }
1322
+ /* -------------------------- environments ---------------------------- */
1323
+ parseEnvironment(beginTok) {
1324
+ const name = this.readGroupName();
1325
+ const options = [];
1326
+ let opt;
1327
+ while (opt = this.parseArg({ kind: "string", optional: true, delimiter: "bracket" })) {
1328
+ options.push(opt);
1329
+ }
1330
+ const def = this.registry.getEnvironment(name);
1331
+ if (!def) {
1332
+ const body2 = this.parseEnvBody(name);
1333
+ const range3 = { start: beginTok.range.start, end: this.previousEnd() };
1334
+ const suggestion = closestMatch(name, this.registry.environmentNames());
1335
+ this.report({
1336
+ severity: "warning" /* Warning */,
1337
+ code: "RTX2002" /* UnknownEnvironment */,
1338
+ message: suggestion ? `Unknown environment "${name}". Did you mean "${suggestion}"?` : `Unknown environment "${name}".`,
1339
+ range: range3
1340
+ });
1341
+ return { type: "group", children: body2, range: range3 };
1342
+ }
1343
+ let body;
1344
+ let entries;
1345
+ if (def.itemCommand) {
1346
+ entries = this.parseEnvEntries(name, def.itemCommand);
1347
+ body = entries.flatMap((e) => e.content);
1348
+ } else {
1349
+ body = this.parseEnvBody(name);
1350
+ }
1351
+ const range2 = { start: beginTok.range.start, end: this.previousEnd() };
1352
+ this.currentRange = range2;
1353
+ const node = def.build ? def.build({
1354
+ name,
1355
+ body,
1356
+ entries,
1357
+ options,
1358
+ report: (d) => this.report(d),
1359
+ utils: this.utils
1360
+ }) : null;
1361
+ if (node == null) {
1362
+ return { type: "group", children: body, range: range2 };
1363
+ }
1364
+ if (Array.isArray(node)) {
1365
+ for (const n of node) if (!n.range) n.range = range2;
1366
+ return node;
1367
+ }
1368
+ if (!node.range) node.range = range2;
1369
+ return node;
1370
+ }
1371
+ parseEnvBody(name) {
1372
+ const body = this.parseContent(() => this.atCommand("end"));
1373
+ this.consumeEnd(name);
1374
+ return body;
1375
+ }
1376
+ parseEnvEntries(name, itemCommand) {
1377
+ const entries = [];
1378
+ const stop = () => this.atCommand(itemCommand) || this.atCommand("end");
1379
+ this.skipTrivia();
1380
+ if (!stop() && !this.at("EOF" /* EOF */)) {
1381
+ const stray = this.parseContent(stop);
1382
+ if (stray.some((n) => n.type !== "text" || n.value.trim() !== "")) {
1383
+ this.report({
1384
+ severity: "warning" /* Warning */,
1385
+ code: "RTX4001" /* CommandOutsideContext */,
1386
+ message: `Content before the first \\${itemCommand} in "${name}" was ignored.`,
1387
+ range: stray[0]?.range ?? this.currentRange
1388
+ });
1389
+ }
1390
+ }
1391
+ while (this.atCommand(itemCommand)) {
1392
+ const markerTok = this.advance();
1393
+ const def = this.registry.getCommand(itemCommand);
1394
+ const margs = this.parseArgs(def?.args ?? []);
1395
+ const marker = {
1396
+ type: "command",
1397
+ name: itemCommand,
1398
+ args: margs,
1399
+ range: { start: markerTok.range.start, end: this.previousEnd() }
1400
+ };
1401
+ const content = this.parseContent(stop);
1402
+ entries.push({ marker, content });
1403
+ }
1404
+ this.consumeEnd(name);
1405
+ return entries;
1406
+ }
1407
+ consumeEnd(name) {
1408
+ if (!this.atCommand("end")) {
1409
+ this.report({
1410
+ severity: "error" /* Error */,
1411
+ code: "RTX2005" /* MissingEnvironmentEnd */,
1412
+ message: `Missing \\end{${name}}.`,
1413
+ range: { start: this.previousEnd(), end: this.previousEnd() }
1414
+ });
1415
+ return;
1416
+ }
1417
+ const endTok = this.advance();
1418
+ const got = this.readGroupName();
1419
+ if (got !== name) {
1420
+ this.report({
1421
+ severity: "error" /* Error */,
1422
+ code: "RTX2006" /* MismatchedEnvironment */,
1423
+ message: `Expected \\end{${name}} but found \\end{${got}}.`,
1424
+ range: { start: endTok.range.start, end: this.previousEnd() }
1425
+ });
1426
+ }
1427
+ }
1428
+ /** Read a `{name}` group's raw contents, reporting if it is missing. */
1429
+ readGroupName() {
1430
+ this.skipInlineTrivia();
1431
+ if (!this.at("LBrace" /* LBrace */)) {
1432
+ this.report({
1433
+ severity: "error" /* Error */,
1434
+ code: "RTX2003" /* MissingRequiredArgument */,
1435
+ message: "Expected '{name}'.",
1436
+ range: { start: this.previousEnd(), end: this.previousEnd() }
1437
+ });
1438
+ return "";
1439
+ }
1440
+ const open = this.advance();
1441
+ const raw = this.readRawUntil("RBrace" /* RBrace */);
1442
+ this.consumeClose("RBrace" /* RBrace */, open, "RTX1003" /* UnterminatedArgument */);
1443
+ return raw.trim();
1444
+ }
1445
+ /** Like {@link readGroupName} but returns "" silently when absent. */
1446
+ tryReadGroupRaw() {
1447
+ this.skipInlineTrivia();
1448
+ if (!this.at("LBrace" /* LBrace */)) return "";
1449
+ const open = this.advance();
1450
+ const raw = this.readRawUntil("RBrace" /* RBrace */);
1451
+ this.consumeClose("RBrace" /* RBrace */, open, "RTX1003" /* UnterminatedArgument */);
1452
+ return raw.trim();
1453
+ }
1454
+ /**
1455
+ * Recursion-limit fallback: consume the current scope's tokens flatly
1456
+ * (tracking nested braces) without descending, so adversarial deep nesting
1457
+ * cannot overflow the stack. Emits one diagnostic per parse.
1458
+ */
1459
+ skipFlat(stop) {
1460
+ if (!this.reportedMaxDepth) {
1461
+ this.reportedMaxDepth = true;
1462
+ this.report({
1463
+ severity: "warning" /* Warning */,
1464
+ code: "RTX1001" /* UnexpectedToken */,
1465
+ message: `Maximum nesting depth (${this.maxDepth}) exceeded; deeply nested content was skipped.`,
1466
+ range: this.peek().range
1467
+ });
1468
+ }
1469
+ let bdepth = 0;
1470
+ while (!this.at("EOF" /* EOF */)) {
1471
+ if (bdepth === 0 && stop()) break;
1472
+ const t = this.peek();
1473
+ if (t.type === "LBrace" /* LBrace */ || t.type === "LBracket" /* LBracket */) bdepth++;
1474
+ else if (t.type === "RBrace" /* RBrace */ || t.type === "RBracket" /* RBracket */) {
1475
+ if (bdepth === 0) break;
1476
+ bdepth--;
1477
+ }
1478
+ this.advance();
1479
+ }
1480
+ return [];
1481
+ }
1482
+ /* ----------------------------- cursor ------------------------------- */
1483
+ consumeClose(close, open, code) {
1484
+ if (this.at(close)) {
1485
+ const end = this.peek().range.end;
1486
+ this.advance();
1487
+ return end;
1488
+ }
1489
+ this.report({
1490
+ severity: "error" /* Error */,
1491
+ code,
1492
+ message: `Unterminated "${open.value}" \u2014 expected a matching close.`,
1493
+ range: open.range
1494
+ });
1495
+ return this.previousEnd();
1496
+ }
1497
+ at(type) {
1498
+ return this.peek().type === type;
1499
+ }
1500
+ atCommand(name) {
1501
+ const t = this.peek();
1502
+ return t.type === "Command" /* Command */ && t.value === name;
1503
+ }
1504
+ skipInlineTrivia() {
1505
+ while (this.at("Whitespace" /* Whitespace */) || this.at("Comment" /* Comment */)) {
1506
+ this.advance();
1507
+ }
1508
+ }
1509
+ skipTrivia() {
1510
+ while (this.at("Whitespace" /* Whitespace */) || this.at("Comment" /* Comment */) || this.at("ParBreak" /* ParBreak */)) {
1511
+ this.advance();
1512
+ }
1513
+ }
1514
+ peek(k = 0) {
1515
+ return this.tokens[this.pos + k] ?? this.tokens[this.tokens.length - 1];
1516
+ }
1517
+ advance() {
1518
+ const t = this.peek();
1519
+ if (this.pos < this.tokens.length - 1) this.pos++;
1520
+ return t;
1521
+ }
1522
+ previousEnd() {
1523
+ return this.tokens[this.pos - 1]?.range.end ?? this.peek().range.start;
1524
+ }
1525
+ report(d) {
1526
+ this.diagnostics.push({ ...d, source: "parser" });
1527
+ }
1528
+ };
1529
+ function parse(tokens, options) {
1530
+ return Parser.parse(tokens, options);
1531
+ }
1532
+
1533
+ // src/validator/validator.ts
1534
+ var ZERO_RANGE3 = {
1535
+ start: { offset: 0, line: 1, column: 1 },
1536
+ end: { offset: 0, line: 1, column: 1 }
1537
+ };
1538
+ function validate(ast, options = {}) {
1539
+ const diagnostics = [];
1540
+ const report = (d) => {
1541
+ diagnostics.push({ ...d, source: "validator" });
1542
+ };
1543
+ walk(ast, {
1544
+ enter(node) {
1545
+ switch (node.type) {
1546
+ case "command":
1547
+ checkStrayCommand(node, report);
1548
+ break;
1549
+ case "section":
1550
+ if (node.title.trim() === "") {
1551
+ report({
1552
+ severity: "warning" /* Warning */,
1553
+ code: "RTX3006" /* EmptyArgument */,
1554
+ message: "Section heading is empty.",
1555
+ range: node.range ?? ZERO_RANGE3
1556
+ });
1557
+ }
1558
+ break;
1559
+ case "contact":
1560
+ if (node.value.trim() === "") {
1561
+ report({
1562
+ severity: "warning" /* Warning */,
1563
+ code: "RTX3006" /* EmptyArgument */,
1564
+ message: `\\${node.field} is empty.`,
1565
+ range: node.range ?? ZERO_RANGE3
1566
+ });
1567
+ }
1568
+ break;
1569
+ case "themecolor":
1570
+ if (options.theme && !(node.token in options.theme.colors)) {
1571
+ report({
1572
+ severity: "warning" /* Warning */,
1573
+ code: "RTX4003" /* UnknownThemeColor */,
1574
+ message: `Theme color "${node.token}" is not defined in theme "${options.theme.name}".`,
1575
+ range: node.range ?? ZERO_RANGE3
1576
+ });
1577
+ }
1578
+ break;
1579
+ case "skills":
1580
+ if (node.items.length === 0) {
1581
+ report({
1582
+ severity: "info" /* Info */,
1583
+ code: "RTX3006" /* EmptyArgument */,
1584
+ message: "\\skills has no entries.",
1585
+ range: node.range ?? ZERO_RANGE3
1586
+ });
1587
+ }
1588
+ break;
1589
+ }
1590
+ }
1591
+ });
1592
+ return diagnostics;
1593
+ }
1594
+ function checkStrayCommand(node, report) {
1595
+ if (node.name === "item") {
1596
+ report({
1597
+ severity: "error" /* Error */,
1598
+ code: "RTX4001" /* CommandOutsideContext */,
1599
+ message: "\\item must appear inside an itemize or enumerate environment.",
1600
+ range: node.range ?? ZERO_RANGE3
1601
+ });
1602
+ } else if (node.name === "column") {
1603
+ report({
1604
+ severity: "error" /* Error */,
1605
+ code: "RTX4001" /* CommandOutsideContext */,
1606
+ message: "\\column must appear inside a columns environment.",
1607
+ range: node.range ?? ZERO_RANGE3
1608
+ });
1609
+ }
1610
+ }
1611
+
1612
+ // src/theme/default.ts
1613
+ var defaultTheme = {
1614
+ name: "default",
1615
+ colors: {
1616
+ primary: "#2563eb",
1617
+ secondary: "#64748b",
1618
+ text: "#1e293b",
1619
+ muted: "#64748b",
1620
+ background: "#ffffff",
1621
+ border: "#e2e8f0",
1622
+ accent: "#2563eb",
1623
+ success: "#16a34a"
1624
+ },
1625
+ fonts: {
1626
+ heading: '"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif',
1627
+ body: '"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif',
1628
+ mono: '"JetBrains Mono", "SF Mono", "Fira Code", Consolas, monospace'
1629
+ },
1630
+ fontSizes: {
1631
+ base: "10.5pt",
1632
+ small: "9pt",
1633
+ large: "12pt",
1634
+ Large: "15pt",
1635
+ Huge: "22pt",
1636
+ name: "26pt",
1637
+ section: "13pt"
1638
+ },
1639
+ spacing: {
1640
+ unit: "4px",
1641
+ section: "1.1rem",
1642
+ item: "0.28rem",
1643
+ page: "0.55in"
1644
+ },
1645
+ page: {
1646
+ size: "Letter",
1647
+ margin: "0.55in",
1648
+ maxWidth: "8.5in"
1649
+ },
1650
+ sectionStyle: "rule"
1651
+ };
1652
+
1653
+ // src/theme/themes.ts
1654
+ function resolveTheme(partial, base = defaultTheme) {
1655
+ if (!partial) return base;
1656
+ return {
1657
+ name: partial.name ?? base.name,
1658
+ colors: { ...base.colors, ...partial.colors },
1659
+ fonts: { ...base.fonts, ...partial.fonts },
1660
+ fontSizes: { ...base.fontSizes, ...partial.fontSizes },
1661
+ spacing: { ...base.spacing, ...partial.spacing },
1662
+ page: { ...base.page, ...partial.page },
1663
+ sectionStyle: partial.sectionStyle ?? base.sectionStyle
1664
+ };
1665
+ }
1666
+ var modernTheme = resolveTheme({
1667
+ name: "modern",
1668
+ colors: { primary: "#7c3aed", accent: "#7c3aed", text: "#111827" },
1669
+ sectionStyle: "underline"
1670
+ });
1671
+ var classicTheme = resolveTheme({
1672
+ name: "classic",
1673
+ colors: { primary: "#111827", secondary: "#4b5563", accent: "#111827" },
1674
+ fonts: {
1675
+ heading: 'Georgia, "Times New Roman", serif',
1676
+ body: 'Georgia, "Times New Roman", serif',
1677
+ mono: 'Consolas, "Courier New", monospace'
1678
+ },
1679
+ sectionStyle: "rule"
1680
+ });
1681
+ var compactTheme = resolveTheme({
1682
+ name: "compact",
1683
+ fontSizes: {
1684
+ base: "9.5pt",
1685
+ small: "8pt",
1686
+ large: "11pt",
1687
+ Large: "13pt",
1688
+ Huge: "18pt",
1689
+ name: "20pt",
1690
+ section: "11pt"
1691
+ },
1692
+ spacing: { unit: "3px", section: "0.7rem", item: "0.18rem", page: "0.4in" },
1693
+ sectionStyle: "bar"
1694
+ });
1695
+ var themes = {
1696
+ default: defaultTheme,
1697
+ modern: modernTheme,
1698
+ classic: classicTheme,
1699
+ compact: compactTheme
1700
+ };
1701
+ function getTheme(name) {
1702
+ return themes[name] ?? defaultTheme;
1703
+ }
1704
+
1705
+ // src/renderers/css.ts
1706
+ var tokenSafe = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "");
1707
+ function themeToCss(theme, prefix = "retex") {
1708
+ const p = prefix;
1709
+ const colorVars = Object.entries(theme.colors).map(([k, v]) => ` --${p}-color-${tokenSafe(k)}: ${v};`).join("\n");
1710
+ return `.${p}-resume {
1711
+ ${colorVars}
1712
+ --${p}-primary: ${theme.colors.primary};
1713
+ --${p}-text: ${theme.colors.text};
1714
+ --${p}-muted: ${theme.colors.muted};
1715
+ --${p}-border: ${theme.colors.border};
1716
+ --${p}-bg: ${theme.colors.background};
1717
+ --${p}-font-heading: ${theme.fonts.heading};
1718
+ --${p}-font-body: ${theme.fonts.body};
1719
+ --${p}-font-mono: ${theme.fonts.mono};
1720
+ --${p}-fs-base: ${theme.fontSizes.base};
1721
+ --${p}-fs-small: ${theme.fontSizes.small};
1722
+ --${p}-fs-large: ${theme.fontSizes.large};
1723
+ --${p}-fs-Large: ${theme.fontSizes.Large};
1724
+ --${p}-fs-Huge: ${theme.fontSizes.Huge};
1725
+ --${p}-fs-name: ${theme.fontSizes.name};
1726
+ --${p}-fs-section: ${theme.fontSizes.section};
1727
+ --${p}-sp-section: ${theme.spacing.section};
1728
+ --${p}-sp-item: ${theme.spacing.item};
1729
+
1730
+ box-sizing: border-box;
1731
+ max-width: ${theme.page.maxWidth};
1732
+ margin: 0 auto;
1733
+ padding: ${theme.spacing.page};
1734
+ background: var(--${p}-bg);
1735
+ color: var(--${p}-text);
1736
+ font-family: var(--${p}-font-body);
1737
+ font-size: var(--${p}-fs-base);
1738
+ line-height: 1.45;
1739
+ }
1740
+ .${p}-resume *, .${p}-resume *::before, .${p}-resume *::after { box-sizing: border-box; }
1741
+
1742
+ .${p}-header { margin-bottom: var(--${p}-sp-section); }
1743
+ .${p}-name {
1744
+ font-family: var(--${p}-font-heading);
1745
+ font-size: var(--${p}-fs-name);
1746
+ font-weight: 700;
1747
+ margin: 0 0 0.1em;
1748
+ color: var(--${p}-text);
1749
+ letter-spacing: -0.01em;
1750
+ }
1751
+ .${p}-title {
1752
+ font-size: var(--${p}-fs-large);
1753
+ color: var(--${p}-primary);
1754
+ margin: 0 0 0.45em;
1755
+ font-weight: 500;
1756
+ }
1757
+ .${p}-contact {
1758
+ display: flex;
1759
+ flex-wrap: wrap;
1760
+ gap: 0.35rem 1rem;
1761
+ font-size: var(--${p}-fs-small);
1762
+ color: var(--${p}-muted);
1763
+ }
1764
+ .${p}-contact-item {
1765
+ display: inline-flex;
1766
+ align-items: center;
1767
+ gap: 0.3em;
1768
+ color: inherit;
1769
+ text-decoration: none;
1770
+ }
1771
+ .${p}-contact-item svg { opacity: 0.8; }
1772
+
1773
+ .${p}-section { margin-bottom: var(--${p}-sp-section); break-inside: avoid; }
1774
+ .${p}-section-title {
1775
+ font-family: var(--${p}-font-heading);
1776
+ font-size: var(--${p}-fs-section);
1777
+ font-weight: 700;
1778
+ text-transform: uppercase;
1779
+ letter-spacing: 0.06em;
1780
+ color: var(--${p}-primary);
1781
+ margin: 0 0 0.5em;
1782
+ }
1783
+ .${p}-section--rule .${p}-section-title { border-bottom: 2px solid var(--${p}-border); padding-bottom: 0.2em; }
1784
+ .${p}-section--underline .${p}-section-title { text-decoration: underline; text-underline-offset: 3px; }
1785
+ .${p}-section--bar .${p}-section-title {
1786
+ border-left: 3px solid var(--${p}-primary);
1787
+ padding-left: 0.5em;
1788
+ border-bottom: none;
1789
+ }
1790
+ .${p}-subsection-title {
1791
+ font-family: var(--${p}-font-heading);
1792
+ font-size: var(--${p}-fs-large);
1793
+ font-weight: 600;
1794
+ margin: 0.6em 0 0.3em;
1795
+ }
1796
+
1797
+ .${p}-entry { margin-bottom: 0.6rem; break-inside: avoid; }
1798
+ .${p}-entry-row {
1799
+ display: flex;
1800
+ justify-content: space-between;
1801
+ align-items: baseline;
1802
+ gap: 1rem;
1803
+ }
1804
+ .${p}-entry-title { font-weight: 700; }
1805
+ .${p}-entry-subtitle { color: var(--${p}-primary); font-weight: 500; }
1806
+ .${p}-entry-dates, .${p}-entry-location {
1807
+ color: var(--${p}-muted);
1808
+ font-size: var(--${p}-fs-small);
1809
+ white-space: nowrap;
1810
+ }
1811
+ .${p}-entry-body { margin-top: 0.15rem; }
1812
+ .${p}-entry-body p { margin: 0.2em 0; }
1813
+
1814
+ .${p}-list { margin: 0.2rem 0 0.2rem 1.1rem; padding: 0; }
1815
+ .${p}-list li { margin-bottom: var(--${p}-sp-item); padding-left: 0.15rem; }
1816
+
1817
+ .${p}-columns { display: flex; gap: 1.5rem; align-items: flex-start; }
1818
+ .${p}-column { min-width: 0; }
1819
+
1820
+ .${p}-skills { display: flex; flex-wrap: wrap; gap: 0.4rem; list-style: none; margin: 0.2rem 0; padding: 0; }
1821
+ .${p}-skill {
1822
+ background: color-mix(in srgb, var(--${p}-primary) 12%, transparent);
1823
+ color: var(--${p}-primary);
1824
+ border-radius: 4px;
1825
+ padding: 0.12rem 0.5rem;
1826
+ font-size: var(--${p}-fs-small);
1827
+ font-weight: 500;
1828
+ white-space: nowrap;
1829
+ }
1830
+
1831
+ .${p}-link { color: var(--${p}-primary); text-decoration: none; }
1832
+ .${p}-link:hover { text-decoration: underline; }
1833
+ .${p}-icon { display: inline-flex; vertical-align: -0.125em; }
1834
+ .${p}-rule { border: none; border-top: 1px solid var(--${p}-border); margin: 0.6rem 0; }
1835
+ .${p}-center { text-align: center; }
1836
+ .${p}-scale-small { font-size: var(--${p}-fs-small); }
1837
+ .${p}-scale-large { font-size: var(--${p}-fs-large); }
1838
+ .${p}-scale-Large { font-size: var(--${p}-fs-Large); }
1839
+ .${p}-scale-Huge { font-size: var(--${p}-fs-Huge); }
1840
+ p.${p}-para { margin: 0 0 0.45em; }
1841
+ p.${p}-para:last-child { margin-bottom: 0; }
1842
+
1843
+ @media print {
1844
+ .${p}-resume { max-width: none; margin: 0; padding: 0; }
1845
+ .${p}-section, .${p}-entry { break-inside: avoid; }
1846
+ a { color: inherit; }
1847
+ }`;
1848
+ }
1849
+
1850
+ // src/renderers/context.ts
1851
+ var BLOCK_TYPES = /* @__PURE__ */ new Set([
1852
+ "section",
1853
+ "job",
1854
+ "education",
1855
+ "project",
1856
+ "skills",
1857
+ "list",
1858
+ "columns",
1859
+ "rule"
1860
+ ]);
1861
+ function isBlockNode(node) {
1862
+ if (BLOCK_TYPES.has(node.type)) return true;
1863
+ if (node.type === "space") return node.axis === "vertical";
1864
+ if (node.type === "command" && node.name === "center") return true;
1865
+ return false;
1866
+ }
1867
+
1868
+ // src/renderers/structure.ts
1869
+ function toRegions(children) {
1870
+ const regions = [{ nodes: [] }];
1871
+ for (const node of children) {
1872
+ if (node.type === "section" && node.level === 1) {
1873
+ regions.push({ section: node, nodes: [] });
1874
+ } else {
1875
+ regions[regions.length - 1].nodes.push(node);
1876
+ }
1877
+ }
1878
+ return regions;
1879
+ }
1880
+ var CONTACTISH = /* @__PURE__ */ new Set(["contact", "icon", "link", "url"]);
1881
+ function splitPreamble(nodes) {
1882
+ const name = nodes.find(
1883
+ (n) => n.type === "contact" && n.field === "name"
1884
+ );
1885
+ const title = nodes.find(
1886
+ (n) => n.type === "contact" && n.field === "title"
1887
+ );
1888
+ const contacts = nodes.filter(
1889
+ (n) => CONTACTISH.has(n.type) && n !== name && n !== title
1890
+ );
1891
+ const other = nodes.filter(
1892
+ (n) => !CONTACTISH.has(n.type) && n.type !== "parbreak" && !(n.type === "text" && n.value.trim() === "")
1893
+ );
1894
+ return { name, title, contacts, other };
1895
+ }
1896
+ function entryParts(fields, kind) {
1897
+ let title;
1898
+ let subtitle;
1899
+ if (kind === "education") {
1900
+ title = fields.school ?? fields.degree ?? "";
1901
+ subtitle = fields.school ? fields.degree ?? "" : "";
1902
+ } else if (kind === "project") {
1903
+ title = fields.name ?? fields.title ?? "";
1904
+ subtitle = fields.organization ?? fields.tech ?? "";
1905
+ } else {
1906
+ title = fields.title ?? fields.role ?? fields.name ?? "";
1907
+ subtitle = fields.company ?? fields.organization ?? "";
1908
+ }
1909
+ return {
1910
+ title,
1911
+ subtitle,
1912
+ dates: dateRange(fields.start, fields.end, fields.date),
1913
+ location: fields.location ?? "",
1914
+ url: fields.url ?? fields.link ?? ""
1915
+ };
1916
+ }
1917
+ function dateRange(start, end, date) {
1918
+ if (date) return date;
1919
+ if (start && end) return `${start} \u2013 ${end}`;
1920
+ return start ?? end ?? "";
1921
+ }
1922
+
1923
+ // src/renderers/html.ts
1924
+ var tokenSafe2 = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "");
1925
+ var HtmlRenderer = class {
1926
+ constructor(options = {}) {
1927
+ this.theme = options.theme ?? resolveTheme();
1928
+ this.prefix = options.classPrefix ?? "retex";
1929
+ this.overrides = options.overrides ?? /* @__PURE__ */ new Map();
1930
+ this.useHeader = options.header ?? true;
1931
+ this.ctx = {
1932
+ theme: this.theme,
1933
+ classPrefix: this.prefix,
1934
+ overrides: this.overrides,
1935
+ renderNodes: (nodes) => this.renderFlow(nodes),
1936
+ cls: (name) => this.cls(name)
1937
+ };
1938
+ }
1939
+ /** Render the resume body as an HTML fragment (no `<html>`/`<style>`). */
1940
+ render(ast) {
1941
+ const regions = toRegions(ast.children);
1942
+ const parts = [];
1943
+ const preamble = regions[0]?.section ? void 0 : regions.shift();
1944
+ if (preamble) parts.push(this.renderPreamble(preamble.nodes));
1945
+ for (const region of regions) {
1946
+ if (region.section) parts.push(this.renderSection(region.section, region.nodes));
1947
+ else parts.push(this.renderFlow(region.nodes));
1948
+ }
1949
+ return `<div class="${this.cls("resume")}">
1950
+ ${parts.filter(Boolean).join("\n")}
1951
+ </div>`;
1952
+ }
1953
+ /** Render a complete, standalone HTML document including the stylesheet. */
1954
+ renderDocument(ast, title = "Resume") {
1955
+ return [
1956
+ "<!DOCTYPE html>",
1957
+ '<html lang="en">',
1958
+ "<head>",
1959
+ '<meta charset="utf-8">',
1960
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
1961
+ `<title>${escapeHtml(title)}</title>`,
1962
+ `<style>
1963
+ ${this.styles()}
1964
+ </style>`,
1965
+ "</head>",
1966
+ "<body>",
1967
+ this.render(ast),
1968
+ "</body>",
1969
+ "</html>"
1970
+ ].join("\n");
1971
+ }
1972
+ /** The stylesheet for the active theme (without `<style>` tags). */
1973
+ styles() {
1974
+ return themeToCss(this.theme, this.prefix);
1975
+ }
1976
+ /* --------------------------- structuring ---------------------------- */
1977
+ renderSection(section, body) {
1978
+ const styleMod = ` ${this.cls("section")}--${this.theme.sectionStyle}`;
1979
+ return `<section class="${this.cls("section")}${styleMod}"><h2 class="${this.cls("section-title")}">${escapeHtml(section.title)}</h2><div class="${this.cls("section-body")}">${this.renderFlow(body)}</div></section>`;
1980
+ }
1981
+ renderPreamble(nodes) {
1982
+ if (!this.useHeader) return this.renderFlow(nodes);
1983
+ const { name, title, contacts, other } = splitPreamble(nodes);
1984
+ const hasHeader = name || title || contacts.length > 0;
1985
+ let html = "";
1986
+ if (hasHeader) {
1987
+ html += `<header class="${this.cls("header")}">`;
1988
+ if (name) html += `<h1 class="${this.cls("name")}">${escapeHtml(name.value)}</h1>`;
1989
+ if (title) html += `<p class="${this.cls("title")}">${escapeHtml(title.value)}</p>`;
1990
+ if (contacts.length > 0) {
1991
+ html += `<div class="${this.cls("contact")}">${contacts.map((c) => this.renderContactItem(c)).join("")}</div>`;
1992
+ }
1993
+ html += `</header>`;
1994
+ }
1995
+ if (other.length > 0) html += this.renderFlow(other);
1996
+ return html;
1997
+ }
1998
+ renderContactItem(node) {
1999
+ const item = this.cls("contact-item");
2000
+ if (node.type === "contact") {
2001
+ switch (node.field) {
2002
+ case "email":
2003
+ return this.contactLink(`mailto:${node.value}`, "email", node.value, item);
2004
+ case "phone":
2005
+ return this.contactLink(
2006
+ `tel:${node.value.replace(/[^\d+]/g, "")}`,
2007
+ "phone",
2008
+ node.value,
2009
+ item
2010
+ );
2011
+ case "website":
2012
+ return this.contactLink(node.value, "website", node.value, item);
2013
+ case "location":
2014
+ return `<span class="${item}">${this.icon("location")}${escapeHtml(node.value)}</span>`;
2015
+ default:
2016
+ return `<span class="${item}">${escapeHtml(node.value)}</span>`;
2017
+ }
2018
+ }
2019
+ if (node.type === "icon")
2020
+ return `<span class="${item}">${this.renderNode(node)}</span>`;
2021
+ if (node.type === "link" || node.type === "url") {
2022
+ return `<span class="${item}">${this.renderNode(node)}</span>`;
2023
+ }
2024
+ return this.renderNode(node);
2025
+ }
2026
+ contactLink(url, icon, label, cls) {
2027
+ const { safe } = sanitizeUrl(url);
2028
+ return `<a class="${cls}" href="${escapeAttribute(safe)}">${this.icon(icon)}${escapeHtml(label)}</a>`;
2029
+ }
2030
+ /* ----------------------------- flow / inline ------------------------ */
2031
+ /** Block-aware rendering: groups inline runs into `<p>`, renders blocks as-is. */
2032
+ renderFlow(nodes) {
2033
+ const out = [];
2034
+ let inline = [];
2035
+ const flush = () => {
2036
+ const html = this.renderInline(inline).trim();
2037
+ if (html) out.push(`<p class="${this.cls("para")}">${html}</p>`);
2038
+ inline = [];
2039
+ };
2040
+ for (const node of nodes) {
2041
+ if (node.type === "parbreak") {
2042
+ flush();
2043
+ } else if (isBlockNode(node)) {
2044
+ flush();
2045
+ out.push(this.renderNode(node));
2046
+ } else {
2047
+ inline.push(node);
2048
+ }
2049
+ }
2050
+ flush();
2051
+ return out.join("\n");
2052
+ }
2053
+ renderInline(nodes) {
2054
+ return nodes.map((n) => this.renderNode(n)).join("");
2055
+ }
2056
+ /* ------------------------------ per node ---------------------------- */
2057
+ renderNode(node) {
2058
+ const override = this.overrides.get(node.type) ?? (node.type === "command" ? this.overrides.get(`command:${node.name}`) : void 0);
2059
+ if (override) return override(node, this.ctx, (ns) => this.renderFlow(ns));
2060
+ switch (node.type) {
2061
+ case "text":
2062
+ return escapeHtml(node.value);
2063
+ case "parbreak":
2064
+ return "";
2065
+ case "linebreak":
2066
+ return "<br>";
2067
+ case "rule":
2068
+ return `<hr class="${this.cls("rule")}">`;
2069
+ case "space":
2070
+ return node.axis === "vertical" ? `<div style="height:${this.dim(node.size)}"></div>` : `<span style="display:inline-block;width:${this.dim(node.size)}"></span>`;
2071
+ case "group":
2072
+ return this.renderInline(node.children);
2073
+ case "bold":
2074
+ return `<strong>${this.renderInline(node.children)}</strong>`;
2075
+ case "italic":
2076
+ return `<em>${this.renderInline(node.children)}</em>`;
2077
+ case "underline":
2078
+ return `<u>${this.renderInline(node.children)}</u>`;
2079
+ case "strike":
2080
+ return `<s>${this.renderInline(node.children)}</s>`;
2081
+ case "color": {
2082
+ const inner = this.renderInline(node.children);
2083
+ if (!node.color || !isSafeColor(node.color)) return inner;
2084
+ return `<span style="color:${node.color}">${inner}</span>`;
2085
+ }
2086
+ case "themecolor": {
2087
+ const inner = this.renderInline(node.children);
2088
+ const tok = tokenSafe2(node.token);
2089
+ return `<span style="color:var(--${this.prefix}-color-${tok}, currentColor)">${inner}</span>`;
2090
+ }
2091
+ case "fontsize": {
2092
+ const inner = this.renderInline(node.children);
2093
+ if (!isSafeDimension(node.size)) return inner;
2094
+ return `<span style="font-size:${node.size}">${inner}</span>`;
2095
+ }
2096
+ case "fontfamily": {
2097
+ const inner = this.renderInline(node.children);
2098
+ const family = node.family === "monospace" ? `var(--${this.prefix}-font-mono)` : sanitizeStyleValue(node.family);
2099
+ if (!family) return inner;
2100
+ return `<span style="font-family:${family}">${inner}</span>`;
2101
+ }
2102
+ case "fontscale": {
2103
+ const inner = this.renderInline(node.children);
2104
+ if (node.scale === "normal") return `<span>${inner}</span>`;
2105
+ return `<span class="${this.cls(`scale-${node.scale}`)}">${inner}</span>`;
2106
+ }
2107
+ case "link":
2108
+ return this.renderLink(node.href, this.renderInline(node.children));
2109
+ case "url":
2110
+ return this.renderLink(node.href, escapeHtml(node.rawHref));
2111
+ case "icon":
2112
+ return this.icon(node.name);
2113
+ case "section":
2114
+ return `<h${node.level + 1} class="${this.cls("subsection-title")}">${escapeHtml(node.title)}</h${node.level + 1}>`;
2115
+ case "list":
2116
+ return this.renderList(node);
2117
+ case "columns":
2118
+ return this.renderColumns(node);
2119
+ case "skills":
2120
+ return this.renderSkills(node.items);
2121
+ case "job":
2122
+ return this.renderEntry(node.fields, node.children, "job");
2123
+ case "education":
2124
+ return this.renderEntry(node.fields, node.children, "education");
2125
+ case "project":
2126
+ return this.renderEntry(node.fields, node.children, "project");
2127
+ case "contact":
2128
+ return this.renderContactItem(node);
2129
+ case "command":
2130
+ return this.renderCommand(node);
2131
+ case "error":
2132
+ return "";
2133
+ default:
2134
+ return "";
2135
+ }
2136
+ }
2137
+ renderLink(href, label) {
2138
+ const { safe } = sanitizeUrl(href);
2139
+ const external = /^https?:/i.test(safe);
2140
+ const rel = external ? ' target="_blank" rel="noopener noreferrer"' : "";
2141
+ return `<a class="${this.cls("link")}" href="${escapeAttribute(safe)}"${rel}>${label}</a>`;
2142
+ }
2143
+ renderList(node) {
2144
+ const tag = node.kind === "enumerate" ? "ol" : "ul";
2145
+ const items = node.items.map((item) => `<li>${this.renderInline(item.children).trim()}</li>`).join("");
2146
+ return `<${tag} class="${this.cls("list")}">${items}</${tag}>`;
2147
+ }
2148
+ renderColumns(node) {
2149
+ const cols = node.columns.map((col) => {
2150
+ const basis = isSafeDimension(col.width) ? col.width : "auto";
2151
+ const flex = basis === "auto" ? "flex:1 1 0" : `flex:0 0 ${basis}`;
2152
+ return `<div class="${this.cls("column")}" style="${flex}">${this.renderFlow(col.children)}</div>`;
2153
+ }).join("");
2154
+ return `<div class="${this.cls("columns")}">${cols}</div>`;
2155
+ }
2156
+ renderSkills(items) {
2157
+ const lis = items.map((s) => `<li class="${this.cls("skill")}">${escapeHtml(s)}</li>`).join("");
2158
+ return `<ul class="${this.cls("skills")}">${lis}</ul>`;
2159
+ }
2160
+ renderEntry(fields, body, kind) {
2161
+ const { title, subtitle, dates, location, url } = entryParts(fields, kind);
2162
+ const titleHtml = url ? this.renderLink(sanitizeUrl(url).safe, escapeHtml(title)) : escapeHtml(title);
2163
+ let html = `<div class="${this.cls("entry")} ${this.cls(kind)}">`;
2164
+ html += `<div class="${this.cls("entry-row")}">`;
2165
+ html += `<span class="${this.cls("entry-title")}">${titleHtml}</span>`;
2166
+ if (dates)
2167
+ html += `<span class="${this.cls("entry-dates")}">${escapeHtml(dates)}</span>`;
2168
+ html += `</div>`;
2169
+ if (subtitle || location) {
2170
+ html += `<div class="${this.cls("entry-row")}">`;
2171
+ html += `<span class="${this.cls("entry-subtitle")}">${escapeHtml(subtitle)}</span>`;
2172
+ if (location)
2173
+ html += `<span class="${this.cls("entry-location")}">${escapeHtml(location)}</span>`;
2174
+ html += `</div>`;
2175
+ }
2176
+ if (body.length > 0) {
2177
+ html += `<div class="${this.cls("entry-body")}">${this.renderFlow(body)}</div>`;
2178
+ }
2179
+ html += `</div>`;
2180
+ return html;
2181
+ }
2182
+ renderCommand(node) {
2183
+ if (node.name === "center") {
2184
+ const inner2 = node.args.flatMap((a) => a.children);
2185
+ return `<div class="${this.cls("center")}">${this.renderFlow(inner2)}</div>`;
2186
+ }
2187
+ const inner = node.args.flatMap((a) => a.children);
2188
+ return inner.length > 0 ? this.renderInline(inner) : "";
2189
+ }
2190
+ /* ------------------------------ helpers ----------------------------- */
2191
+ icon(name) {
2192
+ const svg = iconToSvg(name, { className: this.cls("icon") });
2193
+ if (svg) return svg;
2194
+ return `<span class="${this.cls("icon")}" title="${escapeAttribute(name)}"></span>`;
2195
+ }
2196
+ dim(value) {
2197
+ return isSafeDimension(value) ? value : "0";
2198
+ }
2199
+ cls(name) {
2200
+ return `${this.prefix}-${name}`;
2201
+ }
2202
+ };
2203
+ function renderHtml(ast, options) {
2204
+ return new HtmlRenderer(options).render(ast);
2205
+ }
2206
+ function renderHtmlDocument(ast, options) {
2207
+ return new HtmlRenderer(options).renderDocument(ast, options?.title);
2208
+ }
2209
+
2210
+ // src/renderers/json.ts
2211
+ function toJsonTree(ast, opts = {}) {
2212
+ const strip = opts.stripMeta ?? false;
2213
+ const clone = (value) => {
2214
+ if (Array.isArray(value)) return value.map(clone);
2215
+ if (value && typeof value === "object") {
2216
+ const out = {};
2217
+ for (const [k, v] of Object.entries(value)) {
2218
+ if (strip && (k === "range" || k === "hash")) continue;
2219
+ out[k] = clone(v);
2220
+ }
2221
+ return out;
2222
+ }
2223
+ return value;
2224
+ };
2225
+ return clone(ast);
2226
+ }
2227
+ function renderJson(ast, opts = {}) {
2228
+ return JSON.stringify(toJsonTree(ast, opts), null, opts.indent ?? 2);
2229
+ }
2230
+
2231
+ // src/renderers/pdf.ts
2232
+ function pageCss(theme) {
2233
+ return `@page { size: ${theme.page.size}; margin: ${theme.page.margin}; }
2234
+ html, body { margin: 0; padding: 0; background: #fff; }
2235
+ @media screen { body { background: #f1f5f9; padding: 24px; } .retex-resume { box-shadow: 0 1px 8px rgba(0,0,0,.12); } }`;
2236
+ }
2237
+ function renderPrintHtml(ast, options = {}) {
2238
+ const theme = options.theme ?? resolveTheme();
2239
+ const renderer = new HtmlRenderer({ ...options, theme });
2240
+ const body = renderer.render(ast);
2241
+ const title = options.title ?? "Resume";
2242
+ return [
2243
+ "<!DOCTYPE html>",
2244
+ '<html lang="en">',
2245
+ "<head>",
2246
+ '<meta charset="utf-8">',
2247
+ '<meta name="viewport" content="width=device-width, initial-scale=1">',
2248
+ `<title>${title.replace(/[<>&]/g, "")}</title>`,
2249
+ `<style>
2250
+ ${renderer.styles()}
2251
+ ${pageCss(theme)}
2252
+ </style>`,
2253
+ "</head>",
2254
+ "<body>",
2255
+ body,
2256
+ "</body>",
2257
+ "</html>"
2258
+ ].join("\n");
2259
+ }
2260
+ async function renderPdf(ast, options = {}) {
2261
+ const html = renderPrintHtml(ast, options);
2262
+ const theme = options.theme ?? resolveTheme();
2263
+ const launch = options.launch ?? await defaultLauncher();
2264
+ const browser = await launch();
2265
+ try {
2266
+ const page = await browser.newPage();
2267
+ await page.setContent(html, { waitUntil: "networkidle0" });
2268
+ return await page.pdf({
2269
+ printBackground: options.printBackground ?? true,
2270
+ preferCSSPageSize: true,
2271
+ format: theme.page.size,
2272
+ margin: {
2273
+ top: theme.page.margin,
2274
+ bottom: theme.page.margin,
2275
+ left: theme.page.margin,
2276
+ right: theme.page.margin
2277
+ }
2278
+ });
2279
+ } finally {
2280
+ await browser.close();
2281
+ }
2282
+ }
2283
+ async function defaultLauncher() {
2284
+ try {
2285
+ const mod = await import(
2286
+ /* @vite-ignore */
2287
+ 'puppeteer'
2288
+ );
2289
+ return () => mod.launch({ headless: true });
2290
+ } catch {
2291
+ throw new Error(
2292
+ "PDF export requires a headless browser. Either install `puppeteer` (`npm i -D puppeteer`) or pass `options.launch` with your own Playwright/Chromium launcher. Alternatively, use `renderPrintHtml()` and print to PDF in the browser via `window.print()`."
2293
+ );
2294
+ }
2295
+ }
2296
+
2297
+ // src/renderers/react.ts
2298
+ var tokenSafe3 = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "");
2299
+ var ReactRenderer = class {
2300
+ constructor(options) {
2301
+ this.key = 0;
2302
+ if (typeof options.createElement !== "function") {
2303
+ throw new TypeError(
2304
+ "ReactRenderer requires a `createElement` factory (e.g. React.createElement)."
2305
+ );
2306
+ }
2307
+ this.theme = options.theme ?? resolveTheme();
2308
+ this.prefix = options.classPrefix ?? "retex";
2309
+ this.h = options.createElement;
2310
+ this.Fragment = options.Fragment ?? "div";
2311
+ this.overrides = options.overrides ?? /* @__PURE__ */ new Map();
2312
+ this.useHeader = options.header ?? true;
2313
+ }
2314
+ /** The stylesheet for the active theme. Render it in a `<style>` yourself. */
2315
+ styles() {
2316
+ return themeToCss(this.theme, this.prefix);
2317
+ }
2318
+ /** Render the document AST to a React element. */
2319
+ render(ast) {
2320
+ const regions = toRegions(ast.children);
2321
+ const parts = [];
2322
+ const preamble = regions[0]?.section ? void 0 : regions.shift();
2323
+ if (preamble) parts.push(...this.renderPreamble(preamble.nodes));
2324
+ for (const region of regions) {
2325
+ if (region.section) parts.push(this.renderSection(region.section, region.nodes));
2326
+ else parts.push(...this.renderFlow(region.nodes));
2327
+ }
2328
+ return this.el("div", { className: this.cls("resume") }, parts);
2329
+ }
2330
+ ctx() {
2331
+ return {
2332
+ theme: this.theme,
2333
+ classPrefix: this.prefix,
2334
+ h: this.h,
2335
+ Fragment: this.Fragment,
2336
+ overrides: this.overrides,
2337
+ renderNodes: (nodes) => this.renderFlow(nodes),
2338
+ cls: (name) => this.cls(name)
2339
+ };
2340
+ }
2341
+ /* --------------------------- structuring ---------------------------- */
2342
+ renderSection(section, body) {
2343
+ return this.el(
2344
+ "section",
2345
+ {
2346
+ className: `${this.cls("section")} ${this.cls("section")}--${this.theme.sectionStyle}`
2347
+ },
2348
+ [
2349
+ this.el("h2", { className: this.cls("section-title") }, [section.title]),
2350
+ this.el("div", { className: this.cls("section-body") }, this.renderFlow(body))
2351
+ ]
2352
+ );
2353
+ }
2354
+ renderPreamble(nodes) {
2355
+ if (!this.useHeader) return this.renderFlow(nodes);
2356
+ const { name, title, contacts, other } = splitPreamble(nodes);
2357
+ const out = [];
2358
+ if (name || title || contacts.length > 0) {
2359
+ const head = [];
2360
+ if (name) head.push(this.el("h1", { className: this.cls("name") }, [name.value]));
2361
+ if (title) head.push(this.el("p", { className: this.cls("title") }, [title.value]));
2362
+ if (contacts.length > 0) {
2363
+ head.push(
2364
+ this.el(
2365
+ "div",
2366
+ { className: this.cls("contact") },
2367
+ contacts.map((c) => this.renderNode(c))
2368
+ )
2369
+ );
2370
+ }
2371
+ out.push(this.el("header", { className: this.cls("header") }, head));
2372
+ }
2373
+ if (other.length > 0) out.push(...this.renderFlow(other));
2374
+ return out;
2375
+ }
2376
+ /* ----------------------------- flow / inline ------------------------ */
2377
+ renderFlow(nodes) {
2378
+ const out = [];
2379
+ let inline = [];
2380
+ const flush = () => {
2381
+ if (inline.length === 0) return;
2382
+ const meaningful = inline.some((n) => n.type !== "text" || n.value.trim() !== "");
2383
+ if (meaningful) {
2384
+ out.push(
2385
+ this.el("p", { className: this.cls("para") }, this.renderInline(inline))
2386
+ );
2387
+ }
2388
+ inline = [];
2389
+ };
2390
+ for (const node of nodes) {
2391
+ if (node.type === "parbreak") flush();
2392
+ else if (isBlockNode(node)) {
2393
+ flush();
2394
+ out.push(this.renderNode(node));
2395
+ } else inline.push(node);
2396
+ }
2397
+ flush();
2398
+ return out;
2399
+ }
2400
+ renderInline(nodes) {
2401
+ return nodes.map((n) => this.renderNode(n));
2402
+ }
2403
+ /* ------------------------------ per node ---------------------------- */
2404
+ renderNode(node) {
2405
+ const override = this.overrides.get(node.type) ?? (node.type === "command" ? this.overrides.get(`command:${node.name}`) : void 0);
2406
+ if (override) return override(node, this.ctx(), (ns) => this.renderFlow(ns));
2407
+ switch (node.type) {
2408
+ case "text":
2409
+ return node.value;
2410
+ case "parbreak":
2411
+ return null;
2412
+ case "linebreak":
2413
+ return this.el("br", null, []);
2414
+ case "rule":
2415
+ return this.el("hr", { className: this.cls("rule") }, []);
2416
+ case "space":
2417
+ return node.axis === "vertical" ? this.el("div", { style: { height: this.dim(node.size) } }, []) : this.el(
2418
+ "span",
2419
+ { style: { display: "inline-block", width: this.dim(node.size) } },
2420
+ []
2421
+ );
2422
+ case "group":
2423
+ return this.frag(this.renderInline(node.children));
2424
+ case "bold":
2425
+ return this.el("strong", null, this.renderInline(node.children));
2426
+ case "italic":
2427
+ return this.el("em", null, this.renderInline(node.children));
2428
+ case "underline":
2429
+ return this.el("u", null, this.renderInline(node.children));
2430
+ case "strike":
2431
+ return this.el("s", null, this.renderInline(node.children));
2432
+ case "color": {
2433
+ const inner = this.renderInline(node.children);
2434
+ if (!node.color || !isSafeColor(node.color)) return this.frag(inner);
2435
+ return this.el("span", { style: { color: node.color } }, inner);
2436
+ }
2437
+ case "themecolor":
2438
+ return this.el(
2439
+ "span",
2440
+ {
2441
+ style: {
2442
+ color: `var(--${this.prefix}-color-${tokenSafe3(node.token)}, currentColor)`
2443
+ }
2444
+ },
2445
+ this.renderInline(node.children)
2446
+ );
2447
+ case "fontsize": {
2448
+ const inner = this.renderInline(node.children);
2449
+ if (!isSafeDimension(node.size)) return this.frag(inner);
2450
+ return this.el("span", { style: { fontSize: node.size } }, inner);
2451
+ }
2452
+ case "fontfamily": {
2453
+ const inner = this.renderInline(node.children);
2454
+ const family = node.family === "monospace" ? `var(--${this.prefix}-font-mono)` : sanitizeStyleValue(node.family);
2455
+ if (!family) return this.frag(inner);
2456
+ return this.el("span", { style: { fontFamily: family } }, inner);
2457
+ }
2458
+ case "fontscale":
2459
+ return node.scale === "normal" ? this.frag(this.renderInline(node.children)) : this.el(
2460
+ "span",
2461
+ { className: this.cls(`scale-${node.scale}`) },
2462
+ this.renderInline(node.children)
2463
+ );
2464
+ case "link":
2465
+ return this.renderLink(node.href, this.renderInline(node.children));
2466
+ case "url":
2467
+ return this.renderLink(node.href, [node.rawHref]);
2468
+ case "icon":
2469
+ return this.renderIcon(node.name);
2470
+ case "section":
2471
+ return this.el(
2472
+ `h${node.level + 1}`,
2473
+ { className: this.cls("subsection-title") },
2474
+ [node.title]
2475
+ );
2476
+ case "list":
2477
+ return this.renderList(node);
2478
+ case "columns":
2479
+ return this.renderColumns(node);
2480
+ case "skills":
2481
+ return this.el(
2482
+ "ul",
2483
+ { className: this.cls("skills") },
2484
+ node.items.map((s) => this.el("li", { className: this.cls("skill") }, [s]))
2485
+ );
2486
+ case "job":
2487
+ return this.renderEntry(node, "job");
2488
+ case "education":
2489
+ return this.renderEntry(node, "education");
2490
+ case "project":
2491
+ return this.renderEntry(node, "project");
2492
+ case "contact":
2493
+ return this.renderContact(node);
2494
+ case "command":
2495
+ return this.renderCommand(node);
2496
+ default:
2497
+ return null;
2498
+ }
2499
+ }
2500
+ renderContact(node) {
2501
+ const item = this.cls("contact-item");
2502
+ switch (node.field) {
2503
+ case "email":
2504
+ return this.contactLink(`mailto:${node.value}`, "email", node.value, item);
2505
+ case "phone":
2506
+ return this.contactLink(
2507
+ `tel:${node.value.replace(/[^\d+]/g, "")}`,
2508
+ "phone",
2509
+ node.value,
2510
+ item
2511
+ );
2512
+ case "website":
2513
+ return this.contactLink(node.value, "website", node.value, item);
2514
+ case "location":
2515
+ return this.el("span", { className: item }, [
2516
+ this.renderIcon("location"),
2517
+ node.value
2518
+ ]);
2519
+ default:
2520
+ return this.el("span", { className: item }, [node.value]);
2521
+ }
2522
+ }
2523
+ contactLink(url, icon, label, cls) {
2524
+ const { safe } = sanitizeUrl(url);
2525
+ return this.el("a", { className: cls, href: safe }, [this.renderIcon(icon), label]);
2526
+ }
2527
+ renderLink(href, label) {
2528
+ const { safe } = sanitizeUrl(href);
2529
+ const external = /^https?:/i.test(safe);
2530
+ const props = { className: this.cls("link"), href: safe };
2531
+ if (external) {
2532
+ props.target = "_blank";
2533
+ props.rel = "noopener noreferrer";
2534
+ }
2535
+ return this.el("a", props, label);
2536
+ }
2537
+ renderList(node) {
2538
+ const tag = node.kind === "enumerate" ? "ol" : "ul";
2539
+ return this.el(
2540
+ tag,
2541
+ { className: this.cls("list") },
2542
+ node.items.map((item) => this.el("li", null, this.renderInline(item.children)))
2543
+ );
2544
+ }
2545
+ renderColumns(node) {
2546
+ return this.el(
2547
+ "div",
2548
+ { className: this.cls("columns") },
2549
+ node.columns.map((col) => {
2550
+ const basis = isSafeDimension(col.width) ? col.width : "auto";
2551
+ const style = basis === "auto" ? { flex: "1 1 0" } : { flex: `0 0 ${basis}` };
2552
+ return this.el(
2553
+ "div",
2554
+ { className: this.cls("column"), style },
2555
+ this.renderFlow(col.children)
2556
+ );
2557
+ })
2558
+ );
2559
+ }
2560
+ renderEntry(node, kind) {
2561
+ const { title, subtitle, dates, location, url } = entryParts(node.fields, kind);
2562
+ const rows = [];
2563
+ const titleEl = url ? this.renderLink(sanitizeUrl(url).safe, [title]) : title;
2564
+ rows.push(
2565
+ this.el("div", { className: this.cls("entry-row") }, [
2566
+ this.el("span", { className: this.cls("entry-title") }, [titleEl]),
2567
+ dates ? this.el("span", { className: this.cls("entry-dates") }, [dates]) : null
2568
+ ])
2569
+ );
2570
+ if (subtitle || location) {
2571
+ rows.push(
2572
+ this.el("div", { className: this.cls("entry-row") }, [
2573
+ this.el("span", { className: this.cls("entry-subtitle") }, [subtitle]),
2574
+ location ? this.el("span", { className: this.cls("entry-location") }, [location]) : null
2575
+ ])
2576
+ );
2577
+ }
2578
+ if (node.children.length > 0) {
2579
+ rows.push(
2580
+ this.el(
2581
+ "div",
2582
+ { className: this.cls("entry-body") },
2583
+ this.renderFlow(node.children)
2584
+ )
2585
+ );
2586
+ }
2587
+ return this.el("div", { className: `${this.cls("entry")} ${this.cls(kind)}` }, rows);
2588
+ }
2589
+ renderCommand(node) {
2590
+ const inner = node.args.flatMap((a) => a.children);
2591
+ if (node.name === "center") {
2592
+ return this.el("div", { className: this.cls("center") }, this.renderFlow(inner));
2593
+ }
2594
+ return this.frag(this.renderInline(inner));
2595
+ }
2596
+ renderIcon(name) {
2597
+ const svg = iconToSvg(name);
2598
+ if (!svg) return this.el("span", { className: this.cls("icon"), title: name }, []);
2599
+ return this.el(
2600
+ "span",
2601
+ {
2602
+ className: this.cls("icon"),
2603
+ dangerouslySetInnerHTML: { __html: svg }
2604
+ },
2605
+ []
2606
+ );
2607
+ }
2608
+ /* ------------------------------ helpers ----------------------------- */
2609
+ el(type, props, children) {
2610
+ const filtered = children.filter((c) => c !== null && c !== void 0 && c !== "");
2611
+ const finalProps = { ...props ?? {}, key: this.key++ };
2612
+ return this.h(type, finalProps, ...filtered);
2613
+ }
2614
+ frag(children) {
2615
+ return this.el(this.Fragment, null, children);
2616
+ }
2617
+ dim(value) {
2618
+ return isSafeDimension(value) ? value : "0";
2619
+ }
2620
+ cls(name) {
2621
+ return `${this.prefix}-${name}`;
2622
+ }
2623
+ };
2624
+ function renderReact(ast, options) {
2625
+ return new ReactRenderer(options).render(ast);
2626
+ }
2627
+
2628
+ // src/engine.ts
2629
+ var isTheme = (t) => !!t && "colors" in t && "fonts" in t && "fontSizes" in t && "page" in t;
2630
+ var ReTeXEngine = class {
2631
+ constructor(options = {}) {
2632
+ this.htmlOverrides = /* @__PURE__ */ new Map();
2633
+ this.reactOverrides = /* @__PURE__ */ new Map();
2634
+ this.cache = /* @__PURE__ */ new Map();
2635
+ this.generation = 0;
2636
+ this.registry = createDefaultRegistry();
2637
+ this.theme = isTheme(options.theme) ? options.theme : resolveTheme(options.theme);
2638
+ this.prefix = options.classPrefix ?? "retex";
2639
+ this.cacheSize = options.cacheSize ?? 64;
2640
+ for (const plugin of options.plugins ?? []) this.use(plugin);
2641
+ }
2642
+ /* ----------------------------- plugins ------------------------------ */
2643
+ use(plugin) {
2644
+ for (const cmd of plugin.commands ?? []) this.registerCommand(cmd);
2645
+ for (const env of plugin.environments ?? []) this.registerEnvironment(env);
2646
+ for (const [name, def] of Object.entries(plugin.icons ?? {})) {
2647
+ this.registerIcon(name, def);
2648
+ }
2649
+ for (const [key, fn] of Object.entries(plugin.htmlRenderers ?? {})) {
2650
+ this.registerHtmlRenderer(key, fn);
2651
+ }
2652
+ for (const [key, fn] of Object.entries(plugin.reactRenderers ?? {})) {
2653
+ this.registerReactRenderer(key, fn);
2654
+ }
2655
+ if (plugin.theme) this.setTheme(plugin.theme);
2656
+ plugin.setup?.(this);
2657
+ this.invalidate();
2658
+ return this;
2659
+ }
2660
+ registerCommand(def) {
2661
+ const { render, ...spec } = def;
2662
+ this.registry.registerCommand(spec);
2663
+ if (render?.html) this.htmlOverrides.set(`command:${def.name}`, render.html);
2664
+ if (render?.react) this.reactOverrides.set(`command:${def.name}`, render.react);
2665
+ this.invalidate();
2666
+ return this;
2667
+ }
2668
+ registerEnvironment(def) {
2669
+ this.registry.registerEnvironment(def);
2670
+ this.invalidate();
2671
+ return this;
2672
+ }
2673
+ registerHtmlRenderer(key, fn) {
2674
+ this.htmlOverrides.set(key, fn);
2675
+ return this;
2676
+ }
2677
+ registerReactRenderer(key, fn) {
2678
+ this.reactOverrides.set(key, fn);
2679
+ return this;
2680
+ }
2681
+ registerIcon(name, def) {
2682
+ registerIcon(name, def);
2683
+ return this;
2684
+ }
2685
+ /* ----------------------------- theming ------------------------------ */
2686
+ setTheme(theme) {
2687
+ this.theme = isTheme(theme) ? theme : resolveTheme(theme, this.theme);
2688
+ this.invalidate();
2689
+ return this;
2690
+ }
2691
+ getTheme() {
2692
+ return this.theme;
2693
+ }
2694
+ getRegistry() {
2695
+ return this.registry;
2696
+ }
2697
+ /* --------------------------- compilation ---------------------------- */
2698
+ tokenize(source) {
2699
+ return tokenize(source);
2700
+ }
2701
+ /** Tokenize, parse, and (by default) validate a source string. Cached. */
2702
+ compile(source, options = {}) {
2703
+ const runValidate = options.validate ?? true;
2704
+ const key = `${this.generation}:${runValidate ? "v" : "p"}:${source}`;
2705
+ const cached = this.cache.get(key);
2706
+ if (cached) return cached;
2707
+ const { tokens, diagnostics: lexDiags } = tokenize(source);
2708
+ const { ast, diagnostics: parseDiags } = new Parser(tokens, {
2709
+ registry: this.registry
2710
+ }).parse();
2711
+ const diagnostics = [...lexDiags, ...parseDiags];
2712
+ if (runValidate) diagnostics.push(...validate(ast, { theme: this.theme }));
2713
+ const result = { source, tokens, ast, diagnostics };
2714
+ this.remember(key, result);
2715
+ return result;
2716
+ }
2717
+ parse(source) {
2718
+ return this.compile(source, { validate: false }).ast;
2719
+ }
2720
+ validate(input) {
2721
+ if (typeof input === "string") return this.compile(input).diagnostics;
2722
+ return validate(input, { theme: this.theme });
2723
+ }
2724
+ /* ----------------------------- renderers ---------------------------- */
2725
+ toHtml(input, options = {}) {
2726
+ return this.htmlRenderer(options).render(this.astOf(input));
2727
+ }
2728
+ toHtmlDocument(input, options = {}) {
2729
+ return this.htmlRenderer(options).renderDocument(this.astOf(input), options.title);
2730
+ }
2731
+ toReact(input, options) {
2732
+ return new ReactRenderer({
2733
+ ...options,
2734
+ theme: this.theme,
2735
+ classPrefix: this.prefix,
2736
+ overrides: this.reactOverrides
2737
+ }).render(this.astOf(input));
2738
+ }
2739
+ toJson(input, options) {
2740
+ return renderJson(this.astOf(input), options);
2741
+ }
2742
+ toPrintHtml(input, options = {}) {
2743
+ return renderPrintHtml(this.astOf(input), this.printOptions(options));
2744
+ }
2745
+ toPdf(input, options = {}) {
2746
+ return renderPdf(this.astOf(input), this.printOptions(options));
2747
+ }
2748
+ /** The CSS stylesheet for the active theme. */
2749
+ styles() {
2750
+ return this.htmlRenderer().styles();
2751
+ }
2752
+ /* ------------------------------ internals --------------------------- */
2753
+ astOf(input) {
2754
+ return typeof input === "string" ? this.compile(input, { validate: false }).ast : input;
2755
+ }
2756
+ htmlRenderer(options = {}) {
2757
+ return new HtmlRenderer({
2758
+ theme: this.theme,
2759
+ classPrefix: this.prefix,
2760
+ overrides: this.htmlOverrides,
2761
+ ...options
2762
+ });
2763
+ }
2764
+ printOptions(options) {
2765
+ return {
2766
+ theme: this.theme,
2767
+ classPrefix: this.prefix,
2768
+ overrides: this.htmlOverrides,
2769
+ ...options
2770
+ };
2771
+ }
2772
+ invalidate() {
2773
+ this.generation++;
2774
+ this.cache.clear();
2775
+ }
2776
+ remember(key, result) {
2777
+ if (this.cache.size >= this.cacheSize) {
2778
+ const oldest = this.cache.keys().next().value;
2779
+ if (oldest !== void 0) this.cache.delete(oldest);
2780
+ }
2781
+ this.cache.set(key, result);
2782
+ }
2783
+ };
2784
+ function createEngine(options) {
2785
+ return new ReTeXEngine(options);
2786
+ }
2787
+
2788
+ // src/plugin/examples.ts
2789
+ var badgePlugin = {
2790
+ name: "badge",
2791
+ commands: [
2792
+ {
2793
+ name: "badge",
2794
+ category: "inline",
2795
+ args: [{ kind: "content", name: "label" }],
2796
+ summary: "A small inline badge / pill.",
2797
+ example: "\\badge{New}",
2798
+ render: {
2799
+ html: (node, ctx, renderChildren) => {
2800
+ const n = node;
2801
+ const inner = renderChildren(n.args[0]?.children ?? []);
2802
+ return `<span class="${ctx.cls("badge")}" style="display:inline-block;padding:0.05em 0.45em;border-radius:999px;font-size:0.8em;background:var(--${ctx.classPrefix}-primary);color:#fff;vertical-align:middle">${inner}</span>`;
2803
+ },
2804
+ react: (node, ctx, renderChildren) => {
2805
+ const n = node;
2806
+ return ctx.h(
2807
+ "span",
2808
+ {
2809
+ className: ctx.cls("badge"),
2810
+ style: {
2811
+ display: "inline-block",
2812
+ padding: "0.05em 0.45em",
2813
+ borderRadius: "999px",
2814
+ fontSize: "0.8em",
2815
+ background: `var(--${ctx.classPrefix}-primary)`,
2816
+ color: "#fff",
2817
+ verticalAlign: "middle"
2818
+ }
2819
+ },
2820
+ ...renderChildren(n.args[0]?.children ?? [])
2821
+ );
2822
+ }
2823
+ }
2824
+ }
2825
+ ]
2826
+ };
2827
+ var ratingPlugin = {
2828
+ name: "rating",
2829
+ commands: [
2830
+ {
2831
+ name: "rating",
2832
+ category: "inline",
2833
+ args: [{ kind: "string", name: "score" }],
2834
+ summary: "A 0\u20135 proficiency rating rendered as dots.",
2835
+ example: "\\rating{4}",
2836
+ render: {
2837
+ html: (node, ctx) => {
2838
+ const n = node;
2839
+ const score = clamp(parseFloat(n.args[0]?.raw ?? "0"));
2840
+ let dots = "";
2841
+ for (let i = 1; i <= 5; i++) {
2842
+ dots += `<span style="color:${i <= score ? `var(--${ctx.classPrefix}-primary)` : `var(--${ctx.classPrefix}-border)`}">\u25CF</span>`;
2843
+ }
2844
+ return `<span class="${ctx.cls("rating")}" aria-label="${score} out of 5">${dots}</span>`;
2845
+ }
2846
+ }
2847
+ }
2848
+ ]
2849
+ };
2850
+ function clamp(n) {
2851
+ if (Number.isNaN(n)) return 0;
2852
+ return Math.max(0, Math.min(5, Math.round(n)));
2853
+ }
2854
+
2855
+ // src/editor/printer.ts
2856
+ function escapeText(value) {
2857
+ return value.replace(/([{}\\])/g, "\\$1");
2858
+ }
2859
+ function fieldsToString(fields) {
2860
+ return Object.entries(fields).map(([k, v]) => v === "" ? k : `${k}=${v}`).join(", ");
2861
+ }
2862
+ function printDocument(ast) {
2863
+ return printBlocks(ast.children).trimEnd() + "\n";
2864
+ }
2865
+ function printBlocks(nodes) {
2866
+ const out = [];
2867
+ let inline = [];
2868
+ const flush = () => {
2869
+ const s = printInline(inline).trim();
2870
+ if (s) out.push(s);
2871
+ inline = [];
2872
+ };
2873
+ for (const node of nodes) {
2874
+ if (node.type === "parbreak") {
2875
+ flush();
2876
+ } else if (isBlockNode(node) || node.type === "section" || node.type === "list" || node.type === "columns") {
2877
+ flush();
2878
+ out.push(printBlock(node));
2879
+ } else {
2880
+ inline.push(node);
2881
+ }
2882
+ }
2883
+ flush();
2884
+ return out.join("\n\n");
2885
+ }
2886
+ function printBlock(node, indent = "") {
2887
+ switch (node.type) {
2888
+ case "section":
2889
+ return `${indent}\\${node.level === 2 ? "subsection" : "section"}{${node.title}}`;
2890
+ case "list": {
2891
+ const env = node.kind;
2892
+ const items = node.items.map((it) => `${indent} \\item ${printInline(it.children).trim()}`).join("\n");
2893
+ return `${indent}\\begin{${env}}
2894
+ ${items}
2895
+ ${indent}\\end{${env}}`;
2896
+ }
2897
+ case "columns": {
2898
+ const cols = node.columns.map(
2899
+ (c) => `${indent} \\column{${c.width}}
2900
+ ${indentLines(printBlocks(c.children), indent + " ")}`
2901
+ ).join("\n");
2902
+ return `${indent}\\begin{columns}
2903
+ ${cols}
2904
+ ${indent}\\end{columns}`;
2905
+ }
2906
+ case "job":
2907
+ case "education":
2908
+ case "project": {
2909
+ const head = `${indent}\\${node.type}{${fieldsToString(node.fields)}}`;
2910
+ if (node.children.length === 0) return head;
2911
+ return `${head}{${printInline(node.children).trim()}}`;
2912
+ }
2913
+ case "skills":
2914
+ return `${indent}\\skills{${node.items.join(", ")}}`;
2915
+ case "rule":
2916
+ return `${indent}\\hrule`;
2917
+ case "space":
2918
+ return `${indent}\\${node.axis === "vertical" ? "vspace" : "hspace"}{${node.size}}`;
2919
+ case "command":
2920
+ if (node.name === "center") {
2921
+ const inner = node.args.flatMap((a) => a.children);
2922
+ return `${indent}\\begin{center}
2923
+ ${indentLines(printBlocks(inner), indent + " ")}
2924
+ ${indent}\\end{center}`;
2925
+ }
2926
+ return indent + printInline([node]);
2927
+ default:
2928
+ return indent + printInline([node]);
2929
+ }
2930
+ }
2931
+ function printInline(nodes) {
2932
+ return nodes.map(printNode).join("");
2933
+ }
2934
+ function printNode(node) {
2935
+ switch (node.type) {
2936
+ case "text":
2937
+ return escapeText(node.value);
2938
+ case "parbreak":
2939
+ return "\n\n";
2940
+ case "linebreak":
2941
+ return "\\\\";
2942
+ case "group":
2943
+ return `{${printInline(node.children)}}`;
2944
+ case "bold":
2945
+ return `\\textbf{${printInline(node.children)}}`;
2946
+ case "italic":
2947
+ return `\\textit{${printInline(node.children)}}`;
2948
+ case "underline":
2949
+ return `\\underline{${printInline(node.children)}}`;
2950
+ case "strike":
2951
+ return `\\sout{${printInline(node.children)}}`;
2952
+ case "color":
2953
+ return `\\textcolor{${node.color}}{${printInline(node.children)}}`;
2954
+ case "themecolor":
2955
+ return `\\themecolor{${node.token}}{${printInline(node.children)}}`;
2956
+ case "fontsize":
2957
+ return `\\fontsize{${node.size}}{${printInline(node.children)}}`;
2958
+ case "fontfamily":
2959
+ return `\\fontfamily{${node.family}}{${printInline(node.children)}}`;
2960
+ case "fontscale":
2961
+ return `{\\${node.scale === "normal" ? "normalsize" : node.scale} ${printInline(node.children)}}`;
2962
+ case "link":
2963
+ return `\\href{${node.rawHref}}{${printInline(node.children)}}`;
2964
+ case "url":
2965
+ return `\\url{${node.rawHref}}`;
2966
+ case "icon":
2967
+ return `\\icon{${node.name}}`;
2968
+ case "contact":
2969
+ return `\\${node.field}{${node.value}}`;
2970
+ case "space":
2971
+ return `\\${node.axis === "vertical" ? "vspace" : "hspace"}{${node.size}}`;
2972
+ case "command": {
2973
+ const args = node.args.map((a) => a.raw !== void 0 ? `{${a.raw}}` : `{${printInline(a.children)}}`).join("");
2974
+ return `\\${node.name}${args}`;
2975
+ }
2976
+ // Block nodes appearing in inline context fall back to block printing.
2977
+ default:
2978
+ return printBlock(node);
2979
+ }
2980
+ }
2981
+ function indentLines(text, indent) {
2982
+ return text.split("\n").map((line) => line ? indent + line : line).join("\n");
2983
+ }
2984
+
2985
+ // src/editor/editor.ts
2986
+ var EditorService = class {
2987
+ constructor(options = {}) {
2988
+ this.registry = options.registry ?? createDefaultRegistry();
2989
+ this.theme = options.theme;
2990
+ }
2991
+ /* ----------------------------- diagnostics -------------------------- */
2992
+ getDiagnostics(source) {
2993
+ const { tokens, diagnostics: lex } = tokenize(source);
2994
+ const { ast, diagnostics: parse2 } = new Parser(tokens, {
2995
+ registry: this.registry
2996
+ }).parse();
2997
+ return [...lex, ...parse2, ...validate(ast, this.theme ? { theme: this.theme } : {})];
2998
+ }
2999
+ /* ------------------------------ inspect ----------------------------- */
3000
+ /** Parse and return the AST for inspection / debugging. */
3001
+ inspect(source) {
3002
+ const { tokens } = tokenize(source);
3003
+ return new Parser(tokens, { registry: this.registry }).parse().ast;
3004
+ }
3005
+ /* ------------------------------ format ------------------------------ */
3006
+ format(source) {
3007
+ return printDocument(this.inspect(source));
3008
+ }
3009
+ /* --------------------------- completion ----------------------------- */
3010
+ getCompletions(source, offset) {
3011
+ const before = source.slice(0, offset);
3012
+ const envMatch = /\\(begin|end)\{([a-zA-Z*]*)$/.exec(before);
3013
+ if (envMatch) {
3014
+ const prefix = envMatch[2];
3015
+ const start = offset - prefix.length;
3016
+ return this.registry.allEnvironments().filter((e) => e.name.startsWith(prefix)).map((e) => this.environmentCompletion(e, this.rangeAt(source, start, offset)));
3017
+ }
3018
+ const fieldKeys = this.fieldContext(before);
3019
+ if (fieldKeys) {
3020
+ return fieldKeys.map((key) => ({
3021
+ label: key,
3022
+ kind: "field",
3023
+ detail: "entry field",
3024
+ insertText: `${key}=`
3025
+ }));
3026
+ }
3027
+ const cmdMatch = /\\([a-zA-Z*]*)$/.exec(before);
3028
+ if (cmdMatch) {
3029
+ const prefix = cmdMatch[1];
3030
+ const start = offset - prefix.length - 1;
3031
+ return this.registry.allCommands().filter((c) => c.name.startsWith(prefix)).sort((a, b) => a.name.localeCompare(b.name)).map((c) => this.commandCompletion(c, this.rangeAt(source, start, offset)));
3032
+ }
3033
+ return [];
3034
+ }
3035
+ commandCompletion(def, range2) {
3036
+ const args = def.args ?? [];
3037
+ const snippetArgs = args.map(
3038
+ (a, i) => a.optional ? `[\${${i + 1}:${a.name ?? "opt"}}]` : `{\${${i + 1}:${a.name ?? "arg"}}}`
3039
+ ).join("");
3040
+ const plainArgs = args.map((a) => a.optional ? "[]" : "{}").join("");
3041
+ return {
3042
+ label: `\\${def.name}`,
3043
+ kind: "command",
3044
+ detail: def.summary,
3045
+ documentation: this.commandDoc(def),
3046
+ insertText: `${def.name}${plainArgs}`,
3047
+ snippet: `${def.name}${snippetArgs}`,
3048
+ range: range2
3049
+ };
3050
+ }
3051
+ environmentCompletion(def, range2) {
3052
+ return {
3053
+ label: def.name,
3054
+ kind: "environment",
3055
+ detail: def.summary,
3056
+ documentation: def.documentation ?? def.example,
3057
+ insertText: `${def.name}}`,
3058
+ snippet: `${def.name}}
3059
+ $0
3060
+ \\end{${def.name}}`,
3061
+ range: range2
3062
+ };
3063
+ }
3064
+ /** Suggest field keys when the cursor is inside a `\job/\education/\project{…}`. */
3065
+ fieldContext(before) {
3066
+ const m = /\\(job|education|project)\{([^{}]*)$/.exec(before);
3067
+ if (!m) return null;
3068
+ switch (m[1]) {
3069
+ case "job":
3070
+ return ["title", "company", "location", "start", "end", "url"];
3071
+ case "education":
3072
+ return ["school", "degree", "location", "start", "end", "gpa"];
3073
+ case "project":
3074
+ return ["name", "url", "start", "end", "tech"];
3075
+ default:
3076
+ return null;
3077
+ }
3078
+ }
3079
+ /* ------------------------------ hover ------------------------------- */
3080
+ getHover(source, offset) {
3081
+ const { tokens } = tokenize(source);
3082
+ const idx = tokens.findIndex(
3083
+ (t) => t.type === "Command" /* Command */ && offset >= t.range.start.offset && offset <= t.range.end.offset
3084
+ );
3085
+ if (idx === -1) return null;
3086
+ const token = tokens[idx];
3087
+ if (token.value === "begin" || token.value === "end") {
3088
+ const name = this.readNameAfter(tokens, idx);
3089
+ const env = name ? this.registry.getEnvironment(name) : void 0;
3090
+ if (env) {
3091
+ return {
3092
+ contents: this.environmentDoc(env),
3093
+ range: token.range
3094
+ };
3095
+ }
3096
+ return null;
3097
+ }
3098
+ const def = this.registry.getCommand(token.value);
3099
+ if (!def) {
3100
+ return {
3101
+ contents: `Unknown command \`\\${token.value}\``,
3102
+ range: token.range
3103
+ };
3104
+ }
3105
+ return { contents: this.commandDoc(def), range: token.range };
3106
+ }
3107
+ commandDoc(def) {
3108
+ const sig = `\\${def.name}` + (def.args ?? []).map((a) => a.optional ? `[${a.name ?? "opt"}]` : `{${a.name ?? "arg"}}`).join("");
3109
+ const lines = [`\`\`\`tex
3110
+ ${sig}
3111
+ \`\`\``];
3112
+ if (def.summary) lines.push(def.summary);
3113
+ if (def.documentation) lines.push(def.documentation);
3114
+ if (def.example) lines.push(`**Example:**
3115
+ \`\`\`tex
3116
+ ${def.example}
3117
+ \`\`\``);
3118
+ return lines.join("\n\n");
3119
+ }
3120
+ environmentDoc(def) {
3121
+ const lines = [`\`\`\`tex
3122
+ \\begin{${def.name}} \u2026 \\end{${def.name}}
3123
+ \`\`\``];
3124
+ if (def.summary) lines.push(def.summary);
3125
+ if (def.example) lines.push(`**Example:**
3126
+ \`\`\`tex
3127
+ ${def.example}
3128
+ \`\`\``);
3129
+ return lines.join("\n\n");
3130
+ }
3131
+ /* ------------------------ semantic highlighting --------------------- */
3132
+ getSemanticTokens(source) {
3133
+ const { tokens } = tokenize(source);
3134
+ const out = [];
3135
+ for (let i = 0; i < tokens.length; i++) {
3136
+ const t = tokens[i];
3137
+ switch (t.type) {
3138
+ case "Command" /* Command */: {
3139
+ if (t.value === "begin" || t.value === "end") {
3140
+ out.push({ range: t.range, type: "environment", modifiers: [] });
3141
+ break;
3142
+ }
3143
+ const def = this.registry.getCommand(t.value);
3144
+ out.push({
3145
+ range: t.range,
3146
+ type: def ? "command" : "command-unknown",
3147
+ modifiers: def?.category ? [def.category] : []
3148
+ });
3149
+ break;
3150
+ }
3151
+ case "LBrace" /* LBrace */:
3152
+ case "RBrace" /* RBrace */:
3153
+ out.push({ range: t.range, type: "brace", modifiers: [] });
3154
+ break;
3155
+ case "LBracket" /* LBracket */:
3156
+ case "RBracket" /* RBracket */:
3157
+ out.push({ range: t.range, type: "bracket", modifiers: [] });
3158
+ break;
3159
+ case "Comment" /* Comment */:
3160
+ out.push({ range: t.range, type: "comment", modifiers: [] });
3161
+ break;
3162
+ case "LineBreak" /* LineBreak */:
3163
+ out.push({ range: t.range, type: "linebreak", modifiers: [] });
3164
+ break;
3165
+ case "Text" /* Text */:
3166
+ out.push({ range: t.range, type: "text", modifiers: [] });
3167
+ break;
3168
+ }
3169
+ }
3170
+ return out;
3171
+ }
3172
+ /* ------------------------------ helpers ----------------------------- */
3173
+ readNameAfter(tokens, idx) {
3174
+ let j = idx + 1;
3175
+ while (tokens[j] && tokens[j].type === "Whitespace" /* Whitespace */) j++;
3176
+ if (!tokens[j] || tokens[j].type !== "LBrace" /* LBrace */) return void 0;
3177
+ j++;
3178
+ let name = "";
3179
+ while (tokens[j] && tokens[j].type !== "RBrace" /* RBrace */) {
3180
+ name += tokens[j].value;
3181
+ j++;
3182
+ }
3183
+ return name.trim();
3184
+ }
3185
+ rangeAt(source, start, end) {
3186
+ return {
3187
+ start: this.positionAt(source, start),
3188
+ end: this.positionAt(source, end)
3189
+ };
3190
+ }
3191
+ positionAt(source, offset) {
3192
+ let line = 1;
3193
+ let column = 1;
3194
+ for (let i = 0; i < offset && i < source.length; i++) {
3195
+ if (source[i] === "\n") {
3196
+ line++;
3197
+ column = 1;
3198
+ } else {
3199
+ column++;
3200
+ }
3201
+ }
3202
+ return { offset, line, column };
3203
+ }
3204
+ };
3205
+
3206
+ // src/incremental/incremental.ts
3207
+ var isLetter2 = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z";
3208
+ var IncrementalCompiler = class {
3209
+ constructor(options = {}) {
3210
+ /** Position-independent parse cache, keyed by exact block text. */
3211
+ this.parseCache = /* @__PURE__ */ new Map();
3212
+ /** Position-resolved cache, keyed by `offset:line:text` (already shifted). */
3213
+ this.positioned = /* @__PURE__ */ new Map();
3214
+ this.hits = 0;
3215
+ this.misses = 0;
3216
+ this.registry = options.registry ?? createDefaultRegistry();
3217
+ }
3218
+ /** Clear all caches (e.g. after registering new commands). */
3219
+ reset() {
3220
+ this.parseCache.clear();
3221
+ this.positioned.clear();
3222
+ }
3223
+ compile(source) {
3224
+ this.hits = 0;
3225
+ this.misses = 0;
3226
+ const segments = splitBalancedSegments(source);
3227
+ const children = [];
3228
+ const diagnostics = [];
3229
+ let first = true;
3230
+ const nextPositioned = /* @__PURE__ */ new Map();
3231
+ for (const seg of segments) {
3232
+ const text = source.slice(seg.start, seg.end);
3233
+ if (text.trim() === "") continue;
3234
+ const posKey = `${seg.start}:${seg.startLine}:${text}`;
3235
+ let resolved = this.positioned.get(posKey);
3236
+ if (resolved) {
3237
+ this.hits++;
3238
+ } else {
3239
+ let parsed = this.parseCache.get(text);
3240
+ if (parsed) {
3241
+ this.hits++;
3242
+ } else {
3243
+ this.misses++;
3244
+ const { tokens } = tokenize(text);
3245
+ const { ast: ast2, diagnostics: d } = new Parser(tokens, {
3246
+ registry: this.registry
3247
+ }).parse();
3248
+ parsed = { children: ast2.children, diagnostics: d };
3249
+ this.parseCache.set(text, parsed);
3250
+ }
3251
+ const dOffset = seg.start;
3252
+ const dLine = seg.startLine - 1;
3253
+ resolved = {
3254
+ children: parsed.children.map((n) => shiftClone(n, dOffset, dLine)),
3255
+ diagnostics: parsed.diagnostics.map((d) => shiftClone(d, dOffset, dLine))
3256
+ };
3257
+ }
3258
+ nextPositioned.set(posKey, resolved);
3259
+ if (!first) children.push({ type: "parbreak" });
3260
+ first = false;
3261
+ for (const node of resolved.children) children.push(node);
3262
+ for (const diag of resolved.diagnostics) diagnostics.push(diag);
3263
+ }
3264
+ this.positioned = nextPositioned;
3265
+ const ast = {
3266
+ type: "document",
3267
+ children,
3268
+ range: { start: { offset: 0, line: 1, column: 1 }, end: endPosition(source) }
3269
+ };
3270
+ return {
3271
+ ast,
3272
+ diagnostics,
3273
+ stats: {
3274
+ segments: segments.length,
3275
+ cacheHits: this.hits,
3276
+ cacheMisses: this.misses
3277
+ }
3278
+ };
3279
+ }
3280
+ };
3281
+ function splitBalancedSegments(source) {
3282
+ const raw = [];
3283
+ const re = /\n([ \t]*\n)+/g;
3284
+ let last = 0;
3285
+ let m;
3286
+ while (m = re.exec(source)) {
3287
+ raw.push({ start: last, end: m.index });
3288
+ last = m.index + m[0].length;
3289
+ }
3290
+ raw.push({ start: last, end: source.length });
3291
+ const segments = [];
3292
+ let buffer = null;
3293
+ for (const part of raw) {
3294
+ if (buffer === null) buffer = { start: part.start, end: part.end };
3295
+ else buffer.end = part.end;
3296
+ if (isBalanced(source.slice(buffer.start, buffer.end))) {
3297
+ segments.push({ ...buffer, startLine: lineAt(source, buffer.start) });
3298
+ buffer = null;
3299
+ }
3300
+ }
3301
+ if (buffer !== null) {
3302
+ segments.push({ ...buffer, startLine: lineAt(source, buffer.start) });
3303
+ }
3304
+ return segments;
3305
+ }
3306
+ function isBalanced(text) {
3307
+ let depth = 0;
3308
+ let env = 0;
3309
+ let i = 0;
3310
+ while (i < text.length) {
3311
+ const c = text[i];
3312
+ if (c === "\\") {
3313
+ i++;
3314
+ if (isLetter2(text[i] ?? "")) {
3315
+ let name = "";
3316
+ while (isLetter2(text[i] ?? "")) name += text[i++];
3317
+ if (name === "begin") env++;
3318
+ else if (name === "end") env--;
3319
+ } else {
3320
+ i++;
3321
+ }
3322
+ continue;
3323
+ }
3324
+ if (c === "{" || c === "[") depth++;
3325
+ else if (c === "}" || c === "]") depth--;
3326
+ i++;
3327
+ }
3328
+ return depth === 0 && env === 0;
3329
+ }
3330
+ function shiftClone(value, dOffset, dLine) {
3331
+ if (Array.isArray(value)) {
3332
+ return value.map((v) => shiftClone(v, dOffset, dLine));
3333
+ }
3334
+ if (value && typeof value === "object") {
3335
+ const obj = value;
3336
+ if ("offset" in obj && "line" in obj && "column" in obj) {
3337
+ return {
3338
+ offset: obj.offset + dOffset,
3339
+ line: obj.line + dLine,
3340
+ column: obj.column
3341
+ };
3342
+ }
3343
+ const out = {};
3344
+ for (const [k, v] of Object.entries(obj)) out[k] = shiftClone(v, dOffset, dLine);
3345
+ return out;
3346
+ }
3347
+ return value;
3348
+ }
3349
+ function lineAt(source, offset) {
3350
+ let line = 1;
3351
+ for (let i = 0; i < offset && i < source.length; i++) {
3352
+ if (source[i] === "\n") line++;
3353
+ }
3354
+ return line;
3355
+ }
3356
+ function endPosition(source) {
3357
+ let line = 1;
3358
+ let column = 1;
3359
+ for (let i = 0; i < source.length; i++) {
3360
+ if (source[i] === "\n") {
3361
+ line++;
3362
+ column = 1;
3363
+ } else {
3364
+ column++;
3365
+ }
3366
+ }
3367
+ return { offset: source.length, line, column };
3368
+ }
3369
+
3370
+ exports.BLOCK_TYPES = BLOCK_TYPES;
3371
+ exports.CommandRegistry = CommandRegistry;
3372
+ exports.DiagnosticCode = DiagnosticCode;
3373
+ exports.DiagnosticSeverity = DiagnosticSeverity;
3374
+ exports.EditorService = EditorService;
3375
+ exports.HtmlRenderer = HtmlRenderer;
3376
+ exports.IncrementalCompiler = IncrementalCompiler;
3377
+ exports.Parser = Parser;
3378
+ exports.ReTeXEngine = ReTeXEngine;
3379
+ exports.ReactRenderer = ReactRenderer;
3380
+ exports.SPECIAL_CHARS = SPECIAL_CHARS;
3381
+ exports.TokenType = TokenType;
3382
+ exports.Tokenizer = Tokenizer;
3383
+ exports.badgePlugin = badgePlugin;
3384
+ exports.childrenOf = childrenOf;
3385
+ exports.classicTheme = classicTheme;
3386
+ exports.closestMatch = closestMatch;
3387
+ exports.collect = collect;
3388
+ exports.compactTheme = compactTheme;
3389
+ exports.createDefaultRegistry = createDefaultRegistry;
3390
+ exports.createEngine = createEngine;
3391
+ exports.dateRange = dateRange;
3392
+ exports.defaultTheme = defaultTheme;
3393
+ exports.entryParts = entryParts;
3394
+ exports.escapeAttribute = escapeAttribute;
3395
+ exports.escapeHtml = escapeHtml;
3396
+ exports.flattenText = flattenText;
3397
+ exports.getIcon = getIcon;
3398
+ exports.getTheme = getTheme;
3399
+ exports.hasIcon = hasIcon;
3400
+ exports.iconNames = iconNames;
3401
+ exports.iconToSvg = iconToSvg;
3402
+ exports.isBalanced = isBalanced;
3403
+ exports.isBlockNode = isBlockNode;
3404
+ exports.isNode = isNode;
3405
+ exports.isParent = isParent;
3406
+ exports.isSafeColor = isSafeColor;
3407
+ exports.isSafeDimension = isSafeDimension;
3408
+ exports.levenshtein = levenshtein;
3409
+ exports.mergeRanges = mergeRanges;
3410
+ exports.modernTheme = modernTheme;
3411
+ exports.nodePathAt = nodePathAt;
3412
+ exports.normalizeWhitespace = normalizeWhitespace;
3413
+ exports.pageCss = pageCss;
3414
+ exports.parse = parse;
3415
+ exports.parseKeyValArg = parseKeyValArg;
3416
+ exports.parseListArg = parseListArg;
3417
+ exports.pos = pos;
3418
+ exports.printDocument = printDocument;
3419
+ exports.range = range;
3420
+ exports.rangeContains = rangeContains;
3421
+ exports.ratingPlugin = ratingPlugin;
3422
+ exports.registerIcon = registerIcon;
3423
+ exports.renderHtml = renderHtml;
3424
+ exports.renderHtmlDocument = renderHtmlDocument;
3425
+ exports.renderJson = renderJson;
3426
+ exports.renderPdf = renderPdf;
3427
+ exports.renderPrintHtml = renderPrintHtml;
3428
+ exports.renderReact = renderReact;
3429
+ exports.resolveIconName = resolveIconName;
3430
+ exports.resolveTheme = resolveTheme;
3431
+ exports.sanitizeStyleValue = sanitizeStyleValue;
3432
+ exports.sanitizeUrl = sanitizeUrl;
3433
+ exports.splitBalancedSegments = splitBalancedSegments;
3434
+ exports.splitPreamble = splitPreamble;
3435
+ exports.splitTopLevel = splitTopLevel;
3436
+ exports.themeToCss = themeToCss;
3437
+ exports.themes = themes;
3438
+ exports.toJsonTree = toJsonTree;
3439
+ exports.toRegions = toRegions;
3440
+ exports.tokenize = tokenize;
3441
+ exports.validate = validate;
3442
+ exports.walk = walk;
3443
+ //# sourceMappingURL=index.cjs.map
3444
+ //# sourceMappingURL=index.cjs.map