@regmisatyam/retex 0.1.0 → 0.2.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.js CHANGED
@@ -69,6 +69,8 @@ var DiagnosticCode = /* @__PURE__ */ ((DiagnosticCode2) => {
69
69
  DiagnosticCode2["CommandOutsideContext"] = "RTX4001";
70
70
  DiagnosticCode2["UnknownIcon"] = "RTX4002";
71
71
  DiagnosticCode2["UnknownThemeColor"] = "RTX4003";
72
+ DiagnosticCode2["UnknownLibrary"] = "RTX4004";
73
+ DiagnosticCode2["LibraryRequired"] = "RTX4005";
72
74
  DiagnosticCode2["UnsafeUrlBlocked"] = "RTX5001";
73
75
  return DiagnosticCode2;
74
76
  })(DiagnosticCode || {});
@@ -314,103 +316,6 @@ function parseKeyValArg(raw) {
314
316
  return fields;
315
317
  }
316
318
 
317
- // src/security/sanitize.ts
318
- function escapeHtml(input) {
319
- return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
320
- }
321
- function escapeAttribute(input) {
322
- return escapeHtml(input);
323
- }
324
- var SAFE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:", "ftp:", "sms:"]);
325
- var SCHEME_RE = /^([a-z][a-z0-9+.-]*):/i;
326
- var CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
327
- function sanitizeUrl(input) {
328
- const raw = String(input ?? "").trim();
329
- const cleaned = raw.replace(CONTROL_CHARS_RE, "");
330
- const match = SCHEME_RE.exec(cleaned);
331
- if (!match) {
332
- return { safe: cleaned, blocked: false };
333
- }
334
- const scheme = match[1].toLowerCase() + ":";
335
- if (SAFE_PROTOCOLS.has(scheme)) {
336
- return { safe: cleaned, blocked: false, scheme };
337
- }
338
- return { safe: "#", blocked: true, scheme };
339
- }
340
- function isSafeColor(value) {
341
- const v = value.trim();
342
- if (/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(v)) return true;
343
- if (/^(rgb|rgba|hsl|hsla)\(\s*[0-9.,%\s/]+\)$/i.test(v)) return true;
344
- return CSS_NAMED_COLORS.has(v.toLowerCase());
345
- }
346
- function isSafeDimension(value) {
347
- return /^-?\d*\.?\d+(px|pt|em|rem|ex|ch|vw|vh|vmin|vmax|cm|mm|in|pc|%|fr)?$/.test(
348
- value.trim()
349
- );
350
- }
351
- function sanitizeStyleValue(value) {
352
- const v = value.trim();
353
- if (/[<>;{}]/.test(v) || /expression|url\s*\(|javascript:/i.test(v)) {
354
- return void 0;
355
- }
356
- return v;
357
- }
358
- var CSS_NAMED_COLORS = /* @__PURE__ */ new Set([
359
- "black",
360
- "silver",
361
- "gray",
362
- "grey",
363
- "white",
364
- "maroon",
365
- "red",
366
- "purple",
367
- "fuchsia",
368
- "green",
369
- "lime",
370
- "olive",
371
- "yellow",
372
- "navy",
373
- "blue",
374
- "teal",
375
- "aqua",
376
- "cyan",
377
- "magenta",
378
- "orange",
379
- "pink",
380
- "brown",
381
- "gold",
382
- "indigo",
383
- "violet",
384
- "tan",
385
- "beige",
386
- "ivory",
387
- "coral",
388
- "salmon",
389
- "khaki",
390
- "crimson",
391
- "turquoise",
392
- "lavender",
393
- "plum",
394
- "orchid",
395
- "slateblue",
396
- "slategray",
397
- "steelblue",
398
- "skyblue",
399
- "royalblue",
400
- "midnightblue",
401
- "darkblue",
402
- "darkgreen",
403
- "darkred",
404
- "darkgray",
405
- "darkgrey",
406
- "lightgray",
407
- "lightgrey",
408
- "lightblue",
409
- "transparent",
410
- "currentcolor",
411
- "inherit"
412
- ]);
413
-
414
319
  // src/icons/icons.ts
415
320
  var ICONS = {
416
321
  github: {
@@ -491,10 +396,10 @@ function iconToSvg(name, opts = {}) {
491
396
  const def = getIcon(name);
492
397
  if (!def) return null;
493
398
  const size = opts.size ?? "1em";
494
- const dim = typeof size === "number" ? `${size}` : size;
399
+ const dim2 = typeof size === "number" ? `${size}` : size;
495
400
  const cls = opts.className ? ` class="${opts.className}"` : "";
496
401
  const fillOrStroke = def.stroked ? 'fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"' : 'fill="currentColor"';
497
- return `<svg${cls} width="${dim}" height="${dim}" viewBox="0 0 24 24" ${fillOrStroke} role="img" aria-hidden="true" focusable="false">${def.body}</svg>`;
402
+ return `<svg${cls} width="${dim2}" height="${dim2}" viewBox="0 0 24 24" ${fillOrStroke} role="img" aria-hidden="true" focusable="false">${def.body}</svg>`;
498
403
  }
499
404
 
500
405
  // src/parser/builtins.ts
@@ -506,113 +411,12 @@ var mark = (name, type, summary) => ({
506
411
  example: `\\${name}{...}`,
507
412
  build: ({ args }) => ({ type, children: args[0]?.children ?? [] })
508
413
  });
509
- var scaleSwitch = (name, scale) => ({
510
- name,
511
- category: "switch",
512
- scoped: true,
513
- summary: `Set the font size to "${name}" for the rest of the group.`,
514
- example: `{\\${name} ...}`,
515
- build: ({ scope }) => ({ type: "fontscale", scale, children: scope })
516
- });
517
414
  var TYPOGRAPHY = [
518
415
  mark("textbf", "bold", "Bold text."),
519
416
  mark("textit", "italic", "Italic text."),
520
417
  mark("emph", "italic", "Emphasized (italic) text."),
521
418
  mark("underline", "underline", "Underlined text."),
522
419
  mark("sout", "strike", "Struck-through text."),
523
- {
524
- name: "textcolor",
525
- category: "inline",
526
- args: [
527
- { kind: "string", name: "color", format: "color" },
528
- { kind: "content", name: "content" }
529
- ],
530
- summary: "Color text with a hex/named color.",
531
- example: "\\textcolor{#2563eb}{OpenAI}",
532
- build: ({ args, utils, report }) => {
533
- const color = utils.textOf(args[0]).trim();
534
- if (color && !isSafeColor(color)) {
535
- report({
536
- severity: "warning" /* Warning */,
537
- code: "RTX3001" /* InvalidColor */,
538
- message: `"${color}" is not a recognized color; it will be ignored.`,
539
- range: args[0]?.range ?? ZERO_RANGE
540
- });
541
- }
542
- return {
543
- type: "color",
544
- color: isSafeColor(color) ? color : "",
545
- children: args[1]?.children ?? []
546
- };
547
- }
548
- },
549
- {
550
- name: "fontsize",
551
- category: "inline",
552
- args: [
553
- { kind: "string", name: "size", format: "dimension" },
554
- { kind: "content", name: "content" }
555
- ],
556
- summary: "Set an explicit font size for the wrapped content.",
557
- example: "\\fontsize{14pt}{Custom Size}",
558
- build: ({ args, utils, report }) => {
559
- const size = utils.textOf(args[0]).trim();
560
- if (size && !isSafeDimension(size)) {
561
- report({
562
- severity: "warning" /* Warning */,
563
- code: "RTX3003" /* InvalidDimension */,
564
- message: `"${size}" is not a valid dimension.`,
565
- range: args[0]?.range ?? ZERO_RANGE
566
- });
567
- }
568
- return {
569
- type: "fontsize",
570
- size: isSafeDimension(size) ? size : "1em",
571
- children: args[1]?.children ?? []
572
- };
573
- }
574
- },
575
- {
576
- name: "fontfamily",
577
- category: "inline",
578
- args: [
579
- { kind: "string", name: "family" },
580
- { kind: "content", name: "content" }
581
- ],
582
- summary: "Set the font family for the wrapped content.",
583
- example: "\\fontfamily{Georgia}{Serif text}",
584
- build: ({ args, utils }) => ({
585
- type: "fontfamily",
586
- family: utils.textOf(args[0]).trim() || "inherit",
587
- children: args[1]?.children ?? []
588
- })
589
- },
590
- {
591
- name: "themecolor",
592
- category: "inline",
593
- args: [
594
- { kind: "string", name: "token" },
595
- { kind: "content", name: "content" }
596
- ],
597
- summary: "Color text using a token from the active theme.",
598
- example: "\\themecolor{primary}{Highlighted}",
599
- build: ({ args, utils }) => ({
600
- type: "themecolor",
601
- token: utils.textOf(args[0]).trim() || "primary",
602
- children: args[1]?.children ?? []
603
- })
604
- },
605
- scaleSwitch("small", "small"),
606
- scaleSwitch("large", "large"),
607
- scaleSwitch("Large", "Large"),
608
- scaleSwitch("Huge", "Huge"),
609
- {
610
- name: "normalsize",
611
- category: "switch",
612
- scoped: true,
613
- summary: "Reset the font size to the document base size.",
614
- build: ({ scope }) => ({ type: "fontscale", scale: "normal", children: scope })
615
- },
616
420
  {
617
421
  name: "bfseries",
618
422
  category: "switch",
@@ -626,16 +430,30 @@ var TYPOGRAPHY = [
626
430
  scoped: true,
627
431
  summary: "Switch to italic for the rest of the group.",
628
432
  build: ({ scope }) => ({ type: "italic", children: scope })
433
+ }
434
+ ];
435
+ var DIRECTIVES = [
436
+ {
437
+ name: "usepackage",
438
+ category: "meta",
439
+ args: [{ kind: "list", name: "libraries" }],
440
+ summary: "Import styling libraries (fonts, shapes, colors, modern, \u2026).",
441
+ documentation: "ReTeX is blank by default. `\\usepackage{...}` activates one or more libraries for this document so their commands and styling become available. Multiple libraries can be comma-separated.",
442
+ example: "\\usepackage{shapes, colors}",
443
+ build: ({ args }) => ({
444
+ type: "usepackage",
445
+ names: parseListArg(args[0]?.raw ?? "")
446
+ })
629
447
  },
630
448
  {
631
- name: "ttfamily",
632
- category: "switch",
633
- scoped: true,
634
- summary: "Switch to a monospace font for the rest of the group.",
635
- build: ({ scope }) => ({
636
- type: "fontfamily",
637
- family: "monospace",
638
- children: scope
449
+ name: "use",
450
+ category: "meta",
451
+ args: [{ kind: "list", name: "libraries" }],
452
+ summary: "Alias for \\usepackage \u2014 import styling libraries.",
453
+ example: "\\use{fonts}",
454
+ build: ({ args }) => ({
455
+ type: "usepackage",
456
+ names: parseListArg(args[0]?.raw ?? "")
639
457
  })
640
458
  }
641
459
  ];
@@ -819,42 +637,6 @@ var PRIMITIVES = [
819
637
  summary: "An explicit line break.",
820
638
  build: () => ({ type: "linebreak" })
821
639
  },
822
- {
823
- name: "hrule",
824
- category: "block",
825
- summary: "A horizontal rule / divider.",
826
- build: () => ({ type: "rule" })
827
- },
828
- {
829
- name: "divider",
830
- category: "block",
831
- summary: "A horizontal rule / divider.",
832
- build: () => ({ type: "rule" })
833
- },
834
- {
835
- name: "vspace",
836
- category: "block",
837
- args: [{ kind: "string", name: "size", format: "dimension" }],
838
- summary: "Vertical space.",
839
- example: "\\vspace{1em}",
840
- build: ({ args, utils }) => ({
841
- type: "space",
842
- axis: "vertical",
843
- size: utils.textOf(args[0]).trim() || "1em"
844
- })
845
- },
846
- {
847
- name: "hspace",
848
- category: "inline",
849
- args: [{ kind: "string", name: "size", format: "dimension" }],
850
- summary: "Horizontal space.",
851
- example: "\\hspace{1em}",
852
- build: ({ args, utils }) => ({
853
- type: "space",
854
- axis: "horizontal",
855
- size: utils.textOf(args[0]).trim() || "1em"
856
- })
857
- },
858
640
  // `\item` and `\column` are consumed by their environments; they are
859
641
  // registered so they are recognized (and so the validator can flag misuse
860
642
  // outside the right environment) but have no standalone builder.
@@ -872,6 +654,13 @@ var PRIMITIVES = [
872
654
  example: "\\column{40%}"
873
655
  }
874
656
  ];
657
+ function entryRange(e) {
658
+ const start = e.marker.range?.start;
659
+ if (!start) return void 0;
660
+ let end = e.marker.range?.end;
661
+ for (const n of e.content) if (n.range) end = n.range.end;
662
+ return end ? { start, end } : void 0;
663
+ }
875
664
  var ENVIRONMENTS = [
876
665
  {
877
666
  name: "itemize",
@@ -883,7 +672,11 @@ var ENVIRONMENTS = [
883
672
  type: "list",
884
673
  kind: "itemize",
885
674
  items: (entries ?? []).map(
886
- (e) => ({ type: "item", children: e.content })
675
+ (e) => ({
676
+ type: "item",
677
+ children: e.content,
678
+ range: entryRange(e)
679
+ })
887
680
  )
888
681
  })
889
682
  },
@@ -897,7 +690,11 @@ var ENVIRONMENTS = [
897
690
  type: "list",
898
691
  kind: "enumerate",
899
692
  items: (entries ?? []).map(
900
- (e) => ({ type: "item", children: e.content })
693
+ (e) => ({
694
+ type: "item",
695
+ children: e.content,
696
+ range: entryRange(e)
697
+ })
901
698
  )
902
699
  })
903
700
  },
@@ -911,7 +708,7 @@ var ENVIRONMENTS = [
911
708
  type: "columns",
912
709
  columns: (entries ?? []).map((e) => {
913
710
  const width = utils.textOf(e.marker.args[0]).trim() || "auto";
914
- return { type: "column", width, children: e.content };
711
+ return { type: "column", width, children: e.content, range: entryRange(e) };
915
712
  })
916
713
  })
917
714
  },
@@ -930,6 +727,7 @@ function createDefaultRegistry() {
930
727
  const registry = new CommandRegistry();
931
728
  for (const def of [
932
729
  ...TYPOGRAPHY,
730
+ ...DIRECTIVES,
933
731
  ...LINKS,
934
732
  ...SECTIONS,
935
733
  ...CONTACT,
@@ -1015,25 +813,122 @@ function normalizeWhitespace(s) {
1015
813
  return s.replace(/\s+/g, " ").trim();
1016
814
  }
1017
815
 
1018
- // src/parser/suggest.ts
1019
- function levenshtein(a, b) {
1020
- const m = a.length;
1021
- const n = b.length;
1022
- if (m === 0) return n;
1023
- if (n === 0) return m;
1024
- let prev = new Array(n + 1);
1025
- let curr = new Array(n + 1);
1026
- for (let j = 0; j <= n; j++) prev[j] = j;
1027
- for (let i = 1; i <= m; i++) {
1028
- curr[0] = i;
1029
- for (let j = 1; j <= n; j++) {
1030
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1031
- curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
1032
- }
1033
- [prev, curr] = [curr, prev];
1034
- }
1035
- return prev[n];
1036
- }
816
+ // src/security/sanitize.ts
817
+ function escapeHtml(input) {
818
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
819
+ }
820
+ function escapeAttribute(input) {
821
+ return escapeHtml(input);
822
+ }
823
+ var SAFE_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:", "ftp:", "sms:"]);
824
+ var SCHEME_RE = /^([a-z][a-z0-9+.-]*):/i;
825
+ var CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/g;
826
+ function sanitizeUrl(input) {
827
+ const raw = String(input ?? "").trim();
828
+ const cleaned = raw.replace(CONTROL_CHARS_RE, "");
829
+ const match = SCHEME_RE.exec(cleaned);
830
+ if (!match) {
831
+ return { safe: cleaned, blocked: false };
832
+ }
833
+ const scheme = match[1].toLowerCase() + ":";
834
+ if (SAFE_PROTOCOLS.has(scheme)) {
835
+ return { safe: cleaned, blocked: false, scheme };
836
+ }
837
+ return { safe: "#", blocked: true, scheme };
838
+ }
839
+ function isSafeColor(value) {
840
+ const v = value.trim();
841
+ if (/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(v)) return true;
842
+ if (/^(rgb|rgba|hsl|hsla)\(\s*[0-9.,%\s/]+\)$/i.test(v)) return true;
843
+ return CSS_NAMED_COLORS.has(v.toLowerCase());
844
+ }
845
+ function isSafeDimension(value) {
846
+ return /^-?\d*\.?\d+(px|pt|em|rem|ex|ch|vw|vh|vmin|vmax|cm|mm|in|pc|%|fr)?$/.test(
847
+ value.trim()
848
+ );
849
+ }
850
+ function sanitizeStyleValue(value) {
851
+ const v = value.trim();
852
+ if (/[<>;{}]/.test(v) || /expression|url\s*\(|javascript:/i.test(v)) {
853
+ return void 0;
854
+ }
855
+ return v;
856
+ }
857
+ var CSS_NAMED_COLORS = /* @__PURE__ */ new Set([
858
+ "black",
859
+ "silver",
860
+ "gray",
861
+ "grey",
862
+ "white",
863
+ "maroon",
864
+ "red",
865
+ "purple",
866
+ "fuchsia",
867
+ "green",
868
+ "lime",
869
+ "olive",
870
+ "yellow",
871
+ "navy",
872
+ "blue",
873
+ "teal",
874
+ "aqua",
875
+ "cyan",
876
+ "magenta",
877
+ "orange",
878
+ "pink",
879
+ "brown",
880
+ "gold",
881
+ "indigo",
882
+ "violet",
883
+ "tan",
884
+ "beige",
885
+ "ivory",
886
+ "coral",
887
+ "salmon",
888
+ "khaki",
889
+ "crimson",
890
+ "turquoise",
891
+ "lavender",
892
+ "plum",
893
+ "orchid",
894
+ "slateblue",
895
+ "slategray",
896
+ "steelblue",
897
+ "skyblue",
898
+ "royalblue",
899
+ "midnightblue",
900
+ "darkblue",
901
+ "darkgreen",
902
+ "darkred",
903
+ "darkgray",
904
+ "darkgrey",
905
+ "lightgray",
906
+ "lightgrey",
907
+ "lightblue",
908
+ "transparent",
909
+ "currentcolor",
910
+ "inherit"
911
+ ]);
912
+
913
+ // src/parser/suggest.ts
914
+ function levenshtein(a, b) {
915
+ const m = a.length;
916
+ const n = b.length;
917
+ if (m === 0) return n;
918
+ if (n === 0) return m;
919
+ let prev = new Array(n + 1);
920
+ let curr = new Array(n + 1);
921
+ for (let j = 0; j <= n; j++) prev[j] = j;
922
+ for (let i = 1; i <= m; i++) {
923
+ curr[0] = i;
924
+ for (let j = 1; j <= n; j++) {
925
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
926
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
927
+ }
928
+ [prev, curr] = [curr, prev];
929
+ }
930
+ return prev[n];
931
+ }
1037
932
  function closestMatch(name, candidates) {
1038
933
  let best;
1039
934
  let bestDist = Infinity;
@@ -1504,231 +1399,688 @@ var Parser = class _Parser {
1504
1399
  this.advance();
1505
1400
  }
1506
1401
  }
1507
- skipTrivia() {
1508
- while (this.at("Whitespace" /* Whitespace */) || this.at("Comment" /* Comment */) || this.at("ParBreak" /* ParBreak */)) {
1509
- this.advance();
1402
+ skipTrivia() {
1403
+ while (this.at("Whitespace" /* Whitespace */) || this.at("Comment" /* Comment */) || this.at("ParBreak" /* ParBreak */)) {
1404
+ this.advance();
1405
+ }
1406
+ }
1407
+ peek(k = 0) {
1408
+ return this.tokens[this.pos + k] ?? this.tokens[this.tokens.length - 1];
1409
+ }
1410
+ advance() {
1411
+ const t = this.peek();
1412
+ if (this.pos < this.tokens.length - 1) this.pos++;
1413
+ return t;
1414
+ }
1415
+ previousEnd() {
1416
+ return this.tokens[this.pos - 1]?.range.end ?? this.peek().range.start;
1417
+ }
1418
+ report(d) {
1419
+ this.diagnostics.push({ ...d, source: "parser" });
1420
+ }
1421
+ };
1422
+ function parse(tokens, options) {
1423
+ return Parser.parse(tokens, options);
1424
+ }
1425
+
1426
+ // src/validator/validator.ts
1427
+ var ZERO_RANGE3 = {
1428
+ start: { offset: 0, line: 1, column: 1 },
1429
+ end: { offset: 0, line: 1, column: 1 }
1430
+ };
1431
+ function validate(ast, options = {}) {
1432
+ const diagnostics = [];
1433
+ const report = (d) => {
1434
+ diagnostics.push({ ...d, source: "validator" });
1435
+ };
1436
+ walk(ast, {
1437
+ enter(node) {
1438
+ switch (node.type) {
1439
+ case "command":
1440
+ checkStrayCommand(node, report);
1441
+ break;
1442
+ case "section":
1443
+ if (node.title.trim() === "") {
1444
+ report({
1445
+ severity: "warning" /* Warning */,
1446
+ code: "RTX3006" /* EmptyArgument */,
1447
+ message: "Section heading is empty.",
1448
+ range: node.range ?? ZERO_RANGE3
1449
+ });
1450
+ }
1451
+ break;
1452
+ case "contact":
1453
+ if (node.value.trim() === "") {
1454
+ report({
1455
+ severity: "warning" /* Warning */,
1456
+ code: "RTX3006" /* EmptyArgument */,
1457
+ message: `\\${node.field} is empty.`,
1458
+ range: node.range ?? ZERO_RANGE3
1459
+ });
1460
+ }
1461
+ break;
1462
+ case "themecolor":
1463
+ if (options.theme?.colors && !(node.token in options.theme.colors)) {
1464
+ report({
1465
+ severity: "warning" /* Warning */,
1466
+ code: "RTX4003" /* UnknownThemeColor */,
1467
+ message: `Theme color "${node.token}" is not defined in theme "${options.theme.name}".`,
1468
+ range: node.range ?? ZERO_RANGE3
1469
+ });
1470
+ }
1471
+ break;
1472
+ case "skills":
1473
+ if (node.items.length === 0) {
1474
+ report({
1475
+ severity: "info" /* Info */,
1476
+ code: "RTX3006" /* EmptyArgument */,
1477
+ message: "\\skills has no entries.",
1478
+ range: node.range ?? ZERO_RANGE3
1479
+ });
1480
+ }
1481
+ break;
1482
+ }
1483
+ }
1484
+ });
1485
+ return diagnostics;
1486
+ }
1487
+ function checkStrayCommand(node, report) {
1488
+ if (node.name === "item") {
1489
+ report({
1490
+ severity: "error" /* Error */,
1491
+ code: "RTX4001" /* CommandOutsideContext */,
1492
+ message: "\\item must appear inside an itemize or enumerate environment.",
1493
+ range: node.range ?? ZERO_RANGE3
1494
+ });
1495
+ } else if (node.name === "column") {
1496
+ report({
1497
+ severity: "error" /* Error */,
1498
+ code: "RTX4001" /* CommandOutsideContext */,
1499
+ message: "\\column must appear inside a columns environment.",
1500
+ range: node.range ?? ZERO_RANGE3
1501
+ });
1502
+ }
1503
+ }
1504
+
1505
+ // src/theme/default.ts
1506
+ var blankTheme = {
1507
+ name: "blank"
1508
+ };
1509
+ var defaultTheme = blankTheme;
1510
+
1511
+ // src/theme/themes.ts
1512
+ function resolveTheme(partial, base = defaultTheme) {
1513
+ return {
1514
+ name: partial?.name ?? base.name,
1515
+ colors: { ...base.colors, ...partial?.colors },
1516
+ fonts: { ...base.fonts, ...partial?.fonts },
1517
+ fontSizes: { ...base.fontSizes, ...partial?.fontSizes },
1518
+ spacing: { ...base.spacing, ...partial?.spacing },
1519
+ page: { ...base.page, ...partial?.page },
1520
+ sectionStyle: partial?.sectionStyle ?? base.sectionStyle,
1521
+ headings: { ...base.headings, ...partial?.headings }
1522
+ };
1523
+ }
1524
+ var styledBase = {
1525
+ fonts: {
1526
+ heading: '"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif',
1527
+ body: '"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif',
1528
+ mono: '"JetBrains Mono", "SF Mono", "Fira Code", Consolas, monospace'
1529
+ },
1530
+ fontSizes: {
1531
+ base: "10.5pt",
1532
+ small: "9pt",
1533
+ large: "12pt",
1534
+ Large: "15pt",
1535
+ Huge: "22pt",
1536
+ name: "26pt",
1537
+ section: "13pt"
1538
+ },
1539
+ spacing: { unit: "4px", section: "1.1rem", item: "0.28rem", page: "0.55in" },
1540
+ page: { size: "Letter", margin: "0.55in", maxWidth: "8.5in" },
1541
+ headings: { transform: "uppercase", tracking: "0.06em", nameTracking: "-0.01em" }
1542
+ };
1543
+ var modernTheme = resolveTheme({
1544
+ ...styledBase,
1545
+ name: "modern",
1546
+ colors: {
1547
+ primary: "#7c3aed",
1548
+ secondary: "#64748b",
1549
+ text: "#111827",
1550
+ muted: "#64748b",
1551
+ background: "#ffffff",
1552
+ border: "#e2e8f0",
1553
+ accent: "#7c3aed"
1554
+ },
1555
+ sectionStyle: "underline"
1556
+ });
1557
+ var classicTheme = resolveTheme({
1558
+ ...styledBase,
1559
+ name: "classic",
1560
+ colors: {
1561
+ primary: "#111827",
1562
+ secondary: "#4b5563",
1563
+ text: "#1e293b",
1564
+ muted: "#4b5563",
1565
+ background: "#ffffff",
1566
+ border: "#cbd5e1",
1567
+ accent: "#111827"
1568
+ },
1569
+ fonts: {
1570
+ heading: 'Georgia, "Times New Roman", serif',
1571
+ body: 'Georgia, "Times New Roman", serif',
1572
+ mono: 'Consolas, "Courier New", monospace'
1573
+ },
1574
+ sectionStyle: "rule"
1575
+ });
1576
+ var compactTheme = resolveTheme({
1577
+ ...styledBase,
1578
+ name: "compact",
1579
+ colors: {
1580
+ primary: "#2563eb",
1581
+ secondary: "#64748b",
1582
+ text: "#1e293b",
1583
+ muted: "#64748b",
1584
+ background: "#ffffff",
1585
+ border: "#e2e8f0",
1586
+ accent: "#2563eb"
1587
+ },
1588
+ fontSizes: {
1589
+ base: "9.5pt",
1590
+ small: "8pt",
1591
+ large: "11pt",
1592
+ Large: "13pt",
1593
+ Huge: "18pt",
1594
+ name: "20pt",
1595
+ section: "11pt"
1596
+ },
1597
+ spacing: { unit: "3px", section: "0.7rem", item: "0.18rem", page: "0.4in" },
1598
+ sectionStyle: "bar"
1599
+ });
1600
+ var themes = {
1601
+ blank: blankTheme,
1602
+ default: blankTheme,
1603
+ modern: modernTheme,
1604
+ classic: classicTheme,
1605
+ compact: compactTheme
1606
+ };
1607
+ function getTheme(name) {
1608
+ return themes[name] ?? defaultTheme;
1609
+ }
1610
+
1611
+ // src/library/colors.ts
1612
+ var zero2 = { offset: 0, line: 1, column: 1 };
1613
+ var ZERO_RANGE4 = { start: zero2, end: zero2 };
1614
+ var colorCommands = [
1615
+ {
1616
+ name: "textcolor",
1617
+ category: "inline",
1618
+ args: [
1619
+ { kind: "string", name: "color", format: "color" },
1620
+ { kind: "content", name: "content" }
1621
+ ],
1622
+ summary: "Color text with a hex/named color.",
1623
+ example: "\\textcolor{#2563eb}{OpenAI}",
1624
+ build: ({ args, utils, report }) => {
1625
+ const color2 = utils.textOf(args[0]).trim();
1626
+ if (color2 && !isSafeColor(color2)) {
1627
+ report({
1628
+ severity: "warning" /* Warning */,
1629
+ code: "RTX3001" /* InvalidColor */,
1630
+ message: `"${color2}" is not a recognized color; it will be ignored.`,
1631
+ range: args[0]?.range ?? ZERO_RANGE4
1632
+ });
1633
+ }
1634
+ return {
1635
+ type: "color",
1636
+ color: isSafeColor(color2) ? color2 : "",
1637
+ children: args[1]?.children ?? []
1638
+ };
1639
+ }
1640
+ },
1641
+ {
1642
+ name: "themecolor",
1643
+ category: "inline",
1644
+ args: [
1645
+ { kind: "string", name: "token" },
1646
+ { kind: "content", name: "content" }
1647
+ ],
1648
+ summary: "Color text using a token from the active theme.",
1649
+ example: "\\themecolor{primary}{Highlighted}",
1650
+ build: ({ args, utils }) => ({
1651
+ type: "themecolor",
1652
+ token: utils.textOf(args[0]).trim() || "primary",
1653
+ children: args[1]?.children ?? []
1654
+ })
1655
+ }
1656
+ ];
1657
+ var palettes = {
1658
+ slate: {
1659
+ primary: "#0f172a",
1660
+ secondary: "#475569",
1661
+ text: "#1e293b",
1662
+ muted: "#64748b",
1663
+ border: "#cbd5e1",
1664
+ accent: "#0f172a"
1665
+ },
1666
+ blue: {
1667
+ primary: "#2563eb",
1668
+ secondary: "#64748b",
1669
+ text: "#1e293b",
1670
+ muted: "#64748b",
1671
+ border: "#e2e8f0",
1672
+ accent: "#2563eb"
1673
+ },
1674
+ violet: {
1675
+ primary: "#7c3aed",
1676
+ secondary: "#6b7280",
1677
+ text: "#111827",
1678
+ muted: "#6b7280",
1679
+ border: "#e5e7eb",
1680
+ accent: "#7c3aed"
1681
+ },
1682
+ emerald: {
1683
+ primary: "#059669",
1684
+ secondary: "#6b7280",
1685
+ text: "#111827",
1686
+ muted: "#6b7280",
1687
+ border: "#d1fae5",
1688
+ accent: "#059669"
1689
+ },
1690
+ rose: {
1691
+ primary: "#e11d48",
1692
+ secondary: "#6b7280",
1693
+ text: "#111827",
1694
+ muted: "#6b7280",
1695
+ border: "#fecdd3",
1696
+ accent: "#e11d48"
1697
+ },
1698
+ amber: {
1699
+ primary: "#d97706",
1700
+ secondary: "#6b7280",
1701
+ text: "#111827",
1702
+ muted: "#6b7280",
1703
+ border: "#fde68a",
1704
+ accent: "#d97706"
1705
+ }
1706
+ };
1707
+ var colorsLibrary = {
1708
+ name: "colors",
1709
+ summary: "Text color (\\textcolor, \\themecolor) and named palettes.",
1710
+ commands: colorCommands
1711
+ };
1712
+
1713
+ // src/library/fonts.ts
1714
+ var zero3 = { offset: 0, line: 1, column: 1 };
1715
+ var ZERO_RANGE5 = { start: zero3, end: zero3 };
1716
+ var scaleSwitch = (name, scale) => ({
1717
+ name,
1718
+ category: "switch",
1719
+ scoped: true,
1720
+ summary: `Set the font size to "${name}" for the rest of the group.`,
1721
+ example: `{\\${name} ...}`,
1722
+ build: ({ scope }) => ({ type: "fontscale", scale, children: scope })
1723
+ });
1724
+ var fontCommands = [
1725
+ {
1726
+ name: "fontsize",
1727
+ category: "inline",
1728
+ args: [
1729
+ { kind: "string", name: "size", format: "dimension" },
1730
+ { kind: "content", name: "content" }
1731
+ ],
1732
+ summary: "Set an explicit font size for the wrapped content.",
1733
+ example: "\\fontsize{14pt}{Custom Size}",
1734
+ build: ({ args, utils, report }) => {
1735
+ const size = utils.textOf(args[0]).trim();
1736
+ if (size && !isSafeDimension(size)) {
1737
+ report({
1738
+ severity: "warning" /* Warning */,
1739
+ code: "RTX3003" /* InvalidDimension */,
1740
+ message: `"${size}" is not a valid dimension.`,
1741
+ range: args[0]?.range ?? ZERO_RANGE5
1742
+ });
1743
+ }
1744
+ return {
1745
+ type: "fontsize",
1746
+ size: isSafeDimension(size) ? size : "1em",
1747
+ children: args[1]?.children ?? []
1748
+ };
1749
+ }
1750
+ },
1751
+ {
1752
+ name: "fontfamily",
1753
+ category: "inline",
1754
+ args: [
1755
+ { kind: "string", name: "family" },
1756
+ { kind: "content", name: "content" }
1757
+ ],
1758
+ summary: "Set the font family for the wrapped content.",
1759
+ example: "\\fontfamily{Georgia}{Serif text}",
1760
+ build: ({ args, utils }) => ({
1761
+ type: "fontfamily",
1762
+ family: utils.textOf(args[0]).trim() || "inherit",
1763
+ children: args[1]?.children ?? []
1764
+ })
1765
+ },
1766
+ scaleSwitch("small", "small"),
1767
+ scaleSwitch("large", "large"),
1768
+ scaleSwitch("Large", "Large"),
1769
+ scaleSwitch("Huge", "Huge"),
1770
+ {
1771
+ name: "normalsize",
1772
+ category: "switch",
1773
+ scoped: true,
1774
+ summary: "Reset the font size to the document base size.",
1775
+ build: ({ scope }) => ({ type: "fontscale", scale: "normal", children: scope })
1776
+ },
1777
+ {
1778
+ name: "ttfamily",
1779
+ category: "switch",
1780
+ scoped: true,
1781
+ summary: "Switch to a monospace font for the rest of the group.",
1782
+ build: ({ scope }) => ({
1783
+ type: "fontfamily",
1784
+ family: "monospace",
1785
+ children: scope
1786
+ })
1787
+ }
1788
+ ];
1789
+ var fontStacks = {
1790
+ inter: '"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif',
1791
+ system: 'system-ui, -apple-system, "Segoe UI", Roboto, Arial, sans-serif',
1792
+ helvetica: '"Helvetica Neue", Helvetica, Arial, sans-serif',
1793
+ georgia: 'Georgia, "Times New Roman", Times, serif',
1794
+ times: '"Times New Roman", Times, serif',
1795
+ garamond: '"EB Garamond", Garamond, Georgia, serif',
1796
+ spaceGrotesk: '"Space Grotesk", system-ui, sans-serif',
1797
+ sourceSerif: '"Source Serif 4", Georgia, serif',
1798
+ jetbrainsMono: '"JetBrains Mono", "SF Mono", "Fira Code", Consolas, monospace',
1799
+ firaCode: '"Fira Code", "JetBrains Mono", Consolas, monospace',
1800
+ ibmPlexMono: '"IBM Plex Mono", "SF Mono", Consolas, monospace'
1801
+ };
1802
+ function fontTheme(spec) {
1803
+ const resolve = (v) => v == null ? void 0 : fontStacks[v] ?? v;
1804
+ return {
1805
+ heading: resolve(spec.heading),
1806
+ body: resolve(spec.body),
1807
+ mono: resolve(spec.mono)
1808
+ };
1809
+ }
1810
+ var fontsLibrary = {
1811
+ name: "fonts",
1812
+ summary: "Font family & size control (\\fontfamily, \\fontsize, \\small \u2026 \\Huge).",
1813
+ commands: fontCommands
1814
+ };
1815
+
1816
+ // src/library/shapes.ts
1817
+ var rule = (name, style, summary) => ({
1818
+ name,
1819
+ category: "block",
1820
+ summary,
1821
+ example: `\\${name}`,
1822
+ build: () => style ? { type: "rule", style } : { type: "rule" }
1823
+ });
1824
+ var shapeCommands = [
1825
+ rule("hrule", "solid", "A horizontal rule / divider."),
1826
+ rule("divider", "solid", "A horizontal rule / divider."),
1827
+ rule("dashrule", "dashed", "A dashed horizontal rule."),
1828
+ rule("dotrule", "dotted", "A dotted horizontal rule."),
1829
+ rule("thickrule", "thick", "A thick horizontal rule."),
1830
+ rule("doublerule", "double", "A double horizontal rule."),
1831
+ {
1832
+ name: "vspace",
1833
+ category: "block",
1834
+ args: [{ kind: "string", name: "size", format: "dimension" }],
1835
+ summary: "Vertical space.",
1836
+ example: "\\vspace{1em}",
1837
+ build: ({ args, utils }) => ({
1838
+ type: "space",
1839
+ axis: "vertical",
1840
+ size: utils.textOf(args[0]).trim() || "1em"
1841
+ })
1842
+ },
1843
+ {
1844
+ name: "hspace",
1845
+ category: "inline",
1846
+ args: [{ kind: "string", name: "size", format: "dimension" }],
1847
+ summary: "Horizontal space.",
1848
+ example: "\\hspace{1em}",
1849
+ build: ({ args, utils }) => ({
1850
+ type: "space",
1851
+ axis: "horizontal",
1852
+ size: utils.textOf(args[0]).trim() || "1em"
1853
+ })
1854
+ }
1855
+ ];
1856
+ var shapesLibrary = {
1857
+ name: "shapes",
1858
+ summary: "Horizontal rules / dividers (\\hrule, \\dashrule, \u2026) and spacing.",
1859
+ commands: shapeCommands
1860
+ };
1861
+
1862
+ // src/library/themes.ts
1863
+ var stylingCommands = [...colorCommands, ...fontCommands, ...shapeCommands];
1864
+ var modernLibrary = {
1865
+ name: "modern",
1866
+ summary: "A modern, accent-forward theme (violet, underlined headings).",
1867
+ theme: modernTheme,
1868
+ commands: stylingCommands
1869
+ };
1870
+ var classicLibrary = {
1871
+ name: "classic",
1872
+ summary: "A classic serif theme with ruled section headings.",
1873
+ theme: classicTheme,
1874
+ commands: stylingCommands
1875
+ };
1876
+ var compactLibrary = {
1877
+ name: "compact",
1878
+ summary: "A compact, single-accent theme maximizing content density.",
1879
+ theme: compactTheme,
1880
+ commands: stylingCommands
1881
+ };
1882
+
1883
+ // src/library/registry.ts
1884
+ var builtinLibraries = {
1885
+ colors: colorsLibrary,
1886
+ fonts: fontsLibrary,
1887
+ shapes: shapesLibrary,
1888
+ modern: modernLibrary,
1889
+ classic: classicLibrary,
1890
+ compact: compactLibrary
1891
+ };
1892
+ var ALIASES2 = {
1893
+ color: "colors",
1894
+ font: "fonts",
1895
+ shape: "shapes"
1896
+ };
1897
+ function resolveLibraryName(name) {
1898
+ const key = name.trim().toLowerCase();
1899
+ if (builtinLibraries[key]) return key;
1900
+ if (ALIASES2[key]) return ALIASES2[key];
1901
+ return void 0;
1902
+ }
1903
+ function getBuiltinLibrary(name) {
1904
+ const key = resolveLibraryName(name);
1905
+ return key ? builtinLibraries[key] : void 0;
1906
+ }
1907
+ function builtinLibraryNames() {
1908
+ return Object.keys(builtinLibraries);
1909
+ }
1910
+ var GATED_COMMANDS = (() => {
1911
+ const map = {};
1912
+ const add = (lib) => {
1913
+ for (const c of lib.commands ?? []) map[c.name] = lib.name;
1914
+ };
1915
+ add(colorsLibrary);
1916
+ add(fontsLibrary);
1917
+ add(shapesLibrary);
1918
+ return map;
1919
+ })();
1920
+ var dedupe = (names) => [...new Set(names)];
1921
+ function usedLibraryNamesFromTokens(tokens) {
1922
+ const names = [];
1923
+ for (let i = 0; i < tokens.length; i++) {
1924
+ const t = tokens[i];
1925
+ if (t.type !== "Command" /* Command */ || t.value !== "usepackage" && t.value !== "use") {
1926
+ continue;
1927
+ }
1928
+ let j = i + 1;
1929
+ while (tokens[j] && (tokens[j].type === "Whitespace" /* Whitespace */ || tokens[j].type === "Comment" /* Comment */)) {
1930
+ j++;
1931
+ }
1932
+ if (!tokens[j] || tokens[j].type !== "LBrace" /* LBrace */) continue;
1933
+ j++;
1934
+ let raw = "";
1935
+ let depth = 0;
1936
+ while (tokens[j] && !(depth === 0 && tokens[j].type === "RBrace" /* RBrace */)) {
1937
+ const tj = tokens[j];
1938
+ if (tj.type === "LBrace" /* LBrace */) depth++;
1939
+ else if (tj.type === "RBrace" /* RBrace */) depth--;
1940
+ raw += tj.type === "Command" /* Command */ ? `\\${tj.value}` : tj.value;
1941
+ j++;
1942
+ }
1943
+ for (const part of raw.split(",")) {
1944
+ const name = part.trim();
1945
+ if (name) names.push(name);
1946
+ }
1947
+ i = j;
1948
+ }
1949
+ return dedupe(names);
1950
+ }
1951
+ function usedLibraryNamesFromAst(root) {
1952
+ const names = [];
1953
+ for (const node of collect(root, (n) => n.type === "usepackage")) {
1954
+ for (const name of node.names) {
1955
+ const trimmed = name.trim();
1956
+ if (trimmed) names.push(trimmed);
1510
1957
  }
1511
1958
  }
1512
- peek(k = 0) {
1513
- return this.tokens[this.pos + k] ?? this.tokens[this.tokens.length - 1];
1514
- }
1515
- advance() {
1516
- const t = this.peek();
1517
- if (this.pos < this.tokens.length - 1) this.pos++;
1518
- return t;
1519
- }
1520
- previousEnd() {
1521
- return this.tokens[this.pos - 1]?.range.end ?? this.peek().range.start;
1959
+ return dedupe(names);
1960
+ }
1961
+ function applyLibraries(base, names, lookup = getBuiltinLibrary) {
1962
+ const registry = base.clone();
1963
+ const libraries = [];
1964
+ const missing = [];
1965
+ for (const name of names) {
1966
+ const lib = lookup(name);
1967
+ if (!lib) {
1968
+ missing.push(name);
1969
+ continue;
1970
+ }
1971
+ libraries.push(lib);
1972
+ for (const cmd of lib.commands ?? []) registry.registerCommand(cmd);
1973
+ for (const env of lib.environments ?? []) registry.registerEnvironment(env);
1522
1974
  }
1523
- report(d) {
1524
- this.diagnostics.push({ ...d, source: "parser" });
1975
+ return { registry, libraries, missing };
1976
+ }
1977
+ function mergeLibraryTheme(base, libraries) {
1978
+ let theme = base;
1979
+ for (const lib of libraries) {
1980
+ if (lib.theme) theme = resolveTheme(lib.theme, theme);
1525
1981
  }
1526
- };
1527
- function parse(tokens, options) {
1528
- return Parser.parse(tokens, options);
1982
+ return theme;
1529
1983
  }
1530
-
1531
- // src/validator/validator.ts
1532
- var ZERO_RANGE3 = {
1984
+ var ZERO_RANGE6 = {
1533
1985
  start: { offset: 0, line: 1, column: 1 },
1534
1986
  end: { offset: 0, line: 1, column: 1 }
1535
1987
  };
1536
- function validate(ast, options = {}) {
1537
- const diagnostics = [];
1538
- const report = (d) => {
1539
- diagnostics.push({ ...d, source: "validator" });
1540
- };
1541
- walk(ast, {
1542
- enter(node) {
1543
- switch (node.type) {
1544
- case "command":
1545
- checkStrayCommand(node, report);
1546
- break;
1547
- case "section":
1548
- if (node.title.trim() === "") {
1549
- report({
1550
- severity: "warning" /* Warning */,
1551
- code: "RTX3006" /* EmptyArgument */,
1552
- message: "Section heading is empty.",
1553
- range: node.range ?? ZERO_RANGE3
1554
- });
1555
- }
1556
- break;
1557
- case "contact":
1558
- if (node.value.trim() === "") {
1559
- report({
1560
- severity: "warning" /* Warning */,
1561
- code: "RTX3006" /* EmptyArgument */,
1562
- message: `\\${node.field} is empty.`,
1563
- range: node.range ?? ZERO_RANGE3
1564
- });
1565
- }
1566
- break;
1567
- case "themecolor":
1568
- if (options.theme && !(node.token in options.theme.colors)) {
1569
- report({
1570
- severity: "warning" /* Warning */,
1571
- code: "RTX4003" /* UnknownThemeColor */,
1572
- message: `Theme color "${node.token}" is not defined in theme "${options.theme.name}".`,
1573
- range: node.range ?? ZERO_RANGE3
1574
- });
1575
- }
1576
- break;
1577
- case "skills":
1578
- if (node.items.length === 0) {
1579
- report({
1580
- severity: "info" /* Info */,
1581
- code: "RTX3006" /* EmptyArgument */,
1582
- message: "\\skills has no entries.",
1583
- range: node.range ?? ZERO_RANGE3
1584
- });
1585
- }
1586
- break;
1587
- }
1588
- }
1589
- });
1590
- return diagnostics;
1591
- }
1592
- function checkStrayCommand(node, report) {
1593
- if (node.name === "item") {
1594
- report({
1595
- severity: "error" /* Error */,
1596
- code: "RTX4001" /* CommandOutsideContext */,
1597
- message: "\\item must appear inside an itemize or enumerate environment.",
1598
- range: node.range ?? ZERO_RANGE3
1599
- });
1600
- } else if (node.name === "column") {
1601
- report({
1602
- severity: "error" /* Error */,
1603
- code: "RTX4001" /* CommandOutsideContext */,
1604
- message: "\\column must appear inside a columns environment.",
1605
- range: node.range ?? ZERO_RANGE3
1988
+ function libraryDiagnostics(ast, diagnostics, lookup = getBuiltinLibrary) {
1989
+ const gated = collect(
1990
+ ast,
1991
+ (n) => n.type === "command" && n.name in GATED_COMMANDS
1992
+ );
1993
+ const gatedOffsets = new Set(
1994
+ gated.map((n) => n.range?.start.offset).filter((o) => o != null)
1995
+ );
1996
+ const out = diagnostics.filter(
1997
+ (d) => !(d.code === "RTX2001" /* UnknownCommand */ && gatedOffsets.has(d.range.start.offset))
1998
+ );
1999
+ for (const node of gated) {
2000
+ const lib = GATED_COMMANDS[node.name];
2001
+ out.push({
2002
+ source: "parser",
2003
+ severity: "warning" /* Warning */,
2004
+ code: "RTX4005" /* LibraryRequired */,
2005
+ message: `\\${node.name} requires the "${lib}" library. Add \\usepackage{${lib}} to the preamble (or engine.use(${lib}Library)).`,
2006
+ range: node.range ?? ZERO_RANGE6
1606
2007
  });
1607
2008
  }
2009
+ for (const node of collect(ast, (n) => n.type === "usepackage")) {
2010
+ for (const name of node.names) {
2011
+ if (lookup(name)) continue;
2012
+ out.push({
2013
+ source: "parser",
2014
+ severity: "warning" /* Warning */,
2015
+ code: "RTX4004" /* UnknownLibrary */,
2016
+ message: `Unknown library "${name}". Available: ${builtinLibraryNames().join(", ")}.`,
2017
+ range: node.range ?? ZERO_RANGE6
2018
+ });
2019
+ }
2020
+ }
2021
+ return out;
1608
2022
  }
1609
-
1610
- // src/theme/default.ts
1611
- var defaultTheme = {
1612
- name: "default",
1613
- colors: {
1614
- primary: "#2563eb",
1615
- secondary: "#64748b",
1616
- text: "#1e293b",
1617
- muted: "#64748b",
1618
- background: "#ffffff",
1619
- border: "#e2e8f0",
1620
- accent: "#2563eb",
1621
- success: "#16a34a"
1622
- },
1623
- fonts: {
1624
- heading: '"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif',
1625
- body: '"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif',
1626
- mono: '"JetBrains Mono", "SF Mono", "Fira Code", Consolas, monospace'
1627
- },
1628
- fontSizes: {
1629
- base: "10.5pt",
1630
- small: "9pt",
1631
- large: "12pt",
1632
- Large: "15pt",
1633
- Huge: "22pt",
1634
- name: "26pt",
1635
- section: "13pt"
1636
- },
1637
- spacing: {
1638
- unit: "4px",
1639
- section: "1.1rem",
1640
- item: "0.28rem",
1641
- page: "0.55in"
1642
- },
1643
- page: {
1644
- size: "Letter",
1645
- margin: "0.55in",
1646
- maxWidth: "8.5in"
1647
- },
1648
- sectionStyle: "rule"
1649
- };
1650
-
1651
- // src/theme/themes.ts
1652
- function resolveTheme(partial, base = defaultTheme) {
1653
- if (!partial) return base;
1654
- return {
1655
- name: partial.name ?? base.name,
1656
- colors: { ...base.colors, ...partial.colors },
1657
- fonts: { ...base.fonts, ...partial.fonts },
1658
- fontSizes: { ...base.fontSizes, ...partial.fontSizes },
1659
- spacing: { ...base.spacing, ...partial.spacing },
1660
- page: { ...base.page, ...partial.page },
1661
- sectionStyle: partial.sectionStyle ?? base.sectionStyle
1662
- };
1663
- }
1664
- var modernTheme = resolveTheme({
1665
- name: "modern",
1666
- colors: { primary: "#7c3aed", accent: "#7c3aed", text: "#111827" },
1667
- sectionStyle: "underline"
1668
- });
1669
- var classicTheme = resolveTheme({
1670
- name: "classic",
1671
- colors: { primary: "#111827", secondary: "#4b5563", accent: "#111827" },
1672
- fonts: {
1673
- heading: 'Georgia, "Times New Roman", serif',
1674
- body: 'Georgia, "Times New Roman", serif',
1675
- mono: 'Consolas, "Courier New", monospace'
1676
- },
1677
- sectionStyle: "rule"
1678
- });
1679
- var compactTheme = resolveTheme({
1680
- name: "compact",
1681
- fontSizes: {
1682
- base: "9.5pt",
1683
- small: "8pt",
1684
- large: "11pt",
1685
- Large: "13pt",
1686
- Huge: "18pt",
1687
- name: "20pt",
1688
- section: "11pt"
1689
- },
1690
- spacing: { unit: "3px", section: "0.7rem", item: "0.18rem", page: "0.4in" },
1691
- sectionStyle: "bar"
1692
- });
1693
- var themes = {
1694
- default: defaultTheme,
1695
- modern: modernTheme,
1696
- classic: classicTheme,
1697
- compact: compactTheme
1698
- };
1699
- function getTheme(name) {
1700
- return themes[name] ?? defaultTheme;
2023
+ function collectLibraryOverrides(libraries) {
2024
+ const html = /* @__PURE__ */ new Map();
2025
+ const react = /* @__PURE__ */ new Map();
2026
+ for (const lib of libraries) {
2027
+ for (const cmd of lib.commands ?? []) {
2028
+ if (cmd.render?.html) html.set(`command:${cmd.name}`, cmd.render.html);
2029
+ if (cmd.render?.react) react.set(`command:${cmd.name}`, cmd.render.react);
2030
+ }
2031
+ for (const [k, fn] of Object.entries(lib.htmlRenderers ?? {})) html.set(k, fn);
2032
+ for (const [k, fn] of Object.entries(lib.reactRenderers ?? {})) react.set(k, fn);
2033
+ }
2034
+ return { html, react };
1701
2035
  }
1702
2036
 
1703
2037
  // src/renderers/css.ts
1704
2038
  var tokenSafe = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "");
2039
+ var has = (v) => v != null && v !== "";
2040
+ var color = (v, fallback) => has(v) && isSafeColor(v) ? v : fallback;
2041
+ var dim = (v, fallback) => has(v) && isSafeDimension(v) ? v : fallback;
2042
+ var val = (v, fallback) => has(v) ? sanitizeStyleValue(v) ?? fallback : fallback;
1705
2043
  function themeToCss(theme, prefix = "retex") {
1706
2044
  const p = prefix;
1707
- const colorVars = Object.entries(theme.colors).map(([k, v]) => ` --${p}-color-${tokenSafe(k)}: ${v};`).join("\n");
2045
+ const c = theme.colors ?? {};
2046
+ const f = theme.fonts ?? {};
2047
+ const fs = theme.fontSizes ?? {};
2048
+ const sp = theme.spacing ?? {};
2049
+ const h = theme.headings ?? {};
2050
+ const colorVars = Object.entries(c).filter(([, v]) => has(v) && isSafeColor(v)).map(([k, v]) => ` --${p}-color-${tokenSafe(k)}: ${v};`).join("\n");
2051
+ const primary = color(c.primary, "");
2052
+ const skillBg = primary ? `color-mix(in srgb, ${primary} 12%, transparent)` : "transparent";
2053
+ const skillColor = primary || "inherit";
2054
+ const linkDecoration = primary ? "none" : "underline";
1708
2055
  return `.${p}-resume {
1709
- ${colorVars}
1710
- --${p}-primary: ${theme.colors.primary};
1711
- --${p}-text: ${theme.colors.text};
1712
- --${p}-muted: ${theme.colors.muted};
1713
- --${p}-border: ${theme.colors.border};
1714
- --${p}-bg: ${theme.colors.background};
1715
- --${p}-font-heading: ${theme.fonts.heading};
1716
- --${p}-font-body: ${theme.fonts.body};
1717
- --${p}-font-mono: ${theme.fonts.mono};
1718
- --${p}-fs-base: ${theme.fontSizes.base};
1719
- --${p}-fs-small: ${theme.fontSizes.small};
1720
- --${p}-fs-large: ${theme.fontSizes.large};
1721
- --${p}-fs-Large: ${theme.fontSizes.Large};
1722
- --${p}-fs-Huge: ${theme.fontSizes.Huge};
1723
- --${p}-fs-name: ${theme.fontSizes.name};
1724
- --${p}-fs-section: ${theme.fontSizes.section};
1725
- --${p}-sp-section: ${theme.spacing.section};
1726
- --${p}-sp-item: ${theme.spacing.item};
2056
+ ${colorVars ? colorVars + "\n" : ""} --${p}-primary: ${primary || "inherit"};
2057
+ --${p}-text: ${color(c.text, "inherit")};
2058
+ --${p}-muted: ${color(c.muted, "inherit")};
2059
+ --${p}-border: ${color(c.border, "currentColor")};
2060
+ --${p}-bg: ${color(c.background, "transparent")};
2061
+ --${p}-font-heading: ${val(f.heading, "inherit")};
2062
+ --${p}-font-body: ${val(f.body, "inherit")};
2063
+ --${p}-font-mono: ${val(f.mono, "ui-monospace, SFMono-Regular, Menlo, monospace")};
2064
+ --${p}-fs-base: ${dim(fs.base, "inherit")};
2065
+ --${p}-fs-small: ${dim(fs.small, "0.85em")};
2066
+ --${p}-fs-large: ${dim(fs.large, "1.2em")};
2067
+ --${p}-fs-Large: ${dim(fs.Large, "1.45em")};
2068
+ --${p}-fs-Huge: ${dim(fs.Huge, "2em")};
2069
+ --${p}-fs-name: ${dim(fs.name, "2em")};
2070
+ --${p}-fs-section: ${dim(fs.section, "1.25em")};
2071
+ --${p}-sp-section: ${dim(sp.section, "1.1rem")};
2072
+ --${p}-sp-item: ${dim(sp.item, "0.28rem")};
2073
+ --${p}-section-transform: ${val(h.transform, "none")};
2074
+ --${p}-section-tracking: ${val(h.tracking, "normal")};
2075
+ --${p}-name-tracking: ${val(h.nameTracking, "normal")};
2076
+ --${p}-link-decoration: ${linkDecoration};
2077
+ --${p}-skill-bg: ${skillBg};
2078
+ --${p}-skill-color: ${skillColor};
1727
2079
 
1728
2080
  box-sizing: border-box;
1729
- max-width: ${theme.page.maxWidth};
2081
+ max-width: ${dim(theme.page?.maxWidth, "none")};
1730
2082
  margin: 0 auto;
1731
- padding: ${theme.spacing.page};
2083
+ padding: ${dim(sp.page, "0")};
1732
2084
  background: var(--${p}-bg);
1733
2085
  color: var(--${p}-text);
1734
2086
  font-family: var(--${p}-font-body);
@@ -1744,7 +2096,7 @@ ${colorVars}
1744
2096
  font-weight: 700;
1745
2097
  margin: 0 0 0.1em;
1746
2098
  color: var(--${p}-text);
1747
- letter-spacing: -0.01em;
2099
+ letter-spacing: var(--${p}-name-tracking);
1748
2100
  }
1749
2101
  .${p}-title {
1750
2102
  font-size: var(--${p}-fs-large);
@@ -1773,8 +2125,8 @@ ${colorVars}
1773
2125
  font-family: var(--${p}-font-heading);
1774
2126
  font-size: var(--${p}-fs-section);
1775
2127
  font-weight: 700;
1776
- text-transform: uppercase;
1777
- letter-spacing: 0.06em;
2128
+ text-transform: var(--${p}-section-transform);
2129
+ letter-spacing: var(--${p}-section-tracking);
1778
2130
  color: var(--${p}-primary);
1779
2131
  margin: 0 0 0.5em;
1780
2132
  }
@@ -1817,8 +2169,8 @@ ${colorVars}
1817
2169
 
1818
2170
  .${p}-skills { display: flex; flex-wrap: wrap; gap: 0.4rem; list-style: none; margin: 0.2rem 0; padding: 0; }
1819
2171
  .${p}-skill {
1820
- background: color-mix(in srgb, var(--${p}-primary) 12%, transparent);
1821
- color: var(--${p}-primary);
2172
+ background: var(--${p}-skill-bg);
2173
+ color: var(--${p}-skill-color);
1822
2174
  border-radius: 4px;
1823
2175
  padding: 0.12rem 0.5rem;
1824
2176
  font-size: var(--${p}-fs-small);
@@ -1826,10 +2178,14 @@ ${colorVars}
1826
2178
  white-space: nowrap;
1827
2179
  }
1828
2180
 
1829
- .${p}-link { color: var(--${p}-primary); text-decoration: none; }
2181
+ .${p}-link { color: var(--${p}-primary); text-decoration: var(--${p}-link-decoration); }
1830
2182
  .${p}-link:hover { text-decoration: underline; }
1831
2183
  .${p}-icon { display: inline-flex; vertical-align: -0.125em; }
1832
2184
  .${p}-rule { border: none; border-top: 1px solid var(--${p}-border); margin: 0.6rem 0; }
2185
+ .${p}-rule--dashed { border-top-style: dashed; }
2186
+ .${p}-rule--dotted { border-top-style: dotted; }
2187
+ .${p}-rule--thick { border-top-width: 3px; }
2188
+ .${p}-rule--double { border-top-style: double; border-top-width: 3px; }
1833
2189
  .${p}-center { text-align: center; }
1834
2190
  .${p}-scale-small { font-size: var(--${p}-fs-small); }
1835
2191
  .${p}-scale-large { font-size: var(--${p}-fs-large); }
@@ -1887,7 +2243,7 @@ function splitPreamble(nodes) {
1887
2243
  (n) => CONTACTISH.has(n.type) && n !== name && n !== title
1888
2244
  );
1889
2245
  const other = nodes.filter(
1890
- (n) => !CONTACTISH.has(n.type) && n.type !== "parbreak" && !(n.type === "text" && n.value.trim() === "")
2246
+ (n) => !CONTACTISH.has(n.type) && n.type !== "parbreak" && n.type !== "usepackage" && !(n.type === "text" && n.value.trim() === "")
1891
2247
  );
1892
2248
  return { name, title, contacts, other };
1893
2249
  }
@@ -1922,10 +2278,11 @@ function dateRange(start, end, date) {
1922
2278
  var tokenSafe2 = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "");
1923
2279
  var HtmlRenderer = class {
1924
2280
  constructor(options = {}) {
1925
- this.theme = options.theme ?? resolveTheme();
2281
+ this.theme = resolveTheme(options.theme);
1926
2282
  this.prefix = options.classPrefix ?? "retex";
1927
2283
  this.overrides = options.overrides ?? /* @__PURE__ */ new Map();
1928
2284
  this.useHeader = options.header ?? true;
2285
+ this.sourceMap = options.sourceMap ?? false;
1929
2286
  this.ctx = {
1930
2287
  theme: this.theme,
1931
2288
  classPrefix: this.prefix,
@@ -1973,8 +2330,8 @@ ${this.styles()}
1973
2330
  }
1974
2331
  /* --------------------------- structuring ---------------------------- */
1975
2332
  renderSection(section, body) {
1976
- const styleMod = ` ${this.cls("section")}--${this.theme.sectionStyle}`;
1977
- 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>`;
2333
+ const styleMod = this.theme.sectionStyle ? ` ${this.cls("section")}--${this.theme.sectionStyle}` : "";
2334
+ return `<section class="${this.cls("section")}${styleMod}"${this.src(section)}><h2 class="${this.cls("section-title")}"${this.src(section, "section-title")}>${escapeHtml(section.title)}</h2><div class="${this.cls("section-body")}">${this.renderFlow(body)}</div></section>`;
1978
2335
  }
1979
2336
  renderPreamble(nodes) {
1980
2337
  if (!this.useHeader) return this.renderFlow(nodes);
@@ -1983,8 +2340,10 @@ ${this.styles()}
1983
2340
  let html = "";
1984
2341
  if (hasHeader) {
1985
2342
  html += `<header class="${this.cls("header")}">`;
1986
- if (name) html += `<h1 class="${this.cls("name")}">${escapeHtml(name.value)}</h1>`;
1987
- if (title) html += `<p class="${this.cls("title")}">${escapeHtml(title.value)}</p>`;
2343
+ if (name)
2344
+ html += `<h1 class="${this.cls("name")}"${this.src(name)}>${escapeHtml(name.value)}</h1>`;
2345
+ if (title)
2346
+ html += `<p class="${this.cls("title")}"${this.src(title)}>${escapeHtml(title.value)}</p>`;
1988
2347
  if (contacts.length > 0) {
1989
2348
  html += `<div class="${this.cls("contact")}">${contacts.map((c) => this.renderContactItem(c)).join("")}</div>`;
1990
2349
  }
@@ -1995,35 +2354,37 @@ ${this.styles()}
1995
2354
  }
1996
2355
  renderContactItem(node) {
1997
2356
  const item = this.cls("contact-item");
2357
+ const src = this.src(node);
1998
2358
  if (node.type === "contact") {
1999
2359
  switch (node.field) {
2000
2360
  case "email":
2001
- return this.contactLink(`mailto:${node.value}`, "email", node.value, item);
2361
+ return this.contactLink(`mailto:${node.value}`, "email", node.value, item, src);
2002
2362
  case "phone":
2003
2363
  return this.contactLink(
2004
2364
  `tel:${node.value.replace(/[^\d+]/g, "")}`,
2005
2365
  "phone",
2006
2366
  node.value,
2007
- item
2367
+ item,
2368
+ src
2008
2369
  );
2009
2370
  case "website":
2010
- return this.contactLink(node.value, "website", node.value, item);
2371
+ return this.contactLink(node.value, "website", node.value, item, src);
2011
2372
  case "location":
2012
- return `<span class="${item}">${this.icon("location")}${escapeHtml(node.value)}</span>`;
2373
+ return `<span class="${item}"${src}>${this.icon("location")}${escapeHtml(node.value)}</span>`;
2013
2374
  default:
2014
- return `<span class="${item}">${escapeHtml(node.value)}</span>`;
2375
+ return `<span class="${item}"${src}>${escapeHtml(node.value)}</span>`;
2015
2376
  }
2016
2377
  }
2017
2378
  if (node.type === "icon")
2018
- return `<span class="${item}">${this.renderNode(node)}</span>`;
2379
+ return `<span class="${item}"${src}>${this.renderNode(node)}</span>`;
2019
2380
  if (node.type === "link" || node.type === "url") {
2020
- return `<span class="${item}">${this.renderNode(node)}</span>`;
2381
+ return `<span class="${item}"${src}>${this.renderNode(node)}</span>`;
2021
2382
  }
2022
2383
  return this.renderNode(node);
2023
2384
  }
2024
- contactLink(url, icon, label, cls) {
2385
+ contactLink(url, icon, label, cls, src = "") {
2025
2386
  const { safe } = sanitizeUrl(url);
2026
- return `<a class="${cls}" href="${escapeAttribute(safe)}">${this.icon(icon)}${escapeHtml(label)}</a>`;
2387
+ return `<a class="${cls}"${src} href="${escapeAttribute(safe)}">${this.icon(icon)}${escapeHtml(label)}</a>`;
2027
2388
  }
2028
2389
  /* ----------------------------- flow / inline ------------------------ */
2029
2390
  /** Block-aware rendering: groups inline runs into `<p>`, renders blocks as-is. */
@@ -2032,7 +2393,10 @@ ${this.styles()}
2032
2393
  let inline = [];
2033
2394
  const flush = () => {
2034
2395
  const html = this.renderInline(inline).trim();
2035
- if (html) out.push(`<p class="${this.cls("para")}">${html}</p>`);
2396
+ if (html)
2397
+ out.push(
2398
+ `<p class="${this.cls("para")}"${this.srcSpan(inline, "para")}>${html}</p>`
2399
+ );
2036
2400
  inline = [];
2037
2401
  };
2038
2402
  for (const node of nodes) {
@@ -2060,68 +2424,76 @@ ${this.styles()}
2060
2424
  return escapeHtml(node.value);
2061
2425
  case "parbreak":
2062
2426
  return "";
2427
+ case "usepackage":
2428
+ return "";
2063
2429
  case "linebreak":
2064
2430
  return "<br>";
2065
- case "rule":
2066
- return `<hr class="${this.cls("rule")}">`;
2431
+ case "rule": {
2432
+ const variant = node.style && node.style !== "solid" ? ` ${this.cls(`rule--${node.style}`)}` : "";
2433
+ return `<hr class="${this.cls("rule")}${variant}"${this.src(node)}>`;
2434
+ }
2067
2435
  case "space":
2068
- return node.axis === "vertical" ? `<div style="height:${this.dim(node.size)}"></div>` : `<span style="display:inline-block;width:${this.dim(node.size)}"></span>`;
2436
+ return node.axis === "vertical" ? `<div style="height:${this.dim(node.size)}"${this.src(node)}></div>` : `<span style="display:inline-block;width:${this.dim(node.size)}"${this.src(node)}></span>`;
2069
2437
  case "group":
2070
2438
  return this.renderInline(node.children);
2071
2439
  case "bold":
2072
- return `<strong>${this.renderInline(node.children)}</strong>`;
2440
+ return `<strong${this.src(node)}>${this.renderInline(node.children)}</strong>`;
2073
2441
  case "italic":
2074
- return `<em>${this.renderInline(node.children)}</em>`;
2442
+ return `<em${this.src(node)}>${this.renderInline(node.children)}</em>`;
2075
2443
  case "underline":
2076
- return `<u>${this.renderInline(node.children)}</u>`;
2444
+ return `<u${this.src(node)}>${this.renderInline(node.children)}</u>`;
2077
2445
  case "strike":
2078
- return `<s>${this.renderInline(node.children)}</s>`;
2446
+ return `<s${this.src(node)}>${this.renderInline(node.children)}</s>`;
2079
2447
  case "color": {
2080
2448
  const inner = this.renderInline(node.children);
2081
2449
  if (!node.color || !isSafeColor(node.color)) return inner;
2082
- return `<span style="color:${node.color}">${inner}</span>`;
2450
+ return `<span style="color:${node.color}"${this.src(node)}>${inner}</span>`;
2083
2451
  }
2084
2452
  case "themecolor": {
2085
2453
  const inner = this.renderInline(node.children);
2086
2454
  const tok = tokenSafe2(node.token);
2087
- return `<span style="color:var(--${this.prefix}-color-${tok}, currentColor)">${inner}</span>`;
2455
+ return `<span style="color:var(--${this.prefix}-color-${tok}, currentColor)"${this.src(node)}>${inner}</span>`;
2088
2456
  }
2089
2457
  case "fontsize": {
2090
2458
  const inner = this.renderInline(node.children);
2091
2459
  if (!isSafeDimension(node.size)) return inner;
2092
- return `<span style="font-size:${node.size}">${inner}</span>`;
2460
+ return `<span style="font-size:${node.size}"${this.src(node)}>${inner}</span>`;
2093
2461
  }
2094
2462
  case "fontfamily": {
2095
2463
  const inner = this.renderInline(node.children);
2096
2464
  const family = node.family === "monospace" ? `var(--${this.prefix}-font-mono)` : sanitizeStyleValue(node.family);
2097
2465
  if (!family) return inner;
2098
- return `<span style="font-family:${family}">${inner}</span>`;
2466
+ return `<span style="font-family:${family}"${this.src(node)}>${inner}</span>`;
2099
2467
  }
2100
2468
  case "fontscale": {
2101
2469
  const inner = this.renderInline(node.children);
2102
- if (node.scale === "normal") return `<span>${inner}</span>`;
2103
- return `<span class="${this.cls(`scale-${node.scale}`)}">${inner}</span>`;
2470
+ if (node.scale === "normal") return `<span${this.src(node)}>${inner}</span>`;
2471
+ return `<span class="${this.cls(`scale-${node.scale}`)}"${this.src(node)}>${inner}</span>`;
2104
2472
  }
2105
2473
  case "link":
2106
- return this.renderLink(node.href, this.renderInline(node.children));
2474
+ return this.renderLink(
2475
+ node.href,
2476
+ this.renderInline(node.children),
2477
+ this.src(node)
2478
+ );
2107
2479
  case "url":
2108
- return this.renderLink(node.href, escapeHtml(node.rawHref));
2480
+ return this.renderLink(node.href, escapeHtml(node.rawHref), this.src(node));
2109
2481
  case "icon":
2110
2482
  return this.icon(node.name);
2111
2483
  case "section":
2112
- return `<h${node.level + 1} class="${this.cls("subsection-title")}">${escapeHtml(node.title)}</h${node.level + 1}>`;
2484
+ return `<h${node.level + 1} class="${this.cls("subsection-title")}"${this.src(node)}>${escapeHtml(node.title)}</h${node.level + 1}>`;
2113
2485
  case "list":
2114
2486
  return this.renderList(node);
2115
2487
  case "columns":
2116
2488
  return this.renderColumns(node);
2117
2489
  case "skills":
2118
- return this.renderSkills(node.items);
2490
+ return this.renderSkills(node);
2119
2491
  case "job":
2120
- return this.renderEntry(node.fields, node.children, "job");
2492
+ return this.renderEntry(node, "job");
2121
2493
  case "education":
2122
- return this.renderEntry(node.fields, node.children, "education");
2494
+ return this.renderEntry(node, "education");
2123
2495
  case "project":
2124
- return this.renderEntry(node.fields, node.children, "project");
2496
+ return this.renderEntry(node, "project");
2125
2497
  case "contact":
2126
2498
  return this.renderContactItem(node);
2127
2499
  case "command":
@@ -2132,33 +2504,36 @@ ${this.styles()}
2132
2504
  return "";
2133
2505
  }
2134
2506
  }
2135
- renderLink(href, label) {
2507
+ renderLink(href, label, src = "") {
2136
2508
  const { safe } = sanitizeUrl(href);
2137
2509
  const external = /^https?:/i.test(safe);
2138
2510
  const rel = external ? ' target="_blank" rel="noopener noreferrer"' : "";
2139
- return `<a class="${this.cls("link")}" href="${escapeAttribute(safe)}"${rel}>${label}</a>`;
2511
+ return `<a class="${this.cls("link")}"${src} href="${escapeAttribute(safe)}"${rel}>${label}</a>`;
2140
2512
  }
2141
2513
  renderList(node) {
2142
2514
  const tag = node.kind === "enumerate" ? "ol" : "ul";
2143
- const items = node.items.map((item) => `<li>${this.renderInline(item.children).trim()}</li>`).join("");
2144
- return `<${tag} class="${this.cls("list")}">${items}</${tag}>`;
2515
+ const items = node.items.map(
2516
+ (item) => `<li${this.src(item)}>${this.renderInline(item.children).trim()}</li>`
2517
+ ).join("");
2518
+ return `<${tag} class="${this.cls("list")}"${this.src(node)}>${items}</${tag}>`;
2145
2519
  }
2146
2520
  renderColumns(node) {
2147
2521
  const cols = node.columns.map((col) => {
2148
2522
  const basis = isSafeDimension(col.width) ? col.width : "auto";
2149
2523
  const flex = basis === "auto" ? "flex:1 1 0" : `flex:0 0 ${basis}`;
2150
- return `<div class="${this.cls("column")}" style="${flex}">${this.renderFlow(col.children)}</div>`;
2524
+ return `<div class="${this.cls("column")}" style="${flex}"${this.src(col)}>${this.renderFlow(col.children)}</div>`;
2151
2525
  }).join("");
2152
- return `<div class="${this.cls("columns")}">${cols}</div>`;
2526
+ return `<div class="${this.cls("columns")}"${this.src(node)}>${cols}</div>`;
2153
2527
  }
2154
- renderSkills(items) {
2155
- const lis = items.map((s) => `<li class="${this.cls("skill")}">${escapeHtml(s)}</li>`).join("");
2156
- return `<ul class="${this.cls("skills")}">${lis}</ul>`;
2528
+ renderSkills(node) {
2529
+ const lis = node.items.map((s) => `<li class="${this.cls("skill")}">${escapeHtml(s)}</li>`).join("");
2530
+ return `<ul class="${this.cls("skills")}"${this.src(node)}>${lis}</ul>`;
2157
2531
  }
2158
- renderEntry(fields, body, kind) {
2532
+ renderEntry(node, kind) {
2533
+ const { fields, children: body } = node;
2159
2534
  const { title, subtitle, dates, location, url } = entryParts(fields, kind);
2160
2535
  const titleHtml = url ? this.renderLink(sanitizeUrl(url).safe, escapeHtml(title)) : escapeHtml(title);
2161
- let html = `<div class="${this.cls("entry")} ${this.cls(kind)}">`;
2536
+ let html = `<div class="${this.cls("entry")} ${this.cls(kind)}"${this.src(node)}>`;
2162
2537
  html += `<div class="${this.cls("entry-row")}">`;
2163
2538
  html += `<span class="${this.cls("entry-title")}">${titleHtml}</span>`;
2164
2539
  if (dates)
@@ -2180,12 +2555,38 @@ ${this.styles()}
2180
2555
  renderCommand(node) {
2181
2556
  if (node.name === "center") {
2182
2557
  const inner2 = node.args.flatMap((a) => a.children);
2183
- return `<div class="${this.cls("center")}">${this.renderFlow(inner2)}</div>`;
2558
+ return `<div class="${this.cls("center")}"${this.src(node)}>${this.renderFlow(inner2)}</div>`;
2184
2559
  }
2185
2560
  const inner = node.args.flatMap((a) => a.children);
2186
2561
  return inner.length > 0 ? this.renderInline(inner) : "";
2187
2562
  }
2188
2563
  /* ------------------------------ helpers ----------------------------- */
2564
+ /**
2565
+ * Source-position attributes for a node — `data-rtx-pos="start:end"` plus a
2566
+ * `data-rtx-type`. Returns an empty string unless `sourceMap` is enabled and
2567
+ * the node carries a range. Powers the live-preview two-way sync: a client
2568
+ * reads these to map a clicked element back to the exact source offsets (and,
2569
+ * in reverse, to find the element for a caret offset).
2570
+ */
2571
+ src(node, type) {
2572
+ if (!this.sourceMap || !node?.range) return "";
2573
+ const kind = type ?? node.type;
2574
+ const kindAttr = kind ? ` data-rtx-type="${kind}"` : "";
2575
+ return ` data-rtx-pos="${node.range.start.offset}:${node.range.end.offset}"${kindAttr}`;
2576
+ }
2577
+ /** Like {@link src}, but spans a run of inline nodes (merges their ranges). */
2578
+ srcSpan(nodes, type) {
2579
+ if (!this.sourceMap) return "";
2580
+ let start = Infinity;
2581
+ let end = -Infinity;
2582
+ for (const n of nodes) {
2583
+ if (!n.range) continue;
2584
+ if (n.range.start.offset < start) start = n.range.start.offset;
2585
+ if (n.range.end.offset > end) end = n.range.end.offset;
2586
+ }
2587
+ if (end < 0) return "";
2588
+ return ` data-rtx-pos="${start}:${end}" data-rtx-type="${type}"`;
2589
+ }
2189
2590
  icon(name) {
2190
2591
  const svg = iconToSvg(name, { className: this.cls("icon") });
2191
2592
  if (svg) return svg;
@@ -2227,8 +2628,12 @@ function renderJson(ast, opts = {}) {
2227
2628
  }
2228
2629
 
2229
2630
  // src/renderers/pdf.ts
2631
+ var pageSize = (v) => v && sanitizeStyleValue(v) || "Letter";
2632
+ var pageMargin = (v) => v && isSafeDimension(v) ? v : "0.5in";
2230
2633
  function pageCss(theme) {
2231
- return `@page { size: ${theme.page.size}; margin: ${theme.page.margin}; }
2634
+ const size = pageSize(theme.page?.size);
2635
+ const margin = pageMargin(theme.page?.margin);
2636
+ return `@page { size: ${size}; margin: ${margin}; }
2232
2637
  html, body { margin: 0; padding: 0; background: #fff; }
2233
2638
  @media screen { body { background: #f1f5f9; padding: 24px; } .retex-resume { box-shadow: 0 1px 8px rgba(0,0,0,.12); } }`;
2234
2639
  }
@@ -2263,16 +2668,13 @@ async function renderPdf(ast, options = {}) {
2263
2668
  try {
2264
2669
  const page = await browser.newPage();
2265
2670
  await page.setContent(html, { waitUntil: "networkidle0" });
2671
+ const size = pageSize(theme.page?.size);
2672
+ const margin = pageMargin(theme.page?.margin);
2266
2673
  return await page.pdf({
2267
2674
  printBackground: options.printBackground ?? true,
2268
2675
  preferCSSPageSize: true,
2269
- format: theme.page.size,
2270
- margin: {
2271
- top: theme.page.margin,
2272
- bottom: theme.page.margin,
2273
- left: theme.page.margin,
2274
- right: theme.page.margin
2275
- }
2676
+ format: size,
2677
+ margin: { top: margin, bottom: margin, left: margin, right: margin }
2276
2678
  });
2277
2679
  } finally {
2278
2680
  await browser.close();
@@ -2302,7 +2704,7 @@ var ReactRenderer = class {
2302
2704
  "ReactRenderer requires a `createElement` factory (e.g. React.createElement)."
2303
2705
  );
2304
2706
  }
2305
- this.theme = options.theme ?? resolveTheme();
2707
+ this.theme = resolveTheme(options.theme);
2306
2708
  this.prefix = options.classPrefix ?? "retex";
2307
2709
  this.h = options.createElement;
2308
2710
  this.Fragment = options.Fragment ?? "div";
@@ -2338,10 +2740,11 @@ var ReactRenderer = class {
2338
2740
  }
2339
2741
  /* --------------------------- structuring ---------------------------- */
2340
2742
  renderSection(section, body) {
2743
+ const className = this.theme.sectionStyle ? `${this.cls("section")} ${this.cls("section")}--${this.theme.sectionStyle}` : this.cls("section");
2341
2744
  return this.el(
2342
2745
  "section",
2343
2746
  {
2344
- className: `${this.cls("section")} ${this.cls("section")}--${this.theme.sectionStyle}`
2747
+ className
2345
2748
  },
2346
2749
  [
2347
2750
  this.el("h2", { className: this.cls("section-title") }, [section.title]),
@@ -2407,10 +2810,14 @@ var ReactRenderer = class {
2407
2810
  return node.value;
2408
2811
  case "parbreak":
2409
2812
  return null;
2813
+ case "usepackage":
2814
+ return null;
2410
2815
  case "linebreak":
2411
2816
  return this.el("br", null, []);
2412
- case "rule":
2413
- return this.el("hr", { className: this.cls("rule") }, []);
2817
+ case "rule": {
2818
+ const variant = node.style && node.style !== "solid" ? ` ${this.cls(`rule--${node.style}`)}` : "";
2819
+ return this.el("hr", { className: `${this.cls("rule")}${variant}` }, []);
2820
+ }
2414
2821
  case "space":
2415
2822
  return node.axis === "vertical" ? this.el("div", { style: { height: this.dim(node.size) } }, []) : this.el(
2416
2823
  "span",
@@ -2624,17 +3031,20 @@ function renderReact(ast, options) {
2624
3031
  }
2625
3032
 
2626
3033
  // src/engine.ts
2627
- var isTheme = (t) => !!t && "colors" in t && "fonts" in t && "fontSizes" in t && "page" in t;
3034
+ var isFullTheme = (t) => typeof t?.name === "string" && t.name.length > 0;
2628
3035
  var ReTeXEngine = class {
2629
3036
  constructor(options = {}) {
2630
3037
  this.htmlOverrides = /* @__PURE__ */ new Map();
2631
3038
  this.reactOverrides = /* @__PURE__ */ new Map();
3039
+ /** Libraries available to `\usepackage` (built-ins are resolved separately). */
3040
+ this.customLibraries = /* @__PURE__ */ new Map();
2632
3041
  this.cache = /* @__PURE__ */ new Map();
2633
3042
  this.generation = 0;
2634
3043
  this.registry = createDefaultRegistry();
2635
- this.theme = isTheme(options.theme) ? options.theme : resolveTheme(options.theme);
3044
+ this.theme = resolveTheme(options.theme);
2636
3045
  this.prefix = options.classPrefix ?? "retex";
2637
3046
  this.cacheSize = options.cacheSize ?? 64;
3047
+ for (const lib of options.libraries ?? []) this.provideLibrary(lib);
2638
3048
  for (const plugin of options.plugins ?? []) this.use(plugin);
2639
3049
  }
2640
3050
  /* ----------------------------- plugins ------------------------------ */
@@ -2651,10 +3061,21 @@ var ReTeXEngine = class {
2651
3061
  this.registerReactRenderer(key, fn);
2652
3062
  }
2653
3063
  if (plugin.theme) this.setTheme(plugin.theme);
3064
+ if (plugin.name) this.customLibraries.set(plugin.name.trim().toLowerCase(), plugin);
2654
3065
  plugin.setup?.(this);
2655
3066
  this.invalidate();
2656
3067
  return this;
2657
3068
  }
3069
+ /**
3070
+ * Register a library so it can be activated with `\usepackage{name}`, without
3071
+ * installing it globally. Built-in libraries are always available; use this
3072
+ * for custom ones.
3073
+ */
3074
+ provideLibrary(library) {
3075
+ this.customLibraries.set(library.name.trim().toLowerCase(), library);
3076
+ this.invalidate();
3077
+ return this;
3078
+ }
2658
3079
  registerCommand(def) {
2659
3080
  const { render, ...spec } = def;
2660
3081
  this.registry.registerCommand(spec);
@@ -2682,7 +3103,7 @@ var ReTeXEngine = class {
2682
3103
  }
2683
3104
  /* ----------------------------- theming ------------------------------ */
2684
3105
  setTheme(theme) {
2685
- this.theme = isTheme(theme) ? theme : resolveTheme(theme, this.theme);
3106
+ this.theme = isFullTheme(theme) ? resolveTheme(theme, defaultTheme) : resolveTheme(theme, this.theme);
2686
3107
  this.invalidate();
2687
3108
  return this;
2688
3109
  }
@@ -2703,11 +3124,22 @@ var ReTeXEngine = class {
2703
3124
  const cached = this.cache.get(key);
2704
3125
  if (cached) return cached;
2705
3126
  const { tokens, diagnostics: lexDiags } = tokenize(source);
2706
- const { ast, diagnostics: parseDiags } = new Parser(tokens, {
2707
- registry: this.registry
2708
- }).parse();
2709
- const diagnostics = [...lexDiags, ...parseDiags];
2710
- if (runValidate) diagnostics.push(...validate(ast, { theme: this.theme }));
3127
+ const usedNames = usedLibraryNamesFromTokens(tokens);
3128
+ const { registry, libraries } = applyLibraries(
3129
+ this.registry,
3130
+ usedNames,
3131
+ (n) => this.lookupLibrary(n)
3132
+ );
3133
+ const { ast, diagnostics: parseDiags } = new Parser(tokens, { registry }).parse();
3134
+ let diagnostics = libraryDiagnostics(
3135
+ ast,
3136
+ [...lexDiags, ...parseDiags],
3137
+ (n) => this.lookupLibrary(n)
3138
+ );
3139
+ if (runValidate) {
3140
+ const theme = mergeLibraryTheme(this.theme, libraries);
3141
+ diagnostics.push(...validate(ast, { theme }));
3142
+ }
2711
3143
  const result = { source, tokens, ast, diagnostics };
2712
3144
  this.remember(key, result);
2713
3145
  return result;
@@ -2717,54 +3149,97 @@ var ReTeXEngine = class {
2717
3149
  }
2718
3150
  validate(input) {
2719
3151
  if (typeof input === "string") return this.compile(input).diagnostics;
2720
- return validate(input, { theme: this.theme });
3152
+ return validate(input, { theme: this.bundle(input).theme });
2721
3153
  }
2722
3154
  /* ----------------------------- renderers ---------------------------- */
2723
3155
  toHtml(input, options = {}) {
2724
- return this.htmlRenderer(options).render(this.astOf(input));
3156
+ const b = this.bundle(input);
3157
+ return new HtmlRenderer({
3158
+ theme: b.theme,
3159
+ classPrefix: this.prefix,
3160
+ overrides: b.htmlOverrides,
3161
+ ...options
3162
+ }).render(b.ast);
2725
3163
  }
2726
3164
  toHtmlDocument(input, options = {}) {
2727
- return this.htmlRenderer(options).renderDocument(this.astOf(input), options.title);
3165
+ const b = this.bundle(input);
3166
+ return new HtmlRenderer({
3167
+ theme: b.theme,
3168
+ classPrefix: this.prefix,
3169
+ overrides: b.htmlOverrides,
3170
+ ...options
3171
+ }).renderDocument(b.ast, options.title);
2728
3172
  }
2729
3173
  toReact(input, options) {
3174
+ const b = this.bundle(input);
2730
3175
  return new ReactRenderer({
2731
3176
  ...options,
2732
- theme: this.theme,
3177
+ theme: b.theme,
2733
3178
  classPrefix: this.prefix,
2734
- overrides: this.reactOverrides
2735
- }).render(this.astOf(input));
3179
+ overrides: b.reactOverrides
3180
+ }).render(b.ast);
2736
3181
  }
2737
3182
  toJson(input, options) {
2738
3183
  return renderJson(this.astOf(input), options);
2739
3184
  }
2740
3185
  toPrintHtml(input, options = {}) {
2741
- return renderPrintHtml(this.astOf(input), this.printOptions(options));
3186
+ const b = this.bundle(input);
3187
+ return renderPrintHtml(b.ast, {
3188
+ theme: b.theme,
3189
+ classPrefix: this.prefix,
3190
+ overrides: b.htmlOverrides,
3191
+ ...options
3192
+ });
2742
3193
  }
2743
3194
  toPdf(input, options = {}) {
2744
- return renderPdf(this.astOf(input), this.printOptions(options));
3195
+ const b = this.bundle(input);
3196
+ return renderPdf(b.ast, {
3197
+ theme: b.theme,
3198
+ classPrefix: this.prefix,
3199
+ overrides: b.htmlOverrides,
3200
+ ...options
3201
+ });
2745
3202
  }
2746
- /** The CSS stylesheet for the active theme. */
2747
- styles() {
2748
- return this.htmlRenderer().styles();
3203
+ /**
3204
+ * The CSS stylesheet for the active theme. Pass the document (source or AST)
3205
+ * to fold in any libraries it imports via `\usepackage` (so the styles match
3206
+ * `toHtml(input)`); omit it to get the engine theme's stylesheet.
3207
+ */
3208
+ styles(input) {
3209
+ const theme = input === void 0 ? this.theme : this.bundle(input).theme;
3210
+ return new HtmlRenderer({ theme, classPrefix: this.prefix }).styles();
2749
3211
  }
2750
3212
  /* ------------------------------ internals --------------------------- */
3213
+ /** Resolve a library name against custom libraries first, then built-ins. */
3214
+ lookupLibrary(name) {
3215
+ return this.customLibraries.get(name.trim().toLowerCase()) ?? getBuiltinLibrary(name);
3216
+ }
2751
3217
  astOf(input) {
2752
3218
  return typeof input === "string" ? this.compile(input, { validate: false }).ast : input;
2753
3219
  }
2754
- htmlRenderer(options = {}) {
2755
- return new HtmlRenderer({
2756
- theme: this.theme,
2757
- classPrefix: this.prefix,
2758
- overrides: this.htmlOverrides,
2759
- ...options
2760
- });
2761
- }
2762
- printOptions(options) {
3220
+ /**
3221
+ * Build the per-document render inputs: the AST, the effective theme (engine
3222
+ * theme + any libraries the document imports), and the merged render
3223
+ * overrides. Library use is recovered from `\usepackage` nodes in the AST, so
3224
+ * this works whether the input was a source string or a pre-parsed document.
3225
+ */
3226
+ bundle(input) {
3227
+ const ast = this.astOf(input);
3228
+ const libraries = usedLibraryNamesFromAst(ast).map((n) => this.lookupLibrary(n)).filter((l) => !!l);
3229
+ if (libraries.length === 0) {
3230
+ return {
3231
+ ast,
3232
+ theme: this.theme,
3233
+ htmlOverrides: this.htmlOverrides,
3234
+ reactOverrides: this.reactOverrides
3235
+ };
3236
+ }
3237
+ const { html, react } = collectLibraryOverrides(libraries);
2763
3238
  return {
2764
- theme: this.theme,
2765
- classPrefix: this.prefix,
2766
- overrides: this.htmlOverrides,
2767
- ...options
3239
+ ast,
3240
+ theme: mergeLibraryTheme(this.theme, libraries),
3241
+ htmlOverrides: new Map([...this.htmlOverrides, ...html]),
3242
+ reactOverrides: new Map([...this.reactOverrides, ...react])
2768
3243
  };
2769
3244
  }
2770
3245
  invalidate() {
@@ -2871,7 +3346,7 @@ function printBlocks(nodes) {
2871
3346
  for (const node of nodes) {
2872
3347
  if (node.type === "parbreak") {
2873
3348
  flush();
2874
- } else if (isBlockNode(node) || node.type === "section" || node.type === "list" || node.type === "columns") {
3349
+ } else if (isBlockNode(node) || node.type === "section" || node.type === "list" || node.type === "columns" || node.type === "usepackage") {
2875
3350
  flush();
2876
3351
  out.push(printBlock(node));
2877
3352
  } else {
@@ -2910,8 +3385,10 @@ ${indent}\\end{columns}`;
2910
3385
  }
2911
3386
  case "skills":
2912
3387
  return `${indent}\\skills{${node.items.join(", ")}}`;
3388
+ case "usepackage":
3389
+ return `${indent}\\usepackage{${node.names.join(", ")}}`;
2913
3390
  case "rule":
2914
- return `${indent}\\hrule`;
3391
+ return indent + ruleCommand(node.style);
2915
3392
  case "space":
2916
3393
  return `${indent}\\${node.axis === "vertical" ? "vspace" : "hspace"}{${node.size}}`;
2917
3394
  case "command":
@@ -2963,6 +3440,8 @@ function printNode(node) {
2963
3440
  return `\\url{${node.rawHref}}`;
2964
3441
  case "icon":
2965
3442
  return `\\icon{${node.name}}`;
3443
+ case "usepackage":
3444
+ return `\\usepackage{${node.names.join(", ")}}`;
2966
3445
  case "contact":
2967
3446
  return `\\${node.field}{${node.value}}`;
2968
3447
  case "space":
@@ -2976,6 +3455,20 @@ function printNode(node) {
2976
3455
  return printBlock(node);
2977
3456
  }
2978
3457
  }
3458
+ function ruleCommand(style) {
3459
+ switch (style) {
3460
+ case "dashed":
3461
+ return "\\dashrule";
3462
+ case "dotted":
3463
+ return "\\dotrule";
3464
+ case "thick":
3465
+ return "\\thickrule";
3466
+ case "double":
3467
+ return "\\doublerule";
3468
+ default:
3469
+ return "\\hrule";
3470
+ }
3471
+ }
2979
3472
  function indentLines(text, indent) {
2980
3473
  return text.split("\n").map((line) => line ? indent + line : line).join("\n");
2981
3474
  }
@@ -2983,22 +3476,34 @@ function indentLines(text, indent) {
2983
3476
  // src/editor/editor.ts
2984
3477
  var EditorService = class {
2985
3478
  constructor(options = {}) {
3479
+ this.customLibraries = /* @__PURE__ */ new Map();
3480
+ this.lookupLibrary = (name) => this.customLibraries.get(name.trim().toLowerCase()) ?? getBuiltinLibrary(name);
2986
3481
  this.registry = options.registry ?? createDefaultRegistry();
2987
3482
  this.theme = options.theme;
3483
+ for (const lib of options.libraries ?? []) {
3484
+ this.customLibraries.set(lib.name.trim().toLowerCase(), lib);
3485
+ }
3486
+ }
3487
+ /** Build a registry that includes the libraries the source imports. */
3488
+ registryFor(tokens) {
3489
+ const names = usedLibraryNamesFromTokens(tokens);
3490
+ return applyLibraries(this.registry, names, this.lookupLibrary).registry;
2988
3491
  }
2989
3492
  /* ----------------------------- diagnostics -------------------------- */
2990
3493
  getDiagnostics(source) {
2991
3494
  const { tokens, diagnostics: lex } = tokenize(source);
2992
- const { ast, diagnostics: parse2 } = new Parser(tokens, {
2993
- registry: this.registry
2994
- }).parse();
2995
- return [...lex, ...parse2, ...validate(ast, this.theme ? { theme: this.theme } : {})];
3495
+ const names = usedLibraryNamesFromTokens(tokens);
3496
+ const { registry, libraries } = applyLibraries(this.registry, names, this.lookupLibrary);
3497
+ const { ast, diagnostics: parse2 } = new Parser(tokens, { registry }).parse();
3498
+ const diagnostics = libraryDiagnostics(ast, [...lex, ...parse2], this.lookupLibrary);
3499
+ const theme = this.theme ? mergeLibraryTheme(this.theme, libraries) : void 0;
3500
+ return [...diagnostics, ...validate(ast, theme ? { theme } : {})];
2996
3501
  }
2997
3502
  /* ------------------------------ inspect ----------------------------- */
2998
3503
  /** Parse and return the AST for inspection / debugging. */
2999
3504
  inspect(source) {
3000
3505
  const { tokens } = tokenize(source);
3001
- return new Parser(tokens, { registry: this.registry }).parse().ast;
3506
+ return new Parser(tokens, { registry: this.registryFor(tokens) }).parse().ast;
3002
3507
  }
3003
3508
  /* ------------------------------ format ------------------------------ */
3004
3509
  format(source) {
@@ -3007,11 +3512,12 @@ var EditorService = class {
3007
3512
  /* --------------------------- completion ----------------------------- */
3008
3513
  getCompletions(source, offset) {
3009
3514
  const before = source.slice(0, offset);
3515
+ const registry = this.registryFor(tokenize(source).tokens);
3010
3516
  const envMatch = /\\(begin|end)\{([a-zA-Z*]*)$/.exec(before);
3011
3517
  if (envMatch) {
3012
3518
  const prefix = envMatch[2];
3013
3519
  const start = offset - prefix.length;
3014
- return this.registry.allEnvironments().filter((e) => e.name.startsWith(prefix)).map((e) => this.environmentCompletion(e, this.rangeAt(source, start, offset)));
3520
+ return registry.allEnvironments().filter((e) => e.name.startsWith(prefix)).map((e) => this.environmentCompletion(e, this.rangeAt(source, start, offset)));
3015
3521
  }
3016
3522
  const fieldKeys = this.fieldContext(before);
3017
3523
  if (fieldKeys) {
@@ -3026,7 +3532,18 @@ var EditorService = class {
3026
3532
  if (cmdMatch) {
3027
3533
  const prefix = cmdMatch[1];
3028
3534
  const start = offset - prefix.length - 1;
3029
- 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)));
3535
+ const range2 = this.rangeAt(source, start, offset);
3536
+ const active = registry.allCommands().filter((c) => c.name.startsWith(prefix)).sort((a, b) => a.name.localeCompare(b.name)).map((c) => this.commandCompletion(c, range2));
3537
+ const present = new Set(registry.commandNames());
3538
+ const gated = Object.entries(GATED_COMMANDS).filter(([name]) => name.startsWith(prefix) && !present.has(name)).sort(([a], [b]) => a.localeCompare(b)).map(([name, lib]) => ({
3539
+ label: `\\${name}`,
3540
+ kind: "command",
3541
+ detail: `requires \\usepackage{${lib}}`,
3542
+ documentation: `\`\\${name}\` is provided by the **${lib}** library. Add \`\\usepackage{${lib}}\` to your preamble to use it.`,
3543
+ insertText: name,
3544
+ range: range2
3545
+ }));
3546
+ return [...active, ...gated];
3030
3547
  }
3031
3548
  return [];
3032
3549
  }
@@ -3077,6 +3594,7 @@ var EditorService = class {
3077
3594
  /* ------------------------------ hover ------------------------------- */
3078
3595
  getHover(source, offset) {
3079
3596
  const { tokens } = tokenize(source);
3597
+ const registry = this.registryFor(tokens);
3080
3598
  const idx = tokens.findIndex(
3081
3599
  (t) => t.type === "Command" /* Command */ && offset >= t.range.start.offset && offset <= t.range.end.offset
3082
3600
  );
@@ -3084,7 +3602,7 @@ var EditorService = class {
3084
3602
  const token = tokens[idx];
3085
3603
  if (token.value === "begin" || token.value === "end") {
3086
3604
  const name = this.readNameAfter(tokens, idx);
3087
- const env = name ? this.registry.getEnvironment(name) : void 0;
3605
+ const env = name ? registry.getEnvironment(name) : void 0;
3088
3606
  if (env) {
3089
3607
  return {
3090
3608
  contents: this.environmentDoc(env),
@@ -3093,12 +3611,13 @@ var EditorService = class {
3093
3611
  }
3094
3612
  return null;
3095
3613
  }
3096
- const def = this.registry.getCommand(token.value);
3614
+ const def = registry.getCommand(token.value);
3097
3615
  if (!def) {
3098
- return {
3099
- contents: `Unknown command \`\\${token.value}\``,
3100
- range: token.range
3101
- };
3616
+ const lib = GATED_COMMANDS[token.value];
3617
+ const contents = lib ? `\`\\${token.value}\` is provided by the **${lib}** library.
3618
+
3619
+ Add \`\\usepackage{${lib}}\` to your preamble to use it.` : `Unknown command \`\\${token.value}\``;
3620
+ return { contents, range: token.range };
3102
3621
  }
3103
3622
  return { contents: this.commandDoc(def), range: token.range };
3104
3623
  }
@@ -3129,6 +3648,7 @@ ${def.example}
3129
3648
  /* ------------------------ semantic highlighting --------------------- */
3130
3649
  getSemanticTokens(source) {
3131
3650
  const { tokens } = tokenize(source);
3651
+ const registry = this.registryFor(tokens);
3132
3652
  const out = [];
3133
3653
  for (let i = 0; i < tokens.length; i++) {
3134
3654
  const t = tokens[i];
@@ -3138,7 +3658,7 @@ ${def.example}
3138
3658
  out.push({ range: t.range, type: "environment", modifiers: [] });
3139
3659
  break;
3140
3660
  }
3141
- const def = this.registry.getCommand(t.value);
3661
+ const def = registry.getCommand(t.value);
3142
3662
  out.push({
3143
3663
  range: t.range,
3144
3664
  type: def ? "command" : "command-unknown",
@@ -3201,6 +3721,53 @@ ${def.example}
3201
3721
  }
3202
3722
  };
3203
3723
 
3724
+ // src/editor/sync.ts
3725
+ var SOURCE_POS_ATTR = "data-rtx-pos";
3726
+ var SOURCE_TYPE_ATTR = "data-rtx-type";
3727
+ function parseSourcePos(value) {
3728
+ if (!value) return null;
3729
+ const sep = value.indexOf(":");
3730
+ if (sep === -1) return null;
3731
+ const start = Number.parseInt(value.slice(0, sep), 10);
3732
+ const end = Number.parseInt(value.slice(sep + 1), 10);
3733
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return null;
3734
+ return { start, end };
3735
+ }
3736
+ function readSourcePos(el) {
3737
+ if (!el) return null;
3738
+ return parseSourcePos(el.getAttribute(SOURCE_POS_ATTR));
3739
+ }
3740
+ function readSourceType(el) {
3741
+ return el?.getAttribute(SOURCE_TYPE_ATTR) ?? null;
3742
+ }
3743
+ function closestSourcePos(el) {
3744
+ if (!el) return null;
3745
+ const match = el.closest(`[${SOURCE_POS_ATTR}]`);
3746
+ return readSourcePos(match);
3747
+ }
3748
+ function spanLength(span) {
3749
+ return span.end - span.start;
3750
+ }
3751
+ function spanContains(span, offset) {
3752
+ return offset >= span.start && offset <= span.end;
3753
+ }
3754
+ function pickElementForOffset(elements, offset) {
3755
+ let best = null;
3756
+ let bestLen = Infinity;
3757
+ let bestStart = -1;
3758
+ for (const el of elements) {
3759
+ const span = readSourcePos(el);
3760
+ if (!span || !spanContains(span, offset)) continue;
3761
+ const len = spanLength(span);
3762
+ if (len < bestLen || len === bestLen && span.start > bestStart) {
3763
+ best = el;
3764
+ bestLen = len;
3765
+ bestStart = span.start;
3766
+ }
3767
+ }
3768
+ return best;
3769
+ }
3770
+
3204
3771
  // src/incremental/incremental.ts
3205
3772
  var isLetter2 = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z";
3206
3773
  var IncrementalCompiler = class {
@@ -3212,6 +3779,11 @@ var IncrementalCompiler = class {
3212
3779
  this.hits = 0;
3213
3780
  this.misses = 0;
3214
3781
  this.registry = options.registry ?? createDefaultRegistry();
3782
+ const custom = /* @__PURE__ */ new Map();
3783
+ for (const lib of options.libraries ?? []) {
3784
+ custom.set(lib.name.trim().toLowerCase(), lib);
3785
+ }
3786
+ this.lookupLibrary = (name) => custom.get(name.trim().toLowerCase()) ?? getBuiltinLibrary(name);
3215
3787
  }
3216
3788
  /** Clear all caches (e.g. after registering new commands). */
3217
3789
  reset() {
@@ -3221,6 +3793,9 @@ var IncrementalCompiler = class {
3221
3793
  compile(source) {
3222
3794
  this.hits = 0;
3223
3795
  this.misses = 0;
3796
+ const usedNames = usedLibraryNamesFromTokens(tokenize(source).tokens);
3797
+ const registry = applyLibraries(this.registry, usedNames, this.lookupLibrary).registry;
3798
+ const libsKey = usedNames.join(",");
3224
3799
  const segments = splitBalancedSegments(source);
3225
3800
  const children = [];
3226
3801
  const diagnostics = [];
@@ -3229,22 +3804,21 @@ var IncrementalCompiler = class {
3229
3804
  for (const seg of segments) {
3230
3805
  const text = source.slice(seg.start, seg.end);
3231
3806
  if (text.trim() === "") continue;
3232
- const posKey = `${seg.start}:${seg.startLine}:${text}`;
3807
+ const posKey = `${libsKey}\0${seg.start}:${seg.startLine}:${text}`;
3233
3808
  let resolved = this.positioned.get(posKey);
3234
3809
  if (resolved) {
3235
3810
  this.hits++;
3236
3811
  } else {
3237
- let parsed = this.parseCache.get(text);
3812
+ const cacheKey = `${libsKey}\0${text}`;
3813
+ let parsed = this.parseCache.get(cacheKey);
3238
3814
  if (parsed) {
3239
3815
  this.hits++;
3240
3816
  } else {
3241
3817
  this.misses++;
3242
3818
  const { tokens } = tokenize(text);
3243
- const { ast: ast2, diagnostics: d } = new Parser(tokens, {
3244
- registry: this.registry
3245
- }).parse();
3819
+ const { ast: ast2, diagnostics: d } = new Parser(tokens, { registry }).parse();
3246
3820
  parsed = { children: ast2.children, diagnostics: d };
3247
- this.parseCache.set(text, parsed);
3821
+ this.parseCache.set(cacheKey, parsed);
3248
3822
  }
3249
3823
  const dOffset = seg.start;
3250
3824
  const dLine = seg.startLine - 1;
@@ -3267,7 +3841,7 @@ var IncrementalCompiler = class {
3267
3841
  };
3268
3842
  return {
3269
3843
  ast,
3270
- diagnostics,
3844
+ diagnostics: libraryDiagnostics(ast, diagnostics, this.lookupLibrary),
3271
3845
  stats: {
3272
3846
  segments: segments.length,
3273
3847
  cacheHits: this.hits,
@@ -3365,6 +3939,6 @@ function endPosition(source) {
3365
3939
  return { offset: source.length, line, column };
3366
3940
  }
3367
3941
 
3368
- export { BLOCK_TYPES, CommandRegistry, DiagnosticCode, DiagnosticSeverity, EditorService, HtmlRenderer, IncrementalCompiler, Parser, ReTeXEngine, ReactRenderer, SPECIAL_CHARS, TokenType, Tokenizer, badgePlugin, childrenOf, classicTheme, closestMatch, collect, compactTheme, createDefaultRegistry, createEngine, dateRange, defaultTheme, entryParts, escapeAttribute, escapeHtml, flattenText, getIcon, getTheme, hasIcon, iconNames, iconToSvg, isBalanced, isBlockNode, isNode, isParent, isSafeColor, isSafeDimension, levenshtein, mergeRanges, modernTheme, nodePathAt, normalizeWhitespace, pageCss, parse, parseKeyValArg, parseListArg, pos, printDocument, range, rangeContains, ratingPlugin, registerIcon, renderHtml, renderHtmlDocument, renderJson, renderPdf, renderPrintHtml, renderReact, resolveIconName, resolveTheme, sanitizeStyleValue, sanitizeUrl, splitBalancedSegments, splitPreamble, splitTopLevel, themeToCss, themes, toJsonTree, toRegions, tokenize, validate, walk };
3942
+ export { BLOCK_TYPES, CommandRegistry, DiagnosticCode, DiagnosticSeverity, EditorService, GATED_COMMANDS, HtmlRenderer, IncrementalCompiler, Parser, ReTeXEngine, ReactRenderer, SOURCE_POS_ATTR, SOURCE_TYPE_ATTR, SPECIAL_CHARS, TokenType, Tokenizer, applyLibraries, badgePlugin, blankTheme, builtinLibraries, builtinLibraryNames, childrenOf, classicLibrary, classicTheme, closestMatch, closestSourcePos, collect, collectLibraryOverrides, colorCommands, colorsLibrary, compactLibrary, compactTheme, createDefaultRegistry, createEngine, dateRange, defaultTheme, entryParts, escapeAttribute, escapeHtml, flattenText, fontCommands, fontStacks, fontTheme, fontsLibrary, getBuiltinLibrary, getIcon, getTheme, hasIcon, iconNames, iconToSvg, isBalanced, isBlockNode, isNode, isParent, isSafeColor, isSafeDimension, levenshtein, libraryDiagnostics, mergeLibraryTheme, mergeRanges, modernLibrary, modernTheme, nodePathAt, normalizeWhitespace, pageCss, palettes, parse, parseKeyValArg, parseListArg, parseSourcePos, pickElementForOffset, pos, printDocument, range, rangeContains, ratingPlugin, readSourcePos, readSourceType, registerIcon, renderHtml, renderHtmlDocument, renderJson, renderPdf, renderPrintHtml, renderReact, resolveIconName, resolveLibraryName, resolveTheme, sanitizeStyleValue, sanitizeUrl, shapeCommands, shapesLibrary, spanContains, spanLength, splitBalancedSegments, splitPreamble, splitTopLevel, themeToCss, themes, toJsonTree, toRegions, tokenize, usedLibraryNamesFromAst, usedLibraryNamesFromTokens, validate, walk };
3369
3943
  //# sourceMappingURL=index.js.map
3370
3944
  //# sourceMappingURL=index.js.map