@@ -70,8 +70,6 @@ const TOOL_RE = new RegExp(
7070 "g" ,
7171) ;
7272
73- const CODE_BLOCK_RE = / ` ` ` ( \w * ) \n ( [ \s \S ] * ?) ` ` ` / g;
74-
7573/** Returns true if text[from..to) contains a backtick run of
7674 * exactly `len` characters. Used to detect a closing inline
7775 * code delimiter on the same line as the opener. */
@@ -95,7 +93,7 @@ function hasRunBefore(
9593 * backtick run of length N is closed by the next run of exactly
9694 * N backticks. Fenced code blocks (triple-backtick at line
9795 * start followed by a newline) are excluded — those are handled
98- * separately by CODE_BLOCK_RE .
96+ * separately by codeBlockMatches .
9997 */
10098function scanInlineCodeSpans (
10199 text : string ,
@@ -125,15 +123,13 @@ function scanInlineCodeSpans(
125123 }
126124
127125 // Scan for a closing run of exactly the same length.
128- let found = false ;
129126 for ( let j = i ; j < text . length ; j ++ ) {
130127 if ( text [ j ] !== "`" ) continue ;
131128 const closeStart = j ;
132129 while ( j < text . length && text [ j ] === "`" ) j ++ ;
133130 if ( j - closeStart === runLen ) {
134131 spans . push ( [ openStart , j ] ) ;
135132 i = j ;
136- found = true ;
137133 break ;
138134 }
139135 }
@@ -190,6 +186,92 @@ function insideInlineCode(
190186 return spans . some ( ( [ s , e ] ) => pos > s && pos < e ) ;
191187}
192188
189+ function atFenceLineStart ( text : string , pos : number ) : boolean {
190+ const lineStart = text . lastIndexOf ( "\n" , pos - 1 ) + 1 ;
191+ return / ^ [ \t ] { 0 , 3 } $ / . test ( text . slice ( lineStart , pos ) ) ;
192+ }
193+
194+ function countBackticks ( text : string , pos : number ) : number {
195+ let end = pos ;
196+ while ( end < text . length && text [ end ] === "`" ) end ++ ;
197+ return end - pos ;
198+ }
199+
200+ function closingFence (
201+ text : string ,
202+ contentStart : number ,
203+ fenceLen : number ,
204+ ) : { contentEnd : number ; end : number } | undefined {
205+ let pos = contentStart ;
206+ while ( pos < text . length ) {
207+ const tickStart = text . indexOf ( "`" , pos ) ;
208+ if ( tickStart < 0 ) return undefined ;
209+ const lineStart = text . lastIndexOf ( "\n" , tickStart - 1 ) + 1 ;
210+ const nextLineStart = text . indexOf ( "\n" , tickStart ) ;
211+ const lineEnd =
212+ nextLineStart >= 0 ? nextLineStart : text . length ;
213+
214+ const tickCount = countBackticks ( text , tickStart ) ;
215+ const rest = text . slice ( tickStart + tickCount , lineEnd ) ;
216+ if (
217+ tickCount >= fenceLen &&
218+ atFenceLineStart ( text , tickStart ) &&
219+ / ^ [ \t ] * $ / . test ( rest )
220+ ) {
221+ return { contentEnd : lineStart , end : lineEnd } ;
222+ }
223+
224+ pos = tickStart + tickCount ;
225+ }
226+ return undefined ;
227+ }
228+
229+ function codeBlockMatches ( text : string ) : Match [ ] {
230+ const matches : Match [ ] = [ ] ;
231+ let pos = 0 ;
232+
233+ while ( pos < text . length ) {
234+ const start = text . indexOf ( "```" , pos ) ;
235+ if ( start < 0 ) break ;
236+
237+ if ( ! atFenceLineStart ( text , start ) ) {
238+ pos = start + 1 ;
239+ continue ;
240+ }
241+
242+ const fenceLen = countBackticks ( text , start ) ;
243+ const infoStart = start + fenceLen ;
244+ const lineEnd = text . indexOf ( "\n" , infoStart ) ;
245+ if ( lineEnd < 0 ) break ;
246+
247+ const info = text . slice ( infoStart , lineEnd ) ;
248+ if ( info . includes ( "`" ) ) {
249+ pos = infoStart ;
250+ continue ;
251+ }
252+
253+ const contentStart = lineEnd + 1 ;
254+ const close = closingFence ( text , contentStart , fenceLen ) ;
255+ if ( close === undefined ) {
256+ pos = infoStart ;
257+ continue ;
258+ }
259+
260+ matches . push ( {
261+ start,
262+ end : close . end ,
263+ segment : {
264+ type : "code" ,
265+ content : text . slice ( contentStart , close . contentEnd ) ,
266+ label : info . trim ( ) || undefined ,
267+ } ,
268+ } ) ;
269+ pos = close . end ;
270+ }
271+
272+ return matches ;
273+ }
274+
193275function extractMatches ( text : string , parseTools = true ) : Match [ ] {
194276 const matches : Match [ ] = [ ] ;
195277
@@ -271,22 +353,14 @@ function extractMatches(text: string, parseTools = true): Match[] {
271353 }
272354 }
273355
274- for ( const m of text . matchAll ( CODE_BLOCK_RE ) ) {
275- const idx = m . index ! ;
356+ for ( const m of codeBlockMatches ( text ) ) {
357+ const idx = m . start ;
276358 const insideOther = matches . some (
277359 ( o ) => idx >= o . start && idx < o . end ,
278360 ) ;
279361 if ( insideOther ) continue ;
280362
281- matches . push ( {
282- start : idx ,
283- end : idx + m [ 0 ] . length ,
284- segment : {
285- type : "code" ,
286- content : m [ 2 ] ?? "" ,
287- label : m [ 1 ] || undefined ,
288- } ,
289- } ) ;
363+ matches . push ( m ) ;
290364 }
291365
292366 return matches ;
0 commit comments