@@ -2,6 +2,181 @@ import $ from 'jquery';
22import katex from 'katex' ;
33import 'katex/dist/katex.min.css' ;
44
5+ function isEscaped ( text , index ) {
6+ let count = 0 ;
7+ for ( let i = index - 1 ; i >= 0 && text [ i ] === '\\' ; i -- ) {
8+ count ++ ;
9+ }
10+ return count % 2 === 1 ;
11+ }
12+
13+ function findUnescaped ( text , token , start ) {
14+ let index = start ;
15+ while ( index < text . length ) {
16+ const found = text . indexOf ( token , index ) ;
17+ if ( found === - 1 ) return - 1 ;
18+ if ( ! isEscaped ( text , found ) ) return found ;
19+ index = found + token . length ;
20+ }
21+ return - 1 ;
22+ }
23+
24+ function getTextNodes ( container , shouldAccept ) {
25+ const walker = document . createTreeWalker (
26+ container ,
27+ NodeFilter . SHOW_TEXT ,
28+ {
29+ acceptNode : function ( node ) {
30+ // 跳过已渲染的节点
31+ if (
32+ node . parentNode &&
33+ ( node . parentNode . classList ?. contains ( 'katex' ) ||
34+ node . parentNode . closest ( "pre, code, .katex" ) )
35+ ) {
36+ return NodeFilter . FILTER_REJECT ;
37+ }
38+
39+ return shouldAccept ( node . nodeValue || '' ) ? NodeFilter . FILTER_ACCEPT : NodeFilter . FILTER_REJECT ;
40+ } ,
41+ }
42+ ) ;
43+
44+ const nodesToReplace = [ ] ;
45+ while ( walker . nextNode ( ) ) {
46+ nodesToReplace . push ( walker . currentNode ) ;
47+ }
48+ return nodesToReplace ;
49+ }
50+
51+ function collectInlineMatches ( text ) {
52+ const matches = [ ] ;
53+ let index = 0 ;
54+
55+ while ( index < text . length ) {
56+ const parenStart = text . indexOf ( '\\(' , index ) ;
57+ const dollarStart = text . indexOf ( '$' , index ) ;
58+ const candidates = [ parenStart , dollarStart ] . filter ( ( value ) => value !== - 1 && ! isEscaped ( text , value ) ) ;
59+ const start = candidates . length ? Math . min ( ...candidates ) : - 1 ;
60+ if ( start === - 1 ) break ;
61+
62+ if ( text . slice ( start , start + 2 ) === '\\(' ) {
63+ const end = findUnescaped ( text , '\\)' , start + 2 ) ;
64+ if ( end !== - 1 ) {
65+ const raw = text . slice ( start + 2 , end ) ;
66+ if ( raw && ! raw . includes ( '\n' ) ) {
67+ matches . push ( {
68+ start,
69+ end : end + 2 ,
70+ raw,
71+ full : text . slice ( start , end + 2 ) ,
72+ } ) ;
73+ }
74+ index = end + 2 ;
75+ continue ;
76+ }
77+ } else if ( text [ start ] === '$' && text [ start + 1 ] !== '$' ) {
78+ const end = findUnescaped ( text , '$' , start + 1 ) ;
79+ if ( end !== - 1 && text [ end + 1 ] !== '$' ) {
80+ const raw = text . slice ( start + 1 , end ) ;
81+ if ( raw && ! raw . includes ( '\n' ) ) {
82+ matches . push ( {
83+ start,
84+ end : end + 1 ,
85+ raw,
86+ full : text . slice ( start , end + 1 ) ,
87+ } ) ;
88+ }
89+ index = end + 1 ;
90+ continue ;
91+ }
92+ }
93+
94+ index = start + 1 ;
95+ }
96+
97+ return matches ;
98+ }
99+
100+ function collectBlockMatches ( text ) {
101+ const matches = [ ] ;
102+ let index = 0 ;
103+
104+ while ( index < text . length ) {
105+ const dollarStart = text . indexOf ( '$$' , index ) ;
106+ const bracketStart = text . indexOf ( '\\[' , index ) ;
107+ const candidates = [ dollarStart , bracketStart ] . filter ( ( value ) => value !== - 1 && ! isEscaped ( text , value ) ) ;
108+ const start = candidates . length ? Math . min ( ...candidates ) : - 1 ;
109+ if ( start === - 1 ) break ;
110+
111+ if ( text . slice ( start , start + 2 ) === '$$' ) {
112+ const end = findUnescaped ( text , '$$' , start + 2 ) ;
113+ if ( end !== - 1 ) {
114+ const raw = text . slice ( start + 2 , end ) . trim ( ) ;
115+ if ( raw ) {
116+ matches . push ( {
117+ start,
118+ end : end + 2 ,
119+ raw,
120+ full : text . slice ( start , end + 2 ) ,
121+ } ) ;
122+ }
123+ index = end + 2 ;
124+ continue ;
125+ }
126+ } else if ( text . slice ( start , start + 2 ) === '\\[' ) {
127+ const end = findUnescaped ( text , '\\]' , start + 2 ) ;
128+ if ( end !== - 1 ) {
129+ const raw = text . slice ( start + 2 , end ) . trim ( ) ;
130+ if ( raw ) {
131+ matches . push ( {
132+ start,
133+ end : end + 2 ,
134+ raw,
135+ full : text . slice ( start , end + 2 ) ,
136+ } ) ;
137+ }
138+ index = end + 2 ;
139+ continue ;
140+ }
141+ }
142+
143+ index = start + 1 ;
144+ }
145+
146+ return matches ;
147+ }
148+
149+ function replaceTextNode ( node , matches , renderMatch , logPrefix ) {
150+ if ( ! matches . length ) return ;
151+
152+ const text = node . nodeValue || '' ;
153+ const frag = document . createDocumentFragment ( ) ;
154+ let lastIndex = 0 ;
155+
156+ matches . forEach ( ( match ) => {
157+ if ( match . start > lastIndex ) {
158+ frag . appendChild ( document . createTextNode ( text . slice ( lastIndex , match . start ) ) ) ;
159+ }
160+
161+ try {
162+ frag . appendChild ( renderMatch ( match . raw ) ) ;
163+ } catch ( e ) {
164+ frag . appendChild ( document . createTextNode ( match . full ) ) ;
165+ console . error ( logPrefix , match . raw , e . message ) ;
166+ }
167+
168+ lastIndex = match . end ;
169+ } ) ;
170+
171+ if ( lastIndex < text . length ) {
172+ frag . appendChild ( document . createTextNode ( text . slice ( lastIndex ) ) ) ;
173+ }
174+
175+ if ( frag . hasChildNodes ( ) ) {
176+ node . parentNode . replaceChild ( frag , node ) ;
177+ }
178+ }
179+
5180function renderKatexBlock ( selector = ".w-latex" ) {
6181 try {
7182 $ ( selector ) . each ( function ( ) {
@@ -39,60 +214,11 @@ function renderKatexBlock(selector = ".w-latex") {
39214}
40215
41216function renderKatexInline ( container = document . body ) {
42- // 改进的正则:不匹配换行符,支持转义
43- const inlineRegex = / (?< ! \\ ) ( \\ \( ( .+ ?) \\ \) | (?< ! \\ ) \$ ( [ ^ \n $ ] + ?) (?< ! \\ ) \$ ) / g;
44-
45- const walker = document . createTreeWalker (
46- container ,
47- NodeFilter . SHOW_TEXT ,
48- {
49- acceptNode : function ( node ) {
50- // 跳过已渲染的节点
51- if (
52- node . parentNode &&
53- ( node . parentNode . classList ?. contains ( 'katex' ) ||
54- node . parentNode . closest ( "pre, code, .katex" ) )
55- ) {
56- return NodeFilter . FILTER_REJECT ;
57- }
58-
59- const value = node . nodeValue || '' ;
60- if ( value . includes ( "\\(" ) || value . includes ( "$" ) ) {
61- return NodeFilter . FILTER_ACCEPT ;
62- }
63- return NodeFilter . FILTER_REJECT ;
64- } ,
65- }
66- ) ;
67-
68- const nodesToReplace = [ ] ;
69- while ( walker . nextNode ( ) ) {
70- nodesToReplace . push ( walker . currentNode ) ;
71- }
72-
73- nodesToReplace . forEach ( ( node ) => {
74- const text = node . nodeValue ;
75- const frag = document . createDocumentFragment ( ) ;
76- let lastIndex = 0 ;
77-
78- // 重置正则状态
79- inlineRegex . lastIndex = 0 ;
80-
81- const matches = [ ...text . matchAll ( inlineRegex ) ] ;
82-
83- matches . forEach ( ( match ) => {
84- const fullMatch = match [ 0 ] ;
85- const parenContent = match [ 2 ] ;
86- const dollarContent = match [ 3 ] ;
87- const raw = parenContent || dollarContent ;
88- const matchStart = match . index ;
89-
90- // 添加匹配前的文本
91- if ( matchStart > lastIndex ) {
92- frag . appendChild ( document . createTextNode ( text . slice ( lastIndex , matchStart ) ) ) ;
93- }
94-
95- try {
217+ getTextNodes ( container , ( value ) => value . includes ( "\\(" ) || value . includes ( "$" ) ) . forEach ( ( node ) => {
218+ replaceTextNode (
219+ node ,
220+ collectInlineMatches ( node . nodeValue || '' ) ,
221+ ( raw ) => {
96222 const span = document . createElement ( "span" ) ;
97223 span . className = "katex-inline" ;
98224 span . innerHTML = katex . renderToString ( raw , {
@@ -101,81 +227,19 @@ function renderKatexInline(container = document.body) {
101227 strict : false ,
102228 trust : true
103229 } ) ;
104- frag . appendChild ( span ) ;
105- } catch ( e ) {
106- frag . appendChild ( document . createTextNode ( fullMatch ) ) ;
107- console . error ( "Inline render error:" , raw , e . message ) ;
108- }
109-
110- lastIndex = matchStart + fullMatch . length ;
111- } ) ;
112-
113- // 添加剩余文本
114- if ( lastIndex < text . length ) {
115- frag . appendChild ( document . createTextNode ( text . slice ( lastIndex ) ) ) ;
116- }
117-
118- if ( frag . hasChildNodes ( ) ) {
119- node . parentNode . replaceChild ( frag , node ) ;
120- }
230+ return span ;
231+ } ,
232+ "Inline render error:"
233+ ) ;
121234 } ) ;
122235}
123236
124237function renderKatexDisplayBlock ( container = document . body ) {
125- // 使用非贪婪匹配,允许多行但不跨过多段落
126- const blockRegex = / (?< ! \\ ) ( \$ \$ ( [ \s \S ] + ?) \$ \$ | (?< ! \\ ) \\ \[ ( [ \s \S ] + ?) \\ \] ) / g;
127-
128- const walker = document . createTreeWalker (
129- container ,
130- NodeFilter . SHOW_TEXT ,
131- {
132- acceptNode : function ( node ) {
133- // 跳过已渲染的节点
134- if (
135- node . parentNode &&
136- ( node . parentNode . classList ?. contains ( 'katex' ) ||
137- node . parentNode . closest ( "pre, code, .katex" ) )
138- ) {
139- return NodeFilter . FILTER_REJECT ;
140- }
141-
142- const value = node . nodeValue || '' ;
143- if ( value . includes ( "$$" ) || value . includes ( "\\[" ) ) {
144- return NodeFilter . FILTER_ACCEPT ;
145- }
146- return NodeFilter . FILTER_REJECT ;
147- } ,
148- }
149- ) ;
150-
151- const nodesToReplace = [ ] ;
152- while ( walker . nextNode ( ) ) {
153- nodesToReplace . push ( walker . currentNode ) ;
154- }
155-
156- nodesToReplace . forEach ( ( node ) => {
157- const text = node . nodeValue ;
158- const frag = document . createDocumentFragment ( ) ;
159- let lastIndex = 0 ;
160-
161- // 重置正则状态
162- blockRegex . lastIndex = 0 ;
163-
164- const matches = [ ...text . matchAll ( blockRegex ) ] ;
165-
166- matches . forEach ( ( match ) => {
167- const fullMatch = match [ 0 ] ;
168- const dollarContent = match [ 2 ] ;
169- const bracketContent = match [ 3 ] ;
170- const raw = ( dollarContent || bracketContent ) . trim ( ) ;
171- const matchStart = match . index ;
172-
173- // 添加匹配前的文本
174- if ( matchStart > lastIndex ) {
175- frag . appendChild ( document . createTextNode ( text . slice ( lastIndex , matchStart ) ) ) ;
176- }
177-
178- try {
238+ getTextNodes ( container , ( value ) => value . includes ( "$$" ) || value . includes ( "\\[" ) ) . forEach ( ( node ) => {
239+ replaceTextNode (
240+ node ,
241+ collectBlockMatches ( node . nodeValue || '' ) ,
242+ ( raw ) => {
179243 const div = document . createElement ( "div" ) ;
180244 div . className = "katex-block" ;
181245 div . innerHTML = katex . renderToString ( raw , {
@@ -184,23 +248,10 @@ function renderKatexDisplayBlock(container = document.body) {
184248 strict : false ,
185249 trust : true
186250 } ) ;
187- frag . appendChild ( div ) ;
188- } catch ( e ) {
189- frag . appendChild ( document . createTextNode ( fullMatch ) ) ;
190- console . error ( "Block render error:" , raw , e . message ) ;
191- }
192-
193- lastIndex = matchStart + fullMatch . length ;
194- } ) ;
195-
196- // 添加剩余文本
197- if ( lastIndex < text . length ) {
198- frag . appendChild ( document . createTextNode ( text . slice ( lastIndex ) ) ) ;
199- }
200-
201- if ( frag . hasChildNodes ( ) ) {
202- node . parentNode . replaceChild ( frag , node ) ;
203- }
251+ return div ;
252+ } ,
253+ "Block render error:"
254+ ) ;
204255 } ) ;
205256}
206257
0 commit comments