diff --git a/docs/en/topics/index.md b/docs/en/topics/index.md index e6700518a..d192ed1d3 100644 --- a/docs/en/topics/index.md +++ b/docs/en/topics/index.md @@ -24,6 +24,7 @@ It is where most documentation should live, and is the natural "second step" aft * [Page Types](page-types): What is a "page type" and how do you create one? * [Search](search): Searching for properties in the database as well as other documents * [Security](security): How to develop secure SilverStripe applications with good code examples + * [Shortcodes](shortcodes): Use simple placeholders for powerful content replacements like multimedia embeds * [Templates](templates): SilverStripe template syntax: Variables, loops, includes and much more * [Testing](testing): Functional and Unit Testing with PHPUnit and SilverStripe's testing framework * [Developing Themes](theme-development): Package templates, images and CSS to a reusable theme diff --git a/docs/en/topics/shortcodes.md b/docs/en/topics/shortcodes.md new file mode 100644 index 000000000..d11b07091 --- /dev/null +++ b/docs/en/topics/shortcodes.md @@ -0,0 +1,143 @@ +# Shortcodes: Flexible Content Embedding + +## Overview + +The `[api:ShortcodeParser]` API is simple parser that allows you to map specifically +formatted content to a callback to transform them into something else. +You might know this concept from forum software which don't allow you to insert +direct HTML, instead resorting to a custom syntax. + +In the CMS, authors often want to insert content elements which go beyond +standard formatting, at an arbitrary position in their WYSIWYG editor. +Shortcodes are a semi-technical solution for this. A good example would +be embedding a 3D file viewer or a Google Map at a certain location. + +Here's some syntax variations: + + [myshortcode] + [myshortcode /] + [myshortcode,myparameter="value"] + [myshortcode,myparameter="value"]Enclosed Content[/myshortcode] + +## Usage + +In its most basic form, you can invoke the `[api:ShortcodeParser]` directly: + + :::php + ShortcodeParser::get_active()->parse($myvalue); + +In addition, shortcodes are automatically parsed on any database field which is declared +as `[api:HTMLValue]` or `[api:HTMLText]`, when rendered into a template. +This means you can use shortcodes on common fields like `SiteTree.Content`, +and any other `[api:DataObject::$db]` definitions of these types. + +In order to allow shortcodes in your own template placeholders, +ensure they're casted correctly: + + :::php + class MyObject extends DataObject { + static $db = array('Content' => 'HTMLText'); + static $casting = array('ContentHighlighted' => 'HTMLText'); + public function ContentHighlighted($term) { + return str_replace($term, "$term", $this->Content); + } + } + +There is currently no way to allow shortcodes directly in template markup +(as opposed to return values of template placeholders). + +## Defining Custom Shortcodes + +All you need to do to define a shortcode is to register a callback with the parser that will be called whenever a +shortcode is encountered. This callback will return a string to replace the shortcode with. +If the shortcode is used for template placeholders of type `HTMLText` or `HTMLVarchar`, +the returned value should be valid HTML + +To register a shortcode you call: + + ShortcodeParser::get('default')->register('myshortcode', ); + +These parameters are passed to the callback: + + - 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 ShortcodeParser instance used to parse the content. + - The shortcode tag name that was matched within the parsed content. + +## Example: Google Maps Iframe by Address + +To demonstrate how easy it is to build custom shortcodes, we'll build one to display +a Google Map based on a provided address. Format: + + [googlemap,width=500,height=300]97-99 Courtenay Place, Wellington, New Zealand[/googlemap] + +So we've got the address as "content" of our new `googlemap` shortcode tags, +plus some `width` and `height` arguments. We'll add defaults to those in our shortcode parser so they're optional. + + :::php + ShortcodeParser::get('default')->register('googlemap', function($arguments, $address, $parser, $shortcode) { + $iframeUrl = sprintf( + 'http://maps.google.com/maps?q=%s&hnear=%s&ie=UTF8&hq=&t=m&z=14&output=embed', + urlencode($address), + urlencode($address) + ); + $width = (isset($args['width']) && $args['width']) ? $args['width'] : 400; + $height = (isset($args['height']) && $args['height']) ? $args['height'] : 300; + return sprintf( + '', + $width, + $height, + $iframeUrl + ); + }); + +The hard bits are taken care of (parsing out the shortcodes), everything we need to do is a bit of string replacement. +CMS users still need to remember the specific syntax, but these shortcodes can form the basis +for more advanced editing interfaces (with visual placeholders). See the built-in `[embed]` shortcode as an example +for coupling shortcodes with a form to create and edit placeholders. + +## Built-in Shortcodes + +SilverStripe comes with several shortcode parsers already. + +### Links + +Internal page links keep references to their database IDs rather than +the URL, in order to make these links resilient against moving the target page to a different +location in the page tree. This is done through the `[sitetree_link]` shortcode, which +takes an `id` parameter. Example: `` + +Links to internal `File` database records work exactly the same, but with the `[file_link]` shortcode. + +### Media (Photo, Video and Rich Content) + +Many media formats can be embedded into websites through the `` +tag, but some require plugins like Flash or special markup and attributes. +OEmbed is a standard to discover these formats based on a simple URL, +for example a Youtube link pasted into the "Insert Media" form of the CMS. + +Since TinyMCE can't represent all these varations, we're showing a placeholder +instead, and storing the URL with a custom `[embed]` shortcode. + +Example: `.[embed width=480 height=270 class=left thumbnail=http://i1.ytimg.com/vi/lmWeD-vZAMY/hqdefault.jpg?r=8767]http://www.youtube.com/watch?v=lmWeD-vZAMY[/embed]` + + +## Limitations + +Since the shortcode parser is based on a simple regular expression it cannot properly handle nested shortcodes. For +example the below code will not work as expected: + + [shortcode] + [shortcode][/shortcode] + [/shortcode] + +The parser will recognise this as: + + [shortcode] + [shortcode] + [/shortcode] + +## Related + + * [Wordpress implementation](http://codex.wordpress.org/Shortcode_API) \ No newline at end of file diff --git a/parsers/ShortcodeParser.php b/parsers/ShortcodeParser.php index e9f3470d8..5a1b461d3 100644 --- a/parsers/ShortcodeParser.php +++ b/parsers/ShortcodeParser.php @@ -1,9 +1,11 @@ !empty($match['oesc'][0]) || !empty($match['cesc1'][0]) || !empty($match['cesc2'][0]) ); } - } + } $i = count($tags); while($i--) { @@ -243,14 +245,14 @@ class ShortcodeParser { if ($i == 0) { $err = 'Close tag "'.$tags[$i]['close'].'" is the first found tag, so has no related open tag'; - } + } else if (!$tags[$i-1]['open']) { $err = 'Close tag "'.$tags[$i]['close'].'" preceded by another close tag "'.$tags[$i-1]['close'].'"'; } else if ($tags[$i]['close'] != $tags[$i-1]['open']) { $err = 'Close tag "'.$tags[$i]['close'].'" doesn\'t match preceding open tag "'.$tags[$i-1]['open'].'"'; } - + if($err) { if(self::$error_behavior == self::ERROR) user_error($err, E_USER_ERRROR); } @@ -345,12 +347,12 @@ class ShortcodeParser { } } - return $content; + return $content; }); - } + } } } - + /** * Replace the element-scoped tags with markers * @@ -361,14 +363,14 @@ class ShortcodeParser { if($tags) { $markerClass = self::$marker_class; - + $content = $this->replaceTagsWithText($content, $tags, function($idx, $tag) use ($markerClass) { return ''; }); - } + } return array($content, $tags); - } + } protected function findParentsForMarkers($nodes) { $parents = array(); @@ -378,16 +380,16 @@ class ShortcodeParser { do { $parent = $parent->parentNode; - } + } while($parent instanceof DOMElement && !in_array(strtolower($parent->tagName), self::$block_level_elements)); $node->setAttribute('data-parentid', count($parents)); $parents[] = $parent; - } - - return $parents; } + return $parents; +} + const BEFORE = 'before'; const AFTER = 'after'; const SPLIT = 'split';