@earendil-works/pi-tui 0.84.0 → 0.84.1

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
  }
@@ -759,24 +893,27 @@ class LatexParser {
759
893
  const value = this.parseRequiredArgument(false).trim();
760
894
  const negated = NEGATED_SYMBOLS[value];
761
895
  if (negated !== undefined) {
762
- return negated;
896
+ return ` ${negated} `;
763
897
  }
764
898
  const characters = Array.from(value);
765
899
  if (characters.length === 0) {
766
900
  this.supported = false;
767
901
  return "";
768
902
  }
769
- return `${characters[0]}\u0338${characters.slice(1).join("")}`;
903
+ return ` ${characters[0]}\u0338${characters.slice(1).join("")} `;
770
904
  }
771
905
  if (LIMIT_OPERATORS.has(command)) {
772
906
  return this.parseOperator(command, "bracket", true, true);
773
907
  }
774
908
  const symbol = SYMBOLS[command];
775
909
  if (symbol !== undefined) {
776
- return DISPLAY_LIMIT_SYMBOLS.has(command) ? this.parseOperator(symbol, "script", true) : symbol;
910
+ if (DISPLAY_LIMIT_SYMBOLS.has(command)) {
911
+ return this.parseOperator(symbol, "script", true);
912
+ }
913
+ return command === "cdot" || command === "times" || RELATION_COMMANDS.has(command) ? ` ${symbol} ` : symbol;
777
914
  }
778
915
  if (NAMED_OPERATORS.has(command)) {
779
- return ` ${command} `;
916
+ return `${NAMED_OPERATOR_START}${command}${NAMED_OPERATOR_END}`;
780
917
  }
781
918
  if (SIZE_COMMANDS.has(command)) {
782
919
  return "";
@@ -788,10 +925,10 @@ class LatexParser {
788
925
  return "";
789
926
  }
790
927
  if (command === "frac" || command === "dfrac" || command === "tfrac") {
791
- const shouldStack = this.layoutNodes !== undefined && this.stackFractions && command !== "tfrac";
928
+ const shouldStack = this.display && this.stackFractions && command !== "tfrac";
792
929
  const numerator = this.parseRequiredArgument(!shouldStack);
793
930
  const denominator = this.parseRequiredArgument(!shouldStack);
794
- if (shouldStack && this.layoutNodes) {
931
+ if (shouldStack) {
795
932
  const index = this.layoutNodes.push({
796
933
  type: "fraction",
797
934
  numerator: normalizeOutput(numerator),
@@ -906,7 +1043,7 @@ class LatexParser {
906
1043
  upper = value;
907
1044
  }
908
1045
  }
909
- if (this.layoutNodes && useDisplayLimits && (lower !== undefined || upper !== undefined)) {
1046
+ if (this.display && useDisplayLimits && (lower !== undefined || upper !== undefined)) {
910
1047
  const index = this.layoutNodes.push({ type: "operator", operator, lower, upper }) - 1;
911
1048
  return `${LAYOUT_MARKER_START}${index}${LAYOUT_MARKER_END}`;
912
1049
  }
@@ -1065,34 +1202,37 @@ class LatexParser {
1065
1202
  const cell = row[column] ?? "";
1066
1203
  return `${cell}${PROTECTED_SPACE.repeat(Math.max(0, (columnWidths[column] ?? 0) - visibleWidth(cell)))}`;
1067
1204
  }).join(" │ "));
1205
+ let lines;
1068
1206
  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");
1207
+ lines = rows;
1208
+ }
1209
+ else {
1210
+ const delimiters = {
1211
+ pmatrix: ["⎛", "⎞", "⎜", "⎟", "⎝", "⎠"],
1212
+ bmatrix: ["⎡", "⎤", "⎢", "⎥", "⎣", "⎦"],
1213
+ Bmatrix: ["⎧", "⎫", "⎨", "⎬", "⎩", "⎭"],
1214
+ vmatrix: ["│", "│", "│", "│", "│", "│"],
1215
+ Vmatrix: ["║", "║", "║", "║", "║", "║"],
1216
+ };
1217
+ const delimiter = delimiters[environment];
1218
+ if (!delimiter) {
1219
+ this.supported = false;
1220
+ return rows.join("\n");
1221
+ }
1222
+ lines = rows.map((row, index) => {
1223
+ const left = index === 0 ? delimiter[0] : index === rows.length - 1 ? delimiter[4] : delimiter[2];
1224
+ const right = index === 0 ? delimiter[1] : index === rows.length - 1 ? delimiter[5] : delimiter[3];
1225
+ return `${left} ${row} ${right}`;
1226
+ });
1227
+ }
1228
+ if (lines.length <= 1) {
1229
+ return lines[0] ?? "";
1230
+ }
1231
+ const index = this.layoutNodes.push({ type: "matrix", lines, baseline: 0 }) - 1;
1232
+ return `${LAYOUT_MARKER_START}${index}${LAYOUT_MARKER_END}`;
1093
1233
  }
1094
1234
  renderNested(source, stackFractions = true) {
1095
- const rendered = new LatexParser(source, stackFractions ? this.layoutNodes : undefined).render();
1235
+ const rendered = new LatexParser(source, this.layoutNodes, this.display && stackFractions).render();
1096
1236
  if (rendered === undefined) {
1097
1237
  this.supported = false;
1098
1238
  return source;
@@ -1105,12 +1245,12 @@ class LatexParser {
1105
1245
  * Returns undefined when the expression contains unsupported or malformed syntax.
1106
1246
  */
1107
1247
  export function renderLatex(source, options = {}) {
1108
- const layoutNodes = options.display ? [] : undefined;
1109
- const rendered = new LatexParser(source, layoutNodes).render();
1248
+ const layoutNodes = [];
1249
+ const rendered = new LatexParser(source, layoutNodes, options.display === true).render();
1110
1250
  if (rendered === undefined) {
1111
1251
  return undefined;
1112
1252
  }
1113
- if (!layoutNodes || layoutNodes.length === 0) {
1253
+ if (layoutNodes.length === 0) {
1114
1254
  return rendered.replaceAll(PROTECTED_SPACE, " ");
1115
1255
  }
1116
1256
  const lines = renderLayout(rendered, layoutNodes).lines;