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 ¶
- func DescribedTokens(ctx context.Context, d XmlDescribeWithCargo, policy MixedNodePolicy) iter.Seq[xml.Token]
- type CargoXml
- type DecoderStack
- type DecoderStackFrame
- type DecoderWithCargo
- type EncoderWithCargo
- type GenericXmlItem
- func (node *GenericXmlItem) OnXmlAttribute(decoder *DecoderWithCargo, attr *xml.Attr) bool
- func (parent *GenericXmlItem) OnXmlChildStart(decoder *DecoderWithCargo, child *DecoderStackFrame) error
- func (node *GenericXmlItem) OnXmlEnd(decoder *DecoderWithCargo, frame *DecoderStackFrame) error
- func (node *GenericXmlItem) XmlTokens(ctx context.Context) iter.Seq[xml.Token]
- type MixedNodePolicy
- type NullXmlConsumer
- func (c *NullXmlConsumer) GetCargoXml() *CargoXml
- func (consumer *NullXmlConsumer) OnXmlAttribute(decoder *DecoderWithCargo, attr *xml.Attr) bool
- func (c *NullXmlConsumer) OnXmlChildEnd(decoder *DecoderWithCargo, child_frame *DecoderStackFrame) error
- func (c *NullXmlConsumer) OnXmlChildStart(decoder *DecoderWithCargo, child_frame *DecoderStackFrame) error
- func (c *NullXmlConsumer) OnXmlEnd(decoder *DecoderWithCargo, frame *DecoderStackFrame) error
- func (c *NullXmlConsumer) OnXmlStart(decoder *DecoderWithCargo, frame *DecoderStackFrame) error
- type Trail
- type TrailPosition
- type TrailType
- type Trails
- func (trails *Trails) Add(trail *Trail) *Trail
- func (t *Trails) AddComment(text []byte) *Trail
- func (t *Trails) AddText(text string) *Trail
- func (dst *Trails) AddTrails(src *Trails, mask TrailType, position TrailPosition) *Trails
- func (trails *Trails) Tokens(position TrailPosition) iter.Seq[xml.Token]
- type XmlDescribeWithCargo
- type XmlNodeType
- type XmlTokenConsumer
- type XmlTokenProducer
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 	) 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.
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.
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 ¶
Add appends a trail and returns it, so the caller can keep positioning it: trails.Add(t).Position = TrailInner.
func (*Trails) AddComment ¶
AddComment appends a pending comment trail and returns it.
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 ¶
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.
Source Files
¶
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. |