summaryrefslogtreecommitdiff
path: root/vendor/github.com/beevik/etree/path.go
diff options
context:
space:
mode:
authorxengineering <me@xengineering.eu>2025-12-10 21:08:39 +0100
committerxengineering <me@xengineering.eu>2025-12-10 21:08:39 +0100
commit1f4c3308be296d95163e3e0ea54761410f4da140 (patch)
tree54640f0f423c02aaae07d99a6754beb47a83f79c /vendor/github.com/beevik/etree/path.go
parent83277c420525e06f9d0c234e018f481b4579d6cd (diff)
downloadsia-server-1f4c3308be296d95163e3e0ea54761410f4da140.tar
sia-server-1f4c3308be296d95163e3e0ea54761410f4da140.tar.zst
sia-server-1f4c3308be296d95163e3e0ea54761410f4da140.zip
Add homematic-go v0.1.0
This is the minimal viable product (MVP) of this library suitable to build the MVP of the sia-server.
Diffstat (limited to 'vendor/github.com/beevik/etree/path.go')
-rw-r--r--vendor/github.com/beevik/etree/path.go605
1 files changed, 605 insertions, 0 deletions
diff --git a/vendor/github.com/beevik/etree/path.go b/vendor/github.com/beevik/etree/path.go
new file mode 100644
index 0000000..21760d3
--- /dev/null
+++ b/vendor/github.com/beevik/etree/path.go
@@ -0,0 +1,605 @@
+// Copyright 2015-2019 Brett Vickers.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package etree
+
+import (
+ "iter"
+ "strconv"
+ "strings"
+)
+
+/*
+A Path is a string that represents a search path through an etree starting
+from the document root or an arbitrary element. Paths are used with the
+Element object's Find* methods to locate and return desired elements.
+
+A Path consists of a series of slash-separated "selectors", each of which may
+be modified by one or more bracket-enclosed "filters". Selectors are used to
+traverse the etree from element to element, while filters are used to narrow
+the list of candidate elements at each node.
+
+Although etree Path strings are structurally and behaviorally similar to XPath
+strings (https://www.w3.org/TR/1999/REC-xpath-19991116/), they have a more
+limited set of selectors and filtering options.
+
+The following selectors are supported by etree paths:
+
+ . Select the current element.
+ .. Select the parent of the current element.
+ * Select all child elements of the current element.
+ / Select the root element when used at the start of a path.
+ // Select all descendants of the current element.
+ tag Select all child elements with a name matching the tag.
+
+The following basic filters are supported:
+
+ [@attrib] Keep elements with an attribute named attrib.
+ [@attrib='val'] Keep elements with an attribute named attrib and value matching val.
+ [tag] Keep elements with a child element named tag.
+ [tag='val'] Keep elements with a child element named tag and text matching val.
+ [n] Keep the n-th element, where n is a numeric index starting from 1.
+
+The following function-based filters are supported:
+
+ [text()] Keep elements with non-empty text.
+ [text()='val'] Keep elements whose text matches val.
+ [local-name()='val'] Keep elements whose un-prefixed tag matches val.
+ [name()='val'] Keep elements whose full tag exactly matches val.
+ [namespace-prefix()] Keep elements with non-empty namespace prefixes.
+ [namespace-prefix()='val'] Keep elements whose namespace prefix matches val.
+ [namespace-uri()] Keep elements with non-empty namespace URIs.
+ [namespace-uri()='val'] Keep elements whose namespace URI matches val.
+
+Below are some examples of etree path strings.
+
+Select the bookstore child element of the root element:
+
+ /bookstore
+
+Beginning from the root element, select the title elements of all descendant
+book elements having a 'category' attribute of 'WEB':
+
+ //book[@category='WEB']/title
+
+Beginning from the current element, select the first descendant book element
+with a title child element containing the text 'Great Expectations':
+
+ .//book[title='Great Expectations'][1]
+
+Beginning from the current element, select all child elements of book elements
+with an attribute 'language' set to 'english':
+
+ ./book/*[@language='english']
+
+Beginning from the current element, select all child elements of book elements
+containing the text 'special':
+
+ ./book/*[text()='special']
+
+Beginning from the current element, select all descendant book elements whose
+title child element has a 'language' attribute of 'french':
+
+ .//book/title[@language='french']/..
+
+Beginning from the current element, select all descendant book elements
+belonging to the http://www.w3.org/TR/html4/ namespace:
+
+ .//book[namespace-uri()='http://www.w3.org/TR/html4/']
+*/
+type Path struct {
+ segments []segment
+}
+
+// ErrPath is returned by path functions when an invalid etree path is provided.
+type ErrPath string
+
+// Error returns the string describing a path error.
+func (err ErrPath) Error() string {
+ return "etree: " + string(err)
+}
+
+// CompilePath creates an optimized version of an XPath-like string that
+// can be used to query elements in an element tree.
+func CompilePath(path string) (Path, error) {
+ var comp compiler
+ segments := comp.parsePath(path)
+ if comp.err != ErrPath("") {
+ return Path{nil}, comp.err
+ }
+ return Path{segments}, nil
+}
+
+// MustCompilePath creates an optimized version of an XPath-like string that
+// can be used to query elements in an element tree. Panics if an error
+// occurs. Use this function to create Paths when you know the path is
+// valid (i.e., if it's hard-coded).
+func MustCompilePath(path string) Path {
+ p, err := CompilePath(path)
+ if err != nil {
+ panic(err)
+ }
+ return p
+}
+
+// traverse follows the path from the element e, yielding elements that match
+// the path's selectors and filters using iterators.
+func (p Path) traverse(e *Element) iter.Seq[*Element] {
+ pather := newPather()
+ return func(yield func(*Element) bool) {
+ pather.queue.add(node{e, p.segments})
+ for pather.queue.len() > 0 {
+ if cont := pather.eval(pather.queue.remove(), yield); !cont {
+ return
+ }
+ }
+ }
+}
+
+// A segment is a portion of a path between "/" characters.
+// It contains one selector and zero or more [filters].
+type segment struct {
+ sel selector
+ filters []filter
+}
+
+func (seg *segment) apply(e *Element, p *pather) {
+ seg.sel.apply(e, p)
+ for _, f := range seg.filters {
+ f.apply(p)
+ }
+}
+
+// A selector selects XML elements for consideration by the
+// path traversal.
+type selector interface {
+ apply(e *Element, p *pather)
+}
+
+// A filter pares down a list of candidate XML elements based
+// on a path filter in [brackets].
+type filter interface {
+ apply(p *pather)
+}
+
+// A node represents an element and the remaining path segments that
+// should be applied against it by the pather.
+type node struct {
+ e *Element
+ segments []segment
+}
+
+// A pather is helper object that traverses an element tree using
+// a Path object. It collects and deduplicates all elements matching
+// the path query.
+type pather struct {
+ queue queue[node]
+ results []*Element
+ inResults map[*Element]bool
+ candidates []*Element
+ scratch []*Element // used by filters
+}
+
+// newPather creates a new pather instance.
+func newPather() *pather {
+ return &pather{
+ results: make([]*Element, 0),
+ inResults: make(map[*Element]bool),
+ candidates: make([]*Element, 0),
+ scratch: make([]*Element, 0),
+ }
+}
+
+// eval evaluates the current path node by applying the remaining path's
+// selector rules against the node's element, yielding results via iterator.
+// Returns false if early termination is requested.
+func (p *pather) eval(n node, yield func(*Element) bool) bool {
+ p.candidates = p.candidates[:0]
+ seg, remain := n.segments[0], n.segments[1:]
+ seg.apply(n.e, p)
+
+ if len(remain) == 0 {
+ for _, c := range p.candidates {
+ if in := p.inResults[c]; !in {
+ p.inResults[c] = true
+ if !yield(c) {
+ return false
+ }
+ }
+ }
+ } else {
+ for _, c := range p.candidates {
+ p.queue.add(node{c, remain})
+ }
+ }
+ return true
+}
+
+// A compiler generates a compiled path from a path string.
+type compiler struct {
+ err ErrPath
+}
+
+// parsePath parses an XPath-like string describing a path
+// through an element tree and returns a slice of segment
+// descriptors.
+func (c *compiler) parsePath(path string) []segment {
+ // If path ends with //, fix it
+ if strings.HasSuffix(path, "//") {
+ path += "*"
+ }
+
+ var segments []segment
+
+ // Check for an absolute path
+ if strings.HasPrefix(path, "/") {
+ segments = append(segments, segment{new(selectRoot), []filter{}})
+ path = path[1:]
+ }
+
+ // Split path into segments
+ for _, s := range splitPath(path) {
+ segments = append(segments, c.parseSegment(s))
+ if c.err != ErrPath("") {
+ break
+ }
+ }
+ return segments
+}
+
+func splitPath(path string) []string {
+ var pieces []string
+ start := 0
+ inquote := false
+ var quote byte
+ for i := 0; i+1 <= len(path); i++ {
+ if !inquote {
+ if path[i] == '\'' || path[i] == '"' {
+ inquote, quote = true, path[i]
+ } else if path[i] == '/' {
+ pieces = append(pieces, path[start:i])
+ start = i + 1
+ }
+ } else if path[i] == quote {
+ inquote = false
+ }
+ }
+ return append(pieces, path[start:])
+}
+
+// parseSegment parses a path segment between / characters.
+func (c *compiler) parseSegment(path string) segment {
+ pieces := strings.Split(path, "[")
+ seg := segment{
+ sel: c.parseSelector(pieces[0]),
+ filters: []filter{},
+ }
+ for i := 1; i < len(pieces); i++ {
+ fpath := pieces[i]
+ if len(fpath) == 0 || fpath[len(fpath)-1] != ']' {
+ c.err = ErrPath("path has invalid filter [brackets].")
+ break
+ }
+ seg.filters = append(seg.filters, c.parseFilter(fpath[:len(fpath)-1]))
+ }
+ return seg
+}
+
+// parseSelector parses a selector at the start of a path segment.
+func (c *compiler) parseSelector(path string) selector {
+ switch path {
+ case ".":
+ return new(selectSelf)
+ case "..":
+ return new(selectParent)
+ case "*":
+ return new(selectChildren)
+ case "":
+ return new(selectDescendants)
+ default:
+ return newSelectChildrenByTag(path)
+ }
+}
+
+var fnTable = map[string]func(e *Element) string{
+ "local-name": (*Element).name,
+ "name": (*Element).FullTag,
+ "namespace-prefix": (*Element).namespacePrefix,
+ "namespace-uri": (*Element).NamespaceURI,
+ "text": (*Element).Text,
+}
+
+// parseFilter parses a path filter contained within [brackets].
+func (c *compiler) parseFilter(path string) filter {
+ if len(path) == 0 {
+ c.err = ErrPath("path contains an empty filter expression.")
+ return nil
+ }
+
+ // Filter contains [@attr='val'], [@attr="val"], [fn()='val'],
+ // [fn()="val"], [tag='val'] or [tag="val"]?
+ eqindex := strings.IndexByte(path, '=')
+ if eqindex >= 0 && eqindex+1 < len(path) {
+ quote := path[eqindex+1]
+ if quote == '\'' || quote == '"' {
+ rindex := nextIndex(path, quote, eqindex+2)
+ if rindex != len(path)-1 {
+ c.err = ErrPath("path has mismatched filter quotes.")
+ return nil
+ }
+
+ key := path[:eqindex]
+ value := path[eqindex+2 : rindex]
+
+ switch {
+ case key[0] == '@':
+ return newFilterAttrVal(key[1:], value)
+ case strings.HasSuffix(key, "()"):
+ name := key[:len(key)-2]
+ if fn, ok := fnTable[name]; ok {
+ return newFilterFuncVal(fn, value)
+ }
+ c.err = ErrPath("path has unknown function " + name)
+ return nil
+ default:
+ return newFilterChildText(key, value)
+ }
+ }
+ }
+
+ // Filter contains [@attr], [N], [tag] or [fn()]
+ switch {
+ case path[0] == '@':
+ return newFilterAttr(path[1:])
+ case strings.HasSuffix(path, "()"):
+ name := path[:len(path)-2]
+ if fn, ok := fnTable[name]; ok {
+ return newFilterFunc(fn)
+ }
+ c.err = ErrPath("path has unknown function " + name)
+ return nil
+ case isInteger(path):
+ pos, _ := strconv.Atoi(path)
+ switch {
+ case pos > 0:
+ return newFilterPos(pos - 1)
+ default:
+ return newFilterPos(pos)
+ }
+ default:
+ return newFilterChild(path)
+ }
+}
+
+// selectSelf selects the current element into the candidate list.
+type selectSelf struct{}
+
+func (s *selectSelf) apply(e *Element, p *pather) {
+ p.candidates = append(p.candidates, e)
+}
+
+// selectRoot selects the element's root node.
+type selectRoot struct{}
+
+func (s *selectRoot) apply(e *Element, p *pather) {
+ root := e
+ for root.parent != nil {
+ root = root.parent
+ }
+ p.candidates = append(p.candidates, root)
+}
+
+// selectParent selects the element's parent into the candidate list.
+type selectParent struct{}
+
+func (s *selectParent) apply(e *Element, p *pather) {
+ if e.parent != nil {
+ p.candidates = append(p.candidates, e.parent)
+ }
+}
+
+// selectChildren selects the element's child elements into the
+// candidate list.
+type selectChildren struct{}
+
+func (s *selectChildren) apply(e *Element, p *pather) {
+ for _, c := range e.Child {
+ if c, ok := c.(*Element); ok {
+ p.candidates = append(p.candidates, c)
+ }
+ }
+}
+
+// selectDescendants selects all descendant child elements
+// of the element into the candidate list.
+type selectDescendants struct{}
+
+func (s *selectDescendants) apply(e *Element, p *pather) {
+ var queue queue[*Element]
+ for queue.add(e); queue.len() > 0; {
+ e := queue.remove()
+ p.candidates = append(p.candidates, e)
+ for _, c := range e.Child {
+ if c, ok := c.(*Element); ok {
+ queue.add(c)
+ }
+ }
+ }
+}
+
+// selectChildrenByTag selects into the candidate list all child
+// elements of the element having the specified tag.
+type selectChildrenByTag struct {
+ space, tag string
+}
+
+func newSelectChildrenByTag(path string) *selectChildrenByTag {
+ s, l := spaceDecompose(path)
+ return &selectChildrenByTag{s, l}
+}
+
+func (s *selectChildrenByTag) apply(e *Element, p *pather) {
+ for _, c := range e.Child {
+ if c, ok := c.(*Element); ok && spaceMatch(s.space, c.Space) && s.tag == c.Tag {
+ p.candidates = append(p.candidates, c)
+ }
+ }
+}
+
+// filterPos filters the candidate list, keeping only the
+// candidate at the specified index.
+type filterPos struct {
+ index int
+}
+
+func newFilterPos(pos int) *filterPos {
+ return &filterPos{pos}
+}
+
+func (f *filterPos) apply(p *pather) {
+ if f.index >= 0 {
+ if f.index < len(p.candidates) {
+ p.scratch = append(p.scratch, p.candidates[f.index])
+ }
+ } else {
+ if -f.index <= len(p.candidates) {
+ p.scratch = append(p.scratch, p.candidates[len(p.candidates)+f.index])
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterAttr filters the candidate list for elements having
+// the specified attribute.
+type filterAttr struct {
+ space, key string
+}
+
+func newFilterAttr(str string) *filterAttr {
+ s, l := spaceDecompose(str)
+ return &filterAttr{s, l}
+}
+
+func (f *filterAttr) apply(p *pather) {
+ for _, c := range p.candidates {
+ for _, a := range c.Attr {
+ if spaceMatch(f.space, a.Space) && f.key == a.Key {
+ p.scratch = append(p.scratch, c)
+ break
+ }
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterAttrVal filters the candidate list for elements having
+// the specified attribute with the specified value.
+type filterAttrVal struct {
+ space, key, val string
+}
+
+func newFilterAttrVal(str, value string) *filterAttrVal {
+ s, l := spaceDecompose(str)
+ return &filterAttrVal{s, l, value}
+}
+
+func (f *filterAttrVal) apply(p *pather) {
+ for _, c := range p.candidates {
+ for _, a := range c.Attr {
+ if spaceMatch(f.space, a.Space) && f.key == a.Key && f.val == a.Value {
+ p.scratch = append(p.scratch, c)
+ break
+ }
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterFunc filters the candidate list for elements satisfying a custom
+// boolean function.
+type filterFunc struct {
+ fn func(e *Element) string
+}
+
+func newFilterFunc(fn func(e *Element) string) *filterFunc {
+ return &filterFunc{fn}
+}
+
+func (f *filterFunc) apply(p *pather) {
+ for _, c := range p.candidates {
+ if f.fn(c) != "" {
+ p.scratch = append(p.scratch, c)
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterFuncVal filters the candidate list for elements containing a value
+// matching the result of a custom function.
+type filterFuncVal struct {
+ fn func(e *Element) string
+ val string
+}
+
+func newFilterFuncVal(fn func(e *Element) string, value string) *filterFuncVal {
+ return &filterFuncVal{fn, value}
+}
+
+func (f *filterFuncVal) apply(p *pather) {
+ for _, c := range p.candidates {
+ if f.fn(c) == f.val {
+ p.scratch = append(p.scratch, c)
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterChild filters the candidate list for elements having
+// a child element with the specified tag.
+type filterChild struct {
+ space, tag string
+}
+
+func newFilterChild(str string) *filterChild {
+ s, l := spaceDecompose(str)
+ return &filterChild{s, l}
+}
+
+func (f *filterChild) apply(p *pather) {
+ for _, c := range p.candidates {
+ for _, cc := range c.Child {
+ if cc, ok := cc.(*Element); ok &&
+ spaceMatch(f.space, cc.Space) &&
+ f.tag == cc.Tag {
+ p.scratch = append(p.scratch, c)
+ }
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}
+
+// filterChildText filters the candidate list for elements having
+// a child element with the specified tag and text.
+type filterChildText struct {
+ space, tag, text string
+}
+
+func newFilterChildText(str, text string) *filterChildText {
+ s, l := spaceDecompose(str)
+ return &filterChildText{s, l, text}
+}
+
+func (f *filterChildText) apply(p *pather) {
+ for _, c := range p.candidates {
+ for _, cc := range c.Child {
+ if cc, ok := cc.(*Element); ok &&
+ spaceMatch(f.space, cc.Space) &&
+ f.tag == cc.Tag &&
+ f.text == cc.Text() {
+ p.scratch = append(p.scratch, c)
+ }
+ }
+ }
+ p.candidates, p.scratch = p.scratch, p.candidates[0:0]
+}