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