| var notify = []; |
| var experimentIdCounter = 0; |
| /** |
| * The questions above are answered by running a bunch of experiments |
| * exhaustively for all combinations of HTML element names. |
| * |
| * @param makeHtmlString takes one or more element names. |
| * Its {@code length} property specifies its arity, and runExperiment |
| * calls it iteratively with every permutation of length element names. |
| * @param checkDom receives the element names passed to makeHtmlString, |
| * an HTML document body created by parsing the HTML from makeHtmlString |
| * and initialResult/return value from last call to checkDom. |
| * @param initialResult the first result value to pass to checkDom. |
| * @param opt_elementNames an array of element names which defaults to |
| * window.elementNames. |
| */ |
| function runExperiment(makeHtmlString, checkDom, initialResult, onResult, |
| opt_elementNames) { |
| var experimentIndex = ++experimentIdCounter; |
| var iframes = document.getElementById('experiment-iframes'); |
| var iframe = document.createElement('iframe'); |
| iframes.appendChild(iframe); |
| |
| var elementNames = opt_elementNames || window.elementNames; |
| |
| var nElements = elementNames.length; |
| var arity = makeHtmlString.length; |
| var nRuns = Math.pow(nElements, arity); |
| var runIndex = 0; |
| var paramIndices = new Array(arity); |
| var paramValues = new Array(arity); |
| for (var i = 0; i < arity; ++i) { |
| paramIndices[i] = 0; |
| paramValues[i] = elementNames[0]; |
| } |
| var exhausted = nRuns === 0; |
| |
| var progressCounterContainer = |
| document.getElementById('experiment-progress-counter'); |
| |
| var startTime = Date.now(); |
| var lastProgressUpdateTime = startTime; |
| |
| var result = initialResult; |
| |
| var progressCounter; |
| if (progressCounterContainer) { |
| progressCounter = document.createElement('li'); |
| progressCounter.style.width = '0'; |
| progressCounterContainer.appendChild(progressCounter); |
| } |
| |
| function advance() { |
| // Advance to next permutation. |
| var i; |
| for (i = arity; --i >= 0;) { |
| if (++paramIndices[i] < nElements) { |
| paramValues[i] = elementNames[paramIndices[i]]; |
| break; |
| } |
| paramIndices[i] = 0; |
| paramValues [i] = elementNames[0]; |
| } |
| ++runIndex; |
| if (progressCounter) { |
| var now = Date.now(); |
| if (now - lastProgressUpdateTime > 250 ) { |
| var ratio = runIndex / nRuns; |
| progressCounter.style.width = (100 * ratio).toFixed(2) + '%'; |
| lastProgressUpdateTime = now; |
| var timeSoFar = now - startTime; |
| if (timeSoFar > 5000) { |
| // Assuming time per run is constant: |
| // total_time / nRuns = time_so_far / runIndex |
| // total_time = time_so_far * nRuns / runIndex |
| // = time_so_far / ratio |
| // eta = total_time - time_so_far |
| // = time_so_far / ratio - time_so_far |
| // = time_so_far * (1/ratio - 1) |
| var eta = timeSoFar * (1 / ratio - 1); |
| progressCounter.innerHTML = eta > 250 |
| ? 'ETA:' + (eta / 1000).toFixed(1) + 's' : ''; |
| } |
| } |
| } |
| exhausted = i < 0; |
| } |
| |
| function step() { |
| var htmlString = null; |
| // Try to generate an HTML string. |
| // The maker can return a nullish value to abort or punt on an experiment, |
| // so we loop until we find work to do. |
| while (!exhausted) { |
| paramValues.length = arity; |
| htmlString = makeHtmlString.apply(null, paramValues); |
| if (htmlString != null) { |
| break; |
| } |
| advance(); |
| } |
| |
| if (htmlString == null) { |
| var endTime = Date.now(); |
| console.log('experiment took %d millis for %d runs', |
| (endTime - startTime), nRuns); |
| if (progressCounter) { |
| setTimeout(function () { |
| iframes.removeChild(iframe); |
| progressCounterContainer.removeChild(progressCounter); |
| }, 250); |
| } |
| onResult(result); |
| } else { |
| var notifyIndex = notify.indexOf(void 0); |
| if (notifyIndex < 0) { notifyIndex = notify.length; } |
| notify[notifyIndex] = function () { |
| notify[notifyIndex] = void 0; |
| |
| // Process result |
| paramValues[arity] = iframe.contentDocument.body; |
| paramValues[arity + 1] = result; |
| result = checkDom.apply(null, paramValues); |
| paramValues.length = arity; |
| |
| // Requeue the next step on the parent frames event queue. |
| setTimeout(function () { advance(); step(); }, 0); |
| }; |
| // Start the iframe parsing its body. |
| iframe.srcdoc = ( |
| '<!doctype html><html><head></head>' |
| + '<body onload="parent.notify[' + notifyIndex + ']()">' |
| + htmlString |
| ); |
| } |
| } |
| step(); |
| } |
| |
| function formatDataToJsonHTML(data) { |
| var out = []; |
| var htmlForNullValue = '<span class="json-kw">null</span>'; |
| var htmlForErrorValue = '<span class="json-kw json-err">null</span>'; |
| var depth = 0; |
| var spaces = ' '; |
| format(data); |
| return out.join(''); |
| |
| function format(v) { |
| if (v == null) { |
| out.push(htmlForNullValue); |
| return; |
| } |
| var t = typeof v; |
| if (t === 'boolean') { |
| out.push('<span class="json-kw">', v, '</span>'); |
| } else if (t === 'number') { |
| if (isFinite(v)) { |
| out.push('<span class="json-val">', v, '</span>'); |
| } else { |
| out.push(htmlForErrorValue); |
| } |
| } else if (t === 'string' || v instanceof String) { |
| var token = JSON.stringify(String(v)); |
| token = token.replace(/&/g, '&').replace(/</g, '<'); |
| out.push('<span class="json-str">', token, '</span>'); |
| } else { |
| var length = v.length; |
| var isSeries = ('number' === typeof length |
| && length === (length & 0x7fffffff)); |
| // Don't put properties on their own line if there are only a few. |
| var inlinePropLimit = isSeries ? 8 : 4; |
| var inline = true; |
| var numProps = 0; |
| for (var k in v) { |
| if (!Object.hasOwnProperty.call(v, k)) { continue; } |
| var propValue = v[k]; |
| if ((propValue != null && typeof propValue == 'object') |
| || ++numProps > inlinePropLimit) { |
| inline = false; |
| break; |
| } |
| } |
| // Put the appropriate white-space inside brackets and after commas. |
| function maybeIndent(afterComma) { |
| if (inline) { |
| if (afterComma) { out.push(' '); } |
| } else { |
| out.push('\n'); |
| var nSpaces = depth * 2; |
| while (nSpaces > 0) { |
| var nToPush = Math.min(nSpaces, spaces.length); |
| out.push(spaces.substring(0, nToPush)); |
| nSpaces -= nToPush; |
| } |
| } |
| } |
| var onclick = depth |
| ? ' onclick="return toggleJsonBlock(this, event)"' |
| : ''; |
| // Mark blocks so that we can do expandos on collections. |
| out.push('<span class="json-ext json-block-', depth, |
| depth === 0 || inline ? ' json-nocollapse' : '', |
| '"', onclick, '>', |
| isSeries ? '[' : '{', |
| // Emit link-like ellipses that can serve as a button for |
| // expando-ness. |
| '<span class="json-ell">…</span>', |
| '<span class="json-int">'); |
| ++depth; |
| if (isSeries) { |
| for (var i = 0; i < length; ++i) { |
| if (i) { out.push(','); } |
| maybeIndent(i !== 0); |
| format(v[i]); |
| } |
| } else { |
| var needsComma = false; |
| for (var k in v) { |
| if (!Object.hasOwnProperty.call(v, k)) { continue; } |
| if (needsComma) { |
| out.push(','); |
| } |
| maybeIndent(needsComma); |
| out.push('<span class="json-prop">'); |
| format(String(k)); |
| out.push(': '); |
| format(v[k]); |
| out.push('</span>'); |
| needsComma = true; |
| } |
| } |
| --depth; |
| maybeIndent(false); |
| out.push('</span>', isSeries ? ']' : '}', '</span>'); |
| } |
| } |
| } |
| |
| function displayJson(data, container) { |
| container.innerHTML = formatDataToJsonHTML(data); |
| } |
| |
| function toggleJsonBlock(el, event) { |
| event && event.stopPropagation && event.stopPropagation(); |
| var className = el.className; |
| var classNameCollapsed = className.replace(/\bjson-expanded\b/g, ''); |
| className = className === classNameCollapsed |
| ? className + ' json-expanded' : classNameCollapsed; |
| className = className.replace(/^ +| +$| +( [^ ])/g, "$1"); |
| el.className = className; |
| return false; |
| } |
| |
| function Promise() { |
| if (!(this instanceof Promise)) { return new Promise(); } |
| this.paused = []; |
| this.satisfy = function () { |
| var paused = this.paused; |
| console.log('satisfying ' + paused.length); |
| for (var i = 0, n = paused.length; i < n; ++i) { |
| setTimeout(paused[i], 0); |
| } |
| this.paused.length = 0; |
| }; |
| } |
| Promise.prototype.toString = function () { return "Promise"; }; |
| function when(f, var_args) { |
| var unsatisfied = []; |
| for (var i = 1, n = arguments.length; i < n; ++i) { |
| var argument = arguments[i]; |
| if (argument instanceof Promise) { |
| unsatisfied.push(argument); |
| } |
| } |
| var nToWaitFor = unsatisfied.length; |
| if (nToWaitFor) { |
| var pauser = function pauser() { |
| if (!--nToWaitFor) { |
| setTimeout(f, 0); |
| } |
| }; |
| for (var j = 0; j < nToWaitFor; ++j) { |
| unsatisfied[j].paused.push(pauser); |
| } |
| unsatisfied = null; |
| } else { |
| setTimeout(f, 0); |
| } |
| } |
| |
| function newBlankObject() { |
| return (Object.create || Object)(null); |
| } |
| |
| function getOwn(o, k, opt_default) { |
| return Object.hasOwnProperty.call(o, k) ? o[k] : opt_default; |
| } |
| |
| function breadthFirstSearch(start, isEnd, eq, adjacent) { |
| var stack = [{ node: start, next: null }]; |
| while (stack.length) { |
| var candidate = stack.shift(); |
| if (isEnd(candidate.node)) { |
| var path = [candidate.node]; |
| while (candidate.next) { |
| candidate = candidate.next; |
| path.push(candidate.node); |
| } |
| return path; |
| } |
| var adjacentNodes = adjacent(candidate.node); |
| adj: |
| for (var i = 0, n = adjacentNodes.length; i < n; ++i) { |
| var adjacentNode = adjacentNodes[i]; |
| for (var dupe = candidate; dupe; dupe = dupe.next) { |
| if (eq(dupe.node, adjacentNode)) { continue adj; } |
| } |
| stack.push({ node: adjacentNode, next: candidate }); |
| } |
| } |
| return null; |
| } |
| |
| function reverseMultiMap(multimap) { |
| var reverse = newBlankObject(); |
| for (var k in multimap) { |
| if (Object.hasOwnProperty.call(multimap, k)) { |
| var values = multimap[k]; |
| for (var i = 0, n = values.length; i < n; ++i) { |
| var value = values[i]; |
| var reverseKeys = getOwn(reverse, value) || []; |
| reverse[value] = reverseKeys; |
| reverseKeys.push(k); |
| } |
| } |
| } |
| return reverse; |
| } |
| |
| function innerTextOf(element) { |
| function appendTextOf(node, out) { |
| switch (node.nodeType) { |
| case 1: // Element |
| for (var c = node.firstChild; c; c = c.nextSibling) { |
| appendTextOf(c, out); |
| } |
| break; |
| case 3: case 4: case 6: // Text / CDATA / Entity |
| out.push(node.nodeValue); |
| break; |
| } |
| } |
| var buf = []; |
| if (element) { appendTextOf(element, buf); } |
| return buf.join(''); |
| } |
| |
| function sortedMultiMap(mm) { |
| var props = []; |
| for (var k in mm) { |
| if (!Object.hasOwnProperty.call(mm, k)) { continue; } |
| var v = mm[k]; |
| if (v instanceof Array) { |
| v = v.slice(); |
| v.sort(); |
| } |
| props.push([k, v]); |
| } |
| props.sort( |
| function (a, b) { |
| a = a[0]; |
| b = b[0]; |
| if (a < b) { return -1; } |
| if (b < a) { return 1; } |
| return 0; |
| }); |
| var sorted = newBlankObject(); |
| for (var i = 0, n = props.length; i < n; ++i) { |
| var prop = props[i]; |
| sorted[prop[0]] = prop[1]; |
| } |
| return sorted; |
| } |
| |
| function makeSet(strs) { |
| var s = newBlankObject(); |
| for (var i = 0, n = strs.length; i < n; ++i) { |
| s[strs[i]] = s; |
| } |
| return s; |
| } |
| |
| function inSet(s, str) { |
| return s[str] === s; |
| } |
| |
| function elementContainsComment(el) { |
| return elementContainsNodeOfType(el, 8); |
| } |
| |
| function elementContainsText(el) { |
| return elementContainsNodeOfType(el, 3); |
| } |
| |
| function elementContainsNodeOfType(el, nodeType) { |
| if (el) { |
| for (var c = el.firstChild; c; c = c.nextSibling) { |
| if (c.nodeType === nodeType) { return true; } |
| } |
| return false; |
| } |
| } |