b740cb6335
Changes were mainly straightforward to merge. From-SVN: r181824
1130 lines
27 KiB
Go
1130 lines
27 KiB
Go
// Copyright 2010 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package html
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
// A parser implements the HTML5 parsing algorithm:
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#tree-construction
|
|
type parser struct {
|
|
// tokenizer provides the tokens for the parser.
|
|
tokenizer *Tokenizer
|
|
// tok is the most recently read token.
|
|
tok Token
|
|
// Self-closing tags like <hr/> are re-interpreted as a two-token sequence:
|
|
// <hr> followed by </hr>. hasSelfClosingToken is true if we have just read
|
|
// the synthetic start tag and the next one due is the matching end tag.
|
|
hasSelfClosingToken bool
|
|
// doc is the document root element.
|
|
doc *Node
|
|
// The stack of open elements (section 11.2.3.2) and active formatting
|
|
// elements (section 11.2.3.3).
|
|
oe, afe nodeStack
|
|
// Element pointers (section 11.2.3.4).
|
|
head, form *Node
|
|
// Other parsing state flags (section 11.2.3.5).
|
|
scripting, framesetOK bool
|
|
// originalIM is the insertion mode to go back to after completing a text
|
|
// or inTableText insertion mode.
|
|
originalIM insertionMode
|
|
// fosterParenting is whether new elements should be inserted according to
|
|
// the foster parenting rules (section 11.2.5.3).
|
|
fosterParenting bool
|
|
}
|
|
|
|
func (p *parser) top() *Node {
|
|
if n := p.oe.top(); n != nil {
|
|
return n
|
|
}
|
|
return p.doc
|
|
}
|
|
|
|
// stopTags for use in popUntil. These come from section 11.2.3.2.
|
|
var (
|
|
defaultScopeStopTags = []string{"applet", "caption", "html", "table", "td", "th", "marquee", "object"}
|
|
listItemScopeStopTags = []string{"applet", "caption", "html", "table", "td", "th", "marquee", "object", "ol", "ul"}
|
|
buttonScopeStopTags = []string{"applet", "caption", "html", "table", "td", "th", "marquee", "object", "button"}
|
|
tableScopeStopTags = []string{"html", "table"}
|
|
)
|
|
|
|
// stopTags for use in clearStackToContext.
|
|
var (
|
|
tableRowContextStopTags = []string{"tr", "html"}
|
|
)
|
|
|
|
// popUntil pops the stack of open elements at the highest element whose tag
|
|
// is in matchTags, provided there is no higher element in stopTags. It returns
|
|
// whether or not there was such an element. If there was not, popUntil leaves
|
|
// the stack unchanged.
|
|
//
|
|
// For example, if the stack was:
|
|
// ["html", "body", "font", "table", "b", "i", "u"]
|
|
// then popUntil([]string{"html, "table"}, "font") would return false, but
|
|
// popUntil([]string{"html, "table"}, "i") would return true and the resultant
|
|
// stack would be:
|
|
// ["html", "body", "font", "table", "b"]
|
|
//
|
|
// If an element's tag is in both stopTags and matchTags, then the stack will
|
|
// be popped and the function returns true (provided, of course, there was no
|
|
// higher element in the stack that was also in stopTags). For example,
|
|
// popUntil([]string{"html, "table"}, "table") would return true and leave:
|
|
// ["html", "body", "font"]
|
|
func (p *parser) popUntil(stopTags []string, matchTags ...string) bool {
|
|
if i := p.indexOfElementInScope(stopTags, matchTags...); i != -1 {
|
|
p.oe = p.oe[:i]
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// indexOfElementInScope returns the index in p.oe of the highest element
|
|
// whose tag is in matchTags that is in scope according to stopTags.
|
|
// If no matching element is in scope, it returns -1.
|
|
func (p *parser) indexOfElementInScope(stopTags []string, matchTags ...string) int {
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
tag := p.oe[i].Data
|
|
for _, t := range matchTags {
|
|
if t == tag {
|
|
return i
|
|
}
|
|
}
|
|
for _, t := range stopTags {
|
|
if t == tag {
|
|
return -1
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// elementInScope is like popUntil, except that it doesn't modify the stack of
|
|
// open elements.
|
|
func (p *parser) elementInScope(stopTags []string, matchTags ...string) bool {
|
|
return p.indexOfElementInScope(stopTags, matchTags...) != -1
|
|
}
|
|
|
|
// addChild adds a child node n to the top element, and pushes n onto the stack
|
|
// of open elements if it is an element node.
|
|
func (p *parser) addChild(n *Node) {
|
|
if p.fosterParenting {
|
|
p.fosterParent(n)
|
|
} else {
|
|
p.top().Add(n)
|
|
}
|
|
|
|
if n.Type == ElementNode {
|
|
p.oe = append(p.oe, n)
|
|
}
|
|
}
|
|
|
|
// fosterParent adds a child node according to the foster parenting rules.
|
|
// Section 11.2.5.3, "foster parenting".
|
|
func (p *parser) fosterParent(n *Node) {
|
|
var table, parent *Node
|
|
var i int
|
|
for i = len(p.oe) - 1; i >= 0; i-- {
|
|
if p.oe[i].Data == "table" {
|
|
table = p.oe[i]
|
|
break
|
|
}
|
|
}
|
|
|
|
if table == nil {
|
|
// The foster parent is the html element.
|
|
parent = p.oe[0]
|
|
} else {
|
|
parent = table.Parent
|
|
}
|
|
if parent == nil {
|
|
parent = p.oe[i-1]
|
|
}
|
|
|
|
var child *Node
|
|
for i, child = range parent.Child {
|
|
if child == table {
|
|
break
|
|
}
|
|
}
|
|
|
|
if i > 0 && parent.Child[i-1].Type == TextNode && n.Type == TextNode {
|
|
parent.Child[i-1].Data += n.Data
|
|
return
|
|
}
|
|
|
|
if i == len(parent.Child) {
|
|
parent.Add(n)
|
|
} else {
|
|
// Insert n into parent.Child at index i.
|
|
parent.Child = append(parent.Child[:i+1], parent.Child[i:]...)
|
|
parent.Child[i] = n
|
|
n.Parent = parent
|
|
}
|
|
}
|
|
|
|
// addText adds text to the preceding node if it is a text node, or else it
|
|
// calls addChild with a new text node.
|
|
func (p *parser) addText(text string) {
|
|
// TODO: distinguish whitespace text from others.
|
|
t := p.top()
|
|
if i := len(t.Child); i > 0 && t.Child[i-1].Type == TextNode {
|
|
t.Child[i-1].Data += text
|
|
return
|
|
}
|
|
p.addChild(&Node{
|
|
Type: TextNode,
|
|
Data: text,
|
|
})
|
|
}
|
|
|
|
// addElement calls addChild with an element node.
|
|
func (p *parser) addElement(tag string, attr []Attribute) {
|
|
p.addChild(&Node{
|
|
Type: ElementNode,
|
|
Data: tag,
|
|
Attr: attr,
|
|
})
|
|
}
|
|
|
|
// Section 11.2.3.3.
|
|
func (p *parser) addFormattingElement(tag string, attr []Attribute) {
|
|
p.addElement(tag, attr)
|
|
p.afe = append(p.afe, p.top())
|
|
// TODO.
|
|
}
|
|
|
|
// Section 11.2.3.3.
|
|
func (p *parser) clearActiveFormattingElements() {
|
|
for {
|
|
n := p.afe.pop()
|
|
if len(p.afe) == 0 || n.Type == scopeMarkerNode {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 11.2.3.3.
|
|
func (p *parser) reconstructActiveFormattingElements() {
|
|
n := p.afe.top()
|
|
if n == nil {
|
|
return
|
|
}
|
|
if n.Type == scopeMarkerNode || p.oe.index(n) != -1 {
|
|
return
|
|
}
|
|
i := len(p.afe) - 1
|
|
for n.Type != scopeMarkerNode && p.oe.index(n) == -1 {
|
|
if i == 0 {
|
|
i = -1
|
|
break
|
|
}
|
|
i--
|
|
n = p.afe[i]
|
|
}
|
|
for {
|
|
i++
|
|
clone := p.afe[i].clone()
|
|
p.addChild(clone)
|
|
p.afe[i] = clone
|
|
if i == len(p.afe)-1 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// read reads the next token. This is usually from the tokenizer, but it may
|
|
// be the synthesized end tag implied by a self-closing tag.
|
|
func (p *parser) read() os.Error {
|
|
if p.hasSelfClosingToken {
|
|
p.hasSelfClosingToken = false
|
|
p.tok.Type = EndTagToken
|
|
p.tok.Attr = nil
|
|
return nil
|
|
}
|
|
p.tokenizer.Next()
|
|
p.tok = p.tokenizer.Token()
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
return p.tokenizer.Error()
|
|
case SelfClosingTagToken:
|
|
p.hasSelfClosingToken = true
|
|
p.tok.Type = StartTagToken
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Section 11.2.4.
|
|
func (p *parser) acknowledgeSelfClosingTag() {
|
|
p.hasSelfClosingToken = false
|
|
}
|
|
|
|
// An insertion mode (section 11.2.3.1) is the state transition function from
|
|
// a particular state in the HTML5 parser's state machine. It updates the
|
|
// parser's fields depending on parser.token (where ErrorToken means EOF). In
|
|
// addition to returning the next insertionMode state, it also returns whether
|
|
// the token was consumed.
|
|
type insertionMode func(*parser) (insertionMode, bool)
|
|
|
|
// useTheRulesFor runs the delegate insertionMode over p, returning the actual
|
|
// insertionMode unless the delegate caused a state transition.
|
|
// Section 11.2.3.1, "using the rules for".
|
|
func useTheRulesFor(p *parser, actual, delegate insertionMode) (insertionMode, bool) {
|
|
im, consumed := delegate(p)
|
|
// TODO: do we need to update p.originalMode if it equals delegate?
|
|
if im != delegate {
|
|
return im, consumed
|
|
}
|
|
return actual, consumed
|
|
}
|
|
|
|
// setOriginalIM sets the insertion mode to return to after completing a text or
|
|
// inTableText insertion mode.
|
|
// Section 11.2.3.1, "using the rules for".
|
|
func (p *parser) setOriginalIM(im insertionMode) {
|
|
if p.originalIM != nil {
|
|
panic("html: bad parser state: originalIM was set twice")
|
|
}
|
|
p.originalIM = im
|
|
}
|
|
|
|
// Section 11.2.3.1, "reset the insertion mode".
|
|
func (p *parser) resetInsertionMode() insertionMode {
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
n := p.oe[i]
|
|
if i == 0 {
|
|
// TODO: set n to the context element, for HTML fragment parsing.
|
|
}
|
|
switch n.Data {
|
|
case "select":
|
|
return inSelectIM
|
|
case "td", "th":
|
|
return inCellIM
|
|
case "tr":
|
|
return inRowIM
|
|
case "tbody", "thead", "tfoot":
|
|
return inTableBodyIM
|
|
case "caption":
|
|
// TODO: return inCaptionIM
|
|
case "colgroup":
|
|
// TODO: return inColumnGroupIM
|
|
case "table":
|
|
return inTableIM
|
|
case "head":
|
|
return inBodyIM
|
|
case "body":
|
|
return inBodyIM
|
|
case "frameset":
|
|
// TODO: return inFramesetIM
|
|
case "html":
|
|
return beforeHeadIM
|
|
}
|
|
}
|
|
return inBodyIM
|
|
}
|
|
|
|
// Section 11.2.5.4.1.
|
|
func initialIM(p *parser) (insertionMode, bool) {
|
|
switch p.tok.Type {
|
|
case CommentToken:
|
|
p.doc.Add(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return initialIM, true
|
|
case DoctypeToken:
|
|
p.doc.Add(&Node{
|
|
Type: DoctypeNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return beforeHTMLIM, true
|
|
}
|
|
// TODO: set "quirks mode"? It's defined in the DOM spec instead of HTML5 proper,
|
|
// and so switching on "quirks mode" might belong in a different package.
|
|
return beforeHTMLIM, false
|
|
}
|
|
|
|
// Section 11.2.5.4.2.
|
|
func beforeHTMLIM(p *parser) (insertionMode, bool) {
|
|
var (
|
|
add bool
|
|
attr []Attribute
|
|
implied bool
|
|
)
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
implied = true
|
|
case TextToken:
|
|
// TODO: distinguish whitespace text from others.
|
|
implied = true
|
|
case StartTagToken:
|
|
if p.tok.Data == "html" {
|
|
add = true
|
|
attr = p.tok.Attr
|
|
} else {
|
|
implied = true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.Data {
|
|
case "head", "body", "html", "br":
|
|
implied = true
|
|
default:
|
|
// Ignore the token.
|
|
}
|
|
case CommentToken:
|
|
p.doc.Add(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return beforeHTMLIM, true
|
|
}
|
|
if add || implied {
|
|
p.addElement("html", attr)
|
|
}
|
|
return beforeHeadIM, !implied
|
|
}
|
|
|
|
// Section 11.2.5.4.3.
|
|
func beforeHeadIM(p *parser) (insertionMode, bool) {
|
|
var (
|
|
add bool
|
|
attr []Attribute
|
|
implied bool
|
|
)
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
implied = true
|
|
case TextToken:
|
|
// TODO: distinguish whitespace text from others.
|
|
implied = true
|
|
case StartTagToken:
|
|
switch p.tok.Data {
|
|
case "head":
|
|
add = true
|
|
attr = p.tok.Attr
|
|
case "html":
|
|
return useTheRulesFor(p, beforeHeadIM, inBodyIM)
|
|
default:
|
|
implied = true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.Data {
|
|
case "head", "body", "html", "br":
|
|
implied = true
|
|
default:
|
|
// Ignore the token.
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return beforeHeadIM, true
|
|
}
|
|
if add || implied {
|
|
p.addElement("head", attr)
|
|
}
|
|
return inHeadIM, !implied
|
|
}
|
|
|
|
// Section 11.2.5.4.4.
|
|
func inHeadIM(p *parser) (insertionMode, bool) {
|
|
var (
|
|
pop bool
|
|
implied bool
|
|
)
|
|
switch p.tok.Type {
|
|
case ErrorToken, TextToken:
|
|
implied = true
|
|
case StartTagToken:
|
|
switch p.tok.Data {
|
|
case "meta":
|
|
// TODO.
|
|
case "script", "title":
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
p.setOriginalIM(inHeadIM)
|
|
return textIM, true
|
|
default:
|
|
implied = true
|
|
}
|
|
case EndTagToken:
|
|
if p.tok.Data == "head" {
|
|
pop = true
|
|
}
|
|
// TODO.
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return inHeadIM, true
|
|
}
|
|
if pop || implied {
|
|
n := p.oe.pop()
|
|
if n.Data != "head" {
|
|
panic("html: bad parser state: <head> element not found, in the in-head insertion mode")
|
|
}
|
|
return afterHeadIM, !implied
|
|
}
|
|
return inHeadIM, !implied
|
|
}
|
|
|
|
// Section 11.2.5.4.6.
|
|
func afterHeadIM(p *parser) (insertionMode, bool) {
|
|
var (
|
|
add bool
|
|
attr []Attribute
|
|
framesetOK bool
|
|
implied bool
|
|
)
|
|
switch p.tok.Type {
|
|
case ErrorToken, TextToken:
|
|
implied = true
|
|
framesetOK = true
|
|
case StartTagToken:
|
|
switch p.tok.Data {
|
|
case "html":
|
|
// TODO.
|
|
case "body":
|
|
add = true
|
|
attr = p.tok.Attr
|
|
framesetOK = false
|
|
case "frameset":
|
|
// TODO.
|
|
case "base", "basefont", "bgsound", "link", "meta", "noframes", "script", "style", "title":
|
|
// TODO.
|
|
case "head":
|
|
// TODO.
|
|
default:
|
|
implied = true
|
|
framesetOK = true
|
|
}
|
|
case EndTagToken:
|
|
// TODO.
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return afterHeadIM, true
|
|
}
|
|
if add || implied {
|
|
p.addElement("body", attr)
|
|
p.framesetOK = framesetOK
|
|
}
|
|
return inBodyIM, !implied
|
|
}
|
|
|
|
// Section 11.2.5.4.7.
|
|
func inBodyIM(p *parser) (insertionMode, bool) {
|
|
switch p.tok.Type {
|
|
case TextToken:
|
|
p.reconstructActiveFormattingElements()
|
|
p.addText(p.tok.Data)
|
|
p.framesetOK = false
|
|
case StartTagToken:
|
|
switch p.tok.Data {
|
|
case "address", "article", "aside", "blockquote", "center", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "menu", "nav", "ol", "p", "section", "summary", "ul":
|
|
p.popUntil(buttonScopeStopTags, "p")
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
case "h1", "h2", "h3", "h4", "h5", "h6":
|
|
p.popUntil(buttonScopeStopTags, "p")
|
|
switch n := p.top(); n.Data {
|
|
case "h1", "h2", "h3", "h4", "h5", "h6":
|
|
p.oe.pop()
|
|
}
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
case "a":
|
|
if n := p.afe.forTag("a"); n != nil {
|
|
p.inBodyEndTagFormatting("a")
|
|
p.oe.remove(n)
|
|
p.afe.remove(n)
|
|
}
|
|
p.reconstructActiveFormattingElements()
|
|
p.addFormattingElement(p.tok.Data, p.tok.Attr)
|
|
case "b", "big", "code", "em", "font", "i", "s", "small", "strike", "strong", "tt", "u":
|
|
p.reconstructActiveFormattingElements()
|
|
p.addFormattingElement(p.tok.Data, p.tok.Attr)
|
|
case "applet", "marquee", "object":
|
|
p.reconstructActiveFormattingElements()
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
p.afe = append(p.afe, &scopeMarker)
|
|
p.framesetOK = false
|
|
case "area", "br", "embed", "img", "input", "keygen", "wbr":
|
|
p.reconstructActiveFormattingElements()
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
p.oe.pop()
|
|
p.acknowledgeSelfClosingTag()
|
|
p.framesetOK = false
|
|
case "table":
|
|
p.popUntil(buttonScopeStopTags, "p") // TODO: skip this step in quirks mode.
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
p.framesetOK = false
|
|
return inTableIM, true
|
|
case "hr":
|
|
p.popUntil(buttonScopeStopTags, "p")
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
p.oe.pop()
|
|
p.acknowledgeSelfClosingTag()
|
|
p.framesetOK = false
|
|
case "select":
|
|
p.reconstructActiveFormattingElements()
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
p.framesetOK = false
|
|
// TODO: detect <select> inside a table.
|
|
return inSelectIM, true
|
|
case "li":
|
|
p.framesetOK = false
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
node := p.oe[i]
|
|
switch node.Data {
|
|
case "li":
|
|
p.popUntil(listItemScopeStopTags, "li")
|
|
case "address", "div", "p":
|
|
continue
|
|
default:
|
|
if !isSpecialElement[node.Data] {
|
|
continue
|
|
}
|
|
}
|
|
break
|
|
}
|
|
p.popUntil(buttonScopeStopTags, "p")
|
|
p.addElement("li", p.tok.Attr)
|
|
default:
|
|
// TODO.
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.Data {
|
|
case "body":
|
|
// TODO: autoclose the stack of open elements.
|
|
return afterBodyIM, true
|
|
case "p":
|
|
if !p.elementInScope(buttonScopeStopTags, "p") {
|
|
p.addElement("p", nil)
|
|
}
|
|
p.popUntil(buttonScopeStopTags, "p")
|
|
case "a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u":
|
|
p.inBodyEndTagFormatting(p.tok.Data)
|
|
case "address", "article", "aside", "blockquote", "button", "center", "details", "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "listing", "menu", "nav", "ol", "pre", "section", "summary", "ul":
|
|
p.popUntil(defaultScopeStopTags, p.tok.Data)
|
|
default:
|
|
p.inBodyEndTagOther(p.tok.Data)
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
}
|
|
|
|
return inBodyIM, true
|
|
}
|
|
|
|
func (p *parser) inBodyEndTagFormatting(tag string) {
|
|
// This is the "adoption agency" algorithm, described at
|
|
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#adoptionAgency
|
|
|
|
// TODO: this is a fairly literal line-by-line translation of that algorithm.
|
|
// Once the code successfully parses the comprehensive test suite, we should
|
|
// refactor this code to be more idiomatic.
|
|
|
|
// Steps 1-3. The outer loop.
|
|
for i := 0; i < 8; i++ {
|
|
// Step 4. Find the formatting element.
|
|
var formattingElement *Node
|
|
for j := len(p.afe) - 1; j >= 0; j-- {
|
|
if p.afe[j].Type == scopeMarkerNode {
|
|
break
|
|
}
|
|
if p.afe[j].Data == tag {
|
|
formattingElement = p.afe[j]
|
|
break
|
|
}
|
|
}
|
|
if formattingElement == nil {
|
|
p.inBodyEndTagOther(tag)
|
|
return
|
|
}
|
|
feIndex := p.oe.index(formattingElement)
|
|
if feIndex == -1 {
|
|
p.afe.remove(formattingElement)
|
|
return
|
|
}
|
|
|
|
// Steps 5-6. Find the furthest block.
|
|
var furthestBlock *Node
|
|
for _, e := range p.oe[feIndex:] {
|
|
if isSpecialElement[e.Data] {
|
|
furthestBlock = e
|
|
break
|
|
}
|
|
}
|
|
if furthestBlock == nil {
|
|
e := p.oe.pop()
|
|
for e != formattingElement {
|
|
e = p.oe.pop()
|
|
}
|
|
p.afe.remove(e)
|
|
return
|
|
}
|
|
|
|
// Steps 7-8. Find the common ancestor and bookmark node.
|
|
commonAncestor := p.oe[feIndex-1]
|
|
bookmark := p.afe.index(formattingElement)
|
|
|
|
// Step 9. The inner loop. Find the lastNode to reparent.
|
|
lastNode := furthestBlock
|
|
node := furthestBlock
|
|
x := p.oe.index(node)
|
|
// Steps 9.1-9.3.
|
|
for j := 0; j < 3; j++ {
|
|
// Step 9.4.
|
|
x--
|
|
node = p.oe[x]
|
|
// Step 9.5.
|
|
if p.afe.index(node) == -1 {
|
|
p.oe.remove(node)
|
|
continue
|
|
}
|
|
// Step 9.6.
|
|
if node == formattingElement {
|
|
break
|
|
}
|
|
// Step 9.7.
|
|
clone := node.clone()
|
|
p.afe[p.afe.index(node)] = clone
|
|
p.oe[p.oe.index(node)] = clone
|
|
node = clone
|
|
// Step 9.8.
|
|
if lastNode == furthestBlock {
|
|
bookmark = p.afe.index(node) + 1
|
|
}
|
|
// Step 9.9.
|
|
if lastNode.Parent != nil {
|
|
lastNode.Parent.Remove(lastNode)
|
|
}
|
|
node.Add(lastNode)
|
|
// Step 9.10.
|
|
lastNode = node
|
|
}
|
|
|
|
// Step 10. Reparent lastNode to the common ancestor,
|
|
// or for misnested table nodes, to the foster parent.
|
|
if lastNode.Parent != nil {
|
|
lastNode.Parent.Remove(lastNode)
|
|
}
|
|
switch commonAncestor.Data {
|
|
case "table", "tbody", "tfoot", "thead", "tr":
|
|
p.fosterParent(lastNode)
|
|
default:
|
|
commonAncestor.Add(lastNode)
|
|
}
|
|
|
|
// Steps 11-13. Reparent nodes from the furthest block's children
|
|
// to a clone of the formatting element.
|
|
clone := formattingElement.clone()
|
|
reparentChildren(clone, furthestBlock)
|
|
furthestBlock.Add(clone)
|
|
|
|
// Step 14. Fix up the list of active formatting elements.
|
|
p.afe.remove(formattingElement)
|
|
p.afe.insert(bookmark, clone)
|
|
|
|
// Step 15. Fix up the stack of open elements.
|
|
p.oe.remove(formattingElement)
|
|
p.oe.insert(p.oe.index(furthestBlock)+1, clone)
|
|
}
|
|
}
|
|
|
|
// inBodyEndTagOther performs the "any other end tag" algorithm for inBodyIM.
|
|
func (p *parser) inBodyEndTagOther(tag string) {
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
if p.oe[i].Data == tag {
|
|
p.oe = p.oe[:i]
|
|
break
|
|
}
|
|
if isSpecialElement[p.oe[i].Data] {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 11.2.5.4.8.
|
|
func textIM(p *parser) (insertionMode, bool) {
|
|
switch p.tok.Type {
|
|
case TextToken:
|
|
p.addText(p.tok.Data)
|
|
return textIM, true
|
|
case EndTagToken:
|
|
p.oe.pop()
|
|
}
|
|
o := p.originalIM
|
|
p.originalIM = nil
|
|
return o, p.tok.Type == EndTagToken
|
|
}
|
|
|
|
// Section 11.2.5.4.9.
|
|
func inTableIM(p *parser) (insertionMode, bool) {
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
// Stop parsing.
|
|
return nil, true
|
|
case TextToken:
|
|
// TODO.
|
|
case StartTagToken:
|
|
switch p.tok.Data {
|
|
case "tbody", "tfoot", "thead":
|
|
p.clearStackToContext(tableScopeStopTags)
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
return inTableBodyIM, true
|
|
case "td", "th", "tr":
|
|
p.clearStackToContext(tableScopeStopTags)
|
|
p.addElement("tbody", nil)
|
|
return inTableBodyIM, false
|
|
case "table":
|
|
if p.popUntil(tableScopeStopTags, "table") {
|
|
return p.resetInsertionMode(), false
|
|
}
|
|
// Ignore the token.
|
|
return inTableIM, true
|
|
default:
|
|
// TODO.
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.Data {
|
|
case "table":
|
|
if p.popUntil(tableScopeStopTags, "table") {
|
|
return p.resetInsertionMode(), true
|
|
}
|
|
// Ignore the token.
|
|
return inTableIM, true
|
|
case "body", "caption", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr":
|
|
// Ignore the token.
|
|
return inTableIM, true
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return inTableIM, true
|
|
}
|
|
|
|
switch p.top().Data {
|
|
case "table", "tbody", "tfoot", "thead", "tr":
|
|
p.fosterParenting = true
|
|
defer func() { p.fosterParenting = false }()
|
|
}
|
|
|
|
return useTheRulesFor(p, inTableIM, inBodyIM)
|
|
}
|
|
|
|
// clearStackToContext pops elements off the stack of open elements
|
|
// until an element listed in stopTags is found.
|
|
func (p *parser) clearStackToContext(stopTags []string) {
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
for _, tag := range stopTags {
|
|
if p.oe[i].Data == tag {
|
|
p.oe = p.oe[:i+1]
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 11.2.5.4.13.
|
|
func inTableBodyIM(p *parser) (insertionMode, bool) {
|
|
var (
|
|
add bool
|
|
data string
|
|
attr []Attribute
|
|
consumed bool
|
|
)
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
// TODO.
|
|
case TextToken:
|
|
// TODO.
|
|
case StartTagToken:
|
|
switch p.tok.Data {
|
|
case "tr":
|
|
add = true
|
|
data = p.tok.Data
|
|
attr = p.tok.Attr
|
|
consumed = true
|
|
case "td", "th":
|
|
add = true
|
|
data = "tr"
|
|
consumed = false
|
|
default:
|
|
// TODO.
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.Data {
|
|
case "table":
|
|
if p.popUntil(tableScopeStopTags, "tbody", "thead", "tfoot") {
|
|
return inTableIM, false
|
|
}
|
|
// Ignore the token.
|
|
return inTableBodyIM, true
|
|
case "body", "caption", "col", "colgroup", "html", "td", "th", "tr":
|
|
// Ignore the token.
|
|
return inTableBodyIM, true
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return inTableBodyIM, true
|
|
}
|
|
if add {
|
|
// TODO: clear the stack back to a table body context.
|
|
p.addElement(data, attr)
|
|
return inRowIM, consumed
|
|
}
|
|
return useTheRulesFor(p, inTableBodyIM, inTableIM)
|
|
}
|
|
|
|
// Section 11.2.5.4.14.
|
|
func inRowIM(p *parser) (insertionMode, bool) {
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
// TODO.
|
|
case TextToken:
|
|
// TODO.
|
|
case StartTagToken:
|
|
switch p.tok.Data {
|
|
case "td", "th":
|
|
// TODO: clear the stack back to a table row context.
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
p.afe = append(p.afe, &scopeMarker)
|
|
return inCellIM, true
|
|
default:
|
|
// TODO.
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.Data {
|
|
case "tr":
|
|
if !p.elementInScope(tableScopeStopTags, "tr") {
|
|
return inRowIM, true
|
|
}
|
|
p.clearStackToContext(tableRowContextStopTags)
|
|
p.oe.pop()
|
|
return inTableBodyIM, true
|
|
case "table":
|
|
if p.popUntil(tableScopeStopTags, "tr") {
|
|
return inTableBodyIM, false
|
|
}
|
|
// Ignore the token.
|
|
return inRowIM, true
|
|
case "tbody", "tfoot", "thead":
|
|
// TODO.
|
|
case "body", "caption", "col", "colgroup", "html", "td", "th":
|
|
// Ignore the token.
|
|
return inRowIM, true
|
|
default:
|
|
// TODO.
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return inRowIM, true
|
|
}
|
|
return useTheRulesFor(p, inRowIM, inTableIM)
|
|
}
|
|
|
|
// Section 11.2.5.4.15.
|
|
func inCellIM(p *parser) (insertionMode, bool) {
|
|
var (
|
|
closeTheCellAndReprocess bool
|
|
)
|
|
switch p.tok.Type {
|
|
case StartTagToken:
|
|
switch p.tok.Data {
|
|
case "caption", "col", "colgroup", "tbody", "td", "tfoot", "th", "thead", "tr":
|
|
// TODO: check for "td" or "th" in table scope.
|
|
closeTheCellAndReprocess = true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.Data {
|
|
case "td", "th":
|
|
// TODO.
|
|
case "body", "caption", "col", "colgroup", "html":
|
|
// TODO.
|
|
case "table", "tbody", "tfoot", "thead", "tr":
|
|
// TODO: check for matching element in table scope.
|
|
closeTheCellAndReprocess = true
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return inCellIM, true
|
|
}
|
|
if closeTheCellAndReprocess {
|
|
if p.popUntil(tableScopeStopTags, "td") || p.popUntil(tableScopeStopTags, "th") {
|
|
p.clearActiveFormattingElements()
|
|
return inRowIM, false
|
|
}
|
|
}
|
|
return useTheRulesFor(p, inCellIM, inBodyIM)
|
|
}
|
|
|
|
// Section 11.2.5.4.16.
|
|
func inSelectIM(p *parser) (insertionMode, bool) {
|
|
endSelect := false
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
// TODO.
|
|
case TextToken:
|
|
p.addText(p.tok.Data)
|
|
case StartTagToken:
|
|
switch p.tok.Data {
|
|
case "html":
|
|
// TODO.
|
|
case "option":
|
|
if p.top().Data == "option" {
|
|
p.oe.pop()
|
|
}
|
|
p.addElement(p.tok.Data, p.tok.Attr)
|
|
case "optgroup":
|
|
// TODO.
|
|
case "select":
|
|
endSelect = true
|
|
case "input", "keygen", "textarea":
|
|
// TODO.
|
|
case "script":
|
|
// TODO.
|
|
default:
|
|
// Ignore the token.
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.Data {
|
|
case "option":
|
|
// TODO.
|
|
case "optgroup":
|
|
// TODO.
|
|
case "select":
|
|
endSelect = true
|
|
default:
|
|
// Ignore the token.
|
|
}
|
|
case CommentToken:
|
|
p.doc.Add(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
}
|
|
if endSelect {
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
switch p.oe[i].Data {
|
|
case "select":
|
|
p.oe = p.oe[:i]
|
|
return p.resetInsertionMode(), true
|
|
case "option", "optgroup":
|
|
continue
|
|
default:
|
|
// Ignore the token.
|
|
return inSelectIM, true
|
|
}
|
|
}
|
|
}
|
|
return inSelectIM, true
|
|
}
|
|
|
|
// Section 11.2.5.4.18.
|
|
func afterBodyIM(p *parser) (insertionMode, bool) {
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
// TODO.
|
|
case TextToken:
|
|
// TODO.
|
|
case StartTagToken:
|
|
// TODO.
|
|
case EndTagToken:
|
|
switch p.tok.Data {
|
|
case "html":
|
|
// TODO: autoclose the stack of open elements.
|
|
return afterAfterBodyIM, true
|
|
default:
|
|
// TODO.
|
|
}
|
|
case CommentToken:
|
|
// The comment is attached to the <html> element.
|
|
if len(p.oe) < 1 || p.oe[0].Data != "html" {
|
|
panic("html: bad parser state: <html> element not found, in the after-body insertion mode")
|
|
}
|
|
p.oe[0].Add(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return afterBodyIM, true
|
|
}
|
|
// TODO: should this be "return inBodyIM, true"?
|
|
return afterBodyIM, true
|
|
}
|
|
|
|
// Section 11.2.5.4.21.
|
|
func afterAfterBodyIM(p *parser) (insertionMode, bool) {
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
// Stop parsing.
|
|
return nil, true
|
|
case TextToken:
|
|
// TODO.
|
|
case StartTagToken:
|
|
if p.tok.Data == "html" {
|
|
return useTheRulesFor(p, afterAfterBodyIM, inBodyIM)
|
|
}
|
|
case CommentToken:
|
|
p.doc.Add(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return afterAfterBodyIM, true
|
|
}
|
|
return inBodyIM, false
|
|
}
|
|
|
|
// Parse returns the parse tree for the HTML from the given Reader.
|
|
// The input is assumed to be UTF-8 encoded.
|
|
func Parse(r io.Reader) (*Node, os.Error) {
|
|
p := &parser{
|
|
tokenizer: NewTokenizer(r),
|
|
doc: &Node{
|
|
Type: DocumentNode,
|
|
},
|
|
scripting: true,
|
|
framesetOK: true,
|
|
}
|
|
// Iterate until EOF. Any other error will cause an early return.
|
|
im, consumed := initialIM, true
|
|
for {
|
|
if consumed {
|
|
if err := p.read(); err != nil {
|
|
if err == os.EOF {
|
|
break
|
|
}
|
|
return nil, err
|
|
}
|
|
}
|
|
im, consumed = im(p)
|
|
}
|
|
// Loop until the final token (the ErrorToken signifying EOF) is consumed.
|
|
for {
|
|
if im, consumed = im(p); consumed {
|
|
break
|
|
}
|
|
}
|
|
return p.doc, nil
|
|
}
|