Patrick Ward
9 years ago
10 changed files with 434 additions and 165 deletions
-
1Plugin.php
-
4README.md
-
87classes/ProcessedShortcode.php
-
286classes/Shortcode.php
-
51classes/ShortcodeInterface.php
-
3composer.json
-
60composer.lock
-
23phpunit.xml
-
83tests/ShortcodeTest.php
-
1updates/version.yaml
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
@ -1,153 +1,291 @@ |
|||||
<?php namespace Sensory5\Shortcode\Classes; |
<?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 from: https://github.com/Badcow/Shortcodes |
||||
* |
* |
||||
* Original idea from Pingpong Labs (https://github.com/pingpong-labs/shortcode) |
|
||||
|
* Class Shortcode |
||||
* |
* |
||||
* Changed to fit closer to the Thunderer\Shortcode syntax. |
|
||||
|
* @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|$)/'; |
||||
|
|
||||
use Countable; |
|
||||
use Thunder\Shortcode\HandlerContainer\HandlerContainer; |
|
||||
use Thunder\Shortcode\Parser\RegexParser; |
|
||||
use Thunder\Shortcode\Processor\Processor; |
|
||||
use Thunder\Shortcode\Shortcode\ShortcodeInterface; |
|
||||
|
/** |
||||
|
* Indexed array of tags: shortcode callbacks |
||||
|
* |
||||
|
* @var array |
||||
|
*/ |
||||
|
private $shortcodes = array(); |
||||
|
|
||||
class Shortcode implements Countable |
|
||||
{ |
|
||||
/** @var HandlerContainer */ |
|
||||
private $handlers; |
|
||||
|
/** |
||||
|
* @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; |
||||
|
} |
||||
|
|
||||
/** |
/** |
||||
* The constructor. |
|
||||
|
* @param string $tag |
||||
|
* @return Shortcode |
||||
*/ |
*/ |
||||
public function __construct() |
|
||||
|
public function remove($tag) |
||||
{ |
{ |
||||
$this->handlers = new HandlerContainer(); |
|
||||
|
if (array_key_exists($tag, $this->shortcodes)) { |
||||
|
unset($this->shortcodes[$tag]); |
||||
|
} |
||||
|
|
||||
|
return $this; |
||||
|
|
||||
} |
} |
||||
|
|
||||
/** |
/** |
||||
* Get the names for all registered shortcodes. |
|
||||
* |
|
||||
* @return array |
* @return array |
||||
*/ |
*/ |
||||
public function getNames() |
|
||||
|
public function getShortcodes() |
||||
{ |
{ |
||||
return $this->handlers->getNames(); |
|
||||
|
return $this->shortcodes; |
||||
} |
} |
||||
|
|
||||
/** |
/** |
||||
* Add a new shortcode to the handler container. |
|
||||
* |
|
||||
* @param string $name |
|
||||
* @param mixed $callback |
|
||||
|
* @param $shortcode |
||||
|
* @return bool |
||||
*/ |
*/ |
||||
public function add($name, $callback) |
|
||||
|
public function has($shortcode) |
||||
{ |
{ |
||||
$this->handlers->add($name, $callback); |
|
||||
|
return array_key_exists($shortcode, $this->shortcodes); |
||||
} |
} |
||||
|
|
||||
/** |
/** |
||||
* Remove the specified shortcode name from the handler. |
|
||||
|
* Tests whether content has a particular shortcode |
||||
* |
* |
||||
* @param string $name |
|
||||
|
* @param $content |
||||
|
* @param $tag |
||||
|
* @return bool |
||||
*/ |
*/ |
||||
public function remove($name) |
|
||||
|
public function contentContains($content, $tag) |
||||
{ |
{ |
||||
if ($this->exists($name)) { |
|
||||
$this->handlers->remove($name); |
|
||||
|
if (!$this->has($tag)) { |
||||
|
return false; |
||||
} |
} |
||||
|
|
||||
return $this; |
|
||||
|
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; |
||||
} |
} |
||||
|
|
||||
/** |
/** |
||||
* Remove all registered shortcodes |
|
||||
|
* Returns an array of tag names that have been added |
||||
* |
* |
||||
* @return self |
|
||||
|
* @return array |
||||
*/ |
*/ |
||||
public function destroyAll() |
|
||||
|
public function names() |
||||
{ |
{ |
||||
$this->handlers = new HandlerContainer(); |
|
||||
|
|
||||
return $this; |
|
||||
|
return array_keys($this->shortcodes); |
||||
|
|
||||
} |
} |
||||
|
|
||||
/** |
/** |
||||
* Strip any shortcodes from the content. |
|
||||
|
* Search content for shortcodes and filter shortcodes through their hooks. |
||||
* |
* |
||||
* @param string $content |
|
||||
|
* 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. |
||||
* |
* |
||||
* @return string |
|
||||
|
* @param string $content Content to search for shortcodes |
||||
|
* @return string Content with shortcodes filtered out. |
||||
*/ |
*/ |
||||
public function strip($content) |
|
||||
|
public function parse($content) |
||||
{ |
{ |
||||
$handlers = new HandlerContainer(); |
|
||||
$handlers->setDefault(function(ShortcodeInterface $s) { return $s->getContent(); }); |
|
||||
$processor = new Processor(new RegexParser(), $handlers); |
|
||||
|
if (empty($this->shortcodes)) { |
||||
|
return $content; |
||||
|
} |
||||
|
|
||||
return $processor->process($content); |
|
||||
|
return preg_replace_callback($this->shortcodeRegex(), array($this, 'processTag'), $content); |
||||
} |
} |
||||
|
|
||||
/** |
/** |
||||
* Get count from all shortcodes. |
|
||||
|
* Remove all shortcode tags from the given content. |
||||
* |
* |
||||
* @return int |
|
||||
|
* @uses $shortcode_tags |
||||
|
* |
||||
|
* @param string $content Content to remove shortcode tags. |
||||
|
* @return string Content without shortcode tags. |
||||
*/ |
*/ |
||||
public function count() |
|
||||
|
public function strip($content) |
||||
{ |
{ |
||||
return count($this->handlers->getNames()); |
|
||||
|
if (empty($this->shortcodes)) { |
||||
|
return $content; |
||||
|
} |
||||
|
|
||||
|
return preg_replace_callback($this->shortcodeRegex(), array($this, 'stripShortcodeTag'), $content); |
||||
} |
} |
||||
|
|
||||
/** |
/** |
||||
* Return true is the given name exist in shortcodes array. |
|
||||
|
* Regular Expression callable for do_shortcode() for calling shortcode hook. |
||||
* |
* |
||||
* @param string $name |
|
||||
|
* @see get_shortcode_regex for details of the match array contents. |
||||
* |
* |
||||
* @return bool |
|
||||
|
* @param array $tag Regular expression match array |
||||
|
* @return mixed False on failure. |
||||
*/ |
*/ |
||||
public function exists($name) |
|
||||
|
private function processTag(array $tag) |
||||
{ |
{ |
||||
return $this->handlers->has($name); |
|
||||
|
// 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]; |
||||
|
|
||||
} |
} |
||||
|
|
||||
/** |
/** |
||||
* Return true is the given content contains the named shortcode. |
|
||||
|
* Retrieve all attributes from the shortcodes tag. |
||||
* |
* |
||||
* @param string $content |
|
||||
* @param string $name |
|
||||
|
* 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. |
||||
* |
* |
||||
* @return bool |
|
||||
|
* |
||||
|
* @param string $text |
||||
|
* @return array List of attributes and their value. |
||||
*/ |
*/ |
||||
public function contains($content, $name) |
|
||||
|
private function parseAttributes($text) |
||||
{ |
{ |
||||
$hasShortcode = false; |
|
||||
|
$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(); |
||||
|
|
||||
$handlers = new HandlerContainer(); |
|
||||
$handlers->setDefault(function(ShortcodeInterface $s) use($name, &$hasShortcode) { |
|
||||
if($s->getName() === $name) { |
|
||||
$hasShortcode = true; |
|
||||
|
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]); |
||||
} |
} |
||||
}); |
|
||||
$processor = new Processor(new RegexParser(), $handlers); |
|
||||
$processor->process($content); |
|
||||
|
} |
||||
|
|
||||
return $hasShortcode; |
|
||||
|
return $attr; |
||||
} |
} |
||||
|
|
||||
/** |
/** |
||||
* Parse content and replace parts of it using registered handlers |
|
||||
* |
|
||||
* @param $content |
|
||||
|
* Strips a tag leaving escaped tags |
||||
* |
* |
||||
|
* @param $tag |
||||
* @return string |
* @return string |
||||
*/ |
*/ |
||||
public function parse($content) |
|
||||
|
private function stripShortcodeTag($tag) |
||||
{ |
{ |
||||
$processor = new Processor(new RegexParser(), $this->handlers); |
|
||||
|
if ($tag[1] == '[' && $tag[6] == ']') { |
||||
|
return substr($tag[0], 1, -1); |
||||
|
} |
||||
|
|
||||
return $processor->process($content); |
|
||||
|
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'; |
||||
|
} |
||||
|
} |
@ -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(); |
||||
|
|
||||
|
} |
@ -0,0 +1,23 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<phpunit backupGlobals="false" |
||||
|
backupStaticAttributes="false" |
||||
|
bootstrap="../../../tests/bootstrap.php" |
||||
|
colors="true" |
||||
|
convertErrorsToExceptions="true" |
||||
|
convertNoticesToExceptions="true" |
||||
|
convertWarningsToExceptions="true" |
||||
|
processIsolation="false" |
||||
|
stopOnFailure="false" |
||||
|
syntaxCheck="false" |
||||
|
> |
||||
|
<testsuites> |
||||
|
<testsuite name="PolicyManager Unit Test Suite"> |
||||
|
<directory>./tests</directory> |
||||
|
</testsuite> |
||||
|
</testsuites> |
||||
|
<php> |
||||
|
<env name="APP_ENV" value="testing" /> |
||||
|
<env name="CACHE_DRIVER" value="array" /> |
||||
|
<env name="SESSION_DRIVER" value="array" /> |
||||
|
</php> |
||||
|
</phpunit> |
@ -1 +1,2 @@ |
|||||
1.0.1: First version of Shortcodes |
1.0.1: First version of Shortcodes |
||||
|
1.1.0: Replacing Shortcode implementation with WordPress clone |
Write
Preview
Loading…
Cancel
Save
Reference in new issue