1var notify = []; 2var experimentIdCounter = 0; 3/** 4 * The questions above are answered by running a bunch of experiments 5 * exhaustively for all combinations of HTML element names. 6 * 7 * @param makeHtmlString takes one or more element names. 8 * Its {@code length} property specifies its arity, and runExperiment 9 * calls it iteratively with every permutation of length element names. 10 * @param checkDom receives the element names passed to makeHtmlString, 11 * an HTML document body created by parsing the HTML from makeHtmlString 12 * and initialResult/return value from last call to checkDom. 13 * @param initialResult the first result value to pass to checkDom. 14 * @param opt_elementNames an array of element names which defaults to 15 * window.elementNames. 16 */ 17function runExperiment(makeHtmlString, checkDom, initialResult, onResult, 18 opt_elementNames) { 19 var experimentIndex = ++experimentIdCounter; 20 var iframes = document.getElementById('experiment-iframes'); 21 var iframe = document.createElement('iframe'); 22 iframes.appendChild(iframe); 23 24 var elementNames = opt_elementNames || window.elementNames; 25 26 var nElements = elementNames.length; 27 var arity = makeHtmlString.length; 28 var nRuns = Math.pow(nElements, arity); 29 var runIndex = 0; 30 var paramIndices = new Array(arity); 31 var paramValues = new Array(arity); 32 for (var i = 0; i < arity; ++i) { 33 paramIndices[i] = 0; 34 paramValues[i] = elementNames[0]; 35 } 36 var exhausted = nRuns === 0; 37 38 var progressCounterContainer = 39 document.getElementById('experiment-progress-counter'); 40 41 var startTime = Date.now(); 42 var lastProgressUpdateTime = startTime; 43 44 var result = initialResult; 45 46 var progressCounter; 47 if (progressCounterContainer) { 48 progressCounter = document.createElement('li'); 49 progressCounter.style.width = '0'; 50 progressCounterContainer.appendChild(progressCounter); 51 } 52 53 function advance() { 54 // Advance to next permutation. 55 var i; 56 for (i = arity; --i >= 0;) { 57 if (++paramIndices[i] < nElements) { 58 paramValues[i] = elementNames[paramIndices[i]]; 59 break; 60 } 61 paramIndices[i] = 0; 62 paramValues [i] = elementNames[0]; 63 } 64 ++runIndex; 65 if (progressCounter) { 66 var now = Date.now(); 67 if (now - lastProgressUpdateTime > 250 ) { 68 var ratio = runIndex / nRuns; 69 progressCounter.style.width = (100 * ratio).toFixed(2) + '%'; 70 lastProgressUpdateTime = now; 71 var timeSoFar = now - startTime; 72 if (timeSoFar > 5000) { 73 // Assuming time per run is constant: 74 // total_time / nRuns = time_so_far / runIndex 75 // total_time = time_so_far * nRuns / runIndex 76 // = time_so_far / ratio 77 // eta = total_time - time_so_far 78 // = time_so_far / ratio - time_so_far 79 // = time_so_far * (1/ratio - 1) 80 var eta = timeSoFar * (1 / ratio - 1); 81 progressCounter.innerHTML = eta > 250 82 ? 'ETA:' + (eta / 1000).toFixed(1) + 's' : ''; 83 } 84 } 85 } 86 exhausted = i < 0; 87 } 88 89 function step() { 90 var htmlString = null; 91 // Try to generate an HTML string. 92 // The maker can return a nullish value to abort or punt on an experiment, 93 // so we loop until we find work to do. 94 while (!exhausted) { 95 paramValues.length = arity; 96 htmlString = makeHtmlString.apply(null, paramValues); 97 if (htmlString != null) { 98 break; 99 } 100 advance(); 101 } 102 103 if (htmlString == null) { 104 var endTime = Date.now(); 105 console.log('experiment took %d millis for %d runs', 106 (endTime - startTime), nRuns); 107 if (progressCounter) { 108 setTimeout(function () { 109 iframes.removeChild(iframe); 110 progressCounterContainer.removeChild(progressCounter); 111 }, 250); 112 } 113 onResult(result); 114 } else { 115 var notifyIndex = notify.indexOf(void 0); 116 if (notifyIndex < 0) { notifyIndex = notify.length; } 117 notify[notifyIndex] = function () { 118 notify[notifyIndex] = void 0; 119 120 // Process result 121 paramValues[arity] = iframe.contentDocument.body; 122 paramValues[arity + 1] = result; 123 result = checkDom.apply(null, paramValues); 124 paramValues.length = arity; 125 126 // Requeue the next step on the parent frames event queue. 127 setTimeout(function () { advance(); step(); }, 0); 128 }; 129 // Start the iframe parsing its body. 130 iframe.srcdoc = ( 131 '<!doctype html><html><head></head>' 132 + '<body onload="parent.notify[' + notifyIndex + ']()">' 133 + htmlString 134 ); 135 } 136 } 137 step(); 138} 139 140function formatDataToJsonHTML(data) { 141 var out = []; 142 var htmlForNullValue = '<span class="json-kw">null</span>'; 143 var htmlForErrorValue = '<span class="json-kw json-err">null</span>'; 144 var depth = 0; 145 var spaces = ' '; 146 format(data); 147 return out.join(''); 148 149 function format(v) { 150 if (v == null) { 151 out.push(htmlForNullValue); 152 return; 153 } 154 var t = typeof v; 155 if (t === 'boolean') { 156 out.push('<span class="json-kw">', v, '</span>'); 157 } else if (t === 'number') { 158 if (isFinite(v)) { 159 out.push('<span class="json-val">', v, '</span>'); 160 } else { 161 out.push(htmlForErrorValue); 162 } 163 } else if (t === 'string' || v instanceof String) { 164 var token = JSON.stringify(String(v)); 165 token = token.replace(/&/g, '&').replace(/</g, '<'); 166 out.push('<span class="json-str">', token, '</span>'); 167 } else { 168 var length = v.length; 169 var isSeries = ('number' === typeof length 170 && length === (length & 0x7fffffff)); 171 // Don't put properties on their own line if there are only a few. 172 var inlinePropLimit = isSeries ? 8 : 4; 173 var inline = true; 174 var numProps = 0; 175 for (var k in v) { 176 if (!Object.hasOwnProperty.call(v, k)) { continue; } 177 var propValue = v[k]; 178 if ((propValue != null && typeof propValue == 'object') 179 || ++numProps > inlinePropLimit) { 180 inline = false; 181 break; 182 } 183 } 184 // Put the appropriate white-space inside brackets and after commas. 185 function maybeIndent(afterComma) { 186 if (inline) { 187 if (afterComma) { out.push(' '); } 188 } else { 189 out.push('\n'); 190 var nSpaces = depth * 2; 191 while (nSpaces > 0) { 192 var nToPush = Math.min(nSpaces, spaces.length); 193 out.push(spaces.substring(0, nToPush)); 194 nSpaces -= nToPush; 195 } 196 } 197 } 198 var onclick = depth 199 ? ' onclick="return toggleJsonBlock(this, event)"' 200 : ''; 201 // Mark blocks so that we can do expandos on collections. 202 out.push('<span class="json-ext json-block-', depth, 203 depth === 0 || inline ? ' json-nocollapse' : '', 204 '"', onclick, '>', 205 isSeries ? '[' : '{', 206 // Emit link-like ellipses that can serve as a button for 207 // expando-ness. 208 '<span class="json-ell">…</span>', 209 '<span class="json-int">'); 210 ++depth; 211 if (isSeries) { 212 for (var i = 0; i < length; ++i) { 213 if (i) { out.push(','); } 214 maybeIndent(i !== 0); 215 format(v[i]); 216 } 217 } else { 218 var needsComma = false; 219 for (var k in v) { 220 if (!Object.hasOwnProperty.call(v, k)) { continue; } 221 if (needsComma) { 222 out.push(','); 223 } 224 maybeIndent(needsComma); 225 out.push('<span class="json-prop">'); 226 format(String(k)); 227 out.push(': '); 228 format(v[k]); 229 out.push('</span>'); 230 needsComma = true; 231 } 232 } 233 --depth; 234 maybeIndent(false); 235 out.push('</span>', isSeries ? ']' : '}', '</span>'); 236 } 237 } 238} 239 240function displayJson(data, container) { 241 container.innerHTML = formatDataToJsonHTML(data); 242} 243 244function toggleJsonBlock(el, event) { 245 event && event.stopPropagation && event.stopPropagation(); 246 var className = el.className; 247 var classNameCollapsed = className.replace(/\bjson-expanded\b/g, ''); 248 className = className === classNameCollapsed 249 ? className + ' json-expanded' : classNameCollapsed; 250 className = className.replace(/^ +| +$| +( [^ ])/g, "$1"); 251 el.className = className; 252 return false; 253} 254 255function Promise() { 256 if (!(this instanceof Promise)) { return new Promise(); } 257 this.paused = []; 258 this.satisfy = function () { 259 var paused = this.paused; 260console.log('satisfying ' + paused.length); 261 for (var i = 0, n = paused.length; i < n; ++i) { 262 setTimeout(paused[i], 0); 263 } 264 this.paused.length = 0; 265 }; 266} 267Promise.prototype.toString = function () { return "Promise"; }; 268function when(f, var_args) { 269 var unsatisfied = []; 270 for (var i = 1, n = arguments.length; i < n; ++i) { 271 var argument = arguments[i]; 272 if (argument instanceof Promise) { 273 unsatisfied.push(argument); 274 } 275 } 276 var nToWaitFor = unsatisfied.length; 277 if (nToWaitFor) { 278 var pauser = function pauser() { 279 if (!--nToWaitFor) { 280 setTimeout(f, 0); 281 } 282 }; 283 for (var j = 0; j < nToWaitFor; ++j) { 284 unsatisfied[j].paused.push(pauser); 285 } 286 unsatisfied = null; 287 } else { 288 setTimeout(f, 0); 289 } 290} 291 292function newBlankObject() { 293 return (Object.create || Object)(null); 294} 295 296function getOwn(o, k, opt_default) { 297 return Object.hasOwnProperty.call(o, k) ? o[k] : opt_default; 298} 299 300function breadthFirstSearch(start, isEnd, eq, adjacent) { 301 var stack = [{ node: start, next: null }]; 302 while (stack.length) { 303 var candidate = stack.shift(); 304 if (isEnd(candidate.node)) { 305 var path = [candidate.node]; 306 while (candidate.next) { 307 candidate = candidate.next; 308 path.push(candidate.node); 309 } 310 return path; 311 } 312 var adjacentNodes = adjacent(candidate.node); 313 adj: 314 for (var i = 0, n = adjacentNodes.length; i < n; ++i) { 315 var adjacentNode = adjacentNodes[i]; 316 for (var dupe = candidate; dupe; dupe = dupe.next) { 317 if (eq(dupe.node, adjacentNode)) { continue adj; } 318 } 319 stack.push({ node: adjacentNode, next: candidate }); 320 } 321 } 322 return null; 323} 324 325function reverseMultiMap(multimap) { 326 var reverse = newBlankObject(); 327 for (var k in multimap) { 328 if (Object.hasOwnProperty.call(multimap, k)) { 329 var values = multimap[k]; 330 for (var i = 0, n = values.length; i < n; ++i) { 331 var value = values[i]; 332 var reverseKeys = getOwn(reverse, value) || []; 333 reverse[value] = reverseKeys; 334 reverseKeys.push(k); 335 } 336 } 337 } 338 return reverse; 339} 340 341function innerTextOf(element) { 342 function appendTextOf(node, out) { 343 switch (node.nodeType) { 344 case 1: // Element 345 for (var c = node.firstChild; c; c = c.nextSibling) { 346 appendTextOf(c, out); 347 } 348 break; 349 case 3: case 4: case 6: // Text / CDATA / Entity 350 out.push(node.nodeValue); 351 break; 352 } 353 } 354 var buf = []; 355 if (element) { appendTextOf(element, buf); } 356 return buf.join(''); 357} 358 359function sortedMultiMap(mm) { 360 var props = []; 361 for (var k in mm) { 362 if (!Object.hasOwnProperty.call(mm, k)) { continue; } 363 var v = mm[k]; 364 if (v instanceof Array) { 365 v = v.slice(); 366 v.sort(); 367 } 368 props.push([k, v]); 369 } 370 props.sort( 371 function (a, b) { 372 a = a[0]; 373 b = b[0]; 374 if (a < b) { return -1; } 375 if (b < a) { return 1; } 376 return 0; 377 }); 378 var sorted = newBlankObject(); 379 for (var i = 0, n = props.length; i < n; ++i) { 380 var prop = props[i]; 381 sorted[prop[0]] = prop[1]; 382 } 383 return sorted; 384} 385 386function makeSet(strs) { 387 var s = newBlankObject(); 388 for (var i = 0, n = strs.length; i < n; ++i) { 389 s[strs[i]] = s; 390 } 391 return s; 392} 393 394function inSet(s, str) { 395 return s[str] === s; 396} 397 398function elementContainsComment(el) { 399 return elementContainsNodeOfType(el, 8); 400} 401 402function elementContainsText(el) { 403 return elementContainsNodeOfType(el, 3); 404} 405 406function elementContainsNodeOfType(el, nodeType) { 407 if (el) { 408 for (var c = el.firstChild; c; c = c.nextSibling) { 409 if (c.nodeType === nodeType) { return true; } 410 } 411 return false; 412 } 413} 414