protected function parse($usage) { $tag = new Tag(); $bbcode = new BBCode(); $config = array('tag' => $tag, 'bbcode' => $bbcode, 'passthroughToken' => \null); $usage = \preg_replace_callback('#(\\{(?>HASH)?MAP=)([^:]+:[^,;}]+(?>,[^:]+:[^,;}]+)*)(?=[;}])#', function ($m) { return $m[1] . \base64_encode($m[2]); }, $usage); $usage = \preg_replace_callback('#(\\{(?:PARSE|REGEXP)=)(' . self::REGEXP . '(?:,' . self::REGEXP . ')*)#', function ($m) { return $m[1] . \base64_encode($m[2]); }, $usage); $regexp = '(^' . '\\[(?<bbcodeName>\\S+?)' . '(?<defaultAttribute>=\\S+?)?' . '(?<attributes>(?:\\s+[^=]+=\\S+?)*?)?' . '\\s*(?:/?\\]|\\]\\s*(?<content>.*?)\\s*(?<endTag>\\[/\\1]))$)i'; if (!\preg_match($regexp, \trim($usage), $m)) { throw new InvalidArgumentException('Cannot interpret the BBCode definition'); } $config['bbcodeName'] = BBCode::normalizeName($m['bbcodeName']); $definitions = \preg_split('#\\s+#', \trim($m['attributes']), -1, \PREG_SPLIT_NO_EMPTY); if (!empty($m['defaultAttribute'])) { \array_unshift($definitions, $m['bbcodeName'] . $m['defaultAttribute']); } if (!empty($m['content'])) { $regexp = '#^\\{' . RegexpBuilder::fromList($this->unfilteredTokens) . '[0-9]*\\}$#D'; if (\preg_match($regexp, $m['content'])) { $config['passthroughToken'] = \substr($m['content'], 1, -1); } else { $definitions[] = 'content=' . $m['content']; $bbcode->contentAttributes[] = 'content'; } } $attributeDefinitions = array(); foreach ($definitions as $definition) { $pos = \strpos($definition, '='); $name = \substr($definition, 0, $pos); $value = \substr($definition, 1 + $pos); $value = \preg_replace_callback('#(\\{(?>HASHMAP|MAP|PARSE|REGEXP)=)([A-Za-z0-9+/]+=*)#', function ($m) { return $m[1] . \base64_decode($m[2]); }, $value); if ($name[0] === '$') { $optionName = \substr($name, 1); $bbcode->{$optionName} = $this->convertValue($value); } elseif ($name[0] === '#') { $ruleName = \substr($name, 1); foreach (\explode(',', $value) as $value) { $tag->rules->{$ruleName}($this->convertValue($value)); } } else { $attrName = \strtolower(\trim($name)); $attributeDefinitions[] = array($attrName, $value); } } $tokens = $this->addAttributes($attributeDefinitions, $bbcode, $tag); if (isset($tokens[$config['passthroughToken']])) { $config['passthroughToken'] = \null; } $config['tokens'] = \array_filter($tokens); return $config; }
/** * Get a BBCode and its associated tag from this repository * * @param string $name Name of the entry in the repository * @param array $vars Replacement variables * @return array Array with three elements: "bbcode", "name" and "tag" */ public function get($name, array $vars = []) { // Everything before # should be a BBCode name $name = preg_replace_callback('/^[^#]+/', function ($m) { return BBCode::normalizeName($m[0]); }, $name); $xpath = new DOMXPath($this->dom); $node = $xpath->query('//bbcode[@name="' . htmlspecialchars($name) . '"]')->item(0); if (!$node instanceof DOMElement) { throw new RuntimeException("Could not find '" . $name . "' in repository"); } // Clone the node so we don't end up modifying the node in the repository $clonedNode = $node->cloneNode(true); // Replace all the <var> descendants if applicable foreach ($xpath->query('.//var', $clonedNode) as $varNode) { $varName = $varNode->getAttribute('name'); if (isset($vars[$varName])) { $varNode->parentNode->replaceChild($this->dom->createTextNode($vars[$varName]), $varNode); } } // Now we can parse the BBCode usage and prepare the template. // Grab the content of the <usage> element then use BBCodeMonkey to parse it $usage = $xpath->evaluate('string(usage)', $clonedNode); $template = $xpath->evaluate('string(template)', $clonedNode); $config = $this->bbcodeMonkey->create($usage, $template); $bbcode = $config['bbcode']; $bbcodeName = $config['bbcodeName']; $tag = $config['tag']; // Set the optional tag name if ($node->hasAttribute('tagName')) { $bbcode->tagName = $node->getAttribute('tagName'); } // Set the rules foreach ($xpath->query('rules/*', $node) as $ruleNode) { $methodName = $ruleNode->nodeName; $args = []; if ($ruleNode->textContent) { $args[] = $ruleNode->textContent; } call_user_func_array([$tag->rules, $methodName], $args); } // Set predefined attributes foreach ($node->getElementsByTagName('predefinedAttributes') as $predefinedAttributes) { foreach ($predefinedAttributes->attributes as $attribute) { $bbcode->predefinedAttributes->set($attribute->name, $attribute->value); } } return ['bbcode' => $bbcode, 'bbcodeName' => $bbcodeName, 'tag' => $tag]; }
public function get($name, array $vars = array()) { $name = \preg_replace_callback('/^[^#]+/', function ($m) { return BBCode::normalizeName($m[0]); }, $name); $xpath = new DOMXPath($this->dom); $node = $xpath->query('//bbcode[@name="' . \htmlspecialchars($name) . '"]')->item(0); if (!$node instanceof DOMElement) { throw new RuntimeException("Could not find '" . $name . "' in repository"); } $clonedNode = $node->cloneNode(\true); foreach ($xpath->query('.//var', $clonedNode) as $varNode) { $varName = $varNode->getAttribute('name'); if (isset($vars[$varName])) { $varNode->parentNode->replaceChild($this->dom->createTextNode($vars[$varName]), $varNode); } } $usage = $xpath->evaluate('string(usage)', $clonedNode); $template = $xpath->evaluate('string(template)', $clonedNode); $config = $this->bbcodeMonkey->create($usage, $template); $bbcode = $config['bbcode']; $bbcodeName = $config['bbcodeName']; $tag = $config['tag']; if ($node->hasAttribute('tagName')) { $bbcode->tagName = $node->getAttribute('tagName'); } foreach ($xpath->query('rules/*', $node) as $ruleNode) { $methodName = $ruleNode->nodeName; $args = array(); if ($ruleNode->textContent) { $args[] = $ruleNode->textContent; } \call_user_func_array(array($tag->rules, $methodName), $args); } foreach ($node->getElementsByTagName('predefinedAttributes') as $predefinedAttributes) { foreach ($predefinedAttributes->attributes as $attribute) { $bbcode->predefinedAttributes->set($attribute->name, $attribute->value); } } return array('bbcode' => $bbcode, 'bbcodeName' => $bbcodeName, 'tag' => $tag); }
/** * {@inheritdoc} */ public function normalizeKey($key) { return BBCode::normalizeName($key); }
/** * @testdox asConfig() returns predefinedAttributes in a Dictionary */ public function testAsConfigPredefinedAttributesDictionary() { $bbcode = new BBCode(); $bbcode->predefinedAttributes = ['foo' => 'bar']; $this->assertEquals(['predefinedAttributes' => new Dictionary(['foo' => 'bar'])], $bbcode->asConfig()); }
/** * Create a BBCode based on its reference usage * * @param string $usage BBCode usage, e.g. [B]{TEXT}[/b] * @return array */ protected function parse($usage) { $tag = new Tag(); $bbcode = new BBCode(); // This is the config we will return $config = ['tag' => $tag, 'bbcode' => $bbcode, 'passthroughToken' => null]; // Encode maps to avoid special characters to interfere with definitions $usage = preg_replace_callback('#(\\{(?>HASH)?MAP=)([^:]+:[^,;}]+(?>,[^:]+:[^,;}]+)*)(?=[;}])#', function ($m) { return $m[1] . base64_encode($m[2]); }, $usage); // Encode regexps to avoid special characters to interfere with definitions $usage = preg_replace_callback('#(\\{(?:PARSE|REGEXP)=)(' . self::REGEXP . '(?:,' . self::REGEXP . ')*)#', function ($m) { return $m[1] . base64_encode($m[2]); }, $usage); $regexp = '(^' . '\\[(?<bbcodeName>\\S+?)' . '(?<defaultAttribute>=\\S+?)?' . '(?<attributes>(?:\\s+[^=]+=\\S+?)*?)?' . '\\s*(?:/?\\]|\\]\\s*(?<content>.*?)\\s*(?<endTag>\\[/\\1]))' . '$)i'; if (!preg_match($regexp, trim($usage), $m)) { throw new InvalidArgumentException('Cannot interpret the BBCode definition'); } // Save the BBCode's name $config['bbcodeName'] = BBCode::normalizeName($m['bbcodeName']); // Prepare the attributes definition, e.g. "foo={BAR}" $definitions = preg_split('#\\s+#', trim($m['attributes']), -1, PREG_SPLIT_NO_EMPTY); // If there's a default attribute, we prepend it to the list using the BBCode's name as // attribute name if (!empty($m['defaultAttribute'])) { array_unshift($definitions, $m['bbcodeName'] . $m['defaultAttribute']); } // Append the content token to the attributes list under the name "content" if it's anything // but raw {TEXT} (or other unfiltered tokens) if (!empty($m['content'])) { $regexp = '#^\\{' . RegexpBuilder::fromList($this->unfilteredTokens) . '[0-9]*\\}$#D'; if (preg_match($regexp, $m['content'])) { $config['passthroughToken'] = substr($m['content'], 1, -1); } else { $definitions[] = 'content=' . $m['content']; $bbcode->contentAttributes[] = 'content'; } } // Separate the attribute definitions from the BBCode options $attributeDefinitions = []; foreach ($definitions as $definition) { $pos = strpos($definition, '='); $name = substr($definition, 0, $pos); $value = substr($definition, 1 + $pos); // Decode base64-encoded tokens $value = preg_replace_callback('#(\\{(?>HASHMAP|MAP|PARSE|REGEXP)=)([A-Za-z0-9+/]+=*)#', function ($m) { return $m[1] . base64_decode($m[2]); }, $value); // If name starts with $ then it's a BBCode options, if it starts with # it's a rule and // otherwise it's an attribute definition if ($name[0] === '$') { $optionName = substr($name, 1); $bbcode->{$optionName} = $this->convertValue($value); } elseif ($name[0] === '#') { $ruleName = substr($name, 1); // Supports #denyChild=foo,bar foreach (explode(',', $value) as $value) { $tag->rules->{$ruleName}($this->convertValue($value)); } } else { $attrName = strtolower(trim($name)); $attributeDefinitions[] = [$attrName, $value]; } } // Add the attributes and get the token translation table $tokens = $this->addAttributes($attributeDefinitions, $bbcode, $tag); // Test whether the passthrough token is used for something else, in which case we need // to unset it if (isset($tokens[$config['passthroughToken']])) { $config['passthroughToken'] = null; } // Add the list of known (and only the known) tokens to the config $config['tokens'] = array_filter($tokens); return $config; }