blob: 1f8f6454e40f9d70c48ee3018e6290bed6deb2d8 [file] [log] [blame]
// Copyright (c) 2011, Mike Samuel
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// Neither the name of the OWASP nor the names of its contributors may
// be used to endorse or promote products derived from this software
// without specific prior written permission.
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
// ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
package org.owasp.html;
import java.util.List;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
/**
* Wraps an HTML stream event receiver to fill in missing close tags.
* If the balancer is given the HTML {@code <p>1<p>2}, the wrapped receiver will
* see events equivalent to {@code <p>1</p><p>2</p>}.
*
* @author Mike Samuel <mikesamuel@gmail.com>
*/
@TCB
public class TagBalancingHtmlStreamEventReceiver
implements HtmlStreamEventReceiver {
private final HtmlStreamEventReceiver underlying;
private int nestingLimit = Integer.MAX_VALUE;
private final List<ElementContainmentInfo> openElements
= Lists.newArrayList();
public TagBalancingHtmlStreamEventReceiver(
HtmlStreamEventReceiver underlying) {
this.underlying = underlying;
}
public void setNestingLimit(int limit) {
if (openElements.size() > limit) {
throw new IllegalStateException();
}
this.nestingLimit = limit;
}
public void openDocument() {
underlying.openDocument();
}
public void closeDocument() {
for (int i = Math.min(nestingLimit, openElements.size()); --i >= 0;) {
underlying.closeTag(openElements.get(i).elementName);
}
openElements.clear();
underlying.closeDocument();
}
public void openTag(String elementName, List<String> attrs) {
String canonElementName = HtmlLexer.canonicalName(elementName);
ElementContainmentInfo elInfo = ELEMENT_CONTAINMENT_RELATIONSHIPS.get(
canonElementName);
// Treat unrecognized tags as void, but emit closing tags in closeTag().
if (elInfo == null) {
if (openElements.size() < nestingLimit) {
underlying.openTag(elementName, attrs);
}
return;
}
prepareForContent(elInfo);
if (openElements.size() < nestingLimit) {
underlying.openTag(elInfo.elementName, attrs);
}
if (!elInfo.isVoid) {
openElements.add(elInfo);
}
}
private void prepareForContent(ElementContainmentInfo elInfo) {
int nOpen = openElements.size();
if (nOpen != 0) {
ElementContainmentInfo top = openElements.get(nOpen - 1);
if ((top.contents & elInfo.types) == 0) {
ElementContainmentInfo blockContainerChild = top.blockContainerChild;
// Open implied elements, such as list-items and table cells & rows.
if (blockContainerChild != null
&& (blockContainerChild.contents & elInfo.types) != 0) {
underlying.openTag(
blockContainerChild.elementName, Lists.<String>newArrayList());
openElements.add(blockContainerChild);
top = blockContainerChild;
++nOpen;
}
}
// Close all the elements that cannot contain the element to open.
List<ElementContainmentInfo> toResumeInReverse = null;
while (true) {
if ((top.contents & elInfo.types) != 0) { break; }
if (openElements.size() < nestingLimit) {
underlying.closeTag(top.elementName);
}
openElements.remove(--nOpen);
if (top.resumable) {
if (toResumeInReverse == null) {
toResumeInReverse = Lists.newArrayList();
}
toResumeInReverse.add(top);
}
if (nOpen == 0) { break; }
top = openElements.get(nOpen - 1);
}
if (toResumeInReverse != null) {
resume(toResumeInReverse);
}
}
}
public void closeTag(String elementName) {
String canonElementName = HtmlLexer.canonicalName(elementName);
ElementContainmentInfo elInfo = ELEMENT_CONTAINMENT_RELATIONSHIPS.get(
canonElementName);
if (elInfo == null) { // Allow unrecognized end tags through.
if (openElements.size() < nestingLimit) {
underlying.closeTag(elementName);
}
return;
}
int index = openElements.lastIndexOf(elInfo);
// Let any of </h1>, </h2>, ... close other header tags.
if (isHeaderElementName(canonElementName)) {
for (int i = openElements.size(), limit = index + 1; -- i >= limit;) {
ElementContainmentInfo openEl = openElements.get(i);
if (isHeaderElementName(openEl.elementName)) {
elInfo = openEl;
index = i;
canonElementName = openEl.elementName;
break;
}
}
}
if (index < 0) {
return; // Don't close unopened tags.
}
// Ensure that index is in the scope of closeable elements.
// This approximates the "has an element in *** scope" predicates defined at
// http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html
// #has-an-element-in-the-specific-scope
int blockingScopes = elInfo.blockedByScopes;
for (int i = openElements.size(); --i > index;) {
if ((openElements.get(i).inScopes & blockingScopes) != 0) {
return;
}
}
int last = openElements.size();
// Close all the elements that cannot contain the element to open.
List<ElementContainmentInfo> toResumeInReverse = null;
while (--last > index) {
ElementContainmentInfo unclosed = openElements.remove(last);
if (last + 1 < nestingLimit) {
underlying.closeTag(unclosed.elementName);
}
if (unclosed.resumable) {
if (toResumeInReverse == null) {
toResumeInReverse = Lists.newArrayList();
}
toResumeInReverse.add(unclosed);
}
}
if (openElements.size() < nestingLimit) {
underlying.closeTag(elInfo.elementName);
}
openElements.remove(index);
if (toResumeInReverse != null) {
resume(toResumeInReverse);
}
}
private void resume(List<ElementContainmentInfo> toResumeInReverse) {
for (ElementContainmentInfo toResume : toResumeInReverse) {
// TODO: If resuming of things other than plain formatting tags like <b>
// and <i>, then we need to store the attributes for resumable tags so
// that we can resume with the appropriate attributes.
if (openElements.size() < nestingLimit) {
underlying.openTag(toResume.elementName, Lists.<String>newArrayList());
}
openElements.add(toResume);
}
}
private static final long HTML_SPACE_CHAR_BITMASK =
(1L << ' ')
| (1L << '\t')
| (1L << '\n')
| (1L << '\u000c')
| (1L << '\r');
public static boolean isInterElementWhitespace(String text) {
int n = text.length();
for (int i = 0; i < n; ++i) {
int ch = text.charAt(i);
if (ch > 0x20 || (HTML_SPACE_CHAR_BITMASK & (1L << ch)) == 0) {
return false;
}
}
return true;
}
public void text(String text) {
if (!isInterElementWhitespace(text)) {
prepareForContent(ElementContainmentRelationships.CHARACTER_DATA_ONLY);
}
if (openElements.size() < nestingLimit) {
underlying.text(text);
}
}
private static boolean isHeaderElementName(String canonElementName) {
return canonElementName.length() == 2 && canonElementName.charAt(0) == 'h'
&& canonElementName.charAt(1) <= '9';
}
@Immutable
private static final class ElementContainmentInfo {
final String elementName;
/**
* True if the adoption agency algorithm allows an element to be resumed
* after a mis-nested end tag closes it.
* E.g. in {@code <b>Foo<i>Bar</b>Baz</i>} the {@code <i>} element is
* resumed after the {@code <b>} element is closed.
*/
final boolean resumable;
/** A set of bits of element groups into which the element falls. */
final int types;
/** The type of elements that an element can contain. */
final int contents;
/** True if the element has no content -- not even text content. */
final boolean isVoid;
/** A legal child of this node that can contain block content. */
final @Nullable ElementContainmentInfo blockContainerChild;
/** A bit set of close tag scopes that block this element's close tags. */
final int blockedByScopes;
/** A bit set of scopes groups into which this element falls. */
final int inScopes;
ElementContainmentInfo(
String elementName, boolean resumable, int types, int contents,
@Nullable ElementContainmentInfo blockContainerChild,
int inScopes) {
this.elementName = elementName;
this.resumable = resumable;
this.types = types;
this.contents = contents;
this.isVoid = contents == 0
&& HtmlTextEscapingMode.isVoidElement(elementName);
this.blockContainerChild = blockContainerChild;
this.blockedByScopes =
ElementContainmentRelationships.CloseTagScope.ALL & ~inScopes;
this.inScopes = inScopes;
}
@Override public String toString() {
return "<" + elementName + ">";
}
}
static final ImmutableMap<String, ElementContainmentInfo>
ELEMENT_CONTAINMENT_RELATIONSHIPS
= new ElementContainmentRelationships().toMap();
private static class ElementContainmentRelationships {
private enum ElementGroup {
BLOCK,
INLINE,
INLINE_MINUS_A,
MIXED,
TABLE_CONTENT,
HEAD_CONTENT,
TOP_CONTENT,
AREA_ELEMENT,
FORM_ELEMENT,
LEGEND_ELEMENT,
LI_ELEMENT,
DL_PART,
P_ELEMENT,
OPTIONS_ELEMENT,
OPTION_ELEMENT,
PARAM_ELEMENT,
TABLE_ELEMENT,
TR_ELEMENT,
TD_ELEMENT,
COL_ELEMENT,
CHARACTER_DATA,
;
}
/**
* An identifier for one of the "has a *** element in scope" predicates
* used by HTML5 to decide when a close tag implicitly closes tags above
* the target element on the open element stack.
*/
private enum CloseTagScope {
COMMON,
BUTTON,
LIST_ITEM,
TABLE,
;
static final int ALL = (1 << values().length) - 1;
}
private static int elementGroupBits(ElementGroup a) {
return 1 << a.ordinal();
}
private static int elementGroupBits(
ElementGroup a, ElementGroup b) {
return (1 << a.ordinal()) | (1 << b.ordinal());
}
private static int elementGroupBits(
ElementGroup a, ElementGroup b, ElementGroup c) {
return (1 << a.ordinal()) | (1 << b.ordinal()) | (1 << c.ordinal());
}
private static int elementGroupBits(
ElementGroup... bits) {
int bitField = 0;
for (ElementGroup bit : bits) {
bitField |= (1 << bit.ordinal());
}
return bitField;
}
private static int scopeBits(CloseTagScope a) {
return 1 << a.ordinal();
}
private static int scopeBits(
CloseTagScope a, CloseTagScope b, CloseTagScope c) {
return (1 << a.ordinal()) | (1 << b.ordinal()) | (1 << c.ordinal());
}
private ImmutableMap.Builder<String, ElementContainmentInfo> definitions
= ImmutableMap.builder();
private ElementContainmentInfo defineElement(
String elementName, boolean resumable, int types, int contentTypes) {
return defineElement(elementName, resumable, types, contentTypes, null);
}
private ElementContainmentInfo defineElement(
String elementName, boolean resumable, int types, int contentTypes,
int inScopes) {
return defineElement(
elementName, resumable, types, contentTypes, null, inScopes);
}
private ElementContainmentInfo defineElement(
String elementName, boolean resumable, int types, int contentTypes,
@Nullable ElementContainmentInfo blockContainer) {
return defineElement(
elementName, resumable, types, contentTypes, blockContainer, 0);
}
private ElementContainmentInfo defineElement(
String elementName, boolean resumable, int types, int contentTypes,
@Nullable ElementContainmentInfo blockContainer, int inScopes) {
ElementContainmentInfo info = new ElementContainmentInfo(
elementName, resumable, types, contentTypes, blockContainer,
inScopes);
definitions.put(elementName, info);
return info;
}
private ImmutableMap<String, ElementContainmentInfo> toMap() {
return definitions.build();
}
{
defineElement(
"a", false, elementGroupBits(
ElementGroup.INLINE
), elementGroupBits(
ElementGroup.INLINE_MINUS_A
));
defineElement(
"abbr", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"acronym", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"address", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.INLINE, ElementGroup.P_ELEMENT
));
defineElement(
"applet", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE,
ElementGroup.PARAM_ELEMENT
), scopeBits(
CloseTagScope.COMMON, CloseTagScope.BUTTON,
CloseTagScope.LIST_ITEM
));
defineElement(
"area", false, elementGroupBits(ElementGroup.AREA_ELEMENT), 0);
defineElement(
"audio", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), 0);
defineElement(
"b", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"base", false, elementGroupBits(ElementGroup.HEAD_CONTENT), 0);
defineElement(
"basefont", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), 0);
defineElement(
"bdi", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"bdo", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"big", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"blink", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"blockquote", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
));
defineElement(
"body", false, elementGroupBits(
ElementGroup.TOP_CONTENT
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
));
defineElement(
"br", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), 0);
defineElement(
"button", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
), scopeBits(CloseTagScope.BUTTON));
defineElement(
"canvas", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"caption", false, elementGroupBits(
ElementGroup.TABLE_CONTENT
), elementGroupBits(
ElementGroup.INLINE
), scopeBits(
CloseTagScope.COMMON, CloseTagScope.BUTTON,
CloseTagScope.LIST_ITEM
));
defineElement(
"center", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
));
defineElement(
"cite", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"code", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"col", false, elementGroupBits(
ElementGroup.TABLE_CONTENT, ElementGroup.COL_ELEMENT
), 0);
defineElement(
"colgroup", false, elementGroupBits(
ElementGroup.TABLE_CONTENT
), elementGroupBits(
ElementGroup.COL_ELEMENT
));
ElementContainmentInfo DD = defineElement(
"dd", false, elementGroupBits(
ElementGroup.DL_PART
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
));
defineElement(
"del", true, elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE,
ElementGroup.MIXED
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
));
defineElement(
"dfn", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"dir", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.LI_ELEMENT
));
defineElement(
"div", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
));
defineElement(
"dl", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.DL_PART
),
DD);
defineElement(
"dt", false, elementGroupBits(
ElementGroup.DL_PART
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"em", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"fieldset", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE,
ElementGroup.LEGEND_ELEMENT
));
defineElement(
"font", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"form", false, elementGroupBits(
ElementGroup.BLOCK, ElementGroup.FORM_ELEMENT
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE,
ElementGroup.INLINE_MINUS_A, ElementGroup.TR_ELEMENT,
ElementGroup.TD_ELEMENT
));
defineElement(
"h1", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"h2", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"h3", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"h4", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"h5", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"h6", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"head", false, elementGroupBits(
ElementGroup.TOP_CONTENT
), elementGroupBits(
ElementGroup.HEAD_CONTENT
));
defineElement(
"hr", false, elementGroupBits(ElementGroup.BLOCK), 0);
defineElement(
"html", false, 0, elementGroupBits(ElementGroup.TOP_CONTENT),
CloseTagScope.ALL);
defineElement(
"i", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"iframe", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
));
defineElement(
"img", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), 0);
defineElement(
"input", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), 0);
defineElement(
"ins", true, elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
));
defineElement(
"isindex", false, elementGroupBits(ElementGroup.INLINE), 0);
defineElement(
"kbd", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"label", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"legend", false, elementGroupBits(
ElementGroup.LEGEND_ELEMENT
), elementGroupBits(
ElementGroup.INLINE
));
ElementContainmentInfo LI = defineElement(
"li", false, elementGroupBits(
ElementGroup.LI_ELEMENT
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
));
defineElement(
"link", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.HEAD_CONTENT
), 0);
defineElement(
"listing", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"map", false, elementGroupBits(
ElementGroup.INLINE
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.AREA_ELEMENT
));
defineElement(
"meta", false, elementGroupBits(ElementGroup.HEAD_CONTENT), 0);
defineElement(
"nobr", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"noframes", false, elementGroupBits(
ElementGroup.BLOCK, ElementGroup.TOP_CONTENT
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE,
ElementGroup.TOP_CONTENT
));
defineElement(
"noscript", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
));
defineElement(
"object", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A,
ElementGroup.HEAD_CONTENT
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE,
ElementGroup.PARAM_ELEMENT
), scopeBits(
CloseTagScope.COMMON, CloseTagScope.BUTTON,
CloseTagScope.LIST_ITEM
));
defineElement(
"ol", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.LI_ELEMENT
),
LI,
scopeBits(CloseTagScope.LIST_ITEM));
defineElement(
"optgroup", false, elementGroupBits(
ElementGroup.OPTIONS_ELEMENT
), elementGroupBits(
ElementGroup.OPTIONS_ELEMENT
));
defineElement(
"option", false, elementGroupBits(
ElementGroup.OPTIONS_ELEMENT, ElementGroup.OPTION_ELEMENT
), elementGroupBits(
ElementGroup.CHARACTER_DATA
));
defineElement(
"p", false, elementGroupBits(
ElementGroup.BLOCK, ElementGroup.P_ELEMENT
), elementGroupBits(
ElementGroup.INLINE, ElementGroup.TABLE_ELEMENT
));
defineElement(
"param", false, elementGroupBits(ElementGroup.PARAM_ELEMENT), 0);
defineElement(
"pre", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"q", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"s", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"samp", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"script", false, elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE,
ElementGroup.INLINE_MINUS_A, ElementGroup.MIXED,
ElementGroup.TABLE_CONTENT, ElementGroup.HEAD_CONTENT,
ElementGroup.TOP_CONTENT, ElementGroup.AREA_ELEMENT,
ElementGroup.FORM_ELEMENT, ElementGroup.LEGEND_ELEMENT,
ElementGroup.LI_ELEMENT, ElementGroup.DL_PART,
ElementGroup.P_ELEMENT, ElementGroup.OPTIONS_ELEMENT,
ElementGroup.OPTION_ELEMENT, ElementGroup.PARAM_ELEMENT,
ElementGroup.TABLE_ELEMENT, ElementGroup.TR_ELEMENT,
ElementGroup.TD_ELEMENT, ElementGroup.COL_ELEMENT
), elementGroupBits(
ElementGroup.CHARACTER_DATA));
defineElement(
"select", false, elementGroupBits(
ElementGroup.INLINE
), elementGroupBits(
ElementGroup.OPTIONS_ELEMENT
));
defineElement(
"small", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"span", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"strike", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"strong", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"style", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.HEAD_CONTENT
), elementGroupBits(
ElementGroup.CHARACTER_DATA
));
defineElement(
"sub", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"sup", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"table", false, elementGroupBits(
ElementGroup.BLOCK, ElementGroup.TABLE_ELEMENT
), elementGroupBits(
ElementGroup.TABLE_CONTENT, ElementGroup.FORM_ELEMENT
), CloseTagScope.ALL);
defineElement(
"tbody", false, elementGroupBits(
ElementGroup.TABLE_CONTENT
), elementGroupBits(
ElementGroup.TR_ELEMENT
));
ElementContainmentInfo TD = defineElement(
"td", false, elementGroupBits(
ElementGroup.TD_ELEMENT
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
), scopeBits(
CloseTagScope.COMMON, CloseTagScope.BUTTON,
CloseTagScope.LIST_ITEM
));
defineElement(
"textarea", false,
// No, a textarea cannot be inside a link.
elementGroupBits(ElementGroup.INLINE),
elementGroupBits(ElementGroup.CHARACTER_DATA));
defineElement(
"tfoot", false, elementGroupBits(
ElementGroup.TABLE_CONTENT
), elementGroupBits(
ElementGroup.FORM_ELEMENT, ElementGroup.TR_ELEMENT,
ElementGroup.TD_ELEMENT
));
defineElement(
"th", false, elementGroupBits(
ElementGroup.TD_ELEMENT
), elementGroupBits(
ElementGroup.BLOCK, ElementGroup.INLINE
), scopeBits(
CloseTagScope.COMMON, CloseTagScope.BUTTON,
CloseTagScope.LIST_ITEM
));
defineElement(
"thead", false, elementGroupBits(
ElementGroup.TABLE_CONTENT
), elementGroupBits(
ElementGroup.FORM_ELEMENT, ElementGroup.TR_ELEMENT,
ElementGroup.TD_ELEMENT
));
defineElement(
"title", false, elementGroupBits(ElementGroup.HEAD_CONTENT),
elementGroupBits(ElementGroup.CHARACTER_DATA));
defineElement(
"tr", false, elementGroupBits(
ElementGroup.TABLE_CONTENT, ElementGroup.TR_ELEMENT
), elementGroupBits(
ElementGroup.FORM_ELEMENT, ElementGroup.TD_ELEMENT
),
TD);
defineElement(
"tt", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"u", true, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"ul", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.LI_ELEMENT
),
LI,
scopeBits(CloseTagScope.LIST_ITEM));
defineElement(
"var", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), elementGroupBits(
ElementGroup.INLINE
));
defineElement(
"video", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), 0);
defineElement(
"wbr", false, elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A
), 0);
defineElement(
"xmp", false, elementGroupBits(
ElementGroup.BLOCK
), elementGroupBits(
ElementGroup.INLINE
));
}
private static final ElementContainmentInfo CHARACTER_DATA_ONLY
= new ElementContainmentInfo(
"#text", false,
elementGroupBits(
ElementGroup.INLINE, ElementGroup.INLINE_MINUS_A,
ElementGroup.BLOCK, ElementGroup.CHARACTER_DATA),
0, null, 0);
}
static boolean allowsPlainTextualContent(String canonElementName) {
ElementContainmentInfo info =
ELEMENT_CONTAINMENT_RELATIONSHIPS.get(canonElementName);
if (info == null
|| ((info.contents
& ElementContainmentRelationships.CHARACTER_DATA_ONLY.types)
!= 0)) {
switch (HtmlTextEscapingMode.getModeForTag(canonElementName)) {
case PCDATA: return true;
case RCDATA: return true;
case PLAIN_TEXT: return true;
case VOID: return false;
case CDATA:
case CDATA_SOMETIMES:
return "xmp".equals(canonElementName)
|| "listing".equals(canonElementName);
}
}
return false;
}
}