diff --git a/Plugin.php b/Plugin.php index f60b4c8..2df89f0 100644 --- a/Plugin.php +++ b/Plugin.php @@ -6,7 +6,6 @@ use Html; use Event; use System\Classes\PluginBase; use Illuminate\Foundation\AliasLoader; -use Thunder\Shortcode\Shortcode\ShortcodeInterface; use Sensory5\Shortcode\Models\Settings; /** diff --git a/README.md b/README.md index 0055e88..75e03bf 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ The above registration would enable the following shortcode to be used: ### Use the shortcode within a page or blog post: -The usual shortcode syntax is supported via the [Thunderer\Shortcode](https://github.com/thunderer/Shortcode) project. +The usual shortcode syntax is supported: [code] [code argument="value"] @@ -29,6 +29,8 @@ The usual shortcode syntax is supported via the [Thunderer\Shortcode](https://gi [code]content[/code] [code argument="value"]content[/code] +For nested shortcodes, you must call the `parse` method within the function. + ### Enable shortcodes on all pages To enable shortcodes on all page rendering, go to **Shortcode Settings** in the admin settings panel and check "Enable Shortcodes on all page rendering". diff --git a/classes/ProcessedShortcode.php b/classes/ProcessedShortcode.php new file mode 100644 index 0000000..9135e46 --- /dev/null +++ b/classes/ProcessedShortcode.php @@ -0,0 +1,87 @@ +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; + } + +} diff --git a/classes/Shortcode.php b/classes/Shortcode.php index 91dc955..0f77d95 100644 --- a/classes/Shortcode.php +++ b/classes/Shortcode.php @@ -1,153 +1,291 @@ 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 */ - 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 */ - 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'; + } +} diff --git a/classes/ShortcodeInterface.php b/classes/ShortcodeInterface.php new file mode 100644 index 0000000..6306be8 --- /dev/null +++ b/classes/ShortcodeInterface.php @@ -0,0 +1,51 @@ + + */ +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(); + +} diff --git a/composer.json b/composer.json index 01189e9..ad552dc 100644 --- a/composer.json +++ b/composer.json @@ -15,8 +15,7 @@ "minimum-stability": "dev", "require": { "php": ">=5.4.0", - "composer/installers": "~1.0", - "thunderer/shortcode": "dev-master" + "composer/installers": "~1.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 858d570..291a93b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "1af5d57fe99292fd0a02e59af2fe9fd5", - "content-hash": "fb0e5eeb8a149a654e9b5bea92cd66e7", + "hash": "76dd76ccda7c2f94fab2f61858c44195", + "content-hash": "d098e1add52c1f1376b78a8d954f5408", "packages": [ { "name": "composer/installers", @@ -104,66 +104,12 @@ "zikula" ], "time": "2015-10-29 23:28:48" - }, - { - "name": "thunderer/shortcode", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/thunderer/Shortcode.git", - "reference": "745110c4ef490ba84a910abf7d3a2f26830df541" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thunderer/Shortcode/zipball/745110c4ef490ba84a910abf7d3a2f26830df541", - "reference": "745110c4ef490ba84a910abf7d3a2f26830df541", - "shasum": "" - }, - "require": { - "php": ">=5.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.1" - }, - "suggest": { - "ext-dom": "if you want to use XML serializer", - "ext-json": "if you want to use JSON serializer", - "symfony/yaml": "if you want to use YAML serializer" - }, - "type": "library", - "autoload": { - "psr-4": { - "Thunder\\Shortcode\\": "src/", - "Thunder\\Shortcode\\Tests\\": "tests/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Tomasz Kowalczyk", - "email": "tomasz@kowalczyk.cc" - } - ], - "description": "Advanced shortcode (BBCode) parser and engine for PHP", - "keywords": [ - "bbcode", - "engine", - "library", - "parser", - "shortcode" - ], - "time": "2015-11-12 13:00:02" } ], "packages-dev": [], "aliases": [], "minimum-stability": "dev", - "stability-flags": { - "thunderer/shortcode": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..0c342b9 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,23 @@ + + + + + ./tests + + + + + + + + diff --git a/tests/ShortcodeTest.php b/tests/ShortcodeTest.php index 663068d..797d5a4 100644 --- a/tests/ShortcodeTest.php +++ b/tests/ShortcodeTest.php @@ -1,12 +1,11 @@ assertSame(3, $this->getShortcode()->count()); + $this->assertSame(6, count($this->getShortcode()->getShortCodes())); } - public function testAll() + public function testNames() { - $this->assertSame(['name', 'content', 'nc'], $this->getShortcode()->all()); + $this->assertSame(['name', 'content', 'nc', 'nested', 'params', 'param'], $this->getShortcode()->names()); } - public function testUnregister() + public function testRemove() { - $this->assertSame('[name]', $this->getShortcode()->unregister('name')->parse('[name]')); - } - - public function testDestroy() - { - $this->assertSame('[name]', $this->getShortcode()->destroy()->parse('[name]')); + $this->assertSame('[name]', $this->getShortcode()->remove('name')->parse('[name]')); } public function testStrip() { $this->assertSame('', $this->getShortcode()->strip('[name]')); $this->assertSame('x y', $this->getShortcode()->strip('x [name]y')); - $this->assertSame('x a a y', $this->getShortcode()->strip('x [name] a [content /] a [/name] y')); + // $this->assertSame('x a a y', $this->getShortcode()->strip('x [name] a [content /] a [/name] y')); + $this->assertSame('x y', $this->getShortcode()->strip('x [name] a [content /] a [/name] y')); } - public function testExists() + public function testHas() { $shortcode = $this->getShortcode(); - $this->assertTrue($shortcode->exists('name')); - $this->assertTrue($shortcode->exists('content')); - $this->assertTrue($shortcode->exists('nc')); - $this->assertFalse($shortcode->exists('invalid')); + $this->assertTrue($shortcode->has('name')); + $this->assertTrue($shortcode->has('content')); + $this->assertTrue($shortcode->has('nc')); + $this->assertFalse($shortcode->has('invalid')); } public function testContains() { $shortcode = $this->getShortcode(); - $this->assertTrue($shortcode->contains('[name]', 'name')); - $this->assertFalse($shortcode->contains('[x]', 'name')); + $this->assertTrue($shortcode->contentContains('[name]', 'name')); + $this->assertFalse($shortcode->contentContains('[x]', 'name')); + } + + public function testGetParameters() + { + $shortcode = $this->getShortcode(); + + $this->assertSame('param1,one,param2,two,', $shortcode->parse('[params param1=one param2=two]')); + } + + public function testgetParameter() + { + $shortcode = $this->getShortcode(); + + $this->assertSame('numero uno', $shortcode->parse('[param param1="numero uno"]')); + } private function getShortcode() { $shortcode = new Shortcode(); - $shortcode->register('name', function(ShortcodeInterface $s) { + $shortcode->add('name', function(ShortcodeInterface $s) { return $s->getName(); }); - $shortcode->register('content', function(ShortcodeInterface $s) { + $shortcode->add('content', function(ShortcodeInterface $s) { return $s->getContent(); }); - $shortcode->register('nc', function(ShortcodeInterface $s) { + $shortcode->add('nc', function(ShortcodeInterface $s) { return $s->getName().': '.$s->getContent(); }); - + $shortcode->add('nested', function(ShortcodeInterface $s) use ($shortcode) { + return $shortcode->parse($s->getContent()); + }); + $shortcode->add('params', function(ShortcodeInterface $s) use ($shortcode) { + $params = ''; + foreach($s->getParameters() as $key => $value) { + $params .= $key.','.$value.','; + } + return $params; + }); + $shortcode->add('param', function(ShortcodeInterface $s) use ($shortcode) { + return $s->getParameter('param1'); + }); return $shortcode; } } diff --git a/updates/version.yaml b/updates/version.yaml index e6f5ef8..37d5d0e 100644 --- a/updates/version.yaml +++ b/updates/version.yaml @@ -1 +1,2 @@ 1.0.1: First version of Shortcodes +1.1.0: Replacing Shortcode implementation with WordPress clone