Replacing shortcode implementation

This commit is contained in:
Patrick Ward
2015-11-25 15:29:27 -05:00
parent a31d79a984
commit bbe024ce14
10 changed files with 471 additions and 202 deletions

View File

@ -0,0 +1,87 @@
<?php namespace Sensory5\Shortcode\Classes;
class ProcessedShortcode implements ShortcodeInterface
{
private $attributes;
private $content;
private $name;
private function __construct()
{
}
/**
* Create from shortcode data
*/
public static function create($attributes, $content, $tagName)
{
$self = new self();
$self->attributes = $attributes;
$self->content = $content;
$self->name = $tagName;
return $self;
}
/**
* Returns new instance of given shortcode with changed content
*
* @param string $content
*
* @return self
*/
public function withContent($content)
{
$self = clone $this;
return $self->content = $content;
}
/**
* Returns shortcode name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Returns associative array(name => value) of shortcode parameters
*
* @return array
*/
public function getParameters()
{
return $this->attributes;
}
/**
* Returns parameter value using its name, will return null for parameter
* without value
*
* @param string $name Parameter name
* @param null $default Value returned if there is no parameter with given name
*
* @return mixed
*/
public function getParameter($name, $default = null)
{
return array_get($this->attributes, $name, $default);
}
/**
* Returns shortcode content (data between opening and closing tag). Null
* means that shortcode had no content (was self closing), do not confuse
* that with empty string (hint: use strict comparison operator ===).
*
* @return string|null
*/
public function getContent()
{
return $this->content;
}
}

View File

@ -1,153 +1,291 @@
<?php namespace Sensory5\Shortcode\Classes;
use Sensory5\Shortcode\Classes\ProcessedShortcode;
/**
* Shortcodes
* This is a port of WordPress' brilliant shortcode feature
* for use outside of WordPress. The code has remained largely unchanged
*
* Original idea from Pingpong Labs (https://github.com/pingpong-labs/shortcode)
* Original from: https://github.com/Badcow/Shortcodes
*
* Changed to fit closer to the Thunderer\Shortcode syntax.
* Class Shortcode
*
* @package Shortcode
*/
use Countable;
use Thunder\Shortcode\HandlerContainer\HandlerContainer;
use Thunder\Shortcode\Parser\RegexParser;
use Thunder\Shortcode\Processor\Processor;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class Shortcode implements Countable
class Shortcode
{
/** @var HandlerContainer */
private $handlers;
/**
* 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|$)/';
/**
* The constructor.
* Indexed array of tags: shortcode callbacks
*
* @var array
*/
public function __construct()
private $shortcodes = array();
/**
* @param string $tag
* @param callable $function
* @throws \ErrorException
*/
public function add($tag, $function)
{
$this->handlers = new HandlerContainer();
if (!is_callable($function)) {
throw new \ErrorException("Function must be callable");
}
$this->shortcodes[$tag] = $function;
return $this;
}
/**
* Get the names for all registered shortcodes.
*
* @return array
* @param string $tag
* @return Shortcode
*/
public function getNames()
public function remove($tag)
{
return $this->handlers->getNames();
}
/**
* Add a new shortcode to the handler container.
*
* @param string $name
* @param mixed $callback
*/
public function add($name, $callback)
{
$this->handlers->add($name, $callback);
}
/**
* Remove the specified shortcode name from the handler.
*
* @param string $name
*/
public function remove($name)
{
if ($this->exists($name)) {
$this->handlers->remove($name);
if (array_key_exists($tag, $this->shortcodes)) {
unset($this->shortcodes[$tag]);
}
return $this;
}
/**
* Remove all registered shortcodes
*
* @return self
* @return array
*/
public function destroyAll()
public function getShortcodes()
{
$this->handlers = new HandlerContainer();
return $this;
return $this->shortcodes;
}
/**
* Strip any shortcodes from the content.
*
* @param string $content
*
* @return string
*/
public function strip($content)
{
$handlers = new HandlerContainer();
$handlers->setDefault(function(ShortcodeInterface $s) { return $s->getContent(); });
$processor = new Processor(new RegexParser(), $handlers);
return $processor->process($content);
}
/**
* Get count from all shortcodes.
*
* @return int
*/
public function count()
{
return count($this->handlers->getNames());
}
/**
* Return true is the given name exist in shortcodes array.
*
* @param string $name
*
* @param $shortcode
* @return bool
*/
public function exists($name)
public function has($shortcode)
{
return $this->handlers->has($name);
return array_key_exists($shortcode, $this->shortcodes);
}
/**
* Return true is the given content contains the named shortcode.
*
* @param string $content
* @param string $name
*
* @return bool
*/
public function contains($content, $name)
{
$hasShortcode = false;
$handlers = new HandlerContainer();
$handlers->setDefault(function(ShortcodeInterface $s) use($name, &$hasShortcode) {
if($s->getName() === $name) {
$hasShortcode = true;
}
});
$processor = new Processor(new RegexParser(), $handlers);
$processor->process($content);
return $hasShortcode;
}
/**
* Parse content and replace parts of it using registered handlers
* 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 string
* @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)
{
$processor = new Processor(new RegexParser(), $this->handlers);
if (empty($this->shortcodes)) {
return $content;
}
return $processor->process($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';
}
}

View File

@ -0,0 +1,51 @@
<?php namespace Sensory5\Shortcode\Classes;
/**
* @author Tomasz Kowalczyk <tomasz@kowalczyk.cc>
*/
interface ShortcodeInterface
{
/**
* Returns new instance of given shortcode with changed content
*
* @param string $content
*
* @return self
*/
public function withContent($content);
/**
* Returns shortcode name
*
* @return string
*/
public function getName();
/**
* Returns associative array(name => value) of shortcode parameters
*
* @return array
*/
public function getParameters();
/**
* Returns parameter value using its name, will return null for parameter
* without value
*
* @param string $name Parameter name
* @param null $default Value returned if there is no parameter with given name
*
* @return mixed
*/
public function getParameter($name, $default = null);
/**
* Returns shortcode content (data between opening and closing tag). Null
* means that shortcode had no content (was self closing), do not confuse
* that with empty string (hint: use strict comparison operator ===).
*
* @return string|null
*/
public function getContent();
}