You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

291 lines
8.6 KiB

  1. <?php namespace Sensory5\Shortcode\Classes;
  2. use Sensory5\Shortcode\Classes\ProcessedShortcode;
  3. /**
  4. * This is a port of WordPress' brilliant shortcode feature
  5. * for use outside of WordPress. The code has remained largely unchanged
  6. *
  7. * Original from: https://github.com/Badcow/Shortcodes
  8. *
  9. * Class Shortcode
  10. *
  11. * @package Shortcode
  12. */
  13. class Shortcode
  14. {
  15. /**
  16. * The regex for attributes.
  17. *
  18. * This regex covers the following attribute situations:
  19. * - key = "value"
  20. * - key = 'value'
  21. * - key = value
  22. * - "value"
  23. * - value
  24. *
  25. * @var string
  26. */
  27. private $attrPattern = '/(\w+)\s*=\s*"([^"]*)"(?:\s|$)|(\w+)\s*=\s*\'([^\']*)\'(?:\s|$)|(\w+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/';
  28. /**
  29. * Indexed array of tags: shortcode callbacks
  30. *
  31. * @var array
  32. */
  33. private $shortcodes = array();
  34. /**
  35. * @param string $tag
  36. * @param callable $function
  37. * @throws \ErrorException
  38. */
  39. public function add($tag, $function)
  40. {
  41. if (!is_callable($function)) {
  42. throw new \ErrorException("Function must be callable");
  43. }
  44. $this->shortcodes[$tag] = $function;
  45. return $this;
  46. }
  47. /**
  48. * @param string $tag
  49. * @return Shortcode
  50. */
  51. public function remove($tag)
  52. {
  53. if (array_key_exists($tag, $this->shortcodes)) {
  54. unset($this->shortcodes[$tag]);
  55. }
  56. return $this;
  57. }
  58. /**
  59. * @return array
  60. */
  61. public function getShortcodes()
  62. {
  63. return $this->shortcodes;
  64. }
  65. /**
  66. * @param $shortcode
  67. * @return bool
  68. */
  69. public function has($shortcode)
  70. {
  71. return array_key_exists($shortcode, $this->shortcodes);
  72. }
  73. /**
  74. * Tests whether content has a particular shortcode
  75. *
  76. * @param $content
  77. * @param $tag
  78. * @return bool
  79. */
  80. public function contentContains($content, $tag)
  81. {
  82. if (!$this->has($tag)) {
  83. return false;
  84. }
  85. preg_match_all($this->shortcodeRegex(), $content, $matches, PREG_SET_ORDER);
  86. if (empty($matches)) {
  87. return false;
  88. }
  89. foreach ($matches as $shortcode) {
  90. if ($tag === $shortcode[2]) {
  91. return true;
  92. }
  93. }
  94. return false;
  95. }
  96. /**
  97. * Returns an array of tag names that have been added
  98. *
  99. * @return array
  100. */
  101. public function names()
  102. {
  103. return array_keys($this->shortcodes);
  104. }
  105. /**
  106. * Search content for shortcodes and filter shortcodes through their hooks.
  107. *
  108. * If there are no shortcode tags defined, then the content will be returned
  109. * without any filtering. This might cause issues when plugins are disabled but
  110. * the shortcode will still show up in the post or content.
  111. *
  112. * @param string $content Content to search for shortcodes
  113. * @return string Content with shortcodes filtered out.
  114. */
  115. public function parse($content)
  116. {
  117. if (empty($this->shortcodes)) {
  118. return $content;
  119. }
  120. return preg_replace_callback($this->shortcodeRegex(), array($this, 'processTag'), $content);
  121. }
  122. /**
  123. * Remove all shortcode tags from the given content.
  124. *
  125. * @uses $shortcode_tags
  126. *
  127. * @param string $content Content to remove shortcode tags.
  128. * @return string Content without shortcode tags.
  129. */
  130. public function strip($content)
  131. {
  132. if (empty($this->shortcodes)) {
  133. return $content;
  134. }
  135. return preg_replace_callback($this->shortcodeRegex(), array($this, 'stripShortcodeTag'), $content);
  136. }
  137. /**
  138. * Regular Expression callable for do_shortcode() for calling shortcode hook.
  139. *
  140. * @see get_shortcode_regex for details of the match array contents.
  141. *
  142. * @param array $tag Regular expression match array
  143. * @return mixed False on failure.
  144. */
  145. private function processTag(array $tag)
  146. {
  147. // allow [[foo]] syntax for escaping a tag
  148. if ($tag[1] == '[' && $tag[6] == ']') {
  149. return substr($tag[0], 1, -1);
  150. }
  151. $tagName = $tag[2];
  152. $attr = $this->parseAttributes($tag[3]);
  153. $processed = ProcessedShortcode::create($attr, isset($tag[5]) ? $tag[5] : null, $tagName);
  154. return $tag[1] . call_user_func($this->shortcodes[$tagName], $processed) . $tag[6];
  155. }
  156. /**
  157. * Retrieve all attributes from the shortcodes tag.
  158. *
  159. * The attributes list has the attribute name as the key and the value of the
  160. * attribute as the value in the key/value pair. This allows for easier
  161. * retrieval of the attributes, since all attributes have to be known.
  162. *
  163. *
  164. * @param string $text
  165. * @return array List of attributes and their value.
  166. */
  167. private function parseAttributes($text)
  168. {
  169. $text = preg_replace("/[\x{00a0}\x{200b}]+/u", " ", $text);
  170. if (!preg_match_all($this->attrPattern, $text, $matches, PREG_SET_ORDER)) {
  171. return array(ltrim($text));
  172. }
  173. $attr = array();
  174. foreach ($matches as $match) {
  175. if (!empty($match[1])) {
  176. $attr[strtolower($match[1])] = stripcslashes($match[2]);
  177. } elseif (!empty($match[3])) {
  178. $attr[strtolower($match[3])] = stripcslashes($match[4]);
  179. } elseif (!empty($match[5])) {
  180. $attr[strtolower($match[5])] = stripcslashes($match[6]);
  181. } elseif (isset($match[7]) && strlen($match[7])) {
  182. $attr[] = stripcslashes($match[7]);
  183. } elseif (isset($match[8])) {
  184. $attr[] = stripcslashes($match[8]);
  185. }
  186. }
  187. return $attr;
  188. }
  189. /**
  190. * Strips a tag leaving escaped tags
  191. *
  192. * @param $tag
  193. * @return string
  194. */
  195. private function stripShortcodeTag($tag)
  196. {
  197. if ($tag[1] == '[' && $tag[6] == ']') {
  198. return substr($tag[0], 1, -1);
  199. }
  200. return $tag[1] . $tag[6];
  201. }
  202. /**
  203. * Retrieve the shortcode regular expression for searching.
  204. *
  205. * The regular expression combines the shortcode tags in the regular expression
  206. * in a regex class.
  207. *
  208. * The regular expression contains 6 different sub matches to help with parsing.
  209. *
  210. * 1 - An extra [ to allow for escaping shortcodes with double [[]]
  211. * 2 - The shortcode name
  212. * 3 - The shortcode argument list
  213. * 4 - The self closing /
  214. * 5 - The content of a shortcode when it wraps some content.
  215. * 6 - An extra ] to allow for escaping shortcodes with double [[]]
  216. *
  217. * @return string The shortcode search regular expression
  218. */
  219. private function shortcodeRegex()
  220. {
  221. $tagRegex = join('|', array_map('preg_quote', array_keys($this->shortcodes)));
  222. return
  223. '/'
  224. . '\\[' // Opening bracket
  225. . '(\\[?)' // 1: Optional second opening bracket for escaping shortcodes: [[tag]]
  226. . "($tagRegex)" // 2: Shortcode name
  227. . '(?![\\w-])' // Not followed by word character or hyphen
  228. . '(' // 3: Unroll the loop: Inside the opening shortcode tag
  229. . '[^\\]\\/]*' // Not a closing bracket or forward slash
  230. . '(?:'
  231. . '\\/(?!\\])' // A forward slash not followed by a closing bracket
  232. . '[^\\]\\/]*' // Not a closing bracket or forward slash
  233. . ')*?'
  234. . ')'
  235. . '(?:'
  236. . '(\\/)' // 4: Self closing tag ...
  237. . '\\]' // ... and closing bracket
  238. . '|'
  239. . '\\]' // Closing bracket
  240. . '(?:'
  241. . '(' // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags
  242. . '[^\\[]*+' // Not an opening bracket
  243. . '(?:'
  244. . '\\[(?!\\/\\2\\])' // An opening bracket not followed by the closing shortcode tag
  245. . '[^\\[]*+' // Not an opening bracket
  246. . ')*+'
  247. . ')'
  248. . '\\[\\/\\2\\]' // Closing shortcode tag
  249. . ')?'
  250. . ')'
  251. . '(\\]?)' // 6: Optional second closing brocket for escaping shortcodes: [[tag]]
  252. . '/s';
  253. }
  254. }