@@ -31,14 +31,7 @@ final class LinkParserHelper
3131 public static function parseLinkDestination (Cursor $ cursor ): ?string
3232 {
3333 if ($ cursor ->getCurrentCharacter () === '< ' ) {
34- if ($ res = $ cursor ->match (RegexHelper::REGEX_LINK_DESTINATION_BRACES )) {
35- // Chop off surrounding <..>:
36- return UrlEncoder::unescapeAndEncode (
37- RegexHelper::unescape (\substr ($ res , 1 , -1 ))
38- );
39- }
40-
41- return null ;
34+ return self ::parseDestinationBraces ($ cursor );
4235 }
4336
4437 $ destination = self ::manuallyParseLinkDestination ($ cursor );
@@ -137,4 +130,36 @@ private static function manuallyParseLinkDestination(Cursor $cursor): ?string
137130
138131 return $ destination ;
139132 }
133+
134+ /** @var \WeakReference<Cursor>|null */
135+ private static ?\WeakReference $ lastCursor = null ;
136+ private static bool $ lastCursorLacksClosingBrace = false ;
137+
138+ private static function parseDestinationBraces (Cursor $ cursor ): ?string
139+ {
140+ // Optimization: If we've previously parsed this cursor and returned `null`, we know
141+ // that no closing brace exists, so we can skip the regex entirely. This helps avoid
142+ // certain pathological cases where the regex engine can take a very long time to
143+ // determine that no match exists.
144+ if (self ::$ lastCursor !== null && self ::$ lastCursor ->get () === $ cursor ) {
145+ if (self ::$ lastCursorLacksClosingBrace ) {
146+ return null ;
147+ }
148+ } else {
149+ self ::$ lastCursor = \WeakReference::create ($ cursor );
150+ }
151+
152+ if ($ res = $ cursor ->match (RegexHelper::REGEX_LINK_DESTINATION_BRACES )) {
153+ self ::$ lastCursorLacksClosingBrace = false ;
154+
155+ // Chop off surrounding <..>:
156+ return UrlEncoder::unescapeAndEncode (
157+ RegexHelper::unescape (\substr ($ res , 1 , -1 ))
158+ );
159+ }
160+
161+ self ::$ lastCursorLacksClosingBrace = true ;
162+
163+ return null ;
164+ }
140165}
0 commit comments