├── README.md ├── cmd └── goxpath │ ├── test │ ├── subdir │ │ ├── 2.xml │ │ └── 3.xml │ └── 1.xml │ ├── goxpath.go │ └── goxpath_test.go ├── parser ├── pathexpr │ └── pathexpr.go ├── ast.go └── parser.go ├── tree ├── interfaces.go ├── xmltree │ ├── xmlbuilder │ │ └── xmlbuilder.go │ ├── xmlnode │ │ └── xmlnode.go │ ├── xmlele │ │ └── xmlele.go │ └── xmltree.go ├── xmlstruct │ ├── xmlroot.go │ ├── xmlstruct.go │ ├── xmlnode.go │ ├── xmlstruct_test.go │ └── xmlele.go ├── xfn.go ├── xtypes.go └── tree.go ├── internal ├── xsort │ └── xsort.go └── execxp │ ├── execxp.go │ ├── intfns │ ├── numfns.go │ ├── intfns.go │ ├── boolfns.go │ ├── nodesetfns.go │ └── stringfns.go │ ├── operators.go │ ├── findutil │ └── findUtil.go │ └── paths.go ├── .travis.yml ├── coverage.sh ├── LICENSE ├── value_test.go ├── xconst └── xconst.go ├── marshal.go ├── comp_test.go ├── goxpath.go ├── lexer ├── paths.go ├── xmlchars.go └── lexer.go ├── err_test.go ├── misc_test.go ├── fns_test.go └── path_test.go /README.md: -------------------------------------------------------------------------------- 1 | # Deprecated. Use [xsel](https://github.com/ChrisTrenkamp/xsel) instead. 2 | -------------------------------------------------------------------------------- /cmd/goxpath/test/subdir/2.xml: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /cmd/goxpath/test/subdir/3.xml: -------------------------------------------------------------------------------- 1 | bar2 2 | -------------------------------------------------------------------------------- /cmd/goxpath/test/1.xml: -------------------------------------------------------------------------------- 1 | testpathtest2 2 | -------------------------------------------------------------------------------- /parser/pathexpr/pathexpr.go: -------------------------------------------------------------------------------- 1 | package pathexpr 2 | 3 | import "encoding/xml" 4 | 5 | //PathExpr represents XPath step's. xmltree.XMLTree uses it to find nodes. 6 | type PathExpr struct { 7 | Name xml.Name 8 | Axis string 9 | NodeType string 10 | NS map[string]string 11 | } 12 | -------------------------------------------------------------------------------- /tree/interfaces.go: -------------------------------------------------------------------------------- 1 | package tree 2 | 3 | import "fmt" 4 | 5 | //Result is used for all data types. 6 | type Result interface { 7 | fmt.Stringer 8 | } 9 | 10 | //IsBool is used for the XPath boolean function. It turns the data type to a bool. 11 | type IsBool interface { 12 | Bool() Bool 13 | } 14 | 15 | //IsNum is used for the XPath number function. It turns the data type to a number. 16 | type IsNum interface { 17 | Num() Num 18 | } 19 | -------------------------------------------------------------------------------- /internal/xsort/xsort.go: -------------------------------------------------------------------------------- 1 | package xsort 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/ChrisTrenkamp/goxpath/tree" 7 | ) 8 | 9 | type nodeSort []tree.Node 10 | 11 | func (ns nodeSort) Len() int { return len(ns) } 12 | func (ns nodeSort) Swap(i, j int) { ns[i], ns[j] = ns[j], ns[i] } 13 | func (ns nodeSort) Less(i, j int) bool { 14 | return ns[i].Pos() < ns[j].Pos() 15 | } 16 | 17 | //SortNodes sorts the array by the node document order 18 | func SortNodes(res []tree.Node) { 19 | sort.Sort(nodeSort(res)) 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.6 5 | 6 | before_install: 7 | - go get -u github.com/ChrisTrenkamp/goxpath 8 | - go get -u github.com/ChrisTrenkamp/goxpath/cmd/goxpath 9 | - go get -u github.com/wadey/gocovmerge 10 | 11 | script: 12 | - go list -f '{{if gt (len .TestGoFiles) 0}}"go test -covermode count -coverprofile {{.Name}}.coverprofile -coverpkg ./... {{.ImportPath}}"{{end}}' ./... | xargs -I {} bash -c {} 13 | - gocovmerge `ls *.coverprofile` > coverage.txt 14 | 15 | after_success: 16 | - bash <(curl -s https://codecov.io/bash) 17 | -------------------------------------------------------------------------------- /tree/xmltree/xmlbuilder/xmlbuilder.go: -------------------------------------------------------------------------------- 1 | package xmlbuilder 2 | 3 | import ( 4 | "encoding/xml" 5 | 6 | "github.com/ChrisTrenkamp/goxpath/tree" 7 | ) 8 | 9 | //BuilderOpts supplies all the information needed to create an XML node. 10 | type BuilderOpts struct { 11 | Dec *xml.Decoder 12 | Tok xml.Token 13 | NodeType tree.NodeType 14 | NS map[xml.Name]string 15 | Attrs []*xml.Attr 16 | NodePos int 17 | AttrStartPos int 18 | } 19 | 20 | //XMLBuilder is an interface for creating XML structures. 21 | type XMLBuilder interface { 22 | tree.Node 23 | CreateNode(*BuilderOpts) XMLBuilder 24 | EndElem() XMLBuilder 25 | } 26 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 3 | go get github.com/ChrisTrenkamp/goxpath/cmd/goxpath 4 | if [ $? != 0 ]; then 5 | exit 1 6 | fi 7 | go test >/dev/null 2>&1 8 | if [ $? != 0 ]; then 9 | go test 10 | exit 1 11 | fi 12 | gometalinter --deadline=1m ./... 13 | go list -f '{{if gt (len .TestGoFiles) 0}}"go test -covermode count -coverprofile {{.Name}}.coverprofile -coverpkg ./... {{.ImportPath}}"{{end}} >/dev/null' ./... | xargs -I {} bash -c {} 2>/dev/null 14 | gocovmerge `ls *.coverprofile` > coverage.txt 15 | go tool cover -html=coverage.txt -o coverage.html 16 | firefox coverage.html 17 | rm coverage.html coverage.txt *.coverprofile 18 | -------------------------------------------------------------------------------- /internal/execxp/execxp.go: -------------------------------------------------------------------------------- 1 | package execxp 2 | 3 | import ( 4 | "encoding/xml" 5 | 6 | "github.com/ChrisTrenkamp/goxpath/parser" 7 | "github.com/ChrisTrenkamp/goxpath/tree" 8 | ) 9 | 10 | //Exec executes the XPath expression, xp, against the tree, t, with the 11 | //namespace mappings, ns. 12 | func Exec(n *parser.Node, t tree.Node, ns map[string]string, fns map[xml.Name]tree.Wrap, v map[string]tree.Result) (tree.Result, error) { 13 | f := xpFilt{ 14 | t: t, 15 | ns: ns, 16 | ctx: tree.NodeSet{t}, 17 | fns: fns, 18 | variables: v, 19 | } 20 | 21 | return exec(&f, n) 22 | } 23 | 24 | func exec(f *xpFilt, n *parser.Node) (tree.Result, error) { 25 | err := xfExec(f, n) 26 | return f.ctx, err 27 | } 28 | -------------------------------------------------------------------------------- /tree/xmlstruct/xmlroot.go: -------------------------------------------------------------------------------- 1 | package xmlstruct 2 | 3 | import ( 4 | "encoding/xml" 5 | 6 | "github.com/ChrisTrenkamp/goxpath/tree" 7 | ) 8 | 9 | type XMLRoot struct { 10 | Ele *XMLEle 11 | } 12 | 13 | func (x *XMLRoot) ResValue() string { 14 | return x.Ele.ResValue() 15 | } 16 | 17 | func (x *XMLRoot) Pos() int { 18 | return 0 19 | } 20 | 21 | func (x *XMLRoot) GetToken() xml.Token { 22 | return xml.StartElement{} 23 | } 24 | 25 | func (x *XMLRoot) GetParent() tree.Elem { 26 | return x 27 | } 28 | 29 | func (x *XMLRoot) GetNodeType() tree.NodeType { 30 | return tree.NtRoot 31 | } 32 | 33 | func (x *XMLRoot) GetChildren() []tree.Node { 34 | return []tree.Node{x.Ele} 35 | } 36 | 37 | func (x *XMLRoot) GetAttrs() []tree.Node { 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /tree/xmlstruct/xmlstruct.go: -------------------------------------------------------------------------------- 1 | package xmlstruct 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | func ParseStruct(i interface{}) (*XMLRoot, error) { 9 | ret := &XMLRoot{} 10 | val := reflect.ValueOf(i) 11 | 12 | if val.Kind() == reflect.Ptr { 13 | val = val.Elem() 14 | } 15 | 16 | if val.Kind() != reflect.Struct { 17 | return ret, fmt.Errorf("Interface is not a struct") 18 | } 19 | 20 | if getXMLName(val).Local == "" { 21 | return nil, fmt.Errorf("Invalid XML struct") 22 | } 23 | 24 | ret.Ele = &XMLEle{Val: val, pos: 1, prnt: ret} 25 | 26 | return ret, nil 27 | } 28 | 29 | func MustParseStruct(i interface{}) *XMLRoot { 30 | ret, err := ParseStruct(i) 31 | 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | return ret 37 | } 38 | -------------------------------------------------------------------------------- /tree/xmlstruct/xmlnode.go: -------------------------------------------------------------------------------- 1 | package xmlstruct 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "reflect" 7 | 8 | "github.com/ChrisTrenkamp/goxpath/tree" 9 | ) 10 | 11 | type XMLNode struct { 12 | Val reflect.Value 13 | pos int 14 | prnt tree.Elem 15 | nodeType tree.NodeType 16 | prntTag string 17 | } 18 | 19 | func (x *XMLNode) ResValue() string { 20 | if x.Val.Kind() == reflect.Ptr { 21 | return fmt.Sprintf("%v", x.Val.Elem().Interface()) 22 | } 23 | return fmt.Sprintf("%v", x.Val.Interface()) 24 | } 25 | 26 | func (x *XMLNode) Pos() int { 27 | return x.pos 28 | } 29 | 30 | func (x *XMLNode) GetToken() xml.Token { 31 | switch x.nodeType { 32 | case tree.NtAttr: 33 | return xml.Attr{Name: getTagInfo(x.prntTag).name, Value: x.ResValue()} 34 | case tree.NtChd: 35 | return xml.CharData(x.ResValue()) 36 | } 37 | //case tree.NtComm: 38 | return xml.Comment(x.ResValue()) 39 | } 40 | 41 | func (x *XMLNode) GetParent() tree.Elem { 42 | return x.prnt 43 | } 44 | 45 | func (x *XMLNode) GetNodeType() tree.NodeType { 46 | return x.nodeType 47 | } 48 | -------------------------------------------------------------------------------- /tree/xmlstruct/xmlstruct_test.go: -------------------------------------------------------------------------------- 1 | package xmlstruct 2 | 3 | import ( 4 | "encoding/xml" 5 | "testing" 6 | 7 | "github.com/ChrisTrenkamp/goxpath" 8 | ) 9 | 10 | func TestSingleFields(t *testing.T) { 11 | str := struct { 12 | XMLName xml.Name `xml:"struct"` 13 | Elem string `xml:"elem"` 14 | Attr string `xml:"attr,attr"` 15 | Attr2 string `xml:",attr"` 16 | Comm string `xml:",comment"` 17 | CD string `xml:",chardata"` 18 | Test interface{} 19 | }{ 20 | Elem: "foo", 21 | Attr: "bar", 22 | Attr2: "baz", 23 | Comm: "steak", 24 | CD: "eggs", 25 | Test: struct { 26 | Elem2 string `xml:"elem2"` 27 | Attr3 string `xml:",attr"` 28 | }{ 29 | Elem2: "elem2", 30 | Attr3: "attr3", 31 | }, 32 | } 33 | 34 | x := MustParseStruct(&str) 35 | x1, err := xml.Marshal(str) 36 | str1 := string(x1) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | str2, err := goxpath.MarshalStr(x) 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | if str1 != str2 { 45 | t.Error("Strings not equal") 46 | t.Error(str1) 47 | t.Error(str2) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tree/xmltree/xmlnode/xmlnode.go: -------------------------------------------------------------------------------- 1 | package xmlnode 2 | 3 | import ( 4 | "encoding/xml" 5 | 6 | "github.com/ChrisTrenkamp/goxpath/tree" 7 | ) 8 | 9 | //XMLNode will represent an attribute, character data, comment, or processing instruction node 10 | type XMLNode struct { 11 | xml.Token 12 | tree.NodePos 13 | tree.NodeType 14 | Parent tree.Elem 15 | } 16 | 17 | //GetToken returns the xml.Token representation of the node 18 | func (a XMLNode) GetToken() xml.Token { 19 | if a.NodeType == tree.NtAttr { 20 | ret := a.Token.(*xml.Attr) 21 | return *ret 22 | } 23 | return a.Token 24 | } 25 | 26 | //GetParent returns the parent node 27 | func (a XMLNode) GetParent() tree.Elem { 28 | return a.Parent 29 | } 30 | 31 | //ResValue returns the string value of the attribute 32 | func (a XMLNode) ResValue() string { 33 | switch a.NodeType { 34 | case tree.NtAttr: 35 | return a.Token.(*xml.Attr).Value 36 | case tree.NtChd: 37 | return string(a.Token.(xml.CharData)) 38 | case tree.NtComm: 39 | return string(a.Token.(xml.Comment)) 40 | } 41 | //case tree.NtPi: 42 | return string(a.Token.(xml.ProcInst).Inst) 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 ChrisTrenkamp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tree/xfn.go: -------------------------------------------------------------------------------- 1 | package tree 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | //Ctx represents the current context position, size, node, and the current filtered result 8 | type Ctx struct { 9 | NodeSet 10 | Pos int 11 | Size int 12 | } 13 | 14 | //Fn is a XPath function, written in Go 15 | type Fn func(c Ctx, args ...Result) (Result, error) 16 | 17 | //LastArgOpt sets whether the last argument in a function is optional, variadic, or neither 18 | type LastArgOpt int 19 | 20 | //LastArgOpt options 21 | const ( 22 | None LastArgOpt = iota 23 | Optional 24 | Variadic 25 | ) 26 | 27 | //Wrap interfaces XPath function calls with Go 28 | type Wrap struct { 29 | Fn Fn 30 | //NArgs represents the number of arguments to the XPath function. -1 represents a single optional argument 31 | NArgs int 32 | LastArgOpt LastArgOpt 33 | } 34 | 35 | //Call checks the arguments and calls Fn if they are valid 36 | func (w Wrap) Call(c Ctx, args ...Result) (Result, error) { 37 | switch w.LastArgOpt { 38 | case Optional: 39 | if len(args) == w.NArgs || len(args) == w.NArgs-1 { 40 | return w.Fn(c, args...) 41 | } 42 | case Variadic: 43 | if len(args) >= w.NArgs-1 { 44 | return w.Fn(c, args...) 45 | } 46 | default: 47 | if len(args) == w.NArgs { 48 | return w.Fn(c, args...) 49 | } 50 | } 51 | return nil, fmt.Errorf("Invalid number of arguments") 52 | } 53 | -------------------------------------------------------------------------------- /internal/execxp/intfns/numfns.go: -------------------------------------------------------------------------------- 1 | package intfns 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | "github.com/ChrisTrenkamp/goxpath/tree" 8 | ) 9 | 10 | func number(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 11 | if b, ok := args[0].(tree.IsNum); ok { 12 | return b.Num(), nil 13 | } 14 | 15 | return nil, fmt.Errorf("Cannot convert object to a number") 16 | } 17 | 18 | func sum(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 19 | n, ok := args[0].(tree.NodeSet) 20 | if !ok { 21 | return nil, fmt.Errorf("Cannot convert object to a node-set") 22 | } 23 | 24 | ret := 0.0 25 | for _, i := range n { 26 | ret += float64(tree.GetNodeNum(i)) 27 | } 28 | 29 | return tree.Num(ret), nil 30 | } 31 | 32 | func floor(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 33 | n, ok := args[0].(tree.IsNum) 34 | if !ok { 35 | return nil, fmt.Errorf("Cannot convert object to a number") 36 | } 37 | 38 | return tree.Num(math.Floor(float64(n.Num()))), nil 39 | } 40 | 41 | func ceiling(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 42 | n, ok := args[0].(tree.IsNum) 43 | if !ok { 44 | return nil, fmt.Errorf("Cannot convert object to a number") 45 | } 46 | 47 | return tree.Num(math.Ceil(float64(n.Num()))), nil 48 | } 49 | 50 | func round(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 51 | isn, ok := args[0].(tree.IsNum) 52 | if !ok { 53 | return nil, fmt.Errorf("Cannot convert object to a number") 54 | } 55 | 56 | n := isn.Num() 57 | 58 | if math.IsNaN(float64(n)) || math.IsInf(float64(n), 0) { 59 | return n, nil 60 | } 61 | 62 | if n < -0.5 { 63 | n = tree.Num(int(n - 0.5)) 64 | } else if n > 0.5 { 65 | n = tree.Num(int(n + 0.5)) 66 | } else { 67 | n = 0 68 | } 69 | 70 | return n, nil 71 | } 72 | -------------------------------------------------------------------------------- /value_test.go: -------------------------------------------------------------------------------- 1 | package goxpath 2 | 3 | import ( 4 | "bytes" 5 | "runtime/debug" 6 | "testing" 7 | 8 | "github.com/ChrisTrenkamp/goxpath/tree/xmltree" 9 | ) 10 | 11 | func execVal(xp, x string, exp string, ns map[string]string, t *testing.T) { 12 | defer func() { 13 | if r := recover(); r != nil { 14 | t.Error("Panicked: from XPath expr: '" + xp) 15 | t.Error(r) 16 | t.Error(string(debug.Stack())) 17 | } 18 | }() 19 | res := MustParse(xp).MustExec(xmltree.MustParseXML(bytes.NewBufferString(x)), func(o *Opts) { o.NS = ns }) 20 | 21 | if res.String() != exp { 22 | t.Error("Incorrect result:'" + res.String() + "' from XPath expr: '" + xp + "'. Expecting: '" + exp + "'") 23 | return 24 | } 25 | } 26 | 27 | func TestNodeVal(t *testing.T) { 28 | p := `/test` 29 | x := `testpathtest2` 30 | exp := "testpathtest2" 31 | execVal(p, x, exp, nil, t) 32 | } 33 | 34 | func TestAttrVal(t *testing.T) { 35 | p := `/p1/@test` 36 | x := `` 37 | exp := "foo" 38 | execVal(p, x, exp, nil, t) 39 | } 40 | 41 | func TestCommentVal(t *testing.T) { 42 | p := `//comment()` 43 | x := `` 44 | exp := ` comment ` 45 | execVal(p, x, exp, nil, t) 46 | } 47 | 48 | func TestProcInstVal(t *testing.T) { 49 | p := `//processing-instruction()` 50 | x := `` 51 | exp := `test` 52 | execVal(p, x, exp, nil, t) 53 | } 54 | 55 | func TestNodeNamespaceVal(t *testing.T) { 56 | p := `/test:p1/namespace::test` 57 | x := `` 58 | exp := `http://test` 59 | execVal(p, x, exp, nil, t) 60 | } 61 | -------------------------------------------------------------------------------- /internal/execxp/intfns/intfns.go: -------------------------------------------------------------------------------- 1 | package intfns 2 | 3 | import ( 4 | "encoding/xml" 5 | 6 | "github.com/ChrisTrenkamp/goxpath/tree" 7 | ) 8 | 9 | //BuiltIn contains the list of built-in XPath functions 10 | var BuiltIn = map[xml.Name]tree.Wrap{ 11 | //String functions 12 | {Local: "string"}: {Fn: _string, NArgs: 1, LastArgOpt: tree.Optional}, 13 | {Local: "concat"}: {Fn: concat, NArgs: 3, LastArgOpt: tree.Variadic}, 14 | {Local: "starts-with"}: {Fn: startsWith, NArgs: 2}, 15 | {Local: "contains"}: {Fn: contains, NArgs: 2}, 16 | {Local: "substring-before"}: {Fn: substringBefore, NArgs: 2}, 17 | {Local: "substring-after"}: {Fn: substringAfter, NArgs: 2}, 18 | {Local: "substring"}: {Fn: substring, NArgs: 3, LastArgOpt: tree.Optional}, 19 | {Local: "string-length"}: {Fn: stringLength, NArgs: 1, LastArgOpt: tree.Optional}, 20 | {Local: "normalize-space"}: {Fn: normalizeSpace, NArgs: 1, LastArgOpt: tree.Optional}, 21 | {Local: "translate"}: {Fn: translate, NArgs: 3}, 22 | //Node set functions 23 | {Local: "last"}: {Fn: last}, 24 | {Local: "position"}: {Fn: position}, 25 | {Local: "count"}: {Fn: count, NArgs: 1}, 26 | {Local: "local-name"}: {Fn: localName, NArgs: 1, LastArgOpt: tree.Optional}, 27 | {Local: "namespace-uri"}: {Fn: namespaceURI, NArgs: 1, LastArgOpt: tree.Optional}, 28 | {Local: "name"}: {Fn: name, NArgs: 1, LastArgOpt: tree.Optional}, 29 | //boolean functions 30 | {Local: "boolean"}: {Fn: boolean, NArgs: 1}, 31 | {Local: "not"}: {Fn: not, NArgs: 1}, 32 | {Local: "true"}: {Fn: _true}, 33 | {Local: "false"}: {Fn: _false}, 34 | {Local: "lang"}: {Fn: lang, NArgs: 1}, 35 | //number functions 36 | {Local: "number"}: {Fn: number, NArgs: 1, LastArgOpt: tree.Optional}, 37 | {Local: "sum"}: {Fn: sum, NArgs: 1}, 38 | {Local: "floor"}: {Fn: floor, NArgs: 1}, 39 | {Local: "ceiling"}: {Fn: ceiling, NArgs: 1}, 40 | {Local: "round"}: {Fn: round, NArgs: 1}, 41 | } 42 | -------------------------------------------------------------------------------- /internal/execxp/intfns/boolfns.go: -------------------------------------------------------------------------------- 1 | package intfns 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ChrisTrenkamp/goxpath/tree" 7 | "golang.org/x/text/language" 8 | ) 9 | 10 | func boolean(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 11 | if b, ok := args[0].(tree.IsBool); ok { 12 | return b.Bool(), nil 13 | } 14 | 15 | return nil, fmt.Errorf("Cannot convert object to a boolean") 16 | } 17 | 18 | func not(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 19 | b, ok := args[0].(tree.IsBool) 20 | if !ok { 21 | return nil, fmt.Errorf("Cannot convert object to a boolean") 22 | } 23 | return !b.Bool(), nil 24 | } 25 | 26 | func _true(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 27 | return tree.Bool(true), nil 28 | } 29 | 30 | func _false(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 31 | return tree.Bool(false), nil 32 | } 33 | 34 | func lang(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 35 | lStr := args[0].String() 36 | 37 | var n tree.Elem 38 | 39 | for _, i := range c.NodeSet { 40 | if i.GetNodeType() == tree.NtElem { 41 | n = i.(tree.Elem) 42 | } else { 43 | n = i.GetParent() 44 | } 45 | 46 | for n.GetNodeType() != tree.NtRoot { 47 | if attr, ok := tree.GetAttribute(n, "lang", tree.XMLSpace); ok { 48 | return checkLang(lStr, attr.Value), nil 49 | } 50 | n = n.GetParent() 51 | } 52 | } 53 | 54 | return tree.Bool(false), nil 55 | } 56 | 57 | func checkLang(srcStr, targStr string) tree.Bool { 58 | srcLang := language.Make(srcStr) 59 | srcRegion, srcRegionConf := srcLang.Region() 60 | 61 | targLang := language.Make(targStr) 62 | targRegion, targRegionConf := targLang.Region() 63 | 64 | if srcRegionConf == language.Exact && targRegionConf != language.Exact { 65 | return tree.Bool(false) 66 | } 67 | 68 | if srcRegion != targRegion && srcRegionConf == language.Exact && targRegionConf == language.Exact { 69 | return tree.Bool(false) 70 | } 71 | 72 | _, _, conf := language.NewMatcher([]language.Tag{srcLang}).Match(targLang) 73 | return tree.Bool(conf >= language.High) 74 | } 75 | -------------------------------------------------------------------------------- /xconst/xconst.go: -------------------------------------------------------------------------------- 1 | package xconst 2 | 3 | const ( 4 | //AxisAncestor represents the "ancestor" axis 5 | AxisAncestor = "ancestor" 6 | //AxisAncestorOrSelf represents the "ancestor-or-self" axis 7 | AxisAncestorOrSelf = "ancestor-or-self" 8 | //AxisAttribute represents the "attribute" axis 9 | AxisAttribute = "attribute" 10 | //AxisChild represents the "child" axis 11 | AxisChild = "child" 12 | //AxisDescendent represents the "descendant" axis 13 | AxisDescendent = "descendant" 14 | //AxisDescendentOrSelf represents the "descendant-or-self" axis 15 | AxisDescendentOrSelf = "descendant-or-self" 16 | //AxisFollowing represents the "following" axis 17 | AxisFollowing = "following" 18 | //AxisFollowingSibling represents the "following-sibling" axis 19 | AxisFollowingSibling = "following-sibling" 20 | //AxisNamespace represents the "namespace" axis 21 | AxisNamespace = "namespace" 22 | //AxisParent represents the "parent" axis 23 | AxisParent = "parent" 24 | //AxisPreceding represents the "preceding" axis 25 | AxisPreceding = "preceding" 26 | //AxisPrecedingSibling represents the "preceding-sibling" axis 27 | AxisPrecedingSibling = "preceding-sibling" 28 | //AxisSelf represents the "self" axis 29 | AxisSelf = "self" 30 | ) 31 | 32 | //AxisNames is all the possible Axis identifiers wrapped in an array for convenience 33 | var AxisNames = []string{ 34 | AxisAncestor, 35 | AxisAncestorOrSelf, 36 | AxisAttribute, 37 | AxisChild, 38 | AxisDescendent, 39 | AxisDescendentOrSelf, 40 | AxisFollowing, 41 | AxisFollowingSibling, 42 | AxisNamespace, 43 | AxisParent, 44 | AxisPreceding, 45 | AxisPrecedingSibling, 46 | AxisSelf, 47 | } 48 | 49 | const ( 50 | //NodeTypeComment represents the "comment" node test 51 | NodeTypeComment = "comment" 52 | //NodeTypeText represents the "text" node test 53 | NodeTypeText = "text" 54 | //NodeTypeProcInst represents the "processing-instruction" node test 55 | NodeTypeProcInst = "processing-instruction" 56 | //NodeTypeNode represents the "node" node test 57 | NodeTypeNode = "node" 58 | ) 59 | 60 | //NodeTypes is all the possible node tests wrapped in an array for convenience 61 | var NodeTypes = []string{ 62 | NodeTypeComment, 63 | NodeTypeText, 64 | NodeTypeProcInst, 65 | NodeTypeNode, 66 | } 67 | -------------------------------------------------------------------------------- /marshal.go: -------------------------------------------------------------------------------- 1 | package goxpath 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "io" 7 | 8 | "github.com/ChrisTrenkamp/goxpath/tree" 9 | ) 10 | 11 | //Marshal prints the result tree, r, in XML form to w. 12 | func Marshal(n tree.Node, w io.Writer) error { 13 | return marshal(n, w) 14 | } 15 | 16 | //MarshalStr is like Marhal, but returns a string. 17 | func MarshalStr(n tree.Node) (string, error) { 18 | ret := bytes.NewBufferString("") 19 | err := marshal(n, ret) 20 | 21 | return ret.String(), err 22 | } 23 | 24 | func marshal(n tree.Node, w io.Writer) error { 25 | e := xml.NewEncoder(w) 26 | err := encTok(n, e) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | return e.Flush() 32 | } 33 | 34 | func encTok(n tree.Node, e *xml.Encoder) error { 35 | switch n.GetNodeType() { 36 | case tree.NtAttr: 37 | return encAttr(n.GetToken().(xml.Attr), e) 38 | case tree.NtElem: 39 | return encEle(n.(tree.Elem), e) 40 | case tree.NtNs: 41 | return encNS(n.GetToken().(xml.Attr), e) 42 | case tree.NtRoot: 43 | for _, i := range n.(tree.Elem).GetChildren() { 44 | err := encTok(i, e) 45 | if err != nil { 46 | return err 47 | } 48 | } 49 | return nil 50 | } 51 | 52 | //case tree.NtChd, tree.NtComm, tree.NtPi: 53 | return e.EncodeToken(n.GetToken()) 54 | } 55 | 56 | func encAttr(a xml.Attr, e *xml.Encoder) error { 57 | str := a.Name.Local + `="` + a.Value + `"` 58 | 59 | if a.Name.Space != "" { 60 | str += ` xmlns="` + a.Name.Space + `"` 61 | } 62 | 63 | pi := xml.ProcInst{ 64 | Target: "attribute", 65 | Inst: ([]byte)(str), 66 | } 67 | 68 | return e.EncodeToken(pi) 69 | } 70 | 71 | func encNS(ns xml.Attr, e *xml.Encoder) error { 72 | pi := xml.ProcInst{ 73 | Target: "namespace", 74 | Inst: ([]byte)(ns.Value), 75 | } 76 | return e.EncodeToken(pi) 77 | } 78 | 79 | func encEle(n tree.Elem, e *xml.Encoder) error { 80 | ele := xml.StartElement{ 81 | Name: n.GetToken().(xml.StartElement).Name, 82 | } 83 | 84 | attrs := n.GetAttrs() 85 | ele.Attr = make([]xml.Attr, len(attrs)) 86 | for i := range attrs { 87 | ele.Attr[i] = attrs[i].GetToken().(xml.Attr) 88 | } 89 | 90 | err := e.EncodeToken(ele) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | if x, ok := n.(tree.Elem); ok { 96 | for _, i := range x.GetChildren() { 97 | err := encTok(i, e) 98 | if err != nil { 99 | return err 100 | } 101 | } 102 | } 103 | 104 | return e.EncodeToken(ele.End()) 105 | } 106 | -------------------------------------------------------------------------------- /tree/xtypes.go: -------------------------------------------------------------------------------- 1 | package tree 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | //Boolean strings 11 | const ( 12 | True = "true" 13 | False = "false" 14 | ) 15 | 16 | //Bool is a boolean XPath type 17 | type Bool bool 18 | 19 | //ResValue satisfies the Res interface for Bool 20 | func (b Bool) String() string { 21 | if b { 22 | return True 23 | } 24 | 25 | return False 26 | } 27 | 28 | //Bool satisfies the HasBool interface for Bool's 29 | func (b Bool) Bool() Bool { 30 | return b 31 | } 32 | 33 | //Num satisfies the HasNum interface for Bool's 34 | func (b Bool) Num() Num { 35 | if b { 36 | return Num(1) 37 | } 38 | 39 | return Num(0) 40 | } 41 | 42 | //Num is a number XPath type 43 | type Num float64 44 | 45 | //ResValue satisfies the Res interface for Num 46 | func (n Num) String() string { 47 | if math.IsInf(float64(n), 0) { 48 | if math.IsInf(float64(n), 1) { 49 | return "Infinity" 50 | } 51 | return "-Infinity" 52 | } 53 | return fmt.Sprintf("%g", float64(n)) 54 | } 55 | 56 | //Bool satisfies the HasBool interface for Num's 57 | func (n Num) Bool() Bool { 58 | return n != 0 59 | } 60 | 61 | //Num satisfies the HasNum interface for Num's 62 | func (n Num) Num() Num { 63 | return n 64 | } 65 | 66 | //String is string XPath type 67 | type String string 68 | 69 | //ResValue satisfies the Res interface for String 70 | func (s String) String() string { 71 | return string(s) 72 | } 73 | 74 | //Bool satisfies the HasBool interface for String's 75 | func (s String) Bool() Bool { 76 | return Bool(len(s) > 0) 77 | } 78 | 79 | //Num satisfies the HasNum interface for String's 80 | func (s String) Num() Num { 81 | num, err := strconv.ParseFloat(strings.TrimSpace(string(s)), 64) 82 | if err != nil { 83 | return Num(math.NaN()) 84 | } 85 | return Num(num) 86 | } 87 | 88 | //NodeSet is a node-set XPath type 89 | type NodeSet []Node 90 | 91 | //GetNodeNum converts the node to a string-value and to a number 92 | func GetNodeNum(n Node) Num { 93 | return String(n.ResValue()).Num() 94 | } 95 | 96 | //String satisfies the Res interface for NodeSet 97 | func (n NodeSet) String() string { 98 | if len(n) == 0 { 99 | return "" 100 | } 101 | 102 | return n[0].ResValue() 103 | } 104 | 105 | //Bool satisfies the HasBool interface for node-set's 106 | func (n NodeSet) Bool() Bool { 107 | return Bool(len(n) > 0) 108 | } 109 | 110 | //Num satisfies the HasNum interface for NodeSet's 111 | func (n NodeSet) Num() Num { 112 | return String(n.String()).Num() 113 | } 114 | -------------------------------------------------------------------------------- /parser/ast.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "github.com/ChrisTrenkamp/goxpath/lexer" 4 | 5 | //NodeType enumerations 6 | const ( 7 | Empty lexer.XItemType = "" 8 | ) 9 | 10 | //Node builds an AST tree for operating on XPath expressions 11 | type Node struct { 12 | Val lexer.XItem 13 | Left *Node 14 | Right *Node 15 | Parent *Node 16 | next *Node 17 | } 18 | 19 | var beginPathType = map[lexer.XItemType]bool{ 20 | lexer.XItemAbsLocPath: true, 21 | lexer.XItemAbbrAbsLocPath: true, 22 | lexer.XItemAbbrRelLocPath: true, 23 | lexer.XItemRelLocPath: true, 24 | lexer.XItemFunction: true, 25 | } 26 | 27 | func (n *Node) add(i lexer.XItem) { 28 | if n.Val.Typ == Empty { 29 | n.Val = i 30 | } else if n.Left == nil && n.Right == nil { 31 | n.Left = &Node{Val: n.Val, Parent: n} 32 | n.Val = i 33 | } else if beginPathType[n.Val.Typ] { 34 | next := &Node{Val: n.Val, Left: n.Left, Right: n.Right, Parent: n} 35 | n.Left, n.Right = next, nil 36 | n.Val = i 37 | } else if n.Right == nil { 38 | n.Right = &Node{Val: i, Parent: n} 39 | } else { 40 | next := &Node{Val: n.Val, Left: n.Left, Right: n.Right, Parent: n} 41 | n.Left, n.Right = next, nil 42 | n.Val = i 43 | } 44 | n.next = n 45 | } 46 | 47 | func (n *Node) push(i lexer.XItem) { 48 | if n.Left == nil { 49 | n.Left = &Node{Val: i, Parent: n} 50 | n.next = n.Left 51 | } else if n.Right == nil { 52 | n.Right = &Node{Val: i, Parent: n} 53 | n.next = n.Right 54 | } else { 55 | next := &Node{Val: i, Left: n.Right, Parent: n} 56 | n.Right = next 57 | n.next = n.Right 58 | } 59 | } 60 | 61 | func (n *Node) pushNotEmpty(i lexer.XItem) { 62 | if n.Val.Typ == Empty { 63 | n.add(i) 64 | } else { 65 | n.push(i) 66 | } 67 | } 68 | 69 | /* 70 | func (n *Node) prettyPrint(depth, width int) { 71 | nodes := []*Node{} 72 | n.getLine(depth, &nodes) 73 | fmt.Printf("%*s", (width-depth)*2, "") 74 | toggle := true 75 | if len(nodes) > 1 { 76 | for _, i := range nodes { 77 | if i != nil { 78 | if toggle { 79 | fmt.Print("/ ") 80 | } else { 81 | fmt.Print("\\ ") 82 | } 83 | } 84 | toggle = !toggle 85 | } 86 | fmt.Println() 87 | fmt.Printf("%*s", (width-depth)*2, "") 88 | } 89 | for _, i := range nodes { 90 | if i != nil { 91 | fmt.Print(i.Val.Val, " ") 92 | } 93 | } 94 | fmt.Println() 95 | } 96 | 97 | func (n *Node) getLine(depth int, ret *[]*Node) { 98 | if depth <= 0 && n != nil { 99 | *ret = append(*ret, n) 100 | return 101 | } 102 | if n.Left != nil { 103 | n.Left.getLine(depth-1, ret) 104 | } else if depth-1 <= 0 { 105 | *ret = append(*ret, nil) 106 | } 107 | if n.Right != nil { 108 | n.Right.getLine(depth-1, ret) 109 | } else if depth-1 <= 0 { 110 | *ret = append(*ret, nil) 111 | } 112 | } 113 | */ 114 | -------------------------------------------------------------------------------- /comp_test.go: -------------------------------------------------------------------------------- 1 | package goxpath 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestAddition(t *testing.T) { 9 | p := `1 + 1` 10 | x := `` 11 | exp := "2" 12 | execVal(p, x, exp, nil, t) 13 | } 14 | 15 | func TestParenths(t *testing.T) { 16 | p := `(1 + 2) * 3` 17 | x := `` 18 | exp := "9" 19 | execVal(p, x, exp, nil, t) 20 | } 21 | 22 | func TestParenths2(t *testing.T) { 23 | p := `(1 + 2 * 3)` 24 | x := `` 25 | exp := "7" 26 | execVal(p, x, exp, nil, t) 27 | } 28 | 29 | func TestEquals(t *testing.T) { 30 | p := `/test/test2 = 3` 31 | x := `3` 32 | exp := "true" 33 | execVal(p, x, exp, nil, t) 34 | } 35 | 36 | func TestEqualsStr(t *testing.T) { 37 | p := `/test/test2 = 'foobar'` 38 | x := `foobar` 39 | exp := "true" 40 | execVal(p, x, exp, nil, t) 41 | } 42 | 43 | func TestEqualsStr2(t *testing.T) { 44 | p := `/root[@test="foo"]` 45 | x := `test` 46 | exp := "test" 47 | execVal(p, x, exp, nil, t) 48 | } 49 | 50 | func TestNumberOps(t *testing.T) { 51 | x := `2352` 52 | testFloatMap := make(map[string]float64) 53 | testFloatMap[`/t/t1 * 3`] = 2 * 3 54 | testFloatMap[`5 div /t/t1`] = 5.0 / 2.0 55 | testFloatMap[`/t/t2 + /t/t3`] = 3 + 5 56 | testFloatMap[`/t/t2 - /t/t3`] = 3 - 5 57 | testFloatMap[`/t/t3 mod /t/t1`] = 5 % 2 58 | testFloatMap[`number('5')`] = 5 59 | testFloatMap[`sum(/t/*)`] = 2 + 3 + 5 + 2 60 | testFloatMap[`floor(/t/t3 div /t/t1)`] = 2 61 | testFloatMap[`ceiling(t/t3 div /t/t1)`] = 3 62 | testFloatMap[`round(-1.5)`] = -2 63 | testFloatMap[`round(1.5)`] = 2 64 | testFloatMap[`round(0)`] = 0 65 | for k, v := range testFloatMap { 66 | execVal(k, x, fmt.Sprintf("%g", float64(v)), nil, t) 67 | } 68 | 69 | execVal(`/t/t3 div 0`, x, "Infinity", nil, t) 70 | execVal(`-1 div 0`, x, "-Infinity", nil, t) 71 | execVal(`0 div 0`, x, "NaN", nil, t) 72 | 73 | testBoolMap := make(map[string]string) 74 | testBoolMap[`/t/t1 = 2`] = "true" 75 | testBoolMap[`2 = /t/t1`] = "true" 76 | testBoolMap[`/t/t1 != 2`] = "false" 77 | testBoolMap[`4 = /t/t2`] = "false" 78 | testBoolMap[`/t/t1 != /t/t2`] = "true" 79 | testBoolMap[`2 < /t/t4`] = "false" 80 | testBoolMap[`/t/t1 <= 2`] = "true" 81 | testBoolMap[`/t/t1 > /t/t4`] = "false" 82 | testBoolMap[`2 >= /t/t4`] = "true" 83 | testBoolMap[`/t/t1 >= /t/t4`] = "true" 84 | testBoolMap[`/t/t1 != /t/t2 and /t/t1 < /t/t4`] = "false" 85 | testBoolMap[`/t/t1 != /t/t2 or /t/t1 < /t/t4`] = "true" 86 | testBoolMap[`(/t/t1 != /t/t2 or /t/t1 < /t/t4) = true()`] = "true" 87 | testBoolMap[`(/t/t1 != /t/t2 and /t/t1 < /t/t4) != true()`] = "true" 88 | for k, v := range testBoolMap { 89 | execVal(k, x, v, nil, t) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tree/xmltree/xmlele/xmlele.go: -------------------------------------------------------------------------------- 1 | package xmlele 2 | 3 | import ( 4 | "encoding/xml" 5 | 6 | "github.com/ChrisTrenkamp/goxpath/tree" 7 | "github.com/ChrisTrenkamp/goxpath/tree/xmltree/xmlbuilder" 8 | "github.com/ChrisTrenkamp/goxpath/tree/xmltree/xmlnode" 9 | ) 10 | 11 | //XMLEle is an implementation of XPRes for XML elements 12 | type XMLEle struct { 13 | xml.StartElement 14 | tree.NSBuilder 15 | Attrs []tree.Node 16 | Children []tree.Node 17 | Parent tree.Elem 18 | tree.NodePos 19 | tree.NodeType 20 | } 21 | 22 | //Root is the default root node builder for xmltree.ParseXML 23 | func Root() xmlbuilder.XMLBuilder { 24 | return &XMLEle{NodeType: tree.NtRoot} 25 | } 26 | 27 | //CreateNode is an implementation of xmlbuilder.XMLBuilder. It appends the node 28 | //specified in opts and returns the child if it is an element. Otherwise, it returns x. 29 | func (x *XMLEle) CreateNode(opts *xmlbuilder.BuilderOpts) xmlbuilder.XMLBuilder { 30 | if opts.NodeType == tree.NtElem { 31 | ele := &XMLEle{ 32 | StartElement: opts.Tok.(xml.StartElement), 33 | NSBuilder: tree.NSBuilder{NS: opts.NS}, 34 | Attrs: make([]tree.Node, len(opts.Attrs)), 35 | Parent: x, 36 | NodePos: tree.NodePos(opts.NodePos), 37 | NodeType: opts.NodeType, 38 | } 39 | for i := range opts.Attrs { 40 | ele.Attrs[i] = xmlnode.XMLNode{ 41 | Token: opts.Attrs[i], 42 | NodePos: tree.NodePos(opts.AttrStartPos + i), 43 | NodeType: tree.NtAttr, 44 | Parent: ele, 45 | } 46 | } 47 | x.Children = append(x.Children, ele) 48 | return ele 49 | } 50 | 51 | node := xmlnode.XMLNode{ 52 | Token: opts.Tok, 53 | NodePos: tree.NodePos(opts.NodePos), 54 | NodeType: opts.NodeType, 55 | Parent: x, 56 | } 57 | x.Children = append(x.Children, node) 58 | return x 59 | } 60 | 61 | //EndElem is an implementation of xmlbuilder.XMLBuilder. It returns x's parent. 62 | func (x *XMLEle) EndElem() xmlbuilder.XMLBuilder { 63 | return x.Parent.(*XMLEle) 64 | } 65 | 66 | //GetToken returns the xml.Token representation of the node 67 | func (x *XMLEle) GetToken() xml.Token { 68 | return x.StartElement 69 | } 70 | 71 | //GetParent returns the parent node, or itself if it's the root 72 | func (x *XMLEle) GetParent() tree.Elem { 73 | return x.Parent 74 | } 75 | 76 | //GetChildren returns all child nodes of the element 77 | func (x *XMLEle) GetChildren() []tree.Node { 78 | ret := make([]tree.Node, len(x.Children)) 79 | 80 | for i := range x.Children { 81 | ret[i] = x.Children[i] 82 | } 83 | 84 | return ret 85 | } 86 | 87 | //GetAttrs returns all attributes of the element 88 | func (x *XMLEle) GetAttrs() []tree.Node { 89 | ret := make([]tree.Node, len(x.Attrs)) 90 | for i := range x.Attrs { 91 | ret[i] = x.Attrs[i] 92 | } 93 | return ret 94 | } 95 | 96 | //ResValue returns the string value of the element and children 97 | func (x *XMLEle) ResValue() string { 98 | ret := "" 99 | for i := range x.Children { 100 | switch x.Children[i].GetNodeType() { 101 | case tree.NtChd, tree.NtElem, tree.NtRoot: 102 | ret += x.Children[i].ResValue() 103 | } 104 | } 105 | return ret 106 | } 107 | -------------------------------------------------------------------------------- /internal/execxp/intfns/nodesetfns.go: -------------------------------------------------------------------------------- 1 | package intfns 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | 7 | "github.com/ChrisTrenkamp/goxpath/tree" 8 | ) 9 | 10 | func last(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 11 | return tree.Num(c.Size), nil 12 | } 13 | 14 | func position(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 15 | return tree.Num(c.Pos), nil 16 | } 17 | 18 | func count(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 19 | n, ok := args[0].(tree.NodeSet) 20 | if !ok { 21 | return nil, fmt.Errorf("Cannot convert object to a node-set") 22 | } 23 | 24 | return tree.Num(len(n)), nil 25 | } 26 | 27 | func localName(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 28 | var n tree.NodeSet 29 | ok := true 30 | if len(args) == 1 { 31 | n, ok = args[0].(tree.NodeSet) 32 | } else { 33 | n = c.NodeSet 34 | } 35 | if !ok { 36 | return nil, fmt.Errorf("Cannot convert object to a node-set") 37 | } 38 | 39 | ret := "" 40 | if len(n) == 0 { 41 | return tree.String(ret), nil 42 | } 43 | node := n[0] 44 | 45 | tok := node.GetToken() 46 | 47 | switch node.GetNodeType() { 48 | case tree.NtElem: 49 | ret = tok.(xml.StartElement).Name.Local 50 | case tree.NtAttr: 51 | ret = tok.(xml.Attr).Name.Local 52 | case tree.NtPi: 53 | ret = tok.(xml.ProcInst).Target 54 | } 55 | 56 | return tree.String(ret), nil 57 | } 58 | 59 | func namespaceURI(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 60 | var n tree.NodeSet 61 | ok := true 62 | if len(args) == 1 { 63 | n, ok = args[0].(tree.NodeSet) 64 | } else { 65 | n = c.NodeSet 66 | } 67 | if !ok { 68 | return nil, fmt.Errorf("Cannot convert object to a node-set") 69 | } 70 | 71 | ret := "" 72 | if len(n) == 0 { 73 | return tree.String(ret), nil 74 | } 75 | node := n[0] 76 | 77 | tok := node.GetToken() 78 | 79 | switch node.GetNodeType() { 80 | case tree.NtElem: 81 | ret = tok.(xml.StartElement).Name.Space 82 | case tree.NtAttr: 83 | ret = tok.(xml.Attr).Name.Space 84 | } 85 | 86 | return tree.String(ret), nil 87 | } 88 | 89 | func name(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 90 | var n tree.NodeSet 91 | ok := true 92 | if len(args) == 1 { 93 | n, ok = args[0].(tree.NodeSet) 94 | } else { 95 | n = c.NodeSet 96 | } 97 | if !ok { 98 | return nil, fmt.Errorf("Cannot convert object to a node-set") 99 | } 100 | 101 | ret := "" 102 | if len(n) == 0 { 103 | return tree.String(ret), nil 104 | } 105 | node := n[0] 106 | 107 | switch node.GetNodeType() { 108 | case tree.NtElem: 109 | t := node.GetToken().(xml.StartElement) 110 | space := "" 111 | 112 | if t.Name.Space != "" { 113 | space = fmt.Sprintf("{%s}", t.Name.Space) 114 | } 115 | 116 | ret = fmt.Sprintf("%s%s", space, t.Name.Local) 117 | case tree.NtAttr: 118 | t := node.GetToken().(xml.Attr) 119 | space := "" 120 | 121 | if t.Name.Space != "" { 122 | space = fmt.Sprintf("{%s}", t.Name.Space) 123 | } 124 | 125 | ret = fmt.Sprintf("%s%s", space, t.Name.Local) 126 | case tree.NtPi: 127 | ret = fmt.Sprintf("%s", node.GetToken().(xml.ProcInst).Target) 128 | } 129 | 130 | return tree.String(ret), nil 131 | } 132 | -------------------------------------------------------------------------------- /goxpath.go: -------------------------------------------------------------------------------- 1 | package goxpath 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | 7 | "github.com/ChrisTrenkamp/goxpath/internal/execxp" 8 | "github.com/ChrisTrenkamp/goxpath/parser" 9 | "github.com/ChrisTrenkamp/goxpath/tree" 10 | ) 11 | 12 | //Opts defines namespace mappings and custom functions for XPath expressions. 13 | type Opts struct { 14 | NS map[string]string 15 | Funcs map[xml.Name]tree.Wrap 16 | Vars map[string]tree.Result 17 | } 18 | 19 | //FuncOpts is a function wrapper for Opts. 20 | type FuncOpts func(*Opts) 21 | 22 | //XPathExec is the XPath executor, compiled from an XPath string 23 | type XPathExec struct { 24 | n *parser.Node 25 | } 26 | 27 | //Parse parses the XPath expression, xp, returning an XPath executor. 28 | func Parse(xp string) (XPathExec, error) { 29 | n, err := parser.Parse(xp) 30 | return XPathExec{n: n}, err 31 | } 32 | 33 | //MustParse is like Parse, but panics instead of returning an error. 34 | func MustParse(xp string) XPathExec { 35 | ret, err := Parse(xp) 36 | if err != nil { 37 | panic(err) 38 | } 39 | return ret 40 | } 41 | 42 | //Exec executes the XPath expression, xp, against the tree, t, with the 43 | //namespace mappings, ns, and returns the result as a stringer. 44 | func (xp XPathExec) Exec(t tree.Node, opts ...FuncOpts) (tree.Result, error) { 45 | o := &Opts{ 46 | NS: make(map[string]string), 47 | Funcs: make(map[xml.Name]tree.Wrap), 48 | Vars: make(map[string]tree.Result), 49 | } 50 | for _, i := range opts { 51 | i(o) 52 | } 53 | return execxp.Exec(xp.n, t, o.NS, o.Funcs, o.Vars) 54 | } 55 | 56 | //ExecBool is like Exec, except it will attempt to convert the result to its boolean value. 57 | func (xp XPathExec) ExecBool(t tree.Node, opts ...FuncOpts) (bool, error) { 58 | res, err := xp.Exec(t, opts...) 59 | if err != nil { 60 | return false, err 61 | } 62 | 63 | b, ok := res.(tree.IsBool) 64 | if !ok { 65 | return false, fmt.Errorf("Cannot convert result to a boolean") 66 | } 67 | 68 | return bool(b.Bool()), nil 69 | } 70 | 71 | //ExecNum is like Exec, except it will attempt to convert the result to its number value. 72 | func (xp XPathExec) ExecNum(t tree.Node, opts ...FuncOpts) (float64, error) { 73 | res, err := xp.Exec(t, opts...) 74 | if err != nil { 75 | return 0, err 76 | } 77 | 78 | n, ok := res.(tree.IsNum) 79 | if !ok { 80 | return 0, fmt.Errorf("Cannot convert result to a number") 81 | } 82 | 83 | return float64(n.Num()), nil 84 | } 85 | 86 | //ExecNode is like Exec, except it will attempt to return the result as a node-set. 87 | func (xp XPathExec) ExecNode(t tree.Node, opts ...FuncOpts) (tree.NodeSet, error) { 88 | res, err := xp.Exec(t, opts...) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | n, ok := res.(tree.NodeSet) 94 | if !ok { 95 | return nil, fmt.Errorf("Cannot convert result to a node-set") 96 | } 97 | 98 | return n, nil 99 | } 100 | 101 | //MustExec is like Exec, but panics instead of returning an error. 102 | func (xp XPathExec) MustExec(t tree.Node, opts ...FuncOpts) tree.Result { 103 | res, err := xp.Exec(t, opts...) 104 | if err != nil { 105 | panic(err) 106 | } 107 | return res 108 | } 109 | 110 | //ParseExec parses the XPath string, xpstr, and runs Exec. 111 | func ParseExec(xpstr string, t tree.Node, opts ...FuncOpts) (tree.Result, error) { 112 | xp, err := Parse(xpstr) 113 | if err != nil { 114 | return nil, err 115 | } 116 | return xp.Exec(t, opts...) 117 | } 118 | -------------------------------------------------------------------------------- /internal/execxp/intfns/stringfns.go: -------------------------------------------------------------------------------- 1 | package intfns 2 | 3 | import ( 4 | "math" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/ChrisTrenkamp/goxpath/tree" 9 | ) 10 | 11 | func _string(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 12 | if len(args) == 1 { 13 | return tree.String(args[0].String()), nil 14 | } 15 | 16 | return tree.String(c.NodeSet.String()), nil 17 | } 18 | 19 | func concat(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 20 | ret := "" 21 | 22 | for _, i := range args { 23 | ret += i.String() 24 | } 25 | 26 | return tree.String(ret), nil 27 | } 28 | 29 | func startsWith(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 30 | return tree.Bool(strings.Index(args[0].String(), args[1].String()) == 0), nil 31 | } 32 | 33 | func contains(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 34 | return tree.Bool(strings.Contains(args[0].String(), args[1].String())), nil 35 | } 36 | 37 | func substringBefore(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 38 | ind := strings.Index(args[0].String(), args[1].String()) 39 | if ind == -1 { 40 | return tree.String(""), nil 41 | } 42 | 43 | return tree.String(args[0].String()[:ind]), nil 44 | } 45 | 46 | func substringAfter(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 47 | ind := strings.Index(args[0].String(), args[1].String()) 48 | if ind == -1 { 49 | return tree.String(""), nil 50 | } 51 | 52 | return tree.String(args[0].String()[ind+len(args[1].String()):]), nil 53 | } 54 | 55 | func substring(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 56 | str := args[0].String() 57 | 58 | bNum, bErr := round(c, args[1]) 59 | if bErr != nil { 60 | return nil, bErr 61 | } 62 | 63 | b := bNum.(tree.Num).Num() 64 | 65 | if float64(b-1) >= float64(len(str)) || math.IsNaN(float64(b)) { 66 | return tree.String(""), nil 67 | } 68 | 69 | if len(args) == 2 { 70 | if b <= 1 { 71 | b = 1 72 | } 73 | 74 | return tree.String(str[int(b)-1:]), nil 75 | } 76 | 77 | eNum, eErr := round(c, args[2]) 78 | if eErr != nil { 79 | return nil, eErr 80 | } 81 | 82 | e := eNum.(tree.Num).Num() 83 | 84 | if e <= 0 || math.IsNaN(float64(e)) || (math.IsInf(float64(b), 0) && math.IsInf(float64(e), 0)) { 85 | return tree.String(""), nil 86 | } 87 | 88 | if b <= 1 { 89 | e = b + e - 1 90 | b = 1 91 | } 92 | 93 | if float64(b+e-1) >= float64(len(str)) { 94 | e = tree.Num(len(str)) - b + 1 95 | } 96 | 97 | return tree.String(str[int(b)-1 : int(b+e)-1]), nil 98 | } 99 | 100 | func stringLength(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 101 | var str string 102 | if len(args) == 1 { 103 | str = args[0].String() 104 | } else { 105 | str = c.NodeSet.String() 106 | } 107 | 108 | return tree.Num(len(str)), nil 109 | } 110 | 111 | var spaceTrim = regexp.MustCompile(`\s+`) 112 | 113 | func normalizeSpace(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 114 | var str string 115 | if len(args) == 1 { 116 | str = args[0].String() 117 | } else { 118 | str = c.NodeSet.String() 119 | } 120 | 121 | str = strings.TrimSpace(str) 122 | 123 | return tree.String(spaceTrim.ReplaceAllString(str, " ")), nil 124 | } 125 | 126 | func translate(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 127 | ret := args[0].String() 128 | src := args[1].String() 129 | repl := args[2].String() 130 | 131 | for i := range src { 132 | r := "" 133 | if i < len(repl) { 134 | r = string(repl[i]) 135 | } 136 | 137 | ret = strings.Replace(ret, string(src[i]), r, -1) 138 | } 139 | 140 | return tree.String(ret), nil 141 | } 142 | -------------------------------------------------------------------------------- /tree/xmltree/xmltree.go: -------------------------------------------------------------------------------- 1 | package xmltree 2 | 3 | import ( 4 | "encoding/xml" 5 | "io" 6 | 7 | "golang.org/x/net/html/charset" 8 | 9 | "github.com/ChrisTrenkamp/goxpath/tree" 10 | "github.com/ChrisTrenkamp/goxpath/tree/xmltree/xmlbuilder" 11 | "github.com/ChrisTrenkamp/goxpath/tree/xmltree/xmlele" 12 | ) 13 | 14 | //ParseOptions is a set of methods and function pointers that alter 15 | //the way the XML decoder works and the Node types that are created. 16 | //Options that are not set will default to what is set in internal/defoverride.go 17 | type ParseOptions struct { 18 | Strict bool 19 | XMLRoot func() xmlbuilder.XMLBuilder 20 | } 21 | 22 | //DirectiveParser is an optional interface extended from XMLBuilder that handles 23 | //XML directives. 24 | type DirectiveParser interface { 25 | xmlbuilder.XMLBuilder 26 | Directive(xml.Directive, *xml.Decoder) 27 | } 28 | 29 | //ParseSettings is a function for setting the ParseOptions you want when 30 | //parsing an XML tree. 31 | type ParseSettings func(s *ParseOptions) 32 | 33 | //MustParseXML is like ParseXML, but panics instead of returning an error. 34 | func MustParseXML(r io.Reader, op ...ParseSettings) tree.Node { 35 | ret, err := ParseXML(r, op...) 36 | 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | return ret 42 | } 43 | 44 | //ParseXML creates an XMLTree structure from an io.Reader. 45 | func ParseXML(r io.Reader, op ...ParseSettings) (tree.Node, error) { 46 | ov := ParseOptions{ 47 | Strict: true, 48 | XMLRoot: xmlele.Root, 49 | } 50 | for _, i := range op { 51 | i(&ov) 52 | } 53 | 54 | dec := xml.NewDecoder(r) 55 | dec.CharsetReader = charset.NewReaderLabel 56 | dec.Strict = ov.Strict 57 | 58 | ordrPos := 1 59 | xmlTree := ov.XMLRoot() 60 | 61 | t, err := dec.Token() 62 | 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | if head, ok := t.(xml.ProcInst); ok && head.Target == "xml" { 68 | t, err = dec.Token() 69 | } 70 | 71 | opts := xmlbuilder.BuilderOpts{ 72 | Dec: dec, 73 | } 74 | 75 | for err == nil { 76 | switch xt := t.(type) { 77 | case xml.StartElement: 78 | setEle(&opts, xmlTree, xt, &ordrPos) 79 | xmlTree = xmlTree.CreateNode(&opts) 80 | case xml.CharData: 81 | setNode(&opts, xmlTree, xt, tree.NtChd, &ordrPos) 82 | xmlTree = xmlTree.CreateNode(&opts) 83 | case xml.Comment: 84 | setNode(&opts, xmlTree, xt, tree.NtComm, &ordrPos) 85 | xmlTree = xmlTree.CreateNode(&opts) 86 | case xml.ProcInst: 87 | setNode(&opts, xmlTree, xt, tree.NtPi, &ordrPos) 88 | xmlTree = xmlTree.CreateNode(&opts) 89 | case xml.EndElement: 90 | xmlTree = xmlTree.EndElem() 91 | case xml.Directive: 92 | if dp, ok := xmlTree.(DirectiveParser); ok { 93 | dp.Directive(xt.Copy(), dec) 94 | } 95 | } 96 | 97 | t, err = dec.Token() 98 | } 99 | 100 | if err == io.EOF { 101 | err = nil 102 | } 103 | 104 | return xmlTree, err 105 | } 106 | 107 | func setEle(opts *xmlbuilder.BuilderOpts, xmlTree xmlbuilder.XMLBuilder, ele xml.StartElement, ordrPos *int) { 108 | opts.NodePos = *ordrPos 109 | opts.Tok = ele 110 | opts.Attrs = opts.Attrs[0:0:cap(opts.Attrs)] 111 | opts.NS = make(map[xml.Name]string) 112 | opts.NodeType = tree.NtElem 113 | 114 | for i := range ele.Attr { 115 | attr := ele.Attr[i].Name 116 | val := ele.Attr[i].Value 117 | 118 | if (attr.Local == "xmlns" && attr.Space == "") || attr.Space == "xmlns" { 119 | opts.NS[attr] = val 120 | } else { 121 | opts.Attrs = append(opts.Attrs, &ele.Attr[i]) 122 | } 123 | } 124 | 125 | if nstree, ok := xmlTree.(tree.NSElem); ok { 126 | ns := make(map[xml.Name]string) 127 | 128 | for _, i := range tree.BuildNS(nstree) { 129 | ns[i.Name] = i.Value 130 | } 131 | 132 | for k, v := range opts.NS { 133 | ns[k] = v 134 | } 135 | 136 | if ns[xml.Name{Local: "xmlns"}] == "" { 137 | delete(ns, xml.Name{Local: "xmlns"}) 138 | } 139 | 140 | for k, v := range ns { 141 | opts.NS[k] = v 142 | } 143 | 144 | if xmlTree.GetNodeType() == tree.NtRoot { 145 | opts.NS[xml.Name{Space: "xmlns", Local: "xml"}] = tree.XMLSpace 146 | } 147 | } 148 | 149 | opts.AttrStartPos = len(opts.NS) + len(opts.Attrs) + *ordrPos 150 | *ordrPos = opts.AttrStartPos + 1 151 | } 152 | 153 | func setNode(opts *xmlbuilder.BuilderOpts, xmlTree xmlbuilder.XMLBuilder, tok xml.Token, nt tree.NodeType, ordrPos *int) { 154 | opts.Tok = xml.CopyToken(tok) 155 | opts.NodeType = nt 156 | opts.NodePos = *ordrPos 157 | *ordrPos++ 158 | } 159 | -------------------------------------------------------------------------------- /tree/xmlstruct/xmlele.go: -------------------------------------------------------------------------------- 1 | package xmlstruct 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/ChrisTrenkamp/goxpath/tree" 10 | ) 11 | 12 | type XMLEle struct { 13 | Val reflect.Value 14 | pos int 15 | prnt tree.Elem 16 | prntTag string 17 | } 18 | 19 | func (x *XMLEle) ResValue() string { 20 | if x.Val.Kind() != reflect.Struct { 21 | return fmt.Sprintf("%v", x.Val.Interface()) 22 | } 23 | 24 | ret := "" 25 | for _, i := range x.GetChildren() { 26 | switch i.GetNodeType() { 27 | case tree.NtChd, tree.NtElem, tree.NtRoot: 28 | ret += i.ResValue() 29 | } 30 | } 31 | return ret 32 | } 33 | 34 | func (x *XMLEle) Pos() int { 35 | return x.pos 36 | } 37 | 38 | func (x *XMLEle) GetToken() xml.Token { 39 | ret := xml.StartElement{} 40 | 41 | if x.prntTag != "" { 42 | ret.Name = getTagInfo(x.prntTag).name 43 | } else { 44 | ret.Name = getXMLName(x.Val) 45 | } 46 | 47 | return ret 48 | } 49 | 50 | func (x *XMLEle) GetParent() tree.Elem { 51 | return x.prnt 52 | } 53 | 54 | func (x *XMLEle) GetNodeType() tree.NodeType { 55 | return tree.NtElem 56 | } 57 | 58 | func (x *XMLEle) GetChildren() []tree.Node { 59 | n, _ := getChildren(x, x.Val, x.pos, false) 60 | return n 61 | } 62 | 63 | func (x *XMLEle) GetAttrs() []tree.Node { 64 | n, _ := getChildren(x, x.Val, x.pos, true) 65 | return n 66 | } 67 | 68 | type tagInfo struct { 69 | name xml.Name 70 | attr bool 71 | cdata bool 72 | comment bool 73 | } 74 | 75 | func getTagInfo(tag string) (ret tagInfo) { 76 | spl := strings.Split(tag, ",") 77 | name := strings.Split(spl[0], " ") 78 | if len(spl) >= 2 { 79 | for i := 1; i < len(spl); i++ { 80 | if spl[i] == "chardata" || spl[i] == "cdata" { 81 | ret.cdata = true 82 | } else if spl[i] == "attr" { 83 | ret.attr = true 84 | } else if spl[i] == "comment" { 85 | ret.comment = true 86 | } 87 | } 88 | } 89 | 90 | if len(name) == 2 { 91 | ret.name.Space = name[1] 92 | } 93 | 94 | ret.name.Local = name[0] 95 | return 96 | } 97 | 98 | func getXMLName(val reflect.Value) xml.Name { 99 | n := val.FieldByName("XMLName") 100 | zero := reflect.Value{} 101 | 102 | if zero != n { 103 | field, _ := val.Type().FieldByName("XMLName") 104 | tagInfo := getTagInfo(field.Tag.Get("xml")) 105 | if tagInfo.name.Local != "" { 106 | return tagInfo.name 107 | } 108 | 109 | if name, ok := n.Interface().(xml.Name); ok { 110 | return name 111 | } 112 | } 113 | 114 | return xml.Name{Local: val.Type().Name()} 115 | } 116 | 117 | func getChildren(x *XMLEle, val reflect.Value, pos int, getAttrs bool) ([]tree.Node, int) { 118 | if val.Kind() == reflect.Ptr { 119 | val = val.Elem() 120 | } 121 | 122 | if val.Kind() == reflect.Interface { 123 | val = reflect.ValueOf(val.Interface()) 124 | } 125 | 126 | if val.Kind() != reflect.Struct { 127 | if getAttrs { 128 | return []tree.Node{}, x.pos + 1 129 | } 130 | 131 | return []tree.Node{&XMLNode{ 132 | Val: x.Val, 133 | pos: x.pos + 1, 134 | prnt: x, 135 | nodeType: tree.NtChd, 136 | prntTag: "", 137 | }}, x.pos + 2 138 | } 139 | 140 | ret := make([]tree.Node, 0, val.NumField()) 141 | 142 | for i := 0; i < val.NumField(); i++ { 143 | field := val.Field(i) 144 | name := val.Type().Field(i).Name 145 | 146 | if val.Type().Field(i).Anonymous { 147 | nodes, newPos := getChildren(x, field, pos, getAttrs) 148 | ret = append(ret, nodes...) 149 | pos = newPos 150 | continue 151 | } 152 | 153 | tag := val.Type().Field(i).Tag.Get("xml") 154 | 155 | if tag == "-" || name == "XMLName" { 156 | continue 157 | } 158 | 159 | tagInfo := getTagInfo(tag) 160 | 161 | pos++ 162 | 163 | if tagInfo.attr || tagInfo.cdata || tagInfo.comment { 164 | if !getAttrs && tagInfo.attr || getAttrs && !tagInfo.attr { 165 | continue 166 | } 167 | 168 | child := &XMLNode{ 169 | Val: field, 170 | pos: pos, 171 | prnt: x, 172 | prntTag: tag, 173 | } 174 | 175 | if tagInfo.attr { 176 | child.nodeType = tree.NtAttr 177 | } else if tagInfo.cdata { 178 | child.nodeType = tree.NtChd 179 | } else { 180 | child.nodeType = tree.NtComm 181 | } 182 | 183 | if tagInfo.name.Local == "" { 184 | child.prntTag = name 185 | } 186 | 187 | ret = append(ret, child) 188 | 189 | continue 190 | } 191 | 192 | if getAttrs { 193 | continue 194 | } 195 | 196 | child := &XMLEle{Val: field, pos: pos} 197 | 198 | if tag == "" { 199 | tag = name 200 | } 201 | 202 | child.prntTag = tag 203 | ret = append(ret, child) 204 | } 205 | 206 | return ret, pos + 1 207 | } 208 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ChrisTrenkamp/goxpath/lexer" 7 | ) 8 | 9 | type stateType int 10 | 11 | const ( 12 | defState stateType = iota 13 | xpathState 14 | funcState 15 | paramState 16 | predState 17 | parenState 18 | ) 19 | 20 | type parseStack struct { 21 | stack []*Node 22 | stateTypes []stateType 23 | cur *Node 24 | } 25 | 26 | func (p *parseStack) push(t stateType) { 27 | p.stack = append(p.stack, p.cur) 28 | p.stateTypes = append(p.stateTypes, t) 29 | } 30 | 31 | func (p *parseStack) pop() { 32 | stackPos := len(p.stack) - 1 33 | 34 | p.cur = p.stack[stackPos] 35 | p.stack = p.stack[:stackPos] 36 | p.stateTypes = p.stateTypes[:stackPos] 37 | } 38 | 39 | func (p *parseStack) curState() stateType { 40 | if len(p.stateTypes) == 0 { 41 | return defState 42 | } 43 | return p.stateTypes[len(p.stateTypes)-1] 44 | } 45 | 46 | type lexFn func(*parseStack, lexer.XItem) 47 | 48 | var parseMap = map[lexer.XItemType]lexFn{ 49 | lexer.XItemAbsLocPath: xiXPath, 50 | lexer.XItemAbbrAbsLocPath: xiXPath, 51 | lexer.XItemAbbrRelLocPath: xiXPath, 52 | lexer.XItemRelLocPath: xiXPath, 53 | lexer.XItemEndPath: xiEndPath, 54 | lexer.XItemAxis: xiXPath, 55 | lexer.XItemAbbrAxis: xiXPath, 56 | lexer.XItemNCName: xiXPath, 57 | lexer.XItemQName: xiXPath, 58 | lexer.XItemNodeType: xiXPath, 59 | lexer.XItemProcLit: xiXPath, 60 | lexer.XItemFunction: xiFunc, 61 | lexer.XItemArgument: xiFuncArg, 62 | lexer.XItemEndFunction: xiEndFunc, 63 | lexer.XItemPredicate: xiPred, 64 | lexer.XItemEndPredicate: xiEndPred, 65 | lexer.XItemStrLit: xiValue, 66 | lexer.XItemNumLit: xiValue, 67 | lexer.XItemOperator: xiOp, 68 | lexer.XItemVariable: xiValue, 69 | } 70 | 71 | var opPrecedence = map[string]int{ 72 | "|": 1, 73 | "*": 2, 74 | "div": 2, 75 | "mod": 2, 76 | "+": 3, 77 | "-": 3, 78 | "=": 4, 79 | "!=": 4, 80 | "<": 4, 81 | "<=": 4, 82 | ">": 4, 83 | ">=": 4, 84 | "and": 5, 85 | "or": 6, 86 | } 87 | 88 | //Parse creates an AST tree for XPath expressions. 89 | func Parse(xp string) (*Node, error) { 90 | var err error 91 | c := lexer.Lex(xp) 92 | n := &Node{} 93 | p := &parseStack{cur: n} 94 | 95 | for next := range c { 96 | if next.Typ != lexer.XItemError { 97 | parseMap[next.Typ](p, next) 98 | } else if err == nil { 99 | err = fmt.Errorf(next.Val) 100 | } 101 | } 102 | 103 | return n, err 104 | } 105 | 106 | func xiXPath(p *parseStack, i lexer.XItem) { 107 | if p.curState() == xpathState { 108 | p.cur.push(i) 109 | p.cur = p.cur.next 110 | return 111 | } 112 | 113 | if p.cur.Val.Typ == lexer.XItemFunction { 114 | p.cur.Right = &Node{Val: i, Parent: p.cur} 115 | p.cur.next = p.cur.Right 116 | p.push(xpathState) 117 | p.cur = p.cur.next 118 | return 119 | } 120 | 121 | p.cur.pushNotEmpty(i) 122 | p.push(xpathState) 123 | p.cur = p.cur.next 124 | } 125 | 126 | func xiEndPath(p *parseStack, i lexer.XItem) { 127 | p.pop() 128 | } 129 | 130 | func xiFunc(p *parseStack, i lexer.XItem) { 131 | if p.cur.Val.Typ == Empty { 132 | p.cur.pushNotEmpty(i) 133 | p.push(funcState) 134 | p.cur = p.cur.next 135 | return 136 | } 137 | p.cur.push(i) 138 | p.cur = p.cur.next 139 | p.push(funcState) 140 | } 141 | 142 | func xiFuncArg(p *parseStack, i lexer.XItem) { 143 | if p.curState() != funcState { 144 | p.pop() 145 | } 146 | 147 | p.cur.push(i) 148 | p.cur = p.cur.next 149 | p.push(paramState) 150 | p.cur.push(lexer.XItem{Typ: Empty, Val: ""}) 151 | p.cur = p.cur.next 152 | } 153 | 154 | func xiEndFunc(p *parseStack, i lexer.XItem) { 155 | if p.curState() == paramState { 156 | p.pop() 157 | } 158 | p.pop() 159 | } 160 | 161 | func xiPred(p *parseStack, i lexer.XItem) { 162 | p.cur.push(i) 163 | p.cur = p.cur.next 164 | p.push(predState) 165 | p.cur.push(lexer.XItem{Typ: Empty, Val: ""}) 166 | p.cur = p.cur.next 167 | } 168 | 169 | func xiEndPred(p *parseStack, i lexer.XItem) { 170 | p.pop() 171 | } 172 | 173 | func xiValue(p *parseStack, i lexer.XItem) { 174 | p.cur.add(i) 175 | } 176 | 177 | func xiOp(p *parseStack, i lexer.XItem) { 178 | if i.Val == "(" { 179 | p.cur.push(lexer.XItem{Typ: Empty, Val: ""}) 180 | p.push(parenState) 181 | p.cur = p.cur.next 182 | return 183 | } 184 | 185 | if i.Val == ")" { 186 | p.pop() 187 | return 188 | } 189 | 190 | if p.cur.Val.Typ == lexer.XItemOperator { 191 | if opPrecedence[p.cur.Val.Val] <= opPrecedence[i.Val] { 192 | p.cur.add(i) 193 | } else { 194 | p.cur.push(i) 195 | } 196 | } else { 197 | p.cur.add(i) 198 | } 199 | p.cur = p.cur.next 200 | } 201 | -------------------------------------------------------------------------------- /cmd/goxpath/goxpath.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/ChrisTrenkamp/goxpath" 14 | "github.com/ChrisTrenkamp/goxpath/tree" 15 | "github.com/ChrisTrenkamp/goxpath/tree/xmltree" 16 | ) 17 | 18 | type namespace map[string]string 19 | 20 | func (n *namespace) String() string { 21 | return fmt.Sprint(*n) 22 | } 23 | 24 | func (n *namespace) Set(value string) error { 25 | nsMap := strings.Split(value, "=") 26 | if len(nsMap) != 2 { 27 | nsErr = fmt.Errorf("Invalid namespace mapping: %s\n", value) 28 | } 29 | (*n)[nsMap[0]] = nsMap[1] 30 | return nil 31 | } 32 | 33 | type variables map[string]string 34 | 35 | func (v *variables) String() string { 36 | return fmt.Sprint(*v) 37 | } 38 | 39 | func (v *variables) Set(value string) error { 40 | varMap := strings.Split(value, "=") 41 | if len(varMap) != 2 { 42 | nsErr = fmt.Errorf("Invalid variable mapping: %s\n", value) 43 | } 44 | (*v)[varMap[0]] = varMap[1] 45 | return nil 46 | } 47 | 48 | var rec bool 49 | var value bool 50 | var ns = make(namespace) 51 | var vars = make(variables) 52 | var nsErr error 53 | var unstrict bool 54 | var noFileName bool 55 | var args = []string{} 56 | var stdin io.Reader = os.Stdin 57 | var stdout io.ReadWriter = os.Stdout 58 | var stderr io.ReadWriter = os.Stderr 59 | 60 | var retCode = 0 61 | 62 | func main() { 63 | exec() 64 | os.Exit(retCode) 65 | } 66 | 67 | func exec() { 68 | flag.BoolVar(&rec, "r", false, "Recursive") 69 | flag.BoolVar(&value, "v", false, "Output the string value of the XPath result") 70 | flag.Var(&ns, "ns", "Namespace mappings. e.g. -ns myns=http://example.com") 71 | flag.Var(&vars, "var", "Variables mappings. e.g. -var myvar=myvalue") 72 | flag.BoolVar(&unstrict, "u", false, "Turns off strict XML validation") 73 | flag.BoolVar(&noFileName, "h", false, "Suppress filename prefixes.") 74 | flag.Parse() 75 | args = flag.Args() 76 | 77 | if nsErr != nil { 78 | fmt.Fprintf(stderr, nsErr.Error()) 79 | retCode = 1 80 | return 81 | } 82 | 83 | if len(args) < 1 { 84 | fmt.Fprintf(stderr, "Specify an XPath expression with one or more files, or pipe the XML from stdin. Run 'goxpath --help' for more information.\n") 85 | retCode = 1 86 | return 87 | } 88 | 89 | xp, err := goxpath.Parse(args[0]) 90 | 91 | if err != nil { 92 | fmt.Fprintf(stderr, "%s\n", err.Error()) 93 | retCode = 1 94 | return 95 | } 96 | 97 | if len(args) == 1 { 98 | ret, err := runXPath(xp, stdin, ns, value) 99 | if err != nil { 100 | fmt.Fprintf(stderr, "%s\n", err.Error()) 101 | retCode = 1 102 | } 103 | 104 | printResult(ret, "") 105 | } 106 | 107 | for i := 1; i < len(args); i++ { 108 | procPath(args[i], xp, ns, value) 109 | } 110 | } 111 | 112 | func procPath(path string, x goxpath.XPathExec, ns namespace, value bool) { 113 | fi, err := os.Stat(path) 114 | if err != nil { 115 | fmt.Fprintf(stderr, "Could not open file: %s\n", path) 116 | retCode = 1 117 | return 118 | } 119 | 120 | if fi.IsDir() { 121 | procDir(path, x, ns, value) 122 | return 123 | } 124 | 125 | data, _ := ioutil.ReadFile(path) 126 | ret, err := runXPath(x, bytes.NewBuffer(data), ns, value) 127 | if err != nil { 128 | fmt.Fprintf(stderr, "%s: %s\n", path, err.Error()) 129 | retCode = 1 130 | } 131 | 132 | printResult(ret, path) 133 | } 134 | 135 | func printResult(ret []string, path string) { 136 | for _, j := range ret { 137 | if (len(flag.Args()) > 2 || rec) && !noFileName { 138 | fmt.Fprintf(stdout, "%s:", path) 139 | } 140 | 141 | fmt.Fprintf(stdout, "%s\n", j) 142 | } 143 | } 144 | 145 | func procDir(path string, x goxpath.XPathExec, ns namespace, value bool) { 146 | if !rec { 147 | fmt.Fprintf(stderr, "%s: Is a directory\n", path) 148 | retCode = 1 149 | return 150 | } 151 | 152 | list, _ := ioutil.ReadDir(path) 153 | 154 | for _, i := range list { 155 | procPath(filepath.Join(path, i.Name()), x, ns, value) 156 | } 157 | } 158 | 159 | func runXPath(x goxpath.XPathExec, r io.Reader, ns namespace, value bool) ([]string, error) { 160 | t, err := xmltree.ParseXML(r, func(o *xmltree.ParseOptions) { 161 | o.Strict = !unstrict 162 | }) 163 | 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | res, err := x.Exec(t, func(o *goxpath.Opts) { 169 | o.NS = ns 170 | for k, v := range vars { 171 | o.Vars[k] = tree.String(v) 172 | } 173 | }) 174 | 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | var ret []string 180 | 181 | if nodes, ok := res.(tree.NodeSet); ok && !value { 182 | ret = make([]string, len(nodes)) 183 | for i, v := range nodes { 184 | ret[i], _ = goxpath.MarshalStr(v) 185 | ret[i] = strings.Replace(ret[i], "\n", " ", -1) 186 | } 187 | } else { 188 | str := res.String() 189 | if str != "" { 190 | ret = strings.Split(str, "\n") 191 | } 192 | } 193 | 194 | return ret, nil 195 | } 196 | -------------------------------------------------------------------------------- /internal/execxp/operators.go: -------------------------------------------------------------------------------- 1 | package execxp 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | 7 | "github.com/ChrisTrenkamp/goxpath/tree" 8 | ) 9 | 10 | func bothNodeOperator(left tree.NodeSet, right tree.NodeSet, f *xpFilt, op string) error { 11 | var err error 12 | for _, l := range left { 13 | for _, r := range right { 14 | lStr := l.ResValue() 15 | rStr := r.ResValue() 16 | 17 | if eqOps[op] { 18 | err = equalsOperator(tree.String(lStr), tree.String(rStr), f, op) 19 | if err == nil && f.ctx.String() == tree.True { 20 | return nil 21 | } 22 | } else { 23 | err = numberOperator(tree.String(lStr), tree.String(rStr), f, op) 24 | if err == nil && f.ctx.String() == tree.True { 25 | return nil 26 | } 27 | } 28 | } 29 | } 30 | 31 | f.ctx = tree.Bool(false) 32 | 33 | return nil 34 | } 35 | 36 | func leftNodeOperator(left tree.NodeSet, right tree.Result, f *xpFilt, op string) error { 37 | var err error 38 | for _, l := range left { 39 | lStr := l.ResValue() 40 | 41 | if eqOps[op] { 42 | err = equalsOperator(tree.String(lStr), right, f, op) 43 | if err == nil && f.ctx.String() == tree.True { 44 | return nil 45 | } 46 | } else { 47 | err = numberOperator(tree.String(lStr), right, f, op) 48 | if err == nil && f.ctx.String() == tree.True { 49 | return nil 50 | } 51 | } 52 | } 53 | 54 | f.ctx = tree.Bool(false) 55 | 56 | return nil 57 | } 58 | 59 | func rightNodeOperator(left tree.Result, right tree.NodeSet, f *xpFilt, op string) error { 60 | var err error 61 | for _, r := range right { 62 | rStr := r.ResValue() 63 | 64 | if eqOps[op] { 65 | err = equalsOperator(left, tree.String(rStr), f, op) 66 | if err == nil && f.ctx.String() == "true" { 67 | return nil 68 | } 69 | } else { 70 | err = numberOperator(left, tree.String(rStr), f, op) 71 | if err == nil && f.ctx.String() == "true" { 72 | return nil 73 | } 74 | } 75 | } 76 | 77 | f.ctx = tree.Bool(false) 78 | 79 | return nil 80 | } 81 | 82 | func equalsOperator(left, right tree.Result, f *xpFilt, op string) error { 83 | _, lOK := left.(tree.Bool) 84 | _, rOK := right.(tree.Bool) 85 | 86 | if lOK || rOK { 87 | lTest, lt := left.(tree.IsBool) 88 | rTest, rt := right.(tree.IsBool) 89 | if !lt || !rt { 90 | return fmt.Errorf("Cannot convert argument to boolean") 91 | } 92 | 93 | if op == "=" { 94 | f.ctx = tree.Bool(lTest.Bool() == rTest.Bool()) 95 | } else { 96 | f.ctx = tree.Bool(lTest.Bool() != rTest.Bool()) 97 | } 98 | 99 | return nil 100 | } 101 | 102 | _, lOK = left.(tree.Num) 103 | _, rOK = right.(tree.Num) 104 | if lOK || rOK { 105 | return numberOperator(left, right, f, op) 106 | } 107 | 108 | lStr := left.String() 109 | rStr := right.String() 110 | 111 | if op == "=" { 112 | f.ctx = tree.Bool(lStr == rStr) 113 | } else { 114 | f.ctx = tree.Bool(lStr != rStr) 115 | } 116 | 117 | return nil 118 | } 119 | 120 | func numberOperator(left, right tree.Result, f *xpFilt, op string) error { 121 | lt, lOK := left.(tree.IsNum) 122 | rt, rOK := right.(tree.IsNum) 123 | if !lOK || !rOK { 124 | return fmt.Errorf("Cannot convert data type to number") 125 | } 126 | 127 | ln, rn := lt.Num(), rt.Num() 128 | 129 | switch op { 130 | case "*": 131 | f.ctx = ln * rn 132 | case "div": 133 | if rn != 0 { 134 | f.ctx = ln / rn 135 | } else { 136 | if ln == 0 { 137 | f.ctx = tree.Num(math.NaN()) 138 | } else { 139 | if math.Signbit(float64(ln)) == math.Signbit(float64(rn)) { 140 | f.ctx = tree.Num(math.Inf(1)) 141 | } else { 142 | f.ctx = tree.Num(math.Inf(-1)) 143 | } 144 | } 145 | } 146 | case "mod": 147 | f.ctx = tree.Num(int(ln) % int(rn)) 148 | case "+": 149 | f.ctx = ln + rn 150 | case "-": 151 | f.ctx = ln - rn 152 | case "=": 153 | f.ctx = tree.Bool(ln == rn) 154 | case "!=": 155 | f.ctx = tree.Bool(ln != rn) 156 | case "<": 157 | f.ctx = tree.Bool(ln < rn) 158 | case "<=": 159 | f.ctx = tree.Bool(ln <= rn) 160 | case ">": 161 | f.ctx = tree.Bool(ln > rn) 162 | case ">=": 163 | f.ctx = tree.Bool(ln >= rn) 164 | } 165 | 166 | return nil 167 | } 168 | 169 | func andOrOperator(left, right tree.Result, f *xpFilt, op string) error { 170 | lt, lOK := left.(tree.IsBool) 171 | rt, rOK := right.(tree.IsBool) 172 | 173 | if !lOK || !rOK { 174 | return fmt.Errorf("Cannot convert argument to boolean") 175 | } 176 | 177 | l, r := lt.Bool(), rt.Bool() 178 | 179 | if op == "and" { 180 | f.ctx = l && r 181 | } else { 182 | f.ctx = l || r 183 | } 184 | 185 | return nil 186 | } 187 | 188 | func unionOperator(left, right tree.Result, f *xpFilt, op string) error { 189 | lNode, lOK := left.(tree.NodeSet) 190 | rNode, rOK := right.(tree.NodeSet) 191 | 192 | if !lOK || !rOK { 193 | return fmt.Errorf("Cannot convert data type to node-set") 194 | } 195 | 196 | uniq := make(map[int]tree.Node) 197 | for _, i := range lNode { 198 | uniq[i.Pos()] = i 199 | } 200 | for _, i := range rNode { 201 | uniq[i.Pos()] = i 202 | } 203 | 204 | res := make(tree.NodeSet, 0, len(uniq)) 205 | for _, v := range uniq { 206 | res = append(res, v) 207 | } 208 | 209 | f.ctx = res 210 | 211 | return nil 212 | } 213 | -------------------------------------------------------------------------------- /cmd/goxpath/goxpath_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "flag" 7 | "os" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func setup(in string, args ...string) (*bytes.Buffer, *bytes.Buffer) { 13 | retCode = 0 14 | os.Args = append([]string{"test"}, args...) 15 | flag.CommandLine = flag.NewFlagSet("test", flag.ExitOnError) 16 | out := &bytes.Buffer{} 17 | err := &bytes.Buffer{} 18 | stdout = out 19 | stderr = err 20 | stdin = strings.NewReader(in) 21 | exec() 22 | return out, err 23 | } 24 | 25 | func TestStdinVal(t *testing.T) { 26 | out, _ := setup(xml.Header+"test", "-v", "/root/tag") 27 | if out.String() != "test\n" { 28 | t.Error("Expecting 'test' for the result. Recieved: ", out.String()) 29 | } 30 | if retCode != 0 { 31 | t.Error("Incorrect return value") 32 | } 33 | } 34 | 35 | func TestStdinNonVal(t *testing.T) { 36 | out, _ := setup(xml.Header+"test", "/root/tag") 37 | if out.String() != "test\n" { 38 | t.Error("Expecting 'test' for the result. Recieved: ", out.String()) 39 | } 40 | if retCode != 0 { 41 | t.Error("Incorrect return value") 42 | } 43 | } 44 | 45 | func TestFile(t *testing.T) { 46 | out, _ := setup("", "-ns", "foo=http://foo.bar", "/foo:test/foo:path", "test/1.xml") 47 | if out.String() != `path`+"\n" { 48 | t.Error(`Expecting 'path' for the result. Recieved: `, out.String()) 49 | } 50 | if retCode != 0 { 51 | t.Error("Incorrect return value") 52 | } 53 | } 54 | 55 | func TestDir(t *testing.T) { 56 | out, _ := setup("", "-r", "/foo", "test/subdir") 57 | val := strings.Replace(out.String(), "test\\subdir\\", "test/subdir/", -1) 58 | if val != `test/subdir/2.xml:bar`+"\n"+`test/subdir/3.xml:bar2`+"\n" { 59 | t.Error(`Incorrect result. Recieved: `, val) 60 | } 61 | if retCode != 0 { 62 | t.Error("Incorrect return value") 63 | } 64 | } 65 | 66 | func TestDirNonRec(t *testing.T) { 67 | _, err := setup("", "/foo", "test/subdir") 68 | val := strings.Replace(err.String(), "test\\subdir\\", "test/subdir/", -1) 69 | if val != `test/subdir: Is a directory`+"\n" { 70 | t.Error(`Incorrect result. Recieved: `, val) 71 | } 72 | if retCode != 1 { 73 | t.Error("Incorrect return value") 74 | } 75 | } 76 | 77 | func TestNoXPath(t *testing.T) { 78 | _, err := setup("") 79 | if err.String() != "Specify an XPath expression with one or more files, or pipe the XML from stdin. Run 'goxpath --help' for more information.\n" { 80 | t.Error("No XPath error") 81 | } 82 | if retCode != 1 { 83 | t.Error("Incorrect return value") 84 | } 85 | } 86 | 87 | func TestInvalidXPathExpr(t *testing.T) { 88 | _, err := setup("", "/foo()", "test/1.xml") 89 | if err.String() != "Invalid node-type foo\n" { 90 | t.Error("Invalid XPath error") 91 | } 92 | if retCode != 1 { 93 | t.Error("Incorrect return value") 94 | } 95 | } 96 | 97 | func TestInvalidFilePath(t *testing.T) { 98 | _, err := setup("", "/foo", "foo.xml") 99 | if err.String() != "Could not open file: foo.xml\n" { 100 | t.Error("Invalid error") 101 | } 102 | if retCode != 1 { 103 | t.Error("Incorrect return value") 104 | } 105 | } 106 | 107 | func TestXPathExecErr(t *testing.T) { 108 | _, err := setup("", "foobar()", "test/1.xml") 109 | if err.String() != "test/1.xml: Unknown function: foobar\n" { 110 | t.Error("Invalid error", err.String()) 111 | } 112 | if retCode != 1 { 113 | t.Error("Incorrect return value") 114 | } 115 | } 116 | 117 | func TestXPathExecErrStdin(t *testing.T) { 118 | _, err := setup(xml.Header+"test", "foobar()") 119 | if err.String() != "Unknown function: foobar\n" { 120 | t.Error("Invalid error", err.String()) 121 | } 122 | if retCode != 1 { 123 | t.Error("Incorrect return value") 124 | } 125 | } 126 | 127 | func TestInvalidXML(t *testing.T) { 128 | _, err := setup("", "/root") 129 | if err.String() != "XML syntax error on line 1: unexpected EOF\n" { 130 | t.Error("Invalid error", err.String()) 131 | } 132 | if retCode != 1 { 133 | t.Error("Incorrect return value") 134 | } 135 | } 136 | 137 | func TestVarRef(t *testing.T) { 138 | out, _ := setup(xml.Header+"test", "-var=foo=test", "/root/tag = $foo") 139 | if out.String() != "true\n" { 140 | t.Error("Expecting 'true' for the result. Recieved: ", out.String()) 141 | } 142 | if retCode != 0 { 143 | t.Error("Incorrect return value") 144 | } 145 | } 146 | 147 | func TestInvalidNSMap(t *testing.T) { 148 | _, err := setup(xml.Header+"", "-ns=foo=http://foo=bar", "/root") 149 | if err.String() != "Invalid namespace mapping: foo=http://foo=bar\n" { 150 | t.Error("Invalid error", err.String()) 151 | } 152 | if retCode != 1 { 153 | t.Error("Incorrect return value") 154 | } 155 | } 156 | 157 | func TestInvalidVarMap(t *testing.T) { 158 | _, err := setup(xml.Header+"", "-var=test=blag=foo", "/root") 159 | if err.String() != "Invalid variable mapping: test=blag=foo\n" { 160 | t.Error("Invalid error", err.String()) 161 | } 162 | if retCode != 1 { 163 | t.Error("Incorrect return value") 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /lexer/paths.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ChrisTrenkamp/goxpath/xconst" 7 | ) 8 | 9 | func absLocPathState(l *Lexer) stateFn { 10 | l.emit(XItemAbsLocPath) 11 | return stepState 12 | } 13 | 14 | func abbrAbsLocPathState(l *Lexer) stateFn { 15 | l.emit(XItemAbbrAbsLocPath) 16 | return stepState 17 | } 18 | 19 | func relLocPathState(l *Lexer) stateFn { 20 | l.emit(XItemRelLocPath) 21 | return stepState 22 | } 23 | 24 | func abbrRelLocPathState(l *Lexer) stateFn { 25 | l.emit(XItemAbbrRelLocPath) 26 | return stepState 27 | } 28 | 29 | func stepState(l *Lexer) stateFn { 30 | l.skipWS(true) 31 | r := l.next() 32 | 33 | for isElemChar(r) { 34 | r = l.next() 35 | } 36 | 37 | l.backup() 38 | tok := l.input[l.start:l.pos] 39 | 40 | state, err := parseSeparators(l, tok) 41 | if err != nil { 42 | return l.errorf(err.Error()) 43 | } 44 | 45 | return getNextPathState(l, state) 46 | } 47 | 48 | func parseSeparators(l *Lexer, tok string) (XItemType, error) { 49 | l.skipWS(false) 50 | state := XItemType(XItemQName) 51 | r := l.peek() 52 | 53 | if string(r) == ":" && string(l.peekAt(2)) == ":" { 54 | var err error 55 | if state, err = getAxis(l, tok); err != nil { 56 | return state, fmt.Errorf(err.Error()) 57 | } 58 | } else if string(r) == ":" { 59 | state = XItemNCName 60 | l.emitVal(state, tok) 61 | l.skip(1) 62 | l.skipWS(true) 63 | } else if string(r) == "@" { 64 | state = XItemAbbrAxis 65 | l.emitVal(state, tok) 66 | l.skip(1) 67 | l.skipWS(true) 68 | } else if string(r) == "(" { 69 | var err error 70 | if state, err = getNT(l, tok); err != nil { 71 | return state, fmt.Errorf(err.Error()) 72 | } 73 | } else if len(tok) > 0 { 74 | l.emitVal(state, tok) 75 | } 76 | 77 | return state, nil 78 | } 79 | 80 | func getAxis(l *Lexer, tok string) (XItemType, error) { 81 | var state XItemType 82 | for i := range xconst.AxisNames { 83 | if tok == xconst.AxisNames[i] { 84 | state = XItemAxis 85 | } 86 | } 87 | if state != XItemAxis { 88 | return state, fmt.Errorf("Invalid Axis specifier, %s", tok) 89 | } 90 | l.emitVal(state, tok) 91 | l.skip(2) 92 | l.skipWS(true) 93 | return state, nil 94 | } 95 | 96 | func getNT(l *Lexer, tok string) (XItemType, error) { 97 | isNT := false 98 | for _, i := range xconst.NodeTypes { 99 | if tok == i { 100 | isNT = true 101 | break 102 | } 103 | } 104 | 105 | if isNT { 106 | return procNT(l, tok) 107 | } 108 | 109 | return XItemError, fmt.Errorf("Invalid node-type " + tok) 110 | } 111 | 112 | func procNT(l *Lexer, tok string) (XItemType, error) { 113 | state := XItemType(XItemNodeType) 114 | l.emitVal(state, tok) 115 | l.skip(1) 116 | l.skipWS(true) 117 | n := l.peek() 118 | if tok == xconst.NodeTypeProcInst && (string(n) == `"` || string(n) == `'`) { 119 | if err := getStrLit(l, XItemProcLit); err != nil { 120 | return state, fmt.Errorf(err.Error()) 121 | } 122 | l.skipWS(true) 123 | n = l.next() 124 | } 125 | 126 | if string(n) != ")" { 127 | return state, fmt.Errorf("Missing ) at end of NodeType declaration.") 128 | } 129 | 130 | l.skip(1) 131 | return state, nil 132 | } 133 | 134 | func procFunc(l *Lexer, tok string) error { 135 | state := XItemType(XItemFunction) 136 | l.emitVal(state, tok) 137 | l.skip(1) 138 | l.skipWS(true) 139 | if string(l.peek()) != ")" { 140 | l.emit(XItemArgument) 141 | for { 142 | for state := startState; state != nil; { 143 | state = state(l) 144 | } 145 | l.skipWS(true) 146 | 147 | if string(l.peek()) == "," { 148 | l.emit(XItemArgument) 149 | l.skip(1) 150 | } else if string(l.peek()) == ")" { 151 | l.emit(XItemEndFunction) 152 | l.skip(1) 153 | break 154 | } else if l.peek() == eof { 155 | return fmt.Errorf("Missing ) at end of function declaration.") 156 | } 157 | } 158 | } else { 159 | l.emit(XItemEndFunction) 160 | l.skip(1) 161 | } 162 | 163 | return nil 164 | } 165 | 166 | func getNextPathState(l *Lexer, state XItemType) stateFn { 167 | isMultiPart := state == XItemAxis || state == XItemAbbrAxis || state == XItemNCName 168 | 169 | l.skipWS(true) 170 | 171 | for string(l.peek()) == "[" { 172 | if err := getPred(l); err != nil { 173 | return l.errorf(err.Error()) 174 | } 175 | } 176 | 177 | if string(l.peek()) == "/" && !isMultiPart { 178 | l.skip(1) 179 | if string(l.peek()) == "/" { 180 | l.skip(1) 181 | return abbrRelLocPathState 182 | } 183 | l.skipWS(true) 184 | return relLocPathState 185 | } else if isMultiPart && isElemChar(l.peek()) { 186 | return stepState 187 | } 188 | 189 | if isMultiPart { 190 | return l.errorf("Step is not complete") 191 | } 192 | 193 | l.emit(XItemEndPath) 194 | return findOperatorState 195 | } 196 | 197 | func getPred(l *Lexer) error { 198 | l.emit(XItemPredicate) 199 | l.skip(1) 200 | l.skipWS(true) 201 | 202 | if string(l.peek()) == "]" { 203 | return fmt.Errorf("Missing content in predicate.") 204 | } 205 | 206 | for state := startState; state != nil; { 207 | state = state(l) 208 | } 209 | 210 | l.skipWS(true) 211 | if string(l.peek()) != "]" { 212 | return fmt.Errorf("Missing ] at end of predicate.") 213 | } 214 | l.skip(1) 215 | l.emit(XItemEndPredicate) 216 | l.skipWS(true) 217 | 218 | return nil 219 | } 220 | -------------------------------------------------------------------------------- /tree/tree.go: -------------------------------------------------------------------------------- 1 | package tree 2 | 3 | import ( 4 | "encoding/xml" 5 | "sort" 6 | ) 7 | 8 | //XMLSpace is the W3C XML namespace 9 | const XMLSpace = "http://www.w3.org/XML/1998/namespace" 10 | 11 | //NodePos is a helper for representing the node's document order 12 | type NodePos int 13 | 14 | //Pos returns the node's document order position 15 | func (n NodePos) Pos() int { 16 | return int(n) 17 | } 18 | 19 | //NodeType is a safer way to determine a node's type than type assertions. 20 | type NodeType int 21 | 22 | //GetNodeType returns the node's type. 23 | func (t NodeType) GetNodeType() NodeType { 24 | return t 25 | } 26 | 27 | //These are all the possible node types 28 | const ( 29 | NtAttr NodeType = iota 30 | NtChd 31 | NtComm 32 | NtElem 33 | NtNs 34 | NtRoot 35 | NtPi 36 | ) 37 | 38 | //Node is a XPath result that is a node except elements 39 | type Node interface { 40 | //ResValue prints the node's string value 41 | ResValue() string 42 | //Pos returns the node's position in the document order 43 | Pos() int 44 | //GetToken returns the xml.Token representation of the node 45 | GetToken() xml.Token 46 | //GetParent returns the parent node, which will always be an XML element 47 | GetParent() Elem 48 | //GetNodeType returns the node's type 49 | GetNodeType() NodeType 50 | } 51 | 52 | //Elem is a XPath result that is an element node 53 | type Elem interface { 54 | Node 55 | //GetChildren returns the elements children. 56 | GetChildren() []Node 57 | //GetAttrs returns the attributes of the element 58 | GetAttrs() []Node 59 | } 60 | 61 | //NSElem is a node that keeps track of namespaces. 62 | type NSElem interface { 63 | Elem 64 | GetNS() map[xml.Name]string 65 | } 66 | 67 | //NSBuilder is a helper-struct for satisfying the NSElem interface 68 | type NSBuilder struct { 69 | NS map[xml.Name]string 70 | } 71 | 72 | //GetNS returns the namespaces found on the current element. It should not be 73 | //confused with BuildNS, which actually resolves the namespace nodes. 74 | func (ns NSBuilder) GetNS() map[xml.Name]string { 75 | return ns.NS 76 | } 77 | 78 | type nsValueSort []NS 79 | 80 | func (ns nsValueSort) Len() int { return len(ns) } 81 | func (ns nsValueSort) Swap(i, j int) { 82 | ns[i], ns[j] = ns[j], ns[i] 83 | } 84 | func (ns nsValueSort) Less(i, j int) bool { 85 | return ns[i].Value < ns[j].Value 86 | } 87 | 88 | //BuildNS resolves all the namespace nodes of the element and returns them 89 | func BuildNS(t Elem) (ret []NS) { 90 | vals := make(map[xml.Name]string) 91 | 92 | if nselem, ok := t.(NSElem); ok { 93 | buildNS(nselem, vals) 94 | 95 | ret = make([]NS, 0, len(vals)) 96 | i := 1 97 | 98 | for k, v := range vals { 99 | if !(k.Local == "xmlns" && k.Space == "" && v == "") { 100 | ret = append(ret, NS{ 101 | Attr: xml.Attr{Name: k, Value: v}, 102 | Parent: t, 103 | NodeType: NtNs, 104 | }) 105 | i++ 106 | } 107 | } 108 | 109 | sort.Sort(nsValueSort(ret)) 110 | for i := range ret { 111 | ret[i].NodePos = NodePos(t.Pos() + i + 1) 112 | } 113 | } 114 | 115 | return ret 116 | } 117 | 118 | func buildNS(x NSElem, ret map[xml.Name]string) { 119 | if x.GetNodeType() == NtRoot { 120 | return 121 | } 122 | 123 | if nselem, ok := x.GetParent().(NSElem); ok { 124 | buildNS(nselem, ret) 125 | } 126 | 127 | for k, v := range x.GetNS() { 128 | ret[k] = v 129 | } 130 | } 131 | 132 | //NS is a namespace node. 133 | type NS struct { 134 | xml.Attr 135 | Parent Elem 136 | NodePos 137 | NodeType 138 | } 139 | 140 | //GetToken returns the xml.Token representation of the namespace. 141 | func (ns NS) GetToken() xml.Token { 142 | return ns.Attr 143 | } 144 | 145 | //GetParent returns the parent node of the namespace. 146 | func (ns NS) GetParent() Elem { 147 | return ns.Parent 148 | } 149 | 150 | //ResValue returns the string value of the namespace 151 | func (ns NS) ResValue() string { 152 | return ns.Attr.Value 153 | } 154 | 155 | //GetAttribute is a convenience function for getting the specified attribute from an element. 156 | //false is returned if the attribute is not found. 157 | func GetAttribute(n Elem, local, space string) (xml.Attr, bool) { 158 | attrs := n.GetAttrs() 159 | for _, i := range attrs { 160 | attr := i.GetToken().(xml.Attr) 161 | if local == attr.Name.Local && space == attr.Name.Space { 162 | return attr, true 163 | } 164 | } 165 | return xml.Attr{}, false 166 | } 167 | 168 | //GetAttributeVal is like GetAttribute, except it returns the attribute's value. 169 | func GetAttributeVal(n Elem, local, space string) (string, bool) { 170 | attr, ok := GetAttribute(n, local, space) 171 | return attr.Value, ok 172 | } 173 | 174 | //GetAttrValOrEmpty is like GetAttributeVal, except it returns an empty string if 175 | //the attribute is not found instead of false. 176 | func GetAttrValOrEmpty(n Elem, local, space string) string { 177 | val, ok := GetAttributeVal(n, local, space) 178 | if !ok { 179 | return "" 180 | } 181 | return val 182 | } 183 | 184 | //FindNodeByPos finds a node from the given position. Returns nil if the node 185 | //is not found. 186 | func FindNodeByPos(n Node, pos int) Node { 187 | if n.Pos() == pos { 188 | return n 189 | } 190 | 191 | if elem, ok := n.(Elem); ok { 192 | chldrn := elem.GetChildren() 193 | for i := 1; i < len(chldrn); i++ { 194 | if chldrn[i-1].Pos() <= pos && chldrn[i].Pos() > pos { 195 | return FindNodeByPos(chldrn[i-1], pos) 196 | } 197 | } 198 | 199 | if len(chldrn) > 0 { 200 | if chldrn[len(chldrn)-1].Pos() <= pos { 201 | return FindNodeByPos(chldrn[len(chldrn)-1], pos) 202 | } 203 | } 204 | 205 | attrs := elem.GetAttrs() 206 | for _, i := range attrs { 207 | if i.Pos() == pos { 208 | return i 209 | } 210 | } 211 | 212 | ns := BuildNS(elem) 213 | for _, i := range ns { 214 | if i.Pos() == pos { 215 | return i 216 | } 217 | } 218 | } 219 | 220 | return nil 221 | } 222 | -------------------------------------------------------------------------------- /internal/execxp/findutil/findUtil.go: -------------------------------------------------------------------------------- 1 | package findutil 2 | 3 | import ( 4 | "encoding/xml" 5 | 6 | "github.com/ChrisTrenkamp/goxpath/parser/pathexpr" 7 | "github.com/ChrisTrenkamp/goxpath/tree" 8 | "github.com/ChrisTrenkamp/goxpath/xconst" 9 | ) 10 | 11 | const ( 12 | wildcard = "*" 13 | ) 14 | 15 | type findFunc func(tree.Node, *pathexpr.PathExpr, *[]tree.Node) 16 | 17 | var findMap = map[string]findFunc{ 18 | xconst.AxisAncestor: findAncestor, 19 | xconst.AxisAncestorOrSelf: findAncestorOrSelf, 20 | xconst.AxisAttribute: findAttribute, 21 | xconst.AxisChild: findChild, 22 | xconst.AxisDescendent: findDescendent, 23 | xconst.AxisDescendentOrSelf: findDescendentOrSelf, 24 | xconst.AxisFollowing: findFollowing, 25 | xconst.AxisFollowingSibling: findFollowingSibling, 26 | xconst.AxisNamespace: findNamespace, 27 | xconst.AxisParent: findParent, 28 | xconst.AxisPreceding: findPreceding, 29 | xconst.AxisPrecedingSibling: findPrecedingSibling, 30 | xconst.AxisSelf: findSelf, 31 | } 32 | 33 | //Find finds nodes based on the pathexpr.PathExpr 34 | func Find(x tree.Node, p pathexpr.PathExpr) []tree.Node { 35 | ret := []tree.Node{} 36 | 37 | if p.Axis == "" { 38 | findChild(x, &p, &ret) 39 | return ret 40 | } 41 | 42 | f := findMap[p.Axis] 43 | f(x, &p, &ret) 44 | 45 | return ret 46 | } 47 | 48 | func findAncestor(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 49 | if x.GetNodeType() == tree.NtRoot { 50 | return 51 | } 52 | 53 | addNode(x.GetParent(), p, ret) 54 | findAncestor(x.GetParent(), p, ret) 55 | } 56 | 57 | func findAncestorOrSelf(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 58 | findSelf(x, p, ret) 59 | findAncestor(x, p, ret) 60 | } 61 | 62 | func findAttribute(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 63 | if ele, ok := x.(tree.Elem); ok { 64 | for _, i := range ele.GetAttrs() { 65 | addNode(i, p, ret) 66 | } 67 | } 68 | } 69 | 70 | func findChild(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 71 | if ele, ok := x.(tree.Elem); ok { 72 | ch := ele.GetChildren() 73 | for i := range ch { 74 | addNode(ch[i], p, ret) 75 | } 76 | } 77 | } 78 | 79 | func findDescendent(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 80 | if ele, ok := x.(tree.Elem); ok { 81 | ch := ele.GetChildren() 82 | for i := range ch { 83 | addNode(ch[i], p, ret) 84 | findDescendent(ch[i], p, ret) 85 | } 86 | } 87 | } 88 | 89 | func findDescendentOrSelf(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 90 | findSelf(x, p, ret) 91 | findDescendent(x, p, ret) 92 | } 93 | 94 | func findFollowing(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 95 | if x.GetNodeType() == tree.NtRoot { 96 | return 97 | } 98 | par := x.GetParent() 99 | ch := par.GetChildren() 100 | i := 0 101 | for x != ch[i] { 102 | i++ 103 | } 104 | i++ 105 | for i < len(ch) { 106 | findDescendentOrSelf(ch[i], p, ret) 107 | i++ 108 | } 109 | findFollowing(par, p, ret) 110 | } 111 | 112 | func findFollowingSibling(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 113 | if x.GetNodeType() == tree.NtRoot { 114 | return 115 | } 116 | par := x.GetParent() 117 | ch := par.GetChildren() 118 | i := 0 119 | for x != ch[i] { 120 | i++ 121 | } 122 | i++ 123 | for i < len(ch) { 124 | findSelf(ch[i], p, ret) 125 | i++ 126 | } 127 | } 128 | 129 | func findNamespace(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 130 | if ele, ok := x.(tree.NSElem); ok { 131 | ns := tree.BuildNS(ele) 132 | for _, i := range ns { 133 | addNode(i, p, ret) 134 | } 135 | } 136 | } 137 | 138 | func findParent(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 139 | if x.GetNodeType() != tree.NtRoot { 140 | addNode(x.GetParent(), p, ret) 141 | } 142 | } 143 | 144 | func findPreceding(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 145 | if x.GetNodeType() == tree.NtRoot { 146 | return 147 | } 148 | par := x.GetParent() 149 | ch := par.GetChildren() 150 | i := len(ch) - 1 151 | for x != ch[i] { 152 | i-- 153 | } 154 | i-- 155 | for i >= 0 { 156 | findDescendentOrSelf(ch[i], p, ret) 157 | i-- 158 | } 159 | findPreceding(par, p, ret) 160 | } 161 | 162 | func findPrecedingSibling(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 163 | if x.GetNodeType() == tree.NtRoot { 164 | return 165 | } 166 | par := x.GetParent() 167 | ch := par.GetChildren() 168 | i := len(ch) - 1 169 | for x != ch[i] { 170 | i-- 171 | } 172 | i-- 173 | for i >= 0 { 174 | findSelf(ch[i], p, ret) 175 | i-- 176 | } 177 | } 178 | 179 | func findSelf(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 180 | addNode(x, p, ret) 181 | } 182 | 183 | func addNode(x tree.Node, p *pathexpr.PathExpr, ret *[]tree.Node) { 184 | add := false 185 | tok := x.GetToken() 186 | 187 | switch x.GetNodeType() { 188 | case tree.NtAttr: 189 | add = evalAttr(p, tok.(xml.Attr)) 190 | case tree.NtChd: 191 | add = evalChd(p) 192 | case tree.NtComm: 193 | add = evalComm(p) 194 | case tree.NtElem, tree.NtRoot: 195 | add = evalEle(p, tok.(xml.StartElement)) 196 | case tree.NtNs: 197 | add = evalNS(p, tok.(xml.Attr)) 198 | case tree.NtPi: 199 | add = evalPI(p) 200 | } 201 | 202 | if add { 203 | *ret = append(*ret, x) 204 | } 205 | } 206 | 207 | func evalAttr(p *pathexpr.PathExpr, a xml.Attr) bool { 208 | if p.NodeType == "" { 209 | if p.Name.Space != wildcard { 210 | if a.Name.Space != p.NS[p.Name.Space] { 211 | return false 212 | } 213 | } 214 | 215 | if p.Name.Local == wildcard && p.Axis == xconst.AxisAttribute { 216 | return true 217 | } 218 | 219 | if p.Name.Local == a.Name.Local { 220 | return true 221 | } 222 | } else { 223 | if p.NodeType == xconst.NodeTypeNode { 224 | return true 225 | } 226 | } 227 | 228 | return false 229 | } 230 | 231 | func evalChd(p *pathexpr.PathExpr) bool { 232 | if p.NodeType == xconst.NodeTypeText || p.NodeType == xconst.NodeTypeNode { 233 | return true 234 | } 235 | 236 | return false 237 | } 238 | 239 | func evalComm(p *pathexpr.PathExpr) bool { 240 | if p.NodeType == xconst.NodeTypeComment || p.NodeType == xconst.NodeTypeNode { 241 | return true 242 | } 243 | 244 | return false 245 | } 246 | 247 | func evalEle(p *pathexpr.PathExpr, ele xml.StartElement) bool { 248 | if p.NodeType == "" { 249 | return checkNameAndSpace(p, ele) 250 | } 251 | 252 | if p.NodeType == xconst.NodeTypeNode { 253 | return true 254 | } 255 | 256 | return false 257 | } 258 | 259 | func checkNameAndSpace(p *pathexpr.PathExpr, ele xml.StartElement) bool { 260 | if p.Name.Local == wildcard && p.Name.Space == "" { 261 | return true 262 | } 263 | 264 | if p.Name.Space != wildcard && ele.Name.Space != p.NS[p.Name.Space] { 265 | return false 266 | } 267 | 268 | if p.Name.Local == wildcard && p.Axis != xconst.AxisAttribute && p.Axis != xconst.AxisNamespace { 269 | return true 270 | } 271 | 272 | if p.Name.Local == ele.Name.Local { 273 | return true 274 | } 275 | 276 | return false 277 | } 278 | 279 | func evalNS(p *pathexpr.PathExpr, ns xml.Attr) bool { 280 | if p.NodeType == "" { 281 | if p.Name.Space != "" && p.Name.Space != wildcard { 282 | return false 283 | } 284 | 285 | if p.Name.Local == wildcard && p.Axis == xconst.AxisNamespace { 286 | return true 287 | } 288 | 289 | if p.Name.Local == ns.Name.Local { 290 | return true 291 | } 292 | } else { 293 | if p.NodeType == xconst.NodeTypeNode { 294 | return true 295 | } 296 | } 297 | 298 | return false 299 | } 300 | 301 | func evalPI(p *pathexpr.PathExpr) bool { 302 | if p.NodeType == xconst.NodeTypeProcInst || p.NodeType == xconst.NodeTypeNode { 303 | return true 304 | } 305 | 306 | return false 307 | } 308 | -------------------------------------------------------------------------------- /err_test.go: -------------------------------------------------------------------------------- 1 | package goxpath 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "runtime/debug" 7 | "testing" 8 | 9 | "github.com/ChrisTrenkamp/goxpath/tree" 10 | "github.com/ChrisTrenkamp/goxpath/tree/xmltree" 11 | "github.com/ChrisTrenkamp/goxpath/tree/xmltree/xmlele" 12 | ) 13 | 14 | type dummyType string 15 | 16 | func (d dummyType) String() string { 17 | return string(d) 18 | } 19 | func dummyFunc(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 20 | return dummyType(""), nil 21 | } 22 | 23 | var custFns = map[xml.Name]tree.Wrap{ 24 | {Local: "dummy"}: {Fn: dummyFunc}, 25 | {Space: "http://foo.com", Local: "spaceDummy"}: {Fn: dummyFunc}, 26 | } 27 | 28 | func execErr(xp, x string, errStr string, ns map[string]string, t *testing.T) { 29 | defer func() { 30 | if r := recover(); r != nil { 31 | t.Error("Panicked: from XPath expr: '" + xp) 32 | t.Error(r) 33 | t.Error(string(debug.Stack())) 34 | } 35 | }() 36 | _, err := ParseExec(xp, xmltree.MustParseXML(bytes.NewBufferString(x)), func(o *Opts) { o.NS = ns; o.Funcs = custFns }) 37 | 38 | if err.Error() != errStr { 39 | t.Error("Incorrect result:'" + err.Error() + "' from XPath expr: '" + xp + "'. Expecting: '" + errStr + "'") 40 | return 41 | } 42 | } 43 | 44 | func TestBadAxis(t *testing.T) { 45 | x := `` 46 | execErr(`/test/chil::p2`, x, "Invalid Axis specifier, chil", nil, t) 47 | } 48 | 49 | func TestIncompleteStep(t *testing.T) { 50 | x := `` 51 | execErr(`/child::+2`, x, "Step is not complete", nil, t) 52 | execErr(`/foo:`, x, "Step is not complete", nil, t) 53 | } 54 | 55 | func TestBadNodeType(t *testing.T) { 56 | x := `` 57 | execErr(`/test/foo()`, x, "Invalid node-type foo", nil, t) 58 | } 59 | 60 | func TestXPathErr(t *testing.T) { 61 | x := `` 62 | execErr(`/test/chil::p2`, x, "Invalid Axis specifier, chil", nil, t) 63 | } 64 | 65 | func TestNodeSetConvErr(t *testing.T) { 66 | x := `` 67 | for _, i := range []string{"sum", "count", "local-name", "namespace-uri", "name"} { 68 | execErr("/p1["+i+"(1)]", x, "Cannot convert object to a node-set", nil, t) 69 | } 70 | } 71 | 72 | func TestNodeSetConvUnionErr(t *testing.T) { 73 | x := `` 74 | execErr(`/p1 | 'invalid'`, x, "Cannot convert data type to node-set", nil, t) 75 | } 76 | 77 | func TestUnknownFunction(t *testing.T) { 78 | x := `` 79 | execErr(`invFunc()`, x, "Unknown function: invFunc", nil, t) 80 | } 81 | 82 | func TestUnterminatedString(t *testing.T) { 83 | x := `` 84 | execErr(`"asdf`, x, "Unexpected end of string literal.", nil, t) 85 | } 86 | 87 | func TestUnterminatedParenths(t *testing.T) { 88 | x := `` 89 | execErr(`(1 + 2 * 3`, x, "Missing end )", nil, t) 90 | } 91 | 92 | func TestUnterminatedNTQuotes(t *testing.T) { 93 | x := `` 94 | execErr(`//processing-instruction('foo)`, x, "Unexpected end of string literal.", nil, t) 95 | } 96 | 97 | func TestUnterminatedNTParenths(t *testing.T) { 98 | x := `` 99 | execErr(`//processing-instruction('foo'`, x, "Missing ) at end of NodeType declaration.", nil, t) 100 | } 101 | 102 | func TestUnterminatedFnParenths(t *testing.T) { 103 | x := `` 104 | execErr(`true(`, x, "Missing ) at end of function declaration.", nil, t) 105 | } 106 | 107 | func TestEmptyPred(t *testing.T) { 108 | x := `text` 109 | execErr(`/p1[ ]`, x, "Missing content in predicate.", nil, t) 110 | } 111 | 112 | func TestUnterminatedPred(t *testing.T) { 113 | x := `text` 114 | execErr(`/p1[. = 'text'`, x, "Missing ] at end of predicate.", nil, t) 115 | } 116 | 117 | func TestNotEnoughArgs(t *testing.T) { 118 | x := `text` 119 | execErr(`concat('test')`, x, "Invalid number of arguments", nil, t) 120 | } 121 | 122 | func TestMarshalErr(t *testing.T) { 123 | x := `` 124 | n := xmltree.MustParseXML(bytes.NewBufferString(x)) 125 | f := tree.FindNodeByPos(n, 3).(*xmlele.XMLEle) 126 | f.Name.Local = "" 127 | buf := &bytes.Buffer{} 128 | err := Marshal(n, buf) 129 | if err == nil { 130 | t.Error("No error") 131 | } 132 | } 133 | 134 | func TestParsePanic(t *testing.T) { 135 | errs := 0 136 | defer func() { 137 | if errs != 1 { 138 | t.Error("Err not 1") 139 | } 140 | }() 141 | defer func() { 142 | if r := recover(); r != nil { 143 | errs++ 144 | } 145 | }() 146 | MustParse(`/foo()`) 147 | } 148 | 149 | func TestExecPanic(t *testing.T) { 150 | errs := 0 151 | defer func() { 152 | if errs != 1 { 153 | t.Error("Err not 1") 154 | } 155 | }() 156 | defer func() { 157 | if r := recover(); r != nil { 158 | errs++ 159 | } 160 | }() 161 | MustParse("foo()").MustExec(xmltree.MustParseXML(bytes.NewBufferString(xml.Header + ""))) 162 | } 163 | 164 | func TestDummyType(t *testing.T) { 165 | ns := map[string]string{"foo": "http://foo.com"} 166 | x := `` 167 | execErr(`dummy() = 1`, x, "Cannot convert data type to number", nil, t) 168 | execErr(`dummy() = true()`, x, "Cannot convert argument to boolean", nil, t) 169 | execErr(`dummy() and true()`, x, "Cannot convert argument to boolean", nil, t) 170 | execErr(`not(dummy()) = 1`, x, "Cannot convert object to a boolean", nil, t) 171 | execErr(`1 = not(dummy())`, x, "Cannot convert object to a boolean", nil, t) 172 | execErr(`not(dummy() = 1)`, x, "Cannot convert data type to number", nil, t) 173 | execErr(`/p1[dummy()]`, x, "Cannot convert argument to boolean", nil, t) 174 | for _, i := range []string{"boolean", "not"} { 175 | execErr(i+`(dummy())`, x, "Cannot convert object to a boolean", nil, t) 176 | } 177 | for _, i := range []string{"number", "floor", "ceiling", "round"} { 178 | execErr(i+`(dummy())`, x, "Cannot convert object to a number", nil, t) 179 | } 180 | execErr(`substring("12345", dummy(), 2)`, x, "Cannot convert object to a number", nil, t) 181 | execErr(`substring("12345", 2, dummy())`, x, "Cannot convert object to a number", nil, t) 182 | execErr(`foo:spaceDummy() = 1`, x, "Cannot convert data type to number", ns, t) 183 | } 184 | 185 | func TestGoxpathBool(t *testing.T) { 186 | opts := func(o *Opts) { o.Funcs = custFns } 187 | x := xmltree.MustParseXML(bytes.NewBufferString(``)) 188 | _, err := MustParse(`dummy() = 1`).ExecBool(x, opts) 189 | if err == nil { 190 | t.Error("Error not nil") 191 | } 192 | _, err = MustParse(`dummy()`).ExecBool(x, opts) 193 | if err == nil { 194 | t.Error("Error not nil") 195 | } 196 | b, err := MustParse(`/p1`).ExecBool(x, opts) 197 | if !b || err != nil { 198 | t.Error("Incorrect result") 199 | } 200 | } 201 | 202 | func TestGoxpathNum(t *testing.T) { 203 | opts := func(o *Opts) { o.Funcs = custFns } 204 | x := xmltree.MustParseXML(bytes.NewBufferString(`3`)) 205 | _, err := MustParse(`dummy() = 1`).ExecNum(x, opts) 206 | if err == nil { 207 | t.Error("Error not nil") 208 | } 209 | _, err = MustParse(`dummy()`).ExecNum(x, opts) 210 | if err == nil { 211 | t.Error("Error not nil") 212 | } 213 | n, err := MustParse(`/p1`).ExecNum(x, opts) 214 | if n != 3 || err != nil { 215 | t.Error("Incorrect result") 216 | } 217 | } 218 | 219 | func TestGoxpathNode(t *testing.T) { 220 | opts := func(o *Opts) { o.Funcs = custFns } 221 | x := xmltree.MustParseXML(bytes.NewBufferString(``)) 222 | _, err := MustParse(`dummy() = 1`).ExecNode(x, opts) 223 | if err == nil { 224 | t.Error("Error not nil") 225 | } 226 | _, err = MustParse(`dummy()`).ExecNode(x, opts) 227 | if err == nil { 228 | t.Error("Error not nil") 229 | } 230 | n, err := MustParse(`/p1`).ExecNode(x, opts) 231 | if len(n) != 1 || err != nil { 232 | t.Error("Incorrect result") 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /misc_test.go: -------------------------------------------------------------------------------- 1 | package goxpath 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "testing" 7 | 8 | "github.com/ChrisTrenkamp/goxpath/lexer" 9 | "github.com/ChrisTrenkamp/goxpath/parser" 10 | "github.com/ChrisTrenkamp/goxpath/tree" 11 | "github.com/ChrisTrenkamp/goxpath/tree/xmltree" 12 | ) 13 | 14 | func TestISO_8859_1(t *testing.T) { 15 | p := `/test` 16 | x := `testpathtest2` 17 | exp := "testpathtest2" 18 | execVal(p, x, exp, nil, t) 19 | } 20 | 21 | func TestNodePos(t *testing.T) { 22 | ns := map[string]string{"test": "http://test", "test2": "http://test2", "test3": "http://test3"} 23 | x := `text` 24 | testPos := func(path string, pos int) { 25 | res := MustParse(path).MustExec(xmltree.MustParseXML(bytes.NewBufferString(x)), func(o *Opts) { o.NS = ns }).(tree.NodeSet) 26 | if len(res) != 1 { 27 | t.Errorf("Result length not 1: %s", path) 28 | return 29 | } 30 | exPos := res[0].(tree.Node).Pos() 31 | if exPos != pos { 32 | t.Errorf("Node position not correct. Recieved %d, expected %d", exPos, pos) 33 | } 34 | } 35 | testPos("/", 0) 36 | testPos("/*", 1) 37 | testPos("/*/namespace::*[1]", 2) 38 | testPos("/*/namespace::*[2]", 3) 39 | testPos("/*/attribute::*[1]", 4) 40 | testPos("//*:p2", 5) 41 | testPos("//*:p2/namespace::*[1]", 6) 42 | testPos("//*:p2/namespace::*[2]", 7) 43 | testPos("//*:p2/namespace::*[3]", 8) 44 | testPos("//*:p2/attribute::*[1]", 9) 45 | testPos("//text()", 10) 46 | } 47 | 48 | func TestNSSort(t *testing.T) { 49 | testNS := func(n tree.Node, url string) { 50 | if n.(tree.NS).Value != url { 51 | t.Errorf("Unexpected namespace %s. Expecting %s", n.(tree.NS).Value, url) 52 | } 53 | } 54 | ns := map[string]string{"test": "http://test", "test2": "http://test2", "test3": "http://test3"} 55 | x := `` 56 | res := MustParse("/*:p1/namespace::*").MustExec(xmltree.MustParseXML(bytes.NewBufferString(x)), func(o *Opts) { o.NS = ns }).(tree.NodeSet) 57 | testNS(res[0], ns["test"]) 58 | testNS(res[1], ns["test2"]) 59 | testNS(res[2], ns["test3"]) 60 | testNS(res[3], "http://www.w3.org/XML/1998/namespace") 61 | } 62 | 63 | func TestFindNodeByPos(t *testing.T) { 64 | x := `text` 65 | nt := xmltree.MustParseXML(bytes.NewBufferString(x)) 66 | if tree.FindNodeByPos(nt, 5).GetNodeType() != tree.NtElem { 67 | t.Error("Node 5 not element") 68 | } 69 | if tree.FindNodeByPos(nt, 14).GetNodeType() != tree.NtChd { 70 | t.Error("Node 14 not char data") 71 | } 72 | if tree.FindNodeByPos(nt, 4).GetNodeType() != tree.NtAttr { 73 | t.Error("Node 4 not attribute") 74 | } 75 | if tree.FindNodeByPos(nt, 3).GetNodeType() != tree.NtNs { 76 | t.Error("Node 3 not namespace") 77 | } 78 | if tree.FindNodeByPos(nt, 19) != nil { 79 | t.Error("Invalid node returned") 80 | } 81 | } 82 | 83 | func TestFindAttr(t *testing.T) { 84 | x := `` 85 | nt := xmltree.MustParseXML(bytes.NewBufferString(x)) 86 | res, _ := ParseExec("/p1", nt) 87 | node := res.(tree.NodeSet)[0].(tree.Elem) 88 | if val, ok := tree.GetAttributeVal(node, "attr1", ""); !ok || val != "foo" { 89 | t.Error("attr1 not foo") 90 | } 91 | if val, ok := tree.GetAttributeVal(node, "attr2", "http://test"); !ok || val != "bar" { 92 | t.Error("attr2 not bar") 93 | } 94 | if val, ok := tree.GetAttributeVal(node, "attr3", ""); ok || val != "" { 95 | t.Error("attr3 is set") 96 | } 97 | if val := tree.GetAttrValOrEmpty(node, "attr3", ""); val != "" { 98 | t.Error("attr3 is set") 99 | } 100 | if val := tree.GetAttrValOrEmpty(node, "attr1", ""); val != "foo" { 101 | t.Error("attr1 not foo") 102 | } 103 | } 104 | 105 | func TestVariable(t *testing.T) { 106 | x := xmltree.MustParseXML(bytes.NewBufferString(xml.Header + "foobar")) 107 | xp := MustParse(`/p1/p2`) 108 | res := xp.MustExec(x) 109 | opt := func(o *Opts) { 110 | o.Vars["prev"] = res 111 | } 112 | xp = MustParse(`$prev = 'foo'`) 113 | if res, err := xp.ExecBool(x, opt); err != nil || !res { 114 | t.Error("Incorrect result", res, err) 115 | } 116 | if _, err := xp.ExecBool(x); err == nil { 117 | t.Error("Error not nil") 118 | } 119 | if _, err := Parse(`$ = 'foo'`); err == nil { 120 | t.Error("Parse error not nil") 121 | } 122 | } 123 | 124 | func TestFunctionInteractions(t *testing.T) { 125 | cases := []struct { 126 | name string 127 | expected *parser.Node 128 | xpath string 129 | }{{ 130 | name: "TestFunctionInteractions_1", 131 | expected: &parser.Node{ 132 | Val: lexer.XItem{lexer.XItemOperator, "="}, 133 | Left: &parser.Node{Val: lexer.XItem{lexer.XItemFunction, "current"}}, 134 | Right: &parser.Node{Val: lexer.XItem{lexer.XItemNumLit, "1"}}, 135 | }, 136 | xpath: `current()=1`, 137 | }, { 138 | name: "TestFunctionInteractions_2", 139 | expected: &parser.Node{ 140 | Val: lexer.XItem{lexer.XItemOperator, "="}, 141 | Left: &parser.Node{Val: lexer.XItem{lexer.XItemFunction, "current"}}, 142 | Right: &parser.Node{Val: lexer.XItem{lexer.XItemStrLit, "abc"}}, 143 | }, 144 | xpath: `current() = 'abc'`, 145 | }, { 146 | name: "TestFunctionInteractions_3", 147 | expected: &parser.Node{ 148 | Val: lexer.XItem{lexer.XItemOperator, "="}, 149 | Left: &parser.Node{Val: lexer.XItem{lexer.XItemFunction, "current"}}, 150 | Right: &parser.Node{ 151 | Val: lexer.XItem{lexer.XItemRelLocPath, ""}, 152 | Left: &parser.Node{Val: lexer.XItem{lexer.XItemQName, "abc"}}, 153 | }, 154 | }, 155 | xpath: `current() = abc`, 156 | }, { 157 | name: "TestFunctionInteractions_4", 158 | expected: &parser.Node{ 159 | Val: lexer.XItem{lexer.XItemFunction, "current"}, 160 | Left: nil, 161 | Right: &parser.Node{Val: lexer.XItem{lexer.XItemRelLocPath, ""}, 162 | Left: &parser.Node{Val: lexer.XItem{lexer.XItemNCName, "ns"}, 163 | Left: &parser.Node{ 164 | Val: lexer.XItem{lexer.XItemQName, "sub"}, 165 | }, 166 | }, 167 | }, 168 | }, 169 | xpath: `current()/ns:sub`, 170 | }, { 171 | name: "TestFunctionInteractions_5", 172 | expected: &parser.Node{ 173 | Val: lexer.XItem{lexer.XItemOperator, "="}, 174 | Left: &parser.Node{ 175 | Val: lexer.XItem{lexer.XItemFunction, "current"}, 176 | Left: nil, 177 | Right: &parser.Node{ 178 | Val: lexer.XItem{lexer.XItemRelLocPath, ""}, 179 | Left: &parser.Node{ 180 | Val: lexer.XItem{lexer.XItemNCName, "ns"}, 181 | Left: &parser.Node{ 182 | Val: lexer.XItem{lexer.XItemQName, "sub"}, 183 | }, 184 | }, 185 | }, 186 | }, 187 | Right: &parser.Node{ 188 | Val: lexer.XItem{lexer.XItemStrLit, "abc"}, 189 | }, 190 | }, 191 | xpath: `current()/ns:sub = 'abc'`, 192 | }, { 193 | name: "TestFunctionInteractions_6", 194 | expected: &parser.Node{ 195 | Val: lexer.XItem{lexer.XItemOperator, "="}, 196 | Left: &parser.Node{ 197 | Val: lexer.XItem{lexer.XItemFunction, "current"}, 198 | Left: nil, 199 | Right: &parser.Node{ 200 | Val: lexer.XItem{lexer.XItemRelLocPath, ""}, 201 | Left: &parser.Node{ 202 | Val: lexer.XItem{lexer.XItemNCName, "ns"}, 203 | Left: &parser.Node{ 204 | Val: lexer.XItem{lexer.XItemQName, "sub"}, 205 | }, 206 | }, 207 | }, 208 | }, 209 | Right: &parser.Node{ 210 | Val: lexer.XItem{lexer.XItemRelLocPath, ""}, 211 | Left: &parser.Node{ 212 | Val: lexer.XItem{lexer.XItemQName, "abc"}, 213 | }, 214 | }, 215 | }, 216 | xpath: `current()/ns:sub = abc`, 217 | }, 218 | } 219 | 220 | for _, val := range cases { 221 | t.Run(val.name, func(t *testing.T) { 222 | actual := MustParse(val.xpath).n 223 | if !isEquivalentAST(val.expected, actual) { 224 | t.Errorf("Expected AST tree is not the same as actual tree that goxpath parser returns") 225 | } 226 | }) 227 | } 228 | } 229 | 230 | func isEquivalentAST(node1, node2 *parser.Node) bool { 231 | if node1 == nil && node2 == nil { 232 | return true 233 | } 234 | if node1 != nil && node2 != nil { 235 | return (node1.Val == node2.Val && isEquivalentAST(node1.Left, node2.Left) && isEquivalentAST(node1.Right, node2.Right)) 236 | } 237 | return false 238 | } 239 | -------------------------------------------------------------------------------- /lexer/xmlchars.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import "unicode" 4 | 5 | //first and second was copied from src/encoding/xml/xml.go 6 | var first = &unicode.RangeTable{ 7 | R16: []unicode.Range16{ 8 | {0x003A, 0x003A, 1}, 9 | {0x0041, 0x005A, 1}, 10 | {0x005F, 0x005F, 1}, 11 | {0x0061, 0x007A, 1}, 12 | {0x00C0, 0x00D6, 1}, 13 | {0x00D8, 0x00F6, 1}, 14 | {0x00F8, 0x00FF, 1}, 15 | {0x0100, 0x0131, 1}, 16 | {0x0134, 0x013E, 1}, 17 | {0x0141, 0x0148, 1}, 18 | {0x014A, 0x017E, 1}, 19 | {0x0180, 0x01C3, 1}, 20 | {0x01CD, 0x01F0, 1}, 21 | {0x01F4, 0x01F5, 1}, 22 | {0x01FA, 0x0217, 1}, 23 | {0x0250, 0x02A8, 1}, 24 | {0x02BB, 0x02C1, 1}, 25 | {0x0386, 0x0386, 1}, 26 | {0x0388, 0x038A, 1}, 27 | {0x038C, 0x038C, 1}, 28 | {0x038E, 0x03A1, 1}, 29 | {0x03A3, 0x03CE, 1}, 30 | {0x03D0, 0x03D6, 1}, 31 | {0x03DA, 0x03E0, 2}, 32 | {0x03E2, 0x03F3, 1}, 33 | {0x0401, 0x040C, 1}, 34 | {0x040E, 0x044F, 1}, 35 | {0x0451, 0x045C, 1}, 36 | {0x045E, 0x0481, 1}, 37 | {0x0490, 0x04C4, 1}, 38 | {0x04C7, 0x04C8, 1}, 39 | {0x04CB, 0x04CC, 1}, 40 | {0x04D0, 0x04EB, 1}, 41 | {0x04EE, 0x04F5, 1}, 42 | {0x04F8, 0x04F9, 1}, 43 | {0x0531, 0x0556, 1}, 44 | {0x0559, 0x0559, 1}, 45 | {0x0561, 0x0586, 1}, 46 | {0x05D0, 0x05EA, 1}, 47 | {0x05F0, 0x05F2, 1}, 48 | {0x0621, 0x063A, 1}, 49 | {0x0641, 0x064A, 1}, 50 | {0x0671, 0x06B7, 1}, 51 | {0x06BA, 0x06BE, 1}, 52 | {0x06C0, 0x06CE, 1}, 53 | {0x06D0, 0x06D3, 1}, 54 | {0x06D5, 0x06D5, 1}, 55 | {0x06E5, 0x06E6, 1}, 56 | {0x0905, 0x0939, 1}, 57 | {0x093D, 0x093D, 1}, 58 | {0x0958, 0x0961, 1}, 59 | {0x0985, 0x098C, 1}, 60 | {0x098F, 0x0990, 1}, 61 | {0x0993, 0x09A8, 1}, 62 | {0x09AA, 0x09B0, 1}, 63 | {0x09B2, 0x09B2, 1}, 64 | {0x09B6, 0x09B9, 1}, 65 | {0x09DC, 0x09DD, 1}, 66 | {0x09DF, 0x09E1, 1}, 67 | {0x09F0, 0x09F1, 1}, 68 | {0x0A05, 0x0A0A, 1}, 69 | {0x0A0F, 0x0A10, 1}, 70 | {0x0A13, 0x0A28, 1}, 71 | {0x0A2A, 0x0A30, 1}, 72 | {0x0A32, 0x0A33, 1}, 73 | {0x0A35, 0x0A36, 1}, 74 | {0x0A38, 0x0A39, 1}, 75 | {0x0A59, 0x0A5C, 1}, 76 | {0x0A5E, 0x0A5E, 1}, 77 | {0x0A72, 0x0A74, 1}, 78 | {0x0A85, 0x0A8B, 1}, 79 | {0x0A8D, 0x0A8D, 1}, 80 | {0x0A8F, 0x0A91, 1}, 81 | {0x0A93, 0x0AA8, 1}, 82 | {0x0AAA, 0x0AB0, 1}, 83 | {0x0AB2, 0x0AB3, 1}, 84 | {0x0AB5, 0x0AB9, 1}, 85 | {0x0ABD, 0x0AE0, 0x23}, 86 | {0x0B05, 0x0B0C, 1}, 87 | {0x0B0F, 0x0B10, 1}, 88 | {0x0B13, 0x0B28, 1}, 89 | {0x0B2A, 0x0B30, 1}, 90 | {0x0B32, 0x0B33, 1}, 91 | {0x0B36, 0x0B39, 1}, 92 | {0x0B3D, 0x0B3D, 1}, 93 | {0x0B5C, 0x0B5D, 1}, 94 | {0x0B5F, 0x0B61, 1}, 95 | {0x0B85, 0x0B8A, 1}, 96 | {0x0B8E, 0x0B90, 1}, 97 | {0x0B92, 0x0B95, 1}, 98 | {0x0B99, 0x0B9A, 1}, 99 | {0x0B9C, 0x0B9C, 1}, 100 | {0x0B9E, 0x0B9F, 1}, 101 | {0x0BA3, 0x0BA4, 1}, 102 | {0x0BA8, 0x0BAA, 1}, 103 | {0x0BAE, 0x0BB5, 1}, 104 | {0x0BB7, 0x0BB9, 1}, 105 | {0x0C05, 0x0C0C, 1}, 106 | {0x0C0E, 0x0C10, 1}, 107 | {0x0C12, 0x0C28, 1}, 108 | {0x0C2A, 0x0C33, 1}, 109 | {0x0C35, 0x0C39, 1}, 110 | {0x0C60, 0x0C61, 1}, 111 | {0x0C85, 0x0C8C, 1}, 112 | {0x0C8E, 0x0C90, 1}, 113 | {0x0C92, 0x0CA8, 1}, 114 | {0x0CAA, 0x0CB3, 1}, 115 | {0x0CB5, 0x0CB9, 1}, 116 | {0x0CDE, 0x0CDE, 1}, 117 | {0x0CE0, 0x0CE1, 1}, 118 | {0x0D05, 0x0D0C, 1}, 119 | {0x0D0E, 0x0D10, 1}, 120 | {0x0D12, 0x0D28, 1}, 121 | {0x0D2A, 0x0D39, 1}, 122 | {0x0D60, 0x0D61, 1}, 123 | {0x0E01, 0x0E2E, 1}, 124 | {0x0E30, 0x0E30, 1}, 125 | {0x0E32, 0x0E33, 1}, 126 | {0x0E40, 0x0E45, 1}, 127 | {0x0E81, 0x0E82, 1}, 128 | {0x0E84, 0x0E84, 1}, 129 | {0x0E87, 0x0E88, 1}, 130 | {0x0E8A, 0x0E8D, 3}, 131 | {0x0E94, 0x0E97, 1}, 132 | {0x0E99, 0x0E9F, 1}, 133 | {0x0EA1, 0x0EA3, 1}, 134 | {0x0EA5, 0x0EA7, 2}, 135 | {0x0EAA, 0x0EAB, 1}, 136 | {0x0EAD, 0x0EAE, 1}, 137 | {0x0EB0, 0x0EB0, 1}, 138 | {0x0EB2, 0x0EB3, 1}, 139 | {0x0EBD, 0x0EBD, 1}, 140 | {0x0EC0, 0x0EC4, 1}, 141 | {0x0F40, 0x0F47, 1}, 142 | {0x0F49, 0x0F69, 1}, 143 | {0x10A0, 0x10C5, 1}, 144 | {0x10D0, 0x10F6, 1}, 145 | {0x1100, 0x1100, 1}, 146 | {0x1102, 0x1103, 1}, 147 | {0x1105, 0x1107, 1}, 148 | {0x1109, 0x1109, 1}, 149 | {0x110B, 0x110C, 1}, 150 | {0x110E, 0x1112, 1}, 151 | {0x113C, 0x1140, 2}, 152 | {0x114C, 0x1150, 2}, 153 | {0x1154, 0x1155, 1}, 154 | {0x1159, 0x1159, 1}, 155 | {0x115F, 0x1161, 1}, 156 | {0x1163, 0x1169, 2}, 157 | {0x116D, 0x116E, 1}, 158 | {0x1172, 0x1173, 1}, 159 | {0x1175, 0x119E, 0x119E - 0x1175}, 160 | {0x11A8, 0x11AB, 0x11AB - 0x11A8}, 161 | {0x11AE, 0x11AF, 1}, 162 | {0x11B7, 0x11B8, 1}, 163 | {0x11BA, 0x11BA, 1}, 164 | {0x11BC, 0x11C2, 1}, 165 | {0x11EB, 0x11F0, 0x11F0 - 0x11EB}, 166 | {0x11F9, 0x11F9, 1}, 167 | {0x1E00, 0x1E9B, 1}, 168 | {0x1EA0, 0x1EF9, 1}, 169 | {0x1F00, 0x1F15, 1}, 170 | {0x1F18, 0x1F1D, 1}, 171 | {0x1F20, 0x1F45, 1}, 172 | {0x1F48, 0x1F4D, 1}, 173 | {0x1F50, 0x1F57, 1}, 174 | {0x1F59, 0x1F5B, 0x1F5B - 0x1F59}, 175 | {0x1F5D, 0x1F5D, 1}, 176 | {0x1F5F, 0x1F7D, 1}, 177 | {0x1F80, 0x1FB4, 1}, 178 | {0x1FB6, 0x1FBC, 1}, 179 | {0x1FBE, 0x1FBE, 1}, 180 | {0x1FC2, 0x1FC4, 1}, 181 | {0x1FC6, 0x1FCC, 1}, 182 | {0x1FD0, 0x1FD3, 1}, 183 | {0x1FD6, 0x1FDB, 1}, 184 | {0x1FE0, 0x1FEC, 1}, 185 | {0x1FF2, 0x1FF4, 1}, 186 | {0x1FF6, 0x1FFC, 1}, 187 | {0x2126, 0x2126, 1}, 188 | {0x212A, 0x212B, 1}, 189 | {0x212E, 0x212E, 1}, 190 | {0x2180, 0x2182, 1}, 191 | {0x3007, 0x3007, 1}, 192 | {0x3021, 0x3029, 1}, 193 | {0x3041, 0x3094, 1}, 194 | {0x30A1, 0x30FA, 1}, 195 | {0x3105, 0x312C, 1}, 196 | {0x4E00, 0x9FA5, 1}, 197 | {0xAC00, 0xD7A3, 1}, 198 | }, 199 | } 200 | 201 | var second = &unicode.RangeTable{ 202 | R16: []unicode.Range16{ 203 | {0x002D, 0x002E, 1}, 204 | {0x0030, 0x0039, 1}, 205 | {0x00B7, 0x00B7, 1}, 206 | {0x02D0, 0x02D1, 1}, 207 | {0x0300, 0x0345, 1}, 208 | {0x0360, 0x0361, 1}, 209 | {0x0387, 0x0387, 1}, 210 | {0x0483, 0x0486, 1}, 211 | {0x0591, 0x05A1, 1}, 212 | {0x05A3, 0x05B9, 1}, 213 | {0x05BB, 0x05BD, 1}, 214 | {0x05BF, 0x05BF, 1}, 215 | {0x05C1, 0x05C2, 1}, 216 | {0x05C4, 0x0640, 0x0640 - 0x05C4}, 217 | {0x064B, 0x0652, 1}, 218 | {0x0660, 0x0669, 1}, 219 | {0x0670, 0x0670, 1}, 220 | {0x06D6, 0x06DC, 1}, 221 | {0x06DD, 0x06DF, 1}, 222 | {0x06E0, 0x06E4, 1}, 223 | {0x06E7, 0x06E8, 1}, 224 | {0x06EA, 0x06ED, 1}, 225 | {0x06F0, 0x06F9, 1}, 226 | {0x0901, 0x0903, 1}, 227 | {0x093C, 0x093C, 1}, 228 | {0x093E, 0x094C, 1}, 229 | {0x094D, 0x094D, 1}, 230 | {0x0951, 0x0954, 1}, 231 | {0x0962, 0x0963, 1}, 232 | {0x0966, 0x096F, 1}, 233 | {0x0981, 0x0983, 1}, 234 | {0x09BC, 0x09BC, 1}, 235 | {0x09BE, 0x09BF, 1}, 236 | {0x09C0, 0x09C4, 1}, 237 | {0x09C7, 0x09C8, 1}, 238 | {0x09CB, 0x09CD, 1}, 239 | {0x09D7, 0x09D7, 1}, 240 | {0x09E2, 0x09E3, 1}, 241 | {0x09E6, 0x09EF, 1}, 242 | {0x0A02, 0x0A3C, 0x3A}, 243 | {0x0A3E, 0x0A3F, 1}, 244 | {0x0A40, 0x0A42, 1}, 245 | {0x0A47, 0x0A48, 1}, 246 | {0x0A4B, 0x0A4D, 1}, 247 | {0x0A66, 0x0A6F, 1}, 248 | {0x0A70, 0x0A71, 1}, 249 | {0x0A81, 0x0A83, 1}, 250 | {0x0ABC, 0x0ABC, 1}, 251 | {0x0ABE, 0x0AC5, 1}, 252 | {0x0AC7, 0x0AC9, 1}, 253 | {0x0ACB, 0x0ACD, 1}, 254 | {0x0AE6, 0x0AEF, 1}, 255 | {0x0B01, 0x0B03, 1}, 256 | {0x0B3C, 0x0B3C, 1}, 257 | {0x0B3E, 0x0B43, 1}, 258 | {0x0B47, 0x0B48, 1}, 259 | {0x0B4B, 0x0B4D, 1}, 260 | {0x0B56, 0x0B57, 1}, 261 | {0x0B66, 0x0B6F, 1}, 262 | {0x0B82, 0x0B83, 1}, 263 | {0x0BBE, 0x0BC2, 1}, 264 | {0x0BC6, 0x0BC8, 1}, 265 | {0x0BCA, 0x0BCD, 1}, 266 | {0x0BD7, 0x0BD7, 1}, 267 | {0x0BE7, 0x0BEF, 1}, 268 | {0x0C01, 0x0C03, 1}, 269 | {0x0C3E, 0x0C44, 1}, 270 | {0x0C46, 0x0C48, 1}, 271 | {0x0C4A, 0x0C4D, 1}, 272 | {0x0C55, 0x0C56, 1}, 273 | {0x0C66, 0x0C6F, 1}, 274 | {0x0C82, 0x0C83, 1}, 275 | {0x0CBE, 0x0CC4, 1}, 276 | {0x0CC6, 0x0CC8, 1}, 277 | {0x0CCA, 0x0CCD, 1}, 278 | {0x0CD5, 0x0CD6, 1}, 279 | {0x0CE6, 0x0CEF, 1}, 280 | {0x0D02, 0x0D03, 1}, 281 | {0x0D3E, 0x0D43, 1}, 282 | {0x0D46, 0x0D48, 1}, 283 | {0x0D4A, 0x0D4D, 1}, 284 | {0x0D57, 0x0D57, 1}, 285 | {0x0D66, 0x0D6F, 1}, 286 | {0x0E31, 0x0E31, 1}, 287 | {0x0E34, 0x0E3A, 1}, 288 | {0x0E46, 0x0E46, 1}, 289 | {0x0E47, 0x0E4E, 1}, 290 | {0x0E50, 0x0E59, 1}, 291 | {0x0EB1, 0x0EB1, 1}, 292 | {0x0EB4, 0x0EB9, 1}, 293 | {0x0EBB, 0x0EBC, 1}, 294 | {0x0EC6, 0x0EC6, 1}, 295 | {0x0EC8, 0x0ECD, 1}, 296 | {0x0ED0, 0x0ED9, 1}, 297 | {0x0F18, 0x0F19, 1}, 298 | {0x0F20, 0x0F29, 1}, 299 | {0x0F35, 0x0F39, 2}, 300 | {0x0F3E, 0x0F3F, 1}, 301 | {0x0F71, 0x0F84, 1}, 302 | {0x0F86, 0x0F8B, 1}, 303 | {0x0F90, 0x0F95, 1}, 304 | {0x0F97, 0x0F97, 1}, 305 | {0x0F99, 0x0FAD, 1}, 306 | {0x0FB1, 0x0FB7, 1}, 307 | {0x0FB9, 0x0FB9, 1}, 308 | {0x20D0, 0x20DC, 1}, 309 | {0x20E1, 0x3005, 0x3005 - 0x20E1}, 310 | {0x302A, 0x302F, 1}, 311 | {0x3031, 0x3035, 1}, 312 | {0x3099, 0x309A, 1}, 313 | {0x309D, 0x309E, 1}, 314 | {0x30FC, 0x30FE, 1}, 315 | }, 316 | } 317 | -------------------------------------------------------------------------------- /fns_test.go: -------------------------------------------------------------------------------- 1 | package goxpath 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/ChrisTrenkamp/goxpath/tree" 10 | "github.com/ChrisTrenkamp/goxpath/tree/xmltree" 11 | ) 12 | 13 | func TestStrLit(t *testing.T) { 14 | p := `'strlit'` 15 | x := `` 16 | exp := "strlit" 17 | execVal(p, x, exp, nil, t) 18 | } 19 | 20 | func TestNumLit(t *testing.T) { 21 | p := `123` 22 | x := `` 23 | exp := "123" 24 | execVal(p, x, exp, nil, t) 25 | p = `123.456` 26 | exp = "123.456" 27 | execVal(p, x, exp, nil, t) 28 | } 29 | 30 | func TestLast(t *testing.T) { 31 | p := `/p1/*[last()]` 32 | x := `` 33 | exp := []string{""} 34 | execPath(p, x, exp, nil, t) 35 | p = `/p1/p5[last()]` 36 | exp = []string{} 37 | execPath(p, x, exp, nil, t) 38 | p = `/p1[last()]` 39 | exp = []string{""} 40 | execPath(p, x, exp, nil, t) 41 | } 42 | 43 | func TestCount(t *testing.T) { 44 | p := `count(//p1)` 45 | x := `` 46 | exp := "2" 47 | execVal(p, x, exp, nil, t) 48 | } 49 | 50 | func TestCount2(t *testing.T) { 51 | x := ` 52 | 53 | 54 | 55 | 56 | y31 57 | y32 58 | 59 | 60 | 61 | 62 | 63 | y21 64 | y22 65 | 66 | 67 | 68 | y11 69 | y12 70 | 71 | 72 | y03 73 | y04 74 | 75 | 76 | ` 77 | execVal(`count(//x)`, x, "7", nil, t) 78 | execVal(`count(//x[1])`, x, "4", nil, t) 79 | execVal(`count(//x/y)`, x, "8", nil, t) 80 | execVal(`count(//x/y[1])`, x, "4", nil, t) 81 | execVal(`count(//x[1]/y[1])`, x, "2", nil, t) 82 | } 83 | 84 | func TestNames(t *testing.T) { 85 | x := `` 86 | testMap := make(map[string]map[string]string) 87 | testMap["/*"] = make(map[string]string) 88 | testMap["/*"]["local-name"] = "test" 89 | testMap["/*"]["namespace-uri"] = "http://foo.com" 90 | testMap["/*"]["name"] = "{http://foo.com}test" 91 | 92 | testMap["/none"] = make(map[string]string) 93 | testMap["/none"]["local-name"] = "" 94 | testMap["/none"]["namespace-uri"] = "" 95 | testMap["/none"]["name"] = "" 96 | 97 | testMap["/*/@*:attr"] = make(map[string]string) 98 | testMap["/*/@*:attr"]["local-name"] = "attr" 99 | testMap["/*/@*:attr"]["namespace-uri"] = "http://bar.com" 100 | testMap["/*/@*:attr"]["name"] = "{http://bar.com}attr" 101 | 102 | testMap["//processing-instruction()"] = make(map[string]string) 103 | testMap["//processing-instruction()"]["local-name"] = "pi" 104 | testMap["//processing-instruction()"]["namespace-uri"] = "" 105 | testMap["//processing-instruction()"]["name"] = "pi" 106 | 107 | testMap["//comment()"] = make(map[string]string) 108 | testMap["//comment()"]["local-name"] = "" 109 | testMap["//comment()"]["namespace-uri"] = "" 110 | testMap["//comment()"]["name"] = "" 111 | 112 | for path, i := range testMap { 113 | for nt, res := range i { 114 | p := fmt.Sprintf("%s(%s)", nt, path) 115 | exp := res 116 | execVal(p, x, exp, nil, t) 117 | } 118 | } 119 | 120 | x = `` 121 | execPath("/*[local-name() = 'test']", x, []string{``}, nil, t) 122 | execPath("/*[namespace-uri() = 'http://foo.com']", x, []string{``}, nil, t) 123 | execPath("/*[name() = '{http://foo.com}test']", x, []string{``}, nil, t) 124 | } 125 | 126 | func TestBoolean(t *testing.T) { 127 | x := `` 128 | execVal(`true()`, x, "true", nil, t) 129 | execVal(`false()`, x, "false", nil, t) 130 | p := `boolean(/p1/p2)` 131 | exp := "true" 132 | execVal(p, x, exp, nil, t) 133 | p = `boolean(/p1/p5)` 134 | exp = "false" 135 | execVal(p, x, exp, nil, t) 136 | p = `boolean('123')` 137 | exp = "true" 138 | execVal(p, x, exp, nil, t) 139 | p = `boolean(123)` 140 | exp = "true" 141 | execVal(p, x, exp, nil, t) 142 | p = `boolean('')` 143 | exp = "false" 144 | execVal(p, x, exp, nil, t) 145 | p = `boolean(0)` 146 | exp = "false" 147 | execVal(p, x, exp, nil, t) 148 | } 149 | 150 | func TestNot(t *testing.T) { 151 | x := `` 152 | execVal(`not(false())`, x, "true", nil, t) 153 | execVal(`not(true())`, x, "false", nil, t) 154 | } 155 | 156 | func TestConversions(t *testing.T) { 157 | x := `foo` 158 | execVal(`number(true())`, x, "1", nil, t) 159 | execVal(`number(false())`, x, "0", nil, t) 160 | execVal(`string(/p2)`, x, "", nil, t) 161 | execVal(`/p1[string() = 'foo']`, x, "foo", nil, t) 162 | execVal(`number('abc')`, x, "NaN", nil, t) 163 | } 164 | 165 | func TestLang(t *testing.T) { 166 | x := ` 167 | 168 |

I went up a floor.

169 |

I took the lift.

170 |

I rode the elevator.

171 |
` 172 | execVal(`count(//p[lang('en')])`, x, "3", nil, t) 173 | execVal(`count(//text()[lang('en-GB')])`, x, "1", nil, t) 174 | execVal(`count(//p[lang('en-US')])`, x, "1", nil, t) 175 | execVal(`count(//p[lang('de')])`, x, "0", nil, t) 176 | execVal(`count(/p1[lang('en')])`, x, "0", nil, t) 177 | } 178 | 179 | func TestString(t *testing.T) { 180 | x := `text` 181 | execVal(`string(2 + 2)`, x, "4", nil, t) 182 | execVal(`string(/p1)`, x, "text", nil, t) 183 | } 184 | 185 | func TestConcat(t *testing.T) { 186 | x := `` 187 | execVal(`concat('abc', 'def', 'hij', '123')`, x, "abcdefhij123", nil, t) 188 | } 189 | 190 | func TestStartsWith(t *testing.T) { 191 | x := `` 192 | execVal(`starts-with('abcd', 'ab')`, x, "true", nil, t) 193 | execVal(`starts-with('abcd', 'abd')`, x, "false", nil, t) 194 | } 195 | 196 | func TestContains(t *testing.T) { 197 | x := `` 198 | execVal(`contains('abcd', 'bcd')`, x, "true", nil, t) 199 | execVal(`contains('abcd', 'bd')`, x, "false", nil, t) 200 | } 201 | 202 | func TestSubstrBefore(t *testing.T) { 203 | x := `` 204 | execVal(`substring-before("1999/04/01","/")`, x, "1999", nil, t) 205 | execVal(`substring-before("1999/04/01","2")`, x, "", nil, t) 206 | } 207 | 208 | func TestSubstrAfter(t *testing.T) { 209 | x := `` 210 | execVal(`substring-after("1999/04/01","/")`, x, "04/01", nil, t) 211 | execVal(`substring-after("1999/04/01","19")`, x, "99/04/01", nil, t) 212 | execVal(`substring-after("1999/04/01","a")`, x, "", nil, t) 213 | } 214 | 215 | func TestSubstring(t *testing.T) { 216 | x := `` 217 | execVal(`substring("12345", 2, 3)`, x, "234", nil, t) 218 | execVal(`substring("12345", 2)`, x, "2345", nil, t) 219 | execVal(`substring('abcd', -2, 5)`, x, "ab", nil, t) 220 | execVal(`substring('abcd', 0)`, x, "abcd", nil, t) 221 | execVal(`substring('abcd', 1, 4)`, x, "abcd", nil, t) 222 | execVal(`substring("12345", 1.5, 2.6)`, x, "234", nil, t) 223 | execVal(`substring("12345", 0 div 0, 3)`, x, "", nil, t) 224 | execVal(`substring("12345", 1, 0 div 0)`, x, "", nil, t) 225 | execVal(`substring("12345", -42, 1 div 0)`, x, "12345", nil, t) 226 | execVal(`substring("12345", -1 div 0, 1 div 0)`, x, "", nil, t) 227 | } 228 | 229 | func TestStrLength(t *testing.T) { 230 | x := `abc` 231 | execVal(`string-length('def')`, x, "3", nil, t) 232 | execVal(`/p1[string-length() = 3]`, x, "abc", nil, t) 233 | } 234 | 235 | func TestNormalizeSpace(t *testing.T) { 236 | x := ` 237 | a b 238 | ` 239 | execVal(`normalize-space(/p1)`, x, "a b", nil, t) 240 | execVal(`/p1[normalize-space(/p1) = 'a b']`, x, ` 241 | a b 242 | `, nil, t) 243 | execVal(`/p1[normalize-space() = 'a b']`, x, ` 244 | a b 245 | `, nil, t) 246 | } 247 | 248 | func TestTranslate(t *testing.T) { 249 | x := `` 250 | execVal(`translate("bar","abc","ABC")`, x, "BAr", nil, t) 251 | execVal(`translate("--aaa--","abc-","ABC")`, x, "AAA", nil, t) 252 | } 253 | 254 | func TestPathAfterFunction(t *testing.T) { 255 | x := xmltree.MustParseXML(bytes.NewBufferString(` 256 | 257 | aabb 258 | cc 259 | 260 | `)) 261 | 262 | current, err := MustParse(`/root/a`).ExecNode(x) 263 | if err != nil { 264 | t.Error("err not nil") 265 | } 266 | 267 | currentFn := func(c tree.Ctx, args ...tree.Result) (tree.Result, error) { 268 | return current, nil 269 | } 270 | custFns := map[xml.Name]tree.Wrap{ 271 | {Local: "current"}: {Fn: currentFn}, 272 | } 273 | opts := func(o *Opts) { o.Funcs = custFns } 274 | 275 | result := MustParse(`/root/c/@a[. = current()/b]`).MustExec(x, opts) 276 | if result.String() != `bb` { 277 | t.Error("result not foo:", result.String()) 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /lexer/lexer.go: -------------------------------------------------------------------------------- 1 | package lexer 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "unicode" 7 | "unicode/utf8" 8 | ) 9 | 10 | const ( 11 | //XItemError is an error with the parser input 12 | XItemError XItemType = "Error" 13 | //XItemAbsLocPath is an absolute path 14 | XItemAbsLocPath = "Absolute path" 15 | //XItemAbbrAbsLocPath represents an abbreviated absolute path 16 | XItemAbbrAbsLocPath = "Abbreviated absolute path" 17 | //XItemAbbrRelLocPath marks the start of a path expression 18 | XItemAbbrRelLocPath = "Abbreviated relative path" 19 | //XItemRelLocPath represents a relative location path 20 | XItemRelLocPath = "Relative path" 21 | //XItemEndPath marks the end of a path 22 | XItemEndPath = "End path instruction" 23 | //XItemAxis marks an axis specifier of a path 24 | XItemAxis = "Axis" 25 | //XItemAbbrAxis marks an abbreviated axis specifier (just @ at this point) 26 | XItemAbbrAxis = "Abbreviated attribute axis" 27 | //XItemNCName marks a namespace name in a node test 28 | XItemNCName = "Namespace" 29 | //XItemQName marks the local name in an a node test 30 | XItemQName = "Local name" 31 | //XItemNodeType marks a node type in a node test 32 | XItemNodeType = "Node type" 33 | //XItemProcLit marks a processing-instruction literal 34 | XItemProcLit = "processing-instruction" 35 | //XItemFunction marks a function call 36 | XItemFunction = "function" 37 | //XItemArgument marks a function argument 38 | XItemArgument = "function argument" 39 | //XItemEndFunction marks the end of a function 40 | XItemEndFunction = "end of function" 41 | //XItemPredicate marks a predicate in an axis 42 | XItemPredicate = "predicate" 43 | //XItemEndPredicate marks a predicate in an axis 44 | XItemEndPredicate = "end of predicate" 45 | //XItemStrLit marks a string literal 46 | XItemStrLit = "string literal" 47 | //XItemNumLit marks a numeric literal 48 | XItemNumLit = "numeric literal" 49 | //XItemOperator marks an operator 50 | XItemOperator = "operator" 51 | //XItemVariable marks a variable reference 52 | XItemVariable = "variable" 53 | ) 54 | 55 | const ( 56 | eof = -(iota + 1) 57 | ) 58 | 59 | //XItemType is the parser token types 60 | type XItemType string 61 | 62 | //XItem is the token emitted from the parser 63 | type XItem struct { 64 | Typ XItemType 65 | Val string 66 | } 67 | 68 | type stateFn func(*Lexer) stateFn 69 | 70 | //Lexer lexes out XPath expressions 71 | type Lexer struct { 72 | input string 73 | start int 74 | pos int 75 | width int 76 | items chan XItem 77 | } 78 | 79 | //Lex an XPath expresion on the io.Reader 80 | func Lex(xpath string) chan XItem { 81 | l := &Lexer{ 82 | input: xpath, 83 | items: make(chan XItem), 84 | } 85 | go l.run() 86 | return l.items 87 | } 88 | 89 | func (l *Lexer) run() { 90 | for state := startState; state != nil; { 91 | state = state(l) 92 | } 93 | 94 | if l.peek() != eof { 95 | l.errorf("Malformed XPath expression") 96 | } 97 | 98 | close(l.items) 99 | } 100 | 101 | func (l *Lexer) emit(t XItemType) { 102 | l.items <- XItem{t, l.input[l.start:l.pos]} 103 | l.start = l.pos 104 | } 105 | 106 | func (l *Lexer) emitVal(t XItemType, val string) { 107 | l.items <- XItem{t, val} 108 | l.start = l.pos 109 | } 110 | 111 | func (l *Lexer) next() (r rune) { 112 | if l.pos >= len(l.input) { 113 | l.width = 0 114 | return eof 115 | } 116 | 117 | r, l.width = utf8.DecodeRuneInString(l.input[l.pos:]) 118 | 119 | l.pos += l.width 120 | 121 | return r 122 | } 123 | 124 | func (l *Lexer) ignore() { 125 | l.start = l.pos 126 | } 127 | 128 | func (l *Lexer) backup() { 129 | l.pos -= l.width 130 | } 131 | 132 | func (l *Lexer) peek() rune { 133 | r := l.next() 134 | 135 | l.backup() 136 | return r 137 | } 138 | 139 | func (l *Lexer) peekAt(n int) rune { 140 | if n <= 1 { 141 | return l.peek() 142 | } 143 | 144 | width := 0 145 | var ret rune 146 | 147 | for count := 0; count < n; count++ { 148 | r, s := utf8.DecodeRuneInString(l.input[l.pos+width:]) 149 | width += s 150 | 151 | if l.pos+width > len(l.input) { 152 | return eof 153 | } 154 | 155 | ret = r 156 | } 157 | 158 | return ret 159 | } 160 | 161 | func (l *Lexer) accept(valid string) bool { 162 | if strings.ContainsRune(valid, l.next()) { 163 | return true 164 | } 165 | 166 | l.backup() 167 | return false 168 | } 169 | 170 | func (l *Lexer) acceptRun(valid string) { 171 | for strings.ContainsRune(valid, l.next()) { 172 | } 173 | l.backup() 174 | } 175 | 176 | func (l *Lexer) skip(num int) { 177 | for i := 0; i < num; i++ { 178 | l.next() 179 | } 180 | l.ignore() 181 | } 182 | 183 | func (l *Lexer) skipWS(ig bool) { 184 | for { 185 | n := l.next() 186 | 187 | if n == eof || !unicode.IsSpace(n) { 188 | break 189 | } 190 | } 191 | 192 | l.backup() 193 | 194 | if ig { 195 | l.ignore() 196 | } 197 | } 198 | 199 | func (l *Lexer) errorf(format string, args ...interface{}) stateFn { 200 | l.items <- XItem{ 201 | XItemError, 202 | fmt.Sprintf(format, args...), 203 | } 204 | 205 | return nil 206 | } 207 | 208 | func isElemChar(r rune) bool { 209 | return string(r) != ":" && string(r) != "/" && 210 | (unicode.Is(first, r) || unicode.Is(second, r) || string(r) == "*") && 211 | r != eof 212 | } 213 | 214 | func startState(l *Lexer) stateFn { 215 | l.skipWS(true) 216 | 217 | if string(l.peek()) == "/" { 218 | l.next() 219 | l.ignore() 220 | 221 | if string(l.next()) == "/" { 222 | l.ignore() 223 | return abbrAbsLocPathState 224 | } 225 | 226 | l.backup() 227 | return absLocPathState 228 | } else if string(l.peek()) == `'` || string(l.peek()) == `"` { 229 | if err := getStrLit(l, XItemStrLit); err != nil { 230 | return l.errorf(err.Error()) 231 | } 232 | 233 | if l.peek() != eof { 234 | return startState 235 | } 236 | } else if getNumLit(l) { 237 | l.skipWS(true) 238 | if l.peek() != eof { 239 | return startState 240 | } 241 | } else if string(l.peek()) == "$" { 242 | l.next() 243 | l.ignore() 244 | r := l.peek() 245 | for unicode.Is(first, r) || unicode.Is(second, r) { 246 | l.next() 247 | r = l.peek() 248 | } 249 | tok := l.input[l.start:l.pos] 250 | if len(tok) == 0 { 251 | return l.errorf("Empty variable name") 252 | } 253 | l.emit(XItemVariable) 254 | l.skipWS(true) 255 | if l.peek() != eof { 256 | return startState 257 | } 258 | } else if st := findOperatorState(l); st != nil { 259 | return st 260 | } else { 261 | if isElemChar(l.peek()) { 262 | colons := 0 263 | 264 | for { 265 | if isElemChar(l.peek()) { 266 | l.next() 267 | } else if string(l.peek()) == ":" { 268 | l.next() 269 | colons++ 270 | } else { 271 | break 272 | } 273 | } 274 | 275 | if string(l.peek()) == "(" && colons <= 1 { 276 | tok := l.input[l.start:l.pos] 277 | err := procFunc(l, tok) 278 | if err != nil { 279 | return l.errorf(err.Error()) 280 | } 281 | 282 | l.skipWS(true) 283 | 284 | if string(l.peek()) == "/" { 285 | l.next() 286 | l.ignore() 287 | 288 | if string(l.next()) == "/" { 289 | l.ignore() 290 | return abbrRelLocPathState 291 | } 292 | 293 | l.backup() 294 | return relLocPathState 295 | } 296 | 297 | return startState 298 | } 299 | 300 | l.pos = l.start 301 | return relLocPathState 302 | } else if string(l.peek()) == "@" { 303 | return relLocPathState 304 | } 305 | } 306 | 307 | return nil 308 | } 309 | 310 | func strPeek(str string, l *Lexer) bool { 311 | for i := 0; i < len(str); i++ { 312 | if string(l.peekAt(i+1)) != string(str[i]) { 313 | return false 314 | } 315 | } 316 | return true 317 | } 318 | 319 | func findOperatorState(l *Lexer) stateFn { 320 | l.skipWS(true) 321 | 322 | switch string(l.peek()) { 323 | case ">", "<", "!": 324 | l.next() 325 | if string(l.peek()) == "=" { 326 | l.next() 327 | } 328 | l.emit(XItemOperator) 329 | return startState 330 | case "|", "+", "-", "*", "=": 331 | l.next() 332 | l.emit(XItemOperator) 333 | return startState 334 | case "(": 335 | l.next() 336 | l.emit(XItemOperator) 337 | for state := startState; state != nil; { 338 | state = state(l) 339 | } 340 | l.skipWS(true) 341 | if string(l.next()) != ")" { 342 | return l.errorf("Missing end )") 343 | } 344 | l.emit(XItemOperator) 345 | return startState 346 | } 347 | 348 | if strPeek("and", l) { 349 | l.next() 350 | l.next() 351 | l.next() 352 | l.emit(XItemOperator) 353 | return startState 354 | } 355 | 356 | if strPeek("or", l) { 357 | l.next() 358 | l.next() 359 | l.emit(XItemOperator) 360 | return startState 361 | } 362 | 363 | if strPeek("mod", l) { 364 | l.next() 365 | l.next() 366 | l.next() 367 | l.emit(XItemOperator) 368 | return startState 369 | } 370 | 371 | if strPeek("div", l) { 372 | l.next() 373 | l.next() 374 | l.next() 375 | l.emit(XItemOperator) 376 | return startState 377 | } 378 | 379 | return nil 380 | } 381 | 382 | func getStrLit(l *Lexer, tok XItemType) error { 383 | q := l.next() 384 | var r rune 385 | 386 | l.ignore() 387 | 388 | for r != q { 389 | r = l.next() 390 | if r == eof { 391 | return fmt.Errorf("Unexpected end of string literal.") 392 | } 393 | } 394 | 395 | l.backup() 396 | l.emit(tok) 397 | l.next() 398 | l.ignore() 399 | 400 | return nil 401 | } 402 | 403 | func getNumLit(l *Lexer) bool { 404 | const dig = "0123456789" 405 | l.accept("-") 406 | start := l.pos 407 | l.acceptRun(dig) 408 | 409 | if l.pos == start { 410 | return false 411 | } 412 | 413 | if l.accept(".") { 414 | l.acceptRun(dig) 415 | } 416 | 417 | l.emit(XItemNumLit) 418 | return true 419 | } 420 | -------------------------------------------------------------------------------- /internal/execxp/paths.go: -------------------------------------------------------------------------------- 1 | package execxp 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/ChrisTrenkamp/goxpath/internal/execxp/findutil" 10 | "github.com/ChrisTrenkamp/goxpath/internal/execxp/intfns" 11 | "github.com/ChrisTrenkamp/goxpath/internal/xsort" 12 | "github.com/ChrisTrenkamp/goxpath/lexer" 13 | "github.com/ChrisTrenkamp/goxpath/parser" 14 | "github.com/ChrisTrenkamp/goxpath/parser/pathexpr" 15 | "github.com/ChrisTrenkamp/goxpath/tree" 16 | "github.com/ChrisTrenkamp/goxpath/xconst" 17 | ) 18 | 19 | type xpFilt struct { 20 | t tree.Node 21 | ctx tree.Result 22 | expr pathexpr.PathExpr 23 | ns map[string]string 24 | ctxPos int 25 | ctxSize int 26 | proxPos map[int]int 27 | fns map[xml.Name]tree.Wrap 28 | variables map[string]tree.Result 29 | } 30 | 31 | type xpExecFn func(*xpFilt, string) 32 | 33 | var xpFns = map[lexer.XItemType]xpExecFn{ 34 | lexer.XItemAbsLocPath: xfAbsLocPath, 35 | lexer.XItemAbbrAbsLocPath: xfAbbrAbsLocPath, 36 | lexer.XItemRelLocPath: xfRelLocPath, 37 | lexer.XItemAbbrRelLocPath: xfAbbrRelLocPath, 38 | lexer.XItemAxis: xfAxis, 39 | lexer.XItemAbbrAxis: xfAbbrAxis, 40 | lexer.XItemNCName: xfNCName, 41 | lexer.XItemQName: xfQName, 42 | lexer.XItemNodeType: xfNodeType, 43 | lexer.XItemProcLit: xfProcInstLit, 44 | lexer.XItemStrLit: xfStrLit, 45 | lexer.XItemNumLit: xfNumLit, 46 | } 47 | 48 | func xfExec(f *xpFilt, n *parser.Node) (err error) { 49 | for n != nil { 50 | if fn, ok := xpFns[n.Val.Typ]; ok { 51 | fn(f, n.Val.Val) 52 | n = n.Left 53 | } else if n.Val.Typ == lexer.XItemPredicate { 54 | if err = xfPredicate(f, n.Left); err != nil { 55 | return 56 | } 57 | 58 | n = n.Right 59 | } else if n.Val.Typ == lexer.XItemFunction { 60 | if err = xfFunction(f, n); err != nil { 61 | return 62 | } 63 | 64 | n = n.Right 65 | } else if n.Val.Typ == lexer.XItemOperator { 66 | lf := xpFilt{ 67 | t: f.t, 68 | ns: f.ns, 69 | ctx: f.ctx, 70 | ctxPos: f.ctxPos, 71 | ctxSize: f.ctxSize, 72 | proxPos: f.proxPos, 73 | fns: f.fns, 74 | variables: f.variables, 75 | } 76 | left, err := exec(&lf, n.Left) 77 | if err != nil { 78 | return err 79 | } 80 | 81 | rf := xpFilt{ 82 | t: f.t, 83 | ns: f.ns, 84 | ctx: f.ctx, 85 | fns: f.fns, 86 | variables: f.variables, 87 | } 88 | right, err := exec(&rf, n.Right) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | return xfOperator(left, right, f, n.Val.Val) 94 | } else if n.Val.Typ == lexer.XItemVariable { 95 | if res, ok := f.variables[n.Val.Val]; ok { 96 | f.ctx = res 97 | return nil 98 | } 99 | return fmt.Errorf("Invalid variable '%s'", n.Val.Val) 100 | } else if string(n.Val.Typ) == "" { 101 | n = n.Left 102 | //} else { 103 | // return fmt.Errorf("Cannot process " + string(n.Val.Typ)) 104 | } 105 | } 106 | 107 | return 108 | } 109 | 110 | func xfPredicate(f *xpFilt, n *parser.Node) (err error) { 111 | res := f.ctx.(tree.NodeSet) 112 | newRes := make(tree.NodeSet, 0, len(res)) 113 | 114 | for i := range res { 115 | pf := xpFilt{ 116 | t: f.t, 117 | ns: f.ns, 118 | ctxPos: i, 119 | ctxSize: f.ctxSize, 120 | ctx: tree.NodeSet{res[i]}, 121 | fns: f.fns, 122 | variables: f.variables, 123 | } 124 | 125 | predRes, err := exec(&pf, n) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | ok, err := checkPredRes(predRes, f, res[i]) 131 | if err != nil { 132 | return err 133 | } 134 | 135 | if ok { 136 | newRes = append(newRes, res[i]) 137 | } 138 | } 139 | 140 | f.proxPos = make(map[int]int) 141 | for pos, j := range newRes { 142 | f.proxPos[j.Pos()] = pos + 1 143 | } 144 | 145 | f.ctx = newRes 146 | f.ctxSize = len(newRes) 147 | 148 | return 149 | } 150 | 151 | func checkPredRes(ret tree.Result, f *xpFilt, node tree.Node) (bool, error) { 152 | if num, ok := ret.(tree.Num); ok { 153 | if float64(f.proxPos[node.Pos()]) == float64(num) { 154 | return true, nil 155 | } 156 | return false, nil 157 | } 158 | 159 | if b, ok := ret.(tree.IsBool); ok { 160 | return bool(b.Bool()), nil 161 | } 162 | 163 | return false, fmt.Errorf("Cannot convert argument to boolean") 164 | } 165 | 166 | func xfFunction(f *xpFilt, n *parser.Node) error { 167 | spl := strings.Split(n.Val.Val, ":") 168 | var name xml.Name 169 | if len(spl) == 1 { 170 | name.Local = spl[0] 171 | } else { 172 | name.Space = f.ns[spl[0]] 173 | name.Local = spl[1] 174 | } 175 | fn, ok := intfns.BuiltIn[name] 176 | if !ok { 177 | fn, ok = f.fns[name] 178 | } 179 | 180 | if ok { 181 | args := []tree.Result{} 182 | param := n.Left 183 | 184 | for param != nil { 185 | pf := xpFilt{ 186 | t: f.t, 187 | ctx: f.ctx, 188 | ns: f.ns, 189 | ctxPos: f.ctxPos, 190 | ctxSize: f.ctxSize, 191 | fns: f.fns, 192 | variables: f.variables, 193 | } 194 | res, err := exec(&pf, param.Left) 195 | if err != nil { 196 | return err 197 | } 198 | 199 | args = append(args, res) 200 | param = param.Right 201 | } 202 | 203 | filt, err := fn.Call(tree.Ctx{NodeSet: f.ctx.(tree.NodeSet), Size: f.ctxSize, Pos: f.ctxPos + 1}, args...) 204 | f.ctx = filt 205 | return err 206 | } 207 | 208 | return fmt.Errorf("Unknown function: %s", n.Val.Val) 209 | } 210 | 211 | var eqOps = map[string]bool{ 212 | "=": true, 213 | "!=": true, 214 | } 215 | 216 | var booleanOps = map[string]bool{ 217 | "=": true, 218 | "!=": true, 219 | "<": true, 220 | "<=": true, 221 | ">": true, 222 | ">=": true, 223 | } 224 | 225 | var numOps = map[string]bool{ 226 | "*": true, 227 | "div": true, 228 | "mod": true, 229 | "+": true, 230 | "-": true, 231 | "=": true, 232 | "!=": true, 233 | "<": true, 234 | "<=": true, 235 | ">": true, 236 | ">=": true, 237 | } 238 | 239 | var andOrOps = map[string]bool{ 240 | "and": true, 241 | "or": true, 242 | } 243 | 244 | func xfOperator(left, right tree.Result, f *xpFilt, op string) error { 245 | if booleanOps[op] { 246 | lNode, lOK := left.(tree.NodeSet) 247 | rNode, rOK := right.(tree.NodeSet) 248 | if lOK && rOK { 249 | return bothNodeOperator(lNode, rNode, f, op) 250 | } 251 | 252 | if lOK { 253 | return leftNodeOperator(lNode, right, f, op) 254 | } 255 | 256 | if rOK { 257 | return rightNodeOperator(left, rNode, f, op) 258 | } 259 | 260 | if eqOps[op] { 261 | return equalsOperator(left, right, f, op) 262 | } 263 | } 264 | 265 | if numOps[op] { 266 | return numberOperator(left, right, f, op) 267 | } 268 | 269 | if andOrOps[op] { 270 | return andOrOperator(left, right, f, op) 271 | } 272 | 273 | //if op == "|" { 274 | return unionOperator(left, right, f, op) 275 | //} 276 | 277 | //return fmt.Errorf("Unknown operator " + op) 278 | } 279 | 280 | func xfAbsLocPath(f *xpFilt, val string) { 281 | i := f.t 282 | for i.GetNodeType() != tree.NtRoot { 283 | i = i.GetParent() 284 | } 285 | f.ctx = tree.NodeSet{i} 286 | } 287 | 288 | func xfAbbrAbsLocPath(f *xpFilt, val string) { 289 | i := f.t 290 | for i.GetNodeType() != tree.NtRoot { 291 | i = i.GetParent() 292 | } 293 | f.ctx = tree.NodeSet{i} 294 | f.expr = abbrPathExpr() 295 | find(f) 296 | } 297 | 298 | func xfRelLocPath(f *xpFilt, val string) { 299 | } 300 | 301 | func xfAbbrRelLocPath(f *xpFilt, val string) { 302 | f.expr = abbrPathExpr() 303 | find(f) 304 | } 305 | 306 | func xfAxis(f *xpFilt, val string) { 307 | f.expr.Axis = val 308 | } 309 | 310 | func xfAbbrAxis(f *xpFilt, val string) { 311 | f.expr.Axis = xconst.AxisAttribute 312 | } 313 | 314 | func xfNCName(f *xpFilt, val string) { 315 | f.expr.Name.Space = val 316 | } 317 | 318 | func xfQName(f *xpFilt, val string) { 319 | f.expr.Name.Local = val 320 | find(f) 321 | } 322 | 323 | func xfNodeType(f *xpFilt, val string) { 324 | f.expr.NodeType = val 325 | find(f) 326 | } 327 | 328 | func xfProcInstLit(f *xpFilt, val string) { 329 | filt := tree.NodeSet{} 330 | for _, i := range f.ctx.(tree.NodeSet) { 331 | if i.GetToken().(xml.ProcInst).Target == val { 332 | filt = append(filt, i) 333 | } 334 | } 335 | f.ctx = filt 336 | } 337 | 338 | func xfStrLit(f *xpFilt, val string) { 339 | f.ctx = tree.String(val) 340 | } 341 | 342 | func xfNumLit(f *xpFilt, val string) { 343 | num, _ := strconv.ParseFloat(val, 64) 344 | f.ctx = tree.Num(num) 345 | } 346 | 347 | func abbrPathExpr() pathexpr.PathExpr { 348 | return pathexpr.PathExpr{ 349 | Name: xml.Name{}, 350 | Axis: xconst.AxisDescendentOrSelf, 351 | NodeType: xconst.NodeTypeNode, 352 | } 353 | } 354 | 355 | func find(f *xpFilt) { 356 | dupFilt := make(map[int]tree.Node) 357 | f.proxPos = make(map[int]int) 358 | 359 | if f.expr.Axis == "" && f.expr.NodeType == "" && f.expr.Name.Space == "" { 360 | if f.expr.Name.Local == "." { 361 | f.expr = pathexpr.PathExpr{ 362 | Name: xml.Name{}, 363 | Axis: xconst.AxisSelf, 364 | NodeType: xconst.NodeTypeNode, 365 | } 366 | } 367 | 368 | if f.expr.Name.Local == ".." { 369 | f.expr = pathexpr.PathExpr{ 370 | Name: xml.Name{}, 371 | Axis: xconst.AxisParent, 372 | NodeType: xconst.NodeTypeNode, 373 | } 374 | } 375 | } 376 | 377 | f.expr.NS = f.ns 378 | 379 | for _, i := range f.ctx.(tree.NodeSet) { 380 | for pos, j := range findutil.Find(i, f.expr) { 381 | dupFilt[j.Pos()] = j 382 | f.proxPos[j.Pos()] = pos + 1 383 | } 384 | } 385 | 386 | res := make(tree.NodeSet, 0, len(dupFilt)) 387 | for _, i := range dupFilt { 388 | res = append(res, i) 389 | } 390 | 391 | xsort.SortNodes(res) 392 | 393 | f.expr = pathexpr.PathExpr{} 394 | f.ctxSize = len(res) 395 | f.ctx = res 396 | } 397 | -------------------------------------------------------------------------------- /path_test.go: -------------------------------------------------------------------------------- 1 | package goxpath 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "runtime/debug" 7 | "testing" 8 | 9 | "github.com/ChrisTrenkamp/goxpath/tree" 10 | "github.com/ChrisTrenkamp/goxpath/tree/xmltree" 11 | ) 12 | 13 | func execPath(xp, x string, exp []string, ns map[string]string, t *testing.T) { 14 | defer func() { 15 | if r := recover(); r != nil { 16 | t.Error("Panicked: from XPath expr: '" + xp) 17 | t.Error(r) 18 | t.Error(string(debug.Stack())) 19 | } 20 | }() 21 | res := MustParse(xp).MustExec(xmltree.MustParseXML(bytes.NewBufferString(x)), func(o *Opts) { o.NS = ns }).(tree.NodeSet) 22 | 23 | if len(res) != len(exp) { 24 | t.Error("Result length not valid in XPath expression '"+xp+"':", len(res), ", expecting", len(exp)) 25 | for i := range res { 26 | t.Error(MarshalStr(res[i].(tree.Node))) 27 | } 28 | return 29 | } 30 | 31 | for i := range res { 32 | r, err := MarshalStr(res[i].(tree.Node)) 33 | if err != nil { 34 | t.Error(err.Error()) 35 | return 36 | } 37 | valid := false 38 | for j := range exp { 39 | if r == exp[j] { 40 | valid = true 41 | } 42 | } 43 | if !valid { 44 | t.Error("Incorrect result in XPath expression '" + xp + "':" + r) 45 | t.Error("Expecting one of:") 46 | for j := range exp { 47 | t.Error(exp[j]) 48 | } 49 | return 50 | } 51 | } 52 | } 53 | 54 | func TestRoot(t *testing.T) { 55 | p := `/` 56 | x := `` 57 | exp := []string{""} 58 | execPath(p, x, exp, nil, t) 59 | } 60 | 61 | func TestRoot2(t *testing.T) { 62 | x := xmltree.MustParseXML(bytes.NewBufferString(``)) 63 | x = MustParse("/test/path").MustExec(x).(tree.NodeSet)[0] 64 | for _, i := range []string{"/", "//test"} { 65 | res, err := MarshalStr(MustParse(i).MustExec(x).(tree.NodeSet)[0]) 66 | if err != nil { 67 | t.Error(err) 68 | } 69 | if res != "" { 70 | t.Error("Incorrect result", res) 71 | } 72 | } 73 | } 74 | 75 | func TestAbsPath(t *testing.T) { 76 | p := ` / test / path` 77 | x := `` 78 | exp := []string{""} 79 | execPath(p, x, exp, nil, t) 80 | } 81 | 82 | func TestNonAbsPath(t *testing.T) { 83 | p := ` test ` 84 | x := `` 85 | exp := []string{""} 86 | execPath(p, x, exp, nil, t) 87 | } 88 | 89 | func TestRelPath(t *testing.T) { 90 | p := ` // path ` 91 | x := `` 92 | exp := []string{""} 93 | execPath(p, x, exp, nil, t) 94 | } 95 | 96 | func TestRelPath2(t *testing.T) { 97 | p := ` // path ` 98 | x := `test` 99 | exp := []string{"test", `test`} 100 | execPath(p, x, exp, nil, t) 101 | } 102 | 103 | func TestRelNonRootPath(t *testing.T) { 104 | p := ` / test // path ` 105 | x := `` 106 | exp := []string{""} 107 | execPath(p, x, exp, nil, t) 108 | } 109 | 110 | func TestParent(t *testing.T) { 111 | p := `/test/path/ parent:: test` 112 | x := `` 113 | exp := []string{""} 114 | execPath(p, x, exp, nil, t) 115 | } 116 | 117 | func TestAncestor(t *testing.T) { 118 | p := `/p1/p2/p3/p1/ancestor::p1` 119 | x := `` 120 | exp := []string{``} 121 | execPath(p, x, exp, nil, t) 122 | } 123 | 124 | func TestAncestorOrSelf(t *testing.T) { 125 | p := `/p1/p2/p3/p1/ancestor-or-self::p1` 126 | x := `` 127 | exp := []string{``, ``} 128 | execPath(p, x, exp, nil, t) 129 | } 130 | 131 | func TestDescendant(t *testing.T) { 132 | p := `/p1/descendant::p1` 133 | x := `` 134 | exp := []string{``} 135 | execPath(p, x, exp, nil, t) 136 | } 137 | 138 | func TestDescendantOrSelf(t *testing.T) { 139 | p := `/p1/descendant-or-self::p1` 140 | x := `` 141 | exp := []string{``, ``} 142 | execPath(p, x, exp, nil, t) 143 | } 144 | 145 | func TestAttribute(t *testing.T) { 146 | p := `/p1/attribute::test` 147 | x := `` 148 | exp := []string{``} 149 | execPath(p, x, exp, nil, t) 150 | } 151 | 152 | func TestAttributeSelf(t *testing.T) { 153 | p := `/p1/attribute::test/self::node()` 154 | x := `` 155 | exp := []string{``} 156 | execPath(p, x, exp, nil, t) 157 | } 158 | 159 | func TestAttributeAbbr(t *testing.T) { 160 | p := `/p1/ @ test` 161 | x := `` 162 | exp := []string{``} 163 | execPath(p, x, exp, nil, t) 164 | } 165 | 166 | func TestAttributeNoChild(t *testing.T) { 167 | p := `/p1/attribute::test/p2` 168 | x := `` 169 | exp := []string{} 170 | execPath(p, x, exp, nil, t) 171 | } 172 | 173 | func TestAttributeParent(t *testing.T) { 174 | p := `/p1/attribute::test/ ..` 175 | x := `` 176 | exp := []string{``} 177 | execPath(p, x, exp, nil, t) 178 | } 179 | 180 | func TestNodeTypeNode(t *testing.T) { 181 | p := `/p1/child:: node( ) ` 182 | x := `` 183 | exp := []string{``} 184 | execPath(p, x, exp, nil, t) 185 | } 186 | 187 | func TestNodeTypeNodeAbbr(t *testing.T) { 188 | p := `/p1/ .` 189 | x := `` 190 | exp := []string{``} 191 | execPath(p, x, exp, nil, t) 192 | } 193 | 194 | func TestNodeTypeParent(t *testing.T) { 195 | p := `/p1/p2/parent::node()` 196 | x := `` 197 | exp := []string{``} 198 | execPath(p, x, exp, nil, t) 199 | } 200 | 201 | func TestNodeTypeParentAbbr(t *testing.T) { 202 | p := `/p1/p2/..` 203 | x := `` 204 | exp := []string{``} 205 | execPath(p, x, exp, nil, t) 206 | } 207 | 208 | func TestFollowing(t *testing.T) { 209 | p := `//p3/following::node()` 210 | x := `` 211 | exp := []string{``, ``, ``} 212 | execPath(p, x, exp, nil, t) 213 | } 214 | 215 | func TestPreceding(t *testing.T) { 216 | p := `//p6/preceding::node()` 217 | x := `` 218 | exp := []string{``, ``, ``} 219 | execPath(p, x, exp, nil, t) 220 | } 221 | 222 | func TestPrecedingSibling(t *testing.T) { 223 | p := `//p4/preceding-sibling::node()` 224 | x := `` 225 | exp := []string{``} 226 | execPath(p, x, exp, nil, t) 227 | } 228 | 229 | func TestPrecedingSibling2(t *testing.T) { 230 | p := `/preceding-sibling::node()` 231 | x := `` 232 | exp := []string{} 233 | execPath(p, x, exp, nil, t) 234 | } 235 | 236 | func TestFollowingSibling(t *testing.T) { 237 | p := `//p2/following-sibling::node()` 238 | x := `` 239 | exp := []string{``} 240 | execPath(p, x, exp, nil, t) 241 | } 242 | 243 | func TestFollowingSibling2(t *testing.T) { 244 | p := `/following-sibling::node()` 245 | x := `` 246 | exp := []string{} 247 | execPath(p, x, exp, nil, t) 248 | } 249 | 250 | func TestComment(t *testing.T) { 251 | p := `// comment()` 252 | x := `` 253 | exp := []string{``} 254 | execPath(p, x, exp, nil, t) 255 | } 256 | 257 | func TestCommentInvPath(t *testing.T) { 258 | p := `//comment()/ self :: processing-instruction ( ) ` 259 | x := `` 260 | exp := []string{} 261 | execPath(p, x, exp, nil, t) 262 | } 263 | 264 | func TestCommentParent(t *testing.T) { 265 | p := `//comment()/..` 266 | x := `` 267 | exp := []string{``} 268 | execPath(p, x, exp, nil, t) 269 | } 270 | 271 | func TestText(t *testing.T) { 272 | p := `//text()` 273 | x := `text` 274 | exp := []string{`text`} 275 | execPath(p, x, exp, nil, t) 276 | } 277 | 278 | func TestTextParent(t *testing.T) { 279 | p := `//text()/..` 280 | x := `text` 281 | exp := []string{`text`} 282 | execPath(p, x, exp, nil, t) 283 | } 284 | 285 | func TestProcInst(t *testing.T) { 286 | p := `//processing-instruction()` 287 | x := `` 288 | exp := []string{``} 289 | execPath(p, x, exp, nil, t) 290 | } 291 | 292 | func TestProcInstParent(t *testing.T) { 293 | p := `//processing-instruction()/..` 294 | x := `` 295 | exp := []string{``} 296 | execPath(p, x, exp, nil, t) 297 | } 298 | 299 | func TestProcInstInvPath(t *testing.T) { 300 | p := `//processing-instruction()/self::comment()` 301 | x := `` 302 | exp := []string{} 303 | execPath(p, x, exp, nil, t) 304 | } 305 | 306 | func TestProcInst2(t *testing.T) { 307 | p := `//processing-instruction( 'proc2' ) ` 308 | x := `` 309 | exp := []string{``} 310 | execPath(p, x, exp, nil, t) 311 | } 312 | 313 | func TestNamespace(t *testing.T) { 314 | p := `/*:p1/*:p2/namespace::*` 315 | x := `` 316 | exp := []string{``, ``, ``} 317 | execPath(p, x, exp, nil, t) 318 | } 319 | 320 | func TestRootNamespace(t *testing.T) { 321 | p := `/namespace::*` 322 | x := `` 323 | exp := []string{} 324 | execPath(p, x, exp, nil, t) 325 | } 326 | 327 | func TestNamespaceXml(t *testing.T) { 328 | p := `/*:p1/ * : p2 /namespace::xml` 329 | x := `` 330 | exp := []string{``} 331 | execPath(p, x, exp, nil, t) 332 | } 333 | 334 | func TestNamespaceNodeType(t *testing.T) { 335 | p := `/*:p1/*:p2/namespace::foo/ self :: node ( ) ` 336 | x := `` 337 | exp := []string{``} 338 | execPath(p, x, exp, nil, t) 339 | } 340 | 341 | func TestNamespaceNodeWildcard(t *testing.T) { 342 | p := `/*:p1/*:p2/namespace::*:foo` 343 | x := `` 344 | exp := []string{``} 345 | execPath(p, x, exp, nil, t) 346 | p = `/*:p1/*:p2/namespace::invalid:foo` 347 | exp = []string{} 348 | execPath(p, x, exp, nil, t) 349 | } 350 | 351 | func TestNamespaceChild(t *testing.T) { 352 | p := `/*:p1/namespace::*/ * : p2` 353 | x := `` 354 | exp := []string{} 355 | execPath(p, x, exp, nil, t) 356 | } 357 | 358 | func TestNamespaceParent(t *testing.T) { 359 | p := `/*:p1/*:p2/namespace::foo/..` 360 | x := `` 361 | exp := []string{``} 362 | execPath(p, x, exp, nil, t) 363 | } 364 | 365 | func TestNamespace2(t *testing.T) { 366 | p := `//namespace::test` 367 | x := `` 368 | exp := []string{``} 369 | execPath(p, x, exp, nil, t) 370 | } 371 | 372 | func TestNamespace3(t *testing.T) { 373 | p := `/p1/p2/p3/namespace :: foo` 374 | x := `` 375 | exp := []string{``} 376 | execPath(p, x, exp, nil, t) 377 | } 378 | 379 | func TestNamespace4(t *testing.T) { 380 | p := `/test:p1/p2/p3/namespace::*` 381 | x := `` 382 | exp := []string{``} 383 | execPath(p, x, exp, map[string]string{"test": "http://test"}, t) 384 | } 385 | 386 | func TestNodeNamespace(t *testing.T) { 387 | p := `/ test : p1 ` 388 | x := `` 389 | exp := []string{``} 390 | execPath(p, x, exp, map[string]string{"test": "http://test"}, t) 391 | } 392 | 393 | func TestNodeNamespace2(t *testing.T) { 394 | p := `/test:p1/test2:p2` 395 | x := `` 396 | exp := []string{``} 397 | execPath(p, x, exp, map[string]string{"test": "http://test", "test2": "http://test2"}, t) 398 | } 399 | 400 | func TestNodeNamespace3(t *testing.T) { 401 | p := `/test:p1/foo:p2` 402 | x := `` 403 | exp := []string{``} 404 | execPath(p, x, exp, map[string]string{"test": "http://test", "foo": "http://foo.bar"}, t) 405 | } 406 | 407 | func TestAttrNamespace(t *testing.T) { 408 | p := `/test:p1/@foo:test` 409 | x := `` 410 | exp := []string{``} 411 | execPath(p, x, exp, map[string]string{"test": "http://test", "foo": "http://foo.bar"}, t) 412 | p = `/test:p1/@test:test` 413 | exp = []string{} 414 | execPath(p, x, exp, map[string]string{"test": "http://test", "foo": "http://foo.bar"}, t) 415 | } 416 | 417 | func TestWildcard(t *testing.T) { 418 | p := `/p1/*` 419 | x := `` 420 | exp := []string{``, ``, ``} 421 | execPath(p, x, exp, nil, t) 422 | } 423 | 424 | func TestWildcardNS(t *testing.T) { 425 | p := `//*:p1` 426 | x := `` 427 | exp := []string{``, ``} 428 | execPath(p, x, exp, map[string]string{"test": "http://test", "foo": "http://foo.bar"}, t) 429 | } 430 | 431 | func TestWildcardLocal(t *testing.T) { 432 | p := `//foo:*` 433 | x := `` 434 | exp := []string{``, ``} 435 | execPath(p, x, exp, map[string]string{"test": "http://test", "foo": "http://foo.bar"}, t) 436 | } 437 | 438 | func TestWildcardNSAttr(t *testing.T) { 439 | p := `/p1/@*:attr` 440 | x := `` 441 | exp := []string{``} 442 | execPath(p, x, exp, map[string]string{"test": "http://test", "foo": "http://foo.bar"}, t) 443 | } 444 | 445 | func TestWildcardLocalAttr(t *testing.T) { 446 | p := `/p1/@foo:*` 447 | x := `` 448 | exp := []string{``, ``} 449 | execPath(p, x, exp, map[string]string{"test": "http://test", "foo": "http://foo.bar"}, t) 450 | } 451 | 452 | func TestPredicate1(t *testing.T) { 453 | p := `/p1/p2 [ p3 ] ` 454 | x := `` 455 | exp := []string{``} 456 | execPath(p, x, exp, nil, t) 457 | } 458 | 459 | func TestPredicate2(t *testing.T) { 460 | p := `/p1/p2 [ 2 ] ` 461 | x := `` 462 | exp := []string{``} 463 | execPath(p, x, exp, nil, t) 464 | p = `/p1/p2 [ position() = 2 ] ` 465 | execPath(p, x, exp, nil, t) 466 | } 467 | 468 | func TestPredicate3(t *testing.T) { 469 | p := `/p1/p2 [ last() ] /p3` 470 | x := `` 471 | exp := []string{``} 472 | execPath(p, x, exp, nil, t) 473 | } 474 | 475 | func TestPredicate4(t *testing.T) { 476 | p := `/p1/p2[not(@test)]` 477 | x := `` 478 | exp := []string{``, ``} 479 | execPath(p, x, exp, nil, t) 480 | } 481 | 482 | func TestPredicate5(t *testing.T) { 483 | p := `/p1/p2[.//p1]` 484 | x := `lkj` 485 | exp := []string{`lkj`} 486 | execPath(p, x, exp, nil, t) 487 | } 488 | 489 | func TestPredicate6(t *testing.T) { 490 | p := `/p1/p2[//p1]` 491 | x := `lkj` 492 | exp := []string{``, ``, `lkj`} 493 | execPath(p, x, exp, nil, t) 494 | } 495 | 496 | func TestPredicate7(t *testing.T) { 497 | p := `/p3[//p1]` 498 | x := `lkj` 499 | exp := []string{} 500 | execPath(p, x, exp, nil, t) 501 | } 502 | 503 | func TestPredicate8(t *testing.T) { 504 | p := `/test[@attr1='a' and @attr2='b']/a` 505 | x := `aaaabbb` 506 | exp := []string{`aaaa`} 507 | execPath(p, x, exp, nil, t) 508 | } 509 | 510 | func TestPredicate9(t *testing.T) { 511 | p := `/root/text[@attr="2"][2]` 512 | x := xml.Header + ` 513 | This is some text 1.1. 514 | This is some text 1.2. 515 | This is some text. 2.1 516 | This is some text. 2.2 517 | This is some text. 2.3 518 | ` 519 | exp := []string{`This is some text. 2.2`} 520 | execPath(p, x, exp, nil, t) 521 | } 522 | 523 | func TestPredicate10(t *testing.T) { 524 | p := `/root/text[@attr="2"][last()]` 525 | x := xml.Header + ` 526 | This is some text 1.1. 527 | This is some text 1.2. 528 | This is some text. 2.1 529 | This is some text. 2.2 530 | This is some text. 2.3 531 | ` 532 | exp := []string{`This is some text. 2.3`} 533 | execPath(p, x, exp, nil, t) 534 | } 535 | 536 | func TestUnion(t *testing.T) { 537 | p := `/test/test2 | /test/test3` 538 | x := `foobarhamneggs` 539 | exp := []string{"foobar", "hamneggs"} 540 | execPath(p, x, exp, nil, t) 541 | } 542 | --------------------------------------------------------------------------------