Skip to content

Commit f16ad5c

Browse files
committed
add CompileWithNS
1 parent ef5e98c commit f16ad5c

File tree

4 files changed

+134
-16
lines changed

4 files changed

+134
-16
lines changed

build.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ func axisPredicate(root *axisNode) func(NodeNavigator) bool {
4444
predicate := func(n NodeNavigator) bool {
4545
if typ == n.NodeType() || typ == allNode {
4646
if nametest {
47+
type namespaceURL interface {
48+
NamespaceURL() string
49+
}
50+
if ns, ok := n.(namespaceURL); ok && root.hasNamespaceURI {
51+
return root.LocalName == n.LocalName() && root.namespaceURI == ns.NamespaceURL()
52+
}
4753
if root.LocalName == n.LocalName() && root.Prefix == n.Prefix() {
4854
return true
4955
}
@@ -539,7 +545,7 @@ func (b *builder) processNode(root node) (q query, err error) {
539545
}
540546

541547
// build builds a specified XPath expressions expr.
542-
func build(expr string) (q query, err error) {
548+
func build(expr string, namespaces map[string]string) (q query, err error) {
543549
defer func() {
544550
if e := recover(); e != nil {
545551
switch x := e.(type) {
@@ -552,7 +558,7 @@ func build(expr string) (q query, err error) {
552558
}
553559
}
554560
}()
555-
root := parse(expr)
561+
root := parse(expr, namespaces)
556562
b := &builder{}
557563
return b.processNode(root)
558564
}

parse.go

+28-12
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ const (
6969
)
7070

7171
type parser struct {
72-
r *scanner
73-
d int
72+
r *scanner
73+
d int
74+
namespaces map[string]string
7475
}
7576

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

8687
// newAxisNode returns new axis node AxisNode.
87-
func newAxisNode(axeTyp, localName, prefix, prop string, n node) node {
88-
return &axisNode{
88+
func newAxisNode(axeTyp, localName, prefix, prop string, n node, opts ...func(p *axisNode)) node {
89+
a := axisNode{
8990
nodeType: nodeAxis,
9091
LocalName: localName,
9192
Prefix: prefix,
9293
AxeType: axeTyp,
9394
Prop: prop,
9495
Input: n,
9596
}
97+
for _, o := range opts {
98+
o(&a)
99+
}
100+
return &a
96101
}
97102

98103
// newVariableNode returns new variable node VariableNode.
@@ -469,7 +474,16 @@ func (p *parser) parseNodeTest(n node, axeTyp string) (opnd node) {
469474
if p.r.name == "*" {
470475
name = ""
471476
}
472-
opnd = newAxisNode(axeTyp, name, prefix, "", n)
477+
opnd = newAxisNode(axeTyp, name, prefix, "", n, func(a *axisNode) {
478+
if prefix != "" && p.namespaces != nil {
479+
if ns, ok := p.namespaces[prefix]; ok {
480+
a.hasNamespaceURI = true
481+
a.namespaceURI = ns
482+
} else {
483+
panic(fmt.Sprintf("prefix %s not defined.", prefix))
484+
}
485+
}
486+
})
473487
}
474488
case itemStar:
475489
opnd = newAxisNode(axeTyp, "", "", "", n)
@@ -531,11 +545,11 @@ func (p *parser) parseMethod(n node) node {
531545
}
532546

533547
// Parse parsing the XPath express string expr and returns a tree node.
534-
func parse(expr string) node {
548+
func parse(expr string, namespaces map[string]string) node {
535549
r := &scanner{text: expr}
536550
r.nextChar()
537551
r.nextItem()
538-
p := &parser{r: r}
552+
p := &parser{r: r, namespaces: namespaces}
539553
return p.parseExpression(nil)
540554
}
541555

@@ -563,11 +577,13 @@ func (o *operatorNode) String() string {
563577
// axisNode holds a location step.
564578
type axisNode struct {
565579
nodeType
566-
Input node
567-
Prop string // node-test name.[comment|text|processing-instruction|node]
568-
AxeType string // name of the axes.[attribute|ancestor|child|....]
569-
LocalName string // local part name of node.
570-
Prefix string // prefix name of node.
580+
Input node
581+
Prop string // node-test name.[comment|text|processing-instruction|node]
582+
AxeType string // name of the axes.[attribute|ancestor|child|....]
583+
LocalName string // local part name of node.
584+
Prefix string // prefix name of node.
585+
namespaceURI string // namespace URI of node
586+
hasNamespaceURI bool // if namespace URI is set (can be "")
571587
}
572588

573589
func (a *axisNode) String() string {

xpath.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func Compile(expr string) (*Expr, error) {
141141
if expr == "" {
142142
return nil, errors.New("expr expression is nil")
143143
}
144-
qy, err := build(expr)
144+
qy, err := build(expr, nil)
145145
if err != nil {
146146
return nil, err
147147
}
@@ -159,3 +159,18 @@ func MustCompile(expr string) *Expr {
159159
}
160160
return exp
161161
}
162+
163+
// CompileWithNS compiles an XPath expression string, using given namespaces map.
164+
func CompileWithNS(expr string, namespaces map[string]string) (*Expr, error) {
165+
if expr == "" {
166+
return nil, errors.New("expr expression is nil")
167+
}
168+
qy, err := build(expr, namespaces)
169+
if err != nil {
170+
return nil, err
171+
}
172+
if qy == nil {
173+
return nil, fmt.Errorf(fmt.Sprintf("undeclared variable in XPath expression: %s", expr))
174+
}
175+
return &Expr{s: expr, q: qy}, nil
176+
}

xpath_test.go

+82-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package xpath
22

33
import (
44
"bytes"
5+
"fmt"
56
"strings"
67
"testing"
78
)
@@ -29,6 +30,60 @@ func TestCompile(t *testing.T) {
2930
if err != nil {
3031
t.Fatalf("/a/b/(c, .[not(c)]) should be correct but got error %s", err)
3132
}
33+
_, err = Compile("/pre:foo")
34+
if err != nil {
35+
t.Fatalf("/pre:foo should be correct but got error %s", err)
36+
}
37+
}
38+
39+
func TestCompileWithNS(t *testing.T) {
40+
_, err := CompileWithNS("/foo", nil)
41+
if err != nil {
42+
t.Fatalf("/foo {nil} should be correct but got error %s", err)
43+
}
44+
_, err = CompileWithNS("/foo", map[string]string{})
45+
if err != nil {
46+
t.Fatalf("/foo {} should be correct but got error %s", err)
47+
}
48+
_, err = CompileWithNS("/foo", map[string]string{"a": "b"})
49+
if err != nil {
50+
t.Fatalf("/foo {a:b} should be correct but got error %s", err)
51+
}
52+
_, err = CompileWithNS("/a:foo", map[string]string{"a": "b"})
53+
if err != nil {
54+
t.Fatalf("/a:foo should be correct but got error %s", err)
55+
}
56+
_, err = CompileWithNS("/u:foo", map[string]string{"a": "b"})
57+
msg := fmt.Sprintf("%v", err)
58+
if msg != "prefix u not defined." {
59+
t.Fatalf("expected 'prefix u not defined' but got: %s", msg)
60+
}
61+
}
62+
63+
func TestNamespace(t *testing.T) {
64+
doc := createNode("", RootNode)
65+
books := doc.createChildNode("books", ElementNode)
66+
book1 := books.createChildNode("book", ElementNode)
67+
book1.createChildNode("book1", TextNode)
68+
book2 := books.createChildNode("b:book", ElementNode)
69+
book2.addAttribute("xmlns:b", "ns")
70+
book2.createChildNode("book2", TextNode)
71+
book3 := books.createChildNode("c:book", ElementNode)
72+
book3.addAttribute("xmlns:c", "ns")
73+
book3.createChildNode("book3", TextNode)
74+
75+
// Existing behaviour:
76+
v := joinValues(selectNodes(doc, "//b:book"))
77+
if v != "book2" {
78+
t.Fatalf("expected book2 but got %s", v)
79+
}
80+
81+
// With namespace bindings:
82+
exp, _ := CompileWithNS("//x:book", map[string]string{"x": "ns"})
83+
v = joinValues(iterateNodes(exp.Select(createNavigator(doc))))
84+
if v != "book2,book3" {
85+
t.Fatalf("expected 'book2,book3' but got %s", v)
86+
}
3287
}
3388

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

522+
func joinValues(nodes []*TNode) string {
523+
s := make([]string, 0)
524+
for _, n := range nodes {
525+
s = append(s, n.Value())
526+
}
527+
return strings.Join(s, ",")
528+
}
529+
467530
func createNavigator(n *TNode) *TNodeNavigator {
468531
return &TNodeNavigator{curr: n, root: n, attr: -1}
469532
}
@@ -516,10 +579,28 @@ func (n *TNodeNavigator) LocalName() string {
516579
if n.attr != -1 {
517580
return n.curr.Attr[n.attr].Key
518581
}
519-
return n.curr.Data
582+
name := n.curr.Data
583+
if strings.Contains(name, ":") {
584+
return strings.Split(name, ":")[1]
585+
}
586+
return name
520587
}
521588

522589
func (n *TNodeNavigator) Prefix() string {
590+
if n.attr == -1 && strings.Contains(n.curr.Data, ":") {
591+
return strings.Split(n.curr.Data, ":")[0]
592+
}
593+
return ""
594+
}
595+
596+
func (n *TNodeNavigator) NamespaceURL() string {
597+
if n.Prefix() != "" {
598+
for _, a := range n.curr.Attr {
599+
if a.Key == "xmlns:"+n.Prefix() {
600+
return a.Value
601+
}
602+
}
603+
}
523604
return ""
524605
}
525606

0 commit comments

Comments
 (0)