diff --git a/parsers/ShortcodeParser.php b/parsers/ShortcodeParser.php index 6cd0f737b..37996e50a 100644 --- a/parsers/ShortcodeParser.php +++ b/parsers/ShortcodeParser.php @@ -103,11 +103,47 @@ class ShortcodeParser extends Object { $this->shortcodes = array(); } + /** + * Call a shortcode and return its replacement text + * Returns false if the shortcode isn't registered + */ public function callShortcode($tag, $attributes, $content, $extra = array()) { - if (!isset($this->shortcodes[$tag])) return false; + if (!$tag || !isset($this->shortcodes[$tag])) return false; return call_user_func($this->shortcodes[$tag], $attributes, $content, $this, $tag, $extra); } + /** + * Return the text to insert in place of a shoprtcode. + * Behaviour in the case of missing shortcodes depends on the setting of ShortcodeParser::$error_behavior. + * @param $tag A map containing the the following keys: + * - 'open': The name of the tag + * - 'attrs': Attributes of the tag + * - 'content': Content of the tag + * @param $extra Extra-meta data + * @param $isHTMLAllowed A boolean indicating whether it's okay to insert HTML tags into the result + */ + function getShortcodeReplacementText($tag, $extra = array(), $isHTMLAllowed = true) { + $content = $this->callShortcode($tag['open'], $tag['attrs'], $tag['content'], $extra); + + // Missing tag + if ($content === false) { + if(ShortcodeParser::$error_behavior == ShortcodeParser::ERROR) { + user_error('Unknown shortcode tag '.$tag['open'], E_USER_ERRROR); + } + else if (self::$error_behavior == self::WARN && $isHTMLAllowed) { + $content = ''.$tag['text'].''; + } + else if(ShortcodeParser::$error_behavior == ShortcodeParser::STRIP) { + return ''; + } + else { + return $tag['text']; + } + } + + return $content; + } + // -------------------------------------------------------------------------------------------------------------- protected function removeNode($node) { @@ -207,6 +243,7 @@ class ShortcodeParser extends Object { protected function extractTags($content) { $tags = array(); + // Step 1: perform basic regex scan of individual tags if(preg_match_all(static::tagrx(), $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { foreach($matches as $match) { // Ignore any elements @@ -236,8 +273,9 @@ class ShortcodeParser extends Object { 'escaped' => !empty($match['oesc'][0]) || !empty($match['cesc1'][0]) || !empty($match['cesc2'][0]) ); } - } + } + // Step 2: cluster open/close tag pairs into single entries $i = count($tags); while($i--) { if(!empty($tags[$i]['close'])) { @@ -337,24 +375,11 @@ class ShortcodeParser extends Object { if($tags) { $node->nodeValue = $this->replaceTagsWithText($node->nodeValue, $tags, - function($idx, $tag) use ($parser, $extra){ - $content = $parser->callShortcode($tag['open'], $tag['attrs'], $tag['content'], $extra); - - if ($content === false) { - if(ShortcodeParser::$error_behavior == ShortcodeParser::ERROR) { - user_error('Unknown shortcode tag '.$tag['open'], E_USER_ERRROR); - } - else if(ShortcodeParser::$error_behavior == ShortcodeParser::STRIP) { - return ''; - } - else { - return $tag['text']; - } + function($idx, $tag) use ($parser, $extra) { + return $parser->getShortcodeReplacementText($tag, $extra, false); } - - return $content; - }); - } + ); + } } } @@ -365,17 +390,17 @@ class ShortcodeParser extends Object { */ protected function replaceElementTagsWithMarkers($content) { $tags = $this->extractTags($content); - + if($tags) { $markerClass = self::$marker_class; $content = $this->replaceTagsWithText($content, $tags, function($idx, $tag) use ($markerClass) { return ''; }); - } + } return array($content, $tags); - } + } protected function findParentsForMarkers($nodes) { $parents = array(); @@ -477,23 +502,7 @@ class ShortcodeParser extends Object { * @param array $tag */ protected function replaceMarkerWithContent($node, $tag) { - $content = false; - if($tag['open']) $content = $this->callShortcode($tag['open'], $tag['attrs'], $tag['content']); - - if ($content === false) { - if(self::$error_behavior == self::ERROR) { - user_error('Unknown shortcode tag '.$tag['open'], E_USER_ERRROR); - } - if (self::$error_behavior == self::WARN) { - $content = ''.$tag['text'].''; - } - else if (self::$error_behavior == self::LEAVE) { - $content = $tag['text']; - } - else { - // self::$error_behavior == self::STRIP - NOP - } - } + $content = $this->getShortcodeReplacementText($tag); if ($content) { $parsed = Injector::inst()->create('HTMLValue', $content); @@ -567,8 +576,21 @@ class ShortcodeParser extends Object { $this->replaceMarkerWithContent($shortcode, $tag); } - return $htmlvalue->getContent(); + $content = $htmlvalue->getContent(); + + // Clean up any marker classes left over, for example, those injected into ', + $this->parser->parse('') + ); + } + + public function testNumericShortcodes() { + $this->assertEqualsIgnoringWhitespace( + '[2]', + $this->parser->parse('[2]') + ); + $this->assertEqualsIgnoringWhitespace( + '', + $this->parser->parse('') + ); + + $this->parser->register('2', function($attributes, $content, $this, $tag, $extra) { + return 'this is 2'; + }); + + $this->assertEqualsIgnoringWhitespace( + 'this is 2', + $this->parser->parse('[2]') + ); + $this->assertEqualsIgnoringWhitespace( + '', + $this->parser->parse('') + ); + } + public function testExtraContext() { $this->parser->parse('Test');