blob: 46702ae61778660a225ba5d2b8a0e44f6d5c4e26 [file] [log] [blame]
/// 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 'dom.dart';
import 'src/constants.dart';
import 'src/encoding_parser.dart';
import 'src/token.dart';
import 'src/tokenizer.dart';
import 'src/treebuilder.dart';
import 'src/utils.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 = 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 = 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,
this.generateSpans = false,
String sourceUrl,
TreeBuilder tree})
: tree = tree ?? TreeBuilder(true),
tokenizer = (input is HtmlTokenizer
? input
: HtmlTokenizer(input,
encoding: encoding,
parseMeta: parseMeta,
lowercaseElementName: lowercaseElementName,
lowercaseAttrName: lowercaseAttrName,
generateSpans: generateSpans,
sourceUrl: sourceUrl)) {
tokenizer.parser = this;
_initialPhase = InitialPhase(this);
_beforeHtmlPhase = BeforeHtmlPhase(this);
_beforeHeadPhase = BeforeHeadPhase(this);
_inHeadPhase = 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 = AfterHeadPhase(this);
_inBodyPhase = InBodyPhase(this);
_textPhase = TextPhase(this);
_inTablePhase = InTablePhase(this);
_inTableTextPhase = InTableTextPhase(this);
_inCaptionPhase = InCaptionPhase(this);
_inColumnGroupPhase = InColumnGroupPhase(this);
_inTableBodyPhase = InTableBodyPhase(this);
_inRowPhase = InRowPhase(this);
_inCellPhase = InCellPhase(this);
_inSelectPhase = InSelectPhase(this);
_inSelectInTablePhase = InSelectInTablePhase(this);
_inForeignContentPhase = InForeignContentPhase(this);
_afterBodyPhase = AfterBodyPhase(this);
_inFramesetPhase = InFramesetPhase(this);
_afterFramesetPhase = AfterFramesetPhase(this);
_afterAfterBodyPhase = AfterAfterBodyPhase(this);
_afterAfterFramesetPhase = 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 ArgumentError('container');
innerHTML = container.toLowerCase();
_parse();
return tree.getFragment();
}
void _parse() {
reset();
while (true) {
try {
mainLoop();
break;
} on ReparseException catch (_) {
// 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(Pair(element.namespaceUri, element.localName));
}
}
bool isMathMLTextIntegrationPoint(Element element) {
return mathmlTextIntegrationPointElements
.contains(Pair(element.namespaceUri, element.localName));
}
bool inForeignContent(Token token, int type) {
if (tree.openElements.isEmpty) 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 {
var localPhase = phase;
if (inForeignContent(token, type)) {
localPhase = _inForeignContentPhase;
}
switch (type) {
case TokenKind.characters:
newToken = localPhase.processCharacters(newToken);
break;
case TokenKind.spaceCharacters:
newToken = localPhase.processSpaceCharacters(newToken);
break;
case TokenKind.startTag:
newToken = localPhase.processStartTag(newToken);
break;
case TokenKind.endTag:
newToken = localPhase.processEndTag(newToken);
break;
case TokenKind.comment:
newToken = localPhase.processComment(newToken);
break;
case TokenKind.doctype:
newToken = localPhase.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 = 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': AttributeName('xlink', 'actuate', Namespaces.xlink),
'xlink:arcrole': AttributeName('xlink', 'arcrole', Namespaces.xlink),
'xlink:href': AttributeName('xlink', 'href', Namespaces.xlink),
'xlink:role': AttributeName('xlink', 'role', Namespaces.xlink),
'xlink:show': AttributeName('xlink', 'show', Namespaces.xlink),
'xlink:title': AttributeName('xlink', 'title', Namespaces.xlink),
'xlink:type': AttributeName('xlink', 'type', Namespaces.xlink),
'xml:base': AttributeName('xml', 'base', Namespaces.xml),
'xml:lang': AttributeName('xml', 'lang', Namespaces.xml),
'xml:space': AttributeName('xml', 'space', Namespaces.xml),
'xmlns': AttributeName(null, 'xmlns', Namespaces.xmlns),
'xmlns:xlink': 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;
var 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(this.parser) : tree = parser.tree;
bool processEOF() {
throw 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 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 UnimplementedError();
}
/// Helper method for popping openElements.
void popOpenElementsUntil(EndTagToken token) {
var 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);
@override
Token processSpaceCharacters(SpaceCharactersToken token) {
return null;
}
@override
Token processComment(CommentToken token) {
tree.insertComment(token, tree.document);
return null;
}
@override
Token processDoctype(DoctypeToken token) {
var name = token.name;
var 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');
}
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;
}
@override
Token processCharacters(CharactersToken token) {
parser.parseError(token.span, 'expected-doctype-but-got-chars');
anythingElse();
return token;
}
@override
Token processStartTag(StartTagToken token) {
parser.parseError(
token.span, 'expected-doctype-but-got-start-tag', {'name': token.name});
anythingElse();
return token;
}
@override
Token processEndTag(EndTagToken token) {
parser.parseError(
token.span, 'expected-doctype-but-got-end-tag', {'name': token.name});
anythingElse();
return token;
}
@override
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(
StartTagToken('html', data: LinkedHashMap<dynamic, String>()));
parser.phase = parser._beforeHeadPhase;
}
// other
@override
bool processEOF() {
insertHtmlElement();
return true;
}
@override
Token processComment(CommentToken token) {
tree.insertComment(token, tree.document);
return null;
}
@override
Token processSpaceCharacters(SpaceCharactersToken token) {
return null;
}
@override
Token processCharacters(CharactersToken token) {
insertHtmlElement();
return token;
}
@override
@override
Token processStartTag(StartTagToken token) {
if (token.name == 'html') {
parser.firstStartTag = true;
}
insertHtmlElement();
return token;
}
@override
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);
@override
Token processStartTag(StartTagToken token) {
switch (token.name) {
case 'html':
return startTagHtml(token);
case 'head':
startTagHead(token);
return null;
default:
return startTagOther(token);
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'head':
case 'body':
case 'html':
case 'br':
return endTagImplyHead(token);
default:
endTagOther(token);
return null;
}
}
@override
bool processEOF() {
startTagHead(StartTagToken('head', data: LinkedHashMap<dynamic, String>()));
return true;
}
@override
Token processSpaceCharacters(SpaceCharactersToken token) {
return null;
}
@override
Token processCharacters(CharactersToken token) {
startTagHead(StartTagToken('head', data: LinkedHashMap<dynamic, String>()));
return token;
}
@override
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(StartTagToken('head', data: LinkedHashMap<dynamic, String>()));
return token;
}
Token endTagImplyHead(EndTagToken token) {
startTagHead(StartTagToken('head', data: LinkedHashMap<dynamic, String>()));
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);
@override
Token processStartTag(StartTagToken token) {
switch (token.name) {
case 'html':
return startTagHtml(token);
case 'title':
startTagTitle(token);
return null;
case 'noscript':
case 'noframes':
case 'style':
startTagNoScriptNoFramesStyle(token);
return null;
case 'script':
startTagScript(token);
return null;
case 'base':
case 'basefont':
case 'bgsound':
case 'command':
case 'link':
startTagBaseLinkCommand(token);
return null;
case 'meta':
startTagMeta(token);
return null;
case 'head':
startTagHead(token);
return null;
default:
return startTagOther(token);
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'head':
endTagHead(token);
return null;
case 'br':
case 'html':
case 'body':
return endTagHtmlBodyBr(token);
default:
endTagOther(token);
return null;
}
}
// the real thing
@override
bool processEOF() {
anythingElse();
return true;
}
@override
Token processCharacters(CharactersToken token) {
anythingElse();
return token;
}
@override
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 = EncodingBytes(content);
var codec = 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(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);
@override
Token processStartTag(StartTagToken token) {
switch (token.name) {
case 'html':
return startTagHtml(token);
case 'body':
startTagBody(token);
return null;
case 'frameset':
startTagFrameset(token);
return null;
case 'base':
case 'basefont':
case 'bgsound':
case 'link':
case 'meta':
case 'noframes':
case 'script':
case 'style':
case 'title':
startTagFromHead(token);
return null;
case 'head':
startTagHead(token);
return null;
default:
return startTagOther(token);
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'body':
case 'html':
case 'br':
return endTagHtmlBodyBr(token);
default:
endTagOther(token);
return null;
}
}
@override
bool processEOF() {
anythingElse();
return true;
}
@override
Token processCharacters(CharactersToken token) {
anythingElse();
return token;
}
@override
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(
StartTagToken('body', data: LinkedHashMap<dynamic, String>()));
parser.phase = parser._inBodyPhase;
parser.framesetOK = true;
}
}
typedef TokenProccessor = Token Function(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);
@override
Token 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':
startTagBody(token);
return null;
case 'frameset':
startTagFrameset(token);
return null;
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 'menu':
case 'nav':
case 'ol':
case 'p':
case 'section':
case 'summary':
case 'ul':
startTagCloseP(token);
return null;
// headingElements
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
startTagHeading(token);
return null;
case 'pre':
case 'listing':
startTagPreListing(token);
return null;
case 'form':
startTagForm(token);
return null;
case 'li':
case 'dd':
case 'dt':
startTagListItem(token);
return null;
case 'plaintext':
startTagPlaintext(token);
return null;
case 'a':
startTagA(token);
return null;
case 'b':
case 'big':
case 'code':
case 'em':
case 'font':
case 'i':
case 's':
case 'small':
case 'strike':
case 'strong':
case 'tt':
case 'u':
startTagFormatting(token);
return null;
case 'nobr':
startTagNobr(token);
return null;
case 'button':
return startTagButton(token);
case 'applet':
case 'marquee':
case 'object':
startTagAppletMarqueeObject(token);
return null;
case 'xmp':
startTagXmp(token);
return null;
case 'table':
startTagTable(token);
return null;
case 'area':
case 'br':
case 'embed':
case 'img':
case 'keygen':
case 'wbr':
startTagVoidFormatting(token);
return null;
case 'param':
case 'source':
case 'track':
startTagParamSource(token);
return null;
case 'input':
startTagInput(token);
return null;
case 'hr':
startTagHr(token);
return null;
case 'image':
startTagImage(token);
return null;
case 'isindex':
startTagIsIndex(token);
return null;
case 'textarea':
startTagTextarea(token);
return null;
case 'iframe':
startTagIFrame(token);
return null;
case 'noembed':
case 'noscript':
startTagRawtext(token);
return null;
case 'select':
startTagSelect(token);
return null;
case 'rp':
case 'rt':
startTagRpRt(token);
return null;
case 'option':
case 'optgroup':
startTagOpt(token);
return null;
case 'math':
startTagMath(token);
return null;
case 'svg':
startTagSvg(token);
return null;
case 'caption':
case 'col':
case 'colgroup':
case 'frame':
case 'head':
case 'tbody':
case 'td':
case 'tfoot':
case 'th':
case 'thead':
case 'tr':
startTagMisplaced(token);
return null;
default:
return startTagOther(token);
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'body':
endTagBody(token);
return null;
case 'html':
return endTagHtml(token);
case 'address':
case 'article':
case 'aside':
case 'blockquote':
case 'button':
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':
endTagBlock(token);
return null;
case 'form':
endTagForm(token);
return null;
case 'p':
endTagP(token);
return null;
case 'dd':
case 'dt':
case 'li':
endTagListItem(token);
return null;
// headingElements
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
endTagHeading(token);
return null;
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':
endTagFormatting(token);
return null;
case 'applet':
case 'marquee':
case 'object':
endTagAppletMarqueeObject(token);
return null;
case 'br':
endTagBr(token);
return null;
default:
endTagOther(token);
return null;
}
}
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
@override
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.isNotEmpty) {
tree.reconstructActiveFormattingElements();
tree.insertText(data, token.span);
}
}
@override
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;
}
@override
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(EndTagToken('p'));
}
tree.insertElement(token);
}
void startTagPreListing(StartTagToken token) {
if (tree.elementInScope('p', variant: 'button')) {
endTagP(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(EndTagToken('p'));
}
tree.insertElement(token);
tree.formPointer = tree.openElements.last;
}
}
void startTagListItem(StartTagToken token) {
parser.framesetOK = false;
final stopNamesMap = const {
'li': ['li'],
'dt': ['dt', 'dd'],
'dd': ['dt', 'dd']
};
var stopNames = stopNamesMap[token.name];
for (var node in tree.openElements.reversed) {
if (stopNames.contains(node.localName)) {
parser.phase.processEndTag(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(EndTagToken('p'));
}
tree.insertElement(token);
}
void startTagPlaintext(StartTagToken token) {
if (tree.elementInScope('p', variant: 'button')) {
endTagP(EndTagToken('p'));
}
tree.insertElement(token);
parser.tokenizer.state = parser.tokenizer.plaintextState;
}
void startTagHeading(StartTagToken token) {
if (tree.elementInScope('p', variant: 'button')) {
endTagP(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(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(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(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(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(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(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(
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 = <dynamic, String>{};
var dataAction = token.data['action'];
if (dataAction != null) {
formAttrs['action'] = dataAction;
}
processStartTag(StartTagToken('form', data: formAttrs));
processStartTag(
StartTagToken('hr', data: LinkedHashMap<dynamic, String>()));
processStartTag(
StartTagToken('label', data: LinkedHashMap<dynamic, String>()));
// XXX Localization ...
var prompt = token.data['prompt'];
prompt ??= 'This is a searchable index. Enter search keywords: ';
processCharacters(CharactersToken(prompt));
var attributes = LinkedHashMap<dynamic, String>.from(token.data);
attributes.remove('action');
attributes.remove('prompt');
attributes['name'] = 'isindex';
processStartTag(StartTagToken('input',
data: attributes, selfClosing: token.selfClosing));
processEndTag(EndTagToken('label'));
processStartTag(
StartTagToken('hr', data: LinkedHashMap<dynamic, String>()));
processEndTag(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(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(
StartTagToken('p', data: LinkedHashMap<dynamic, String>()));
parser.parseError(token.span, 'unexpected-end-tag', {'name': 'p'});
endTagP(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 (var 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(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) {
String 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)) {
var 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.
void 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.
var 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;
for (var element in slice(tree.openElements, afeIndex)) {
if (specialElements.contains(getElementNameTuple(element))) {
furthestBlock = element;
break;
}
}
// Step 3
if (furthestBlock == null) {
var 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
var lastNode = furthestBlock;
var node = furthestBlock;
var 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(
StartTagToken('br', data: LinkedHashMap<dynamic, String>()));
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) {
// noop
}
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
@override
// ignore: missing_return
Token processStartTag(StartTagToken token) {
assert(false);
}
@override
Token processEndTag(EndTagToken token) {
if (token.name == 'script') {
endTagScript(token);
return null;
}
endTagOther(token);
return null;
}
@override
Token processCharacters(CharactersToken token) {
tree.insertText(token.data, token.span);
return null;
}
@override
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);
@override
Token processStartTag(StartTagToken token) {
switch (token.name) {
case 'html':
return startTagHtml(token);
case 'caption':
startTagCaption(token);
return null;
case 'colgroup':
startTagColgroup(token);
return null;
case 'col':
return startTagCol(token);
case 'tbody':
case 'tfoot':
case 'thead':
startTagRowGroup(token);
return null;
case 'td':
case 'th':
case 'tr':
return startTagImplyTbody(token);
case 'table':
return startTagTable(token);
case 'style':
case 'script':
return startTagStyleScript(token);
case 'input':
startTagInput(token);
return null;
case 'form':
startTagForm(token);
return null;
default:
startTagOther(token);
return null;
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'table':
endTagTable(token);
return null;
case 'body':
case 'caption':
case 'col':
case 'colgroup':
case 'html':
case 'tbody':
case 'td':
case 'tfoot':
case 'th':
case 'thead':
case 'tr':
endTagIgnore(token);
return null;
default:
endTagOther(token);
return null;
}
}
// 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
@override
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;
}
@override
Token processSpaceCharacters(SpaceCharactersToken token) {
var originalPhase = parser.phase;
parser.phase = parser._inTableTextPhase;
parser._inTableTextPhase.originalPhase = originalPhase;
parser.phase.processSpaceCharacters(token);
return null;
}
@override
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(
StartTagToken('colgroup', data: LinkedHashMap<dynamic, String>()));
return token;
}
void startTagRowGroup(StartTagToken token) {
clearStackToTableContext();
tree.insertElement(token);
parser.phase = parser._inTableBodyPhase;
}
Token startTagImplyTbody(StartTagToken token) {
startTagRowGroup(
StartTagToken('tbody', data: LinkedHashMap<dynamic, String>()));
return token;
}
Token startTagTable(StartTagToken token) {
parser.parseError(token.span, 'unexpected-start-tag-implies-end-tag',
{'startName': 'table', 'endName': 'table'});
parser.phase.processEndTag(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(HtmlParser parser)
: characterTokens = <StringToken>[],
super(parser);
void flushCharacters() {
if (characterTokens.isEmpty) return;
// TODO(sigmund,jmesserly): remove '' (dartbug.com/8480)
var data = characterTokens.map((t) => t.data).join('');
FileSpan span;
if (parser.generateSpans) {
span = characterTokens[0].span.expand(characterTokens.last.span);
}
if (!allWhitespace(data)) {
parser._inTablePhase.insertText(CharactersToken(data)..span = span);
} else if (data.isNotEmpty) {
tree.insertText(data, span);
}
characterTokens = <StringToken>[];
}
@override
Token processComment(CommentToken token) {
flushCharacters();
parser.phase = originalPhase;
return token;
}
@override
bool processEOF() {
flushCharacters();
parser.phase = originalPhase;
return true;
}
@override
Token processCharacters(CharactersToken token) {
if (token.data == '\u0000') {
return null;
}
characterTokens.add(token);
return null;
}
@override
Token processSpaceCharacters(SpaceCharactersToken token) {
//pretty sure we should never reach here
characterTokens.add(token);
// XXX assert(false);
return null;
}
@override
Token processStartTag(StartTagToken token) {
flushCharacters();
parser.phase = originalPhase;
return token;
}
@override
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);
@override
Token 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);
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'caption':
endTagCaption(token);
return null;
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':
endTagIgnore(token);
return null;
default:
return endTagOther(token);
}
}
bool ignoreEndTagCaption() {
return !tree.elementInScope('caption', variant: 'table');
}
@override
bool processEOF() {
parser._inBodyPhase.processEOF();
return false;
}
@override
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(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(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);
@override
Token processStartTag(StartTagToken token) {
switch (token.name) {
case 'html':
return startTagHtml(token);
case 'col':
startTagCol(token);
return null;
default:
return startTagOther(token);
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'colgroup':
endTagColgroup(token);
return null;
case 'col':
endTagCol(token);
return null;
default:
return endTagOther(token);
}
}
bool ignoreEndTagColgroup() {
return tree.openElements.last.localName == 'html';
}
@override
bool processEOF() {
var ignoreEndTag = ignoreEndTagColgroup();
if (ignoreEndTag) {
assert(parser.innerHTMLMode);
return false;
} else {
endTagColgroup(EndTagToken('colgroup'));
return true;
}
}
@override
Token processCharacters(CharactersToken token) {
var ignoreEndTag = ignoreEndTagColgroup();
endTagColgroup(EndTagToken('colgroup'));
return ignoreEndTag ? null : token;
}
void startTagCol(StartTagToken token) {
tree.insertElement(token);
tree.openElements.removeLast();
}
Token startTagOther(StartTagToken token) {
var ignoreEndTag = ignoreEndTagColgroup();
endTagColgroup(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(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);
@override
Token processStartTag(StartTagToken token) {
switch (token.name) {
case 'html':
return startTagHtml(token);
case 'tr':
startTagTr(token);
return null;
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);
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'tbody':
case 'tfoot':
case 'thead':
endTagTableRowGroup(token);
return null;
case 'table':
return endTagTable(token);
case 'body':
case 'caption':
case 'col':
case 'colgroup':
case 'html':
case 'td':
case 'th':
case 'tr':
endTagIgnore(token);
return null;
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
@override
bool processEOF() {
parser._inTablePhase.processEOF();
return false;
}
@override
Token processSpaceCharacters(SpaceCharactersToken token) {
return parser._inTablePhase.processSpaceCharacters(token);
}
@override
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(StartTagToken('tr', data: LinkedHashMap<dynamic, String>()));
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(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);
@override
Token processStartTag(StartTagToken token) {
switch (token.name) {
case 'html':
return startTagHtml(token);
case 'td':
case 'th':
startTagTableCell(token);
return null;
case 'caption':
case 'col':
case 'colgroup':
case 'tbody':
case 'tfoot':
case 'thead':
case 'tr':
return startTagTableOther(token);
default:
return startTagOther(token);
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'tr':
endTagTr(token);
return null;
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':
endTagIgnore(token);
return null;
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
@override
bool processEOF() {
parser._inTablePhase.processEOF();
return false;
}
@override
Token processSpaceCharacters(SpaceCharactersToken token) {
return parser._inTablePhase.processSpaceCharacters(token);
}
@override
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) {
var ignoreEndTag = ignoreEndTagTr();
endTagTr(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(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(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);
@override
Token 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);
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'td':
case 'th':
endTagTableCell(token);
return null;
case 'body':
case 'caption':
case 'col':
case 'colgroup':
case 'html':
endTagIgnore(token);
return null;
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(EndTagToken('td'));
} else if (tree.elementInScope('th', variant: 'table')) {
endTagTableCell(EndTagToken('th'));
}
}
// the rest
@override
bool processEOF() {
parser._inBodyPhase.processEOF();
return false;
}
@override
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);
@override
Token processStartTag(StartTagToken token) {
switch (token.name) {
case 'html':
return startTagHtml(token);
case 'option':
startTagOption(token);
return null;
case 'optgroup':
startTagOptgroup(token);
return null;
case 'select':
startTagSelect(token);
return null;
case 'input':
case 'keygen':
case 'textarea':
return startTagInput(token);
case 'script':
return startTagScript(token);
default:
return startTagOther(token);
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'option':
endTagOption(token);
return null;
case 'optgroup':
endTagOptgroup(token);
return null;
case 'select':
endTagSelect(token);
return null;
default:
endTagOther(token);
return null;
}
}
// http://www.whatwg.org/specs/web-apps/current-work///in-select
@override
bool processEOF() {
var last = tree.openElements.last;
if (last.localName != 'html') {
parser.parseError(last.sourceSpan, 'eof-in-select');
} else {
assert(parser.innerHTMLMode);
}
return false;
}
@override
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(EndTagToken('select'));
}
Token startTagInput(StartTagToken token) {
parser.parseError(token.span, 'unexpected-input-in-select');
if (tree.elementInScope('select', variant: 'select')) {
endTagSelect(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);
@override
Token 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);
}
}
@override
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);
}
}
@override
bool processEOF() {
parser._inSelectPhase.processEOF();
return false;
}
@override
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(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(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 = [
'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;
}
}
@override
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);
}
@override
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;
}
}
@override
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});
}
Token newToken;
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.isNotEmpty);
}
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);
@override
Token processStartTag(StartTagToken token) {
if (token.name == 'html') return startTagHtml(token);
return startTagOther(token);
}
@override
Token processEndTag(EndTagToken token) {
if (token.name == 'html') {
endTagHtml(token);
return null;
}
return endTagOther(token);
}
//Stop parsing
@override
bool processEOF() => false;
@override
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;
}
@override
Token processCharacters(CharactersToken token) {
parser.parseError(token.span, 'unexpected-char-after-body');
parser.phase = parser._inBodyPhase;
return token;
}
@override
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);
@override
Token processStartTag(StartTagToken token) {
switch (token.name) {
case 'html':
return startTagHtml(token);
case 'frameset':
startTagFrameset(token);
return null;
case 'frame':
startTagFrame(token);
return null;
case 'noframes':
return startTagNoframes(token);
default:
return startTagOther(token);
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'frameset':
endTagFrameset(token);
return null;
default:
endTagOther(token);
return null;
}
}
@override
bool processEOF() {
var last = tree.openElements.last;
if (last.localName != 'html') {
parser.parseError(last.sourceSpan, 'eof-in-frameset');
} else {
assert(parser.innerHTMLMode);
}
return false;
}
@override
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);
@override
Token processStartTag(StartTagToken token) {
switch (token.name) {
case 'html':
return startTagHtml(token);
case 'noframes':
return startTagNoframes(token);
default:
startTagOther(token);
return null;
}
}
@override
Token processEndTag(EndTagToken token) {
switch (token.name) {
case 'html':
endTagHtml(token);
return null;
default:
endTagOther(token);
return null;
}
}
// Stop parsing
@override
bool processEOF() => false;
@override
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);
@override
Token processStartTag(StartTagToken token) {
if (token.name == 'html') return startTagHtml(token);
return startTagOther(token);
}
@override
bool processEOF() => false;
@override
Token processComment(CommentToken token) {
tree.insertComment(token, tree.document);
return null;
}
@override
Token processSpaceCharacters(SpaceCharactersToken token) {
return parser._inBodyPhase.processSpaceCharacters(token);
}
@override
Token processCharacters(CharactersToken token) {
parser.parseError(token.span, 'expected-eof-but-got-char');
parser.phase = parser._inBodyPhase;
return token;
}
@override
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;
}
@override
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);
@override
Token processStartTag(StartTagToken token) {
switch (token.name) {
case 'html':
return startTagHtml(token);
case 'noframes':
return startTagNoFrames(token);
default:
startTagOther(token);
return null;
}
}
@override
bool processEOF() => false;
@override
Token processComment(CommentToken token) {
tree.insertComment(token, tree.document);
return null;
}
@override
Token processSpaceCharacters(SpaceCharactersToken token) {
return parser._inBodyPhase.processSpaceCharacters(token);
}
@override
Token processCharacters(CharactersToken token) {
parser.parseError(token.span, 'expected-eof-but-got-char');
return null;
}
@override
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});
}
@override
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;
@override
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.
@override
String get message => formatStr(errorMessages[errorCode], data);
@override
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 ?? Namespaces.html;
return Pair(ns, e.localName);
}