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