[ Index ]

PHP Cross Reference of phpBB-3.2.8-deutsch

title

Body

[close]

/vendor/s9e/text-formatter/src/ -> Parser.php (source)

   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", '&#10;', \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  }


Generated: Tue Apr 7 19:42:26 2020 Cross-referenced by PHPXref 0.7.1