Index: mojo/public/dart/third_party/html/lib/parser.dart |
diff --git a/mojo/public/dart/third_party/html/lib/parser.dart b/mojo/public/dart/third_party/html/lib/parser.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..dc95941dff632a9e239f12c39b5dba7566a2dd3f |
--- /dev/null |
+++ b/mojo/public/dart/third_party/html/lib/parser.dart |
@@ -0,0 +1,3822 @@ |
+/// This library has a parser for HTML5 documents, that lets you parse HTML |
+/// easily from a script or server side application: |
+/// |
+/// import 'package:html/parser.dart' show parse; |
+/// import 'package:html/dom.dart'; |
+/// main() { |
+/// var document = parse( |
+/// '<body>Hello world! <a href="www.html5rocks.com">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_span/source_span.dart'; |
+ |
+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'; |
+ |
+/// Parse the [input] html5 document into a tree. The [input] can be |
+/// a [String], [List<int>] 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 [SourceSpan]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<int>] 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 [SourceSpan]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 [SourceSpan]s for the [Node.sourceSpan] property. |
+ final bool generateSpans; |
+ |
+ final HtmlTokenizer tokenizer; |
+ |
+ final TreeBuilder tree; |
+ |
+ final List<ParseError> errors = <ParseError>[]; |
+ |
+ 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 an HtmlParser and configure the [tree] builder and [strict] mode. |
+ /// The [input] can be a [String], [List<int>] 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(Element element) { |
+ if (element.localName == "annotation-xml" && |
+ element.namespaceUri == 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.namespaceUri, element.localName)); |
+ } |
+ } |
+ |
+ bool isMathMLTextIntegrationPoint(Element element) { |
+ return mathmlTextIntegrationPointElements |
+ .contains(new Pair(element.namespaceUri, element.localName)); |
+ } |
+ |
+ bool inForeignContent(Token token, int type) { |
+ if (tree.openElements.length == 0) return false; |
+ |
+ var node = tree.openElements.last; |
+ if (node.namespaceUri == 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.localName == "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. |
+ SourceSpan get _lastSpan { |
+ if (tokenizer.stream.fileInfo == null) return null; |
+ var pos = tokenizer.stream.position; |
+ return tokenizer.stream.fileInfo.location(pos).pointSpan(); |
+ } |
+ |
+ void parseError(SourceSpan 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 (var node in tree.openElements.reversed) { |
+ var nodeName = node.localName; |
+ 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.namespaceUri != 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"); |
+ |
+ 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); |
+ return null; |
+ } |
+ |
+ Token processDoctype(DoctypeToken token) { |
+ parser.parseError(token.span, "unexpected-doctype"); |
+ return null; |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ tree.insertText(token.data, token.span); |
+ return null; |
+ } |
+ |
+ Token processSpaceCharacters(SpaceCharactersToken token) { |
+ tree.insertText(token.data, token.span); |
+ return null; |
+ } |
+ |
+ 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(). |
+ tree.openElements[0].sourceSpan = token.span; |
+ token.data.forEach((attr, value) { |
+ tree.openElements[0].attributes.putIfAbsent(attr, () => value); |
+ }); |
+ parser.firstStartTag = false; |
+ return null; |
+ } |
+ |
+ Token processEndTag(EndTagToken token) { |
+ throw new UnimplementedError(); |
+ } |
+ |
+ /// Helper method for popping openElements. |
+ void popOpenElementsUntil(EndTagToken token) { |
+ String name = token.name; |
+ var node = tree.openElements.removeLast(); |
+ while (node.localName != name) { |
+ node = tree.openElements.removeLast(); |
+ } |
+ if (node != null) { |
+ node.endSourceSpan = token.span; |
+ } |
+ } |
+} |
+ |
+class InitialPhase extends Phase { |
+ InitialPhase(parser) : super(parser); |
+ |
+ Token processSpaceCharacters(SpaceCharactersToken token) { |
+ return null; |
+ } |
+ |
+ Token processComment(CommentToken token) { |
+ tree.insertComment(token, tree.document); |
+ return null; |
+ } |
+ |
+ 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; |
+ return null; |
+ } |
+ |
+ 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); |
+ return null; |
+ } |
+ |
+ Token processSpaceCharacters(SpaceCharactersToken token) { |
+ return null; |
+ } |
+ |
+ 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) { |
+ return null; |
+ } |
+ |
+ 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.localName == "head"); |
+ node.endSourceSpan = token.span; |
+ 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 (var node in tree.openElements.reversed) { |
+ if (node.localName == "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(Element node1, Element node2) { |
+ if (node1.localName != node2.localName || |
+ node1.namespaceUri != node2.namespaceUri) { |
+ 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 (var node in tree.openElements.reversed) { |
+ switch (node.localName) { |
+ 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 <pre>, <listing>, and <textarea> blocks) we |
+ // want to drop leading newlines |
+ var data = token.data; |
+ dropNewline = false; |
+ if (data.startsWith("\n")) { |
+ var lastOpen = tree.openElements.last; |
+ if (const ["pre", "listing", "textarea"].contains(lastOpen.localName) && |
+ !lastOpen.hasContent()) { |
+ data = data.substring(1); |
+ } |
+ } |
+ if (data.length > 0) { |
+ tree.reconstructActiveFormattingElements(); |
+ tree.insertText(data, token.span); |
+ } |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ if (token.data == "\u0000") { |
+ //The tokenizer should always emit null on its own |
+ return null; |
+ } |
+ tree.reconstructActiveFormattingElements(); |
+ tree.insertText(token.data, token.span); |
+ if (parser.framesetOK && !allWhitespace(token.data)) { |
+ parser.framesetOK = false; |
+ } |
+ return null; |
+ } |
+ |
+ Token processSpaceCharacters(SpaceCharactersToken token) { |
+ if (dropNewline) { |
+ processSpaceCharactersDropNewline(token); |
+ } else { |
+ tree.reconstructActiveFormattingElements(); |
+ tree.insertText(token.data, token.span); |
+ } |
+ return null; |
+ } |
+ |
+ Token startTagProcessInHead(StartTagToken token) { |
+ return parser._inHeadPhase.processStartTag(token); |
+ } |
+ |
+ void startTagBody(StartTagToken token) { |
+ parser.parseError(token.span, "unexpected-start-tag", {"name": "body"}); |
+ if (tree.openElements.length == 1 || |
+ tree.openElements[1].localName != "body") { |
+ assert(parser.innerHTMLMode); |
+ } else { |
+ parser.framesetOK = false; |
+ token.data.forEach((attr, value) { |
+ tree.openElements[1].attributes.putIfAbsent(attr, () => value); |
+ }); |
+ } |
+ } |
+ |
+ void startTagFrameset(StartTagToken token) { |
+ parser.parseError(token.span, "unexpected-start-tag", {"name": "frameset"}); |
+ if ((tree.openElements.length == 1 || |
+ tree.openElements[1].localName != "body")) { |
+ assert(parser.innerHTMLMode); |
+ } else if (parser.framesetOK) { |
+ if (tree.openElements[1].parentNode != null) { |
+ tree.openElements[1].parentNode.nodes.remove(tree.openElements[1]); |
+ } |
+ while (tree.openElements.last.localName != "html") { |
+ tree.openElements.removeLast(); |
+ } |
+ tree.insertElement(token); |
+ parser.phase = parser._inFramesetPhase; |
+ } |
+ } |
+ |
+ void startTagCloseP(StartTagToken token) { |
+ if (tree.elementInScope("p", variant: "button")) { |
+ endTagP(new EndTagToken("p")); |
+ } |
+ tree.insertElement(token); |
+ } |
+ |
+ void startTagPreListing(StartTagToken token) { |
+ if (tree.elementInScope("p", variant: "button")) { |
+ endTagP(new EndTagToken("p")); |
+ } |
+ tree.insertElement(token); |
+ parser.framesetOK = false; |
+ dropNewline = true; |
+ } |
+ |
+ void startTagForm(StartTagToken token) { |
+ if (tree.formPointer != null) { |
+ parser.parseError(token.span, "unexpected-start-tag", {"name": "form"}); |
+ } else { |
+ if (tree.elementInScope("p", variant: "button")) { |
+ endTagP(new EndTagToken("p")); |
+ } |
+ tree.insertElement(token); |
+ tree.formPointer = tree.openElements.last; |
+ } |
+ } |
+ |
+ void startTagListItem(StartTagToken token) { |
+ parser.framesetOK = false; |
+ |
+ final stopNamesMap = const { |
+ "li": const ["li"], |
+ "dt": const ["dt", "dd"], |
+ "dd": const ["dt", "dd"] |
+ }; |
+ var stopNames = stopNamesMap[token.name]; |
+ for (var node in tree.openElements.reversed) { |
+ if (stopNames.contains(node.localName)) { |
+ parser.phase.processEndTag(new EndTagToken(node.localName)); |
+ break; |
+ } |
+ if (specialElements.contains(getElementNameTuple(node)) && |
+ !const ["address", "div", "p"].contains(node.localName)) { |
+ break; |
+ } |
+ } |
+ |
+ if (tree.elementInScope("p", variant: "button")) { |
+ parser.phase.processEndTag(new EndTagToken("p")); |
+ } |
+ |
+ tree.insertElement(token); |
+ } |
+ |
+ void startTagPlaintext(StartTagToken token) { |
+ if (tree.elementInScope("p", variant: "button")) { |
+ endTagP(new EndTagToken("p")); |
+ } |
+ tree.insertElement(token); |
+ parser.tokenizer.state = parser.tokenizer.plaintextState; |
+ } |
+ |
+ void startTagHeading(StartTagToken token) { |
+ if (tree.elementInScope("p", variant: "button")) { |
+ endTagP(new EndTagToken("p")); |
+ } |
+ if (headingElements.contains(tree.openElements.last.localName)) { |
+ parser.parseError( |
+ token.span, "unexpected-start-tag", {"name": token.name}); |
+ tree.openElements.removeLast(); |
+ } |
+ tree.insertElement(token); |
+ } |
+ |
+ void startTagA(StartTagToken token) { |
+ var afeAElement = tree.elementInActiveFormattingElements("a"); |
+ if (afeAElement != null) { |
+ parser.parseError(token.span, "unexpected-start-tag-implies-end-tag", { |
+ "startName": "a", |
+ "endName": "a" |
+ }); |
+ endTagFormatting(new EndTagToken("a")); |
+ tree.openElements.remove(afeAElement); |
+ tree.activeFormattingElements.remove(afeAElement); |
+ } |
+ tree.reconstructActiveFormattingElements(); |
+ addFormattingElement(token); |
+ } |
+ |
+ void startTagFormatting(StartTagToken token) { |
+ tree.reconstructActiveFormattingElements(); |
+ addFormattingElement(token); |
+ } |
+ |
+ void startTagNobr(StartTagToken token) { |
+ tree.reconstructActiveFormattingElements(); |
+ if (tree.elementInScope("nobr")) { |
+ parser.parseError(token.span, "unexpected-start-tag-implies-end-tag", { |
+ "startName": "nobr", |
+ "endName": "nobr" |
+ }); |
+ processEndTag(new EndTagToken("nobr")); |
+ // XXX Need tests that trigger the following |
+ tree.reconstructActiveFormattingElements(); |
+ } |
+ addFormattingElement(token); |
+ } |
+ |
+ Token startTagButton(StartTagToken token) { |
+ if (tree.elementInScope("button")) { |
+ parser.parseError(token.span, "unexpected-start-tag-implies-end-tag", { |
+ "startName": "button", |
+ "endName": "button" |
+ }); |
+ processEndTag(new EndTagToken("button")); |
+ return token; |
+ } else { |
+ tree.reconstructActiveFormattingElements(); |
+ tree.insertElement(token); |
+ parser.framesetOK = false; |
+ } |
+ return null; |
+ } |
+ |
+ void startTagAppletMarqueeObject(StartTagToken token) { |
+ tree.reconstructActiveFormattingElements(); |
+ tree.insertElement(token); |
+ tree.activeFormattingElements.add(Marker); |
+ parser.framesetOK = false; |
+ } |
+ |
+ void startTagXmp(StartTagToken token) { |
+ if (tree.elementInScope("p", variant: "button")) { |
+ endTagP(new EndTagToken("p")); |
+ } |
+ tree.reconstructActiveFormattingElements(); |
+ parser.framesetOK = false; |
+ parser.parseRCDataRawtext(token, "RAWTEXT"); |
+ } |
+ |
+ void startTagTable(StartTagToken token) { |
+ if (parser.compatMode != "quirks") { |
+ if (tree.elementInScope("p", variant: "button")) { |
+ processEndTag(new EndTagToken("p")); |
+ } |
+ } |
+ tree.insertElement(token); |
+ parser.framesetOK = false; |
+ parser.phase = parser._inTablePhase; |
+ } |
+ |
+ void startTagVoidFormatting(StartTagToken token) { |
+ tree.reconstructActiveFormattingElements(); |
+ tree.insertElement(token); |
+ tree.openElements.removeLast(); |
+ token.selfClosingAcknowledged = true; |
+ parser.framesetOK = false; |
+ } |
+ |
+ void startTagInput(StartTagToken token) { |
+ var savedFramesetOK = parser.framesetOK; |
+ startTagVoidFormatting(token); |
+ if (asciiUpper2Lower(token.data["type"]) == "hidden") { |
+ //input type=hidden doesn't change framesetOK |
+ parser.framesetOK = savedFramesetOK; |
+ } |
+ } |
+ |
+ void startTagParamSource(StartTagToken token) { |
+ tree.insertElement(token); |
+ tree.openElements.removeLast(); |
+ token.selfClosingAcknowledged = true; |
+ } |
+ |
+ void startTagHr(StartTagToken token) { |
+ if (tree.elementInScope("p", variant: "button")) { |
+ endTagP(new EndTagToken("p")); |
+ } |
+ tree.insertElement(token); |
+ tree.openElements.removeLast(); |
+ token.selfClosingAcknowledged = true; |
+ parser.framesetOK = false; |
+ } |
+ |
+ void startTagImage(StartTagToken token) { |
+ // No really... |
+ parser.parseError(token.span, "unexpected-start-tag-treated-as", { |
+ "originalName": "image", |
+ "newName": "img" |
+ }); |
+ processStartTag(new StartTagToken("img", |
+ data: token.data, selfClosing: token.selfClosing)); |
+ } |
+ |
+ void startTagIsIndex(StartTagToken token) { |
+ parser.parseError(token.span, "deprecated-tag", {"name": "isindex"}); |
+ if (tree.formPointer != null) { |
+ return; |
+ } |
+ var formAttrs = {}; |
+ var dataAction = token.data["action"]; |
+ if (dataAction != null) { |
+ formAttrs["action"] = dataAction; |
+ } |
+ processStartTag(new StartTagToken("form", data: formAttrs)); |
+ processStartTag(new StartTagToken("hr", data: {})); |
+ processStartTag(new StartTagToken("label", data: {})); |
+ // XXX Localization ... |
+ var prompt = token.data["prompt"]; |
+ if (prompt == null) { |
+ prompt = "This is a searchable index. Enter search keywords: "; |
+ } |
+ processCharacters(new CharactersToken(prompt)); |
+ var attributes = new LinkedHashMap.from(token.data); |
+ attributes.remove('action'); |
+ attributes.remove('prompt'); |
+ attributes["name"] = "isindex"; |
+ processStartTag(new StartTagToken( |
+ "input", data: attributes, selfClosing: token.selfClosing)); |
+ processEndTag(new EndTagToken("label")); |
+ processStartTag(new StartTagToken("hr", data: {})); |
+ processEndTag(new EndTagToken("form")); |
+ } |
+ |
+ void startTagTextarea(StartTagToken token) { |
+ tree.insertElement(token); |
+ parser.tokenizer.state = parser.tokenizer.rcdataState; |
+ dropNewline = true; |
+ parser.framesetOK = false; |
+ } |
+ |
+ void startTagIFrame(StartTagToken token) { |
+ parser.framesetOK = false; |
+ startTagRawtext(token); |
+ } |
+ |
+ /// iframe, noembed noframes, noscript(if scripting enabled). |
+ void startTagRawtext(StartTagToken token) { |
+ parser.parseRCDataRawtext(token, "RAWTEXT"); |
+ } |
+ |
+ void startTagOpt(StartTagToken token) { |
+ if (tree.openElements.last.localName == "option") { |
+ parser.phase.processEndTag(new EndTagToken("option")); |
+ } |
+ tree.reconstructActiveFormattingElements(); |
+ parser.tree.insertElement(token); |
+ } |
+ |
+ void startTagSelect(StartTagToken token) { |
+ tree.reconstructActiveFormattingElements(); |
+ tree.insertElement(token); |
+ parser.framesetOK = false; |
+ |
+ if (parser._inTablePhase == parser.phase || |
+ parser._inCaptionPhase == parser.phase || |
+ parser._inColumnGroupPhase == parser.phase || |
+ parser._inTableBodyPhase == parser.phase || |
+ parser._inRowPhase == parser.phase || |
+ parser._inCellPhase == parser.phase) { |
+ parser.phase = parser._inSelectInTablePhase; |
+ } else { |
+ parser.phase = parser._inSelectPhase; |
+ } |
+ } |
+ |
+ void startTagRpRt(StartTagToken token) { |
+ if (tree.elementInScope("ruby")) { |
+ tree.generateImpliedEndTags(); |
+ var last = tree.openElements.last; |
+ if (last.localName != "ruby") { |
+ parser.parseError(last.sourceSpan, 'undefined-error'); |
+ } |
+ } |
+ tree.insertElement(token); |
+ } |
+ |
+ void startTagMath(StartTagToken token) { |
+ tree.reconstructActiveFormattingElements(); |
+ parser.adjustMathMLAttributes(token); |
+ parser.adjustForeignAttributes(token); |
+ token.namespace = Namespaces.mathml; |
+ tree.insertElement(token); |
+ //Need to get the parse error right for the case where the token |
+ //has a namespace not equal to the xmlns attribute |
+ if (token.selfClosing) { |
+ tree.openElements.removeLast(); |
+ token.selfClosingAcknowledged = true; |
+ } |
+ } |
+ |
+ void startTagSvg(StartTagToken token) { |
+ tree.reconstructActiveFormattingElements(); |
+ parser.adjustSVGAttributes(token); |
+ parser.adjustForeignAttributes(token); |
+ token.namespace = Namespaces.svg; |
+ tree.insertElement(token); |
+ //Need to get the parse error right for the case where the token |
+ //has a namespace not equal to the xmlns attribute |
+ if (token.selfClosing) { |
+ tree.openElements.removeLast(); |
+ token.selfClosingAcknowledged = true; |
+ } |
+ } |
+ |
+ /// Elements that should be children of other elements that have a |
+ /// different insertion mode; here they are ignored |
+ /// "caption", "col", "colgroup", "frame", "frameset", "head", |
+ /// "option", "optgroup", "tbody", "td", "tfoot", "th", "thead", |
+ /// "tr", "noscript" |
+ void startTagMisplaced(StartTagToken token) { |
+ parser.parseError( |
+ token.span, "unexpected-start-tag-ignored", {"name": token.name}); |
+ } |
+ |
+ Token startTagOther(StartTagToken token) { |
+ tree.reconstructActiveFormattingElements(); |
+ tree.insertElement(token); |
+ return null; |
+ } |
+ |
+ void endTagP(EndTagToken token) { |
+ if (!tree.elementInScope("p", variant: "button")) { |
+ startTagCloseP(new StartTagToken("p", data: {})); |
+ parser.parseError(token.span, "unexpected-end-tag", {"name": "p"}); |
+ endTagP(new EndTagToken("p")); |
+ } else { |
+ tree.generateImpliedEndTags("p"); |
+ if (tree.openElements.last.localName != "p") { |
+ parser.parseError(token.span, "unexpected-end-tag", {"name": "p"}); |
+ } |
+ popOpenElementsUntil(token); |
+ } |
+ } |
+ |
+ void endTagBody(EndTagToken token) { |
+ if (!tree.elementInScope("body")) { |
+ parser.parseError(token.span, 'undefined-error'); |
+ return; |
+ } else if (tree.openElements.last.localName == "body") { |
+ tree.openElements.last.endSourceSpan = token.span; |
+ } else { |
+ for (Element node in slice(tree.openElements, 2)) { |
+ switch (node.localName) { |
+ case "dd": |
+ case "dt": |
+ case "li": |
+ case "optgroup": |
+ case "option": |
+ case "p": |
+ case "rp": |
+ case "rt": |
+ case "tbody": |
+ case "td": |
+ case "tfoot": |
+ case "th": |
+ case "thead": |
+ case "tr": |
+ case "body": |
+ case "html": |
+ continue; |
+ } |
+ // Not sure this is the correct name for the parse error |
+ parser.parseError(token.span, "expected-one-end-tag-but-got-another", { |
+ "gotName": "body", |
+ "expectedName": node.localName |
+ }); |
+ break; |
+ } |
+ } |
+ parser.phase = parser._afterBodyPhase; |
+ } |
+ |
+ Token endTagHtml(EndTagToken token) { |
+ //We repeat the test for the body end tag token being ignored here |
+ if (tree.elementInScope("body")) { |
+ endTagBody(new EndTagToken("body")); |
+ return token; |
+ } |
+ return null; |
+ } |
+ |
+ void endTagBlock(EndTagToken token) { |
+ //Put us back in the right whitespace handling mode |
+ if (token.name == "pre") { |
+ dropNewline = false; |
+ } |
+ var inScope = tree.elementInScope(token.name); |
+ if (inScope) { |
+ tree.generateImpliedEndTags(); |
+ } |
+ if (tree.openElements.last.localName != token.name) { |
+ parser.parseError(token.span, "end-tag-too-early", {"name": token.name}); |
+ } |
+ if (inScope) { |
+ popOpenElementsUntil(token); |
+ } |
+ } |
+ |
+ void endTagForm(EndTagToken token) { |
+ var node = tree.formPointer; |
+ tree.formPointer = null; |
+ if (node == null || !tree.elementInScope(node)) { |
+ parser.parseError(token.span, "unexpected-end-tag", {"name": "form"}); |
+ } else { |
+ tree.generateImpliedEndTags(); |
+ if (tree.openElements.last != node) { |
+ parser.parseError( |
+ token.span, "end-tag-too-early-ignored", {"name": "form"}); |
+ } |
+ tree.openElements.remove(node); |
+ node.endSourceSpan = token.span; |
+ } |
+ } |
+ |
+ void endTagListItem(EndTagToken token) { |
+ var variant; |
+ if (token.name == "li") { |
+ variant = "list"; |
+ } else { |
+ variant = null; |
+ } |
+ if (!tree.elementInScope(token.name, variant: variant)) { |
+ parser.parseError(token.span, "unexpected-end-tag", {"name": token.name}); |
+ } else { |
+ tree.generateImpliedEndTags(token.name); |
+ if (tree.openElements.last.localName != token.name) { |
+ parser.parseError( |
+ token.span, "end-tag-too-early", {"name": token.name}); |
+ } |
+ popOpenElementsUntil(token); |
+ } |
+ } |
+ |
+ void endTagHeading(EndTagToken token) { |
+ for (var item in headingElements) { |
+ if (tree.elementInScope(item)) { |
+ tree.generateImpliedEndTags(); |
+ break; |
+ } |
+ } |
+ if (tree.openElements.last.localName != token.name) { |
+ parser.parseError(token.span, "end-tag-too-early", {"name": token.name}); |
+ } |
+ |
+ for (var item in headingElements) { |
+ if (tree.elementInScope(item)) { |
+ Element node = tree.openElements.removeLast(); |
+ while (!headingElements.contains(node.localName)) { |
+ node = tree.openElements.removeLast(); |
+ } |
+ if (node != null) { |
+ node.endSourceSpan = token.span; |
+ } |
+ break; |
+ } |
+ } |
+ } |
+ |
+ /// The much-feared adoption agency algorithm. |
+ endTagFormatting(EndTagToken token) { |
+ // http://www.whatwg.org/specs/web-apps/current-work/multipage/tree-construction.html#adoptionAgency |
+ // TODO(jmesserly): the comments here don't match the numbered steps in the |
+ // updated spec. This needs a pass over it to verify that it still matches. |
+ // In particular the html5lib Python code skiped "step 4", I'm not sure why. |
+ // XXX Better parseError messages appreciated. |
+ int outerLoopCounter = 0; |
+ while (outerLoopCounter < 8) { |
+ outerLoopCounter += 1; |
+ |
+ // Step 1 paragraph 1 |
+ var formattingElement = |
+ tree.elementInActiveFormattingElements(token.name); |
+ if (formattingElement == null || |
+ (tree.openElements.contains(formattingElement) && |
+ !tree.elementInScope(formattingElement.localName))) { |
+ parser.parseError( |
+ token.span, "adoption-agency-1.1", {"name": token.name}); |
+ return; |
+ // Step 1 paragraph 2 |
+ } else if (!tree.openElements.contains(formattingElement)) { |
+ parser.parseError( |
+ token.span, "adoption-agency-1.2", {"name": token.name}); |
+ tree.activeFormattingElements.remove(formattingElement); |
+ return; |
+ } |
+ |
+ // Step 1 paragraph 3 |
+ if (formattingElement != tree.openElements.last) { |
+ parser.parseError( |
+ token.span, "adoption-agency-1.3", {"name": token.name}); |
+ } |
+ |
+ // Step 2 |
+ // Start of the adoption agency algorithm proper |
+ var afeIndex = tree.openElements.indexOf(formattingElement); |
+ Node furthestBlock = null; |
+ for (Node element in slice(tree.openElements, afeIndex)) { |
+ if (specialElements.contains(getElementNameTuple(element))) { |
+ furthestBlock = element; |
+ break; |
+ } |
+ } |
+ // Step 3 |
+ if (furthestBlock == null) { |
+ Element element = tree.openElements.removeLast(); |
+ while (element != formattingElement) { |
+ element = tree.openElements.removeLast(); |
+ } |
+ if (element != null) { |
+ element.endSourceSpan = token.span; |
+ } |
+ tree.activeFormattingElements.remove(element); |
+ return; |
+ } |
+ |
+ var commonAncestor = tree.openElements[afeIndex - 1]; |
+ |
+ // Step 5 |
+ // The bookmark is supposed to help us identify where to reinsert |
+ // nodes in step 12. We have to ensure that we reinsert nodes after |
+ // the node before the active formatting element. Note the bookmark |
+ // can move in step 7.4 |
+ var bookmark = tree.activeFormattingElements.indexOf(formattingElement); |
+ |
+ // Step 6 |
+ Node lastNode = furthestBlock; |
+ var node = furthestBlock; |
+ int innerLoopCounter = 0; |
+ |
+ var index = tree.openElements.indexOf(node); |
+ while (innerLoopCounter < 3) { |
+ innerLoopCounter += 1; |
+ |
+ // Node is element before node in open elements |
+ index -= 1; |
+ node = tree.openElements[index]; |
+ if (!tree.activeFormattingElements.contains(node)) { |
+ tree.openElements.remove(node); |
+ continue; |
+ } |
+ // Step 6.3 |
+ if (node == formattingElement) { |
+ break; |
+ } |
+ // Step 6.4 |
+ if (lastNode == furthestBlock) { |
+ bookmark = (tree.activeFormattingElements.indexOf(node) + 1); |
+ } |
+ // Step 6.5 |
+ //cite = node.parent |
+ var clone = node.clone(false); |
+ // Replace node with clone |
+ tree.activeFormattingElements[ |
+ tree.activeFormattingElements.indexOf(node)] = clone; |
+ tree.openElements[tree.openElements.indexOf(node)] = clone; |
+ node = clone; |
+ |
+ // Step 6.6 |
+ // Remove lastNode from its parents, if any |
+ if (lastNode.parentNode != null) { |
+ lastNode.parentNode.nodes.remove(lastNode); |
+ } |
+ node.nodes.add(lastNode); |
+ // Step 7.7 |
+ lastNode = node; |
+ // End of inner loop |
+ } |
+ |
+ // Step 7 |
+ // Foster parent lastNode if commonAncestor is a |
+ // table, tbody, tfoot, thead, or tr we need to foster parent the |
+ // lastNode |
+ if (lastNode.parentNode != null) { |
+ lastNode.parentNode.nodes.remove(lastNode); |
+ } |
+ |
+ if (const [ |
+ "table", |
+ "tbody", |
+ "tfoot", |
+ "thead", |
+ "tr" |
+ ].contains(commonAncestor.localName)) { |
+ var nodePos = tree.getTableMisnestedNodePosition(); |
+ nodePos[0].insertBefore(lastNode, nodePos[1]); |
+ } else { |
+ commonAncestor.nodes.add(lastNode); |
+ } |
+ |
+ // Step 8 |
+ var clone = formattingElement.clone(false); |
+ |
+ // Step 9 |
+ furthestBlock.reparentChildren(clone); |
+ |
+ // Step 10 |
+ furthestBlock.nodes.add(clone); |
+ |
+ // Step 11 |
+ tree.activeFormattingElements.remove(formattingElement); |
+ tree.activeFormattingElements.insert( |
+ min(bookmark, tree.activeFormattingElements.length), clone); |
+ |
+ // Step 12 |
+ tree.openElements.remove(formattingElement); |
+ tree.openElements.insert( |
+ tree.openElements.indexOf(furthestBlock) + 1, clone); |
+ } |
+ } |
+ |
+ void endTagAppletMarqueeObject(EndTagToken token) { |
+ if (tree.elementInScope(token.name)) { |
+ tree.generateImpliedEndTags(); |
+ } |
+ if (tree.openElements.last.localName != token.name) { |
+ parser.parseError(token.span, "end-tag-too-early", {"name": token.name}); |
+ } |
+ if (tree.elementInScope(token.name)) { |
+ popOpenElementsUntil(token); |
+ tree.clearActiveFormattingElements(); |
+ } |
+ } |
+ |
+ void endTagBr(EndTagToken token) { |
+ parser.parseError(token.span, "unexpected-end-tag-treated-as", { |
+ "originalName": "br", |
+ "newName": "br element" |
+ }); |
+ tree.reconstructActiveFormattingElements(); |
+ tree.insertElement(new StartTagToken("br", data: {})); |
+ tree.openElements.removeLast(); |
+ } |
+ |
+ void endTagOther(EndTagToken token) { |
+ for (var node in tree.openElements.reversed) { |
+ if (node.localName == token.name) { |
+ tree.generateImpliedEndTags(token.name); |
+ if (tree.openElements.last.localName != token.name) { |
+ parser.parseError( |
+ token.span, "unexpected-end-tag", {"name": token.name}); |
+ } |
+ while (tree.openElements.removeLast() != node); |
+ node.endSourceSpan = token.span; |
+ break; |
+ } else { |
+ if (specialElements.contains(getElementNameTuple(node))) { |
+ parser.parseError( |
+ token.span, "unexpected-end-tag", {"name": token.name}); |
+ break; |
+ } |
+ } |
+ } |
+ } |
+} |
+ |
+class TextPhase extends Phase { |
+ TextPhase(parser) : super(parser); |
+ |
+ // "Tried to process start tag %s in RCDATA/RAWTEXT mode"%token.name |
+ processStartTag(StartTagToken token) { |
+ assert(false); |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ if (token.name == 'script') return endTagScript(token); |
+ return endTagOther(token); |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ tree.insertText(token.data, token.span); |
+ return null; |
+ } |
+ |
+ bool processEOF() { |
+ var last = tree.openElements.last; |
+ parser.parseError(last.sourceSpan, "expected-named-closing-tag-but-got-eof", |
+ {'name': last.localName}); |
+ tree.openElements.removeLast(); |
+ parser.phase = parser.originalPhase; |
+ return true; |
+ } |
+ |
+ void endTagScript(EndTagToken token) { |
+ var node = tree.openElements.removeLast(); |
+ assert(node.localName == "script"); |
+ parser.phase = parser.originalPhase; |
+ //The rest of this method is all stuff that only happens if |
+ //document.write works |
+ } |
+ |
+ void endTagOther(EndTagToken token) { |
+ tree.openElements.removeLast(); |
+ parser.phase = parser.originalPhase; |
+ } |
+} |
+ |
+class InTablePhase extends Phase { |
+ // http://www.whatwg.org/specs/web-apps/current-work///in-table |
+ InTablePhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ switch (token.name) { |
+ case "html": |
+ return startTagHtml(token); |
+ case "caption": |
+ return startTagCaption(token); |
+ case "colgroup": |
+ return startTagColgroup(token); |
+ case "col": |
+ return startTagCol(token); |
+ case "tbody": |
+ case "tfoot": |
+ case "thead": |
+ return startTagRowGroup(token); |
+ case "td": |
+ case "th": |
+ case "tr": |
+ return startTagImplyTbody(token); |
+ case "table": |
+ return startTagTable(token); |
+ case "style": |
+ case "script": |
+ return startTagStyleScript(token); |
+ case "input": |
+ return startTagInput(token); |
+ case "form": |
+ return startTagForm(token); |
+ default: |
+ return startTagOther(token); |
+ } |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ switch (token.name) { |
+ case "table": |
+ return endTagTable(token); |
+ case "body": |
+ case "caption": |
+ case "col": |
+ case "colgroup": |
+ case "html": |
+ case "tbody": |
+ case "td": |
+ case "tfoot": |
+ case "th": |
+ case "thead": |
+ case "tr": |
+ return endTagIgnore(token); |
+ default: |
+ return endTagOther(token); |
+ } |
+ } |
+ |
+ // helper methods |
+ void clearStackToTableContext() { |
+ // "clear the stack back to a table context" |
+ while (tree.openElements.last.localName != "table" && |
+ tree.openElements.last.localName != "html") { |
+ //parser.parseError(token.span, "unexpected-implied-end-tag-in-table", |
+ // {"name": tree.openElements.last.name}) |
+ tree.openElements.removeLast(); |
+ } |
+ // When the current node is <html> it's an innerHTML case |
+ } |
+ |
+ // processing methods |
+ bool processEOF() { |
+ var last = tree.openElements.last; |
+ if (last.localName != "html") { |
+ parser.parseError(last.sourceSpan, "eof-in-table"); |
+ } else { |
+ assert(parser.innerHTMLMode); |
+ } |
+ //Stop parsing |
+ return false; |
+ } |
+ |
+ Token processSpaceCharacters(SpaceCharactersToken token) { |
+ var originalPhase = parser.phase; |
+ parser.phase = parser._inTableTextPhase; |
+ parser._inTableTextPhase.originalPhase = originalPhase; |
+ parser.phase.processSpaceCharacters(token); |
+ return null; |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ var originalPhase = parser.phase; |
+ parser.phase = parser._inTableTextPhase; |
+ parser._inTableTextPhase.originalPhase = originalPhase; |
+ parser.phase.processCharacters(token); |
+ return null; |
+ } |
+ |
+ void insertText(CharactersToken token) { |
+ // If we get here there must be at least one non-whitespace character |
+ // Do the table magic! |
+ tree.insertFromTable = true; |
+ parser._inBodyPhase.processCharacters(token); |
+ tree.insertFromTable = false; |
+ } |
+ |
+ void startTagCaption(StartTagToken token) { |
+ clearStackToTableContext(); |
+ tree.activeFormattingElements.add(Marker); |
+ tree.insertElement(token); |
+ parser.phase = parser._inCaptionPhase; |
+ } |
+ |
+ void startTagColgroup(StartTagToken token) { |
+ clearStackToTableContext(); |
+ tree.insertElement(token); |
+ parser.phase = parser._inColumnGroupPhase; |
+ } |
+ |
+ Token startTagCol(StartTagToken token) { |
+ startTagColgroup(new StartTagToken("colgroup", data: {})); |
+ return token; |
+ } |
+ |
+ void startTagRowGroup(StartTagToken token) { |
+ clearStackToTableContext(); |
+ tree.insertElement(token); |
+ parser.phase = parser._inTableBodyPhase; |
+ } |
+ |
+ Token startTagImplyTbody(StartTagToken token) { |
+ startTagRowGroup(new StartTagToken("tbody", data: {})); |
+ return token; |
+ } |
+ |
+ Token startTagTable(StartTagToken token) { |
+ parser.parseError(token.span, "unexpected-start-tag-implies-end-tag", { |
+ "startName": "table", |
+ "endName": "table" |
+ }); |
+ parser.phase.processEndTag(new EndTagToken("table")); |
+ if (!parser.innerHTMLMode) { |
+ return token; |
+ } |
+ return null; |
+ } |
+ |
+ Token startTagStyleScript(StartTagToken token) { |
+ return parser._inHeadPhase.processStartTag(token); |
+ } |
+ |
+ void startTagInput(StartTagToken token) { |
+ if (asciiUpper2Lower(token.data["type"]) == "hidden") { |
+ parser.parseError(token.span, "unexpected-hidden-input-in-table"); |
+ tree.insertElement(token); |
+ // XXX associate with form |
+ tree.openElements.removeLast(); |
+ } else { |
+ startTagOther(token); |
+ } |
+ } |
+ |
+ void startTagForm(StartTagToken token) { |
+ parser.parseError(token.span, "unexpected-form-in-table"); |
+ if (tree.formPointer == null) { |
+ tree.insertElement(token); |
+ tree.formPointer = tree.openElements.last; |
+ tree.openElements.removeLast(); |
+ } |
+ } |
+ |
+ void startTagOther(StartTagToken token) { |
+ parser.parseError(token.span, "unexpected-start-tag-implies-table-voodoo", { |
+ "name": token.name |
+ }); |
+ // Do the table magic! |
+ tree.insertFromTable = true; |
+ parser._inBodyPhase.processStartTag(token); |
+ tree.insertFromTable = false; |
+ } |
+ |
+ void endTagTable(EndTagToken token) { |
+ if (tree.elementInScope("table", variant: "table")) { |
+ tree.generateImpliedEndTags(); |
+ var last = tree.openElements.last; |
+ if (last.localName != "table") { |
+ parser.parseError(token.span, "end-tag-too-early-named", { |
+ "gotName": "table", |
+ "expectedName": last.localName |
+ }); |
+ } |
+ while (tree.openElements.last.localName != "table") { |
+ tree.openElements.removeLast(); |
+ } |
+ var node = tree.openElements.removeLast(); |
+ node.endSourceSpan = token.span; |
+ parser.resetInsertionMode(); |
+ } else { |
+ // innerHTML case |
+ assert(parser.innerHTMLMode); |
+ parser.parseError(token.span, "undefined-error"); |
+ } |
+ } |
+ |
+ void endTagIgnore(EndTagToken token) { |
+ parser.parseError(token.span, "unexpected-end-tag", {"name": token.name}); |
+ } |
+ |
+ void endTagOther(EndTagToken token) { |
+ parser.parseError(token.span, "unexpected-end-tag-implies-table-voodoo", { |
+ "name": token.name |
+ }); |
+ // Do the table magic! |
+ tree.insertFromTable = true; |
+ parser._inBodyPhase.processEndTag(token); |
+ tree.insertFromTable = false; |
+ } |
+} |
+ |
+class InTableTextPhase extends Phase { |
+ Phase originalPhase; |
+ List<StringToken> characterTokens; |
+ |
+ InTableTextPhase(parser) |
+ : characterTokens = <StringToken>[], |
+ super(parser); |
+ |
+ void flushCharacters() { |
+ if (characterTokens.length == 0) return; |
+ |
+ // TODO(sigmund,jmesserly): remove '' (dartbug.com/8480) |
+ var data = characterTokens.map((t) => t.data).join(''); |
+ var span = null; |
+ |
+ if (parser.generateSpans) { |
+ span = characterTokens[0].span.expand(characterTokens.last.span); |
+ } |
+ |
+ if (!allWhitespace(data)) { |
+ parser._inTablePhase.insertText(new CharactersToken(data)..span = span); |
+ } else if (data.length > 0) { |
+ tree.insertText(data, span); |
+ } |
+ characterTokens = <StringToken>[]; |
+ } |
+ |
+ Token processComment(CommentToken token) { |
+ flushCharacters(); |
+ parser.phase = originalPhase; |
+ return token; |
+ } |
+ |
+ bool processEOF() { |
+ flushCharacters(); |
+ parser.phase = originalPhase; |
+ return true; |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ if (token.data == "\u0000") { |
+ return null; |
+ } |
+ characterTokens.add(token); |
+ return null; |
+ } |
+ |
+ Token processSpaceCharacters(SpaceCharactersToken token) { |
+ //pretty sure we should never reach here |
+ characterTokens.add(token); |
+ // XXX assert(false); |
+ return null; |
+ } |
+ |
+ Token processStartTag(StartTagToken token) { |
+ flushCharacters(); |
+ parser.phase = originalPhase; |
+ return token; |
+ } |
+ |
+ Token processEndTag(EndTagToken token) { |
+ flushCharacters(); |
+ parser.phase = originalPhase; |
+ return token; |
+ } |
+} |
+ |
+class InCaptionPhase extends Phase { |
+ // http://www.whatwg.org/specs/web-apps/current-work///in-caption |
+ InCaptionPhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ switch (token.name) { |
+ case "html": |
+ return startTagHtml(token); |
+ case "caption": |
+ case "col": |
+ case "colgroup": |
+ case "tbody": |
+ case "td": |
+ case "tfoot": |
+ case "th": |
+ case "thead": |
+ case "tr": |
+ return startTagTableElement(token); |
+ default: |
+ return startTagOther(token); |
+ } |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ switch (token.name) { |
+ case "caption": |
+ return endTagCaption(token); |
+ case "table": |
+ return endTagTable(token); |
+ case "body": |
+ case "col": |
+ case "colgroup": |
+ case "html": |
+ case "tbody": |
+ case "td": |
+ case "tfoot": |
+ case "th": |
+ case "thead": |
+ case "tr": |
+ return endTagIgnore(token); |
+ default: |
+ return endTagOther(token); |
+ } |
+ } |
+ |
+ bool ignoreEndTagCaption() { |
+ return !tree.elementInScope("caption", variant: "table"); |
+ } |
+ |
+ bool processEOF() { |
+ parser._inBodyPhase.processEOF(); |
+ return false; |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ return parser._inBodyPhase.processCharacters(token); |
+ } |
+ |
+ Token startTagTableElement(StartTagToken token) { |
+ parser.parseError(token.span, "undefined-error"); |
+ //XXX Have to duplicate logic here to find out if the tag is ignored |
+ var ignoreEndTag = ignoreEndTagCaption(); |
+ parser.phase.processEndTag(new EndTagToken("caption")); |
+ if (!ignoreEndTag) { |
+ return token; |
+ } |
+ return null; |
+ } |
+ |
+ Token startTagOther(StartTagToken token) { |
+ return parser._inBodyPhase.processStartTag(token); |
+ } |
+ |
+ void endTagCaption(EndTagToken token) { |
+ if (!ignoreEndTagCaption()) { |
+ // AT this code is quite similar to endTagTable in "InTable" |
+ tree.generateImpliedEndTags(); |
+ if (tree.openElements.last.localName != "caption") { |
+ parser.parseError(token.span, "expected-one-end-tag-but-got-another", { |
+ "gotName": "caption", |
+ "expectedName": tree.openElements.last.localName |
+ }); |
+ } |
+ while (tree.openElements.last.localName != "caption") { |
+ tree.openElements.removeLast(); |
+ } |
+ var node = tree.openElements.removeLast(); |
+ node.endSourceSpan = token.span; |
+ tree.clearActiveFormattingElements(); |
+ parser.phase = parser._inTablePhase; |
+ } else { |
+ // innerHTML case |
+ assert(parser.innerHTMLMode); |
+ parser.parseError(token.span, "undefined-error"); |
+ } |
+ } |
+ |
+ Token endTagTable(EndTagToken token) { |
+ parser.parseError(token.span, "undefined-error"); |
+ var ignoreEndTag = ignoreEndTagCaption(); |
+ parser.phase.processEndTag(new EndTagToken("caption")); |
+ if (!ignoreEndTag) { |
+ return token; |
+ } |
+ return null; |
+ } |
+ |
+ void endTagIgnore(EndTagToken token) { |
+ parser.parseError(token.span, "unexpected-end-tag", {"name": token.name}); |
+ } |
+ |
+ Token endTagOther(EndTagToken token) { |
+ return parser._inBodyPhase.processEndTag(token); |
+ } |
+} |
+ |
+class InColumnGroupPhase extends Phase { |
+ // http://www.whatwg.org/specs/web-apps/current-work///in-column |
+ InColumnGroupPhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ switch (token.name) { |
+ case "html": |
+ return startTagHtml(token); |
+ case "col": |
+ return startTagCol(token); |
+ default: |
+ return startTagOther(token); |
+ } |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ switch (token.name) { |
+ case "colgroup": |
+ return endTagColgroup(token); |
+ case "col": |
+ return endTagCol(token); |
+ default: |
+ return endTagOther(token); |
+ } |
+ } |
+ |
+ bool ignoreEndTagColgroup() { |
+ return tree.openElements.last.localName == "html"; |
+ } |
+ |
+ bool processEOF() { |
+ var ignoreEndTag = ignoreEndTagColgroup(); |
+ if (ignoreEndTag) { |
+ assert(parser.innerHTMLMode); |
+ return false; |
+ } else { |
+ endTagColgroup(new EndTagToken("colgroup")); |
+ return true; |
+ } |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ var ignoreEndTag = ignoreEndTagColgroup(); |
+ endTagColgroup(new EndTagToken("colgroup")); |
+ return ignoreEndTag ? null : token; |
+ } |
+ |
+ void startTagCol(StartTagToken token) { |
+ tree.insertElement(token); |
+ tree.openElements.removeLast(); |
+ } |
+ |
+ Token startTagOther(StartTagToken token) { |
+ var ignoreEndTag = ignoreEndTagColgroup(); |
+ endTagColgroup(new EndTagToken("colgroup")); |
+ return ignoreEndTag ? null : token; |
+ } |
+ |
+ void endTagColgroup(EndTagToken token) { |
+ if (ignoreEndTagColgroup()) { |
+ // innerHTML case |
+ assert(parser.innerHTMLMode); |
+ parser.parseError(token.span, "undefined-error"); |
+ } else { |
+ var node = tree.openElements.removeLast(); |
+ node.endSourceSpan = token.span; |
+ parser.phase = parser._inTablePhase; |
+ } |
+ } |
+ |
+ void endTagCol(EndTagToken token) { |
+ parser.parseError(token.span, "no-end-tag", {"name": "col"}); |
+ } |
+ |
+ Token endTagOther(EndTagToken token) { |
+ var ignoreEndTag = ignoreEndTagColgroup(); |
+ endTagColgroup(new EndTagToken("colgroup")); |
+ return ignoreEndTag ? null : token; |
+ } |
+} |
+ |
+class InTableBodyPhase extends Phase { |
+ // http://www.whatwg.org/specs/web-apps/current-work///in-table0 |
+ InTableBodyPhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ switch (token.name) { |
+ case "html": |
+ return startTagHtml(token); |
+ case "tr": |
+ return startTagTr(token); |
+ case "td": |
+ case "th": |
+ return startTagTableCell(token); |
+ case "caption": |
+ case "col": |
+ case "colgroup": |
+ case "tbody": |
+ case "tfoot": |
+ case "thead": |
+ return startTagTableOther(token); |
+ default: |
+ return startTagOther(token); |
+ } |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ switch (token.name) { |
+ case "tbody": |
+ case "tfoot": |
+ case "thead": |
+ return endTagTableRowGroup(token); |
+ case "table": |
+ return endTagTable(token); |
+ case "body": |
+ case "caption": |
+ case "col": |
+ case "colgroup": |
+ case "html": |
+ case "td": |
+ case "th": |
+ case "tr": |
+ return endTagIgnore(token); |
+ default: |
+ return endTagOther(token); |
+ } |
+ } |
+ |
+ // helper methods |
+ void clearStackToTableBodyContext() { |
+ var tableTags = const ["tbody", "tfoot", "thead", "html"]; |
+ while (!tableTags.contains(tree.openElements.last.localName)) { |
+ //XXX parser.parseError(token.span, "unexpected-implied-end-tag-in-table", |
+ // {"name": tree.openElements.last.name}) |
+ tree.openElements.removeLast(); |
+ } |
+ if (tree.openElements.last.localName == "html") { |
+ assert(parser.innerHTMLMode); |
+ } |
+ } |
+ |
+ // the rest |
+ bool processEOF() { |
+ parser._inTablePhase.processEOF(); |
+ return false; |
+ } |
+ |
+ Token processSpaceCharacters(SpaceCharactersToken token) { |
+ return parser._inTablePhase.processSpaceCharacters(token); |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ return parser._inTablePhase.processCharacters(token); |
+ } |
+ |
+ void startTagTr(StartTagToken token) { |
+ clearStackToTableBodyContext(); |
+ tree.insertElement(token); |
+ parser.phase = parser._inRowPhase; |
+ } |
+ |
+ Token startTagTableCell(StartTagToken token) { |
+ parser.parseError( |
+ token.span, "unexpected-cell-in-table-body", {"name": token.name}); |
+ startTagTr(new StartTagToken("tr", data: {})); |
+ return token; |
+ } |
+ |
+ Token startTagTableOther(token) => endTagTable(token); |
+ |
+ Token startTagOther(StartTagToken token) { |
+ return parser._inTablePhase.processStartTag(token); |
+ } |
+ |
+ void endTagTableRowGroup(EndTagToken token) { |
+ if (tree.elementInScope(token.name, variant: "table")) { |
+ clearStackToTableBodyContext(); |
+ var node = tree.openElements.removeLast(); |
+ node.endSourceSpan = token.span; |
+ parser.phase = parser._inTablePhase; |
+ } else { |
+ parser.parseError( |
+ token.span, "unexpected-end-tag-in-table-body", {"name": token.name}); |
+ } |
+ } |
+ |
+ Token endTagTable(TagToken token) { |
+ // XXX AT Any ideas on how to share this with endTagTable? |
+ if (tree.elementInScope("tbody", variant: "table") || |
+ tree.elementInScope("thead", variant: "table") || |
+ tree.elementInScope("tfoot", variant: "table")) { |
+ clearStackToTableBodyContext(); |
+ endTagTableRowGroup(new EndTagToken(tree.openElements.last.localName)); |
+ return token; |
+ } else { |
+ // innerHTML case |
+ assert(parser.innerHTMLMode); |
+ parser.parseError(token.span, "undefined-error"); |
+ } |
+ return null; |
+ } |
+ |
+ void endTagIgnore(EndTagToken token) { |
+ parser.parseError( |
+ token.span, "unexpected-end-tag-in-table-body", {"name": token.name}); |
+ } |
+ |
+ Token endTagOther(EndTagToken token) { |
+ return parser._inTablePhase.processEndTag(token); |
+ } |
+} |
+ |
+class InRowPhase extends Phase { |
+ // http://www.whatwg.org/specs/web-apps/current-work///in-row |
+ InRowPhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ switch (token.name) { |
+ case "html": |
+ return startTagHtml(token); |
+ case "td": |
+ case "th": |
+ return startTagTableCell(token); |
+ case "caption": |
+ case "col": |
+ case "colgroup": |
+ case "tbody": |
+ case "tfoot": |
+ case "thead": |
+ case "tr": |
+ return startTagTableOther(token); |
+ default: |
+ return startTagOther(token); |
+ } |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ switch (token.name) { |
+ case "tr": |
+ return endTagTr(token); |
+ case "table": |
+ return endTagTable(token); |
+ case "tbody": |
+ case "tfoot": |
+ case "thead": |
+ return endTagTableRowGroup(token); |
+ case "body": |
+ case "caption": |
+ case "col": |
+ case "colgroup": |
+ case "html": |
+ case "td": |
+ case "th": |
+ return endTagIgnore(token); |
+ default: |
+ return endTagOther(token); |
+ } |
+ } |
+ |
+ // helper methods (XXX unify this with other table helper methods) |
+ void clearStackToTableRowContext() { |
+ while (true) { |
+ var last = tree.openElements.last; |
+ if (last.localName == "tr" || last.localName == "html") break; |
+ |
+ parser.parseError(last.sourceSpan, |
+ "unexpected-implied-end-tag-in-table-row", { |
+ "name": tree.openElements.last.localName |
+ }); |
+ tree.openElements.removeLast(); |
+ } |
+ } |
+ |
+ bool ignoreEndTagTr() { |
+ return !tree.elementInScope("tr", variant: "table"); |
+ } |
+ |
+ // the rest |
+ bool processEOF() { |
+ parser._inTablePhase.processEOF(); |
+ return false; |
+ } |
+ |
+ Token processSpaceCharacters(SpaceCharactersToken token) { |
+ return parser._inTablePhase.processSpaceCharacters(token); |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ return parser._inTablePhase.processCharacters(token); |
+ } |
+ |
+ void startTagTableCell(StartTagToken token) { |
+ clearStackToTableRowContext(); |
+ tree.insertElement(token); |
+ parser.phase = parser._inCellPhase; |
+ tree.activeFormattingElements.add(Marker); |
+ } |
+ |
+ Token startTagTableOther(StartTagToken token) { |
+ bool ignoreEndTag = ignoreEndTagTr(); |
+ endTagTr(new EndTagToken("tr")); |
+ // XXX how are we sure it's always ignored in the innerHTML case? |
+ return ignoreEndTag ? null : token; |
+ } |
+ |
+ Token startTagOther(StartTagToken token) { |
+ return parser._inTablePhase.processStartTag(token); |
+ } |
+ |
+ void endTagTr(EndTagToken token) { |
+ if (!ignoreEndTagTr()) { |
+ clearStackToTableRowContext(); |
+ var node = tree.openElements.removeLast(); |
+ node.endSourceSpan = token.span; |
+ parser.phase = parser._inTableBodyPhase; |
+ } else { |
+ // innerHTML case |
+ assert(parser.innerHTMLMode); |
+ parser.parseError(token.span, "undefined-error"); |
+ } |
+ } |
+ |
+ Token endTagTable(EndTagToken token) { |
+ var ignoreEndTag = ignoreEndTagTr(); |
+ endTagTr(new EndTagToken("tr")); |
+ // Reprocess the current tag if the tr end tag was not ignored |
+ // XXX how are we sure it's always ignored in the innerHTML case? |
+ return ignoreEndTag ? null : token; |
+ } |
+ |
+ Token endTagTableRowGroup(EndTagToken token) { |
+ if (tree.elementInScope(token.name, variant: "table")) { |
+ endTagTr(new EndTagToken("tr")); |
+ return token; |
+ } else { |
+ parser.parseError(token.span, "undefined-error"); |
+ return null; |
+ } |
+ } |
+ |
+ void endTagIgnore(EndTagToken token) { |
+ parser.parseError( |
+ token.span, "unexpected-end-tag-in-table-row", {"name": token.name}); |
+ } |
+ |
+ Token endTagOther(EndTagToken token) { |
+ return parser._inTablePhase.processEndTag(token); |
+ } |
+} |
+ |
+class InCellPhase extends Phase { |
+ // http://www.whatwg.org/specs/web-apps/current-work///in-cell |
+ InCellPhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ switch (token.name) { |
+ case "html": |
+ return startTagHtml(token); |
+ case "caption": |
+ case "col": |
+ case "colgroup": |
+ case "tbody": |
+ case "td": |
+ case "tfoot": |
+ case "th": |
+ case "thead": |
+ case "tr": |
+ return startTagTableOther(token); |
+ default: |
+ return startTagOther(token); |
+ } |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ switch (token.name) { |
+ case "td": |
+ case "th": |
+ return endTagTableCell(token); |
+ case "body": |
+ case "caption": |
+ case "col": |
+ case "colgroup": |
+ case "html": |
+ return endTagIgnore(token); |
+ case "table": |
+ case "tbody": |
+ case "tfoot": |
+ case "thead": |
+ case "tr": |
+ return endTagImply(token); |
+ default: |
+ return endTagOther(token); |
+ } |
+ } |
+ |
+ // helper |
+ void closeCell() { |
+ if (tree.elementInScope("td", variant: "table")) { |
+ endTagTableCell(new EndTagToken("td")); |
+ } else if (tree.elementInScope("th", variant: "table")) { |
+ endTagTableCell(new EndTagToken("th")); |
+ } |
+ } |
+ |
+ // the rest |
+ bool processEOF() { |
+ parser._inBodyPhase.processEOF(); |
+ return false; |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ return parser._inBodyPhase.processCharacters(token); |
+ } |
+ |
+ Token startTagTableOther(StartTagToken token) { |
+ if (tree.elementInScope("td", variant: "table") || |
+ tree.elementInScope("th", variant: "table")) { |
+ closeCell(); |
+ return token; |
+ } else { |
+ // innerHTML case |
+ assert(parser.innerHTMLMode); |
+ parser.parseError(token.span, "undefined-error"); |
+ return null; |
+ } |
+ } |
+ |
+ Token startTagOther(StartTagToken token) { |
+ return parser._inBodyPhase.processStartTag(token); |
+ } |
+ |
+ void endTagTableCell(EndTagToken token) { |
+ if (tree.elementInScope(token.name, variant: "table")) { |
+ tree.generateImpliedEndTags(token.name); |
+ if (tree.openElements.last.localName != token.name) { |
+ parser.parseError( |
+ token.span, "unexpected-cell-end-tag", {"name": token.name}); |
+ popOpenElementsUntil(token); |
+ } else { |
+ var node = tree.openElements.removeLast(); |
+ node.endSourceSpan = token.span; |
+ } |
+ tree.clearActiveFormattingElements(); |
+ parser.phase = parser._inRowPhase; |
+ } else { |
+ parser.parseError(token.span, "unexpected-end-tag", {"name": token.name}); |
+ } |
+ } |
+ |
+ void endTagIgnore(EndTagToken token) { |
+ parser.parseError(token.span, "unexpected-end-tag", {"name": token.name}); |
+ } |
+ |
+ Token endTagImply(EndTagToken token) { |
+ if (tree.elementInScope(token.name, variant: "table")) { |
+ closeCell(); |
+ return token; |
+ } else { |
+ // sometimes innerHTML case |
+ parser.parseError(token.span, "undefined-error"); |
+ } |
+ return null; |
+ } |
+ |
+ Token endTagOther(EndTagToken token) { |
+ return parser._inBodyPhase.processEndTag(token); |
+ } |
+} |
+ |
+class InSelectPhase extends Phase { |
+ InSelectPhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ switch (token.name) { |
+ case "html": |
+ return startTagHtml(token); |
+ case "option": |
+ return startTagOption(token); |
+ case "optgroup": |
+ return startTagOptgroup(token); |
+ case "select": |
+ return startTagSelect(token); |
+ case "input": |
+ case "keygen": |
+ case "textarea": |
+ return startTagInput(token); |
+ case "script": |
+ return startTagScript(token); |
+ default: |
+ return startTagOther(token); |
+ } |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ switch (token.name) { |
+ case "option": |
+ return endTagOption(token); |
+ case "optgroup": |
+ return endTagOptgroup(token); |
+ case "select": |
+ return endTagSelect(token); |
+ default: |
+ return endTagOther(token); |
+ } |
+ } |
+ |
+ // http://www.whatwg.org/specs/web-apps/current-work///in-select |
+ bool processEOF() { |
+ var last = tree.openElements.last; |
+ if (last.localName != "html") { |
+ parser.parseError(last.sourceSpan, "eof-in-select"); |
+ } else { |
+ assert(parser.innerHTMLMode); |
+ } |
+ return false; |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ if (token.data == "\u0000") { |
+ return null; |
+ } |
+ tree.insertText(token.data, token.span); |
+ return null; |
+ } |
+ |
+ void startTagOption(StartTagToken token) { |
+ // We need to imply </option> if <option> is the current node. |
+ if (tree.openElements.last.localName == "option") { |
+ tree.openElements.removeLast(); |
+ } |
+ tree.insertElement(token); |
+ } |
+ |
+ void startTagOptgroup(StartTagToken token) { |
+ if (tree.openElements.last.localName == "option") { |
+ tree.openElements.removeLast(); |
+ } |
+ if (tree.openElements.last.localName == "optgroup") { |
+ tree.openElements.removeLast(); |
+ } |
+ tree.insertElement(token); |
+ } |
+ |
+ void startTagSelect(StartTagToken token) { |
+ parser.parseError(token.span, "unexpected-select-in-select"); |
+ endTagSelect(new EndTagToken("select")); |
+ } |
+ |
+ Token startTagInput(StartTagToken token) { |
+ parser.parseError(token.span, "unexpected-input-in-select"); |
+ if (tree.elementInScope("select", variant: "select")) { |
+ endTagSelect(new EndTagToken("select")); |
+ return token; |
+ } else { |
+ assert(parser.innerHTMLMode); |
+ } |
+ return null; |
+ } |
+ |
+ Token startTagScript(StartTagToken token) { |
+ return parser._inHeadPhase.processStartTag(token); |
+ } |
+ |
+ Token startTagOther(StartTagToken token) { |
+ parser.parseError( |
+ token.span, "unexpected-start-tag-in-select", {"name": token.name}); |
+ return null; |
+ } |
+ |
+ void endTagOption(EndTagToken token) { |
+ if (tree.openElements.last.localName == "option") { |
+ var node = tree.openElements.removeLast(); |
+ node.endSourceSpan = token.span; |
+ } else { |
+ parser.parseError( |
+ token.span, "unexpected-end-tag-in-select", {"name": "option"}); |
+ } |
+ } |
+ |
+ void endTagOptgroup(EndTagToken token) { |
+ // </optgroup> implicitly closes <option> |
+ if (tree.openElements.last.localName == "option" && |
+ tree.openElements[tree.openElements.length - 2].localName == |
+ "optgroup") { |
+ tree.openElements.removeLast(); |
+ } |
+ // It also closes </optgroup> |
+ if (tree.openElements.last.localName == "optgroup") { |
+ var node = tree.openElements.removeLast(); |
+ node.endSourceSpan = token.span; |
+ // But nothing else |
+ } else { |
+ parser.parseError( |
+ token.span, "unexpected-end-tag-in-select", {"name": "optgroup"}); |
+ } |
+ } |
+ |
+ void endTagSelect(EndTagToken token) { |
+ if (tree.elementInScope("select", variant: "select")) { |
+ popOpenElementsUntil(token); |
+ parser.resetInsertionMode(); |
+ } else { |
+ // innerHTML case |
+ assert(parser.innerHTMLMode); |
+ parser.parseError(token.span, "undefined-error"); |
+ } |
+ } |
+ |
+ void endTagOther(EndTagToken token) { |
+ parser.parseError( |
+ token.span, "unexpected-end-tag-in-select", {"name": token.name}); |
+ } |
+} |
+ |
+class InSelectInTablePhase extends Phase { |
+ InSelectInTablePhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ switch (token.name) { |
+ case "caption": |
+ case "table": |
+ case "tbody": |
+ case "tfoot": |
+ case "thead": |
+ case "tr": |
+ case "td": |
+ case "th": |
+ return startTagTable(token); |
+ default: |
+ return startTagOther(token); |
+ } |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ switch (token.name) { |
+ case "caption": |
+ case "table": |
+ case "tbody": |
+ case "tfoot": |
+ case "thead": |
+ case "tr": |
+ case "td": |
+ case "th": |
+ return endTagTable(token); |
+ default: |
+ return endTagOther(token); |
+ } |
+ } |
+ |
+ bool processEOF() { |
+ parser._inSelectPhase.processEOF(); |
+ return false; |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ return parser._inSelectPhase.processCharacters(token); |
+ } |
+ |
+ Token startTagTable(StartTagToken token) { |
+ parser.parseError(token.span, |
+ "unexpected-table-element-start-tag-in-select-in-table", { |
+ "name": token.name |
+ }); |
+ endTagOther(new EndTagToken("select")); |
+ return token; |
+ } |
+ |
+ Token startTagOther(StartTagToken token) { |
+ return parser._inSelectPhase.processStartTag(token); |
+ } |
+ |
+ Token endTagTable(EndTagToken token) { |
+ parser.parseError(token.span, |
+ "unexpected-table-element-end-tag-in-select-in-table", { |
+ "name": token.name |
+ }); |
+ if (tree.elementInScope(token.name, variant: "table")) { |
+ endTagOther(new EndTagToken("select")); |
+ return token; |
+ } |
+ return null; |
+ } |
+ |
+ Token endTagOther(EndTagToken token) { |
+ return parser._inSelectPhase.processEndTag(token); |
+ } |
+} |
+ |
+class InForeignContentPhase extends Phase { |
+ // TODO(jmesserly): this is sorted so we could binary search. |
+ static const breakoutElements = const [ |
+ 'b', |
+ 'big', |
+ 'blockquote', |
+ 'body', |
+ 'br', |
+ 'center', |
+ 'code', |
+ 'dd', |
+ 'div', |
+ 'dl', |
+ 'dt', |
+ 'em', |
+ 'embed', |
+ 'h1', |
+ 'h2', |
+ 'h3', |
+ 'h4', |
+ 'h5', |
+ 'h6', |
+ 'head', |
+ 'hr', |
+ 'i', |
+ 'img', |
+ 'li', |
+ 'listing', |
+ 'menu', |
+ 'meta', |
+ 'nobr', |
+ 'ol', |
+ 'p', |
+ 'pre', |
+ 'ruby', |
+ 's', |
+ 'small', |
+ 'span', |
+ 'strike', |
+ 'strong', |
+ 'sub', |
+ 'sup', |
+ 'table', |
+ 'tt', |
+ 'u', |
+ 'ul', |
+ 'var' |
+ ]; |
+ |
+ InForeignContentPhase(parser) : super(parser); |
+ |
+ void adjustSVGTagNames(token) { |
+ final replacements = const { |
+ "altglyph": "altGlyph", |
+ "altglyphdef": "altGlyphDef", |
+ "altglyphitem": "altGlyphItem", |
+ "animatecolor": "animateColor", |
+ "animatemotion": "animateMotion", |
+ "animatetransform": "animateTransform", |
+ "clippath": "clipPath", |
+ "feblend": "feBlend", |
+ "fecolormatrix": "feColorMatrix", |
+ "fecomponenttransfer": "feComponentTransfer", |
+ "fecomposite": "feComposite", |
+ "feconvolvematrix": "feConvolveMatrix", |
+ "fediffuselighting": "feDiffuseLighting", |
+ "fedisplacementmap": "feDisplacementMap", |
+ "fedistantlight": "feDistantLight", |
+ "feflood": "feFlood", |
+ "fefunca": "feFuncA", |
+ "fefuncb": "feFuncB", |
+ "fefuncg": "feFuncG", |
+ "fefuncr": "feFuncR", |
+ "fegaussianblur": "feGaussianBlur", |
+ "feimage": "feImage", |
+ "femerge": "feMerge", |
+ "femergenode": "feMergeNode", |
+ "femorphology": "feMorphology", |
+ "feoffset": "feOffset", |
+ "fepointlight": "fePointLight", |
+ "fespecularlighting": "feSpecularLighting", |
+ "fespotlight": "feSpotLight", |
+ "fetile": "feTile", |
+ "feturbulence": "feTurbulence", |
+ "foreignobject": "foreignObject", |
+ "glyphref": "glyphRef", |
+ "lineargradient": "linearGradient", |
+ "radialgradient": "radialGradient", |
+ "textpath": "textPath" |
+ }; |
+ |
+ var replace = replacements[token.name]; |
+ if (replace != null) { |
+ token.name = replace; |
+ } |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ if (token.data == "\u0000") { |
+ token.replaceData("\uFFFD"); |
+ } else if (parser.framesetOK && !allWhitespace(token.data)) { |
+ parser.framesetOK = false; |
+ } |
+ return super.processCharacters(token); |
+ } |
+ |
+ Token processStartTag(StartTagToken token) { |
+ var currentNode = tree.openElements.last; |
+ if (breakoutElements.contains(token.name) || |
+ (token.name == "font" && |
+ (token.data.containsKey("color") || |
+ token.data.containsKey("face") || |
+ token.data.containsKey("size")))) { |
+ parser.parseError(token.span, |
+ "unexpected-html-element-in-foreign-content", {'name': token.name}); |
+ while (tree.openElements.last.namespaceUri != tree.defaultNamespace && |
+ !parser.isHTMLIntegrationPoint(tree.openElements.last) && |
+ !parser.isMathMLTextIntegrationPoint(tree.openElements.last)) { |
+ tree.openElements.removeLast(); |
+ } |
+ return token; |
+ } else { |
+ if (currentNode.namespaceUri == Namespaces.mathml) { |
+ parser.adjustMathMLAttributes(token); |
+ } else if (currentNode.namespaceUri == Namespaces.svg) { |
+ adjustSVGTagNames(token); |
+ parser.adjustSVGAttributes(token); |
+ } |
+ parser.adjustForeignAttributes(token); |
+ token.namespace = currentNode.namespaceUri; |
+ tree.insertElement(token); |
+ if (token.selfClosing) { |
+ tree.openElements.removeLast(); |
+ token.selfClosingAcknowledged = true; |
+ } |
+ return null; |
+ } |
+ } |
+ |
+ Token processEndTag(EndTagToken token) { |
+ var nodeIndex = tree.openElements.length - 1; |
+ var node = tree.openElements.last; |
+ if (asciiUpper2Lower(node.localName) != token.name) { |
+ parser.parseError(token.span, "unexpected-end-tag", {"name": token.name}); |
+ } |
+ |
+ var newToken = null; |
+ while (true) { |
+ if (asciiUpper2Lower(node.localName) == token.name) { |
+ //XXX this isn't in the spec but it seems necessary |
+ if (parser.phase == parser._inTableTextPhase) { |
+ InTableTextPhase inTableText = parser.phase; |
+ inTableText.flushCharacters(); |
+ parser.phase = inTableText.originalPhase; |
+ } |
+ while (tree.openElements.removeLast() != node) { |
+ assert(tree.openElements.length > 0); |
+ } |
+ newToken = null; |
+ break; |
+ } |
+ nodeIndex -= 1; |
+ |
+ node = tree.openElements[nodeIndex]; |
+ if (node.namespaceUri != tree.defaultNamespace) { |
+ continue; |
+ } else { |
+ newToken = parser.phase.processEndTag(token); |
+ break; |
+ } |
+ } |
+ return newToken; |
+ } |
+} |
+ |
+class AfterBodyPhase extends Phase { |
+ AfterBodyPhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ if (token.name == "html") return startTagHtml(token); |
+ return startTagOther(token); |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ if (token.name == "html") return endTagHtml(token); |
+ return endTagOther(token); |
+ } |
+ |
+ //Stop parsing |
+ bool processEOF() => false; |
+ |
+ Token processComment(CommentToken token) { |
+ // This is needed because data is to be appended to the <html> element |
+ // here and not to whatever is currently open. |
+ tree.insertComment(token, tree.openElements[0]); |
+ return null; |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ parser.parseError(token.span, "unexpected-char-after-body"); |
+ parser.phase = parser._inBodyPhase; |
+ return token; |
+ } |
+ |
+ Token startTagHtml(StartTagToken token) { |
+ return parser._inBodyPhase.processStartTag(token); |
+ } |
+ |
+ Token startTagOther(StartTagToken token) { |
+ parser.parseError( |
+ token.span, "unexpected-start-tag-after-body", {"name": token.name}); |
+ parser.phase = parser._inBodyPhase; |
+ return token; |
+ } |
+ |
+ void endTagHtml(Token token) { |
+ for (var node in tree.openElements.reversed) { |
+ if (node.localName == 'html') { |
+ node.endSourceSpan = token.span; |
+ break; |
+ } |
+ } |
+ if (parser.innerHTMLMode) { |
+ parser.parseError(token.span, "unexpected-end-tag-after-body-innerhtml"); |
+ } else { |
+ parser.phase = parser._afterAfterBodyPhase; |
+ } |
+ } |
+ |
+ Token endTagOther(EndTagToken token) { |
+ parser.parseError( |
+ token.span, "unexpected-end-tag-after-body", {"name": token.name}); |
+ parser.phase = parser._inBodyPhase; |
+ return token; |
+ } |
+} |
+ |
+class InFramesetPhase extends Phase { |
+ // http://www.whatwg.org/specs/web-apps/current-work///in-frameset |
+ InFramesetPhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ switch (token.name) { |
+ case "html": |
+ return startTagHtml(token); |
+ case "frameset": |
+ return startTagFrameset(token); |
+ case "frame": |
+ return startTagFrame(token); |
+ case "noframes": |
+ return startTagNoframes(token); |
+ default: |
+ return startTagOther(token); |
+ } |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ switch (token.name) { |
+ case "frameset": |
+ return endTagFrameset(token); |
+ default: |
+ return endTagOther(token); |
+ } |
+ } |
+ |
+ bool processEOF() { |
+ var last = tree.openElements.last; |
+ if (last.localName != "html") { |
+ parser.parseError(last.sourceSpan, "eof-in-frameset"); |
+ } else { |
+ assert(parser.innerHTMLMode); |
+ } |
+ return false; |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ parser.parseError(token.span, "unexpected-char-in-frameset"); |
+ return null; |
+ } |
+ |
+ void startTagFrameset(StartTagToken token) { |
+ tree.insertElement(token); |
+ } |
+ |
+ void startTagFrame(StartTagToken token) { |
+ tree.insertElement(token); |
+ tree.openElements.removeLast(); |
+ } |
+ |
+ Token startTagNoframes(StartTagToken token) { |
+ return parser._inBodyPhase.processStartTag(token); |
+ } |
+ |
+ Token startTagOther(StartTagToken token) { |
+ parser.parseError( |
+ token.span, "unexpected-start-tag-in-frameset", {"name": token.name}); |
+ return null; |
+ } |
+ |
+ void endTagFrameset(EndTagToken token) { |
+ if (tree.openElements.last.localName == "html") { |
+ // innerHTML case |
+ parser.parseError( |
+ token.span, "unexpected-frameset-in-frameset-innerhtml"); |
+ } else { |
+ var node = tree.openElements.removeLast(); |
+ node.endSourceSpan = token.span; |
+ } |
+ if (!parser.innerHTMLMode && |
+ tree.openElements.last.localName != "frameset") { |
+ // If we're not in innerHTML mode and the the current node is not a |
+ // "frameset" element (anymore) then switch. |
+ parser.phase = parser._afterFramesetPhase; |
+ } |
+ } |
+ |
+ void endTagOther(EndTagToken token) { |
+ parser.parseError( |
+ token.span, "unexpected-end-tag-in-frameset", {"name": token.name}); |
+ } |
+} |
+ |
+class AfterFramesetPhase extends Phase { |
+ // http://www.whatwg.org/specs/web-apps/current-work///after3 |
+ AfterFramesetPhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ switch (token.name) { |
+ case "html": |
+ return startTagHtml(token); |
+ case "noframes": |
+ return startTagNoframes(token); |
+ default: |
+ return startTagOther(token); |
+ } |
+ } |
+ |
+ processEndTag(EndTagToken token) { |
+ switch (token.name) { |
+ case "html": |
+ return endTagHtml(token); |
+ default: |
+ return endTagOther(token); |
+ } |
+ } |
+ |
+ // Stop parsing |
+ bool processEOF() => false; |
+ |
+ Token processCharacters(CharactersToken token) { |
+ parser.parseError(token.span, "unexpected-char-after-frameset"); |
+ return null; |
+ } |
+ |
+ Token startTagNoframes(StartTagToken token) { |
+ return parser._inHeadPhase.processStartTag(token); |
+ } |
+ |
+ void startTagOther(StartTagToken token) { |
+ parser.parseError(token.span, "unexpected-start-tag-after-frameset", { |
+ "name": token.name |
+ }); |
+ } |
+ |
+ void endTagHtml(EndTagToken token) { |
+ parser.phase = parser._afterAfterFramesetPhase; |
+ } |
+ |
+ void endTagOther(EndTagToken token) { |
+ parser.parseError( |
+ token.span, "unexpected-end-tag-after-frameset", {"name": token.name}); |
+ } |
+} |
+ |
+class AfterAfterBodyPhase extends Phase { |
+ AfterAfterBodyPhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ if (token.name == 'html') return startTagHtml(token); |
+ return startTagOther(token); |
+ } |
+ |
+ bool processEOF() => false; |
+ |
+ Token processComment(CommentToken token) { |
+ tree.insertComment(token, tree.document); |
+ return null; |
+ } |
+ |
+ Token processSpaceCharacters(SpaceCharactersToken token) { |
+ return parser._inBodyPhase.processSpaceCharacters(token); |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ parser.parseError(token.span, "expected-eof-but-got-char"); |
+ parser.phase = parser._inBodyPhase; |
+ return token; |
+ } |
+ |
+ Token startTagHtml(StartTagToken token) { |
+ return parser._inBodyPhase.processStartTag(token); |
+ } |
+ |
+ Token startTagOther(StartTagToken token) { |
+ parser.parseError( |
+ token.span, "expected-eof-but-got-start-tag", {"name": token.name}); |
+ parser.phase = parser._inBodyPhase; |
+ return token; |
+ } |
+ |
+ Token processEndTag(EndTagToken token) { |
+ parser.parseError( |
+ token.span, "expected-eof-but-got-end-tag", {"name": token.name}); |
+ parser.phase = parser._inBodyPhase; |
+ return token; |
+ } |
+} |
+ |
+class AfterAfterFramesetPhase extends Phase { |
+ AfterAfterFramesetPhase(parser) : super(parser); |
+ |
+ processStartTag(StartTagToken token) { |
+ switch (token.name) { |
+ case "html": |
+ return startTagHtml(token); |
+ case "noframes": |
+ return startTagNoFrames(token); |
+ default: |
+ return startTagOther(token); |
+ } |
+ } |
+ |
+ bool processEOF() => false; |
+ |
+ Token processComment(CommentToken token) { |
+ tree.insertComment(token, tree.document); |
+ return null; |
+ } |
+ |
+ Token processSpaceCharacters(SpaceCharactersToken token) { |
+ return parser._inBodyPhase.processSpaceCharacters(token); |
+ } |
+ |
+ Token processCharacters(CharactersToken token) { |
+ parser.parseError(token.span, "expected-eof-but-got-char"); |
+ return null; |
+ } |
+ |
+ Token startTagHtml(StartTagToken token) { |
+ return parser._inBodyPhase.processStartTag(token); |
+ } |
+ |
+ Token startTagNoFrames(StartTagToken token) { |
+ return parser._inHeadPhase.processStartTag(token); |
+ } |
+ |
+ void startTagOther(StartTagToken token) { |
+ parser.parseError( |
+ token.span, "expected-eof-but-got-start-tag", {"name": token.name}); |
+ } |
+ |
+ Token processEndTag(EndTagToken token) { |
+ parser.parseError( |
+ token.span, "expected-eof-but-got-end-tag", {"name": token.name}); |
+ return null; |
+ } |
+} |
+ |
+/// Error in parsed document. |
+class ParseError implements SourceSpanException { |
+ final String errorCode; |
+ final SourceSpan span; |
+ final Map data; |
+ |
+ ParseError(this.errorCode, this.span, this.data); |
+ |
+ int get line => span.start.line; |
+ |
+ int get column => span.start.column; |
+ |
+ /// Gets the human readable error message for this error. Use |
+ /// [span.getLocationMessage] or [toString] to get a message including span |
+ /// information. If there is a file associated with the span, both |
+ /// [span.getLocationMessage] and [toString] are equivalent. Otherwise, |
+ /// [span.getLocationMessage] will not show any source url information, but |
+ /// [toString] will include 'ParserError:' as a prefix. |
+ String get message => formatStr(errorMessages[errorCode], data); |
+ |
+ String toString({color}) { |
+ var res = span.message(message, color: color); |
+ return span.sourceUrl == null ? 'ParserError on $res' : 'On $res'; |
+ } |
+} |
+ |
+/// Convenience function to get the pair of namespace and localName. |
+Pair<String, String> getElementNameTuple(Element e) { |
+ var ns = e.namespaceUri; |
+ if (ns == null) ns = Namespaces.html; |
+ return new Pair(ns, e.localName); |
+} |