From 8bad84e5d6aab9dd370a2a7897dbc5f14dd953e2 Mon Sep 17 00:00:00 2001
From: s3lph <5564491+s3lph@users.noreply.github.com>
Date: Fri, 25 Sep 2020 02:25:50 +0200
Subject: [PATCH] Mumble bridge: Add HTML/Markdown conversion and image
handling
---
bridge/mumble/handlers.go | 36 +-
bridge/mumble/helpers.go | 119 +++
bridge/mumble/mumble.go | 26 +-
go.mod | 2 +
go.sum | 13 +
.../html-to-markdown/.gitignore | 14 +
.../html-to-markdown/CONTRIBUTING.md | 0
.../JohannesKaufmann/html-to-markdown/LICENSE | 21 +
.../html-to-markdown/README.md | 125 +++
.../html-to-markdown/commonmark.go | 351 ++++++++
.../html-to-markdown/escape/escape.go | 66 ++
.../JohannesKaufmann/html-to-markdown/from.go | 417 +++++++++
.../JohannesKaufmann/html-to-markdown/go.mod | 12 +
.../JohannesKaufmann/html-to-markdown/go.sum | 40 +
.../html-to-markdown/logo.png | Bin 0 -> 38000 bytes
.../html-to-markdown/markdown.go | 208 +++++
.../html-to-markdown/utils.go | 260 ++++++
.../PuerkitoBio/goquery/.gitattributes | 1 +
.../github.com/PuerkitoBio/goquery/.gitignore | 16 +
.../PuerkitoBio/goquery/.travis.yml | 17 +
vendor/github.com/PuerkitoBio/goquery/LICENSE | 12 +
.../github.com/PuerkitoBio/goquery/README.md | 183 ++++
.../github.com/PuerkitoBio/goquery/array.go | 124 +++
vendor/github.com/PuerkitoBio/goquery/doc.go | 123 +++
.../github.com/PuerkitoBio/goquery/expand.go | 70 ++
.../github.com/PuerkitoBio/goquery/filter.go | 163 ++++
vendor/github.com/PuerkitoBio/goquery/go.mod | 8 +
vendor/github.com/PuerkitoBio/goquery/go.sum | 8 +
.../PuerkitoBio/goquery/iteration.go | 39 +
.../PuerkitoBio/goquery/manipulation.go | 574 ++++++++++++
.../PuerkitoBio/goquery/property.go | 275 ++++++
.../github.com/PuerkitoBio/goquery/query.go | 49 ++
.../PuerkitoBio/goquery/traversal.go | 698 +++++++++++++++
vendor/github.com/PuerkitoBio/goquery/type.go | 141 +++
.../PuerkitoBio/goquery/utilities.go | 161 ++++
.../andybalholm/cascadia/.travis.yml | 14 +
.../github.com/andybalholm/cascadia/LICENSE | 24 +
.../github.com/andybalholm/cascadia/README.md | 9 +
vendor/github.com/andybalholm/cascadia/go.mod | 3 +
.../github.com/andybalholm/cascadia/parser.go | 801 +++++++++++++++++
.../andybalholm/cascadia/selector.go | 833 ++++++++++++++++++
.../andybalholm/cascadia/specificity.go | 26 +
.../vincent-petithory/dataurl/LICENSE | 20 +
.../vincent-petithory/dataurl/README.md | 81 ++
.../vincent-petithory/dataurl/dataurl.go | 291 ++++++
.../vincent-petithory/dataurl/doc.go | 28 +
.../vincent-petithory/dataurl/lex.go | 521 +++++++++++
.../vincent-petithory/dataurl/rfc2396.go | 130 +++
.../vincent-petithory/dataurl/wercker.yml | 1 +
vendor/modules.txt | 9 +
50 files changed, 7154 insertions(+), 9 deletions(-)
create mode 100644 bridge/mumble/helpers.go
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/.gitignore
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/CONTRIBUTING.md
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/LICENSE
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/README.md
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/commonmark.go
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/escape/escape.go
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/from.go
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/go.mod
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/go.sum
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/logo.png
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/markdown.go
create mode 100644 vendor/github.com/JohannesKaufmann/html-to-markdown/utils.go
create mode 100644 vendor/github.com/PuerkitoBio/goquery/.gitattributes
create mode 100644 vendor/github.com/PuerkitoBio/goquery/.gitignore
create mode 100644 vendor/github.com/PuerkitoBio/goquery/.travis.yml
create mode 100644 vendor/github.com/PuerkitoBio/goquery/LICENSE
create mode 100644 vendor/github.com/PuerkitoBio/goquery/README.md
create mode 100644 vendor/github.com/PuerkitoBio/goquery/array.go
create mode 100644 vendor/github.com/PuerkitoBio/goquery/doc.go
create mode 100644 vendor/github.com/PuerkitoBio/goquery/expand.go
create mode 100644 vendor/github.com/PuerkitoBio/goquery/filter.go
create mode 100644 vendor/github.com/PuerkitoBio/goquery/go.mod
create mode 100644 vendor/github.com/PuerkitoBio/goquery/go.sum
create mode 100644 vendor/github.com/PuerkitoBio/goquery/iteration.go
create mode 100644 vendor/github.com/PuerkitoBio/goquery/manipulation.go
create mode 100644 vendor/github.com/PuerkitoBio/goquery/property.go
create mode 100644 vendor/github.com/PuerkitoBio/goquery/query.go
create mode 100644 vendor/github.com/PuerkitoBio/goquery/traversal.go
create mode 100644 vendor/github.com/PuerkitoBio/goquery/type.go
create mode 100644 vendor/github.com/PuerkitoBio/goquery/utilities.go
create mode 100644 vendor/github.com/andybalholm/cascadia/.travis.yml
create mode 100644 vendor/github.com/andybalholm/cascadia/LICENSE
create mode 100644 vendor/github.com/andybalholm/cascadia/README.md
create mode 100644 vendor/github.com/andybalholm/cascadia/go.mod
create mode 100644 vendor/github.com/andybalholm/cascadia/parser.go
create mode 100644 vendor/github.com/andybalholm/cascadia/selector.go
create mode 100644 vendor/github.com/andybalholm/cascadia/specificity.go
create mode 100644 vendor/github.com/vincent-petithory/dataurl/LICENSE
create mode 100644 vendor/github.com/vincent-petithory/dataurl/README.md
create mode 100644 vendor/github.com/vincent-petithory/dataurl/dataurl.go
create mode 100644 vendor/github.com/vincent-petithory/dataurl/doc.go
create mode 100644 vendor/github.com/vincent-petithory/dataurl/lex.go
create mode 100644 vendor/github.com/vincent-petithory/dataurl/rfc2396.go
create mode 100644 vendor/github.com/vincent-petithory/dataurl/wercker.yml
diff --git a/bridge/mumble/handlers.go b/bridge/mumble/handlers.go
index 34e17f2c..cb37268a 100644
--- a/bridge/mumble/handlers.go
+++ b/bridge/mumble/handlers.go
@@ -1,10 +1,13 @@
package bmumble
import (
+ "strconv"
+
"layeh.com/gumble/gumble"
"layeh.com/gumble/gumbleutil"
"github.com/42wim/matterbridge/bridge/config"
+ "github.com/42wim/matterbridge/bridge/helper"
)
func (b *Bmumble) handleServerConfig(event *gumble.ServerConfigEvent) {
@@ -12,15 +15,32 @@ func (b *Bmumble) handleServerConfig(event *gumble.ServerConfigEvent) {
}
func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
- rmsg := config.Message{
- Text: event.TextMessage.Message,
- Channel: event.Client.Self.Channel.Name,
- Username: event.TextMessage.Sender.Name,
- UserID: event.TextMessage.Sender.Name + "@" + b.Host,
- Account: b.Account,
+ // Convert Mumble HTML messages to markdown
+ parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message)
+ if err != nil {
+ b.Log.Error(err)
+ }
+ for i, part := range parts {
+ // Construct matterbridge message and pass on to the gateway
+ rmsg := config.Message{
+ Channel: event.Client.Self.Channel.Name,
+ Username: event.TextMessage.Sender.Name,
+ UserID: event.TextMessage.Sender.Name + "@" + b.Host,
+ Account: b.Account,
+ }
+ if part.Image == nil {
+ rmsg.Text = part.Text
+ } else {
+ rmsg.Extra = make(map[string][]interface{})
+ if err = helper.HandleDownloadSize(b.Log, &rmsg, "image"+strconv.Itoa(i)+part.FileExtension, int64(len(part.Image)), b.General); err != nil {
+ b.Log.WithError(err).Warn("not including image in message")
+ continue
+ }
+ helper.HandleDownloadData(b.Log, &rmsg, "image"+strconv.Itoa(i)+part.FileExtension, "", "", &part.Image, b.General)
+ }
+ b.Log.Debugf("<= Remote message is %+v", rmsg)
+ b.Remote <- rmsg
}
- b.Log.Debugf("<= Remote message is %+v", rmsg)
- b.Remote <- rmsg
}
func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
diff --git a/bridge/mumble/helpers.go b/bridge/mumble/helpers.go
new file mode 100644
index 00000000..4a2507fa
--- /dev/null
+++ b/bridge/mumble/helpers.go
@@ -0,0 +1,119 @@
+package bmumble
+
+import (
+ "fmt"
+ "mime"
+ "net/http"
+ "regexp"
+ "strings"
+
+ "github.com/42wim/matterbridge/bridge/config"
+ md "github.com/JohannesKaufmann/html-to-markdown"
+ "github.com/vincent-petithory/dataurl"
+)
+
+type MessagePart struct {
+ Text string
+ FileExtension string
+ Image []byte
+}
+
+func (b *Bmumble) tokenize(t *string) ([]MessagePart, error) {
+ // `^(.*?)` matches everyting before the image
+ // `!\[[^\]]*\]\(` matches the `]+)` matches the data: URI used by Mumble
+ // `\)` matches the closing parenthesis after the URI
+ // `(.*)$` matches the remaining text to be examined in the next iteration
+ p, err := regexp.Compile(`^(.*?)!\[[^\]]*\]\((data:image\/[^)]+)\)(.*)$`)
+ if err != nil {
+ return nil, err
+ }
+ remaining := *t
+ var parts []MessagePart
+ for {
+ tokens := p.FindStringSubmatch(remaining)
+
+ if tokens == nil {
+ b.Log.Debugf("Last text token: %s", remaining)
+ // no match -> remaining string is non-image text
+ if len(remaining) > 0 {
+ parts = append(parts, MessagePart{remaining, "", nil})
+ }
+ return parts, nil
+ }
+ if len(tokens[1]) > 0 {
+ parts = append(parts, MessagePart{tokens[1], "", nil})
+ }
+ uri, err := dataurl.UnescapeToString(strings.ReplaceAll(tokens[2], " ", ""))
+ if err != nil {
+ b.Log.WithError(err).Info("URL unescaping failed")
+ } else {
+ b.Log.Debugf("Raw data: URL: %s", uri)
+ image, err := dataurl.DecodeString(uri)
+ if err == nil {
+ ext, err := mime.ExtensionsByType(image.MediaType.ContentType())
+ if ext != nil && len(ext) > 0 {
+ parts = append(parts, MessagePart{"", ext[0], image.Data})
+ } else {
+ b.Log.WithError(err).Infof("No file extension registered for MIME type '%s'", image.MediaType.ContentType())
+ }
+ } else {
+ b.Log.WithError(err).Info("No image extracted")
+ }
+ }
+ remaining = tokens[3]
+ }
+}
+
+func (b *Bmumble) convertHTMLtoMarkdown(html string) ([]MessagePart, error) {
+ converter := md.NewConverter("", true, nil)
+ markdown, err := converter.ConvertString(html)
+ if err != nil {
+ return nil, err
+ }
+ return b.tokenize(&markdown)
+}
+
+func (b *Bmumble) extractFiles(msg *config.Message) []config.Message {
+ var messages []config.Message
+ if msg.Extra == nil || len(msg.Extra["file"]) == 0 {
+ return messages
+ }
+ for _, f := range msg.Extra["file"] {
+ fi := f.(config.FileInfo)
+ if fi.Data == nil || len(*fi.Data) == 0 {
+ // Mumble needs the raw data
+ b.Log.Info("Not forwarding file without local data")
+ continue
+ }
+ mimeType := http.DetectContentType(*fi.Data)
+ if !strings.HasPrefix(mimeType, "image/") {
+ // Mumble only supports images
+ b.Log.Infof("Not forwarding file of type %s", mimeType)
+ continue
+ }
+ mimeType = strings.TrimSpace(strings.Split(mimeType, ";")[0])
+ // Build image message
+ du := dataurl.New(*fi.Data, mimeType)
+ url, err := du.MarshalText()
+ if err != nil {
+ b.Log.WithError(err).Infof("Image Serialization into data URL failed (type: %s, length: %d)", mimeType, len(*fi.Data))
+ continue
+ }
+ imsg := config.Message{
+ Text: fmt.Sprintf(``, url),
+ Channel: msg.Channel,
+ Username: msg.Username,
+ UserID: msg.UserID,
+ Account: msg.Account,
+ Protocol: msg.Protocol,
+ Timestamp: msg.Timestamp,
+ Extra: make(map[string][]interface{}),
+ Event: "mumble_image",
+ }
+ messages = append(messages, imsg)
+ }
+ // Remove files from original message
+ msg.Extra["file"] = nil
+ return messages
+}
diff --git a/bridge/mumble/mumble.go b/bridge/mumble/mumble.go
index 01db859a..6f33fecf 100644
--- a/bridge/mumble/mumble.go
+++ b/bridge/mumble/mumble.go
@@ -90,7 +90,11 @@ func (b *Bmumble) Send(msg config.Message) (string, error) {
return "", nil
}
+ attachments := b.extractFiles(&msg)
b.local <- msg
+ for _, a := range attachments {
+ b.local <- a
+ }
return "", nil
}
@@ -213,8 +217,28 @@ func (b *Bmumble) doSend() {
func (b *Bmumble) processMessage(msg *config.Message) {
b.Log.Debugf("Processing message %s", msg.Text)
+ allowHTML := true
+ if b.serverConfig.AllowHTML != nil {
+ allowHTML = *b.serverConfig.AllowHTML
+ }
+
+ // If this is a specially generated image message, send it unmodified
+ if msg.Event == "mumble_image" {
+ if allowHTML {
+ b.Log.Debugf("Sending image message: %s%s", msg.Username, msg.Text)
+ b.client.Self.Channel.Send(msg.Username+msg.Text, false)
+ } else {
+ b.Log.Info("Can't send image, server does not allow HTML messages")
+ }
+ return
+ }
+
+ // Don't process empty messages
+ if len(msg.Text) == 0 {
+ return
+ }
// If HTML is allowed, convert markdown into HTML, otherwise strip markdown
- if allowHTML := b.serverConfig.AllowHTML; allowHTML == nil || !*allowHTML {
+ if allowHTML {
msg.Text = helper.ParseMarkdown(msg.Text)
} else {
msg.Text = stripmd.Strip(msg.Text)
diff --git a/go.mod b/go.mod
index 1311a7ae..182fa98f 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ require (
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/Jeffail/gabs v1.1.1 // indirect
+ github.com/JohannesKaufmann/html-to-markdown v1.2.0
github.com/Philipp15b/go-steam v1.0.1-0.20190816133340-b04c5a83c1c0
github.com/Rhymen/go-whatsapp v0.1.1-0.20200818115958-f07a700b9819
github.com/d5/tengo/v2 v2.6.0
@@ -43,6 +44,7 @@ require (
github.com/slack-go/slack v0.6.6
github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.6.1
+ github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50
github.com/writeas/go-strip-markdown v2.0.1+incompatible
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
github.com/yaegashi/msgraph.go v0.1.4
diff --git a/go.sum b/go.sum
index b68b0d7d..da3f1f70 100644
--- a/go.sum
+++ b/go.sum
@@ -54,6 +54,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/Jeffail/gabs v1.1.1 h1:V0uzR08Hj22EX8+8QMhyI9sX2hwRu+/RJhJUmnwda/E=
github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
+github.com/JohannesKaufmann/html-to-markdown v1.2.0 h1:UGTGZeFq6ljcd6CSxLW+CzUXQQDMJMzQIQ4lVexFCOk=
+github.com/JohannesKaufmann/html-to-markdown v1.2.0/go.mod h1:uFuht6eFsIHpym/0KHVOxLSbNh8SJiaKRR5yewSag1I=
github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Masterminds/squirrel v1.2.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA=
@@ -63,6 +65,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk=
github.com/Philipp15b/go-steam v1.0.1-0.20190816133340-b04c5a83c1c0 h1:TO7d4rocnNFng6ZQrPe7U6WqHtK5eHEMrgrnnM/72IQ=
github.com/Philipp15b/go-steam v1.0.1-0.20190816133340-b04c5a83c1c0/go.mod h1:HuVM+sZFzumUdKPWiz+IlCMb4RdsKdT3T+nQBKL+sYg=
+github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
+github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/Rhymen/go-whatsapp v0.0.0/go.mod h1:rdQr95g2C1xcOfM7QGOhza58HeI3I+tZ/bbluv7VazA=
github.com/Rhymen/go-whatsapp v0.1.1-0.20200818115958-f07a700b9819 h1:LthbEFUDcL9ZSRIs9m9JjThBSKrW6aIj8YGIT7G/SSk=
github.com/Rhymen/go-whatsapp v0.1.1-0.20200818115958-f07a700b9819/go.mod h1:o7jjkvKnigfu432dMbQ/w4PH0Yp5u4Y6ysCNjUlcYCk=
@@ -81,6 +85,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexcesaro/log v0.0.0-20150915221235-61e686294e58/go.mod h1:YNfsMyWSs+h+PaYkxGeMVmVCX75Zj/pqdjbu12ciCYE=
+github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
+github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
@@ -582,9 +588,11 @@ github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxT
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/satori/go.uuid v0.0.0-20180103174451-36e9d2ebbde5/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/sebdah/goldie/v2 v2.5.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
+github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1 h1:Lx3BlDGFElJt4u/zKc9A3BuGYbQAGlEFyPuUA3jeMD0=
github.com/shazow/rateio v0.0.0-20150116013248-e8e00881e5c1/go.mod h1:vt2jWY/3Qw1bIzle5thrJWucsLuuX9iUNnp20CqCciI=
github.com/shazow/ssh-chat v1.8.3-0.20200308224626-80ddf1f43a98 h1:sN07ff+PSRsUNhpSod4uGKAQ+Nc0FXsBPG9FmYMNg4w=
@@ -693,6 +701,8 @@ github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
+github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 h1:uxE3GYdXIOfhMv3unJKETJEhw78gvzuQqRX/rVirc2A=
+github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/wiggin77/cfg v1.0.2/go.mod h1:b3gotba2e5bXTqTW48DwIFoLc+4lWKP7WPi/CdvZ4aE=
github.com/wiggin77/logr v1.0.4/go.mod h1:h98FF6GPfThhDrHCg063hZA1sIyOEzQ/P85wgqI0IqE=
github.com/wiggin77/merror v1.0.2/go.mod h1:uQTcIU0Z6jRK4OwqganPYerzQxSFJ4GSHM3aurxxQpg=
@@ -713,6 +723,7 @@ github.com/yaegashi/wtz.go v0.0.2/go.mod h1:nOLA5QXsmdkRxBkP5tljhua13ADHCKirLBrz
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.0/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2 h1:UQwvu7FjUEdVYofx0U6bsc5odNE7wa5TSA0fl559GcA=
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2/go.mod h1:0MsIttMJIF/8Y7x0XjonJP7K99t3sR6bjj4m5S4JmqU=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
@@ -804,6 +815,7 @@ golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -835,6 +847,7 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
diff --git a/vendor/github.com/JohannesKaufmann/html-to-markdown/.gitignore b/vendor/github.com/JohannesKaufmann/html-to-markdown/.gitignore
new file mode 100644
index 00000000..9193b327
--- /dev/null
+++ b/vendor/github.com/JohannesKaufmann/html-to-markdown/.gitignore
@@ -0,0 +1,14 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+.DS_Store
diff --git a/vendor/github.com/JohannesKaufmann/html-to-markdown/CONTRIBUTING.md b/vendor/github.com/JohannesKaufmann/html-to-markdown/CONTRIBUTING.md
new file mode 100644
index 00000000..e69de29b
diff --git a/vendor/github.com/JohannesKaufmann/html-to-markdown/LICENSE b/vendor/github.com/JohannesKaufmann/html-to-markdown/LICENSE
new file mode 100644
index 00000000..0adaa6b5
--- /dev/null
+++ b/vendor/github.com/JohannesKaufmann/html-to-markdown/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Johannes Kaufmann
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/JohannesKaufmann/html-to-markdown/README.md b/vendor/github.com/JohannesKaufmann/html-to-markdown/README.md
new file mode 100644
index 00000000..36a275fb
--- /dev/null
+++ b/vendor/github.com/JohannesKaufmann/html-to-markdown/README.md
@@ -0,0 +1,125 @@
+# html-to-markdown
+
+[](https://goreportcard.com/report/github.com/JohannesKaufmann/html-to-markdown)
+[](https://codecov.io/gh/JohannesKaufmann/html-to-markdown)
+
+[](http://godoc.org/github.com/JohannesKaufmann/html-to-markdown)
+
+
+
+Convert HTML into Markdown with Go. It is using an [HTML Parser](https://github.com/PuerkitoBio/goquery) to avoid the use of `regexp` as much as possible. That should prevent some [weird cases](https://stackoverflow.com/a/1732454) and allows it to be used for cases where the input is totally unknown.
+
+## Installation
+
+```
+go get github.com/JohannesKaufmann/html-to-markdown
+```
+
+## Usage
+
+```go
+import md "github.com/JohannesKaufmann/html-to-markdown"
+
+converter := md.NewConverter("", true, nil)
+
+html = `Important`
+
+markdown, err := converter.ConvertString(html)
+if err != nil {
+ log.Fatal(err)
+}
+fmt.Println("md ->", markdown)
+```
+
+If you are already using [goquery](https://github.com/PuerkitoBio/goquery) you can pass a selection to `Convert`.
+
+```go
+markdown, err := converter.Convert(selec)
+```
+
+### Using it on the command line
+
+If you want to make use of `html-to-markdown` on the command line without any Go coding, check out [`html2md`](https://github.com/suntong/html2md#usage), a cli wrapper for `html-to-markdown` that has all the following options and plugins builtin.
+
+
+## Options
+
+The third parameter to `md.NewConverter` is `*md.Options`.
+
+For example you can change the character that is around a bold text ("`**`") to a different one (for example "`__`") by changing the value of `StrongDelimiter`.
+
+```go
+opt := &md.Options{
+ StrongDelimiter: "__", // default: **
+ // ...
+}
+converter := md.NewConverter("", true, opt)
+```
+
+For all the possible options look at [godocs](https://godoc.org/github.com/JohannesKaufmann/html-to-markdown/#Options) and for a example look at the [example](/examples/options/main.go).
+
+## Adding Rules
+
+```go
+converter.AddRules(
+ md.Rule{
+ Filter: []string{"del", "s", "strike"},
+ Replacement: func(content string, selec *goquery.Selection, opt *md.Options) *string {
+ // You need to return a pointer to a string (md.String is just a helper function).
+ // If you return nil the next function for that html element
+ // will be picked. For example you could only convert an element
+ // if it has a certain class name and fallback if not.
+ content = strings.TrimSpace(content)
+ return md.String("~" + content + "~")
+ },
+ },
+ // more rules
+)
+```
+
+For more information have a look at the example [add_rules](/examples/add_rules/main.go).
+
+## Using Plugins
+
+If you want plugins (github flavored markdown like striketrough, tables, ...) you can pass it to `Use`.
+
+```go
+import "github.com/JohannesKaufmann/html-to-markdown/plugin"
+
+// Use the `GitHubFlavored` plugin from the `plugin` package.
+converter.Use(plugin.GitHubFlavored())
+```
+
+Or if you only want to use the `Strikethrough` plugin. You can change the character that distinguishes
+the text that is crossed out by setting the first argument to a different value (for example "~~" instead of "~").
+
+```go
+converter.Use(plugin.Strikethrough(""))
+```
+
+For more information have a look at the example [github_flavored](/examples/github_flavored/main.go).
+
+## Writing Plugins
+
+Have a look at the [plugin folder](/plugin) for a reference implementation. The most basic one is [Strikethrough](/plugin/strikethrough.go).
+
+## Other Methods
+
+[Godoc](https://godoc.org/github.com/JohannesKaufmann/html-to-markdown)
+
+### `func (c *Converter) Keep(tags ...string) *Converter`
+
+Determines which elements are to be kept and rendered as HTML.
+
+### `func (c *Converter) Remove(tags ...string) *Converter`
+
+Determines which elements are to be removed altogether i.e. converted to an empty string.
+
+## Issues
+
+If you find HTML snippets (or even full websites) that don't produce the expected results, please open an issue!
+
+## Related Projects
+
+- [turndown (js)](https://github.com/domchristie/turndown), a very good library written in javascript.
+- [lunny/html2md](https://github.com/lunny/html2md), which is using [regex instead of goquery](https://stackoverflow.com/a/1732454). I came around a few edge case when using it (leaving some html comments, ...) so I wrote my own.
diff --git a/vendor/github.com/JohannesKaufmann/html-to-markdown/commonmark.go b/vendor/github.com/JohannesKaufmann/html-to-markdown/commonmark.go
new file mode 100644
index 00000000..f91e4708
--- /dev/null
+++ b/vendor/github.com/JohannesKaufmann/html-to-markdown/commonmark.go
@@ -0,0 +1,351 @@
+package md
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+
+ "github.com/JohannesKaufmann/html-to-markdown/escape"
+ "github.com/PuerkitoBio/goquery"
+)
+
+var multipleSpacesR = regexp.MustCompile(` +`)
+
+var commonmark = []Rule{
+ {
+ Filter: []string{"ul", "ol"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ parent := selec.Parent()
+
+ // we have a nested list, were the ul/ol is inside a list item
+ // -> based on work done by @requilence from @anytypeio
+ if parent.Is("li") && parent.Children().Last().IsSelection(selec) {
+ // add a line break prefix if the parent's text node doesn't have it.
+ // that makes sure that every list item is on its on line
+ lastContentTextNode := strings.TrimRight(parent.Nodes[0].FirstChild.Data, " \t")
+ if !strings.HasSuffix(lastContentTextNode, "\n") {
+ content = "\n" + content
+ }
+
+ // remove empty lines between lists
+ trimmedSpaceContent := strings.TrimRight(content, " \t")
+ if strings.HasSuffix(trimmedSpaceContent, "\n") {
+ content = strings.TrimRightFunc(content, unicode.IsSpace)
+ }
+ } else {
+ content = "\n\n" + content + "\n\n"
+ }
+ return &content
+ },
+ },
+ {
+ Filter: []string{"li"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ if strings.TrimSpace(content) == "" {
+ return nil
+ }
+
+ parent := selec.Parent()
+ index := selec.Index()
+
+ var prefix string
+ if parent.Is("ol") {
+ prefix = strconv.Itoa(index+1) + ". "
+ } else {
+ prefix = opt.BulletListMarker + " "
+ }
+ // remove leading newlines
+ content = leadingNewlinesR.ReplaceAllString(content, "")
+ // replace trailing newlines with just a single one
+ content = trailingNewlinesR.ReplaceAllString(content, "\n")
+ // indent
+ content = indentR.ReplaceAllString(content, "\n ")
+
+ return String(prefix + content + "\n")
+ },
+ },
+ {
+ Filter: []string{"#text"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ text := selec.Text()
+ if trimmed := strings.TrimSpace(text); trimmed == "" {
+ return String("")
+ }
+ text = tabR.ReplaceAllString(text, " ")
+
+ // replace multiple spaces by one space: dont accidentally make
+ // normal text be indented and thus be a code block.
+ text = multipleSpacesR.ReplaceAllString(text, " ")
+
+ text = escape.MarkdownCharacters(text)
+ return &text
+ },
+ },
+ {
+ Filter: []string{"p", "div"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ parent := goquery.NodeName(selec.Parent())
+ if IsInlineElement(parent) || parent == "li" {
+ content = "\n" + content + "\n"
+ return &content
+ }
+
+ // remove unnecessary spaces to have clean markdown
+ content = TrimpLeadingSpaces(content)
+
+ content = "\n\n" + content + "\n\n"
+ return &content
+ },
+ },
+ {
+ Filter: []string{"h1", "h2", "h3", "h4", "h5", "h6"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ if strings.TrimSpace(content) == "" {
+ return nil
+ }
+
+ content = strings.Replace(content, "\n", " ", -1)
+ content = strings.Replace(content, "\r", " ", -1)
+ content = strings.Replace(content, `#`, `\#`, -1)
+
+ insideLink := selec.ParentsFiltered("a").Length() > 0
+ if insideLink {
+ text := opt.StrongDelimiter + content + opt.StrongDelimiter
+ return &text
+ }
+
+ node := goquery.NodeName(selec)
+ level, err := strconv.Atoi(node[1:])
+ if err != nil {
+ return nil
+ }
+
+ if opt.HeadingStyle == "setext" && level < 3 {
+ line := "-"
+ if level == 1 {
+ line = "="
+ }
+
+ underline := strings.Repeat(line, len(content))
+ return String("\n\n" + content + "\n" + underline + "\n\n")
+ }
+
+ prefix := strings.Repeat("#", level)
+ content = strings.TrimSpace(content)
+ text := "\n\n" + prefix + " " + content + "\n\n"
+ return &text
+ },
+ },
+ {
+ Filter: []string{"strong", "b"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ // only use one bold tag if they are nested
+ parent := selec.Parent()
+ if parent.Is("strong") || parent.Is("b") {
+ return &content
+ }
+
+ trimmed := strings.TrimSpace(content)
+ if trimmed == "" {
+ return &trimmed
+ }
+ trimmed = opt.StrongDelimiter + trimmed + opt.StrongDelimiter
+
+ // always have a space to the side to recognize the delimiter
+ trimmed = AddSpaceIfNessesary(selec, trimmed)
+
+ return &trimmed
+ },
+ },
+ {
+ Filter: []string{"i", "em"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ // only use one italic tag if they are nested
+ parent := selec.Parent()
+ if parent.Is("i") || parent.Is("em") {
+ return &content
+ }
+
+ trimmed := strings.TrimSpace(content)
+ if trimmed == "" {
+ return &trimmed
+ }
+ trimmed = opt.EmDelimiter + trimmed + opt.EmDelimiter
+
+ // always have a space to the side to recognize the delimiter
+ trimmed = AddSpaceIfNessesary(selec, trimmed)
+
+ return &trimmed
+ },
+ },
+ {
+ Filter: []string{"img"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ src := selec.AttrOr("src", "")
+ src = strings.TrimSpace(src)
+ if src == "" {
+ return String("")
+ }
+
+ src = opt.GetAbsoluteURL(selec, src, opt.domain)
+
+ alt := selec.AttrOr("alt", "")
+ alt = strings.Replace(alt, "\n", " ", -1)
+
+ text := fmt.Sprintf("", alt, src)
+ return &text
+ },
+ },
+ {
+ Filter: []string{"a"},
+ AdvancedReplacement: func(content string, selec *goquery.Selection, opt *Options) (AdvancedResult, bool) {
+ // if there is no href, no link is used. So just return the content inside the link
+ href, ok := selec.Attr("href")
+ if !ok || strings.TrimSpace(href) == "" || strings.TrimSpace(href) == "#" {
+ return AdvancedResult{
+ Markdown: content,
+ }, false
+ }
+
+ href = opt.GetAbsoluteURL(selec, href, opt.domain)
+
+ // having multiline content inside a link is a bit tricky
+ content = EscapeMultiLine(content)
+
+ var title string
+ if t, ok := selec.Attr("title"); ok {
+ t = strings.Replace(t, "\n", " ", -1)
+ title = fmt.Sprintf(` "%s"`, t)
+ }
+
+ // if there is no link content (for example because it contains an svg)
+ // the 'title' or 'aria-label' attribute is used instead.
+ if strings.TrimSpace(content) == "" {
+ content = selec.AttrOr("title", selec.AttrOr("aria-label", ""))
+ }
+
+ // a link without text won't de displayed anyway
+ if content == "" {
+ return AdvancedResult{}, true
+ }
+
+ if opt.LinkStyle == "inlined" {
+ md := fmt.Sprintf("[%s](%s%s)", content, href, title)
+ md = AddSpaceIfNessesary(selec, md)
+
+ return AdvancedResult{
+ Markdown: md,
+ }, false
+ }
+
+ var replacement string
+ var reference string
+
+ switch opt.LinkReferenceStyle {
+ case "collapsed":
+
+ replacement = "[" + content + "][]"
+ reference = "[" + content + "]: " + href + title
+ case "shortcut":
+ replacement = "[" + content + "]"
+ reference = "[" + content + "]: " + href + title
+
+ default:
+ id := selec.AttrOr("data-index", "")
+ replacement = "[" + content + "][" + id + "]"
+ reference = "[" + id + "]: " + href + title
+ }
+
+ replacement = AddSpaceIfNessesary(selec, replacement)
+ return AdvancedResult{Markdown: replacement, Footer: reference}, false
+ },
+ },
+ {
+ Filter: []string{"code"},
+ Replacement: func(_ string, selec *goquery.Selection, opt *Options) *string {
+ code := selec.Text()
+
+ fenceChar := '`'
+ maxCount := calculateCodeFenceOccurrences(fenceChar, code)
+ maxCount++
+
+ fence := strings.Repeat(string(fenceChar), maxCount)
+
+ // code block contains a backtick as first character
+ if strings.HasPrefix(code, "`") {
+ code = " " + code
+ }
+ // code block contains a backtick as last character
+ if strings.HasSuffix(code, "`") {
+ code = code + " "
+ }
+
+ // TODO: configure delimeter in options?
+ text := fence + code + fence
+ return &text
+ },
+ },
+ {
+ Filter: []string{"pre"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ codeElement := selec.Find("code")
+ language := codeElement.AttrOr("class", "")
+ language = strings.Replace(language, "language-", "", 1)
+
+ code := codeElement.Text()
+ if codeElement.Length() == 0 {
+ code = selec.Text()
+ }
+
+ fenceChar, _ := utf8.DecodeRuneInString(opt.Fence)
+ fence := CalculateCodeFence(fenceChar, code)
+
+ text := "\n\n" + fence + language + "\n" +
+ code +
+ "\n" + fence + "\n\n"
+ return &text
+ },
+ },
+ {
+ Filter: []string{"hr"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ text := "\n\n" + opt.HorizontalRule + "\n\n"
+ return &text
+ },
+ },
+ {
+ Filter: []string{"br"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ return String("\n\n")
+ },
+ },
+ {
+ Filter: []string{"blockquote"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ content = strings.TrimSpace(content)
+ if content == "" {
+ return nil
+ }
+
+ content = multipleNewLinesRegex.ReplaceAllString(content, "\n\n")
+
+ var beginningR = regexp.MustCompile(`(?m)^`)
+ content = beginningR.ReplaceAllString(content, "> ")
+
+ text := "\n\n" + content + "\n\n"
+ return &text
+ },
+ },
+ {
+ Filter: []string{"noscript"},
+ Replacement: func(content string, selec *goquery.Selection, opt *Options) *string {
+ // for now remove the contents of noscript. But in the future we could
+ // tell goquery to parse the contents of the tag.
+ // -> https://github.com/PuerkitoBio/goquery/issues/139#issuecomment-517526070
+ return nil
+ },
+ },
+}
diff --git a/vendor/github.com/JohannesKaufmann/html-to-markdown/escape/escape.go b/vendor/github.com/JohannesKaufmann/html-to-markdown/escape/escape.go
new file mode 100644
index 00000000..16bb8892
--- /dev/null
+++ b/vendor/github.com/JohannesKaufmann/html-to-markdown/escape/escape.go
@@ -0,0 +1,66 @@
+// Package escape escapes characters that are commonly used in
+// markdown like the * for strong/italic.
+package escape
+
+import (
+ "regexp"
+ "strings"
+)
+
+var backslash = regexp.MustCompile(`\\(\S)`)
+var heading = regexp.MustCompile(`(?m)^(#{1,6} )`)
+var orderedList = regexp.MustCompile(`(?m)^(\W* {0,3})(\d+)\. `)
+var unorderedList = regexp.MustCompile(`(?m)^([^\\\w]*)[*+-] `)
+var horizontalDivider = regexp.MustCompile(`(?m)^([-*_] *){3,}$`)
+var blockquote = regexp.MustCompile(`(?m)^(\W* {0,3})> `)
+
+var replacer = strings.NewReplacer(
+ `*`, `\*`,
+ `_`, `\_`,
+ "`", "\\`",
+ `|`, `\|`,
+)
+
+// MarkdownCharacters escapes common markdown characters so that
+// `
**Not Bold**
ends up as correct markdown `\*\*Not Strong\*\*`. +// No worry, the escaped characters will display fine, just without the formatting. +func MarkdownCharacters(text string) string { + // Escape backslash escapes! + text = backslash.ReplaceAllString(text, `\\$1`) + + // Escape headings + text = heading.ReplaceAllString(text, `\$1`) + + // Escape hr + text = horizontalDivider.ReplaceAllStringFunc(text, func(t string) string { + if strings.Contains(t, "-") { + return strings.Replace(t, "-", `\-`, 3) + } else if strings.Contains(t, "_") { + return strings.Replace(t, "_", `\_`, 3) + } + return strings.Replace(t, "*", `\*`, 3) + }) + + // Escape ol bullet points + text = orderedList.ReplaceAllString(text, `$1$2\. `) + + // Escape ul bullet points + text = unorderedList.ReplaceAllStringFunc(text, func(t string) string { + return regexp.MustCompile(`([*+-])`).ReplaceAllString(t, `\$1`) + }) + + // Escape blockquote indents + text = blockquote.ReplaceAllString(text, `$1\> `) + + // Escape em/strong * + // Escape em/strong _ + // Escape code _ + text = replacer.Replace(text) + + // Escape link brackets + // (disabled) + // var link = regexp.MustCompile(`[\[\]]`) + // text = link.ReplaceAllString(text, `\$&`) + + return text +} diff --git a/vendor/github.com/JohannesKaufmann/html-to-markdown/from.go b/vendor/github.com/JohannesKaufmann/html-to-markdown/from.go new file mode 100644 index 00000000..f686f579 --- /dev/null +++ b/vendor/github.com/JohannesKaufmann/html-to-markdown/from.go @@ -0,0 +1,417 @@ +// Package md converts html to markdown. +// +// converter := md.NewConverter("", true, nil) +// +// html = `Important` +// +// markdown, err := converter.ConvertString(html) +// if err != nil { +// log.Fatal(err) +// } +// fmt.Println("md ->", markdown) +// Or if you are already using goquery: +// markdown, err := converter.Convert(selec) +package md + +import ( + "bytes" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/PuerkitoBio/goquery" +) + +type simpleRuleFunc func(content string, selec *goquery.Selection, options *Options) *string +type ruleFunc func(content string, selec *goquery.Selection, options *Options) (res AdvancedResult, skip bool) + +type BeforeHook func(selec *goquery.Selection) +type Afterhook func(markdown string) string + +// Converter is initialized by NewConverter. +type Converter struct { + mutex sync.RWMutex + rules map[string][]ruleFunc + keep map[string]struct{} + remove map[string]struct{} + + before []BeforeHook + after []Afterhook + + domain string + options Options +} + +func validate(val string, possible ...string) error { + for _, e := range possible { + if e == val { + return nil + } + } + return fmt.Errorf("field must be one of %v but got %s", possible, val) +} +func validateOptions(opt Options) error { + if err := validate(opt.HeadingStyle, "setext", "atx"); err != nil { + return err + } + if strings.Count(opt.HorizontalRule, "*") < 3 && + strings.Count(opt.HorizontalRule, "_") < 3 && + strings.Count(opt.HorizontalRule, "-") < 3 { + return errors.New("HorizontalRule must be at least 3 characters of '*', '_' or '-' but got " + opt.HorizontalRule) + } + + if err := validate(opt.BulletListMarker, "-", "+", "*"); err != nil { + return err + } + if err := validate(opt.CodeBlockStyle, "indented", "fenced"); err != nil { + return err + } + if err := validate(opt.Fence, "```", "~~~"); err != nil { + return err + } + if err := validate(opt.EmDelimiter, "_", "*"); err != nil { + return err + } + if err := validate(opt.StrongDelimiter, "**", "__"); err != nil { + return err + } + if err := validate(opt.LinkStyle, "inlined", "referenced"); err != nil { + return err + } + if err := validate(opt.LinkReferenceStyle, "full", "collapsed", "shortcut"); err != nil { + return err + } + + return nil +} + +// NewConverter initializes a new converter and holds all the rules. +// - `domain` is used for links and images to convert relative urls ("/image.png") to absolute urls. +// - CommonMark is the default set of rules. Set enableCommonmark to false if you want +// to customize everything using AddRules and DONT want to fallback to default rules. +func NewConverter(domain string, enableCommonmark bool, options *Options) *Converter { + conv := &Converter{ + domain: domain, + rules: make(map[string][]ruleFunc), + keep: make(map[string]struct{}), + remove: make(map[string]struct{}), + } + + conv.before = append(conv.before, func(selec *goquery.Selection) { + selec.Find("a[href]").Each(func(i int, s *goquery.Selection) { + s.SetAttr("data-index", strconv.Itoa(i+1)) + }) + }) + conv.after = append(conv.after, func(markdown string) string { + markdown = strings.TrimSpace(markdown) + markdown = multipleNewLinesRegex.ReplaceAllString(markdown, "\n\n") + + // remove unnecessary trailing spaces to have clean markdown + markdown = TrimTrailingSpaces(markdown) + + return markdown + }) + + if enableCommonmark { + conv.AddRules(commonmark...) + conv.remove["script"] = struct{}{} + conv.remove["style"] = struct{}{} + conv.remove["textarea"] = struct{}{} + } + + // TODO: put domain in options? + if options == nil { + options = &Options{} + } + if options.HeadingStyle == "" { + options.HeadingStyle = "atx" + } + if options.HorizontalRule == "" { + options.HorizontalRule = "* * *" + } + if options.BulletListMarker == "" { + options.BulletListMarker = "-" + } + if options.CodeBlockStyle == "" { + options.CodeBlockStyle = "indented" + } + if options.Fence == "" { + options.Fence = "```" + } + if options.EmDelimiter == "" { + options.EmDelimiter = "_" + } + if options.StrongDelimiter == "" { + options.StrongDelimiter = "**" + } + if options.LinkStyle == "" { + options.LinkStyle = "inlined" + } + if options.LinkReferenceStyle == "" { + options.LinkReferenceStyle = "full" + } + + // for now, store it in the options + options.domain = domain + + if options.GetAbsoluteURL == nil { + options.GetAbsoluteURL = DefaultGetAbsoluteURL + } + + conv.options = *options + err := validateOptions(conv.options) + if err != nil { + log.Println("markdown options is not valid:", err) + } + + return conv +} +func (conv *Converter) getRuleFuncs(tag string) []ruleFunc { + conv.mutex.RLock() + defer conv.mutex.RUnlock() + + r, ok := conv.rules[tag] + if !ok || len(r) == 0 { + if _, keep := conv.keep[tag]; keep { + return []ruleFunc{wrap(ruleKeep)} + } + if _, remove := conv.remove[tag]; remove { + return nil // TODO: + } + + return []ruleFunc{wrap(ruleDefault)} + } + + return r +} + +func wrap(simple simpleRuleFunc) ruleFunc { + return func(content string, selec *goquery.Selection, opt *Options) (AdvancedResult, bool) { + res := simple(content, selec, opt) + if res == nil { + return AdvancedResult{}, true + } + return AdvancedResult{Markdown: *res}, false + } +} + +// Before registers a hook that is run before the convertion. It +// can be used to transform the original goquery html document. +// +// For example, the default before hook adds an index to every link, +// so that the `a` tag rule (for "reference" "full") can have an incremental number. +func (conv *Converter) Before(hooks ...BeforeHook) *Converter { + conv.mutex.Lock() + defer conv.mutex.Unlock() + + for _, hook := range hooks { + conv.before = append(conv.before, hook) + } + + return conv +} + +// After registers a hook that is run after the convertion. It +// can be used to transform the markdown document that is about to be returned. +// +// For example, the default after hook trims the returned markdown. +func (conv *Converter) After(hooks ...Afterhook) *Converter { + conv.mutex.Lock() + defer conv.mutex.Unlock() + + for _, hook := range hooks { + conv.after = append(conv.after, hook) + } + + return conv +} + +// AddRules adds the rules that are passed in to the converter. +// +// By default it overrides the rule for that html tag. You can +// fall back to the default rule by returning nil. +func (conv *Converter) AddRules(rules ...Rule) *Converter { + conv.mutex.Lock() + defer conv.mutex.Unlock() + + for _, rule := range rules { + if len(rule.Filter) == 0 { + log.Println("you need to specify at least one filter for your rule") + } + for _, filter := range rule.Filter { + r, _ := conv.rules[filter] + + if rule.AdvancedReplacement != nil { + r = append(r, rule.AdvancedReplacement) + } else { + r = append(r, wrap(rule.Replacement)) + } + conv.rules[filter] = r + } + } + + return conv +} + +// Keep certain html tags in the generated output. +func (conv *Converter) Keep(tags ...string) *Converter { + conv.mutex.Lock() + defer conv.mutex.Unlock() + + for _, tag := range tags { + conv.keep[tag] = struct{}{} + } + return conv +} + +// Remove certain html tags from the source. +func (conv *Converter) Remove(tags ...string) *Converter { + conv.mutex.Lock() + defer conv.mutex.Unlock() + for _, tag := range tags { + conv.remove[tag] = struct{}{} + } + return conv +} + +// Plugin can be used to extends functionality beyond what +// is offered by commonmark. +type Plugin func(conv *Converter) []Rule + +// Use can be used to add additional functionality to the converter. It is +// used when its not sufficient to use only rules for example in Plugins. +func (conv *Converter) Use(plugins ...Plugin) *Converter { + for _, plugin := range plugins { + rules := plugin(conv) + conv.AddRules(rules...) // TODO: for better performance only use one lock for all plugins + } + return conv +} + +// Timeout for the http client +var Timeout = time.Second * 10 +var netClient = &http.Client{ + Timeout: Timeout, +} + +// DomainFromURL returns `u.Host` from the parsed url. +func DomainFromURL(rawURL string) string { + u, err := url.Parse(rawURL) + if err != nil { + return "" + } + return u.Host +} + +// Reduce many newline characters `\n` to at most 2 new line characters. +var multipleNewLinesRegex = regexp.MustCompile(`[\n]{2,}`) + +// Convert returns the content from a goquery selection. +// If you have a goquery document just pass in doc.Selection. +func (conv *Converter) Convert(selec *goquery.Selection) string { + conv.mutex.RLock() + domain := conv.domain + options := conv.options + l := len(conv.rules) + if l == 0 { + log.Println("you have added no rules. either enable commonmark or add you own.") + } + before := conv.before + after := conv.after + conv.mutex.RUnlock() + + // before hook + for _, hook := range before { + hook(selec) + } + + res := conv.selecToMD(domain, selec, &options) + markdown := res.Markdown + + if res.Header != "" { + markdown = res.Header + "\n\n" + markdown + } + if res.Footer != "" { + markdown += "\n\n" + res.Footer + } + + // after hook + for _, hook := range after { + markdown = hook(markdown) + } + + return markdown +} + +// ConvertReader returns the content from a reader and returns a buffer. +func (conv *Converter) ConvertReader(reader io.Reader) (bytes.Buffer, error) { + var buffer bytes.Buffer + doc, err := goquery.NewDocumentFromReader(reader) + if err != nil { + return buffer, err + } + buffer.WriteString( + conv.Convert(doc.Selection), + ) + + return buffer, nil +} + +// ConvertResponse returns the content from a html response. +func (conv *Converter) ConvertResponse(res *http.Response) (string, error) { + doc, err := goquery.NewDocumentFromResponse(res) + if err != nil { + return "", err + } + return conv.Convert(doc.Selection), nil +} + +// ConvertString returns the content from a html string. If you +// already have a goquery selection use `Convert`. +func (conv *Converter) ConvertString(html string) (string, error) { + doc, err := goquery.NewDocumentFromReader(strings.NewReader(html)) + if err != nil { + return "", err + } + return conv.Convert(doc.Selection), nil +} + +// ConvertBytes returns the content from a html byte array. +func (conv *Converter) ConvertBytes(bytes []byte) ([]byte, error) { + res, err := conv.ConvertString(string(bytes)) + if err != nil { + return nil, err + } + return []byte(res), nil +} + +// ConvertURL returns the content from the page with that url. +func (conv *Converter) ConvertURL(url string) (string, error) { + // not using goquery.NewDocument directly because of the timeout + resp, err := netClient.Get(url) + if err != nil { + return "", err + } + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return "", fmt.Errorf("expected a status code in the 2xx range but got %d", resp.StatusCode) + } + + doc, err := goquery.NewDocumentFromResponse(resp) + if err != nil { + return "", err + } + domain := DomainFromURL(url) + if conv.domain != domain { + log.Printf("expected '%s' as the domain but got '%s' \n", conv.domain, domain) + } + return conv.Convert(doc.Selection), nil +} diff --git a/vendor/github.com/JohannesKaufmann/html-to-markdown/go.mod b/vendor/github.com/JohannesKaufmann/html-to-markdown/go.mod new file mode 100644 index 00000000..336d8bd7 --- /dev/null +++ b/vendor/github.com/JohannesKaufmann/html-to-markdown/go.mod @@ -0,0 +1,12 @@ +module github.com/JohannesKaufmann/html-to-markdown + +go 1.13 + +require ( + github.com/PuerkitoBio/goquery v1.5.1 + github.com/sebdah/goldie/v2 v2.5.1 + github.com/sergi/go-diff v1.1.0 + github.com/yuin/goldmark v1.2.0 + golang.org/x/net v0.0.0-20200320220750-118fecf932d8 + gopkg.in/yaml.v2 v2.2.8 +) diff --git a/vendor/github.com/JohannesKaufmann/html-to-markdown/go.sum b/vendor/github.com/JohannesKaufmann/html-to-markdown/go.sum new file mode 100644 index 00000000..c7a20a16 --- /dev/null +++ b/vendor/github.com/JohannesKaufmann/html-to-markdown/go.sum @@ -0,0 +1,40 @@ +github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sebdah/goldie/v2 v2.5.1 h1:hh70HvG4n3T3MNRJN2z/baxPR8xutxo7JVxyi2svl+s= +github.com/sebdah/goldie/v2 v2.5.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/yuin/goldmark v1.2.0 h1:WOOcyaJPlzb8fZ8TloxFe8QZkhOOJx87leDa9MIT9dc= +github.com/yuin/goldmark v1.2.0/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200320220750-118fecf932d8 h1:1+zQlQqEEhUeStBTi653GZAnAuivZq/2hz+Iz+OP7rg= +golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/JohannesKaufmann/html-to-markdown/logo.png b/vendor/github.com/JohannesKaufmann/html-to-markdown/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fe862facb94ade2e6b8e83a525700b2c458471a6 GIT binary patch literal 38000 zcmeFXWmJ{Xw>Jv7kqtu6!W%yuW-3@X$L=3T)~4AxMw+ *Vx5^S@Th?nOtXerT;3P@kA?A$3Z5nAYf8EX!c>J^;;
z=A0zPhh-WxCY3;07)g(Lfs$zD27RNmj?OC|w%Ji~JbYcwVqbdcOIEKTP+kffQ(wj7
zDh8%v>&4jb_YDE4QOwp#1KR*7SH))w!?Oq1u`2o$&tJR^IO>9y*hIhTOwW+e#(PV!
z7ZFAGlLqzMNyf$&?LC^?K&Bp0BhI+r;CX>4%<|VaKCCz!hB2{lt^>i&>A@6B@Kfa~LP>>LbC4DjxEpf3P;6N}~HZBr&@lzb{qTOW3pl~KIt
z7sG*Q9X6l7!F~0@8+LsCN&9m;iA7VuS)g{k7-zslMsXCV4+=mv7PVJvL3b>($x=C9
zTrZB2{}nSntPMK*U1b$>%Qo+d=XEqJdtZfK!WH^=SQQfdN3!xQJ^
77m!j
z5Wv}%n8o%#weUJCE?rlfsn&Q{7ZWBvR}^mCJE7=rL4MH04Z+S6vIsH
qd?ACP?MBJS!2I0(zw|#FjWDH<90$G02Ltg4usKj&Y>PMhUHBI
+
+## License
+
+The [BSD 3-Clause license][bsd], the same as the [Go language][golic]. Cascadia's license is [here][caslic].
+
+[jquery]: http://jquery.com/
+[go]: http://golang.org/
+[cascadia]: https://github.com/andybalholm/cascadia
+[cascadiacli]: https://github.com/suntong/cascadia
+[bsd]: http://opensource.org/licenses/BSD-3-Clause
+[golic]: http://golang.org/LICENSE
+[caslic]: https://github.com/andybalholm/cascadia/blob/master/LICENSE
+[doc]: http://godoc.org/github.com/PuerkitoBio/goquery
+[index]: http://api.jquery.com/index/
+[gonet]: https://github.com/golang/net/
+[html]: http://godoc.org/golang.org/x/net/html
+[wiki]: https://github.com/PuerkitoBio/goquery/wiki/Tips-and-tricks
+[thatguystone]: https://github.com/thatguystone
+[piotr]: https://github.com/piotrkowalczuk
+[goq]: https://github.com/andrewstuart/goq
diff --git a/vendor/github.com/PuerkitoBio/goquery/array.go b/vendor/github.com/PuerkitoBio/goquery/array.go
new file mode 100644
index 00000000..1b1f6cbe
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/array.go
@@ -0,0 +1,124 @@
+package goquery
+
+import (
+ "golang.org/x/net/html"
+)
+
+const (
+ maxUint = ^uint(0)
+ maxInt = int(maxUint >> 1)
+
+ // ToEnd is a special index value that can be used as end index in a call
+ // to Slice so that all elements are selected until the end of the Selection.
+ // It is equivalent to passing (*Selection).Length().
+ ToEnd = maxInt
+)
+
+// First reduces the set of matched elements to the first in the set.
+// It returns a new Selection object, and an empty Selection object if the
+// the selection is empty.
+func (s *Selection) First() *Selection {
+ return s.Eq(0)
+}
+
+// Last reduces the set of matched elements to the last in the set.
+// It returns a new Selection object, and an empty Selection object if
+// the selection is empty.
+func (s *Selection) Last() *Selection {
+ return s.Eq(-1)
+}
+
+// Eq reduces the set of matched elements to the one at the specified index.
+// If a negative index is given, it counts backwards starting at the end of the
+// set. It returns a new Selection object, and an empty Selection object if the
+// index is invalid.
+func (s *Selection) Eq(index int) *Selection {
+ if index < 0 {
+ index += len(s.Nodes)
+ }
+
+ if index >= len(s.Nodes) || index < 0 {
+ return newEmptySelection(s.document)
+ }
+
+ return s.Slice(index, index+1)
+}
+
+// Slice reduces the set of matched elements to a subset specified by a range
+// of indices. The start index is 0-based and indicates the index of the first
+// element to select. The end index is 0-based and indicates the index at which
+// the elements stop being selected (the end index is not selected).
+//
+// The indices may be negative, in which case they represent an offset from the
+// end of the selection.
+//
+// The special value ToEnd may be specified as end index, in which case all elements
+// until the end are selected. This works both for a positive and negative start
+// index.
+func (s *Selection) Slice(start, end int) *Selection {
+ if start < 0 {
+ start += len(s.Nodes)
+ }
+ if end == ToEnd {
+ end = len(s.Nodes)
+ } else if end < 0 {
+ end += len(s.Nodes)
+ }
+ return pushStack(s, s.Nodes[start:end])
+}
+
+// Get retrieves the underlying node at the specified index.
+// Get without parameter is not implemented, since the node array is available
+// on the Selection object.
+func (s *Selection) Get(index int) *html.Node {
+ if index < 0 {
+ index += len(s.Nodes) // Negative index gets from the end
+ }
+ return s.Nodes[index]
+}
+
+// Index returns the position of the first element within the Selection object
+// relative to its sibling elements.
+func (s *Selection) Index() int {
+ if len(s.Nodes) > 0 {
+ return newSingleSelection(s.Nodes[0], s.document).PrevAll().Length()
+ }
+ return -1
+}
+
+// IndexSelector returns the position of the first element within the
+// Selection object relative to the elements matched by the selector, or -1 if
+// not found.
+func (s *Selection) IndexSelector(selector string) int {
+ if len(s.Nodes) > 0 {
+ sel := s.document.Find(selector)
+ return indexInSlice(sel.Nodes, s.Nodes[0])
+ }
+ return -1
+}
+
+// IndexMatcher returns the position of the first element within the
+// Selection object relative to the elements matched by the matcher, or -1 if
+// not found.
+func (s *Selection) IndexMatcher(m Matcher) int {
+ if len(s.Nodes) > 0 {
+ sel := s.document.FindMatcher(m)
+ return indexInSlice(sel.Nodes, s.Nodes[0])
+ }
+ return -1
+}
+
+// IndexOfNode returns the position of the specified node within the Selection
+// object, or -1 if not found.
+func (s *Selection) IndexOfNode(node *html.Node) int {
+ return indexInSlice(s.Nodes, node)
+}
+
+// IndexOfSelection returns the position of the first node in the specified
+// Selection object within this Selection object, or -1 if not found.
+func (s *Selection) IndexOfSelection(sel *Selection) int {
+ if sel != nil && len(sel.Nodes) > 0 {
+ return indexInSlice(s.Nodes, sel.Nodes[0])
+ }
+ return -1
+}
diff --git a/vendor/github.com/PuerkitoBio/goquery/doc.go b/vendor/github.com/PuerkitoBio/goquery/doc.go
new file mode 100644
index 00000000..71146a78
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/doc.go
@@ -0,0 +1,123 @@
+// Copyright (c) 2012-2016, Martin Angers & Contributors
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above copyright notice,
+// this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation and/or
+// other materials provided with the distribution.
+// * Neither the name of the author nor the names of its contributors may be used to
+// endorse or promote products derived from this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
+// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+// WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+/*
+Package goquery implements features similar to jQuery, including the chainable
+syntax, to manipulate and query an HTML document.
+
+It brings a syntax and a set of features similar to jQuery to the Go language.
+It is based on Go's net/html package and the CSS Selector library cascadia.
+Since the net/html parser returns nodes, and not a full-featured DOM
+tree, jQuery's stateful manipulation functions (like height(), css(), detach())
+have been left off.
+
+Also, because the net/html parser requires UTF-8 encoding, so does goquery: it is
+the caller's responsibility to ensure that the source document provides UTF-8 encoded HTML.
+See the repository's wiki for various options on how to do this.
+
+Syntax-wise, it is as close as possible to jQuery, with the same method names when
+possible, and that warm and fuzzy chainable interface. jQuery being the
+ultra-popular library that it is, writing a similar HTML-manipulating
+library was better to follow its API than to start anew (in the same spirit as
+Go's fmt package), even though some of its methods are less than intuitive (looking
+at you, index()...).
+
+It is hosted on GitHub, along with additional documentation in the README.md
+file: https://github.com/puerkitobio/goquery
+
+Please note that because of the net/html dependency, goquery requires Go1.1+.
+
+The various methods are split into files based on the category of behavior.
+The three dots (...) indicate that various "overloads" are available.
+
+* array.go : array-like positional manipulation of the selection.
+ - Eq()
+ - First()
+ - Get()
+ - Index...()
+ - Last()
+ - Slice()
+
+* expand.go : methods that expand or augment the selection's set.
+ - Add...()
+ - AndSelf()
+ - Union(), which is an alias for AddSelection()
+
+* filter.go : filtering methods, that reduce the selection's set.
+ - End()
+ - Filter...()
+ - Has...()
+ - Intersection(), which is an alias of FilterSelection()
+ - Not...()
+
+* iteration.go : methods to loop over the selection's nodes.
+ - Each()
+ - EachWithBreak()
+ - Map()
+
+* manipulation.go : methods for modifying the document
+ - After...()
+ - Append...()
+ - Before...()
+ - Clone()
+ - Empty()
+ - Prepend...()
+ - Remove...()
+ - ReplaceWith...()
+ - Unwrap()
+ - Wrap...()
+ - WrapAll...()
+ - WrapInner...()
+
+* property.go : methods that inspect and get the node's properties values.
+ - Attr*(), RemoveAttr(), SetAttr()
+ - AddClass(), HasClass(), RemoveClass(), ToggleClass()
+ - Html()
+ - Length()
+ - Size(), which is an alias for Length()
+ - Text()
+
+* query.go : methods that query, or reflect, a node's identity.
+ - Contains()
+ - Is...()
+
+* traversal.go : methods to traverse the HTML document tree.
+ - Children...()
+ - Contents()
+ - Find...()
+ - Next...()
+ - Parent[s]...()
+ - Prev...()
+ - Siblings...()
+
+* type.go : definition of the types exposed by goquery.
+ - Document
+ - Selection
+ - Matcher
+
+* utilities.go : definition of helper functions (and not methods on a *Selection)
+that are not part of jQuery, but are useful to goquery.
+ - NodeName
+ - OuterHtml
+*/
+package goquery
diff --git a/vendor/github.com/PuerkitoBio/goquery/expand.go b/vendor/github.com/PuerkitoBio/goquery/expand.go
new file mode 100644
index 00000000..7caade53
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/expand.go
@@ -0,0 +1,70 @@
+package goquery
+
+import "golang.org/x/net/html"
+
+// Add adds the selector string's matching nodes to those in the current
+// selection and returns a new Selection object.
+// The selector string is run in the context of the document of the current
+// Selection object.
+func (s *Selection) Add(selector string) *Selection {
+ return s.AddNodes(findWithMatcher([]*html.Node{s.document.rootNode}, compileMatcher(selector))...)
+}
+
+// AddMatcher adds the matcher's matching nodes to those in the current
+// selection and returns a new Selection object.
+// The matcher is run in the context of the document of the current
+// Selection object.
+func (s *Selection) AddMatcher(m Matcher) *Selection {
+ return s.AddNodes(findWithMatcher([]*html.Node{s.document.rootNode}, m)...)
+}
+
+// AddSelection adds the specified Selection object's nodes to those in the
+// current selection and returns a new Selection object.
+func (s *Selection) AddSelection(sel *Selection) *Selection {
+ if sel == nil {
+ return s.AddNodes()
+ }
+ return s.AddNodes(sel.Nodes...)
+}
+
+// Union is an alias for AddSelection.
+func (s *Selection) Union(sel *Selection) *Selection {
+ return s.AddSelection(sel)
+}
+
+// AddNodes adds the specified nodes to those in the
+// current selection and returns a new Selection object.
+func (s *Selection) AddNodes(nodes ...*html.Node) *Selection {
+ return pushStack(s, appendWithoutDuplicates(s.Nodes, nodes, nil))
+}
+
+// AndSelf adds the previous set of elements on the stack to the current set.
+// It returns a new Selection object containing the current Selection combined
+// with the previous one.
+// Deprecated: This function has been deprecated and is now an alias for AddBack().
+func (s *Selection) AndSelf() *Selection {
+ return s.AddBack()
+}
+
+// AddBack adds the previous set of elements on the stack to the current set.
+// It returns a new Selection object containing the current Selection combined
+// with the previous one.
+func (s *Selection) AddBack() *Selection {
+ return s.AddSelection(s.prevSel)
+}
+
+// AddBackFiltered reduces the previous set of elements on the stack to those that
+// match the selector string, and adds them to the current set.
+// It returns a new Selection object containing the current Selection combined
+// with the filtered previous one
+func (s *Selection) AddBackFiltered(selector string) *Selection {
+ return s.AddSelection(s.prevSel.Filter(selector))
+}
+
+// AddBackMatcher reduces the previous set of elements on the stack to those that match
+// the mateher, and adds them to the curernt set.
+// It returns a new Selection object containing the current Selection combined
+// with the filtered previous one
+func (s *Selection) AddBackMatcher(m Matcher) *Selection {
+ return s.AddSelection(s.prevSel.FilterMatcher(m))
+}
diff --git a/vendor/github.com/PuerkitoBio/goquery/filter.go b/vendor/github.com/PuerkitoBio/goquery/filter.go
new file mode 100644
index 00000000..9138ffb3
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/filter.go
@@ -0,0 +1,163 @@
+package goquery
+
+import "golang.org/x/net/html"
+
+// Filter reduces the set of matched elements to those that match the selector string.
+// It returns a new Selection object for this subset of matching elements.
+func (s *Selection) Filter(selector string) *Selection {
+ return s.FilterMatcher(compileMatcher(selector))
+}
+
+// FilterMatcher reduces the set of matched elements to those that match
+// the given matcher. It returns a new Selection object for this subset
+// of matching elements.
+func (s *Selection) FilterMatcher(m Matcher) *Selection {
+ return pushStack(s, winnow(s, m, true))
+}
+
+// Not removes elements from the Selection that match the selector string.
+// It returns a new Selection object with the matching elements removed.
+func (s *Selection) Not(selector string) *Selection {
+ return s.NotMatcher(compileMatcher(selector))
+}
+
+// NotMatcher removes elements from the Selection that match the given matcher.
+// It returns a new Selection object with the matching elements removed.
+func (s *Selection) NotMatcher(m Matcher) *Selection {
+ return pushStack(s, winnow(s, m, false))
+}
+
+// FilterFunction reduces the set of matched elements to those that pass the function's test.
+// It returns a new Selection object for this subset of elements.
+func (s *Selection) FilterFunction(f func(int, *Selection) bool) *Selection {
+ return pushStack(s, winnowFunction(s, f, true))
+}
+
+// NotFunction removes elements from the Selection that pass the function's test.
+// It returns a new Selection object with the matching elements removed.
+func (s *Selection) NotFunction(f func(int, *Selection) bool) *Selection {
+ return pushStack(s, winnowFunction(s, f, false))
+}
+
+// FilterNodes reduces the set of matched elements to those that match the specified nodes.
+// It returns a new Selection object for this subset of elements.
+func (s *Selection) FilterNodes(nodes ...*html.Node) *Selection {
+ return pushStack(s, winnowNodes(s, nodes, true))
+}
+
+// NotNodes removes elements from the Selection that match the specified nodes.
+// It returns a new Selection object with the matching elements removed.
+func (s *Selection) NotNodes(nodes ...*html.Node) *Selection {
+ return pushStack(s, winnowNodes(s, nodes, false))
+}
+
+// FilterSelection reduces the set of matched elements to those that match a
+// node in the specified Selection object.
+// It returns a new Selection object for this subset of elements.
+func (s *Selection) FilterSelection(sel *Selection) *Selection {
+ if sel == nil {
+ return pushStack(s, winnowNodes(s, nil, true))
+ }
+ return pushStack(s, winnowNodes(s, sel.Nodes, true))
+}
+
+// NotSelection removes elements from the Selection that match a node in the specified
+// Selection object. It returns a new Selection object with the matching elements removed.
+func (s *Selection) NotSelection(sel *Selection) *Selection {
+ if sel == nil {
+ return pushStack(s, winnowNodes(s, nil, false))
+ }
+ return pushStack(s, winnowNodes(s, sel.Nodes, false))
+}
+
+// Intersection is an alias for FilterSelection.
+func (s *Selection) Intersection(sel *Selection) *Selection {
+ return s.FilterSelection(sel)
+}
+
+// Has reduces the set of matched elements to those that have a descendant
+// that matches the selector.
+// It returns a new Selection object with the matching elements.
+func (s *Selection) Has(selector string) *Selection {
+ return s.HasSelection(s.document.Find(selector))
+}
+
+// HasMatcher reduces the set of matched elements to those that have a descendant
+// that matches the matcher.
+// It returns a new Selection object with the matching elements.
+func (s *Selection) HasMatcher(m Matcher) *Selection {
+ return s.HasSelection(s.document.FindMatcher(m))
+}
+
+// HasNodes reduces the set of matched elements to those that have a
+// descendant that matches one of the nodes.
+// It returns a new Selection object with the matching elements.
+func (s *Selection) HasNodes(nodes ...*html.Node) *Selection {
+ return s.FilterFunction(func(_ int, sel *Selection) bool {
+ // Add all nodes that contain one of the specified nodes
+ for _, n := range nodes {
+ if sel.Contains(n) {
+ return true
+ }
+ }
+ return false
+ })
+}
+
+// HasSelection reduces the set of matched elements to those that have a
+// descendant that matches one of the nodes of the specified Selection object.
+// It returns a new Selection object with the matching elements.
+func (s *Selection) HasSelection(sel *Selection) *Selection {
+ if sel == nil {
+ return s.HasNodes()
+ }
+ return s.HasNodes(sel.Nodes...)
+}
+
+// End ends the most recent filtering operation in the current chain and
+// returns the set of matched elements to its previous state.
+func (s *Selection) End() *Selection {
+ if s.prevSel != nil {
+ return s.prevSel
+ }
+ return newEmptySelection(s.document)
+}
+
+// Filter based on the matcher, and the indicator to keep (Filter) or
+// to get rid of (Not) the matching elements.
+func winnow(sel *Selection, m Matcher, keep bool) []*html.Node {
+ // Optimize if keep is requested
+ if keep {
+ return m.Filter(sel.Nodes)
+ }
+ // Use grep
+ return grep(sel, func(i int, s *Selection) bool {
+ return !m.Match(s.Get(0))
+ })
+}
+
+// Filter based on an array of nodes, and the indicator to keep (Filter) or
+// to get rid of (Not) the matching elements.
+func winnowNodes(sel *Selection, nodes []*html.Node, keep bool) []*html.Node {
+ if len(nodes)+len(sel.Nodes) < minNodesForSet {
+ return grep(sel, func(i int, s *Selection) bool {
+ return isInSlice(nodes, s.Get(0)) == keep
+ })
+ }
+
+ set := make(map[*html.Node]bool)
+ for _, n := range nodes {
+ set[n] = true
+ }
+ return grep(sel, func(i int, s *Selection) bool {
+ return set[s.Get(0)] == keep
+ })
+}
+
+// Filter based on a function test, and the indicator to keep (Filter) or
+// to get rid of (Not) the matching elements.
+func winnowFunction(sel *Selection, f func(int, *Selection) bool, keep bool) []*html.Node {
+ return grep(sel, func(i int, s *Selection) bool {
+ return f(i, s) == keep
+ })
+}
diff --git a/vendor/github.com/PuerkitoBio/goquery/go.mod b/vendor/github.com/PuerkitoBio/goquery/go.mod
new file mode 100644
index 00000000..95826ad3
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/go.mod
@@ -0,0 +1,8 @@
+module github.com/PuerkitoBio/goquery
+
+require (
+ github.com/andybalholm/cascadia v1.1.0
+ golang.org/x/net v0.0.0-20200202094626-16171245cfb2
+)
+
+go 1.13
diff --git a/vendor/github.com/PuerkitoBio/goquery/go.sum b/vendor/github.com/PuerkitoBio/goquery/go.sum
new file mode 100644
index 00000000..bc79107e
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/go.sum
@@ -0,0 +1,8 @@
+github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
+github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
diff --git a/vendor/github.com/PuerkitoBio/goquery/iteration.go b/vendor/github.com/PuerkitoBio/goquery/iteration.go
new file mode 100644
index 00000000..e246f2e0
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/iteration.go
@@ -0,0 +1,39 @@
+package goquery
+
+// Each iterates over a Selection object, executing a function for each
+// matched element. It returns the current Selection object. The function
+// f is called for each element in the selection with the index of the
+// element in that selection starting at 0, and a *Selection that contains
+// only that element.
+func (s *Selection) Each(f func(int, *Selection)) *Selection {
+ for i, n := range s.Nodes {
+ f(i, newSingleSelection(n, s.document))
+ }
+ return s
+}
+
+// EachWithBreak iterates over a Selection object, executing a function for each
+// matched element. It is identical to Each except that it is possible to break
+// out of the loop by returning false in the callback function. It returns the
+// current Selection object.
+func (s *Selection) EachWithBreak(f func(int, *Selection) bool) *Selection {
+ for i, n := range s.Nodes {
+ if !f(i, newSingleSelection(n, s.document)) {
+ return s
+ }
+ }
+ return s
+}
+
+// Map passes each element in the current matched set through a function,
+// producing a slice of string holding the returned values. The function
+// f is called for each element in the selection with the index of the
+// element in that selection starting at 0, and a *Selection that contains
+// only that element.
+func (s *Selection) Map(f func(int, *Selection) string) (result []string) {
+ for i, n := range s.Nodes {
+ result = append(result, f(i, newSingleSelection(n, s.document)))
+ }
+
+ return result
+}
diff --git a/vendor/github.com/PuerkitoBio/goquery/manipulation.go b/vendor/github.com/PuerkitoBio/goquery/manipulation.go
new file mode 100644
index 00000000..34eb7570
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/manipulation.go
@@ -0,0 +1,574 @@
+package goquery
+
+import (
+ "strings"
+
+ "golang.org/x/net/html"
+)
+
+// After applies the selector from the root document and inserts the matched elements
+// after the elements in the set of matched elements.
+//
+// If one of the matched elements in the selection is not currently in the
+// document, it's impossible to insert nodes after it, so it will be ignored.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) After(selector string) *Selection {
+ return s.AfterMatcher(compileMatcher(selector))
+}
+
+// AfterMatcher applies the matcher from the root document and inserts the matched elements
+// after the elements in the set of matched elements.
+//
+// If one of the matched elements in the selection is not currently in the
+// document, it's impossible to insert nodes after it, so it will be ignored.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) AfterMatcher(m Matcher) *Selection {
+ return s.AfterNodes(m.MatchAll(s.document.rootNode)...)
+}
+
+// AfterSelection inserts the elements in the selection after each element in the set of matched
+// elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) AfterSelection(sel *Selection) *Selection {
+ return s.AfterNodes(sel.Nodes...)
+}
+
+// AfterHtml parses the html and inserts it after the set of matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) AfterHtml(html string) *Selection {
+ return s.AfterNodes(parseHtml(html)...)
+}
+
+// AfterNodes inserts the nodes after each element in the set of matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) AfterNodes(ns ...*html.Node) *Selection {
+ return s.manipulateNodes(ns, true, func(sn *html.Node, n *html.Node) {
+ if sn.Parent != nil {
+ sn.Parent.InsertBefore(n, sn.NextSibling)
+ }
+ })
+}
+
+// Append appends the elements specified by the selector to the end of each element
+// in the set of matched elements, following those rules:
+//
+// 1) The selector is applied to the root document.
+//
+// 2) Elements that are part of the document will be moved to the new location.
+//
+// 3) If there are multiple locations to append to, cloned nodes will be
+// appended to all target locations except the last one, which will be moved
+// as noted in (2).
+func (s *Selection) Append(selector string) *Selection {
+ return s.AppendMatcher(compileMatcher(selector))
+}
+
+// AppendMatcher appends the elements specified by the matcher to the end of each element
+// in the set of matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) AppendMatcher(m Matcher) *Selection {
+ return s.AppendNodes(m.MatchAll(s.document.rootNode)...)
+}
+
+// AppendSelection appends the elements in the selection to the end of each element
+// in the set of matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) AppendSelection(sel *Selection) *Selection {
+ return s.AppendNodes(sel.Nodes...)
+}
+
+// AppendHtml parses the html and appends it to the set of matched elements.
+func (s *Selection) AppendHtml(html string) *Selection {
+ return s.AppendNodes(parseHtml(html)...)
+}
+
+// AppendNodes appends the specified nodes to each node in the set of matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) AppendNodes(ns ...*html.Node) *Selection {
+ return s.manipulateNodes(ns, false, func(sn *html.Node, n *html.Node) {
+ sn.AppendChild(n)
+ })
+}
+
+// Before inserts the matched elements before each element in the set of matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) Before(selector string) *Selection {
+ return s.BeforeMatcher(compileMatcher(selector))
+}
+
+// BeforeMatcher inserts the matched elements before each element in the set of matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) BeforeMatcher(m Matcher) *Selection {
+ return s.BeforeNodes(m.MatchAll(s.document.rootNode)...)
+}
+
+// BeforeSelection inserts the elements in the selection before each element in the set of matched
+// elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) BeforeSelection(sel *Selection) *Selection {
+ return s.BeforeNodes(sel.Nodes...)
+}
+
+// BeforeHtml parses the html and inserts it before the set of matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) BeforeHtml(html string) *Selection {
+ return s.BeforeNodes(parseHtml(html)...)
+}
+
+// BeforeNodes inserts the nodes before each element in the set of matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) BeforeNodes(ns ...*html.Node) *Selection {
+ return s.manipulateNodes(ns, false, func(sn *html.Node, n *html.Node) {
+ if sn.Parent != nil {
+ sn.Parent.InsertBefore(n, sn)
+ }
+ })
+}
+
+// Clone creates a deep copy of the set of matched nodes. The new nodes will not be
+// attached to the document.
+func (s *Selection) Clone() *Selection {
+ ns := newEmptySelection(s.document)
+ ns.Nodes = cloneNodes(s.Nodes)
+ return ns
+}
+
+// Empty removes all children nodes from the set of matched elements.
+// It returns the children nodes in a new Selection.
+func (s *Selection) Empty() *Selection {
+ var nodes []*html.Node
+
+ for _, n := range s.Nodes {
+ for c := n.FirstChild; c != nil; c = n.FirstChild {
+ n.RemoveChild(c)
+ nodes = append(nodes, c)
+ }
+ }
+
+ return pushStack(s, nodes)
+}
+
+// Prepend prepends the elements specified by the selector to each element in
+// the set of matched elements, following the same rules as Append.
+func (s *Selection) Prepend(selector string) *Selection {
+ return s.PrependMatcher(compileMatcher(selector))
+}
+
+// PrependMatcher prepends the elements specified by the matcher to each
+// element in the set of matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) PrependMatcher(m Matcher) *Selection {
+ return s.PrependNodes(m.MatchAll(s.document.rootNode)...)
+}
+
+// PrependSelection prepends the elements in the selection to each element in
+// the set of matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) PrependSelection(sel *Selection) *Selection {
+ return s.PrependNodes(sel.Nodes...)
+}
+
+// PrependHtml parses the html and prepends it to the set of matched elements.
+func (s *Selection) PrependHtml(html string) *Selection {
+ return s.PrependNodes(parseHtml(html)...)
+}
+
+// PrependNodes prepends the specified nodes to each node in the set of
+// matched elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) PrependNodes(ns ...*html.Node) *Selection {
+ return s.manipulateNodes(ns, true, func(sn *html.Node, n *html.Node) {
+ // sn.FirstChild may be nil, in which case this functions like
+ // sn.AppendChild()
+ sn.InsertBefore(n, sn.FirstChild)
+ })
+}
+
+// Remove removes the set of matched elements from the document.
+// It returns the same selection, now consisting of nodes not in the document.
+func (s *Selection) Remove() *Selection {
+ for _, n := range s.Nodes {
+ if n.Parent != nil {
+ n.Parent.RemoveChild(n)
+ }
+ }
+
+ return s
+}
+
+// RemoveFiltered removes the set of matched elements by selector.
+// It returns the Selection of removed nodes.
+func (s *Selection) RemoveFiltered(selector string) *Selection {
+ return s.RemoveMatcher(compileMatcher(selector))
+}
+
+// RemoveMatcher removes the set of matched elements.
+// It returns the Selection of removed nodes.
+func (s *Selection) RemoveMatcher(m Matcher) *Selection {
+ return s.FilterMatcher(m).Remove()
+}
+
+// ReplaceWith replaces each element in the set of matched elements with the
+// nodes matched by the given selector.
+// It returns the removed elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) ReplaceWith(selector string) *Selection {
+ return s.ReplaceWithMatcher(compileMatcher(selector))
+}
+
+// ReplaceWithMatcher replaces each element in the set of matched elements with
+// the nodes matched by the given Matcher.
+// It returns the removed elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) ReplaceWithMatcher(m Matcher) *Selection {
+ return s.ReplaceWithNodes(m.MatchAll(s.document.rootNode)...)
+}
+
+// ReplaceWithSelection replaces each element in the set of matched elements with
+// the nodes from the given Selection.
+// It returns the removed elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) ReplaceWithSelection(sel *Selection) *Selection {
+ return s.ReplaceWithNodes(sel.Nodes...)
+}
+
+// ReplaceWithHtml replaces each element in the set of matched elements with
+// the parsed HTML.
+// It returns the removed elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) ReplaceWithHtml(html string) *Selection {
+ return s.ReplaceWithNodes(parseHtml(html)...)
+}
+
+// ReplaceWithNodes replaces each element in the set of matched elements with
+// the given nodes.
+// It returns the removed elements.
+//
+// This follows the same rules as Selection.Append.
+func (s *Selection) ReplaceWithNodes(ns ...*html.Node) *Selection {
+ s.AfterNodes(ns...)
+ return s.Remove()
+}
+
+// SetHtml sets the html content of each element in the selection to
+// specified html string.
+func (s *Selection) SetHtml(html string) *Selection {
+ return setHtmlNodes(s, parseHtml(html)...)
+}
+
+// SetText sets the content of each element in the selection to specified content.
+// The provided text string is escaped.
+func (s *Selection) SetText(text string) *Selection {
+ return s.SetHtml(html.EscapeString(text))
+}
+
+// Unwrap removes the parents of the set of matched elements, leaving the matched
+// elements (and their siblings, if any) in their place.
+// It returns the original selection.
+func (s *Selection) Unwrap() *Selection {
+ s.Parent().Each(func(i int, ss *Selection) {
+ // For some reason, jquery allows unwrap to remove the element, so
+ // allowing it here too. Same for . Why it allows those elements to
+ // be unwrapped while not allowing body is a mystery to me.
+ if ss.Nodes[0].Data != "body" {
+ ss.ReplaceWithSelection(ss.Contents())
+ }
+ })
+
+ return s
+}
+
+// Wrap wraps each element in the set of matched elements inside the first
+// element matched by the given selector. The matched child is cloned before
+// being inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) Wrap(selector string) *Selection {
+ return s.WrapMatcher(compileMatcher(selector))
+}
+
+// WrapMatcher wraps each element in the set of matched elements inside the
+// first element matched by the given matcher. The matched child is cloned
+// before being inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapMatcher(m Matcher) *Selection {
+ return s.wrapNodes(m.MatchAll(s.document.rootNode)...)
+}
+
+// WrapSelection wraps each element in the set of matched elements inside the
+// first element in the given Selection. The element is cloned before being
+// inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapSelection(sel *Selection) *Selection {
+ return s.wrapNodes(sel.Nodes...)
+}
+
+// WrapHtml wraps each element in the set of matched elements inside the inner-
+// most child of the given HTML.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapHtml(html string) *Selection {
+ return s.wrapNodes(parseHtml(html)...)
+}
+
+// WrapNode wraps each element in the set of matched elements inside the inner-
+// most child of the given node. The given node is copied before being inserted
+// into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapNode(n *html.Node) *Selection {
+ return s.wrapNodes(n)
+}
+
+func (s *Selection) wrapNodes(ns ...*html.Node) *Selection {
+ s.Each(func(i int, ss *Selection) {
+ ss.wrapAllNodes(ns...)
+ })
+
+ return s
+}
+
+// WrapAll wraps a single HTML structure, matched by the given selector, around
+// all elements in the set of matched elements. The matched child is cloned
+// before being inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapAll(selector string) *Selection {
+ return s.WrapAllMatcher(compileMatcher(selector))
+}
+
+// WrapAllMatcher wraps a single HTML structure, matched by the given Matcher,
+// around all elements in the set of matched elements. The matched child is
+// cloned before being inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapAllMatcher(m Matcher) *Selection {
+ return s.wrapAllNodes(m.MatchAll(s.document.rootNode)...)
+}
+
+// WrapAllSelection wraps a single HTML structure, the first node of the given
+// Selection, around all elements in the set of matched elements. The matched
+// child is cloned before being inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapAllSelection(sel *Selection) *Selection {
+ return s.wrapAllNodes(sel.Nodes...)
+}
+
+// WrapAllHtml wraps the given HTML structure around all elements in the set of
+// matched elements. The matched child is cloned before being inserted into the
+// document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapAllHtml(html string) *Selection {
+ return s.wrapAllNodes(parseHtml(html)...)
+}
+
+func (s *Selection) wrapAllNodes(ns ...*html.Node) *Selection {
+ if len(ns) > 0 {
+ return s.WrapAllNode(ns[0])
+ }
+ return s
+}
+
+// WrapAllNode wraps the given node around the first element in the Selection,
+// making all other nodes in the Selection children of the given node. The node
+// is cloned before being inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapAllNode(n *html.Node) *Selection {
+ if s.Size() == 0 {
+ return s
+ }
+
+ wrap := cloneNode(n)
+
+ first := s.Nodes[0]
+ if first.Parent != nil {
+ first.Parent.InsertBefore(wrap, first)
+ first.Parent.RemoveChild(first)
+ }
+
+ for c := getFirstChildEl(wrap); c != nil; c = getFirstChildEl(wrap) {
+ wrap = c
+ }
+
+ newSingleSelection(wrap, s.document).AppendSelection(s)
+
+ return s
+}
+
+// WrapInner wraps an HTML structure, matched by the given selector, around the
+// content of element in the set of matched elements. The matched child is
+// cloned before being inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapInner(selector string) *Selection {
+ return s.WrapInnerMatcher(compileMatcher(selector))
+}
+
+// WrapInnerMatcher wraps an HTML structure, matched by the given selector,
+// around the content of element in the set of matched elements. The matched
+// child is cloned before being inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapInnerMatcher(m Matcher) *Selection {
+ return s.wrapInnerNodes(m.MatchAll(s.document.rootNode)...)
+}
+
+// WrapInnerSelection wraps an HTML structure, matched by the given selector,
+// around the content of element in the set of matched elements. The matched
+// child is cloned before being inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapInnerSelection(sel *Selection) *Selection {
+ return s.wrapInnerNodes(sel.Nodes...)
+}
+
+// WrapInnerHtml wraps an HTML structure, matched by the given selector, around
+// the content of element in the set of matched elements. The matched child is
+// cloned before being inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapInnerHtml(html string) *Selection {
+ return s.wrapInnerNodes(parseHtml(html)...)
+}
+
+// WrapInnerNode wraps an HTML structure, matched by the given selector, around
+// the content of element in the set of matched elements. The matched child is
+// cloned before being inserted into the document.
+//
+// It returns the original set of elements.
+func (s *Selection) WrapInnerNode(n *html.Node) *Selection {
+ return s.wrapInnerNodes(n)
+}
+
+func (s *Selection) wrapInnerNodes(ns ...*html.Node) *Selection {
+ if len(ns) == 0 {
+ return s
+ }
+
+ s.Each(func(i int, s *Selection) {
+ contents := s.Contents()
+
+ if contents.Size() > 0 {
+ contents.wrapAllNodes(ns...)
+ } else {
+ s.AppendNodes(cloneNode(ns[0]))
+ }
+ })
+
+ return s
+}
+
+func parseHtml(h string) []*html.Node {
+ // Errors are only returned when the io.Reader returns any error besides
+ // EOF, but strings.Reader never will
+ nodes, err := html.ParseFragment(strings.NewReader(h), &html.Node{Type: html.ElementNode})
+ if err != nil {
+ panic("goquery: failed to parse HTML: " + err.Error())
+ }
+ return nodes
+}
+
+func setHtmlNodes(s *Selection, ns ...*html.Node) *Selection {
+ for _, n := range s.Nodes {
+ for c := n.FirstChild; c != nil; c = n.FirstChild {
+ n.RemoveChild(c)
+ }
+ for _, c := range ns {
+ n.AppendChild(cloneNode(c))
+ }
+ }
+ return s
+}
+
+// Get the first child that is an ElementNode
+func getFirstChildEl(n *html.Node) *html.Node {
+ c := n.FirstChild
+ for c != nil && c.Type != html.ElementNode {
+ c = c.NextSibling
+ }
+ return c
+}
+
+// Deep copy a slice of nodes.
+func cloneNodes(ns []*html.Node) []*html.Node {
+ cns := make([]*html.Node, 0, len(ns))
+
+ for _, n := range ns {
+ cns = append(cns, cloneNode(n))
+ }
+
+ return cns
+}
+
+// Deep copy a node. The new node has clones of all the original node's
+// children but none of its parents or siblings.
+func cloneNode(n *html.Node) *html.Node {
+ nn := &html.Node{
+ Type: n.Type,
+ DataAtom: n.DataAtom,
+ Data: n.Data,
+ Attr: make([]html.Attribute, len(n.Attr)),
+ }
+
+ copy(nn.Attr, n.Attr)
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ nn.AppendChild(cloneNode(c))
+ }
+
+ return nn
+}
+
+func (s *Selection) manipulateNodes(ns []*html.Node, reverse bool,
+ f func(sn *html.Node, n *html.Node)) *Selection {
+
+ lasti := s.Size() - 1
+
+ // net.Html doesn't provide document fragments for insertion, so to get
+ // things in the correct order with After() and Prepend(), the callback
+ // needs to be called on the reverse of the nodes.
+ if reverse {
+ for i, j := 0, len(ns)-1; i < j; i, j = i+1, j-1 {
+ ns[i], ns[j] = ns[j], ns[i]
+ }
+ }
+
+ for i, sn := range s.Nodes {
+ for _, n := range ns {
+ if i != lasti {
+ f(sn, cloneNode(n))
+ } else {
+ if n.Parent != nil {
+ n.Parent.RemoveChild(n)
+ }
+ f(sn, n)
+ }
+ }
+ }
+
+ return s
+}
diff --git a/vendor/github.com/PuerkitoBio/goquery/property.go b/vendor/github.com/PuerkitoBio/goquery/property.go
new file mode 100644
index 00000000..411126db
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/property.go
@@ -0,0 +1,275 @@
+package goquery
+
+import (
+ "bytes"
+ "regexp"
+ "strings"
+
+ "golang.org/x/net/html"
+)
+
+var rxClassTrim = regexp.MustCompile("[\t\r\n]")
+
+// Attr gets the specified attribute's value for the first element in the
+// Selection. To get the value for each element individually, use a looping
+// construct such as Each or Map method.
+func (s *Selection) Attr(attrName string) (val string, exists bool) {
+ if len(s.Nodes) == 0 {
+ return
+ }
+ return getAttributeValue(attrName, s.Nodes[0])
+}
+
+// AttrOr works like Attr but returns default value if attribute is not present.
+func (s *Selection) AttrOr(attrName, defaultValue string) string {
+ if len(s.Nodes) == 0 {
+ return defaultValue
+ }
+
+ val, exists := getAttributeValue(attrName, s.Nodes[0])
+ if !exists {
+ return defaultValue
+ }
+
+ return val
+}
+
+// RemoveAttr removes the named attribute from each element in the set of matched elements.
+func (s *Selection) RemoveAttr(attrName string) *Selection {
+ for _, n := range s.Nodes {
+ removeAttr(n, attrName)
+ }
+
+ return s
+}
+
+// SetAttr sets the given attribute on each element in the set of matched elements.
+func (s *Selection) SetAttr(attrName, val string) *Selection {
+ for _, n := range s.Nodes {
+ attr := getAttributePtr(attrName, n)
+ if attr == nil {
+ n.Attr = append(n.Attr, html.Attribute{Key: attrName, Val: val})
+ } else {
+ attr.Val = val
+ }
+ }
+
+ return s
+}
+
+// Text gets the combined text contents of each element in the set of matched
+// elements, including their descendants.
+func (s *Selection) Text() string {
+ var buf bytes.Buffer
+
+ // Slightly optimized vs calling Each: no single selection object created
+ var f func(*html.Node)
+ f = func(n *html.Node) {
+ if n.Type == html.TextNode {
+ // Keep newlines and spaces, like jQuery
+ buf.WriteString(n.Data)
+ }
+ if n.FirstChild != nil {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ f(c)
+ }
+ }
+ }
+ for _, n := range s.Nodes {
+ f(n)
+ }
+
+ return buf.String()
+}
+
+// Size is an alias for Length.
+func (s *Selection) Size() int {
+ return s.Length()
+}
+
+// Length returns the number of elements in the Selection object.
+func (s *Selection) Length() int {
+ return len(s.Nodes)
+}
+
+// Html gets the HTML contents of the first element in the set of matched
+// elements. It includes text and comment nodes.
+func (s *Selection) Html() (ret string, e error) {
+ // Since there is no .innerHtml, the HTML content must be re-created from
+ // the nodes using html.Render.
+ var buf bytes.Buffer
+
+ if len(s.Nodes) > 0 {
+ for c := s.Nodes[0].FirstChild; c != nil; c = c.NextSibling {
+ e = html.Render(&buf, c)
+ if e != nil {
+ return
+ }
+ }
+ ret = buf.String()
+ }
+
+ return
+}
+
+// AddClass adds the given class(es) to each element in the set of matched elements.
+// Multiple class names can be specified, separated by a space or via multiple arguments.
+func (s *Selection) AddClass(class ...string) *Selection {
+ classStr := strings.TrimSpace(strings.Join(class, " "))
+
+ if classStr == "" {
+ return s
+ }
+
+ tcls := getClassesSlice(classStr)
+ for _, n := range s.Nodes {
+ curClasses, attr := getClassesAndAttr(n, true)
+ for _, newClass := range tcls {
+ if !strings.Contains(curClasses, " "+newClass+" ") {
+ curClasses += newClass + " "
+ }
+ }
+
+ setClasses(n, attr, curClasses)
+ }
+
+ return s
+}
+
+// HasClass determines whether any of the matched elements are assigned the
+// given class.
+func (s *Selection) HasClass(class string) bool {
+ class = " " + class + " "
+ for _, n := range s.Nodes {
+ classes, _ := getClassesAndAttr(n, false)
+ if strings.Contains(classes, class) {
+ return true
+ }
+ }
+ return false
+}
+
+// RemoveClass removes the given class(es) from each element in the set of matched elements.
+// Multiple class names can be specified, separated by a space or via multiple arguments.
+// If no class name is provided, all classes are removed.
+func (s *Selection) RemoveClass(class ...string) *Selection {
+ var rclasses []string
+
+ classStr := strings.TrimSpace(strings.Join(class, " "))
+ remove := classStr == ""
+
+ if !remove {
+ rclasses = getClassesSlice(classStr)
+ }
+
+ for _, n := range s.Nodes {
+ if remove {
+ removeAttr(n, "class")
+ } else {
+ classes, attr := getClassesAndAttr(n, true)
+ for _, rcl := range rclasses {
+ classes = strings.Replace(classes, " "+rcl+" ", " ", -1)
+ }
+
+ setClasses(n, attr, classes)
+ }
+ }
+
+ return s
+}
+
+// ToggleClass adds or removes the given class(es) for each element in the set of matched elements.
+// Multiple class names can be specified, separated by a space or via multiple arguments.
+func (s *Selection) ToggleClass(class ...string) *Selection {
+ classStr := strings.TrimSpace(strings.Join(class, " "))
+
+ if classStr == "" {
+ return s
+ }
+
+ tcls := getClassesSlice(classStr)
+
+ for _, n := range s.Nodes {
+ classes, attr := getClassesAndAttr(n, true)
+ for _, tcl := range tcls {
+ if strings.Contains(classes, " "+tcl+" ") {
+ classes = strings.Replace(classes, " "+tcl+" ", " ", -1)
+ } else {
+ classes += tcl + " "
+ }
+ }
+
+ setClasses(n, attr, classes)
+ }
+
+ return s
+}
+
+func getAttributePtr(attrName string, n *html.Node) *html.Attribute {
+ if n == nil {
+ return nil
+ }
+
+ for i, a := range n.Attr {
+ if a.Key == attrName {
+ return &n.Attr[i]
+ }
+ }
+ return nil
+}
+
+// Private function to get the specified attribute's value from a node.
+func getAttributeValue(attrName string, n *html.Node) (val string, exists bool) {
+ if a := getAttributePtr(attrName, n); a != nil {
+ val = a.Val
+ exists = true
+ }
+ return
+}
+
+// Get and normalize the "class" attribute from the node.
+func getClassesAndAttr(n *html.Node, create bool) (classes string, attr *html.Attribute) {
+ // Applies only to element nodes
+ if n.Type == html.ElementNode {
+ attr = getAttributePtr("class", n)
+ if attr == nil && create {
+ n.Attr = append(n.Attr, html.Attribute{
+ Key: "class",
+ Val: "",
+ })
+ attr = &n.Attr[len(n.Attr)-1]
+ }
+ }
+
+ if attr == nil {
+ classes = " "
+ } else {
+ classes = rxClassTrim.ReplaceAllString(" "+attr.Val+" ", " ")
+ }
+
+ return
+}
+
+func getClassesSlice(classes string) []string {
+ return strings.Split(rxClassTrim.ReplaceAllString(" "+classes+" ", " "), " ")
+}
+
+func removeAttr(n *html.Node, attrName string) {
+ for i, a := range n.Attr {
+ if a.Key == attrName {
+ n.Attr[i], n.Attr[len(n.Attr)-1], n.Attr =
+ n.Attr[len(n.Attr)-1], html.Attribute{}, n.Attr[:len(n.Attr)-1]
+ return
+ }
+ }
+}
+
+func setClasses(n *html.Node, attr *html.Attribute, classes string) {
+ classes = strings.TrimSpace(classes)
+ if classes == "" {
+ removeAttr(n, "class")
+ return
+ }
+
+ attr.Val = classes
+}
diff --git a/vendor/github.com/PuerkitoBio/goquery/query.go b/vendor/github.com/PuerkitoBio/goquery/query.go
new file mode 100644
index 00000000..fe86bf0b
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/query.go
@@ -0,0 +1,49 @@
+package goquery
+
+import "golang.org/x/net/html"
+
+// Is checks the current matched set of elements against a selector and
+// returns true if at least one of these elements matches.
+func (s *Selection) Is(selector string) bool {
+ return s.IsMatcher(compileMatcher(selector))
+}
+
+// IsMatcher checks the current matched set of elements against a matcher and
+// returns true if at least one of these elements matches.
+func (s *Selection) IsMatcher(m Matcher) bool {
+ if len(s.Nodes) > 0 {
+ if len(s.Nodes) == 1 {
+ return m.Match(s.Nodes[0])
+ }
+ return len(m.Filter(s.Nodes)) > 0
+ }
+
+ return false
+}
+
+// IsFunction checks the current matched set of elements against a predicate and
+// returns true if at least one of these elements matches.
+func (s *Selection) IsFunction(f func(int, *Selection) bool) bool {
+ return s.FilterFunction(f).Length() > 0
+}
+
+// IsSelection checks the current matched set of elements against a Selection object
+// and returns true if at least one of these elements matches.
+func (s *Selection) IsSelection(sel *Selection) bool {
+ return s.FilterSelection(sel).Length() > 0
+}
+
+// IsNodes checks the current matched set of elements against the specified nodes
+// and returns true if at least one of these elements matches.
+func (s *Selection) IsNodes(nodes ...*html.Node) bool {
+ return s.FilterNodes(nodes...).Length() > 0
+}
+
+// Contains returns true if the specified Node is within,
+// at any depth, one of the nodes in the Selection object.
+// It is NOT inclusive, to behave like jQuery's implementation, and
+// unlike Javascript's .contains, so if the contained
+// node is itself in the selection, it returns false.
+func (s *Selection) Contains(n *html.Node) bool {
+ return sliceContains(s.Nodes, n)
+}
diff --git a/vendor/github.com/PuerkitoBio/goquery/traversal.go b/vendor/github.com/PuerkitoBio/goquery/traversal.go
new file mode 100644
index 00000000..5fa5315a
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/traversal.go
@@ -0,0 +1,698 @@
+package goquery
+
+import "golang.org/x/net/html"
+
+type siblingType int
+
+// Sibling type, used internally when iterating over children at the same
+// level (siblings) to specify which nodes are requested.
+const (
+ siblingPrevUntil siblingType = iota - 3
+ siblingPrevAll
+ siblingPrev
+ siblingAll
+ siblingNext
+ siblingNextAll
+ siblingNextUntil
+ siblingAllIncludingNonElements
+)
+
+// Find gets the descendants of each element in the current set of matched
+// elements, filtered by a selector. It returns a new Selection object
+// containing these matched elements.
+func (s *Selection) Find(selector string) *Selection {
+ return pushStack(s, findWithMatcher(s.Nodes, compileMatcher(selector)))
+}
+
+// FindMatcher gets the descendants of each element in the current set of matched
+// elements, filtered by the matcher. It returns a new Selection object
+// containing these matched elements.
+func (s *Selection) FindMatcher(m Matcher) *Selection {
+ return pushStack(s, findWithMatcher(s.Nodes, m))
+}
+
+// FindSelection gets the descendants of each element in the current
+// Selection, filtered by a Selection. It returns a new Selection object
+// containing these matched elements.
+func (s *Selection) FindSelection(sel *Selection) *Selection {
+ if sel == nil {
+ return pushStack(s, nil)
+ }
+ return s.FindNodes(sel.Nodes...)
+}
+
+// FindNodes gets the descendants of each element in the current
+// Selection, filtered by some nodes. It returns a new Selection object
+// containing these matched elements.
+func (s *Selection) FindNodes(nodes ...*html.Node) *Selection {
+ return pushStack(s, mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
+ if sliceContains(s.Nodes, n) {
+ return []*html.Node{n}
+ }
+ return nil
+ }))
+}
+
+// Contents gets the children of each element in the Selection,
+// including text and comment nodes. It returns a new Selection object
+// containing these elements.
+func (s *Selection) Contents() *Selection {
+ return pushStack(s, getChildrenNodes(s.Nodes, siblingAllIncludingNonElements))
+}
+
+// ContentsFiltered gets the children of each element in the Selection,
+// filtered by the specified selector. It returns a new Selection
+// object containing these elements. Since selectors only act on Element nodes,
+// this function is an alias to ChildrenFiltered unless the selector is empty,
+// in which case it is an alias to Contents.
+func (s *Selection) ContentsFiltered(selector string) *Selection {
+ if selector != "" {
+ return s.ChildrenFiltered(selector)
+ }
+ return s.Contents()
+}
+
+// ContentsMatcher gets the children of each element in the Selection,
+// filtered by the specified matcher. It returns a new Selection
+// object containing these elements. Since matchers only act on Element nodes,
+// this function is an alias to ChildrenMatcher.
+func (s *Selection) ContentsMatcher(m Matcher) *Selection {
+ return s.ChildrenMatcher(m)
+}
+
+// Children gets the child elements of each element in the Selection.
+// It returns a new Selection object containing these elements.
+func (s *Selection) Children() *Selection {
+ return pushStack(s, getChildrenNodes(s.Nodes, siblingAll))
+}
+
+// ChildrenFiltered gets the child elements of each element in the Selection,
+// filtered by the specified selector. It returns a new
+// Selection object containing these elements.
+func (s *Selection) ChildrenFiltered(selector string) *Selection {
+ return filterAndPush(s, getChildrenNodes(s.Nodes, siblingAll), compileMatcher(selector))
+}
+
+// ChildrenMatcher gets the child elements of each element in the Selection,
+// filtered by the specified matcher. It returns a new
+// Selection object containing these elements.
+func (s *Selection) ChildrenMatcher(m Matcher) *Selection {
+ return filterAndPush(s, getChildrenNodes(s.Nodes, siblingAll), m)
+}
+
+// Parent gets the parent of each element in the Selection. It returns a
+// new Selection object containing the matched elements.
+func (s *Selection) Parent() *Selection {
+ return pushStack(s, getParentNodes(s.Nodes))
+}
+
+// ParentFiltered gets the parent of each element in the Selection filtered by a
+// selector. It returns a new Selection object containing the matched elements.
+func (s *Selection) ParentFiltered(selector string) *Selection {
+ return filterAndPush(s, getParentNodes(s.Nodes), compileMatcher(selector))
+}
+
+// ParentMatcher gets the parent of each element in the Selection filtered by a
+// matcher. It returns a new Selection object containing the matched elements.
+func (s *Selection) ParentMatcher(m Matcher) *Selection {
+ return filterAndPush(s, getParentNodes(s.Nodes), m)
+}
+
+// Closest gets the first element that matches the selector by testing the
+// element itself and traversing up through its ancestors in the DOM tree.
+func (s *Selection) Closest(selector string) *Selection {
+ cs := compileMatcher(selector)
+ return s.ClosestMatcher(cs)
+}
+
+// ClosestMatcher gets the first element that matches the matcher by testing the
+// element itself and traversing up through its ancestors in the DOM tree.
+func (s *Selection) ClosestMatcher(m Matcher) *Selection {
+ return pushStack(s, mapNodes(s.Nodes, func(i int, n *html.Node) []*html.Node {
+ // For each node in the selection, test the node itself, then each parent
+ // until a match is found.
+ for ; n != nil; n = n.Parent {
+ if m.Match(n) {
+ return []*html.Node{n}
+ }
+ }
+ return nil
+ }))
+}
+
+// ClosestNodes gets the first element that matches one of the nodes by testing the
+// element itself and traversing up through its ancestors in the DOM tree.
+func (s *Selection) ClosestNodes(nodes ...*html.Node) *Selection {
+ set := make(map[*html.Node]bool)
+ for _, n := range nodes {
+ set[n] = true
+ }
+ return pushStack(s, mapNodes(s.Nodes, func(i int, n *html.Node) []*html.Node {
+ // For each node in the selection, test the node itself, then each parent
+ // until a match is found.
+ for ; n != nil; n = n.Parent {
+ if set[n] {
+ return []*html.Node{n}
+ }
+ }
+ return nil
+ }))
+}
+
+// ClosestSelection gets the first element that matches one of the nodes in the
+// Selection by testing the element itself and traversing up through its ancestors
+// in the DOM tree.
+func (s *Selection) ClosestSelection(sel *Selection) *Selection {
+ if sel == nil {
+ return pushStack(s, nil)
+ }
+ return s.ClosestNodes(sel.Nodes...)
+}
+
+// Parents gets the ancestors of each element in the current Selection. It
+// returns a new Selection object with the matched elements.
+func (s *Selection) Parents() *Selection {
+ return pushStack(s, getParentsNodes(s.Nodes, nil, nil))
+}
+
+// ParentsFiltered gets the ancestors of each element in the current
+// Selection. It returns a new Selection object with the matched elements.
+func (s *Selection) ParentsFiltered(selector string) *Selection {
+ return filterAndPush(s, getParentsNodes(s.Nodes, nil, nil), compileMatcher(selector))
+}
+
+// ParentsMatcher gets the ancestors of each element in the current
+// Selection. It returns a new Selection object with the matched elements.
+func (s *Selection) ParentsMatcher(m Matcher) *Selection {
+ return filterAndPush(s, getParentsNodes(s.Nodes, nil, nil), m)
+}
+
+// ParentsUntil gets the ancestors of each element in the Selection, up to but
+// not including the element matched by the selector. It returns a new Selection
+// object containing the matched elements.
+func (s *Selection) ParentsUntil(selector string) *Selection {
+ return pushStack(s, getParentsNodes(s.Nodes, compileMatcher(selector), nil))
+}
+
+// ParentsUntilMatcher gets the ancestors of each element in the Selection, up to but
+// not including the element matched by the matcher. It returns a new Selection
+// object containing the matched elements.
+func (s *Selection) ParentsUntilMatcher(m Matcher) *Selection {
+ return pushStack(s, getParentsNodes(s.Nodes, m, nil))
+}
+
+// ParentsUntilSelection gets the ancestors of each element in the Selection,
+// up to but not including the elements in the specified Selection. It returns a
+// new Selection object containing the matched elements.
+func (s *Selection) ParentsUntilSelection(sel *Selection) *Selection {
+ if sel == nil {
+ return s.Parents()
+ }
+ return s.ParentsUntilNodes(sel.Nodes...)
+}
+
+// ParentsUntilNodes gets the ancestors of each element in the Selection,
+// up to but not including the specified nodes. It returns a
+// new Selection object containing the matched elements.
+func (s *Selection) ParentsUntilNodes(nodes ...*html.Node) *Selection {
+ return pushStack(s, getParentsNodes(s.Nodes, nil, nodes))
+}
+
+// ParentsFilteredUntil is like ParentsUntil, with the option to filter the
+// results based on a selector string. It returns a new Selection
+// object containing the matched elements.
+func (s *Selection) ParentsFilteredUntil(filterSelector, untilSelector string) *Selection {
+ return filterAndPush(s, getParentsNodes(s.Nodes, compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
+}
+
+// ParentsFilteredUntilMatcher is like ParentsUntilMatcher, with the option to filter the
+// results based on a matcher. It returns a new Selection object containing the matched elements.
+func (s *Selection) ParentsFilteredUntilMatcher(filter, until Matcher) *Selection {
+ return filterAndPush(s, getParentsNodes(s.Nodes, until, nil), filter)
+}
+
+// ParentsFilteredUntilSelection is like ParentsUntilSelection, with the
+// option to filter the results based on a selector string. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) ParentsFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
+ return s.ParentsMatcherUntilSelection(compileMatcher(filterSelector), sel)
+}
+
+// ParentsMatcherUntilSelection is like ParentsUntilSelection, with the
+// option to filter the results based on a matcher. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) ParentsMatcherUntilSelection(filter Matcher, sel *Selection) *Selection {
+ if sel == nil {
+ return s.ParentsMatcher(filter)
+ }
+ return s.ParentsMatcherUntilNodes(filter, sel.Nodes...)
+}
+
+// ParentsFilteredUntilNodes is like ParentsUntilNodes, with the
+// option to filter the results based on a selector string. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) ParentsFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
+ return filterAndPush(s, getParentsNodes(s.Nodes, nil, nodes), compileMatcher(filterSelector))
+}
+
+// ParentsMatcherUntilNodes is like ParentsUntilNodes, with the
+// option to filter the results based on a matcher. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) ParentsMatcherUntilNodes(filter Matcher, nodes ...*html.Node) *Selection {
+ return filterAndPush(s, getParentsNodes(s.Nodes, nil, nodes), filter)
+}
+
+// Siblings gets the siblings of each element in the Selection. It returns
+// a new Selection object containing the matched elements.
+func (s *Selection) Siblings() *Selection {
+ return pushStack(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil))
+}
+
+// SiblingsFiltered gets the siblings of each element in the Selection
+// filtered by a selector. It returns a new Selection object containing the
+// matched elements.
+func (s *Selection) SiblingsFiltered(selector string) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil), compileMatcher(selector))
+}
+
+// SiblingsMatcher gets the siblings of each element in the Selection
+// filtered by a matcher. It returns a new Selection object containing the
+// matched elements.
+func (s *Selection) SiblingsMatcher(m Matcher) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil), m)
+}
+
+// Next gets the immediately following sibling of each element in the
+// Selection. It returns a new Selection object containing the matched elements.
+func (s *Selection) Next() *Selection {
+ return pushStack(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil))
+}
+
+// NextFiltered gets the immediately following sibling of each element in the
+// Selection filtered by a selector. It returns a new Selection object
+// containing the matched elements.
+func (s *Selection) NextFiltered(selector string) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil), compileMatcher(selector))
+}
+
+// NextMatcher gets the immediately following sibling of each element in the
+// Selection filtered by a matcher. It returns a new Selection object
+// containing the matched elements.
+func (s *Selection) NextMatcher(m Matcher) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil), m)
+}
+
+// NextAll gets all the following siblings of each element in the
+// Selection. It returns a new Selection object containing the matched elements.
+func (s *Selection) NextAll() *Selection {
+ return pushStack(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil))
+}
+
+// NextAllFiltered gets all the following siblings of each element in the
+// Selection filtered by a selector. It returns a new Selection object
+// containing the matched elements.
+func (s *Selection) NextAllFiltered(selector string) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil), compileMatcher(selector))
+}
+
+// NextAllMatcher gets all the following siblings of each element in the
+// Selection filtered by a matcher. It returns a new Selection object
+// containing the matched elements.
+func (s *Selection) NextAllMatcher(m Matcher) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil), m)
+}
+
+// Prev gets the immediately preceding sibling of each element in the
+// Selection. It returns a new Selection object containing the matched elements.
+func (s *Selection) Prev() *Selection {
+ return pushStack(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil))
+}
+
+// PrevFiltered gets the immediately preceding sibling of each element in the
+// Selection filtered by a selector. It returns a new Selection object
+// containing the matched elements.
+func (s *Selection) PrevFiltered(selector string) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil), compileMatcher(selector))
+}
+
+// PrevMatcher gets the immediately preceding sibling of each element in the
+// Selection filtered by a matcher. It returns a new Selection object
+// containing the matched elements.
+func (s *Selection) PrevMatcher(m Matcher) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil), m)
+}
+
+// PrevAll gets all the preceding siblings of each element in the
+// Selection. It returns a new Selection object containing the matched elements.
+func (s *Selection) PrevAll() *Selection {
+ return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil))
+}
+
+// PrevAllFiltered gets all the preceding siblings of each element in the
+// Selection filtered by a selector. It returns a new Selection object
+// containing the matched elements.
+func (s *Selection) PrevAllFiltered(selector string) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil), compileMatcher(selector))
+}
+
+// PrevAllMatcher gets all the preceding siblings of each element in the
+// Selection filtered by a matcher. It returns a new Selection object
+// containing the matched elements.
+func (s *Selection) PrevAllMatcher(m Matcher) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil), m)
+}
+
+// NextUntil gets all following siblings of each element up to but not
+// including the element matched by the selector. It returns a new Selection
+// object containing the matched elements.
+func (s *Selection) NextUntil(selector string) *Selection {
+ return pushStack(s, getSiblingNodes(s.Nodes, siblingNextUntil,
+ compileMatcher(selector), nil))
+}
+
+// NextUntilMatcher gets all following siblings of each element up to but not
+// including the element matched by the matcher. It returns a new Selection
+// object containing the matched elements.
+func (s *Selection) NextUntilMatcher(m Matcher) *Selection {
+ return pushStack(s, getSiblingNodes(s.Nodes, siblingNextUntil,
+ m, nil))
+}
+
+// NextUntilSelection gets all following siblings of each element up to but not
+// including the element matched by the Selection. It returns a new Selection
+// object containing the matched elements.
+func (s *Selection) NextUntilSelection(sel *Selection) *Selection {
+ if sel == nil {
+ return s.NextAll()
+ }
+ return s.NextUntilNodes(sel.Nodes...)
+}
+
+// NextUntilNodes gets all following siblings of each element up to but not
+// including the element matched by the nodes. It returns a new Selection
+// object containing the matched elements.
+func (s *Selection) NextUntilNodes(nodes ...*html.Node) *Selection {
+ return pushStack(s, getSiblingNodes(s.Nodes, siblingNextUntil,
+ nil, nodes))
+}
+
+// PrevUntil gets all preceding siblings of each element up to but not
+// including the element matched by the selector. It returns a new Selection
+// object containing the matched elements.
+func (s *Selection) PrevUntil(selector string) *Selection {
+ return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
+ compileMatcher(selector), nil))
+}
+
+// PrevUntilMatcher gets all preceding siblings of each element up to but not
+// including the element matched by the matcher. It returns a new Selection
+// object containing the matched elements.
+func (s *Selection) PrevUntilMatcher(m Matcher) *Selection {
+ return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
+ m, nil))
+}
+
+// PrevUntilSelection gets all preceding siblings of each element up to but not
+// including the element matched by the Selection. It returns a new Selection
+// object containing the matched elements.
+func (s *Selection) PrevUntilSelection(sel *Selection) *Selection {
+ if sel == nil {
+ return s.PrevAll()
+ }
+ return s.PrevUntilNodes(sel.Nodes...)
+}
+
+// PrevUntilNodes gets all preceding siblings of each element up to but not
+// including the element matched by the nodes. It returns a new Selection
+// object containing the matched elements.
+func (s *Selection) PrevUntilNodes(nodes ...*html.Node) *Selection {
+ return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
+ nil, nodes))
+}
+
+// NextFilteredUntil is like NextUntil, with the option to filter
+// the results based on a selector string.
+// It returns a new Selection object containing the matched elements.
+func (s *Selection) NextFilteredUntil(filterSelector, untilSelector string) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
+ compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
+}
+
+// NextFilteredUntilMatcher is like NextUntilMatcher, with the option to filter
+// the results based on a matcher.
+// It returns a new Selection object containing the matched elements.
+func (s *Selection) NextFilteredUntilMatcher(filter, until Matcher) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
+ until, nil), filter)
+}
+
+// NextFilteredUntilSelection is like NextUntilSelection, with the
+// option to filter the results based on a selector string. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) NextFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
+ return s.NextMatcherUntilSelection(compileMatcher(filterSelector), sel)
+}
+
+// NextMatcherUntilSelection is like NextUntilSelection, with the
+// option to filter the results based on a matcher. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) NextMatcherUntilSelection(filter Matcher, sel *Selection) *Selection {
+ if sel == nil {
+ return s.NextMatcher(filter)
+ }
+ return s.NextMatcherUntilNodes(filter, sel.Nodes...)
+}
+
+// NextFilteredUntilNodes is like NextUntilNodes, with the
+// option to filter the results based on a selector string. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) NextFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
+ nil, nodes), compileMatcher(filterSelector))
+}
+
+// NextMatcherUntilNodes is like NextUntilNodes, with the
+// option to filter the results based on a matcher. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) NextMatcherUntilNodes(filter Matcher, nodes ...*html.Node) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
+ nil, nodes), filter)
+}
+
+// PrevFilteredUntil is like PrevUntil, with the option to filter
+// the results based on a selector string.
+// It returns a new Selection object containing the matched elements.
+func (s *Selection) PrevFilteredUntil(filterSelector, untilSelector string) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
+ compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
+}
+
+// PrevFilteredUntilMatcher is like PrevUntilMatcher, with the option to filter
+// the results based on a matcher.
+// It returns a new Selection object containing the matched elements.
+func (s *Selection) PrevFilteredUntilMatcher(filter, until Matcher) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
+ until, nil), filter)
+}
+
+// PrevFilteredUntilSelection is like PrevUntilSelection, with the
+// option to filter the results based on a selector string. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) PrevFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
+ return s.PrevMatcherUntilSelection(compileMatcher(filterSelector), sel)
+}
+
+// PrevMatcherUntilSelection is like PrevUntilSelection, with the
+// option to filter the results based on a matcher. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) PrevMatcherUntilSelection(filter Matcher, sel *Selection) *Selection {
+ if sel == nil {
+ return s.PrevMatcher(filter)
+ }
+ return s.PrevMatcherUntilNodes(filter, sel.Nodes...)
+}
+
+// PrevFilteredUntilNodes is like PrevUntilNodes, with the
+// option to filter the results based on a selector string. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) PrevFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
+ nil, nodes), compileMatcher(filterSelector))
+}
+
+// PrevMatcherUntilNodes is like PrevUntilNodes, with the
+// option to filter the results based on a matcher. It returns a new
+// Selection object containing the matched elements.
+func (s *Selection) PrevMatcherUntilNodes(filter Matcher, nodes ...*html.Node) *Selection {
+ return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
+ nil, nodes), filter)
+}
+
+// Filter and push filters the nodes based on a matcher, and pushes the results
+// on the stack, with the srcSel as previous selection.
+func filterAndPush(srcSel *Selection, nodes []*html.Node, m Matcher) *Selection {
+ // Create a temporary Selection with the specified nodes to filter using winnow
+ sel := &Selection{nodes, srcSel.document, nil}
+ // Filter based on matcher and push on stack
+ return pushStack(srcSel, winnow(sel, m, true))
+}
+
+// Internal implementation of Find that return raw nodes.
+func findWithMatcher(nodes []*html.Node, m Matcher) []*html.Node {
+ // Map nodes to find the matches within the children of each node
+ return mapNodes(nodes, func(i int, n *html.Node) (result []*html.Node) {
+ // Go down one level, becausejQuery's Find selects only within descendants
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type == html.ElementNode {
+ result = append(result, m.MatchAll(c)...)
+ }
+ }
+ return
+ })
+}
+
+// Internal implementation to get all parent nodes, stopping at the specified
+// node (or nil if no stop).
+func getParentsNodes(nodes []*html.Node, stopm Matcher, stopNodes []*html.Node) []*html.Node {
+ return mapNodes(nodes, func(i int, n *html.Node) (result []*html.Node) {
+ for p := n.Parent; p != nil; p = p.Parent {
+ sel := newSingleSelection(p, nil)
+ if stopm != nil {
+ if sel.IsMatcher(stopm) {
+ break
+ }
+ } else if len(stopNodes) > 0 {
+ if sel.IsNodes(stopNodes...) {
+ break
+ }
+ }
+ if p.Type == html.ElementNode {
+ result = append(result, p)
+ }
+ }
+ return
+ })
+}
+
+// Internal implementation of sibling nodes that return a raw slice of matches.
+func getSiblingNodes(nodes []*html.Node, st siblingType, untilm Matcher, untilNodes []*html.Node) []*html.Node {
+ var f func(*html.Node) bool
+
+ // If the requested siblings are ...Until, create the test function to
+ // determine if the until condition is reached (returns true if it is)
+ if st == siblingNextUntil || st == siblingPrevUntil {
+ f = func(n *html.Node) bool {
+ if untilm != nil {
+ // Matcher-based condition
+ sel := newSingleSelection(n, nil)
+ return sel.IsMatcher(untilm)
+ } else if len(untilNodes) > 0 {
+ // Nodes-based condition
+ sel := newSingleSelection(n, nil)
+ return sel.IsNodes(untilNodes...)
+ }
+ return false
+ }
+ }
+
+ return mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
+ return getChildrenWithSiblingType(n.Parent, st, n, f)
+ })
+}
+
+// Gets the children nodes of each node in the specified slice of nodes,
+// based on the sibling type request.
+func getChildrenNodes(nodes []*html.Node, st siblingType) []*html.Node {
+ return mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
+ return getChildrenWithSiblingType(n, st, nil, nil)
+ })
+}
+
+// Gets the children of the specified parent, based on the requested sibling
+// type, skipping a specified node if required.
+func getChildrenWithSiblingType(parent *html.Node, st siblingType, skipNode *html.Node,
+ untilFunc func(*html.Node) bool) (result []*html.Node) {
+
+ // Create the iterator function
+ var iter = func(cur *html.Node) (ret *html.Node) {
+ // Based on the sibling type requested, iterate the right way
+ for {
+ switch st {
+ case siblingAll, siblingAllIncludingNonElements:
+ if cur == nil {
+ // First iteration, start with first child of parent
+ // Skip node if required
+ if ret = parent.FirstChild; ret == skipNode && skipNode != nil {
+ ret = skipNode.NextSibling
+ }
+ } else {
+ // Skip node if required
+ if ret = cur.NextSibling; ret == skipNode && skipNode != nil {
+ ret = skipNode.NextSibling
+ }
+ }
+ case siblingPrev, siblingPrevAll, siblingPrevUntil:
+ if cur == nil {
+ // Start with previous sibling of the skip node
+ ret = skipNode.PrevSibling
+ } else {
+ ret = cur.PrevSibling
+ }
+ case siblingNext, siblingNextAll, siblingNextUntil:
+ if cur == nil {
+ // Start with next sibling of the skip node
+ ret = skipNode.NextSibling
+ } else {
+ ret = cur.NextSibling
+ }
+ default:
+ panic("Invalid sibling type.")
+ }
+ if ret == nil || ret.Type == html.ElementNode || st == siblingAllIncludingNonElements {
+ return
+ }
+ // Not a valid node, try again from this one
+ cur = ret
+ }
+ }
+
+ for c := iter(nil); c != nil; c = iter(c) {
+ // If this is an ...Until case, test before append (returns true
+ // if the until condition is reached)
+ if st == siblingNextUntil || st == siblingPrevUntil {
+ if untilFunc(c) {
+ return
+ }
+ }
+ result = append(result, c)
+ if st == siblingNext || st == siblingPrev {
+ // Only one node was requested (immediate next or previous), so exit
+ return
+ }
+ }
+ return
+}
+
+// Internal implementation of parent nodes that return a raw slice of Nodes.
+func getParentNodes(nodes []*html.Node) []*html.Node {
+ return mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
+ if n.Parent != nil && n.Parent.Type == html.ElementNode {
+ return []*html.Node{n.Parent}
+ }
+ return nil
+ })
+}
+
+// Internal map function used by many traversing methods. Takes the source nodes
+// to iterate on and the mapping function that returns an array of nodes.
+// Returns an array of nodes mapped by calling the callback function once for
+// each node in the source nodes.
+func mapNodes(nodes []*html.Node, f func(int, *html.Node) []*html.Node) (result []*html.Node) {
+ set := make(map[*html.Node]bool)
+ for i, n := range nodes {
+ if vals := f(i, n); len(vals) > 0 {
+ result = appendWithoutDuplicates(result, vals, set)
+ }
+ }
+ return result
+}
diff --git a/vendor/github.com/PuerkitoBio/goquery/type.go b/vendor/github.com/PuerkitoBio/goquery/type.go
new file mode 100644
index 00000000..6ad51dbc
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/type.go
@@ -0,0 +1,141 @@
+package goquery
+
+import (
+ "errors"
+ "io"
+ "net/http"
+ "net/url"
+
+ "github.com/andybalholm/cascadia"
+
+ "golang.org/x/net/html"
+)
+
+// Document represents an HTML document to be manipulated. Unlike jQuery, which
+// is loaded as part of a DOM document, and thus acts upon its containing
+// document, GoQuery doesn't know which HTML document to act upon. So it needs
+// to be told, and that's what the Document class is for. It holds the root
+// document node to manipulate, and can make selections on this document.
+type Document struct {
+ *Selection
+ Url *url.URL
+ rootNode *html.Node
+}
+
+// NewDocumentFromNode is a Document constructor that takes a root html Node
+// as argument.
+func NewDocumentFromNode(root *html.Node) *Document {
+ return newDocument(root, nil)
+}
+
+// NewDocument is a Document constructor that takes a string URL as argument.
+// It loads the specified document, parses it, and stores the root Document
+// node, ready to be manipulated.
+//
+// Deprecated: Use the net/http standard library package to make the request
+// and validate the response before calling goquery.NewDocumentFromReader
+// with the response's body.
+func NewDocument(url string) (*Document, error) {
+ // Load the URL
+ res, e := http.Get(url)
+ if e != nil {
+ return nil, e
+ }
+ return NewDocumentFromResponse(res)
+}
+
+// NewDocumentFromReader returns a Document from an io.Reader.
+// It returns an error as second value if the reader's data cannot be parsed
+// as html. It does not check if the reader is also an io.Closer, the
+// provided reader is never closed by this call. It is the responsibility
+// of the caller to close it if required.
+func NewDocumentFromReader(r io.Reader) (*Document, error) {
+ root, e := html.Parse(r)
+ if e != nil {
+ return nil, e
+ }
+ return newDocument(root, nil), nil
+}
+
+// NewDocumentFromResponse is another Document constructor that takes an http response as argument.
+// It loads the specified response's document, parses it, and stores the root Document
+// node, ready to be manipulated. The response's body is closed on return.
+//
+// Deprecated: Use goquery.NewDocumentFromReader with the response's body.
+func NewDocumentFromResponse(res *http.Response) (*Document, error) {
+ if res == nil {
+ return nil, errors.New("Response is nil")
+ }
+ defer res.Body.Close()
+ if res.Request == nil {
+ return nil, errors.New("Response.Request is nil")
+ }
+
+ // Parse the HTML into nodes
+ root, e := html.Parse(res.Body)
+ if e != nil {
+ return nil, e
+ }
+
+ // Create and fill the document
+ return newDocument(root, res.Request.URL), nil
+}
+
+// CloneDocument creates a deep-clone of a document.
+func CloneDocument(doc *Document) *Document {
+ return newDocument(cloneNode(doc.rootNode), doc.Url)
+}
+
+// Private constructor, make sure all fields are correctly filled.
+func newDocument(root *html.Node, url *url.URL) *Document {
+ // Create and fill the document
+ d := &Document{nil, url, root}
+ d.Selection = newSingleSelection(root, d)
+ return d
+}
+
+// Selection represents a collection of nodes matching some criteria. The
+// initial Selection can be created by using Document.Find, and then
+// manipulated using the jQuery-like chainable syntax and methods.
+type Selection struct {
+ Nodes []*html.Node
+ document *Document
+ prevSel *Selection
+}
+
+// Helper constructor to create an empty selection
+func newEmptySelection(doc *Document) *Selection {
+ return &Selection{nil, doc, nil}
+}
+
+// Helper constructor to create a selection of only one node
+func newSingleSelection(node *html.Node, doc *Document) *Selection {
+ return &Selection{[]*html.Node{node}, doc, nil}
+}
+
+// Matcher is an interface that defines the methods to match
+// HTML nodes against a compiled selector string. Cascadia's
+// Selector implements this interface.
+type Matcher interface {
+ Match(*html.Node) bool
+ MatchAll(*html.Node) []*html.Node
+ Filter([]*html.Node) []*html.Node
+}
+
+// compileMatcher compiles the selector string s and returns
+// the corresponding Matcher. If s is an invalid selector string,
+// it returns a Matcher that fails all matches.
+func compileMatcher(s string) Matcher {
+ cs, err := cascadia.Compile(s)
+ if err != nil {
+ return invalidMatcher{}
+ }
+ return cs
+}
+
+// invalidMatcher is a Matcher that always fails to match.
+type invalidMatcher struct{}
+
+func (invalidMatcher) Match(n *html.Node) bool { return false }
+func (invalidMatcher) MatchAll(n *html.Node) []*html.Node { return nil }
+func (invalidMatcher) Filter(ns []*html.Node) []*html.Node { return nil }
diff --git a/vendor/github.com/PuerkitoBio/goquery/utilities.go b/vendor/github.com/PuerkitoBio/goquery/utilities.go
new file mode 100644
index 00000000..b4c061a4
--- /dev/null
+++ b/vendor/github.com/PuerkitoBio/goquery/utilities.go
@@ -0,0 +1,161 @@
+package goquery
+
+import (
+ "bytes"
+
+ "golang.org/x/net/html"
+)
+
+// used to determine if a set (map[*html.Node]bool) should be used
+// instead of iterating over a slice. The set uses more memory and
+// is slower than slice iteration for small N.
+const minNodesForSet = 1000
+
+var nodeNames = []string{
+ html.ErrorNode: "#error",
+ html.TextNode: "#text",
+ html.DocumentNode: "#document",
+ html.CommentNode: "#comment",
+}
+
+// NodeName returns the node name of the first element in the selection.
+// It tries to behave in a similar way as the DOM's nodeName property
+// (https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeName).
+//
+// Go's net/html package defines the following node types, listed with
+// the corresponding returned value from this function:
+//
+// ErrorNode : #error
+// TextNode : #text
+// DocumentNode : #document
+// ElementNode : the element's tag name
+// CommentNode : #comment
+// DoctypeNode : the name of the document type
+//
+func NodeName(s *Selection) string {
+ if s.Length() == 0 {
+ return ""
+ }
+ switch n := s.Get(0); n.Type {
+ case html.ElementNode, html.DoctypeNode:
+ return n.Data
+ default:
+ if n.Type >= 0 && int(n.Type) < len(nodeNames) {
+ return nodeNames[n.Type]
+ }
+ return ""
+ }
+}
+
+// OuterHtml returns the outer HTML rendering of the first item in
+// the selection - that is, the HTML including the first element's
+// tag and attributes.
+//
+// Unlike InnerHtml, this is a function and not a method on the Selection,
+// because this is not a jQuery method (in javascript-land, this is
+// a property provided by the DOM).
+func OuterHtml(s *Selection) (string, error) {
+ var buf bytes.Buffer
+
+ if s.Length() == 0 {
+ return "", nil
+ }
+ n := s.Get(0)
+ if err := html.Render(&buf, n); err != nil {
+ return "", err
+ }
+ return buf.String(), nil
+}
+
+// Loop through all container nodes to search for the target node.
+func sliceContains(container []*html.Node, contained *html.Node) bool {
+ for _, n := range container {
+ if nodeContains(n, contained) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// Checks if the contained node is within the container node.
+func nodeContains(container *html.Node, contained *html.Node) bool {
+ // Check if the parent of the contained node is the container node, traversing
+ // upward until the top is reached, or the container is found.
+ for contained = contained.Parent; contained != nil; contained = contained.Parent {
+ if container == contained {
+ return true
+ }
+ }
+ return false
+}
+
+// Checks if the target node is in the slice of nodes.
+func isInSlice(slice []*html.Node, node *html.Node) bool {
+ return indexInSlice(slice, node) > -1
+}
+
+// Returns the index of the target node in the slice, or -1.
+func indexInSlice(slice []*html.Node, node *html.Node) int {
+ if node != nil {
+ for i, n := range slice {
+ if n == node {
+ return i
+ }
+ }
+ }
+ return -1
+}
+
+// Appends the new nodes to the target slice, making sure no duplicate is added.
+// There is no check to the original state of the target slice, so it may still
+// contain duplicates. The target slice is returned because append() may create
+// a new underlying array. If targetSet is nil, a local set is created with the
+// target if len(target) + len(nodes) is greater than minNodesForSet.
+func appendWithoutDuplicates(target []*html.Node, nodes []*html.Node, targetSet map[*html.Node]bool) []*html.Node {
+ // if there are not that many nodes, don't use the map, faster to just use nested loops
+ // (unless a non-nil targetSet is passed, in which case the caller knows better).
+ if targetSet == nil && len(target)+len(nodes) < minNodesForSet {
+ for _, n := range nodes {
+ if !isInSlice(target, n) {
+ target = append(target, n)
+ }
+ }
+ return target
+ }
+
+ // if a targetSet is passed, then assume it is reliable, otherwise create one
+ // and initialize it with the current target contents.
+ if targetSet == nil {
+ targetSet = make(map[*html.Node]bool, len(target))
+ for _, n := range target {
+ targetSet[n] = true
+ }
+ }
+ for _, n := range nodes {
+ if !targetSet[n] {
+ target = append(target, n)
+ targetSet[n] = true
+ }
+ }
+
+ return target
+}
+
+// Loop through a selection, returning only those nodes that pass the predicate
+// function.
+func grep(sel *Selection, predicate func(i int, s *Selection) bool) (result []*html.Node) {
+ for i, n := range sel.Nodes {
+ if predicate(i, newSingleSelection(n, sel.document)) {
+ result = append(result, n)
+ }
+ }
+ return result
+}
+
+// Creates a new Selection object based on the specified nodes, and keeps the
+// source Selection object on the stack (linked list).
+func pushStack(fromSel *Selection, nodes []*html.Node) *Selection {
+ result := &Selection{nodes, fromSel.document, fromSel}
+ return result
+}
diff --git a/vendor/github.com/andybalholm/cascadia/.travis.yml b/vendor/github.com/andybalholm/cascadia/.travis.yml
new file mode 100644
index 00000000..6f227517
--- /dev/null
+++ b/vendor/github.com/andybalholm/cascadia/.travis.yml
@@ -0,0 +1,14 @@
+language: go
+
+go:
+ - 1.3
+ - 1.4
+
+install:
+ - go get github.com/andybalholm/cascadia
+
+script:
+ - go test -v
+
+notifications:
+ email: false
diff --git a/vendor/github.com/andybalholm/cascadia/LICENSE b/vendor/github.com/andybalholm/cascadia/LICENSE
new file mode 100644
index 00000000..ee5ad35a
--- /dev/null
+++ b/vendor/github.com/andybalholm/cascadia/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2011 Andy Balholm. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/andybalholm/cascadia/README.md b/vendor/github.com/andybalholm/cascadia/README.md
new file mode 100644
index 00000000..26f4c37b
--- /dev/null
+++ b/vendor/github.com/andybalholm/cascadia/README.md
@@ -0,0 +1,9 @@
+# cascadia
+
+[](https://travis-ci.org/andybalholm/cascadia)
+
+The Cascadia package implements CSS selectors for use with the parse trees produced by the html package.
+
+To test CSS selectors without writing Go code, check out [cascadia](https://github.com/suntong/cascadia) the command line tool, a thin wrapper around this package.
+
+[Refer to godoc here](https://godoc.org/github.com/andybalholm/cascadia).
diff --git a/vendor/github.com/andybalholm/cascadia/go.mod b/vendor/github.com/andybalholm/cascadia/go.mod
new file mode 100644
index 00000000..e6febbbf
--- /dev/null
+++ b/vendor/github.com/andybalholm/cascadia/go.mod
@@ -0,0 +1,3 @@
+module "github.com/andybalholm/cascadia"
+
+require "golang.org/x/net" v0.0.0-20180218175443-cbe0f9307d01
diff --git a/vendor/github.com/andybalholm/cascadia/parser.go b/vendor/github.com/andybalholm/cascadia/parser.go
new file mode 100644
index 00000000..4f8810e3
--- /dev/null
+++ b/vendor/github.com/andybalholm/cascadia/parser.go
@@ -0,0 +1,801 @@
+// Package cascadia is an implementation of CSS selectors.
+package cascadia
+
+import (
+ "errors"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+// a parser for CSS selectors
+type parser struct {
+ s string // the source text
+ i int // the current position
+}
+
+// parseEscape parses a backslash escape.
+func (p *parser) parseEscape() (result string, err error) {
+ if len(p.s) < p.i+2 || p.s[p.i] != '\\' {
+ return "", errors.New("invalid escape sequence")
+ }
+
+ start := p.i + 1
+ c := p.s[start]
+ switch {
+ case c == '\r' || c == '\n' || c == '\f':
+ return "", errors.New("escaped line ending outside string")
+ case hexDigit(c):
+ // unicode escape (hex)
+ var i int
+ for i = start; i < p.i+6 && i < len(p.s) && hexDigit(p.s[i]); i++ {
+ // empty
+ }
+ v, _ := strconv.ParseUint(p.s[start:i], 16, 21)
+ if len(p.s) > i {
+ switch p.s[i] {
+ case '\r':
+ i++
+ if len(p.s) > i && p.s[i] == '\n' {
+ i++
+ }
+ case ' ', '\t', '\n', '\f':
+ i++
+ }
+ }
+ p.i = i
+ return string(rune(v)), nil
+ }
+
+ // Return the literal character after the backslash.
+ result = p.s[start : start+1]
+ p.i += 2
+ return result, nil
+}
+
+// toLowerASCII returns s with all ASCII capital letters lowercased.
+func toLowerASCII(s string) string {
+ var b []byte
+ for i := 0; i < len(s); i++ {
+ if c := s[i]; 'A' <= c && c <= 'Z' {
+ if b == nil {
+ b = make([]byte, len(s))
+ copy(b, s)
+ }
+ b[i] = s[i] + ('a' - 'A')
+ }
+ }
+
+ if b == nil {
+ return s
+ }
+
+ return string(b)
+}
+
+func hexDigit(c byte) bool {
+ return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F'
+}
+
+// nameStart returns whether c can be the first character of an identifier
+// (not counting an initial hyphen, or an escape sequence).
+func nameStart(c byte) bool {
+ return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || c > 127
+}
+
+// nameChar returns whether c can be a character within an identifier
+// (not counting an escape sequence).
+func nameChar(c byte) bool {
+ return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || c > 127 ||
+ c == '-' || '0' <= c && c <= '9'
+}
+
+// parseIdentifier parses an identifier.
+func (p *parser) parseIdentifier() (result string, err error) {
+ startingDash := false
+ if len(p.s) > p.i && p.s[p.i] == '-' {
+ startingDash = true
+ p.i++
+ }
+
+ if len(p.s) <= p.i {
+ return "", errors.New("expected identifier, found EOF instead")
+ }
+
+ if c := p.s[p.i]; !(nameStart(c) || c == '\\') {
+ return "", fmt.Errorf("expected identifier, found %c instead", c)
+ }
+
+ result, err = p.parseName()
+ if startingDash && err == nil {
+ result = "-" + result
+ }
+ return
+}
+
+// parseName parses a name (which is like an identifier, but doesn't have
+// extra restrictions on the first character).
+func (p *parser) parseName() (result string, err error) {
+ i := p.i
+loop:
+ for i < len(p.s) {
+ c := p.s[i]
+ switch {
+ case nameChar(c):
+ start := i
+ for i < len(p.s) && nameChar(p.s[i]) {
+ i++
+ }
+ result += p.s[start:i]
+ case c == '\\':
+ p.i = i
+ val, err := p.parseEscape()
+ if err != nil {
+ return "", err
+ }
+ i = p.i
+ result += val
+ default:
+ break loop
+ }
+ }
+
+ if result == "" {
+ return "", errors.New("expected name, found EOF instead")
+ }
+
+ p.i = i
+ return result, nil
+}
+
+// parseString parses a single- or double-quoted string.
+func (p *parser) parseString() (result string, err error) {
+ i := p.i
+ if len(p.s) < i+2 {
+ return "", errors.New("expected string, found EOF instead")
+ }
+
+ quote := p.s[i]
+ i++
+
+loop:
+ for i < len(p.s) {
+ switch p.s[i] {
+ case '\\':
+ if len(p.s) > i+1 {
+ switch c := p.s[i+1]; c {
+ case '\r':
+ if len(p.s) > i+2 && p.s[i+2] == '\n' {
+ i += 3
+ continue loop
+ }
+ fallthrough
+ case '\n', '\f':
+ i += 2
+ continue loop
+ }
+ }
+ p.i = i
+ val, err := p.parseEscape()
+ if err != nil {
+ return "", err
+ }
+ i = p.i
+ result += val
+ case quote:
+ break loop
+ case '\r', '\n', '\f':
+ return "", errors.New("unexpected end of line in string")
+ default:
+ start := i
+ for i < len(p.s) {
+ if c := p.s[i]; c == quote || c == '\\' || c == '\r' || c == '\n' || c == '\f' {
+ break
+ }
+ i++
+ }
+ result += p.s[start:i]
+ }
+ }
+
+ if i >= len(p.s) {
+ return "", errors.New("EOF in string")
+ }
+
+ // Consume the final quote.
+ i++
+
+ p.i = i
+ return result, nil
+}
+
+// parseRegex parses a regular expression; the end is defined by encountering an
+// unmatched closing ')' or ']' which is not consumed
+func (p *parser) parseRegex() (rx *regexp.Regexp, err error) {
+ i := p.i
+ if len(p.s) < i+2 {
+ return nil, errors.New("expected regular expression, found EOF instead")
+ }
+
+ // number of open parens or brackets;
+ // when it becomes negative, finished parsing regex
+ open := 0
+
+loop:
+ for i < len(p.s) {
+ switch p.s[i] {
+ case '(', '[':
+ open++
+ case ')', ']':
+ open--
+ if open < 0 {
+ break loop
+ }
+ }
+ i++
+ }
+
+ if i >= len(p.s) {
+ return nil, errors.New("EOF in regular expression")
+ }
+ rx, err = regexp.Compile(p.s[p.i:i])
+ p.i = i
+ return rx, err
+}
+
+// skipWhitespace consumes whitespace characters and comments.
+// It returns true if there was actually anything to skip.
+func (p *parser) skipWhitespace() bool {
+ i := p.i
+ for i < len(p.s) {
+ switch p.s[i] {
+ case ' ', '\t', '\r', '\n', '\f':
+ i++
+ continue
+ case '/':
+ if strings.HasPrefix(p.s[i:], "/*") {
+ end := strings.Index(p.s[i+len("/*"):], "*/")
+ if end != -1 {
+ i += end + len("/**/")
+ continue
+ }
+ }
+ }
+ break
+ }
+
+ if i > p.i {
+ p.i = i
+ return true
+ }
+
+ return false
+}
+
+// consumeParenthesis consumes an opening parenthesis and any following
+// whitespace. It returns true if there was actually a parenthesis to skip.
+func (p *parser) consumeParenthesis() bool {
+ if p.i < len(p.s) && p.s[p.i] == '(' {
+ p.i++
+ p.skipWhitespace()
+ return true
+ }
+ return false
+}
+
+// consumeClosingParenthesis consumes a closing parenthesis and any preceding
+// whitespace. It returns true if there was actually a parenthesis to skip.
+func (p *parser) consumeClosingParenthesis() bool {
+ i := p.i
+ p.skipWhitespace()
+ if p.i < len(p.s) && p.s[p.i] == ')' {
+ p.i++
+ return true
+ }
+ p.i = i
+ return false
+}
+
+// parseTypeSelector parses a type selector (one that matches by tag name).
+func (p *parser) parseTypeSelector() (result tagSelector, err error) {
+ tag, err := p.parseIdentifier()
+ if err != nil {
+ return
+ }
+ return tagSelector{tag: toLowerASCII(tag)}, nil
+}
+
+// parseIDSelector parses a selector that matches by id attribute.
+func (p *parser) parseIDSelector() (idSelector, error) {
+ if p.i >= len(p.s) {
+ return idSelector{}, fmt.Errorf("expected id selector (#id), found EOF instead")
+ }
+ if p.s[p.i] != '#' {
+ return idSelector{}, fmt.Errorf("expected id selector (#id), found '%c' instead", p.s[p.i])
+ }
+
+ p.i++
+ id, err := p.parseName()
+ if err != nil {
+ return idSelector{}, err
+ }
+
+ return idSelector{id: id}, nil
+}
+
+// parseClassSelector parses a selector that matches by class attribute.
+func (p *parser) parseClassSelector() (classSelector, error) {
+ if p.i >= len(p.s) {
+ return classSelector{}, fmt.Errorf("expected class selector (.class), found EOF instead")
+ }
+ if p.s[p.i] != '.' {
+ return classSelector{}, fmt.Errorf("expected class selector (.class), found '%c' instead", p.s[p.i])
+ }
+
+ p.i++
+ class, err := p.parseIdentifier()
+ if err != nil {
+ return classSelector{}, err
+ }
+
+ return classSelector{class: class}, nil
+}
+
+// parseAttributeSelector parses a selector that matches by attribute value.
+func (p *parser) parseAttributeSelector() (attrSelector, error) {
+ if p.i >= len(p.s) {
+ return attrSelector{}, fmt.Errorf("expected attribute selector ([attribute]), found EOF instead")
+ }
+ if p.s[p.i] != '[' {
+ return attrSelector{}, fmt.Errorf("expected attribute selector ([attribute]), found '%c' instead", p.s[p.i])
+ }
+
+ p.i++
+ p.skipWhitespace()
+ key, err := p.parseIdentifier()
+ if err != nil {
+ return attrSelector{}, err
+ }
+ key = toLowerASCII(key)
+
+ p.skipWhitespace()
+ if p.i >= len(p.s) {
+ return attrSelector{}, errors.New("unexpected EOF in attribute selector")
+ }
+
+ if p.s[p.i] == ']' {
+ p.i++
+ return attrSelector{key: key, operation: ""}, nil
+ }
+
+ if p.i+2 >= len(p.s) {
+ return attrSelector{}, errors.New("unexpected EOF in attribute selector")
+ }
+
+ op := p.s[p.i : p.i+2]
+ if op[0] == '=' {
+ op = "="
+ } else if op[1] != '=' {
+ return attrSelector{}, fmt.Errorf(`expected equality operator, found "%s" instead`, op)
+ }
+ p.i += len(op)
+
+ p.skipWhitespace()
+ if p.i >= len(p.s) {
+ return attrSelector{}, errors.New("unexpected EOF in attribute selector")
+ }
+ var val string
+ var rx *regexp.Regexp
+ if op == "#=" {
+ rx, err = p.parseRegex()
+ } else {
+ switch p.s[p.i] {
+ case '\'', '"':
+ val, err = p.parseString()
+ default:
+ val, err = p.parseIdentifier()
+ }
+ }
+ if err != nil {
+ return attrSelector{}, err
+ }
+
+ p.skipWhitespace()
+ if p.i >= len(p.s) {
+ return attrSelector{}, errors.New("unexpected EOF in attribute selector")
+ }
+ if p.s[p.i] != ']' {
+ return attrSelector{}, fmt.Errorf("expected ']', found '%c' instead", p.s[p.i])
+ }
+ p.i++
+
+ switch op {
+ case "=", "!=", "~=", "|=", "^=", "$=", "*=", "#=":
+ return attrSelector{key: key, val: val, operation: op, regexp: rx}, nil
+ default:
+ return attrSelector{}, fmt.Errorf("attribute operator %q is not supported", op)
+ }
+}
+
+var errExpectedParenthesis = errors.New("expected '(' but didn't find it")
+var errExpectedClosingParenthesis = errors.New("expected ')' but didn't find it")
+var errUnmatchedParenthesis = errors.New("unmatched '('")
+
+// parsePseudoclassSelector parses a pseudoclass selector like :not(p)
+func (p *parser) parsePseudoclassSelector() (out Sel, err error) {
+ if p.i >= len(p.s) {
+ return nil, fmt.Errorf("expected pseudoclass selector (:pseudoclass), found EOF instead")
+ }
+ if p.s[p.i] != ':' {
+ return nil, fmt.Errorf("expected attribute selector (:pseudoclass), found '%c' instead", p.s[p.i])
+ }
+
+ p.i++
+ if p.s[p.i] == ':' { // we found a pseudo-element
+ p.i++
+ }
+
+ name, err := p.parseIdentifier()
+ if err != nil {
+ return
+ }
+ name = toLowerASCII(name)
+ switch name {
+ case "not", "has", "haschild":
+ if !p.consumeParenthesis() {
+ return out, errExpectedParenthesis
+ }
+ sel, parseErr := p.parseSelectorGroup()
+ if parseErr != nil {
+ return out, parseErr
+ }
+ if !p.consumeClosingParenthesis() {
+ return out, errExpectedClosingParenthesis
+ }
+
+ out = relativePseudoClassSelector{name: name, match: sel}
+
+ case "contains", "containsown":
+ if !p.consumeParenthesis() {
+ return out, errExpectedParenthesis
+ }
+ if p.i == len(p.s) {
+ return out, errUnmatchedParenthesis
+ }
+ var val string
+ switch p.s[p.i] {
+ case '\'', '"':
+ val, err = p.parseString()
+ default:
+ val, err = p.parseIdentifier()
+ }
+ if err != nil {
+ return out, err
+ }
+ val = strings.ToLower(val)
+ p.skipWhitespace()
+ if p.i >= len(p.s) {
+ return out, errors.New("unexpected EOF in pseudo selector")
+ }
+ if !p.consumeClosingParenthesis() {
+ return out, errExpectedClosingParenthesis
+ }
+
+ out = containsPseudoClassSelector{own: name == "containsown", value: val}
+
+ case "matches", "matchesown":
+ if !p.consumeParenthesis() {
+ return out, errExpectedParenthesis
+ }
+ rx, err := p.parseRegex()
+ if err != nil {
+ return out, err
+ }
+ if p.i >= len(p.s) {
+ return out, errors.New("unexpected EOF in pseudo selector")
+ }
+ if !p.consumeClosingParenthesis() {
+ return out, errExpectedClosingParenthesis
+ }
+
+ out = regexpPseudoClassSelector{own: name == "matchesown", regexp: rx}
+
+ case "nth-child", "nth-last-child", "nth-of-type", "nth-last-of-type":
+ if !p.consumeParenthesis() {
+ return out, errExpectedParenthesis
+ }
+ a, b, err := p.parseNth()
+ if err != nil {
+ return out, err
+ }
+ if !p.consumeClosingParenthesis() {
+ return out, errExpectedClosingParenthesis
+ }
+ last := name == "nth-last-child" || name == "nth-last-of-type"
+ ofType := name == "nth-of-type" || name == "nth-last-of-type"
+ out = nthPseudoClassSelector{a: a, b: b, last: last, ofType: ofType}
+
+ case "first-child":
+ out = nthPseudoClassSelector{a: 0, b: 1, ofType: false, last: false}
+ case "last-child":
+ out = nthPseudoClassSelector{a: 0, b: 1, ofType: false, last: true}
+ case "first-of-type":
+ out = nthPseudoClassSelector{a: 0, b: 1, ofType: true, last: false}
+ case "last-of-type":
+ out = nthPseudoClassSelector{a: 0, b: 1, ofType: true, last: true}
+ case "only-child":
+ out = onlyChildPseudoClassSelector{ofType: false}
+ case "only-of-type":
+ out = onlyChildPseudoClassSelector{ofType: true}
+ case "input":
+ out = inputPseudoClassSelector{}
+ case "empty":
+ out = emptyElementPseudoClassSelector{}
+ case "root":
+ out = rootPseudoClassSelector{}
+ case "after", "backdrop", "before", "cue", "first-letter", "first-line", "grammar-error", "marker", "placeholder", "selection", "spelling-error":
+ return out, errors.New("pseudo-elements are not yet supported")
+ default:
+ return out, fmt.Errorf("unknown pseudoclass or pseudoelement :%s", name)
+ }
+ return
+}
+
+// parseInteger parses a decimal integer.
+func (p *parser) parseInteger() (int, error) {
+ i := p.i
+ start := i
+ for i < len(p.s) && '0' <= p.s[i] && p.s[i] <= '9' {
+ i++
+ }
+ if i == start {
+ return 0, errors.New("expected integer, but didn't find it")
+ }
+ p.i = i
+
+ val, err := strconv.Atoi(p.s[start:i])
+ if err != nil {
+ return 0, err
+ }
+
+ return val, nil
+}
+
+// parseNth parses the argument for :nth-child (normally of the form an+b).
+func (p *parser) parseNth() (a, b int, err error) {
+ // initial state
+ if p.i >= len(p.s) {
+ goto eof
+ }
+ switch p.s[p.i] {
+ case '-':
+ p.i++
+ goto negativeA
+ case '+':
+ p.i++
+ goto positiveA
+ case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
+ goto positiveA
+ case 'n', 'N':
+ a = 1
+ p.i++
+ goto readN
+ case 'o', 'O', 'e', 'E':
+ id, nameErr := p.parseName()
+ if nameErr != nil {
+ return 0, 0, nameErr
+ }
+ id = toLowerASCII(id)
+ if id == "odd" {
+ return 2, 1, nil
+ }
+ if id == "even" {
+ return 2, 0, nil
+ }
+ return 0, 0, fmt.Errorf("expected 'odd' or 'even', but found '%s' instead", id)
+ default:
+ goto invalid
+ }
+
+positiveA:
+ if p.i >= len(p.s) {
+ goto eof
+ }
+ switch p.s[p.i] {
+ case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
+ a, err = p.parseInteger()
+ if err != nil {
+ return 0, 0, err
+ }
+ goto readA
+ case 'n', 'N':
+ a = 1
+ p.i++
+ goto readN
+ default:
+ goto invalid
+ }
+
+negativeA:
+ if p.i >= len(p.s) {
+ goto eof
+ }
+ switch p.s[p.i] {
+ case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
+ a, err = p.parseInteger()
+ if err != nil {
+ return 0, 0, err
+ }
+ a = -a
+ goto readA
+ case 'n', 'N':
+ a = -1
+ p.i++
+ goto readN
+ default:
+ goto invalid
+ }
+
+readA:
+ if p.i >= len(p.s) {
+ goto eof
+ }
+ switch p.s[p.i] {
+ case 'n', 'N':
+ p.i++
+ goto readN
+ default:
+ // The number we read as a is actually b.
+ return 0, a, nil
+ }
+
+readN:
+ p.skipWhitespace()
+ if p.i >= len(p.s) {
+ goto eof
+ }
+ switch p.s[p.i] {
+ case '+':
+ p.i++
+ p.skipWhitespace()
+ b, err = p.parseInteger()
+ if err != nil {
+ return 0, 0, err
+ }
+ return a, b, nil
+ case '-':
+ p.i++
+ p.skipWhitespace()
+ b, err = p.parseInteger()
+ if err != nil {
+ return 0, 0, err
+ }
+ return a, -b, nil
+ default:
+ return a, 0, nil
+ }
+
+eof:
+ return 0, 0, errors.New("unexpected EOF while attempting to parse expression of form an+b")
+
+invalid:
+ return 0, 0, errors.New("unexpected character while attempting to parse expression of form an+b")
+}
+
+// parseSimpleSelectorSequence parses a selector sequence that applies to
+// a single element.
+func (p *parser) parseSimpleSelectorSequence() (Sel, error) {
+ var selectors []Sel
+
+ if p.i >= len(p.s) {
+ return nil, errors.New("expected selector, found EOF instead")
+ }
+
+ switch p.s[p.i] {
+ case '*':
+ // It's the universal selector. Just skip over it, since it doesn't affect the meaning.
+ p.i++
+ case '#', '.', '[', ':':
+ // There's no type selector. Wait to process the other till the main loop.
+ default:
+ r, err := p.parseTypeSelector()
+ if err != nil {
+ return nil, err
+ }
+ selectors = append(selectors, r)
+ }
+
+loop:
+ for p.i < len(p.s) {
+ var (
+ ns Sel
+ err error
+ )
+ switch p.s[p.i] {
+ case '#':
+ ns, err = p.parseIDSelector()
+ case '.':
+ ns, err = p.parseClassSelector()
+ case '[':
+ ns, err = p.parseAttributeSelector()
+ case ':':
+ ns, err = p.parsePseudoclassSelector()
+ default:
+ break loop
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ selectors = append(selectors, ns)
+ }
+ if len(selectors) == 1 { // no need wrap the selectors in compoundSelector
+ return selectors[0], nil
+ }
+ return compoundSelector{selectors: selectors}, nil
+}
+
+// parseSelector parses a selector that may include combinators.
+func (p *parser) parseSelector() (Sel, error) {
+ p.skipWhitespace()
+ result, err := p.parseSimpleSelectorSequence()
+ if err != nil {
+ return nil, err
+ }
+
+ for {
+ var (
+ combinator byte
+ c Sel
+ )
+ if p.skipWhitespace() {
+ combinator = ' '
+ }
+ if p.i >= len(p.s) {
+ return result, nil
+ }
+
+ switch p.s[p.i] {
+ case '+', '>', '~':
+ combinator = p.s[p.i]
+ p.i++
+ p.skipWhitespace()
+ case ',', ')':
+ // These characters can't begin a selector, but they can legally occur after one.
+ return result, nil
+ }
+
+ if combinator == 0 {
+ return result, nil
+ }
+
+ c, err = p.parseSimpleSelectorSequence()
+ if err != nil {
+ return nil, err
+ }
+ result = combinedSelector{first: result, combinator: combinator, second: c}
+ }
+}
+
+// parseSelectorGroup parses a group of selectors, separated by commas.
+func (p *parser) parseSelectorGroup() (SelectorGroup, error) {
+ current, err := p.parseSelector()
+ if err != nil {
+ return nil, err
+ }
+ result := SelectorGroup{current}
+
+ for p.i < len(p.s) {
+ if p.s[p.i] != ',' {
+ break
+ }
+ p.i++
+ c, err := p.parseSelector()
+ if err != nil {
+ return nil, err
+ }
+ result = append(result, c)
+ }
+ return result, nil
+}
diff --git a/vendor/github.com/andybalholm/cascadia/selector.go b/vendor/github.com/andybalholm/cascadia/selector.go
new file mode 100644
index 00000000..18ce1164
--- /dev/null
+++ b/vendor/github.com/andybalholm/cascadia/selector.go
@@ -0,0 +1,833 @@
+package cascadia
+
+import (
+ "bytes"
+ "fmt"
+ "regexp"
+ "strings"
+
+ "golang.org/x/net/html"
+)
+
+// Matcher is the interface for basic selector functionality.
+// Match returns whether a selector matches n.
+type Matcher interface {
+ Match(n *html.Node) bool
+}
+
+// Sel is the interface for all the functionality provided by selectors.
+// It is currently the same as Matcher, but other methods may be added in the
+// future.
+type Sel interface {
+ Matcher
+ Specificity() Specificity
+}
+
+// Parse parses a selector.
+func Parse(sel string) (Sel, error) {
+ p := &parser{s: sel}
+ compiled, err := p.parseSelector()
+ if err != nil {
+ return nil, err
+ }
+
+ if p.i < len(sel) {
+ return nil, fmt.Errorf("parsing %q: %d bytes left over", sel, len(sel)-p.i)
+ }
+
+ return compiled, nil
+}
+
+// ParseGroup parses a selector, or a group of selectors separated by commas.
+func ParseGroup(sel string) (SelectorGroup, error) {
+ p := &parser{s: sel}
+ compiled, err := p.parseSelectorGroup()
+ if err != nil {
+ return nil, err
+ }
+
+ if p.i < len(sel) {
+ return nil, fmt.Errorf("parsing %q: %d bytes left over", sel, len(sel)-p.i)
+ }
+
+ return compiled, nil
+}
+
+// A Selector is a function which tells whether a node matches or not.
+//
+// This type is maintained for compatibility; I recommend using the newer and
+// more idiomatic interfaces Sel and Matcher.
+type Selector func(*html.Node) bool
+
+// Compile parses a selector and returns, if successful, a Selector object
+// that can be used to match against html.Node objects.
+func Compile(sel string) (Selector, error) {
+ compiled, err := ParseGroup(sel)
+ if err != nil {
+ return nil, err
+ }
+
+ return Selector(compiled.Match), nil
+}
+
+// MustCompile is like Compile, but panics instead of returning an error.
+func MustCompile(sel string) Selector {
+ compiled, err := Compile(sel)
+ if err != nil {
+ panic(err)
+ }
+ return compiled
+}
+
+// MatchAll returns a slice of the nodes that match the selector,
+// from n and its children.
+func (s Selector) MatchAll(n *html.Node) []*html.Node {
+ return s.matchAllInto(n, nil)
+}
+
+func (s Selector) matchAllInto(n *html.Node, storage []*html.Node) []*html.Node {
+ if s(n) {
+ storage = append(storage, n)
+ }
+
+ for child := n.FirstChild; child != nil; child = child.NextSibling {
+ storage = s.matchAllInto(child, storage)
+ }
+
+ return storage
+}
+
+func queryInto(n *html.Node, m Matcher, storage []*html.Node) []*html.Node {
+ for child := n.FirstChild; child != nil; child = child.NextSibling {
+ if m.Match(child) {
+ storage = append(storage, child)
+ }
+ storage = queryInto(child, m, storage)
+ }
+
+ return storage
+}
+
+// QueryAll returns a slice of all the nodes that match m, from the descendants
+// of n.
+func QueryAll(n *html.Node, m Matcher) []*html.Node {
+ return queryInto(n, m, nil)
+}
+
+// Match returns true if the node matches the selector.
+func (s Selector) Match(n *html.Node) bool {
+ return s(n)
+}
+
+// MatchFirst returns the first node that matches s, from n and its children.
+func (s Selector) MatchFirst(n *html.Node) *html.Node {
+ if s.Match(n) {
+ return n
+ }
+
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ m := s.MatchFirst(c)
+ if m != nil {
+ return m
+ }
+ }
+ return nil
+}
+
+// Query returns the first node that matches m, from the descendants of n.
+// If none matches, it returns nil.
+func Query(n *html.Node, m Matcher) *html.Node {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if m.Match(c) {
+ return c
+ }
+ if matched := Query(c, m); matched != nil {
+ return matched
+ }
+ }
+
+ return nil
+}
+
+// Filter returns the nodes in nodes that match the selector.
+func (s Selector) Filter(nodes []*html.Node) (result []*html.Node) {
+ for _, n := range nodes {
+ if s(n) {
+ result = append(result, n)
+ }
+ }
+ return result
+}
+
+// Filter returns the nodes that match m.
+func Filter(nodes []*html.Node, m Matcher) (result []*html.Node) {
+ for _, n := range nodes {
+ if m.Match(n) {
+ result = append(result, n)
+ }
+ }
+ return result
+}
+
+type tagSelector struct {
+ tag string
+}
+
+// Matches elements with a given tag name.
+func (t tagSelector) Match(n *html.Node) bool {
+ return n.Type == html.ElementNode && n.Data == t.tag
+}
+
+func (c tagSelector) Specificity() Specificity {
+ return Specificity{0, 0, 1}
+}
+
+type classSelector struct {
+ class string
+}
+
+// Matches elements by class attribute.
+func (t classSelector) Match(n *html.Node) bool {
+ return matchAttribute(n, "class", func(s string) bool {
+ return matchInclude(t.class, s)
+ })
+}
+
+func (c classSelector) Specificity() Specificity {
+ return Specificity{0, 1, 0}
+}
+
+type idSelector struct {
+ id string
+}
+
+// Matches elements by id attribute.
+func (t idSelector) Match(n *html.Node) bool {
+ return matchAttribute(n, "id", func(s string) bool {
+ return s == t.id
+ })
+}
+
+func (c idSelector) Specificity() Specificity {
+ return Specificity{1, 0, 0}
+}
+
+type attrSelector struct {
+ key, val, operation string
+ regexp *regexp.Regexp
+}
+
+// Matches elements by attribute value.
+func (t attrSelector) Match(n *html.Node) bool {
+ switch t.operation {
+ case "":
+ return matchAttribute(n, t.key, func(string) bool { return true })
+ case "=":
+ return matchAttribute(n, t.key, func(s string) bool { return s == t.val })
+ case "!=":
+ return attributeNotEqualMatch(t.key, t.val, n)
+ case "~=":
+ // matches elements where the attribute named key is a whitespace-separated list that includes val.
+ return matchAttribute(n, t.key, func(s string) bool { return matchInclude(t.val, s) })
+ case "|=":
+ return attributeDashMatch(t.key, t.val, n)
+ case "^=":
+ return attributePrefixMatch(t.key, t.val, n)
+ case "$=":
+ return attributeSuffixMatch(t.key, t.val, n)
+ case "*=":
+ return attributeSubstringMatch(t.key, t.val, n)
+ case "#=":
+ return attributeRegexMatch(t.key, t.regexp, n)
+ default:
+ panic(fmt.Sprintf("unsuported operation : %s", t.operation))
+ }
+}
+
+// matches elements where the attribute named key satisifes the function f.
+func matchAttribute(n *html.Node, key string, f func(string) bool) bool {
+ if n.Type != html.ElementNode {
+ return false
+ }
+ for _, a := range n.Attr {
+ if a.Key == key && f(a.Val) {
+ return true
+ }
+ }
+ return false
+}
+
+// attributeNotEqualMatch matches elements where
+// the attribute named key does not have the value val.
+func attributeNotEqualMatch(key, val string, n *html.Node) bool {
+ if n.Type != html.ElementNode {
+ return false
+ }
+ for _, a := range n.Attr {
+ if a.Key == key && a.Val == val {
+ return false
+ }
+ }
+ return true
+}
+
+// returns true if s is a whitespace-separated list that includes val.
+func matchInclude(val, s string) bool {
+ for s != "" {
+ i := strings.IndexAny(s, " \t\r\n\f")
+ if i == -1 {
+ return s == val
+ }
+ if s[:i] == val {
+ return true
+ }
+ s = s[i+1:]
+ }
+ return false
+}
+
+// matches elements where the attribute named key equals val or starts with val plus a hyphen.
+func attributeDashMatch(key, val string, n *html.Node) bool {
+ return matchAttribute(n, key,
+ func(s string) bool {
+ if s == val {
+ return true
+ }
+ if len(s) <= len(val) {
+ return false
+ }
+ if s[:len(val)] == val && s[len(val)] == '-' {
+ return true
+ }
+ return false
+ })
+}
+
+// attributePrefixMatch returns a Selector that matches elements where
+// the attribute named key starts with val.
+func attributePrefixMatch(key, val string, n *html.Node) bool {
+ return matchAttribute(n, key,
+ func(s string) bool {
+ if strings.TrimSpace(s) == "" {
+ return false
+ }
+ return strings.HasPrefix(s, val)
+ })
+}
+
+// attributeSuffixMatch matches elements where
+// the attribute named key ends with val.
+func attributeSuffixMatch(key, val string, n *html.Node) bool {
+ return matchAttribute(n, key,
+ func(s string) bool {
+ if strings.TrimSpace(s) == "" {
+ return false
+ }
+ return strings.HasSuffix(s, val)
+ })
+}
+
+// attributeSubstringMatch matches nodes where
+// the attribute named key contains val.
+func attributeSubstringMatch(key, val string, n *html.Node) bool {
+ return matchAttribute(n, key,
+ func(s string) bool {
+ if strings.TrimSpace(s) == "" {
+ return false
+ }
+ return strings.Contains(s, val)
+ })
+}
+
+// attributeRegexMatch matches nodes where
+// the attribute named key matches the regular expression rx
+func attributeRegexMatch(key string, rx *regexp.Regexp, n *html.Node) bool {
+ return matchAttribute(n, key,
+ func(s string) bool {
+ return rx.MatchString(s)
+ })
+}
+
+func (c attrSelector) Specificity() Specificity {
+ return Specificity{0, 1, 0}
+}
+
+// ---------------- Pseudo class selectors ----------------
+// we use severals concrete types of pseudo-class selectors
+
+type relativePseudoClassSelector struct {
+ name string // one of "not", "has", "haschild"
+ match SelectorGroup
+}
+
+func (s relativePseudoClassSelector) Match(n *html.Node) bool {
+ if n.Type != html.ElementNode {
+ return false
+ }
+ switch s.name {
+ case "not":
+ // matches elements that do not match a.
+ return !s.match.Match(n)
+ case "has":
+ // matches elements with any descendant that matches a.
+ return hasDescendantMatch(n, s.match)
+ case "haschild":
+ // matches elements with a child that matches a.
+ return hasChildMatch(n, s.match)
+ default:
+ panic(fmt.Sprintf("unsupported relative pseudo class selector : %s", s.name))
+ }
+}
+
+// hasChildMatch returns whether n has any child that matches a.
+func hasChildMatch(n *html.Node, a Matcher) bool {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if a.Match(c) {
+ return true
+ }
+ }
+ return false
+}
+
+// hasDescendantMatch performs a depth-first search of n's descendants,
+// testing whether any of them match a. It returns true as soon as a match is
+// found, or false if no match is found.
+func hasDescendantMatch(n *html.Node, a Matcher) bool {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if a.Match(c) || (c.Type == html.ElementNode && hasDescendantMatch(c, a)) {
+ return true
+ }
+ }
+ return false
+}
+
+// Specificity returns the specificity of the most specific selectors
+// in the pseudo-class arguments.
+// See https://www.w3.org/TR/selectors/#specificity-rules
+func (s relativePseudoClassSelector) Specificity() Specificity {
+ var max Specificity
+ for _, sel := range s.match {
+ newSpe := sel.Specificity()
+ if max.Less(newSpe) {
+ max = newSpe
+ }
+ }
+ return max
+}
+
+type containsPseudoClassSelector struct {
+ own bool
+ value string
+}
+
+func (s containsPseudoClassSelector) Match(n *html.Node) bool {
+ var text string
+ if s.own {
+ // matches nodes that directly contain the given text
+ text = strings.ToLower(nodeOwnText(n))
+ } else {
+ // matches nodes that contain the given text.
+ text = strings.ToLower(nodeText(n))
+ }
+ return strings.Contains(text, s.value)
+}
+
+func (s containsPseudoClassSelector) Specificity() Specificity {
+ return Specificity{0, 1, 0}
+}
+
+type regexpPseudoClassSelector struct {
+ own bool
+ regexp *regexp.Regexp
+}
+
+func (s regexpPseudoClassSelector) Match(n *html.Node) bool {
+ var text string
+ if s.own {
+ // matches nodes whose text directly matches the specified regular expression
+ text = nodeOwnText(n)
+ } else {
+ // matches nodes whose text matches the specified regular expression
+ text = nodeText(n)
+ }
+ return s.regexp.MatchString(text)
+}
+
+// writeNodeText writes the text contained in n and its descendants to b.
+func writeNodeText(n *html.Node, b *bytes.Buffer) {
+ switch n.Type {
+ case html.TextNode:
+ b.WriteString(n.Data)
+ case html.ElementNode:
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ writeNodeText(c, b)
+ }
+ }
+}
+
+// nodeText returns the text contained in n and its descendants.
+func nodeText(n *html.Node) string {
+ var b bytes.Buffer
+ writeNodeText(n, &b)
+ return b.String()
+}
+
+// nodeOwnText returns the contents of the text nodes that are direct
+// children of n.
+func nodeOwnText(n *html.Node) string {
+ var b bytes.Buffer
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type == html.TextNode {
+ b.WriteString(c.Data)
+ }
+ }
+ return b.String()
+}
+
+func (s regexpPseudoClassSelector) Specificity() Specificity {
+ return Specificity{0, 1, 0}
+}
+
+type nthPseudoClassSelector struct {
+ a, b int
+ last, ofType bool
+}
+
+func (s nthPseudoClassSelector) Match(n *html.Node) bool {
+ if s.a == 0 {
+ if s.last {
+ return simpleNthLastChildMatch(s.b, s.ofType, n)
+ } else {
+ return simpleNthChildMatch(s.b, s.ofType, n)
+ }
+ }
+ return nthChildMatch(s.a, s.b, s.last, s.ofType, n)
+}
+
+// nthChildMatch implements :nth-child(an+b).
+// If last is true, implements :nth-last-child instead.
+// If ofType is true, implements :nth-of-type instead.
+func nthChildMatch(a, b int, last, ofType bool, n *html.Node) bool {
+ if n.Type != html.ElementNode {
+ return false
+ }
+
+ parent := n.Parent
+ if parent == nil {
+ return false
+ }
+
+ if parent.Type == html.DocumentNode {
+ return false
+ }
+
+ i := -1
+ count := 0
+ for c := parent.FirstChild; c != nil; c = c.NextSibling {
+ if (c.Type != html.ElementNode) || (ofType && c.Data != n.Data) {
+ continue
+ }
+ count++
+ if c == n {
+ i = count
+ if !last {
+ break
+ }
+ }
+ }
+
+ if i == -1 {
+ // This shouldn't happen, since n should always be one of its parent's children.
+ return false
+ }
+
+ if last {
+ i = count - i + 1
+ }
+
+ i -= b
+ if a == 0 {
+ return i == 0
+ }
+
+ return i%a == 0 && i/a >= 0
+}
+
+// simpleNthChildMatch implements :nth-child(b).
+// If ofType is true, implements :nth-of-type instead.
+func simpleNthChildMatch(b int, ofType bool, n *html.Node) bool {
+ if n.Type != html.ElementNode {
+ return false
+ }
+
+ parent := n.Parent
+ if parent == nil {
+ return false
+ }
+
+ if parent.Type == html.DocumentNode {
+ return false
+ }
+
+ count := 0
+ for c := parent.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type != html.ElementNode || (ofType && c.Data != n.Data) {
+ continue
+ }
+ count++
+ if c == n {
+ return count == b
+ }
+ if count >= b {
+ return false
+ }
+ }
+ return false
+}
+
+// simpleNthLastChildMatch implements :nth-last-child(b).
+// If ofType is true, implements :nth-last-of-type instead.
+func simpleNthLastChildMatch(b int, ofType bool, n *html.Node) bool {
+ if n.Type != html.ElementNode {
+ return false
+ }
+
+ parent := n.Parent
+ if parent == nil {
+ return false
+ }
+
+ if parent.Type == html.DocumentNode {
+ return false
+ }
+
+ count := 0
+ for c := parent.LastChild; c != nil; c = c.PrevSibling {
+ if c.Type != html.ElementNode || (ofType && c.Data != n.Data) {
+ continue
+ }
+ count++
+ if c == n {
+ return count == b
+ }
+ if count >= b {
+ return false
+ }
+ }
+ return false
+}
+
+// Specificity for nth-child pseudo-class.
+// Does not support a list of selectors
+func (s nthPseudoClassSelector) Specificity() Specificity {
+ return Specificity{0, 1, 0}
+}
+
+type onlyChildPseudoClassSelector struct {
+ ofType bool
+}
+
+// Match implements :only-child.
+// If `ofType` is true, it implements :only-of-type instead.
+func (s onlyChildPseudoClassSelector) Match(n *html.Node) bool {
+ if n.Type != html.ElementNode {
+ return false
+ }
+
+ parent := n.Parent
+ if parent == nil {
+ return false
+ }
+
+ if parent.Type == html.DocumentNode {
+ return false
+ }
+
+ count := 0
+ for c := parent.FirstChild; c != nil; c = c.NextSibling {
+ if (c.Type != html.ElementNode) || (s.ofType && c.Data != n.Data) {
+ continue
+ }
+ count++
+ if count > 1 {
+ return false
+ }
+ }
+
+ return count == 1
+}
+
+func (s onlyChildPseudoClassSelector) Specificity() Specificity {
+ return Specificity{0, 1, 0}
+}
+
+type inputPseudoClassSelector struct{}
+
+// Matches input, select, textarea and button elements.
+func (s inputPseudoClassSelector) Match(n *html.Node) bool {
+ return n.Type == html.ElementNode && (n.Data == "input" || n.Data == "select" || n.Data == "textarea" || n.Data == "button")
+}
+
+func (s inputPseudoClassSelector) Specificity() Specificity {
+ return Specificity{0, 1, 0}
+}
+
+type emptyElementPseudoClassSelector struct{}
+
+// Matches empty elements.
+func (s emptyElementPseudoClassSelector) Match(n *html.Node) bool {
+ if n.Type != html.ElementNode {
+ return false
+ }
+
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ switch c.Type {
+ case html.ElementNode, html.TextNode:
+ return false
+ }
+ }
+
+ return true
+}
+
+func (s emptyElementPseudoClassSelector) Specificity() Specificity {
+ return Specificity{0, 1, 0}
+}
+
+type rootPseudoClassSelector struct{}
+
+// Match implements :root
+func (s rootPseudoClassSelector) Match(n *html.Node) bool {
+ if n.Type != html.ElementNode {
+ return false
+ }
+ if n.Parent == nil {
+ return false
+ }
+ return n.Parent.Type == html.DocumentNode
+}
+
+func (s rootPseudoClassSelector) Specificity() Specificity {
+ return Specificity{0, 1, 0}
+}
+
+type compoundSelector struct {
+ selectors []Sel
+}
+
+// Matches elements if each sub-selectors matches.
+func (t compoundSelector) Match(n *html.Node) bool {
+ if len(t.selectors) == 0 {
+ return n.Type == html.ElementNode
+ }
+
+ for _, sel := range t.selectors {
+ if !sel.Match(n) {
+ return false
+ }
+ }
+ return true
+}
+
+func (s compoundSelector) Specificity() Specificity {
+ var out Specificity
+ for _, sel := range s.selectors {
+ out = out.Add(sel.Specificity())
+ }
+ return out
+}
+
+type combinedSelector struct {
+ first Sel
+ combinator byte
+ second Sel
+}
+
+func (t combinedSelector) Match(n *html.Node) bool {
+ if t.first == nil {
+ return false // maybe we should panic
+ }
+ switch t.combinator {
+ case 0:
+ return t.first.Match(n)
+ case ' ':
+ return descendantMatch(t.first, t.second, n)
+ case '>':
+ return childMatch(t.first, t.second, n)
+ case '+':
+ return siblingMatch(t.first, t.second, true, n)
+ case '~':
+ return siblingMatch(t.first, t.second, false, n)
+ default:
+ panic("unknown combinator")
+ }
+}
+
+// matches an element if it matches d and has an ancestor that matches a.
+func descendantMatch(a, d Matcher, n *html.Node) bool {
+ if !d.Match(n) {
+ return false
+ }
+
+ for p := n.Parent; p != nil; p = p.Parent {
+ if a.Match(p) {
+ return true
+ }
+ }
+
+ return false
+}
+
+// matches an element if it matches d and its parent matches a.
+func childMatch(a, d Matcher, n *html.Node) bool {
+ return d.Match(n) && n.Parent != nil && a.Match(n.Parent)
+}
+
+// matches an element if it matches s2 and is preceded by an element that matches s1.
+// If adjacent is true, the sibling must be immediately before the element.
+func siblingMatch(s1, s2 Matcher, adjacent bool, n *html.Node) bool {
+ if !s2.Match(n) {
+ return false
+ }
+
+ if adjacent {
+ for n = n.PrevSibling; n != nil; n = n.PrevSibling {
+ if n.Type == html.TextNode || n.Type == html.CommentNode {
+ continue
+ }
+ return s1.Match(n)
+ }
+ return false
+ }
+
+ // Walk backwards looking for element that matches s1
+ for c := n.PrevSibling; c != nil; c = c.PrevSibling {
+ if s1.Match(c) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (s combinedSelector) Specificity() Specificity {
+ spec := s.first.Specificity()
+ if s.second != nil {
+ spec = spec.Add(s.second.Specificity())
+ }
+ return spec
+}
+
+// A SelectorGroup is a list of selectors, which matches if any of the
+// individual selectors matches.
+type SelectorGroup []Sel
+
+// Match returns true if the node matches one of the single selectors.
+func (s SelectorGroup) Match(n *html.Node) bool {
+ for _, sel := range s {
+ if sel.Match(n) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/vendor/github.com/andybalholm/cascadia/specificity.go b/vendor/github.com/andybalholm/cascadia/specificity.go
new file mode 100644
index 00000000..8db864f9
--- /dev/null
+++ b/vendor/github.com/andybalholm/cascadia/specificity.go
@@ -0,0 +1,26 @@
+package cascadia
+
+// Specificity is the CSS specificity as defined in
+// https://www.w3.org/TR/selectors/#specificity-rules
+// with the convention Specificity = [A,B,C].
+type Specificity [3]int
+
+// returns `true` if s < other (strictly), false otherwise
+func (s Specificity) Less(other Specificity) bool {
+ for i := range s {
+ if s[i] < other[i] {
+ return true
+ }
+ if s[i] > other[i] {
+ return false
+ }
+ }
+ return false
+}
+
+func (s Specificity) Add(other Specificity) Specificity {
+ for i, sp := range other {
+ s[i] += sp
+ }
+ return s
+}
diff --git a/vendor/github.com/vincent-petithory/dataurl/LICENSE b/vendor/github.com/vincent-petithory/dataurl/LICENSE
new file mode 100644
index 00000000..ae6cb62b
--- /dev/null
+++ b/vendor/github.com/vincent-petithory/dataurl/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Vincent Petithory
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/github.com/vincent-petithory/dataurl/README.md b/vendor/github.com/vincent-petithory/dataurl/README.md
new file mode 100644
index 00000000..1ac59ad2
--- /dev/null
+++ b/vendor/github.com/vincent-petithory/dataurl/README.md
@@ -0,0 +1,81 @@
+# Data URL Schemes for Go [](https://app.wercker.com/project/bykey/6f9a2e144dfcc59e862c52459b452928) [](https://godoc.org/github.com/vincent-petithory/dataurl)
+
+This package parses and generates Data URL Schemes for the Go language, according to [RFC 2397](http://tools.ietf.org/html/rfc2397).
+
+Data URLs are small chunks of data commonly used in browsers to display inline data,
+typically like small images, or when you use the FileReader API of the browser.
+
+Common use-cases:
+
+ * generate a data URL out of a `string`, `[]byte`, `io.Reader` for inclusion in HTML templates,
+ * parse a data URL sent by a browser in a http.Handler, and do something with the data (save to disk, etc.)
+ * ...
+
+Install the package with:
+~~~
+go get github.com/vincent-petithory/dataurl
+~~~
+
+## Usage
+
+~~~ go
+package main
+
+import (
+ "github.com/vincent-petithory/dataurl"
+ "fmt"
+)
+
+func main() {
+ dataURL, err := dataurl.DecodeString(`data:text/plain;charset=utf-8;base64,aGV5YQ==`)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ fmt.Printf("content type: %s, data: %s\n", dataURL.MediaType.ContentType(), string(dataURL.Data))
+ // Output: content type: text/plain, data: heya
+}
+~~~
+
+From a `http.Handler`:
+
+~~~ go
+func handleDataURLUpload(w http.ResponseWriter, r *http.Request) {
+ dataURL, err := dataurl.Decode(r.Body)
+ defer r.Body.Close()
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusBadRequest)
+ return
+ }
+ if dataURL.ContentType() == "image/png" {
+ ioutil.WriteFile("image.png", dataURL.Data, 0644)
+ } else {
+ http.Error(w, "not a png", http.StatusBadRequest)
+ }
+}
+~~~
+
+## Command
+
+For convenience, a `dataurl` command is provided to encode/decode dataurl streams.
+
+~~~
+dataurl - Encode or decode dataurl data and print to standard output
+
+Usage: dataurl [OPTION]... [FILE]
+
+ dataurl encodes or decodes FILE or standard input if FILE is - or omitted, and prints to standard output.
+ Unless -mimetype is used, when FILE is specified, dataurl will attempt to detect its mimetype using Go's mime.TypeByExtension (http://golang.org/pkg/mime/#TypeByExtension). If this fails or data is read from STDIN, the mimetype will default to application/octet-stream.
+
+Options:
+ -a=false: encode data using ascii instead of base64
+ -ascii=false: encode data using ascii instead of base64
+ -d=false: decode data instead of encoding
+ -decode=false: decode data instead of encoding
+ -m="": force the mimetype of the data to encode to this value
+ -mimetype="": force the mimetype of the data to encode to this value
+~~~
+
+## Contributing
+
+Feel free to file an issue/make a pull request if you find any bug, or want to suggest enhancements.
diff --git a/vendor/github.com/vincent-petithory/dataurl/dataurl.go b/vendor/github.com/vincent-petithory/dataurl/dataurl.go
new file mode 100644
index 00000000..7a9fe67e
--- /dev/null
+++ b/vendor/github.com/vincent-petithory/dataurl/dataurl.go
@@ -0,0 +1,291 @@
+package dataurl
+
+import (
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+const (
+ // EncodingBase64 is base64 encoding for the data url
+ EncodingBase64 = "base64"
+ // EncodingASCII is ascii encoding for the data url
+ EncodingASCII = "ascii"
+)
+
+func defaultMediaType() MediaType {
+ return MediaType{
+ "text",
+ "plain",
+ map[string]string{"charset": "US-ASCII"},
+ }
+}
+
+// MediaType is the combination of a media type, a media subtype
+// and optional parameters.
+type MediaType struct {
+ Type string
+ Subtype string
+ Params map[string]string
+}
+
+// ContentType returns the content type of the dataurl's data, in the form type/subtype.
+func (mt *MediaType) ContentType() string {
+ return fmt.Sprintf("%s/%s", mt.Type, mt.Subtype)
+}
+
+// String implements the Stringer interface.
+//
+// Params values are escaped with the Escape function, rather than in a quoted string.
+func (mt *MediaType) String() string {
+ var (
+ buf bytes.Buffer
+ keys = make([]string, len(mt.Params))
+ i int
+ )
+ for k := range mt.Params {
+ keys[i] = k
+ i++
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ v := mt.Params[k]
+ fmt.Fprintf(&buf, ";%s=%s", k, EscapeString(v))
+ }
+ return mt.ContentType() + (&buf).String()
+}
+
+// DataURL is the combination of a MediaType describing the type of its Data.
+type DataURL struct {
+ MediaType
+ Encoding string
+ Data []byte
+}
+
+// New returns a new DataURL initialized with data and
+// a MediaType parsed from mediatype and paramPairs.
+// mediatype must be of the form "type/subtype" or it will panic.
+// paramPairs must have an even number of elements or it will panic.
+// For more complex DataURL, initialize a DataURL struct.
+// The DataURL is initialized with base64 encoding.
+func New(data []byte, mediatype string, paramPairs ...string) *DataURL {
+ parts := strings.Split(mediatype, "/")
+ if len(parts) != 2 {
+ panic("dataurl: invalid mediatype")
+ }
+
+ nParams := len(paramPairs)
+ if nParams%2 != 0 {
+ panic("dataurl: requires an even number of param pairs")
+ }
+ params := make(map[string]string)
+ for i := 0; i < nParams; i += 2 {
+ params[paramPairs[i]] = paramPairs[i+1]
+ }
+
+ mt := MediaType{
+ parts[0],
+ parts[1],
+ params,
+ }
+ return &DataURL{
+ MediaType: mt,
+ Encoding: EncodingBase64,
+ Data: data,
+ }
+}
+
+// String implements the Stringer interface.
+//
+// Note: it doesn't guarantee the returned string is equal to
+// the initial source string that was used to create this DataURL.
+// The reasons for that are:
+// * Insertion of default values for MediaType that were maybe not in the initial string,
+// * Various ways to encode the MediaType parameters (quoted string or url encoded string, the latter is used),
+func (du *DataURL) String() string {
+ var buf bytes.Buffer
+ du.WriteTo(&buf)
+ return (&buf).String()
+}
+
+// WriteTo implements the WriterTo interface.
+// See the note about String().
+func (du *DataURL) WriteTo(w io.Writer) (n int64, err error) {
+ var ni int
+ ni, _ = fmt.Fprint(w, "data:")
+ n += int64(ni)
+
+ ni, _ = fmt.Fprint(w, du.MediaType.String())
+ n += int64(ni)
+
+ if du.Encoding == EncodingBase64 {
+ ni, _ = fmt.Fprint(w, ";base64")
+ n += int64(ni)
+ }
+
+ ni, _ = fmt.Fprint(w, ",")
+ n += int64(ni)
+
+ if du.Encoding == EncodingBase64 {
+ encoder := base64.NewEncoder(base64.StdEncoding, w)
+ ni, err = encoder.Write(du.Data)
+ if err != nil {
+ return
+ }
+ encoder.Close()
+ } else if du.Encoding == EncodingASCII {
+ ni, _ = fmt.Fprint(w, Escape(du.Data))
+ n += int64(ni)
+ } else {
+ err = fmt.Errorf("dataurl: invalid encoding %s", du.Encoding)
+ return
+ }
+
+ return
+}
+
+// UnmarshalText decodes a Data URL string and sets it to *du
+func (du *DataURL) UnmarshalText(text []byte) error {
+ decoded, err := DecodeString(string(text))
+ if err != nil {
+ return err
+ }
+ *du = *decoded
+ return nil
+}
+
+// MarshalText writes du as a Data URL
+func (du *DataURL) MarshalText() ([]byte, error) {
+ buf := bytes.NewBuffer(nil)
+ if _, err := du.WriteTo(buf); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+type encodedDataReader func(string) ([]byte, error)
+
+var asciiDataReader encodedDataReader = func(s string) ([]byte, error) {
+ us, err := Unescape(s)
+ if err != nil {
+ return nil, err
+ }
+ return []byte(us), nil
+}
+
+var base64DataReader encodedDataReader = func(s string) ([]byte, error) {
+ data, err := base64.StdEncoding.DecodeString(s)
+ if err != nil {
+ return nil, err
+ }
+ return []byte(data), nil
+}
+
+type parser struct {
+ du *DataURL
+ l *lexer
+ currentAttr string
+ unquoteParamVal bool
+ encodedDataReaderFn encodedDataReader
+}
+
+func (p *parser) parse() error {
+ for item := range p.l.items {
+ switch item.t {
+ case itemError:
+ return errors.New(item.String())
+ case itemMediaType:
+ p.du.MediaType.Type = item.val
+ // Should we clear the default
+ // "charset" parameter at this point?
+ delete(p.du.MediaType.Params, "charset")
+ case itemMediaSubType:
+ p.du.MediaType.Subtype = item.val
+ case itemParamAttr:
+ p.currentAttr = item.val
+ case itemLeftStringQuote:
+ p.unquoteParamVal = true
+ case itemParamVal:
+ val := item.val
+ if p.unquoteParamVal {
+ p.unquoteParamVal = false
+ us, err := strconv.Unquote("\"" + val + "\"")
+ if err != nil {
+ return err
+ }
+ val = us
+ } else {
+ us, err := UnescapeToString(val)
+ if err != nil {
+ return err
+ }
+ val = us
+ }
+ p.du.MediaType.Params[p.currentAttr] = val
+ case itemBase64Enc:
+ p.du.Encoding = EncodingBase64
+ p.encodedDataReaderFn = base64DataReader
+ case itemDataComma:
+ if p.encodedDataReaderFn == nil {
+ p.encodedDataReaderFn = asciiDataReader
+ }
+ case itemData:
+ reader, err := p.encodedDataReaderFn(item.val)
+ if err != nil {
+ return err
+ }
+ p.du.Data = reader
+ case itemEOF:
+ if p.du.Data == nil {
+ p.du.Data = []byte("")
+ }
+ return nil
+ }
+ }
+ panic("EOF not found")
+}
+
+// DecodeString decodes a Data URL scheme string.
+func DecodeString(s string) (*DataURL, error) {
+ du := &DataURL{
+ MediaType: defaultMediaType(),
+ Encoding: EncodingASCII,
+ }
+
+ parser := &parser{
+ du: du,
+ l: lex(s),
+ }
+ if err := parser.parse(); err != nil {
+ return nil, err
+ }
+ return du, nil
+}
+
+// Decode decodes a Data URL scheme from a io.Reader.
+func Decode(r io.Reader) (*DataURL, error) {
+ data, err := ioutil.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+ return DecodeString(string(data))
+}
+
+// EncodeBytes encodes the data bytes into a Data URL string, using base 64 encoding.
+//
+// The media type of data is detected using http.DetectContentType.
+func EncodeBytes(data []byte) string {
+ mt := http.DetectContentType(data)
+ // http.DetectContentType may add spurious spaces between ; and a parameter.
+ // The canonical way is to not have them.
+ cleanedMt := strings.Replace(mt, "; ", ";", -1)
+
+ return New(data, cleanedMt).String()
+}
diff --git a/vendor/github.com/vincent-petithory/dataurl/doc.go b/vendor/github.com/vincent-petithory/dataurl/doc.go
new file mode 100644
index 00000000..56461d04
--- /dev/null
+++ b/vendor/github.com/vincent-petithory/dataurl/doc.go
@@ -0,0 +1,28 @@
+/*
+Package dataurl parses Data URL Schemes
+according to RFC 2397
+(http://tools.ietf.org/html/rfc2397).
+
+Data URLs are small chunks of data commonly used in browsers to display inline data,
+typically like small images, or when you use the FileReader API of the browser.
+
+A dataurl looks like:
+
+ data:text/plain;charset=utf-8,A%20brief%20note
+
+Or, with base64 encoding:
+
+ data:image/vnd.microsoft.icon;name=golang%20favicon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAD///8AVE44//7hdv/+4Xb//uF2//7hdv/+4Xb//uF2//7hdv/+4Xb//uF2//7hdv/+4Xb/
+ /uF2/1ROOP////8A////AFROOP/+4Xb//uF2//7hdv/+4Xb//uF2//7hdv/+4Xb//uF2//7hdv/+
+ ...
+ /6CcjP97c07/e3NO/1dOMf9BOiX/TkUn/2VXLf97c07/e3NO/6CcjP/h4uX/////AP///wD///8A
+ ////AP///wD///8A////AP///wDq6/H/3N/j/9fZ3f/q6/H/////AP///wD///8A////AP///wD/
+ //8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+ AAAAAAAAAAAAAA==
+
+Common functions are Decode and DecodeString to obtain a DataURL,
+and DataURL.String() and DataURL.WriteTo to generate a Data URL string.
+
+*/
+package dataurl
diff --git a/vendor/github.com/vincent-petithory/dataurl/lex.go b/vendor/github.com/vincent-petithory/dataurl/lex.go
new file mode 100644
index 00000000..1a8717f5
--- /dev/null
+++ b/vendor/github.com/vincent-petithory/dataurl/lex.go
@@ -0,0 +1,521 @@
+package dataurl
+
+import (
+ "fmt"
+ "strings"
+ "unicode"
+ "unicode/utf8"
+)
+
+type item struct {
+ t itemType
+ val string
+}
+
+func (i item) String() string {
+ switch i.t {
+ case itemEOF:
+ return "EOF"
+ case itemError:
+ return i.val
+ }
+ if len(i.val) > 10 {
+ return fmt.Sprintf("%.10q...", i.val)
+ }
+ return fmt.Sprintf("%q", i.val)
+}
+
+type itemType int
+
+const (
+ itemError itemType = iota
+ itemEOF
+
+ itemDataPrefix
+
+ itemMediaType
+ itemMediaSep
+ itemMediaSubType
+ itemParamSemicolon
+ itemParamAttr
+ itemParamEqual
+ itemLeftStringQuote
+ itemRightStringQuote
+ itemParamVal
+
+ itemBase64Enc
+
+ itemDataComma
+ itemData
+)
+
+const eof rune = -1
+
+func isTokenRune(r rune) bool {
+ return r <= unicode.MaxASCII &&
+ !unicode.IsControl(r) &&
+ !unicode.IsSpace(r) &&
+ !isTSpecialRune(r)
+}
+
+func isTSpecialRune(r rune) bool {
+ return r == '(' ||
+ r == ')' ||
+ r == '<' ||
+ r == '>' ||
+ r == '@' ||
+ r == ',' ||
+ r == ';' ||
+ r == ':' ||
+ r == '\\' ||
+ r == '"' ||
+ r == '/' ||
+ r == '[' ||
+ r == ']' ||
+ r == '?' ||
+ r == '='
+}
+
+// See http://tools.ietf.org/html/rfc2045
+// This doesn't include extension-token case
+// as it's handled separatly
+func isDiscreteType(s string) bool {
+ if strings.HasPrefix(s, "text") ||
+ strings.HasPrefix(s, "image") ||
+ strings.HasPrefix(s, "audio") ||
+ strings.HasPrefix(s, "video") ||
+ strings.HasPrefix(s, "application") {
+ return true
+ }
+ return false
+}
+
+// See http://tools.ietf.org/html/rfc2045
+// This doesn't include extension-token case
+// as it's handled separatly
+func isCompositeType(s string) bool {
+ if strings.HasPrefix(s, "message") ||
+ strings.HasPrefix(s, "multipart") {
+ return true
+ }
+ return false
+}
+
+func isURLCharRune(r rune) bool {
+ // We're a bit permissive here,
+ // by not including '%' in delims
+ // This is okay, since url unescaping will validate
+ // that later in the parser.
+ return r <= unicode.MaxASCII &&
+ !(r >= 0x00 && r <= 0x1F) && r != 0x7F && /* control */
+ // delims
+ r != ' ' &&
+ r != '<' &&
+ r != '>' &&
+ r != '#' &&
+ r != '"' &&
+ // unwise
+ r != '{' &&
+ r != '}' &&
+ r != '|' &&
+ r != '\\' &&
+ r != '^' &&
+ r != '[' &&
+ r != ']' &&
+ r != '`'
+}
+
+func isBase64Rune(r rune) bool {
+ return (r >= 'a' && r <= 'z') ||
+ (r >= 'A' && r <= 'Z') ||
+ (r >= '0' && r <= '9') ||
+ r == '+' ||
+ r == '/' ||
+ r == '=' ||
+ r == '\n'
+}
+
+type stateFn func(*lexer) stateFn
+
+// lexer lexes the data URL scheme input string.
+// The implementation is from the text/template/parser package.
+type lexer struct {
+ input string
+ start int
+ pos int
+ width int
+ seenBase64Item bool
+ items chan item
+}
+
+func (l *lexer) run() {
+ for state := lexBeforeDataPrefix; state != nil; {
+ state = state(l)
+ }
+ close(l.items)
+}
+
+func (l *lexer) emit(t itemType) {
+ l.items <- item{t, l.input[l.start:l.pos]}
+ l.start = l.pos
+}
+
+func (l *lexer) next() (r rune) {
+ if l.pos >= len(l.input) {
+ l.width = 0
+ return eof
+ }
+ r, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
+ l.pos += l.width
+ return r
+}
+
+func (l *lexer) backup() {
+ l.pos -= l.width
+}
+
+func (l *lexer) ignore() {
+ l.start = l.pos
+}
+
+func (l *lexer) errorf(format string, args ...interface{}) stateFn {
+ l.items <- item{itemError, fmt.Sprintf(format, args...)}
+ return nil
+}
+
+func lex(input string) *lexer {
+ l := &lexer{
+ input: input,
+ items: make(chan item),
+ }
+ go l.run() // Concurrently run state machine.
+ return l
+}
+
+const (
+ dataPrefix = "data:"
+ mediaSep = '/'
+ paramSemicolon = ';'
+ paramEqual = '='
+ dataComma = ','
+)
+
+// start lexing by detecting data prefix
+func lexBeforeDataPrefix(l *lexer) stateFn {
+ if strings.HasPrefix(l.input[l.pos:], dataPrefix) {
+ return lexDataPrefix
+ }
+ return l.errorf("missing data prefix")
+}
+
+// lex data prefix
+func lexDataPrefix(l *lexer) stateFn {
+ l.pos += len(dataPrefix)
+ l.emit(itemDataPrefix)
+ return lexAfterDataPrefix
+}
+
+// lex what's after data prefix.
+// it can be the media type/subtype separator,
+// the base64 encoding, or the comma preceding the data
+func lexAfterDataPrefix(l *lexer) stateFn {
+ switch r := l.next(); {
+ case r == paramSemicolon:
+ l.backup()
+ return lexParamSemicolon
+ case r == dataComma:
+ l.backup()
+ return lexDataComma
+ case r == eof:
+ return l.errorf("missing comma before data")
+ case r == 'x' || r == 'X':
+ if l.next() == '-' {
+ return lexXTokenMediaType
+ }
+ return lexInDiscreteMediaType
+ case isTokenRune(r):
+ return lexInDiscreteMediaType
+ default:
+ return l.errorf("invalid character after data prefix")
+ }
+}
+
+func lexXTokenMediaType(l *lexer) stateFn {
+ for {
+ switch r := l.next(); {
+ case r == mediaSep:
+ l.backup()
+ return lexMediaType
+ case r == eof:
+ return l.errorf("missing media type slash")
+ case isTokenRune(r):
+ default:
+ return l.errorf("invalid character for media type")
+ }
+ }
+}
+
+func lexInDiscreteMediaType(l *lexer) stateFn {
+ for {
+ switch r := l.next(); {
+ case r == mediaSep:
+ l.backup()
+ // check it's valid discrete type
+ if !isDiscreteType(l.input[l.start:l.pos]) &&
+ !isCompositeType(l.input[l.start:l.pos]) {
+ return l.errorf("invalid media type")
+ }
+ return lexMediaType
+ case r == eof:
+ return l.errorf("missing media type slash")
+ case isTokenRune(r):
+ default:
+ return l.errorf("invalid character for media type")
+ }
+ }
+}
+
+func lexMediaType(l *lexer) stateFn {
+ if l.pos > l.start {
+ l.emit(itemMediaType)
+ }
+ return lexMediaSep
+}
+
+func lexMediaSep(l *lexer) stateFn {
+ l.next()
+ l.emit(itemMediaSep)
+ return lexAfterMediaSep
+}
+
+func lexAfterMediaSep(l *lexer) stateFn {
+ for {
+ switch r := l.next(); {
+ case r == paramSemicolon || r == dataComma:
+ l.backup()
+ return lexMediaSubType
+ case r == eof:
+ return l.errorf("incomplete media type")
+ case isTokenRune(r):
+ default:
+ return l.errorf("invalid character for media subtype")
+ }
+ }
+}
+
+func lexMediaSubType(l *lexer) stateFn {
+ if l.pos > l.start {
+ l.emit(itemMediaSubType)
+ }
+ return lexAfterMediaSubType
+}
+
+func lexAfterMediaSubType(l *lexer) stateFn {
+ switch r := l.next(); {
+ case r == paramSemicolon:
+ l.backup()
+ return lexParamSemicolon
+ case r == dataComma:
+ l.backup()
+ return lexDataComma
+ case r == eof:
+ return l.errorf("missing comma before data")
+ default:
+ return l.errorf("expected semicolon or comma")
+ }
+}
+
+func lexParamSemicolon(l *lexer) stateFn {
+ l.next()
+ l.emit(itemParamSemicolon)
+ return lexAfterParamSemicolon
+}
+
+func lexAfterParamSemicolon(l *lexer) stateFn {
+ switch r := l.next(); {
+ case r == eof:
+ return l.errorf("unterminated parameter sequence")
+ case r == paramEqual || r == dataComma:
+ return l.errorf("unterminated parameter sequence")
+ case isTokenRune(r):
+ l.backup()
+ return lexInParamAttr
+ default:
+ return l.errorf("invalid character for parameter attribute")
+ }
+}
+
+func lexBase64Enc(l *lexer) stateFn {
+ if l.pos > l.start {
+ if v := l.input[l.start:l.pos]; v != "base64" {
+ return l.errorf("expected base64, got %s", v)
+ }
+ l.seenBase64Item = true
+ l.emit(itemBase64Enc)
+ }
+ return lexDataComma
+}
+
+func lexInParamAttr(l *lexer) stateFn {
+ for {
+ switch r := l.next(); {
+ case r == paramEqual:
+ l.backup()
+ return lexParamAttr
+ case r == dataComma:
+ l.backup()
+ return lexBase64Enc
+ case r == eof:
+ return l.errorf("unterminated parameter sequence")
+ case isTokenRune(r):
+ default:
+ return l.errorf("invalid character for parameter attribute")
+ }
+ }
+}
+
+func lexParamAttr(l *lexer) stateFn {
+ if l.pos > l.start {
+ l.emit(itemParamAttr)
+ }
+ return lexParamEqual
+}
+
+func lexParamEqual(l *lexer) stateFn {
+ l.next()
+ l.emit(itemParamEqual)
+ return lexAfterParamEqual
+}
+
+func lexAfterParamEqual(l *lexer) stateFn {
+ switch r := l.next(); {
+ case r == '"':
+ l.emit(itemLeftStringQuote)
+ return lexInQuotedStringParamVal
+ case r == eof:
+ return l.errorf("missing comma before data")
+ case isTokenRune(r):
+ return lexInParamVal
+ default:
+ return l.errorf("invalid character for parameter value")
+ }
+}
+
+func lexInQuotedStringParamVal(l *lexer) stateFn {
+ for {
+ switch r := l.next(); {
+ case r == eof:
+ return l.errorf("unclosed quoted string")
+ case r == '\\':
+ return lexEscapedChar
+ case r == '"':
+ l.backup()
+ return lexQuotedStringParamVal
+ case r <= unicode.MaxASCII:
+ default:
+ return l.errorf("invalid character for parameter value")
+ }
+ }
+}
+
+func lexEscapedChar(l *lexer) stateFn {
+ switch r := l.next(); {
+ case r <= unicode.MaxASCII:
+ return lexInQuotedStringParamVal
+ case r == eof:
+ return l.errorf("unexpected eof")
+ default:
+ return l.errorf("invalid escaped character")
+ }
+}
+
+func lexInParamVal(l *lexer) stateFn {
+ for {
+ switch r := l.next(); {
+ case r == paramSemicolon || r == dataComma:
+ l.backup()
+ return lexParamVal
+ case r == eof:
+ return l.errorf("missing comma before data")
+ case isTokenRune(r):
+ default:
+ return l.errorf("invalid character for parameter value")
+ }
+ }
+}
+
+func lexQuotedStringParamVal(l *lexer) stateFn {
+ if l.pos > l.start {
+ l.emit(itemParamVal)
+ }
+ l.next()
+ l.emit(itemRightStringQuote)
+ return lexAfterParamVal
+}
+
+func lexParamVal(l *lexer) stateFn {
+ if l.pos > l.start {
+ l.emit(itemParamVal)
+ }
+ return lexAfterParamVal
+}
+
+func lexAfterParamVal(l *lexer) stateFn {
+ switch r := l.next(); {
+ case r == paramSemicolon:
+ l.backup()
+ return lexParamSemicolon
+ case r == dataComma:
+ l.backup()
+ return lexDataComma
+ case r == eof:
+ return l.errorf("missing comma before data")
+ default:
+ return l.errorf("expected semicolon or comma")
+ }
+}
+
+func lexDataComma(l *lexer) stateFn {
+ l.next()
+ l.emit(itemDataComma)
+ if l.seenBase64Item {
+ return lexBase64Data
+ }
+ return lexData
+}
+
+func lexData(l *lexer) stateFn {
+Loop:
+ for {
+ switch r := l.next(); {
+ case r == eof:
+ break Loop
+ case isURLCharRune(r):
+ default:
+ return l.errorf("invalid data character")
+ }
+ }
+ if l.pos > l.start {
+ l.emit(itemData)
+ }
+ l.emit(itemEOF)
+ return nil
+}
+
+func lexBase64Data(l *lexer) stateFn {
+Loop:
+ for {
+ switch r := l.next(); {
+ case r == eof:
+ break Loop
+ case isBase64Rune(r):
+ default:
+ return l.errorf("invalid data character")
+ }
+ }
+ if l.pos > l.start {
+ l.emit(itemData)
+ }
+ l.emit(itemEOF)
+ return nil
+}
diff --git a/vendor/github.com/vincent-petithory/dataurl/rfc2396.go b/vendor/github.com/vincent-petithory/dataurl/rfc2396.go
new file mode 100644
index 00000000..e2ea0cac
--- /dev/null
+++ b/vendor/github.com/vincent-petithory/dataurl/rfc2396.go
@@ -0,0 +1,130 @@
+package dataurl
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "strings"
+)
+
+// Escape implements URL escaping, as defined in RFC 2397 (http://tools.ietf.org/html/rfc2397).
+// It differs a bit from net/url's QueryEscape and QueryUnescape, e.g how spaces are treated (+ instead of %20):
+//
+// Only ASCII chars are allowed. Reserved chars are escaped to their %xx form.
+// Unreserved chars are [a-z], [A-Z], [0-9], and -_.!~*\().
+func Escape(data []byte) string {
+ var buf = new(bytes.Buffer)
+ for _, b := range data {
+ switch {
+ case isUnreserved(b):
+ buf.WriteByte(b)
+ default:
+ fmt.Fprintf(buf, "%%%02X", b)
+ }
+ }
+ return buf.String()
+}
+
+// EscapeString is like Escape, but taking
+// a string as argument.
+func EscapeString(s string) string {
+ return Escape([]byte(s))
+}
+
+// isUnreserved return true
+// if the byte c is an unreserved char,
+// as defined in RFC 2396.
+func isUnreserved(c byte) bool {
+ return (c >= 'a' && c <= 'z') ||
+ (c >= 'A' && c <= 'Z') ||
+ (c >= '0' && c <= '9') ||
+ c == '-' ||
+ c == '_' ||
+ c == '.' ||
+ c == '!' ||
+ c == '~' ||
+ c == '*' ||
+ c == '\'' ||
+ c == '(' ||
+ c == ')'
+}
+
+func isHex(c byte) bool {
+ switch {
+ case c >= 'a' && c <= 'f':
+ return true
+ case c >= 'A' && c <= 'F':
+ return true
+ case c >= '0' && c <= '9':
+ return true
+ }
+ return false
+}
+
+// borrowed from net/url/url.go
+func unhex(c byte) byte {
+ switch {
+ case '0' <= c && c <= '9':
+ return c - '0'
+ case 'a' <= c && c <= 'f':
+ return c - 'a' + 10
+ case 'A' <= c && c <= 'F':
+ return c - 'A' + 10
+ }
+ return 0
+}
+
+// Unescape unescapes a character sequence
+// escaped with Escape(String?).
+func Unescape(s string) ([]byte, error) {
+ var buf = new(bytes.Buffer)
+ reader := strings.NewReader(s)
+
+ for {
+ r, size, err := reader.ReadRune()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ if size > 1 {
+ return nil, fmt.Errorf("rfc2396: non-ASCII char detected")
+ }
+
+ switch r {
+ case '%':
+ eb1, err := reader.ReadByte()
+ if err == io.EOF {
+ return nil, fmt.Errorf("rfc2396: unexpected end of unescape sequence")
+ }
+ if err != nil {
+ return nil, err
+ }
+ if !isHex(eb1) {
+ return nil, fmt.Errorf("rfc2396: invalid char 0x%x in unescape sequence", r)
+ }
+ eb0, err := reader.ReadByte()
+ if err == io.EOF {
+ return nil, fmt.Errorf("rfc2396: unexpected end of unescape sequence")
+ }
+ if err != nil {
+ return nil, err
+ }
+ if !isHex(eb0) {
+ return nil, fmt.Errorf("rfc2396: invalid char 0x%x in unescape sequence", r)
+ }
+ buf.WriteByte(unhex(eb0) + unhex(eb1)*16)
+ default:
+ buf.WriteByte(byte(r))
+ }
+ }
+ return buf.Bytes(), nil
+}
+
+// UnescapeToString is like Unescape, but returning
+// a string.
+func UnescapeToString(s string) (string, error) {
+ b, err := Unescape(s)
+ return string(b), err
+}
diff --git a/vendor/github.com/vincent-petithory/dataurl/wercker.yml b/vendor/github.com/vincent-petithory/dataurl/wercker.yml
new file mode 100644
index 00000000..3ab8084c
--- /dev/null
+++ b/vendor/github.com/vincent-petithory/dataurl/wercker.yml
@@ -0,0 +1 @@
+box: wercker/default
\ No newline at end of file
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 5458aebb..41ef9b46 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -4,6 +4,9 @@ github.com/42wim/go-gitter
github.com/Baozisoftware/qrcode-terminal-go
# github.com/Jeffail/gabs v1.1.1
github.com/Jeffail/gabs
+# github.com/JohannesKaufmann/html-to-markdown v1.2.0
+github.com/JohannesKaufmann/html-to-markdown
+github.com/JohannesKaufmann/html-to-markdown/escape
# github.com/Philipp15b/go-steam v1.0.1-0.20190816133340-b04c5a83c1c0
github.com/Philipp15b/go-steam
github.com/Philipp15b/go-steam/cryptoutil
@@ -15,6 +18,8 @@ github.com/Philipp15b/go-steam/protocol/steamlang
github.com/Philipp15b/go-steam/rwu
github.com/Philipp15b/go-steam/socialcache
github.com/Philipp15b/go-steam/steamid
+# github.com/PuerkitoBio/goquery v1.5.1
+github.com/PuerkitoBio/goquery
# github.com/Rhymen/go-whatsapp v0.1.1-0.20200818115958-f07a700b9819
github.com/Rhymen/go-whatsapp
github.com/Rhymen/go-whatsapp/binary
@@ -23,6 +28,8 @@ github.com/Rhymen/go-whatsapp/binary/token
github.com/Rhymen/go-whatsapp/crypto/cbc
github.com/Rhymen/go-whatsapp/crypto/curve25519
github.com/Rhymen/go-whatsapp/crypto/hkdf
+# github.com/andybalholm/cascadia v1.1.0
+github.com/andybalholm/cascadia
# github.com/blang/semver v3.5.1+incompatible
github.com/blang/semver
# github.com/d5/tengo/v2 v2.6.0
@@ -213,6 +220,8 @@ github.com/technoweenie/multipartstreamer
github.com/valyala/bytebufferpool
# github.com/valyala/fasttemplate v1.2.1
github.com/valyala/fasttemplate
+# github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50
+github.com/vincent-petithory/dataurl
# github.com/writeas/go-strip-markdown v2.0.1+incompatible
github.com/writeas/go-strip-markdown
# github.com/yaegashi/msgraph.go v0.1.4