1/** 2 * Plugin Name: Glossarizer 3 * Author : Vinay @Pebbleroad 4 */ 5 6// Expose plugin as an AMD module if AMD loader is present: 7(function (factory) { 8 "use strict"; 9 if (typeof define === 'function' && define.amd) { 10 // AMD. Register as an anonymous module. 11 define(['jquery'], factory); 12 } else if (typeof exports === 'object' && typeof require === 'function') { 13 // Browserify 14 factory(require('jquery')); 15 } else { 16 // Browser globals 17 factory(jQuery); 18 } 19}(function ($) { 20 'use strict'; 21 /** 22 * Defaults 23 */ 24 25 var pluginName = 'glossarizer', 26 defaults = { 27 sourceURL: '', /* URL of the JSON file with format {"term": "", "description": ""} */ 28 replaceTag: 'abbr', /* Matching words will be wrapped with abbr tags by default */ 29 lookupTagName: 'p, ul, a, div', /* Lookup in either paragraphs or lists. Do not replace in headings */ 30 callback: null, /* Callback once all tags are replaced: Call or tooltip or anything you like */ 31 replaceOnce: false, /* Replace only once in a TextNode */ 32 replaceClass: 'glossarizer_replaced', 33 caseSensitive: false, 34 exactMatch: false 35 } 36 37 /** 38 * Constructor 39 */ 40 41 function Glossarizer (el, options) { 42 var base = this 43 44 base.el = el 45 46 /* Element */ 47 base.$el = $(el) 48 49 /* Extend options */ 50 51 base.options = $.extend({}, defaults, options) 52 53 /* Terms */ 54 55 base.terms = [] 56 57 /* Excludes array */ 58 59 base.excludes = [] 60 61 /* Replaced words array */ 62 63 base.replaced = [] 64 65 /* Regex Tags */ 66 67 base.regexOption = (base.options.caseSensitive ? '' : 'i') + (base.options.replaceOnce ? '' : 'g') 68 69 /* Fetch glossary JSON */ 70 base.glossary = this.options.sourceURL 71 72 if (!base.glossary.length || base.glossary.length == 0) return 73 74 /** 75 * Get all terms 76 */ 77 78 for (var i = 0; i < base.glossary.length; i++) { 79 var terms = base.glossary[i].term.split(',') 80 81 for (var j = 0; j < terms.length; j++) { 82 /* Trim */ 83 84 var trimmed = terms[j].replace(/^\s+|\s+$/g, ''), 85 isExclusion = trimmed.indexOf('!') 86 87 if (isExclusion == -1 || isExclusion != 0) { 88 /* Glossary terms array */ 89 90 base.terms.push(trimmed) 91 } else { 92 /* Excluded terms array */ 93 94 base.excludes.push(trimmed.substr(1)) 95 } 96 } 97 } 98 99 /** 100 * Wrap terms 101 */ 102 103 base.wrapTerms() 104 } 105 106 /** 107 * Prototypes 108 */ 109 Glossarizer.prototype = { 110 getDescription: function (term) { 111 var regex = new RegExp('(\,|\s*)' + this.clean(term) + '\\s*|\\,$', 'i') 112 113 /** 114 * Matches 115 * 1. Starts with \s* (zero or more spaces) 116 * 2. Ends with zero or more spaces 117 * 3. Ends with comma 118 */ 119 120 for (var i = 0; i < this.glossary.length; i++) { 121 if (this.options.exactMatch) { 122 if (this.glossary[i].term == this.clean(term)) { 123 return this.glossary[i].description.replace(/\"/gi, '"') 124 } 125 } else { 126 if (this.glossary[i].term.match(regex)) { 127 return this.glossary[i].description.replace(/\"/gi, '"') 128 } 129 } 130 } 131 }, 132 getLink: function (term) { 133 var regex = new RegExp('(\,|\s*)' + this.clean(term) + '\\s*|\\,$', 'i') 134 135 /** 136 * Matches 137 * 1. Starts with \s* (zero or more spaces) 138 * 2. Ends with zero or more spaces 139 * 3. Ends with comma 140 */ 141 142 for (var i = 0; i < this.glossary.length; i++) { 143 if (this.options.exactMatch) { 144 if (this.glossary[i].term == this.clean(term)) { 145 return this.glossary[i].link.replace(/\"/gi, '"') 146 } 147 } else { 148 if (this.glossary[i].term.match(regex)) { 149 return this.glossary[i].link.replace(/\"/gi, '"') 150 } 151 } 152 } 153 }, 154 getSlug: function (term) { 155 var regex = new RegExp('(\,|\s*)' + this.clean(term) + '\\s*|\\,$', 'i') 156 157 /** 158 * Matches
159 * 1. Starts with \s* (zero or more spaces) 160 * 2. Ends with zero or more spaces 161 * 3. Ends with comma 162 */ 163 164 for (var i = 0; i < this.glossary.length; i++) { 165 if (this.options.exactMatch) { 166 if (this.glossary[i].term == this.clean(term)) { 167 return this.glossary[i].slug.replace(/\"/gi, '"') 168 } 169 } else { 170 if (this.glossary[i].term.match(regex)) { 171 return this.glossary[i].slug.replace(/\"/gi, '"') 172 } 173 } 174 } 175 }, 176 clean: function (text) { 177 var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g') 178 179 return text.replace(reEscape, '\\$1') 180 }, 181 182 /** 183 * Wraps the matched terms by calling traverser 184 */ 185 wrapTerms: function () { 186 this.cleanedTerms = this.clean(this.terms.join('|')) 187 this.excludedTerms = this.clean(this.excludes.join('|')) 188 189 var nodes = this.el.querySelectorAll(this.options.lookupTagName) 190 191 for (var i = 0; i < nodes.length; i++) { 192 this.traverser(nodes[i]) 193 } 194 195 /** 196 * Callback 197 */ 198 199 if (this.options.callback) this.options.callback.call(this.$el) 200 }, 201 202 /** 203 * Traverses through nodes to find the matching terms in TEXTNODES 204 */ 205 206 traverser: function (node) { 207 var next, 208 base = this 209 210 if (node.nodeType === 1) { 211 /* 212 Element Node 213 */ 214 215 if (node = node.firstChild) { 216 do { 217 // Recursively call traverseChildNodes 218 // on each child node 219 next = node.nextSibling 220 221 /** 222 * Check if the node is not glossarized 223 */ 224 225 if (node.className != this.options.replaceClass) { 226 this.traverser(node) 227 } 228 } while (node = next) 229 } 230 } else if (node.nodeType === 3) { 231 /* 232 Text Node 233 */ 234 235 var temp = document.createElement('div'), 236 data = node.data 237 238 var re = new RegExp('(?:^|\\b)(' + this.cleanedTerms + ')(?!\\w)', base.regexOption), 239 reEx = new RegExp('(?:^|\\b)(' + this.excludedTerms + ')(?!\\w)', base.regexOption) 240 241 if (re.test(data)) { 242 var excl = reEx.exec(data) 243 244 data = data.replace(re, function (match, item , offset, string) { 245 if (base.options.replaceOnce && inArrayIn(match, base.replaced) >= 0) { 246 return match 247 } 248 249 base.replaced.push(match) 250 251 var ir = new RegExp('(?:^|\\b)' + base.clean(match) + '(?!\\w)'), 252 result = ir.exec(data) 253 254 if (result) { 255 if (excl && base.excludes.length) { 256 var id = offset, 257 exid = excl.index, 258 exl = excl.index + excl[0].length 259 260 if (exid <= id && id <= exl) { 261 return match 262 } else { 263 return '<' + base.options.replaceTag + ' href="' + base.getLink(match) + '" aria-describedby="' + base.getSlug(match) + '" class="' + base.options.replaceClass + '">' + match + '<span class="c-glossTerm__tip" role="tooltip" id="' + base.getSlug(match) + '">' + base.getDescription(match) + '</span></' + base.options.replaceTag + '>' 264 } 265 } else { 266 return '<' + base.options.replaceTag + ' href="' + base.getLink(match) + '" aria-describedby="' + base.getSlug(match) + '" class="' + base.options.replaceClass + '">' + match + '<span class="c-glossTerm__tip" role="tooltip" id="' + base.getSlug(match) + '">' + base.getDescription(match) + '</span></' + base.options.replaceTag + '>' 267 } 268 } 269 }) 270 271 /** 272 * Only replace when a match is found 273 * Resorting to jQuery html() because of IE8 whitespace issue. 274 * IE 8 removes leading whitespace from Text Nodes. Hence innerhtml doesnt work. 275 * 276 */ 277 278 $(temp).html(data) 279 280 while (temp.firstChild) { 281 node.parentNode.insertBefore(temp.firstChild, node) 282 } 283 284 node.parentNode.removeChild(node) 285 } 286 } 287 }, 288 289 } 290 291 /** 292 * Public Methods 293 */ 294 295 var methods = { 296 destroy: function () { 297 this.$el.removeData('plugin_' + pluginName) 298 299 /* Remove abbr tag */ 300 this.$el.find('.' + this.options.replaceClass).each(function () { 301 var $this = $(this), 302 text = $this.text() 303 304 $this.replaceWith(text) 305 }) 306 } 307 } 308 309 /** 310 * Extend Prototype 311 */ 312 313 Glossarizer.prototype = $.extend({}, Glossarizer.prototype, methods) 314 315 /** 316 * Plugin 317 * @param {[type]} options 318 */ 319 $.fn[pluginName] = function (options) { 320 return this.each(function () { 321 var $this = $(this), 322 glossarizer = $this.data('plugin_' + pluginName) 323 324 /* 325 Check if its a method 326 */ 327 328 if (typeof options == 'string' && glossarizer && methods.hasOwnProperty(options)) { 329 glossarizer[options].apply(glossarizer) 330 } else { 331 /* Destroy if exists */ 332 333 if (glossarizer) glossarizer['destroy'].apply(glossarizer) 334 335 /* Initialize */ 336 337 $.data(this, 'plugin_' + pluginName, new Glossarizer(this, options)) 338 } 339 }) 340 } 341 342 /** 343 * In Array 344 */ 345 346 function inArrayIn (elem, arr, i) { 347 if (typeof elem !== 'string') { 348 return $.inArray.apply(this, arguments) 349 } 350 351 if (arr) { 352 var len = arr.length 353 i = i ? (i < 0 ? Math.max(0, len + i) : i) : 0 354 elem = elem.toLowerCase() 355 for (; i < len; i++) { 356 if (i in arr && arr[i].toLowerCase() == elem) { 357 return i 358 } 359 } 360 } 361 return -1 362 } 363}));
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.