@earendil-works/pi-tui 0.84.0 → 0.84.2

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/latex.js CHANGED
@@ -283,6 +283,89 @@ const DISPLAY_LIMIT_SYMBOLS = new Set([
283
283
  "prod",
284
284
  "sum",
285
285
  ]);
286
+ const RELATION_COMMANDS = new Set([
287
+ "Leftarrow",
288
+ "Leftrightarrow",
289
+ "Longleftarrow",
290
+ "Longleftrightarrow",
291
+ "Longrightarrow",
292
+ "Rightarrow",
293
+ "Vdash",
294
+ "Vvdash",
295
+ "approx",
296
+ "asymp",
297
+ "cong",
298
+ "dashv",
299
+ "doteq",
300
+ "downarrow",
301
+ "equiv",
302
+ "ge",
303
+ "geq",
304
+ "geqslant",
305
+ "gets",
306
+ "gg",
307
+ "hookleftarrow",
308
+ "hookrightarrow",
309
+ "iff",
310
+ "implies",
311
+ "in",
312
+ "leadsto",
313
+ "le",
314
+ "leftarrow",
315
+ "leftharpoondown",
316
+ "leftharpoonup",
317
+ "leftrightarrow",
318
+ "leftrightharpoons",
319
+ "leq",
320
+ "leqslant",
321
+ "ll",
322
+ "longleftarrow",
323
+ "longleftrightarrow",
324
+ "longmapsto",
325
+ "longrightarrow",
326
+ "mapsto",
327
+ "mid",
328
+ "models",
329
+ "ne",
330
+ "nearrow",
331
+ "neq",
332
+ "ni",
333
+ "notin",
334
+ "nvdash",
335
+ "nvDash",
336
+ "nwarrow",
337
+ "parallel",
338
+ "perp",
339
+ "prec",
340
+ "preceq",
341
+ "propto",
342
+ "rightharpoondown",
343
+ "rightharpoonup",
344
+ "rightleftharpoons",
345
+ "rightarrow",
346
+ "rightsquigarrow",
347
+ "searrow",
348
+ "sim",
349
+ "simeq",
350
+ "sqsubset",
351
+ "sqsubseteq",
352
+ "sqsupset",
353
+ "sqsupseteq",
354
+ "subset",
355
+ "subseteq",
356
+ "succ",
357
+ "succeq",
358
+ "supset",
359
+ "supseteq",
360
+ "swarrow",
361
+ "to",
362
+ "triangleleft",
363
+ "triangleright",
364
+ "twoheadleftarrow",
365
+ "twoheadrightarrow",
366
+ "uparrow",
367
+ "vdash",
368
+ ]);
286
369
  const NEGATED_SYMBOLS = {
287
370
  "<": "≮",
288
371
  ">": "≯",
@@ -504,7 +587,7 @@ function replaceCharacters(value, replacements) {
504
587
  function formatScript(value, kind) {
505
588
  value = value.trim();
506
589
  const replacements = kind === "sub" ? SUBSCRIPTS : SUPERSCRIPTS;
507
- const unicode = replaceCharacters(value, replacements);
590
+ const unicode = replaceCharacters(value.replace(/\s*([=+-])\s*/g, "$1"), replacements);
508
591
  if (unicode !== undefined) {
509
592
  return unicode;
510
593
  }
@@ -525,8 +608,16 @@ function formatRoot(value, symbol = "√") {
525
608
  value = value.trim();
526
609
  return /^[\p{L}\p{N}.]+$/u.test(value) ? `${symbol}${value}` : `${symbol}(${value})`;
527
610
  }
611
+ const NAMED_OPERATOR_START = "\u{f0004}";
612
+ const NAMED_OPERATOR_END = "\u{f0005}";
613
+ const NAMED_OPERATOR_LEFT_SPACING_PATTERN = /(?<=[\p{L}\p{N})\]}\u{f0001}])\u{f0004}/gu;
614
+ const NAMED_OPERATOR_RIGHT_SPACING_PATTERN = /\u{f0005}(?=[\p{L}\p{N}√\u{f0000}])/gu;
528
615
  function normalizeOutput(value) {
529
616
  return value
617
+ .replace(NAMED_OPERATOR_LEFT_SPACING_PATTERN, " ")
618
+ .replaceAll(NAMED_OPERATOR_START, "")
619
+ .replace(NAMED_OPERATOR_RIGHT_SPACING_PATTERN, " ")
620
+ .replaceAll(NAMED_OPERATOR_END, "")
530
621
  .split("\n")
531
622
  .map((line) => line.replace(/[ \t]+/g, " ").trim())
532
623
  .filter((line, index, lines) => line.length > 0 || (index > 0 && index < lines.length - 1))
@@ -536,7 +627,8 @@ function normalizeOutput(value) {
536
627
  const LAYOUT_MARKER_START = "\u{f0000}";
537
628
  const LAYOUT_MARKER_END = "\u{f0001}";
538
629
  const LAYOUT_MARKER_PATTERN = /\u{f0000}(\d+)\u{f0001}/gu;
539
- const PROTECTED_SPACE = "\u00a0";
630
+ const TRAILING_LAYOUT_MARKER_PATTERN = /\u{f0000}(\d+)\u{f0001}$/u;
631
+ const PROTECTED_SPACE = "\u{f0002}";
540
632
  function padLayoutLine(line, width, centered = false) {
541
633
  const padding = Math.max(0, width - visibleWidth(line));
542
634
  const left = centered ? Math.floor(padding / 2) : 0;
@@ -572,18 +664,25 @@ function renderLayout(source, nodes) {
572
664
  for (const sourceLine of source.split("\n")) {
573
665
  const layouts = [];
574
666
  let position = 0;
575
- let previousWasNode = false;
667
+ let previousNode;
576
668
  for (const match of sourceLine.matchAll(LAYOUT_MARKER_PATTERN)) {
577
669
  const index = match.index;
578
- if (index > position) {
579
- const sliced = sourceLine.slice(position, index);
580
- const text = (previousWasNode ? sliced.trimStart() : sliced).trimEnd();
581
- layouts.push({ lines: [text], width: visibleWidth(text), baseline: 0 });
582
- }
583
670
  const node = nodes[Number(match[1])];
584
671
  if (!node) {
585
672
  continue;
586
673
  }
674
+ if (index > position) {
675
+ const sliced = sourceLine.slice(position, index);
676
+ const trimmed = (previousNode ? sliced.trimStart() : sliced).trimEnd();
677
+ const preserveLeadingSpace = previousNode?.type === "matrix" && /^\s/.test(sliced);
678
+ const preserveTrailingSpace = node.type === "matrix" && /\s$/.test(sliced);
679
+ const text = trimmed
680
+ ? `${preserveLeadingSpace ? " " : ""}${trimmed}${preserveTrailingSpace ? " " : ""}`
681
+ : preserveLeadingSpace || preserveTrailingSpace
682
+ ? " "
683
+ : "";
684
+ layouts.push({ lines: [text], width: visibleWidth(text), baseline: 0 });
685
+ }
587
686
  if (node.type === "fraction") {
588
687
  const numerator = renderLayout(node.numerator, nodes);
589
688
  const denominator = renderLayout(node.denominator, nodes);
@@ -599,7 +698,7 @@ function renderLayout(source, nodes) {
599
698
  baseline: numerator.lines.length,
600
699
  });
601
700
  }
602
- else {
701
+ else if (node.type === "operator") {
603
702
  const contentWidth = Math.max(visibleWidth(node.operator), node.lower === undefined ? 0 : visibleWidth(node.lower), node.upper === undefined ? 0 : visibleWidth(node.upper));
604
703
  const lines = [];
605
704
  if (node.upper !== undefined) {
@@ -615,12 +714,21 @@ function renderLayout(source, nodes) {
615
714
  baseline: node.upper === undefined ? 0 : 1,
616
715
  });
617
716
  }
717
+ else {
718
+ const width = Math.max(0, ...node.lines.map((line) => visibleWidth(line)));
719
+ layouts.push({
720
+ lines: node.lines.map((line) => padLayoutLine(line, width)),
721
+ width,
722
+ baseline: node.baseline,
723
+ });
724
+ }
618
725
  position = index + match[0].length;
619
- previousWasNode = true;
726
+ previousNode = node;
620
727
  }
621
728
  if (position < sourceLine.length) {
622
729
  const sliced = sourceLine.slice(position);
623
- const text = previousWasNode ? sliced.trimStart() : sliced;
730
+ const trimmed = previousNode ? sliced.trimStart() : sliced;
731
+ const text = previousNode?.type === "matrix" && /^\s/.test(sliced) ? ` ${trimmed}` : trimmed;
624
732
  layouts.push({ lines: [text], width: visibleWidth(text), baseline: 0 });
625
733
  }
626
734
  const lineLayout = joinLayouts(layouts);
@@ -638,12 +746,14 @@ function renderLayout(source, nodes) {
638
746
  class LatexParser {
639
747
  source;
640
748
  layoutNodes;
749
+ display;
641
750
  position = 0;
642
751
  supported = true;
643
752
  stackFractions = true;
644
- constructor(source, layoutNodes) {
753
+ constructor(source, layoutNodes, display) {
645
754
  this.source = source;
646
755
  this.layoutNodes = layoutNodes;
756
+ this.display = display;
647
757
  }
648
758
  render() {
649
759
  const rendered = this.parseSequence();
@@ -673,6 +783,9 @@ class LatexParser {
673
783
  const command = this.parseCommand();
674
784
  if (command === NEGATIVE_SPACE) {
675
785
  result = result.trimEnd();
786
+ if (result.endsWith(NAMED_OPERATOR_END)) {
787
+ result = result.slice(0, -NAMED_OPERATOR_END.length);
788
+ }
676
789
  }
677
790
  else {
678
791
  result += command;
@@ -682,13 +795,24 @@ class LatexParser {
682
795
  if (character === "^" || character === "_") {
683
796
  this.position++;
684
797
  result = result.trimEnd();
685
- result += formatScript(this.parseRequiredArgument(false), character === "_" ? "sub" : "sup");
798
+ const script = formatScript(this.parseRequiredArgument(false), character === "_" ? "sub" : "sup");
799
+ if (result.endsWith(NAMED_OPERATOR_END)) {
800
+ result = `${result.slice(0, -NAMED_OPERATOR_END.length)}${script}${NAMED_OPERATOR_END}`;
801
+ }
802
+ else {
803
+ result += script;
804
+ }
686
805
  continue;
687
806
  }
688
807
  if (/\s/.test(character)) {
689
808
  result += this.parseWhitespace();
690
809
  continue;
691
810
  }
811
+ if (character === "=" || character === "<" || character === ">") {
812
+ result = `${result.trimEnd()} ${character} `;
813
+ this.position++;
814
+ continue;
815
+ }
692
816
  if (character === "&") {
693
817
  this.position++;
694
818
  continue;
@@ -698,6 +822,16 @@ class LatexParser {
698
822
  result += " ";
699
823
  continue;
700
824
  }
825
+ if (character === ".") {
826
+ const marker = TRAILING_LAYOUT_MARKER_PATTERN.exec(result);
827
+ const node = marker ? this.layoutNodes[Number(marker[1])] : undefined;
828
+ if (node?.type === "matrix") {
829
+ const lastLine = node.lines.length - 1;
830
+ node.lines[lastLine] = `${node.lines[lastLine] ?? ""}${character}`;
831
+ this.position++;
832
+ continue;
833
+ }
834
+ }
701
835
  result += character;
702
836
  this.position++;
703
837
  }
@@ -720,6 +854,13 @@ class LatexParser {
720
854
  }
721
855
  let command = "";
722
856
  const first = this.source[this.position] ?? "";
857
+ if (first === "\n" || first === "\r") {
858
+ this.position++;
859
+ if (first === "\r" && this.source[this.position] === "\n") {
860
+ this.position++;
861
+ }
862
+ return " ";
863
+ }
723
864
  if (/[A-Za-z]/.test(first)) {
724
865
  const start = this.position;
725
866
  while (this.position < this.source.length && /[A-Za-z]/.test(this.source[this.position] ?? "")) {
@@ -759,24 +900,27 @@ class LatexParser {
759
900
  const value = this.parseRequiredArgument(false).trim();
760
901
  const negated = NEGATED_SYMBOLS[value];
761
902
  if (negated !== undefined) {
762
- return negated;
903
+ return ` ${negated} `;
763
904
  }
764
905
  const characters = Array.from(value);
765
906
  if (characters.length === 0) {
766
907
  this.supported = false;
767
908
  return "";
768
909
  }
769
- return `${characters[0]}\u0338${characters.slice(1).join("")}`;
910
+ return ` ${characters[0]}\u0338${characters.slice(1).join("")} `;
770
911
  }
771
912
  if (LIMIT_OPERATORS.has(command)) {
772
913
  return this.parseOperator(command, "bracket", true, true);
773
914
  }
774
915
  const symbol = SYMBOLS[command];
775
916
  if (symbol !== undefined) {
776
- return DISPLAY_LIMIT_SYMBOLS.has(command) ? this.parseOperator(symbol, "script", true) : symbol;
917
+ if (DISPLAY_LIMIT_SYMBOLS.has(command)) {
918
+ return this.parseOperator(symbol, "script", true);
919
+ }
920
+ return command === "cdot" || command === "times" || RELATION_COMMANDS.has(command) ? ` ${symbol} ` : symbol;
777
921
  }
778
922
  if (NAMED_OPERATORS.has(command)) {
779
- return ` ${command} `;
923
+ return `${NAMED_OPERATOR_START}${command}${NAMED_OPERATOR_END}`;
780
924
  }
781
925
  if (SIZE_COMMANDS.has(command)) {
782
926
  return "";
@@ -788,10 +932,10 @@ class LatexParser {
788
932
  return "";
789
933
  }
790
934
  if (command === "frac" || command === "dfrac" || command === "tfrac") {
791
- const shouldStack = this.layoutNodes !== undefined && this.stackFractions && command !== "tfrac";
935
+ const shouldStack = this.display && this.stackFractions && command !== "tfrac";
792
936
  const numerator = this.parseRequiredArgument(!shouldStack);
793
937
  const denominator = this.parseRequiredArgument(!shouldStack);
794
- if (shouldStack && this.layoutNodes) {
938
+ if (shouldStack) {
795
939
  const index = this.layoutNodes.push({
796
940
  type: "fraction",
797
941
  numerator: normalizeOutput(numerator),
@@ -906,7 +1050,7 @@ class LatexParser {
906
1050
  upper = value;
907
1051
  }
908
1052
  }
909
- if (this.layoutNodes && useDisplayLimits && (lower !== undefined || upper !== undefined)) {
1053
+ if (this.display && useDisplayLimits && (lower !== undefined || upper !== undefined)) {
910
1054
  const index = this.layoutNodes.push({ type: "operator", operator, lower, upper }) - 1;
911
1055
  return `${LAYOUT_MARKER_START}${index}${LAYOUT_MARKER_END}`;
912
1056
  }
@@ -927,7 +1071,7 @@ class LatexParser {
927
1071
  return value;
928
1072
  }
929
1073
  parseRequiredArgumentValue() {
930
- while (this.position < this.source.length && /[ \t]/.test(this.source[this.position] ?? "")) {
1074
+ while (this.position < this.source.length && /\s/.test(this.source[this.position] ?? "")) {
931
1075
  this.position++;
932
1076
  }
933
1077
  if (this.position >= this.source.length) {
@@ -1065,34 +1209,37 @@ class LatexParser {
1065
1209
  const cell = row[column] ?? "";
1066
1210
  return `${cell}${PROTECTED_SPACE.repeat(Math.max(0, (columnWidths[column] ?? 0) - visibleWidth(cell)))}`;
1067
1211
  }).join(" │ "));
1212
+ let lines;
1068
1213
  if (environment === "array" || environment === "matrix" || environment === "smallmatrix") {
1069
- return rows.join("\n");
1070
- }
1071
- const delimiters = {
1072
- pmatrix: ["⎛", "⎞", "⎜", "⎟", "⎝", "⎠"],
1073
- bmatrix: ["⎡", "⎤", "⎢", "⎥", "⎣", "⎦"],
1074
- Bmatrix: ["⎧", "⎫", "⎨", "⎬", "⎩", "⎭"],
1075
- vmatrix: ["│", "│", "│", "│", "│", "│"],
1076
- Vmatrix: ["║", "║", "║", "║", "║", "║"],
1077
- };
1078
- const delimiter = delimiters[environment];
1079
- if (!delimiter) {
1080
- this.supported = false;
1081
- return rows.join("\n");
1082
- }
1083
- if (rows.length === 1) {
1084
- return `${delimiter[0]} ${rows[0]} ${delimiter[1]}`;
1085
- }
1086
- return rows
1087
- .map((row, index) => {
1088
- const left = index === 0 ? delimiter[0] : index === rows.length - 1 ? delimiter[4] : delimiter[2];
1089
- const right = index === 0 ? delimiter[1] : index === rows.length - 1 ? delimiter[5] : delimiter[3];
1090
- return `${left} ${row} ${right}`;
1091
- })
1092
- .join("\n");
1214
+ lines = rows;
1215
+ }
1216
+ else {
1217
+ const delimiters = {
1218
+ pmatrix: ["⎛", "⎞", "⎜", "⎟", "⎝", "⎠"],
1219
+ bmatrix: ["⎡", "⎤", "⎢", "⎥", "⎣", "⎦"],
1220
+ Bmatrix: ["⎧", "⎫", "⎨", "⎬", "⎩", "⎭"],
1221
+ vmatrix: ["│", "│", "│", "│", "│", "│"],
1222
+ Vmatrix: ["║", "║", "║", "║", "║", "║"],
1223
+ };
1224
+ const delimiter = delimiters[environment];
1225
+ if (!delimiter) {
1226
+ this.supported = false;
1227
+ return rows.join("\n");
1228
+ }
1229
+ lines = rows.map((row, index) => {
1230
+ const left = index === 0 ? delimiter[0] : index === rows.length - 1 ? delimiter[4] : delimiter[2];
1231
+ const right = index === 0 ? delimiter[1] : index === rows.length - 1 ? delimiter[5] : delimiter[3];
1232
+ return `${left} ${row} ${right}`;
1233
+ });
1234
+ }
1235
+ if (lines.length <= 1) {
1236
+ return lines[0] ?? "";
1237
+ }
1238
+ const index = this.layoutNodes.push({ type: "matrix", lines, baseline: 0 }) - 1;
1239
+ return `${LAYOUT_MARKER_START}${index}${LAYOUT_MARKER_END}`;
1093
1240
  }
1094
1241
  renderNested(source, stackFractions = true) {
1095
- const rendered = new LatexParser(source, stackFractions ? this.layoutNodes : undefined).render();
1242
+ const rendered = new LatexParser(source, this.layoutNodes, this.display && stackFractions).render();
1096
1243
  if (rendered === undefined) {
1097
1244
  this.supported = false;
1098
1245
  return source;
@@ -1105,12 +1252,12 @@ class LatexParser {
1105
1252
  * Returns undefined when the expression contains unsupported or malformed syntax.
1106
1253
  */
1107
1254
  export function renderLatex(source, options = {}) {
1108
- const layoutNodes = options.display ? [] : undefined;
1109
- const rendered = new LatexParser(source, layoutNodes).render();
1255
+ const layoutNodes = [];
1256
+ const rendered = new LatexParser(source, layoutNodes, options.display === true).render();
1110
1257
  if (rendered === undefined) {
1111
1258
  return undefined;
1112
1259
  }
1113
- if (!layoutNodes || layoutNodes.length === 0) {
1260
+ if (layoutNodes.length === 0) {
1114
1261
  return rendered.replaceAll(PROTECTED_SPACE, " ");
1115
1262
  }
1116
1263
  const lines = renderLayout(rendered, layoutNodes).lines;