View source
<?php
namespace Drupal\xbbcode\Parser;
use Drupal\xbbcode\Parser\Tree\NodeElementInterface;
use Drupal\xbbcode\Parser\Tree\RootElement;
use Drupal\xbbcode\Parser\Tree\TagElement;
use Drupal\xbbcode\Parser\Tree\TagElementInterface;
use Drupal\xbbcode\Parser\Tree\TextElement;
use function strlen;
class XBBCodeParser implements ParserInterface {
protected $processors;
public function __construct($processors = NULL) {
$this->processors = $processors;
}
public function parse($text) : NodeElementInterface {
$tokens = static::tokenize($text, $this->processors);
$tokens = static::validateTokens($tokens);
$tree = static::buildTree($text, $tokens);
if ($this->processors) {
static::decorateTree($tree, $this->processors);
}
return $tree;
}
public static function tokenize($text, $allowed = NULL) : array {
$matches = [];
preg_match_all("%\n \\[\n (?'closing'/?)\n (?'name'[a-z0-9_-]+)\n (?'argument'\n (?:(?=\\k'closing') # only take an argument in opening tags.\n (?:\n =(?:\\\\.|[^\\\\\\[\\]])* # unquoted option must escape brackets.\n |\n =(?'quote1'['\"]|"|&\\#039;)\n (?:\\\\.|(?!\\k'quote1')[^\\\\])*\n \\k'quote1'\n |\n (?:\\s+[\\w-]+=\n (?:\n (?'quote2'['\"]|"|&\\#039;)\n (?:\\\\.|(?!\\k'quote2')[^\\\\])*\n \\k'quote2'\n |\n (?!\\g'quote2') # unquoted values cannot begin with quotes.\n (?:\\\\.|[^\\[\\]\\s\\\\])*\n )\n )*\n )\n )?\n )\n ]\n %x", $text, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
$tokens = [];
foreach ($matches as $i => $match) {
$name = $match['name'][0];
if ($allowed && empty($allowed[$name])) {
continue;
}
$start = $match[0][1];
$tokens[] = [
'name' => $name,
'start' => $start,
'end' => $start + strlen($match[0][0]),
'argument' => $match['argument'][0],
'closing' => !empty($match['closing'][0]),
];
}
return $tokens;
}
public static function parseAttributes($argument) : array {
$assignments = [];
preg_match_all("/\n (?<=\\s) # preceded by whitespace.\n (?'key'[\\w-]+)=\n (?:\n (?'quote'['\"]|"|&\\#039;) # quotes may be encoded.\n (?'value'\n (?:\\\\.|(?!\\\\|\\k'quote')[^\\\\])* # value can contain the delimiter.\n )\n \\k'quote'\n |\n (?'unquoted'\n (?!\\g'quote') # unquoted values cannot start with a quote.\n (?:\\\\.|[^\\s\\\\])*\n )\n )\n (?=\\s|\$)/x", $argument, $assignments, PREG_SET_ORDER);
$attributes = [];
foreach ($assignments as $assignment) {
$value = $assignment['value'] ?: $assignment['unquoted'];
$attributes[$assignment['key']] = stripslashes($value);
}
return $attributes;
}
public static function parseOption($argument) : string {
if (preg_match("/\n ^=\n (?'quote'['\"]|"|&\\#039;)\n (?'value'.*)\n \\k'quote'\n \$/x", $argument, $match)) {
$value = $match['value'];
}
else {
$value = substr($argument, 1);
}
return stripslashes($value);
}
public static function validateTokens(array $tokens) : array {
$counter = [];
foreach ($tokens as $token) {
$counter[$token['name']] = 0;
}
$stack = [];
foreach ($tokens as $i => $token) {
if ($token['closing']) {
if ($counter[$token['name']] > 0) {
do {
$last = array_pop($stack);
$counter[$last['name']]--;
} while ($last['name'] !== $token['name']);
$tokens[$last['id']] += [
'length' => $token['start'] - $last['end'],
'verified' => TRUE,
];
$tokens[$i]['verified'] = TRUE;
}
}
else {
$stack[] = $token + [
'id' => $i,
];
$counter[$token['name']]++;
}
}
return array_filter($tokens, static function ($token) {
return !empty($token['verified']);
});
}
public static function buildTree($text, array $tokens) : NodeElementInterface {
$stack = [
new RootElement(),
];
$index = 0;
foreach ($tokens as $token) {
$leading = substr($text, $index, $token['start'] - $index);
if ($leading) {
end($stack)
->append(new TextElement($leading));
}
$index = $token['end'];
if (!$token['closing']) {
$stack[] = new TagElement($token['name'], $token['argument'], substr($text, $token['end'], $token['length']));
}
else {
$element = array_pop($stack);
end($stack)
->append($element);
}
}
$final = substr($text, $index);
if ($final) {
end($stack)
->append(new TextElement($final));
}
return array_pop($stack);
}
public static function decorateTree(NodeElementInterface $node, $processors) : void {
foreach ($node
->getChildren() as $child) {
if ($child instanceof TagElementInterface) {
$child
->setParent($node);
if ($processor = $processors[$child
->getName()]) {
$child
->setProcessor($processor);
}
static::decorateTree($child, $processors);
}
}
}
}