Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add CompileWithNS #82

Merged
merged 1 commit into from
Feb 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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