diff --git a/all_test.go b/all_test.go index dd8ae18..2613cd3 100644 --- a/all_test.go +++ b/all_test.go @@ -240,6 +240,11 @@ var libraryTable = []struct { {"//*[contains(born,'1922')]/name", "Charles M Schulz"}, {"library/book[not(@id)]", exists(false)}, {"library/book[not(@foo) and @id='b0883556316']/isbn", []string{"0883556316"}}, + {"//character[starts-with(@id, 'Snoo')]/name", "Snoopy"}, + {"//character[starts-with(@id, 'Snoopy ')]/name", exists(false)}, + {"//character[starts-with(@id, 'noopy')]/name", exists(false)}, + {"//title[starts-with(.,'Barney Goo')]", "Barney Google and Snuffy Smith"}, + {"//title[starts-with(., 'noopy')]", exists(false)}, // Multiple predicates. {"library/book/character[@id='Snoopy' and ./born='1950-10-04']/born", []string{"1950-10-04"}}, diff --git a/doc.go b/doc.go index e81d737..65ce298 100644 --- a/doc.go +++ b/doc.go @@ -15,7 +15,7 @@ // - All axes are supported ("child", "following-sibling", etc) // - All abbreviated forms are supported (".", "//", etc) // - All node types except for namespace are supported -// - Predicates may be [N], [path], [not(path)], [path=literal] or [contains(path, literal)] +// - Predicates may be [N], [path], [not(path)], [path=literal], [contains(path, literal)] or [starts-with(@path, literal)] // - Predicates may be joined with "or", "and", and parenthesis // - Richer expressions and namespaces are not supported // @@ -58,6 +58,7 @@ // //book[author/@id='CMS']/title => "Being a Dog Is a Full-Time Job", // /library/book/preceding::comment() => " Great book. " // //*[contains(born,'1922')]/name => "Charles M Schulz" +// //character[starts-with(@id, 'Snoo')]/name => "Snoopy" // //*[@id='PP' or @id='Snoopy']/born => {"1966-08-22", "1950-10-04"} // // To run an expression, compile it, and then apply the compiled path to any diff --git a/parser.go b/parser.go index 4b5c448..69e379b 100644 --- a/parser.go +++ b/parser.go @@ -164,6 +164,32 @@ func (node *Node) contains(s string) (ok bool) { return false } +// startswith returns whether the string value of node has prefix s +func (node *Node) startsWith(s string) (ok bool) { + if len(s) == 0 { + return true + } + if node.kind == attrNode { + return strings.HasPrefix(node.attr, s) + } + si := 0 + for i := node.pos; i < node.end; i++ { + if node.nodes[i].kind != textNode { + continue + } + for _, c := range node.nodes[i].text { + if c != s[si] { + break + } + si++ + if si == len(s) { + return true + } + } + } + return false +} + // Parse reads an xml document from r, parses it, and returns its root node. func Parse(r io.Reader) (*Node, error) { return ParseDecoder(xml.NewDecoder(r)) diff --git a/path.go b/path.go index db38ed5..f3329a1 100644 --- a/path.go +++ b/path.go @@ -155,6 +155,13 @@ func (s *pathStepState) test(pred predicate) bool { return true } } + case startsWithPredicate: + iter := pred.path.Iter(s.node) + for iter.Next() { + if iter.Node().startsWith(pred.value) { + return true + } + } case notPredicate: iter := pred.path.Iter(s.node) if !iter.Next() { @@ -381,6 +388,11 @@ type containsPredicate struct { value string } +type startsWithPredicate struct { + path *Path + value string +} + type notPredicate struct { path *Path } @@ -398,13 +410,14 @@ type predicate interface { predicate() } -func (positionPredicate) predicate() {} -func (existsPredicate) predicate() {} -func (equalsPredicate) predicate() {} -func (containsPredicate) predicate() {} -func (notPredicate) predicate() {} -func (andPredicate) predicate() {} -func (orPredicate) predicate() {} +func (positionPredicate) predicate() {} +func (existsPredicate) predicate() {} +func (equalsPredicate) predicate() {} +func (containsPredicate) predicate() {} +func (startsWithPredicate) predicate() {} +func (notPredicate) predicate() {} +func (andPredicate) predicate() {} +func (orPredicate) predicate() {} type pathStep struct { root bool @@ -600,6 +613,25 @@ func (c *pathCompiler) parsePath() (path *Path, err error) { return nil, c.errorf("contains() missing ')'") } next = containsPredicate{path, value} + } else if c.skipString("starts-with(") { + path, err := c.parsePath() + if err != nil { + return nil, err + } + c.skipSpaces() + if !c.skipByte(',') { + return nil, c.errorf("starts-with() expected ',' followed by a literal string") + } + c.skipSpaces() + value, err := c.parseLiteral() + if err != nil { + return nil, err + } + c.skipSpaces() + if !c.skipByte(')') { + return nil, c.errorf("starts-with() missing ')'") + } + next = startsWithPredicate{path, value} } else if c.skipString("not(") { // TODO Generalize to handle any predicate expression. path, err := c.parsePath()