mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
FEATURE: Added the Shortcode API (ShortcodeParser) to allow you to replace simple BBCode-like tags in a string with the results of a callback.
From: Andrew Short <andrewjshort@gmail.com> git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@88472 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
parent
7dfadd869c
commit
4ece35937f
149
parsers/ShortcodeParser.php
Executable file
149
parsers/ShortcodeParser.php
Executable file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
/**
|
||||
* A simple parser that allows you to map BBCode-like "shortcodes" to an arbitrary callback.
|
||||
*
|
||||
* Shortcodes can take the form:
|
||||
* <code>
|
||||
* [shortcode]
|
||||
* [shortcode attributes="example" /]
|
||||
* [shortcode]enclosed content[/shortcode]
|
||||
* </code>
|
||||
*
|
||||
* @package sapphire
|
||||
* @subpackage misc
|
||||
*/
|
||||
class ShortcodeParser {
|
||||
|
||||
private static $instances = array();
|
||||
|
||||
private static $active_instance = 'default';
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
protected $shortcodes = array();
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get the {@link ShortcodeParser} instance that is attached to a particular identifier.
|
||||
*
|
||||
* @param string $identifier Defaults to "default".
|
||||
* @return ShortcodeParser
|
||||
*/
|
||||
public static function get($identifier = 'default') {
|
||||
if(!array_key_exists($identifier, self::$instances)) {
|
||||
self::$instances[$identifier] = new ShortcodeParser();
|
||||
}
|
||||
|
||||
return self::$instances[$identifier];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently active/default {@link ShortcodeParser} instance.
|
||||
*
|
||||
* @return ShortcodeParser
|
||||
*/
|
||||
public static function get_active() {
|
||||
return self::get(self::$active_instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the identifier to use for the current active/default {@link ShortcodeParser} instance.
|
||||
*
|
||||
* @param string $identifier
|
||||
*/
|
||||
public static function set_active($identifier) {
|
||||
self::$active_instance = (string) $identifier;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Register a shortcode, and attach it to a PHP callback.
|
||||
*
|
||||
* The callback for a shortcode will have the following arguments passed to it:
|
||||
* - Any parameters attached to the shortcode as an associative array (keys are lower-case).
|
||||
* - Any content enclosed within the shortcode (if it is an enclosing shortcode). Note that any content within
|
||||
* this will not have been parsed, and can optionally be fed back into the parser.
|
||||
* - The {@link ShortcodeParser} instance used to parse the content.
|
||||
* - The shortcode tag name that was matched within the parsed content.
|
||||
*
|
||||
* @param string $shortcode The shortcode tag to map to the callback - normally in lowercase_underscore format.
|
||||
* @param callback $callback The callback to replace the shortcode with.
|
||||
*/
|
||||
public function register($shortcode, $callback) {
|
||||
if(is_callable($callback)) $this->shortcodes[$shortcode] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a shortcode has been registered.
|
||||
*
|
||||
* @param string $shortcode
|
||||
* @return bool
|
||||
*/
|
||||
public function registered($shortcode) {
|
||||
return array_key_exists($shortcode, $this->shortcodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific registered shortcode.
|
||||
*
|
||||
* @param string $shortcode
|
||||
*/
|
||||
public function unregister($shortcode) {
|
||||
if($this->registered($shortcode)) unset($this->shortcodes[$shortcode]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all registered shortcodes.
|
||||
*/
|
||||
public function clear() {
|
||||
$this->shortcodes = array();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Parse a string, and replace any registered shortcodes within it with the result of the mapped callback.
|
||||
*
|
||||
* @param string $content
|
||||
* @return string
|
||||
*/
|
||||
public function parse($content) {
|
||||
if(!$this->shortcodes) return $content;
|
||||
|
||||
$shortcodes = implode('|', array_map('preg_quote', array_keys($this->shortcodes)));
|
||||
$pattern = "/(.?)\[($shortcodes)(.*?)(\/)?\](?(4)|(?:(.+?)\[\/\s*\\2\s*\]))?(.?)/s";
|
||||
|
||||
return preg_replace_callback($pattern, array($this, 'handleShortcode'), $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
protected function handleShortcode($matches) {
|
||||
$prefix = $matches[1];
|
||||
$suffix = $matches[6];
|
||||
$shortcode = $matches[2];
|
||||
|
||||
// allow for escaping shortcodes by enclosing them in double brackets ([[shortcode]])
|
||||
if($prefix == '[' && $suffix == ']') {
|
||||
return substr($matches[0], 1, -1);
|
||||
}
|
||||
|
||||
$attributes = array(); // Parse attributes into into this array.
|
||||
|
||||
if(preg_match_all('/(\w+) *= *(?:([\'"])(.*?)\\2|([^ "\'>]+))/', $matches[3], $match, PREG_SET_ORDER)) {
|
||||
foreach($match as $attribute) {
|
||||
if(!empty($attribute[4])) {
|
||||
$attributes[strtolower($attribute[1])] = $attribute[4];
|
||||
} elseif(!empty($attribute[3])) {
|
||||
$attributes[strtolower($attribute[1])] = $attribute[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $prefix . call_user_func($this->shortcodes[$shortcode], $attributes, $matches[5], $this, $shortcode) . $suffix;
|
||||
}
|
||||
|
||||
}
|
112
tests/ShortcodeParserTest.php
Executable file
112
tests/ShortcodeParserTest.php
Executable file
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
/**
|
||||
* @package sapphire
|
||||
* @subpackage tests
|
||||
*/
|
||||
class ShortcodeParserTest extends SapphireTest {
|
||||
|
||||
protected $arguments, $contents, $tagName, $parser;
|
||||
|
||||
public function setUp() {
|
||||
ShortcodeParser::get('test')->register('test_shortcode', array($this, 'shortcodeSaver'));
|
||||
$this->parser = ShortcodeParser::get('test');
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that valid short codes that have not been registered are not replaced.
|
||||
*/
|
||||
public function testNotRegisteredShortcode() {
|
||||
$this->assertEquals('[not_shortcode]', $this->parser->parse('[not_shortcode]'));
|
||||
$this->assertEquals('[not_shortcode /]', $this->parser->parse('[not_shortcode /]'));
|
||||
$this->assertEquals('[not_shortcode foo="bar"]', $this->parser->parse('[not_shortcode foo="bar"]'));
|
||||
$this->assertEquals('[not_shortcode]a[/not_shortcode]', $this->parser->parse('[not_shortcode]a[/not_shortcode]'));
|
||||
}
|
||||
|
||||
public function testSimpleTag() {
|
||||
$tests = array('[test_shortcode]', '[test_shortcode ]', '[test_shortcode/]', '[test_shortcode /]');
|
||||
|
||||
foreach($tests as $test) {
|
||||
$this->parser->parse($test);
|
||||
|
||||
$this->assertEquals(array(), $this->arguments, $test);
|
||||
$this->assertEquals('', $this->contents, $test);
|
||||
$this->assertEquals('test_shortcode', $this->tagName, $test);
|
||||
}
|
||||
}
|
||||
|
||||
public function testOneArgument() {
|
||||
$tests = array (
|
||||
'[test_shortcode foo="bar"]',
|
||||
"[test_shortcode foo='bar']",
|
||||
'[test_shortcode foo = "bar" /]'
|
||||
);
|
||||
|
||||
foreach($tests as $test) {
|
||||
$this->parser->parse($test);
|
||||
|
||||
$this->assertEquals(array('foo' => 'bar'), $this->arguments, $test);
|
||||
$this->assertEquals('', $this->contents, $test);
|
||||
$this->assertEquals('test_shortcode', $this->tagName, $test);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMultipleArguments() {
|
||||
$this->parser->parse('[test_shortcode foo = "bar" bar=\'foo\' baz="buz"]');
|
||||
|
||||
$this->assertEquals(array('foo' => 'bar', 'bar' => 'foo', 'baz' => 'buz'), $this->arguments);
|
||||
$this->assertEquals('', $this->contents);
|
||||
$this->assertEquals('test_shortcode', $this->tagName);
|
||||
}
|
||||
|
||||
public function testEnclosing() {
|
||||
$this->parser->parse('[test_shortcode]foo[/test_shortcode]');
|
||||
|
||||
$this->assertEquals(array(), $this->arguments);
|
||||
$this->assertEquals('foo', $this->contents);
|
||||
$this->assertEquals('test_shortcode', $this->tagName);
|
||||
}
|
||||
|
||||
public function testEnclosingWithArguments() {
|
||||
$this->parser->parse('[test_shortcode foo = "bar" bar=\'foo\' baz="buz"]foo[/test_shortcode]');
|
||||
|
||||
$this->assertEquals(array('foo' => 'bar', 'bar' => 'foo', 'baz' => 'buz'), $this->arguments);
|
||||
$this->assertEquals('foo', $this->contents);
|
||||
$this->assertEquals('test_shortcode', $this->tagName);
|
||||
}
|
||||
|
||||
public function testShortcodeEscaping() {
|
||||
$this->assertEquals('[test_shortcode]', $this->parser->parse('[[test_shortcode]]'));
|
||||
$this->assertEquals('[test_shortcode]content[/test_shortcode]', $this->parser->parse('[[test_shortcode]content[/test_shortcode]]'));
|
||||
}
|
||||
|
||||
public function testUnquotedArguments() {
|
||||
$this->assertEquals('', $this->parser->parse('[test_shortcode foo=bar baz = buz]'));
|
||||
$this->assertEquals(array('foo' => 'bar', 'baz' => 'buz'), $this->arguments);
|
||||
}
|
||||
|
||||
public function testSelfClosingTag() {
|
||||
$this->assertEquals (
|
||||
'morecontent',
|
||||
$this->parser->parse('[test_shortcode id="1"/]more[test_shortcode id="2"]content[/test_shortcode]'),
|
||||
'Assert that self-closing tags are respected during parsing.'
|
||||
);
|
||||
|
||||
$this->assertEquals(2, $this->arguments['id']);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Stores the result of a shortcode parse in object properties for easy testing access.
|
||||
*/
|
||||
public function shortcodeSaver($arguments, $content = null, $parser, $tagName = null) {
|
||||
$this->arguments = $arguments;
|
||||
$this->contents = $content;
|
||||
$this->tagName = $tagName;
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user