cargoxml

package module
v0.5.1 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 6 Imported by: 0

README

cargoxml

Parse and rewrite XML preserving anything foreign to your spec — forward compatibility and free room for auxiliary data. Use with github.com/pablo-botella/cargoxml.

Go Reference

Install

go get github.com/pablo-botella/cargoxml    # library

Overview

cargoxml reads and writes XML on top of encoding/xml — same tokens, same escaping, same well-formedness — adding the one thing the stdlib drops: content foreign to your specification survives — unknown attributes and elements, third-party extensions, newer-version fields — plus the document's comments and formatting.

That buys you two things: forward compatibility (documents from newer or richer specs flow through your tool without loss) and free housing for auxiliary data (annotations and tooling extras live in the document without your types modeling them).

Example: in your Go ecosystem, mkskill can ride piggyback inside miniskin's XML files using mkskill- prefixed attributes — miniskin processes what it knows, the rest travels in the cargo and survives every rewrite.

  • Reading: your types implement XmlTokenConsumer and claim what they know. Everything else — comments, whitespace, processing instructions, unknown attributes and children — is preserved: positioned as trails and stored in each consumer's cargo (or parsed as GenericXmlItem for fully unclaimed subtrees).
  • Writing: types stream themselves as tokens (XmlTokenProducer). The easy path is describing yourself (XmlDescribeWithCargo + DescribedTokens): the cargo is woven back in automatically.

The philosophy, everywhere: easy is easy; needing more means writing your own XmlTokens stream. The package knows nothing about your types — and doesn't want to: it speaks interfaces and stdlib tokens only.

Reading — DecoderWithCargo

d := cargoxml.NewDecoderWithCargo(xml.NewDecoder(r))
d.Root = myRootConsumer // nil → the whole document parses as GenericXmlItem
err := d.Parse()

The decoder walks the token stream and dispatches events to XmlTokenConsumer:

  • OnXmlChildStart — the parent decides who consumes each child (setting child.Consumer). Unclaimed children fall to the generic fallback, or are skipped entirely when SkipUnknownChildren is set (the flag inherits from the parent frame; the parent can override it per child).
  • OnXmlStart / OnXmlEnd — the element's own lifecycle; at OnXmlEnd its trails are already positioned and its cargo complete.
  • OnXmlAttribute — claim it (true) or let it fall to the cargo (false).
  • OnXmlChildEnd — the parent sees each closed child; the child frame's trails are positioned (harvest text content here).
  • GetCargoXml — return a *CargoXml to preserve the unclaimed (attributes, generic children, trails); nil to discard it.

Embed NullXmlConsumer and override only what you need. The decoder-level hooks OnRootStart/OnRootEnd cover the prolog and the document level.

The decoder's Context field accompanies the run: every callback can reach it (decoder.Context) for application parameters, and Parse honors its cancellation between tokens. nil means context.Background().

Trails — where foreign content lives

A Trail is one non-element token: whitespace, text, comment, processing instruction or directive. Each trail belongs to exactly one element, with a position:

  • Before — it announced the element ("a comment belongs to what follows"): anything between the previous sibling (or the parent's start tag) and this element.
  • Inner — inside the element, after its last child (an element with only text keeps that text here).
  • After — only the root ever has them: the epilog after the document element.

The single-owner rule makes re-emission deterministic: the prolog is the root's Before, a comment between siblings is the next sibling's Before, and trailing content is the parent's Inner.

Writing — producers and the encoder

Breaking change (v0.0.2, breaks v0.0.1): XmlTokens and every XmlDescribe* question now receive the run's context.Context, and DescribedTokens takes it as its first argument.

The boundary is one method — tokens out, the run's context in:

type XmlTokenProducer interface {
	XmlTokens(ctx context.Context) iter.Seq[xml.Token]
}

The easy path is describing yourself — plain data in, the helper assembles the stream:

type XmlDescribeWithCargo interface {
	XmlDescribeNodeName(ctx context.Context) xml.Name
	XmlDescribeNodeType(ctx context.Context, policy MixedNodePolicy) XmlNodeType // what will you serialize: Mixed (zero) | Text | Container
	XmlDescribeAttributes(ctx context.Context) []xml.Attr
	XmlDescribeInitialComments(ctx context.Context) []string
	XmlDescribeText(ctx context.Context) []string            // leaf: either text…
	XmlDescribeItems(ctx context.Context) []XmlTokenProducer // …or nodes (Items adapts typed slices)
	GetCargoXml() *CargoXml                                  // nil when nothing extra to preserve
}

func (p *Product) XmlTokens(ctx context.Context) iter.Seq[xml.Token] {
	return cargoxml.DescribedTokens(ctx, p, cargoxml.PreserveMixed)
}

Every answer that can be absent is a slice: nil means "I don't have that". The declared node type is how a node is known before serializing it — a token stream cannot be asked whether it has children. The policy argument is the caller's preference; the answer is the type's decision. Mixed (the zero value) changes nothing: the element emits what it wants and the cargo emits what it has. A strict answer (Text/Container) filters the type's own answers and knowingly kills the part of the cargo that does not correspond — never by accident: the type decided with the cargo in hand. A Text node drops the child elements, a Container drops the inner text; attributes always survive, and comment policy lives in another layer (the encoder's SkipComments). DescribedTokens emits in fixed order: cargo Before trails → initial comments → start tag (own + cargo attributes) → text runs → items → cargo children → cargo Inner trails → end tag → cargo After trails. Text and items together are allowed, but there is no interleaving — the base serializer does not organize content. Need more? Build your own XmlTokens stream.

Encoding is a pipe into the stdlib:

enc := xml.NewEncoder(w)
e := cargoxml.NewEncoderWithCargo(enc)
e.SkipWhiteSpace = true // optional: drop the preserved formatting
err := e.Encode(root)   // remember enc.Flush() afterwards

SkipWhiteSpace alone minifies; combined with enc.Indent it reformats — comments, PIs and real text survive, and adjacent text fragments count as one unit. SkipComments drops every comment. MixedNodePolicy decides mixed elements (real text and children under the same parent): PreserveMixed (default) emits everything, PreserveChildren drops the text of mixed elements, PreserveText drops their child elements. All defaults preserve: the document round-trips as is.

The run's context

Both DecoderWithCargo and EncoderWithCargo carry a Context context.Context field — free room for application parameters that the package never looks inside (nil means context.Background()). On the reading side every consumer callback can reach it through the decoder; on the writing side Encode injects it into the producer chain, so every XmlTokens and every describe question receives it at any depth. That makes an output variation a property of the run, not of your model:

e := cargoxml.NewEncoderWithCargo(enc)
e.Context = WithDebug(nil, true) // your own context values
e.Encode(project)                // same tree, debug output

// and in a describe answer, at any level:
if IsDebug(ctx) { items = append(items, extraDebugItems...) }

Cancellation is honored too: Parse and Encode check the context between tokens, so a timeout or a cancel() aborts a run cleanly (context.Canceled / context.DeadlineExceeded).

Autonomy scales in three tiers: producing is free (a pure XmlTokenProducer decides what it emits, no questions asked), describing is a pact (DescribedTokens enforces the type's own declaration), and emitting is governed (the encoder's policies apply to every stream alike — described, generic or hand-rolled).

GenericXmlItem and CargoXml

GenericXmlItem is the reference implementation of both interfaces and the decoder's fallback: name, attributes, children and trails in plain fields. An untouched generic parse re-encodes to a semantically identical document.

CargoXml is the package's only storage contract — what a consumer preserved without claiming: MoreAttributes, MoreChildren (generic items, already producers) and Trails. Everything else about your types is your own business: the package neither knows nor cares.

Example — edit without losing anything

product.xml, written by hand or by some other tool:

<product sku="A1" price="9.99" currency="EUR">
  <!-- bestseller -->
  <name>Coffee</name>
</product>

A type that models only sku and price, and preserves the rest:

type Product struct {
	cargoxml.NullXmlConsumer
	Cargo      *cargoxml.CargoXml
	Sku, Price string
}

// reading: claim what you know, keep a cargo for the rest
func (p *Product) GetCargoXml() *cargoxml.CargoXml {
	if p.Cargo == nil {
		p.Cargo = cargoxml.NewCargoXml()
	}
	return p.Cargo
}

func (p *Product) OnXmlAttribute(d *cargoxml.DecoderWithCargo, a *xml.Attr) bool {
	switch a.Name.Local {
	case "sku":
		p.Sku = a.Value
		return true
	case "price":
		p.Price = a.Value
		return true
	}
	return false // unclaimed → the cargo
}

// writing: describe yourself; Mixed keeps the cargo whole
func (p *Product) XmlDescribeNodeName(ctx context.Context) xml.Name { return xml.Name{Local: "product"} }
func (p *Product) XmlDescribeNodeType(ctx context.Context, policy cargoxml.MixedNodePolicy) cargoxml.XmlNodeType {
	return cargoxml.XmlMixedNode
}
func (p *Product) XmlDescribeAttributes(ctx context.Context) []xml.Attr {
	return []xml.Attr{
		{Name: xml.Name{Local: "sku"}, Value: p.Sku},
		{Name: xml.Name{Local: "price"}, Value: p.Price},
	}
}
func (p *Product) XmlDescribeInitialComments(ctx context.Context) []string          { return nil }
func (p *Product) XmlDescribeText(ctx context.Context) []string                     { return nil }
func (p *Product) XmlDescribeItems(ctx context.Context) []cargoxml.XmlTokenProducer { return nil }

func (p *Product) XmlTokens(ctx context.Context) iter.Seq[xml.Token] {
	return cargoxml.DescribedTokens(ctx, p, cargoxml.PreserveMixed)
}

Parse, edit, rewrite:

product := &Product{}
d := cargoxml.NewDecoderWithCargo(xml.NewDecoder(in))
d.Root = product
if err := d.Parse(); err != nil { /* … */ }

product.Price = "10.99"

enc := xml.NewEncoder(out)
if err := cargoxml.NewEncoderWithCargo(enc).Encode(product); err != nil { /* … */ }
enc.Flush()

The output keeps everything the type never modeled:

<product sku="A1" price="10.99" currency="EUR">
  <!-- bestseller -->
  <name>Coffee</name>
</product>

The runnable version of this and every other pattern — hand-rolled producers, fresh authored types, reformatting, mixed-node policies — lives under test/.

Limits — equivalent, not byte-identical

Both ends are deliberately the concrete stdlib types (*xml.Decoder / *xml.Encoder): this package sits on top of encoding/xml and does not reinvent that wheel. Output is well-formed and semantically equivalent, never byte-faithful:

  • <a/> comes out as <a></a>; escaping is normalized; line endings become LF.
  • Namespaces are rewritten the stdlib way (URLs, not the original prefixes).
  • CDATA sections come back as escaped text: the stdlib neither marks them when decoding nor writes them at token level.
  • The interleaving between claimed and unclaimed children is not recorded: cargo children re-emit after the owner's.

License

MIT — see LICENSE.

Documentation

Overview

Package cargoxml reads and writes XML on top of encoding/xml, preserving what you didn't model: unclaimed attributes, children, comments and whitespace survive as trails and cargo, ready to be rewritten.

Easy is easy; needing more means writing your own XmlTokens stream.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DescribedTokens

func DescribedTokens(ctx context.Context, d XmlDescribeWithCargo, policy MixedNodePolicy) iter.Seq[xml.Token]

DescribedTokens is the helper that turns a described type into its token stream. It first asks for everything — plain arrays of strings and attributes, the items as one producer via Items, the cargo if any — and then assembles the emission, weaving the cargo in:

cargo Before trails
initial comments
<name  own attributes + cargo.MoreAttributes>
    own text runs
    own items...
    cargo.MoreChildren...
    cargo Inner trails
</name>
cargo After trails   — only a root ever has them

Answering both text and items is allowed, but the order is fixed — all the text, then all the items — with no interleaving: this base serializer does not try to determine how content is organized.

Known limitation: the original interleaving between claimed and unclaimed children was not recorded while reading, so the unclaimed ones are emitted after the owner's — semantically equivalent, not byte-faithful.

In both cases the way out is the same: if you need more, you can always build your own custom XmlTokens stream.

The context is the run's context (Encode injects the encoder's): every describe question receives it, and it propagates to the item streams.

Types

type CargoXml

type CargoXml struct {
	MoreAttributes []xml.Attr        // attributes the consumer did not claim
	MoreChildren   []*GenericXmlItem // unclaimed children, parsed generically — producers ready to re-emit
	Trails         *Trails           // the element's preserved trails, already positioned
}

CargoXml is the package's storage contract: everything a consumer preserved without claiming while its element was parsed. The decoder fills it (when GetCargoXml returns one) and DescribedTokens weaves it back on writing, so foreign content survives a rewrite untouched.

func NewCargoXml

func NewCargoXml() *CargoXml

NewCargoXml creates an empty cargo with its Trails ready. Consumers that preserve return one of these from GetCargoXml (create it lazily there) and the decoder does the rest.

type DecoderStack

type DecoderStack []*DecoderStackFrame

DecoderStack is the decoder's stack of open elements, innermost last.

func (DecoderStack) Current

func (stack DecoderStack) Current() *DecoderStackFrame

Current returns the innermost frame, or nil if the stack is empty.

func (DecoderStack) Level

func (stack DecoderStack) Level() int

Level returns the number of open elements.

func (DecoderStack) Parent

func (stack DecoderStack) Parent() *DecoderStackFrame

Parent returns the frame enclosing the current one, or nil.

func (*DecoderStack) Pop

func (stack *DecoderStack) Pop() *DecoderStackFrame

Pop closes the innermost level and returns its frame, or nil if empty.

func (*DecoderStack) Push

func (stack *DecoderStack) Push(frame *DecoderStackFrame)

Push opens a new level with the given frame.

type DecoderStackFrame

type DecoderStackFrame struct {
	Consumer            XmlTokenConsumer // who consumes this element; assigned by the parent, the generic fallback at minimum
	Trails              *Trails          // the element's trails: pending while open, positioned (Before/Inner) by the decoder
	NodeName            *xml.Name        // the element's qualified name
	Cargo               *CargoXml        // the consumer's cargo (from GetCargoXml); nil for consumers that discard
	SkipUnknownChildren bool             // if true, unclaimed children are skipped instead of parsed as GenericXmlItem; inherited from the parent frame, overridable per child in OnXmlChildStart
	// contains filtered or unexported fields
}

DecoderStackFrame holds the parsing state of one open element. Consumers receive it in every event: it is where the parent assigns the child's Consumer, where the trails accumulate, and where the cargo gets wired.

func (*DecoderStackFrame) AddTrails

func (frame *DecoderStackFrame) AddTrails(trails *Trails, mask TrailType, position TrailPosition) *Trails

AddTrails moves the trails matching the type mask into the frame's own trails, stamping them with the given position, and returns the remainder — a positioning helper for consumers that reorganize trails in hooks like OnRootStart.

func (*DecoderStackFrame) RequireCargo

func (frame *DecoderStackFrame) RequireCargo() *CargoXml

RequireCargo returns the frame's Cargo, creating it if needed.

func (*DecoderStackFrame) RequireTrails

func (frame *DecoderStackFrame) RequireTrails() *Trails

RequireTrails returns the frame's Trails, creating it if needed.

type DecoderWithCargo

type DecoderWithCargo struct {
	// Decoder is the stdlib decoder the tokens come from — deliberately
	// the concrete type: this package sits on top of encoding/xml.
	Decoder *xml.Decoder

	// Trails collects the document-level trails: the prolog while no
	// element is open, the epilog after the root closes (moved to
	// RootFrame as After trails at EOF).
	Trails *Trails

	// Root is the consumer for the root element; nil parses the whole
	// document as a GenericXmlItem.
	Root XmlTokenConsumer

	// RootFrame keeps the root's frame after it closes — where the epilog
	// lands, and how the parsed tree is reached after Parse (its Consumer
	// holds the root's consumer). Also the second-root guard.
	RootFrame *DecoderStackFrame

	// OnRootStart, if set, runs when the root element opens and is
	// responsible for claiming the prolog trails from Trails — unclaimed
	// prolog trails are discarded. When nil, the whole prolog goes to the
	// root frame as Before trails.
	OnRootStart func(decoder *DecoderWithCargo, root *DecoderStackFrame) error

	// OnRootEnd, if set, runs right after the root element closes (its
	// consumer has already received OnXmlEnd).
	OnRootEnd func(decoder *DecoderWithCargo, root *DecoderStackFrame) error

	// Stack is the stack of open elements, innermost last.
	Stack DecoderStack

	// Context accompanies the run: every consumer callback can reach it
	// through the decoder, and Parse honors its cancellation between
	// tokens. Free room for application parameters — the package never
	// looks inside. nil means context.Background().
	Context context.Context
}

DecoderWithCargo drives an xml.Decoder over a tree of XmlTokenConsumers: the parent decides who consumes each child, the unclaimed is preserved (trails, cargo, generic fallback), and Parse walks the whole document.

func NewDecoderWithCargo

func NewDecoderWithCargo(decoder *xml.Decoder) *DecoderWithCargo

NewDecoderWithCargo creates a new DecoderWithCargo for the given xml.Decoder.

func (*DecoderWithCargo) Parse

func (d *DecoderWithCargo) Parse() error

Parse consumes the whole document, dispatching to the consumers.

type EncoderWithCargo

type EncoderWithCargo struct {
	Encoder *xml.Encoder

	// SkipWhiteSpace drops whitespace-only text: the preserved original
	// formatting is discarded. Alone it minifies; combined with
	// Encoder.Indent it reformats (comments, PIs and real text survive).
	// Adjacent CharData fragments count as one text unit — a whitespace
	// fragment inside real text (fragmented text runs, text split by
	// CDATA) is kept, and the unit is emitted coalesced. Caveat: an
	// element whose significant content is only whitespace loses it too.
	// Default false: the preserved format is emitted as is.
	SkipWhiteSpace bool

	// SkipComments drops every comment token on emission. Default false:
	// comments are preserved.
	SkipComments bool

	// MixedNodePolicy decides what happens to mixed elements — real text
	// and child elements under the same parent. PreserveMixed (default)
	// emits everything; PreserveChildren drops the text of mixed
	// elements; PreserveText drops their child elements. The cost is a
	// flag per open element plus holding the undecided run (text, or
	// candidate subtrees under PreserveText) until the element proves
	// mixed or closes.
	MixedNodePolicy MixedNodePolicy

	// Context accompanies the run: Encode injects it into the producer
	// chain (every XmlTokens receives it) and honors its cancellation
	// between tokens. Free room for application parameters — the package
	// never looks inside. nil means context.Background().
	Context context.Context

	// Raw, when set, restores byte-faithful formatting: whitespace-only
	// text — the preserved indentation — bypasses the xml.Encoder (which
	// would escape tabs as &#x9;) and is written verbatim here, after a
	// Flush so the stream order holds. Point it at the same writer the
	// xml.Encoder wraps. Safe by construction: whitespace between tokens
	// cannot break well-formedness. Pointless combined with SkipWhiteSpace
	// (the formatting is dropped before it gets here) and not meant for
	// Encoder.Indent reformatting. nil (default): every token goes through
	// the stdlib encoder, as always.
	Raw io.Writer
}

EncoderWithCargo pipes producers into an xml.Encoder.

func NewEncoderWithCargo

func NewEncoderWithCargo(encoder *xml.Encoder) *EncoderWithCargo

NewEncoderWithCargo creates a new EncoderWithCargo for the given xml.Encoder (remember to Flush or Close it after encoding).

func (*EncoderWithCargo) Encode

func (e *EncoderWithCargo) Encode(producer XmlTokenProducer) error

Encode streams the producer's tokens into the encoder, stopping at the first error. The run's Context is injected into the producer chain and its cancellation is honored between tokens.

type GenericXmlItem

type GenericXmlItem struct {
	NullXmlConsumer
	Name       *xml.Name         // the element's qualified name
	Attributes []xml.Attr        // the attributes, in document order
	Children   []*GenericXmlItem // the children, parsed recursively
	Trails     *Trails           // the element's trails (Before/Inner; After only on a root)
}

GenericXmlItem is the package's reference implementation of both sides — consumer and producer — and the decoder's fallback for unclaimed subtrees: it claims everything into plain fields, so an untouched generic parse re-encodes to a semantically identical document. It has no cargo (GetCargoXml, inherited from NullXmlConsumer, returns nil): preserving whole is what it already does.

func NewGenericXmlItem

func NewGenericXmlItem() *GenericXmlItem

NewGenericXmlItem creates an empty, usable item (Name and Trails ready) — also the authoring vehicle for building generic nodes by hand.

func (*GenericXmlItem) OnXmlAttribute

func (node *GenericXmlItem) OnXmlAttribute(decoder *DecoderWithCargo, attr *xml.Attr) bool

OnXmlAttribute claims every attribute into Attributes.

func (*GenericXmlItem) OnXmlChildStart

func (parent *GenericXmlItem) OnXmlChildStart(decoder *DecoderWithCargo, child *DecoderStackFrame) error

OnXmlChildStart adopts every child as a new generic item and claims its consumption, so unclaimed subtrees stay generic all the way down.

func (*GenericXmlItem) OnXmlEnd

func (node *GenericXmlItem) OnXmlEnd(decoder *DecoderWithCargo, frame *DecoderStackFrame) error

OnXmlEnd adopts the element's trails, already positioned (Before/Inner) by the decoder — the decoder is the single authority on trail bookkeeping.

func (*GenericXmlItem) XmlTokens

func (node *GenericXmlItem) XmlTokens(ctx context.Context) iter.Seq[xml.Token]

XmlTokens implements XmlTokenProducer: Before tokens, the StartElement with its attributes, the children recursively (the context propagates to them), Inner tokens, the EndElement and — only the root ever has them — After tokens.

type MixedNodePolicy

type MixedNodePolicy int

MixedNodePolicy is the encoder's declared policy on mixed elements.

const (
	PreserveMixed    MixedNodePolicy = iota // default: emit everything as it comes
	PreserveText                            // mixed element: the text wins, its child elements are dropped
	PreserveChildren                        // mixed element: the children win, its text is dropped
)

type NullXmlConsumer

type NullXmlConsumer struct{}

NullXmlConsumer is the embeddable no-op base: every event ignored, nothing claimed (OnXmlAttribute returns false), no cargo (GetCargoXml returns nil). Embed it and override only what you need.

func (*NullXmlConsumer) GetCargoXml

func (c *NullXmlConsumer) GetCargoXml() *CargoXml

func (*NullXmlConsumer) OnXmlAttribute

func (consumer *NullXmlConsumer) OnXmlAttribute(decoder *DecoderWithCargo, attr *xml.Attr) bool

func (*NullXmlConsumer) OnXmlChildEnd

func (c *NullXmlConsumer) OnXmlChildEnd(decoder *DecoderWithCargo, child_frame *DecoderStackFrame) error

func (*NullXmlConsumer) OnXmlChildStart

func (c *NullXmlConsumer) OnXmlChildStart(decoder *DecoderWithCargo, child_frame *DecoderStackFrame) error

func (*NullXmlConsumer) OnXmlEnd

func (c *NullXmlConsumer) OnXmlEnd(decoder *DecoderWithCargo, frame *DecoderStackFrame) error

func (*NullXmlConsumer) OnXmlStart

func (c *NullXmlConsumer) OnXmlStart(decoder *DecoderWithCargo, frame *DecoderStackFrame) error

type Trail

type Trail struct {
	Type     TrailType
	Position TrailPosition
	Target   string // processing instructions only: the <?target ...?>
	Content  []byte // the token's content, detached from the decoder's buffer
}

Trail is one preserved non-element token — whitespace, text, comment, processing instruction or directive — with the position it was assigned to. Each trail belongs to exactly one element, which makes re-emission deterministic.

func (*Trail) Token

func (trail *Trail) Token() xml.Token

Token converts the trail to the xml.Token it stands for: CharData for text and whitespace, Comment, ProcInst or Directive.

type TrailPosition

type TrailPosition int

TrailPosition tells which element a Trail belongs to and where: a trail starts life as TrailNone (pending) and the decoder — the single authority on trail bookkeeping — assigns its final position.

const (
	TrailNone   TrailPosition = 0           // pending: not yet assigned to an element
	TrailBefore TrailPosition = (1 << iota) // it announced the element: between the previous sibling (or the parent's start tag) and this element
	TrailInner                              // inside the element, after its last child
	TrailAfter                              // only the root ever has them: the epilog after the document element
)

type TrailType

type TrailType int

TrailType tells what kind of non-element token a Trail preserves. The values are bit flags so they can be combined into masks (see TrailAnyText and TrailAny).

const (
	TrailWhiteSpace            TrailType = (1 << iota)                 // whitespace-only text (formatting)
	TrailText                                                          // real text content
	TrailComment                                                       // <!-- ... -->
	TrailDirective                                                     // <!DOCTYPE ...> and friends
	TrailProcessingInstruction                                         // <?target ...?>
	TrailAnyText               TrailType = TrailWhiteSpace | TrailText // mask: any kind of character data
	TrailAny                   TrailType = 0xFFFF                      // mask: everything
)

type Trails

type Trails []*Trail

Trails is an ordered list of trails — document order is preserved.

func NewTrails

func NewTrails() *Trails

NewTrails creates an empty, usable trail list (the zero *Trails is not: its methods have pointer receivers that append in place).

func (*Trails) Add

func (trails *Trails) Add(trail *Trail) *Trail

Add appends a trail and returns it, so the caller can keep positioning it: trails.Add(t).Position = TrailInner.

func (*Trails) AddComment

func (t *Trails) AddComment(text []byte) *Trail

AddComment appends a pending comment trail and returns it.

func (*Trails) AddText

func (t *Trails) AddText(text string) *Trail

AddText appends a pending text trail and returns it.

func (*Trails) AddTrails

func (dst *Trails) AddTrails(src *Trails, mask TrailType, position TrailPosition) *Trails

AddTrails moves the trails of src matching the type mask into dst, stamping them with the given position, and returns the remainder (the trails that did not match).

func (*Trails) Tokens

func (trails *Trails) Tokens(position TrailPosition) iter.Seq[xml.Token]

Tokens streams, in document order, the trails matching the given position, already converted to xml.Tokens.

type XmlDescribeWithCargo

type XmlDescribeWithCargo interface {
	// XmlDescribeNodeName is the name of the element that will be produced.
	XmlDescribeNodeName(ctx context.Context) xml.Name
	// XmlDescribeNodeType answers "tell me what you are going to
	// serialize". The policy argument is the caller's preference — the
	// answer is the type's decision. XmlMixedNode (the zero value)
	// changes nothing: the element emits what it wants and the cargo
	// emits what it has. A strict answer kills — never by accident: the
	// type decided with the cargo in hand — the part that does not
	// correspond, own answers and cargo alike: XmlTextNode drops the
	// child elements, XmlContainerNode drops the inner text. Attributes
	// always survive; comments and whitespace are another layer's
	// business.
	XmlDescribeNodeType(ctx context.Context, policy MixedNodePolicy) XmlNodeType
	// XmlDescribeAttributes is the list of attributes that will be produced.
	XmlDescribeAttributes(ctx context.Context) []xml.Attr
	// XmlDescribeInitialComments is the list of comments that will be produced before my start tag; plain comment texts
	XmlDescribeInitialComments(ctx context.Context) []string
	// XmlDescribeText is the list of text runs that will be produced right after the start tag
	// usually a node have text or items but not both, but the interface allows both to be present
	XmlDescribeText(ctx context.Context) []string // leaf: my own text runs, right after the start tag
	// XmlDescribeItems is the list of child nodes that will be produced in order
	// usually a node have text or items but not both, but the interface allows both to be present
	XmlDescribeItems(ctx context.Context) []XmlTokenProducer // container: my nodes, in order
	// GetCargoXml pointer to the cargo object if any , so the helpper will emit tokens
	// also for the extra attributes and children
	GetCargoXml() *CargoXml // nil when nothing extra to preserve
}

XmlDescribeWithCargo is the structured way to become a producer: the type answers the questions — who am I, what am I, my known attributes, my known children, and my cargo (what I preserved without claiming) — and DescribedTokens combines them into a correct-by-construction stream.

Every question receives the run's context, so the answers can consult application parameters (a debug mode, a locale…) without the type carrying serialization state.

GetCargoXml is deliberately the same method XmlTokenConsumer has: a type that reads keeping a cargo already owns half of this interface. Every answer that can be absent is a slice: nil means "I don't have that" and DescribedTokens — the one generating the tokens — just skips it. The usual non-generic item brings either text or nodes, and maybe a leading comment.

type XmlNodeType

type XmlNodeType int

XmlNodeType is a described type's declaration of what its element is — advance knowledge only the type itself has ("does it have children?" cannot be asked to a token stream). The zero value is the permissive one: a declaration nobody thought about never drops anything.

const (
	XmlMixedNode     XmlNodeType = iota // default: text and items both emit (text first)
	XmlTextNode                         // leaf: only the text answers emit
	XmlContainerNode                    // container: only the item answers emit
)

type XmlTokenConsumer

type XmlTokenConsumer interface {
	// OnXmlAttribute offers one attribute of the element. Return true to
	// claim it; return false to let it fall to the cargo (if the consumer
	// keeps one) or be discarded.
	OnXmlAttribute(decoder *DecoderWithCargo, attr *xml.Attr) bool

	// OnXmlStart tells the consumer its own element just opened, with the
	// frame already on the stack and the consumer finally settled.
	OnXmlStart(decoder *DecoderWithCargo, frame *DecoderStackFrame) error

	// OnXmlEnd tells the consumer its own element just closed. By then the
	// frame's trails are positioned (Before/Inner) and its cargo, if any,
	// is complete.
	OnXmlEnd(decoder *DecoderWithCargo, frame *DecoderStackFrame) error

	// OnXmlChildStart tells the consumer a child element just opened —
	// this is where the parent decides who consumes it, by setting
	// child_frame.Consumer (and may adjust child_frame.SkipUnknownChildren
	// per child). Leaving Consumer nil delegates to the generic fallback
	// or, under SkipUnknownChildren, skips the subtree entirely.
	OnXmlChildStart(decoder *DecoderWithCargo, child_frame *DecoderStackFrame) error

	// OnXmlChildEnd tells the consumer a child element just closed. The
	// child frame's trails are positioned — harvest text content here.
	// A skipped child never fires this event.
	OnXmlChildEnd(decoder *DecoderWithCargo, child_frame *DecoderStackFrame) error

	// GetCargoXml returns the consumer's cargo — the store for whatever it
	// does not claim (attributes, generic children, trails) — or nil to
	// discard the unclaimed instead of preserving it.
	GetCargoXml() *CargoXml
}

XmlTokenConsumer is implemented by application objects that want to receive the parts of an XML element as the decoder walks the document. Embed NullXmlConsumer and override only the events you care about.

type XmlTokenProducer

type XmlTokenProducer interface {
	XmlTokens(ctx context.Context) iter.Seq[xml.Token]
}

XmlTokenProducer is anything able to stream itself as XML tokens — the mirror of XmlTokenConsumer: where the consumer reacts to the tokens the decoder reads, the producer supplies the tokens the encoder writes.

The boundary speaks stdlib vocabulary only: tokens out, the run's context in. The context accompanies every level of the chain — Encode injects the encoder's Context and nested producers propagate it — so application parameters reach any depth without touching the types (see the debug-output pattern in the docs). Children are just nested iterations inside the stream. Trails and CargoXml stay package machinery that helps producers assemble their stream.

GenericXmlItem is the reference implementation; custom types describe themselves through XmlDescribeWithCargo and delegate to DescribedTokens (Items adapts their typed child lists).

func Items

func Items[T XmlTokenProducer](items []T) XmlTokenProducer

Items adapts a whole typed slice as one producer that streams its elements in order, propagating the run's context to each of them. It exists because Go does not convert []*Product to []XmlTokenProducer on its own: this single generic function replaces that per-call conversion loop, for any element type that produces tokens.

Directories

Path Synopsis
test
trail_inspect command
trail_inspect parses an XML file generically with cargoxml and prints the tree, showing where every attribute, comment and text ended up.
trail_inspect parses an XML file generically with cargoxml and prints the tree, showing where every attribute, comment and text ended up.

Jump to

Keyboard shortcuts

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