.=< { Star Gans Tq } >=.
<?php
/**
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2020 The s9e authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Plugins\BBCodes;
use RuntimeException;
use s9e\TextFormatter\Parser\Tag;
use s9e\TextFormatter\Plugins\ParserBase;
class Parser extends ParserBase
{
/**
* @var array Attributes of the BBCode being parsed
*/
protected $attributes;
/**
* @var array Configuration for the BBCode being parsed
*/
protected $bbcodeConfig;
/**
* @var string Name of the BBCode being parsed
*/
protected $bbcodeName;
/**
* @var string Suffix of the BBCode being parsed, including its colon
*/
protected $bbcodeSuffix;
/**
* @var integer Position of the cursor in the original text
*/
protected $pos;
/**
* @var integer Position of the start of the BBCode being parsed
*/
protected $startPos;
/**
* @var string Text being parsed
*/
protected $text;
/**
* @var integer Length of the text being parsed
*/
protected $textLen;
/**
* @var string Text being parsed, normalized to uppercase
*/
protected $uppercaseText;
/**
* {@inheritdoc}
*/
public function parse($text, array $matches)
{
$this->text = $text;
$this->textLen = strlen($text);
$this->uppercaseText = '';
foreach ($matches as $m)
{
$this->bbcodeName = strtoupper($m[1][0]);
if (!isset($this->config['bbcodes'][$this->bbcodeName]))
{
continue;
}
$this->bbcodeConfig = $this->config['bbcodes'][$this->bbcodeName];
$this->startPos = $m[0][1];
$this->pos = $this->startPos + strlen($m[0][0]);
try
{
$this->parseBBCode();
}
catch (RuntimeException $e)
{
// Do nothing
}
}
}
/**
* Add the end tag that matches current BBCode
*
* @return Tag
*/
protected function addBBCodeEndTag()
{
return $this->parser->addEndTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos);
}
/**
* Add the self-closing tag that matches current BBCode
*
* @return Tag
*/
protected function addBBCodeSelfClosingTag()
{
$tag = $this->parser->addSelfClosingTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos);
$tag->setAttributes($this->attributes);
return $tag;
}
/**
* Add the start tag that matches current BBCode
*
* @return Tag
*/
protected function addBBCodeStartTag()
{
$tag = $this->parser->addStartTag($this->getTagName(), $this->startPos, $this->pos - $this->startPos);
$tag->setAttributes($this->attributes);
return $tag;
}
/**
* Parse the end tag that matches given BBCode name and suffix starting at current position
*
* @return Tag|null
*/
protected function captureEndTag()
{
if (empty($this->uppercaseText))
{
$this->uppercaseText = strtoupper($this->text);
}
$match = '[/' . $this->bbcodeName . $this->bbcodeSuffix . ']';
$endTagPos = strpos($this->uppercaseText, $match, $this->pos);
if ($endTagPos === false)
{
return;
}
return $this->parser->addEndTag($this->getTagName(), $endTagPos, strlen($match));
}
/**
* Get the tag name for current BBCode
*
* @return string
*/
protected function getTagName()
{
// Use the configured tagName if available, or reuse the BBCode's name otherwise
return (isset($this->bbcodeConfig['tagName']))
? $this->bbcodeConfig['tagName']
: $this->bbcodeName;
}
/**
* Parse attributes starting at current position
*
* @return void
*/
protected function parseAttributes()
{
$firstPos = $this->pos;
$this->attributes = [];
while ($this->pos < $this->textLen)
{
$c = $this->text[$this->pos];
if (strpos(" \n\t", $c) !== false)
{
++$this->pos;
continue;
}
if (strpos('/]', $c) !== false)
{
return;
}
// Capture the attribute name
$spn = strspn($this->text, 'abcdefghijklmnopqrstuvwxyz_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-', $this->pos);
if ($spn)
{
$attrName = strtolower(substr($this->text, $this->pos, $spn));
$this->pos += $spn;
if ($this->pos >= $this->textLen)
{
// The attribute name extends to the end of the text
throw new RuntimeException;
}
if ($this->text[$this->pos] !== '=')
{
// It's an attribute name not followed by an equal sign, ignore it
continue;
}
}
elseif ($c === '=' && $this->pos === $firstPos)
{
// This is the default param, e.g. [quote=foo]
$attrName = (isset($this->bbcodeConfig['defaultAttribute']))
? $this->bbcodeConfig['defaultAttribute']
: strtolower($this->bbcodeName);
}
else
{
throw new RuntimeException;
}
// Move past the = and make sure we're not at the end of the text
if (++$this->pos >= $this->textLen)
{
throw new RuntimeException;
}
$this->attributes[$attrName] = $this->parseAttributeValue();
}
}
/**
* Parse the attribute value starting at current position
*
* @return string
*/
protected function parseAttributeValue()
{
// Test whether the value is in quotes
if ($this->text[$this->pos] === '"' || $this->text[$this->pos] === "'")
{
return $this->parseQuotedAttributeValue();
}
// Capture everything up to whichever comes first:
// - an endline
// - whitespace followed by a slash and a closing bracket
// - a closing bracket, optionally preceded by whitespace
// - whitespace followed by another attribute (name followed by equal sign)
//
// NOTE: this is for compatibility with some forums (such as vBulletin it seems)
// that do not put attribute values in quotes, e.g.
// [quote=John Smith;123456] (quoting "John Smith" from post #123456)
preg_match('((?:[^\\s\\]]|[ \\t](?!\\s*(?:[-\\w]+=|/?\\])))*)', $this->text, $m, null, $this->pos);
$attrValue = $m[0];
$this->pos += strlen($attrValue);
return $attrValue;
}
/**
* Parse current BBCode
*
* @return void
*/
protected function parseBBCode()
{
$this->parseBBCodeSuffix();
// Test whether this is an end tag
if ($this->text[$this->startPos + 1] === '/')
{
// Test whether the tag is properly closed and whether this tag has an identifier.
// We skip end tags that carry an identifier because they're automatically added
// when their start tag is processed
if (substr($this->text, $this->pos, 1) === ']' && $this->bbcodeSuffix === '')
{
++$this->pos;
$this->addBBCodeEndTag();
}
return;
}
// Parse attributes
$this->parseAttributes();
// Test whether the tag is properly closed
if (substr($this->text, $this->pos, 1) === ']')
{
++$this->pos;
}
else
{
// Test whether this is a self-closing tag
if (substr($this->text, $this->pos, 2) === '/]')
{
$this->pos += 2;
$this->addBBCodeSelfClosingTag();
}
return;
}
// Record the names of attributes that need the content of this tag
$contentAttributes = [];
if (isset($this->bbcodeConfig['contentAttributes']))
{
foreach ($this->bbcodeConfig['contentAttributes'] as $attrName)
{
if (!isset($this->attributes[$attrName]))
{
$contentAttributes[] = $attrName;
}
}
}
// Look ahead and parse the end tag that matches this tag, if applicable
$requireEndTag = ($this->bbcodeSuffix || !empty($this->bbcodeConfig['forceLookahead']));
$endTag = ($requireEndTag || !empty($contentAttributes)) ? $this->captureEndTag() : null;
if (isset($endTag))
{
foreach ($contentAttributes as $attrName)
{
$this->attributes[$attrName] = substr($this->text, $this->pos, $endTag->getPos() - $this->pos);
}
}
elseif ($requireEndTag)
{
return;
}
// Create this start tag
$tag = $this->addBBCodeStartTag();
// If an end tag was created, pair it with this start tag
if (isset($endTag))
{
$tag->pairWith($endTag);
}
}
/**
* Parse the BBCode suffix starting at current position
*
* Used to explicitly pair specific tags together, e.g.
* [code:123][code]type your code here[/code][/code:123]
*
* @return void
*/
protected function parseBBCodeSuffix()
{
$this->bbcodeSuffix = '';
if ($this->text[$this->pos] === ':')
{
// Capture the colon and the (0 or more) digits following it
$spn = 1 + strspn($this->text, '0123456789', 1 + $this->pos);
$this->bbcodeSuffix = substr($this->text, $this->pos, $spn);
// Move past the suffix
$this->pos += $spn;
}
}
/**
* Parse a quoted attribute value that starts at current offset
*
* @return string
*/
protected function parseQuotedAttributeValue()
{
$quote = $this->text[$this->pos];
$valuePos = $this->pos + 1;
do
{
// Look for the next quote
$this->pos = strpos($this->text, $quote, $this->pos + 1);
if ($this->pos === false)
{
// No matching quote. Apparently that string never ends...
throw new RuntimeException;
}
// Test for an odd number of backslashes before this character
$n = 1;
while ($this->text[$this->pos - $n] === '\\')
{
++$n;
}
}
while ($n % 2 === 0);
$attrValue = substr($this->text, $valuePos, $this->pos - $valuePos);
if (strpos($attrValue, '\\') !== false)
{
$attrValue = strtr($attrValue, ['\\\\' => '\\', '\\"' => '"', "\\'" => "'"]);
}
// Skip past the closing quote
++$this->pos;
return $attrValue;
}
}