Skip to content
Open
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
34 changes: 33 additions & 1 deletion src/__tests__/plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,12 +284,44 @@ describe('Latex — tokenizer', () => {
it('tokenizes block math $$...$$', async () => {
const { default: Latex } = await import('../plugins/Latex/index.js');
const md = new XMarkdownMini({ extensions: [Latex()] });
// Block math requires newline after $$ (matching the old @alipay/markdown-x-math pattern)
// Canonical delimiters on their own lines remain supported.
const tokens = md.parse('$$\nx^2 + y^2 = z^2\n$$');
const types = tokens.map((t: any) => t.type);
expect(types).toContain('blockKatex');
});

it('keeps tokenizing newline-delimited $...$ as block math', async () => {
const { default: Latex } = await import('../plugins/Latex/index.js');
const md = new XMarkdownMini({ extensions: [Latex()] });
const tokens = md.parse('$\nx^2\n$');

expect(tokens.map((t: any) => t.type)).toEqual(['blockKatex']);
expect((tokens[0] as any).text).toBe('x^2');
});

it('tokenizes same-line $$...$$ as block math', async () => {
const { default: Latex } = await import('../plugins/Latex/index.js');
const md = new XMarkdownMini({ extensions: [Latex()] });
const tokens = md.parse('$$x^2 + y^2 = z^2$$');

expect(tokens.map((t: any) => t.type)).toEqual(['blockKatex']);
expect((tokens[0] as any).text).toBe('x^2 + y^2 = z^2');
expect((tokens[0] as any).displayMode).toBe(true);
});

it('tokenizes attached multiline $$ delimiters without consuming the following inline formula', async () => {
const { default: Latex } = await import('../plugins/Latex/index.js');
const md = new XMarkdownMini({ extensions: [Latex()] });
const tokens = md.parse(
'$$\\begin{cases}\na = 1 \\\\\nb = 2\n\\end{cases}$$\n\n$\\therefore a + b = 3$',
);

expect(tokens[0].type).toBe('blockKatex');
expect((tokens[0] as any).text).toContain('\\begin{cases}');
const paragraph = tokens.find((token: any) => token.type === 'paragraph') as any;
expect(paragraph.tokens.map((token: any) => token.type)).toContain('inlineKatex');
});

it('a `$` closing a line does not swallow inline math or split the paragraph', async () => {
// The block `start` must verify blockRule before asking marked to cut a
// paragraph — else a `$` ending a line (here an inline `$x=1$`) falsely
Expand Down
19 changes: 19 additions & 0 deletions src/__tests__/streamingFixup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,25 @@ describe('XMarkdownMini — streamingFixup option', () => {
expect(flat.some((n) => String(n.attrs?.class ?? '').includes('katex-display'))).toBe(true);
});

it('renders compact block math across split opening and closing delimiters', () => {
const md = new XMarkdownMini({ extensions: [Latex()] });
const patches: MiniNode[][] = [];

const updates = ['$', '$$x^2 + y^2 = z^2', '$$x^2 + y^2 = z^2$', '$$x^2 + y^2 = z^2$$'];
updates.forEach((content, index) => {
md.renderNodes({
content,
streaming: { hasNextChunk: index < updates.length - 1 },
onPatch: (nodes) => patches.push(nodes),
});
});

expect(patches).toHaveLength(updates.length);
const flat = flatten(patches[patches.length - 1]);
expect(flat.some((n) => String(n.attrs?.class ?? '').includes('katex-display'))).toBe(true);
expect(flat.some((n) => String(n.attrs?.class ?? '').includes('katex-inline'))).toBe(false);
});

it('semantic streaming config accepts chunkDelay for typewriter mode', () => {
vi.useFakeTimers();
try {
Expand Down
10 changes: 6 additions & 4 deletions src/plugins/Latex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ function stampFontClasses(list: MiniNode[], inherited: string): void {

// --- Tokenizers ---

const inlineRule = /^(?:\${1,2}([^\$\n]+?)\${1,2}|\\\((.+?)\\\))/;
const blockRule = /^(\${1,2})\n([\s\S]+?)\n\1(?:\n|$)|^\\\[((?:\\.|[^\\])+?)\\\]/;
const inlineRule = /^(?:\$(?!\$)([^$\n]+?)\$(?!\$)|\\\((.+?)\\\))/;
const blockRule =
/^(?:(\${1,2})\n([\s\S]+?)\n\1|\$\$([\s\S]+?)\$\$)(?:\n|$)|^\\\[((?:\\.|[^\\])+?)\\\]/;

function inlineKatexTokenizer(this: any, src: string): Tokens.Generic | undefined {
const match = src.match(inlineRule);
Expand All @@ -112,7 +113,7 @@ function inlineKatexTokenizer(this: any, src: string): Tokens.Generic | undefine
function blockKatexTokenizer(this: any, src: string): Tokens.Generic | undefined {
const match = src.match(blockRule);
if (!match) return undefined;
const text = replaceAlign((match[2] || match[3]).trim());
const text = replaceAlign((match[2] || match[3] || match[4]).trim());
return {
type: 'blockKatex',
raw: match[0],
Expand All @@ -138,7 +139,8 @@ function blockStart(src: string): number | undefined {
const bracket = src.indexOf('\\[', from);
if (dollar === -1 && bracket === -1) return undefined;
const index = Math.min(dollar === -1 ? Infinity : dollar, bracket === -1 ? Infinity : bracket);
if (blockRule.test(src.slice(index))) return index;
const startsLine = index === 0 || src.charCodeAt(index - 1) === 10;
if (startsLine && blockRule.test(src.slice(index))) return index;
from = index + 1;
}
return undefined;
Expand Down