1 <?php
2 /**
3 * A simple parser that allows you to map BBCode-like "shortcodes" to an arbitrary callback.
4 *
5 * Shortcodes can take the form:
6 * <code>
7 * [shortcode]
8 * [shortcode attributes="example" /]
9 * [shortcode]enclosed content[/shortcode]
10 * </code>
11 *
12 * @package sapphire
13 * @subpackage misc
14 */
15 class ShortcodeParser {
16
17 private static $instances = array();
18
19 private static $active_instance = 'default';
20
21 // -----------------------------------------------------------------------------------------------------------------
22
23 protected $shortcodes = array();
24
25 // -----------------------------------------------------------------------------------------------------------------
26
27 /**
28 * Get the {@link ShortcodeParser} instance that is attached to a particular identifier.
29 *
30 * @param string $identifier Defaults to "default".
31 * @return ShortcodeParser
32 */
33 public static function get($identifier = 'default') {
34 if(!array_key_exists($identifier, self::$instances)) {
35 self::$instances[$identifier] = new ShortcodeParser();
36 }
37
38 return self::$instances[$identifier];
39 }
40
41 /**
42 * Get the currently active/default {@link ShortcodeParser} instance.
43 *
44 * @return ShortcodeParser
45 */
46 public static function get_active() {
47 return self::get(self::$active_instance);
48 }
49
50 /**
51 * Set the identifier to use for the current active/default {@link ShortcodeParser} instance.
52 *
53 * @param string $identifier
54 */
55 public static function set_active($identifier) {
56 self::$active_instance = (string) $identifier;
57 }
58
59 // -----------------------------------------------------------------------------------------------------------------
60
61 /**
62 * Register a shortcode, and attach it to a PHP callback.
63 *
64 * The callback for a shortcode will have the following arguments passed to it:
65 * - Any parameters attached to the shortcode as an associative array (keys are lower-case).
66 * - Any content enclosed within the shortcode (if it is an enclosing shortcode). Note that any content within
67 * this will not have been parsed, and can optionally be fed back into the parser.
68 * - The {@link ShortcodeParser} instance used to parse the content.
69 * - The shortcode tag name that was matched within the parsed content.
70 *
71 * @param string $shortcode The shortcode tag to map to the callback - normally in lowercase_underscore format.
72 * @param callback $callback The callback to replace the shortcode with.
73 */
74 public function register($shortcode, $callback) {
75 if(is_callable($callback)) $this->shortcodes[$shortcode] = $callback;
76 }
77
78 /**
79 * Check if a shortcode has been registered.
80 *
81 * @param string $shortcode
82 * @return bool
83 */
84 public function registered($shortcode) {
85 return array_key_exists($shortcode, $this->shortcodes);
86 }
87
88 /**
89 * Remove a specific registered shortcode.
90 *
91 * @param string $shortcode
92 */
93 public function unregister($shortcode) {
94 if($this->registered($shortcode)) unset($this->shortcodes[$shortcode]);
95 }
96
97 /**
98 * Remove all registered shortcodes.
99 */
100 public function clear() {
101 $this->shortcodes = array();
102 }
103
104 // -----------------------------------------------------------------------------------------------------------------
105
106 /**
107 * Parse a string, and replace any registered shortcodes within it with the result of the mapped callback.
108 *
109 * @param string $content
110 * @return string
111 */
112 public function parse($content) {
113 if(!$this->shortcodes) return $content;
114
115 $shortcodes = implode('|', array_map('preg_quote', array_keys($this->shortcodes)));
116 $pattern = "/(.?)\[($shortcodes)(.*?)(\/)?\](?(4)|(?:(.+?)\[\/\s*\\2\s*\]))?(.?)/s";
117
118 return preg_replace_callback($pattern, array($this, 'handleShortcode'), $content);
119 }
120
121 /**
122 * @ignore
123 */
124 protected function handleShortcode($matches) {
125 $prefix = $matches[1];
126 $suffix = $matches[6];
127 $shortcode = $matches[2];
128
129 // allow for escaping shortcodes by enclosing them in double brackets ([[shortcode]])
130 if($prefix == '[' && $suffix == ']') {
131 return substr($matches[0], 1, -1);
132 }
133
134 $attributes = array(); // Parse attributes into into this array.
135
136 if(preg_match_all('/(\w+) *= *(?:([\'"])(.*?)\\2|([^ "\'>]+))/', $matches[3], $match, PREG_SET_ORDER)) {
137 foreach($match as $attribute) {
138 if(!empty($attribute[4])) {
139 $attributes[strtolower($attribute[1])] = $attribute[4];
140 } elseif(!empty($attribute[3])) {
141 $attributes[strtolower($attribute[1])] = $attribute[3];
142 }
143 }
144 }
145
146 return $prefix . call_user_func($this->shortcodes[$shortcode], $attributes, $matches[5], $this, $shortcode) . $suffix;
147 }
148
149 }