1 (function (GCN) { 2 3 'use strict'; 4 5 /** 6 * Only grows, never shrinks. 7 * 8 * @private 9 * @type {number} 10 */ 11 var uniqueIdCounter = 0; 12 13 /** 14 * Generates a unique id with an optional prefix. 15 * 16 * The returned value is only unique among other returned values, 17 * not globally. 18 * 19 * @public 20 * @param {string} 21 * Optional prefix for the id to be generated. 22 * @return {string} 23 * Never the same string more than once. 24 */ 25 function uniqueId(prefix) { 26 return (prefix || '') + (++uniqueIdCounter); 27 } 28 29 /** 30 * Escapes the given property name by prefixing dots with backslashes. 31 * 32 * @param {string} name The property name to escape. 33 * @return {string} Escaed string. 34 */ 35 function escapePropertyName(name) { 36 return name.replace(/\./g, '\\.'); 37 } 38 39 /** 40 * A regular expression to identify executable script tags. 41 * 42 * @type {RegExp} 43 */ 44 var rgxpScriptType = /\/(java|ecma)script/i; 45 46 47 /** 48 * A string to be used in regular expressions matching script tags. 49 * 50 * TODO: The string does not work correctly for complex cases and 51 * should be improved. 52 * 53 * @type {string} 54 */ 55 var SCRIPT_TAG = '<script(\\s[^>]*?)?>'; 56 57 /** 58 * This regular expression is used to find opening script tags. 59 * 60 * It uses the global flag to find several matching tags, for 61 * example to replace them. 62 * 63 * @type {RegExp} 64 */ 65 var rgxpScriptTags = new RegExp(SCRIPT_TAG, 'ig'); 66 67 /** 68 * This regular expression is used to find opening script tags. 69 * It does not use the global flag, and can be used to test if 70 * a string contains scripts at all. 71 * 72 * For testing strings, this expression should be used instead 73 * of rgxpScriptTags, because the latter keeps an index of found 74 * occurrences (due to its global flag), which causes the 75 * regular expression to often fail (in Firefox). 76 * 77 * @type {RegExp} 78 */ 79 var rgxpScriptTag = new RegExp(SCRIPT_TAG, 'i'); 80 81 82 83 /** 84 * A regular expression to find script types. 85 * 86 * @type {RegExp} 87 */ 88 var rgxpType = new RegExp( 89 ' type\\s*=\\s*' 90 + '[\\"\\\']' 91 + '([^\\"\\\']*[^\\s][^\\"\\\']*)' 92 + '[\\"\\\']', 93 'i' 94 ); 95 96 var rand = Math.random().toString().replace('.', ''); 97 98 /** 99 * Places sentinal strings in front of every executable script tag. 100 * 101 * @param {string} html HTML markup 102 * @return {string} HTML string with marked <script> tags. 103 */ 104 function markScriptTagLocations(html) { 105 var i = 0; 106 rgxpScriptTags.lastIndex = 0; 107 return html.replace(rgxpScriptTags, function (str, substr, offset) { 108 var type = substr && substr.match(rgxpType); 109 if (!type || rgxpScriptType.test(type)) { 110 return rand + (i++) + str; 111 } 112 return str; 113 }); 114 } 115 116 /** 117 * Masks a <script> tag's `type' attribute by replacing it with a random 118 * string. 119 * 120 * Masking script tags is done to prevent them from being handled specially 121 * by jQuery, which removes javascript/ecmascript tags when appending DOM 122 * elements into the document, but executes them. 123 * 124 * unmakeScriptType() reverses this. 125 * 126 * @param {jQuery.<HTMLElement>} $script jQuery unit set containing the 127 * <script> tag that is to have its 128 * type attribute masked. 129 */ 130 function maskScriptType($script) { 131 var type = $script.attr('type'); 132 $script.attr('type', rand).attr('data-origtype', type); 133 } 134 135 /** 136 * Restores a <script> tag's original type attribute value if it had been 137 * masked using maskScriptType(). 138 * 139 * Essentially the reverse of maskScriptType(). 140 * 141 * @param {jQuery.<HTMLElement>} $script jQuery unit set containing the 142 * <script> tag that is to have its 143 * type attribute unmasked. 144 */ 145 function unmaskScriptType($script) { 146 var orig = $script.attr('data-origtype'); 147 if (typeof orig === 'string') { 148 $script.attr('type', orig).removeAttr('data-origtype'); 149 } else { 150 $script.removeAttr('type'); 151 } 152 } 153 154 /** 155 * Replaces the type attribute of <script> tags with a value that will 156 * protect them from being specially handled by jQuery. 157 * 158 * @param {string} html Markup 159 * @return {string} Markup with <script> tags protected from jQuery. 160 */ 161 function protectScriptTags(html, $scripts) { 162 var i; 163 var type; 164 var $script; 165 for (i = 0; i < $scripts.length; i++) { 166 $script = $scripts.eq(i); 167 type = $script.attr('type'); 168 if (!type || rgxpScriptType.test(type)) { 169 maskScriptType($script); 170 html = html.replace(rand + i, $script[0].outerHTML); 171 unmaskScriptType($script); 172 } 173 } 174 return html; 175 } 176 177 /** 178 * Restores the type attribute for <script> tags that have been processed 179 * via protectScriptTags(). 180 * 181 * @param {jQuery.<HTMLElement>} $element Root HTMLElement in which to 182 * restore <script> tags. 183 */ 184 function restoreScriptTagTypes($element) { 185 var $scripts = $element.find('script[type="' + rand + '"]'); 186 var $script; 187 var i; 188 for (i = 0; i < $scripts.length; i++) { 189 $script = $scripts.eq(i); 190 $script.removeClass(rand + i); 191 unmaskScriptType($script); 192 } 193 } 194 195 /** 196 * Joins the innerHTML of multiple elements. 197 * 198 * @param {jQuery.<HTMLElement>} $elements 199 * @return {string} A concatenated string of the contents of the given set 200 * of elements. 201 */ 202 function joinContents($elements) { 203 var contents = ''; 204 $elements.each(function () { 205 contents += jQuery(this).html(); 206 }); 207 return contents; 208 } 209 210 /** 211 * Inserts the inner HTML of the given HTML markup while preserving 212 * <script> tags, and still allowing jQuery to execute them. 213 * 214 * @param {jQuery.<HTMLElement>} $element 215 * @param {string} html 216 */ 217 function insertInnerHTMLWithScriptTags($element, html) { 218 if (!rgxpScriptTag.test(html)) { 219 $element.html(jQuery(html).contents()); 220 return; 221 } 222 var marked = markScriptTagLocations(html); 223 var $html = jQuery(marked); 224 // beware, starting with jQuery 1.9.0, the handling of scripts changed. Before 1.9.0 when doing e.g. 225 // jQuery('<div><script></script></div>') the script tag would be moved out of the div and appended to the end. 226 // therefore using the filter() method would give us all scripts found in the markup 227 // Starting with jQuery 1.9.0, the script tags would remain were they are and therefore using the filter() method 228 // would return an empty jQuery() object, unless the markup consists only of the script tag. 229 var $scripts = jQuery(html).filter('script'); 230 var contents; 231 // now we do a detection of the script-tag handling 232 if ($scripts.length === 0) { 233 // we are using jQuery >= 1.9.0, so we use .find() to extract all the script-nodes 234 $scripts = jQuery(html).find('script'); 235 // from the $html, we now remove the script-nodes 236 $html.find('script').remove(); 237 contents = $html.html(); 238 } else { 239 // we are using jQuery < 1.9.0, to $html is already separated (non-script nodes and script nodes) 240 // so with .filter() we can get all non-script nodes and join them 241 contents = joinContents($html.filter(':not(script)')); 242 } 243 // when we get here, contents will consists of the original html markup with all script tags replaced by placeholders 244 // next we will re-insert the script tags at their original place, but with the type replaced by something else than text/javascript 245 // the purpose is that jQuery will NOT evaluate the scripts when inserting into the DOM 246 contents = protectScriptTags(contents, $scripts); 247 // insert the markup into the element. masked script nodes will be generated but not executed 248 $element.html(contents); 249 250 // Trap script errors originating from rendered tags and log it on the 251 // console. 252 try { 253 // jQuery will now execute but not really append the scripts 254 $element.append($scripts); 255 } catch (ex) { 256 var _console = 'console'; // Because jslint is paranoid. 257 if (typeof window[_console] === 'function') { 258 window[_console].error(ex); 259 } 260 } 261 262 // finally, we unmask the script nodes to have them appear like in the original markup 263 restoreScriptTagTypes($element); 264 } 265 266 /** 267 * Merge class names from one element into another. 268 * 269 * The merge result will be a unqiue set of space-seperated class names. 270 * 271 * @param {jQuery.<HTMLElement>} $first jQuery unit set containing the DOM 272 * whose class names are to be merged. 273 * @param {jQuery.<HTMLElement>} $second jQuery unit set containing the DOM 274 * whose class names are to be merged. 275 * @return {string} The merge result of the merge: a unqiue set of 276 * space-seperated class names. 277 */ 278 function mergeClassNames($first, $second) { 279 var first = ($first.attr('class') || '').split(' '); 280 var second = ($second.attr('class') || '').split(' '); 281 var names = first.concat(second).sort(); 282 var i; 283 for (i = 1; i < names.length; i++) { 284 if (names[i] === names[i - 1]) { 285 names.splice(i--, 1); 286 } 287 } 288 return names.join(' '); 289 } 290 291 /** 292 * Creates a map of attributes merged from their value in $from with their 293 * value in $to. 294 * 295 * Class names--unlike other attributes, which are simply copied from $from 296 * into $to--are treaded specially to produce a unique set of 297 * space-seperated class names. 298 * 299 * @param {jQuery.<HTMLElement>} $to jQuery unit set containing the DOM 300 * element which should receive the 301 * merged attributes. 302 * @param {jQuery.<HTMLElement>} $from jQuery unit set containing the DOM 303 * element whose attributes will be 304 * merged into the other. 305 * @param {object<string, string>} A associate array of attributes. 306 */ 307 function mergeAttributes($to, $from) { 308 var from = $from[0].attributes; 309 var to = $to[0].attributes; 310 var i; 311 var attr = {}; 312 for (i = 0; i < from.length; i++) { 313 attr[from[i].name] = ('class' === from[i].name) 314 ? mergeClassNames($to, $from) 315 : $from.attr(from[i].name); 316 } 317 return attr; 318 } 319 320 /** 321 * Renders the given HTML string onto (not into) an DOM element. 322 * Does nearly the equivelent of $.replaceWith() or changing the element's 323 * outerHTML. 324 * 325 * http://bugs.jquery.com/ticket/8142#comment:6 326 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 327 * 'All of jQuery's insertion methods use a domManip function internally to 328 * clean/process elements before and after they are inserted into the DOM. 329 * One of the things the domManip function does is pull out any script 330 * elements about to be inserted and run them through an "evalScript 331 * routine" rather than inject them with the rest of the DOM fragment. It 332 * inserts the scripts separately, evaluates them, and then removes them 333 * from the DOM. 334 * 335 * 'I believe that one of the reasons jQuery does this is to avoid 336 * "Permission Denied" errors that can occur in Internet Explorer when 337 * inserting scripts under certain circumstances. It also avoids repeatedly 338 * inserting/evaluating the same script (which could potentially cause 339 * problems) if it is within a containing element that you are inserting and 340 * then moving around the DOM.' 341 * 342 * @param {jQuery.<HTMLElement>} $element jQuery unit set containing the 343 * DOM element we wish to render the 344 * given html content onto. 345 * @param {string} html HTML content which will become give to the element. 346 */ 347 function renderOnto($element, html) { 348 insertInnerHTMLWithScriptTags($element, html); 349 var attr = mergeAttributes($element, jQuery(html)); 350 var name; 351 for (name in attr) { 352 if (attr.hasOwnProperty(name)) { 353 $element.attr(name, attr[name]); 354 } 355 } 356 } 357 358 /** 359 * Removes the given chainback instance from its cached location. 360 * 361 * @param {Chainback} chainback Instance to remove from cache. 362 */ 363 function decache(chainback) { 364 if (chainback._constructor.__gcncache__[chainback.__gcnhash__]) { 365 delete chainback._constructor.__gcncache__[chainback.__gcnhash__]; 366 } 367 } 368 369 /** 370 * Maps constructs, that were fetched via the Rest API, to a hashmap, using 371 * their keyword as the keys. 372 * 373 * @param {object<string, object>} constructs Consturcts mapped against 374 * their id. 375 * @return {object<string, object>} Constructs mapped against their keys. 376 */ 377 function mapConstructs(constructs) { 378 if (!constructs) { 379 return {}; 380 } 381 var map = {}; 382 var constructId; 383 for (constructId in constructs) { 384 if (constructs.hasOwnProperty(constructId)) { 385 map[constructs[constructId].keyword] = constructs[constructId]; 386 } 387 } 388 return map; 389 } 390 391 GCN.uniqueId = uniqueId; 392 GCN.escapePropertyName = escapePropertyName; 393 GCN.renderOnto = renderOnto; 394 GCN.decache = decache; 395 GCN.mapConstructs = mapConstructs; 396 397 }(GCN)); 398