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 * A regular expression to find opening script tags. 48 * 49 * @type {RegExp} 50 */ 51 var rgxpScriptTag = new RegExp('<script(\\s[^>]*?)?>', 'ig'); 52 53 /** 54 * A regular expression to find script types. 55 * 56 * @type {RegExp} 57 */ 58 var rgxpType = new RegExp( 59 ' type\\s*=\\s*' 60 + '[\\"\\\']' 61 + '([^\\"\\\']*[^\\s][^\\"\\\']*)' 62 + '[\\"\\\']', 63 'i' 64 ); 65 66 var rand = Math.random().toString().replace('.', ''); 67 68 /** 69 * Places sentinal strings in front of every executable script tag. 70 * 71 * @param {string} html HTML markup 72 * @return {string} HTML string with marked <script> tags. 73 */ 74 function markScriptTagLocations(html) { 75 var i = 0; 76 return html.replace(rgxpScriptTag, function (str, substr, offset) { 77 var type = substr && substr.match(rgxpType); 78 if (!type || rgxpScriptType.test(type)) { 79 return rand + (i++) + str; 80 } 81 return str; 82 }); 83 } 84 85 /** 86 * Masks a <script> tag's `type' attribute by replacing it with a random 87 * string. 88 * 89 * Masking script tags is done to prevent them from being handled specially 90 * by jQuery, which removes javascript/ecmascript tags when appending DOM 91 * elements into the document, but executes them. 92 * 93 * unmakeScriptType() reverses this. 94 * 95 * @param {jQuery.<HTMLElement>} $script jQuery unit set containing the 96 * <script> tag that is to have its 97 * type attribute masked. 98 */ 99 function maskScriptType($script) { 100 var type = $script.attr('type'); 101 $script.attr('type', rand).attr('data-origtype', type); 102 } 103 104 /** 105 * Restores a <script> tag's original type attribute value if it had been 106 * masked using maskScriptType(). 107 * 108 * Essentially the reverse of maskScriptType(). 109 * 110 * @param {jQuery.<HTMLElement>} $script jQuery unit set containing the 111 * <script> tag that is to have its 112 * type attribute unmasked. 113 */ 114 function unmaskScriptType($script) { 115 var orig = $script.attr('data-origtype'); 116 if (typeof orig === 'string') { 117 $script.attr('type', orig).removeAttr('data-origtype'); 118 } else { 119 $script.removeAttr('type'); 120 } 121 } 122 123 /** 124 * Replaces the type attribute of <script> tags with a value that will 125 * protect them from being specially handled by jQuery. 126 * 127 * @param {string} html Markup 128 * @return {string} Markup with <script> tags protected from jQuery. 129 */ 130 function protectScriptTags(html, $scripts) { 131 var i; 132 var type; 133 var $script; 134 for (i = 0; i < $scripts.length; i++) { 135 $script = $scripts.eq(i); 136 type = $script.attr('type'); 137 if (!type || rgxpScriptType.test(type)) { 138 maskScriptType($script); 139 html = html.replace(rand + i, $script[0].outerHTML); 140 unmaskScriptType($script); 141 } 142 } 143 return html; 144 } 145 146 /** 147 * Restores the type attribute for <script> tags that have been processed 148 * via protectScriptTags(). 149 * 150 * @param {jQuery.<HTMLElement>} $element Root HTMLElement in which to 151 * restore <script> tags. 152 */ 153 function restoreScriptTagTypes($element) { 154 var $scripts = $element.find('script[type="' + rand + '"]'); 155 var $script; 156 var i; 157 for (i = 0; i < $scripts.length; i++) { 158 $script = $scripts.eq(i); 159 $script.removeClass(rand + i); 160 unmaskScriptType($script); 161 } 162 } 163 164 /** 165 * Joins the innerHTML of multiple elements. 166 * 167 * @param {jQuery.<HTMLElement>} $elements 168 * @return {string} A concatenated string of the contents of the given set 169 * of elements. 170 */ 171 function joinContents($elements) { 172 var contents = ''; 173 $elements.each(function () { 174 contents += jQuery(this).html(); 175 }); 176 return contents; 177 } 178 179 /** 180 * Inserts the inner HTML of the given HTML markup while preserving 181 * <script> tags, and still allowing jQuery to execute them. 182 * 183 * @param {jQuery.<HTMLElement>} $element 184 * @param {string} html 185 */ 186 function insertInnerHTMLWithScriptTags($element, html) { 187 if (!rgxpScriptTag.test(html)) { 188 $element.html(jQuery(html).contents()); 189 return; 190 } 191 var marked = markScriptTagLocations(html); 192 var $scripts = jQuery(html).filter('script'); 193 var $html = jQuery(marked); 194 var contents = joinContents($html.filter(':not(script)')); 195 contents = protectScriptTags(contents, $scripts); 196 $element.html(contents); 197 198 // Trap script errors originating from rendered tags and log it on the 199 // console. 200 try { 201 $element.append($scripts); 202 } catch (ex) { 203 var _console = 'console'; // Because jslint is paranoid. 204 if (typeof window[_console] === 'function') { 205 window[_console].error(ex); 206 } 207 } 208 209 restoreScriptTagTypes($element); 210 } 211 212 /** 213 * Merge class names from one element into another. 214 * 215 * The merge result will be a unqiue set of space-seperated class names. 216 * 217 * @param {jQuery.<HTMLElement>} $first jQuery unit set containing the DOM 218 * whose class names are to be merged. 219 * @param {jQuery.<HTMLElement>} $second jQuery unit set containing the DOM 220 * whose class names are to be merged. 221 * @return {string} The merge result of the merge: a unqiue set of 222 * space-seperated class names. 223 */ 224 function mergeClassNames($first, $second) { 225 var first = ($first.attr('class') || '').split(' '); 226 var second = ($second.attr('class') || '').split(' '); 227 var names = first.concat(second).sort(); 228 var i; 229 for (i = 1; i < names.length; i++) { 230 if (names[i] === names[i - 1]) { 231 names.splice(i--, 1); 232 } 233 } 234 return names.join(' '); 235 } 236 237 /** 238 * Creates a map of attributes merged from their value in $from with their 239 * value in $to. 240 * 241 * Class names--unlike other attributes, which are simply copied from $from 242 * into $to--are treaded specially to produce a unique set of 243 * space-seperated class names. 244 * 245 * @param {jQuery.<HTMLElement>} $to jQuery unit set containing the DOM 246 * element which should receive the 247 * merged attributes. 248 * @param {jQuery.<HTMLElement>} $from jQuery unit set containing the DOM 249 * element whose attributes will be 250 * merged into the other. 251 * @param {object<string, string>} A associate array of attributes. 252 */ 253 function mergeAttributes($to, $from) { 254 var from = $from[0].attributes; 255 var to = $to[0].attributes; 256 var i; 257 var attr = {}; 258 for (i = 0; i < from.length; i++) { 259 attr[from[i].name] = ('class' === from[i].name) 260 ? mergeClassNames($to, $from) 261 : $from.attr(from[i].name); 262 } 263 return attr; 264 } 265 266 /** 267 * Renders the given HTML string onto (not into) an DOM element. 268 * Does nearly the equivelent of $.replaceWith() or changing the element's 269 * outerHTML. 270 * 271 * http://bugs.jquery.com/ticket/8142#comment:6 272 * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 273 * 'All of jQuery's insertion methods use a domManip function internally to 274 * clean/process elements before and after they are inserted into the DOM. 275 * One of the things the domManip function does is pull out any script 276 * elements about to be inserted and run them through an "evalScript 277 * routine" rather than inject them with the rest of the DOM fragment. It 278 * inserts the scripts separately, evaluates them, and then removes them 279 * from the DOM. 280 * 281 * 'I believe that one of the reasons jQuery does this is to avoid 282 * "Permission Denied" errors that can occur in Internet Explorer when 283 * inserting scripts under certain circumstances. It also avoids repeatedly 284 * inserting/evaluating the same script (which could potentially cause 285 * problems) if it is within a containing element that you are inserting and 286 * then moving around the DOM.' 287 * 288 * @param {jQuery.<HTMLElement>} $element jQuery unit set containing the 289 * DOM element we wish to render the 290 * given html content onto. 291 * @param {string} html HTML content which will become give to the element. 292 */ 293 function renderOnto($element, html) { 294 insertInnerHTMLWithScriptTags($element, html); 295 var attr = mergeAttributes($element, jQuery(html)); 296 var name; 297 for (name in attr) { 298 if (attr.hasOwnProperty(name)) { 299 $element.attr(name, attr[name]); 300 } 301 } 302 } 303 304 /** 305 * Removes the given chainback instance from its cached location. 306 * 307 * @param {Chainback} chainback Instance to remove from cache. 308 */ 309 function decache(chainback) { 310 if (chainback._constructor.__gcncache__[chainback.__gcnhash__]) { 311 delete chainback._constructor.__gcncache__[chainback.__gcnhash__]; 312 } 313 } 314 315 GCN.uniqueId = uniqueId; 316 GCN.escapePropertyName = escapePropertyName; 317 GCN.renderOnto = renderOnto; 318 GCN.decache = decache; 319 320 }(GCN)); 321