2 Copyright (c) 2010, Yahoo! Inc. All rights reserved.
3 Code licensed under the BSD License:
4 http://developer.yahoo.com/yui/license.html
8 YUI.add('highlight-base', function(Y) {
11 * Provides methods for highlighting strings within other strings by wrapping
20 * Provides methods for highlighting strings within other strings by wrapping
25 * The highlight methods first escape any special HTML characters in the input
26 * strings and then highlight the appropriate substrings by wrapping them in a
27 * <code><b class="yui3-highlight"></b></code> element. The
28 * <code><b></code> element is used rather than
29 * <code><strong></code> in accordance with HTML5's definition of
30 * <code><b></code> as being purely presentational, which is exactly what
35 * @submodule highlight-base
42 WordBreak = Y.Text.WordBreak,
44 isArray = Y.Lang.isArray,
48 // Regex string that captures zero or one unclosed HTML entities. Used in
49 // the static regex template properties below. The entity matching is
50 // intentionally loose here, since there's a world of complexity involved in
51 // doing strict matching for this use case.
52 UNCLOSED_ENTITY = '(&[^;\\s]*)?',
55 // -- Protected Static Properties ------------------------------------------
59 * Regular expression template for highlighting a match that occurs anywhere
60 * in a string. The placeholder <code>%needles</code> will be replaced with
61 * a list of needles to match, joined by <code>|</code> characters.
65 * This regex should have two capturing subpatterns: the first should match
66 * an unclosed HTML entity (e.g. "&" without a ";" at the end) 0 or 1
67 * times; the second should contain the <code>%needles</code> placeholder.
68 * The first subpattern match is used to emulate a negative lookbehind
69 * assertion, in order to prevent highlighting inside HTML entities.
78 _REGEX: UNCLOSED_ENTITY + '(%needles)',
81 * Regex replacer function or string for normal matches.
84 * @type {Function|String}
89 _REPLACER: function (match, p1, p2) {
90 // Mimicking a negative lookbehind assertion to prevent matches inside
91 // HTML entities. Hat tip to Steven Levithan for the technique:
92 // http://blog.stevenlevithan.com/archives/mimic-lookbehind-javascript
93 return p1 && !(/\s/).test(p2) ? match :
94 Highlight._TEMPLATE.replace(/\{s\}/g, p2);
99 * Regular expression template for highlighting start-of-string matches
100 * (i.e., only matches that occur at the beginning of a string). The
101 * placeholder <code>%needles</code> will be replaced with a list of needles
102 * to match, joined by <code>|</code> characters.
106 * See <code>_REGEX</code> for a description of the capturing subpatterns
107 * this regex should contain.
110 * @property _START_REGEX
116 _START_REGEX: '^' + UNCLOSED_ENTITY + '(%needles)',
119 * Highlight template which will be used as a replacement for matched
120 * substrings. The placeholder <code>{s}</code> will be replaced with the
123 * @property _TEMPLATE
125 * @default '<b class="yui3-highlight">{s}</b>'
130 _TEMPLATE: '<b class="yui3-highlight">{s}</b>',
132 // -- Public Static Methods ------------------------------------------------
135 * Highlights all occurrences in the <em>haystack</em> string of the items
136 * in the <em>needles</em> array, regardless of where they occur. The
137 * returned string will have all HTML characters escaped except for the
138 * highlighting markup.
141 * @param {String} haystack String to apply highlighting to.
142 * @param {String|Array} needles String or array of strings that should be
144 * @param {Object} options (optional) Options object, which may contain
145 * zero or more of the following properties:
148 * <dt>caseSensitive (Boolean)</dt>
150 * If <code>true</code>, matching will be case-sensitive. Default is
151 * <code>false</code>.
154 * <dt>startsWith (Boolean)<dt>
156 * By default, needles are highlighted wherever they appear in the
157 * haystack. If <code>startsWith</code> is <code>true</code>, matches
158 * must be anchored to the beginning of the string.
162 * @return {String} Escaped and highlighted copy of <em>haystack</em>.
165 all: function (haystack, needles, options) {
166 var i, len, regex, replacer;
169 options = EMPTY_OBJECT;
172 // TODO: document options.replacer
173 regex = options.startsWith ? Highlight._START_REGEX : Highlight._REGEX;
174 replacer = options.replacer || Highlight._REPLACER;
176 // Create a local copy of needles so we can safely modify it in the next
178 needles = isArray(needles) ? needles.concat() : [needles];
180 // Escape HTML characters and special regular expression characters in
181 // the needles so they can be used in a regex and matched against the
183 for (i = 0, len = needles.length; i < len; ++i) {
184 needles[i] = Escape.regex(Escape.html(needles[i]));
187 // Escape HTML characters in the haystack to prevent HTML injection.
188 haystack = Escape.html(haystack);
190 return haystack.replace(
192 regex.replace('%needles', needles.join('|')),
193 options.caseSensitive ? 'g' : 'gi'
200 * Same as <code>all()</code>, but case-sensitive by default.
203 * @param {String} haystack String to apply highlighting to.
204 * @param {String|Array} needles String or array of strings that should be
206 * @param {Object} options (optional) Options object. See <code>all()</code>
208 * @return {String} Escaped and highlighted copy of <em>haystack</em>.
211 allCase: function (haystack, needles, options) {
212 return Highlight.all(haystack, needles,
213 Y.merge(options || EMPTY_OBJECT, {caseSensitive: true}));
217 * Highlights <em>needles</em> that occur at the start of <em>haystack</em>.
218 * The returned string will have all HTML characters escaped except for the
219 * highlighting markup.
222 * @param {String} haystack String to apply highlighting to.
223 * @param {String|Array} needles String or array of strings that should be
225 * @param {Object} options (optional) Options object, which may contain
226 * zero or more of the following properties:
229 * <dt>caseSensitive (Boolean)</dt>
231 * If <code>true</code>, matching will be case-sensitive. Default is
232 * <code>false</code>.
236 * @return {String} Escaped and highlighted copy of <em>haystack</em>.
239 start: function (haystack, needles, options) {
240 return Highlight.all(haystack, needles,
241 Y.merge(options || EMPTY_OBJECT, {startsWith: true}));
245 * Same as <code>start()</code>, but case-sensitive by default.
248 * @param {String} haystack String to apply highlighting to.
249 * @param {String|Array} needles String or array of strings that should be
251 * @return {String} Escaped and highlighted copy of <em>haystack</em>.
254 startCase: function (haystack, needles) {
255 // No options passthru for now, since it would be redundant. If start()
256 // ever supports more options than caseSensitive, then we'll start
257 // passing the options through.
258 return Highlight.start(haystack, needles, {caseSensitive: true});
262 * Highlights complete words in the <em>haystack</em> string that are also
263 * in the <em>needles</em> array. The returned string will have all HTML
264 * characters escaped except for the highlighting markup.
267 * @param {String} haystack String to apply highlighting to.
268 * @param {String|Array} needles String or array of strings containing words
269 * that should be highlighted. If a string is passed, it will be split
270 * into words; if an array is passed, it is assumed to have already been
272 * @param {Object} options (optional) Options object, which may contain
273 * zero or more of the following properties:
276 * <dt>caseSensitive (Boolean)</dt>
278 * If <code>true</code>, matching will be case-sensitive. Default is
279 * <code>false</code>.
283 * @return {String} Escaped and highlighted copy of <em>haystack</em>.
286 words: function (haystack, needles, options) {
289 template = Highlight._TEMPLATE,
293 options = EMPTY_OBJECT;
296 caseSensitive = !!options.caseSensitive;
298 // Convert needles to a hash for faster lookups.
299 needles = YArray.hash(
300 isArray(needles) ? needles : WordBreak.getUniqueWords(needles, {
301 ignoreCase: !caseSensitive
305 // The default word mapping function can be overridden with a custom
306 // one. This is used to implement accent-folded highlighting in the
307 // highlight-accentfold module.
308 mapper = options.mapper || function (word, needles) {
309 if (needles.hasOwnProperty(caseSensitive ? word : word.toLowerCase())) {
310 return template.replace(/\{s\}/g, Escape.html(word));
313 return Escape.html(word);
316 // Split the haystack into an array of words, including punctuation and
317 // whitespace so we can rebuild the string later.
318 words = WordBreak.getWords(haystack, {
319 includePunctuation: true,
320 includeWhitespace : true
323 return YArray.map(words, function (word) {
324 return mapper(word, needles);
329 * Same as <code>words()</code>, but case-sensitive by default.
332 * @param {String} haystack String to apply highlighting to.
333 * @param {String|Array} needles String or array of strings containing words
334 * that should be highlighted. If a string is passed, it will be split
335 * into words; if an array is passed, it is assumed to have already been
337 * @return {String} Escaped and highlighted copy of <em>haystack</em>.
340 wordsCase: function (haystack, needles) {
341 // No options passthru for now, since it would be redundant. If words()
342 // ever supports more options than caseSensitive, then we'll start
343 // passing the options through.
344 return Highlight.words(haystack, needles, {caseSensitive: true});
348 Y.Highlight = Highlight;
351 }, '3.3.0' ,{requires:['array-extras', 'escape', 'text-wordbreak']});
352 YUI.add('highlight-accentfold', function(Y) {
355 * Adds accent-folding highlighters to <code>Y.Highlight</code>.
358 * @submodule highlight-accentfold
366 var AccentFold = Y.Text.AccentFold,
371 Highlight = Y.mix(Y.Highlight, {
372 // -- Public Static Methods ------------------------------------------------
375 * Accent-folding version of <code>all()</code>.
378 * @param {String} haystack String to apply highlighting to.
379 * @param {String|Array} needles String or array of strings that should be
381 * @param {Object} options (optional) Options object, which may contain
382 * zero or more of the following properties:
385 * <dt>startsWith (Boolean)<dt>
387 * By default, needles are highlighted wherever they appear in the
388 * haystack. If <code>startsWith</code> is <code>true</code>, matches
389 * must be anchored to the beginning of the string.
393 * @return {String} Escaped and highlighted copy of <em>haystack</em>.
396 allFold: function (haystack, needles, options) {
397 var template = Highlight._TEMPLATE,
402 // While the highlight regex operates on the accent-folded strings,
403 // this replacer will highlight the matched positions in the
406 // Note: this implementation doesn't handle multi-character folds,
407 // like "æ" -> "ae". Doing so correctly would be prohibitively
408 // expensive both in terms of code size and runtime performance, so
409 // I've chosen to take the pragmatic route and just not do it at
410 // all. This is one of many reasons why accent folding is best done
412 replacer: function (match, p1, foldedNeedle, pos) {
415 // Ignore matches inside HTML entities.
416 if (p1 && !(/\s/).test(foldedNeedle)) {
420 len = foldedNeedle.length;
422 result.push(haystack.substring(startPos, pos) +
423 template.replace(/\{s\}/g, haystack.substr(pos, len)));
425 startPos = pos + len;
427 }, options || EMPTY_OBJECT);
429 // Run the highlighter on the folded strings. We don't care about the
430 // output; our replacer function will build the canonical highlighted
431 // string, with original accented characters.
432 Highlight.all(AccentFold.fold(haystack), AccentFold.fold(needles),
435 // Tack on the remainder of the haystack that wasn't highlighted, if
437 if (startPos < haystack.length - 1) {
438 result.push(haystack.substr(startPos));
441 return result.join('');
445 * Accent-folding version of <code>start()</code>.
448 * @param {String} haystack String to apply highlighting to.
449 * @param {String|Array} needles String or array of strings that should be
451 * @return {String} Escaped and highlighted copy of <em>haystack</em>.
454 startFold: function (haystack, needles) {
455 return Highlight.allFold(haystack, needles, {startsWith: true});
459 * Accent-folding version of <code>words()</code>.
462 * @param {String} haystack String to apply highlighting to.
463 * @param {String|Array} needles String or array of strings containing words
464 * that should be highlighted. If a string is passed, it will be split
465 * into words; if an array is passed, it is assumed to have already been
467 * @return {String} Escaped and highlighted copy of <em>haystack</em>.
470 wordsFold: function (haystack, needles) {
471 var template = Highlight._TEMPLATE;
473 return Highlight.words(haystack, AccentFold.fold(needles), {
474 mapper: function (word, needles) {
475 if (needles.hasOwnProperty(AccentFold.fold(word))) {
476 return template.replace(/\{s\}/g, Escape.html(word));
479 return Escape.html(word);
486 }, '3.3.0' ,{requires:['highlight-base', 'text-accentfold']});
489 YUI.add('highlight', function(Y){}, '3.3.0' ,{use:['highlight-base', 'highlight-accentfold']});