[ Index ] |
PHP Cross Reference of phpBB-3.2.11-deutsch |
[Summary view] [Print] [Text view]
1 <?php 2 3 /* 4 * @package s9e\TextFormatter 5 * @copyright Copyright (c) 2010-2019 The s9e Authors 6 * @license http://www.opensource.org/licenses/mit-license.php The MIT License 7 */ 8 namespace s9e\TextFormatter; 9 use InvalidArgumentException; 10 use RuntimeException; 11 use s9e\TextFormatter\Parser\FilterProcessing; 12 use s9e\TextFormatter\Parser\Logger; 13 use s9e\TextFormatter\Parser\Tag; 14 class Parser 15 { 16 const RULE_AUTO_CLOSE = 1; 17 const RULE_AUTO_REOPEN = 2; 18 const RULE_BREAK_PARAGRAPH = 4; 19 const RULE_CREATE_PARAGRAPHS = 8; 20 const RULE_DISABLE_AUTO_BR = 16; 21 const RULE_ENABLE_AUTO_BR = 32; 22 const RULE_IGNORE_TAGS = 64; 23 const RULE_IGNORE_TEXT = 128; 24 const RULE_IGNORE_WHITESPACE = 256; 25 const RULE_IS_TRANSPARENT = 512; 26 const RULE_PREVENT_BR = 1024; 27 const RULE_SUSPEND_AUTO_BR = 2048; 28 const RULE_TRIM_FIRST_LINE = 4096; 29 const RULES_AUTO_LINEBREAKS = 2096; 30 const RULES_INHERITANCE = 32; 31 const WHITESPACE = ' 32 '; 33 protected $cntOpen; 34 protected $cntTotal; 35 protected $context; 36 protected $currentFixingCost; 37 protected $currentTag; 38 protected $isRich; 39 protected $logger; 40 public $maxFixingCost = 10000; 41 protected $namespaces; 42 protected $openTags; 43 protected $output; 44 protected $pos; 45 protected $pluginParsers = []; 46 protected $pluginsConfig; 47 public $registeredVars = []; 48 protected $rootContext; 49 protected $tagsConfig; 50 protected $tagStack; 51 protected $tagStackIsSorted; 52 protected $text; 53 protected $textLen; 54 protected $uid = 0; 55 protected $wsPos; 56 public function __construct(array $config) 57 { 58 $this->pluginsConfig = $config['plugins']; 59 $this->registeredVars = $config['registeredVars']; 60 $this->rootContext = $config['rootContext']; 61 $this->tagsConfig = $config['tags']; 62 $this->__wakeup(); 63 } 64 public function __sleep() 65 { 66 return ['pluginsConfig', 'registeredVars', 'rootContext', 'tagsConfig']; 67 } 68 public function __wakeup() 69 { 70 $this->logger = new Logger; 71 } 72 protected function reset($text) 73 { 74 if (!\preg_match('//u', $text)) 75 throw new InvalidArgumentException('Invalid UTF-8 input'); 76 $text = \preg_replace('/\\r\\n?/', "\n", $text); 77 $text = \preg_replace('/[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]+/S', '', $text); 78 $this->logger->clear(); 79 $this->cntOpen = []; 80 $this->cntTotal = []; 81 $this->currentFixingCost = 0; 82 $this->currentTag = \null; 83 $this->isRich = \false; 84 $this->namespaces = []; 85 $this->openTags = []; 86 $this->output = ''; 87 $this->pos = 0; 88 $this->tagStack = []; 89 $this->tagStackIsSorted = \false; 90 $this->text = $text; 91 $this->textLen = \strlen($text); 92 $this->wsPos = 0; 93 $this->context = $this->rootContext; 94 $this->context['inParagraph'] = \false; 95 ++$this->uid; 96 } 97 protected function setTagOption($tagName, $optionName, $optionValue) 98 { 99 if (isset($this->tagsConfig[$tagName])) 100 { 101 $tagConfig = $this->tagsConfig[$tagName]; 102 unset($this->tagsConfig[$tagName]); 103 $tagConfig[$optionName] = $optionValue; 104 $this->tagsConfig[$tagName] = $tagConfig; 105 } 106 } 107 public function disableTag($tagName) 108 { 109 $this->setTagOption($tagName, 'isDisabled', \true); 110 } 111 public function enableTag($tagName) 112 { 113 if (isset($this->tagsConfig[$tagName])) 114 unset($this->tagsConfig[$tagName]['isDisabled']); 115 } 116 public function getLogger() 117 { 118 return $this->logger; 119 } 120 public function getText() 121 { 122 return $this->text; 123 } 124 public function parse($text) 125 { 126 $this->reset($text); 127 $uid = $this->uid; 128 $this->executePluginParsers(); 129 $this->processTags(); 130 $this->finalizeOutput(); 131 if ($this->uid !== $uid) 132 throw new RuntimeException('The parser has been reset during execution'); 133 if ($this->currentFixingCost > $this->maxFixingCost) 134 $this->logger->warn('Fixing cost limit exceeded'); 135 return $this->output; 136 } 137 public function setTagLimit($tagName, $tagLimit) 138 { 139 $this->setTagOption($tagName, 'tagLimit', $tagLimit); 140 } 141 public function setNestingLimit($tagName, $nestingLimit) 142 { 143 $this->setTagOption($tagName, 'nestingLimit', $nestingLimit); 144 } 145 protected function finalizeOutput() 146 { 147 $this->outputText($this->textLen, 0, \true); 148 do 149 { 150 $this->output = \preg_replace('(<([^ />]++)[^>]*></\\1>)', '', $this->output, -1, $cnt); 151 } 152 while ($cnt > 0); 153 if (\strpos($this->output, '</i><i>') !== \false) 154 $this->output = \str_replace('</i><i>', '', $this->output); 155 $this->output = \preg_replace('([\\x00-\\x08\\x0B-\\x1F])', '', $this->output); 156 $this->output = Utils::encodeUnicodeSupplementaryCharacters($this->output); 157 $tagName = ($this->isRich) ? 'r' : 't'; 158 $tmp = '<' . $tagName; 159 foreach (\array_keys($this->namespaces) as $prefix) 160 $tmp .= ' xmlns:' . $prefix . '="urn:s9e:TextFormatter:' . $prefix . '"'; 161 $this->output = $tmp . '>' . $this->output . '</' . $tagName . '>'; 162 } 163 protected function outputTag(Tag $tag) 164 { 165 $this->isRich = \true; 166 $tagName = $tag->getName(); 167 $tagPos = $tag->getPos(); 168 $tagLen = $tag->getLen(); 169 $tagFlags = $tag->getFlags(); 170 if ($tagFlags & self::RULE_IGNORE_WHITESPACE) 171 { 172 $skipBefore = 1; 173 $skipAfter = ($tag->isEndTag()) ? 2 : 1; 174 } 175 else 176 $skipBefore = $skipAfter = 0; 177 $closeParagraph = \false; 178 if ($tag->isStartTag()) 179 { 180 if ($tagFlags & self::RULE_BREAK_PARAGRAPH) 181 $closeParagraph = \true; 182 } 183 else 184 $closeParagraph = \true; 185 $this->outputText($tagPos, $skipBefore, $closeParagraph); 186 $tagText = ($tagLen) 187 ? \htmlspecialchars(\substr($this->text, $tagPos, $tagLen), \ENT_NOQUOTES, 'UTF-8') 188 : ''; 189 if ($tag->isStartTag()) 190 { 191 if (!($tagFlags & self::RULE_BREAK_PARAGRAPH)) 192 $this->outputParagraphStart($tagPos); 193 $colonPos = \strpos($tagName, ':'); 194 if ($colonPos) 195 $this->namespaces[\substr($tagName, 0, $colonPos)] = 0; 196 $this->output .= '<' . $tagName; 197 $attributes = $tag->getAttributes(); 198 \ksort($attributes); 199 foreach ($attributes as $attrName => $attrValue) 200 $this->output .= ' ' . $attrName . '="' . \str_replace("\n", ' ', \htmlspecialchars($attrValue, \ENT_COMPAT, 'UTF-8')) . '"'; 201 if ($tag->isSelfClosingTag()) 202 if ($tagLen) 203 $this->output .= '>' . $tagText . '</' . $tagName . '>'; 204 else 205 $this->output .= '/>'; 206 elseif ($tagLen) 207 $this->output .= '><s>' . $tagText . '</s>'; 208 else 209 $this->output .= '>'; 210 } 211 else 212 { 213 if ($tagLen) 214 $this->output .= '<e>' . $tagText . '</e>'; 215 $this->output .= '</' . $tagName . '>'; 216 } 217 $this->pos = $tagPos + $tagLen; 218 $this->wsPos = $this->pos; 219 while ($skipAfter && $this->wsPos < $this->textLen && $this->text[$this->wsPos] === "\n") 220 { 221 --$skipAfter; 222 ++$this->wsPos; 223 } 224 } 225 protected function outputText($catchupPos, $maxLines, $closeParagraph) 226 { 227 if ($closeParagraph) 228 if (!($this->context['flags'] & self::RULE_CREATE_PARAGRAPHS)) 229 $closeParagraph = \false; 230 else 231 $maxLines = -1; 232 if ($this->pos >= $catchupPos) 233 { 234 if ($closeParagraph) 235 $this->outputParagraphEnd(); 236 return; 237 } 238 if ($this->wsPos > $this->pos) 239 { 240 $skipPos = \min($catchupPos, $this->wsPos); 241 $this->output .= \substr($this->text, $this->pos, $skipPos - $this->pos); 242 $this->pos = $skipPos; 243 if ($this->pos >= $catchupPos) 244 { 245 if ($closeParagraph) 246 $this->outputParagraphEnd(); 247 return; 248 } 249 } 250 if ($this->context['flags'] & self::RULE_IGNORE_TEXT) 251 { 252 $catchupLen = $catchupPos - $this->pos; 253 $catchupText = \substr($this->text, $this->pos, $catchupLen); 254 if (\strspn($catchupText, " \n\t") < $catchupLen) 255 $catchupText = '<i>' . \htmlspecialchars($catchupText, \ENT_NOQUOTES, 'UTF-8') . '</i>'; 256 $this->output .= $catchupText; 257 $this->pos = $catchupPos; 258 if ($closeParagraph) 259 $this->outputParagraphEnd(); 260 return; 261 } 262 $ignorePos = $catchupPos; 263 $ignoreLen = 0; 264 while ($maxLines && --$ignorePos >= $this->pos) 265 { 266 $c = $this->text[$ignorePos]; 267 if (\strpos(self::WHITESPACE, $c) === \false) 268 break; 269 if ($c === "\n") 270 --$maxLines; 271 ++$ignoreLen; 272 } 273 $catchupPos -= $ignoreLen; 274 if ($this->context['flags'] & self::RULE_CREATE_PARAGRAPHS) 275 { 276 if (!$this->context['inParagraph']) 277 { 278 $this->outputWhitespace($catchupPos); 279 if ($catchupPos > $this->pos) 280 $this->outputParagraphStart($catchupPos); 281 } 282 $pbPos = \strpos($this->text, "\n\n", $this->pos); 283 while ($pbPos !== \false && $pbPos < $catchupPos) 284 { 285 $this->outputText($pbPos, 0, \true); 286 $this->outputParagraphStart($catchupPos); 287 $pbPos = \strpos($this->text, "\n\n", $this->pos); 288 } 289 } 290 if ($catchupPos > $this->pos) 291 { 292 $catchupText = \htmlspecialchars( 293 \substr($this->text, $this->pos, $catchupPos - $this->pos), 294 \ENT_NOQUOTES, 295 'UTF-8' 296 ); 297 if (($this->context['flags'] & self::RULES_AUTO_LINEBREAKS) === self::RULE_ENABLE_AUTO_BR) 298 $catchupText = \str_replace("\n", "<br/>\n", $catchupText); 299 $this->output .= $catchupText; 300 } 301 if ($closeParagraph) 302 $this->outputParagraphEnd(); 303 if ($ignoreLen) 304 $this->output .= \substr($this->text, $catchupPos, $ignoreLen); 305 $this->pos = $catchupPos + $ignoreLen; 306 } 307 protected function outputBrTag(Tag $tag) 308 { 309 $this->outputText($tag->getPos(), 0, \false); 310 $this->output .= '<br/>'; 311 } 312 protected function outputIgnoreTag(Tag $tag) 313 { 314 $tagPos = $tag->getPos(); 315 $tagLen = $tag->getLen(); 316 $ignoreText = \substr($this->text, $tagPos, $tagLen); 317 $this->outputText($tagPos, 0, \false); 318 $this->output .= '<i>' . \htmlspecialchars($ignoreText, \ENT_NOQUOTES, 'UTF-8') . '</i>'; 319 $this->isRich = \true; 320 $this->pos = $tagPos + $tagLen; 321 } 322 protected function outputParagraphStart($maxPos) 323 { 324 if ($this->context['inParagraph'] 325 || !($this->context['flags'] & self::RULE_CREATE_PARAGRAPHS)) 326 return; 327 $this->outputWhitespace($maxPos); 328 if ($this->pos < $this->textLen) 329 { 330 $this->output .= '<p>'; 331 $this->context['inParagraph'] = \true; 332 } 333 } 334 protected function outputParagraphEnd() 335 { 336 if (!$this->context['inParagraph']) 337 return; 338 $this->output .= '</p>'; 339 $this->context['inParagraph'] = \false; 340 } 341 protected function outputVerbatim(Tag $tag) 342 { 343 $flags = $this->context['flags']; 344 $this->context['flags'] = $tag->getFlags(); 345 $this->outputText($this->currentTag->getPos() + $this->currentTag->getLen(), 0, \false); 346 $this->context['flags'] = $flags; 347 } 348 protected function outputWhitespace($maxPos) 349 { 350 if ($maxPos > $this->pos) 351 { 352 $spn = \strspn($this->text, self::WHITESPACE, $this->pos, $maxPos - $this->pos); 353 if ($spn) 354 { 355 $this->output .= \substr($this->text, $this->pos, $spn); 356 $this->pos += $spn; 357 } 358 } 359 } 360 public function disablePlugin($pluginName) 361 { 362 if (isset($this->pluginsConfig[$pluginName])) 363 { 364 $pluginConfig = $this->pluginsConfig[$pluginName]; 365 unset($this->pluginsConfig[$pluginName]); 366 $pluginConfig['isDisabled'] = \true; 367 $this->pluginsConfig[$pluginName] = $pluginConfig; 368 } 369 } 370 public function enablePlugin($pluginName) 371 { 372 if (isset($this->pluginsConfig[$pluginName])) 373 $this->pluginsConfig[$pluginName]['isDisabled'] = \false; 374 } 375 protected function executePluginParser($pluginName) 376 { 377 $pluginConfig = $this->pluginsConfig[$pluginName]; 378 if (isset($pluginConfig['quickMatch']) && \strpos($this->text, $pluginConfig['quickMatch']) === \false) 379 return; 380 $matches = []; 381 if (isset($pluginConfig['regexp'])) 382 { 383 $matches = $this->getMatches($pluginConfig['regexp'], $pluginConfig['regexpLimit']); 384 if (empty($matches)) 385 return; 386 } 387 \call_user_func($this->getPluginParser($pluginName), $this->text, $matches); 388 } 389 protected function executePluginParsers() 390 { 391 foreach ($this->pluginsConfig as $pluginName => $pluginConfig) 392 if (empty($pluginConfig['isDisabled'])) 393 $this->executePluginParser($pluginName); 394 } 395 protected function getMatches($regexp, $limit) 396 { 397 $cnt = \preg_match_all($regexp, $this->text, $matches, \PREG_SET_ORDER | \PREG_OFFSET_CAPTURE); 398 if ($cnt > $limit) 399 $matches = \array_slice($matches, 0, $limit); 400 return $matches; 401 } 402 protected function getPluginParser($pluginName) 403 { 404 if (!isset($this->pluginParsers[$pluginName])) 405 { 406 $pluginConfig = $this->pluginsConfig[$pluginName]; 407 $className = (isset($pluginConfig['className'])) 408 ? $pluginConfig['className'] 409 : 's9e\\TextFormatter\\Plugins\\' . $pluginName . '\\Parser'; 410 $this->pluginParsers[$pluginName] = [new $className($this, $pluginConfig), 'parse']; 411 } 412 return $this->pluginParsers[$pluginName]; 413 } 414 public function registerParser($pluginName, $parser, $regexp = \null, $limit = \PHP_INT_MAX) 415 { 416 if (!\is_callable($parser)) 417 throw new InvalidArgumentException('Argument 1 passed to ' . __METHOD__ . ' must be a valid callback'); 418 if (!isset($this->pluginsConfig[$pluginName])) 419 $this->pluginsConfig[$pluginName] = []; 420 if (isset($regexp)) 421 { 422 $this->pluginsConfig[$pluginName]['regexp'] = $regexp; 423 $this->pluginsConfig[$pluginName]['regexpLimit'] = $limit; 424 } 425 $this->pluginParsers[$pluginName] = $parser; 426 } 427 protected function closeAncestor(Tag $tag) 428 { 429 if (!empty($this->openTags)) 430 { 431 $tagName = $tag->getName(); 432 $tagConfig = $this->tagsConfig[$tagName]; 433 if (!empty($tagConfig['rules']['closeAncestor'])) 434 { 435 $i = \count($this->openTags); 436 while (--$i >= 0) 437 { 438 $ancestor = $this->openTags[$i]; 439 $ancestorName = $ancestor->getName(); 440 if (isset($tagConfig['rules']['closeAncestor'][$ancestorName])) 441 { 442 ++$this->currentFixingCost; 443 $this->tagStack[] = $tag; 444 $this->addMagicEndTag($ancestor, $tag->getPos(), $tag->getSortPriority() - 1); 445 return \true; 446 } 447 } 448 } 449 } 450 return \false; 451 } 452 protected function closeParent(Tag $tag) 453 { 454 if (!empty($this->openTags)) 455 { 456 $tagName = $tag->getName(); 457 $tagConfig = $this->tagsConfig[$tagName]; 458 if (!empty($tagConfig['rules']['closeParent'])) 459 { 460 $parent = \end($this->openTags); 461 $parentName = $parent->getName(); 462 if (isset($tagConfig['rules']['closeParent'][$parentName])) 463 { 464 ++$this->currentFixingCost; 465 $this->tagStack[] = $tag; 466 $this->addMagicEndTag($parent, $tag->getPos(), $tag->getSortPriority() - 1); 467 return \true; 468 } 469 } 470 } 471 return \false; 472 } 473 protected function createChild(Tag $tag) 474 { 475 $tagConfig = $this->tagsConfig[$tag->getName()]; 476 if (isset($tagConfig['rules']['createChild'])) 477 { 478 $priority = -1000; 479 $tagPos = $this->pos + \strspn($this->text, " \n\r\t", $this->pos); 480 foreach ($tagConfig['rules']['createChild'] as $tagName) 481 $this->addStartTag($tagName, $tagPos, 0, ++$priority); 482 } 483 } 484 protected function fosterParent(Tag $tag) 485 { 486 if (!empty($this->openTags)) 487 { 488 $tagName = $tag->getName(); 489 $tagConfig = $this->tagsConfig[$tagName]; 490 if (!empty($tagConfig['rules']['fosterParent'])) 491 { 492 $parent = \end($this->openTags); 493 $parentName = $parent->getName(); 494 if (isset($tagConfig['rules']['fosterParent'][$parentName])) 495 { 496 if ($parentName !== $tagName && $this->currentFixingCost < $this->maxFixingCost) 497 $this->addFosterTag($tag, $parent); 498 $this->tagStack[] = $tag; 499 $this->addMagicEndTag($parent, $tag->getPos(), $tag->getSortPriority() - 1); 500 $this->currentFixingCost += 4; 501 return \true; 502 } 503 } 504 } 505 return \false; 506 } 507 protected function requireAncestor(Tag $tag) 508 { 509 $tagName = $tag->getName(); 510 $tagConfig = $this->tagsConfig[$tagName]; 511 if (isset($tagConfig['rules']['requireAncestor'])) 512 { 513 foreach ($tagConfig['rules']['requireAncestor'] as $ancestorName) 514 if (!empty($this->cntOpen[$ancestorName])) 515 return \false; 516 $this->logger->err('Tag requires an ancestor', [ 517 'requireAncestor' => \implode(',', $tagConfig['rules']['requireAncestor']), 518 'tag' => $tag 519 ]); 520 return \true; 521 } 522 return \false; 523 } 524 protected function addFosterTag(Tag $tag, Tag $fosterTag) 525 { 526 list($childPos, $childPrio) = $this->getMagicStartCoords($tag->getPos() + $tag->getLen()); 527 $childTag = $this->addCopyTag($fosterTag, $childPos, 0, $childPrio); 528 $tag->cascadeInvalidationTo($childTag); 529 } 530 protected function addMagicEndTag(Tag $startTag, $tagPos, $prio = 0) 531 { 532 $tagName = $startTag->getName(); 533 if (($this->currentTag->getFlags() | $startTag->getFlags()) & self::RULE_IGNORE_WHITESPACE) 534 $tagPos = $this->getMagicEndPos($tagPos); 535 $endTag = $this->addEndTag($tagName, $tagPos, 0, $prio); 536 $endTag->pairWith($startTag); 537 return $endTag; 538 } 539 protected function getMagicEndPos($tagPos) 540 { 541 while ($tagPos > $this->pos && \strpos(self::WHITESPACE, $this->text[$tagPos - 1]) !== \false) 542 --$tagPos; 543 return $tagPos; 544 } 545 protected function getMagicStartCoords($tagPos) 546 { 547 if (empty($this->tagStack)) 548 { 549 $nextPos = $this->textLen + 1; 550 $nextPrio = 0; 551 } 552 else 553 { 554 $nextTag = \end($this->tagStack); 555 $nextPos = $nextTag->getPos(); 556 $nextPrio = $nextTag->getSortPriority(); 557 } 558 while ($tagPos < $nextPos && \strpos(self::WHITESPACE, $this->text[$tagPos]) !== \false) 559 ++$tagPos; 560 $prio = ($tagPos === $nextPos) ? $nextPrio - 1 : 0; 561 return [$tagPos, $prio]; 562 } 563 protected function isFollowedByClosingTag(Tag $tag) 564 { 565 return (empty($this->tagStack)) ? \false : \end($this->tagStack)->canClose($tag); 566 } 567 protected function processTags() 568 { 569 if (empty($this->tagStack)) 570 return; 571 foreach (\array_keys($this->tagsConfig) as $tagName) 572 { 573 $this->cntOpen[$tagName] = 0; 574 $this->cntTotal[$tagName] = 0; 575 } 576 do 577 { 578 while (!empty($this->tagStack)) 579 { 580 if (!$this->tagStackIsSorted) 581 $this->sortTags(); 582 $this->currentTag = \array_pop($this->tagStack); 583 $this->processCurrentTag(); 584 } 585 foreach ($this->openTags as $startTag) 586 $this->addMagicEndTag($startTag, $this->textLen); 587 } 588 while (!empty($this->tagStack)); 589 } 590 protected function processCurrentTag() 591 { 592 if (($this->context['flags'] & self::RULE_IGNORE_TAGS) 593 && !$this->currentTag->canClose(\end($this->openTags)) 594 && !$this->currentTag->isSystemTag()) 595 $this->currentTag->invalidate(); 596 $tagPos = $this->currentTag->getPos(); 597 $tagLen = $this->currentTag->getLen(); 598 if ($this->pos > $tagPos && !$this->currentTag->isInvalid()) 599 { 600 $startTag = $this->currentTag->getStartTag(); 601 if ($startTag && \in_array($startTag, $this->openTags, \true)) 602 { 603 $this->addEndTag( 604 $startTag->getName(), 605 $this->pos, 606 \max(0, $tagPos + $tagLen - $this->pos) 607 )->pairWith($startTag); 608 return; 609 } 610 if ($this->currentTag->isIgnoreTag()) 611 { 612 $ignoreLen = $tagPos + $tagLen - $this->pos; 613 if ($ignoreLen > 0) 614 { 615 $this->addIgnoreTag($this->pos, $ignoreLen); 616 return; 617 } 618 } 619 $this->currentTag->invalidate(); 620 } 621 if ($this->currentTag->isInvalid()) 622 return; 623 if ($this->currentTag->isIgnoreTag()) 624 $this->outputIgnoreTag($this->currentTag); 625 elseif ($this->currentTag->isBrTag()) 626 { 627 if (!($this->context['flags'] & self::RULE_PREVENT_BR)) 628 $this->outputBrTag($this->currentTag); 629 } 630 elseif ($this->currentTag->isParagraphBreak()) 631 $this->outputText($this->currentTag->getPos(), 0, \true); 632 elseif ($this->currentTag->isVerbatim()) 633 $this->outputVerbatim($this->currentTag); 634 elseif ($this->currentTag->isStartTag()) 635 $this->processStartTag($this->currentTag); 636 else 637 $this->processEndTag($this->currentTag); 638 } 639 protected function processStartTag(Tag $tag) 640 { 641 $tagName = $tag->getName(); 642 $tagConfig = $this->tagsConfig[$tagName]; 643 if ($this->cntTotal[$tagName] >= $tagConfig['tagLimit']) 644 { 645 $this->logger->err( 646 'Tag limit exceeded', 647 [ 648 'tag' => $tag, 649 'tagName' => $tagName, 650 'tagLimit' => $tagConfig['tagLimit'] 651 ] 652 ); 653 $tag->invalidate(); 654 return; 655 } 656 FilterProcessing::filterTag($tag, $this, $this->tagsConfig, $this->openTags); 657 if ($tag->isInvalid()) 658 return; 659 if ($this->currentFixingCost < $this->maxFixingCost) 660 if ($this->fosterParent($tag) || $this->closeParent($tag) || $this->closeAncestor($tag)) 661 return; 662 if ($this->cntOpen[$tagName] >= $tagConfig['nestingLimit']) 663 { 664 $this->logger->err( 665 'Nesting limit exceeded', 666 [ 667 'tag' => $tag, 668 'tagName' => $tagName, 669 'nestingLimit' => $tagConfig['nestingLimit'] 670 ] 671 ); 672 $tag->invalidate(); 673 return; 674 } 675 if (!$this->tagIsAllowed($tagName)) 676 { 677 $msg = 'Tag is not allowed in this context'; 678 $context = ['tag' => $tag, 'tagName' => $tagName]; 679 if ($tag->getLen() > 0) 680 $this->logger->warn($msg, $context); 681 else 682 $this->logger->debug($msg, $context); 683 $tag->invalidate(); 684 return; 685 } 686 if ($this->requireAncestor($tag)) 687 { 688 $tag->invalidate(); 689 return; 690 } 691 if ($tag->getFlags() & self::RULE_AUTO_CLOSE 692 && !$tag->getEndTag() 693 && !$this->isFollowedByClosingTag($tag)) 694 { 695 $newTag = new Tag(Tag::SELF_CLOSING_TAG, $tagName, $tag->getPos(), $tag->getLen()); 696 $newTag->setAttributes($tag->getAttributes()); 697 $newTag->setFlags($tag->getFlags()); 698 $tag = $newTag; 699 } 700 if ($tag->getFlags() & self::RULE_TRIM_FIRST_LINE 701 && !$tag->getEndTag() 702 && \substr($this->text, $tag->getPos() + $tag->getLen(), 1) === "\n") 703 $this->addIgnoreTag($tag->getPos() + $tag->getLen(), 1); 704 $this->outputTag($tag); 705 $this->pushContext($tag); 706 $this->createChild($tag); 707 } 708 protected function processEndTag(Tag $tag) 709 { 710 $tagName = $tag->getName(); 711 if (empty($this->cntOpen[$tagName])) 712 return; 713 $closeTags = []; 714 $i = \count($this->openTags); 715 while (--$i >= 0) 716 { 717 $openTag = $this->openTags[$i]; 718 if ($tag->canClose($openTag)) 719 break; 720 $closeTags[] = $openTag; 721 ++$this->currentFixingCost; 722 } 723 if ($i < 0) 724 { 725 $this->logger->debug('Skipping end tag with no start tag', ['tag' => $tag]); 726 return; 727 } 728 $flags = $tag->getFlags(); 729 foreach ($closeTags as $openTag) 730 $flags |= $openTag->getFlags(); 731 $ignoreWhitespace = (bool) ($flags & self::RULE_IGNORE_WHITESPACE); 732 $keepReopening = (bool) ($this->currentFixingCost < $this->maxFixingCost); 733 $reopenTags = []; 734 foreach ($closeTags as $openTag) 735 { 736 $openTagName = $openTag->getName(); 737 if ($keepReopening) 738 if ($openTag->getFlags() & self::RULE_AUTO_REOPEN) 739 $reopenTags[] = $openTag; 740 else 741 $keepReopening = \false; 742 $tagPos = $tag->getPos(); 743 if ($ignoreWhitespace) 744 $tagPos = $this->getMagicEndPos($tagPos); 745 $endTag = new Tag(Tag::END_TAG, $openTagName, $tagPos, 0); 746 $endTag->setFlags($openTag->getFlags()); 747 $this->outputTag($endTag); 748 $this->popContext(); 749 } 750 $this->outputTag($tag); 751 $this->popContext(); 752 if (!empty($closeTags) && $this->currentFixingCost < $this->maxFixingCost) 753 { 754 $ignorePos = $this->pos; 755 $i = \count($this->tagStack); 756 while (--$i >= 0 && ++$this->currentFixingCost < $this->maxFixingCost) 757 { 758 $upcomingTag = $this->tagStack[$i]; 759 if ($upcomingTag->getPos() > $ignorePos 760 || $upcomingTag->isStartTag()) 761 break; 762 $j = \count($closeTags); 763 while (--$j >= 0 && ++$this->currentFixingCost < $this->maxFixingCost) 764 if ($upcomingTag->canClose($closeTags[$j])) 765 { 766 \array_splice($closeTags, $j, 1); 767 if (isset($reopenTags[$j])) 768 \array_splice($reopenTags, $j, 1); 769 $ignorePos = \max( 770 $ignorePos, 771 $upcomingTag->getPos() + $upcomingTag->getLen() 772 ); 773 break; 774 } 775 } 776 if ($ignorePos > $this->pos) 777 $this->outputIgnoreTag(new Tag(Tag::SELF_CLOSING_TAG, 'i', $this->pos, $ignorePos - $this->pos)); 778 } 779 foreach ($reopenTags as $startTag) 780 { 781 $newTag = $this->addCopyTag($startTag, $this->pos, 0); 782 $endTag = $startTag->getEndTag(); 783 if ($endTag) 784 $newTag->pairWith($endTag); 785 } 786 } 787 protected function popContext() 788 { 789 $tag = \array_pop($this->openTags); 790 --$this->cntOpen[$tag->getName()]; 791 $this->context = $this->context['parentContext']; 792 } 793 protected function pushContext(Tag $tag) 794 { 795 $tagName = $tag->getName(); 796 $tagFlags = $tag->getFlags(); 797 $tagConfig = $this->tagsConfig[$tagName]; 798 ++$this->cntTotal[$tagName]; 799 if ($tag->isSelfClosingTag()) 800 return; 801 $allowed = []; 802 if ($tagFlags & self::RULE_IS_TRANSPARENT) 803 foreach ($this->context['allowed'] as $k => $v) 804 $allowed[] = $tagConfig['allowed'][$k] & $v; 805 else 806 foreach ($this->context['allowed'] as $k => $v) 807 $allowed[] = $tagConfig['allowed'][$k] & (($v & 0xFF00) | ($v >> 8)); 808 $flags = $tagFlags | ($this->context['flags'] & self::RULES_INHERITANCE); 809 if ($flags & self::RULE_DISABLE_AUTO_BR) 810 $flags &= ~self::RULE_ENABLE_AUTO_BR; 811 ++$this->cntOpen[$tagName]; 812 $this->openTags[] = $tag; 813 $this->context = [ 814 'allowed' => $allowed, 815 'flags' => $flags, 816 'inParagraph' => \false, 817 'parentContext' => $this->context 818 ]; 819 } 820 protected function tagIsAllowed($tagName) 821 { 822 $n = $this->tagsConfig[$tagName]['bitNumber']; 823 return (bool) ($this->context['allowed'][$n >> 3] & (1 << ($n & 7))); 824 } 825 public function addStartTag($name, $pos, $len, $prio = 0) 826 { 827 return $this->addTag(Tag::START_TAG, $name, $pos, $len, $prio); 828 } 829 public function addEndTag($name, $pos, $len, $prio = 0) 830 { 831 return $this->addTag(Tag::END_TAG, $name, $pos, $len, $prio); 832 } 833 public function addSelfClosingTag($name, $pos, $len, $prio = 0) 834 { 835 return $this->addTag(Tag::SELF_CLOSING_TAG, $name, $pos, $len, $prio); 836 } 837 public function addBrTag($pos, $prio = 0) 838 { 839 return $this->addTag(Tag::SELF_CLOSING_TAG, 'br', $pos, 0, $prio); 840 } 841 public function addIgnoreTag($pos, $len, $prio = 0) 842 { 843 return $this->addTag(Tag::SELF_CLOSING_TAG, 'i', $pos, \min($len, $this->textLen - $pos), $prio); 844 } 845 public function addParagraphBreak($pos, $prio = 0) 846 { 847 return $this->addTag(Tag::SELF_CLOSING_TAG, 'pb', $pos, 0, $prio); 848 } 849 public function addCopyTag(Tag $tag, $pos, $len, $prio = \null) 850 { 851 if (!isset($prio)) 852 $prio = $tag->getSortPriority(); 853 $copy = $this->addTag($tag->getType(), $tag->getName(), $pos, $len, $prio); 854 $copy->setAttributes($tag->getAttributes()); 855 return $copy; 856 } 857 protected function addTag($type, $name, $pos, $len, $prio) 858 { 859 $tag = new Tag($type, $name, $pos, $len, $prio); 860 if (isset($this->tagsConfig[$name])) 861 $tag->setFlags($this->tagsConfig[$name]['rules']['flags']); 862 if ((!isset($this->tagsConfig[$name]) && !$tag->isSystemTag()) 863 || $this->isInvalidTextSpan($pos, $len)) 864 $tag->invalidate(); 865 elseif (!empty($this->tagsConfig[$name]['isDisabled'])) 866 { 867 $this->logger->warn( 868 'Tag is disabled', 869 [ 870 'tag' => $tag, 871 'tagName' => $name 872 ] 873 ); 874 $tag->invalidate(); 875 } 876 else 877 $this->insertTag($tag); 878 return $tag; 879 } 880 protected function isInvalidTextSpan($pos, $len) 881 { 882 return ($len < 0 || $pos < 0 || $pos + $len > $this->textLen || \preg_match('([\\x80-\\xBF])', \substr($this->text, $pos, 1) . \substr($this->text, $pos + $len, 1))); 883 } 884 protected function insertTag(Tag $tag) 885 { 886 if (!$this->tagStackIsSorted) 887 $this->tagStack[] = $tag; 888 else 889 { 890 $i = \count($this->tagStack); 891 while ($i > 0 && self::compareTags($this->tagStack[$i - 1], $tag) > 0) 892 { 893 $this->tagStack[$i] = $this->tagStack[$i - 1]; 894 --$i; 895 } 896 $this->tagStack[$i] = $tag; 897 } 898 } 899 public function addTagPair($name, $startPos, $startLen, $endPos, $endLen, $prio = 0) 900 { 901 $endTag = $this->addEndTag($name, $endPos, $endLen, -$prio); 902 $startTag = $this->addStartTag($name, $startPos, $startLen, $prio); 903 $startTag->pairWith($endTag); 904 return $startTag; 905 } 906 public function addVerbatim($pos, $len, $prio = 0) 907 { 908 return $this->addTag(Tag::SELF_CLOSING_TAG, 'v', $pos, $len, $prio); 909 } 910 protected function sortTags() 911 { 912 \usort($this->tagStack, __CLASS__ . '::compareTags'); 913 $this->tagStackIsSorted = \true; 914 } 915 protected static function compareTags(Tag $a, Tag $b) 916 { 917 $aPos = $a->getPos(); 918 $bPos = $b->getPos(); 919 if ($aPos !== $bPos) 920 return $bPos - $aPos; 921 if ($a->getSortPriority() !== $b->getSortPriority()) 922 return $b->getSortPriority() - $a->getSortPriority(); 923 $aLen = $a->getLen(); 924 $bLen = $b->getLen(); 925 if (!$aLen || !$bLen) 926 { 927 if (!$aLen && !$bLen) 928 { 929 $order = [ 930 Tag::END_TAG => 0, 931 Tag::SELF_CLOSING_TAG => 1, 932 Tag::START_TAG => 2 933 ]; 934 return $order[$b->getType()] - $order[$a->getType()]; 935 } 936 return ($aLen) ? -1 : 1; 937 } 938 return $aLen - $bLen; 939 } 940 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Nov 11 20:33:01 2020 | Cross-referenced by PHPXref 0.7.1 |