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