diff --git a/package-lock.json b/package-lock.json index 80fefe8..3a43305 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "render-latex", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "render-latex", - "version": "1.0.1", + "version": "1.0.2", "license": "ISC", "dependencies": { "katex": "^0.16.22" diff --git a/package.json b/package.json index b82bbda..45d6748 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "LICENSE" ], "scripts": { - "dev": "tsc && node ./dist/index.js", "build": "tsc", "test": "jest --coverage", "test:serve": "jest --coverage && cd ./coverage/lcov-report && echo \"Code coverage server running at http://$(hostname -I | awk '{print $1}'):8005/\nHit CTRL-C to stop the server\" && npx http-server -p 8005 -s" diff --git a/src/index.d.ts b/src/index.d.ts deleted file mode 100644 index 59191a0..0000000 --- a/src/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Parses and renders LaTeX content in a string using KaTeX. - * - * @param {string} input - The input string containing LaTeX content. - * @returns {string} The rendered output with LaTeX content replaced by their corresponding HTML representation. - */ -declare const renderMath: (input: string) => string; -export { renderMath }; \ No newline at end of file diff --git a/src/index.test.ts b/src/index.test.ts index fdc6e50..f48c4e2 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,253 +1,325 @@ -import * as katex from 'katex'; -import { - renderMath, - parseDelimiters, - ParsedSegment, - escapeRegex, - replacePredefinedEscapeSequences, - unescapeCharacters, - revertEscapedCharacters, -} from './index'; - -// Mock the katex library to control its behavior in tests. -jest.mock('katex', () => ({ +import * as katex from "katex"; +import { renderMath, parseDelimiters, ParsedSegment, escapeRegex, replacePredefinedEscapeSequences, unescapeCharacters, revertEscapedCharacters } from "./index"; + +jest.mock("katex", () => ({ renderToString: jest.fn((str, options) => { - // Simple mock: return the input string, noting if it's display mode. - const displayClass = options?.displayMode ? 'katex-display' : ''; - // Simulate a KaTeX parse error for testing the error handling. - if (str.includes('\\invalid')) { - throw new Error('ParseError: KaTeX parse error: \\invalid command'); + const displayClass = options?.displayMode ? "katex-display" : ""; + const classList = ["katex", displayClass].filter(Boolean).join(" "); + + if (str.includes("\\invalid")) { + throw new Error("ParseError: KaTeX parse error: \\invalid command"); } - return `${str}`; + return `${str}`; }), })); -// Create a typed mock for easier use and type-safe assertions. const mockedKatex = katex as jest.Mocked; -// Clear mocks before each test to ensure a clean slate. beforeEach(() => { mockedKatex.renderToString.mockClear(); }); -describe('Helper Functions', () => { - describe('escapeRegex', () => { - it('should escape special regex characters', () => { - const input = '.*+?^${}()|[]\\'; - const expected = '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\'; +describe("Helper Functions", () => { + describe("escapeRegex", () => { + it("should escape all special regex characters", () => { + const input = ".*+?^${}()|[]\\"; + const expected = "\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\"; expect(escapeRegex(input)).toBe(expected); }); + + it("should return an empty string if given an empty string", () => { + expect(escapeRegex("")).toBe(""); + }); + + it("should not alter a string with no special characters", () => { + const input = "abc123"; + expect(escapeRegex(input)).toBe(input); + }); }); - describe('Escape/Unescape Utilities', () => { - // Placeholders are used to prevent `\$` and `\\` from being treated as delimiters. - const escapes = { '\\\\': '𜰀', '\\$': '𜰃' }; - const unEscapes = { '𜰀': '\\', '𜰃': '$' }; - // For KaTeX, a literal backslash `\` must be escaped as `\\`. - const mathUnEscapes = { '𜰀': '\\\\', '𜰃': '$' }; + describe("Escape/Unescape Utilities", () => { + const escapes = { "\\\\": "𜰀", "\\$": "𜰃" }; + const unEscapes = { "𜰀": "\\", "𜰃": "$" }; + const mathUnEscapes = { "𜰀": "\\\\", "𜰃": "$" }; - it('replacePredefinedEscapeSequences should replace a sequence with its placeholder', () => { - const input = 'This is a test with \\$ and another \\$'; - const expected = `This is a test with ${escapes['\\$']} and another ${escapes['\\$']}`; - expect(replacePredefinedEscapeSequences(input, '\\$')).toBe(expected); + it("replacePredefinedEscapeSequences should replace a sequence with its placeholder", () => { + const input = "This is a test with \\$ and another \\$"; + const expected = `This is a test with ${escapes["\\$"]} and another ${escapes["\\$"]}`; + expect(replacePredefinedEscapeSequences(input, "\\$")).toBe(expected); }); - it('replacePredefinedEscapeSequences should throw an error for an undefined sequence', () => { - const input = 'Some text'; - const invalidSequence = '\\unknown'; // A sequence not in the `escapes` map. - expect(() => replacePredefinedEscapeSequences(input, invalidSequence)).toThrow( - 'Escape sequence "\\unknown" not found in escapes map.' - ); + it("replacePredefinedEscapeSequences should throw an error for an undefined sequence", () => { + const input = "Some text"; + const invalidSequence = "\\unknown"; // A sequence not in the `escapes` map. + expect(() => replacePredefinedEscapeSequences(input, invalidSequence)).toThrow('Escape sequence "\\unknown" not found in escapes map.'); }); - it('unescapeCharacters should revert placeholders to plain text characters', () => { - const input = `This has a placeholder ${escapes['\\$']} and a backslash ${escapes['\\\\']}.`; + it("unescapeCharacters should revert placeholders to plain text characters", () => { + const input = `This has a placeholder ${escapes["\\$"]} and a backslash ${escapes["\\\\"]}.`; const expected = `This has a placeholder $ and a backslash \\.`; expect(unescapeCharacters(input)).toBe(expected); }); - it('revertEscapedCharacters should revert placeholders to KaTeX-compatible sequences', () => { - const input = `E=mc^2, price is ${escapes['\\$']}5, path is C:${escapes['\\\\']}Users`; - // Note the difference: `\\` becomes `\\\\` for KaTeX to render a literal backslash. + it("revertEscapedCharacters should revert placeholders to KaTeX-compatible sequences", () => { + const input = `E=mc^2, price is ${escapes["\\$"]}5, path is C:${escapes["\\\\"]}Users`; const expected = `E=mc^2, price is $5, path is C:\\\\Users`; expect(revertEscapedCharacters(input)).toBe(expected); }); + + it("should handle empty strings for all escape/unescape functions", () => { + expect(replacePredefinedEscapeSequences("", "\\$")).toBe(""); + expect(unescapeCharacters("")).toBe(""); + expect(revertEscapedCharacters("")).toBe(""); + }); + + it("should handle strings with no placeholders for unescape functions", () => { + const input = "just plain text"; + expect(unescapeCharacters(input)).toBe(input); + expect(revertEscapedCharacters(input)).toBe(input); + }); }); }); -describe('parseDelimiters', () => { - it('should return a single text segment for input with no delimiters', () => { - const input = 'Hello world, this is plain text.'; - const expected: ParsedSegment[] = [ - { type: 'text', display: false, content: input }, - ]; +describe("parseDelimiters", () => { + it("should return a single text segment for input with no delimiters", () => { + const input = "Hello world, this is plain text."; + const expected: ParsedSegment[] = [{ type: "text", display: false, content: input }]; expect(parseDelimiters(input)).toEqual(expected); }); - it('should parse simple inline math with $...$', () => { - const input = 'An equation $E=mc^2$'; + it("should parse simple inline math with $...$", () => { + const input = "An equation $E=mc^2$"; const expected: ParsedSegment[] = [ - { type: 'text', display: false, content: 'An equation ' }, - { type: 'math', display: false, content: 'E=mc^2' }, + { type: "text", display: false, content: "An equation " }, + { type: "math", display: false, content: "E=mc^2" }, ]; expect(parseDelimiters(input)).toEqual(expected); }); - it('should parse simple display math with $$...$$', () => { - const input = 'The formula is $$a^2+b^2=c^2$$'; + it("should parse simple display math with $$...$$", () => { + const input = "The formula is $$a^2+b^2=c^2$$"; const expected: ParsedSegment[] = [ - { type: 'text', display: false, content: 'The formula is ' }, - { type: 'math', display: true, content: 'a^2+b^2=c^2' }, + { type: "text", display: false, content: "The formula is " }, + { type: "math", display: true, content: "a^2+b^2=c^2" }, ]; expect(parseDelimiters(input)).toEqual(expected); }); - it('should parse parenthetical delimiters \\(...\\) and \\[...\\]', () => { - const input = 'Inline \\(x\\) and display \\[y\\]'; + it("should parse parenthetical delimiters \\(...\\) and \\[...\\]", () => { + const input = "Inline \\(x\\) and display \\[y\\]"; const expected: ParsedSegment[] = [ - { type: 'text', display: false, content: 'Inline ' }, - { type: 'math', display: false, content: 'x' }, - { type: 'text', display: false, content: ' and display ' }, - { type: 'math', display: true, content: 'y' }, + { type: "text", display: false, content: "Inline " }, + { type: "math", display: false, content: "x" }, + { type: "text", display: false, content: " and display " }, + { type: "math", display: true, content: "y" }, ]; expect(parseDelimiters(input)).toEqual(expected); }); - it('should handle text before, between, and after math segments', () => { - const input = 'First $a$, then $b$, and finally end.'; + it("should handle text before, between, and after math segments", () => { + const input = "First $a$, then $b$, and finally end."; const expected: ParsedSegment[] = [ - { type: 'text', display: false, content: 'First ' }, - { type: 'math', display: false, content: 'a' }, - { type: 'text', display: false, content: ', then ' }, - { type: 'math', display: false, content: 'b' }, - { type: 'text', display: false, content: ', and finally end.' }, + { type: "text", display: false, content: "First " }, + { type: "math", display: false, content: "a" }, + { type: "text", display: false, content: ", then " }, + { type: "math", display: false, content: "b" }, + { type: "text", display: false, content: ", and finally end." }, ]; expect(parseDelimiters(input)).toEqual(expected); }); - it('should prioritize longer delimiters like $$ over $', () => { - const input = '$$a+b$$'; - const expected: ParsedSegment[] = [ - { type: 'math', display: true, content: 'a+b' }, - ]; - expect(parseDelimiters(input)).toEqual(expected); + it("should handle an empty string as input", () => { + expect(parseDelimiters("")).toEqual([]); }); - it('should not parse math inside an already matched block', () => { - // The inner `$b$` should be treated as part of the display math content. - const input = '$$ a $b$ c $$'; - const expected: ParsedSegment[] = [ - { type: 'math', display: true, content: ' a $b$ c ' }, - ]; - expect(parseDelimiters(input)).toEqual(expected); - }); + describe("Edge Cases and Complex Scenarios", () => { + it("should prioritize longer delimiters like $$ over $ at the same position", () => { + const input = "$$a+b$$"; + const expected: ParsedSegment[] = [{ type: "math", display: true, content: "a+b" }]; + expect(parseDelimiters(input)).toEqual(expected); + }); - it('should parse custom regex delimiters like \\begin{align}', () => { - const input = '\\begin{align}x &= 1\\\\y &= 2\\end{align}'; - const expected: ParsedSegment[] = [ - { - type: 'math', - display: true, - // The custom regex is configured to match the entire block, including delimiters. - content: '\\begin{align}x &= 1\\\\y &= 2\\end{align}', - }, - ]; - expect(parseDelimiters(input)).toEqual(expected); - }); + it("should not parse math inside an already matched block", () => { + const input = "$$ a $b$ c $$"; + const expected: ParsedSegment[] = [{ type: "math", display: true, content: " a $b$ c " }]; + expect(parseDelimiters(input)).toEqual(expected); + }); - it('should treat escaped delimiter placeholders as text', () => { - const placeholderForDollar = '𜰃'; // Represents `\$` - const input = `Price is ${placeholderForDollar}5, not $math$`; - const expected: ParsedSegment[] = [ - { type: 'text', display: false, content: `Price is ${placeholderForDollar}5, not ` }, - { type: 'math', display: false, content: 'math' }, - ]; - expect(parseDelimiters(input)).toEqual(expected); - }); + it("should handle unclosed delimiters by treating them as text", () => { + const input = "This is text with an unclosed $ delimiter"; + const expected: ParsedSegment[] = [{ type: "text", display: false, content: input }]; + expect(parseDelimiters(input)).toEqual(expected); + }); + + it("should handle mismatched delimiters by treating them as text", () => { + const input = "This is text with mismatched \\( delimiters \\]"; + const expected: ParsedSegment[] = [{ type: "text", display: false, content: input }]; + expect(parseDelimiters(input)).toEqual(expected); + }); + + it("should handle adjacent math blocks correctly", () => { + const input = "$a$$b$"; + const expected: ParsedSegment[] = [ + { type: "math", display: false, content: "a" }, + { type: "math", display: false, content: "b" }, + ]; + expect(parseDelimiters(input)).toEqual(expected); + }); + + it("should handle math at the start and end of the string", () => { + const input = "$start$ middle $end$"; + const expected: ParsedSegment[] = [ + { type: "math", display: false, content: "start" }, + { type: "text", display: false, content: " middle " }, + { type: "math", display: false, content: "end" }, + ]; + expect(parseDelimiters(input)).toEqual(expected); + }); + + it("should handle empty math content", () => { + const input = "An empty block: $$$$"; + const expected: ParsedSegment[] = [ + { type: "text", display: false, content: "An empty block: " }, + { type: "math", display: true, content: "" }, + ]; + expect(parseDelimiters(input)).toEqual(expected); + }); + + it("should treat escaped delimiter placeholders as text", () => { + const placeholderForDollar = "𜰃"; // Represents `\$` + const input = `Price is ${placeholderForDollar}5, not $math$`; + const expected: ParsedSegment[] = [ + { type: "text", display: false, content: `Price is ${placeholderForDollar}5, not ` }, + { type: "math", display: false, content: "math" }, + ]; + expect(parseDelimiters(input)).toEqual(expected); + }); - it('should handle an empty string as input', () => { - expect(parseDelimiters('')).toEqual([]); + it("should correctly parse custom regex delimiters like \\begin{align}", () => { + const alignBlock = "\\begin{align}x &= 1\\\\y &= 2\\end{align}"; + const input = `Equation: ${alignBlock}`; + const expected: ParsedSegment[] = [ + { type: "text", display: false, content: "Equation: " }, + { + type: "math", + display: true, + content: alignBlock, + }, + ]; + expect(parseDelimiters(input)).toEqual(expected); + }); }); }); -describe('renderMath', () => { - it('should render a simple string with inline math', () => { - const input = 'Let $x=5$.'; - const expected = 'Let x=5.'; +describe("renderMath", () => { + it("should render a simple string with inline math", () => { + const input = "Let $x=5$."; + const expected = 'Let x=5.'; const result = renderMath(input); expect(result).toBe(expected); expect(mockedKatex.renderToString).toHaveBeenCalledTimes(1); - expect(mockedKatex.renderToString).toHaveBeenCalledWith('x=5', { - displayMode: false, - throwOnError: false, - strict: false, - trust: true, - }); + expect(mockedKatex.renderToString).toHaveBeenCalledWith("x=5", expect.any(Object)); }); - it('should render a simple string with display math', () => { - const input = 'The formula: $$E=mc^2$$'; + it("should render a simple string with display math", () => { + const input = "The formula: $$E=mc^2$$"; const expected = 'The formula: E=mc^2'; const result = renderMath(input); expect(result).toBe(expected); expect(mockedKatex.renderToString).toHaveBeenCalledTimes(1); - expect(mockedKatex.renderToString).toHaveBeenCalledWith('E=mc^2', { - displayMode: true, - throwOnError: false, - strict: false, - trust: true, - }); + expect(mockedKatex.renderToString).toHaveBeenCalledWith("E=mc^2", expect.objectContaining({ displayMode: true })); }); - it('should treat escaped dollar signs (\\$) as literal text', () => { - const input = 'This costs \\$5.'; - const expected = 'This costs $5.'; // No KaTeX rendering should occur. + it("should treat escaped dollar signs (\\$) as literal text", () => { + const input = "This costs \\$5."; + const expected = "This costs $5."; const result = renderMath(input); expect(result).toBe(expected); expect(mockedKatex.renderToString).not.toHaveBeenCalled(); }); - it('should correctly pass escaped backslashes (\\\\) to KaTeX', () => { - // To create a single literal backslash in a JS string, you type `\\`. - // To create the string `\\`, which KaTeX needs for a newline, you must type `\\\\`. - const input = 'A matrix $\\begin{pmatrix} a \\\\ b \\end{pmatrix}$'; - const expectedMathContent = '\\begin{pmatrix} a \\\\ b \\end{pmatrix}'; - const expectedOutput = `A matrix ${expectedMathContent}`; + it("should correctly pass escaped backslashes (\\\\) to KaTeX for newlines", () => { + const input = "A matrix $\\begin{pmatrix} a \\\\ b \\end{pmatrix}$"; + const expectedMathContent = "\\begin{pmatrix} a \\\\ b \\end{pmatrix}"; + const expectedOutput = `A matrix ${expectedMathContent}`; const result = renderMath(input); expect(result).toBe(expectedOutput); expect(mockedKatex.renderToString).toHaveBeenCalledWith(expectedMathContent, expect.any(Object)); }); - it('should render content with custom delimiters like \\begin{align*}', () => { - const alignContent = '\\begin{align*}E &= mc^2\\end{align*}'; - const input = `Aligned equation: ${alignContent}`; - const expectedOutput = `Aligned equation: ${alignContent}`; - + it("should correctly un-escape a double backslash in text parts", () => { + const input = "A windows path: C:\\\\Users\\\\Test"; + const expected = "A windows path: C:\\Users\\Test"; const result = renderMath(input); - expect(result).toBe(expectedOutput); - expect(mockedKatex.renderToString).toHaveBeenCalledWith(alignContent, expect.objectContaining({ displayMode: true })); + + expect(result).toBe(expected); + expect(mockedKatex.renderToString).not.toHaveBeenCalled(); }); - it('should handle and reformat KaTeX parse errors', () => { - const input = 'This has an error: $a + \\invalid$'; + describe("Integration and Advanced Scenarios", () => { + it("should strip pre-existing placeholder characters from input before processing", () => { + const placeholderForDollar = "𜰃"; + const input = `This should not be a price $5, but this is math $x=1$`; + const maliciousInput = input.replace("$", placeholderForDollar); - // Configure the mock to throw an error that our main function will catch and reformat. - mockedKatex.renderToString.mockImplementation((str) => { - if (str.includes('\\invalid')) { - // This simulates the structure of the error message KaTeX produces. - return 'Error'; - } - return str; + const expected = 'This should not be a price 5, but this is math x=1'; + const result = renderMath(maliciousInput); + expect(result).toBe(expected); }); - const result = renderMath(input); + it("should render a complex string with multiple delimiter types", () => { + const alignContent = "\\begin{align*}E &= mc^2\\end{align*}"; + const input = `Inline \\(a\\), display \\[b\\], inline again $c$, display again $$d$$, and an environment: ${alignContent}. Final text.`; + const expected = `Inline a, display b, inline again c, display again d, and an environment: ${alignContent}. Final text.`; + + const result = renderMath(input); + expect(result).toBe(expected); + expect(mockedKatex.renderToString).toHaveBeenCalledTimes(5); + }); - // Check that the error title was replaced with the user-friendly version. - expect(result).toContain('title="LaTeX Error: something"'); - expect(result).toContain('This has an error: '); + it("should correctly handle interaction between escaped chars and delimiters", () => { + const input = "The price is \\$5, not $5$. A backslash \\\\ is not math."; + const expected = 'The price is $5, not 5. A backslash \\ is not math.'; + + const result = renderMath(input); + expect(result).toBe(expected); + expect(mockedKatex.renderToString).toHaveBeenCalledTimes(1); + }); + + it("should render empty math blocks without crashing", () => { + const input = "Empty inline: $ $, empty display: $$$$"; + const expected = 'Empty inline: , empty display: '; + const result = renderMath(input); + expect(result).toBe(expected); + }); + + it("should handle and reformat KaTeX parse errors", () => { + const input = "This has an error: $a + \\invalid$"; + + mockedKatex.renderToString.mockImplementation((str) => { + if (str.includes("\\invalid")) { + return 'Error'; + } + return str; + }); + + const result = renderMath(input); + expect(result).toContain("title=\"LaTeX Error: Unknown command '\\invalid'\""); + expect(result).toContain("This has an error: "); + }); + + it("should not reformat non-KaTeX parse errors", () => { + const input = "An error: $a + b$"; + mockedKatex.renderToString.mockImplementation(() => { + return 'Error'; + }); + + const result = renderMath(input); + expect(result).toContain('title="SomeOtherError: A different issue"'); + }); }); }); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index d500215..31e550d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ interface Delimiter { end: string; type: 'display' | 'inline'; customRegex?: RegExp; + compiledRegex?: RegExp; } interface ParsedSegment { @@ -20,6 +21,20 @@ interface Match { end: number; } +const BACKSLASH_PLACEHOLDER = '𜰀'; // Placeholder for a literal backslash +const DOLLAR_PLACEHOLDER = '𜰃'; // Placeholder for a literal dollar sign + +/** + * Escapes characters that have special meaning in regular expressions. + * Use this function to match a literal string that might contain characters + * like `.` `*` `+` `?` `^` `$` `{` `}` `(` `)` `|` `[` `]` `\`. + * @param string - The string to escape. + * @returns The string with special regex characters escaped. + */ +const escapeRegex = (string: string): string => { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +}; + const delimiters: Delimiter[] = [ { start: '$$', end: '$$', type: 'display' }, { start: '$', end: '$', type: 'inline' }, @@ -37,7 +52,18 @@ const delimiters: Delimiter[] = [ type: 'display', customRegex: /(?:\\begin{align})([\s\S]*?)(?:\\end{align})/g, }, -]; +].map(d => ({ + ...d, + type: d.type as 'display' | 'inline', + // Pre-compile regex for efficiency + compiledRegex: d.customRegex + ? new RegExp(d.customRegex.source, 'g') + : new RegExp( + String.raw`(?:${escapeRegex(d.start)})([\s\S]*?)(?:${escapeRegex(d.end)})`, + 'g' + ), +})); + /** * The escaping mechanism relies on replacing specific sequences (like `\$` or `\\`) @@ -48,37 +74,30 @@ const delimiters: Delimiter[] = [ // Maps sequences to escape to a temporary placeholder character. const escapes: Record = { - '\\\\': '𜰀', // Placeholder for a literal backslash - '\\$': '𜰃', // Placeholder for a literal dollar sign + '\\\\': BACKSLASH_PLACEHOLDER, + '\\$': DOLLAR_PLACEHOLDER, }; // Reverses the `escapes` mapping for plain text segments. const unEscapes: Record = { - '𜰀': '\\', - '𜰃': '$', + [BACKSLASH_PLACEHOLDER]: '\\', + [DOLLAR_PLACEHOLDER]: '$', }; // Reverses the `escapes` mapping for math segments. // A literal backslash in LaTeX is `\\`, so we map the placeholder to `\\\\`. const mathUnEscapes: Record = { - '𜰀': '\\\\', - '𜰃': '$', + [BACKSLASH_PLACEHOLDER]: '\\\\', + [DOLLAR_PLACEHOLDER]: '$', }; // Helper arrays derived from the escape maps. const escapedSequences: string[] = Object.values(escapes); const escapeSequences: string[] = Object.keys(escapes); -/** - * Escapes characters that have special meaning in regular expressions. - * Use this function to match a literal string that might contain characters - * like `.` `*` `+` `?` `^` `$` `{` `}` `(` `)` `|` `[` `]` `\`. - * @param string - The string to escape. - * @returns The string with special regex characters escaped. - */ -const escapeRegex = (string: string): string => { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -}; +// Pre-compile a single regex for efficient unescaping in text and math +const unescapeAllPlaceholdersRegex = new RegExp(`(${escapedSequences.map(s => escapeRegex(s)).join('|')})`, 'g'); + /** * Replaces a predefined escape sequence (e.g., `\$`) with its corresponding @@ -99,31 +118,24 @@ const replacePredefinedEscapeSequences = (input: string, sequenceToReplace: stri /** * Unescapes characters in non-math text content by reverting internal placeholders - * (e.g., `𜰃`) back to their original representations (e.g., `$`). + * (e.g., `DOLLAR_PLACEHOLDER`) back to their original representations (e.g., `$`). * @param input - The string to unescape. * @returns The unescaped string. */ const unescapeCharacters = (input: string): string => { - let unescaped = input; - escapedSequences.forEach((sequence: string) => { - unescaped = unescaped.replaceAll(sequence, unEscapes[sequence]); - }); - return unescaped; + return input.replace(unescapeAllPlaceholdersRegex, (match: string) => unEscapes[match]); }; /** * Reverts internal placeholders in math content back to their KaTeX-compatible - * LaTeX representations. For instance, `𜰀` becomes `\\\\` for KaTeX to correctly + * LaTeX representations. For instance, `BACKSLASH_PLACEHOLDER` becomes `\\\\` for KaTeX to correctly * render a literal backslash within a formula. * @param input - The string containing placeholders to revert. * @returns The reverted string, ready for KaTeX rendering. */ const revertEscapedCharacters = (input: string): string => { - let unescaped = input; - escapedSequences.forEach((sequence: string) => { - unescaped = unescaped.replaceAll(sequence, mathUnEscapes[sequence]); - }); - return unescaped; + // Optimized: Use a single replace with a replacer function + return input.replace(unescapeAllPlaceholdersRegex, (match: string) => mathUnEscapes[match]); }; /** @@ -141,18 +153,14 @@ const parseDelimiters = (input: string): ParsedSegment[] => { // Step 1: Find all possible matches for all defined delimiters. delimiters.forEach((delimiter: Delimiter) => { - const delimiterRegex = delimiter.customRegex - ? new RegExp(delimiter.customRegex.source, 'g') - : new RegExp( - String.raw`(?:${escapeRegex(delimiter.start)})([\s\S]*?)(?:${escapeRegex(delimiter.end)})`, - 'g' - ); + // Use the pre-compiled regex + const delimiterRegex = delimiter.compiledRegex!; + delimiterRegex.lastIndex = 0; let match: RegExpExecArray | null; while ((match = delimiterRegex.exec(input)) !== null) { const startIndex = match.index; const fullMatchContent = match[0]; - // For custom regex, the content is the full match; otherwise, it's the first capture group. const contentToExtract = delimiter.customRegex ? fullMatchContent : match[1]; const endIndex = startIndex + fullMatchContent.length; @@ -276,4 +284,4 @@ export { // Export types for use in tests ParsedSegment, Delimiter, -}; \ No newline at end of file +}; diff --git a/tsconfig.json b/tsconfig.json index c0e27c7..3aa5d69 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "declaration": true, "sourceMap": true, "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, "skipLibCheck": true, "moduleResolution": "node" },