289b30715d
This commit converts the dependency management from Godeps to the vendor folder, also switching the tool from godep to trash. Since the upstream tool lacks a few features proposed via a few PRs, until those PRs are merged in (if), use github.com/karalabe/trash. You can update dependencies via trash --update. All dependencies have been updated to their latest version. Parts of the build system are reworked to drop old notions of Godeps and invocation of the go vet command so that it doesn't run against the vendor folder, as that will just blow up during vetting. The conversion drops OpenCL (and hence GPU mining support) from ethash and our codebase. The short reasoning is that there's noone to maintain and having opencl libs in our deps messes up builds as go install ./... tries to build them, failing with unsatisfied link errors for the C OpenCL deps. golang.org/x/net/context is not vendored in. We expect it to be fetched by the user (i.e. using go get). To keep ci.go builds reproducible the package is "vendored" in build/_vendor.
2095 lines
48 KiB
Go
2095 lines
48 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 (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
a "golang.org/x/net/html/atom"
|
|
)
|
|
|
|
// A parser implements the HTML5 parsing algorithm:
|
|
// https://html.spec.whatwg.org/multipage/syntax.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 treated as start tags, except that
|
|
// hasSelfClosingToken is set while they are being processed.
|
|
hasSelfClosingToken bool
|
|
// doc is the document root element.
|
|
doc *Node
|
|
// The stack of open elements (section 12.2.3.2) and active formatting
|
|
// elements (section 12.2.3.3).
|
|
oe, afe nodeStack
|
|
// Element pointers (section 12.2.3.4).
|
|
head, form *Node
|
|
// Other parsing state flags (section 12.2.3.5).
|
|
scripting, framesetOK bool
|
|
// im is the current insertion mode.
|
|
im insertionMode
|
|
// 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 12.2.5.3).
|
|
fosterParenting bool
|
|
// quirks is whether the parser is operating in "quirks mode."
|
|
quirks bool
|
|
// fragment is whether the parser is parsing an HTML fragment.
|
|
fragment bool
|
|
// context is the context element when parsing an HTML fragment
|
|
// (section 12.4).
|
|
context *Node
|
|
}
|
|
|
|
func (p *parser) top() *Node {
|
|
if n := p.oe.top(); n != nil {
|
|
return n
|
|
}
|
|
return p.doc
|
|
}
|
|
|
|
// Stop tags for use in popUntil. These come from section 12.2.3.2.
|
|
var (
|
|
defaultScopeStopTags = map[string][]a.Atom{
|
|
"": {a.Applet, a.Caption, a.Html, a.Table, a.Td, a.Th, a.Marquee, a.Object, a.Template},
|
|
"math": {a.AnnotationXml, a.Mi, a.Mn, a.Mo, a.Ms, a.Mtext},
|
|
"svg": {a.Desc, a.ForeignObject, a.Title},
|
|
}
|
|
)
|
|
|
|
type scope int
|
|
|
|
const (
|
|
defaultScope scope = iota
|
|
listItemScope
|
|
buttonScope
|
|
tableScope
|
|
tableRowScope
|
|
tableBodyScope
|
|
selectScope
|
|
)
|
|
|
|
// popUntil pops the stack of open elements at the highest element whose tag
|
|
// is in matchTags, provided there is no higher element in the scope's stop
|
|
// tags (as defined in section 12.2.3.2). It returns whether or not there was
|
|
// such an element. If there was not, popUntil leaves the stack unchanged.
|
|
//
|
|
// For example, the set of stop tags for table scope is: "html", "table". If
|
|
// the stack was:
|
|
// ["html", "body", "font", "table", "b", "i", "u"]
|
|
// then popUntil(tableScope, "font") would return false, but
|
|
// popUntil(tableScope, "i") would return true and the stack would become:
|
|
// ["html", "body", "font", "table", "b"]
|
|
//
|
|
// If an element's tag is in both the stop tags 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 the stop tags). For example,
|
|
// popUntil(tableScope, "table") returns true and leaves:
|
|
// ["html", "body", "font"]
|
|
func (p *parser) popUntil(s scope, matchTags ...a.Atom) bool {
|
|
if i := p.indexOfElementInScope(s, 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. If no matching element is in scope, it
|
|
// returns -1.
|
|
func (p *parser) indexOfElementInScope(s scope, matchTags ...a.Atom) int {
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
tagAtom := p.oe[i].DataAtom
|
|
if p.oe[i].Namespace == "" {
|
|
for _, t := range matchTags {
|
|
if t == tagAtom {
|
|
return i
|
|
}
|
|
}
|
|
switch s {
|
|
case defaultScope:
|
|
// No-op.
|
|
case listItemScope:
|
|
if tagAtom == a.Ol || tagAtom == a.Ul {
|
|
return -1
|
|
}
|
|
case buttonScope:
|
|
if tagAtom == a.Button {
|
|
return -1
|
|
}
|
|
case tableScope:
|
|
if tagAtom == a.Html || tagAtom == a.Table {
|
|
return -1
|
|
}
|
|
case selectScope:
|
|
if tagAtom != a.Optgroup && tagAtom != a.Option {
|
|
return -1
|
|
}
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
}
|
|
switch s {
|
|
case defaultScope, listItemScope, buttonScope:
|
|
for _, t := range defaultScopeStopTags[p.oe[i].Namespace] {
|
|
if t == tagAtom {
|
|
return -1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// elementInScope is like popUntil, except that it doesn't modify the stack of
|
|
// open elements.
|
|
func (p *parser) elementInScope(s scope, matchTags ...a.Atom) bool {
|
|
return p.indexOfElementInScope(s, matchTags...) != -1
|
|
}
|
|
|
|
// clearStackToContext pops elements off the stack of open elements until a
|
|
// scope-defined element is found.
|
|
func (p *parser) clearStackToContext(s scope) {
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
tagAtom := p.oe[i].DataAtom
|
|
switch s {
|
|
case tableScope:
|
|
if tagAtom == a.Html || tagAtom == a.Table {
|
|
p.oe = p.oe[:i+1]
|
|
return
|
|
}
|
|
case tableRowScope:
|
|
if tagAtom == a.Html || tagAtom == a.Tr {
|
|
p.oe = p.oe[:i+1]
|
|
return
|
|
}
|
|
case tableBodyScope:
|
|
if tagAtom == a.Html || tagAtom == a.Tbody || tagAtom == a.Tfoot || tagAtom == a.Thead {
|
|
p.oe = p.oe[:i+1]
|
|
return
|
|
}
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
}
|
|
}
|
|
|
|
// generateImpliedEndTags pops nodes off the stack of open elements as long as
|
|
// the top node has a tag name of dd, dt, li, option, optgroup, p, rp, or rt.
|
|
// If exceptions are specified, nodes with that name will not be popped off.
|
|
func (p *parser) generateImpliedEndTags(exceptions ...string) {
|
|
var i int
|
|
loop:
|
|
for i = len(p.oe) - 1; i >= 0; i-- {
|
|
n := p.oe[i]
|
|
if n.Type == ElementNode {
|
|
switch n.DataAtom {
|
|
case a.Dd, a.Dt, a.Li, a.Option, a.Optgroup, a.P, a.Rp, a.Rt:
|
|
for _, except := range exceptions {
|
|
if n.Data == except {
|
|
break loop
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
break
|
|
}
|
|
|
|
p.oe = p.oe[:i+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.shouldFosterParent() {
|
|
p.fosterParent(n)
|
|
} else {
|
|
p.top().AppendChild(n)
|
|
}
|
|
|
|
if n.Type == ElementNode {
|
|
p.oe = append(p.oe, n)
|
|
}
|
|
}
|
|
|
|
// shouldFosterParent returns whether the next node to be added should be
|
|
// foster parented.
|
|
func (p *parser) shouldFosterParent() bool {
|
|
if p.fosterParenting {
|
|
switch p.top().DataAtom {
|
|
case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// fosterParent adds a child node according to the foster parenting rules.
|
|
// Section 12.2.5.3, "foster parenting".
|
|
func (p *parser) fosterParent(n *Node) {
|
|
var table, parent, prev *Node
|
|
var i int
|
|
for i = len(p.oe) - 1; i >= 0; i-- {
|
|
if p.oe[i].DataAtom == a.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]
|
|
}
|
|
|
|
if table != nil {
|
|
prev = table.PrevSibling
|
|
} else {
|
|
prev = parent.LastChild
|
|
}
|
|
if prev != nil && prev.Type == TextNode && n.Type == TextNode {
|
|
prev.Data += n.Data
|
|
return
|
|
}
|
|
|
|
parent.InsertBefore(n, table)
|
|
}
|
|
|
|
// 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) {
|
|
if text == "" {
|
|
return
|
|
}
|
|
|
|
if p.shouldFosterParent() {
|
|
p.fosterParent(&Node{
|
|
Type: TextNode,
|
|
Data: text,
|
|
})
|
|
return
|
|
}
|
|
|
|
t := p.top()
|
|
if n := t.LastChild; n != nil && n.Type == TextNode {
|
|
n.Data += text
|
|
return
|
|
}
|
|
p.addChild(&Node{
|
|
Type: TextNode,
|
|
Data: text,
|
|
})
|
|
}
|
|
|
|
// addElement adds a child element based on the current token.
|
|
func (p *parser) addElement() {
|
|
p.addChild(&Node{
|
|
Type: ElementNode,
|
|
DataAtom: p.tok.DataAtom,
|
|
Data: p.tok.Data,
|
|
Attr: p.tok.Attr,
|
|
})
|
|
}
|
|
|
|
// Section 12.2.3.3.
|
|
func (p *parser) addFormattingElement() {
|
|
tagAtom, attr := p.tok.DataAtom, p.tok.Attr
|
|
p.addElement()
|
|
|
|
// Implement the Noah's Ark clause, but with three per family instead of two.
|
|
identicalElements := 0
|
|
findIdenticalElements:
|
|
for i := len(p.afe) - 1; i >= 0; i-- {
|
|
n := p.afe[i]
|
|
if n.Type == scopeMarkerNode {
|
|
break
|
|
}
|
|
if n.Type != ElementNode {
|
|
continue
|
|
}
|
|
if n.Namespace != "" {
|
|
continue
|
|
}
|
|
if n.DataAtom != tagAtom {
|
|
continue
|
|
}
|
|
if len(n.Attr) != len(attr) {
|
|
continue
|
|
}
|
|
compareAttributes:
|
|
for _, t0 := range n.Attr {
|
|
for _, t1 := range attr {
|
|
if t0.Key == t1.Key && t0.Namespace == t1.Namespace && t0.Val == t1.Val {
|
|
// Found a match for this attribute, continue with the next attribute.
|
|
continue compareAttributes
|
|
}
|
|
}
|
|
// If we get here, there is no attribute that matches a.
|
|
// Therefore the element is not identical to the new one.
|
|
continue findIdenticalElements
|
|
}
|
|
|
|
identicalElements++
|
|
if identicalElements >= 3 {
|
|
p.afe.remove(n)
|
|
}
|
|
}
|
|
|
|
p.afe = append(p.afe, p.top())
|
|
}
|
|
|
|
// Section 12.2.3.3.
|
|
func (p *parser) clearActiveFormattingElements() {
|
|
for {
|
|
n := p.afe.pop()
|
|
if len(p.afe) == 0 || n.Type == scopeMarkerNode {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 12.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
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 12.2.4.
|
|
func (p *parser) acknowledgeSelfClosingTag() {
|
|
p.hasSelfClosingToken = false
|
|
}
|
|
|
|
// An insertion mode (section 12.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.tok (where ErrorToken means EOF).
|
|
// It returns whether the token was consumed.
|
|
type insertionMode func(*parser) bool
|
|
|
|
// setOriginalIM sets the insertion mode to return to after completing a text or
|
|
// inTableText insertion mode.
|
|
// Section 12.2.3.1, "using the rules for".
|
|
func (p *parser) setOriginalIM() {
|
|
if p.originalIM != nil {
|
|
panic("html: bad parser state: originalIM was set twice")
|
|
}
|
|
p.originalIM = p.im
|
|
}
|
|
|
|
// Section 12.2.3.1, "reset the insertion mode".
|
|
func (p *parser) resetInsertionMode() {
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
n := p.oe[i]
|
|
if i == 0 && p.context != nil {
|
|
n = p.context
|
|
}
|
|
|
|
switch n.DataAtom {
|
|
case a.Select:
|
|
p.im = inSelectIM
|
|
case a.Td, a.Th:
|
|
p.im = inCellIM
|
|
case a.Tr:
|
|
p.im = inRowIM
|
|
case a.Tbody, a.Thead, a.Tfoot:
|
|
p.im = inTableBodyIM
|
|
case a.Caption:
|
|
p.im = inCaptionIM
|
|
case a.Colgroup:
|
|
p.im = inColumnGroupIM
|
|
case a.Table:
|
|
p.im = inTableIM
|
|
case a.Head:
|
|
p.im = inBodyIM
|
|
case a.Body:
|
|
p.im = inBodyIM
|
|
case a.Frameset:
|
|
p.im = inFramesetIM
|
|
case a.Html:
|
|
p.im = beforeHeadIM
|
|
default:
|
|
continue
|
|
}
|
|
return
|
|
}
|
|
p.im = inBodyIM
|
|
}
|
|
|
|
const whitespace = " \t\r\n\f"
|
|
|
|
// Section 12.2.5.4.1.
|
|
func initialIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case TextToken:
|
|
p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace)
|
|
if len(p.tok.Data) == 0 {
|
|
// It was all whitespace, so ignore it.
|
|
return true
|
|
}
|
|
case CommentToken:
|
|
p.doc.AppendChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return true
|
|
case DoctypeToken:
|
|
n, quirks := parseDoctype(p.tok.Data)
|
|
p.doc.AppendChild(n)
|
|
p.quirks = quirks
|
|
p.im = beforeHTMLIM
|
|
return true
|
|
}
|
|
p.quirks = true
|
|
p.im = beforeHTMLIM
|
|
return false
|
|
}
|
|
|
|
// Section 12.2.5.4.2.
|
|
func beforeHTMLIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case DoctypeToken:
|
|
// Ignore the token.
|
|
return true
|
|
case TextToken:
|
|
p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace)
|
|
if len(p.tok.Data) == 0 {
|
|
// It was all whitespace, so ignore it.
|
|
return true
|
|
}
|
|
case StartTagToken:
|
|
if p.tok.DataAtom == a.Html {
|
|
p.addElement()
|
|
p.im = beforeHeadIM
|
|
return true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Head, a.Body, a.Html, a.Br:
|
|
p.parseImpliedToken(StartTagToken, a.Html, a.Html.String())
|
|
return false
|
|
default:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case CommentToken:
|
|
p.doc.AppendChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return true
|
|
}
|
|
p.parseImpliedToken(StartTagToken, a.Html, a.Html.String())
|
|
return false
|
|
}
|
|
|
|
// Section 12.2.5.4.3.
|
|
func beforeHeadIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case TextToken:
|
|
p.tok.Data = strings.TrimLeft(p.tok.Data, whitespace)
|
|
if len(p.tok.Data) == 0 {
|
|
// It was all whitespace, so ignore it.
|
|
return true
|
|
}
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Head:
|
|
p.addElement()
|
|
p.head = p.top()
|
|
p.im = inHeadIM
|
|
return true
|
|
case a.Html:
|
|
return inBodyIM(p)
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Head, a.Body, a.Html, a.Br:
|
|
p.parseImpliedToken(StartTagToken, a.Head, a.Head.String())
|
|
return false
|
|
default:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return true
|
|
case DoctypeToken:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
|
|
p.parseImpliedToken(StartTagToken, a.Head, a.Head.String())
|
|
return false
|
|
}
|
|
|
|
// Section 12.2.5.4.4.
|
|
func inHeadIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
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 true
|
|
}
|
|
p.tok.Data = s
|
|
}
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Html:
|
|
return inBodyIM(p)
|
|
case a.Base, a.Basefont, a.Bgsound, a.Command, a.Link, a.Meta:
|
|
p.addElement()
|
|
p.oe.pop()
|
|
p.acknowledgeSelfClosingTag()
|
|
return true
|
|
case a.Script, a.Title, a.Noscript, a.Noframes, a.Style:
|
|
p.addElement()
|
|
p.setOriginalIM()
|
|
p.im = textIM
|
|
return true
|
|
case a.Head:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Head:
|
|
n := p.oe.pop()
|
|
if n.DataAtom != a.Head {
|
|
panic("html: bad parser state: <head> element not found, in the in-head insertion mode")
|
|
}
|
|
p.im = afterHeadIM
|
|
return true
|
|
case a.Body, a.Html, a.Br:
|
|
p.parseImpliedToken(EndTagToken, a.Head, a.Head.String())
|
|
return false
|
|
default:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return true
|
|
case DoctypeToken:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
|
|
p.parseImpliedToken(EndTagToken, a.Head, a.Head.String())
|
|
return false
|
|
}
|
|
|
|
// Section 12.2.5.4.6.
|
|
func afterHeadIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
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 true
|
|
}
|
|
p.tok.Data = s
|
|
}
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Html:
|
|
return inBodyIM(p)
|
|
case a.Body:
|
|
p.addElement()
|
|
p.framesetOK = false
|
|
p.im = inBodyIM
|
|
return true
|
|
case a.Frameset:
|
|
p.addElement()
|
|
p.im = inFramesetIM
|
|
return true
|
|
case a.Base, a.Basefont, a.Bgsound, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Title:
|
|
p.oe = append(p.oe, p.head)
|
|
defer p.oe.remove(p.head)
|
|
return inHeadIM(p)
|
|
case a.Head:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Body, a.Html, a.Br:
|
|
// Drop down to creating an implied <body> tag.
|
|
default:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return true
|
|
case DoctypeToken:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
|
|
p.parseImpliedToken(StartTagToken, a.Body, a.Body.String())
|
|
p.framesetOK = true
|
|
return false
|
|
}
|
|
|
|
// copyAttributes copies attributes of src not found on dst to dst.
|
|
func copyAttributes(dst *Node, src Token) {
|
|
if len(src.Attr) == 0 {
|
|
return
|
|
}
|
|
attr := map[string]string{}
|
|
for _, t := range dst.Attr {
|
|
attr[t.Key] = t.Val
|
|
}
|
|
for _, t := range src.Attr {
|
|
if _, ok := attr[t.Key]; !ok {
|
|
dst.Attr = append(dst.Attr, t)
|
|
attr[t.Key] = t.Val
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 12.2.5.4.7.
|
|
func inBodyIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case TextToken:
|
|
d := p.tok.Data
|
|
switch n := p.oe.top(); n.DataAtom {
|
|
case a.Pre, a.Listing:
|
|
if n.FirstChild == nil {
|
|
// Ignore a newline at the start of a <pre> block.
|
|
if d != "" && d[0] == '\r' {
|
|
d = d[1:]
|
|
}
|
|
if d != "" && d[0] == '\n' {
|
|
d = d[1:]
|
|
}
|
|
}
|
|
}
|
|
d = strings.Replace(d, "\x00", "", -1)
|
|
if d == "" {
|
|
return true
|
|
}
|
|
p.reconstructActiveFormattingElements()
|
|
p.addText(d)
|
|
if p.framesetOK && strings.TrimLeft(d, whitespace) != "" {
|
|
// There were non-whitespace characters inserted.
|
|
p.framesetOK = false
|
|
}
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Html:
|
|
copyAttributes(p.oe[0], p.tok)
|
|
case a.Base, a.Basefont, a.Bgsound, a.Command, a.Link, a.Meta, a.Noframes, a.Script, a.Style, a.Title:
|
|
return inHeadIM(p)
|
|
case a.Body:
|
|
if len(p.oe) >= 2 {
|
|
body := p.oe[1]
|
|
if body.Type == ElementNode && body.DataAtom == a.Body {
|
|
p.framesetOK = false
|
|
copyAttributes(body, p.tok)
|
|
}
|
|
}
|
|
case a.Frameset:
|
|
if !p.framesetOK || len(p.oe) < 2 || p.oe[1].DataAtom != a.Body {
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
body := p.oe[1]
|
|
if body.Parent != nil {
|
|
body.Parent.RemoveChild(body)
|
|
}
|
|
p.oe = p.oe[:1]
|
|
p.addElement()
|
|
p.im = inFramesetIM
|
|
return true
|
|
case a.Address, a.Article, a.Aside, a.Blockquote, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Menu, a.Nav, a.Ol, a.P, a.Section, a.Summary, a.Ul:
|
|
p.popUntil(buttonScope, a.P)
|
|
p.addElement()
|
|
case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
|
|
p.popUntil(buttonScope, a.P)
|
|
switch n := p.top(); n.DataAtom {
|
|
case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
|
|
p.oe.pop()
|
|
}
|
|
p.addElement()
|
|
case a.Pre, a.Listing:
|
|
p.popUntil(buttonScope, a.P)
|
|
p.addElement()
|
|
// The newline, if any, will be dealt with by the TextToken case.
|
|
p.framesetOK = false
|
|
case a.Form:
|
|
if p.form == nil {
|
|
p.popUntil(buttonScope, a.P)
|
|
p.addElement()
|
|
p.form = p.top()
|
|
}
|
|
case a.Li:
|
|
p.framesetOK = false
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
node := p.oe[i]
|
|
switch node.DataAtom {
|
|
case a.Li:
|
|
p.oe = p.oe[:i]
|
|
case a.Address, a.Div, a.P:
|
|
continue
|
|
default:
|
|
if !isSpecialElement(node) {
|
|
continue
|
|
}
|
|
}
|
|
break
|
|
}
|
|
p.popUntil(buttonScope, a.P)
|
|
p.addElement()
|
|
case a.Dd, a.Dt:
|
|
p.framesetOK = false
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
node := p.oe[i]
|
|
switch node.DataAtom {
|
|
case a.Dd, a.Dt:
|
|
p.oe = p.oe[:i]
|
|
case a.Address, a.Div, a.P:
|
|
continue
|
|
default:
|
|
if !isSpecialElement(node) {
|
|
continue
|
|
}
|
|
}
|
|
break
|
|
}
|
|
p.popUntil(buttonScope, a.P)
|
|
p.addElement()
|
|
case a.Plaintext:
|
|
p.popUntil(buttonScope, a.P)
|
|
p.addElement()
|
|
case a.Button:
|
|
p.popUntil(defaultScope, a.Button)
|
|
p.reconstructActiveFormattingElements()
|
|
p.addElement()
|
|
p.framesetOK = false
|
|
case a.A:
|
|
for i := len(p.afe) - 1; i >= 0 && p.afe[i].Type != scopeMarkerNode; i-- {
|
|
if n := p.afe[i]; n.Type == ElementNode && n.DataAtom == a.A {
|
|
p.inBodyEndTagFormatting(a.A)
|
|
p.oe.remove(n)
|
|
p.afe.remove(n)
|
|
break
|
|
}
|
|
}
|
|
p.reconstructActiveFormattingElements()
|
|
p.addFormattingElement()
|
|
case a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U:
|
|
p.reconstructActiveFormattingElements()
|
|
p.addFormattingElement()
|
|
case a.Nobr:
|
|
p.reconstructActiveFormattingElements()
|
|
if p.elementInScope(defaultScope, a.Nobr) {
|
|
p.inBodyEndTagFormatting(a.Nobr)
|
|
p.reconstructActiveFormattingElements()
|
|
}
|
|
p.addFormattingElement()
|
|
case a.Applet, a.Marquee, a.Object:
|
|
p.reconstructActiveFormattingElements()
|
|
p.addElement()
|
|
p.afe = append(p.afe, &scopeMarker)
|
|
p.framesetOK = false
|
|
case a.Table:
|
|
if !p.quirks {
|
|
p.popUntil(buttonScope, a.P)
|
|
}
|
|
p.addElement()
|
|
p.framesetOK = false
|
|
p.im = inTableIM
|
|
return true
|
|
case a.Area, a.Br, a.Embed, a.Img, a.Input, a.Keygen, a.Wbr:
|
|
p.reconstructActiveFormattingElements()
|
|
p.addElement()
|
|
p.oe.pop()
|
|
p.acknowledgeSelfClosingTag()
|
|
if p.tok.DataAtom == a.Input {
|
|
for _, t := range p.tok.Attr {
|
|
if t.Key == "type" {
|
|
if strings.ToLower(t.Val) == "hidden" {
|
|
// Skip setting framesetOK = false
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
p.framesetOK = false
|
|
case a.Param, a.Source, a.Track:
|
|
p.addElement()
|
|
p.oe.pop()
|
|
p.acknowledgeSelfClosingTag()
|
|
case a.Hr:
|
|
p.popUntil(buttonScope, a.P)
|
|
p.addElement()
|
|
p.oe.pop()
|
|
p.acknowledgeSelfClosingTag()
|
|
p.framesetOK = false
|
|
case a.Image:
|
|
p.tok.DataAtom = a.Img
|
|
p.tok.Data = a.Img.String()
|
|
return false
|
|
case a.Isindex:
|
|
if p.form != nil {
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
action := ""
|
|
prompt := "This is a searchable index. Enter search keywords: "
|
|
attr := []Attribute{{Key: "name", Val: "isindex"}}
|
|
for _, t := range p.tok.Attr {
|
|
switch t.Key {
|
|
case "action":
|
|
action = t.Val
|
|
case "name":
|
|
// Ignore the attribute.
|
|
case "prompt":
|
|
prompt = t.Val
|
|
default:
|
|
attr = append(attr, t)
|
|
}
|
|
}
|
|
p.acknowledgeSelfClosingTag()
|
|
p.popUntil(buttonScope, a.P)
|
|
p.parseImpliedToken(StartTagToken, a.Form, a.Form.String())
|
|
if action != "" {
|
|
p.form.Attr = []Attribute{{Key: "action", Val: action}}
|
|
}
|
|
p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
|
|
p.parseImpliedToken(StartTagToken, a.Label, a.Label.String())
|
|
p.addText(prompt)
|
|
p.addChild(&Node{
|
|
Type: ElementNode,
|
|
DataAtom: a.Input,
|
|
Data: a.Input.String(),
|
|
Attr: attr,
|
|
})
|
|
p.oe.pop()
|
|
p.parseImpliedToken(EndTagToken, a.Label, a.Label.String())
|
|
p.parseImpliedToken(StartTagToken, a.Hr, a.Hr.String())
|
|
p.parseImpliedToken(EndTagToken, a.Form, a.Form.String())
|
|
case a.Textarea:
|
|
p.addElement()
|
|
p.setOriginalIM()
|
|
p.framesetOK = false
|
|
p.im = textIM
|
|
case a.Xmp:
|
|
p.popUntil(buttonScope, a.P)
|
|
p.reconstructActiveFormattingElements()
|
|
p.framesetOK = false
|
|
p.addElement()
|
|
p.setOriginalIM()
|
|
p.im = textIM
|
|
case a.Iframe:
|
|
p.framesetOK = false
|
|
p.addElement()
|
|
p.setOriginalIM()
|
|
p.im = textIM
|
|
case a.Noembed, a.Noscript:
|
|
p.addElement()
|
|
p.setOriginalIM()
|
|
p.im = textIM
|
|
case a.Select:
|
|
p.reconstructActiveFormattingElements()
|
|
p.addElement()
|
|
p.framesetOK = false
|
|
p.im = inSelectIM
|
|
return true
|
|
case a.Optgroup, a.Option:
|
|
if p.top().DataAtom == a.Option {
|
|
p.oe.pop()
|
|
}
|
|
p.reconstructActiveFormattingElements()
|
|
p.addElement()
|
|
case a.Rp, a.Rt:
|
|
if p.elementInScope(defaultScope, a.Ruby) {
|
|
p.generateImpliedEndTags()
|
|
}
|
|
p.addElement()
|
|
case a.Math, a.Svg:
|
|
p.reconstructActiveFormattingElements()
|
|
if p.tok.DataAtom == a.Math {
|
|
adjustAttributeNames(p.tok.Attr, mathMLAttributeAdjustments)
|
|
} else {
|
|
adjustAttributeNames(p.tok.Attr, svgAttributeAdjustments)
|
|
}
|
|
adjustForeignAttributes(p.tok.Attr)
|
|
p.addElement()
|
|
p.top().Namespace = p.tok.Data
|
|
if p.hasSelfClosingToken {
|
|
p.oe.pop()
|
|
p.acknowledgeSelfClosingTag()
|
|
}
|
|
return true
|
|
case a.Caption, a.Col, a.Colgroup, a.Frame, a.Head, a.Tbody, a.Td, a.Tfoot, a.Th, a.Thead, a.Tr:
|
|
// Ignore the token.
|
|
default:
|
|
p.reconstructActiveFormattingElements()
|
|
p.addElement()
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Body:
|
|
if p.elementInScope(defaultScope, a.Body) {
|
|
p.im = afterBodyIM
|
|
}
|
|
case a.Html:
|
|
if p.elementInScope(defaultScope, a.Body) {
|
|
p.parseImpliedToken(EndTagToken, a.Body, a.Body.String())
|
|
return false
|
|
}
|
|
return true
|
|
case a.Address, a.Article, a.Aside, a.Blockquote, a.Button, a.Center, a.Details, a.Dir, a.Div, a.Dl, a.Fieldset, a.Figcaption, a.Figure, a.Footer, a.Header, a.Hgroup, a.Listing, a.Menu, a.Nav, a.Ol, a.Pre, a.Section, a.Summary, a.Ul:
|
|
p.popUntil(defaultScope, p.tok.DataAtom)
|
|
case a.Form:
|
|
node := p.form
|
|
p.form = nil
|
|
i := p.indexOfElementInScope(defaultScope, a.Form)
|
|
if node == nil || i == -1 || p.oe[i] != node {
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
p.generateImpliedEndTags()
|
|
p.oe.remove(node)
|
|
case a.P:
|
|
if !p.elementInScope(buttonScope, a.P) {
|
|
p.parseImpliedToken(StartTagToken, a.P, a.P.String())
|
|
}
|
|
p.popUntil(buttonScope, a.P)
|
|
case a.Li:
|
|
p.popUntil(listItemScope, a.Li)
|
|
case a.Dd, a.Dt:
|
|
p.popUntil(defaultScope, p.tok.DataAtom)
|
|
case a.H1, a.H2, a.H3, a.H4, a.H5, a.H6:
|
|
p.popUntil(defaultScope, a.H1, a.H2, a.H3, a.H4, a.H5, a.H6)
|
|
case a.A, a.B, a.Big, a.Code, a.Em, a.Font, a.I, a.Nobr, a.S, a.Small, a.Strike, a.Strong, a.Tt, a.U:
|
|
p.inBodyEndTagFormatting(p.tok.DataAtom)
|
|
case a.Applet, a.Marquee, a.Object:
|
|
if p.popUntil(defaultScope, p.tok.DataAtom) {
|
|
p.clearActiveFormattingElements()
|
|
}
|
|
case a.Br:
|
|
p.tok.Type = StartTagToken
|
|
return false
|
|
default:
|
|
p.inBodyEndTagOther(p.tok.DataAtom)
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (p *parser) inBodyEndTagFormatting(tagAtom a.Atom) {
|
|
// This is the "adoption agency" algorithm, described at
|
|
// https://html.spec.whatwg.org/multipage/syntax.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-4. The outer loop.
|
|
for i := 0; i < 8; i++ {
|
|
// Step 5. 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].DataAtom == tagAtom {
|
|
formattingElement = p.afe[j]
|
|
break
|
|
}
|
|
}
|
|
if formattingElement == nil {
|
|
p.inBodyEndTagOther(tagAtom)
|
|
return
|
|
}
|
|
feIndex := p.oe.index(formattingElement)
|
|
if feIndex == -1 {
|
|
p.afe.remove(formattingElement)
|
|
return
|
|
}
|
|
if !p.elementInScope(defaultScope, tagAtom) {
|
|
// Ignore the tag.
|
|
return
|
|
}
|
|
|
|
// Steps 9-10. Find the furthest block.
|
|
var furthestBlock *Node
|
|
for _, e := range p.oe[feIndex:] {
|
|
if isSpecialElement(e) {
|
|
furthestBlock = e
|
|
break
|
|
}
|
|
}
|
|
if furthestBlock == nil {
|
|
e := p.oe.pop()
|
|
for e != formattingElement {
|
|
e = p.oe.pop()
|
|
}
|
|
p.afe.remove(e)
|
|
return
|
|
}
|
|
|
|
// Steps 11-12. Find the common ancestor and bookmark node.
|
|
commonAncestor := p.oe[feIndex-1]
|
|
bookmark := p.afe.index(formattingElement)
|
|
|
|
// Step 13. The inner loop. Find the lastNode to reparent.
|
|
lastNode := furthestBlock
|
|
node := furthestBlock
|
|
x := p.oe.index(node)
|
|
// Steps 13.1-13.2
|
|
for j := 0; j < 3; j++ {
|
|
// Step 13.3.
|
|
x--
|
|
node = p.oe[x]
|
|
// Step 13.4 - 13.5.
|
|
if p.afe.index(node) == -1 {
|
|
p.oe.remove(node)
|
|
continue
|
|
}
|
|
// Step 13.6.
|
|
if node == formattingElement {
|
|
break
|
|
}
|
|
// Step 13.7.
|
|
clone := node.clone()
|
|
p.afe[p.afe.index(node)] = clone
|
|
p.oe[p.oe.index(node)] = clone
|
|
node = clone
|
|
// Step 13.8.
|
|
if lastNode == furthestBlock {
|
|
bookmark = p.afe.index(node) + 1
|
|
}
|
|
// Step 13.9.
|
|
if lastNode.Parent != nil {
|
|
lastNode.Parent.RemoveChild(lastNode)
|
|
}
|
|
node.AppendChild(lastNode)
|
|
// Step 13.10.
|
|
lastNode = node
|
|
}
|
|
|
|
// Step 14. Reparent lastNode to the common ancestor,
|
|
// or for misnested table nodes, to the foster parent.
|
|
if lastNode.Parent != nil {
|
|
lastNode.Parent.RemoveChild(lastNode)
|
|
}
|
|
switch commonAncestor.DataAtom {
|
|
case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
|
|
p.fosterParent(lastNode)
|
|
default:
|
|
commonAncestor.AppendChild(lastNode)
|
|
}
|
|
|
|
// Steps 15-17. Reparent nodes from the furthest block's children
|
|
// to a clone of the formatting element.
|
|
clone := formattingElement.clone()
|
|
reparentChildren(clone, furthestBlock)
|
|
furthestBlock.AppendChild(clone)
|
|
|
|
// Step 18. 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 19. 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.
|
|
// "Any other end tag" handling from 12.2.5.5 The rules for parsing tokens in foreign content
|
|
// https://html.spec.whatwg.org/multipage/syntax.html#parsing-main-inforeign
|
|
func (p *parser) inBodyEndTagOther(tagAtom a.Atom) {
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
if p.oe[i].DataAtom == tagAtom {
|
|
p.oe = p.oe[:i]
|
|
break
|
|
}
|
|
if isSpecialElement(p.oe[i]) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Section 12.2.5.4.8.
|
|
func textIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
p.oe.pop()
|
|
case TextToken:
|
|
d := p.tok.Data
|
|
if n := p.oe.top(); n.DataAtom == a.Textarea && n.FirstChild == nil {
|
|
// Ignore a newline at the start of a <textarea> block.
|
|
if d != "" && d[0] == '\r' {
|
|
d = d[1:]
|
|
}
|
|
if d != "" && d[0] == '\n' {
|
|
d = d[1:]
|
|
}
|
|
}
|
|
if d == "" {
|
|
return true
|
|
}
|
|
p.addText(d)
|
|
return true
|
|
case EndTagToken:
|
|
p.oe.pop()
|
|
}
|
|
p.im = p.originalIM
|
|
p.originalIM = nil
|
|
return p.tok.Type == EndTagToken
|
|
}
|
|
|
|
// Section 12.2.5.4.9.
|
|
func inTableIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
// Stop parsing.
|
|
return true
|
|
case TextToken:
|
|
p.tok.Data = strings.Replace(p.tok.Data, "\x00", "", -1)
|
|
switch p.oe.top().DataAtom {
|
|
case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
|
|
if strings.Trim(p.tok.Data, whitespace) == "" {
|
|
p.addText(p.tok.Data)
|
|
return true
|
|
}
|
|
}
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Caption:
|
|
p.clearStackToContext(tableScope)
|
|
p.afe = append(p.afe, &scopeMarker)
|
|
p.addElement()
|
|
p.im = inCaptionIM
|
|
return true
|
|
case a.Colgroup:
|
|
p.clearStackToContext(tableScope)
|
|
p.addElement()
|
|
p.im = inColumnGroupIM
|
|
return true
|
|
case a.Col:
|
|
p.parseImpliedToken(StartTagToken, a.Colgroup, a.Colgroup.String())
|
|
return false
|
|
case a.Tbody, a.Tfoot, a.Thead:
|
|
p.clearStackToContext(tableScope)
|
|
p.addElement()
|
|
p.im = inTableBodyIM
|
|
return true
|
|
case a.Td, a.Th, a.Tr:
|
|
p.parseImpliedToken(StartTagToken, a.Tbody, a.Tbody.String())
|
|
return false
|
|
case a.Table:
|
|
if p.popUntil(tableScope, a.Table) {
|
|
p.resetInsertionMode()
|
|
return false
|
|
}
|
|
// Ignore the token.
|
|
return true
|
|
case a.Style, a.Script:
|
|
return inHeadIM(p)
|
|
case a.Input:
|
|
for _, t := range p.tok.Attr {
|
|
if t.Key == "type" && strings.ToLower(t.Val) == "hidden" {
|
|
p.addElement()
|
|
p.oe.pop()
|
|
return true
|
|
}
|
|
}
|
|
// Otherwise drop down to the default action.
|
|
case a.Form:
|
|
if p.form != nil {
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
p.addElement()
|
|
p.form = p.oe.pop()
|
|
case a.Select:
|
|
p.reconstructActiveFormattingElements()
|
|
switch p.top().DataAtom {
|
|
case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
|
|
p.fosterParenting = true
|
|
}
|
|
p.addElement()
|
|
p.fosterParenting = false
|
|
p.framesetOK = false
|
|
p.im = inSelectInTableIM
|
|
return true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Table:
|
|
if p.popUntil(tableScope, a.Table) {
|
|
p.resetInsertionMode()
|
|
return true
|
|
}
|
|
// Ignore the token.
|
|
return true
|
|
case a.Body, a.Caption, a.Col, a.Colgroup, a.Html, a.Tbody, a.Td, a.Tfoot, a.Th, a.Thead, a.Tr:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return true
|
|
case DoctypeToken:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
|
|
p.fosterParenting = true
|
|
defer func() { p.fosterParenting = false }()
|
|
|
|
return inBodyIM(p)
|
|
}
|
|
|
|
// Section 12.2.5.4.11.
|
|
func inCaptionIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Caption, a.Col, a.Colgroup, a.Tbody, a.Td, a.Tfoot, a.Thead, a.Tr:
|
|
if p.popUntil(tableScope, a.Caption) {
|
|
p.clearActiveFormattingElements()
|
|
p.im = inTableIM
|
|
return false
|
|
} else {
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case a.Select:
|
|
p.reconstructActiveFormattingElements()
|
|
p.addElement()
|
|
p.framesetOK = false
|
|
p.im = inSelectInTableIM
|
|
return true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Caption:
|
|
if p.popUntil(tableScope, a.Caption) {
|
|
p.clearActiveFormattingElements()
|
|
p.im = inTableIM
|
|
}
|
|
return true
|
|
case a.Table:
|
|
if p.popUntil(tableScope, a.Caption) {
|
|
p.clearActiveFormattingElements()
|
|
p.im = inTableIM
|
|
return false
|
|
} else {
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case a.Body, a.Col, a.Colgroup, a.Html, a.Tbody, a.Td, a.Tfoot, a.Th, a.Thead, a.Tr:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
}
|
|
return inBodyIM(p)
|
|
}
|
|
|
|
// Section 12.2.5.4.12.
|
|
func inColumnGroupIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
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 true
|
|
}
|
|
p.tok.Data = s
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return true
|
|
case DoctypeToken:
|
|
// Ignore the token.
|
|
return true
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Html:
|
|
return inBodyIM(p)
|
|
case a.Col:
|
|
p.addElement()
|
|
p.oe.pop()
|
|
p.acknowledgeSelfClosingTag()
|
|
return true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Colgroup:
|
|
if p.oe.top().DataAtom != a.Html {
|
|
p.oe.pop()
|
|
p.im = inTableIM
|
|
}
|
|
return true
|
|
case a.Col:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
}
|
|
if p.oe.top().DataAtom != a.Html {
|
|
p.oe.pop()
|
|
p.im = inTableIM
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Section 12.2.5.4.13.
|
|
func inTableBodyIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Tr:
|
|
p.clearStackToContext(tableBodyScope)
|
|
p.addElement()
|
|
p.im = inRowIM
|
|
return true
|
|
case a.Td, a.Th:
|
|
p.parseImpliedToken(StartTagToken, a.Tr, a.Tr.String())
|
|
return false
|
|
case a.Caption, a.Col, a.Colgroup, a.Tbody, a.Tfoot, a.Thead:
|
|
if p.popUntil(tableScope, a.Tbody, a.Thead, a.Tfoot) {
|
|
p.im = inTableIM
|
|
return false
|
|
}
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Tbody, a.Tfoot, a.Thead:
|
|
if p.elementInScope(tableScope, p.tok.DataAtom) {
|
|
p.clearStackToContext(tableBodyScope)
|
|
p.oe.pop()
|
|
p.im = inTableIM
|
|
}
|
|
return true
|
|
case a.Table:
|
|
if p.popUntil(tableScope, a.Tbody, a.Thead, a.Tfoot) {
|
|
p.im = inTableIM
|
|
return false
|
|
}
|
|
// Ignore the token.
|
|
return true
|
|
case a.Body, a.Caption, a.Col, a.Colgroup, a.Html, a.Td, a.Th, a.Tr:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return true
|
|
}
|
|
|
|
return inTableIM(p)
|
|
}
|
|
|
|
// Section 12.2.5.4.14.
|
|
func inRowIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Td, a.Th:
|
|
p.clearStackToContext(tableRowScope)
|
|
p.addElement()
|
|
p.afe = append(p.afe, &scopeMarker)
|
|
p.im = inCellIM
|
|
return true
|
|
case a.Caption, a.Col, a.Colgroup, a.Tbody, a.Tfoot, a.Thead, a.Tr:
|
|
if p.popUntil(tableScope, a.Tr) {
|
|
p.im = inTableBodyIM
|
|
return false
|
|
}
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Tr:
|
|
if p.popUntil(tableScope, a.Tr) {
|
|
p.im = inTableBodyIM
|
|
return true
|
|
}
|
|
// Ignore the token.
|
|
return true
|
|
case a.Table:
|
|
if p.popUntil(tableScope, a.Tr) {
|
|
p.im = inTableBodyIM
|
|
return false
|
|
}
|
|
// Ignore the token.
|
|
return true
|
|
case a.Tbody, a.Tfoot, a.Thead:
|
|
if p.elementInScope(tableScope, p.tok.DataAtom) {
|
|
p.parseImpliedToken(EndTagToken, a.Tr, a.Tr.String())
|
|
return false
|
|
}
|
|
// Ignore the token.
|
|
return true
|
|
case a.Body, a.Caption, a.Col, a.Colgroup, a.Html, a.Td, a.Th:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
}
|
|
|
|
return inTableIM(p)
|
|
}
|
|
|
|
// Section 12.2.5.4.15.
|
|
func inCellIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Caption, a.Col, a.Colgroup, a.Tbody, a.Td, a.Tfoot, a.Th, a.Thead, a.Tr:
|
|
if p.popUntil(tableScope, a.Td, a.Th) {
|
|
// Close the cell and reprocess.
|
|
p.clearActiveFormattingElements()
|
|
p.im = inRowIM
|
|
return false
|
|
}
|
|
// Ignore the token.
|
|
return true
|
|
case a.Select:
|
|
p.reconstructActiveFormattingElements()
|
|
p.addElement()
|
|
p.framesetOK = false
|
|
p.im = inSelectInTableIM
|
|
return true
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Td, a.Th:
|
|
if !p.popUntil(tableScope, p.tok.DataAtom) {
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
p.clearActiveFormattingElements()
|
|
p.im = inRowIM
|
|
return true
|
|
case a.Body, a.Caption, a.Col, a.Colgroup, a.Html:
|
|
// Ignore the token.
|
|
return true
|
|
case a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr:
|
|
if !p.elementInScope(tableScope, p.tok.DataAtom) {
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
// Close the cell and reprocess.
|
|
p.popUntil(tableScope, a.Td, a.Th)
|
|
p.clearActiveFormattingElements()
|
|
p.im = inRowIM
|
|
return false
|
|
}
|
|
}
|
|
return inBodyIM(p)
|
|
}
|
|
|
|
// Section 12.2.5.4.16.
|
|
func inSelectIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
// Stop parsing.
|
|
return true
|
|
case TextToken:
|
|
p.addText(strings.Replace(p.tok.Data, "\x00", "", -1))
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Html:
|
|
return inBodyIM(p)
|
|
case a.Option:
|
|
if p.top().DataAtom == a.Option {
|
|
p.oe.pop()
|
|
}
|
|
p.addElement()
|
|
case a.Optgroup:
|
|
if p.top().DataAtom == a.Option {
|
|
p.oe.pop()
|
|
}
|
|
if p.top().DataAtom == a.Optgroup {
|
|
p.oe.pop()
|
|
}
|
|
p.addElement()
|
|
case a.Select:
|
|
p.tok.Type = EndTagToken
|
|
return false
|
|
case a.Input, a.Keygen, a.Textarea:
|
|
if p.elementInScope(selectScope, a.Select) {
|
|
p.parseImpliedToken(EndTagToken, a.Select, a.Select.String())
|
|
return false
|
|
}
|
|
// In order to properly ignore <textarea>, we need to change the tokenizer mode.
|
|
p.tokenizer.NextIsNotRawText()
|
|
// Ignore the token.
|
|
return true
|
|
case a.Script:
|
|
return inHeadIM(p)
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Option:
|
|
if p.top().DataAtom == a.Option {
|
|
p.oe.pop()
|
|
}
|
|
case a.Optgroup:
|
|
i := len(p.oe) - 1
|
|
if p.oe[i].DataAtom == a.Option {
|
|
i--
|
|
}
|
|
if p.oe[i].DataAtom == a.Optgroup {
|
|
p.oe = p.oe[:i]
|
|
}
|
|
case a.Select:
|
|
if p.popUntil(selectScope, a.Select) {
|
|
p.resetInsertionMode()
|
|
}
|
|
}
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
case DoctypeToken:
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Section 12.2.5.4.17.
|
|
func inSelectInTableIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case StartTagToken, EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Caption, a.Table, a.Tbody, a.Tfoot, a.Thead, a.Tr, a.Td, a.Th:
|
|
if p.tok.Type == StartTagToken || p.elementInScope(tableScope, p.tok.DataAtom) {
|
|
p.parseImpliedToken(EndTagToken, a.Select, a.Select.String())
|
|
return false
|
|
} else {
|
|
// Ignore the token.
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return inSelectIM(p)
|
|
}
|
|
|
|
// Section 12.2.5.4.18.
|
|
func afterBodyIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
// Stop parsing.
|
|
return true
|
|
case TextToken:
|
|
s := strings.TrimLeft(p.tok.Data, whitespace)
|
|
if len(s) == 0 {
|
|
// It was all whitespace.
|
|
return inBodyIM(p)
|
|
}
|
|
case StartTagToken:
|
|
if p.tok.DataAtom == a.Html {
|
|
return inBodyIM(p)
|
|
}
|
|
case EndTagToken:
|
|
if p.tok.DataAtom == a.Html {
|
|
if !p.fragment {
|
|
p.im = afterAfterBodyIM
|
|
}
|
|
return true
|
|
}
|
|
case CommentToken:
|
|
// The comment is attached to the <html> element.
|
|
if len(p.oe) < 1 || p.oe[0].DataAtom != a.Html {
|
|
panic("html: bad parser state: <html> element not found, in the after-body insertion mode")
|
|
}
|
|
p.oe[0].AppendChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return true
|
|
}
|
|
p.im = inBodyIM
|
|
return false
|
|
}
|
|
|
|
// Section 12.2.5.4.19.
|
|
func inFramesetIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
case TextToken:
|
|
// Ignore all text but whitespace.
|
|
s := strings.Map(func(c rune) rune {
|
|
switch c {
|
|
case ' ', '\t', '\n', '\f', '\r':
|
|
return c
|
|
}
|
|
return -1
|
|
}, p.tok.Data)
|
|
if s != "" {
|
|
p.addText(s)
|
|
}
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Html:
|
|
return inBodyIM(p)
|
|
case a.Frameset:
|
|
p.addElement()
|
|
case a.Frame:
|
|
p.addElement()
|
|
p.oe.pop()
|
|
p.acknowledgeSelfClosingTag()
|
|
case a.Noframes:
|
|
return inHeadIM(p)
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Frameset:
|
|
if p.oe.top().DataAtom != a.Html {
|
|
p.oe.pop()
|
|
if p.oe.top().DataAtom != a.Frameset {
|
|
p.im = afterFramesetIM
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
// Ignore the token.
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Section 12.2.5.4.20.
|
|
func afterFramesetIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
case TextToken:
|
|
// Ignore all text but whitespace.
|
|
s := strings.Map(func(c rune) rune {
|
|
switch c {
|
|
case ' ', '\t', '\n', '\f', '\r':
|
|
return c
|
|
}
|
|
return -1
|
|
}, p.tok.Data)
|
|
if s != "" {
|
|
p.addText(s)
|
|
}
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Html:
|
|
return inBodyIM(p)
|
|
case a.Noframes:
|
|
return inHeadIM(p)
|
|
}
|
|
case EndTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Html:
|
|
p.im = afterAfterFramesetIM
|
|
return true
|
|
}
|
|
default:
|
|
// Ignore the token.
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Section 12.2.5.4.21.
|
|
func afterAfterBodyIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case ErrorToken:
|
|
// Stop parsing.
|
|
return true
|
|
case TextToken:
|
|
s := strings.TrimLeft(p.tok.Data, whitespace)
|
|
if len(s) == 0 {
|
|
// It was all whitespace.
|
|
return inBodyIM(p)
|
|
}
|
|
case StartTagToken:
|
|
if p.tok.DataAtom == a.Html {
|
|
return inBodyIM(p)
|
|
}
|
|
case CommentToken:
|
|
p.doc.AppendChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
return true
|
|
case DoctypeToken:
|
|
return inBodyIM(p)
|
|
}
|
|
p.im = inBodyIM
|
|
return false
|
|
}
|
|
|
|
// Section 12.2.5.4.22.
|
|
func afterAfterFramesetIM(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case CommentToken:
|
|
p.doc.AppendChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
case TextToken:
|
|
// Ignore all text but whitespace.
|
|
s := strings.Map(func(c rune) rune {
|
|
switch c {
|
|
case ' ', '\t', '\n', '\f', '\r':
|
|
return c
|
|
}
|
|
return -1
|
|
}, p.tok.Data)
|
|
if s != "" {
|
|
p.tok.Data = s
|
|
return inBodyIM(p)
|
|
}
|
|
case StartTagToken:
|
|
switch p.tok.DataAtom {
|
|
case a.Html:
|
|
return inBodyIM(p)
|
|
case a.Noframes:
|
|
return inHeadIM(p)
|
|
}
|
|
case DoctypeToken:
|
|
return inBodyIM(p)
|
|
default:
|
|
// Ignore the token.
|
|
}
|
|
return true
|
|
}
|
|
|
|
const whitespaceOrNUL = whitespace + "\x00"
|
|
|
|
// Section 12.2.5.5.
|
|
func parseForeignContent(p *parser) bool {
|
|
switch p.tok.Type {
|
|
case TextToken:
|
|
if p.framesetOK {
|
|
p.framesetOK = strings.TrimLeft(p.tok.Data, whitespaceOrNUL) == ""
|
|
}
|
|
p.tok.Data = strings.Replace(p.tok.Data, "\x00", "\ufffd", -1)
|
|
p.addText(p.tok.Data)
|
|
case CommentToken:
|
|
p.addChild(&Node{
|
|
Type: CommentNode,
|
|
Data: p.tok.Data,
|
|
})
|
|
case StartTagToken:
|
|
b := breakout[p.tok.Data]
|
|
if p.tok.DataAtom == a.Font {
|
|
loop:
|
|
for _, attr := range p.tok.Attr {
|
|
switch attr.Key {
|
|
case "color", "face", "size":
|
|
b = true
|
|
break loop
|
|
}
|
|
}
|
|
}
|
|
if b {
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
n := p.oe[i]
|
|
if n.Namespace == "" || htmlIntegrationPoint(n) || mathMLTextIntegrationPoint(n) {
|
|
p.oe = p.oe[:i+1]
|
|
break
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
switch p.top().Namespace {
|
|
case "math":
|
|
adjustAttributeNames(p.tok.Attr, mathMLAttributeAdjustments)
|
|
case "svg":
|
|
// Adjust SVG tag names. The tokenizer lower-cases tag names, but
|
|
// SVG wants e.g. "foreignObject" with a capital second "O".
|
|
if x := svgTagNameAdjustments[p.tok.Data]; x != "" {
|
|
p.tok.DataAtom = a.Lookup([]byte(x))
|
|
p.tok.Data = x
|
|
}
|
|
adjustAttributeNames(p.tok.Attr, svgAttributeAdjustments)
|
|
default:
|
|
panic("html: bad parser state: unexpected namespace")
|
|
}
|
|
adjustForeignAttributes(p.tok.Attr)
|
|
namespace := p.top().Namespace
|
|
p.addElement()
|
|
p.top().Namespace = namespace
|
|
if namespace != "" {
|
|
// Don't let the tokenizer go into raw text mode in foreign content
|
|
// (e.g. in an SVG <title> tag).
|
|
p.tokenizer.NextIsNotRawText()
|
|
}
|
|
if p.hasSelfClosingToken {
|
|
p.oe.pop()
|
|
p.acknowledgeSelfClosingTag()
|
|
}
|
|
case EndTagToken:
|
|
for i := len(p.oe) - 1; i >= 0; i-- {
|
|
if p.oe[i].Namespace == "" {
|
|
return p.im(p)
|
|
}
|
|
if strings.EqualFold(p.oe[i].Data, p.tok.Data) {
|
|
p.oe = p.oe[:i]
|
|
break
|
|
}
|
|
}
|
|
return true
|
|
default:
|
|
// Ignore the token.
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Section 12.2.5.
|
|
func (p *parser) inForeignContent() bool {
|
|
if len(p.oe) == 0 {
|
|
return false
|
|
}
|
|
n := p.oe[len(p.oe)-1]
|
|
if n.Namespace == "" {
|
|
return false
|
|
}
|
|
if mathMLTextIntegrationPoint(n) {
|
|
if p.tok.Type == StartTagToken && p.tok.DataAtom != a.Mglyph && p.tok.DataAtom != a.Malignmark {
|
|
return false
|
|
}
|
|
if p.tok.Type == TextToken {
|
|
return false
|
|
}
|
|
}
|
|
if n.Namespace == "math" && n.DataAtom == a.AnnotationXml && p.tok.Type == StartTagToken && p.tok.DataAtom == a.Svg {
|
|
return false
|
|
}
|
|
if htmlIntegrationPoint(n) && (p.tok.Type == StartTagToken || p.tok.Type == TextToken) {
|
|
return false
|
|
}
|
|
if p.tok.Type == ErrorToken {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// parseImpliedToken parses a token as though it had appeared in the parser's
|
|
// input.
|
|
func (p *parser) parseImpliedToken(t TokenType, dataAtom a.Atom, data string) {
|
|
realToken, selfClosing := p.tok, p.hasSelfClosingToken
|
|
p.tok = Token{
|
|
Type: t,
|
|
DataAtom: dataAtom,
|
|
Data: data,
|
|
}
|
|
p.hasSelfClosingToken = false
|
|
p.parseCurrentToken()
|
|
p.tok, p.hasSelfClosingToken = realToken, selfClosing
|
|
}
|
|
|
|
// parseCurrentToken runs the current token through the parsing routines
|
|
// until it is consumed.
|
|
func (p *parser) parseCurrentToken() {
|
|
if p.tok.Type == SelfClosingTagToken {
|
|
p.hasSelfClosingToken = true
|
|
p.tok.Type = StartTagToken
|
|
}
|
|
|
|
consumed := false
|
|
for !consumed {
|
|
if p.inForeignContent() {
|
|
consumed = parseForeignContent(p)
|
|
} else {
|
|
consumed = p.im(p)
|
|
}
|
|
}
|
|
|
|
if p.hasSelfClosingToken {
|
|
// This is a parse error, but ignore it.
|
|
p.hasSelfClosingToken = false
|
|
}
|
|
}
|
|
|
|
func (p *parser) parse() error {
|
|
// Iterate until EOF. Any other error will cause an early return.
|
|
var err error
|
|
for err != io.EOF {
|
|
// CDATA sections are allowed only in foreign content.
|
|
n := p.oe.top()
|
|
p.tokenizer.AllowCDATA(n != nil && n.Namespace != "")
|
|
// Read and parse the next token.
|
|
p.tokenizer.Next()
|
|
p.tok = p.tokenizer.Token()
|
|
if p.tok.Type == ErrorToken {
|
|
err = p.tokenizer.Err()
|
|
if err != nil && err != io.EOF {
|
|
return err
|
|
}
|
|
}
|
|
p.parseCurrentToken()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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, error) {
|
|
p := &parser{
|
|
tokenizer: NewTokenizer(r),
|
|
doc: &Node{
|
|
Type: DocumentNode,
|
|
},
|
|
scripting: true,
|
|
framesetOK: true,
|
|
im: initialIM,
|
|
}
|
|
err := p.parse()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.doc, nil
|
|
}
|
|
|
|
// ParseFragment parses a fragment of HTML and returns the nodes that were
|
|
// found. If the fragment is the InnerHTML for an existing element, pass that
|
|
// element in context.
|
|
func ParseFragment(r io.Reader, context *Node) ([]*Node, error) {
|
|
contextTag := ""
|
|
if context != nil {
|
|
if context.Type != ElementNode {
|
|
return nil, errors.New("html: ParseFragment of non-element Node")
|
|
}
|
|
// The next check isn't just context.DataAtom.String() == context.Data because
|
|
// it is valid to pass an element whose tag isn't a known atom. For example,
|
|
// DataAtom == 0 and Data = "tagfromthefuture" is perfectly consistent.
|
|
if context.DataAtom != a.Lookup([]byte(context.Data)) {
|
|
return nil, fmt.Errorf("html: inconsistent Node: DataAtom=%q, Data=%q", context.DataAtom, context.Data)
|
|
}
|
|
contextTag = context.DataAtom.String()
|
|
}
|
|
p := &parser{
|
|
tokenizer: NewTokenizerFragment(r, contextTag),
|
|
doc: &Node{
|
|
Type: DocumentNode,
|
|
},
|
|
scripting: true,
|
|
fragment: true,
|
|
context: context,
|
|
}
|
|
|
|
root := &Node{
|
|
Type: ElementNode,
|
|
DataAtom: a.Html,
|
|
Data: a.Html.String(),
|
|
}
|
|
p.doc.AppendChild(root)
|
|
p.oe = nodeStack{root}
|
|
p.resetInsertionMode()
|
|
|
|
for n := context; n != nil; n = n.Parent {
|
|
if n.Type == ElementNode && n.DataAtom == a.Form {
|
|
p.form = n
|
|
break
|
|
}
|
|
}
|
|
|
|
err := p.parse()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
parent := p.doc
|
|
if context != nil {
|
|
parent = root
|
|
}
|
|
|
|
var result []*Node
|
|
for c := parent.FirstChild; c != nil; {
|
|
next := c.NextSibling
|
|
parent.RemoveChild(c)
|
|
result = append(result, c)
|
|
c = next
|
|
}
|
|
return result, nil
|
|
}
|