[ Index ]

PHP Cross Reference of phpBB-3.3.14-deutsch

title

Body

[close]

/vendor/s9e/text-formatter/src/Plugins/Litedown/Parser/Passes/ -> Blocks.php (source)

   1  <?php
   2  
   3  /**
   4  * @package   s9e\TextFormatter
   5  * @copyright Copyright (c) 2010-2022 The s9e authors
   6  * @license   http://www.opensource.org/licenses/mit-license.php The MIT License
   7  */
   8  namespace s9e\TextFormatter\Plugins\Litedown\Parser\Passes;
   9  
  10  use s9e\TextFormatter\Parser as Rules;
  11  
  12  class Blocks extends AbstractPass
  13  {
  14      /**
  15      * @var array
  16      */
  17      protected $setextLines = [];
  18  
  19      /**
  20      * {@inheritdoc}
  21      */
  22  	public function parse()
  23      {
  24          $this->matchSetextLines();
  25  
  26          $blocks       = [];
  27          $blocksCnt    = 0;
  28          $codeFence    = null;
  29          $codeIndent   = 4;
  30          $codeTag      = null;
  31          $lineIsEmpty  = true;
  32          $lists        = [];
  33          $listsCnt     = 0;
  34          $newContext   = false;
  35          $textBoundary = 0;
  36  
  37          $regexp = '/^(?:(?=[-*+\\d \\t>`~#_])((?: {0,3}>(?:(?!!)|!(?![^\\n>]*?!<)) ?)+)?([ \\t]+)?(\\* *\\* *\\*[* ]*$|- *- *-[- ]*$|_ *_ *_[_ ]*$|=+$)?((?:[-*+]|\\d+\\.)[ \\t]+(?=\\S))?[ \\t]*(#{1,6}[ \\t]+|```+[^`\\n]*$|~~~+[^~\\n]*$)?)?/m';
  38          preg_match_all($regexp, $this->text, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
  39  
  40          foreach ($matches as $m)
  41          {
  42              $blockDepth = 0;
  43              $blockMarks = [];
  44              $ignoreLen  = 0;
  45              $matchLen   = strlen($m[0][0]);
  46              $matchPos   = $m[0][1];
  47  
  48              // If the last line was empty then this is not a continuation, and vice-versa
  49              $continuation = !$lineIsEmpty;
  50  
  51              // Capture the position of the end of the line and determine whether the line is empty
  52              $lfPos       = $this->text->indexOf("\n", $matchPos);
  53              $lineIsEmpty = ($lfPos === $matchPos + $matchLen && empty($m[3][0]) && empty($m[4][0]) && empty($m[5][0]));
  54  
  55              // If the line is empty and it's the first empty line then we break current paragraph.
  56              $breakParagraph = ($lineIsEmpty && $continuation);
  57  
  58              // Count block marks
  59              if (!empty($m[1][0]))
  60              {
  61                  $blockMarks = $this->getBlockMarks($m[1][0]);
  62                  $blockDepth = count($blockMarks);
  63                  $ignoreLen  = strlen($m[1][0]);
  64                  if (isset($codeTag) && $codeTag->hasAttribute('blockDepth'))
  65                  {
  66                      $blockDepth = min($blockDepth, $codeTag->getAttribute('blockDepth'));
  67                      $ignoreLen  = $this->computeBlockIgnoreLen($m[1][0], $blockDepth);
  68                  }
  69  
  70                  // Overwrite block markup
  71                  $this->text->overwrite($matchPos, $ignoreLen);
  72              }
  73  
  74              // Close supernumerary blocks
  75              if ($blockDepth < $blocksCnt && !$continuation)
  76              {
  77                  $newContext = true;
  78                  do
  79                  {
  80                      $startTag = array_pop($blocks);
  81                      $this->parser->addEndTag($startTag->getName(), $textBoundary, 0)
  82                                   ->pairWith($startTag);
  83                  }
  84                  while ($blockDepth < --$blocksCnt);
  85              }
  86  
  87              // Open new blocks
  88              if ($blockDepth > $blocksCnt && !$lineIsEmpty)
  89              {
  90                  $newContext = true;
  91                  do
  92                  {
  93                      $tagName  = ($blockMarks[$blocksCnt] === '>!') ? 'SPOILER' : 'QUOTE';
  94                      $blocks[] = $this->parser->addStartTag($tagName, $matchPos, 0, -999);
  95                  }
  96                  while ($blockDepth > ++$blocksCnt);
  97              }
  98  
  99              // Compute the width of the indentation
 100              $indentWidth = 0;
 101              $indentPos   = 0;
 102              if (!empty($m[2][0]) && !$codeFence)
 103              {
 104                  $indentStr = $m[2][0];
 105                  $indentLen = strlen($indentStr);
 106                  do
 107                  {
 108                      if ($indentStr[$indentPos] === ' ')
 109                      {
 110                          ++$indentWidth;
 111                      }
 112                      else
 113                      {
 114                          $indentWidth = ($indentWidth + 4) & ~3;
 115                      }
 116                  }
 117                  while (++$indentPos < $indentLen && $indentWidth < $codeIndent);
 118              }
 119  
 120              // Test whether we're out of a code block
 121              if (isset($codeTag) && !$codeFence && $indentWidth < $codeIndent && !$lineIsEmpty)
 122              {
 123                  $newContext = true;
 124              }
 125  
 126              if ($newContext)
 127              {
 128                  $newContext = false;
 129  
 130                  // Close the code block if applicable
 131                  if (isset($codeTag))
 132                  {
 133                      if ($textBoundary > $codeTag->getPos())
 134                      {
 135                          // Overwrite the whole block
 136                          $this->text->overwrite($codeTag->getPos(), $textBoundary - $codeTag->getPos());
 137                          $codeTag->pairWith($this->parser->addEndTag('CODE', $textBoundary, 0, -1));
 138                      }
 139                      else
 140                      {
 141                          // The code block is empty
 142                          $codeTag->invalidate();
 143                      }
 144  
 145                      $codeTag = null;
 146                      $codeFence = null;
 147                  }
 148  
 149                  // Close all the lists
 150                  foreach ($lists as $list)
 151                  {
 152                      $this->closeList($list, $textBoundary);
 153                  }
 154                  $lists    = [];
 155                  $listsCnt = 0;
 156  
 157                  // Mark the block boundary
 158                  if ($matchPos)
 159                  {
 160                      $this->text->markBoundary($matchPos - 1);
 161                  }
 162              }
 163  
 164              if ($indentWidth >= $codeIndent)
 165              {
 166                  if (isset($codeTag) || !$continuation)
 167                  {
 168                      // Adjust the amount of text being ignored
 169                      $ignoreLen += $indentPos;
 170  
 171                      if (!isset($codeTag))
 172                      {
 173                          // Create code block
 174                          $codeTag = $this->parser->addStartTag('CODE', $matchPos + $ignoreLen, 0, -999);
 175                      }
 176  
 177                      // Clear the captures to prevent any further processing
 178                      $m = [];
 179                  }
 180              }
 181              elseif (!isset($codeTag))
 182              {
 183                  $hasListItem = !empty($m[4][0]);
 184  
 185                  if (!$indentWidth && !$continuation && !$hasListItem)
 186                  {
 187                      // Start of a new context
 188                      $listIndex = -1;
 189                  }
 190                  elseif ($continuation && !$hasListItem)
 191                  {
 192                      // Continuation of current list item or paragraph
 193                      $listIndex = $listsCnt - 1;
 194                  }
 195                  elseif (!$listsCnt)
 196                  {
 197                      // We're not inside of a list already, we can start one if there's a list item
 198                      $listIndex = ($hasListItem) ? 0 : -1;
 199                  }
 200                  else
 201                  {
 202                      // We're inside of a list but we need to compute the depth
 203                      $listIndex = 0;
 204                      while ($listIndex < $listsCnt && $indentWidth > $lists[$listIndex]['maxIndent'])
 205                      {
 206                          ++$listIndex;
 207                      }
 208                  }
 209  
 210                  // Close deeper lists
 211                  while ($listIndex < $listsCnt - 1)
 212                  {
 213                      $this->closeList(array_pop($lists), $textBoundary);
 214                      --$listsCnt;
 215                  }
 216  
 217                  // If there's no list item at current index, we'll need to either create one or
 218                  // drop down to previous index, in which case we have to adjust maxIndent
 219                  if ($listIndex === $listsCnt && !$hasListItem)
 220                  {
 221                      --$listIndex;
 222                  }
 223  
 224                  if ($hasListItem && $listIndex >= 0)
 225                  {
 226                      $breakParagraph = true;
 227  
 228                      // Compute the position and amount of text consumed by the item tag
 229                      $tagPos = $matchPos + $ignoreLen + $indentPos;
 230                      $tagLen = strlen($m[4][0]);
 231  
 232                      // Create a LI tag that consumes its markup
 233                      $itemTag = $this->parser->addStartTag('LI', $tagPos, $tagLen);
 234  
 235                      // Overwrite the markup
 236                      $this->text->overwrite($tagPos, $tagLen);
 237  
 238                      // If the list index is within current lists count it means this is not a new
 239                      // list and we have to close the last item. Otherwise, it's a new list that we
 240                      // have to create
 241                      if ($listIndex < $listsCnt)
 242                      {
 243                          $this->parser->addEndTag('LI', $textBoundary, 0)
 244                                       ->pairWith($lists[$listIndex]['itemTag']);
 245  
 246                          // Record the item in the list
 247                          $lists[$listIndex]['itemTag']    = $itemTag;
 248                          $lists[$listIndex]['itemTags'][] = $itemTag;
 249                      }
 250                      else
 251                      {
 252                          ++$listsCnt;
 253  
 254                          if ($listIndex)
 255                          {
 256                              $minIndent = $lists[$listIndex - 1]['maxIndent'] + 1;
 257                              $maxIndent = max($minIndent, $listIndex * 4);
 258                          }
 259                          else
 260                          {
 261                              $minIndent = 0;
 262                              $maxIndent = $indentWidth;
 263                          }
 264  
 265                          // Create a 0-width LIST tag right before the item tag LI
 266                          $listTag = $this->parser->addStartTag('LIST', $tagPos, 0);
 267  
 268                          // Test whether the list item ends with a dot, as in "1."
 269                          if (strpos($m[4][0], '.') !== false)
 270                          {
 271                              $listTag->setAttribute('type', 'decimal');
 272  
 273                              $start = (int) $m[4][0];
 274                              if ($start !== 1)
 275                              {
 276                                  $listTag->setAttribute('start', $start);
 277                              }
 278                          }
 279  
 280                          // Record the new list depth
 281                          $lists[] = [
 282                              'listTag'   => $listTag,
 283                              'itemTag'   => $itemTag,
 284                              'itemTags'  => [$itemTag],
 285                              'minIndent' => $minIndent,
 286                              'maxIndent' => $maxIndent,
 287                              'tight'     => true
 288                          ];
 289                      }
 290                  }
 291  
 292                  // If we're in a list, on a non-empty line preceded with a blank line...
 293                  if ($listsCnt && !$continuation && !$lineIsEmpty)
 294                  {
 295                      // ...and this is not the first item of the list...
 296                      if (count($lists[0]['itemTags']) > 1 || !$hasListItem)
 297                      {
 298                          // ...every list that is currently open becomes loose
 299                          foreach ($lists as &$list)
 300                          {
 301                              $list['tight'] = false;
 302                          }
 303                          unset($list);
 304                      }
 305                  }
 306  
 307                  $codeIndent = ($listsCnt + 1) * 4;
 308              }
 309  
 310              if (isset($m[5]))
 311              {
 312                  // Headers
 313                  if ($m[5][0][0] === '#')
 314                  {
 315                      $startLen = strlen($m[5][0]);
 316                      $startPos = $matchPos + $matchLen - $startLen;
 317                      $endLen   = $this->getAtxHeaderEndTagLen($matchPos + $matchLen, $lfPos);
 318                      $endPos   = $lfPos - $endLen;
 319  
 320                      $this->parser->addTagPair('H' . strspn($m[5][0], '#', 0, 6), $startPos, $startLen, $endPos, $endLen);
 321  
 322                      // Mark the start and the end of the header as boundaries
 323                      $this->text->markBoundary($startPos);
 324                      $this->text->markBoundary($lfPos);
 325  
 326                      if ($continuation)
 327                      {
 328                          $breakParagraph = true;
 329                      }
 330                  }
 331                  // Code fence
 332                  elseif ($m[5][0][0] === '`' || $m[5][0][0] === '~')
 333                  {
 334                      $tagPos = $matchPos + $ignoreLen;
 335                      $tagLen = $lfPos - $tagPos;
 336  
 337                      if (isset($codeTag) && $m[5][0] === $codeFence)
 338                      {
 339                          $codeTag->pairWith($this->parser->addEndTag('CODE', $tagPos, $tagLen, -1));
 340                          $this->parser->addIgnoreTag($textBoundary, $tagPos - $textBoundary);
 341  
 342                          // Overwrite the whole block
 343                          $this->text->overwrite($codeTag->getPos(), $tagPos + $tagLen - $codeTag->getPos());
 344                          $codeTag = null;
 345                          $codeFence = null;
 346                      }
 347                      elseif (!isset($codeTag))
 348                      {
 349                          // Create code block
 350                          $codeTag   = $this->parser->addStartTag('CODE', $tagPos, $tagLen);
 351                          $codeFence = substr($m[5][0], 0, strspn($m[5][0], '`~'));
 352                          $codeTag->setAttribute('blockDepth', $blockDepth);
 353  
 354                          // Ignore the next character, which should be a newline
 355                          $this->parser->addIgnoreTag($tagPos + $tagLen, 1);
 356  
 357                          // Add the language if present, e.g. ```php
 358                          $lang = trim(trim($m[5][0], '`~'));
 359                          if ($lang !== '')
 360                          {
 361                              $codeTag->setAttribute('lang', $lang);
 362                          }
 363                      }
 364                  }
 365              }
 366              elseif (!empty($m[3][0]) && !$listsCnt && $this->text->charAt($matchPos + $matchLen) !== "\x17")
 367              {
 368                  // Horizontal rule
 369                  $this->parser->addSelfClosingTag('HR', $matchPos + $ignoreLen, $matchLen - $ignoreLen);
 370                  $breakParagraph = true;
 371  
 372                  // Mark the end of the line as a boundary
 373                  $this->text->markBoundary($lfPos);
 374              }
 375              elseif (isset($this->setextLines[$lfPos]) && $this->setextLines[$lfPos]['blockDepth'] === $blockDepth && !$lineIsEmpty && !$listsCnt && !isset($codeTag))
 376              {
 377                  // Setext-style header
 378                  $this->parser->addTagPair(
 379                      $this->setextLines[$lfPos]['tagName'],
 380                      $matchPos + $ignoreLen,
 381                      0,
 382                      $this->setextLines[$lfPos]['endPos'],
 383                      $this->setextLines[$lfPos]['endLen']
 384                  );
 385  
 386                  // Mark the end of the Setext line
 387                  $this->text->markBoundary($this->setextLines[$lfPos]['endPos'] + $this->setextLines[$lfPos]['endLen']);
 388              }
 389  
 390              if ($breakParagraph)
 391              {
 392                  $this->parser->addParagraphBreak($textBoundary);
 393                  $this->text->markBoundary($textBoundary);
 394              }
 395  
 396              if (!$lineIsEmpty)
 397              {
 398                  $textBoundary = $lfPos;
 399              }
 400  
 401              if ($ignoreLen)
 402              {
 403                  $this->parser->addIgnoreTag($matchPos, $ignoreLen, 1000);
 404              }
 405          }
 406      }
 407  
 408      /**
 409      * Close a list at given offset
 410      *
 411      * @param  array   $list
 412      * @param  integer $textBoundary
 413      * @return void
 414      */
 415  	protected function closeList(array $list, $textBoundary)
 416      {
 417          $this->parser->addEndTag('LIST', $textBoundary, 0)->pairWith($list['listTag']);
 418          $this->parser->addEndTag('LI',   $textBoundary, 0)->pairWith($list['itemTag']);
 419  
 420          if ($list['tight'])
 421          {
 422              foreach ($list['itemTags'] as $itemTag)
 423              {
 424                  $itemTag->removeFlags(Rules::RULE_CREATE_PARAGRAPHS);
 425              }
 426          }
 427      }
 428  
 429      /**
 430      * Compute the amount of text to ignore at the start of a block line
 431      *
 432      * @param  string  $str           Original block markup
 433      * @param  integer $maxBlockDepth Maximum block depth
 434      * @return integer                Number of characters to ignore
 435      */
 436  	protected function computeBlockIgnoreLen($str, $maxBlockDepth)
 437      {
 438          $remaining = $str;
 439          while (--$maxBlockDepth >= 0)
 440          {
 441              $remaining = preg_replace('/^ *>!? ?/', '', $remaining);
 442          }
 443  
 444          return strlen($str) - strlen($remaining);
 445      }
 446  
 447      /**
 448      * Return the length of the markup at the end of an ATX header
 449      *
 450      * @param  integer $startPos Start of the header's text
 451      * @param  integer $endPos   End of the header's text
 452      * @return integer
 453      */
 454  	protected function getAtxHeaderEndTagLen($startPos, $endPos)
 455      {
 456          $content = substr($this->text, $startPos, $endPos - $startPos);
 457          preg_match('/[ \\t]*#*[ \\t]*$/', $content, $m);
 458  
 459          return strlen($m[0]);
 460      }
 461  
 462      /**
 463      * Capture and return block marks from given string
 464      *
 465      * @param  string   $str Block markup, composed of ">", "!" and whitespace
 466      * @return string[]
 467      */
 468  	protected function getBlockMarks($str)
 469      {
 470          preg_match_all('(>!?)', $str, $m);
 471  
 472          return $m[0];
 473      }
 474  
 475      /**
 476      * Capture and store lines that contain a Setext-tyle header
 477      *
 478      * @return void
 479      */
 480  	protected function matchSetextLines()
 481      {
 482          if ($this->text->indexOf('-') === false && $this->text->indexOf('=') === false)
 483          {
 484              return;
 485          }
 486  
 487          // Capture the any series of - or = alone on a line, optionally preceded with the
 488          // angle brackets notation used in block markup
 489          $regexp = '/^(?=[-=>])(?:>!? ?)*(?=[-=])(?:-+|=+) *$/m';
 490          if (!preg_match_all($regexp, $this->text, $matches, PREG_OFFSET_CAPTURE))
 491          {
 492              return;
 493          }
 494  
 495          foreach ($matches[0] as list($match, $matchPos))
 496          {
 497              // Compute the position of the end tag. We start on the LF character before the
 498              // match and keep rewinding until we find a non-space character
 499              $endPos = $matchPos - 1;
 500              while ($endPos > 0 && $this->text->charAt($endPos - 1) === ' ')
 501              {
 502                  --$endPos;
 503              }
 504  
 505              // Store at the offset of the LF character
 506              $this->setextLines[$matchPos - 1] = [
 507                  'endLen'     => $matchPos + strlen($match) - $endPos,
 508                  'endPos'     => $endPos,
 509                  'blockDepth' => substr_count($match, '>'),
 510                  'tagName'    => ($match[0] === '=') ? 'H1' : 'H2'
 511              ];
 512          }
 513      }
 514  }


Generated: Mon Nov 25 19:05:08 2024 Cross-referenced by PHPXref 0.7.1