|
6 | 6 |
|
7 | 7 | final class File |
8 | 8 | { |
| 9 | + private const array NON_ELEMENT_DELIMITERS = [ |
| 10 | + '<!--' => '-->', |
| 11 | + '<![CDATA[' => ']]>', |
| 12 | + '<?' => '?>', |
| 13 | + ]; |
| 14 | + |
9 | 15 | /** @var non-empty-list<int>|null */ |
10 | 16 | private ?array $lineBeginOffsets = null; |
11 | 17 |
|
| 18 | + private ?string $maskedContent = null; |
| 19 | + |
12 | 20 | public function __construct( |
13 | 21 | public readonly string $path, |
14 | 22 | public readonly string $content, |
@@ -48,6 +56,33 @@ public function withContent(string $content): self |
48 | 56 | : new self($this->path, $content); |
49 | 57 | } |
50 | 58 |
|
| 59 | + public function contentWithNonElementMarkupMasked(): string |
| 60 | + { |
| 61 | + if ($this->maskedContent !== null) { |
| 62 | + return $this->maskedContent; |
| 63 | + } |
| 64 | + |
| 65 | + $masked = $this->content; |
| 66 | + $offset = 0; |
| 67 | + |
| 68 | + while (false !== $start = strpos($this->content, '<', $offset)) { |
| 69 | + $endOffset = $this->nonElementMarkupEndOffset($start); |
| 70 | + |
| 71 | + if ($endOffset === null) { |
| 72 | + $offset = $start + 1; |
| 73 | + continue; |
| 74 | + } |
| 75 | + |
| 76 | + for ($i = $start; $i < $endOffset; $i++) { |
| 77 | + $masked[$i] = ' '; |
| 78 | + } |
| 79 | + |
| 80 | + $offset = $endOffset; |
| 81 | + } |
| 82 | + |
| 83 | + return $this->maskedContent = $masked; |
| 84 | + } |
| 85 | + |
51 | 86 | /** @return non-empty-list<int> */ |
52 | 87 | private function lineBeginOffsets(): array |
53 | 88 | { |
@@ -108,6 +143,68 @@ private function lineIndexAtOffset(int $offset, array $lineBeginOffsets): int |
108 | 143 | return $low; |
109 | 144 | } |
110 | 145 |
|
| 146 | + private function nonElementMarkupEndOffset(int $start): ?int |
| 147 | + { |
| 148 | + foreach (self::NON_ELEMENT_DELIMITERS as $opening => $closing) { |
| 149 | + if (substr_compare($this->content, $opening, $start, strlen($opening)) === 0) { |
| 150 | + return $this->offsetAfterDelimiter($closing, $start); |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + if (substr_compare($this->content, '<!', $start, 2) === 0) { |
| 155 | + return $this->declarationEndOffset($start); |
| 156 | + } |
| 157 | + |
| 158 | + return null; |
| 159 | + } |
| 160 | + |
| 161 | + private function offsetAfterDelimiter(string $delimiter, int $offset): int |
| 162 | + { |
| 163 | + $end = strpos($this->content, $delimiter, $offset); |
| 164 | + |
| 165 | + return $end === false ? strlen($this->content) : $end + strlen($delimiter); |
| 166 | + } |
| 167 | + |
| 168 | + private function declarationEndOffset(int $offset): int |
| 169 | + { |
| 170 | + $length = strlen($this->content); |
| 171 | + $quote = null; |
| 172 | + $bracketDepth = 0; |
| 173 | + |
| 174 | + for ($i = $offset; $i < $length; $i++) { |
| 175 | + $character = $this->content[$i]; |
| 176 | + |
| 177 | + if ($quote !== null) { |
| 178 | + if ($character === $quote) { |
| 179 | + $quote = null; |
| 180 | + } |
| 181 | + |
| 182 | + continue; |
| 183 | + } |
| 184 | + |
| 185 | + if ($character === '"' || $character === "'") { |
| 186 | + $quote = $character; |
| 187 | + continue; |
| 188 | + } |
| 189 | + |
| 190 | + if ($character === '[') { |
| 191 | + $bracketDepth++; |
| 192 | + continue; |
| 193 | + } |
| 194 | + |
| 195 | + if ($character === ']') { |
| 196 | + $bracketDepth--; |
| 197 | + continue; |
| 198 | + } |
| 199 | + |
| 200 | + if ($character === '>' && $bracketDepth === 0) { |
| 201 | + return $i + 1; |
| 202 | + } |
| 203 | + } |
| 204 | + |
| 205 | + return $length; |
| 206 | + } |
| 207 | + |
111 | 208 | private function createLineAtOffset(int $lineNumber, int $lineBeginOffset): Line |
112 | 209 | { |
113 | 210 | $sourceLength = strlen($this->content); |
|
0 commit comments