@@ -64,6 +64,14 @@ export const KATEX_COMMANDS = [
6464 'prod' ,
6565 'int' ,
6666 'sqrt' ,
67+ 'fbox' ,
68+ 'boxed' ,
69+ 'color' ,
70+ 'rule' ,
71+ 'edef' ,
72+ 'fcolorbox' ,
73+ 'hline' ,
74+ 'hdashline' ,
6775 'cdot' ,
6876 'times' ,
6977 'pm' ,
@@ -96,13 +104,31 @@ export const ESCAPED_KATEX_COMMANDS = KATEX_COMMANDS
96104 . join ( '|' )
97105const CONTROL_CHARS_CLASS = '[\t\r\b\f\v]'
98106
107+ // Hoisted map of control characters -> escaped letter (e.g. '\t' -> 't').
108+ // Kept at module scope to avoid recreating on every normalization call.
109+ const CONTROL_MAP : Record < string , string > = {
110+ '\t' : 't' ,
111+ '\r' : 'r' ,
112+ '\b' : 'b' ,
113+ '\f' : 'f' ,
114+ '\v' : 'v' ,
115+ }
116+
99117// Precompiled regexes for isMathLike to avoid reconstructing them per-call
118+ // and prebuilt default regexes for normalizeStandaloneBackslashT when the
119+ // default command set is used.
100120const TEX_CMD_RE = / \\ [ a - z ] + / i
101121const PREFIX_CLASS = '(?:\\\\|\\u0008)'
102122const TEX_CMD_WITH_BRACES_RE = new RegExp ( `${ PREFIX_CLASS } (?:${ ESCAPED_TEX_BRACE_COMMANDS } )\\s*\\{[^}]+\\}` , 'i' )
103123const TEX_SPECIFIC_RE = / \\ (?: t e x t | f r a c | l e f t | r i g h t | t i m e s ) /
104124const SUPER_SUB_RE = / \^ | _ /
105- const OPS_RE = / [ = + \- * / ^ < > ] | \\ t i m e s | \\ p m | \\ c d o t | \\ l e | \\ g e | \\ n e q /
125+ // Match common math operator symbols or named commands.
126+ // Avoid treating the C/C++ increment operator ("++") as a math operator by
127+ // ensuring a lone '+' isn't matched when it's part of a '++' sequence.
128+ // Use a RegExp constructed from a string to avoid issues escaping '/' in a
129+ // regex literal on some platforms/linters.
130+ // eslint-disable-next-line prefer-regex-literals
131+ const OPS_RE = new RegExp ( '(?<!\\+)\\+(?!\\+)|[=\\-*/^<>]|\\\\times|\\\\pm|\\\\cdot|\\\\le|\\\\ge|\\\\neq' )
106132const FUNC_CALL_RE = / [ A - Z ] + \s * \( [ ^ ) ] + \) / i
107133const WORDS_RE = / \b (?: s i n | c o s | t a n | l o g | l n | e x p | s q r t | f r a c | s u m | l i m | i n t | p r o d ) \b /
108134
@@ -142,54 +168,26 @@ export function isMathLike(s: string) {
142168}
143169
144170export function normalizeStandaloneBackslashT ( s : string , opts ?: MathOptions ) {
145- // Map of characters or words that may have lost a leading backslash when
146- // interpreted in JS string literals (for example "\b" -> backspace U+0008)
147- // Keys may use backslash escapes in the source; the actual string keys
148- // become the unescaped character/word (e.g. '\\t' -> '\t' -> tab char).
149- // Keys are the actual control characters as they appear in JS strings when
150- // an escape was interpreted (e.g. '\\t' -> actual tab char '\t').
151- const controlMap : Record < string , string > = {
152- '\t' : 't' ,
153- '\r' : 'r' ,
154- '\b' : 'b' ,
155- '\f' : 'f' ,
156- '\v' : 'v' ,
157- // Note: deliberately omitting \n since real newlines are structural and
158- // shouldn't be collapsed into a two-character escape in most cases.
159- }
160-
161- // use top-level KATEX_COMMANDS constant
162-
163- // Build a regex that matches either a lone control character (tab, etc.)
164- // or one of the known command words that is NOT already prefixed by a
165- // backslash. We ensure the matched word isn't part of a larger word by
166- // using a word boundary where appropriate.
167171 const commands = opts ?. commands ?? KATEX_COMMANDS
168172 const escapeExclamation = opts ?. escapeExclamation ?? true
169173
170- // Choose a prebuilt regex when using default command set for performance,
171- // otherwise build one from the provided commands. Use a negative
172- // lookbehind to ensure the matched command isn't already escaped (i.e.
173- // not preceded by a backslash) and not part of a larger word. We also
174- // match literal control characters (tab, backspace, etc.). This form
175- // avoids capturing the prefix (p1) which previously caused overlapping
176- // replacement issues.
177- const commandPattern = ( opts ?. commands == null )
178- ? `(?:${ ESCAPED_KATEX_COMMANDS } )`
179- : `(?:${ commands . slice ( ) . sort ( ( a , b ) => b . length - a . length ) . map ( c => c . replace ( / [ . * + ? ^ $ { } ( ) | [ \\ ] \\ " \] / g, '\\$&' ) ) . join ( '|' ) } )`
180-
181- // Match either a control character or an unescaped command word.
182- const re = new RegExp ( `${ CONTROL_CHARS_CLASS } |(?<!\\\\|\\w)(${ commandPattern } )\\b` , 'g' )
183-
184- let out = s . replace ( re , ( m , cmd ) => {
185- // If m is a literal control character (e.g. '\t' as actual tab), map it.
186- if ( controlMap [ m ] !== undefined )
187- return `\\${ controlMap [ m ] } `
188-
189- // Otherwise cmd will be populated with the matched command word.
174+ const useDefault = opts ?. commands == null
175+
176+ // Build or reuse regex: match control chars or unescaped command words.
177+ let re : RegExp
178+ if ( useDefault ) {
179+ re = new RegExp ( `${ CONTROL_CHARS_CLASS } |(?<!\\\\|\\w)(${ ESCAPED_KATEX_COMMANDS } )\\b` , 'g' )
180+ }
181+ else {
182+ const commandPattern = `(?:${ commands . slice ( ) . sort ( ( a , b ) => b . length - a . length ) . map ( c => c . replace ( / [ . * + ? ^ $ { } ( ) | [ \\ ] \\ " \] / g, '\\$&' ) ) . join ( '|' ) } )`
183+ re = new RegExp ( `${ CONTROL_CHARS_CLASS } |(?<!\\\\|\\w)(${ commandPattern } )\\b` , 'g' )
184+ }
185+
186+ let out = s . replace ( re , ( m : string , cmd ?: string ) => {
187+ if ( CONTROL_MAP [ m ] !== undefined )
188+ return `\\${ CONTROL_MAP [ m ] } `
190189 if ( cmd && commands . includes ( cmd ) )
191190 return `\\${ cmd } `
192-
193191 return m
194192 } )
195193
@@ -204,14 +202,17 @@ export function normalizeStandaloneBackslashT(s: string, opts?: MathOptions) {
204202 // Use default escaped list when possible. Include TEX_BRACE_COMMANDS so
205203 // known brace-taking TeX commands (e.g. `text`, `boldsymbol`) are also
206204 // restored when their leading backslash was lost.
207- const braceEscaped = ( opts ?. commands == null )
205+ const braceEscaped = useDefault
208206 ? [ ESCAPED_TEX_BRACE_COMMANDS , ESCAPED_KATEX_COMMANDS ] . filter ( Boolean ) . join ( '|' )
209207 : [ commands . map ( c => c . replace ( / [ . * + ? ^ $ { } ( ) | [ \\ ] \\ \] / g, '\\$&' ) ) . join ( '|' ) , ESCAPED_TEX_BRACE_COMMANDS ] . filter ( Boolean ) . join ( '|' )
208+ let result = out
210209 if ( braceEscaped ) {
211210 const braceCmdRe = new RegExp ( `(^|[^\\\\])(${ braceEscaped } )\\s*\\{` , 'g' )
212- out = out . replace ( braceCmdRe , ( _m , p1 , p2 ) => `${ p1 } \\${ p2 } {` )
211+ result = result . replace ( braceCmdRe , ( _m : string , p1 : string , p2 : string ) => `${ p1 } \\${ p2 } {` )
213212 }
214- return out
213+ result = result . replace ( / s p a n \{ ( [ ^ } ] + ) \} / , 'span\\{$1\\}' )
214+ . replace ( / \\ o p e r a t o r n a m e \{ s p a n \} \{ ( (?: [ ^ { } ] | \{ [ ^ } ] * \} ) + ) \} / , '\\operatorname{span}\\{$1\\}' )
215+ return result
215216}
216217export function applyMath ( md : MarkdownIt , mathOpts ?: MathOptions ) {
217218 // Inline rule for \(...\) and $$...$$ and $...$
@@ -225,6 +226,7 @@ export function applyMath(md: MarkdownIt, mathOpts?: MathOptions) {
225226 [ '\(' , '\)' ] ,
226227 ]
227228 let searchPos = 0
229+ let jump = true
228230 // use findMatchingClose from util
229231 for ( const [ open , close ] of delimiters ) {
230232 // We'll scan the entire inline source and tokenize all occurrences
@@ -282,10 +284,24 @@ export function applyMath(md: MarkdownIt, mathOpts?: MathOptions) {
282284 continue
283285 }
284286 const content = src . slice ( index + open . length , endIdx )
285-
287+ if ( ! isMathLike ( content ) ) {
288+ // push remaining text after last match
289+ // not math-like; skip this match and continue scanning
290+ const temp = searchPos
291+ searchPos = endIdx + close . length
292+ if ( ! src . includes ( open , endIdx + close . length ) ) {
293+ const text = src . slice ( temp , searchPos )
294+ if ( ! state . pending && state . pos + open . length < searchPos )
295+ pushText ( text )
296+ if ( jump )
297+ return false
298+ }
299+ continue
300+ }
286301 foundAny = true
287302
288303 if ( ! silent ) {
304+ jump = false
289305 // push text before this math
290306 const before = src . slice ( 0 , index )
291307 // If we already consumed some content, avoid duplicating the prefix
@@ -314,7 +330,8 @@ export function applyMath(md: MarkdownIt, mathOpts?: MathOptions) {
314330 }
315331
316332 // strong prefix handling (preserve previous behavior)
317- pushText ( isStrongPrefix ? toPushBefore . replace ( / ^ \* + / , '' ) : toPushBefore )
333+ if ( state . pending !== toPushBefore )
334+ pushText ( isStrongPrefix ? toPushBefore . replace ( / ^ \* + / , '' ) : toPushBefore )
318335
319336 const token = state . push ( 'math_inline' , 'math' , 0 )
320337 token . content = normalizeStandaloneBackslashT ( content , mathOpts )
@@ -435,7 +452,7 @@ export function applyMath(md: MarkdownIt, mathOpts?: MathOptions) {
435452 = openDelim === '$$' ? '$$' : openDelim === '[' ? '[]' : '\\[\\]'
436453 token . map = [ startLine , startLine + 1 ]
437454 token . block = true
438-
455+ token . loading = false
439456 state . line = startLine + 1
440457 return true
441458 }
0 commit comments