] of bytes or an [HtmlTokenizer].
*
* If [input] is not a [HtmlTokenizer], you can specify a few more arguments.
*
* The [encoding] must be a string that indicates the encoding. If specified,
* that encoding will be used, regardless of any BOM or later declaration
* (such as in a meta element).
*
* Set [parseMeta] to false if you want to disable parsing the meta element.
*
* Set [lowercaseElementName] or [lowercaseAttrName] to false to disable the
* automatic conversion of element and attribute names to lower case. Note
* that standard way to parse HTML is to lowercase, which is what the browser
* DOM will do if you request [Node.outerHTML], for example.
*/
HtmlParser(input, {String encoding, bool parseMeta: true,
bool lowercaseElementName: true, bool lowercaseAttrName: true,
this.strict: false, bool generateSpans: false, String sourceUrl,
TreeBuilder tree})
: generateSpans = generateSpans,
tree = tree != null ? tree : new TreeBuilder(true),
tokenizer = (input is HtmlTokenizer ? input :
new HtmlTokenizer(input, encoding: encoding, parseMeta: parseMeta,
lowercaseElementName: lowercaseElementName,
lowercaseAttrName: lowercaseAttrName,
generateSpans: generateSpans, sourceUrl: sourceUrl)) {
tokenizer.parser = this;
_initialPhase = new InitialPhase(this);
_beforeHtmlPhase = new BeforeHtmlPhase(this);
_beforeHeadPhase = new BeforeHeadPhase(this);
_inHeadPhase = new InHeadPhase(this);
// TODO(jmesserly): html5lib did not implement the no script parsing mode
// More information here:
// http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html#scripting-flag
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html#parsing-main-inheadnoscript
// "inHeadNoscript": new InHeadNoScriptPhase(this);
_afterHeadPhase = new AfterHeadPhase(this);
_inBodyPhase = new InBodyPhase(this);
_textPhase = new TextPhase(this);
_inTablePhase = new InTablePhase(this);
_inTableTextPhase = new InTableTextPhase(this);
_inCaptionPhase = new InCaptionPhase(this);
_inColumnGroupPhase = new InColumnGroupPhase(this);
_inTableBodyPhase = new InTableBodyPhase(this);
_inRowPhase = new InRowPhase(this);
_inCellPhase = new InCellPhase(this);
_inSelectPhase = new InSelectPhase(this);
_inSelectInTablePhase = new InSelectInTablePhase(this);
_inForeignContentPhase = new InForeignContentPhase(this);
_afterBodyPhase = new AfterBodyPhase(this);
_inFramesetPhase = new InFramesetPhase(this);
_afterFramesetPhase = new AfterFramesetPhase(this);
_afterAfterBodyPhase = new AfterAfterBodyPhase(this);
_afterAfterFramesetPhase = new AfterAfterFramesetPhase(this);
}
bool get innerHTMLMode => innerHTML != null;
/**
* Parse an html5 document into a tree.
* After parsing, [errors] will be populated with parse errors, if any.
*/
Document parse() {
innerHTML = null;
_parse();
return tree.getDocument();
}
/**
* Parse an html5 document fragment into a tree.
* Pass a [container] to change the type of the containing element.
* After parsing, [errors] will be populated with parse errors, if any.
*/
DocumentFragment parseFragment([String container = "div"]) {
if (container == null) throw new ArgumentError('container');
innerHTML = container.toLowerCase();
_parse();
return tree.getFragment();
}
void _parse() {
reset();
while (true) {
try {
mainLoop();
break;
} on ReparseException catch (e) {
// Note: this happens if we start parsing but the character encoding
// changes. So we should only need to restart very early in the parse.
reset();
}
}
}
void reset() {
tokenizer.reset();
tree.reset();
firstStartTag = false;
errors.clear();
// "quirks" / "limited quirks" / "no quirks"
compatMode = "no quirks";
if (innerHTMLMode) {
if (cdataElements.contains(innerHTML)) {
tokenizer.state = tokenizer.rcdataState;
} else if (rcdataElements.contains(innerHTML)) {
tokenizer.state = tokenizer.rawtextState;
} else if (innerHTML == 'plaintext') {
tokenizer.state = tokenizer.plaintextState;
} else {
// state already is data state
// tokenizer.state = tokenizer.dataState;
}
phase = _beforeHtmlPhase;
_beforeHtmlPhase.insertHtmlElement();
resetInsertionMode();
} else {
phase = _initialPhase;
}
lastPhase = null;
beforeRCDataPhase = null;
framesetOK = true;
}
bool isHTMLIntegrationPoint(Node element) {
if (element.tagName == "annotation-xml" &&
element.namespace == Namespaces.mathml) {
var enc = element.attributes["encoding"];
if (enc != null) enc = asciiUpper2Lower(enc);
return enc == "text/html" || enc == "application/xhtml+xml";
} else {
return htmlIntegrationPointElements.contains(
new Pair(element.namespace, element.tagName));
}
}
bool isMathMLTextIntegrationPoint(Node element) {
return mathmlTextIntegrationPointElements.contains(
new Pair(element.namespace, element.tagName));
}
bool inForeignContent(Token token, int type) {
if (tree.openElements.length == 0) return false;
var node = tree.openElements.last;
if (node.namespace == tree.defaultNamespace) return false;
if (isMathMLTextIntegrationPoint(node)) {
if (type == TokenKind.startTag &&
(token as StartTagToken).name != "mglyph" &&
(token as StartTagToken).name != "malignmark") {
return false;
}
if (type == TokenKind.characters || type == TokenKind.spaceCharacters) {
return false;
}
}
if (node.tagName == "annotation-xml" && type == TokenKind.startTag &&
(token as StartTagToken).name == "svg") {
return false;
}
if (isHTMLIntegrationPoint(node)) {
if (type == TokenKind.startTag ||
type == TokenKind.characters ||
type == TokenKind.spaceCharacters) {
return false;
}
}
return true;
}
void mainLoop() {
while (tokenizer.moveNext()) {
var token = tokenizer.current;
var newToken = token;
int type;
while (newToken != null) {
type = newToken.kind;
// Note: avoid "is" test here, see http://dartbug.com/4795
if (type == TokenKind.parseError) {
ParseErrorToken error = newToken;
parseError(error.span, error.data, error.messageParams);
newToken = null;
} else {
Phase phase_ = phase;
if (inForeignContent(token, type)) {
phase_ = _inForeignContentPhase;
}
switch (type) {
case TokenKind.characters:
newToken = phase_.processCharacters(newToken);
break;
case TokenKind.spaceCharacters:
newToken = phase_.processSpaceCharacters(newToken);
break;
case TokenKind.startTag:
newToken = phase_.processStartTag(newToken);
break;
case TokenKind.endTag:
newToken = phase_.processEndTag(newToken);
break;
case TokenKind.comment:
newToken = phase_.processComment(newToken);
break;
case TokenKind.doctype:
newToken = phase_.processDoctype(newToken);
break;
}
}
}
if (token is StartTagToken) {
if (token.selfClosing && !token.selfClosingAcknowledged) {
parseError(token.span, "non-void-element-with-trailing-solidus",
{"name": token.name});
}
}
}
// When the loop finishes it's EOF
var reprocess = true;
var reprocessPhases = [];
while (reprocess) {
reprocessPhases.add(phase);
reprocess = phase.processEOF();
if (reprocess) {
assert(!reprocessPhases.contains(phase));
}
}
}
/**
* The last span available. Used for EOF errors if we don't have something
* better.
*/
Span get _lastSpan {
var pos = tokenizer.stream.position;
return new FileSpan(tokenizer.stream.fileInfo, pos, pos);
}
void parseError(Span span, String errorcode,
[Map datavars = const {}]) {
if (!generateSpans && span == null) {
span = _lastSpan;
}
var err = new ParseError(errorcode, span, datavars);
errors.add(err);
if (strict) throw err;
}
void adjustMathMLAttributes(StartTagToken token) {
var orig = token.data.remove("definitionurl");
if (orig != null) {
token.data["definitionURL"] = orig;
}
}
void adjustSVGAttributes(StartTagToken token) {
final replacements = const {
"attributename":"attributeName",
"attributetype":"attributeType",
"basefrequency":"baseFrequency",
"baseprofile":"baseProfile",
"calcmode":"calcMode",
"clippathunits":"clipPathUnits",
"contentscripttype":"contentScriptType",
"contentstyletype":"contentStyleType",
"diffuseconstant":"diffuseConstant",
"edgemode":"edgeMode",
"externalresourcesrequired":"externalResourcesRequired",
"filterres":"filterRes",
"filterunits":"filterUnits",
"glyphref":"glyphRef",
"gradienttransform":"gradientTransform",
"gradientunits":"gradientUnits",
"kernelmatrix":"kernelMatrix",
"kernelunitlength":"kernelUnitLength",
"keypoints":"keyPoints",
"keysplines":"keySplines",
"keytimes":"keyTimes",
"lengthadjust":"lengthAdjust",
"limitingconeangle":"limitingConeAngle",
"markerheight":"markerHeight",
"markerunits":"markerUnits",
"markerwidth":"markerWidth",
"maskcontentunits":"maskContentUnits",
"maskunits":"maskUnits",
"numoctaves":"numOctaves",
"pathlength":"pathLength",
"patterncontentunits":"patternContentUnits",
"patterntransform":"patternTransform",
"patternunits":"patternUnits",
"pointsatx":"pointsAtX",
"pointsaty":"pointsAtY",
"pointsatz":"pointsAtZ",
"preservealpha":"preserveAlpha",
"preserveaspectratio":"preserveAspectRatio",
"primitiveunits":"primitiveUnits",
"refx":"refX",
"refy":"refY",
"repeatcount":"repeatCount",
"repeatdur":"repeatDur",
"requiredextensions":"requiredExtensions",
"requiredfeatures":"requiredFeatures",
"specularconstant":"specularConstant",
"specularexponent":"specularExponent",
"spreadmethod":"spreadMethod",
"startoffset":"startOffset",
"stddeviation":"stdDeviation",
"stitchtiles":"stitchTiles",
"surfacescale":"surfaceScale",
"systemlanguage":"systemLanguage",
"tablevalues":"tableValues",
"targetx":"targetX",
"targety":"targetY",
"textlength":"textLength",
"viewbox":"viewBox",
"viewtarget":"viewTarget",
"xchannelselector":"xChannelSelector",
"ychannelselector":"yChannelSelector",
"zoomandpan":"zoomAndPan"
};
for (var originalName in token.data.keys.toList()) {
var svgName = replacements[originalName];
if (svgName != null) {
token.data[svgName] = token.data.remove(originalName);
}
}
}
void adjustForeignAttributes(StartTagToken token) {
// TODO(jmesserly): I don't like mixing non-string objects with strings in
// the Node.attributes Map. Is there another solution?
final replacements = const {
"xlink:actuate": const AttributeName("xlink", "actuate",
Namespaces.xlink),
"xlink:arcrole": const AttributeName("xlink", "arcrole",
Namespaces.xlink),
"xlink:href": const AttributeName("xlink", "href", Namespaces.xlink),
"xlink:role": const AttributeName("xlink", "role", Namespaces.xlink),
"xlink:show": const AttributeName("xlink", "show", Namespaces.xlink),
"xlink:title": const AttributeName("xlink", "title", Namespaces.xlink),
"xlink:type": const AttributeName("xlink", "type", Namespaces.xlink),
"xml:base": const AttributeName("xml", "base", Namespaces.xml),
"xml:lang": const AttributeName("xml", "lang", Namespaces.xml),
"xml:space": const AttributeName("xml", "space", Namespaces.xml),
"xmlns": const AttributeName(null, "xmlns", Namespaces.xmlns),
"xmlns:xlink": const AttributeName("xmlns", "xlink", Namespaces.xmlns)
};
for (var originalName in token.data.keys.toList()) {
var foreignName = replacements[originalName];
if (foreignName != null) {
token.data[foreignName] = token.data.remove(originalName);
}
}
}
void resetInsertionMode() {
// The name of this method is mostly historical. (It's also used in the
// specification.)
for (Node node in tree.openElements.reversed) {
var nodeName = node.tagName;
bool last = node == tree.openElements[0];
if (last) {
assert(innerHTMLMode);
nodeName = innerHTML;
}
// Check for conditions that should only happen in the innerHTML
// case
switch (nodeName) {
case "select": case "colgroup": case "head": case "html":
assert(innerHTMLMode);
break;
}
if (!last && node.namespace != tree.defaultNamespace) {
continue;
}
switch (nodeName) {
case "select": phase = _inSelectPhase; return;
case "td": phase = _inCellPhase; return;
case "th": phase = _inCellPhase; return;
case "tr": phase = _inRowPhase; return;
case "tbody": phase = _inTableBodyPhase; return;
case "thead": phase = _inTableBodyPhase; return;
case "tfoot": phase = _inTableBodyPhase; return;
case "caption": phase = _inCaptionPhase; return;
case "colgroup": phase = _inColumnGroupPhase; return;
case "table": phase = _inTablePhase; return;
case "head": phase = _inBodyPhase; return;
case "body": phase = _inBodyPhase; return;
case "frameset": phase = _inFramesetPhase; return;
case "html": phase = _beforeHeadPhase; return;
}
}
phase = _inBodyPhase;
}
/**
* Generic RCDATA/RAWTEXT Parsing algorithm
* [contentType] - RCDATA or RAWTEXT
*/
void parseRCDataRawtext(Token token, String contentType) {
assert(contentType == "RAWTEXT" || contentType == "RCDATA");
var element = tree.insertElement(token);
if (contentType == "RAWTEXT") {
tokenizer.state = tokenizer.rawtextState;
} else {
tokenizer.state = tokenizer.rcdataState;
}
originalPhase = phase;
phase = _textPhase;
}
}
/** Base class for helper object that implements each phase of processing. */
class Phase {
// Order should be (they can be omitted):
// * EOF
// * Comment
// * Doctype
// * SpaceCharacters
// * Characters
// * StartTag
// - startTag* methods
// * EndTag
// - endTag* methods
final HtmlParser parser;
final TreeBuilder tree;
Phase(HtmlParser parser) : parser = parser, tree = parser.tree;
bool processEOF() {
throw new UnimplementedError();
}
Token processComment(CommentToken token) {
// For most phases the following is correct. Where it's not it will be
// overridden.
tree.insertComment(token, tree.openElements.last);
}
Token processDoctype(DoctypeToken token) {
parser.parseError(token.span, "unexpected-doctype");
}
Token processCharacters(CharactersToken token) {
tree.insertText(token.data, token.span);
}
Token processSpaceCharacters(SpaceCharactersToken token) {
tree.insertText(token.data, token.span);
}
Token processStartTag(StartTagToken token) {
throw new UnimplementedError();
}
Token startTagHtml(StartTagToken token) {
if (parser.firstStartTag == false && token.name == "html") {
parser.parseError(token.span, "non-html-root");
}
// XXX Need a check here to see if the first start tag token emitted is
// this token... If it's not, invoke parser.parseError().
token.data.forEach((attr, value) {
tree.openElements[0].attributes.putIfAbsent(attr, () => value);
});
parser.firstStartTag = false;
}
Token processEndTag(EndTagToken token) {
throw new UnimplementedError();
}
/** Helper method for popping openElements. */
void popOpenElementsUntil(String name) {
var node = tree.openElements.removeLast();
while (node.tagName != name) {
node = tree.openElements.removeLast();
}
}
}
class InitialPhase extends Phase {
InitialPhase(parser) : super(parser);
Token processSpaceCharacters(SpaceCharactersToken token) {
}
Token processComment(CommentToken token) {
tree.insertComment(token, tree.document);
}
Token processDoctype(DoctypeToken token) {
var name = token.name;
String publicId = token.publicId;
var systemId = token.systemId;
var correct = token.correct;
if ((name != "html" || publicId != null ||
systemId != null && systemId != "about:legacy-compat")) {
parser.parseError(token.span, "unknown-doctype");
}
if (publicId == null) {
publicId = "";
}
tree.insertDoctype(token);
if (publicId != "") {
publicId = asciiUpper2Lower(publicId);
}
if (!correct || token.name != "html"
|| startsWithAny(publicId, const [
"+//silmaril//dtd html pro v0r11 19970101//",
"-//advasoft ltd//dtd html 3.0 aswedit + extensions//",
"-//as//dtd html 3.0 aswedit + extensions//",
"-//ietf//dtd html 2.0 level 1//",
"-//ietf//dtd html 2.0 level 2//",
"-//ietf//dtd html 2.0 strict level 1//",
"-//ietf//dtd html 2.0 strict level 2//",
"-//ietf//dtd html 2.0 strict//",
"-//ietf//dtd html 2.0//",
"-//ietf//dtd html 2.1e//",
"-//ietf//dtd html 3.0//",
"-//ietf//dtd html 3.2 final//",
"-//ietf//dtd html 3.2//",
"-//ietf//dtd html 3//",
"-//ietf//dtd html level 0//",
"-//ietf//dtd html level 1//",
"-//ietf//dtd html level 2//",
"-//ietf//dtd html level 3//",
"-//ietf//dtd html strict level 0//",
"-//ietf//dtd html strict level 1//",
"-//ietf//dtd html strict level 2//",
"-//ietf//dtd html strict level 3//",
"-//ietf//dtd html strict//",
"-//ietf//dtd html//",
"-//metrius//dtd metrius presentational//",
"-//microsoft//dtd internet explorer 2.0 html strict//",
"-//microsoft//dtd internet explorer 2.0 html//",
"-//microsoft//dtd internet explorer 2.0 tables//",
"-//microsoft//dtd internet explorer 3.0 html strict//",
"-//microsoft//dtd internet explorer 3.0 html//",
"-//microsoft//dtd internet explorer 3.0 tables//",
"-//netscape comm. corp.//dtd html//",
"-//netscape comm. corp.//dtd strict html//",
"-//o'reilly and associates//dtd html 2.0//",
"-//o'reilly and associates//dtd html extended 1.0//",
"-//o'reilly and associates//dtd html extended relaxed 1.0//",
"-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//",
"-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//",
"-//spyglass//dtd html 2.0 extended//",
"-//sq//dtd html 2.0 hotmetal + extensions//",
"-//sun microsystems corp.//dtd hotjava html//",
"-//sun microsystems corp.//dtd hotjava strict html//",
"-//w3c//dtd html 3 1995-03-24//",
"-//w3c//dtd html 3.2 draft//",
"-//w3c//dtd html 3.2 final//",
"-//w3c//dtd html 3.2//",
"-//w3c//dtd html 3.2s draft//",
"-//w3c//dtd html 4.0 frameset//",
"-//w3c//dtd html 4.0 transitional//",
"-//w3c//dtd html experimental 19960712//",
"-//w3c//dtd html experimental 970421//",
"-//w3c//dtd w3 html//",
"-//w3o//dtd w3 html 3.0//",
"-//webtechs//dtd mozilla html 2.0//",
"-//webtechs//dtd mozilla html//"])
|| const ["-//w3o//dtd w3 html strict 3.0//en//",
"-/w3c/dtd html 4.0 transitional/en",
"html"].contains(publicId)
|| startsWithAny(publicId, const [
"-//w3c//dtd html 4.01 frameset//",
"-//w3c//dtd html 4.01 transitional//"]) && systemId == null
|| systemId != null && systemId.toLowerCase() ==
"http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd") {
parser.compatMode = "quirks";
} else if (startsWithAny(publicId, const [
"-//w3c//dtd xhtml 1.0 frameset//",
"-//w3c//dtd xhtml 1.0 transitional//"])
|| startsWithAny(publicId, const [
"-//w3c//dtd html 4.01 frameset//",
"-//w3c//dtd html 4.01 transitional//"]) &&
systemId != null) {
parser.compatMode = "limited quirks";
}
parser.phase = parser._beforeHtmlPhase;
}
void anythingElse() {
parser.compatMode = "quirks";
parser.phase = parser._beforeHtmlPhase;
}
Token processCharacters(CharactersToken token) {
parser.parseError(token.span, "expected-doctype-but-got-chars");
anythingElse();
return token;
}
Token processStartTag(StartTagToken token) {
parser.parseError(token.span, "expected-doctype-but-got-start-tag",
{"name": token.name});
anythingElse();
return token;
}
Token processEndTag(EndTagToken token) {
parser.parseError(token.span, "expected-doctype-but-got-end-tag",
{"name": token.name});
anythingElse();
return token;
}
bool processEOF() {
parser.parseError(parser._lastSpan, "expected-doctype-but-got-eof");
anythingElse();
return true;
}
}
class BeforeHtmlPhase extends Phase {
BeforeHtmlPhase(parser) : super(parser);
// helper methods
void insertHtmlElement() {
tree.insertRoot(new StartTagToken("html", data: {}));
parser.phase = parser._beforeHeadPhase;
}
// other
bool processEOF() {
insertHtmlElement();
return true;
}
Token processComment(CommentToken token) {
tree.insertComment(token, tree.document);
}
Token processSpaceCharacters(SpaceCharactersToken token) {
}
Token processCharacters(CharactersToken token) {
insertHtmlElement();
return token;
}
Token processStartTag(StartTagToken token) {
if (token.name == "html") {
parser.firstStartTag = true;
}
insertHtmlElement();
return token;
}
Token processEndTag(EndTagToken token) {
switch (token.name) {
case "head": case "body": case "html": case "br":
insertHtmlElement();
return token;
default:
parser.parseError(token.span, "unexpected-end-tag-before-html",
{"name": token.name});
return null;
}
}
}
class BeforeHeadPhase extends Phase {
BeforeHeadPhase(parser) : super(parser);
processStartTag(StartTagToken token) {
switch (token.name) {
case 'html': return startTagHtml(token);
case 'head': return startTagHead(token);
default: return startTagOther(token);
}
}
processEndTag(EndTagToken token) {
switch (token.name) {
case "head": case "body": case "html": case "br":
return endTagImplyHead(token);
default: return endTagOther(token);
}
}
bool processEOF() {
startTagHead(new StartTagToken("head", data: {}));
return true;
}
Token processSpaceCharacters(SpaceCharactersToken token) {
}
Token processCharacters(CharactersToken token) {
startTagHead(new StartTagToken("head", data: {}));
return token;
}
Token startTagHtml(StartTagToken token) {
return parser._inBodyPhase.processStartTag(token);
}
void startTagHead(StartTagToken token) {
tree.insertElement(token);
tree.headPointer = tree.openElements.last;
parser.phase = parser._inHeadPhase;
}
Token startTagOther(StartTagToken token) {
startTagHead(new StartTagToken("head", data: {}));
return token;
}
Token endTagImplyHead(EndTagToken token) {
startTagHead(new StartTagToken("head", data: {}));
return token;
}
void endTagOther(EndTagToken token) {
parser.parseError(token.span, "end-tag-after-implied-root",
{"name": token.name});
}
}
class InHeadPhase extends Phase {
InHeadPhase(parser) : super(parser);
processStartTag(StartTagToken token) {
switch (token.name) {
case "html": return startTagHtml(token);
case "title": return startTagTitle(token);
case "noscript": case "noframes": case "style":
return startTagNoScriptNoFramesStyle(token);
case "script": return startTagScript(token);
case "base": case "basefont": case "bgsound": case "command": case "link":
return startTagBaseLinkCommand(token);
case "meta": return startTagMeta(token);
case "head": return startTagHead(token);
default: return startTagOther(token);
}
}
processEndTag(EndTagToken token) {
switch (token.name) {
case "head": return endTagHead(token);
case "br": case "html": case "body": return endTagHtmlBodyBr(token);
default: return endTagOther(token);
}
}
// the real thing
bool processEOF() {
anythingElse();
return true;
}
Token processCharacters(CharactersToken token) {
anythingElse();
return token;
}
Token startTagHtml(StartTagToken token) {
return parser._inBodyPhase.processStartTag(token);
}
void startTagHead(StartTagToken token) {
parser.parseError(token.span, "two-heads-are-not-better-than-one");
}
void startTagBaseLinkCommand(StartTagToken token) {
tree.insertElement(token);
tree.openElements.removeLast();
token.selfClosingAcknowledged = true;
}
void startTagMeta(StartTagToken token) {
tree.insertElement(token);
tree.openElements.removeLast();
token.selfClosingAcknowledged = true;
var attributes = token.data;
if (!parser.tokenizer.stream.charEncodingCertain) {
var charset = attributes["charset"];
var content = attributes["content"];
if (charset != null) {
parser.tokenizer.stream.changeEncoding(charset);
} else if (content != null) {
var data = new EncodingBytes(content);
var codec = new ContentAttrParser(data).parse();
parser.tokenizer.stream.changeEncoding(codec);
}
}
}
void startTagTitle(StartTagToken token) {
parser.parseRCDataRawtext(token, "RCDATA");
}
void startTagNoScriptNoFramesStyle(StartTagToken token) {
// Need to decide whether to implement the scripting-disabled case
parser.parseRCDataRawtext(token, "RAWTEXT");
}
void startTagScript(StartTagToken token) {
tree.insertElement(token);
parser.tokenizer.state = parser.tokenizer.scriptDataState;
parser.originalPhase = parser.phase;
parser.phase = parser._textPhase;
}
Token startTagOther(StartTagToken token) {
anythingElse();
return token;
}
void endTagHead(EndTagToken token) {
var node = parser.tree.openElements.removeLast();
assert(node.tagName == "head");
parser.phase = parser._afterHeadPhase;
}
Token endTagHtmlBodyBr(EndTagToken token) {
anythingElse();
return token;
}
void endTagOther(EndTagToken token) {
parser.parseError(token.span, "unexpected-end-tag", {"name": token.name});
}
void anythingElse() {
endTagHead(new EndTagToken("head"));
}
}
// XXX If we implement a parser for which scripting is disabled we need to
// implement this phase.
//
// class InHeadNoScriptPhase extends Phase {
class AfterHeadPhase extends Phase {
AfterHeadPhase(parser) : super(parser);
processStartTag(StartTagToken token) {
switch (token.name) {
case "html": return startTagHtml(token);
case "body": return startTagBody(token);
case "frameset": return startTagFrameset(token);
case "base": case "basefont": case "bgsound": case "link": case "meta":
case "noframes": case "script": case "style": case "title":
return startTagFromHead(token);
case "head": return startTagHead(token);
default: return startTagOther(token);
}
}
processEndTag(EndTagToken token) {
switch (token.name) {
case "body": case "html": case "br":
return endTagHtmlBodyBr(token);
default: return endTagOther(token);
}
}
bool processEOF() {
anythingElse();
return true;
}
Token processCharacters(CharactersToken token) {
anythingElse();
return token;
}
Token startTagHtml(StartTagToken token) {
return parser._inBodyPhase.processStartTag(token);
}
void startTagBody(StartTagToken token) {
parser.framesetOK = false;
tree.insertElement(token);
parser.phase = parser._inBodyPhase;
}
void startTagFrameset(StartTagToken token) {
tree.insertElement(token);
parser.phase = parser._inFramesetPhase;
}
void startTagFromHead(StartTagToken token) {
parser.parseError(token.span, "unexpected-start-tag-out-of-my-head",
{"name": token.name});
tree.openElements.add(tree.headPointer);
parser._inHeadPhase.processStartTag(token);
for (Node node in tree.openElements.reversed) {
if (node.tagName == "head") {
tree.openElements.remove(node);
break;
}
}
}
void startTagHead(StartTagToken token) {
parser.parseError(token.span, "unexpected-start-tag", {"name": token.name});
}
Token startTagOther(StartTagToken token) {
anythingElse();
return token;
}
Token endTagHtmlBodyBr(EndTagToken token) {
anythingElse();
return token;
}
void endTagOther(EndTagToken token) {
parser.parseError(token.span, "unexpected-end-tag", {"name": token.name});
}
void anythingElse() {
tree.insertElement(new StartTagToken("body", data: {}));
parser.phase = parser._inBodyPhase;
parser.framesetOK = true;
}
}
typedef Token TokenProccessor(Token token);
class InBodyPhase extends Phase {
bool dropNewline = false;
// http://www.whatwg.org/specs/web-apps/current-work///parsing-main-inbody
// the really-really-really-very crazy mode
InBodyPhase(parser) : super(parser);
processStartTag(StartTagToken token) {
switch (token.name) {
case "html":
return startTagHtml(token);
case "base": case "basefont": case "bgsound": case "command": case "link":
case "meta": case "noframes": case "script": case "style": case "title":
return startTagProcessInHead(token);
case "body":
return startTagBody(token);
case "frameset":
return startTagFrameset(token);
case "address": case "article": case "aside": case "blockquote":
case "center": case "details": case "details": case "dir": case "div":
case "dl": case "fieldset": case "figcaption": case "figure":
case "footer": case "header": case "hgroup": case "menu": case "nav":
case "ol": case "p": case "section": case "summary": case "ul":
return startTagCloseP(token);
// headingElements
case "h1": case "h2": case "h3": case "h4": case "h5": case "h6":
return startTagHeading(token);
case "pre": case "listing":
return startTagPreListing(token);
case "form":
return startTagForm(token);
case "li": case "dd": case "dt":
return startTagListItem(token);
case "plaintext":
return startTagPlaintext(token);
case "a": return startTagA(token);
case "b": case "big": case "code": case "em": case "font": case "i":
case "s": case "small": case "strike": case "strong": case "tt": case "u":
return startTagFormatting(token);
case "nobr":
return startTagNobr(token);
case "button":
return startTagButton(token);
case "applet": case "marquee": case "object":
return startTagAppletMarqueeObject(token);
case "xmp":
return startTagXmp(token);
case "table":
return startTagTable(token);
case "area": case "br": case "embed": case "img": case "keygen":
case "wbr":
return startTagVoidFormatting(token);
case "param": case "source": case "track":
return startTagParamSource(token);
case "input":
return startTagInput(token);
case "hr":
return startTagHr(token);
case "image":
return startTagImage(token);
case "isindex":
return startTagIsIndex(token);
case "textarea":
return startTagTextarea(token);
case "iframe":
return startTagIFrame(token);
case "noembed": case "noframes": case "noscript":
return startTagRawtext(token);
case "select":
return startTagSelect(token);
case "rp": case "rt":
return startTagRpRt(token);
case "option": case "optgroup":
return startTagOpt(token);
case "math":
return startTagMath(token);
case "svg":
return startTagSvg(token);
case "caption": case "col": case "colgroup": case "frame": case "head":
case "tbody": case "td": case "tfoot": case "th": case "thead": case "tr":
return startTagMisplaced(token);
default: return startTagOther(token);
}
}
processEndTag(EndTagToken token) {
switch (token.name) {
case "body": return endTagBody(token);
case "html": return endTagHtml(token);
case "address": case "article": case "aside": case "blockquote":
case "center": case "details": case "dir": case "div": case "dl":
case "fieldset": case "figcaption": case "figure": case "footer":
case "header": case "hgroup": case "listing": case "menu": case "nav":
case "ol": case "pre": case "section": case "summary": case "ul":
return endTagBlock(token);
case "form": return endTagForm(token);
case "p": return endTagP(token);
case "dd": case "dt": case "li": return endTagListItem(token);
// headingElements
case "h1": case "h2": case "h3": case "h4": case "h5": case "h6":
return endTagHeading(token);
case "a": case "b": case "big": case "code": case "em": case "font":
case "i": case "nobr": case "s": case "small": case "strike":
case "strong": case "tt": case "u":
return endTagFormatting(token);
case "applet": case "marquee": case "object":
return endTagAppletMarqueeObject(token);
case "br": return endTagBr(token);
default: return endTagOther(token);
}
}
bool isMatchingFormattingElement(Node node1, Node node2) {
if (node1.tagName != node2.tagName || node1.namespace != node2.namespace) {
return false;
} else if (node1.attributes.length != node2.attributes.length) {
return false;
} else {
for (var key in node1.attributes.keys) {
if (node1.attributes[key] != node2.attributes[key]) {
return false;
}
}
}
return true;
}
// helper
void addFormattingElement(token) {
tree.insertElement(token);
var element = tree.openElements.last;
var matchingElements = [];
for (Node node in tree.activeFormattingElements.reversed) {
if (node == Marker) {
break;
} else if (isMatchingFormattingElement(node, element)) {
matchingElements.add(node);
}
}
assert(matchingElements.length <= 3);
if (matchingElements.length == 3) {
tree.activeFormattingElements.remove(matchingElements.last);
}
tree.activeFormattingElements.add(element);
}
// the real deal
bool processEOF() {
for (Node node in tree.openElements.reversed) {
switch (node.tagName) {
case "dd": case "dt": case "li": case "p": case "tbody": case "td":
case "tfoot": case "th": case "thead": case "tr": case "body":
case "html":
continue;
}
parser.parseError(node.sourceSpan, "expected-closing-tag-but-got-eof");
break;
}
//Stop parsing
return false;
}
void processSpaceCharactersDropNewline(StringToken token) {
// Sometimes (start of , , and