@@ -30,30 +30,78 @@ public function lock(string $markup): string
3030 */
3131 public function unlock (string $ markup ): string
3232 {
33- if (!str_contains ($ markup , $ this ->getProtectedOpeningTag ())) {
33+ $ openTag = $ this ->getProtectedOpeningTag ();
34+
35+ if (!str_contains ($ markup , $ openTag )) {
3436 return $ markup ;
3537 }
3638
37- return $ this ->unwrapProtectedMarkup ($ markup );
39+ do {
40+ $ previous = $ markup ;
41+ $ markup = $ this ->unwrapProtectedMarkup ($ markup );
42+ } while ($ markup !== $ previous && str_contains ($ markup , $ openTag ));
43+
44+ return $ markup ;
3845 }
3946
4047 /**
41- * Repeatedly unwraps one nesting level at a time until no wrappers remain.
48+ * Walks the markup string and strips every protected wrapper while preserving
49+ * inner content. Uses a depth counter to correctly handle nested wrappers
50+ * without relying on PCRE, which fails with a backtrack-limit error on large
51+ * full-page HTML documents.
4252 *
4353 * @param string $markup Protected markup.
4454 *
4555 * @return string
4656 */
4757 private function unwrapProtectedMarkup (string $ markup ): string
4858 {
49- $ unwrappedMarkup = $ markup ;
59+ $ openTag = $ this ->getProtectedOpeningTag ();
60+ $ closeTag = $ this ->getProtectedClosingTag ();
61+ $ openLen = strlen ($ openTag );
62+ $ closeLen = strlen ($ closeTag );
63+ $ result = '' ;
64+ $ remaining = $ markup ;
5065
51- do {
52- $ previousMarkup = $ unwrappedMarkup ;
53- $ unwrappedMarkup = preg_replace ($ this ->getProtectedWrapperPattern (), '$1 ' , $ unwrappedMarkup ) ?? $ previousMarkup ;
54- } while ($ unwrappedMarkup !== $ previousMarkup );
66+ while (($ startPos = strpos ($ remaining , $ openTag )) !== false ) {
67+ $ result .= substr ($ remaining , 0 , $ startPos );
68+ $ afterOpen = substr ($ remaining , $ startPos + $ openLen );
69+
70+ $ depth = 1 ;
71+ $ searchPos = 0 ;
72+ $ innerEndPos = null ;
5573
56- return $ unwrappedMarkup ;
74+ while ($ searchPos < strlen ($ afterOpen )) {
75+ $ nextOpen = strpos ($ afterOpen , $ openTag , $ searchPos );
76+ $ nextClose = strpos ($ afterOpen , $ closeTag , $ searchPos );
77+
78+ if ($ nextClose === false ) {
79+ // Malformed markup: no matching closing tag; keep wrapper as-is.
80+ $ result .= $ openTag . $ afterOpen ;
81+ $ remaining = '' ;
82+ break 2 ;
83+ }
84+
85+ if ($ nextOpen !== false && $ nextOpen < $ nextClose ) {
86+ $ depth ++;
87+ $ searchPos = $ nextOpen + $ openLen ;
88+ } else {
89+ $ depth --;
90+ if ($ depth === 0 ) {
91+ $ innerEndPos = $ nextClose ;
92+ break ;
93+ }
94+ $ searchPos = $ nextClose + $ closeLen ;
95+ }
96+ }
97+
98+ if ($ innerEndPos !== null ) {
99+ $ result .= substr ($ afterOpen , 0 , $ innerEndPos );
100+ $ remaining = substr ($ afterOpen , $ innerEndPos + $ closeLen );
101+ }
102+ }
103+
104+ return $ result . $ remaining ;
57105 }
58106
59107 /**
@@ -76,20 +124,4 @@ private function getProtectedClosingTag(): string
76124 return '</ ' . self ::PROTECTED_TAG . '> ' ;
77125 }
78126
79- /**
80- * Matches the innermost protected wrapper so nested sections can be released safely.
81- *
82- * @return string
83- */
84- private function getProtectedWrapperPattern (): string
85- {
86- return '# '
87- . preg_quote ($ this ->getProtectedOpeningTag (), '# ' )
88- . '((?:(?! '
89- . preg_quote ($ this ->getProtectedOpeningTag (), '# ' )
90- . ').)*) '
91- . preg_quote ($ this ->getProtectedClosingTag (), '# ' )
92- . '#is ' ;
93- }
94-
95127}
0 commit comments