tex2typst 0.3.29 → 0.4.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/index.d.ts +13 -9
- package/dist/index.js +233 -224
- package/dist/tex2typst.min.js +12 -13
- package/package.json +2 -2
- package/src/convert.ts +88 -36
- package/src/exposed-types.ts +13 -9
- package/src/generic.ts +6 -6
- package/src/index.ts +23 -12
- package/src/map.ts +1 -0
- package/src/tex-parser.ts +19 -47
- package/src/tex-tokenizer.ts +1 -0
- package/src/tex-types.ts +3 -1
- package/src/tex-writer.ts +10 -1
- package/src/typst-parser.ts +114 -160
- package/tests/cheat-sheet.test.ts +0 -42
- package/tests/cheat-sheet.toml +0 -304
- package/tests/example.ts +0 -15
- package/tests/general-symbols.test.ts +0 -22
- package/tests/general-symbols.toml +0 -755
- package/tests/integration-tex2typst.yaml +0 -89
- package/tests/struct-bidirection.yaml +0 -194
- package/tests/struct-tex2typst.yaml +0 -457
- package/tests/struct-typst2tex.yaml +0 -412
- package/tests/symbol.yml +0 -126
- package/tests/test-common.ts +0 -26
- package/tests/tex-parser.test.ts +0 -97
- package/tests/tex-to-typst.test.ts +0 -136
- package/tests/typst-parser.test.ts +0 -134
- package/tests/typst-to-tex.test.ts +0 -76
- /package/src/{util.ts → utils.ts} +0 -0
package/src/convert.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TexNode, TexToken, TexTokenType, TexFuncCall, TexGroup, TexSupSub,
|
|
2
2
|
TexText, TexBeginEnd, TexLeftRight, TexTerminal} from "./tex-types";
|
|
3
|
-
import type { Tex2TypstOptions } from "./exposed-types";
|
|
3
|
+
import type { Tex2TypstOptions, Typst2TexOptions } from "./exposed-types";
|
|
4
4
|
import { TypstFraction, TypstFuncCall, TypstGroup, TypstLeftright, TypstMarkupFunc, TypstMatrixLike, TypstNode, TypstSupsub, TypstTerminal } from "./typst-types";
|
|
5
5
|
import { TypstNamedParams } from "./typst-types";
|
|
6
6
|
import { TypstSupsubData } from "./typst-types";
|
|
@@ -8,7 +8,7 @@ import { TypstToken } from "./typst-types";
|
|
|
8
8
|
import { TypstTokenType } from "./typst-types";
|
|
9
9
|
import { symbolMap, reverseSymbolMap } from "./map";
|
|
10
10
|
import { array_includes, array_intersperse, array_split } from "./generic";
|
|
11
|
-
import { assert } from "./
|
|
11
|
+
import { assert } from "./utils";
|
|
12
12
|
import { TEX_BINARY_COMMANDS, TEX_UNARY_COMMANDS } from "./tex-tokenizer";
|
|
13
13
|
|
|
14
14
|
|
|
@@ -224,7 +224,7 @@ function appendWithBracketsIfNeeded(node: TypstNode): TypstNode {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
export function convert_tex_node_to_typst(abstractNode: TexNode, options: Tex2TypstOptions
|
|
227
|
+
export function convert_tex_node_to_typst(abstractNode: TexNode, options: Tex2TypstOptions): TypstNode {
|
|
228
228
|
switch (abstractNode.type) {
|
|
229
229
|
case 'terminal': {
|
|
230
230
|
const node = abstractNode as TexTerminal;
|
|
@@ -433,6 +433,22 @@ export function convert_tex_node_to_typst(abstractNode: TexNode, options: Tex2Ty
|
|
|
433
433
|
);
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
// \not X -> X.not
|
|
437
|
+
if (node.head.value === '\\not') {
|
|
438
|
+
const sym = convert_tex_node_to_typst(node.args[0], options);
|
|
439
|
+
assert(sym.type === "terminal");
|
|
440
|
+
if(sym.head.type === TypstTokenType.SYMBOL) {
|
|
441
|
+
return new TypstToken(TypstTokenType.SYMBOL, sym.head.value + '.not').toNode();
|
|
442
|
+
} else {
|
|
443
|
+
switch(sym.head.value) {
|
|
444
|
+
case '=':
|
|
445
|
+
return new TypstToken(TypstTokenType.SYMBOL, 'eq.not').toNode();
|
|
446
|
+
default:
|
|
447
|
+
throw new Error(`Not supported: \\not ${sym.head.value}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
436
452
|
if (node.head.value === '\\overset') {
|
|
437
453
|
return convert_overset(node, options);
|
|
438
454
|
}
|
|
@@ -573,14 +589,6 @@ const TYPST_BINARY_FUNCTIONS: string[] = [
|
|
|
573
589
|
];
|
|
574
590
|
*/
|
|
575
591
|
|
|
576
|
-
function apply_escape_if_needed(c: TexToken): TexToken {
|
|
577
|
-
if (['{', '}', '%'].includes(c.value)) {
|
|
578
|
-
return new TexToken(TexTokenType.ELEMENT, '\\' + c.value);
|
|
579
|
-
}
|
|
580
|
-
return c;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
|
|
584
592
|
function typst_token_to_tex(token: TypstToken): TexToken {
|
|
585
593
|
switch (token.type) {
|
|
586
594
|
case TypstTokenType.NONE:
|
|
@@ -588,12 +596,28 @@ function typst_token_to_tex(token: TypstToken): TexToken {
|
|
|
588
596
|
return TexToken.EMPTY;
|
|
589
597
|
case TypstTokenType.SYMBOL: {
|
|
590
598
|
const _typst_symbol_to_tex = function(symbol: string): string {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
599
|
+
switch(symbol) {
|
|
600
|
+
case 'eq':
|
|
601
|
+
return '=';
|
|
602
|
+
case 'plus':
|
|
603
|
+
return '+';
|
|
604
|
+
case 'minus':
|
|
605
|
+
return '-';
|
|
606
|
+
case 'percent':
|
|
607
|
+
return '%';
|
|
608
|
+
default: {
|
|
609
|
+
if (reverseSymbolMap.has(symbol)) {
|
|
610
|
+
return '\\' + reverseSymbolMap.get(symbol);
|
|
611
|
+
} else {
|
|
612
|
+
return '\\' + symbol;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
595
615
|
}
|
|
596
616
|
}
|
|
617
|
+
if (token.value.endsWith('.not')) {
|
|
618
|
+
const sym = _typst_symbol_to_tex(token.value.slice(0, -4));
|
|
619
|
+
return new TexToken(TexTokenType.COMMAND, sym.startsWith('\\') ? `\\not${sym}` : `\\not ${sym}`);
|
|
620
|
+
}
|
|
597
621
|
return new TexToken(TexTokenType.COMMAND, _typst_symbol_to_tex(token.value));
|
|
598
622
|
}
|
|
599
623
|
case TypstTokenType.ELEMENT: {
|
|
@@ -637,7 +661,9 @@ function typst_token_to_tex(token: TypstToken): TexToken {
|
|
|
637
661
|
|
|
638
662
|
const TEX_NODE_COMMA = new TexToken(TexTokenType.ELEMENT, ',').toNode();
|
|
639
663
|
|
|
640
|
-
export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
664
|
+
export function convert_typst_node_to_tex(abstractNode: TypstNode, options: Typst2TexOptions): TexNode {
|
|
665
|
+
const convert_node = (node: TypstNode) => convert_typst_node_to_tex(node, options);
|
|
666
|
+
|
|
641
667
|
switch (abstractNode.type) {
|
|
642
668
|
case 'terminal': {
|
|
643
669
|
const node = abstractNode as TypstTerminal;
|
|
@@ -676,7 +702,7 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
676
702
|
|
|
677
703
|
case 'group': {
|
|
678
704
|
const node = abstractNode as TypstGroup;
|
|
679
|
-
const args = node.items.map(
|
|
705
|
+
const args = node.items.map(convert_node);
|
|
680
706
|
const alignment_char = new TexToken(TexTokenType.CONTROL, '&').toNode();
|
|
681
707
|
const newline_char = new TexToken(TexTokenType.CONTROL, '\\\\').toNode();
|
|
682
708
|
if (array_includes(args, alignment_char)) {
|
|
@@ -693,7 +719,7 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
693
719
|
}
|
|
694
720
|
case 'leftright': {
|
|
695
721
|
const node = abstractNode as TypstLeftright;
|
|
696
|
-
const body =
|
|
722
|
+
const body = convert_node(node.body);
|
|
697
723
|
let left = node.left? typst_token_to_tex(node.left) : new TexToken(TexTokenType.ELEMENT, '.');
|
|
698
724
|
let right = node.right? typst_token_to_tex(node.right) : new TexToken(TexTokenType.ELEMENT, '.');
|
|
699
725
|
// const is_over_high = node.isOverHigh();
|
|
@@ -715,7 +741,7 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
715
741
|
// `\left\| a + \frac{1}{3} \right\|` <- `norm(a + 1/3)`
|
|
716
742
|
case 'norm': {
|
|
717
743
|
const arg0 = node.args[0];
|
|
718
|
-
const body =
|
|
744
|
+
const body = convert_node(arg0);
|
|
719
745
|
if (node.isOverHigh()) {
|
|
720
746
|
return new TexLeftRight({
|
|
721
747
|
body: body,
|
|
@@ -736,7 +762,7 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
736
762
|
const left = "\\l" + node.head.value;
|
|
737
763
|
const right = "\\r" + node.head.value;
|
|
738
764
|
const arg0 = node.args[0];
|
|
739
|
-
const body =
|
|
765
|
+
const body = convert_node(arg0);
|
|
740
766
|
const left_node = new TexToken(TexTokenType.COMMAND, left);
|
|
741
767
|
const right_node = new TexToken(TexTokenType.COMMAND, right);
|
|
742
768
|
if (node.isOverHigh()) {
|
|
@@ -752,22 +778,22 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
752
778
|
// special hook for root
|
|
753
779
|
case 'root': {
|
|
754
780
|
const [degree, radicand] = node.args;
|
|
755
|
-
const data =
|
|
756
|
-
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\sqrt'), [
|
|
781
|
+
const data = convert_node(degree);
|
|
782
|
+
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\sqrt'), [convert_node(radicand)], data);
|
|
757
783
|
}
|
|
758
784
|
// special hook for overbrace and underbrace
|
|
759
785
|
case 'overbrace':
|
|
760
786
|
case 'underbrace': {
|
|
761
787
|
const [body, label] = node.args;
|
|
762
|
-
const base = new TexFuncCall(typst_token_to_tex(node.head), [
|
|
763
|
-
const script =
|
|
788
|
+
const base = new TexFuncCall(typst_token_to_tex(node.head), [convert_node(body)]);
|
|
789
|
+
const script = convert_node(label);
|
|
764
790
|
const data = node.head.value === 'overbrace' ? { base, sup: script, sub: null } : { base, sub: script, sup: null };
|
|
765
791
|
return new TexSupSub(data);
|
|
766
792
|
}
|
|
767
793
|
// special hook for vec
|
|
768
794
|
// "vec(a, b, c)" -> "\begin{pmatrix}a\\ b\\ c\end{pmatrix}"
|
|
769
795
|
case 'vec': {
|
|
770
|
-
const tex_matrix = node.args.map(
|
|
796
|
+
const tex_matrix = node.args.map(arg => [convert_node(arg)]);
|
|
771
797
|
return new TexBeginEnd(new TexToken(TexTokenType.LITERAL, 'pmatrix'), tex_matrix);
|
|
772
798
|
}
|
|
773
799
|
// special hook for op
|
|
@@ -798,21 +824,47 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
798
824
|
}
|
|
799
825
|
return new TexFuncCall(
|
|
800
826
|
new TexToken(TexTokenType.COMMAND, command),
|
|
801
|
-
[
|
|
827
|
+
[convert_node(node.args[1])]
|
|
802
828
|
);
|
|
803
829
|
}
|
|
830
|
+
// display(...) -> \displaystyle ... \textstyle
|
|
831
|
+
// The postprocessor will remove \textstyle if it is the end of the math code
|
|
832
|
+
case 'display': {
|
|
833
|
+
const arg0 = node.args[0];
|
|
834
|
+
const group = new TexGroup([
|
|
835
|
+
TexToken.COMMAND_DISPLAYSTYLE.toNode(),
|
|
836
|
+
convert_node(arg0),
|
|
837
|
+
]);
|
|
838
|
+
if (!options.blockMathMode) {
|
|
839
|
+
group.items.push(TexToken.COMMAND_TEXTSTYLE.toNode());
|
|
840
|
+
}
|
|
841
|
+
return group;
|
|
842
|
+
}
|
|
843
|
+
// inline(...) -> \textstyle ... \displaystyle
|
|
844
|
+
// The postprocessor will remove \displaystyle if it is the end of the math code
|
|
845
|
+
case 'inline': {
|
|
846
|
+
const arg0 = node.args[0];
|
|
847
|
+
const group = new TexGroup([
|
|
848
|
+
TexToken.COMMAND_TEXTSTYLE.toNode(),
|
|
849
|
+
convert_node(arg0),
|
|
850
|
+
]);
|
|
851
|
+
if (options.blockMathMode) {
|
|
852
|
+
group.items.push(TexToken.COMMAND_DISPLAYSTYLE.toNode());
|
|
853
|
+
}
|
|
854
|
+
return group;
|
|
855
|
+
}
|
|
804
856
|
// general case
|
|
805
857
|
default: {
|
|
806
858
|
const func_name_tex = typst_token_to_tex(node.head);
|
|
807
859
|
const is_known_func = TEX_UNARY_COMMANDS.includes(func_name_tex.value.substring(1))
|
|
808
860
|
|| TEX_BINARY_COMMANDS.includes(func_name_tex.value.substring(1));
|
|
809
861
|
if (func_name_tex.value.length > 0 && is_known_func) {
|
|
810
|
-
return new TexFuncCall(func_name_tex, node.args.map(
|
|
862
|
+
return new TexFuncCall(func_name_tex, node.args.map(convert_node));
|
|
811
863
|
} else {
|
|
812
864
|
return new TexGroup([
|
|
813
865
|
typst_token_to_tex(node.head).toNode(),
|
|
814
866
|
new TexToken(TexTokenType.ELEMENT, '(').toNode(),
|
|
815
|
-
...array_intersperse(node.args.map(
|
|
867
|
+
...array_intersperse(node.args.map(convert_node), TEX_NODE_COMMA),
|
|
816
868
|
new TexToken(TexTokenType.ELEMENT, ')').toNode()
|
|
817
869
|
]);
|
|
818
870
|
}
|
|
@@ -828,7 +880,7 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
828
880
|
const color = node.options['fill'];
|
|
829
881
|
return new TexFuncCall(
|
|
830
882
|
new TexToken(TexTokenType.COMMAND, '\\textcolor'),
|
|
831
|
-
[
|
|
883
|
+
[convert_node(color), convert_node(node.fragments[0])]
|
|
832
884
|
)
|
|
833
885
|
}
|
|
834
886
|
}
|
|
@@ -840,8 +892,8 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
840
892
|
case 'supsub': {
|
|
841
893
|
const node = abstractNode as TypstSupsub;
|
|
842
894
|
const { base, sup, sub } = node;
|
|
843
|
-
const sup_tex = sup?
|
|
844
|
-
const sub_tex = sub?
|
|
895
|
+
const sup_tex = sup? convert_node(sup) : null;
|
|
896
|
+
const sub_tex = sub? convert_node(sub) : null;
|
|
845
897
|
|
|
846
898
|
// special hook for limits
|
|
847
899
|
// `limits(+)^a` -> `\overset{a}{+}`
|
|
@@ -849,7 +901,7 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
849
901
|
// `limits(+)_a^b` -> `\overset{b}{\underset{a}{+}}`
|
|
850
902
|
if (base.head.eq(new TypstToken(TypstTokenType.SYMBOL, 'limits'))) {
|
|
851
903
|
const limits = base as TypstFuncCall;
|
|
852
|
-
const body_in_limits =
|
|
904
|
+
const body_in_limits = convert_node(limits.args[0]);
|
|
853
905
|
if (sup_tex !== null && sub_tex === null) {
|
|
854
906
|
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\overset'), [sup_tex, body_in_limits]);
|
|
855
907
|
} else if (sup_tex === null && sub_tex !== null) {
|
|
@@ -860,7 +912,7 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
860
912
|
}
|
|
861
913
|
}
|
|
862
914
|
|
|
863
|
-
const base_tex =
|
|
915
|
+
const base_tex = convert_node(base);
|
|
864
916
|
|
|
865
917
|
const res = new TexSupSub({
|
|
866
918
|
base: base_tex,
|
|
@@ -871,7 +923,7 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
871
923
|
}
|
|
872
924
|
case 'matrixLike': {
|
|
873
925
|
const node = abstractNode as TypstMatrixLike;
|
|
874
|
-
const tex_matrix = node.matrix.map(row => row.map(
|
|
926
|
+
const tex_matrix = node.matrix.map(row => row.map(convert_node));
|
|
875
927
|
if (node.head.eq(TypstMatrixLike.MAT)) {
|
|
876
928
|
let env_type = 'pmatrix'; // typst mat use delim:"(" by default
|
|
877
929
|
if (node.options) {
|
|
@@ -918,8 +970,8 @@ export function convert_typst_node_to_tex(abstractNode: TypstNode): TexNode {
|
|
|
918
970
|
case 'fraction': {
|
|
919
971
|
const node = abstractNode as TypstFraction;
|
|
920
972
|
const [numerator, denominator] = node.args;
|
|
921
|
-
const num_tex =
|
|
922
|
-
const den_tex =
|
|
973
|
+
const num_tex = convert_node(numerator);
|
|
974
|
+
const den_tex = convert_node(denominator);
|
|
923
975
|
return new TexFuncCall(new TexToken(TexTokenType.COMMAND, '\\frac'), [num_tex, den_tex]);
|
|
924
976
|
}
|
|
925
977
|
default:
|
package/src/exposed-types.ts
CHANGED
|
@@ -6,17 +6,21 @@
|
|
|
6
6
|
* Any undocumented options may be not working at present or break in the future!
|
|
7
7
|
*/
|
|
8
8
|
export interface Tex2TypstOptions {
|
|
9
|
-
nonStrict
|
|
10
|
-
preferShorthands
|
|
11
|
-
keepSpaces
|
|
12
|
-
fracToSlash
|
|
13
|
-
inftyToOo
|
|
14
|
-
optimize
|
|
15
|
-
customTexMacros
|
|
9
|
+
nonStrict: boolean; /** default is true */
|
|
10
|
+
preferShorthands: boolean; /** default is true */
|
|
11
|
+
keepSpaces: boolean; /** default is false */
|
|
12
|
+
fracToSlash: boolean; /** default is true */
|
|
13
|
+
inftyToOo: boolean; /** default is false */
|
|
14
|
+
optimize: boolean; /** default is true */
|
|
15
|
+
customTexMacros: { [key: string]: string; };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export
|
|
19
|
-
|
|
18
|
+
export interface Typst2TexOptions {
|
|
19
|
+
blockMathMode: boolean; /** default is true */
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare function tex2typst(tex: string, options?: Partial<Tex2TypstOptions>): string;
|
|
23
|
+
export declare function typst2tex(typst: string, options?: Partial<Typst2TexOptions>): string;
|
|
20
24
|
|
|
21
25
|
export declare const symbolMap: Map<string, string>;
|
|
22
26
|
export declare const shorthandMap: Map<string, string>;
|
package/src/generic.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
interface IEquatable {
|
|
2
|
-
eq(other:
|
|
1
|
+
interface IEquatable<T> {
|
|
2
|
+
eq(other: T): boolean;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
export function array_equal<T extends IEquatable
|
|
5
|
+
export function array_equal<T extends IEquatable<T>>(a: T[], b: T[]): boolean {
|
|
6
6
|
/*
|
|
7
7
|
if (a.length !== b.length) {
|
|
8
8
|
return false;
|
|
@@ -17,7 +17,7 @@ export function array_equal<T extends IEquatable>(a: T[], b: T[]): boolean {
|
|
|
17
17
|
return a.length === b.length && a.every((x, i) => x.eq(b[i]));
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function array_find<T extends IEquatable
|
|
20
|
+
export function array_find<T extends IEquatable<T>>(array: T[], item: T, start: number = 0): number {
|
|
21
21
|
/*
|
|
22
22
|
for (let i = start; i < array.length; i++) {
|
|
23
23
|
if (array[i].eq(item)) {
|
|
@@ -30,7 +30,7 @@ export function array_find<T extends IEquatable>(array: T[], item: T, start: num
|
|
|
30
30
|
return index === -1 ? -1 : index + start;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function array_includes<T extends IEquatable
|
|
33
|
+
export function array_includes<T extends IEquatable<T>>(array: T[], item: T): boolean {
|
|
34
34
|
/*
|
|
35
35
|
for (const x of array) {
|
|
36
36
|
if (x.eq(item)) {
|
|
@@ -44,7 +44,7 @@ export function array_includes<T extends IEquatable>(array: T[], item: T): boole
|
|
|
44
44
|
|
|
45
45
|
// e.g. input array=['a', 'b', '+', 'c', '+', 'd', 'e'], sep = '+'
|
|
46
46
|
// return [['a', 'b'], ['c'], ['d', 'e']]
|
|
47
|
-
export function array_split<T extends IEquatable
|
|
47
|
+
export function array_split<T extends IEquatable<T>>(array: T[], sep: T): T[][] {
|
|
48
48
|
const res: T[][] = [];
|
|
49
49
|
let current_slice: T[] = [];
|
|
50
50
|
for (const i of array) {
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseTex } from "./tex-parser";
|
|
2
|
-
import type { Tex2TypstOptions } from "./exposed-types";
|
|
2
|
+
import type { Tex2TypstOptions, Typst2TexOptions } from "./exposed-types";
|
|
3
3
|
import { TypstWriter } from "./typst-writer";
|
|
4
4
|
import { type TypstWriterOptions } from "./typst-types";
|
|
5
5
|
import { convert_tex_node_to_typst, convert_typst_node_to_tex } from "./convert";
|
|
@@ -9,7 +9,7 @@ import { TexWriter } from "./tex-writer";
|
|
|
9
9
|
import { shorthandMap } from "./typst-shorthands";
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
export function tex2typst(tex: string, options
|
|
12
|
+
export function tex2typst(tex: string, options: Partial<Tex2TypstOptions> = {}): string {
|
|
13
13
|
const opt: Tex2TypstOptions = {
|
|
14
14
|
nonStrict: true,
|
|
15
15
|
preferShorthands: true,
|
|
@@ -20,14 +20,12 @@ export function tex2typst(tex: string, options?: Tex2TypstOptions): string {
|
|
|
20
20
|
customTexMacros: {}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
if(options !==
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
opt[key as keyof Tex2TypstOptions] = options[key as keyof Tex2TypstOptions] as any;
|
|
30
|
-
}
|
|
23
|
+
if (typeof options !== 'object') {
|
|
24
|
+
throw new Error("options must be an object");
|
|
25
|
+
}
|
|
26
|
+
for (const key in opt) {
|
|
27
|
+
if (key in options) {
|
|
28
|
+
opt[key as keyof Tex2TypstOptions] = options[key as keyof Tex2TypstOptions] as any;
|
|
31
29
|
}
|
|
32
30
|
}
|
|
33
31
|
|
|
@@ -38,9 +36,22 @@ export function tex2typst(tex: string, options?: Tex2TypstOptions): string {
|
|
|
38
36
|
return writer.finalize();
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
export function typst2tex(typst: string): string {
|
|
39
|
+
export function typst2tex(typst: string, options: Partial<Typst2TexOptions> = {}): string {
|
|
40
|
+
const opt: Typst2TexOptions = {
|
|
41
|
+
blockMathMode: true,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (typeof options !== 'object') {
|
|
45
|
+
throw new Error("options must be an object");
|
|
46
|
+
}
|
|
47
|
+
for (const key in opt) {
|
|
48
|
+
if (key in options) {
|
|
49
|
+
opt[key as keyof Typst2TexOptions] = options[key as keyof Typst2TexOptions] as any;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
const typstTree = parseTypst(typst);
|
|
43
|
-
const texTree = convert_typst_node_to_tex(typstTree);
|
|
54
|
+
const texTree = convert_typst_node_to_tex(typstTree, opt);
|
|
44
55
|
const writer = new TexWriter();
|
|
45
56
|
writer.append(texTree);
|
|
46
57
|
return writer.finalize();
|
package/src/map.ts
CHANGED
package/src/tex-parser.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TexBeginEnd, TexFuncCall, TexLeftRight, TexNode, TexGroup, TexSupSub, TexSupsubData, TexText, TexToken, TexTokenType } from "./tex-types";
|
|
2
|
-
import { assert } from "./
|
|
3
|
-
import { array_find, array_join } from "./generic";
|
|
2
|
+
import { assert } from "./utils";
|
|
3
|
+
import { array_find, array_join, array_split } from "./generic";
|
|
4
4
|
import { TEX_BINARY_COMMANDS, TEX_UNARY_COMMANDS, tokenize_tex } from "./tex-tokenizer";
|
|
5
5
|
|
|
6
6
|
const IGNORED_COMMANDS = [
|
|
@@ -446,59 +446,31 @@ export class LatexParser {
|
|
|
446
446
|
// ignore whitespaces and '\n' after \begin{envName}
|
|
447
447
|
pos += eat_whitespaces(tokens, pos).length;
|
|
448
448
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
allRows.push(row);
|
|
452
|
-
let group = new TexGroup([]);
|
|
453
|
-
row.push(group);
|
|
449
|
+
let closure: TexNode;
|
|
450
|
+
[closure, pos] = this.parseClosure(tokens, pos, closingToken);
|
|
454
451
|
|
|
455
|
-
|
|
456
|
-
if (tokens[pos].eq(closingToken)) {
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const [res, newPos] = this.parseNextExpr(tokens, pos);
|
|
461
|
-
pos = newPos;
|
|
462
|
-
|
|
463
|
-
if (res.head.type === TexTokenType.SPACE || res.head.type === TexTokenType.NEWLINE) {
|
|
464
|
-
if (!this.space_sensitive && res.head.value.replace(/ /g, '').length === 0) {
|
|
465
|
-
continue;
|
|
466
|
-
}
|
|
467
|
-
if (!this.newline_sensitive && res.head.value === '\n') {
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (res.head.eq(new TexToken(TexTokenType.CONTROL, '\\\\'))) {
|
|
473
|
-
row = [];
|
|
474
|
-
group = new TexGroup([]);
|
|
475
|
-
row.push(group);
|
|
476
|
-
allRows.push(row);
|
|
477
|
-
} else if (res.head.eq(new TexToken(TexTokenType.CONTROL, '&'))) {
|
|
478
|
-
group = new TexGroup([]);
|
|
479
|
-
row.push(group);
|
|
480
|
-
} else {
|
|
481
|
-
group.items.push(res);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (pos >= tokens.length) {
|
|
452
|
+
if (pos === -1) {
|
|
486
453
|
return [[], -1];
|
|
487
454
|
}
|
|
488
455
|
|
|
489
|
-
|
|
490
|
-
if (
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
last_cell_items.pop();
|
|
496
|
-
}
|
|
456
|
+
let allRows: TexNode[][];
|
|
457
|
+
if (closure.type === 'ordgroup') {
|
|
458
|
+
const elements = (closure as TexGroup).items;
|
|
459
|
+
// ignore spaces and '\n' before \end{envName}
|
|
460
|
+
while(elements.length > 0 && [TexTokenType.SPACE, TexTokenType.NEWLINE].includes(elements[elements.length - 1].head.type)) {
|
|
461
|
+
elements.pop();
|
|
497
462
|
}
|
|
463
|
+
allRows = array_split(elements, new TexToken(TexTokenType.CONTROL, '\\\\').toNode())
|
|
464
|
+
.map(row => {
|
|
465
|
+
return array_split(row, new TexToken(TexTokenType.CONTROL, '&').toNode())
|
|
466
|
+
.map(arr => new TexGroup(arr));
|
|
467
|
+
});
|
|
468
|
+
} else {
|
|
469
|
+
allRows = [[closure]];
|
|
498
470
|
}
|
|
499
471
|
|
|
500
472
|
this.alignmentDepth--;
|
|
501
|
-
return [allRows, pos
|
|
473
|
+
return [allRows, pos];
|
|
502
474
|
}
|
|
503
475
|
}
|
|
504
476
|
|
package/src/tex-tokenizer.ts
CHANGED
package/src/tex-types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assert } from "./
|
|
1
|
+
import { assert } from "./utils";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* ELEMENT: 0-9, a-z, A-Z, punctuations such as +-/*,:; etc.
|
|
@@ -44,6 +44,8 @@ export class TexToken {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
public static readonly EMPTY = new TexToken(TexTokenType.EMPTY, '');
|
|
47
|
+
public static readonly COMMAND_DISPLAYSTYLE = new TexToken(TexTokenType.COMMAND, '\\displaystyle');
|
|
48
|
+
public static readonly COMMAND_TEXTSTYLE = new TexToken(TexTokenType.COMMAND, '\\textstyle');
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
|
package/src/tex-writer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TexNode, TexToken, writeTexTokenBuffer } from "./tex-types";
|
|
1
|
+
import { TexNode, TexToken, TexTokenType, writeTexTokenBuffer } from "./tex-types";
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
@@ -12,6 +12,15 @@ export class TexWriter {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
protected flushQueue() {
|
|
15
|
+
// remove \textstyle or \displaystyle if it is the end of the math code
|
|
16
|
+
while (this.queue.length > 0) {
|
|
17
|
+
const last_token = this.queue[this.queue.length - 1];
|
|
18
|
+
if (last_token.eq(TexToken.COMMAND_DISPLAYSTYLE) || last_token.eq(TexToken.COMMAND_TEXTSTYLE)) {
|
|
19
|
+
this.queue.pop();
|
|
20
|
+
} else {
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
15
24
|
for (let i = 0; i < this.queue.length; i++) {
|
|
16
25
|
this.buffer = writeTexTokenBuffer(this.buffer, this.queue[i]);
|
|
17
26
|
}
|