shortcodes[$tag] = $function; return $this; } /** * @param string $tag * @return Shortcode */ public function remove($tag) { if (array_key_exists($tag, $this->shortcodes)) { unset($this->shortcodes[$tag]); } return $this; } /** * @return array */ public function getShortcodes() { return $this->shortcodes; } /** * @param $shortcode * @return bool */ public function has($shortcode) { return array_key_exists($shortcode, $this->shortcodes); } /** * Tests whether content has a particular shortcode * * @param $content * @param $tag * @return bool */ public function contentContains($content, $tag) { if (!$this->has($tag)) { return false; } preg_match_all($this->shortcodeRegex(), $content, $matches, PREG_SET_ORDER); if (empty($matches)) { return false; } foreach ($matches as $shortcode) { if ($tag === $shortcode[2]) { return true; } } return false; } /** * Returns an array of tag names that have been added * * @return array */ public function names() { return array_keys($this->shortcodes); } /** * Search content for shortcodes and filter shortcodes through their hooks. * * If there are no shortcode tags defined, then the content will be returned * without any filtering. This might cause issues when plugins are disabled but * the shortcode will still show up in the post or content. * * @param string $content Content to search for shortcodes * @return string Content with shortcodes filtered out. */ public function parse($content) { if (empty($this->shortcodes)) { return $content; } return preg_replace_callback($this->shortcodeRegex(), array($this, 'processTag'), $content); } /** * Remove all shortcode tags from the given content. * * @uses $shortcode_tags * * @param string $content Content to remove shortcode tags. * @return string Content without shortcode tags. */ public function strip($content) { if (empty($this->shortcodes)) { return $content; } return preg_replace_callback($this->shortcodeRegex(), array($this, 'stripShortcodeTag'), $content); } /** * Regular Expression callable for do_shortcode() for calling shortcode hook. * * @see get_shortcode_regex for details of the match array contents. * * @param array $tag Regular expression match array * @return mixed False on failure. */ private function processTag(array $tag) { // allow [[foo]] syntax for escaping a tag if ($tag[1] == '[' && $tag[6] == ']') { return substr($tag[0], 1, -1); } $tagName = $tag[2]; $attr = $this->parseAttributes($tag[3]); $processed = ProcessedShortcode::create($attr, isset($tag[5]) ? $tag[5] : null, $tagName); return $tag[1] . call_user_func($this->shortcodes[$tagName], $processed) . $tag[6]; } /** * Retrieve all attributes from the shortcodes tag. * * The attributes list has the attribute name as the key and the value of the * attribute as the value in the key/value pair. This allows for easier * retrieval of the attributes, since all attributes have to be known. * * * @param string $text * @return array List of attributes and their value. */ private function parseAttributes($text) { $text = preg_replace("/[\x{00a0}\x{200b}]+/u", " ", $text); if (!preg_match_all($this->attrPattern, $text, $matches, PREG_SET_ORDER)) { return array(ltrim($text)); } $attr = array(); foreach ($matches as $match) { if (!empty($match[1])) { $attr[strtolower($match[1])] = stripcslashes($match[2]); } elseif (!empty($match[3])) { $attr[strtolower($match[3])] = stripcslashes($match[4]); } elseif (!empty($match[5])) { $attr[strtolower($match[5])] = stripcslashes($match[6]); } elseif (isset($match[7]) && strlen($match[7])) { $attr[] = stripcslashes($match[7]); } elseif (isset($match[8])) { $attr[] = stripcslashes($match[8]); } } return $attr; } /** * Strips a tag leaving escaped tags * * @param $tag * @return string */ private function stripShortcodeTag($tag) { if ($tag[1] == '[' && $tag[6] == ']') { return substr($tag[0], 1, -1); } return $tag[1] . $tag[6]; } /** * Retrieve the shortcode regular expression for searching. * * The regular expression combines the shortcode tags in the regular expression * in a regex class. * * The regular expression contains 6 different sub matches to help with parsing. * * 1 - An extra [ to allow for escaping shortcodes with double [[]] * 2 - The shortcode name * 3 - The shortcode argument list * 4 - The self closing / * 5 - The content of a shortcode when it wraps some content. * 6 - An extra ] to allow for escaping shortcodes with double [[]] * * @return string The shortcode search regular expression */ private function shortcodeRegex() { $tagRegex = join('|', array_map('preg_quote', array_keys($this->shortcodes))); return '/' . '\\[' // Opening bracket . '(\\[?)' // 1: Optional second opening bracket for escaping shortcodes: [[tag]] . "($tagRegex)" // 2: Shortcode name . '(?![\\w-])' // Not followed by word character or hyphen . '(' // 3: Unroll the loop: Inside the opening shortcode tag . '[^\\]\\/]*' // Not a closing bracket or forward slash . '(?:' . '\\/(?!\\])' // A forward slash not followed by a closing bracket . '[^\\]\\/]*' // Not a closing bracket or forward slash . ')*?' . ')' . '(?:' . '(\\/)' // 4: Self closing tag ... . '\\]' // ... and closing bracket . '|' . '\\]' // Closing bracket . '(?:' . '(' // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags . '[^\\[]*+' // Not an opening bracket . '(?:' . '\\[(?!\\/\\2\\])' // An opening bracket not followed by the closing shortcode tag . '[^\\[]*+' // Not an opening bracket . ')*+' . ')' . '\\[\\/\\2\\]' // Closing shortcode tag . ')?' . ')' . '(\\]?)' // 6: Optional second closing brocket for escaping shortcodes: [[tag]] . '/s'; } }