vendor: 24,554 bytes, lines 1-479
1/**************** 2 Blast.js 3****************/ 4 5/*! Blast.js (2.0.0): julian.com/research/blast (C) 2015 Julian Shapiro. MIT @license: en.wikipedia.org/wiki/MIT_License */ 6 7;(function ($, window, document, undefined) { 8 9 /********************* 10 Helper Functions 11 *********************/ 12 13 /* IE detection. Gist: https://gist.github.com/julianshapiro/9098609 */ 14 var IE = (function () { 15 if (document.documentMode) { 16 return document.documentMode; 17 } else { 18 for (var i = 7; i > 0; i--) { 19 var div = document.createElement("div"); 20 21 div.innerHTML = "<!--[if IE " + i + "]><span></span><![endif]-->"; 22 23 if (div.getElementsByTagName("span").length) { 24 div = null; 25 26 return i; 27 } 28 29 div = null; 30 } 31 } 32 33 return undefined; 34 })(); 35 36 /* Shim to prevent console.log() from throwing errors on IE<=7. */ 37 var console = window.console || { log: function () {}, time: function () {} }; 38 39 /***************** 40 Constants 41 *****************/ 42 43 var NAME = "blast", 44 characterRanges = { 45 latinPunctuation: "âââ²â'ââ³â\"(«.â¦Â¡Â¿â²â'ââ³â\")».â¦!?", 46 latinLetters: "\\u0041-\\u005A\\u0061-\\u007A\\u00C0-\\u017F\\u0100-\\u01FF\\u0180-\\u027F" 47 }, 48 Reg = { 49 /* If the abbreviations RegEx is missing a title abbreviation that you find yourself needing to often escape manually, tweet me: @Shapiro. */ 50 abbreviations: new RegExp("[^" + characterRanges.latinLetters + "](e\\.g\\.)|(i\\.e\\.)|(mr\\.)|(mrs\\.)|(ms\\.)|(dr\\.)|(prof\\.)|(esq\\.)|(sr\\.)|(jr\\.)[^" + characterRanges.latinLetters + "]", "ig"), 51 innerWordPeriod: new RegExp("[" + characterRanges.latinLetters + "]\.[" + characterRanges.latinLetters + "]", "ig"), 52 onlyContainsPunctuation: new RegExp("[^" + characterRanges.latinPunctuation + "]"), 53 adjoinedPunctuation: new RegExp("^[" + characterRanges.latinPunctuation + "]+|[" + characterRanges.latinPunctuation + "]+$", "g"), 54 skippedElements: /(script|style|select|textarea)/i, 55 hasPluginClass: new RegExp("(^| )" + NAME + "( |$)", "gi") 56 }; 57 58 /**************** 59 $.fn.blast 60 ****************/ 61 62 $.fn[NAME] = function (options) { 63 64 /************************* 65 Punctuation Escaping 66 *************************/ 67 68 /* Escape likely false-positives of sentence-final periods. Escaping is performed by wrapping a character's ASCII equivalent in double curly brackets, 69 which is then reversed (deencodcoded) after delimiting. */ 70 function encodePunctuation (text) { 71 return text 72 /* Escape the following Latin abbreviations and English titles: e.g., i.e., Mr., Mrs., Ms., Dr., Prof., Esq., Sr., and Jr. */ 73 .replace(Reg.abbreviations, function(match) { 74 return match.replace(/\./g, "{{46}}"); 75 }) 76 /* Escape inner-word (non-space-delimited) periods. For example, the period inside "Blast.js". */ 77 .replace(Reg.innerWordPeriod, function(match) { 78 return match.replace(/\./g, "{{46}}"); 79 }); 80 } 81 82 /* Used to decode both the output of encodePunctuation() and punctuation that has been manually escaped by users. */ 83 function decodePunctuation (text) { 84 return text.replace(/{{(\d{1,3})}}/g, function(fullMatch, subMatch) { 85 return String.fromCharCode(subMatch); 86 }); 87 } 88 89 /****************** 90 DOM Traversal 91 ******************/ 92 93 function wrapNode (node, opts) { 94 var wrapper = document.createElement(opts.tag); 95 96 /* Assign the element a class of "blast". */ 97 wrapper.className = NAME; 98 99 /* If a custom class was provided, assign that too. */ 100 if (opts.customClass) { 101 wrapper.className += " " + opts.customClass; 102 103 /* If an opts.customClass is provided, generate an ID consisting of customClass and a number indicating the match's iteration. */ 104 if (opts.generateIndexID) { 105 wrapper.id = opts.customClass + "-" + Element.blastedIndex; 106 } 107 } 108 109 /* For the "all" delimiter, prevent space characters from collapsing. */ 110 if (opts.delimiter === "all" && /\s/.test(node.data)) { 111 wrapper.style.whiteSpace = "pre-line"; 112 } 113 114 /* Assign the element a class equal to its escaped inner text. Only applicable to the character and word delimiters (since they do not contain spaces). */ 115 if (opts.generateValueClass === true && !opts.search && (opts.delimiter === "character" || opts.delimiter === "word")) { 116 var valueClass, 117 text = node.data; 118 119 /* For the word delimiter, remove adjoined punctuation, which is unlikely to be desired as part of the match -- unless the text 120 consists solely of punctuation (e.g. "!!!"), in which case we leave the text as-is. */ 121 if (opts.delimiter === "word" && Reg.onlyContainsPunctuation.test(text)) { 122 /* E: Remove punctuation that's adjoined to either side of the word match. */ 123 text = text.replace(Reg.adjoinedPunctuation, ""); 124 } 125 126 valueClass = NAME + "-" + opts.delimiter.toLowerCase() + "-" + text.toLowerCase(); 127 128 wrapper.className += " " + valueClass; 129 } 130 131 /* Hide the wrapper elements from screenreaders now that we've set the target's aria-label attribute. */ 132 if (opts.aria) { 133 wrapper.setAttribute("aria-hidden", "true"); 134 } 135 136 wrapper.appendChild(node.cloneNode(false)); 137 138 return wrapper; 139 } 140 141 function traverseDOM (node, opts) { 142 var matchPosition = -1, 143 skipNodeBit = 0; 144 145 /* Only proceed if the node is a text node and isn't empty. */ 146 if (node.nodeType === 3 && node.data.length) { 147 /* Perform punctuation encoding/decoding once per original whole text node (before it gets split up into bits). */ 148 if (Element.nodeBeginning) { 149 /* For the sentence delimiter, we first escape likely false-positive sentence-final punctuation. For all other delimiters, 150 we must decode the user's manually-escaped punctuation so that the RegEx can match correctly (without being thrown off by characters in {{ASCII}}). */ 151 node.data = (!opts.search && opts.delimiter === "sentence") ? encodePunctuation(node.data) : decodePunctuation(node.data); 152 153 Element.nodeBeginning = false; 154 } 155 156 matchPosition = node.data.search(delimiterRegex); 157 158 /* If there's a RegEx match in this text node, proceed with element wrapping. */ 159 if (matchPosition !== -1) { 160 var match = node.data.match(delimiterRegex), 161 matchText = match[0], 162 subMatchText = match[1] || false; 163 164 /* RegEx queries that can return empty strings (e.g ".*") produce an empty matchText which throws the entire traversal process into an infinite loop due to the position index not incrementing. 165 Thus, we bump up the position index manually, resulting in a zero-width split at this location followed by the continuation of the traversal process. */ 166 if (matchText === "") { 167 matchPosition++; 168 /* If a RegEx submatch is produced that is not identical to the full string match, use the submatch's index position and text. 169 This technique allows us to avoid writing multi-part RegEx queries for submatch finding. */ 170 } else if (subMatchText && subMatchText !== matchText) { 171 matchPosition += matchText.indexOf(subMatchText); 172 matchText = subMatchText; 173 } 174 175 /* Split this text node into two separate nodes at the position of the match, returning the node that begins after the match position. */ 176 var middleBit = node.splitText(matchPosition); 177 178 /* Split the newly-produced text node at the end of the match's text so that middleBit is a text node that consists solely of the matched text. The other newly-created text node, which begins 179 at the end of the match's text, is what will be traversed in the subsequent loop (in order to find additional matches in the containing text node). */ 180 middleBit.splitText(matchText.length); 181 182 /* Over-increment the loop counter (see below) so that we skip the extra node (middleBit) that we've just created (and already processed). */ 183 skipNodeBit = 1; 184 185 if (!opts.search && opts.delimiter === "sentence") { 186 /* Now that we've forcefully escaped all likely false-positive sentence-final punctuation, we must decode the punctuation back from ASCII. */ 187 middleBit.data = decodePunctuation(middleBit.data); 188 } 189 190 /* Create the wrapped node. */ 191 var wrappedNode = wrapNode(middleBit, opts, Element.blastedIndex); 192 /* Then replace the middleBit text node with its wrapped version. */ 193 middleBit.parentNode.replaceChild(wrappedNode, middleBit); 194 195 /* Push the wrapper onto the Element.wrappers array (for later use with stack manipulation). */ 196 Element.wrappers.push(wrappedNode); 197 198 Element.blastedIndex++; 199 200 /* Note: We use this slow splice-then-iterate method because every match needs to be converted into an HTML element node. A text node's text cannot have HTML elements inserted into it. */ 201 /* TODO: To improve performance, use documentFragments to delay node manipulation so that DOM queries and updates can be batched across elements. */ 202 } 203 /* Traverse the DOM tree until we find text nodes. Skip script and style elements. Skip select and textarea elements since they contain special text nodes that users would not want wrapped. 204 Additionally, check for the existence of our plugin's class to ensure that we do not retraverse elements that have already been blasted. */ 205 /* Note: This basic DOM traversal technique is copyright Johann Burkard <http://johannburkard.de>. Licensed under the MIT License: http://en.wikipedia.org/wiki/MIT_License */ 206 } else if (node.nodeType === 1 && node.hasChildNodes() && !Reg.skippedElements.test(node.tagName) && !Reg.hasPluginClass.test(node.className)) { 207 /* Note: We don't cache childNodes' length since it's a live nodeList (which changes dynamically with the use of splitText() above). */ 208 for (var i = 0; i < node.childNodes.length; i++) { 209 Element.nodeBeginning = true; 210 211 i += traverseDOM(node.childNodes[i], opts); 212 } 213 } 214 215 return skipNodeBit; 216 } 217 218 /******************* 219 Call Variables 220 *******************/ 221 222 var opts = $.extend({}, $.fn[NAME].defaults, options), 223 delimiterRegex, 224 /* Container for variables specific to each element targeted by the Blast call. */ 225 Element = {}; 226 227 /*********************** 228 Delimiter Creation 229 ***********************/ 230 231 /* Ensure that the opts.delimiter search variable is a non-empty string. */ 232 if (opts.search.length && (typeof opts.search === "string" || /^\d/.test(parseFloat(opts.search)))) { 233 /* Since the search is performed as a Regex (see below), we escape the string's Regex meta-characters. */ 234 opts.delimiter = opts.search.toString().replace(/[-[\]{,}(.)*+?|^$\\\/]/g, "\\$&"); 235 236 /* Note: This matches the apostrophe+s of the phrase's possessive form: {PHRASE's}. */ 237 /* Note: This will not match text that is part of a compound word (two words adjoined with a dash), e.g. "front" won't match inside "front-end". */ 238 /* Note: Based on the way the search algorithm is implemented, it is not possible to search for a string that consists solely of punctuation characters. */ 239 /* Note: By creating boundaries at Latin alphabet ranges instead of merely spaces, we effectively match phrases that are inlined alongside any type of non-Latin-letter, 240 e.g. word|, word!, â¥word⥠will all match. */ 241 delimiterRegex = new RegExp("(?:^|[^-" + characterRanges.latinLetters + "])(" + opts.delimiter + "('s)?)(?![-" + characterRanges.latinLetters + "])", "i"); 242 } else { 243 /* Normalize the string's case for the delimiter switch check below. */ 244 if (typeof opts.delimiter === "string") { 245 opts.delimiter = opts.delimiter.toLowerCase(); 246 } 247 248 switch (opts.delimiter) { 249 case "all": 250 /* Matches every character then later sets spaces to "white-space: pre-line" so they don't collapse. */ 251 delimiterRegex = /(.)/; 252 break; 253 254 case "letter": 255 case "char": 256 case "character": 257 /* Matches every non-space character. */ 258 /* Note: This is the slowest delimiter. However, its slowness is only noticeable when it's used on larger bodies of text (of over 500 characters) on <=IE8. 259 (Run Blast with opts.debug=true to monitor execution times.) */ 260 delimiterRegex = /(\S)/; 261 break; 262 263 case "word": 264 /* Matches strings in between space characters. */ 265 /* Note: Matches will include any punctuation that's adjoined to the word, e.g. "Hey!" will be a full match. */ 266 /* Note: Remember that, with Blast, every HTML element marks the start of a brand new string. Hence, "in<b>si</b>de" matches as three separate words. */ 267 delimiterRegex = /\s*(\S+)\s*/; 268 break; 269 270 case "sentence": 271 /* Matches phrases either ending in Latin alphabet punctuation or located at the end of the text. (Linebreaks are not considered punctuation.) */ 272 /* Note: If you don't want punctuation to demarcate a sentence match, replace the punctuation character with {{ASCII_CODE_FOR_DESIRED_PUNCTUATION}}. ASCII codes: .={{46}}, ?={{63}}, !={{33}} */ 273 delimiterRegex = /(?=\S)(([.]{2,})?[^!?]+?([.â¦!?]+|(?=\s+$)|$)(\s*[â²â'ââ³â")»]+)*)/; 274 /* RegExp explanation (Tip: Use Regex101.com to play around with this expression and see which strings it matches): 275 - Expanded view: /(?=\S) ( ([.]{2,})? [^!?]+? ([.â¦!?]+|(?=\s+$)|$) (\s*[â²â'ââ³â")»]+)* ) 276 - (?=\S) --> Match must contain a non-space character. 277 - ([.]{2,})? --> Match may begin with a group of periods. 278 - [^!?]+? --> Grab everything that isn't an unequivocally-terminating punctuation character, but stop at the following condition... 279 - ([.â¦!?]+|(?=\s+$)|$) --> Match the last occurrence of sentence-final punctuation or the end of the text (optionally with left-side trailing spaces). 280 - (\s*[â²â'ââ³â")»]+)* --> After the final punctuation, match any and all pairs of (optionally space-delimited) quotes and parentheses. 281 */ 282 break; 283 284 case "element": 285 /* Matches text between HTML tags. */ 286 /* Note: Wrapping always occurs inside of elements, i.e. <b><span class="blast">Bolded text here</span></b>. */ 287 delimiterRegex = /(?=\S)([\S\s]*\S)/; 288 break; 289 290 /***************** 291 Custom Regex 292 *****************/ 293 294 default: 295 /* You can pass in /your-own-regex/. */ 296 if (opts.delimiter instanceof RegExp) { 297 delimiterRegex = opts.delimiter; 298 } else { 299 console.log(NAME + ": Unrecognized delimiter, empty search string, or invalid custom Regex. Aborting."); 300 301 /* Abort this Blast call. */ 302 return true; 303 } 304 } 305 } 306 307 /********************** 308 Element Iteration 309 **********************/ 310 311 this.each(function() { 312 var $this = $(this), 313 text = $this.text(); 314 315 /* When anything except false is passed in for the options object, Blast is initiated. */ 316 if (options !== false) { 317 318 /********************** 319 Element Variables 320 **********************/ 321 322 Element = { 323 /* The index of each wrapper element generated by blasting. */ 324 blastedIndex: 0, 325 /* Whether we're just entering this node. */ 326 nodeBeginning: false, 327 /* Keep track of the elements generated by Blast so that they can (optionally) be pushed onto the jQuery call stack. */ 328 wrappers: Element.wrappers || [] 329 }; 330 331 /***************** 332 Housekeeping 333 *****************/ 334 335 /* Unless a consecutive opts.search is being performed, an element's existing Blast call is reversed before proceeding. */ 336 if ($this.data(NAME) !== undefined && ($this.data(NAME) !== "search" || opts.search === false)) { 337 reverse($this, opts); 338 339 if (opts.debug) console.log(NAME + ": Removed element's existing Blast call."); 340 } 341 342 /* Store the current delimiter type so that it can be compared against on subsequent calls (see above). */ 343 $this.data(NAME, opts.search !== false ? "search" : opts.delimiter); 344 345 if (opts.aria) { 346 $this.attr("aria-label", text); 347 } 348 349 /**************** 350 Preparation 351 ****************/ 352 353 /* Perform optional HTML tag stripping. */ 354 if (opts.stripHTMLTags) { 355 $this.html(text); 356 } 357 358 /* If the browser throws an error for the provided element type (browers whitelist the letters and types of the elements they accept), fall back to using "span". */ 359 try { 360 document.createElement(opts.tag); 361 } catch (error) { 362 opts.tag = "span"; 363 364 if (opts.debug) console.log(NAME + ": Invalid tag supplied. Defaulting to span."); 365 } 366 367 /* For reference purposes when reversing Blast, assign the target element a root class. */ 368 $this.addClass(NAME + "-root"); 369 370 /* Initiate the DOM traversal process. */ 371 if (opts.debug) console.time(NAME); 372 traverseDOM(this, opts); 373 if (opts.debug) console.timeEnd(NAME); 374 375 /* If false is passed in as the first parameter, reverse Blast. */ 376 } else if (options === false && $this.data(NAME) !== undefined) { 377 reverse($this, opts); 378 } 379 380 /************** 381 Debugging 382 **************/ 383 384 /* Output the full string of each wrapper element and color alternate the wrappers. This is in addition to the performance timing that has already been outputted. */ 385 if (opts.debug) { 386 $.each(Element.wrappers, function(index, element) { 387 console.log(NAME + " [" + opts.delimiter + "] " + this.outerHTML); 388 this.style.backgroundColor = index % 2 ? "#f12185" : "#075d9a"; 389 }); 390 } 391 }); 392 393 /************ 394 Reverse 395 ************/ 396 397 function reverse ($this, opts) { 398 if (opts.debug) console.time("blast reversal"); 399 400 var skippedDescendantRoot = false; 401 402 $this 403 .removeClass(NAME + "-root") 404 .removeAttr("aria-label") 405 .find("." + NAME) 406 .each(function () { 407 var $this = $(this); 408 /* Do not reverse Blast on descendant root elements. (Before you can reverse Blast on an element, you must reverse Blast on any parent elements that have been Blasted.) */ 409 if (!$this.closest("." + NAME + "-root").length) { 410 var thisParentNode = this.parentNode; 411 412 /* This triggers some sort of node layout, thereby solving a node normalization bug in <=IE7 for reasons unknown. If you know the specific reason, tweet me: @Shapiro. */ 413 if (IE <= 7) (thisParentNode.firstChild.nodeName); 414 415 /* Strip the HTML tags off of the wrapper elements by replacing the elements with their child node's text. */ 416 thisParentNode.replaceChild(this.firstChild, this); 417 418 /* Normalize() parents to remove empty text nodes and concatenate sibling text nodes. (This cleans up the DOM after our manipulation.) */ 419 thisParentNode.normalize(); 420 } else { 421 skippedDescendantRoot = true; 422 } 423 }); 424 425 /* Zepto core doesn't include cache-based $.data(), so we mimic data-attr removal by setting it to undefined. */ 426 if (window.Zepto) { 427 $this.data(NAME, undefined); 428 } else { 429 $this.removeData(NAME); 430 } 431 432 if (opts.debug) { 433 console.log(NAME + ": Reversed Blast" + ($this.attr("id") ? " on #" + $this.attr("id") + "." : ".") + (skippedDescendantRoot ? " Skipped reversal on the children of one or more descendant root elements." : "")); 434 console.timeEnd("blast reversal"); 435 } 436 } 437 438 /************* 439 Chain 440 *************/ 441 442 /* Either return a stack composed of our call's Element.wrappers or return the element(s) originally targeted by the Blast call. */ 443 /* Note: returnGenerated can only be disabled on a per-call basis (not a per-element basis). */ 444 if (options !== false && opts.returnGenerated === true) { 445 /* A reimplementation of jQuery's $.pushStack() (since Zepto does not provide this function). */ 446 var newStack = $().add(Element.wrappers); 447 newStack.prevObject = this; 448 newStack.context = this.context; 449 450 return newStack; 451 } else { 452 return this; 453 } 454 }; 455 456 /*************** 457 Defaults 458 ***************/ 459 460 $.fn.blast.defaults = { 461 returnGenerated: true, 462 delimiter: "word", 463 tag: "span", 464 search: false, 465 customClass: "", 466 generateIndexID: false, 467 generateValueClass: false, 468 stripHTMLTags: false, 469 aria: true, 470 debug: false 471 }; 472})(window.jQuery || window.Zepto, window, document); 473 474/***************** 475 Known Issues 476*****************/ 477 478/* In <=IE7, when Blast is called on the same element more than once with opts.stripHTMLTags=false, calls after the first may not target the entirety of the element and/or may 479 inject excess spacing between inner text parts due to <=IE7's faulty node normalization. */
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.