| // 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" |
| "strings" |
| ) |
| |
| // 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) { |
| p.fosterParenting = false |
| 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 |
| } |
| |
| const whitespace = " \t\r\n\f" |
| |
| // Section 11.2.5.4.4. |
| func inHeadIM(p *parser) (insertionMode, bool) { |
| var ( |
| pop bool |
| implied bool |
| ) |
| switch p.tok.Type { |
| case ErrorToken: |
| implied = true |
| case TextToken: |
| s := strings.TrimLeft(p.tok.Data, whitespace) |
| if len(s) < len(p.tok.Data) { |
| // Add the initial whitespace to the current node. |
| p.addText(p.tok.Data[:len(p.tok.Data)-len(s)]) |
| if s == "" { |
| return inHeadIM, true |
| } |
| p.tok.Data = s |
| } |
| implied = true |
| case StartTagToken: |
| switch p.tok.Data { |
| case "meta": |
| // TODO. |
| case "script", "title", "noscript", "noframes", "style": |
| 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, true |
| } |
| |
| // 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": |
| for i := len(p.afe) - 1; i >= 0 && p.afe[i].Type != scopeMarkerNode; i-- { |
| if n := p.afe[i]; n.Type == ElementNode && n.Data == "a" { |
| p.inBodyEndTagFormatting("a") |
| p.oe.remove(n) |
| p.afe.remove(n) |
| break |
| } |
| } |
| 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) |
| case "optgroup", "option": |
| if p.top().Data == "option" { |
| p.oe.pop() |
| } |
| p.reconstructActiveFormattingElements() |
| p.addElement(p.tok.Data, 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 |
| } |
| if !p.elementInScope(defaultScopeStopTags, tag) { |
| // Ignore the tag. |
| 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. |
| if oldLoc := p.afe.index(formattingElement); oldLoc != -1 && oldLoc < bookmark { |
| // Move the bookmark with the rest of the list. |
| bookmark-- |
| } |
| 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 ErrorToken: |
| p.oe.pop() |
| 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": |
| if !p.popUntil(tableScopeStopTags, p.tok.Data) { |
| // Ignore the token. |
| return inCellIM, true |
| } |
| p.clearActiveFormattingElements() |
| return inRowIM, true |
| 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 |
| } |