Skip to content

Commit

Permalink
Merge branch '#82'
Browse files Browse the repository at this point in the history
  • Loading branch information
zhengchun committed Feb 15, 2023
2 parents ef5e98c + f16ad5c commit adca7e3
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 16 deletions.
10 changes: 8 additions & 2 deletions build.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ func axisPredicate(root *axisNode) func(NodeNavigator) bool {
predicate := func(n NodeNavigator) bool {
if typ == n.NodeType() || typ == allNode {
if nametest {
type namespaceURL interface {
NamespaceURL() string
}
if ns, ok := n.(namespaceURL); ok && root.hasNamespaceURI {
return root.LocalName == n.LocalName() && root.namespaceURI == ns.NamespaceURL()
}
if root.LocalName == n.LocalName() && root.Prefix == n.Prefix() {
return true
}
Expand Down Expand Up @@ -539,7 +545,7 @@ func (b *builder) processNode(root node) (q query, err error) {
}

// build builds a specified XPath expressions expr.
func build(expr string) (q query, err error) {
func build(expr string, namespaces map[string]string) (q query, err error) {
defer func() {
if e := recover(); e != nil {
switch x := e.(type) {
Expand All @@ -552,7 +558,7 @@ func build(expr string) (q query, err error) {
}
}
}()
root := parse(expr)
root := parse(expr, namespaces)
b := &builder{}
return b.processNode(root)
}
40 changes: 28 additions & 12 deletions parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ const (
)

type parser struct {
r *scanner
d int
r *scanner
d int
namespaces map[string]string
}

// newOperatorNode returns new operator node OperatorNode.
Expand All @@ -84,15 +85,19 @@ func newOperandNode(v interface{}) node {
}

// newAxisNode returns new axis node AxisNode.
func newAxisNode(axeTyp, localName, prefix, prop string, n node) node {
return &axisNode{
func newAxisNode(axeTyp, localName, prefix, prop string, n node, opts ...func(p *axisNode)) node {
a := axisNode{
nodeType: nodeAxis,
LocalName: localName,
Prefix: prefix,
AxeType: axeTyp,
Prop: prop,
Input: n,
}
for _, o := range opts {
o(&a)
}
return &a
}

// newVariableNode returns new variable node VariableNode.
Expand Down Expand Up @@ -469,7 +474,16 @@ func (p *parser) parseNodeTest(n node, axeTyp string) (opnd node) {
if p.r.name == "*" {
name = ""
}
opnd = newAxisNode(axeTyp, name, prefix, "", n)
opnd = newAxisNode(axeTyp, name, prefix, "", n, func(a *axisNode) {
if prefix != "" && p.namespaces != nil {
if ns, ok := p.namespaces[prefix]; ok {
a.hasNamespaceURI = true
a.namespaceURI = ns
} else {
panic(fmt.Sprintf("prefix %s not defined.", prefix))
}
}
})
}
case itemStar:
opnd = newAxisNode(axeTyp, "", "", "", n)
Expand Down Expand Up @@ -531,11 +545,11 @@ func (p *parser) parseMethod(n node) node {
}

// Parse parsing the XPath express string expr and returns a tree node.
func parse(expr string) node {
func parse(expr string, namespaces map[string]string) node {
r := &scanner{text: expr}
r.nextChar()
r.nextItem()
p := &parser{r: r}
p := &parser{r: r, namespaces: namespaces}
return p.parseExpression(nil)
}

Expand Down Expand Up @@ -563,11 +577,13 @@ func (o *operatorNode) String() string {
// axisNode holds a location step.
type axisNode struct {
nodeType
Input node
Prop string // node-test name.[comment|text|processing-instruction|node]
AxeType string // name of the axes.[attribute|ancestor|child|....]
LocalName string // local part name of node.
Prefix string // prefix name of node.
Input node
Prop string // node-test name.[comment|text|processing-instruction|node]
AxeType string // name of the axes.[attribute|ancestor|child|....]
LocalName string // local part name of node.
Prefix string // prefix name of node.
namespaceURI string // namespace URI of node
hasNamespaceURI bool // if namespace URI is set (can be "")
}

func (a *axisNode) String() string {
Expand Down
17 changes: 16 additions & 1 deletion xpath.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func Compile(expr string) (*Expr, error) {
if expr == "" {
return nil, errors.New("expr expression is nil")
}
qy, err := build(expr)
qy, err := build(expr, nil)
if err != nil {
return nil, err
}
Expand All @@ -159,3 +159,18 @@ func MustCompile(expr string) *Expr {
}
return exp
}

// CompileWithNS compiles an XPath expression string, using given namespaces map.
func CompileWithNS(expr string, namespaces map[string]string) (*Expr, error) {
if expr == "" {
return nil, errors.New("expr expression is nil")
}
qy, err := build(expr, namespaces)
if err != nil {
return nil, err
}
if qy == nil {
return nil, fmt.Errorf(fmt.Sprintf("undeclared variable in XPath expression: %s", expr))
}
return &Expr{s: expr, q: qy}, nil
}
83 changes: 82 additions & 1 deletion xpath_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package xpath

import (
"bytes"
"fmt"
"strings"
"testing"
)
Expand Down Expand Up @@ -29,6 +30,60 @@ func TestCompile(t *testing.T) {
if err != nil {
t.Fatalf("/a/b/(c, .[not(c)]) should be correct but got error %s", err)
}
_, err = Compile("/pre:foo")
if err != nil {
t.Fatalf("/pre:foo should be correct but got error %s", err)
}
}

func TestCompileWithNS(t *testing.T) {
_, err := CompileWithNS("/foo", nil)
if err != nil {
t.Fatalf("/foo {nil} should be correct but got error %s", err)
}
_, err = CompileWithNS("/foo", map[string]string{})
if err != nil {
t.Fatalf("/foo {} should be correct but got error %s", err)
}
_, err = CompileWithNS("/foo", map[string]string{"a": "b"})
if err != nil {
t.Fatalf("/foo {a:b} should be correct but got error %s", err)
}
_, err = CompileWithNS("/a:foo", map[string]string{"a": "b"})
if err != nil {
t.Fatalf("/a:foo should be correct but got error %s", err)
}
_, err = CompileWithNS("/u:foo", map[string]string{"a": "b"})
msg := fmt.Sprintf("%v", err)
if msg != "prefix u not defined." {
t.Fatalf("expected 'prefix u not defined' but got: %s", msg)
}
}

func TestNamespace(t *testing.T) {
doc := createNode("", RootNode)
books := doc.createChildNode("books", ElementNode)
book1 := books.createChildNode("book", ElementNode)
book1.createChildNode("book1", TextNode)
book2 := books.createChildNode("b:book", ElementNode)
book2.addAttribute("xmlns:b", "ns")
book2.createChildNode("book2", TextNode)
book3 := books.createChildNode("c:book", ElementNode)
book3.addAttribute("xmlns:c", "ns")
book3.createChildNode("book3", TextNode)

// Existing behaviour:
v := joinValues(selectNodes(doc, "//b:book"))
if v != "book2" {
t.Fatalf("expected book2 but got %s", v)
}

// With namespace bindings:
exp, _ := CompileWithNS("//x:book", map[string]string{"x": "ns"})
v = joinValues(iterateNodes(exp.Select(createNavigator(doc))))
if v != "book2,book3" {
t.Fatalf("expected 'book2,book3' but got %s", v)
}
}

func TestMustCompile(t *testing.T) {
Expand Down Expand Up @@ -464,6 +519,14 @@ func selectNodes(root *TNode, expr string) []*TNode {
return iterateNodes(t)
}

func joinValues(nodes []*TNode) string {
s := make([]string, 0)
for _, n := range nodes {
s = append(s, n.Value())
}
return strings.Join(s, ",")
}

func createNavigator(n *TNode) *TNodeNavigator {
return &TNodeNavigator{curr: n, root: n, attr: -1}
}
Expand Down Expand Up @@ -516,10 +579,28 @@ func (n *TNodeNavigator) LocalName() string {
if n.attr != -1 {
return n.curr.Attr[n.attr].Key
}
return n.curr.Data
name := n.curr.Data
if strings.Contains(name, ":") {
return strings.Split(name, ":")[1]
}
return name
}

func (n *TNodeNavigator) Prefix() string {
if n.attr == -1 && strings.Contains(n.curr.Data, ":") {
return strings.Split(n.curr.Data, ":")[0]
}
return ""
}

func (n *TNodeNavigator) NamespaceURL() string {
if n.Prefix() != "" {
for _, a := range n.curr.Attr {
if a.Key == "xmlns:"+n.Prefix() {
return a.Value
}
}
}
return ""
}

Expand Down

0 comments on commit adca7e3

Please sign in to comment.