Skip to content

Commit bcc6068

Browse files
committed
fix: remove any trailing "pre.wpautop-protected" elements from content
1 parent 8ce684d commit bcc6068

2 files changed

Lines changed: 76 additions & 25 deletions

File tree

library/Content/WpAutopContentGuard/WpAutopContentGuard.php

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

library/Content/WpAutopContentGuard/WpAutopContentGuardTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,23 @@ public function testUnlockWithUnprotectedContent(): void {
5454

5555
static::assertEquals($expectedOutput, $this->guard->unlock($input));
5656
}
57+
58+
#[TestDox('unlocks empty localized content without errors')]
59+
public function testUnlockWithEmptyLocalizedContent(): void {
60+
$input = $this->guard->lock(' ');
61+
$expectedOutput = ' ';
62+
63+
static::assertEquals($expectedOutput, $this->guard->unlock($input));
64+
}
65+
66+
#[TestDox('unlocks protected content embedded in a large full-page HTML document without PCRE backtrack-limit errors')]
67+
public function testUnlockInsideLargeDocument(): void {
68+
$filler = str_repeat('<p>' . str_repeat('a', 100) . '</p>', 2000); // ~250 KB of surrounding HTML
69+
$protected = $this->guard->lock('<div class="c-acceptance"><template><iframe src="https://youtube.com/embed/test"></iframe></template></div>');
70+
$input = '<html><body>' . $filler . $protected . $filler . '</body></html>';
71+
$output = $this->guard->unlock($input);
72+
73+
static::assertStringNotContainsString('wpautop-protected', $output);
74+
static::assertStringContainsString('<div class="c-acceptance">', $output);
75+
}
5776
}

0 commit comments

Comments
 (0)