253 lines
6.3 KiB
Go
253 lines
6.3 KiB
Go
|
package etreeutils
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
|
||
|
"fmt"
|
||
|
|
||
|
"sort"
|
||
|
|
||
|
"github.com/beevik/etree"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
defaultPrefix = ""
|
||
|
xmlnsPrefix = "xmlns"
|
||
|
xmlPrefix = "xml"
|
||
|
|
||
|
XMLNamespace = "http://www.w3.org/XML/1998/namespace"
|
||
|
XMLNSNamespace = "http://www.w3.org/2000/xmlns/"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
DefaultNSContext = NSContext{
|
||
|
prefixes: map[string]string{
|
||
|
defaultPrefix: XMLNamespace,
|
||
|
xmlPrefix: XMLNamespace,
|
||
|
xmlnsPrefix: XMLNSNamespace,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
EmptyNSContext = NSContext{}
|
||
|
|
||
|
ErrReservedNamespace = errors.New("disallowed declaration of reserved namespace")
|
||
|
ErrInvalidDefaultNamespace = errors.New("invalid default namespace declaration")
|
||
|
ErrTraversalHalted = errors.New("traversal halted")
|
||
|
)
|
||
|
|
||
|
type ErrUndeclaredNSPrefix struct {
|
||
|
Prefix string
|
||
|
}
|
||
|
|
||
|
func (e ErrUndeclaredNSPrefix) Error() string {
|
||
|
return fmt.Sprintf("undeclared namespace prefix: '%s'", e.Prefix)
|
||
|
}
|
||
|
|
||
|
type NSContext struct {
|
||
|
prefixes map[string]string
|
||
|
}
|
||
|
|
||
|
func (ctx NSContext) SubContext(el *etree.Element) (NSContext, error) {
|
||
|
// The subcontext should inherit existing declared prefixes
|
||
|
prefixes := make(map[string]string, len(ctx.prefixes)+4)
|
||
|
for k, v := range ctx.prefixes {
|
||
|
prefixes[k] = v
|
||
|
}
|
||
|
|
||
|
// Merge new namespace declarations on top of existing ones.
|
||
|
for _, attr := range el.Attr {
|
||
|
if attr.Space == xmlnsPrefix {
|
||
|
// This attribute is a namespace declaration of the form "xmlns:<prefix>"
|
||
|
|
||
|
// The 'xml' namespace may only be re-declared with the name 'http://www.w3.org/XML/1998/namespace'
|
||
|
if attr.Key == xmlPrefix && attr.Value != XMLNamespace {
|
||
|
return ctx, ErrReservedNamespace
|
||
|
}
|
||
|
|
||
|
// The 'xmlns' namespace may not be re-declared
|
||
|
if attr.Key == xmlnsPrefix {
|
||
|
return ctx, ErrReservedNamespace
|
||
|
}
|
||
|
|
||
|
prefixes[attr.Key] = attr.Value
|
||
|
} else if attr.Space == defaultPrefix && attr.Key == xmlnsPrefix {
|
||
|
// This attribute is a default namespace declaration
|
||
|
|
||
|
// The xmlns namespace value may not be declared as the default namespace
|
||
|
if attr.Value == XMLNSNamespace {
|
||
|
return ctx, ErrInvalidDefaultNamespace
|
||
|
}
|
||
|
|
||
|
prefixes[defaultPrefix] = attr.Value
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return NSContext{prefixes: prefixes}, nil
|
||
|
}
|
||
|
|
||
|
// LookupPrefix attempts to find a declared namespace for the specified prefix. If the prefix
|
||
|
// is an empty string this will be the default namespace for this context. If the prefix is
|
||
|
// undeclared in this context an ErrUndeclaredNSPrefix will be returned.
|
||
|
func (ctx NSContext) LookupPrefix(prefix string) (string, error) {
|
||
|
if namespace, ok := ctx.prefixes[prefix]; ok {
|
||
|
return namespace, nil
|
||
|
}
|
||
|
|
||
|
return "", ErrUndeclaredNSPrefix{
|
||
|
Prefix: prefix,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func nsTraverse(ctx NSContext, el *etree.Element, handle func(NSContext, *etree.Element) error) error {
|
||
|
ctx, err := ctx.SubContext(el)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
err = handle(ctx, el)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Recursively traverse child elements.
|
||
|
for _, child := range el.ChildElements() {
|
||
|
err := nsTraverse(ctx, child, handle)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func detachWithNamespaces(ctx NSContext, el *etree.Element) (*etree.Element, error) {
|
||
|
ctx, err := ctx.SubContext(el)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
el = el.Copy()
|
||
|
|
||
|
// Build a new attribute list
|
||
|
attrs := make([]etree.Attr, 0, len(el.Attr))
|
||
|
|
||
|
// First copy over anything that isn't a namespace declaration
|
||
|
for _, attr := range el.Attr {
|
||
|
if attr.Space == xmlnsPrefix {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if attr.Space == defaultPrefix && attr.Key == xmlnsPrefix {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
attrs = append(attrs, attr)
|
||
|
}
|
||
|
|
||
|
// Append all in-context namespace declarations
|
||
|
for prefix, namespace := range ctx.prefixes {
|
||
|
// Skip the implicit "xml" and "xmlns" prefix declarations
|
||
|
if prefix == xmlnsPrefix || prefix == xmlPrefix {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Also skip declararing the default namespace as XMLNamespace
|
||
|
if prefix == defaultPrefix && namespace == XMLNamespace {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if prefix != defaultPrefix {
|
||
|
attrs = append(attrs, etree.Attr{
|
||
|
Space: xmlnsPrefix,
|
||
|
Key: prefix,
|
||
|
Value: namespace,
|
||
|
})
|
||
|
} else {
|
||
|
attrs = append(attrs, etree.Attr{
|
||
|
Key: xmlnsPrefix,
|
||
|
Value: namespace,
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sort.Sort(SortedAttrs(attrs))
|
||
|
|
||
|
el.Attr = attrs
|
||
|
|
||
|
return el, nil
|
||
|
}
|
||
|
|
||
|
// NSSelectOne conducts a depth-first search for an element with the specified namespace
|
||
|
// and tag. If such an element is found, a new *etree.Element is returned which is a
|
||
|
// copy of the found element, but with all in-context namespace declarations attached
|
||
|
// to the element as attributes.
|
||
|
func NSSelectOne(el *etree.Element, namespace, tag string) (*etree.Element, error) {
|
||
|
var found *etree.Element
|
||
|
|
||
|
err := nsTraverse(DefaultNSContext, el, func(ctx NSContext, el *etree.Element) error {
|
||
|
currentNS, err := ctx.LookupPrefix(el.Space)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Base case, el is the sought after element.
|
||
|
if currentNS == namespace && el.Tag == tag {
|
||
|
found, err = detachWithNamespaces(ctx, el)
|
||
|
return ErrTraversalHalted
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
})
|
||
|
|
||
|
if err != nil && err != ErrTraversalHalted {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return found, nil
|
||
|
}
|
||
|
|
||
|
// NSFindIterate conducts a depth-first traversal searching for elements with the
|
||
|
// specified tag in the specified namespace. For each such element, the passed
|
||
|
// handler function is invoked. If the handler function returns an error
|
||
|
// traversal is immediately halted. If the error returned by the handler is
|
||
|
// ErrTraversalHalted then nil will be returned by NSFindIterate. If any other
|
||
|
// error is returned by the handler, that error will be returned by NSFindIterate.
|
||
|
func NSFindIterate(el *etree.Element, namespace, tag string, handle func(*etree.Element) error) error {
|
||
|
err := nsTraverse(DefaultNSContext, el, func(ctx NSContext, el *etree.Element) error {
|
||
|
currentNS, err := ctx.LookupPrefix(el.Space)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Base case, el is the sought after element.
|
||
|
if currentNS == namespace && el.Tag == tag {
|
||
|
return handle(el)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
})
|
||
|
|
||
|
if err != nil && err != ErrTraversalHalted {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// NSFindOne conducts a depth-first search for the specified element. If such an element
|
||
|
// is found a reference to it is returned.
|
||
|
func NSFindOne(el *etree.Element, namespace, tag string) (*etree.Element, error) {
|
||
|
var found *etree.Element
|
||
|
|
||
|
err := NSFindIterate(el, namespace, tag, func(el *etree.Element) error {
|
||
|
found = el
|
||
|
return ErrTraversalHalted
|
||
|
})
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return found, nil
|
||
|
}
|