shortcode.js 10 KB

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