diff --git a/Gopkg.lock b/Gopkg.lock index 1e81eb5c..b8c0dd67 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -9,6 +9,22 @@ revision = "a32116e4989e2b0e17c057ee378b4d5246add74e" version = "v1.1.0" +[[projects]] + branch = "master" + digest = "1:1d692605b66a4fbe2a3de27131619e8e2c88ccf142ee43a7a8c902339942e0cc" + name = "github.com/antchfx/jsonquery" + packages = ["."] + pruneopts = "UT" + revision = "a2896be8c82bb2229d1cf26204863180e34b2b31" + +[[projects]] + branch = "master" + digest = "1:7e4a9eaa42cb3b4cd48bd0cd2072a029fe25a47af20cfac529ea8c365f8559ac" + name = "github.com/antchfx/xpath" + packages = ["."] + pruneopts = "UT" + revision = "c8489ed3251e7d55ec2b7f18a2bc3a9a7222f0af" + [[projects]] branch = "master" digest = "1:c309b41787813f80ec393023471014b0be16346bba6e16bf5fa01ce1d310a4ea" @@ -292,6 +308,7 @@ analyzer-version = 1 input-imports = [ "github.com/adrianmo/go-nmea", + "github.com/antchfx/jsonquery", "github.com/bettercap/gatt", "github.com/bettercap/nrf24", "github.com/bettercap/readline", diff --git a/modules/events_stream/events_stream.go b/modules/events_stream/events_stream.go index 1acbcc52..6dcebccb 100644 --- a/modules/events_stream/events_stream.go +++ b/modules/events_stream/events_stream.go @@ -29,6 +29,7 @@ type EventsStream struct { output *os.File rotation rotation ignoreList *IgnoreList + triggerList *TriggerList waitFor string waitChan chan *session.Event eventListener <-chan session.Event @@ -45,6 +46,7 @@ func NewEventsStream(s *session.Session) *EventsStream { waitChan: make(chan *session.Event), waitFor: "", ignoreList: NewIgnoreList(), + triggerList: NewTriggerList(), } mod.AddHandler(session.NewModuleHandler("events.stream on", "", @@ -70,6 +72,38 @@ func NewEventsStream(s *session.Session) *EventsStream { return mod.Show(limit) })) + on := session.NewModuleHandler("events.on TAG COMMANDS", `events\.on ([^\s]+) (.+)`, + "Run COMMANDS when an event with the specified TAG is triggered.", + func(args []string) error { + return mod.addTrigger(args[0], args[1]) + }) + + on.Complete("events.on", s.EventsCompleter) + + mod.AddHandler(on) + + mod.AddHandler(session.NewModuleHandler("events.triggers", "", + "Show the list of event triggers created by the events.on command.", + func(args []string) error { + return mod.showTriggers() + })) + + onClear := session.NewModuleHandler("events.trigger.delete TRIGGER_ID", `events\.trigger\.delete ([^\s]+)`, + "Remove an event trigger given its TRIGGER_ID (use events.triggers to see the list of triggers).", + func(args []string) error { + return mod.clearTrigger(args[0]) + }) + + onClear.Complete("events.trigger.delete", mod.triggerList.Completer) + + mod.AddHandler(onClear) + + mod.AddHandler(session.NewModuleHandler("events.triggers.clear", "", + "Remove all event triggers (use events.triggers to see the list of triggers).", + func(args []string) error { + return mod.clearTrigger("") + })) + mod.AddHandler(session.NewModuleHandler("events.waitfor TAG TIMEOUT?", `events.waitfor ([^\s]+)([\s\d]*)`, "Wait for an event with the given tag either forever or for a timeout in seconds.", func(args []string) error { @@ -242,6 +276,10 @@ func (mod *EventsStream) Start() error { mod.View(e, true) } + // this could generate sys.log events and lock the whole + // events.stream, make it async + go mod.dispatchTriggers(e) + case <-mod.quit: return } diff --git a/modules/events_stream/events_triggers.go b/modules/events_stream/events_triggers.go new file mode 100644 index 00000000..0c50c9cc --- /dev/null +++ b/modules/events_stream/events_triggers.go @@ -0,0 +1,62 @@ +package events_stream + +import ( + "os" + + "github.com/bettercap/bettercap/session" + + "github.com/evilsocket/islazy/tui" +) + +func (mod *EventsStream) addTrigger(tag string, command string) error { + if err, id := mod.triggerList.Add(tag, command); err != nil { + return err + } else { + mod.Info("trigger for event %s added with identifier '%s'", tui.Green(tag), tui.Bold(id)) + } + return nil +} + +func (mod *EventsStream) clearTrigger(id string) error { + if err := mod.triggerList.Del(id); err != nil { + return err + } + return nil +} + +func (mod *EventsStream) showTriggers() error { + colNames := []string{ + "ID", + "Event", + "Action", + } + rows := [][]string{} + + mod.triggerList.Each(func(id string, t Trigger) { + rows = append(rows, []string{ + tui.Bold(id), + tui.Green(t.For), + t.Action, + }) + }) + + if len(rows) > 0 { + tui.Table(os.Stdout, colNames, rows) + mod.Session.Refresh() + } + + return nil +} + +func (mod *EventsStream) dispatchTriggers(e session.Event) { + if id, cmds, err, found := mod.triggerList.Dispatch(e); err != nil { + mod.Error("error while dispatching event %s: %v", e.Tag, err) + } else if found { + mod.Debug("running trigger %s (cmds:'%s') for event %v", id, cmds, e) + for _, cmd := range session.ParseCommands(cmds) { + if err := mod.Session.Run(cmd); err != nil { + mod.Error("%s", err.Error()) + } + } + } +} diff --git a/modules/events_stream/events_ignore_list.go b/modules/events_stream/ignore_list.go similarity index 100% rename from modules/events_stream/events_ignore_list.go rename to modules/events_stream/ignore_list.go diff --git a/modules/events_stream/trigger_list.go b/modules/events_stream/trigger_list.go new file mode 100644 index 00000000..08bb7ff7 --- /dev/null +++ b/modules/events_stream/trigger_list.go @@ -0,0 +1,136 @@ +package events_stream + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + "sync" + + "github.com/bettercap/bettercap/session" + + "github.com/antchfx/jsonquery" + "github.com/evilsocket/islazy/str" + "github.com/evilsocket/islazy/tui" +) + +var reQueryCapture = regexp.MustCompile(`{{([^}]+)}}`) + +type Trigger struct { + For string + Action string +} + +type TriggerList struct { + sync.Mutex + triggers map[string]Trigger +} + +func NewTriggerList() *TriggerList { + return &TriggerList{ + triggers: make(map[string]Trigger), + } +} + +func (l *TriggerList) Add(tag string, command string) (error, string) { + l.Lock() + defer l.Unlock() + + idNum := 0 + command = str.Trim(command) + + for id, t := range l.triggers { + if t.For == tag { + if t.Action == command { + return fmt.Errorf("duplicate: trigger '%s' found for action '%s'", tui.Bold(id), command), "" + } + idNum++ + } + } + + id := fmt.Sprintf("%s-%d", tag, idNum) + l.triggers[id] = Trigger{ + For: tag, + Action: command, + } + + return nil, id +} + +func (l *TriggerList) Del(id string) (err error) { + l.Lock() + defer l.Unlock() + if _, found := l.triggers[id]; found { + delete(l.triggers, id) + } else { + err = fmt.Errorf("trigger '%s' not found", tui.Bold(id)) + } + return err +} + +func (l *TriggerList) Each(cb func(id string, t Trigger)) { + l.Lock() + defer l.Unlock() + for id, t := range l.triggers { + cb(id, t) + } +} + +func (l *TriggerList) Completer(prefix string) []string { + ids := []string{} + l.Each(func(id string, t Trigger) { + if prefix == "" || strings.HasPrefix(id, prefix) { + ids = append(ids, id) + } + }) + return ids +} + +func (l *TriggerList) Dispatch(e session.Event) (ident string, cmd string, err error, found bool) { + l.Lock() + defer l.Unlock() + + for id, t := range l.triggers { + if e.Tag == t.For { + found = true + ident = id + // this is ugly but it's also the only way to allow + // the user to do this easily - since each event Data + // field is an interface and type casting is not possible + // via golang default text/template system, we transform + // the field to JSON, parse it again and then allow the + // user to access it in the command via JSON-Query, example: + // + // events.on wifi.client.new "wifi.deauth {{Client\mac}}" + buf := ([]byte)(nil) + doc := (*jsonquery.Node)(nil) + cmd = t.Action + // parse each {EXPR} + for _, m := range reQueryCapture.FindAllString(t.Action, -1) { + // parse the event Data field as a JSON objects once + if doc == nil { + if buf, err = json.Marshal(e.Data); err != nil { + err = fmt.Errorf("error while encoding event for trigger %s: %v", tui.Bold(id), err) + return + } else if doc, err = jsonquery.Parse(strings.NewReader(string(buf))); err != nil { + err = fmt.Errorf("error while parsing event for trigger %s: %v", tui.Bold(id), err) + return + } + } + // {EXPR} -> EXPR + expr := strings.Trim(m, "{}") + // use EXPR as a JSON query + if node := jsonquery.FindOne(doc, expr); node != nil { + cmd = strings.Replace(cmd, m, node.InnerText(), -1) + } else { + err = fmt.Errorf("error while parsing expressionfor trigger %s: '%s' doesn't resolve any object", tui.Bold(id), expr, expr, err) + return + } + } + + return + } + } + + return +} diff --git a/vendor/github.com/antchfx/jsonquery/.gitignore b/vendor/github.com/antchfx/jsonquery/.gitignore new file mode 100644 index 00000000..4d5d27b1 --- /dev/null +++ b/vendor/github.com/antchfx/jsonquery/.gitignore @@ -0,0 +1,32 @@ +# vscode +.vscode +debug +*.test + +./build + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof \ No newline at end of file diff --git a/vendor/github.com/antchfx/jsonquery/.travis.yml b/vendor/github.com/antchfx/jsonquery/.travis.yml new file mode 100644 index 00000000..91a354fe --- /dev/null +++ b/vendor/github.com/antchfx/jsonquery/.travis.yml @@ -0,0 +1,14 @@ +language: go + +go: + - 1.6 + - 1.7 + - 1.8 + - 1.9 + +install: + - go get github.com/antchfx/xpath + - go get github.com/mattn/goveralls + +script: + - $HOME/gopath/bin/goveralls -service=travis-ci \ No newline at end of file diff --git a/vendor/github.com/antchfx/jsonquery/LICENSE b/vendor/github.com/antchfx/jsonquery/LICENSE new file mode 100644 index 00000000..e14c3714 --- /dev/null +++ b/vendor/github.com/antchfx/jsonquery/LICENSE @@ -0,0 +1,17 @@ +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. \ No newline at end of file diff --git a/vendor/github.com/antchfx/jsonquery/README.md b/vendor/github.com/antchfx/jsonquery/README.md new file mode 100644 index 00000000..72d8408b --- /dev/null +++ b/vendor/github.com/antchfx/jsonquery/README.md @@ -0,0 +1,169 @@ +jsonquery +==== +[![Build Status](https://travis-ci.org/antchfx/jsonquery.svg?branch=master)](https://travis-ci.org/antchfx/jsonquery) +[![Coverage Status](https://coveralls.io/repos/github/antchfx/jsonquery/badge.svg?branch=master)](https://coveralls.io/github/antchfx/jsonquery?branch=master) +[![GoDoc](https://godoc.org/github.com/antchfx/jsonquery?status.svg)](https://godoc.org/github.com/antchfx/jsonquery) +[![Go Report Card](https://goreportcard.com/badge/github.com/antchfx/jsonquery)](https://goreportcard.com/report/github.com/antchfx/jsonquery) + +Overview +=== + +jsonquery is an XPath query package for JSON document, lets you extract data from JSON documents through an XPath expression. + +Getting Started +=== + +### Install Package + +> $ go get github.com/antchfx/jsonquery + +#### Load JSON document from URL. + +```go +doc, err := jsonquery.LoadURL("http://www.example.com/feed?json") +``` + +#### Load JSON document from string. + +```go +s :=`{ + "name":"John", + "age":31, + "city":"New York" + }` +doc, err := jsonquery.Parse(strings.NewReader(s)) +``` + +#### Load JSON document from io.Reader. + +```go +f, err := os.Open("./books.json") +doc, err := jsonquery.Parse(f) +``` + +#### Find authors of all books in the store. +```go +list := jsonquery.Find(doc, "store/book/*/author") +// or equal to +list := jsonquery.Find(doc, "//author") +``` + +#### Find the third book. + +```go +book := jsonquery.Find(doc, "//book/*[3]") +``` + +#### Find the last book. + +```go +book := jsonquery.Find(doc, "//book/*[last()]") +``` + +#### Find all books with isbn number. + +```go +list := jsonquery.Find(doc, "//book/*[isbn]") +``` + +#### Find all books cheapier than 10. + +```go +list := jsonquery.Find(doc, "//book/*[price<10]") +``` + +Quick Tutorial +=== + +```go +func main() { + s := `{ + "name": "John", + "age" : 26, + "address" : { + "streetAddress": "naist street", + "city" : "Nara", + "postalCode" : "630-0192" + }, + "phoneNumbers": [ + { + "type" : "iPhone", + "number": "0123-4567-8888" + }, + { + "type" : "home", + "number": "0123-4567-8910" + } + ] + }` + doc, _ := jsonquery.Parse(strings.NewReader(s)) + name := jsonquery.FindOne(doc, "name") + fmt.Printf("name: %s\n", name.InnerText()) + var a []string + for _, n := range jsonquery.Find(doc, "phoneNumbers/*/number") { + a = append(a, n.InnerText()) + } + fmt.Printf("phone number: %s\n", strings.Join(a, ",")) + if n := jsonquery.FindOne(doc, "address/streetAddress"); n != nil { + fmt.Printf("address: %s\n", n.InnerText()) + } +} +``` + +Implement Principle +=== +If you are familiar with XPath and XML, you can quick start to known how +write your XPath expression. + +```json +{ +"name":"John", +"age":30, +"cars": [ + { "name":"Ford", "models":[ "Fiesta", "Focus", "Mustang" ] }, + { "name":"BMW", "models":[ "320", "X3", "X5" ] }, + { "name":"Fiat", "models":[ "500", "Panda" ] } +] +} +``` +The above JSON document will be convert to similar to XML document by the *JSONQuery*, like below: + +```XML +John +30 + + + Ford + + Fiesta + Focus + Mustang + + + + BMW + + 320 + X3 + X5 + + + + Fiat + + 500 + Panda + + + +``` + +Notes: `element` is empty element that have no any name. + +List of supported XPath query packages +=== +|Name |Description | +|--------------------------|----------------| +|[htmlquery](https://github.com/antchfx/htmlquery) | XPath query package for the HTML document| +|[xmlquery](https://github.com/antchfx/xmlquery) | XPath query package for the XML document| +|[jsonquery](https://github.com/antchfx/jsonquery) | XPath query package for the JSON document| diff --git a/vendor/github.com/antchfx/jsonquery/books.json b/vendor/github.com/antchfx/jsonquery/books.json new file mode 100644 index 00000000..e4bd9462 --- /dev/null +++ b/vendor/github.com/antchfx/jsonquery/books.json @@ -0,0 +1,36 @@ +{ + "store": { + "book": [ + { + "category": "reference", + "author": "Nigel Rees", + "title": "Sayings of the Century", + "price": 8.95 + }, + { + "category": "fiction", + "author": "Evelyn Waugh", + "title": "Sword of Honour", + "price": 12.99 + }, + { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99 + }, + { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99 + } + ], + "bicycle": { + "color": "red", + "price": 19.95 + } + } +} \ No newline at end of file diff --git a/vendor/github.com/antchfx/jsonquery/node.go b/vendor/github.com/antchfx/jsonquery/node.go new file mode 100644 index 00000000..76032bb2 --- /dev/null +++ b/vendor/github.com/antchfx/jsonquery/node.go @@ -0,0 +1,157 @@ +package jsonquery + +import ( + "bytes" + "encoding/json" + "io" + "io/ioutil" + "net/http" + "sort" + "strconv" +) + +// A NodeType is the type of a Node. +type NodeType uint + +const ( + // DocumentNode is a document object that, as the root of the document tree, + // provides access to the entire XML document. + DocumentNode NodeType = iota + // ElementNode is an element. + ElementNode + // TextNode is the text content of a node. + TextNode +) + +// A Node consists of a NodeType and some Data (tag name for +// element nodes, content for text) and are part of a tree of Nodes. +type Node struct { + Parent, PrevSibling, NextSibling, FirstChild, LastChild *Node + + Type NodeType + Data string + + level int +} + +// ChildNodes gets all child nodes of the node. +func (n *Node) ChildNodes() []*Node { + var a []*Node + for nn := n.FirstChild; nn != nil; nn = nn.NextSibling { + a = append(a, nn) + } + return a +} + +// InnerText gets the value of the node and all its child nodes. +func (n *Node) InnerText() string { + var output func(*bytes.Buffer, *Node) + output = func(buf *bytes.Buffer, n *Node) { + if n.Type == TextNode { + buf.WriteString(n.Data) + return + } + for child := n.FirstChild; child != nil; child = child.NextSibling { + output(buf, child) + } + } + var buf bytes.Buffer + output(&buf, n) + return buf.String() +} + +// SelectElement finds the first of child elements with the +// specified name. +func (n *Node) SelectElement(name string) *Node { + for nn := n.FirstChild; nn != nil; nn = nn.NextSibling { + if nn.Data == name { + return nn + } + } + return nil +} + +// LoadURL loads the JSON document from the specified URL. +func LoadURL(url string) (*Node, error) { + resp, err := http.Get(url) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return Parse(resp.Body) +} + +func parseValue(x interface{}, top *Node, level int) { + addNode := func(n *Node) { + if n.level == top.level { + top.NextSibling = n + n.PrevSibling = top + n.Parent = top.Parent + if top.Parent != nil { + top.Parent.LastChild = n + } + } else if n.level > top.level { + n.Parent = top + if top.FirstChild == nil { + top.FirstChild = n + top.LastChild = n + } else { + t := top.LastChild + t.NextSibling = n + n.PrevSibling = t + top.LastChild = n + } + } + } + switch v := x.(type) { + case []interface{}: + for _, vv := range v { + n := &Node{Type: ElementNode, level: level} + addNode(n) + parseValue(vv, n, level+1) + } + case map[string]interface{}: + // The Go’s map iteration order is random. + // (https://blog.golang.org/go-maps-in-action#Iteration-order) + var keys []string + for key := range v { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + n := &Node{Data: key, Type: ElementNode, level: level} + addNode(n) + parseValue(v[key], n, level+1) + } + case string: + n := &Node{Data: v, Type: TextNode, level: level} + addNode(n) + case float64: + s := strconv.FormatFloat(v, 'f', -1, 64) + n := &Node{Data: s, Type: TextNode, level: level} + addNode(n) + case bool: + s := strconv.FormatBool(v) + n := &Node{Data: s, Type: TextNode, level: level} + addNode(n) + } +} + +func parse(b []byte) (*Node, error) { + var v interface{} + if err := json.Unmarshal(b, &v); err != nil { + return nil, err + } + doc := &Node{Type: DocumentNode} + parseValue(v, doc, 1) + return doc, nil +} + +// Parse JSON document. +func Parse(r io.Reader) (*Node, error) { + b, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + return parse(b) +} diff --git a/vendor/github.com/antchfx/jsonquery/query.go b/vendor/github.com/antchfx/jsonquery/query.go new file mode 100644 index 00000000..04f96d98 --- /dev/null +++ b/vendor/github.com/antchfx/jsonquery/query.go @@ -0,0 +1,149 @@ +package jsonquery + +import ( + "fmt" + + "github.com/antchfx/xpath" +) + +var _ xpath.NodeNavigator = &NodeNavigator{} + +// CreateXPathNavigator creates a new xpath.NodeNavigator for the specified html.Node. +func CreateXPathNavigator(top *Node) *NodeNavigator { + return &NodeNavigator{cur: top, root: top} +} + +// Find searches the Node that matches by the specified XPath expr. +func Find(top *Node, expr string) []*Node { + exp, err := xpath.Compile(expr) + if err != nil { + panic(err) + } + t := exp.Select(CreateXPathNavigator(top)) + var elems []*Node + for t.MoveNext() { + elems = append(elems, (t.Current().(*NodeNavigator)).cur) + } + return elems +} + +// FindOne searches the Node that matches by the specified XPath expr, +// and returns first element of matched. +func FindOne(top *Node, expr string) *Node { + exp, err := xpath.Compile(expr) + if err != nil { + panic(err) + } + t := exp.Select(CreateXPathNavigator(top)) + var elem *Node + if t.MoveNext() { + elem = (t.Current().(*NodeNavigator)).cur + } + return elem +} + +// NodeNavigator is for navigating JSON document. +type NodeNavigator struct { + root, cur *Node +} + +func (a *NodeNavigator) Current() *Node { + return a.cur +} + +func (a *NodeNavigator) NodeType() xpath.NodeType { + switch a.cur.Type { + case TextNode: + return xpath.TextNode + case DocumentNode: + return xpath.RootNode + case ElementNode: + return xpath.ElementNode + default: + panic(fmt.Sprintf("unknown node type %v", a.cur.Type)) + } +} + +func (a *NodeNavigator) LocalName() string { + return a.cur.Data + +} + +func (a *NodeNavigator) Prefix() string { + return "" +} + +func (a *NodeNavigator) Value() string { + switch a.cur.Type { + case ElementNode: + return a.cur.InnerText() + case TextNode: + return a.cur.Data + } + return "" +} + +func (a *NodeNavigator) Copy() xpath.NodeNavigator { + n := *a + return &n +} + +func (a *NodeNavigator) MoveToRoot() { + a.cur = a.root +} + +func (a *NodeNavigator) MoveToParent() bool { + if n := a.cur.Parent; n != nil { + a.cur = n + return true + } + return false +} + +func (x *NodeNavigator) MoveToNextAttribute() bool { + return false +} + +func (a *NodeNavigator) MoveToChild() bool { + if n := a.cur.FirstChild; n != nil { + a.cur = n + return true + } + return false +} + +func (a *NodeNavigator) MoveToFirst() bool { + for n := a.cur.PrevSibling; n != nil; n = n.PrevSibling { + a.cur = n + } + return true +} + +func (a *NodeNavigator) String() string { + return a.Value() +} + +func (a *NodeNavigator) MoveToNext() bool { + if n := a.cur.NextSibling; n != nil { + a.cur = n + return true + } + return false +} + +func (a *NodeNavigator) MoveToPrevious() bool { + if n := a.cur.PrevSibling; n != nil { + a.cur = n + return true + } + return false +} + +func (a *NodeNavigator) MoveTo(other xpath.NodeNavigator) bool { + node, ok := other.(*NodeNavigator) + if !ok || node.root != a.root { + return false + } + a.cur = node.cur + return true +} diff --git a/vendor/github.com/antchfx/xpath/.gitignore b/vendor/github.com/antchfx/xpath/.gitignore new file mode 100644 index 00000000..4d5d27b1 --- /dev/null +++ b/vendor/github.com/antchfx/xpath/.gitignore @@ -0,0 +1,32 @@ +# vscode +.vscode +debug +*.test + +./build + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof \ No newline at end of file diff --git a/vendor/github.com/antchfx/xpath/.travis.yml b/vendor/github.com/antchfx/xpath/.travis.yml new file mode 100644 index 00000000..6b63957a --- /dev/null +++ b/vendor/github.com/antchfx/xpath/.travis.yml @@ -0,0 +1,12 @@ +language: go + +go: + - 1.6 + - 1.9 + - '1.10' + +install: + - go get github.com/mattn/goveralls + +script: + - $HOME/gopath/bin/goveralls -service=travis-ci \ No newline at end of file diff --git a/vendor/github.com/antchfx/xpath/LICENSE b/vendor/github.com/antchfx/xpath/LICENSE new file mode 100644 index 00000000..e14c3714 --- /dev/null +++ b/vendor/github.com/antchfx/xpath/LICENSE @@ -0,0 +1,17 @@ +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. \ No newline at end of file diff --git a/vendor/github.com/antchfx/xpath/README.md b/vendor/github.com/antchfx/xpath/README.md new file mode 100644 index 00000000..414114dc --- /dev/null +++ b/vendor/github.com/antchfx/xpath/README.md @@ -0,0 +1,167 @@ +XPath +==== +[![GoDoc](https://godoc.org/github.com/antchfx/xpath?status.svg)](https://godoc.org/github.com/antchfx/xpath) +[![Coverage Status](https://coveralls.io/repos/github/antchfx/xpath/badge.svg?branch=master)](https://coveralls.io/github/antchfx/xpath?branch=master) +[![Build Status](https://travis-ci.org/antchfx/xpath.svg?branch=master)](https://travis-ci.org/antchfx/xpath) +[![Go Report Card](https://goreportcard.com/badge/github.com/antchfx/xpath)](https://goreportcard.com/report/github.com/antchfx/xpath) + +XPath is Go package provides selecting nodes from XML, HTML or other documents using XPath expression. + +Implementation +=== + +- [htmlquery](https://github.com/antchfx/htmlquery) - an XPath query package for HTML document + +- [xmlquery](https://github.com/antchfx/xmlquery) - an XPath query package for XML document. + +- [jsonquery](https://github.com/antchfx/jsonquery) - an XPath query package for JSON document + +Supported Features +=== + +#### The basic XPath patterns. + +> The basic XPath patterns cover 90% of the cases that most stylesheets will need. + +- `node` : Selects all child elements with nodeName of node. + +- `*` : Selects all child elements. + +- `@attr` : Selects the attribute attr. + +- `@*` : Selects all attributes. + +- `node()` : Matches an org.w3c.dom.Node. + +- `text()` : Matches a org.w3c.dom.Text node. + +- `comment()` : Matches a comment. + +- `.` : Selects the current node. + +- `..` : Selects the parent of current node. + +- `/` : Selects the document node. + +- `a[expr]` : Select only those nodes matching a which also satisfy the expression expr. + +- `a[n]` : Selects the nth matching node matching a When a filter's expression is a number, XPath selects based on position. + +- `a/b` : For each node matching a, add the nodes matching b to the result. + +- `a//b` : For each node matching a, add the descendant nodes matching b to the result. + +- `//b` : Returns elements in the entire document matching b. + +- `a|b` : All nodes matching a or b, union operation(not boolean or). + +- `(a, b, c)` : Evaluates each of its operands and concatenates the resulting sequences, in order, into a single result sequence + + +#### Node Axes + +- `child::*` : The child axis selects children of the current node. + +- `descendant::*` : The descendant axis selects descendants of the current node. It is equivalent to '//'. + +- `descendant-or-self::*` : Selects descendants including the current node. + +- `attribute::*` : Selects attributes of the current element. It is equivalent to @* + +- `following-sibling::*` : Selects nodes after the current node. + +- `preceding-sibling::*` : Selects nodes before the current node. + +- `following::*` : Selects the first matching node following in document order, excluding descendants. + +- `preceding::*` : Selects the first matching node preceding in document order, excluding ancestors. + +- `parent::*` : Selects the parent if it matches. The '..' pattern from the core is equivalent to 'parent::node()'. + +- `ancestor::*` : Selects matching ancestors. + +- `ancestor-or-self::*` : Selects ancestors including the current node. + +- `self::*` : Selects the current node. '.' is equivalent to 'self::node()'. + +#### Expressions + + The gxpath supported three types: number, boolean, string. + +- `path` : Selects nodes based on the path. + +- `a = b` : Standard comparisons. + + * a = b True if a equals b. + * a != b True if a is not equal to b. + * a < b True if a is less than b. + * a <= b True if a is less than or equal to b. + * a > b True if a is greater than b. + * a >= b True if a is greater than or equal to b. + +- `a + b` : Arithmetic expressions. + + * `- a` Unary minus + * a + b Add + * a - b Substract + * a * b Multiply + * a div b Divide + * a mod b Floating point mod, like Java. + +- `a or b` : Boolean `or` operation. + +- `a and b` : Boolean `and` operation. + +- `(expr)` : Parenthesized expressions. + +- `fun(arg1, ..., argn)` : Function calls: + +| Function | Supported | +| --- | --- | +`boolean()`| ✓ | +`ceiling()`| ✓ | +`choose()`| ✗ | +`concat()`| ✓ | +`contains()`| ✓ | +`count()`| ✓ | +`current()`| ✗ | +`document()`| ✗ | +`element-available()`| ✗ | +`ends-with()`| ✓ | +`false()`| ✓ | +`floor()`| ✓ | +`format-number()`| ✗ | +`function-available()`| ✗ | +`generate-id()`| ✗ | +`id()`| ✗ | +`key()`| ✗ | +`lang()`| ✗ | +`last()`| ✓ | +`local-name()`| ✓ | +`name()`| ✓ | +`namespace-uri()`| ✓ | +`normalize-space()`| ✓ | +`not()`| ✓ | +`number()`| ✓ | +`position()`| ✓ | +`round()`| ✓ | +`starts-with()`| ✓ | +`string()`| ✓ | +`string-length()`| ✓ | +`substring()`| ✓ | +`substring-after()`| ✓ | +`substring-before()`| ✓ | +`sum()`| ✓ | +`system-property()`| ✗ | +`translate()`| ✓ | +`true()`| ✓ | +`unparsed-entity-url()` | ✗ | + +Changelogs +=== + +2019-01-29 +- improvement `normalize-space` function. [#32](https://github.com/antchfx/xpath/issues/32) + +2018-12-07 +- supports XPath 2.0 Sequence expressions. [#30](https://github.com/antchfx/xpath/pull/30) by [@minherz](https://github.com/minherz). \ No newline at end of file diff --git a/vendor/github.com/antchfx/xpath/build.go b/vendor/github.com/antchfx/xpath/build.go new file mode 100644 index 00000000..74f266b0 --- /dev/null +++ b/vendor/github.com/antchfx/xpath/build.go @@ -0,0 +1,483 @@ +package xpath + +import ( + "errors" + "fmt" +) + +type flag int + +const ( + noneFlag flag = iota + filterFlag +) + +// builder provides building an XPath expressions. +type builder struct { + depth int + flag flag + firstInput query +} + +// axisPredicate creates a predicate to predicating for this axis node. +func axisPredicate(root *axisNode) func(NodeNavigator) bool { + // get current axix node type. + typ := ElementNode + switch root.AxeType { + case "attribute": + typ = AttributeNode + case "self", "parent": + typ = allNode + default: + switch root.Prop { + case "comment": + typ = CommentNode + case "text": + typ = TextNode + // case "processing-instruction": + // typ = ProcessingInstructionNode + case "node": + typ = allNode + } + } + nametest := root.LocalName != "" || root.Prefix != "" + predicate := func(n NodeNavigator) bool { + if typ == n.NodeType() || typ == allNode || typ == TextNode { + if nametest { + if root.LocalName == n.LocalName() && root.Prefix == n.Prefix() { + return true + } + } else { + return true + } + } + return false + } + + return predicate +} + +// processAxisNode processes a query for the XPath axis node. +func (b *builder) processAxisNode(root *axisNode) (query, error) { + var ( + err error + qyInput query + qyOutput query + predicate = axisPredicate(root) + ) + + if root.Input == nil { + qyInput = &contextQuery{} + } else { + if root.AxeType == "child" && (root.Input.Type() == nodeAxis) { + if input := root.Input.(*axisNode); input.AxeType == "descendant-or-self" { + var qyGrandInput query + if input.Input != nil { + qyGrandInput, _ = b.processNode(input.Input) + } else { + qyGrandInput = &contextQuery{} + } + qyOutput = &descendantQuery{Input: qyGrandInput, Predicate: predicate, Self: true} + return qyOutput, nil + } + } + qyInput, err = b.processNode(root.Input) + if err != nil { + return nil, err + } + } + + switch root.AxeType { + case "ancestor": + qyOutput = &ancestorQuery{Input: qyInput, Predicate: predicate} + case "ancestor-or-self": + qyOutput = &ancestorQuery{Input: qyInput, Predicate: predicate, Self: true} + case "attribute": + qyOutput = &attributeQuery{Input: qyInput, Predicate: predicate} + case "child": + filter := func(n NodeNavigator) bool { + v := predicate(n) + switch root.Prop { + case "text": + v = v && n.NodeType() == TextNode + case "node": + v = v && (n.NodeType() == ElementNode || n.NodeType() == TextNode) + case "comment": + v = v && n.NodeType() == CommentNode + } + return v + } + qyOutput = &childQuery{Input: qyInput, Predicate: filter} + case "descendant": + qyOutput = &descendantQuery{Input: qyInput, Predicate: predicate} + case "descendant-or-self": + qyOutput = &descendantQuery{Input: qyInput, Predicate: predicate, Self: true} + case "following": + qyOutput = &followingQuery{Input: qyInput, Predicate: predicate} + case "following-sibling": + qyOutput = &followingQuery{Input: qyInput, Predicate: predicate, Sibling: true} + case "parent": + qyOutput = &parentQuery{Input: qyInput, Predicate: predicate} + case "preceding": + qyOutput = &precedingQuery{Input: qyInput, Predicate: predicate} + case "preceding-sibling": + qyOutput = &precedingQuery{Input: qyInput, Predicate: predicate, Sibling: true} + case "self": + qyOutput = &selfQuery{Input: qyInput, Predicate: predicate} + case "namespace": + // haha,what will you do someting?? + default: + err = fmt.Errorf("unknown axe type: %s", root.AxeType) + return nil, err + } + return qyOutput, nil +} + +// processFilterNode builds query for the XPath filter predicate. +func (b *builder) processFilterNode(root *filterNode) (query, error) { + b.flag |= filterFlag + + qyInput, err := b.processNode(root.Input) + if err != nil { + return nil, err + } + qyCond, err := b.processNode(root.Condition) + if err != nil { + return nil, err + } + qyOutput := &filterQuery{Input: qyInput, Predicate: qyCond} + return qyOutput, nil +} + +// processFunctionNode processes query for the XPath function node. +func (b *builder) processFunctionNode(root *functionNode) (query, error) { + var qyOutput query + switch root.FuncName { + case "starts-with": + arg1, err := b.processNode(root.Args[0]) + if err != nil { + return nil, err + } + arg2, err := b.processNode(root.Args[1]) + if err != nil { + return nil, err + } + qyOutput = &functionQuery{Input: b.firstInput, Func: startwithFunc(arg1, arg2)} + case "ends-with": + arg1, err := b.processNode(root.Args[0]) + if err != nil { + return nil, err + } + arg2, err := b.processNode(root.Args[1]) + if err != nil { + return nil, err + } + qyOutput = &functionQuery{Input: b.firstInput, Func: endwithFunc(arg1, arg2)} + case "contains": + arg1, err := b.processNode(root.Args[0]) + if err != nil { + return nil, err + } + arg2, err := b.processNode(root.Args[1]) + if err != nil { + return nil, err + } + + qyOutput = &functionQuery{Input: b.firstInput, Func: containsFunc(arg1, arg2)} + case "substring": + //substring( string , start [, length] ) + if len(root.Args) < 2 { + return nil, errors.New("xpath: substring function must have at least two parameter") + } + var ( + arg1, arg2, arg3 query + err error + ) + if arg1, err = b.processNode(root.Args[0]); err != nil { + return nil, err + } + if arg2, err = b.processNode(root.Args[1]); err != nil { + return nil, err + } + if len(root.Args) == 3 { + if arg3, err = b.processNode(root.Args[2]); err != nil { + return nil, err + } + } + qyOutput = &functionQuery{Input: b.firstInput, Func: substringFunc(arg1, arg2, arg3)} + case "substring-before", "substring-after": + //substring-xxxx( haystack, needle ) + if len(root.Args) != 2 { + return nil, errors.New("xpath: substring-before function must have two parameters") + } + var ( + arg1, arg2 query + err error + ) + if arg1, err = b.processNode(root.Args[0]); err != nil { + return nil, err + } + if arg2, err = b.processNode(root.Args[1]); err != nil { + return nil, err + } + qyOutput = &functionQuery{ + Input: b.firstInput, + Func: substringIndFunc(arg1, arg2, root.FuncName == "substring-after"), + } + case "string-length": + // string-length( [string] ) + if len(root.Args) < 1 { + return nil, errors.New("xpath: string-length function must have at least one parameter") + } + arg1, err := b.processNode(root.Args[0]) + if err != nil { + return nil, err + } + qyOutput = &functionQuery{Input: b.firstInput, Func: stringLengthFunc(arg1)} + case "normalize-space": + if len(root.Args) == 0 { + return nil, errors.New("xpath: normalize-space function must have at least one parameter") + } + argQuery, err := b.processNode(root.Args[0]) + if err != nil { + return nil, err + } + qyOutput = &functionQuery{Input: argQuery, Func: normalizespaceFunc} + case "translate": + //translate( string , string, string ) + if len(root.Args) != 3 { + return nil, errors.New("xpath: translate function must have three parameters") + } + var ( + arg1, arg2, arg3 query + err error + ) + if arg1, err = b.processNode(root.Args[0]); err != nil { + return nil, err + } + if arg2, err = b.processNode(root.Args[1]); err != nil { + return nil, err + } + if arg3, err = b.processNode(root.Args[2]); err != nil { + return nil, err + } + qyOutput = &functionQuery{Input: b.firstInput, Func: translateFunc(arg1, arg2, arg3)} + case "not": + if len(root.Args) == 0 { + return nil, errors.New("xpath: not function must have at least one parameter") + } + argQuery, err := b.processNode(root.Args[0]) + if err != nil { + return nil, err + } + qyOutput = &functionQuery{Input: argQuery, Func: notFunc} + case "name", "local-name", "namespace-uri": + inp := b.firstInput + if len(root.Args) > 1 { + return nil, fmt.Errorf("xpath: %s function must have at most one parameter", root.FuncName) + } + if len(root.Args) == 1 { + argQuery, err := b.processNode(root.Args[0]) + if err != nil { + return nil, err + } + inp = argQuery + } + f := &functionQuery{Input: inp} + switch root.FuncName { + case "name": + f.Func = nameFunc + case "local-name": + f.Func = localNameFunc + case "namespace-uri": + f.Func = namespaceFunc + } + qyOutput = f + case "true", "false": + val := root.FuncName == "true" + qyOutput = &functionQuery{ + Input: b.firstInput, + Func: func(_ query, _ iterator) interface{} { + return val + }, + } + case "last": + qyOutput = &functionQuery{Input: b.firstInput, Func: lastFunc} + case "position": + qyOutput = &functionQuery{Input: b.firstInput, Func: positionFunc} + case "boolean", "number", "string": + inp := b.firstInput + if len(root.Args) > 1 { + return nil, fmt.Errorf("xpath: %s function must have at most one parameter", root.FuncName) + } + if len(root.Args) == 1 { + argQuery, err := b.processNode(root.Args[0]) + if err != nil { + return nil, err + } + inp = argQuery + } + f := &functionQuery{Input: inp} + switch root.FuncName { + case "boolean": + f.Func = booleanFunc + case "string": + f.Func = stringFunc + case "number": + f.Func = numberFunc + } + qyOutput = f + case "count": + //if b.firstInput == nil { + // return nil, errors.New("xpath: expression must evaluate to node-set") + //} + if len(root.Args) == 0 { + return nil, fmt.Errorf("xpath: count(node-sets) function must with have parameters node-sets") + } + argQuery, err := b.processNode(root.Args[0]) + if err != nil { + return nil, err + } + qyOutput = &functionQuery{Input: argQuery, Func: countFunc} + case "sum": + if len(root.Args) == 0 { + return nil, fmt.Errorf("xpath: sum(node-sets) function must with have parameters node-sets") + } + argQuery, err := b.processNode(root.Args[0]) + if err != nil { + return nil, err + } + qyOutput = &functionQuery{Input: argQuery, Func: sumFunc} + case "ceiling", "floor", "round": + if len(root.Args) == 0 { + return nil, fmt.Errorf("xpath: ceiling(node-sets) function must with have parameters node-sets") + } + argQuery, err := b.processNode(root.Args[0]) + if err != nil { + return nil, err + } + f := &functionQuery{Input: argQuery} + switch root.FuncName { + case "ceiling": + f.Func = ceilingFunc + case "floor": + f.Func = floorFunc + case "round": + f.Func = roundFunc + } + qyOutput = f + case "concat": + if len(root.Args) < 2 { + return nil, fmt.Errorf("xpath: concat() must have at least two arguments") + } + var args []query + for _, v := range root.Args { + q, err := b.processNode(v) + if err != nil { + return nil, err + } + args = append(args, q) + } + qyOutput = &functionQuery{Input: b.firstInput, Func: concatFunc(args...)} + default: + return nil, fmt.Errorf("not yet support this function %s()", root.FuncName) + } + return qyOutput, nil +} + +func (b *builder) processOperatorNode(root *operatorNode) (query, error) { + left, err := b.processNode(root.Left) + if err != nil { + return nil, err + } + right, err := b.processNode(root.Right) + if err != nil { + return nil, err + } + var qyOutput query + switch root.Op { + case "+", "-", "div", "mod": // Numeric operator + var exprFunc func(interface{}, interface{}) interface{} + switch root.Op { + case "+": + exprFunc = plusFunc + case "-": + exprFunc = minusFunc + case "div": + exprFunc = divFunc + case "mod": + exprFunc = modFunc + } + qyOutput = &numericQuery{Left: left, Right: right, Do: exprFunc} + case "=", ">", ">=", "<", "<=", "!=": + var exprFunc func(iterator, interface{}, interface{}) interface{} + switch root.Op { + case "=": + exprFunc = eqFunc + case ">": + exprFunc = gtFunc + case ">=": + exprFunc = geFunc + case "<": + exprFunc = ltFunc + case "<=": + exprFunc = leFunc + case "!=": + exprFunc = neFunc + } + qyOutput = &logicalQuery{Left: left, Right: right, Do: exprFunc} + case "or", "and": + isOr := false + if root.Op == "or" { + isOr = true + } + qyOutput = &booleanQuery{Left: left, Right: right, IsOr: isOr} + case "|": + qyOutput = &unionQuery{Left: left, Right: right} + } + return qyOutput, nil +} + +func (b *builder) processNode(root node) (q query, err error) { + if b.depth = b.depth + 1; b.depth > 1024 { + err = errors.New("the xpath expressions is too complex") + return + } + + switch root.Type() { + case nodeConstantOperand: + n := root.(*operandNode) + q = &constantQuery{Val: n.Val} + case nodeRoot: + q = &contextQuery{Root: true} + case nodeAxis: + q, err = b.processAxisNode(root.(*axisNode)) + b.firstInput = q + case nodeFilter: + q, err = b.processFilterNode(root.(*filterNode)) + case nodeFunction: + q, err = b.processFunctionNode(root.(*functionNode)) + case nodeOperator: + q, err = b.processOperatorNode(root.(*operatorNode)) + } + return +} + +// build builds a specified XPath expressions expr. +func build(expr string) (q query, err error) { + defer func() { + if e := recover(); e != nil { + switch x := e.(type) { + case string: + err = errors.New(x) + case error: + err = x + default: + err = errors.New("unknown panic") + } + } + }() + root := parse(expr) + b := &builder{} + return b.processNode(root) +} diff --git a/vendor/github.com/antchfx/xpath/func.go b/vendor/github.com/antchfx/xpath/func.go new file mode 100644 index 00000000..3c0fde9f --- /dev/null +++ b/vendor/github.com/antchfx/xpath/func.go @@ -0,0 +1,484 @@ +package xpath + +import ( + "errors" + "fmt" + "math" + "regexp" + "strconv" + "strings" +) + +// The XPath function list. + +func predicate(q query) func(NodeNavigator) bool { + type Predicater interface { + Test(NodeNavigator) bool + } + if p, ok := q.(Predicater); ok { + return p.Test + } + return func(NodeNavigator) bool { return true } +} + +// positionFunc is a XPath Node Set functions position(). +func positionFunc(q query, t iterator) interface{} { + var ( + count = 1 + node = t.Current() + ) + test := predicate(q) + for node.MoveToPrevious() { + if test(node) { + count++ + } + } + return float64(count) +} + +// lastFunc is a XPath Node Set functions last(). +func lastFunc(q query, t iterator) interface{} { + var ( + count = 0 + node = t.Current() + ) + node.MoveToFirst() + test := predicate(q) + for { + if test(node) { + count++ + } + if !node.MoveToNext() { + break + } + } + return float64(count) +} + +// countFunc is a XPath Node Set functions count(node-set). +func countFunc(q query, t iterator) interface{} { + var count = 0 + test := predicate(q) + switch typ := q.Evaluate(t).(type) { + case query: + for node := typ.Select(t); node != nil; node = typ.Select(t) { + if test(node) { + count++ + } + } + } + return float64(count) +} + +// sumFunc is a XPath Node Set functions sum(node-set). +func sumFunc(q query, t iterator) interface{} { + var sum float64 + switch typ := q.Evaluate(t).(type) { + case query: + for node := typ.Select(t); node != nil; node = typ.Select(t) { + if v, err := strconv.ParseFloat(node.Value(), 64); err == nil { + sum += v + } + } + case float64: + sum = typ + case string: + v, err := strconv.ParseFloat(typ, 64) + if err != nil { + panic(errors.New("sum() function argument type must be a node-set or number")) + } + sum = v + } + return sum +} + +func asNumber(t iterator, o interface{}) float64 { + switch typ := o.(type) { + case query: + node := typ.Select(t) + if node == nil { + return float64(0) + } + if v, err := strconv.ParseFloat(node.Value(), 64); err == nil { + return v + } + case float64: + return typ + case string: + v, err := strconv.ParseFloat(typ, 64) + if err != nil { + panic(errors.New("ceiling() function argument type must be a node-set or number")) + } + return v + } + return 0 +} + +// ceilingFunc is a XPath Node Set functions ceiling(node-set). +func ceilingFunc(q query, t iterator) interface{} { + val := asNumber(t, q.Evaluate(t)) + return math.Ceil(val) +} + +// floorFunc is a XPath Node Set functions floor(node-set). +func floorFunc(q query, t iterator) interface{} { + val := asNumber(t, q.Evaluate(t)) + return math.Floor(val) +} + +// roundFunc is a XPath Node Set functions round(node-set). +func roundFunc(q query, t iterator) interface{} { + val := asNumber(t, q.Evaluate(t)) + //return math.Round(val) + return round(val) +} + +// nameFunc is a XPath functions name([node-set]). +func nameFunc(q query, t iterator) interface{} { + v := q.Select(t) + if v == nil { + return "" + } + ns := v.Prefix() + if ns == "" { + return v.LocalName() + } + return ns + ":" + v.LocalName() +} + +// localNameFunc is a XPath functions local-name([node-set]). +func localNameFunc(q query, t iterator) interface{} { + v := q.Select(t) + if v == nil { + return "" + } + return v.LocalName() +} + +// namespaceFunc is a XPath functions namespace-uri([node-set]). +func namespaceFunc(q query, t iterator) interface{} { + v := q.Select(t) + if v == nil { + return "" + } + return v.Prefix() +} + +func asBool(t iterator, v interface{}) bool { + switch v := v.(type) { + case nil: + return false + case *NodeIterator: + return v.MoveNext() + case bool: + return bool(v) + case float64: + return v != 0 + case string: + return v != "" + case query: + return v.Select(t) != nil + default: + panic(fmt.Errorf("unexpected type: %T", v)) + } +} + +func asString(t iterator, v interface{}) string { + switch v := v.(type) { + case nil: + return "" + case bool: + if v { + return "true" + } + return "false" + case float64: + return strconv.FormatFloat(v, 'g', -1, 64) + case string: + return v + case query: + node := v.Select(t) + if node == nil { + return "" + } + return node.Value() + default: + panic(fmt.Errorf("unexpected type: %T", v)) + } +} + +// booleanFunc is a XPath functions boolean([node-set]). +func booleanFunc(q query, t iterator) interface{} { + v := q.Evaluate(t) + return asBool(t, v) +} + +// numberFunc is a XPath functions number([node-set]). +func numberFunc(q query, t iterator) interface{} { + v := q.Evaluate(t) + return asNumber(t, v) +} + +// stringFunc is a XPath functions string([node-set]). +func stringFunc(q query, t iterator) interface{} { + v := q.Evaluate(t) + return asString(t, v) +} + +// startwithFunc is a XPath functions starts-with(string, string). +func startwithFunc(arg1, arg2 query) func(query, iterator) interface{} { + return func(q query, t iterator) interface{} { + var ( + m, n string + ok bool + ) + switch typ := arg1.Evaluate(t).(type) { + case string: + m = typ + case query: + node := typ.Select(t) + if node == nil { + return false + } + m = node.Value() + default: + panic(errors.New("starts-with() function argument type must be string")) + } + n, ok = arg2.Evaluate(t).(string) + if !ok { + panic(errors.New("starts-with() function argument type must be string")) + } + return strings.HasPrefix(m, n) + } +} + +// endwithFunc is a XPath functions ends-with(string, string). +func endwithFunc(arg1, arg2 query) func(query, iterator) interface{} { + return func(q query, t iterator) interface{} { + var ( + m, n string + ok bool + ) + switch typ := arg1.Evaluate(t).(type) { + case string: + m = typ + case query: + node := typ.Select(t) + if node == nil { + return false + } + m = node.Value() + default: + panic(errors.New("ends-with() function argument type must be string")) + } + n, ok = arg2.Evaluate(t).(string) + if !ok { + panic(errors.New("ends-with() function argument type must be string")) + } + return strings.HasSuffix(m, n) + } +} + +// containsFunc is a XPath functions contains(string or @attr, string). +func containsFunc(arg1, arg2 query) func(query, iterator) interface{} { + return func(q query, t iterator) interface{} { + var ( + m, n string + ok bool + ) + + switch typ := arg1.Evaluate(t).(type) { + case string: + m = typ + case query: + node := typ.Select(t) + if node == nil { + return false + } + m = node.Value() + default: + panic(errors.New("contains() function argument type must be string")) + } + + n, ok = arg2.Evaluate(t).(string) + if !ok { + panic(errors.New("contains() function argument type must be string")) + } + + return strings.Contains(m, n) + } +} + +var ( + regnewline = regexp.MustCompile(`[\r\n\t]`) + regseqspace = regexp.MustCompile(`\s{2,}`) +) + +// normalizespaceFunc is XPath functions normalize-space(string?) +func normalizespaceFunc(q query, t iterator) interface{} { + var m string + switch typ := q.Evaluate(t).(type) { + case string: + m = typ + case query: + node := typ.Select(t) + if node == nil { + return "" + } + m = node.Value() + } + m = strings.TrimSpace(m) + m = regnewline.ReplaceAllString(m, " ") + m = regseqspace.ReplaceAllString(m, " ") + return m +} + +// substringFunc is XPath functions substring function returns a part of a given string. +func substringFunc(arg1, arg2, arg3 query) func(query, iterator) interface{} { + return func(q query, t iterator) interface{} { + var m string + switch typ := arg1.Evaluate(t).(type) { + case string: + m = typ + case query: + node := typ.Select(t) + if node == nil { + return "" + } + m = node.Value() + } + + var start, length float64 + var ok bool + + if start, ok = arg2.Evaluate(t).(float64); !ok { + panic(errors.New("substring() function first argument type must be int")) + } else if start < 1 { + panic(errors.New("substring() function first argument type must be >= 1")) + } + start-- + if arg3 != nil { + if length, ok = arg3.Evaluate(t).(float64); !ok { + panic(errors.New("substring() function second argument type must be int")) + } + } + if (len(m) - int(start)) < int(length) { + panic(errors.New("substring() function start and length argument out of range")) + } + if length > 0 { + return m[int(start):int(length+start)] + } + return m[int(start):] + } +} + +// substringIndFunc is XPath functions substring-before/substring-after function returns a part of a given string. +func substringIndFunc(arg1, arg2 query, after bool) func(query, iterator) interface{} { + return func(q query, t iterator) interface{} { + var str string + switch v := arg1.Evaluate(t).(type) { + case string: + str = v + case query: + node := v.Select(t) + if node == nil { + return "" + } + str = node.Value() + } + var word string + switch v := arg2.Evaluate(t).(type) { + case string: + word = v + case query: + node := v.Select(t) + if node == nil { + return "" + } + word = node.Value() + } + if word == "" { + return "" + } + + i := strings.Index(str, word) + if i < 0 { + return "" + } + if after { + return str[i+len(word):] + } + return str[:i] + } +} + +// stringLengthFunc is XPATH string-length( [string] ) function that returns a number +// equal to the number of characters in a given string. +func stringLengthFunc(arg1 query) func(query, iterator) interface{} { + return func(q query, t iterator) interface{} { + switch v := arg1.Evaluate(t).(type) { + case string: + return float64(len(v)) + case query: + node := v.Select(t) + if node == nil { + break + } + return float64(len(node.Value())) + } + return float64(0) + } +} + +// translateFunc is XPath functions translate() function returns a replaced string. +func translateFunc(arg1, arg2, arg3 query) func(query, iterator) interface{} { + return func(q query, t iterator) interface{} { + str := asString(t, arg1.Evaluate(t)) + src := asString(t, arg2.Evaluate(t)) + dst := asString(t, arg3.Evaluate(t)) + + var replace []string + for i, s := range src { + d := "" + if i < len(dst) { + d = string(dst[i]) + } + replace = append(replace, string(s), d) + } + return strings.NewReplacer(replace...).Replace(str) + } +} + +// notFunc is XPATH functions not(expression) function operation. +func notFunc(q query, t iterator) interface{} { + switch v := q.Evaluate(t).(type) { + case bool: + return !v + case query: + node := v.Select(t) + return node == nil + default: + return false + } +} + +// concatFunc is the concat function concatenates two or more +// strings and returns the resulting string. +// concat( string1 , string2 [, stringn]* ) +func concatFunc(args ...query) func(query, iterator) interface{} { + return func(q query, t iterator) interface{} { + var a []string + for _, v := range args { + switch v := v.Evaluate(t).(type) { + case string: + a = append(a, v) + case query: + node := v.Select(t) + if node != nil { + a = append(a, node.Value()) + } + } + } + return strings.Join(a, "") + } +} diff --git a/vendor/github.com/antchfx/xpath/func_go110.go b/vendor/github.com/antchfx/xpath/func_go110.go new file mode 100644 index 00000000..500880fa --- /dev/null +++ b/vendor/github.com/antchfx/xpath/func_go110.go @@ -0,0 +1,9 @@ +// +build go1.10 + +package xpath + +import "math" + +func round(f float64) int { + return int(math.Round(f)) +} diff --git a/vendor/github.com/antchfx/xpath/func_pre_go110.go b/vendor/github.com/antchfx/xpath/func_pre_go110.go new file mode 100644 index 00000000..043616b3 --- /dev/null +++ b/vendor/github.com/antchfx/xpath/func_pre_go110.go @@ -0,0 +1,15 @@ +// +build !go1.10 + +package xpath + +import "math" + +// math.Round() is supported by Go 1.10+, +// This method just compatible for version <1.10. +// https://github.com/golang/go/issues/20100 +func round(f float64) int { + if math.Abs(f) < 0.5 { + return 0 + } + return int(f + math.Copysign(0.5, f)) +} diff --git a/vendor/github.com/antchfx/xpath/operator.go b/vendor/github.com/antchfx/xpath/operator.go new file mode 100644 index 00000000..308d3cbc --- /dev/null +++ b/vendor/github.com/antchfx/xpath/operator.go @@ -0,0 +1,295 @@ +package xpath + +import ( + "fmt" + "reflect" + "strconv" +) + +// The XPath number operator function list. + +// valueType is a return value type. +type valueType int + +const ( + booleanType valueType = iota + numberType + stringType + nodeSetType +) + +func getValueType(i interface{}) valueType { + v := reflect.ValueOf(i) + switch v.Kind() { + case reflect.Float64: + return numberType + case reflect.String: + return stringType + case reflect.Bool: + return booleanType + default: + if _, ok := i.(query); ok { + return nodeSetType + } + } + panic(fmt.Errorf("xpath unknown value type: %v", v.Kind())) +} + +type logical func(iterator, string, interface{}, interface{}) bool + +var logicalFuncs = [][]logical{ + {cmpBooleanBoolean, nil, nil, nil}, + {nil, cmpNumericNumeric, cmpNumericString, cmpNumericNodeSet}, + {nil, cmpStringNumeric, cmpStringString, cmpStringNodeSet}, + {nil, cmpNodeSetNumeric, cmpNodeSetString, cmpNodeSetNodeSet}, +} + +// number vs number +func cmpNumberNumberF(op string, a, b float64) bool { + switch op { + case "=": + return a == b + case ">": + return a > b + case "<": + return a < b + case ">=": + return a >= b + case "<=": + return a <= b + case "!=": + return a != b + } + return false +} + +// string vs string +func cmpStringStringF(op string, a, b string) bool { + switch op { + case "=": + return a == b + case ">": + return a > b + case "<": + return a < b + case ">=": + return a >= b + case "<=": + return a <= b + case "!=": + return a != b + } + return false +} + +func cmpBooleanBooleanF(op string, a, b bool) bool { + switch op { + case "or": + return a || b + case "and": + return a && b + } + return false +} + +func cmpNumericNumeric(t iterator, op string, m, n interface{}) bool { + a := m.(float64) + b := n.(float64) + return cmpNumberNumberF(op, a, b) +} + +func cmpNumericString(t iterator, op string, m, n interface{}) bool { + a := m.(float64) + b := n.(string) + num, err := strconv.ParseFloat(b, 64) + if err != nil { + panic(err) + } + return cmpNumberNumberF(op, a, num) +} + +func cmpNumericNodeSet(t iterator, op string, m, n interface{}) bool { + a := m.(float64) + b := n.(query) + + for { + node := b.Select(t) + if node == nil { + break + } + num, err := strconv.ParseFloat(node.Value(), 64) + if err != nil { + panic(err) + } + if cmpNumberNumberF(op, a, num) { + return true + } + } + return false +} + +func cmpNodeSetNumeric(t iterator, op string, m, n interface{}) bool { + a := m.(query) + b := n.(float64) + for { + node := a.Select(t) + if node == nil { + break + } + num, err := strconv.ParseFloat(node.Value(), 64) + if err != nil { + panic(err) + } + if cmpNumberNumberF(op, num, b) { + return true + } + } + return false +} + +func cmpNodeSetString(t iterator, op string, m, n interface{}) bool { + a := m.(query) + b := n.(string) + for { + node := a.Select(t) + if node == nil { + break + } + if cmpStringStringF(op, b, node.Value()) { + return true + } + } + return false +} + +func cmpNodeSetNodeSet(t iterator, op string, m, n interface{}) bool { + return false +} + +func cmpStringNumeric(t iterator, op string, m, n interface{}) bool { + a := m.(string) + b := n.(float64) + num, err := strconv.ParseFloat(a, 64) + if err != nil { + panic(err) + } + return cmpNumberNumberF(op, b, num) +} + +func cmpStringString(t iterator, op string, m, n interface{}) bool { + a := m.(string) + b := n.(string) + return cmpStringStringF(op, a, b) +} + +func cmpStringNodeSet(t iterator, op string, m, n interface{}) bool { + a := m.(string) + b := n.(query) + for { + node := b.Select(t) + if node == nil { + break + } + if cmpStringStringF(op, a, node.Value()) { + return true + } + } + return false +} + +func cmpBooleanBoolean(t iterator, op string, m, n interface{}) bool { + a := m.(bool) + b := n.(bool) + return cmpBooleanBooleanF(op, a, b) +} + +// eqFunc is an `=` operator. +func eqFunc(t iterator, m, n interface{}) interface{} { + t1 := getValueType(m) + t2 := getValueType(n) + return logicalFuncs[t1][t2](t, "=", m, n) +} + +// gtFunc is an `>` operator. +func gtFunc(t iterator, m, n interface{}) interface{} { + t1 := getValueType(m) + t2 := getValueType(n) + return logicalFuncs[t1][t2](t, ">", m, n) +} + +// geFunc is an `>=` operator. +func geFunc(t iterator, m, n interface{}) interface{} { + t1 := getValueType(m) + t2 := getValueType(n) + return logicalFuncs[t1][t2](t, ">=", m, n) +} + +// ltFunc is an `<` operator. +func ltFunc(t iterator, m, n interface{}) interface{} { + t1 := getValueType(m) + t2 := getValueType(n) + return logicalFuncs[t1][t2](t, "<", m, n) +} + +// leFunc is an `<=` operator. +func leFunc(t iterator, m, n interface{}) interface{} { + t1 := getValueType(m) + t2 := getValueType(n) + return logicalFuncs[t1][t2](t, "<=", m, n) +} + +// neFunc is an `!=` operator. +func neFunc(t iterator, m, n interface{}) interface{} { + t1 := getValueType(m) + t2 := getValueType(n) + return logicalFuncs[t1][t2](t, "!=", m, n) +} + +// orFunc is an `or` operator. +var orFunc = func(t iterator, m, n interface{}) interface{} { + t1 := getValueType(m) + t2 := getValueType(n) + return logicalFuncs[t1][t2](t, "or", m, n) +} + +func numericExpr(m, n interface{}, cb func(float64, float64) float64) float64 { + typ := reflect.TypeOf(float64(0)) + a := reflect.ValueOf(m).Convert(typ) + b := reflect.ValueOf(n).Convert(typ) + return cb(a.Float(), b.Float()) +} + +// plusFunc is an `+` operator. +var plusFunc = func(m, n interface{}) interface{} { + return numericExpr(m, n, func(a, b float64) float64 { + return a + b + }) +} + +// minusFunc is an `-` operator. +var minusFunc = func(m, n interface{}) interface{} { + return numericExpr(m, n, func(a, b float64) float64 { + return a - b + }) +} + +// mulFunc is an `*` operator. +var mulFunc = func(m, n interface{}) interface{} { + return numericExpr(m, n, func(a, b float64) float64 { + return a * b + }) +} + +// divFunc is an `DIV` operator. +var divFunc = func(m, n interface{}) interface{} { + return numericExpr(m, n, func(a, b float64) float64 { + return a / b + }) +} + +// modFunc is an 'MOD' operator. +var modFunc = func(m, n interface{}) interface{} { + return numericExpr(m, n, func(a, b float64) float64 { + return float64(int(a) % int(b)) + }) +} diff --git a/vendor/github.com/antchfx/xpath/parse.go b/vendor/github.com/antchfx/xpath/parse.go new file mode 100644 index 00000000..fb9abe31 --- /dev/null +++ b/vendor/github.com/antchfx/xpath/parse.go @@ -0,0 +1,1186 @@ +package xpath + +import ( + "bytes" + "errors" + "fmt" + "strconv" + "unicode" +) + +// A XPath expression token type. +type itemType int + +const ( + itemComma itemType = iota // ',' + itemSlash // '/' + itemAt // '@' + itemDot // '.' + itemLParens // '(' + itemRParens // ')' + itemLBracket // '[' + itemRBracket // ']' + itemStar // '*' + itemPlus // '+' + itemMinus // '-' + itemEq // '=' + itemLt // '<' + itemGt // '>' + itemBang // '!' + itemDollar // '$' + itemApos // '\'' + itemQuote // '"' + itemUnion // '|' + itemNe // '!=' + itemLe // '<=' + itemGe // '>=' + itemAnd // '&&' + itemOr // '||' + itemDotDot // '..' + itemSlashSlash // '//' + itemName // XML Name + itemString // Quoted string constant + itemNumber // Number constant + itemAxe // Axe (like child::) + itemEOF // END +) + +// A node is an XPath node in the parse tree. +type node interface { + Type() nodeType +} + +// nodeType identifies the type of a parse tree node. +type nodeType int + +func (t nodeType) Type() nodeType { + return t +} + +const ( + nodeRoot nodeType = iota + nodeAxis + nodeFilter + nodeFunction + nodeOperator + nodeVariable + nodeConstantOperand +) + +type parser struct { + r *scanner + d int +} + +// newOperatorNode returns new operator node OperatorNode. +func newOperatorNode(op string, left, right node) node { + return &operatorNode{nodeType: nodeOperator, Op: op, Left: left, Right: right} +} + +// newOperand returns new constant operand node OperandNode. +func newOperandNode(v interface{}) node { + return &operandNode{nodeType: nodeConstantOperand, Val: v} +} + +// newAxisNode returns new axis node AxisNode. +func newAxisNode(axeTyp, localName, prefix, prop string, n node) node { + return &axisNode{ + nodeType: nodeAxis, + LocalName: localName, + Prefix: prefix, + AxeType: axeTyp, + Prop: prop, + Input: n, + } +} + +// newVariableNode returns new variable node VariableNode. +func newVariableNode(prefix, name string) node { + return &variableNode{nodeType: nodeVariable, Name: name, Prefix: prefix} +} + +// newFilterNode returns a new filter node FilterNode. +func newFilterNode(n, m node) node { + return &filterNode{nodeType: nodeFilter, Input: n, Condition: m} +} + +// newRootNode returns a root node. +func newRootNode(s string) node { + return &rootNode{nodeType: nodeRoot, slash: s} +} + +// newFunctionNode returns function call node. +func newFunctionNode(name, prefix string, args []node) node { + return &functionNode{nodeType: nodeFunction, Prefix: prefix, FuncName: name, Args: args} +} + +// testOp reports whether current item name is an operand op. +func testOp(r *scanner, op string) bool { + return r.typ == itemName && r.prefix == "" && r.name == op +} + +func isPrimaryExpr(r *scanner) bool { + switch r.typ { + case itemString, itemNumber, itemDollar, itemLParens: + return true + case itemName: + return r.canBeFunc && !isNodeType(r) + } + return false +} + +func isNodeType(r *scanner) bool { + switch r.name { + case "node", "text", "processing-instruction", "comment": + return r.prefix == "" + } + return false +} + +func isStep(item itemType) bool { + switch item { + case itemDot, itemDotDot, itemAt, itemAxe, itemStar, itemName: + return true + } + return false +} + +func checkItem(r *scanner, typ itemType) { + if r.typ != typ { + panic(fmt.Sprintf("%s has an invalid token", r.text)) + } +} + +// parseExpression parsing the expression with input node n. +func (p *parser) parseExpression(n node) node { + if p.d = p.d + 1; p.d > 200 { + panic("the xpath query is too complex(depth > 200)") + } + n = p.parseOrExpr(n) + p.d-- + return n +} + +// next scanning next item on forward. +func (p *parser) next() bool { + return p.r.nextItem() +} + +func (p *parser) skipItem(typ itemType) { + checkItem(p.r, typ) + p.next() +} + +// OrExpr ::= AndExpr | OrExpr 'or' AndExpr +func (p *parser) parseOrExpr(n node) node { + opnd := p.parseAndExpr(n) + for { + if !testOp(p.r, "or") { + break + } + p.next() + opnd = newOperatorNode("or", opnd, p.parseAndExpr(n)) + } + return opnd +} + +// AndExpr ::= EqualityExpr | AndExpr 'and' EqualityExpr +func (p *parser) parseAndExpr(n node) node { + opnd := p.parseEqualityExpr(n) + for { + if !testOp(p.r, "and") { + break + } + p.next() + opnd = newOperatorNode("and", opnd, p.parseEqualityExpr(n)) + } + return opnd +} + +// EqualityExpr ::= RelationalExpr | EqualityExpr '=' RelationalExpr | EqualityExpr '!=' RelationalExpr +func (p *parser) parseEqualityExpr(n node) node { + opnd := p.parseRelationalExpr(n) +Loop: + for { + var op string + switch p.r.typ { + case itemEq: + op = "=" + case itemNe: + op = "!=" + default: + break Loop + } + p.next() + opnd = newOperatorNode(op, opnd, p.parseRelationalExpr(n)) + } + return opnd +} + +// RelationalExpr ::= AdditiveExpr | RelationalExpr '<' AdditiveExpr | RelationalExpr '>' AdditiveExpr +// | RelationalExpr '<=' AdditiveExpr +// | RelationalExpr '>=' AdditiveExpr +func (p *parser) parseRelationalExpr(n node) node { + opnd := p.parseAdditiveExpr(n) +Loop: + for { + var op string + switch p.r.typ { + case itemLt: + op = "<" + case itemGt: + op = ">" + case itemLe: + op = "<=" + case itemGe: + op = ">=" + default: + break Loop + } + p.next() + opnd = newOperatorNode(op, opnd, p.parseAdditiveExpr(n)) + } + return opnd +} + +// AdditiveExpr ::= MultiplicativeExpr | AdditiveExpr '+' MultiplicativeExpr | AdditiveExpr '-' MultiplicativeExpr +func (p *parser) parseAdditiveExpr(n node) node { + opnd := p.parseMultiplicativeExpr(n) +Loop: + for { + var op string + switch p.r.typ { + case itemPlus: + op = "+" + case itemMinus: + op = "-" + default: + break Loop + } + p.next() + opnd = newOperatorNode(op, opnd, p.parseMultiplicativeExpr(n)) + } + return opnd +} + +// MultiplicativeExpr ::= UnaryExpr | MultiplicativeExpr MultiplyOperator(*) UnaryExpr +// | MultiplicativeExpr 'div' UnaryExpr | MultiplicativeExpr 'mod' UnaryExpr +func (p *parser) parseMultiplicativeExpr(n node) node { + opnd := p.parseUnaryExpr(n) +Loop: + for { + var op string + if p.r.typ == itemStar { + op = "*" + } else if testOp(p.r, "div") || testOp(p.r, "mod") { + op = p.r.name + } else { + break Loop + } + p.next() + opnd = newOperatorNode(op, opnd, p.parseUnaryExpr(n)) + } + return opnd +} + +// UnaryExpr ::= UnionExpr | '-' UnaryExpr +func (p *parser) parseUnaryExpr(n node) node { + minus := false + // ignore '-' sequence + for p.r.typ == itemMinus { + p.next() + minus = !minus + } + opnd := p.parseUnionExpr(n) + if minus { + opnd = newOperatorNode("*", opnd, newOperandNode(float64(-1))) + } + return opnd +} + +// UnionExpr ::= PathExpr | UnionExpr '|' PathExpr +func (p *parser) parseUnionExpr(n node) node { + opnd := p.parsePathExpr(n) +Loop: + for { + if p.r.typ != itemUnion { + break Loop + } + p.next() + opnd2 := p.parsePathExpr(n) + // Checking the node type that must be is node set type? + opnd = newOperatorNode("|", opnd, opnd2) + } + return opnd +} + +// PathExpr ::= LocationPath | FilterExpr | FilterExpr '/' RelativeLocationPath | FilterExpr '//' RelativeLocationPath +func (p *parser) parsePathExpr(n node) node { + var opnd node + if isPrimaryExpr(p.r) { + opnd = p.parseFilterExpr(n) + switch p.r.typ { + case itemSlash: + p.next() + opnd = p.parseRelativeLocationPath(opnd) + case itemSlashSlash: + p.next() + opnd = p.parseRelativeLocationPath(newAxisNode("descendant-or-self", "", "", "", opnd)) + } + } else { + opnd = p.parseLocationPath(nil) + } + return opnd +} + +// FilterExpr ::= PrimaryExpr | FilterExpr Predicate +func (p *parser) parseFilterExpr(n node) node { + opnd := p.parsePrimaryExpr(n) + if p.r.typ == itemLBracket { + opnd = newFilterNode(opnd, p.parsePredicate(opnd)) + } + return opnd +} + +// Predicate ::= '[' PredicateExpr ']' +func (p *parser) parsePredicate(n node) node { + p.skipItem(itemLBracket) + opnd := p.parseExpression(n) + p.skipItem(itemRBracket) + return opnd +} + +// LocationPath ::= RelativeLocationPath | AbsoluteLocationPath +func (p *parser) parseLocationPath(n node) (opnd node) { + switch p.r.typ { + case itemSlash: + p.next() + opnd = newRootNode("/") + if isStep(p.r.typ) { + opnd = p.parseRelativeLocationPath(opnd) // ?? child:: or self ?? + } + case itemSlashSlash: + p.next() + opnd = newRootNode("//") + opnd = p.parseRelativeLocationPath(newAxisNode("descendant-or-self", "", "", "", opnd)) + default: + opnd = p.parseRelativeLocationPath(n) + } + return opnd +} + +// RelativeLocationPath ::= Step | RelativeLocationPath '/' Step | AbbreviatedRelativeLocationPath +func (p *parser) parseRelativeLocationPath(n node) node { + opnd := n +Loop: + for { + opnd = p.parseStep(opnd) + switch p.r.typ { + case itemSlashSlash: + p.next() + opnd = newAxisNode("descendant-or-self", "", "", "", opnd) + case itemSlash: + p.next() + default: + break Loop + } + } + return opnd +} + +// Step ::= AxisSpecifier NodeTest Predicate* | AbbreviatedStep +func (p *parser) parseStep(n node) (opnd node) { + axeTyp := "child" // default axes value. + if p.r.typ == itemDot || p.r.typ == itemDotDot { + if p.r.typ == itemDot { + axeTyp = "self" + } else { + axeTyp = "parent" + } + p.next() + opnd = newAxisNode(axeTyp, "", "", "", n) + if p.r.typ != itemLBracket { + return opnd + } + } else { + switch p.r.typ { + case itemAt: + p.next() + axeTyp = "attribute" + case itemAxe: + axeTyp = p.r.name + p.next() + case itemLParens: + return p.parseSequence(n) + } + opnd = p.parseNodeTest(n, axeTyp) + } + for p.r.typ == itemLBracket { + opnd = newFilterNode(opnd, p.parsePredicate(opnd)) + } + return opnd +} + +// Expr ::= '(' Step ("," Step)* ')' +func (p *parser) parseSequence(n node) (opnd node) { + p.skipItem(itemLParens) + opnd = p.parseStep(n) + for { + if p.r.typ != itemComma { + break + } + p.next() + opnd2 := p.parseStep(n) + opnd = newOperatorNode("|", opnd, opnd2) + } + p.skipItem(itemRParens) + return opnd +} + +// NodeTest ::= NameTest | nodeType '(' ')' | 'processing-instruction' '(' Literal ')' +func (p *parser) parseNodeTest(n node, axeTyp string) (opnd node) { + switch p.r.typ { + case itemName: + if p.r.canBeFunc && isNodeType(p.r) { + var prop string + switch p.r.name { + case "comment", "text", "processing-instruction", "node": + prop = p.r.name + } + var name string + p.next() + p.skipItem(itemLParens) + if prop == "processing-instruction" && p.r.typ != itemRParens { + checkItem(p.r, itemString) + name = p.r.strval + p.next() + } + p.skipItem(itemRParens) + opnd = newAxisNode(axeTyp, name, "", prop, n) + } else { + prefix := p.r.prefix + name := p.r.name + p.next() + if p.r.name == "*" { + name = "" + } + opnd = newAxisNode(axeTyp, name, prefix, "", n) + } + case itemStar: + opnd = newAxisNode(axeTyp, "", "", "", n) + p.next() + default: + panic("expression must evaluate to a node-set") + } + return opnd +} + +// PrimaryExpr ::= VariableReference | '(' Expr ')' | Literal | Number | FunctionCall +func (p *parser) parsePrimaryExpr(n node) (opnd node) { + switch p.r.typ { + case itemString: + opnd = newOperandNode(p.r.strval) + p.next() + case itemNumber: + opnd = newOperandNode(p.r.numval) + p.next() + case itemDollar: + p.next() + checkItem(p.r, itemName) + opnd = newVariableNode(p.r.prefix, p.r.name) + p.next() + case itemLParens: + p.next() + opnd = p.parseExpression(n) + p.skipItem(itemRParens) + case itemName: + if p.r.canBeFunc && !isNodeType(p.r) { + opnd = p.parseMethod(nil) + } + } + return opnd +} + +// FunctionCall ::= FunctionName '(' ( Argument ( ',' Argument )* )? ')' +func (p *parser) parseMethod(n node) node { + var args []node + name := p.r.name + prefix := p.r.prefix + + p.skipItem(itemName) + p.skipItem(itemLParens) + if p.r.typ != itemRParens { + for { + args = append(args, p.parseExpression(n)) + if p.r.typ == itemRParens { + break + } + p.skipItem(itemComma) + } + } + p.skipItem(itemRParens) + return newFunctionNode(name, prefix, args) +} + +// Parse parsing the XPath express string expr and returns a tree node. +func parse(expr string) node { + r := &scanner{text: expr} + r.nextChar() + r.nextItem() + p := &parser{r: r} + return p.parseExpression(nil) +} + +// rootNode holds a top-level node of tree. +type rootNode struct { + nodeType + slash string +} + +func (r *rootNode) String() string { + return r.slash +} + +// operatorNode holds two Nodes operator. +type operatorNode struct { + nodeType + Op string + Left, Right node +} + +func (o *operatorNode) String() string { + return fmt.Sprintf("%v%s%v", o.Left, o.Op, o.Right) +} + +// axisNode holds a location step. +type axisNode struct { + nodeType + Input node + Prop string // node-test name.[comment|text|processing-instruction|node] + AxeType string // name of the axes.[attribute|ancestor|child|....] + LocalName string // local part name of node. + Prefix string // prefix name of node. +} + +func (a *axisNode) String() string { + var b bytes.Buffer + if a.AxeType != "" { + b.Write([]byte(a.AxeType + "::")) + } + if a.Prefix != "" { + b.Write([]byte(a.Prefix + ":")) + } + b.Write([]byte(a.LocalName)) + if a.Prop != "" { + b.Write([]byte("/" + a.Prop + "()")) + } + return b.String() +} + +// operandNode holds a constant operand. +type operandNode struct { + nodeType + Val interface{} +} + +func (o *operandNode) String() string { + return fmt.Sprintf("%v", o.Val) +} + +// filterNode holds a condition filter. +type filterNode struct { + nodeType + Input, Condition node +} + +func (f *filterNode) String() string { + return fmt.Sprintf("%s[%s]", f.Input, f.Condition) +} + +// variableNode holds a variable. +type variableNode struct { + nodeType + Name, Prefix string +} + +func (v *variableNode) String() string { + if v.Prefix == "" { + return v.Name + } + return fmt.Sprintf("%s:%s", v.Prefix, v.Name) +} + +// functionNode holds a function call. +type functionNode struct { + nodeType + Args []node + Prefix string + FuncName string // function name +} + +func (f *functionNode) String() string { + var b bytes.Buffer + // fun(arg1, ..., argn) + b.Write([]byte(f.FuncName)) + b.Write([]byte("(")) + for i, arg := range f.Args { + if i > 0 { + b.Write([]byte(",")) + } + b.Write([]byte(fmt.Sprintf("%s", arg))) + } + b.Write([]byte(")")) + return b.String() +} + +type scanner struct { + text, name, prefix string + + pos int + curr rune + typ itemType + strval string // text value at current pos + numval float64 // number value at current pos + canBeFunc bool +} + +func (s *scanner) nextChar() bool { + if s.pos >= len(s.text) { + s.curr = rune(0) + return false + } + s.curr = rune(s.text[s.pos]) + s.pos++ + return true +} + +func (s *scanner) nextItem() bool { + s.skipSpace() + switch s.curr { + case 0: + s.typ = itemEOF + return false + case ',', '@', '(', ')', '|', '*', '[', ']', '+', '-', '=', '#', '$': + s.typ = asItemType(s.curr) + s.nextChar() + case '<': + s.typ = itemLt + s.nextChar() + if s.curr == '=' { + s.typ = itemLe + s.nextChar() + } + case '>': + s.typ = itemGt + s.nextChar() + if s.curr == '=' { + s.typ = itemGe + s.nextChar() + } + case '!': + s.typ = itemBang + s.nextChar() + if s.curr == '=' { + s.typ = itemNe + s.nextChar() + } + case '.': + s.typ = itemDot + s.nextChar() + if s.curr == '.' { + s.typ = itemDotDot + s.nextChar() + } else if isDigit(s.curr) { + s.typ = itemNumber + s.numval = s.scanFraction() + } + case '/': + s.typ = itemSlash + s.nextChar() + if s.curr == '/' { + s.typ = itemSlashSlash + s.nextChar() + } + case '"', '\'': + s.typ = itemString + s.strval = s.scanString() + default: + if isDigit(s.curr) { + s.typ = itemNumber + s.numval = s.scanNumber() + } else if isName(s.curr) { + s.typ = itemName + s.name = s.scanName() + s.prefix = "" + // "foo:bar" is one itemem not three because it doesn't allow spaces in between + // We should distinct it from "foo::" and need process "foo ::" as well + if s.curr == ':' { + s.nextChar() + // can be "foo:bar" or "foo::" + if s.curr == ':' { + // "foo::" + s.nextChar() + s.typ = itemAxe + } else { // "foo:*", "foo:bar" or "foo: " + s.prefix = s.name + if s.curr == '*' { + s.nextChar() + s.name = "*" + } else if isName(s.curr) { + s.name = s.scanName() + } else { + panic(fmt.Sprintf("%s has an invalid qualified name.", s.text)) + } + } + } else { + s.skipSpace() + if s.curr == ':' { + s.nextChar() + // it can be "foo ::" or just "foo :" + if s.curr == ':' { + s.nextChar() + s.typ = itemAxe + } else { + panic(fmt.Sprintf("%s has an invalid qualified name.", s.text)) + } + } + } + s.skipSpace() + s.canBeFunc = s.curr == '(' + } else { + panic(fmt.Sprintf("%s has an invalid token.", s.text)) + } + } + return true +} + +func (s *scanner) skipSpace() { +Loop: + for { + if !unicode.IsSpace(s.curr) || !s.nextChar() { + break Loop + } + } +} + +func (s *scanner) scanFraction() float64 { + var ( + i = s.pos - 2 + c = 1 // '.' + ) + for isDigit(s.curr) { + s.nextChar() + c++ + } + v, err := strconv.ParseFloat(s.text[i:i+c], 64) + if err != nil { + panic(fmt.Errorf("xpath: scanFraction parse float got error: %v", err)) + } + return v +} + +func (s *scanner) scanNumber() float64 { + var ( + c int + i = s.pos - 1 + ) + for isDigit(s.curr) { + s.nextChar() + c++ + } + if s.curr == '.' { + s.nextChar() + c++ + for isDigit(s.curr) { + s.nextChar() + c++ + } + } + v, err := strconv.ParseFloat(s.text[i:i+c], 64) + if err != nil { + panic(fmt.Errorf("xpath: scanNumber parse float got error: %v", err)) + } + return v +} + +func (s *scanner) scanString() string { + var ( + c = 0 + end = s.curr + ) + s.nextChar() + i := s.pos - 1 + for s.curr != end { + if !s.nextChar() { + panic(errors.New("xpath: scanString got unclosed string")) + } + c++ + } + s.nextChar() + return s.text[i : i+c] +} + +func (s *scanner) scanName() string { + var ( + c int + i = s.pos - 1 + ) + for isName(s.curr) { + c++ + if !s.nextChar() { + break + } + } + return s.text[i : i+c] +} + +func isName(r rune) bool { + return string(r) != ":" && string(r) != "/" && + (unicode.Is(first, r) || unicode.Is(second, r) || string(r) == "*") +} + +func isDigit(r rune) bool { + return unicode.IsDigit(r) +} + +func asItemType(r rune) itemType { + switch r { + case ',': + return itemComma + case '@': + return itemAt + case '(': + return itemLParens + case ')': + return itemRParens + case '|': + return itemUnion + case '*': + return itemStar + case '[': + return itemLBracket + case ']': + return itemRBracket + case '+': + return itemPlus + case '-': + return itemMinus + case '=': + return itemEq + case '$': + return itemDollar + } + panic(fmt.Errorf("unknown item: %v", r)) +} + +var first = &unicode.RangeTable{ + R16: []unicode.Range16{ + {0x003A, 0x003A, 1}, + {0x0041, 0x005A, 1}, + {0x005F, 0x005F, 1}, + {0x0061, 0x007A, 1}, + {0x00C0, 0x00D6, 1}, + {0x00D8, 0x00F6, 1}, + {0x00F8, 0x00FF, 1}, + {0x0100, 0x0131, 1}, + {0x0134, 0x013E, 1}, + {0x0141, 0x0148, 1}, + {0x014A, 0x017E, 1}, + {0x0180, 0x01C3, 1}, + {0x01CD, 0x01F0, 1}, + {0x01F4, 0x01F5, 1}, + {0x01FA, 0x0217, 1}, + {0x0250, 0x02A8, 1}, + {0x02BB, 0x02C1, 1}, + {0x0386, 0x0386, 1}, + {0x0388, 0x038A, 1}, + {0x038C, 0x038C, 1}, + {0x038E, 0x03A1, 1}, + {0x03A3, 0x03CE, 1}, + {0x03D0, 0x03D6, 1}, + {0x03DA, 0x03E0, 2}, + {0x03E2, 0x03F3, 1}, + {0x0401, 0x040C, 1}, + {0x040E, 0x044F, 1}, + {0x0451, 0x045C, 1}, + {0x045E, 0x0481, 1}, + {0x0490, 0x04C4, 1}, + {0x04C7, 0x04C8, 1}, + {0x04CB, 0x04CC, 1}, + {0x04D0, 0x04EB, 1}, + {0x04EE, 0x04F5, 1}, + {0x04F8, 0x04F9, 1}, + {0x0531, 0x0556, 1}, + {0x0559, 0x0559, 1}, + {0x0561, 0x0586, 1}, + {0x05D0, 0x05EA, 1}, + {0x05F0, 0x05F2, 1}, + {0x0621, 0x063A, 1}, + {0x0641, 0x064A, 1}, + {0x0671, 0x06B7, 1}, + {0x06BA, 0x06BE, 1}, + {0x06C0, 0x06CE, 1}, + {0x06D0, 0x06D3, 1}, + {0x06D5, 0x06D5, 1}, + {0x06E5, 0x06E6, 1}, + {0x0905, 0x0939, 1}, + {0x093D, 0x093D, 1}, + {0x0958, 0x0961, 1}, + {0x0985, 0x098C, 1}, + {0x098F, 0x0990, 1}, + {0x0993, 0x09A8, 1}, + {0x09AA, 0x09B0, 1}, + {0x09B2, 0x09B2, 1}, + {0x09B6, 0x09B9, 1}, + {0x09DC, 0x09DD, 1}, + {0x09DF, 0x09E1, 1}, + {0x09F0, 0x09F1, 1}, + {0x0A05, 0x0A0A, 1}, + {0x0A0F, 0x0A10, 1}, + {0x0A13, 0x0A28, 1}, + {0x0A2A, 0x0A30, 1}, + {0x0A32, 0x0A33, 1}, + {0x0A35, 0x0A36, 1}, + {0x0A38, 0x0A39, 1}, + {0x0A59, 0x0A5C, 1}, + {0x0A5E, 0x0A5E, 1}, + {0x0A72, 0x0A74, 1}, + {0x0A85, 0x0A8B, 1}, + {0x0A8D, 0x0A8D, 1}, + {0x0A8F, 0x0A91, 1}, + {0x0A93, 0x0AA8, 1}, + {0x0AAA, 0x0AB0, 1}, + {0x0AB2, 0x0AB3, 1}, + {0x0AB5, 0x0AB9, 1}, + {0x0ABD, 0x0AE0, 0x23}, + {0x0B05, 0x0B0C, 1}, + {0x0B0F, 0x0B10, 1}, + {0x0B13, 0x0B28, 1}, + {0x0B2A, 0x0B30, 1}, + {0x0B32, 0x0B33, 1}, + {0x0B36, 0x0B39, 1}, + {0x0B3D, 0x0B3D, 1}, + {0x0B5C, 0x0B5D, 1}, + {0x0B5F, 0x0B61, 1}, + {0x0B85, 0x0B8A, 1}, + {0x0B8E, 0x0B90, 1}, + {0x0B92, 0x0B95, 1}, + {0x0B99, 0x0B9A, 1}, + {0x0B9C, 0x0B9C, 1}, + {0x0B9E, 0x0B9F, 1}, + {0x0BA3, 0x0BA4, 1}, + {0x0BA8, 0x0BAA, 1}, + {0x0BAE, 0x0BB5, 1}, + {0x0BB7, 0x0BB9, 1}, + {0x0C05, 0x0C0C, 1}, + {0x0C0E, 0x0C10, 1}, + {0x0C12, 0x0C28, 1}, + {0x0C2A, 0x0C33, 1}, + {0x0C35, 0x0C39, 1}, + {0x0C60, 0x0C61, 1}, + {0x0C85, 0x0C8C, 1}, + {0x0C8E, 0x0C90, 1}, + {0x0C92, 0x0CA8, 1}, + {0x0CAA, 0x0CB3, 1}, + {0x0CB5, 0x0CB9, 1}, + {0x0CDE, 0x0CDE, 1}, + {0x0CE0, 0x0CE1, 1}, + {0x0D05, 0x0D0C, 1}, + {0x0D0E, 0x0D10, 1}, + {0x0D12, 0x0D28, 1}, + {0x0D2A, 0x0D39, 1}, + {0x0D60, 0x0D61, 1}, + {0x0E01, 0x0E2E, 1}, + {0x0E30, 0x0E30, 1}, + {0x0E32, 0x0E33, 1}, + {0x0E40, 0x0E45, 1}, + {0x0E81, 0x0E82, 1}, + {0x0E84, 0x0E84, 1}, + {0x0E87, 0x0E88, 1}, + {0x0E8A, 0x0E8D, 3}, + {0x0E94, 0x0E97, 1}, + {0x0E99, 0x0E9F, 1}, + {0x0EA1, 0x0EA3, 1}, + {0x0EA5, 0x0EA7, 2}, + {0x0EAA, 0x0EAB, 1}, + {0x0EAD, 0x0EAE, 1}, + {0x0EB0, 0x0EB0, 1}, + {0x0EB2, 0x0EB3, 1}, + {0x0EBD, 0x0EBD, 1}, + {0x0EC0, 0x0EC4, 1}, + {0x0F40, 0x0F47, 1}, + {0x0F49, 0x0F69, 1}, + {0x10A0, 0x10C5, 1}, + {0x10D0, 0x10F6, 1}, + {0x1100, 0x1100, 1}, + {0x1102, 0x1103, 1}, + {0x1105, 0x1107, 1}, + {0x1109, 0x1109, 1}, + {0x110B, 0x110C, 1}, + {0x110E, 0x1112, 1}, + {0x113C, 0x1140, 2}, + {0x114C, 0x1150, 2}, + {0x1154, 0x1155, 1}, + {0x1159, 0x1159, 1}, + {0x115F, 0x1161, 1}, + {0x1163, 0x1169, 2}, + {0x116D, 0x116E, 1}, + {0x1172, 0x1173, 1}, + {0x1175, 0x119E, 0x119E - 0x1175}, + {0x11A8, 0x11AB, 0x11AB - 0x11A8}, + {0x11AE, 0x11AF, 1}, + {0x11B7, 0x11B8, 1}, + {0x11BA, 0x11BA, 1}, + {0x11BC, 0x11C2, 1}, + {0x11EB, 0x11F0, 0x11F0 - 0x11EB}, + {0x11F9, 0x11F9, 1}, + {0x1E00, 0x1E9B, 1}, + {0x1EA0, 0x1EF9, 1}, + {0x1F00, 0x1F15, 1}, + {0x1F18, 0x1F1D, 1}, + {0x1F20, 0x1F45, 1}, + {0x1F48, 0x1F4D, 1}, + {0x1F50, 0x1F57, 1}, + {0x1F59, 0x1F5B, 0x1F5B - 0x1F59}, + {0x1F5D, 0x1F5D, 1}, + {0x1F5F, 0x1F7D, 1}, + {0x1F80, 0x1FB4, 1}, + {0x1FB6, 0x1FBC, 1}, + {0x1FBE, 0x1FBE, 1}, + {0x1FC2, 0x1FC4, 1}, + {0x1FC6, 0x1FCC, 1}, + {0x1FD0, 0x1FD3, 1}, + {0x1FD6, 0x1FDB, 1}, + {0x1FE0, 0x1FEC, 1}, + {0x1FF2, 0x1FF4, 1}, + {0x1FF6, 0x1FFC, 1}, + {0x2126, 0x2126, 1}, + {0x212A, 0x212B, 1}, + {0x212E, 0x212E, 1}, + {0x2180, 0x2182, 1}, + {0x3007, 0x3007, 1}, + {0x3021, 0x3029, 1}, + {0x3041, 0x3094, 1}, + {0x30A1, 0x30FA, 1}, + {0x3105, 0x312C, 1}, + {0x4E00, 0x9FA5, 1}, + {0xAC00, 0xD7A3, 1}, + }, +} + +var second = &unicode.RangeTable{ + R16: []unicode.Range16{ + {0x002D, 0x002E, 1}, + {0x0030, 0x0039, 1}, + {0x00B7, 0x00B7, 1}, + {0x02D0, 0x02D1, 1}, + {0x0300, 0x0345, 1}, + {0x0360, 0x0361, 1}, + {0x0387, 0x0387, 1}, + {0x0483, 0x0486, 1}, + {0x0591, 0x05A1, 1}, + {0x05A3, 0x05B9, 1}, + {0x05BB, 0x05BD, 1}, + {0x05BF, 0x05BF, 1}, + {0x05C1, 0x05C2, 1}, + {0x05C4, 0x0640, 0x0640 - 0x05C4}, + {0x064B, 0x0652, 1}, + {0x0660, 0x0669, 1}, + {0x0670, 0x0670, 1}, + {0x06D6, 0x06DC, 1}, + {0x06DD, 0x06DF, 1}, + {0x06E0, 0x06E4, 1}, + {0x06E7, 0x06E8, 1}, + {0x06EA, 0x06ED, 1}, + {0x06F0, 0x06F9, 1}, + {0x0901, 0x0903, 1}, + {0x093C, 0x093C, 1}, + {0x093E, 0x094C, 1}, + {0x094D, 0x094D, 1}, + {0x0951, 0x0954, 1}, + {0x0962, 0x0963, 1}, + {0x0966, 0x096F, 1}, + {0x0981, 0x0983, 1}, + {0x09BC, 0x09BC, 1}, + {0x09BE, 0x09BF, 1}, + {0x09C0, 0x09C4, 1}, + {0x09C7, 0x09C8, 1}, + {0x09CB, 0x09CD, 1}, + {0x09D7, 0x09D7, 1}, + {0x09E2, 0x09E3, 1}, + {0x09E6, 0x09EF, 1}, + {0x0A02, 0x0A3C, 0x3A}, + {0x0A3E, 0x0A3F, 1}, + {0x0A40, 0x0A42, 1}, + {0x0A47, 0x0A48, 1}, + {0x0A4B, 0x0A4D, 1}, + {0x0A66, 0x0A6F, 1}, + {0x0A70, 0x0A71, 1}, + {0x0A81, 0x0A83, 1}, + {0x0ABC, 0x0ABC, 1}, + {0x0ABE, 0x0AC5, 1}, + {0x0AC7, 0x0AC9, 1}, + {0x0ACB, 0x0ACD, 1}, + {0x0AE6, 0x0AEF, 1}, + {0x0B01, 0x0B03, 1}, + {0x0B3C, 0x0B3C, 1}, + {0x0B3E, 0x0B43, 1}, + {0x0B47, 0x0B48, 1}, + {0x0B4B, 0x0B4D, 1}, + {0x0B56, 0x0B57, 1}, + {0x0B66, 0x0B6F, 1}, + {0x0B82, 0x0B83, 1}, + {0x0BBE, 0x0BC2, 1}, + {0x0BC6, 0x0BC8, 1}, + {0x0BCA, 0x0BCD, 1}, + {0x0BD7, 0x0BD7, 1}, + {0x0BE7, 0x0BEF, 1}, + {0x0C01, 0x0C03, 1}, + {0x0C3E, 0x0C44, 1}, + {0x0C46, 0x0C48, 1}, + {0x0C4A, 0x0C4D, 1}, + {0x0C55, 0x0C56, 1}, + {0x0C66, 0x0C6F, 1}, + {0x0C82, 0x0C83, 1}, + {0x0CBE, 0x0CC4, 1}, + {0x0CC6, 0x0CC8, 1}, + {0x0CCA, 0x0CCD, 1}, + {0x0CD5, 0x0CD6, 1}, + {0x0CE6, 0x0CEF, 1}, + {0x0D02, 0x0D03, 1}, + {0x0D3E, 0x0D43, 1}, + {0x0D46, 0x0D48, 1}, + {0x0D4A, 0x0D4D, 1}, + {0x0D57, 0x0D57, 1}, + {0x0D66, 0x0D6F, 1}, + {0x0E31, 0x0E31, 1}, + {0x0E34, 0x0E3A, 1}, + {0x0E46, 0x0E46, 1}, + {0x0E47, 0x0E4E, 1}, + {0x0E50, 0x0E59, 1}, + {0x0EB1, 0x0EB1, 1}, + {0x0EB4, 0x0EB9, 1}, + {0x0EBB, 0x0EBC, 1}, + {0x0EC6, 0x0EC6, 1}, + {0x0EC8, 0x0ECD, 1}, + {0x0ED0, 0x0ED9, 1}, + {0x0F18, 0x0F19, 1}, + {0x0F20, 0x0F29, 1}, + {0x0F35, 0x0F39, 2}, + {0x0F3E, 0x0F3F, 1}, + {0x0F71, 0x0F84, 1}, + {0x0F86, 0x0F8B, 1}, + {0x0F90, 0x0F95, 1}, + {0x0F97, 0x0F97, 1}, + {0x0F99, 0x0FAD, 1}, + {0x0FB1, 0x0FB7, 1}, + {0x0FB9, 0x0FB9, 1}, + {0x20D0, 0x20DC, 1}, + {0x20E1, 0x3005, 0x3005 - 0x20E1}, + {0x302A, 0x302F, 1}, + {0x3031, 0x3035, 1}, + {0x3099, 0x309A, 1}, + {0x309D, 0x309E, 1}, + {0x30FC, 0x30FE, 1}, + }, +} diff --git a/vendor/github.com/antchfx/xpath/query.go b/vendor/github.com/antchfx/xpath/query.go new file mode 100644 index 00000000..9735ef09 --- /dev/null +++ b/vendor/github.com/antchfx/xpath/query.go @@ -0,0 +1,791 @@ +package xpath + +import ( + "reflect" +) + +type iterator interface { + Current() NodeNavigator +} + +// An XPath query interface. +type query interface { + // Select traversing iterator returns a query matched node NodeNavigator. + Select(iterator) NodeNavigator + + // Evaluate evaluates query and returns values of the current query. + Evaluate(iterator) interface{} + + Clone() query +} + +// contextQuery is returns current node on the iterator object query. +type contextQuery struct { + count int + Root bool // Moving to root-level node in the current context iterator. +} + +func (c *contextQuery) Select(t iterator) (n NodeNavigator) { + if c.count == 0 { + c.count++ + n = t.Current().Copy() + if c.Root { + n.MoveToRoot() + } + } + return n +} + +func (c *contextQuery) Evaluate(iterator) interface{} { + c.count = 0 + return c +} + +func (c *contextQuery) Clone() query { + return &contextQuery{count: 0, Root: c.Root} +} + +// ancestorQuery is an XPath ancestor node query.(ancestor::*|ancestor-self::*) +type ancestorQuery struct { + iterator func() NodeNavigator + + Self bool + Input query + Predicate func(NodeNavigator) bool +} + +func (a *ancestorQuery) Select(t iterator) NodeNavigator { + for { + if a.iterator == nil { + node := a.Input.Select(t) + if node == nil { + return nil + } + first := true + a.iterator = func() NodeNavigator { + if first && a.Self { + first = false + if a.Predicate(node) { + return node + } + } + for node.MoveToParent() { + if !a.Predicate(node) { + continue + } + return node + } + return nil + } + } + + if node := a.iterator(); node != nil { + return node + } + a.iterator = nil + } +} + +func (a *ancestorQuery) Evaluate(t iterator) interface{} { + a.Input.Evaluate(t) + a.iterator = nil + return a +} + +func (a *ancestorQuery) Test(n NodeNavigator) bool { + return a.Predicate(n) +} + +func (a *ancestorQuery) Clone() query { + return &ancestorQuery{Self: a.Self, Input: a.Input.Clone(), Predicate: a.Predicate} +} + +// attributeQuery is an XPath attribute node query.(@*) +type attributeQuery struct { + iterator func() NodeNavigator + + Input query + Predicate func(NodeNavigator) bool +} + +func (a *attributeQuery) Select(t iterator) NodeNavigator { + for { + if a.iterator == nil { + node := a.Input.Select(t) + if node == nil { + return nil + } + node = node.Copy() + a.iterator = func() NodeNavigator { + for { + onAttr := node.MoveToNextAttribute() + if !onAttr { + return nil + } + if a.Predicate(node) { + return node + } + } + } + } + + if node := a.iterator(); node != nil { + return node + } + a.iterator = nil + } +} + +func (a *attributeQuery) Evaluate(t iterator) interface{} { + a.Input.Evaluate(t) + a.iterator = nil + return a +} + +func (a *attributeQuery) Test(n NodeNavigator) bool { + return a.Predicate(n) +} + +func (a *attributeQuery) Clone() query { + return &attributeQuery{Input: a.Input.Clone(), Predicate: a.Predicate} +} + +// childQuery is an XPath child node query.(child::*) +type childQuery struct { + posit int + iterator func() NodeNavigator + + Input query + Predicate func(NodeNavigator) bool +} + +func (c *childQuery) Select(t iterator) NodeNavigator { + for { + if c.iterator == nil { + c.posit = 0 + node := c.Input.Select(t) + if node == nil { + return nil + } + node = node.Copy() + first := true + c.iterator = func() NodeNavigator { + for { + if (first && !node.MoveToChild()) || (!first && !node.MoveToNext()) { + return nil + } + first = false + if c.Predicate(node) { + return node + } + } + } + } + + if node := c.iterator(); node != nil { + c.posit++ + return node + } + c.iterator = nil + } +} + +func (c *childQuery) Evaluate(t iterator) interface{} { + c.Input.Evaluate(t) + c.iterator = nil + return c +} + +func (c *childQuery) Test(n NodeNavigator) bool { + return c.Predicate(n) +} + +func (c *childQuery) Clone() query { + return &childQuery{Input: c.Input.Clone(), Predicate: c.Predicate} +} + +// position returns a position of current NodeNavigator. +func (c *childQuery) position() int { + return c.posit +} + +// descendantQuery is an XPath descendant node query.(descendant::* | descendant-or-self::*) +type descendantQuery struct { + iterator func() NodeNavigator + posit int + + Self bool + Input query + Predicate func(NodeNavigator) bool +} + +func (d *descendantQuery) Select(t iterator) NodeNavigator { + for { + if d.iterator == nil { + d.posit = 0 + node := d.Input.Select(t) + if node == nil { + return nil + } + node = node.Copy() + level := 0 + first := true + d.iterator = func() NodeNavigator { + if first && d.Self { + first = false + if d.Predicate(node) { + return node + } + } + + for { + if node.MoveToChild() { + level++ + } else { + for { + if level == 0 { + return nil + } + if node.MoveToNext() { + break + } + node.MoveToParent() + level-- + } + } + if d.Predicate(node) { + return node + } + } + } + } + + if node := d.iterator(); node != nil { + d.posit++ + return node + } + d.iterator = nil + } +} + +func (d *descendantQuery) Evaluate(t iterator) interface{} { + d.Input.Evaluate(t) + d.iterator = nil + return d +} + +func (d *descendantQuery) Test(n NodeNavigator) bool { + return d.Predicate(n) +} + +// position returns a position of current NodeNavigator. +func (d *descendantQuery) position() int { + return d.posit +} + +func (d *descendantQuery) Clone() query { + return &descendantQuery{Self: d.Self, Input: d.Input.Clone(), Predicate: d.Predicate} +} + +// followingQuery is an XPath following node query.(following::*|following-sibling::*) +type followingQuery struct { + iterator func() NodeNavigator + + Input query + Sibling bool // The matching sibling node of current node. + Predicate func(NodeNavigator) bool +} + +func (f *followingQuery) Select(t iterator) NodeNavigator { + for { + if f.iterator == nil { + node := f.Input.Select(t) + if node == nil { + return nil + } + node = node.Copy() + if f.Sibling { + f.iterator = func() NodeNavigator { + for { + if !node.MoveToNext() { + return nil + } + if f.Predicate(node) { + return node + } + } + } + } else { + var q query // descendant query + f.iterator = func() NodeNavigator { + for { + if q == nil { + for !node.MoveToNext() { + if !node.MoveToParent() { + return nil + } + } + q = &descendantQuery{ + Self: true, + Input: &contextQuery{}, + Predicate: f.Predicate, + } + t.Current().MoveTo(node) + } + if node := q.Select(t); node != nil { + return node + } + q = nil + } + } + } + } + + if node := f.iterator(); node != nil { + return node + } + f.iterator = nil + } +} + +func (f *followingQuery) Evaluate(t iterator) interface{} { + f.Input.Evaluate(t) + return f +} + +func (f *followingQuery) Test(n NodeNavigator) bool { + return f.Predicate(n) +} + +func (f *followingQuery) Clone() query { + return &followingQuery{Input: f.Input.Clone(), Sibling: f.Sibling, Predicate: f.Predicate} +} + +// precedingQuery is an XPath preceding node query.(preceding::*) +type precedingQuery struct { + iterator func() NodeNavigator + Input query + Sibling bool // The matching sibling node of current node. + Predicate func(NodeNavigator) bool +} + +func (p *precedingQuery) Select(t iterator) NodeNavigator { + for { + if p.iterator == nil { + node := p.Input.Select(t) + if node == nil { + return nil + } + node = node.Copy() + if p.Sibling { + p.iterator = func() NodeNavigator { + for { + for !node.MoveToPrevious() { + return nil + } + if p.Predicate(node) { + return node + } + } + } + } else { + var q query + p.iterator = func() NodeNavigator { + for { + if q == nil { + for !node.MoveToPrevious() { + if !node.MoveToParent() { + return nil + } + } + q = &descendantQuery{ + Self: true, + Input: &contextQuery{}, + Predicate: p.Predicate, + } + t.Current().MoveTo(node) + } + if node := q.Select(t); node != nil { + return node + } + q = nil + } + } + } + } + if node := p.iterator(); node != nil { + return node + } + p.iterator = nil + } +} + +func (p *precedingQuery) Evaluate(t iterator) interface{} { + p.Input.Evaluate(t) + return p +} + +func (p *precedingQuery) Test(n NodeNavigator) bool { + return p.Predicate(n) +} + +func (p *precedingQuery) Clone() query { + return &precedingQuery{Input: p.Input.Clone(), Sibling: p.Sibling, Predicate: p.Predicate} +} + +// parentQuery is an XPath parent node query.(parent::*) +type parentQuery struct { + Input query + Predicate func(NodeNavigator) bool +} + +func (p *parentQuery) Select(t iterator) NodeNavigator { + for { + node := p.Input.Select(t) + if node == nil { + return nil + } + node = node.Copy() + if node.MoveToParent() && p.Predicate(node) { + return node + } + } +} + +func (p *parentQuery) Evaluate(t iterator) interface{} { + p.Input.Evaluate(t) + return p +} + +func (p *parentQuery) Clone() query { + return &parentQuery{Input: p.Input.Clone(), Predicate: p.Predicate} +} + +func (p *parentQuery) Test(n NodeNavigator) bool { + return p.Predicate(n) +} + +// selfQuery is an Self node query.(self::*) +type selfQuery struct { + Input query + Predicate func(NodeNavigator) bool +} + +func (s *selfQuery) Select(t iterator) NodeNavigator { + for { + node := s.Input.Select(t) + if node == nil { + return nil + } + + if s.Predicate(node) { + return node + } + } +} + +func (s *selfQuery) Evaluate(t iterator) interface{} { + s.Input.Evaluate(t) + return s +} + +func (s *selfQuery) Test(n NodeNavigator) bool { + return s.Predicate(n) +} + +func (s *selfQuery) Clone() query { + return &selfQuery{Input: s.Input.Clone(), Predicate: s.Predicate} +} + +// filterQuery is an XPath query for predicate filter. +type filterQuery struct { + Input query + Predicate query +} + +func (f *filterQuery) do(t iterator) bool { + val := reflect.ValueOf(f.Predicate.Evaluate(t)) + switch val.Kind() { + case reflect.Bool: + return val.Bool() + case reflect.String: + return len(val.String()) > 0 + case reflect.Float64: + pt := float64(getNodePosition(f.Input)) + return int(val.Float()) == int(pt) + default: + if q, ok := f.Predicate.(query); ok { + return q.Select(t) != nil + } + } + return false +} + +func (f *filterQuery) Select(t iterator) NodeNavigator { + for { + node := f.Input.Select(t) + if node == nil { + return node + } + node = node.Copy() + //fmt.Println(node.LocalName()) + + t.Current().MoveTo(node) + if f.do(t) { + return node + } + } +} + +func (f *filterQuery) Evaluate(t iterator) interface{} { + f.Input.Evaluate(t) + return f +} + +func (f *filterQuery) Clone() query { + return &filterQuery{Input: f.Input.Clone(), Predicate: f.Predicate.Clone()} +} + +// functionQuery is an XPath function that call a function to returns +// value of current NodeNavigator node. +type functionQuery struct { + Input query // Node Set + Func func(query, iterator) interface{} // The xpath function. +} + +func (f *functionQuery) Select(t iterator) NodeNavigator { + return nil +} + +// Evaluate call a specified function that will returns the +// following value type: number,string,boolean. +func (f *functionQuery) Evaluate(t iterator) interface{} { + return f.Func(f.Input, t) +} + +func (f *functionQuery) Clone() query { + return &functionQuery{Input: f.Input.Clone(), Func: f.Func} +} + +// constantQuery is an XPath constant operand. +type constantQuery struct { + Val interface{} +} + +func (c *constantQuery) Select(t iterator) NodeNavigator { + return nil +} + +func (c *constantQuery) Evaluate(t iterator) interface{} { + return c.Val +} + +func (c *constantQuery) Clone() query { + return c +} + +// logicalQuery is an XPath logical expression. +type logicalQuery struct { + Left, Right query + + Do func(iterator, interface{}, interface{}) interface{} +} + +func (l *logicalQuery) Select(t iterator) NodeNavigator { + // When a XPath expr is logical expression. + node := t.Current().Copy() + val := l.Evaluate(t) + switch val.(type) { + case bool: + if val.(bool) == true { + return node + } + } + return nil +} + +func (l *logicalQuery) Evaluate(t iterator) interface{} { + m := l.Left.Evaluate(t) + n := l.Right.Evaluate(t) + return l.Do(t, m, n) +} + +func (l *logicalQuery) Clone() query { + return &logicalQuery{Left: l.Left.Clone(), Right: l.Right.Clone(), Do: l.Do} +} + +// numericQuery is an XPath numeric operator expression. +type numericQuery struct { + Left, Right query + + Do func(interface{}, interface{}) interface{} +} + +func (n *numericQuery) Select(t iterator) NodeNavigator { + return nil +} + +func (n *numericQuery) Evaluate(t iterator) interface{} { + m := n.Left.Evaluate(t) + k := n.Right.Evaluate(t) + return n.Do(m, k) +} + +func (n *numericQuery) Clone() query { + return &numericQuery{Left: n.Left.Clone(), Right: n.Right.Clone(), Do: n.Do} +} + +type booleanQuery struct { + IsOr bool + Left, Right query + iterator func() NodeNavigator +} + +func (b *booleanQuery) Select(t iterator) NodeNavigator { + if b.iterator == nil { + var list []NodeNavigator + i := 0 + root := t.Current().Copy() + if b.IsOr { + for { + node := b.Left.Select(t) + if node == nil { + break + } + node = node.Copy() + list = append(list, node) + } + t.Current().MoveTo(root) + for { + node := b.Right.Select(t) + if node == nil { + break + } + node = node.Copy() + list = append(list, node) + } + } else { + var m []NodeNavigator + var n []NodeNavigator + for { + node := b.Left.Select(t) + if node == nil { + break + } + node = node.Copy() + list = append(m, node) + } + t.Current().MoveTo(root) + for { + node := b.Right.Select(t) + if node == nil { + break + } + node = node.Copy() + list = append(n, node) + } + for _, k := range m { + for _, j := range n { + if k == j { + list = append(list, k) + } + } + } + } + + b.iterator = func() NodeNavigator { + if i >= len(list) { + return nil + } + node := list[i] + i++ + return node + } + } + return b.iterator() +} + +func (b *booleanQuery) Evaluate(t iterator) interface{} { + m := b.Left.Evaluate(t) + left := asBool(t, m) + if b.IsOr && left { + return true + } else if !b.IsOr && !left { + return false + } + m = b.Right.Evaluate(t) + return asBool(t, m) +} + +func (b *booleanQuery) Clone() query { + return &booleanQuery{IsOr: b.IsOr, Left: b.Left.Clone(), Right: b.Right.Clone()} +} + +type unionQuery struct { + Left, Right query + iterator func() NodeNavigator +} + +func (u *unionQuery) Select(t iterator) NodeNavigator { + if u.iterator == nil { + var list []NodeNavigator + var i int + root := t.Current().Copy() + for { + node := u.Left.Select(t) + if node == nil { + break + } + node = node.Copy() + list = append(list, node) + } + t.Current().MoveTo(root) + for { + node := u.Right.Select(t) + if node == nil { + break + } + node = node.Copy() + var exists bool + for _, x := range list { + if reflect.DeepEqual(x, node) { + exists = true + break + } + } + if !exists { + list = append(list, node) + } + } + u.iterator = func() NodeNavigator { + if i >= len(list) { + return nil + } + node := list[i] + i++ + return node + } + } + return u.iterator() +} + +func (u *unionQuery) Evaluate(t iterator) interface{} { + u.iterator = nil + u.Left.Evaluate(t) + u.Right.Evaluate(t) + return u +} + +func (u *unionQuery) Clone() query { + return &unionQuery{Left: u.Left.Clone(), Right: u.Right.Clone()} +} + +func getNodePosition(q query) int { + type Position interface { + position() int + } + if count, ok := q.(Position); ok { + return count.position() + } + return 1 +} diff --git a/vendor/github.com/antchfx/xpath/xpath.go b/vendor/github.com/antchfx/xpath/xpath.go new file mode 100644 index 00000000..7e3f52c3 --- /dev/null +++ b/vendor/github.com/antchfx/xpath/xpath.go @@ -0,0 +1,157 @@ +package xpath + +import ( + "errors" +) + +// NodeType represents a type of XPath node. +type NodeType int + +const ( + // RootNode is a root node of the XML document or node tree. + RootNode NodeType = iota + + // ElementNode is an element, such as . + ElementNode + + // AttributeNode is an attribute, such as id='123'. + AttributeNode + + // TextNode is the text content of a node. + TextNode + + // CommentNode is a comment node, such as + CommentNode + + // allNode is any types of node, used by xpath package only to predicate match. + allNode +) + +// NodeNavigator provides cursor model for navigating XML data. +type NodeNavigator interface { + // NodeType returns the XPathNodeType of the current node. + NodeType() NodeType + + // LocalName gets the Name of the current node. + LocalName() string + + // Prefix returns namespace prefix associated with the current node. + Prefix() string + + // Value gets the value of current node. + Value() string + + // Copy does a deep copy of the NodeNavigator and all its components. + Copy() NodeNavigator + + // MoveToRoot moves the NodeNavigator to the root node of the current node. + MoveToRoot() + + // MoveToParent moves the NodeNavigator to the parent node of the current node. + MoveToParent() bool + + // MoveToNextAttribute moves the NodeNavigator to the next attribute on current node. + MoveToNextAttribute() bool + + // MoveToChild moves the NodeNavigator to the first child node of the current node. + MoveToChild() bool + + // MoveToFirst moves the NodeNavigator to the first sibling node of the current node. + MoveToFirst() bool + + // MoveToNext moves the NodeNavigator to the next sibling node of the current node. + MoveToNext() bool + + // MoveToPrevious moves the NodeNavigator to the previous sibling node of the current node. + MoveToPrevious() bool + + // MoveTo moves the NodeNavigator to the same position as the specified NodeNavigator. + MoveTo(NodeNavigator) bool +} + +// NodeIterator holds all matched Node object. +type NodeIterator struct { + node NodeNavigator + query query +} + +// Current returns current node which matched. +func (t *NodeIterator) Current() NodeNavigator { + return t.node +} + +// MoveNext moves Navigator to the next match node. +func (t *NodeIterator) MoveNext() bool { + n := t.query.Select(t) + if n != nil { + if !t.node.MoveTo(n) { + t.node = n.Copy() + } + return true + } + return false +} + +// Select selects a node set using the specified XPath expression. +// This method is deprecated, recommend using Expr.Select() method instead. +func Select(root NodeNavigator, expr string) *NodeIterator { + exp, err := Compile(expr) + if err != nil { + panic(err) + } + return exp.Select(root) +} + +// Expr is an XPath expression for query. +type Expr struct { + s string + q query +} + +type iteratorFunc func() NodeNavigator + +func (f iteratorFunc) Current() NodeNavigator { + return f() +} + +// Evaluate returns the result of the expression. +// The result type of the expression is one of the follow: bool,float64,string,NodeIterator). +func (expr *Expr) Evaluate(root NodeNavigator) interface{} { + val := expr.q.Evaluate(iteratorFunc(func() NodeNavigator { return root })) + switch val.(type) { + case query: + return &NodeIterator{query: expr.q.Clone(), node: root} + } + return val +} + +// Select selects a node set using the specified XPath expression. +func (expr *Expr) Select(root NodeNavigator) *NodeIterator { + return &NodeIterator{query: expr.q.Clone(), node: root} +} + +// String returns XPath expression string. +func (expr *Expr) String() string { + return expr.s +} + +// Compile compiles an XPath expression string. +func Compile(expr string) (*Expr, error) { + if expr == "" { + return nil, errors.New("expr expression is nil") + } + qy, err := build(expr) + if err != nil { + return nil, err + } + return &Expr{s: expr, q: qy}, nil +} + +// MustCompile compiles an XPath expression string and ignored error. +func MustCompile(expr string) *Expr { + exp, err := Compile(expr) + if err != nil { + return nil + } + return exp +}