@@ -277,17 +277,22 @@ protected function parseInlines(Node $parent, string $text): void
277277 $ textBuffer = '' ;
278278 $ result = $ this ->parseLink ($ text , $ pos );
279279 if ($ result !== null ) {
280- // Check if this returned literal text (for unclosed link)
281- if (isset ($ result ['literal ' ])) {
282- $ textBuffer .= $ result ['literal ' ];
283- $ pos = $ result ['pos ' ];
280+ // Check if this is an unclosed link (special handling)
281+ if (isset ($ result ['unclosed_link ' ])) {
282+ // Output [ then parse linkText in isolation then output ](
283+ $ parent ->appendChild (new Text ('[ ' ));
284+ $ this ->parseInlines ($ parent , $ result ['link_text ' ]);
285+ $ parent ->appendChild (new Text (']( ' ));
286+ $ pos = $ result ['continue_pos ' ];
284287
285288 continue ;
286289 }
287- $ parent ->appendChild ($ result ['node ' ]);
288- $ pos = $ result ['pos ' ];
290+ if (isset ($ result ['node ' ])) {
291+ $ parent ->appendChild ($ result ['node ' ]);
292+ $ pos = $ result ['pos ' ];
289293
290- continue ;
294+ continue ;
295+ }
291296 }
292297 }
293298
@@ -544,7 +549,7 @@ protected function parseCodeSpan(string $text, int $pos): ?array
544549 }
545550
546551 /**
547- * @return array{node: \Djot\Node\Inline\Link|\Djot\Node\Inline\Span, pos: int}|null
552+ * @return array{node: \Djot\Node\Inline\Link|\Djot\Node\Inline\Span, pos: int}|array{unclosed_link: true, link_text: string, continue_pos: int}| null
548553 */
549554 protected function parseLink (string $ text , int $ pos ): ?array
550555 {
@@ -620,12 +625,13 @@ protected function parseLink(string $text, int $pos): ?array
620625 ];
621626 }
622627
623- // Unclosed parenthesis - the entire [text](... is literal text
624- // In djot, emphasis openers in [..] can't match closers in (..)
625- // so we return the whole thing as literal text to prevent cross- boundary emphasis
628+ // Unclosed parenthesis - not a valid link
629+ // Parse [text] as isolated inline content, then continue from after (
630+ // This prevents emphasis from crossing the [ text]( boundary
626631 return [
627- 'literal ' => substr ($ text , $ pos , $ length - $ pos ),
628- 'pos ' => $ length ,
632+ 'unclosed_link ' => true ,
633+ 'link_text ' => $ linkText ,
634+ 'continue_pos ' => $ urlStart , // Position after (
629635 ];
630636 }
631637
@@ -641,7 +647,7 @@ protected function parseLink(string $text, int $pos): ?array
641647 $ ref = $ this ->normalizeReferenceLabel ($ linkText );
642648 } else {
643649 // Explicit reference [text][ref] - only normalize whitespace, keep formatting chars
644- $ ref = preg_replace ('/\s+/ ' , ' ' , trim ($ ref ));
650+ $ ref = preg_replace ('/\s+/ ' , ' ' , trim ($ ref )) ?? $ ref ;
645651 }
646652
647653 $ refDef = $ this ->blockParser ->getReference ($ ref );
@@ -731,6 +737,11 @@ protected function parseImage(string $text, int $pos): ?array
731737 return null ;
732738 }
733739
740+ // Unclosed links can't be images, and we need node/pos to exist
741+ if (isset ($ result ['unclosed_link ' ]) || !isset ($ result ['node ' ])) {
742+ return null ;
743+ }
744+
734745 $ link = $ result ['node ' ];
735746 if (!$ link instanceof Link) {
736747 return null ;
@@ -1618,10 +1629,10 @@ protected function normalizeReferenceLabel(string $label): string
16181629 {
16191630 // Strip inline formatting markers: _ * ~ ^ + = { }
16201631 // But keep the content between them
1621- $ label = preg_replace ('/[_*~^+={}]/ ' , '' , $ label );
1632+ $ label = preg_replace ('/[_*~^+={}]/ ' , '' , $ label ) ?? $ label ;
16221633
16231634 // Normalize whitespace: collapse multiple spaces/newlines to single space
1624- $ label = preg_replace ('/\s+/ ' , ' ' , $ label );
1635+ $ label = preg_replace ('/\s+/ ' , ' ' , $ label ) ?? $ label ;
16251636
16261637 // Trim
16271638 return trim ($ label );
0 commit comments