Skip to content

Commit 2790803

Browse files
authored
⦕ Complex brackets (#23)
Fixes brackets to be spelled out if they are not balanced. Fixes jupyter-book/mystmd#1766
1 parent b7f4ba0 commit 2790803

File tree

6 files changed

+135
-23
lines changed

6 files changed

+135
-23
lines changed

src/index.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from '@unified-latex/unified-latex-util-arguments';
1010
import { typstEnvs, typstMacros, typstStrings } from './macros.js';
1111
import type { IState, LatexNode, StateData } from './types.js';
12+
import { areBracketsBalanced, BRACKETS } from './utils.js';
1213

1314
export function parseLatex(value: string) {
1415
const file = unified()
@@ -20,12 +21,18 @@ export function parseLatex(value: string) {
2021
boldsymbol: { signature: 'm' },
2122
left: { signature: 'm' },
2223
right: { signature: 'm' },
23-
Big: { signature: 'm' },
24-
Bigr: { signature: 'm' },
25-
Bigl: { signature: 'm' },
2624
big: { signature: 'm' },
2725
bigr: { signature: 'm' },
2826
bigl: { signature: 'm' },
27+
Big: { signature: 'm' },
28+
Bigr: { signature: 'm' },
29+
Bigl: { signature: 'm' },
30+
bigg: { signature: 'm' },
31+
biggr: { signature: 'm' },
32+
biggl: { signature: 'm' },
33+
Bigg: { signature: 'm' },
34+
Biggr: { signature: 'm' },
35+
Biggl: { signature: 'm' },
2936
dot: { signature: 'm' },
3037
ddot: { signature: 'm' },
3138
hat: { signature: 'm' },
@@ -150,9 +157,9 @@ class State implements IState {
150157
_value: string;
151158
data: StateData;
152159

153-
constructor() {
160+
constructor(opts?: { writeOutBrackets?: boolean }) {
154161
this._value = '';
155-
this.data = {};
162+
this.data = { writeOutBrackets: opts?.writeOutBrackets ?? false };
156163
}
157164

158165
get value() {
@@ -177,6 +184,11 @@ class State implements IState {
177184

178185
write(str?: string) {
179186
if (!str) return;
187+
if (Object.keys(BRACKETS).includes(str) && this.data.inFunction && this.data.writeOutBrackets) {
188+
this.addWhitespace();
189+
this._value += BRACKETS[str];
190+
return;
191+
}
180192
// This is a bit verbose, but the statements are much easier to read
181193
if (this._scriptsSimplified && str === '(') {
182194
this.addWhitespace();
@@ -313,9 +325,18 @@ function postProcess(typst: string) {
313325
);
314326
}
315327

316-
export function texToTypst(value: string): { value: string; macros?: Set<string> } {
328+
export function texToTypst(
329+
value: string,
330+
options?: { writeOutBrackets?: boolean },
331+
): { value: string; macros?: Set<string> } {
317332
const tree = parseLatex(value);
318333
walkLatex(tree);
319-
const state = writeTypst(tree);
320-
return { value: postProcess(state.value), macros: state.data.macros };
334+
const state = writeTypst(tree, new State({ writeOutBrackets: options?.writeOutBrackets }));
335+
const typstValue = postProcess(state.value);
336+
if (options?.writeOutBrackets || areBracketsBalanced(typstValue)) {
337+
return { value: typstValue, macros: state.data.macros };
338+
}
339+
// This could be improved to a single pass if we have an intermediate AST for writing typst
340+
// However, we are just writing this out twice at the moment if we find unbalanced brackets.
341+
return texToTypst(value, { writeOutBrackets: true });
321342
}

src/macros.ts

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { IState, LatexNode } from './types.js';
2+
import { BRACKETS } from './utils.js';
23

34
function isEmptyNode(node?: LatexNode): boolean {
45
if (!node?.content || node.content.length === 0) return true;
@@ -17,22 +18,12 @@ export const typstStrings: Record<string, string | ((state: IState) => string)>
1718
'"': '\\"',
1819
};
1920

20-
const brackets: Record<string, string> = {
21-
'[': 'bracket.l',
22-
']': 'bracket.r',
23-
'{': 'brace.l',
24-
'}': 'brace.r',
25-
'(': 'paren.l',
26-
')': 'paren.r',
27-
'|': 'bar.v',
28-
};
29-
3021
function createBrackets(scale: string): (state: IState, node: LatexNode) => string {
3122
return (state: IState, node: LatexNode) => {
3223
const args = node.args;
3324
node.args = [];
3425
const b = (args?.[0].content?.[0] as LatexNode).content as string;
35-
const typstB = brackets[b];
26+
const typstB = BRACKETS[b];
3627
if (!typstB) throw new Error(`Undefined left bracket: ${b}`);
3728
return `#scale(x: ${scale}, y: ${scale})[$${typstB}$]`;
3829
};
@@ -103,12 +94,18 @@ export const typstMacros: Record<string, string | ((state: IState, node: LatexNo
10394
splitStrings(node);
10495
return '^';
10596
},
97+
big: createBrackets('120%'),
10698
bigl: createBrackets('120%'),
10799
bigr: createBrackets('120%'),
108-
big: createBrackets('120%'),
100+
Big: createBrackets('180%'),
109101
Bigl: createBrackets('180%'),
110102
Bigr: createBrackets('180%'),
111-
Big: createBrackets('180%'),
103+
bigg: createBrackets('240%'),
104+
biggr: createBrackets('240%'),
105+
biggl: createBrackets('240%'),
106+
Bigg: createBrackets('300%'),
107+
Biggl: createBrackets('300%'),
108+
Biggr: createBrackets('300%'),
112109
left: (state, node) => {
113110
const args = node.args;
114111
node.args = [];
@@ -202,8 +199,6 @@ export const typstMacros: Record<string, string | ((state: IState, node: LatexNo
202199
lfloor: 'floor.l',
203200
rfloor: 'floor.r',
204201
implies: 'arrow.r.double.long',
205-
biggl: '',
206-
biggr: '',
207202
' ': '" "',
208203
mathbb: (state, node) => {
209204
const text =

src/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ export type LatexNode = {
88
} & Record<string, any>;
99

1010
export type StateData = {
11+
/**
12+
* In the `writeTypst` function, we first try writing
13+
* in simple mode with just the brackets/braces/parens
14+
* then check to see if the output string is balanced
15+
*
16+
* If so, then we return that, if not, we run the
17+
* `writeTypst` function again with this flag to ensure
18+
* brackets in a function will be written out.
19+
*
20+
* This ensures that the simple outputs like `e_(f (x))`
21+
* Stay simple, but `e_(f[x) g_(,y])` will spell out the
22+
* brackets that are content.
23+
*/
24+
writeOutBrackets?: boolean;
1125
inFunction?: boolean;
1226
inArray?: boolean;
1327
previousMatRows?: number;

src/utils.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { areBracketsBalanced } from './utils';
3+
4+
describe('areBracketsBalanced', () => {
5+
it.each([
6+
// Each test case is an object (or array) with the inputs and expected result
7+
{ input: '', expected: true },
8+
{ input: '()', expected: true },
9+
{ input: '()[]{}', expected: true },
10+
{ input: '([{}])', expected: true },
11+
{ input: '(([]){})', expected: true },
12+
{ input: '{([([])])}', expected: true },
13+
{ input: '(]', expected: false },
14+
{ input: '([)]', expected: false },
15+
{ input: '(()', expected: false },
16+
{ input: '(()]', expected: false },
17+
])('should return $expected for "$input"', ({ input, expected }) => {
18+
expect(areBracketsBalanced(input)).toBe(expected);
19+
});
20+
21+
it('should ignore non-bracket characters', () => {
22+
expect(areBracketsBalanced('abc(def)ghi')).toBe(true);
23+
expect(areBracketsBalanced('(abc]def')).toBe(false);
24+
});
25+
});

src/utils.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
export const BRACKETS: Record<string, string> = {
2+
'[': 'bracket.l',
3+
']': 'bracket.r',
4+
'{': 'brace.l',
5+
'}': 'brace.r',
6+
'(': 'paren.l',
7+
')': 'paren.r',
8+
'|': 'bar.v',
9+
lfloor: 'floor.l',
10+
'⌊': 'floor.l',
11+
rfloor: 'floor.r',
12+
'⌋': 'floor.r',
13+
rceil: 'ceil.r',
14+
'⌉': 'ceil.r',
15+
lceil: 'ceil.l',
16+
'⌈': 'ceil.l',
17+
};
18+
19+
export function areBracketsBalanced(input: string): boolean {
20+
// This stack will hold the opening brackets as they appear
21+
const stack: string[] = [];
22+
23+
// A map from closing brackets to their corresponding opening bracket
24+
const bracketMap: Record<string, string> = {
25+
')': '(',
26+
']': '[',
27+
'}': '{',
28+
};
29+
30+
// Check each character in the string
31+
for (const char of input) {
32+
// If it’s an opening bracket, push it to the stack
33+
if (char === '(' || char === '[' || char === '{') {
34+
stack.push(char);
35+
}
36+
// If it’s a closing bracket, verify the top of the stack
37+
else if (char === ')' || char === ']' || char === '}') {
38+
// If stack is empty or the top of the stack doesn't match the correct opening bracket, it’s unbalanced
39+
if (!stack.length || bracketMap[char] !== stack.pop()) {
40+
return false;
41+
}
42+
}
43+
// Ignore other characters
44+
}
45+
46+
// If the stack is empty, every opening bracket had a matching closing bracket
47+
return stack.length === 0;
48+
}

tests/math.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,12 @@ cases:
314314
- title: Bigl
315315
tex: '\Bigl| \frac{\lambda-\alpha(1-\lambda)}{1-\alpha(1-\lambda)} \Bigr| < 1'
316316
typst: '#scale(x: 180%, y: 180%)[$bar.v$] frac(lambda -alpha (1 -lambda), 1 -alpha (1 -lambda)) #scale(x: 180%, y: 180%)[$bar.v$] < 1'
317+
- title: bigg
318+
tex: '\frac{1}{4i} \bigg( \frac{-i}{2} e^{2i\omega} - \frac{i}{2} e^{-2i\omega} + C_1 \bigg)'
319+
typst: 'frac(1, 4 i) #scale(x: 240%, y: 240%)[$paren.l$] frac(-i, 2) e^(2 i omega) -frac(i, 2) e^(-2 i omega) + C_1 #scale(x: 240%, y: 240%)[$paren.r$]'
320+
- title: bigr floor
321+
tex: '\bigr \rfloor'
322+
typst: '#scale(x: 120%, y: 120%)[$floor.r$]'
317323
- title: big no space
318324
tex: '\theta = \tan^{-1} \Big( \frac{y}{x} \Big)'
319325
typst: 'theta = tan^(-1) #scale(x: 180%, y: 180%)[$paren.l$] frac(y, x) #scale(x: 180%, y: 180%)[$paren.r$]'
@@ -367,3 +373,6 @@ cases:
367373
description: The space in `dot.op()` vs `dot.op ()` is important!!
368374
tex: '(dx^1 \wedge dx^2 \wedge dx^4) \cdot (\mathbf{u} \otimes \mathbf{v} \otimes \mathbf{w})='
369375
typst: '(d x^1 and d x^2 and d x^4) dot.op (bold(u) times.circle bold(v) times.circle bold(w)) ='
376+
- title: complex brackets
377+
tex: '\partial_{[i} f_{j]}'
378+
typst: 'diff_(bracket.l i) f_(j bracket.r)'

0 commit comments

Comments
 (0)