shortcode.module 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. <?php
  2. /*
  3. * TODO
  4. *
  5. * strip_shortcodes - remove all shortcodes from the processed text
  6. *
  7. * correct element cascading
  8. *
  9. */
  10. /**
  11. * Build a list of all shortcodes (for filter).
  12. * Calls the shortcode hook with the list parameter on all module
  13. */
  14. function shortcode_list_all($reset = FALSE) {
  15. $shortcodes = &drupal_static(__FUNCTION__);
  16. if(!isset($shortcodes) || $reset) {
  17. $shortcodes = array();
  18. $shortcodes += module_invoke_all('shortcode_info');
  19. }
  20. return $shortcodes;
  21. }
  22. /**
  23. * Returns only enabled shortcodes for a format
  24. */
  25. function shortcode_list_all_enabled($format, $reset = FALSE) {
  26. if (is_string($format)) {
  27. $format = filter_format_load($format);
  28. }
  29. $shortcodes_enabled = &drupal_static(__FUNCTION__, array());
  30. if (isset($shortcodes_enabled[$format->format]) && !$reset) {
  31. return $shortcodes_enabled[$format->format];
  32. }
  33. $shortcodes_enabled[$format->format] = array();
  34. $shortcodes = shortcode_list_all($reset);
  35. $filters = filter_list_format($format->format);
  36. if (empty($filters['shortcode'])) {
  37. return array();
  38. }
  39. foreach($filters['shortcode']->settings as $name => $enabled) { // Run through all shortcodes
  40. if ($enabled) {
  41. $shortcodes_enabled[$format->format][$name] = $shortcodes[$name];
  42. }
  43. }
  44. return $shortcodes_enabled[$format->format];
  45. }
  46. /**
  47. * Implementation of hook_filter_info().
  48. */
  49. function shortcode_filter_info() {
  50. $filters['shortcode'] = array(
  51. 'title' => t('Shortcodes'),
  52. 'description' => t('Provides WP like shortcodes to this text format.'),
  53. 'process callback' => '_shortcode_process',
  54. 'settings callback' => '_shortcode_settings_form',
  55. 'tips callback' => '_shortcode_filter_tips',
  56. );
  57. $filters['shortcode_text_corrector'] = array(
  58. 'title' => t('Shortcodes - html corrector'),
  59. 'description' => t('Trying to correct the html around shortcodes. Enable only if you using wysiwyg editor.'),
  60. 'process callback' => '_shortcode_postprocess_text',
  61. );
  62. return $filters;
  63. }
  64. /**
  65. * Filter tips callback
  66. */
  67. function _shortcode_filter_tips($filter, $format, $long = FALSE) {
  68. $shortcodes = shortcode_list_all_enabled($format);
  69. $tips = array();
  70. $args = func_get_args();
  71. foreach($filter->settings as $name => $enabled) { // Run through all shortcodes
  72. if($enabled && !empty($shortcodes[$name]['tips callback']) && function_exists($shortcodes[$name]['tips callback'])) {
  73. $tips[] = call_user_func_array($shortcodes[$name]['tips callback'], array($format, $long));
  74. }
  75. }
  76. return theme('item_list',
  77. array(
  78. 'title' => t('Shortcodes usage'),
  79. 'items' => $tips,
  80. 'type' => 'ol',
  81. )
  82. );
  83. }
  84. /**
  85. * Settings form
  86. */
  87. function _shortcode_settings_form($form, &$form_state, $filter, $format, $defaults) {
  88. $settings = array();
  89. $filter->settings += $defaults;
  90. $shortcodes = shortcode_list_all();
  91. foreach ($shortcodes as $key => $shortcode) {
  92. $settings[$key] = array(
  93. '#type' => 'checkbox',
  94. '#title' => t('Enable %name shortcode', array('%name' => $shortcode['title'])),
  95. '#default_value' => array(),
  96. '#description' => 'Enable or disable this shortcode in this input format',
  97. );
  98. if (!empty($filter->settings[$key])) {
  99. $settings[$key]['#default_value'] = $filter->settings[$key];
  100. }
  101. elseif( !empty($defaults[$key])) {
  102. $settings[$key]['#default_value'] = $defaults[$key];
  103. }
  104. }
  105. return $settings;
  106. }
  107. /**
  108. * Tags cache
  109. * @param $tags
  110. *
  111. * @access private
  112. */
  113. function _shortcode_tags($tags = NULL) {
  114. $shortcodes = &drupal_static(__FUNCTION__, array());
  115. if ($tags) {
  116. $shortcodes = $tags;
  117. return TRUE;
  118. }
  119. return $shortcodes;
  120. }
  121. /**
  122. * Process the shortcodes according to the text and the text format.
  123. */
  124. function _shortcode_process($text, $filter) {
  125. // TODO: need cache for list_all for the given filter!
  126. $shortcodes = shortcode_list_all();
  127. $shortcodes_enabled = array();
  128. foreach($filter->settings as $name => $value) { // run through all shortcodes
  129. if($value && $shortcodes[$name]['process callback']) {
  130. $shortcodes_enabled[$name] = array(
  131. 'function' => $shortcodes[$name]['process callback'],
  132. );
  133. }
  134. }
  135. if (empty($shortcodes_enabled)) {
  136. return $text;
  137. }
  138. // save the shortcodes
  139. _shortcode_tags($shortcodes_enabled);
  140. // improved version - recursive processing - embed tags within other tags is supported!
  141. $chunks = preg_split('!(\[.*?\])!', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
  142. //dpr($chunks);
  143. $heap = array();
  144. $heap_index = array();
  145. foreach ($chunks as $c) {
  146. if (!$c) {
  147. continue;
  148. }
  149. // shortcode or not
  150. if (($c[0] == '[') && (substr($c, -1, 1) == ']')) {
  151. // $c contains shortcode
  152. // self-closing tag or not
  153. $c = substr($c, 1, -1);
  154. //dpr('process: ' . $c);
  155. if (substr($c, -1, 1) == '/') {
  156. // process a self closing tag - it has / at the end!
  157. //dpr('self closing: ' . $c);
  158. /*
  159. * 0 - the full tag text?
  160. * 1/5 - An extra [ or ] to allow for escaping shortcodes with double [[]]
  161. * 2 - The shortcode name
  162. * 3 - The shortcode argument list
  163. * 4 - The content of a shortcode when it wraps some content.
  164. * */
  165. $ts = explode(' ', trim($c));
  166. $tag = array_shift($ts);
  167. $m = array(
  168. $c,
  169. '',
  170. $tag,
  171. implode(' ', $ts),
  172. NULL,
  173. ''
  174. );
  175. array_unshift($heap_index, '_string_');
  176. array_unshift($heap, _shortcode_process_tag($m));
  177. }
  178. elseif ($c[0] == '/') {
  179. // closing tag - process the heap
  180. $closing_tag = substr($c, 1);
  181. //dpr('closing tag: ' . $closing_tag );
  182. $process_heap = array();
  183. $process_heap_index = array();
  184. $found = FALSE;
  185. // get elements from heap and process
  186. do {
  187. $tag = array_shift($heap_index);
  188. $heap_text = array_shift($heap);
  189. if($closing_tag == $tag) {
  190. // process the whole tag
  191. $m = array(
  192. $tag . ' ' . $heap_text,
  193. '',
  194. $tag,
  195. $heap_text,
  196. implode('', $process_heap),
  197. ''
  198. );
  199. $str = _shortcode_process_tag($m);
  200. array_unshift($heap_index, '_string_');
  201. array_unshift($heap, $str);
  202. $found = TRUE;
  203. }
  204. else {
  205. array_unshift($process_heap, $heap_text);
  206. array_unshift($process_heap_index, $tag);
  207. }
  208. } while(!$found && $heap);
  209. if(!$found) {
  210. foreach($process_heap as $val) {
  211. array_unshift($heap, $val);
  212. }
  213. foreach($process_heap_index as $val) {
  214. array_unshift($heap_index, $val);
  215. }
  216. }
  217. }
  218. else {
  219. // starting tag. put to the heap
  220. //dpr('tag pattern: ' . $c);
  221. $ts = explode(' ', trim($c));
  222. $tag = array_shift($ts);
  223. // dpr('start tag: ' . $tag);
  224. array_unshift($heap_index, $tag);
  225. array_unshift($heap, implode(' ', $ts));
  226. }
  227. }
  228. else {
  229. // not found a pair?
  230. array_unshift($heap_index, '_string_');
  231. array_unshift($heap, $c);
  232. }
  233. }
  234. return(implode('', array_reverse($heap)));
  235. }
  236. /*
  237. * Html corrector for wysiwyg editors
  238. *
  239. * Correcting p elements around the divs. No div are allowed in p so remove them.
  240. *
  241. */
  242. function _shortcode_postprocess_text($text, $filter) {
  243. $patterns = array(
  244. '|#!#|is',
  245. '!<p>(&nbsp;|\s)*(<\/*div>)!is',
  246. '!<p>(&nbsp;|\s)*(<div)!is',
  247. '!(<\/div.*?>)\s*</p>!is',
  248. '!(<div.*?>)\s*</p>!is',
  249. );
  250. //$replacements = array('!!\\2', '###\\2', '@@@\\1');
  251. $replacements = array('', '\\2', '\\2', '\\1', '\\1');
  252. return preg_replace($patterns, $replacements, $text);
  253. }
  254. /**
  255. * Regular Expression callable for do_shortcode() for calling shortcode hook.
  256. * @see get_shortcode_regex for details of the match array contents.
  257. *
  258. * @since 2.5
  259. * @access private
  260. * @uses $shortcode_tags
  261. *
  262. * @param array $m Regular expression match array
  263. * @return mixed False on failure.
  264. */
  265. function _shortcode_process_tag($m) {
  266. // get tags from static cache
  267. $shortcodes = _shortcode_tags();
  268. // allow [[foo]] syntax for escaping a tag
  269. if ($m[1] == '[' && $m[5] == ']') {
  270. return substr($m[0], 1, -1);
  271. }
  272. $tag = $m[2];
  273. if (!empty($shortcodes[$tag])) {
  274. // tag exists (enabled)
  275. $attr = _shortcode_parse_attrs($m[3]);
  276. /*
  277. * 0 - the full tag text?
  278. * 1/5 - An extra [ or ] to allow for escaping shortcodes with double [[]]
  279. * 2 - The shortcode name
  280. * 3 - The shortcode argument list
  281. * 4 - The content of a shortcode when it wraps some content.
  282. * */
  283. if (! is_null($m[4]) ) {
  284. // enclosing tag - extra parameter
  285. return $m[1] . call_user_func($shortcodes[$tag]['function'], $attr, $m[4], $m[2]) . $m[5];
  286. }
  287. else {
  288. // self-closing tag
  289. //dpr('fv self closing: ' . $shortcodes[$tag]->function);
  290. return $m[1] . call_user_func($shortcodes[$tag]['function'], $attr, NULL, $m[2]) . $m[5];
  291. }
  292. }
  293. elseif(is_null($m[4])) {
  294. return $m[4];
  295. }
  296. return '';
  297. }
  298. /**
  299. * Retrieve all attributes from the shortcodes tag.
  300. *
  301. * The attributes list has the attribute name as the key and the value of the
  302. * attribute as the value in the key/value pair. This allows for easier
  303. * retrieval of the attributes, since all attributes have to be known.
  304. *
  305. * @since 2.5
  306. *
  307. * @param string $text
  308. * @return array List of attributes and their value.
  309. */
  310. function _shortcode_parse_attrs($text) {
  311. $atts = array();
  312. $pattern = '/(\w+)\s*=\s*"([^"]*)"(?:\s|$)|(\w+)\s*=\s*\'([^\']*)\'(?:\s|$)|(\w+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/';
  313. $text = preg_replace("/[\x{00a0}\x{200b}]+/u", " ", $text);
  314. if ( preg_match_all($pattern, $text, $match, PREG_SET_ORDER) ) {
  315. foreach ($match as $m) {
  316. if (!empty($m[1]))
  317. $atts[strtolower($m[1])] = stripcslashes($m[2]);
  318. elseif (!empty($m[3]))
  319. $atts[strtolower($m[3])] = stripcslashes($m[4]);
  320. elseif (!empty($m[5]))
  321. $atts[strtolower($m[5])] = stripcslashes($m[6]);
  322. elseif (isset($m[7]) and strlen($m[7]))
  323. $atts[] = stripcslashes($m[7]);
  324. elseif (isset($m[8]))
  325. $atts[] = stripcslashes($m[8]);
  326. }
  327. }
  328. else {
  329. $atts = ltrim($text);
  330. }
  331. return $atts;
  332. }
  333. /**
  334. * Retrieve the shortcode regular expression for searching.
  335. *
  336. * The regular expression combines the shortcode tags in the regular expression
  337. * in a regex class.
  338. *
  339. * The regular expresion contains 6 different sub matches to help with parsing.
  340. *
  341. * 1/6 - An extra [ or ] to allow for escaping shortcodes with double [[]]
  342. * 2 - The shortcode name
  343. * 3 - The shortcode argument list
  344. * 4 - The self closing /
  345. * 5 - The content of a shortcode when it wraps some content.
  346. *
  347. * @return string The shortcode search regular expression
  348. */
  349. function _shortcode_get_shortcode_regex($names) {
  350. $tagregexp = join( '|', array_map('preg_quote', $names) );
  351. // WARNING! Do not change this regex without changing do_shortcode_tag() and strip_shortcodes()
  352. return '(.?)\[('.$tagregexp.')\b(.*?)(?:(\/))?\](?:(.+?)\[\/\2\])?(.?)';
  353. }
  354. /**
  355. * Combine user attributes with known attributes and fill in defaults when needed.
  356. *
  357. * The pairs should be considered to be all of the attributes which are
  358. * supported by the caller and given as a list. The returned attributes will
  359. * only contain the attributes in the $pairs list.
  360. *
  361. * If the $atts list has unsupported attributes, then they will be ignored and
  362. * removed from the final returned list.
  363. *
  364. * @since 2.5
  365. *
  366. * @param array $pairs Entire list of supported attributes and their defaults.
  367. * @param array $atts User defined attributes in shortcode tag.
  368. * @return array Combined and filtered attribute list.
  369. */
  370. function shortcode_attrs($pairs, $attrs) {
  371. $attrs = (array)$attrs;
  372. $out = array();
  373. foreach ($pairs as $name => $default) {
  374. if (array_key_exists($name, $attrs)) {
  375. $out[$name] = $attrs[$name];
  376. }
  377. else {
  378. $out[$name] = $default;
  379. }
  380. }
  381. return $out;
  382. }
  383. /**
  384. * Helper function to decide the given param is a bool value
  385. * @param mixed $var
  386. *
  387. * @return bool
  388. */
  389. function shortcode_bool($var) {
  390. switch (strtolower($var)) {
  391. case false:
  392. case 'false':
  393. case 'no':
  394. case '0':
  395. $res = FALSE;
  396. break;
  397. default:
  398. $res = TRUE;
  399. break;
  400. }
  401. return $res;
  402. }
  403. /**
  404. * Class parameter helper function
  405. * @param $class
  406. * @param $default
  407. */
  408. function shortcode_add_class($class='', $default='') {
  409. if ($class) {
  410. if (! is_array($class)) {
  411. $class = explode(' ', $class);
  412. }
  413. array_unshift($class, $default);
  414. $class = array_unique($class);
  415. }
  416. else {
  417. $class[] = $default;
  418. }
  419. return implode(' ', $class);
  420. } //shortcode_add_class
  421. // shortcode implementations
  422. /**
  423. * Generates a random code
  424. *
  425. * Calling
  426. * [random length=X /]
  427. *
  428. * Where X is the length of the random text.
  429. * If the length empty or invalid, between 1-99, the length will be 8
  430. *
  431. */
  432. function shortcode_shortcode_random($attrs, $text) {
  433. extract( shortcode_attrs( array(
  434. 'length' => 8,
  435. ), $attrs ));
  436. $length = intval($length);
  437. if (($length < 0) || ($length > 99)) {
  438. $length = 8;
  439. }
  440. $text = '';
  441. for ($i=0; $i < $length; ++$i) {
  442. $text .= chr(rand(32, 126));
  443. }
  444. return $text;
  445. }