You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
725 lines
25 KiB
725 lines
25 KiB
<!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>
|