1 (function (GCN) { 2 3 'use strict'; 4 5 /** 6 * Only grows, never shrinks. 7 * @private 8 * @type {number} 9 */ 10 var uniqueIdCounter = 0; 11 12 /** 13 * Generates a unique id with an optional prefix. 14 * 15 * The returned value is only unique among other returned values, 16 * not globally. 17 * 18 * @public 19 * @param {string} 20 * Optional prefix for the id to be generated. 21 * @return {string} 22 * Never the same string more than once. 23 */ 24 function uniqueId(prefix) { 25 return (prefix || '') + (++uniqueIdCounter); 26 } 27 28 function escapePropertyName(name) { 29 return name.replace(/\./g, '\\.'); 30 } 31 32 /** 33 * A regular expression to identify executable script tags. 34 * 35 * @type {RegExp} 36 */ 37 var rgxpScriptType = /\/(java|ecma)script/i; 38 39 /** 40 * A regular expression to find script tags. 41 * 42 * @type {RegExp} 43 */ 44 var rgxpScriptTag = new RegExp('<script(\\s[^>]*?)?>', 'ig'); 45 46 /** 47 * A regular expression to find script types. 48 * 49 * @type {RegExp} 50 */ 51 var rgxpType = new RegExp( 52 ' type\\s*=\\s*' 53 + '[\\"\\\']' 54 + '([^\\"\\\']*[^\\s][^\\"\\\']*)' 55 + '[\\"\\\']', 56 'i' 57 ); 58 59 var rand = Math.random().toString().replace('.', ''); 60 61 /** 62 * Places sentinal strings in front of every executable script tag. 63 * 64 * @param {string} html HTML markup 65 * @return {string} HTML string with marked <script> tags. 66 */ 67 function markScriptTagLocations(html) { 68 var i = 0; 69 return html.replace(rgxpScriptTag, function (str, substr, offset) { 70 var type = substr && substr.match(rgxpType); 71 if (!type || rgxpScriptType.test(type)) { 72 return rand + (i++) + str; 73 } 74 return str; 75 }); 76 } 77 78 /** 79 * Masks a <script> tag's `type' attribute by replacing it with a random 80 * string. 81 * 82 * Masking script tags is don to prevent them from being handled specially 83 * by jQuery which removes javascript/ecmascript tags when appending DOM 84 * elements into the document, but executes them. 85 * 86 * unmakeScriptType() does the reverse of this. 87 * 88 * @param {jQuery.<HTMLElement>} $script jQuery unit set containing the 89 * <script> tag that is to have its 90 * type attribute masked. 91 */ 92 function maskScriptType($script) { 93 var type = $script.attr('type'); 94 $script.attr('type', rand).attr('data-origtype', type); 95 } 96 97 /** 98 * Restores a <script> tag's original type attribute value, if it had been 99 * masked using maskScriptType(). 100 * 101 * Essentially the reverse of maskScriptType(). 102 * 103 * @param {jQuery.<HTMLElement>} $script jQuery unit set containing the 104 * <script> tag that is to have its 105 * type attribute unmasked. 106 */ 107 function unmaskScriptType($script) { 108 var orig = $script.attr('data-origtype'); 109 if (typeof orig === 'string') { 110 $script.attr('type', orig).removeAttr('data-origtype'); 111 } else { 112 $script.removeAttr('type'); 113 } 114 } 115 116 /** 117 * Replaces the type attribute of <script> tags with a value that will 118 * protect them from being specially handled by jQuery. 119 * 120 * @param {string} html Markup 121 * @return {string} Markup with <script> tags protected from jQuery. 122 */ 123 function protectScriptTags(html, $scripts) { 124 var i; 125 var $script; 126 var type; 127 for (i = 0; i < $scripts.length; i++) { 128 $script = $scripts.eq(i); 129 type = $script.attr('type'); 130 if (!type || rgxpScriptType.test(type)) { 131 maskScriptType($script); 132 html = html.replace(rand + i, $script[0].outerHTML); 133 unmaskScriptType($script); 134 } 135 } 136 return html; 137 } 138 139 /** 140 * Restores the type attribute for <script> tags that have been processed 141 * via protectScriptTags(). 142 * 143 * @param {jQuery.<HTMLElement>} $element Root HTMLElement in which to 144 * restore <script> tags. 145 */ 146 function restoreScriptTagTypes($element) { 147 var $scripts = $element.find('script[type="' + rand + '"]'); 148 var $script; 149 var i; 150 for (i = 0; i < $scripts.length; i++) { 151 $script = $scripts.eq(i); 152 $script.removeClass(rand + i); 153 unmaskScriptType($script); 154 } 155 } 156 157 /** 158 * Joins the innerHTML of multiple elements. 159 * 160 * @param {jQuery.<HTMLElement>} $elements 161 */ 162 function joinContents($elements) { 163 var contents = ''; 164 $elements.each(function () { 165 contents += jQuery(this).html(); 166 }); 167 return contents; 168 } 169 170 /** 171 * Inserts the inner HTML of the given HTML markup while preserving 172 * <script> tags, and still allowing jQuery to execute them. 173 * 174 * @param {jQuery.<HTMLElement>} $element 175 * @param {string} html 176 */ 177 function insertInnerHTMLWithScriptTags($element, html) { 178 if (!rgxpScriptTag.test(html)) { 179 $element.html(jQuery(html).contents()); 180 return; 181 } 182 var marked = markScriptTagLocations(html); 183 var $scripts = jQuery(html).filter('script'); 184 var $html = jQuery(marked); 185 var contents = joinContents($html.filter(':not(script)')); 186 contents = protectScriptTags(contents, $scripts); 187 $element.html(contents); 188 189 // Trap script errors originating from rendered tags and log it on the 190 // console. 191 try { 192 $element.append($scripts); 193 } catch (ex) { 194 var _console = 'console'; // Avoid jslint warnings 195 if (typeof window[_console] === 'function') { 196 window[_console].error(ex); 197 } 198 } 199 200 restoreScriptTagTypes($element); 201 } 202 203 /** 204 * Merge class names from one element into another. 205 * 206 * The merge result will be a unqiue set of space-seperated class names. 207 * 208 * @param {jQuery.<HTMLElement>} $first jQuery unit set containing the DOM 209 * whose class names are to be merged. 210 * @param {jQuery.<HTMLElement>} $second jQuery unit set containing the DOM 211 * whose class names are to be merged. 212 * @return {string} The merge result of the merge: a unqiue set of 213 * space-seperated class names. 214 */ 215 function mergeClassNames($first, $second) { 216 var first = ($first.attr('class') || '').split(' '); 217 var second = ($second.attr('class') || '').split(' '); 218 var names = first.concat(second).sort(); 219 var i; 220 for (i = 1; i < names.length; i++) { 221 if (names[i] === names[i - 1]) { 222 names.splice(i--, 1); 223 } 224 } 225 return names.join(' '); 226 } 227 228 /** 229 * Creates a map of attributes merged from their value in $from with their 230 * value in $to. 231 * 232 * Unlike other attributes, which are simply copied from $from into $to, 233 * class names are treaded specially to produce unique set of 234 * space-seperated class names. 235 * 236 * @param {jQuery.<HTMLElement>} $to jQuery unit set containing the DOM 237 * element which should receive the 238 * merged attributes. 239 * @param {jQuery.<HTMLElement>} $from jQuery unit set containing the DOM 240 * element whose attributes will be 241 * merged into the other. 242 * @param {object<string, string>} A associate array of attributes. 243 */ 244 function mergeAttributes($to, $from) { 245 var to = $to[0].attributes; 246 var from = $from[0].attributes; 247 var i; 248 var attr = {}; 249 for (i = 0; i < from.length; i++) { 250 attr[from[i].name] = ('class' === from[i].name) 251 ? mergeClassNames($to, $from) 252 : $from.attr(from[i].name); 253 } 254 return attr; 255 } 256 257 /** 258 * Renders the given HTML string onto (not into) an DOM element. 259 * Does nearly the equivelent of changes the elements' outerHTML. 260 * 261 * @param {jQuery.<HTMLElement>} $element jQuery unit set containing the 262 * DOM element we wish to render the 263 * given html content onto. 264 * @param {string} html HTML content which will become give to the element. 265 */ 266 function renderOnto($element, html) { 267 insertInnerHTMLWithScriptTags($element, html); 268 var attr = mergeAttributes($element, jQuery(html)); 269 var name; 270 for (name in attr) { 271 if (attr.hasOwnProperty(name)) { 272 $element.attr(name, attr[name]); 273 } 274 } 275 } 276 277 GCN.uniqueId = uniqueId; 278 GCN.escapePropertyName = escapePropertyName; 279 GCN.renderOnto = renderOnto; 280 281 }(GCN)); 282