shortcode.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. /**
  2. * Utility functions for parsing and handling shortcodes in JavaScript.
  3. *
  4. * @output wp-includes/js/shortcode.js
  5. */
  6. /**
  7. * Ensure the global `wp` object exists.
  8. *
  9. * @namespace wp
  10. */
  11. window.wp = window.wp || {};
  12. (function(){
  13. wp.shortcode = {
  14. /*
  15. * ### Find the next matching shortcode.
  16. *
  17. * Given a shortcode `tag`, a block of `text`, and an optional starting
  18. * `index`, returns the next matching shortcode or `undefined`.
  19. *
  20. * Shortcodes are formatted as an object that contains the match
  21. * `content`, the matching `index`, and the parsed `shortcode` object.
  22. */
  23. next: function( tag, text, index ) {
  24. var re = wp.shortcode.regexp( tag ),
  25. match, result;
  26. re.lastIndex = index || 0;
  27. match = re.exec( text );
  28. if ( ! match ) {
  29. return;
  30. }
  31. // If we matched an escaped shortcode, try again.
  32. if ( '[' === match[1] && ']' === match[7] ) {
  33. return wp.shortcode.next( tag, text, re.lastIndex );
  34. }
  35. result = {
  36. index: match.index,
  37. content: match[0],
  38. shortcode: wp.shortcode.fromMatch( match )
  39. };
  40. // If we matched a leading `[`, strip it from the match
  41. // and increment the index accordingly.
  42. if ( match[1] ) {
  43. result.content = result.content.slice( 1 );
  44. result.index++;
  45. }
  46. // If we matched a trailing `]`, strip it from the match.
  47. if ( match[7] ) {
  48. result.content = result.content.slice( 0, -1 );
  49. }
  50. return result;
  51. },
  52. /*
  53. * ### Replace matching shortcodes in a block of text.
  54. *
  55. * Accepts a shortcode `tag`, content `text` to scan, and a `callback`
  56. * to process the shortcode matches and return a replacement string.
  57. * Returns the `text` with all shortcodes replaced.
  58. *
  59. * Shortcode matches are objects that contain the shortcode `tag`,
  60. * a shortcode `attrs` object, the `content` between shortcode tags,
  61. * and a boolean flag to indicate if the match was a `single` tag.
  62. */
  63. replace: function( tag, text, callback ) {
  64. return text.replace( wp.shortcode.regexp( tag ), function( match, left, tag, attrs, slash, content, closing, right ) {
  65. // If both extra brackets exist, the shortcode has been
  66. // properly escaped.
  67. if ( left === '[' && right === ']' ) {
  68. return match;
  69. }
  70. // Create the match object and pass it through the callback.
  71. var result = callback( wp.shortcode.fromMatch( arguments ) );
  72. // Make sure to return any of the extra brackets if they
  73. // weren't used to escape the shortcode.
  74. return result ? left + result + right : match;
  75. });
  76. },
  77. /*
  78. * ### Generate a string from shortcode parameters.
  79. *
  80. * Creates a `wp.shortcode` instance and returns a string.
  81. *
  82. * Accepts the same `options` as the `wp.shortcode()` constructor,
  83. * containing a `tag` string, a string or object of `attrs`, a boolean
  84. * indicating whether to format the shortcode using a `single` tag, and a
  85. * `content` string.
  86. */
  87. string: function( options ) {
  88. return new wp.shortcode( options ).string();
  89. },
  90. /*
  91. * ### Generate a RegExp to identify a shortcode.
  92. *
  93. * The base regex is functionally equivalent to the one found in
  94. * `get_shortcode_regex()` in `wp-includes/shortcodes.php`.
  95. *
  96. * Capture groups:
  97. *
  98. * 1. An extra `[` to allow for escaping shortcodes with double `[[]]`.
  99. * 2. The shortcode name.
  100. * 3. The shortcode argument list.
  101. * 4. The self closing `/`.
  102. * 5. The content of a shortcode when it wraps some content.
  103. * 6. The closing tag.
  104. * 7. An extra `]` to allow for escaping shortcodes with double `[[]]`.
  105. */
  106. regexp: _.memoize( function( tag ) {
  107. return new RegExp( '\\[(\\[?)(' + tag + ')(?![\\w-])([^\\]\\/]*(?:\\/(?!\\])[^\\]\\/]*)*?)(?:(\\/)\\]|\\](?:([^\\[]*(?:\\[(?!\\/\\2\\])[^\\[]*)*)(\\[\\/\\2\\]))?)(\\]?)', 'g' );
  108. }),
  109. /*
  110. * ### Parse shortcode attributes.
  111. *
  112. * Shortcodes accept many types of attributes. These can chiefly be
  113. * divided into named and numeric attributes:
  114. *
  115. * Named attributes are assigned on a key/value basis, while numeric
  116. * attributes are treated as an array.
  117. *
  118. * Named attributes can be formatted as either `name="value"`,
  119. * `name='value'`, or `name=value`. Numeric attributes can be formatted
  120. * as `"value"` or just `value`.
  121. */
  122. attrs: _.memoize( function( text ) {
  123. var named = {},
  124. numeric = [],
  125. pattern, match;
  126. /*
  127. * This regular expression is reused from `shortcode_parse_atts()`
  128. * in `wp-includes/shortcodes.php`.
  129. *
  130. * Capture groups:
  131. *
  132. * 1. An attribute name, that corresponds to...
  133. * 2. a value in double quotes.
  134. * 3. An attribute name, that corresponds to...
  135. * 4. a value in single quotes.
  136. * 5. An attribute name, that corresponds to...
  137. * 6. an unquoted value.
  138. * 7. A numeric attribute in double quotes.
  139. * 8. A numeric attribute in single quotes.
  140. * 9. An unquoted numeric attribute.
  141. */
  142. pattern = /([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)|([\w-]+)\s*=\s*'([^']*)'(?:\s|$)|([\w-]+)\s*=\s*([^\s'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|'([^']*)'(?:\s|$)|(\S+)(?:\s|$)/g;
  143. // Map zero-width spaces to actual spaces.
  144. text = text.replace( /[\u00a0\u200b]/g, ' ' );
  145. // Match and normalize attributes.
  146. while ( (match = pattern.exec( text )) ) {
  147. if ( match[1] ) {
  148. named[ match[1].toLowerCase() ] = match[2];
  149. } else if ( match[3] ) {
  150. named[ match[3].toLowerCase() ] = match[4];
  151. } else if ( match[5] ) {
  152. named[ match[5].toLowerCase() ] = match[6];
  153. } else if ( match[7] ) {
  154. numeric.push( match[7] );
  155. } else if ( match[8] ) {
  156. numeric.push( match[8] );
  157. } else if ( match[9] ) {
  158. numeric.push( match[9] );
  159. }
  160. }
  161. return {
  162. named: named,
  163. numeric: numeric
  164. };
  165. }),
  166. /*
  167. * ### Generate a Shortcode Object from a RegExp match.
  168. *
  169. * Accepts a `match` object from calling `regexp.exec()` on a `RegExp`
  170. * generated by `wp.shortcode.regexp()`. `match` can also be set
  171. * to the `arguments` from a callback passed to `regexp.replace()`.
  172. */
  173. fromMatch: function( match ) {
  174. var type;
  175. if ( match[4] ) {
  176. type = 'self-closing';
  177. } else if ( match[6] ) {
  178. type = 'closed';
  179. } else {
  180. type = 'single';
  181. }
  182. return new wp.shortcode({
  183. tag: match[2],
  184. attrs: match[3],
  185. type: type,
  186. content: match[5]
  187. });
  188. }
  189. };
  190. /*
  191. * Shortcode Objects
  192. * -----------------
  193. *
  194. * Shortcode objects are generated automatically when using the main
  195. * `wp.shortcode` methods: `next()`, `replace()`, and `string()`.
  196. *
  197. * To access a raw representation of a shortcode, pass an `options` object,
  198. * containing a `tag` string, a string or object of `attrs`, a string
  199. * indicating the `type` of the shortcode ('single', 'self-closing',
  200. * or 'closed'), and a `content` string.
  201. */
  202. wp.shortcode = _.extend( function( options ) {
  203. _.extend( this, _.pick( options || {}, 'tag', 'attrs', 'type', 'content' ) );
  204. var attrs = this.attrs;
  205. // Ensure we have a correctly formatted `attrs` object.
  206. this.attrs = {
  207. named: {},
  208. numeric: []
  209. };
  210. if ( ! attrs ) {
  211. return;
  212. }
  213. // Parse a string of attributes.
  214. if ( _.isString( attrs ) ) {
  215. this.attrs = wp.shortcode.attrs( attrs );
  216. // Identify a correctly formatted `attrs` object.
  217. } else if ( _.difference( _.keys( attrs ), [ 'named', 'numeric' ] ).length === 0 ) {
  218. this.attrs = _.defaults( attrs, this.attrs );
  219. // Handle a flat object of attributes.
  220. } else {
  221. _.each( options.attrs, function( value, key ) {
  222. this.set( key, value );
  223. }, this );
  224. }
  225. }, wp.shortcode );
  226. _.extend( wp.shortcode.prototype, {
  227. /*
  228. * ### Get a shortcode attribute.
  229. *
  230. * Automatically detects whether `attr` is named or numeric and routes
  231. * it accordingly.
  232. */
  233. get: function( attr ) {
  234. return this.attrs[ _.isNumber( attr ) ? 'numeric' : 'named' ][ attr ];
  235. },
  236. /*
  237. * ### Set a shortcode attribute.
  238. *
  239. * Automatically detects whether `attr` is named or numeric and routes
  240. * it accordingly.
  241. */
  242. set: function( attr, value ) {
  243. this.attrs[ _.isNumber( attr ) ? 'numeric' : 'named' ][ attr ] = value;
  244. return this;
  245. },
  246. // ### Transform the shortcode match into a string.
  247. string: function() {
  248. var text = '[' + this.tag;
  249. _.each( this.attrs.numeric, function( value ) {
  250. if ( /\s/.test( value ) ) {
  251. text += ' "' + value + '"';
  252. } else {
  253. text += ' ' + value;
  254. }
  255. });
  256. _.each( this.attrs.named, function( value, name ) {
  257. text += ' ' + name + '="' + value + '"';
  258. });
  259. // If the tag is marked as `single` or `self-closing`, close the
  260. // tag and ignore any additional content.
  261. if ( 'single' === this.type ) {
  262. return text + ']';
  263. } else if ( 'self-closing' === this.type ) {
  264. return text + ' /]';
  265. }
  266. // Complete the opening tag.
  267. text += ']';
  268. if ( this.content ) {
  269. text += this.content;
  270. }
  271. // Add the closing tag.
  272. return text + '[/' + this.tag + ']';
  273. }
  274. });
  275. }());
  276. /*
  277. * HTML utility functions
  278. * ----------------------
  279. *
  280. * Experimental. These functions may change or be removed in the future.
  281. */
  282. (function(){
  283. wp.html = _.extend( wp.html || {}, {
  284. /*
  285. * ### Parse HTML attributes.
  286. *
  287. * Converts `content` to a set of parsed HTML attributes.
  288. * Utilizes `wp.shortcode.attrs( content )`, which is a valid superset of
  289. * the HTML attribute specification. Reformats the attributes into an
  290. * object that contains the `attrs` with `key:value` mapping, and a record
  291. * of the attributes that were entered using `empty` attribute syntax (i.e.
  292. * with no value).
  293. */
  294. attrs: function( content ) {
  295. var result, attrs;
  296. // If `content` ends in a slash, strip it.
  297. if ( '/' === content[ content.length - 1 ] ) {
  298. content = content.slice( 0, -1 );
  299. }
  300. result = wp.shortcode.attrs( content );
  301. attrs = result.named;
  302. _.each( result.numeric, function( key ) {
  303. if ( /\s/.test( key ) ) {
  304. return;
  305. }
  306. attrs[ key ] = '';
  307. });
  308. return attrs;
  309. },
  310. // ### Convert an HTML-representation of an object to a string.
  311. string: function( options ) {
  312. var text = '<' + options.tag,
  313. content = options.content || '';
  314. _.each( options.attrs, function( value, attr ) {
  315. text += ' ' + attr;
  316. // Convert boolean values to strings.
  317. if ( _.isBoolean( value ) ) {
  318. value = value ? 'true' : 'false';
  319. }
  320. text += '="' + value + '"';
  321. });
  322. // Return the result if it is a self-closing tag.
  323. if ( options.single ) {
  324. return text + ' />';
  325. }
  326. // Complete the opening tag.
  327. text += '>';
  328. // If `content` is an object, recursively call this function.
  329. text += _.isObject( content ) ? wp.html.string( content ) : content;
  330. return text + '</' + options.tag + '>';
  331. }
  332. });
  333. }());