| <!doctype html> |
| <html> |
| <head> |
| <title>HTML containment</title> |
| <script> |
| if (!Date.now) { Date.now = function () { return +new Date; }; } |
| </script> |
| <script src="html-containment.js"></script> |
| <script> |
| // Extract URL query parameters into options |
| var opts = { |
| // use a short list for quick iteration and debugging |
| shortlist: false, |
| rerun: false |
| }; |
| var cannedData; |
| (function () { |
| location.search.replace( |
| /[?&]([^&=]*)(?:=(?:false|no|([^&]*))(?![^&]))?/ig, |
| function (_, keyEncoded, valueEncoded) { |
| var key = decodeURIComponent(keyEncoded); |
| var value = valueEncoded == null ? "true" |
| : decodeURIComponent(valueEncoded); |
| opts[key] = value; |
| }); |
| |
| if (opts.rerun) { |
| cannedData = newBlankObject(); |
| } else { |
| document.write('<script src="canned-data.js"><\/script>'); |
| } |
| })(); |
| </script> |
| <script> |
| // Includes both conforming and obsolete elements from |
| // http://dev.w3.org/html5/html-author/#index-of-elements |
| // It does not include foreign content. |
| var elementNames = |
| opts.shortlist |
| ? [ |
| 'a', 'font', 'form', 'frameset', 'h1', 'h2', 'iframe', |
| 'img', 'li', 'ol', 'plaintext', 'script', 'select', 'table', 'tbody', |
| 'textarea', 'td', 'tr', 'video', 'xmp' |
| ] |
| : [ |
| 'a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', |
| 'audio', 'b', 'base', 'basefont', 'bb', 'bdo', 'bgsound', 'big', 'blink', |
| 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', |
| 'code', 'col', 'colgroup', 'command', 'datagrid', 'datalist', 'dd', 'del', |
| 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed', |
| 'fieldset', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', |
| 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hr', 'html', 'i', 'iframe', |
| 'img', 'input', 'ins', 'isindex', 'kbd', 'label', 'legend', 'li', 'link', |
| 'listing', 'map', 'mark', 'marquee', 'menu', 'meta', 'meter', 'nav', 'nobr', |
| 'noembed', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', |
| 'output', 'p', 'param', 'plaintext', 'pre', 'progress', 'q', 'rp', 'rt', |
| 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', |
| 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'sup', 'table', |
| 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', |
| 'tt', 'u', 'ul', 'var', 'video', 'wbr', 'xmp', |
| |
| 'xcustom' |
| ]; |
| </script> |
| <style> |
| pre.json { white-space: pre-wrap } |
| .json-kw { color: #800 } |
| .json-str { color: #080 } |
| .json-val { color: #008 } |
| .json-sep { background: white } |
| .json-ell { color: blue } /* ellipses are linky */ |
| |
| /* Collapse inner blocks except on roll-over. */ |
| .json-int { display: none } |
| .json-ext.json-expanded > .json-int, |
| .json-ext.json-nocollapse > .json-int { display: inline } |
| .json-ext.json-nocollapse > .json-ell { display: none } |
| .json-ext.json-expanded > .json-ell { color: transparent } |
| |
| #experiment-progress-counter:empty { display: none } |
| #experiment-progress-counter { |
| width: 25em; |
| display: block; |
| list-style-type: none; |
| -webkit-padding-start: 0; |
| } |
| div #experiment-progress-counter:empty { |
| border-width: 0px solid black; |
| padding: 0 0 0 0; |
| } |
| div #experiment-progress-counter { |
| border:1px solid black; |
| padding: 0 0 2px 2px; |
| } |
| #experiment-progress-counter li { |
| display: block; |
| border: 1px solid black; |
| padding: 2px; |
| margin-top: 2px; |
| height: 1em; |
| background: #ddf; |
| white-space: nowrap; |
| font-size:8pt; |
| } |
| #experiment-iframes iframe { |
| visibility:hidden; |
| width:40em; |
| height:1em; |
| } |
| em { color: #fff; font-weight: bold; background: #800; border: 1px solid #800; padding: 1px } |
| </style> |
| </head> |
| <body> |
| <p> |
| This page tries to exhaustively combine tags for all pairings of HTML elements |
| to answer the following questions about how HTML browsers parse tag soup:</p> |
| <ul> |
| <li><a href="#nests-in-body">Which elements can appear directly in the body of an HTML document?</ad></li> |
| <li><a href="#can-contain">Which elements can nest directly in which other elements?</a></li> |
| <li><a href="#text-content-model">Which elements can contain text content, comments, entities?</a></li> |
| <li><a href="#containment-stack-json">Which elements can be introduced between the body and an element |
| to allow it to nest properly?</a></li> |
| <li>Which elements are implied by which tags? (TODO)</li> |
| <li><a href="#explicit-closers">Which open tags close which other elements?</a></li> |
| <li><a href="#closed-by-close">Which close tags close which elements?</a></li> |
| <li><a href="#closed-by-open">Which open tags close which elements?</a></li> |
| </ul> |
| |
| <p>A <a href="#result-dump">JSON dump</a> |
| of the results is available at the end once running is done.</p> |
| |
| <div><ul id="experiment-progress-counter"></ul></div> |
| |
| <p>A few query parameters affect the behavior of this page:</p> |
| <ul> |
| <li><a href="?rerun"><tt><span class="basename"></span>?rerun</tt></a> — |
| <em style="font-size:66%">¡VERY SLOW!</em> |
| Rerun experiments on the browser intead of using the canned results from Chrome. |
| <li><a href="?rerun&shortlist"><tt><span class="basename"></span>?rerun&shortlist</tt></a> — |
| Rerun experiments on the browser instead of using the canned results from Chrome, |
| but with a short list of elements instead of the full 128+ HTML elements |
| which speeds debugging.</li> |
| <li><a href="?"><tt><span class="basename"></span>?</tt></a> — |
| Quick browsing of canned results from Chrome.</li> |
| </ul> |
| <script>(function () { |
| var basename = location.pathname.replace(/^[\s\S]*\//, ''); |
| function toCss(s) { |
| return ('\x22' |
| + s.replace(/[^\w\-.]/g, function (c) { |
| return '\\' + c.charCodeAt(0).toString(16) + ' '; |
| }) |
| + '\x22'); |
| } |
| document.write('<style>.basename:after { content: ' + toCss(basename) + ' }<\/style>'); |
| }());</script> |
| |
| |
| |
| <!-- Contains iframes that are used to parse HTML since innerHTML parsing differs |
| from regular parsing in many respects. --> |
| <div id="experiment-iframes"></div> |
| |
| <h2 id="nests-in-body">Nests in body</h2> |
| <p>Does a tag <tt><X></tt> directly inside |
| <tt><body>…</body></tt> parse to an element named X |
| directly inside the document body?</p> |
| <pre id="nests-in-body-json" class="json"></pre> |
| <script> |
| var canAppearInBody = getOwn(cannedData, 'canAppearInBody') || new Promise(); |
| (function () { |
| // Generates HTML for the experiment. |
| function nestInBody(elementName) { |
| return '<' + elementName + '></' + elementName + '>'; |
| } |
| // Examines the resulting body to fold a single experiment into the result. |
| function isNestedInBody(elementName, body, result) { |
| result[elementName] = !!( |
| body.firstChild && body.firstChild.nodeName.toLowerCase() === elementName |
| ); |
| return result; |
| } |
| // When the experiment is finished, replace the promise so that we can |
| // kick off experiments that depend on the result of this experiment. |
| function finish(result) { |
| var toSatisfy = canAppearInBody; |
| if (toSatisfy instanceof Promise) { |
| canAppearInBody = result; |
| toSatisfy.satisfy(); |
| } |
| displayJson(result, document.getElementById('nests-in-body-json')) |
| } |
| if (canAppearInBody instanceof Promise) { |
| runExperiment(nestInBody, isNestedInBody, newBlankObject(), finish); |
| } else { |
| finish(canAppearInBody); |
| } |
| }()); |
| </script> |
| |
| <h2 id="can-contain">Containment</h2> |
| <p>For each element, what elements can contain it?</p> |
| <p>E.g., <code>canAppearIn['x'].indexOf('y') >= 0</code> when |
| <code><x><y></y></x></code> parses to |
| an element <tt>x</tt> that contains an element <tt>y</tt> when embedded |
| in an element that can contain <code><x></code>.</p> |
| <h3>Can Contain</h3> |
| <pre class="json" id="can-contain-json"></pre> |
| <h3>Can Appear In</h3> |
| <pre class="json" id="can-appear-in-json"></pre> |
| <h3>Containment stack</h3> |
| <pre class="json" id="containment-stack-json"></pre> |
| <script> |
| // We use promises to allow experiment chaining where one |
| // experiment depends on the results of another. |
| |
| var canContain = getOwn(cannedData, 'canContain') || new Promise(); |
| var canAppearIn = getOwn(cannedData, 'canAppearIn') || new Promise(); |
| // For a given element name, give a stack of elements that can |
| // be validly embedded in body that have the element at the top. |
| var containmentStackFor = new Promise(); |
| |
| // HTML for the elements in the with the body HTML inside the |
| // top-most element. |
| function tagStackToHtml(stack, body) { |
| var stackReverse = stack.slice(); |
| stackReverse.reverse(); |
| return ( |
| '<' + stack.join('><') + '>' |
| + body |
| + '</' + stackReverse.join('></') + '>' |
| ); |
| } |
| |
| (function () { |
| var nNeededLast = Infinity; |
| |
| // We need a function that tells us which elements we need to have on the |
| // open element stack so that we can get the outer element on the stack to |
| // test whether an inner tag leads to an inner element inside it. |
| // For example, to test whether an <a> tag nestes properly in a <td>, we |
| // need to construct <table><tbody><tr><td><a>. |
| // |
| // Knowing what needs to be on the open element stack for <td> requires |
| // knowing what needs to be on the open element stack for <tr>. |
| function containmentStackMaker(canAppearIn) { |
| var memoTable = newBlankObject(); |
| return function (elementName, opt_exclusions) { |
| var memoKey = opt_exclusions |
| ? elementName + ' ' + opt_exclusions.join(' / ') : elementName; |
| |
| if (getOwn(canAppearInBody, elementName)) { return [elementName]; } |
| var prior = getOwn(memoKey, elementName, void 0); |
| if (prior !== void 0) { return prior ? prior.slice() : null; } |
| var empty = []; |
| |
| function end(e) { |
| return getOwn(canAppearInBody, e, false); |
| } |
| function eq (e, f) { return e === f; } |
| function neighbors(e) { |
| var neighbors = getOwn(canAppearIn, e, empty); |
| if (opt_exclusions) { |
| var exclusions = makeSet(opt_exclusions); |
| var included = null; |
| for (var i = 0, n = neighbors.length; i < n; ++i) { |
| var neighbor = neighbors[i]; |
| if (inSet(exclusions, neighbor)) { |
| if (!included) { included = neighbors.slice(0, i); } |
| } else if (included) { |
| included.push(neighbor); |
| } |
| } |
| if (included) { neighbors = included; } |
| } |
| return neighbors; |
| } |
| var result = breadthFirstSearch(elementName, end, eq, neighbors) || null; |
| memoTable[memoKey] = result; |
| return result ? result.slice() : null; |
| }; |
| } |
| |
| function run(result) { |
| |
| function makeContainerHtmlString(outer, inner) { |
| if (neededSet[outer] !== neededSet) { return null; } |
| // We try to assemble a stack of elements that can contain outer before |
| // checking whether it can contain inner. |
| // If we cannot, we punt so that we can retry later after we've fleshed |
| // out more of canAppearIn. |
| var stack = containmentStack(outer); |
| if (!stack) { return null; } |
| stack.push(inner); |
| return tagStackToHtml(stack, ''); |
| } |
| |
| function checkCanContain(outer, inner, body, canContain) { |
| var outerEls = body.getElementsByTagName(outer); |
| if (outerEls.length) { |
| var containees = getOwn(canContain, outer) || []; |
| canContain[outer] = containees; |
| var outerEl = outerEls[0]; |
| var firstChild = outerEl.firstChild; |
| if (((firstChild && firstChild.nodeName.toLowerCase() === inner) |
| || outerEl.getElementsByTagName(inner).length) |
| && containees.indexOf(inner) < 0) { |
| containees.push(inner); |
| } |
| } |
| return canContain; |
| } |
| |
| var elementNamesNeeded = []; |
| for (var i = 0, n = elementNames.length; i < n; ++i) { |
| var elementName = elementNames[i]; |
| if (!Object.hasOwnProperty.call(result, elementName)) { |
| elementNamesNeeded.push(elementName); |
| } |
| } |
| console.log('nNeededLast=%s, nNeeded=%d, result=%o', |
| nNeededLast, elementNamesNeeded.length, result); |
| if (elementNamesNeeded.length === nNeededLast) { |
| // We made no progress last run. |
| console.log('cannot place ' + elementNamesNeeded); |
| elementNamesNeeded.length = 0; |
| } |
| |
| var containmentStack = containmentStackMaker(reverseMultiMap(result)); |
| |
| var neededSet = newBlankObject(); |
| for (var i = elementNamesNeeded.length; --i >= 0;) { |
| neededSet[elementNamesNeeded[i]] = neededSet; |
| } |
| |
| if (elementNamesNeeded.length) { |
| nNeededLast = elementNamesNeeded.length; |
| return runExperiment( |
| makeContainerHtmlString, checkCanContain, result, run, |
| elementNames); |
| } else { |
| finishCanContain(result); |
| return result; |
| } |
| } |
| |
| function finishCanContain(result) { |
| var toSatisfy = canContain; |
| if (toSatisfy instanceof Promise) { |
| canContain = sortedMultiMap(result); |
| toSatisfy.satisfy(); |
| } |
| displayJson(canContain, document.getElementById('can-contain-json')); |
| } |
| |
| if (canContain instanceof Promise) { |
| when(function () { run(newBlankObject()); }, canAppearInBody); |
| } else { |
| finishCanContain(canContain); |
| } |
| |
| function reverseMap() { |
| var toSatisfy = canAppearIn; |
| if (toSatisfy instanceof Promise) { |
| canAppearIn = sortedMultiMap(reverseMultiMap(canContain)); |
| toSatisfy.satisfy(); |
| } |
| displayJson(canAppearIn, document.getElementById('can-appear-in-json')); |
| toSatisfy = containmentStackFor; |
| |
| containmentStackFor = containmentStackMaker(canAppearIn); |
| toSatisfy.satisfy(); |
| } |
| |
| when(function () { reverseMap(); }, canContain); |
| |
| function mapStacks() { |
| var containmentStackMap = newBlankObject(); |
| for (var i = 0, n = elementNames.length; i < n; ++i) { |
| var elementName = elementNames[i]; |
| var stack = containmentStackFor(elementName); |
| if (stack) { --stack.length; } |
| containmentStackMap[elementName] = stack; |
| } |
| displayJson(containmentStackMap, |
| document.getElementById('containment-stack-json')); |
| } |
| when(mapStacks, containmentStackFor); |
| }()); |
| </script> |
| |
| <h2 id="text-content-model">Text and comment content</h2> |
| |
| <p>Tests which elements can contain a non-whitespace text node and which can |
| contain comments or other non-text elements as a result of parsing.</p> |
| <p><code>textContentModel['x'].text</code> is true when |
| <code><x>text</x></code> parses to an X element containing |
| a text node.</p> |
| <p><code>textContentModel['x'].comments</code> is true when |
| <code><x><!--comment--></x></code> parses to an X element |
| containing a comment node.</p> |
| <p><code>textContentModel['x'].xml</code> is true when |
| <code><x>&amp;<![[CDATA&]]>;</x></code> parses to an X |
| element contains text nodes that normalize to <code>&&</code>.</p> |
| <p><code>textContentModel['x'].raw</code> is true when |
| <code><x><br></x></code> parses to an X element |
| containing a text node.</p> |
| <p><code>textContentModel['x'].entities</code> is true when |
| <code><x>&amp;;</x></code> parses to an X element |
| containing a text node <tt>&amp;</tt>.</p> |
| <pre class="json" id="text-content-model-json"></pre> |
| <script> |
| var textContentModel = getOwn(cannedData, 'textContentModel') || new Promise(); |
| (function () { |
| function run() { |
| function makeHtmlStringWithText(elementName) { |
| var stack = containmentStackFor(elementName); |
| if (stack == null) { return null; } |
| return tagStackToHtml( |
| stack, '/*1&2<![CDATA[&]]>3<!---->4<br>5*/'); |
| } |
| function checkText(elementName, body, result) { |
| var el = body.getElementsByTagName(elementName)[0]; |
| var text = innerTextOf(el); |
| var model = newBlankObject(); |
| switch (text) { |
| case '': |
| if (elementContainsComment(el)) { model.comments = true; } |
| break; |
| case '/*1&2345*/': // CDATA section treated as "bogus comment" |
| model.text = model.entities = model.comments = true; |
| break; |
| case '/*1&2&345*/': // CDATA section treated as per XML |
| model.text = model.entities = model.xml = model.comments = true; |
| break; |
| case '/*1&2<![CDATA[&]]>3<!---->4<br>5*/': // '<' is raw |
| model.text = model.entities = model.raw = true; |
| break; |
| case '/*1&2<![CDATA[&]]>3<!---->4<br>5*/': // '<' and '&' raw |
| model.text = model.raw = true; |
| break; |
| case '/*1&2<![CDATA[&]]>3<!---->4<br>5*/</' + elementName + '>': |
| // </plaintext> does not close <plaintext> |
| model.text = model.raw = model.unended = true; |
| break; |
| default: |
| console.log('unexpected text `%s` in %s', text, elementName); |
| } |
| result[elementName] = sortedMultiMap(model); |
| return result; |
| } |
| |
| runExperiment(makeHtmlStringWithText, checkText, newBlankObject(), finish); |
| } |
| |
| function finish(result) { |
| var toSatisfy = textContentModel; |
| if (toSatisfy instanceof Promise) { |
| textContentModel = sortedMultiMap(result); |
| toSatisfy.satisfy(); |
| } |
| displayJson(textContentModel, |
| document.getElementById('text-content-model-json')); |
| } |
| |
| if (textContentModel instanceof Promise) { |
| when(run, containmentStackFor); |
| } else { |
| finish(textContentModel); |
| } |
| }()); |
| </script> |
| |
| <h2>Tag Closers</h2> |
| <h3 id="explicit-closers">Explicit closers</h3> |
| <p>Are there any close tags besides the tag name itself that close the tag?</p> |
| <pre class="json" id="explicit-closers-json"></pre> |
| <script> |
| var explicitClosers = getOwn(cannedData, 'explicitClosers') || new Promise(); |
| (function () { |
| function run() { |
| var contentForElement = newBlankObject(); |
| function nestableContent(openTag, excludedTag) { |
| var content = getOwn(contentForElement, openTag); |
| if (content === undefined) { |
| var tcm = getOwn(textContentModel, openTag); |
| if (tcm && tcm.text) { |
| content = '#text'; |
| } else { |
| content = getOwn(canContain, openTag, null); |
| } |
| contentForElement[openTag] = content; |
| } |
| // arrays are element names |
| if (content instanceof Array) { |
| for (var i = 0, n = content.length; i < n; ++i) { |
| var tag = content[i]; |
| if (tag === openTag || tag === excludedTag) { continue; } |
| return tag; |
| } |
| return null; |
| } else { |
| return content; |
| } |
| } |
| |
| function makeHtmlString(openTag, closeTag) { |
| if (openTag === closeTag) { return null; } |
| var stack = containmentStackFor(openTag, [closeTag]); |
| if (stack == null) { return null; } |
| if (closeTag === 'body' || closeTag === 'html') { |
| return null; |
| } |
| var content = nestableContent(openTag, closeTag); |
| if (content === null) { return null; } |
| if (content !== '#text') { |
| content = '<' + content + '></' + content + '>'; |
| } |
| return tagStackToHtml(stack, '</' + closeTag + '>' + content); |
| } |
| |
| function check(openTag, closeTag, body, result) { |
| var content = nestableContent(openTag, closeTag); |
| var element = body.getElementsByTagName(openTag)[0]; |
| if (element) { |
| var closed = (content === '#text') |
| ? innerTextOf(element) === '' |
| : !element.getElementsByTagName(content).length; |
| if (closed) { |
| var closeTags = getOwn(result, openTag) || []; |
| closeTags.push(closeTag); |
| result[openTag] = closeTags; |
| } |
| } |
| return result; |
| } |
| |
| runExperiment(makeHtmlString, check, newBlankObject(), finish); |
| } |
| |
| function finish(result) { |
| var toSatisfy = explicitClosers; |
| if (toSatisfy instanceof Promise) { |
| explicitClosers = sortedMultiMap(result); |
| toSatisfy.satisfy(); |
| } |
| displayJson(explicitClosers, |
| document.getElementById('explicit-closers-json')); |
| } |
| |
| if (explicitClosers instanceof Promise) { |
| when(run, containmentStackFor, textContentModel); |
| } else { |
| finish(explicitClosers); |
| } |
| }()); |
| </script> |
| |
| |
| <h3 id="closed-by-open">Open tags close which elements</h3> |
| <p>Which open tags close the element when embedded between it and content it |
| could otherwise contain?</p> |
| <p>Which <code>C</code> close <code>T</code> in |
| <code><T><C>X</C></T></code> |
| leading to X being a sibling of the element T instead of its child as it would |
| be if parsed as <code><T>X</T></code>. |
| <pre class="json" id="closed-by-open-json"></pre> |
| <script> |
| var closedOnOpen = getOwn(cannedData, 'closedOnOpen') || new Promise(); |
| (function () { |
| function run() { |
| function makeHtmlString(outer, inner) { |
| if (textContentModel[outer] && textContentModel[outer].comments |
| && !(textContentModel[inner] && textContentModel[inner].unended)) { |
| var stack = containmentStackFor(outer, [inner]); |
| if (stack) { |
| // <outer><inner></inner><!--After inner --></outer> |
| return tagStackToHtml( |
| stack, '<' + inner + '></' + inner + '><!-- After inner -->'); |
| } |
| } |
| return null; |
| } |
| |
| function check(outer, inner, body, result) { |
| var outerEl = body.getElementsByTagName(outer)[0]; |
| var hasComment = elementContainsComment(outerEl); |
| var closers = getOwn(result, outer) || []; |
| if (!hasComment) { |
| closers.push(inner); |
| } |
| result[outer] = closers; |
| return result; |
| } |
| |
| runExperiment(makeHtmlString, check, newBlankObject(), finish); |
| } |
| |
| function finish(result) { |
| var toSatisfy = closedOnOpen; |
| if (toSatisfy instanceof Promise) { |
| closedOnOpen = sortedMultiMap(result); |
| toSatisfy.satisfy(); |
| } |
| displayJson(closedOnOpen, |
| document.getElementById('closed-by-open-json')); |
| } |
| |
| if (closedOnOpen instanceof Promise) { |
| when(run, containmentStackFor, textContentModel, canContain); |
| } else { |
| finish(closedOnOpen); |
| } |
| }()); |
| </script> |
| |
| |
| <h3 id="closed-by-close">Close tags close which elements</h3> |
| <p>Which <code>C</code> close <code>T</code> in |
| <code><C><T></C>X</T></code> |
| leading to X being a sibling of the element T instead of its child as it |
| would be if parsed as <code><T>X</T></code>. |
| <pre class="json" id="closed-by-close-json"></pre> |
| <script> |
| var closedOnClose = getOwn(cannedData, 'closedOnClose') || new Promise(); |
| (function () { |
| function run() { |
| function makeHtmlString(outer, inner) { |
| var outerTc = textContentModel[outer]; |
| var innerTc = textContentModel[inner]; |
| if (outerTc && innerTc && outerTc.comments && innerTc.comments) { |
| var stack = containmentStackFor(outer, [inner]); |
| if (stack) { |
| --stack.length; // strip outer. |
| // <outer><inner></outer><!--X--></inner> |
| return tagStackToHtml( |
| stack, |
| '<' + outer + '><' + inner + '>' |
| + '</' + outer + '><!--X--></' + inner + '>'); |
| } |
| } |
| return null; |
| } |
| |
| function check(outer, inner, body, result) { |
| var innerEl = body.getElementsByTagName(inner)[0]; |
| var closers = getOwn(result, inner) || []; |
| if (!elementContainsComment(innerEl)) { |
| closers.push(outer); |
| } |
| result[inner] = closers; |
| return result; |
| } |
| |
| runExperiment(makeHtmlString, check, newBlankObject(), finish); |
| } |
| |
| function finish(result) { |
| var toSatisfy = closedOnClose; |
| if (toSatisfy instanceof Promise) { |
| closedOnClose = sortedMultiMap(result); |
| toSatisfy.satisfy(); |
| } |
| displayJson(closedOnClose, |
| document.getElementById('closed-by-close-json')); |
| } |
| |
| if (closedOnClose instanceof Promise) { |
| when(run, containmentStackFor, textContentModel, canContain); |
| } else { |
| finish(closedOnClose); |
| } |
| }()); |
| </script> |
| |
| <h2 id="result-dump">JSON Dump</h2> |
| <p id="working"><em>working</em></p> |
| <script> |
| var fullJson = { |
| "canAppearInBody": canAppearInBody, |
| "canContain": canContain, |
| "canAppearIn": canAppearIn, |
| "containmentStackFor": containmentStackFor, |
| "textContentModel": textContentModel, |
| "explicitClosers": explicitClosers, |
| "closedOnOpen": closedOnOpen, |
| "closedOnClose": closedOnClose |
| }; |
| |
| (function () { |
| |
| function run() { |
| for (var k in fullJson) { |
| if (fullJson.hasOwnProperty(k)) { |
| fullJson[k] = window[k]; |
| } |
| } |
| |
| var textarea = document.createElement('textarea'); |
| textarea.setAttribute('cols', '80'); |
| textarea.setAttribute('rows', '20'); |
| textarea.setAttribute('readonly', 'readonly'); |
| textarea.onclick = function () { textarea.select(); }; |
| textarea.value = JSON.stringify(fullJson); |
| var resultDumpHeader = document.getElementById('result-dump'); |
| resultDumpHeader.parentNode.insertBefore( |
| textarea, resultDumpHeader.nextSibling); |
| var workingNote = document.getElementById('working'); |
| workingNote.parentNode.removeChild(workingNote); |
| } |
| |
| var whenArgs = [run]; |
| for (var k in fullJson) { |
| if (fullJson.hasOwnProperty(k)) { |
| whenArgs.push(fullJson[k]); |
| } |
| } |
| |
| when.apply(null, whenArgs); |
| }()); |
| </script> |
| |
| </body> |
| </html> |