Document extractor service (#15665)
* Document extractor service * Fixing vendor modules * Addressing PR Review comments * Some small simplifications * Fixing a linter complain * simplifying a bit the code using package variables Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
04ef5c682e
Коммит
8d5be2d657
1
vendor/github.com/levigross/exp-html/README
сгенерированный
поставляемый
Обычный файл
1
vendor/github.com/levigross/exp-html/README
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1 @@
|
||||
Copied from weekly.2012-03-27
|
||||
100
vendor/github.com/levigross/exp-html/const.go
сгенерированный
поставляемый
Обычный файл
100
vendor/github.com/levigross/exp-html/const.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,100 @@
|
||||
// Copyright 2011 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
|
||||
|
||||
// Section 12.2.3.2 of the HTML5 specification says "The following elements
|
||||
// have varying levels of special parsing rules".
|
||||
// http://www.whatwg.org/specs/web-apps/current-work/multipage/parsing.html#the-stack-of-open-elements
|
||||
var isSpecialElementMap = map[string]bool{
|
||||
"address": true,
|
||||
"applet": true,
|
||||
"area": true,
|
||||
"article": true,
|
||||
"aside": true,
|
||||
"base": true,
|
||||
"basefont": true,
|
||||
"bgsound": true,
|
||||
"blockquote": true,
|
||||
"body": true,
|
||||
"br": true,
|
||||
"button": true,
|
||||
"caption": true,
|
||||
"center": true,
|
||||
"col": true,
|
||||
"colgroup": true,
|
||||
"command": true,
|
||||
"dd": true,
|
||||
"details": true,
|
||||
"dir": true,
|
||||
"div": true,
|
||||
"dl": true,
|
||||
"dt": true,
|
||||
"embed": true,
|
||||
"fieldset": true,
|
||||
"figcaption": true,
|
||||
"figure": true,
|
||||
"footer": true,
|
||||
"form": true,
|
||||
"frame": true,
|
||||
"frameset": true,
|
||||
"h1": true,
|
||||
"h2": true,
|
||||
"h3": true,
|
||||
"h4": true,
|
||||
"h5": true,
|
||||
"h6": true,
|
||||
"head": true,
|
||||
"header": true,
|
||||
"hgroup": true,
|
||||
"hr": true,
|
||||
"html": true,
|
||||
"iframe": true,
|
||||
"img": true,
|
||||
"input": true,
|
||||
"isindex": true,
|
||||
"li": true,
|
||||
"link": true,
|
||||
"listing": true,
|
||||
"marquee": true,
|
||||
"menu": true,
|
||||
"meta": true,
|
||||
"nav": true,
|
||||
"noembed": true,
|
||||
"noframes": true,
|
||||
"noscript": true,
|
||||
"object": true,
|
||||
"ol": true,
|
||||
"p": true,
|
||||
"param": true,
|
||||
"plaintext": true,
|
||||
"pre": true,
|
||||
"script": true,
|
||||
"section": true,
|
||||
"select": true,
|
||||
"style": true,
|
||||
"summary": true,
|
||||
"table": true,
|
||||
"tbody": true,
|
||||
"td": true,
|
||||
"textarea": true,
|
||||
"tfoot": true,
|
||||
"th": true,
|
||||
"thead": true,
|
||||
"title": true,
|
||||
"tr": true,
|
||||
"ul": true,
|
||||
"wbr": true,
|
||||
"xmp": true,
|
||||
}
|
||||
|
||||
func isSpecialElement(element *Node) bool {
|
||||
switch element.Namespace {
|
||||
case "", "html":
|
||||
return isSpecialElementMap[element.Data]
|
||||
case "svg":
|
||||
return element.Data == "foreignObject"
|
||||
}
|
||||
return false
|
||||
}
|
||||
107
vendor/github.com/levigross/exp-html/doc.go
сгенерированный
поставляемый
Обычный файл
107
vendor/github.com/levigross/exp-html/doc.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,107 @@
|
||||
// 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 implements an HTML5-compliant tokenizer and parser.
|
||||
INCOMPLETE.
|
||||
|
||||
Tokenization is done by creating a Tokenizer for an io.Reader r. It is the
|
||||
caller's responsibility to ensure that r provides UTF-8 encoded HTML.
|
||||
|
||||
z := html.NewTokenizer(r)
|
||||
|
||||
Given a Tokenizer z, the HTML is tokenized by repeatedly calling z.Next(),
|
||||
which parses the next token and returns its type, or an error:
|
||||
|
||||
for {
|
||||
tt := z.Next()
|
||||
if tt == html.ErrorToken {
|
||||
// ...
|
||||
return ...
|
||||
}
|
||||
// Process the current token.
|
||||
}
|
||||
|
||||
There are two APIs for retrieving the current token. The high-level API is to
|
||||
call Token; the low-level API is to call Text or TagName / TagAttr. Both APIs
|
||||
allow optionally calling Raw after Next but before Token, Text, TagName, or
|
||||
TagAttr. In EBNF notation, the valid call sequence per token is:
|
||||
|
||||
Next {Raw} [ Token | Text | TagName {TagAttr} ]
|
||||
|
||||
Token returns an independent data structure that completely describes a token.
|
||||
Entities (such as "<") are unescaped, tag names and attribute keys are
|
||||
lower-cased, and attributes are collected into a []Attribute. For example:
|
||||
|
||||
for {
|
||||
if z.Next() == html.ErrorToken {
|
||||
// Returning io.EOF indicates success.
|
||||
return z.Err()
|
||||
}
|
||||
emitToken(z.Token())
|
||||
}
|
||||
|
||||
The low-level API performs fewer allocations and copies, but the contents of
|
||||
the []byte values returned by Text, TagName and TagAttr may change on the next
|
||||
call to Next. For example, to extract an HTML page's anchor text:
|
||||
|
||||
depth := 0
|
||||
for {
|
||||
tt := z.Next()
|
||||
switch tt {
|
||||
case ErrorToken:
|
||||
return z.Err()
|
||||
case TextToken:
|
||||
if depth > 0 {
|
||||
// emitBytes should copy the []byte it receives,
|
||||
// if it doesn't process it immediately.
|
||||
emitBytes(z.Text())
|
||||
}
|
||||
case StartTagToken, EndTagToken:
|
||||
tn, _ := z.TagName()
|
||||
if len(tn) == 1 && tn[0] == 'a' {
|
||||
if tt == StartTagToken {
|
||||
depth++
|
||||
} else {
|
||||
depth--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Parsing is done by calling Parse with an io.Reader, which returns the root of
|
||||
the parse tree (the document element) as a *Node. It is the caller's
|
||||
responsibility to ensure that the Reader provides UTF-8 encoded HTML. For
|
||||
example, to process each anchor node in depth-first order:
|
||||
|
||||
doc, err := html.Parse(r)
|
||||
if err != nil {
|
||||
// ...
|
||||
}
|
||||
var f func(*html.Node)
|
||||
f = func(n *html.Node) {
|
||||
if n.Type == html.ElementNode && n.Data == "a" {
|
||||
// Do something with n...
|
||||
}
|
||||
for _, c := range n.Child {
|
||||
f(c)
|
||||
}
|
||||
}
|
||||
f(doc)
|
||||
|
||||
The relevant specifications include:
|
||||
http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html and
|
||||
http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html
|
||||
*/
|
||||
package html
|
||||
|
||||
// The tokenization algorithm implemented by this package is not a line-by-line
|
||||
// transliteration of the relatively verbose state-machine in the WHATWG
|
||||
// specification. A more direct approach is used instead, where the program
|
||||
// counter implies the state, such as whether it is tokenizing a tag or a text
|
||||
// node. Specification compliance is verified by checking expected and actual
|
||||
// outputs over a test suite rather than aiming for algorithmic fidelity.
|
||||
|
||||
// TODO(nigeltao): Does a DOM API belong in this package or a separate one?
|
||||
// TODO(nigeltao): How does parsing interact with a JavaScript engine?
|
||||
156
vendor/github.com/levigross/exp-html/doctype.go
сгенерированный
поставляемый
Обычный файл
156
vendor/github.com/levigross/exp-html/doctype.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,156 @@
|
||||
// Copyright 2011 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 (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// parseDoctype parses the data from a DoctypeToken into a name,
|
||||
// public identifier, and system identifier. It returns a Node whose Type
|
||||
// is DoctypeNode, whose Data is the name, and which has attributes
|
||||
// named "system" and "public" for the two identifiers if they were present.
|
||||
// quirks is whether the document should be parsed in "quirks mode".
|
||||
func parseDoctype(s string) (n *Node, quirks bool) {
|
||||
n = &Node{Type: DoctypeNode}
|
||||
|
||||
// Find the name.
|
||||
space := strings.IndexAny(s, whitespace)
|
||||
if space == -1 {
|
||||
space = len(s)
|
||||
}
|
||||
n.Data = s[:space]
|
||||
// The comparison to "html" is case-sensitive.
|
||||
if n.Data != "html" {
|
||||
quirks = true
|
||||
}
|
||||
n.Data = strings.ToLower(n.Data)
|
||||
s = strings.TrimLeft(s[space:], whitespace)
|
||||
|
||||
if len(s) < 6 {
|
||||
// It can't start with "PUBLIC" or "SYSTEM".
|
||||
// Ignore the rest of the string.
|
||||
return n, quirks || s != ""
|
||||
}
|
||||
|
||||
key := strings.ToLower(s[:6])
|
||||
s = s[6:]
|
||||
for key == "public" || key == "system" {
|
||||
s = strings.TrimLeft(s, whitespace)
|
||||
if s == "" {
|
||||
break
|
||||
}
|
||||
quote := s[0]
|
||||
if quote != '"' && quote != '\'' {
|
||||
break
|
||||
}
|
||||
s = s[1:]
|
||||
q := strings.IndexRune(s, rune(quote))
|
||||
var id string
|
||||
if q == -1 {
|
||||
id = s
|
||||
s = ""
|
||||
} else {
|
||||
id = s[:q]
|
||||
s = s[q+1:]
|
||||
}
|
||||
n.Attr = append(n.Attr, Attribute{Key: key, Val: id})
|
||||
if key == "public" {
|
||||
key = "system"
|
||||
} else {
|
||||
key = ""
|
||||
}
|
||||
}
|
||||
|
||||
if key != "" || s != "" {
|
||||
quirks = true
|
||||
} else if len(n.Attr) > 0 {
|
||||
if n.Attr[0].Key == "public" {
|
||||
public := strings.ToLower(n.Attr[0].Val)
|
||||
switch public {
|
||||
case "-//w3o//dtd w3 html strict 3.0//en//", "-/w3d/dtd html 4.0 transitional/en", "html":
|
||||
quirks = true
|
||||
default:
|
||||
for _, q := range quirkyIDs {
|
||||
if strings.HasPrefix(public, q) {
|
||||
quirks = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// The following two public IDs only cause quirks mode if there is no system ID.
|
||||
if len(n.Attr) == 1 && (strings.HasPrefix(public, "-//w3c//dtd html 4.01 frameset//") ||
|
||||
strings.HasPrefix(public, "-//w3c//dtd html 4.01 transitional//")) {
|
||||
quirks = true
|
||||
}
|
||||
}
|
||||
if lastAttr := n.Attr[len(n.Attr)-1]; lastAttr.Key == "system" &&
|
||||
strings.ToLower(lastAttr.Val) == "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd" {
|
||||
quirks = true
|
||||
}
|
||||
}
|
||||
|
||||
return n, quirks
|
||||
}
|
||||
|
||||
// quirkyIDs is a list of public doctype identifiers that cause a document
|
||||
// to be interpreted in quirks mode. The identifiers should be in lower case.
|
||||
var quirkyIDs = []string{
|
||||
"+//silmaril//dtd html pro v0r11 19970101//",
|
||||
"-//advasoft ltd//dtd html 3.0 aswedit + extensions//",
|
||||
"-//as//dtd html 3.0 aswedit + extensions//",
|
||||
"-//ietf//dtd html 2.0 level 1//",
|
||||
"-//ietf//dtd html 2.0 level 2//",
|
||||
"-//ietf//dtd html 2.0 strict level 1//",
|
||||
"-//ietf//dtd html 2.0 strict level 2//",
|
||||
"-//ietf//dtd html 2.0 strict//",
|
||||
"-//ietf//dtd html 2.0//",
|
||||
"-//ietf//dtd html 2.1e//",
|
||||
"-//ietf//dtd html 3.0//",
|
||||
"-//ietf//dtd html 3.2 final//",
|
||||
"-//ietf//dtd html 3.2//",
|
||||
"-//ietf//dtd html 3//",
|
||||
"-//ietf//dtd html level 0//",
|
||||
"-//ietf//dtd html level 1//",
|
||||
"-//ietf//dtd html level 2//",
|
||||
"-//ietf//dtd html level 3//",
|
||||
"-//ietf//dtd html strict level 0//",
|
||||
"-//ietf//dtd html strict level 1//",
|
||||
"-//ietf//dtd html strict level 2//",
|
||||
"-//ietf//dtd html strict level 3//",
|
||||
"-//ietf//dtd html strict//",
|
||||
"-//ietf//dtd html//",
|
||||
"-//metrius//dtd metrius presentational//",
|
||||
"-//microsoft//dtd internet explorer 2.0 html strict//",
|
||||
"-//microsoft//dtd internet explorer 2.0 html//",
|
||||
"-//microsoft//dtd internet explorer 2.0 tables//",
|
||||
"-//microsoft//dtd internet explorer 3.0 html strict//",
|
||||
"-//microsoft//dtd internet explorer 3.0 html//",
|
||||
"-//microsoft//dtd internet explorer 3.0 tables//",
|
||||
"-//netscape comm. corp.//dtd html//",
|
||||
"-//netscape comm. corp.//dtd strict html//",
|
||||
"-//o'reilly and associates//dtd html 2.0//",
|
||||
"-//o'reilly and associates//dtd html extended 1.0//",
|
||||
"-//o'reilly and associates//dtd html extended relaxed 1.0//",
|
||||
"-//softquad software//dtd hotmetal pro 6.0::19990601::extensions to html 4.0//",
|
||||
"-//softquad//dtd hotmetal pro 4.0::19971010::extensions to html 4.0//",
|
||||
"-//spyglass//dtd html 2.0 extended//",
|
||||
"-//sq//dtd html 2.0 hotmetal + extensions//",
|
||||
"-//sun microsystems corp.//dtd hotjava html//",
|
||||
"-//sun microsystems corp.//dtd hotjava strict html//",
|
||||
"-//w3c//dtd html 3 1995-03-24//",
|
||||
"-//w3c//dtd html 3.2 draft//",
|
||||
"-//w3c//dtd html 3.2 final//",
|
||||
"-//w3c//dtd html 3.2//",
|
||||
"-//w3c//dtd html 3.2s draft//",
|
||||
"-//w3c//dtd html 4.0 frameset//",
|
||||
"-//w3c//dtd html 4.0 transitional//",
|
||||
"-//w3c//dtd html experimental 19960712//",
|
||||
"-//w3c//dtd html experimental 970421//",
|
||||
"-//w3c//dtd w3 html//",
|
||||
"-//w3o//dtd w3 html 3.0//",
|
||||
"-//webtechs//dtd mozilla html 2.0//",
|
||||
"-//webtechs//dtd mozilla html//",
|
||||
}
|
||||
2253
vendor/github.com/levigross/exp-html/entity.go
сгенерированный
поставляемый
Обычный файл
2253
vendor/github.com/levigross/exp-html/entity.go
сгенерированный
поставляемый
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
253
vendor/github.com/levigross/exp-html/escape.go
сгенерированный
поставляемый
Обычный файл
253
vendor/github.com/levigross/exp-html/escape.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,253 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// These replacements permit compatibility with old numeric entities that
|
||||
// assumed Windows-1252 encoding.
|
||||
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#consume-a-character-reference
|
||||
var replacementTable = [...]rune{
|
||||
'\u20AC', // First entry is what 0x80 should be replaced with.
|
||||
'\u0081',
|
||||
'\u201A',
|
||||
'\u0192',
|
||||
'\u201E',
|
||||
'\u2026',
|
||||
'\u2020',
|
||||
'\u2021',
|
||||
'\u02C6',
|
||||
'\u2030',
|
||||
'\u0160',
|
||||
'\u2039',
|
||||
'\u0152',
|
||||
'\u008D',
|
||||
'\u017D',
|
||||
'\u008F',
|
||||
'\u0090',
|
||||
'\u2018',
|
||||
'\u2019',
|
||||
'\u201C',
|
||||
'\u201D',
|
||||
'\u2022',
|
||||
'\u2013',
|
||||
'\u2014',
|
||||
'\u02DC',
|
||||
'\u2122',
|
||||
'\u0161',
|
||||
'\u203A',
|
||||
'\u0153',
|
||||
'\u009D',
|
||||
'\u017E',
|
||||
'\u0178', // Last entry is 0x9F.
|
||||
// 0x00->'\uFFFD' is handled programmatically.
|
||||
// 0x0D->'\u000D' is a no-op.
|
||||
}
|
||||
|
||||
// unescapeEntity reads an entity like "<" from b[src:] and writes the
|
||||
// corresponding "<" to b[dst:], returning the incremented dst and src cursors.
|
||||
// Precondition: b[src] == '&' && dst <= src.
|
||||
// attribute should be true if parsing an attribute value.
|
||||
func unescapeEntity(b []byte, dst, src int, attribute bool) (dst1, src1 int) {
|
||||
// http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#consume-a-character-reference
|
||||
|
||||
// i starts at 1 because we already know that s[0] == '&'.
|
||||
i, s := 1, b[src:]
|
||||
|
||||
if len(s) <= 1 {
|
||||
b[dst] = b[src]
|
||||
return dst + 1, src + 1
|
||||
}
|
||||
|
||||
if s[i] == '#' {
|
||||
if len(s) <= 3 { // We need to have at least "&#.".
|
||||
b[dst] = b[src]
|
||||
return dst + 1, src + 1
|
||||
}
|
||||
i++
|
||||
c := s[i]
|
||||
hex := false
|
||||
if c == 'x' || c == 'X' {
|
||||
hex = true
|
||||
i++
|
||||
}
|
||||
|
||||
x := '\x00'
|
||||
for i < len(s) {
|
||||
c = s[i]
|
||||
i++
|
||||
if hex {
|
||||
if '0' <= c && c <= '9' {
|
||||
x = 16*x + rune(c) - '0'
|
||||
continue
|
||||
} else if 'a' <= c && c <= 'f' {
|
||||
x = 16*x + rune(c) - 'a' + 10
|
||||
continue
|
||||
} else if 'A' <= c && c <= 'F' {
|
||||
x = 16*x + rune(c) - 'A' + 10
|
||||
continue
|
||||
}
|
||||
} else if '0' <= c && c <= '9' {
|
||||
x = 10*x + rune(c) - '0'
|
||||
continue
|
||||
}
|
||||
if c != ';' {
|
||||
i--
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if i <= 3 { // No characters matched.
|
||||
b[dst] = b[src]
|
||||
return dst + 1, src + 1
|
||||
}
|
||||
|
||||
if 0x80 <= x && x <= 0x9F {
|
||||
// Replace characters from Windows-1252 with UTF-8 equivalents.
|
||||
x = replacementTable[x-0x80]
|
||||
} else if x == 0 || (0xD800 <= x && x <= 0xDFFF) || x > 0x10FFFF {
|
||||
// Replace invalid characters with the replacement character.
|
||||
x = '\uFFFD'
|
||||
}
|
||||
|
||||
return dst + utf8.EncodeRune(b[dst:], x), src + i
|
||||
}
|
||||
|
||||
// Consume the maximum number of characters possible, with the
|
||||
// consumed characters matching one of the named references.
|
||||
|
||||
for i < len(s) {
|
||||
c := s[i]
|
||||
i++
|
||||
// Lower-cased characters are more common in entities, so we check for them first.
|
||||
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
|
||||
continue
|
||||
}
|
||||
if c != ';' {
|
||||
i--
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
entityName := string(s[1:i])
|
||||
if entityName == "" {
|
||||
// No-op.
|
||||
} else if attribute && entityName[len(entityName)-1] != ';' && len(s) > i && s[i] == '=' {
|
||||
// No-op.
|
||||
} else if x := entity[entityName]; x != 0 {
|
||||
return dst + utf8.EncodeRune(b[dst:], x), src + i
|
||||
} else if x := entity2[entityName]; x[0] != 0 {
|
||||
dst1 := dst + utf8.EncodeRune(b[dst:], x[0])
|
||||
return dst1 + utf8.EncodeRune(b[dst1:], x[1]), src + i
|
||||
} else if !attribute {
|
||||
maxLen := len(entityName) - 1
|
||||
if maxLen > longestEntityWithoutSemicolon {
|
||||
maxLen = longestEntityWithoutSemicolon
|
||||
}
|
||||
for j := maxLen; j > 1; j-- {
|
||||
if x := entity[entityName[:j]]; x != 0 {
|
||||
return dst + utf8.EncodeRune(b[dst:], x), src + j + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dst1, src1 = dst+i, src+i
|
||||
copy(b[dst:dst1], b[src:src1])
|
||||
return dst1, src1
|
||||
}
|
||||
|
||||
// unescape unescapes b's entities in-place, so that "a<b" becomes "a<b".
|
||||
func unescape(b []byte) []byte {
|
||||
for i, c := range b {
|
||||
if c == '&' {
|
||||
dst, src := unescapeEntity(b, i, i, false)
|
||||
for src < len(b) {
|
||||
c := b[src]
|
||||
if c == '&' {
|
||||
dst, src = unescapeEntity(b, dst, src, false)
|
||||
} else {
|
||||
b[dst] = c
|
||||
dst, src = dst+1, src+1
|
||||
}
|
||||
}
|
||||
return b[0:dst]
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// lower lower-cases the A-Z bytes in b in-place, so that "aBc" becomes "abc".
|
||||
func lower(b []byte) []byte {
|
||||
for i, c := range b {
|
||||
if 'A' <= c && c <= 'Z' {
|
||||
b[i] = c + 'a' - 'A'
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
const escapedChars = `&'<>"`
|
||||
|
||||
func escape(w writer, s string) error {
|
||||
i := strings.IndexAny(s, escapedChars)
|
||||
for i != -1 {
|
||||
if _, err := w.WriteString(s[:i]); err != nil {
|
||||
return err
|
||||
}
|
||||
var esc string
|
||||
switch s[i] {
|
||||
case '&':
|
||||
esc = "&"
|
||||
case '\'':
|
||||
esc = "'"
|
||||
case '<':
|
||||
esc = "<"
|
||||
case '>':
|
||||
esc = ">"
|
||||
case '"':
|
||||
esc = """
|
||||
default:
|
||||
panic("unrecognized escape character")
|
||||
}
|
||||
s = s[i+1:]
|
||||
if _, err := w.WriteString(esc); err != nil {
|
||||
return err
|
||||
}
|
||||
i = strings.IndexAny(s, escapedChars)
|
||||
}
|
||||
_, err := w.WriteString(s)
|
||||
return err
|
||||
}
|
||||
|
||||
// EscapeString escapes special characters like "<" to become "<". It
|
||||
// escapes only five such characters: amp, apos, lt, gt and quot.
|
||||
// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't
|
||||
// always true.
|
||||
func EscapeString(s string) string {
|
||||
if strings.IndexAny(s, escapedChars) == -1 {
|
||||
return s
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
escape(&buf, s)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// UnescapeString unescapes entities like "<" to become "<". It unescapes a
|
||||
// larger range of entities than EscapeString escapes. For example, "á"
|
||||
// unescapes to "á", as does "á" and "&xE1;".
|
||||
// UnescapeString(EscapeString(s)) == s always holds, but the converse isn't
|
||||
// always true.
|
||||
func UnescapeString(s string) string {
|
||||
for _, c := range s {
|
||||
if c == '&' {
|
||||
return string(unescape([]byte(s)))
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
132
vendor/github.com/levigross/exp-html/foreign.go
сгенерированный
поставляемый
Обычный файл
132
vendor/github.com/levigross/exp-html/foreign.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,132 @@
|
||||
// Copyright 2011 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 (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func adjustForeignAttributes(aa []Attribute) {
|
||||
for i, a := range aa {
|
||||
if a.Key == "" || a.Key[0] != 'x' {
|
||||
continue
|
||||
}
|
||||
switch a.Key {
|
||||
case "xlink:actuate", "xlink:arcrole", "xlink:href", "xlink:role", "xlink:show",
|
||||
"xlink:title", "xlink:type", "xml:base", "xml:lang", "xml:space", "xmlns:xlink":
|
||||
j := strings.Index(a.Key, ":")
|
||||
aa[i].Namespace = a.Key[:j]
|
||||
aa[i].Key = a.Key[j+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func htmlIntegrationPoint(n *Node) bool {
|
||||
if n.Type != ElementNode {
|
||||
return false
|
||||
}
|
||||
switch n.Namespace {
|
||||
case "math":
|
||||
// TODO: annotation-xml elements whose start tags have "text/html" or
|
||||
// "application/xhtml+xml" encodings.
|
||||
case "svg":
|
||||
switch n.Data {
|
||||
case "desc", "foreignObject", "title":
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Section 12.2.5.5.
|
||||
var breakout = map[string]bool{
|
||||
"b": true,
|
||||
"big": true,
|
||||
"blockquote": true,
|
||||
"body": true,
|
||||
"br": true,
|
||||
"center": true,
|
||||
"code": true,
|
||||
"dd": true,
|
||||
"div": true,
|
||||
"dl": true,
|
||||
"dt": true,
|
||||
"em": true,
|
||||
"embed": true,
|
||||
"font": true,
|
||||
"h1": true,
|
||||
"h2": true,
|
||||
"h3": true,
|
||||
"h4": true,
|
||||
"h5": true,
|
||||
"h6": true,
|
||||
"head": true,
|
||||
"hr": true,
|
||||
"i": true,
|
||||
"img": true,
|
||||
"li": true,
|
||||
"listing": true,
|
||||
"menu": true,
|
||||
"meta": true,
|
||||
"nobr": true,
|
||||
"ol": true,
|
||||
"p": true,
|
||||
"pre": true,
|
||||
"ruby": true,
|
||||
"s": true,
|
||||
"small": true,
|
||||
"span": true,
|
||||
"strong": true,
|
||||
"strike": true,
|
||||
"sub": true,
|
||||
"sup": true,
|
||||
"table": true,
|
||||
"tt": true,
|
||||
"u": true,
|
||||
"ul": true,
|
||||
"var": true,
|
||||
}
|
||||
|
||||
// Section 12.2.5.5.
|
||||
var svgTagNameAdjustments = map[string]string{
|
||||
"altglyph": "altGlyph",
|
||||
"altglyphdef": "altGlyphDef",
|
||||
"altglyphitem": "altGlyphItem",
|
||||
"animatecolor": "animateColor",
|
||||
"animatemotion": "animateMotion",
|
||||
"animatetransform": "animateTransform",
|
||||
"clippath": "clipPath",
|
||||
"feblend": "feBlend",
|
||||
"fecolormatrix": "feColorMatrix",
|
||||
"fecomponenttransfer": "feComponentTransfer",
|
||||
"fecomposite": "feComposite",
|
||||
"feconvolvematrix": "feConvolveMatrix",
|
||||
"fediffuselighting": "feDiffuseLighting",
|
||||
"fedisplacementmap": "feDisplacementMap",
|
||||
"fedistantlight": "feDistantLight",
|
||||
"feflood": "feFlood",
|
||||
"fefunca": "feFuncA",
|
||||
"fefuncb": "feFuncB",
|
||||
"fefuncg": "feFuncG",
|
||||
"fefuncr": "feFuncR",
|
||||
"fegaussianblur": "feGaussianBlur",
|
||||
"feimage": "feImage",
|
||||
"femerge": "feMerge",
|
||||
"femergenode": "feMergeNode",
|
||||
"femorphology": "feMorphology",
|
||||
"feoffset": "feOffset",
|
||||
"fepointlight": "fePointLight",
|
||||
"fespecularlighting": "feSpecularLighting",
|
||||
"fespotlight": "feSpotLight",
|
||||
"fetile": "feTile",
|
||||
"feturbulence": "feTurbulence",
|
||||
"foreignobject": "foreignObject",
|
||||
"glyphref": "glyphRef",
|
||||
"lineargradient": "linearGradient",
|
||||
"radialgradient": "radialGradient",
|
||||
"textpath": "textPath",
|
||||
}
|
||||
|
||||
// TODO: add look-up tables for MathML and SVG attribute adjustments.
|
||||
154
vendor/github.com/levigross/exp-html/node.go
сгенерированный
поставляемый
Обычный файл
154
vendor/github.com/levigross/exp-html/node.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,154 @@
|
||||
// Copyright 2011 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
|
||||
|
||||
// A NodeType is the type of a Node.
|
||||
type NodeType int
|
||||
|
||||
const (
|
||||
ErrorNode NodeType = iota
|
||||
TextNode
|
||||
DocumentNode
|
||||
ElementNode
|
||||
CommentNode
|
||||
DoctypeNode
|
||||
scopeMarkerNode
|
||||
)
|
||||
|
||||
// Section 12.2.3.3 says "scope markers are inserted when entering applet
|
||||
// elements, buttons, object elements, marquees, table cells, and table
|
||||
// captions, and are used to prevent formatting from 'leaking'".
|
||||
var scopeMarker = Node{Type: scopeMarkerNode}
|
||||
|
||||
// A Node consists of a NodeType and some Data (tag name for element nodes,
|
||||
// content for text) and are part of a tree of Nodes. Element nodes may also
|
||||
// have a Namespace and contain a slice of Attributes. Data is unescaped, so
|
||||
// that it looks like "a<b" rather than "a<b".
|
||||
//
|
||||
// An empty Namespace implies a "http://www.w3.org/1999/xhtml" namespace.
|
||||
// Similarly, "math" is short for "http://www.w3.org/1998/Math/MathML", and
|
||||
// "svg" is short for "http://www.w3.org/2000/svg".
|
||||
type Node struct {
|
||||
Parent *Node
|
||||
Child []*Node
|
||||
Type NodeType
|
||||
Data string
|
||||
Namespace string
|
||||
Attr []Attribute
|
||||
}
|
||||
|
||||
// Add adds a node as a child of n.
|
||||
// It will panic if the child's parent is not nil.
|
||||
func (n *Node) Add(child *Node) {
|
||||
if child.Parent != nil {
|
||||
panic("html: Node.Add called for a child Node that already has a parent")
|
||||
}
|
||||
child.Parent = n
|
||||
n.Child = append(n.Child, child)
|
||||
}
|
||||
|
||||
// Remove removes a node as a child of n.
|
||||
// It will panic if the child's parent is not n.
|
||||
func (n *Node) Remove(child *Node) {
|
||||
if child.Parent == n {
|
||||
child.Parent = nil
|
||||
for i, m := range n.Child {
|
||||
if m == child {
|
||||
copy(n.Child[i:], n.Child[i+1:])
|
||||
j := len(n.Child) - 1
|
||||
n.Child[j] = nil
|
||||
n.Child = n.Child[:j]
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
panic("html: Node.Remove called for a non-child Node")
|
||||
}
|
||||
|
||||
// reparentChildren reparents all of src's child nodes to dst.
|
||||
func reparentChildren(dst, src *Node) {
|
||||
for _, n := range src.Child {
|
||||
if n.Parent != src {
|
||||
panic("html: nodes have an inconsistent parent/child relationship")
|
||||
}
|
||||
n.Parent = dst
|
||||
}
|
||||
dst.Child = append(dst.Child, src.Child...)
|
||||
src.Child = nil
|
||||
}
|
||||
|
||||
// clone returns a new node with the same type, data and attributes.
|
||||
// The clone has no parent and no children.
|
||||
func (n *Node) clone() *Node {
|
||||
m := &Node{
|
||||
Type: n.Type,
|
||||
Data: n.Data,
|
||||
Attr: make([]Attribute, len(n.Attr)),
|
||||
}
|
||||
copy(m.Attr, n.Attr)
|
||||
return m
|
||||
}
|
||||
|
||||
// nodeStack is a stack of nodes.
|
||||
type nodeStack []*Node
|
||||
|
||||
// pop pops the stack. It will panic if s is empty.
|
||||
func (s *nodeStack) pop() *Node {
|
||||
i := len(*s)
|
||||
n := (*s)[i-1]
|
||||
*s = (*s)[:i-1]
|
||||
return n
|
||||
}
|
||||
|
||||
// top returns the most recently pushed node, or nil if s is empty.
|
||||
func (s *nodeStack) top() *Node {
|
||||
if i := len(*s); i > 0 {
|
||||
return (*s)[i-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// index returns the index of the top-most occurrence of n in the stack, or -1
|
||||
// if n is not present.
|
||||
func (s *nodeStack) index(n *Node) int {
|
||||
for i := len(*s) - 1; i >= 0; i-- {
|
||||
if (*s)[i] == n {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// insert inserts a node at the given index.
|
||||
func (s *nodeStack) insert(i int, n *Node) {
|
||||
(*s) = append(*s, nil)
|
||||
copy((*s)[i+1:], (*s)[i:])
|
||||
(*s)[i] = n
|
||||
}
|
||||
|
||||
// remove removes a node from the stack. It is a no-op if n is not present.
|
||||
func (s *nodeStack) remove(n *Node) {
|
||||
i := s.index(n)
|
||||
if i == -1 {
|
||||
return
|
||||
}
|
||||
copy((*s)[i:], (*s)[i+1:])
|
||||
j := len(*s) - 1
|
||||
(*s)[j] = nil
|
||||
*s = (*s)[:j]
|
||||
}
|
||||
|
||||
// TODO(nigeltao): forTag no longer used. Should it be deleted?
|
||||
|
||||
// forTag returns the top-most element node with the given tag.
|
||||
func (s *nodeStack) forTag(tag string) *Node {
|
||||
for i := len(*s) - 1; i >= 0; i-- {
|
||||
n := (*s)[i]
|
||||
if n.Type == ElementNode && n.Data == tag {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
1869
vendor/github.com/levigross/exp-html/parse.go
сгенерированный
поставляемый
Обычный файл
1869
vendor/github.com/levigross/exp-html/parse.go
сгенерированный
поставляемый
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
277
vendor/github.com/levigross/exp-html/render.go
сгенерированный
поставляемый
Обычный файл
277
vendor/github.com/levigross/exp-html/render.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,277 @@
|
||||
// Copyright 2011 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 (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type writer interface {
|
||||
io.Writer
|
||||
WriteByte(byte) error
|
||||
WriteString(string) (int, error)
|
||||
}
|
||||
|
||||
// Render renders the parse tree n to the given writer.
|
||||
//
|
||||
// Rendering is done on a 'best effort' basis: calling Parse on the output of
|
||||
// Render will always result in something similar to the original tree, but it
|
||||
// is not necessarily an exact clone unless the original tree was 'well-formed'.
|
||||
// 'Well-formed' is not easily specified; the HTML5 specification is
|
||||
// complicated.
|
||||
//
|
||||
// Calling Parse on arbitrary input typically results in a 'well-formed' parse
|
||||
// tree. However, it is possible for Parse to yield a 'badly-formed' parse tree.
|
||||
// For example, in a 'well-formed' parse tree, no <a> element is a child of
|
||||
// another <a> element: parsing "<a><a>" results in two sibling elements.
|
||||
// Similarly, in a 'well-formed' parse tree, no <a> element is a child of a
|
||||
// <table> element: parsing "<p><table><a>" results in a <p> with two sibling
|
||||
// children; the <a> is reparented to the <table>'s parent. However, calling
|
||||
// Parse on "<a><table><a>" does not return an error, but the result has an <a>
|
||||
// element with an <a> child, and is therefore not 'well-formed'.
|
||||
//
|
||||
// Programmatically constructed trees are typically also 'well-formed', but it
|
||||
// is possible to construct a tree that looks innocuous but, when rendered and
|
||||
// re-parsed, results in a different tree. A simple example is that a solitary
|
||||
// text node would become a tree containing <html>, <head> and <body> elements.
|
||||
// Another example is that the programmatic equivalent of "a<head>b</head>c"
|
||||
// becomes "<html><head><head/><body>abc</body></html>".
|
||||
func Render(w io.Writer, n *Node) error {
|
||||
if x, ok := w.(writer); ok {
|
||||
return render(x, n)
|
||||
}
|
||||
buf := bufio.NewWriter(w)
|
||||
if err := render(buf, n); err != nil {
|
||||
return err
|
||||
}
|
||||
return buf.Flush()
|
||||
}
|
||||
|
||||
// plaintextAbort is returned from render1 when a <plaintext> element
|
||||
// has been rendered. No more end tags should be rendered after that.
|
||||
var plaintextAbort = errors.New("html: internal error (plaintext abort)")
|
||||
|
||||
func render(w writer, n *Node) error {
|
||||
err := render1(w, n)
|
||||
if err == plaintextAbort {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func render1(w writer, n *Node) error {
|
||||
// Render non-element nodes; these are the easy cases.
|
||||
switch n.Type {
|
||||
case ErrorNode:
|
||||
return errors.New("html: cannot render an ErrorNode node")
|
||||
case TextNode:
|
||||
return escape(w, n.Data)
|
||||
case DocumentNode:
|
||||
for _, c := range n.Child {
|
||||
if err := render1(w, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case ElementNode:
|
||||
// No-op.
|
||||
case CommentNode:
|
||||
if _, err := w.WriteString("<!--"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(n.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString("-->"); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case DoctypeNode:
|
||||
if _, err := w.WriteString("<!DOCTYPE "); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(n.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
if n.Attr != nil {
|
||||
var p, s string
|
||||
for _, a := range n.Attr {
|
||||
switch a.Key {
|
||||
case "public":
|
||||
p = a.Val
|
||||
case "system":
|
||||
s = a.Val
|
||||
}
|
||||
}
|
||||
if p != "" {
|
||||
if _, err := w.WriteString(" PUBLIC "); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeQuoted(w, p); err != nil {
|
||||
return err
|
||||
}
|
||||
if s != "" {
|
||||
if err := w.WriteByte(' '); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeQuoted(w, s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if s != "" {
|
||||
if _, err := w.WriteString(" SYSTEM "); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeQuoted(w, s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return w.WriteByte('>')
|
||||
default:
|
||||
return errors.New("html: unknown node type")
|
||||
}
|
||||
|
||||
// Render the <xxx> opening tag.
|
||||
if err := w.WriteByte('<'); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(n.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, a := range n.Attr {
|
||||
if err := w.WriteByte(' '); err != nil {
|
||||
return err
|
||||
}
|
||||
if a.Namespace != "" {
|
||||
if _, err := w.WriteString(a.Namespace); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteByte(':'); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := w.WriteString(a.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(`="`); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := escape(w, a.Val); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteByte('"'); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if voidElements[n.Data] {
|
||||
if len(n.Child) != 0 {
|
||||
return fmt.Errorf("html: void element <%s> has child nodes", n.Data)
|
||||
}
|
||||
_, err := w.WriteString("/>")
|
||||
return err
|
||||
}
|
||||
if err := w.WriteByte('>'); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Add initial newline where there is danger of a newline beging ignored.
|
||||
if len(n.Child) > 0 && n.Child[0].Type == TextNode && strings.HasPrefix(n.Child[0].Data, "\n") {
|
||||
switch n.Data {
|
||||
case "pre", "listing", "textarea":
|
||||
if err := w.WriteByte('\n'); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render any child nodes.
|
||||
switch n.Data {
|
||||
case "iframe", "noembed", "noframes", "noscript", "plaintext", "script", "style", "xmp":
|
||||
for _, c := range n.Child {
|
||||
if c.Type != TextNode {
|
||||
return fmt.Errorf("html: raw text element <%s> has non-text child node", n.Data)
|
||||
}
|
||||
if _, err := w.WriteString(c.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if n.Data == "plaintext" {
|
||||
// Don't render anything else. <plaintext> must be the
|
||||
// last element in the file, with no closing tag.
|
||||
return plaintextAbort
|
||||
}
|
||||
case "textarea", "title":
|
||||
for _, c := range n.Child {
|
||||
if c.Type != TextNode {
|
||||
return fmt.Errorf("html: RCDATA element <%s> has non-text child node", n.Data)
|
||||
}
|
||||
if err := render1(w, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
default:
|
||||
for _, c := range n.Child {
|
||||
if err := render1(w, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render the </xxx> closing tag.
|
||||
if _, err := w.WriteString("</"); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(n.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.WriteByte('>')
|
||||
}
|
||||
|
||||
// writeQuoted writes s to w surrounded by quotes. Normally it will use double
|
||||
// quotes, but if s contains a double quote, it will use single quotes.
|
||||
// It is used for writing the identifiers in a doctype declaration.
|
||||
// In valid HTML, they can't contain both types of quotes.
|
||||
func writeQuoted(w writer, s string) error {
|
||||
var q byte = '"'
|
||||
if strings.Contains(s, `"`) {
|
||||
q = '\''
|
||||
}
|
||||
if err := w.WriteByte(q); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.WriteString(s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.WriteByte(q); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Section 12.1.2, "Elements", gives this list of void elements. Void elements
|
||||
// are those that can't have any contents.
|
||||
var voidElements = map[string]bool{
|
||||
"area": true,
|
||||
"base": true,
|
||||
"br": true,
|
||||
"col": true,
|
||||
"command": true,
|
||||
"embed": true,
|
||||
"hr": true,
|
||||
"img": true,
|
||||
"input": true,
|
||||
"keygen": true,
|
||||
"link": true,
|
||||
"meta": true,
|
||||
"param": true,
|
||||
"source": true,
|
||||
"track": true,
|
||||
"wbr": true,
|
||||
}
|
||||
779
vendor/github.com/levigross/exp-html/token.go
сгенерированный
поставляемый
Обычный файл
779
vendor/github.com/levigross/exp-html/token.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,779 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A TokenType is the type of a Token.
|
||||
type TokenType int
|
||||
|
||||
const (
|
||||
// ErrorToken means that an error occurred during tokenization.
|
||||
ErrorToken TokenType = iota
|
||||
// TextToken means a text node.
|
||||
TextToken
|
||||
// A StartTagToken looks like <a>.
|
||||
StartTagToken
|
||||
// An EndTagToken looks like </a>.
|
||||
EndTagToken
|
||||
// A SelfClosingTagToken tag looks like <br/>.
|
||||
SelfClosingTagToken
|
||||
// A CommentToken looks like <!--x-->.
|
||||
CommentToken
|
||||
// A DoctypeToken looks like <!DOCTYPE x>
|
||||
DoctypeToken
|
||||
)
|
||||
|
||||
// String returns a string representation of the TokenType.
|
||||
func (t TokenType) String() string {
|
||||
switch t {
|
||||
case ErrorToken:
|
||||
return "Error"
|
||||
case TextToken:
|
||||
return "Text"
|
||||
case StartTagToken:
|
||||
return "StartTag"
|
||||
case EndTagToken:
|
||||
return "EndTag"
|
||||
case SelfClosingTagToken:
|
||||
return "SelfClosingTag"
|
||||
case CommentToken:
|
||||
return "Comment"
|
||||
case DoctypeToken:
|
||||
return "Doctype"
|
||||
}
|
||||
return "Invalid(" + strconv.Itoa(int(t)) + ")"
|
||||
}
|
||||
|
||||
// An Attribute is an attribute namespace-key-value triple. Namespace is
|
||||
// non-empty for foreign attributes like xlink, Key is alphabetic (and hence
|
||||
// does not contain escapable characters like '&', '<' or '>'), and Val is
|
||||
// unescaped (it looks like "a<b" rather than "a<b").
|
||||
//
|
||||
// Namespace is only used by the parser, not the tokenizer.
|
||||
type Attribute struct {
|
||||
Namespace, Key, Val string
|
||||
}
|
||||
|
||||
// A Token consists of a TokenType and some Data (tag name for start and end
|
||||
// tags, content for text, comments and doctypes). A tag Token may also contain
|
||||
// a slice of Attributes. Data is unescaped for all Tokens (it looks like "a<b"
|
||||
// rather than "a<b").
|
||||
type Token struct {
|
||||
Type TokenType
|
||||
Data string
|
||||
Attr []Attribute
|
||||
}
|
||||
|
||||
// tagString returns a string representation of a tag Token's Data and Attr.
|
||||
func (t Token) tagString() string {
|
||||
if len(t.Attr) == 0 {
|
||||
return t.Data
|
||||
}
|
||||
buf := bytes.NewBufferString(t.Data)
|
||||
for _, a := range t.Attr {
|
||||
buf.WriteByte(' ')
|
||||
buf.WriteString(a.Key)
|
||||
buf.WriteString(`="`)
|
||||
escape(buf, a.Val)
|
||||
buf.WriteByte('"')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// String returns a string representation of the Token.
|
||||
func (t Token) String() string {
|
||||
switch t.Type {
|
||||
case ErrorToken:
|
||||
return ""
|
||||
case TextToken:
|
||||
return EscapeString(t.Data)
|
||||
case StartTagToken:
|
||||
return "<" + t.tagString() + ">"
|
||||
case EndTagToken:
|
||||
return "</" + t.tagString() + ">"
|
||||
case SelfClosingTagToken:
|
||||
return "<" + t.tagString() + "/>"
|
||||
case CommentToken:
|
||||
return "<!--" + t.Data + "-->"
|
||||
case DoctypeToken:
|
||||
return "<!DOCTYPE " + t.Data + ">"
|
||||
}
|
||||
return "Invalid(" + strconv.Itoa(int(t.Type)) + ")"
|
||||
}
|
||||
|
||||
// span is a range of bytes in a Tokenizer's buffer. The start is inclusive,
|
||||
// the end is exclusive.
|
||||
type span struct {
|
||||
start, end int
|
||||
}
|
||||
|
||||
// A Tokenizer returns a stream of HTML Tokens.
|
||||
type Tokenizer struct {
|
||||
// r is the source of the HTML text.
|
||||
r io.Reader
|
||||
// tt is the TokenType of the current token.
|
||||
tt TokenType
|
||||
// err is the first error encountered during tokenization. It is possible
|
||||
// for tt != Error && err != nil to hold: this means that Next returned a
|
||||
// valid token but the subsequent Next call will return an error token.
|
||||
// For example, if the HTML text input was just "plain", then the first
|
||||
// Next call would set z.err to io.EOF but return a TextToken, and all
|
||||
// subsequent Next calls would return an ErrorToken.
|
||||
// err is never reset. Once it becomes non-nil, it stays non-nil.
|
||||
err error
|
||||
// buf[raw.start:raw.end] holds the raw bytes of the current token.
|
||||
// buf[raw.end:] is buffered input that will yield future tokens.
|
||||
raw span
|
||||
buf []byte
|
||||
// buf[data.start:data.end] holds the raw bytes of the current token's data:
|
||||
// a text token's text, a tag token's tag name, etc.
|
||||
data span
|
||||
// pendingAttr is the attribute key and value currently being tokenized.
|
||||
// When complete, pendingAttr is pushed onto attr. nAttrReturned is
|
||||
// incremented on each call to TagAttr.
|
||||
pendingAttr [2]span
|
||||
attr [][2]span
|
||||
nAttrReturned int
|
||||
// rawTag is the "script" in "</script>" that closes the next token. If
|
||||
// non-empty, the subsequent call to Next will return a raw or RCDATA text
|
||||
// token: one that treats "<p>" as text instead of an element.
|
||||
// rawTag's contents are lower-cased.
|
||||
rawTag string
|
||||
// textIsRaw is whether the current text token's data is not escaped.
|
||||
textIsRaw bool
|
||||
}
|
||||
|
||||
// Err returns the error associated with the most recent ErrorToken token.
|
||||
// This is typically io.EOF, meaning the end of tokenization.
|
||||
func (z *Tokenizer) Err() error {
|
||||
if z.tt != ErrorToken {
|
||||
return nil
|
||||
}
|
||||
return z.err
|
||||
}
|
||||
|
||||
// readByte returns the next byte from the input stream, doing a buffered read
|
||||
// from z.r into z.buf if necessary. z.buf[z.raw.start:z.raw.end] remains a contiguous byte
|
||||
// slice that holds all the bytes read so far for the current token.
|
||||
// It sets z.err if the underlying reader returns an error.
|
||||
// Pre-condition: z.err == nil.
|
||||
func (z *Tokenizer) readByte() byte {
|
||||
if z.raw.end >= len(z.buf) {
|
||||
// Our buffer is exhausted and we have to read from z.r.
|
||||
// We copy z.buf[z.raw.start:z.raw.end] to the beginning of z.buf. If the length
|
||||
// z.raw.end - z.raw.start is more than half the capacity of z.buf, then we
|
||||
// allocate a new buffer before the copy.
|
||||
c := cap(z.buf)
|
||||
d := z.raw.end - z.raw.start
|
||||
var buf1 []byte
|
||||
if 2*d > c {
|
||||
buf1 = make([]byte, d, 2*c)
|
||||
} else {
|
||||
buf1 = z.buf[:d]
|
||||
}
|
||||
copy(buf1, z.buf[z.raw.start:z.raw.end])
|
||||
if x := z.raw.start; x != 0 {
|
||||
// Adjust the data/attr spans to refer to the same contents after the copy.
|
||||
z.data.start -= x
|
||||
z.data.end -= x
|
||||
z.pendingAttr[0].start -= x
|
||||
z.pendingAttr[0].end -= x
|
||||
z.pendingAttr[1].start -= x
|
||||
z.pendingAttr[1].end -= x
|
||||
for i := range z.attr {
|
||||
z.attr[i][0].start -= x
|
||||
z.attr[i][0].end -= x
|
||||
z.attr[i][1].start -= x
|
||||
z.attr[i][1].end -= x
|
||||
}
|
||||
}
|
||||
z.raw.start, z.raw.end, z.buf = 0, d, buf1[:d]
|
||||
// Now that we have copied the live bytes to the start of the buffer,
|
||||
// we read from z.r into the remainder.
|
||||
n, err := z.r.Read(buf1[d:cap(buf1)])
|
||||
if err != nil {
|
||||
z.err = err
|
||||
return 0
|
||||
}
|
||||
z.buf = buf1[:d+n]
|
||||
}
|
||||
x := z.buf[z.raw.end]
|
||||
z.raw.end++
|
||||
return x
|
||||
}
|
||||
|
||||
// skipWhiteSpace skips past any white space.
|
||||
func (z *Tokenizer) skipWhiteSpace() {
|
||||
if z.err != nil {
|
||||
return
|
||||
}
|
||||
for {
|
||||
c := z.readByte()
|
||||
if z.err != nil {
|
||||
return
|
||||
}
|
||||
switch c {
|
||||
case ' ', '\n', '\r', '\t', '\f':
|
||||
// No-op.
|
||||
default:
|
||||
z.raw.end--
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readRawOrRCDATA reads until the next "</foo>", where "foo" is z.rawTag and
|
||||
// is typically something like "script" or "textarea".
|
||||
func (z *Tokenizer) readRawOrRCDATA() {
|
||||
loop:
|
||||
for {
|
||||
c := z.readByte()
|
||||
if z.err != nil {
|
||||
break loop
|
||||
}
|
||||
if c != '<' {
|
||||
continue loop
|
||||
}
|
||||
c = z.readByte()
|
||||
if z.err != nil {
|
||||
break loop
|
||||
}
|
||||
if c != '/' {
|
||||
continue loop
|
||||
}
|
||||
for i := 0; i < len(z.rawTag); i++ {
|
||||
c = z.readByte()
|
||||
if z.err != nil {
|
||||
break loop
|
||||
}
|
||||
if c != z.rawTag[i] && c != z.rawTag[i]-('a'-'A') {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
c = z.readByte()
|
||||
if z.err != nil {
|
||||
break loop
|
||||
}
|
||||
switch c {
|
||||
case ' ', '\n', '\r', '\t', '\f', '/', '>':
|
||||
// The 3 is 2 for the leading "</" plus 1 for the trailing character c.
|
||||
z.raw.end -= 3 + len(z.rawTag)
|
||||
break loop
|
||||
case '<':
|
||||
// Step back one, to catch "</foo</foo>".
|
||||
z.raw.end--
|
||||
}
|
||||
}
|
||||
z.data.end = z.raw.end
|
||||
// A textarea's or title's RCDATA can contain escaped entities.
|
||||
z.textIsRaw = z.rawTag != "textarea" && z.rawTag != "title"
|
||||
z.rawTag = ""
|
||||
}
|
||||
|
||||
// readComment reads the next comment token starting with "<!--". The opening
|
||||
// "<!--" has already been consumed.
|
||||
func (z *Tokenizer) readComment() {
|
||||
z.data.start = z.raw.end
|
||||
defer func() {
|
||||
if z.data.end < z.data.start {
|
||||
// It's a comment with no data, like <!-->.
|
||||
z.data.end = z.data.start
|
||||
}
|
||||
}()
|
||||
for dashCount := 2; ; {
|
||||
c := z.readByte()
|
||||
if z.err != nil {
|
||||
// Ignore up to two dashes at EOF.
|
||||
if dashCount > 2 {
|
||||
dashCount = 2
|
||||
}
|
||||
z.data.end = z.raw.end - dashCount
|
||||
return
|
||||
}
|
||||
switch c {
|
||||
case '-':
|
||||
dashCount++
|
||||
continue
|
||||
case '>':
|
||||
if dashCount >= 2 {
|
||||
z.data.end = z.raw.end - len("-->")
|
||||
return
|
||||
}
|
||||
case '!':
|
||||
if dashCount >= 2 {
|
||||
c = z.readByte()
|
||||
if z.err != nil {
|
||||
z.data.end = z.raw.end
|
||||
return
|
||||
}
|
||||
if c == '>' {
|
||||
z.data.end = z.raw.end - len("--!>")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
dashCount = 0
|
||||
}
|
||||
}
|
||||
|
||||
// readUntilCloseAngle reads until the next ">".
|
||||
func (z *Tokenizer) readUntilCloseAngle() {
|
||||
z.data.start = z.raw.end
|
||||
for {
|
||||
c := z.readByte()
|
||||
if z.err != nil {
|
||||
z.data.end = z.raw.end
|
||||
return
|
||||
}
|
||||
if c == '>' {
|
||||
z.data.end = z.raw.end - len(">")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readMarkupDeclaration reads the next token starting with "<!". It might be
|
||||
// a "<!--comment-->", a "<!DOCTYPE foo>", or "<!a bogus comment". The opening
|
||||
// "<!" has already been consumed.
|
||||
func (z *Tokenizer) readMarkupDeclaration() TokenType {
|
||||
z.data.start = z.raw.end
|
||||
var c [2]byte
|
||||
for i := 0; i < 2; i++ {
|
||||
c[i] = z.readByte()
|
||||
if z.err != nil {
|
||||
z.data.end = z.raw.end
|
||||
return CommentToken
|
||||
}
|
||||
}
|
||||
if c[0] == '-' && c[1] == '-' {
|
||||
z.readComment()
|
||||
return CommentToken
|
||||
}
|
||||
z.raw.end -= 2
|
||||
const s = "DOCTYPE"
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := z.readByte()
|
||||
if z.err != nil {
|
||||
z.data.end = z.raw.end
|
||||
return CommentToken
|
||||
}
|
||||
if c != s[i] && c != s[i]+('a'-'A') {
|
||||
// Back up to read the fragment of "DOCTYPE" again.
|
||||
z.raw.end = z.data.start
|
||||
z.readUntilCloseAngle()
|
||||
return CommentToken
|
||||
}
|
||||
}
|
||||
if z.skipWhiteSpace(); z.err != nil {
|
||||
z.data.start = z.raw.end
|
||||
z.data.end = z.raw.end
|
||||
return DoctypeToken
|
||||
}
|
||||
z.readUntilCloseAngle()
|
||||
return DoctypeToken
|
||||
}
|
||||
|
||||
// startTagIn returns whether the start tag in z.buf[z.data.start:z.data.end]
|
||||
// case-insensitively matches any element of ss.
|
||||
func (z *Tokenizer) startTagIn(ss ...string) bool {
|
||||
loop:
|
||||
for _, s := range ss {
|
||||
if z.data.end-z.data.start != len(s) {
|
||||
continue loop
|
||||
}
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := z.buf[z.data.start+i]
|
||||
if 'A' <= c && c <= 'Z' {
|
||||
c += 'a' - 'A'
|
||||
}
|
||||
if c != s[i] {
|
||||
continue loop
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// readStartTag reads the next start tag token. The opening "<a" has already
|
||||
// been consumed, where 'a' means anything in [A-Za-z].
|
||||
func (z *Tokenizer) readStartTag() TokenType {
|
||||
z.attr = z.attr[:0]
|
||||
z.nAttrReturned = 0
|
||||
// Read the tag name and attribute key/value pairs.
|
||||
z.readTagName()
|
||||
if z.skipWhiteSpace(); z.err != nil {
|
||||
return ErrorToken
|
||||
}
|
||||
for {
|
||||
c := z.readByte()
|
||||
if z.err != nil || c == '>' {
|
||||
break
|
||||
}
|
||||
z.raw.end--
|
||||
z.readTagAttrKey()
|
||||
z.readTagAttrVal()
|
||||
// Save pendingAttr if it has a non-empty key.
|
||||
if z.pendingAttr[0].start != z.pendingAttr[0].end {
|
||||
z.attr = append(z.attr, z.pendingAttr)
|
||||
}
|
||||
if z.skipWhiteSpace(); z.err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Several tags flag the tokenizer's next token as raw.
|
||||
c, raw := z.buf[z.data.start], false
|
||||
if 'A' <= c && c <= 'Z' {
|
||||
c += 'a' - 'A'
|
||||
}
|
||||
switch c {
|
||||
case 'i':
|
||||
raw = z.startTagIn("iframe")
|
||||
case 'n':
|
||||
raw = z.startTagIn("noembed", "noframes", "noscript")
|
||||
case 'p':
|
||||
raw = z.startTagIn("plaintext")
|
||||
case 's':
|
||||
raw = z.startTagIn("script", "style")
|
||||
case 't':
|
||||
raw = z.startTagIn("textarea", "title")
|
||||
case 'x':
|
||||
raw = z.startTagIn("xmp")
|
||||
}
|
||||
if raw {
|
||||
z.rawTag = strings.ToLower(string(z.buf[z.data.start:z.data.end]))
|
||||
}
|
||||
// Look for a self-closing token like "<br/>".
|
||||
if z.err == nil && z.buf[z.raw.end-2] == '/' {
|
||||
return SelfClosingTagToken
|
||||
}
|
||||
return StartTagToken
|
||||
}
|
||||
|
||||
// readEndTag reads the next end tag token. The opening "</a" has already
|
||||
// been consumed, where 'a' means anything in [A-Za-z].
|
||||
func (z *Tokenizer) readEndTag() {
|
||||
z.attr = z.attr[:0]
|
||||
z.nAttrReturned = 0
|
||||
z.readTagName()
|
||||
for {
|
||||
c := z.readByte()
|
||||
if z.err != nil || c == '>' {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readTagName sets z.data to the "div" in "<div k=v>". The reader (z.raw.end)
|
||||
// is positioned such that the first byte of the tag name (the "d" in "<div")
|
||||
// has already been consumed.
|
||||
func (z *Tokenizer) readTagName() {
|
||||
z.data.start = z.raw.end - 1
|
||||
for {
|
||||
c := z.readByte()
|
||||
if z.err != nil {
|
||||
z.data.end = z.raw.end
|
||||
return
|
||||
}
|
||||
switch c {
|
||||
case ' ', '\n', '\r', '\t', '\f':
|
||||
z.data.end = z.raw.end - 1
|
||||
return
|
||||
case '/', '>':
|
||||
z.raw.end--
|
||||
z.data.end = z.raw.end
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readTagAttrKey sets z.pendingAttr[0] to the "k" in "<div k=v>".
|
||||
// Precondition: z.err == nil.
|
||||
func (z *Tokenizer) readTagAttrKey() {
|
||||
z.pendingAttr[0].start = z.raw.end
|
||||
for {
|
||||
c := z.readByte()
|
||||
if z.err != nil {
|
||||
z.pendingAttr[0].end = z.raw.end
|
||||
return
|
||||
}
|
||||
switch c {
|
||||
case ' ', '\n', '\r', '\t', '\f', '/':
|
||||
z.pendingAttr[0].end = z.raw.end - 1
|
||||
return
|
||||
case '=', '>':
|
||||
z.raw.end--
|
||||
z.pendingAttr[0].end = z.raw.end
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readTagAttrVal sets z.pendingAttr[1] to the "v" in "<div k=v>".
|
||||
func (z *Tokenizer) readTagAttrVal() {
|
||||
z.pendingAttr[1].start = z.raw.end
|
||||
z.pendingAttr[1].end = z.raw.end
|
||||
if z.skipWhiteSpace(); z.err != nil {
|
||||
return
|
||||
}
|
||||
c := z.readByte()
|
||||
if z.err != nil {
|
||||
return
|
||||
}
|
||||
if c != '=' {
|
||||
z.raw.end--
|
||||
return
|
||||
}
|
||||
if z.skipWhiteSpace(); z.err != nil {
|
||||
return
|
||||
}
|
||||
quote := z.readByte()
|
||||
if z.err != nil {
|
||||
return
|
||||
}
|
||||
switch quote {
|
||||
case '>':
|
||||
z.raw.end--
|
||||
return
|
||||
|
||||
case '\'', '"':
|
||||
z.pendingAttr[1].start = z.raw.end
|
||||
for {
|
||||
c := z.readByte()
|
||||
if z.err != nil {
|
||||
z.pendingAttr[1].end = z.raw.end
|
||||
return
|
||||
}
|
||||
if c == quote {
|
||||
z.pendingAttr[1].end = z.raw.end - 1
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
z.pendingAttr[1].start = z.raw.end - 1
|
||||
for {
|
||||
c := z.readByte()
|
||||
if z.err != nil {
|
||||
z.pendingAttr[1].end = z.raw.end
|
||||
return
|
||||
}
|
||||
switch c {
|
||||
case ' ', '\n', '\r', '\t', '\f':
|
||||
z.pendingAttr[1].end = z.raw.end - 1
|
||||
return
|
||||
case '>':
|
||||
z.raw.end--
|
||||
z.pendingAttr[1].end = z.raw.end
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next scans the next token and returns its type.
|
||||
func (z *Tokenizer) Next() TokenType {
|
||||
if z.err != nil {
|
||||
z.tt = ErrorToken
|
||||
return z.tt
|
||||
}
|
||||
z.raw.start = z.raw.end
|
||||
z.data.start = z.raw.end
|
||||
z.data.end = z.raw.end
|
||||
if z.rawTag != "" {
|
||||
if z.rawTag == "plaintext" {
|
||||
// Read everything up to EOF.
|
||||
for z.err == nil {
|
||||
z.readByte()
|
||||
}
|
||||
z.textIsRaw = true
|
||||
} else {
|
||||
z.readRawOrRCDATA()
|
||||
}
|
||||
if z.data.end > z.data.start {
|
||||
z.tt = TextToken
|
||||
return z.tt
|
||||
}
|
||||
}
|
||||
z.textIsRaw = false
|
||||
|
||||
loop:
|
||||
for {
|
||||
c := z.readByte()
|
||||
if z.err != nil {
|
||||
break loop
|
||||
}
|
||||
if c != '<' {
|
||||
continue loop
|
||||
}
|
||||
|
||||
// Check if the '<' we have just read is part of a tag, comment
|
||||
// or doctype. If not, it's part of the accumulated text token.
|
||||
c = z.readByte()
|
||||
if z.err != nil {
|
||||
break loop
|
||||
}
|
||||
var tokenType TokenType
|
||||
switch {
|
||||
case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z':
|
||||
tokenType = StartTagToken
|
||||
case c == '/':
|
||||
tokenType = EndTagToken
|
||||
case c == '!' || c == '?':
|
||||
// We use CommentToken to mean any of "<!--actual comments-->",
|
||||
// "<!DOCTYPE declarations>" and "<?xml processing instructions?>".
|
||||
tokenType = CommentToken
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// We have a non-text token, but we might have accumulated some text
|
||||
// before that. If so, we return the text first, and return the non-
|
||||
// text token on the subsequent call to Next.
|
||||
if x := z.raw.end - len("<a"); z.raw.start < x {
|
||||
z.raw.end = x
|
||||
z.data.end = x
|
||||
z.tt = TextToken
|
||||
return z.tt
|
||||
}
|
||||
switch tokenType {
|
||||
case StartTagToken:
|
||||
z.tt = z.readStartTag()
|
||||
return z.tt
|
||||
case EndTagToken:
|
||||
c = z.readByte()
|
||||
if z.err != nil {
|
||||
break loop
|
||||
}
|
||||
if c == '>' {
|
||||
// "</>" does not generate a token at all.
|
||||
// Reset the tokenizer state and start again.
|
||||
z.raw.start = z.raw.end
|
||||
z.data.start = z.raw.end
|
||||
z.data.end = z.raw.end
|
||||
continue loop
|
||||
}
|
||||
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' {
|
||||
z.readEndTag()
|
||||
z.tt = EndTagToken
|
||||
return z.tt
|
||||
}
|
||||
z.raw.end--
|
||||
z.readUntilCloseAngle()
|
||||
z.tt = CommentToken
|
||||
return z.tt
|
||||
case CommentToken:
|
||||
if c == '!' {
|
||||
z.tt = z.readMarkupDeclaration()
|
||||
return z.tt
|
||||
}
|
||||
z.raw.end--
|
||||
z.readUntilCloseAngle()
|
||||
z.tt = CommentToken
|
||||
return z.tt
|
||||
}
|
||||
}
|
||||
if z.raw.start < z.raw.end {
|
||||
z.data.end = z.raw.end
|
||||
z.tt = TextToken
|
||||
return z.tt
|
||||
}
|
||||
z.tt = ErrorToken
|
||||
return z.tt
|
||||
}
|
||||
|
||||
// Raw returns the unmodified text of the current token. Calling Next, Token,
|
||||
// Text, TagName or TagAttr may change the contents of the returned slice.
|
||||
func (z *Tokenizer) Raw() []byte {
|
||||
return z.buf[z.raw.start:z.raw.end]
|
||||
}
|
||||
|
||||
// Text returns the unescaped text of a text, comment or doctype token. The
|
||||
// contents of the returned slice may change on the next call to Next.
|
||||
func (z *Tokenizer) Text() []byte {
|
||||
switch z.tt {
|
||||
case TextToken, CommentToken, DoctypeToken:
|
||||
s := z.buf[z.data.start:z.data.end]
|
||||
z.data.start = z.raw.end
|
||||
z.data.end = z.raw.end
|
||||
if !z.textIsRaw {
|
||||
s = unescape(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TagName returns the lower-cased name of a tag token (the `img` out of
|
||||
// `<IMG SRC="foo">`) and whether the tag has attributes.
|
||||
// The contents of the returned slice may change on the next call to Next.
|
||||
func (z *Tokenizer) TagName() (name []byte, hasAttr bool) {
|
||||
if z.data.start < z.data.end {
|
||||
switch z.tt {
|
||||
case StartTagToken, EndTagToken, SelfClosingTagToken:
|
||||
s := z.buf[z.data.start:z.data.end]
|
||||
z.data.start = z.raw.end
|
||||
z.data.end = z.raw.end
|
||||
return lower(s), z.nAttrReturned < len(z.attr)
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// TagAttr returns the lower-cased key and unescaped value of the next unparsed
|
||||
// attribute for the current tag token and whether there are more attributes.
|
||||
// The contents of the returned slices may change on the next call to Next.
|
||||
func (z *Tokenizer) TagAttr() (key, val []byte, moreAttr bool) {
|
||||
if z.nAttrReturned < len(z.attr) {
|
||||
switch z.tt {
|
||||
case StartTagToken, SelfClosingTagToken:
|
||||
x := z.attr[z.nAttrReturned]
|
||||
z.nAttrReturned++
|
||||
key = z.buf[x[0].start:x[0].end]
|
||||
val = z.buf[x[1].start:x[1].end]
|
||||
return lower(key), unescape(val), z.nAttrReturned < len(z.attr)
|
||||
}
|
||||
}
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
// Token returns the next Token. The result's Data and Attr values remain valid
|
||||
// after subsequent Next calls.
|
||||
func (z *Tokenizer) Token() Token {
|
||||
t := Token{Type: z.tt}
|
||||
switch z.tt {
|
||||
case TextToken, CommentToken, DoctypeToken:
|
||||
t.Data = string(z.Text())
|
||||
case StartTagToken, SelfClosingTagToken:
|
||||
var attr []Attribute
|
||||
name, moreAttr := z.TagName()
|
||||
for moreAttr {
|
||||
var key, val []byte
|
||||
key, val, moreAttr = z.TagAttr()
|
||||
attr = append(attr, Attribute{"", string(key), string(val)})
|
||||
}
|
||||
t.Data = string(name)
|
||||
t.Attr = attr
|
||||
case EndTagToken:
|
||||
name, _ := z.TagName()
|
||||
t.Data = string(name)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// NewTokenizer returns a new HTML Tokenizer for the given Reader.
|
||||
// The input is assumed to be UTF-8 encoded.
|
||||
func NewTokenizer(r io.Reader) *Tokenizer {
|
||||
return &Tokenizer{
|
||||
r: r,
|
||||
buf: make([]byte, 0, 4096),
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user