You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
291 lines
8.6 KiB
291 lines
8.6 KiB
<?php namespace Sensory5\Shortcode\Classes;
|
|
|
|
use Sensory5\Shortcode\Classes\ProcessedShortcode;
|
|
|
|
/**
|
|
* This is a port of WordPress' brilliant shortcode feature
|
|
* for use outside of WordPress. The code has remained largely unchanged
|
|
*
|
|
* Original from: https://github.com/Badcow/Shortcodes
|
|
*
|
|
* Class Shortcode
|
|
*
|
|
* @package Shortcode
|
|
*/
|
|
class Shortcode
|
|
{
|
|
/**
|
|
* The regex for attributes.
|
|
*
|
|
* This regex covers the following attribute situations:
|
|
* - key = "value"
|
|
* - key = 'value'
|
|
* - key = value
|
|
* - "value"
|
|
* - value
|
|
*
|
|
* @var string
|
|
*/
|
|
private $attrPattern = '/(\w+)\s*=\s*"([^"]*)"(?:\s|$)|(\w+)\s*=\s*\'([^\']*)\'(?:\s|$)|(\w+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/';
|
|
|
|
/**
|
|
* Indexed array of tags: shortcode callbacks
|
|
*
|
|
* @var array
|
|
*/
|
|
private $shortcodes = array();
|
|
|
|
/**
|
|
* @param string $tag
|
|
* @param callable $function
|
|
* @throws \ErrorException
|
|
*/
|
|
public function add($tag, $function)
|
|
{
|
|
if (!is_callable($function)) {
|
|
throw new \ErrorException("Function must be callable");
|
|
}
|
|
|
|
$this->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';
|
|
}
|
|
}
|