xdm

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package xdm implements the XQuery/XPath Data Model (XDM) that XPath 2.0 and XSLT 2.0 are defined over.

The central difference from XPath 1.0 is that every value is a *sequence* of items, and an item is either a node or a typed atomic value. XPath 1.0 had four types (node-set, string, number, boolean) with implicit coercion everywhere; 2.0 has the full XML Schema datatype hierarchy with explicit promotion rules. Modelling that faithfully here is what lets the rest of the engine avoid the 1.0-style "just call ToString" shortcuts that make 2.0 stylesheets silently produce wrong answers.

Index

Constants

View Source
const (
	// DefaultMaxDepth is the nesting limit.
	DefaultMaxDepth = 1000

	// DefaultMaxBytes is the source-size limit: 64 MB, far above any
	// schema or stylesheet and above most real instance documents, while
	// still bounding what a single parse can be asked to read.
	DefaultMaxBytes int64 = 64 << 20

	// DefaultMaxNodes is the node-count limit. At roughly 200 bytes a node
	// this bounds a tree to about 2 GB, which is the point of it: the
	// number is chosen to bound *memory*, and it is the limit that actually
	// binds on the documents designed to be expensive.
	DefaultMaxNodes = 10_000_000
)

Limits applied when the corresponding ParseOptions field is zero.

View Source
const (
	NSXSL    = "http://www.w3.org/1999/XSL/Transform"
	NSXML    = "http://www.w3.org/XML/1998/namespace"
	NSXMLNS  = "http://www.w3.org/2000/xmlns/"
	NSXS     = "http://www.w3.org/2001/XMLSchema"
	NSXSI    = "http://www.w3.org/2001/XMLSchema-instance"
	NSFN     = "http://www.w3.org/2005/xpath-functions"
	NSErr    = "http://www.w3.org/2005/xqt-errors"
	NSSVRL   = "http://purl.oclc.org/dsdl/svrl"
	NSSchema = "http://purl.oclc.org/dsdl/schematron"

	// NSGoxslt is this engine's extension namespace. Extensions live outside
	// the fn: namespace so that a stylesheet written for another processor
	// cannot silently pick one up in place of a standard function, and so
	// that a stylesheet using them is visibly engine-specific.
	NSGoxslt = "https://github.com/knroy/go-xml"
)

Well-known namespace URIs used throughout the engine.

Variables

This section is empty.

Functions

func AnnotationLocal added in v1.0.0

func AnnotationLocal(annotation string) string

AnnotationLocal returns just the local part of an annotation key.

func AnnotationName added in v1.0.0

func AnnotationName(uri, local string) string

AnnotationName builds the key a type annotation is recorded and compared under.

The data model keys annotations by a single string, and that string used to be the type's bare local name. That conflated every type sharing a local part across namespaces, and the conflation was not theoretical: the W3C's own schema-for-xslt20.xsd deliberately declares an xsl:QName of its own, as a restriction of xs:Name, and says in its text why ("This schema does not use the built-in type xs:QName... a schema processor would expand unprefixed QNames incorrectly"). Keyed by "QName", loading that schema overwrote the built-in's entry in a package-level, process-global map, so every later schema in the same process saw xs:QName deriving from xs:Name.

Both meanings have to coexist rather than one displacing the other: import-schema-029 asserts that the SHADOWING xsl:QName does erase to a string, while type-functions-0501 asserts that the built-in xs:QName still atomises to a QName value. A registration that refuses to shadow breaks the first; a global "something shadowed a built-in" flag breaks the second, because one flag cannot hold two answers at once. Only qualifying the key separates them.

The encoding is Clark notation, {uri}local, which the codebase already spells through QName.Clark, with one deliberate exception: a type in the XML Schema namespace keys under its BARE local name. That exception is what keeps the change tractable. Built-in annotations are compared against bare literals — "QName", "NOTATION", "ID", "string" — at roughly a hundred sites across four packages, in switch statements, map lookups and equality tests. Qualifying them would have required rewriting every one of those, whereas leaving them bare means only names that were previously AMBIGUOUS change spelling, and a built-in's key is the same string it has always been.

The empty URI also keys bare, which is the no-namespace case and is already unambiguous.

func CompareDT

func CompareDT(a, b *DateTime, implicitTZ int) int

CompareDT orders two date/time values on the normalised timeline.

func DerivedBase added in v1.0.0

func DerivedBase(name string) string

DerivedBase returns the type a schema type derives from, or "" if the name is not a registered schema type. The name is an annotation name, and so is the result, so a chain can be walked by feeding one back in.

It is what makes the subtype relation work for schema types: a value annotated as a restriction of xs:NOTATION is an instance of xs:NOTATION as well as of its own type, and answering that means walking the chain the schema recorded.

func ErrCast

func ErrCast(format string, args ...any) error

ErrCast is the XPath cast error, FORG0001.

func ErrType

func ErrType(format string, args ...any) error

ErrType is the XPath type error, XPTY0004. It is returned rather than panicked so that a stylesheet error degrades one transform.

func ErrorCode

func ErrorCode(err error) string

ErrorCode returns the spec error code carried by err, or "" if it has none.

It unwraps, so a code survives being wrapped with fmt.Errorf("%w"). Errors produced before this type existed still carry their code as a message prefix, so those are recognised too rather than silently reporting "".

func Errorf

func Errorf(code, format string, args ...any) error

Errorf builds an Error with the given code.

func HasSimpleTypeAnnotation added in v1.0.0

func HasSimpleTypeAnnotation(annotation string) bool

HasSimpleTypeAnnotation reports whether an annotation names a simple type, or a complex type with simple content.

XSLT 2.0 section 4.4 preserves whitespace-only text in such an element *regardless* of xsl:strip-space: that text is the element's entire typed value, which the schema validated, and stripping it would leave a node whose annotation describes a value it no longer holds. An element with element-only or mixed content has no such value and is stripped normally.

The registration table is the oracle rather than a list of names, because the annotation on such an element is the built-in its content type erases to — "string" for both an element of type xs:string and one whose anonymous complex type extends xs:string, which is exactly the pair section 4.4 groups together. A complex type with element-only content registers no derivation to a built-in, so it answers false, which is the distinction being drawn.

func IsGregorian

func IsGregorian(t TypeCode) bool

IsGregorian reports whether t is one of the five Gregorian types.

func IsNCName

func IsNCName(s string) bool

IsNCName reports whether s is an XML non-colonised name: a name with no prefix, which is what an element, attribute or processing-instruction name must be once the prefix has been split off.

It lives here rather than in a consumer because more than one caller needs it, and because the cost of *not* checking is that a computed name reaches the serialiser unvalidated. A name is written to output as-is, so a name holding "><script>" produces markup rather than a name — output that is either malformed or, in HTML, an injected element.

func IsQualifiedAnnotation added in v1.0.0

func IsQualifiedAnnotation(annotation string) bool

IsQualifiedAnnotation reports whether an annotation key carries a namespace, which is to say it names a type that is neither a built-in nor in no namespace.

func IsXMLWhitespace

func IsXMLWhitespace(s string) bool

IsXMLWhitespace reports whether s consists entirely of XML whitespace.

XML defines whitespace as exactly four characters: space, tab, carriage return and line feed. Go's strings.TrimSpace uses unicode.IsSpace, which additionally matches U+00A0 (no-break space) and other Unicode separators — so using it to decide whether a text node is "just whitespace" silently deletes a &nbsp; that the author put there deliberately.

func LexicalGregorian

func LexicalGregorian(dt *DateTime, t TypeCode) string

LexicalGregorian returns the canonical lexical form.

func ListItemOf added in v1.0.0

func ListItemOf(name string) string

ListItemOf returns the item type registered for a list type, or "" when the name is not a registered list.

func RegisterDerivedType added in v1.0.0

func RegisterDerivedType(name, primitive string)

RegisterDerivedType records that a schema type erases to a built-in one.

Both arguments are annotation names, which AnnotationName builds; passing a bare local name for a type that has a namespace re-creates the conflation this keying exists to prevent.

The xsd package calls this as it loads a schema, so that a node annotated with a user-defined type still atomises to a typed value rather than to untypedAtomic. Without it, "instance of my:partNumberType" could never be true for a value read out of a validated document, because the value would have discarded the annotation on the way out of the tree.

func RegisterListType added in v1.0.0

func RegisterListType(name, itemType string)

RegisterListType records that a schema type is a list, and what its items are.

The xsd package calls this as it loads a schema, for the same reason it calls RegisterDerivedType: the typed value of a list-typed node is a SEQUENCE of one atomic per token, and nothing in the data model can work out from a bare type name that "numbers" is a list of xs:decimal. Without it a list-typed node atomises to one untypedAtomic holding the whole literal, so count(data(@list)) answers 1 and "data(@list) instance of xs:untypedAtomic" answers true for a node the schema plainly gave a typed value.

Both arguments are annotation names, as RegisterDerivedType's are.

itemType is the item type's own name, which may itself be a registered schema type; atomicForAnnotation and the derivation walk resolve it.

func RegisterUnionType added in v1.0.0

func RegisterUnionType(name string, members []string)

RegisterUnionType records that a schema type is a union, and what its member types are.

The xsd package calls this as it loads a schema, for the reason it calls RegisterListType: a union's base is always xs:anySimpleType, so the derivation chain RegisterDerivedType records dead-ends immediately and carries no information about what the value actually is. Without the member list a union-typed node atomises to xs:untypedAtomic — the walk finds anySimpleType, cannot build a value for it, and gives up — which makes "data(u) instance of xs:untypedAtomic" true for a node the schema plainly gave a typed value, and makes every question about the member it validated as answer false.

The name and every member are annotation names, as RegisterDerivedType's arguments are. This registry is keyed by the same strings as derivedPrimitives and listItems, so qualifying one of the three and not the others would leave unions silently unresolvable.

The members are the *declared* members, in declaration order; which of them a given value belongs to is a per-value fact recorded on the node, because XSD 1.0 §3.14.4 chooses the member by trying each one's lexical space against the value in turn.

func SplitAnnotationName added in v1.0.0

func SplitAnnotationName(annotation string) (uri, local string)

SplitAnnotationName is the inverse of AnnotationName: it returns the namespace URI and local part of an annotation key.

A bare key is a built-in or a no-namespace type, and the two are told apart by nothing here — the URI comes back empty for both, because the callers that care (the built-in switches) match on the local part they already expect. What this function exists for is the comparison path, which needs the local part of a qualified key without mistaking "{uri}local" for a prefixed lexical QName.

It must be used in place of SplitQName wherever the input is an annotation. SplitQName cuts at the first colon, so handed "{http://x}foo" it returns the prefix "{http" and the local part "//x}foo" — nonsense rather than an error, and silently wrong.

func SplitQName

func SplitQName(s string) (prefix, local string)

SplitQName splits a lexical QName into prefix and local part. It does not resolve the prefix; resolution needs a namespace context and is done by the caller that has one.

func UnionMembersOf added in v1.0.0

func UnionMembersOf(name string) []string

UnionMembersOf returns the member types of a registered union type, or nil when the name does not denote one.

The result must not be modified: it is the stored slice, shared with every other caller.

Types

type Atomic

type Atomic struct {
	Type TypeCode
	// contains filtered or unexported fields
}

Atomic is a typed atomic value.

The representation is a tagged union rather than an interface per type. The evaluator switches on Type constantly — every arithmetic op, comparison and function call — and a type switch across seventeen concrete types in those hot paths costs more than a single integer compare. It also keeps the numeric tower in one place, where the promotion rules are easy to audit.

func NewAnyURI

func NewAnyURI(s string) *Atomic

NewAnyURI returns an xs:anyURI.

func NewBinary

func NewBinary(s string, t TypeCode) *Atomic

NewBinary returns an xs:hexBinary or xs:base64Binary holding the given lexical form.

The value keeps its own type rather than collapsing to xs:string, because the two binary types are inter-convertible: casting hexBinary to base64Binary has to re-encode the underlying octets, and a value that has forgotten which encoding its lexical form uses cannot be decoded.

func NewBoolean

func NewBoolean(v bool) *Atomic

NewBoolean returns an xs:boolean.

func NewDateTime

func NewDateTime(dt *DateTime, t TypeCode) *Atomic

NewDateTime returns a date, time or dateTime atomic value.

func NewDecimal

func NewDecimal(r *big.Rat) *Atomic

NewDecimal returns an xs:decimal holding an exact value.

func NewDouble

func NewDouble(v float64) *Atomic

NewDouble returns an xs:double.

func NewDuration

func NewDuration(d *Duration, t TypeCode) *Atomic

NewDuration returns a duration atomic value of the given duration type.

func NewFloat

func NewFloat(v float64) *Atomic

NewFloat returns an xs:float. The value is rounded to float32 precision on construction, because xs:float operations must produce float32 results.

func NewGregorian

func NewGregorian(dt *DateTime, t TypeCode) *Atomic

NewGregorian returns one of the five Gregorian atomic values.

func NewInteger

func NewInteger(v int64) *Atomic

NewInteger returns an xs:integer. Integers are held as exact rationals so that they participate in decimal arithmetic without precision loss.

func NewIntegerFromRat

func NewIntegerFromRat(r *big.Rat) *Atomic

NewIntegerFromRat returns an xs:integer from an exact rational, which must have denominator 1. Used by arithmetic that has already established integrality (idiv, string-length, count).

func NewQNameValue

func NewQNameValue(q QName) *Atomic

NewQNameValue returns an xs:QName.

func NewString

func NewString(s string) *Atomic

NewString returns an xs:string.

func NewUntypedAtomic

func NewUntypedAtomic(s string) *Atomic

NewUntypedAtomic returns an xs:untypedAtomic, the type produced by atomising a node in a document that has not been schema-validated.

func (*Atomic) Bool

func (a *Atomic) Bool() bool

Bool returns the boolean value. Valid only for TypeBoolean.

func (*Atomic) DateTimeVal

func (a *Atomic) DateTimeVal() *DateTime

DateTimeVal returns the date/time value, or nil.

func (*Atomic) Derived

func (a *Atomic) Derived() string

Derived returns the narrower XML Schema type this value was constructed as, or "" if it was not built by a derived-type constructor.

func (*Atomic) DerivedMember added in v1.0.0

func (a *Atomic) DerivedMember() string

DerivedMember returns the union member type this value was validated as, or "" when the value's type is not a union.

It is a second answer alongside Derived, not a replacement for it: a value of a union type is an instance of both the union and the selected member.

func (*Atomic) DurationVal

func (a *Atomic) DurationVal() *Duration

Duration returns the duration value, or nil.

func (*Atomic) FitsInt64

func (a *Atomic) FitsInt64() bool

FitsInt64 reports whether the value can be represented as an int64 without wrapping.

xs:integer is arbitrary-precision, so this is a real question: Int64() truncates the big.Int and silently returns a different number, which is worse than refusing.

func (*Atomic) Float64

func (a *Atomic) Float64() float64

Float64 returns the value as a float64 for any numeric type. Decimal and integer values are converted, which may lose precision; callers doing exact arithmetic must use Rat instead.

func (*Atomic) Int64

func (a *Atomic) Int64() int64

Int64 returns the value truncated to an int64. Valid for numeric types.

func (*Atomic) IsNaN

func (a *Atomic) IsNaN() bool

IsNaN reports whether a is a double or float NaN. NaN needs its own check throughout comparison, because it is the one value where the general "compare and negate" shortcut produces wrong answers.

func (*Atomic) QName

func (a *Atomic) QName() *QName

QName returns the QName value, or nil.

func (*Atomic) Rat

func (a *Atomic) Rat() *big.Rat

Rat returns the exact value for integer and decimal types, or nil.

func (*Atomic) Str

func (a *Atomic) Str() string

Str returns the lexical/string content for string-like and date-like types.

func (*Atomic) String

func (a *Atomic) String() string

String returns the XPath 2.0 canonical lexical representation, which is what fn:string and every implicit string conversion must produce. It is not a debug format: the exact spelling of doubles and decimals here is observable in stylesheet output.

func (*Atomic) TypeName

func (a *Atomic) TypeName() string

TypeName implements Item.

func (*Atomic) WithDerived

func (a *Atomic) WithDerived(name string) *Atomic

WithDerived returns a copy of a annotated as the named derived type.

The union member is cleared: re-annotating the value as a different type makes any member recorded for the previous one meaningless, and carrying it forward would let a value claim membership in a union it no longer has.

func (*Atomic) WithDerivedUnion added in v1.0.0

func (a *Atomic) WithDerivedUnion(name, member string) *Atomic

WithDerivedUnion returns a copy of a annotated as the named union type with the named member recorded as the one that accepted it.

type DateTime

type DateTime struct {
	Year   int      // proleptic Gregorian; negative for BCE. No year zero.
	Month  int      // 1-12
	Day    int      // 1-31
	Hour   int      // 0-24 (24 only as the lexical form 24:00:00)
	Minute int      // 0-59
	Second *big.Rat // seconds including fraction, [0,60)

	// TZOffset is the timezone offset in minutes east of UTC.
	// HasTZ distinguishes "no timezone" from "+00:00", which are different
	// values under XML Schema equality.
	TZOffset int
	HasTZ    bool
}

DateTime represents xs:date, xs:time and xs:dateTime.

It is not time.Time. XML Schema dates carry three properties that time.Time cannot express: an optional timezone (distinct from UTC — an unzoned date is a different value from a UTC one), a year range that exceeds int64 nanoseconds, and second values with arbitrary fractional precision. Comparison of unzoned values against zoned ones is defined against an implicit timezone supplied by the dynamic context, which only works if "absent" is representable.

func ParseDateTime

func ParseDateTime(s string, t TypeCode) (*DateTime, error)

ParseDateTime parses the lexical form of xs:date, xs:time or xs:dateTime according to the requested type.

func ParseGregorian

func ParseGregorian(s string, t TypeCode) (*DateTime, error)

ParseGregorian parses the lexical form of one of the five Gregorian types.

Each has its own leading-hyphen convention — "--01" is a month, "---15" a day — which exists so that the forms cannot be confused with a truncated date. Getting the hyphen count wrong silently reinterprets the value, so each form is matched exactly rather than by a permissive scan.

func (*DateTime) Lexical

func (dt *DateTime) Lexical(t TypeCode) string

Lexical returns the canonical lexical form for the given type.

func (*DateTime) ToSeconds

func (dt *DateTime) ToSeconds(implicitTZ int) *big.Rat

ToSeconds returns the value as seconds since 1972-12-31T00:00:00Z, adjusted to UTC using implicitTZ (in minutes) when the value carries no timezone.

Comparison and subtraction are defined on this normalised timeline, so having one conversion point means the timezone rules are applied uniformly rather than re-derived at each comparison site.

type Duration

type Duration struct {
	Negative bool
	Months   int      // years*12 + months
	Seconds  *big.Rat // days*86400 + hours*3600 + minutes*60 + seconds
}

Duration represents xs:duration and its two subtypes.

XML Schema durations have two independent components — months and seconds — that cannot be converted into one another, because the number of days in a month is not fixed. That is why xs:duration is only partially ordered and why the two totally-ordered subtypes (xs:yearMonthDuration and xs:dayTimeDuration) exist. Keeping the components separate rather than normalising to a single scalar is what makes the ordering rules implementable at all.

func ParseDuration

func ParseDuration(s string, t TypeCode) (*Duration, error)

ParseDuration parses the lexical form of xs:duration, xs:yearMonthDuration or xs:dayTimeDuration, rejecting components the requested subtype does not permit.

func (*Duration) Lexical

func (d *Duration) Lexical(t TypeCode) string

Lexical returns the canonical lexical form for the given duration type.

func (*Duration) SignedMonths

func (d *Duration) SignedMonths() int

SignedMonths returns the month component with the sign applied.

func (*Duration) SignedSeconds

func (d *Duration) SignedSeconds() *big.Rat

SignedSeconds returns the second component with the sign applied.

type EntityResolver added in v1.0.0

type EntityResolver interface {
	ResolveEntity(systemID, publicID, base string) (io.ReadCloser, string, error)
}

EntityResolver fetches the resource an external entity or an external DTD subset names.

It is the caller's, deliberately: xdm has no filesystem and no network, so every decision about what may be read — which schemes, which directories, how symlinks resolve — is made in code the caller owns and can audit. A resolver MUST refuse anything it is not certain of; returning an error makes the reference fail, which is the safe outcome.

systemID is the system identifier exactly as the document wrote it, which is usually relative. base is the absolute URI of the entity that contains the reference, against which systemID is to be resolved — note that for an entity declared inside an external DTD subset this is the SUBSET's URI, not the document's, as XML requires.

It returns the resource's content and the absolute URI it resolved to. That URI becomes the base for anything the fetched text itself references, so a resolver must return the URI it actually read, not the one it was asked for.

type Error

type Error struct {
	// Code is the spec error code, such as "XPTY0004". Codes live in the
	// http://www.w3.org/2005/xqt-errors namespace; the local name alone is
	// carried here because it is unique across the specs and is how the
	// documents themselves refer to them.
	Code string
	// Message is the human-readable detail, without the code prefix.
	Message string
	// Err is an underlying cause, if any.
	Err error
}

Error is an XPath, XQuery or XSLT error carrying its specification error code.

The specs define a code for every error condition — XPTY0004 for a type error, FORG0001 for a failed cast, FODC0002 for an unretrievable document — and those codes are the stable, translatable part of an error. A message is prose that may be reworded; a code is what a caller can branch on and what a conformance suite compares.

The codes were already present as string prefixes on every error this engine produces, which reads correctly but cannot be inspected: a caller wanting to distinguish "the document was malformed" from "the stylesheet is wrong" had to match on substrings. This type makes the code a field while keeping the rendered message byte-identical, so nothing that reads error text changes.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

type Item

type Item interface {

	// TypeName returns the QName of the item's type, for error messages and
	// instance-of tests.
	TypeName() string
	// contains filtered or unexported methods
}

Item is a single member of a sequence: either a Node or an atomic value.

The interface is closed to outside implementations (unexported marker method). XDM defines exactly these two kinds of item in XPath 2.0; function items arrive in 3.0 and would be added here.

type Node

type Node struct {
	Kind NodeKind
	Name QName

	// Value is the text content for text, comment, PI and attribute nodes,
	// and the namespace URI for namespace nodes. Element and document nodes
	// derive their string value from descendants; see StringValue.
	Value string

	Parent   *Node
	Children []*Node

	// Attrs and Namespaces hold attribute and namespace nodes for elements.
	// They are kept out of Children because the child axis must not return
	// them — a fact that a single mixed slice makes easy to get wrong.
	Attrs      []*Node
	Namespaces []*Node

	// BaseURI is the resolved base URI, used by fn:document and fn:doc.
	BaseURI string

	// DocumentURI is the data model's dm:document-uri property, which
	// fn:document-uri returns. It is meaningful only on a document node.
	//
	// It is deliberately NOT the same field as BaseURI, and not derived from
	// it. dm:base-uri and dm:document-uri are separate accessors in the XDM,
	// and the difference is observable: dm:document-uri is the absolute URI a
	// document was RETRIEVED BY, so it is empty for any document that was not
	// retrieved by URI at all — a temporary tree built by xsl:variable, a
	// document node constructed by xsl:document, a tree parsed from a string.
	// Those trees still need a base URI, for fn:base-uri and for resolving a
	// relative reference written inside them, so BaseURI on a temporary tree
	// is set on purpose (xslt/runtime.go does this) and cannot double as the
	// document URI. XPath F&O fn:document-uri: "returns the empty sequence if
	// $arg is not a document node, or if the document node was not retrieved
	// via a URI".
	//
	// The invariant a caller must maintain: set this ONLY when the document
	// was fetched by that URI and registered in the document pool, so that
	// fn:doc of this value returns this same node. Setting it on a tree that
	// fn:doc cannot retrieve would make "doc(document-uri($d)) is $d" false
	// while claiming it should be true. Parse sets it from
	// ParseOptions.DocumentURI, which defaults to empty.
	DocumentURI string

	// TypeAnnotation records a schema type when the document has been
	// validated. Untyped documents leave this empty, and atomisation then
	// yields xs:untypedAtomic, which is the schemaless default.
	//
	// It holds an ANNOTATION NAME, which AnnotationName builds and
	// SplitAnnotationName takes apart: a type in the XML Schema namespace
	// keys under its bare local name ("string", "QName"), and any other type
	// under Clark notation, {uri}local. Producers must go through
	// AnnotationName; consumers comparing against a qualified name must use
	// SplitAnnotationName rather than SplitQName, which would cut a Clark key
	// at the colon inside its URI and yield nonsense without an error.
	//
	// The namespace is load-bearing rather than decorative. This string is
	// the key into a process-global derivation table, so a bare local name
	// let one schema's type displace a built-in of the same name for every
	// later schema in the process — see the commentary on derivedPrimitives.
	TypeAnnotation string

	// UnionMember records which member type of a union simple type actually
	// accepted this node's value, when TypeAnnotation names (or has simple
	// content of) a union.
	//
	// It is separate state from TypeAnnotation because the two facts are
	// different and both are needed. XSD 1.0 §3.14.4 makes member selection a
	// property of the *value*, not of the type: "100" validated against
	// union(my:partNumberType, xs:integer) is an xs:integer while "123-AB" is
	// a my:partNumberType, and the same annotation covers both. Folding the
	// winner into TypeAnnotation would answer "instance of xs:integer" at the
	// cost of "instance of my:partIntegerUnion", which the union's own
	// identity requires; keeping only the union answers the second and loses
	// the first. A node must answer both, so both are recorded.
	//
	// It is an annotation name, like TypeAnnotation, and for the same reason:
	// it is compared against, and walked through, the same registries.
	//
	// Empty for every node whose type is not a union, which is almost all of
	// them, so the common path pays only the field.
	UnionMember string

	// IsID and IsIDREFS are the data model's is-id and is-idrefs properties
	// (XDM §5.2, §6.2). They are deliberately *separate* state from
	// TypeAnnotation rather than being derived from it, because XSLT 2.0
	// §3.5 requires them to survive input-type-annotations="strip": that
	// setting turns every annotation into xs:untyped/xs:untypedAtomic while
	// leaving is-id and is-idrefs exactly as they were. Deriving them from
	// the annotation would lose them at precisely the point the
	// specification says they must be kept, and fn:id/fn:idref — which are
	// defined over these properties, not over the annotation — would then
	// find nothing in a stripped document whose ID attributes happen not to
	// be spelled "id".
	//
	// Two bools rather than one enum: an attribute of a union type can in
	// principle be neither, and nothing in the model makes them exclusive.
	// They are set wherever an annotation is assigned (schema assessment,
	// DTD attribute types) by whoever knows the declared type; a node whose
	// type was never determined leaves both false, which is the correct
	// answer for an unvalidated document.
	IsID     bool
	IsIDREFS bool
	// contains filtered or unexported fields
}

Node is a node in an XDM tree.

This is a concrete struct rather than an interface. Every node kind shares most of its fields, the evaluator switches on Kind rather than dispatching, and the axes need to walk parent/sibling links tens of thousands of times per document — an interface would add a pointer chase and an indirect call to each step for no expressiveness gained.

Trees are built by the parser in this package and are immutable afterwards. That immutability is what makes it safe to share one compiled stylesheet tree across concurrent transforms.

func (*Node) AddAttr

func (n *Node) AddAttr(a *Node)

AddAttr links a as an attribute of n.

func (*Node) AddNamespace

func (n *Node) AddNamespace(prefix, uri string)

AddNamespace links a namespace node to n.

func (*Node) AppendChild

func (n *Node) AppendChild(c *Node)

AppendChild links c as the last child of n, setting the parent link. It does not assign document order; call Finalize once the tree is complete.

func (*Node) Atomize

func (n *Node) Atomize() *Atomic

Atomize returns the typed value of a node. Without schema validation every node atomises to xs:untypedAtomic, which is what makes untyped comparison rules apply throughout a schemaless transform.

func (*Node) AtomizeList added in v1.0.0

func (n *Node) AtomizeList() (Sequence, bool)

AtomizeList returns the typed value of a node whose annotation is a list type, as one atomic value per whitespace-separated token.

The second result reports whether the annotation is in fact a list type; a caller that gets false must fall back to Atomize, which yields the single value that every non-list node has.

Only the three built-in list types are recognised here. A user-defined list type is registered by the schema layer with its item type, and that derivation chain is what DerivedBase walks; a list type derived by restriction from one of these three therefore resolves to it and is expanded with its item type.

The empty string atomizes to the empty sequence rather than to one zero-length token, which is what "a list of no items" means and what strings.Fields already produces.

func (*Node) Attr

func (n *Node) Attr(uri, local string) *Node

Attr returns the attribute node with the given expanded name, or nil.

func (*Node) AttrValue

func (n *Node) AttrValue(local string) string

AttrValue returns the value of a no-namespace attribute, or "". Most attributes the stylesheet compiler reads (match, select, name, test) are unprefixed, so this is the common case worth a helper.

func (*Node) ChildElements

func (n *Node) ChildElements() []*Node

ChildElements returns the element children, which is what almost every stylesheet-compilation walk wants.

func (*Node) Compare

func (n *Node) Compare(o *Node) int

Compare orders two nodes in document order, returning -1, 0 or 1. Nodes in different trees are ordered by tree id, which is stable within a transform.

func (*Node) InScopeNamespaces

func (n *Node) InScopeNamespaces() map[string]string

InScopeNamespaces returns every prefix-to-URI binding visible at n, with inner declarations shadowing outer ones. Used when copying elements and when resolving QNames in stylesheet attribute values.

func (*Node) IsElement

func (n *Node) IsElement(uri, local string) bool

IsElement reports whether n is an element with the given expanded name.

func (*Node) LookupPrefix

func (n *Node) LookupPrefix(prefix string) (string, bool)

LookupPrefix resolves a namespace prefix against the in-scope namespaces of n, walking up the tree. Returns the URI and whether the prefix was bound.

func (*Node) Order

func (n *Node) Order() int

Order returns a number that identifies the node uniquely within the process.

It is the document-order index within the node's own tree, combined with the tree's identity so that nodes from two documents cannot collide. Callers wanting relative position must use Compare: this value orders nodes within one tree but says nothing across trees.

The combination is what fn:generate-id() needs. Returning the bare per-tree index gave the same answer to the first node of every document, so a stylesheet comparing generated identities across documents — the case key-042 in the XSLT suite exists to check — saw distinct nodes as identical.

A tree built by a sequence constructor is never finalized and has no tree of its own; those nodes take the identity assigned on demand to their root, the same one cross-tree comparison uses, so two parentless elements are also distinguished.

func (*Node) Position

func (n *Node) Position() (line, col int, ok bool)

Position returns the 1-based line and column where the node starts, and false if the position is unknown — the node was built by a transform rather than parsed, or the source text was not retained.

func (*Node) Root

func (n *Node) Root() *Node

Root returns the root of the containing tree, walking parent links. For a well-formed parsed document this is the document node.

func (*Node) SetSynthesizedOrder added in v1.0.0

func (n *Node) SetSynthesizedOrder(owner *Node, offset int)

SetSynthesizedOrder places a node the parser did not build into the document order of an existing tree, immediately after owner.

The namespace axis is the case this exists for: its nodes are synthesized on demand from the in-scope bindings, so they have no order of their own. Left at zero they sort before every real node, and — because generate-id() is derived from the order — every one of them answers "N0", colliding with each other and with the document node.

The offset separates the bindings of one element from each other while keeping them all adjacent to their owner. It is deliberately not an attempt at a spec-defined position: XPath leaves the relative order of namespace nodes implementation-dependent, and what a caller needs is that the order is stable and the identities distinct.

func (*Node) SetTypeAnnotation added in v1.0.0

func (n *Node) SetTypeAnnotation(annotation string)

SetTypeAnnotation records a type annotation and the is-id / is-idrefs properties that go with it.

It exists so that every producer of annotations — schema assessment, DTD attribute types, the XSLT validation instructions — sets the two properties the same way. Assigning TypeAnnotation directly is still legal but leaves is-id and is-idrefs at whatever they were, which is what a caller deliberately preserving them across a strip wants and what a caller annotating a fresh node does not.

The properties are only ever turned *on* here. A node that was already marked keeps its marking when re-annotated with a non-ID type, because the data model's properties describe how the node was validated originally and XSLT's stripping rules are the only thing entitled to change them — and those rules say the properties do not change at all.

func (*Node) StringValue

func (n *Node) StringValue() string

StringValue returns the node's string value per XDM: the concatenation of all descendant text for document and element nodes, and the value itself for the leaf kinds.

func (*Node) Tree

func (n *Node) Tree() *Tree

Tree returns the containing tree.

func (*Node) TypeName

func (n *Node) TypeName() string

TypeName implements Item.

type NodeKind

type NodeKind int

NodeKind enumerates the seven node kinds of the XDM.

const (
	KindDocument NodeKind = iota
	KindElement
	KindAttribute
	KindText
	KindComment
	KindPI
	KindNamespace
)

func (NodeKind) String

func (k NodeKind) String() string

type Opaque

type Opaque struct {
	// Label names the kind of value, for error messages.
	Label string
	// Value is the wrapped payload.
	Value any
}

Opaque wraps an arbitrary Go value as an Item.

It exists so that layers above this package can thread their own state through an evaluation context, which binds sequences rather than typed fields. The XSLT engine uses it for the transform runtime and grouping state, which the xpath package cannot name without an import cycle.

An Opaque is not a legal XDM value: it has no string value, does not atomise, and must never reach a stylesheet. Every producer binds it under a reserved namespace that no stylesheet can spell.

func (*Opaque) TypeName

func (o *Opaque) TypeName() string

TypeName implements Item.

type ParseOptions

type ParseOptions struct {
	// BaseURI is recorded on the document node and used to resolve relative
	// references in fn:document and xsl:include.
	BaseURI string

	// DocumentURI is recorded on the document node as its dm:document-uri
	// property, which is what fn:document-uri returns. It is separate from
	// BaseURI because the two accessors are separate in the data model: see
	// Node.DocumentURI for why they cannot be the same field.
	//
	// It defaults to empty, which is the right answer for every caller that
	// is parsing something it did not retrieve by URI — a stylesheet string,
	// a re-parsed entity expansion, a test fixture. A caller that DID fetch
	// the document from a URI, and that registers it in a document pool so
	// that fn:doc of the same URI returns this same tree, sets it to that URI.
	DocumentURI string

	// StripSpace removes whitespace-only text nodes. XSLT applies this per
	// element name via xsl:strip-space, so the transform layer passes a
	// predicate; a plain bool here would not express "strip in these elements
	// only".
	StripSpace func(elem QName) bool

	// AllowDOCTYPE permits a DOCTYPE declaration. It defaults to false: a
	// DOCTYPE is the entry point for both XXE (parser-executed file:// and
	// http:// reads) and entity-expansion blowup, and a validator that
	// happily expands entities from untrusted input is a liability. Callers
	// that genuinely need DTD-declared entities opt in explicitly.
	AllowDOCTYPE bool

	// ExternalEntities permits external entities — those declared SYSTEM or
	// PUBLIC, and an external DTD subset — to be read, by supplying the
	// resolver that reads them.
	//
	// It is nil by default, and nil means every external entity is refused
	// exactly as before. It is deliberately SEPARATE from AllowDOCTYPE and
	// is not implied by it: AllowDOCTYPE admits a DOCTYPE and its internal
	// declarations, which cost nothing outside the document, while this
	// admits reads of other resources — the XXE surface proper. A caller
	// that wants entity declarations does not thereby want file reads.
	//
	// xdm has no filesystem and no network, so it can only read what a
	// resolver hands it. Confinement — permitted schemes, permitted
	// directories, symlink resolution — is entirely the resolver's, and
	// xslt.FileResolver implements it. Expansion remains bounded by this
	// package: fetched bytes are charged to the document's shared budget
	// before they are expanded, and the number and nesting of fetches are
	// capped. See xdm/dtd_external.go.
	ExternalEntities EntityResolver

	// TrackPositions records where each element starts, so that a validator
	// can report the line a failure occurred on. It retains the source text
	// for the life of the tree, which measures at about 10% more memory on a
	// typical invoice and no extra parse time. It is opt-in because that cost
	// buys nothing for a caller that never asks for a position.
	TrackPositions bool

	// MaxDepth bounds nesting. Deeply nested input is the cheapest way to
	// drive a recursive descent into stack exhaustion, so the limit is
	// enforced during construction rather than left to the runtime.
	MaxDepth int

	// MaxBytes bounds the source document. Zero means DefaultMaxBytes;
	// a negative value means no limit, for a caller reading input it
	// produced itself.
	MaxBytes int64

	// MaxNodes bounds the tree. Zero means DefaultMaxNodes; a negative
	// value means no limit.
	//
	// Both limits exist because neither alone is a memory bound. A node
	// costs a fixed ~200 bytes whatever it contains, so the heap a document
	// needs depends on how many nodes it has rather than how long it is:
	// a megabyte of "<a/>" is fifty times the memory of a megabyte of text.
	// MaxBytes bounds the read; MaxNodes bounds what the read can allocate.
	MaxNodes int
	// contains filtered or unexported fields
}

ParseOptions controls document construction.

type QName

type QName struct {
	Prefix string
	URI    string
	Local  string
}

QName is an expanded name: namespace URI plus local part, with the prefix retained only for serialisation.

Equality in XPath is defined on (URI, Local) alone — the prefix is not part of the value — so Equal deliberately ignores Prefix. Keeping the prefix around anyway matters because a literal result element must be serialised with the prefix the stylesheet author wrote, not one we invent.

func (QName) Clark

func (q QName) Clark() string

Clark returns the {uri}local form, which is unambiguous without a namespace context and is therefore what error messages and map keys use.

func (QName) Equal

func (q QName) Equal(o QName) bool

Equal reports QName equality per XPath: namespace URI and local name, prefix ignored.

func (QName) Lexical

func (q QName) Lexical() string

Lexical returns the prefix:local form used for serialisation, or just the local name when there is no prefix.

type Sequence

type Sequence []Item

Sequence is an ordered list of items. The empty sequence is a nil or zero-length slice; both are treated identically by every operation, so callers never have to normalise before comparing.

A sequence is flat: XDM has no nested sequences, and every constructor in this package maintains that invariant.

func Atomize

func Atomize(seq Sequence) Sequence

Atomize converts a sequence to atomic values, replacing each node with its typed value. This is the fn:data() operation, applied implicitly wherever XPath 2.0 requires atomic operands.

Every item in the result is an *Atomic. Callers rely on that: two dozen of them assert the type without checking, because within the data model there is nothing else atomisation can produce.

Opaque items are the exception, and they are dropped here rather than passed through. They carry engine-internal state — the transform runtime, grouping bookkeeping — through the closed Item interface, and a stylesheet that names the internal namespace could reach one:

xmlns:gi="urn:goxslt:internal" ... distinct-values($gi:runtime)

Passing it through made that expression panic with an interface-conversion error, which in a server embedding this engine is a denial of service triggered by stylesheet text. An Opaque has no typed value, so dropping it is also what the data model implies: it is not a node and not an atomic value, so fn:data has nothing to return for it.

func Concat

func Concat(seqs ...Sequence) Sequence

Concat joins sequences, preserving order and flatness.

func Empty

func Empty() Sequence

Empty is the canonical empty sequence.

A function rather than a variable. An exported package-level var of slice type is writable by anyone who imports the package, and a single stray assignment would corrupt the value every other caller reads -- a process-wide fault with no owner and no way to detect it. The value is nil, so this compiles to nothing.

func Except

func Except(a, b Sequence) Sequence

Except returns the nodes of a that are not in b, in document order.

func Intersect

func Intersect(a, b Sequence) Sequence

Intersect returns the nodes present in both sequences, in document order.

func One

func One(it Item) Sequence

One wraps a single item as a sequence. Named for how often it is needed: most XPath operations produce exactly one item and must still return a sequence.

func SortDocumentOrder

func SortDocumentOrder(seq Sequence) Sequence

SortDocumentOrder sorts a sequence of nodes into document order and removes duplicates.

Every path expression in XPath 2.0 returns nodes in document order with duplicates removed, and so do the union, intersect and except operators. Doing it in one place means the axis implementations can emit nodes in whatever order is natural for them (reverse axes emit backwards) without each having to re-sort.

Items that are not nodes are an error at the call sites that use this, so they are passed through unsorted rather than silently dropped; the caller type-checks first.

func Union

func Union(a, b Sequence) Sequence

Union returns the document-ordered union of two node sequences.

func (Sequence) First

func (s Sequence) First() Item

First returns the first item, or nil if the sequence is empty. Callers that require exactly one item should use Single instead so that a length > 1 is reported rather than silently truncated.

func (Sequence) IsEmpty

func (s Sequence) IsEmpty() bool

IsEmpty reports whether s has no items.

func (Sequence) Single

func (s Sequence) Single() (Item, error)

Single returns the sole item of a one-item sequence. It reports an error for any other length, because the places that call it (operands of arithmetic, the argument of a function declared to take exactly one item) are precisely the places where XPath 2.0 raises XPTY0004 rather than coercing.

type Tree

type Tree struct {
	Root *Node
	// DocType is the DOCTYPE declaration's text, when the document had one
	// and AllowDOCTYPE permitted it. Empty otherwise.
	//
	// It is retained because the internal subset is the only place a
	// document's own DTD lives, and validating against it needs the text —
	// encoding/xml hands the declaration over as one opaque token and keeps
	// nothing. The dtd package parses it; this package applies only the two
	// declarations whose absence is visible in the data model.
	DocType string
	// contains filtered or unexported fields
}

Tree owns a document and the counter used to assign document order.

func NewTree

func NewTree() *Tree

NewTree creates an empty tree with a document node as its root.

func Parse

func Parse(r io.Reader, opts ParseOptions) (*Tree, error)

Parse builds an XDM tree from an XML document.

It uses encoding/xml as a tokeniser only. The Go decoder's own namespace handling is not usable here: it resolves prefixes into Name.Space but discards the prefix and the declarations themselves, and XSLT needs both — namespace nodes are addressable on the namespace axis, and a literal result element must be serialised with the prefix the author wrote.

func ParseString

func ParseString(s string, opts ParseOptions) (*Tree, error)

ParseString is Parse over a string, which is what most tests and the stylesheet compiler want.

func (*Tree) Finalize

func (t *Tree) Finalize()

Finalize assigns document-order indices across the whole tree in a single pre-order walk. It must be called after the tree is fully built and before any node comparison; every parser entry point in this package does so.

func (*Tree) UnparsedEntity added in v1.0.0

func (t *Tree) UnparsedEntity(name string) (systemID, publicID, notation string, ok bool)

UnparsedEntity returns the system identifier and notation of an unparsed entity declared in a document's internal subset.

An unparsed entity is the one kind a processor never reads: it is declared SYSTEM or PUBLIC with an NDATA notation, referenced from an attribute of type ENTITY, and its identifier is data for the application rather than something to fetch. fn:unparsed-entity-uri and fn:unparsed-entity-public-id return exactly these.

The declarations are re-read from the retained DOCTYPE text rather than carried on every tree, since a document with unparsed entities is rare and the lookup happens at most once per call.

type TypeCode

type TypeCode int

TypeCode identifies an atomic type from the XML Schema built-in hierarchy.

Only the types XPath 2.0 gives special treatment are enumerated. The rest of the schema hierarchy (xs:token, xs:NMTOKEN, and the other string subtypes) behaves identically to its base type for every operation this engine performs, so carrying them as distinct codes would add branches with no behavioural difference.

const (
	// TypeUntypedAtomic is the type of atomised nodes in a schemaless
	// document. It is the reason XPath 2.0 needs so few explicit casts: an
	// untypedAtomic operand is converted to the required type at the point of
	// use, but *only* in the specific contexts the spec lists.
	TypeUntypedAtomic TypeCode = iota
	TypeString
	TypeBoolean
	TypeDecimal
	TypeInteger
	TypeDouble
	TypeFloat
	TypeQName
	TypeAnyURI
	TypeDate
	TypeTime
	TypeDateTime
	TypeDuration
	TypeYearMonthDuration
	TypeDayTimeDuration
	TypeHexBinary
	TypeBase64Binary
	// The five Gregorian types denote a recurring or partial calendar point:
	// a year, a year and month, a month, a month and day, or a day.
	TypeGYear
	TypeGYearMonth
	TypeGMonth
	TypeGMonthDay
	TypeGDay
)

func NumericPromote

func NumericPromote(a, b TypeCode) TypeCode

NumericPromote returns the common type for a binary numeric operation, per the XPath 2.0 promotion lattice: integer -> decimal -> float -> double. Both operands are converted to that type before the operation runs.

func (TypeCode) IsNumeric

func (t TypeCode) IsNumeric() bool

IsNumeric reports whether t is one of the four numeric types. Numeric operands are promoted to a common type before arithmetic and comparison, which is what NumericPromote implements.

func (TypeCode) String

func (t TypeCode) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL