Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⦕ Complex brackets #23

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@unified-latex/unified-latex-util-arguments';
import { typstEnvs, typstMacros, typstStrings } from './macros.js';
import type { IState, LatexNode, StateData } from './types.js';
import { areBracketsBalanced, BRACKETS } from './utils.js';

export function parseLatex(value: string) {
const file = unified()
Expand All @@ -20,12 +21,18 @@ export function parseLatex(value: string) {
boldsymbol: { signature: 'm' },
left: { signature: 'm' },
right: { signature: 'm' },
Big: { signature: 'm' },
Bigr: { signature: 'm' },
Bigl: { signature: 'm' },
big: { signature: 'm' },
bigr: { signature: 'm' },
bigl: { signature: 'm' },
Big: { signature: 'm' },
Bigr: { signature: 'm' },
Bigl: { signature: 'm' },
bigg: { signature: 'm' },
biggr: { signature: 'm' },
biggl: { signature: 'm' },
Bigg: { signature: 'm' },
Biggr: { signature: 'm' },
Biggl: { signature: 'm' },
dot: { signature: 'm' },
ddot: { signature: 'm' },
hat: { signature: 'm' },
Expand Down Expand Up @@ -150,9 +157,9 @@ class State implements IState {
_value: string;
data: StateData;

constructor() {
constructor(opts?: { writeOutBrackets?: boolean }) {
this._value = '';
this.data = {};
this.data = { writeOutBrackets: opts?.writeOutBrackets ?? false };
}

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

write(str?: string) {
if (!str) return;
if (Object.keys(BRACKETS).includes(str) && this.data.inFunction && this.data.writeOutBrackets) {
this.addWhitespace();
this._value += BRACKETS[str];
return;
}
// This is a bit verbose, but the statements are much easier to read
if (this._scriptsSimplified && str === '(') {
this.addWhitespace();
Expand Down Expand Up @@ -313,9 +325,18 @@ function postProcess(typst: string) {
);
}

export function texToTypst(value: string): { value: string; macros?: Set<string> } {
export function texToTypst(
value: string,
options?: { writeOutBrackets?: boolean },
): { value: string; macros?: Set<string> } {
const tree = parseLatex(value);
walkLatex(tree);
const state = writeTypst(tree);
return { value: postProcess(state.value), macros: state.data.macros };
const state = writeTypst(tree, new State({ writeOutBrackets: options?.writeOutBrackets }));
const typstValue = postProcess(state.value);
if (options?.writeOutBrackets || areBracketsBalanced(typstValue)) {
return { value: typstValue, macros: state.data.macros };
}
// This could be improved to a single pass if we have an intermediate AST for writing typst
// However, we are just writing this out twice at the moment if we find unbalanced brackets.
return texToTypst(value, { writeOutBrackets: true });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably fine for now, since the unbalanced brackets are rare, so the most common case of balanced brackets will be fast.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was my thought.

My original solution was just to do this all the time, however, that gives some pretty verbose exports for very simple things like e_(f(x)) to e_(f paren.l x paren.r), which I didn't want to do. :)

I think in the future we should have an intermediate tree that is meant for writing, and then do the unbalanced test on that tree, then write to string.

Put this PR together to unblock near term as that is going to be a larger change.

}
25 changes: 10 additions & 15 deletions src/macros.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { IState, LatexNode } from './types.js';
import { BRACKETS } from './utils.js';

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

const brackets: Record<string, string> = {
'[': 'bracket.l',
']': 'bracket.r',
'{': 'brace.l',
'}': 'brace.r',
'(': 'paren.l',
')': 'paren.r',
'|': 'bar.v',
};

function createBrackets(scale: string): (state: IState, node: LatexNode) => string {
return (state: IState, node: LatexNode) => {
const args = node.args;
node.args = [];
const b = (args?.[0].content?.[0] as LatexNode).content as string;
const typstB = brackets[b];
const typstB = BRACKETS[b];
if (!typstB) throw new Error(`Undefined left bracket: ${b}`);
return `#scale(x: ${scale}, y: ${scale})[$${typstB}$]`;
};
Expand Down Expand Up @@ -103,12 +94,18 @@ export const typstMacros: Record<string, string | ((state: IState, node: LatexNo
splitStrings(node);
return '^';
},
big: createBrackets('120%'),
bigl: createBrackets('120%'),
bigr: createBrackets('120%'),
big: createBrackets('120%'),
Big: createBrackets('180%'),
Bigl: createBrackets('180%'),
Bigr: createBrackets('180%'),
Big: createBrackets('180%'),
bigg: createBrackets('240%'),
biggr: createBrackets('240%'),
biggl: createBrackets('240%'),
Bigg: createBrackets('300%'),
Biggl: createBrackets('300%'),
Biggr: createBrackets('300%'),
left: (state, node) => {
const args = node.args;
node.args = [];
Expand Down Expand Up @@ -202,8 +199,6 @@ export const typstMacros: Record<string, string | ((state: IState, node: LatexNo
lfloor: 'floor.l',
rfloor: 'floor.r',
implies: 'arrow.r.double.long',
biggl: '',
biggr: '',
' ': '" "',
mathbb: (state, node) => {
const text =
Expand Down
14 changes: 14 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ export type LatexNode = {
} & Record<string, any>;

export type StateData = {
/**
* In the `writeTypst` function, we first try writing
* in simple mode with just the brackets/braces/parens
* then check to see if the output string is balanced
*
* If so, then we return that, if not, we run the
* `writeTypst` function again with this flag to ensure
* brackets in a function will be written out.
*
* This ensures that the simple outputs like `e_(f (x))`
* Stay simple, but `e_(f[x) g_(,y])` will spell out the
* brackets that are content.
*/
writeOutBrackets?: boolean;
inFunction?: boolean;
inArray?: boolean;
previousMatRows?: number;
Expand Down
25 changes: 25 additions & 0 deletions src/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, it, expect } from 'vitest';
import { areBracketsBalanced } from './utils';

describe('areBracketsBalanced', () => {
it.each([
// Each test case is an object (or array) with the inputs and expected result
{ input: '', expected: true },
{ input: '()', expected: true },
{ input: '()[]{}', expected: true },
{ input: '([{}])', expected: true },
{ input: '(([]){})', expected: true },
{ input: '{([([])])}', expected: true },
{ input: '(]', expected: false },
{ input: '([)]', expected: false },
{ input: '(()', expected: false },
{ input: '(()]', expected: false },
])('should return $expected for "$input"', ({ input, expected }) => {
expect(areBracketsBalanced(input)).toBe(expected);
});

it('should ignore non-bracket characters', () => {
expect(areBracketsBalanced('abc(def)ghi')).toBe(true);
expect(areBracketsBalanced('(abc]def')).toBe(false);
});
});
48 changes: 48 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
export const BRACKETS: Record<string, string> = {
'[': 'bracket.l',
']': 'bracket.r',
'{': 'brace.l',
'}': 'brace.r',
'(': 'paren.l',
')': 'paren.r',
'|': 'bar.v',
lfloor: 'floor.l',
'⌊': 'floor.l',
rfloor: 'floor.r',
'⌋': 'floor.r',
rceil: 'ceil.r',
'⌉': 'ceil.r',
lceil: 'ceil.l',
'⌈': 'ceil.l',
};

export function areBracketsBalanced(input: string): boolean {
// This stack will hold the opening brackets as they appear
const stack: string[] = [];

// A map from closing brackets to their corresponding opening bracket
const bracketMap: Record<string, string> = {
')': '(',
']': '[',
'}': '{',
};

// Check each character in the string
for (const char of input) {
// If it’s an opening bracket, push it to the stack
if (char === '(' || char === '[' || char === '{') {
stack.push(char);
}
// If it’s a closing bracket, verify the top of the stack
else if (char === ')' || char === ']' || char === '}') {
// If stack is empty or the top of the stack doesn't match the correct opening bracket, it’s unbalanced
if (!stack.length || bracketMap[char] !== stack.pop()) {
return false;
}
}
// Ignore other characters
}

// If the stack is empty, every opening bracket had a matching closing bracket
return stack.length === 0;
}
9 changes: 9 additions & 0 deletions tests/math.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,12 @@ cases:
- title: Bigl
tex: '\Bigl| \frac{\lambda-\alpha(1-\lambda)}{1-\alpha(1-\lambda)} \Bigr| < 1'
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'
- title: bigg
tex: '\frac{1}{4i} \bigg( \frac{-i}{2} e^{2i\omega} - \frac{i}{2} e^{-2i\omega} + C_1 \bigg)'
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$]'
- title: bigr floor
tex: '\bigr \rfloor'
typst: '#scale(x: 120%, y: 120%)[$floor.r$]'
- title: big no space
tex: '\theta = \tan^{-1} \Big( \frac{y}{x} \Big)'
typst: 'theta = tan^(-1) #scale(x: 180%, y: 180%)[$paren.l$] frac(y, x) #scale(x: 180%, y: 180%)[$paren.r$]'
Expand Down Expand Up @@ -367,3 +373,6 @@ cases:
description: The space in `dot.op()` vs `dot.op ()` is important!!
tex: '(dx^1 \wedge dx^2 \wedge dx^4) \cdot (\mathbf{u} \otimes \mathbf{v} \otimes \mathbf{w})='
typst: '(d x^1 and d x^2 and d x^4) dot.op (bold(u) times.circle bold(v) times.circle bold(w)) ='
- title: complex brackets
tex: '\partial_{[i} f_{j]}'
typst: 'diff_(bracket.l i) f_(j bracket.r)'
Loading