Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/BbCode/Parser.php
<?php

namespace XF\BbCode;

use function
count, strlen, strval;

class
Parser
{
    protected
$ast = [];
    protected
$astReference;
    protected
$tagStack = [];
    protected
$pendingText = '';
    protected
$plainTag = null;
    protected
$depth = 0;
    protected
$maxDepth = 20;

   
/**
     * @var RuleSet
     */
   
protected $ruleSet;

    public function
parse($text, RuleSet $ruleSet)
    {
       
$this->ruleSet = $ruleSet;

       
$this->ast = [];
       
$this->astReference =& $this->ast;
       
$this->tagStack = [];
       
$this->pendingText = '';
       
$this->plainTag = null;
       
$this->depth = 0;

       
// coerce to a string value in cas it is an object to avoid repeated __toString calls
       
$text = strval($text);

       
$position = 0;
       
$length = strlen($text);

        while (
preg_match(
           
'#(?:\[(\w+)( |=|\])|\[/(\w+)])#i',
           
$text, $match, PREG_OFFSET_CAPTURE, $position
       
))
        {
            if (
$match[0][1] > $position)
            {
               
// push text
               
$plainText = substr($text, $position, $match[0][1] - $position);
               
$this->pushText($plainText);
            }

           
$fullMatch = $match[0][0];
           
$position = $match[0][1] + strlen($fullMatch);

            if (isset(
$match[3]))
            {
               
$this->closeTag($fullMatch, $match[3][0]);
            }
            else if (
$this->plainTag)
            {
               
// options below here relate to tag opens, which aren't allowed inside a plain tag, so
                // eat the [ and continue parsing
               
$this->pushText($fullMatch[0]);
               
$position = $match[0][1] + 1;
            }
            else if (
$match[2][0] == ']')
            {
               
// simple, optionless tag
               
$this->openTag($fullMatch, $match[1][0]);
            }
            else if (
$match[2][0] == ' ')
            {
               
// complex tag gen 2 - [tag attr=val attr2=val attr3="val"]

               
$startPos = $position;
               
$_options = [];

                while (
preg_match('/\G\s*(\w+)=(?:(("|\')(.*)\3)|([^"\'\s\]]+))(?= |\])/iU', $text, $optionMatches, PREG_OFFSET_CAPTURE, $startPos))
                {
                   
$optionKey = strtolower($optionMatches[1][0]);

                   
$_options[$optionKey] = isset($optionMatches[5]) ? $optionMatches[5][0] : $optionMatches[4][0];

                   
$startPos += strlen($optionMatches[0][0]);
                }

               
$endChar = substr($text, $startPos, 1);

                if (
$_options && $endChar == ']')
                {
                   
$this->openTag(
                       
substr($text, $match[0][1], $startPos + 1 - $match[0][1]),
                       
$match[1][0],
                       
$_options
                   
);

                   
$position = $startPos + 1;
                }
                else
                {
                   
// no options or options don't end with a ] so invalid tag, just skip the initial match part
                   
$this->pushText($fullMatch);
                }
            }
            else
            {
               
// complex tag - [tag=attributevalue]
               
if ($position >= $length)
                {
                   
$this->pushText($fullMatch);
                }
                else
                {
                   
$delim = substr($text, $position, 1);

                    if (
$delim == '"' || $delim == "'")
                    {
                       
$startPos = $position + 1;
                       
$endPos = strpos($text, "$delim]", $startPos);
                       
$startMatch = $delim;
                       
$endMatch = "$delim]";
                    }
                    else
                    {
                       
$startPos = $position;
                       
$endPos = strpos($text, ']', $startPos);
                       
$startMatch = '';
                       
$endMatch = ']';
                    }
                    if (
$endPos)
                    {
                       
$option = substr($text, $startPos, $endPos - $startPos);
                       
$this->openTag(
                           
$fullMatch . $startMatch . $option . $endMatch,
                           
$match[1][0], $option
                       
);

                       
$position = $endPos + strlen($endMatch);
                    }
                    else
                    {
                       
$this->pushText($fullMatch);
                    }
                }
            }
        }

        if (
$position < $length)
        {
           
$this->pushText(substr($text, $position));
        }

       
$this->finalizeText();

       
$ast = $this->ast;
       
$this->ast = [];

       
$null = null;
       
$this->astReference = &$null;

        return
$ast;
    }

    protected function
pushText($text)
    {
       
$this->pendingText .= $text;
    }

    protected function
finalizeText()
    {
        if (
strlen($this->pendingText))
        {
           
$this->astReference[] = $this->pendingText;
           
$this->pendingText = '';
        }
    }

    protected function
closeTag($originalText, $tag)
    {
        if (!
$this->tagStack)
        {
           
$this->pushText($originalText);
            return;
        }

       
$tagLower = strtolower($tag);

        if (
$this->plainTag && $this->plainTag != $tagLower)
        {
           
$this->pushText($originalText);
            return;
        }

       
$stackEntry = null;
       
$stackEntryPos = null;

        foreach (
$this->tagStack AS $i => $stack)
        {
            if (
$stack['tag'] == $tagLower)
            {
               
$stackEntry = $stack;
               
$stackEntryPos = $i;
                break;
            }
        }

        if (!
$stackEntry)
        {
           
$this->pushText($originalText);
            return;
        }

       
$this->finalizeText();

       
$stackEntry['entry']['original'][1] = $originalText;

       
$reopens = [];
        if (
$stackEntryPos)
        {
            for (
$i = 0; $i < $stackEntryPos; $i++)
            {
               
$reopens[] = array_shift($this->tagStack);
            }
        }

       
array_shift($this->tagStack); // close the current tag

       
$this->astReference =& $stackEntry['parent'];
       
$this->depth = max(0, $this->depth - $stackEntryPos - 1);
       
$this->plainTag = null;

        if (
$reopens)
        {
            foreach (
array_reverse($reopens) AS $reopen)
            {
               
$reopenEntry = $reopen['entry'];
               
$this->openTag($reopenEntry['original'][0], $reopenEntry['tag'], $reopenEntry['option']);
            }
        }
    }

    protected function
openTag($originalText, $tag, $option = null)
    {
        if (
$this->plainTag)
        {
           
$this->pushText($originalText);
            return;
        }

       
$tagLower = strtolower($tag);

        if (!
$this->ruleSet->validateTag($tagLower, $option, $modifiers, $this->tagStack))
        {
           
$this->pushText($originalText);
            return;
        }

       
$this->finalizeText();

       
$i = count($this->astReference);
       
$this->astReference[$i] = [
           
'tag' => $tagLower,
           
'option' => $option,
           
'original' => [$originalText, "[/$tag]"],
           
'children' => []
        ];

       
array_unshift($this->tagStack, [
           
'tag' => $tagLower,
           
'parent' => &$this->astReference,
           
'entry' => &$this->astReference[$i],
           
'modifiers' => $modifiers
       
]);

       
$this->astReference =& $this->astReference[$i]['children'];

        if (!empty(
$modifiers['plain']))
        {
           
$this->plainTag = $tagLower;
        }

       
$this->depth++;
    }
}