/** * This library has a parser for HTML5 documents, that lets you parse HTML * easily from a script or server side application: * * import 'package:html5lib/parser.dart' show parse; * import 'package:html5lib/dom.dart'; * main() { * var document = parse( * 'Hello world! HTML5 rocks!'); * print(document.outerHtml); * } * * The resulting document you get back has a DOM-like API for easy tree * traversal and manipulation. */ library parser; import 'dart:collection'; import 'dart:math'; import 'package:source_maps/span.dart' show Span, FileSpan; import 'src/treebuilder.dart'; import 'src/constants.dart'; import 'src/encoding_parser.dart'; import 'src/token.dart'; import 'src/tokenizer.dart'; import 'src/utils.dart'; import 'dom.dart'; import 'dom_parsing.dart'; /** * Parse the [input] html5 document into a tree. The [input] can be * a [String], [List] of bytes or an [HtmlTokenizer]. * * If [input] is not a [HtmlTokenizer], you can optionally specify the file's * [encoding], which must be a string. If specified, that encoding will be used, * regardless of any BOM or later declaration (such as in a meta element). * * Set [generateSpans] if you want to generate [Span]s, otherwise the * [Node.sourceSpan] property will be `null`. When using [generateSpans] you can * additionally pass [sourceUrl] to indicate where the [input] was extracted * from. */ Document parse(input, {String encoding, bool generateSpans: false, String sourceUrl}) { var p = new HtmlParser(input, encoding: encoding, generateSpans: generateSpans, sourceUrl: sourceUrl); return p.parse(); } /** * Parse the [input] html5 document fragment into a tree. The [input] can be * a [String], [List] of bytes or an [HtmlTokenizer]. The [container] * element can optionally be specified, otherwise it defaults to "div". * * If [input] is not a [HtmlTokenizer], you can optionally specify the file's * [encoding], which must be a string. If specified, that encoding will be used, * regardless of any BOM or later declaration (such as in a meta element). * * Set [generateSpans] if you want to generate [Span]s, otherwise the * [Node.sourceSpan] property will be `null`. When using [generateSpans] you can * additionally pass [sourceUrl] to indicate where the [input] was extracted * from. */ DocumentFragment parseFragment(input, {String container: "div", String encoding, bool generateSpans: false, String sourceUrl}) { var p = new HtmlParser(input, encoding: encoding, generateSpans: generateSpans, sourceUrl: sourceUrl); return p.parseFragment(container); } /** * Parser for HTML, which generates a tree structure from a stream of * (possibly malformed) characters. */ class HtmlParser { /** Raise an exception on the first error encountered. */ final bool strict; /** True to generate [Span]s for the [Node.sourceSpan] property. */ final bool generateSpans; final HtmlTokenizer tokenizer; final TreeBuilder tree; final List errors = []; String container; bool firstStartTag = false; // TODO(jmesserly): use enum? /** "quirks" / "limited quirks" / "no quirks" */ String compatMode = "no quirks"; /** innerHTML container when parsing document fragment. */ String innerHTML; Phase phase; Phase lastPhase; Phase originalPhase; Phase beforeRCDataPhase; bool framesetOK; // These fields hold the different phase singletons. At any given time one // of them will be active. InitialPhase _initialPhase; BeforeHtmlPhase _beforeHtmlPhase; BeforeHeadPhase _beforeHeadPhase; InHeadPhase _inHeadPhase; AfterHeadPhase _afterHeadPhase; InBodyPhase _inBodyPhase; TextPhase _textPhase; InTablePhase _inTablePhase; InTableTextPhase _inTableTextPhase; InCaptionPhase _inCaptionPhase; InColumnGroupPhase _inColumnGroupPhase; InTableBodyPhase _inTableBodyPhase; InRowPhase _inRowPhase; InCellPhase _inCellPhase; InSelectPhase _inSelectPhase; InSelectInTablePhase _inSelectInTablePhase; InForeignContentPhase _inForeignContentPhase; AfterBodyPhase _afterBodyPhase; InFramesetPhase _inFramesetPhase; AfterFramesetPhase _afterFramesetPhase; AfterAfterBodyPhase _afterAfterBodyPhase; AfterAfterFramesetPhase _afterAfterFramesetPhase; /** * Create a new HtmlParser and configure the [tree] builder and [strict] mode. * The [input] can be a [String], [List] 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