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) Copy() NSContext { prefixes := make(map[string]string, len(ctx.prefixes)+4) for k, v := range ctx.prefixes { prefixes[k] = v } return NSContext{prefixes: prefixes} } func (ctx NSContext) declare(prefix, namespace string) etree.Attr { ctx.prefixes[prefix] = namespace switch prefix { case defaultPrefix: return etree.Attr{ Key: xmlnsPrefix, Value: namespace, } default: return etree.Attr{ Space: xmlnsPrefix, Key: prefix, Value: namespace, } } } func (ctx NSContext) SubContext(el *etree.Element) (NSContext, error) { // The subcontext should inherit existing declared prefixes newCtx := ctx.Copy() // 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:" // 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 } newCtx.declare(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 } newCtx.declare(defaultPrefix, attr.Value) } } return newCtx, nil } // Prefixes returns a copy of this context's prefix map. func (ctx NSContext) Prefixes() map[string]string { prefixes := make(map[string]string, len(ctx.prefixes)) for k, v := range ctx.prefixes { prefixes[k] = v } return prefixes } // 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, } } // NSIterHandler is a function which is invoked with a element and its surrounding // NSContext during traversals. type NSIterHandler func(NSContext, *etree.Element) error // NSTraverse traverses an element tree, invoking the passed handler for each element // in the tree. func NSTraverse(ctx NSContext, el *etree.Element, handle NSIterHandler) 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 } // NSDetatch makes a copy of the passed element, and declares any namespaces in // the passed context onto the new element before returning it. func NSDetatch(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 behaves identically to NSSelectOneCtx, but uses DefaultNSContext as the // surrounding context. func NSSelectOne(el *etree.Element, namespace, tag string) (*etree.Element, error) { return NSSelectOneCtx(DefaultNSContext, el, namespace, tag) } // NSSelectOneCtx 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 NSSelectOneCtx(ctx NSContext, el *etree.Element, namespace, tag string) (*etree.Element, error) { var found *etree.Element err := NSFindIterateCtx(ctx, el, namespace, tag, func(ctx NSContext, el *etree.Element) error { var err error found, err = NSDetatch(ctx, el) if err != nil { return err } return ErrTraversalHalted }) if err != nil { return nil, err } return found, nil } // NSFindIterate behaves identically to NSFindIterateCtx, but uses DefaultNSContext // as the surrounding context. func NSFindIterate(el *etree.Element, namespace, tag string, handle NSIterHandler) error { return NSFindIterateCtx(DefaultNSContext, el, namespace, tag, handle) } // NSFindIterateCtx conducts a depth-first traversal searching for elements with the // specified tag in the specified namespace. It uses the passed NSContext for prefix // lookups. 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 NSFindIterateCtx(ctx NSContext, el *etree.Element, namespace, tag string, handle NSIterHandler) error { err := NSTraverse(ctx, 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(ctx, el) } return nil }) if err != nil && err != ErrTraversalHalted { return err } return nil } // NSFindOne behaves identically to NSFindOneCtx, but uses DefaultNSContext for // context. func NSFindOne(el *etree.Element, namespace, tag string) (*etree.Element, error) { return NSFindOneCtx(DefaultNSContext, el, namespace, tag) } // NSFindOneCtx conducts a depth-first search for the specified element. If such an element // is found a reference to it is returned. func NSFindOneCtx(ctx NSContext, el *etree.Element, namespace, tag string) (*etree.Element, error) { var found *etree.Element err := NSFindIterateCtx(ctx, el, namespace, tag, func(ctx NSContext, el *etree.Element) error { found = el return ErrTraversalHalted }) if err != nil { return nil, err } return found, nil } // NSBuildParentContext recurses upward from an element in order to build an NSContext // for its immediate parent. If the element has no parent DefaultNSContext // is returned. func NSBuildParentContext(el *etree.Element) (NSContext, error) { parent := el.Parent() if parent == nil { return DefaultNSContext, nil } ctx, err := NSBuildParentContext(parent) if err != nil { return ctx, err } return ctx.SubContext(parent) }