@@ -250,9 +250,10 @@ function parseImage(
250250 start : number
251251) : { html : string ; end : number } | null {
252252 //  or 
253- const altStart = start + 2 ; // after ![
254- const altEnd = text . indexOf ( "]" , altStart ) ;
253+ // Use balanced bracket matching to handle nested/escaped brackets in alt text
254+ const altEnd = findClosingBracket ( text , start + 1 ) ;
255255 if ( altEnd === - 1 ) { return null ; }
256+ const altStart = start + 2 ; // after ![
256257
257258 if ( text [ altEnd + 1 ] !== "(" ) { return null ; }
258259
@@ -303,7 +304,7 @@ function parseLink(
303304 if ( parenEnd === - 1 ) { return null ; }
304305
305306 const linkText = text . substring ( textStart , textEnd ) ;
306- const url = text . substring ( urlStart , parenEnd ) . trim ( ) ;
307+ const url = extractDestination ( text . substring ( urlStart , parenEnd ) . trim ( ) ) ;
307308
308309 return {
309310 html : `<a href="${ escapeHtml ( url ) } ">${ parseInline ( linkText ) } </a>` ,
@@ -343,6 +344,35 @@ function findClosingParen(text: string, openPos: number): number {
343344 return - 1 ;
344345}
345346
347+ /**
348+ * Extract the destination URL from a link/image URL+title string.
349+ * Handles angle-bracket destinations and strips optional titles.
350+ * E.g., `<url>` → `url`, `url "title"` → `url`
351+ */
352+ function extractDestination ( raw : string ) : string {
353+ // Angle-bracket destination: <url>
354+ if ( raw . startsWith ( "<" ) && raw . endsWith ( ">" ) ) {
355+ return raw . substring ( 1 , raw . length - 1 ) ;
356+ }
357+ if ( raw . startsWith ( "<" ) ) {
358+ const close = raw . indexOf ( ">" ) ;
359+ if ( close !== - 1 ) {
360+ return raw . substring ( 1 , close ) ;
361+ }
362+ }
363+ // Split at first unescaped whitespace to separate destination from title
364+ for ( let i = 0 ; i < raw . length ; i ++ ) {
365+ if ( raw [ i ] === "\\" && i + 1 < raw . length ) {
366+ i ++ ; // skip escaped char
367+ continue ;
368+ }
369+ if ( raw [ i ] === " " || raw [ i ] === "\t" || raw [ i ] === "\n" ) {
370+ return raw . substring ( 0 , i ) ;
371+ }
372+ }
373+ return raw ;
374+ }
375+
346376function parseDelimited (
347377 text : string ,
348378 start : number ,
@@ -478,8 +508,8 @@ function tokenize(markdown: string): Token[] {
478508 continue ;
479509 }
480510
481- // Fenced code block
482- const fenceMatch = line . match ( / ^ ( ` { 3 , } | ~ { 3 , } ) ( .* ) $ / ) ;
511+ // Fenced code block (0-3 leading spaces allowed per CommonMark)
512+ const fenceMatch = line . match ( / ^ { 0 , 3 } ( ` { 3 , } | ~ { 3 , } ) ( .* ) $ / ) ;
483513 if ( fenceMatch ) {
484514 const fence = fenceMatch [ 1 ] ;
485515 const fenceChar = fence [ 0 ] ;
@@ -489,7 +519,7 @@ function tokenize(markdown: string): Token[] {
489519 i ++ ;
490520 while ( i < lines . length ) {
491521 const closingMatch = lines [ i ] . match (
492- new RegExp ( `^${ fenceChar } {${ fenceLen } ,}\\s*$` )
522+ new RegExp ( `^ {0,3} ${ fenceChar } {${ fenceLen } ,}\\s*$` )
493523 ) ;
494524 if ( closingMatch ) {
495525 i ++ ;
@@ -750,11 +780,15 @@ function tryParseTable(
750780 const headerLine = lines [ start ] ;
751781 const separatorLine = lines [ start + 1 ] ;
752782
753- // Check separator line format: | --- | --- | or | :--- | ---: |
754- if ( ! / ^ \s * \| ( \s * : ? - + : ? \s * \| ) + \s * $ / . test ( separatorLine ) ) { return null ; }
783+ // Check separator line format: | --- | --- | or --- | --- (outer pipes optional)
784+ // Must contain at least one pipe and only dashes, colons, pipes, and whitespace
785+ if (
786+ ! separatorLine . includes ( "|" ) ||
787+ ! / ^ \s * \| ? \s * : ? - + : ? \s * ( \| \s * : ? - + : ? \s * ) * \| ? \s * $ / . test ( separatorLine )
788+ ) { return null ; }
755789
756- // Check header line format: | ... | ... |
757- if ( ! / ^ \s * \| ( . + \| ) + \s * $ / . test ( headerLine ) ) { return null ; }
790+ // Check header line has at least one pipe (required to distinguish from plain text)
791+ if ( ! headerLine . includes ( "|" ) ) { return null ; }
758792
759793 const headers = parsePipeCells ( headerLine ) ;
760794 const alignments = parseAlignments ( separatorLine ) ;
@@ -763,7 +797,7 @@ function tryParseTable(
763797 let i = start + 2 ;
764798 while ( i < lines . length ) {
765799 const line = lines [ i ] ;
766- if ( ! / ^ \s * \| ( . + \| ) + \s * $ / . test ( line ) ) { break ; }
800+ if ( ! line . includes ( "|" ) ) { break ; }
767801 rows . push ( parsePipeCells ( line ) ) ;
768802 i ++ ;
769803 }
0 commit comments