Verzeichnisstruktur phpBB-3.3.15


Veröffentlicht
28.08.2024

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

(Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

Blocks.php

Zuletzt modifiziert: 02.04.2025, 15:04 - Dateigröße: 13.51 KiB


001  <?php
002   
003  /**
004  * @package   s9e\TextFormatter
005  * @copyright Copyright (c) 2010-2022 The s9e authors
006  * @license   http://www.opensource.org/licenses/mit-license.php The MIT License
007  */
008  namespace s9e\TextFormatter\Plugins\Litedown\Parser\Passes;
009   
010  use s9e\TextFormatter\Parser as Rules;
011   
012  class Blocks extends AbstractPass
013  {
014      /**
015      * @var array
016      */
017      protected $setextLines = [];
018   
019      /**
020      * {@inheritdoc}
021      */
022      public function parse()
023      {
024          $this->matchSetextLines();
025   
026          $blocks       = [];
027          $blocksCnt    = 0;
028          $codeFence    = null;
029          $codeIndent   = 4;
030          $codeTag      = null;
031          $lineIsEmpty  = true;
032          $lists        = [];
033          $listsCnt     = 0;
034          $newContext   = false;
035          $textBoundary = 0;
036   
037          $regexp = '/^(?:(?=[-*+\\d \\t>`~#_])((?: {0,3}>(?:(?!!)|!(?![^\\n>]*?!<)) ?)+)?([ \\t]+)?(\\* *\\* *\\*[* ]*$|- *- *-[- ]*$|_ *_ *_[_ ]*$|=+$)?((?:[-*+]|\\d+\\.)[ \\t]+(?=\\S))?[ \\t]*(#{1,6}[ \\t]+|```+[^`\\n]*$|~~~+[^~\\n]*$)?)?/m';
038          preg_match_all($regexp, $this->text, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
039   
040          foreach ($matches as $m)
041          {
042              $blockDepth = 0;
043              $blockMarks = [];
044              $ignoreLen  = 0;
045              $matchLen   = strlen($m[0][0]);
046              $matchPos   = $m[0][1];
047   
048              // If the last line was empty then this is not a continuation, and vice-versa
049              $continuation = !$lineIsEmpty;
050   
051              // Capture the position of the end of the line and determine whether the line is empty
052              $lfPos       = $this->text->indexOf("\n", $matchPos);
053              $lineIsEmpty = ($lfPos === $matchPos + $matchLen && empty($m[3][0]) && empty($m[4][0]) && empty($m[5][0]));
054   
055              // If the line is empty and it's the first empty line then we break current paragraph.
056              $breakParagraph = ($lineIsEmpty && $continuation);
057   
058              // Count block marks
059              if (!empty($m[1][0]))
060              {
061                  $blockMarks = $this->getBlockMarks($m[1][0]);
062                  $blockDepth = count($blockMarks);
063                  $ignoreLen  = strlen($m[1][0]);
064                  if (isset($codeTag) && $codeTag->hasAttribute('blockDepth'))
065                  {
066                      $blockDepth = min($blockDepth, $codeTag->getAttribute('blockDepth'));
067                      $ignoreLen  = $this->computeBlockIgnoreLen($m[1][0], $blockDepth);
068                  }
069   
070                  // Overwrite block markup
071                  $this->text->overwrite($matchPos, $ignoreLen);
072              }
073   
074              // Close supernumerary blocks
075              if ($blockDepth < $blocksCnt && !$continuation)
076              {
077                  $newContext = true;
078                  do
079                  {
080                      $startTag = array_pop($blocks);
081                      $this->parser->addEndTag($startTag->getName(), $textBoundary, 0)
082                                   ->pairWith($startTag);
083                  }
084                  while ($blockDepth < --$blocksCnt);
085              }
086   
087              // Open new blocks
088              if ($blockDepth > $blocksCnt && !$lineIsEmpty)
089              {
090                  $newContext = true;
091                  do
092                  {
093                      $tagName  = ($blockMarks[$blocksCnt] === '>!') ? 'SPOILER' : 'QUOTE';
094                      $blocks[] = $this->parser->addStartTag($tagName, $matchPos, 0, -999);
095                  }
096                  while ($blockDepth > ++$blocksCnt);
097              }
098   
099              // 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  }