Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to XML.jl will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.1] - 2026-07-05

### Added

- `eachelement(node)` / `elements(node)` — element-only child iteration for `Node` and
`LazyNode`, skipping the whitespace `Text` nodes that v0.4 preserves on pretty-printed
documents (and any other non-element node). The explicit idiom for the common
"iterate the child elements" loop ([#78](https://github.com/JuliaData/XML.jl/issues/78)).

## [0.4.0] - 2026-07-03

> **Upgrading from 0.3.x?** See the standalone [v0.4 migration guide](MIGRATING_TO_v0.4.md).
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "XML"
uuid = "72c71f33-b9b6-44de-8c94-c961784809e2"
version = "0.4.0"
version = "0.4.1"
authors = ["Josh Day <emailjoshday@gmail.com> and contributors"]

[weakdeps]
Expand Down
26 changes: 26 additions & 0 deletions src/XML.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export
Node, LazyNode, NodeType, Attributes,
CData, Comment, Declaration, Document, DTD, Element, ProcessingInstruction, Text,
nodetype, tag, attributes, value, children, children!, eachchildnode, eachattribute,
eachelement, elements,
foreach_attr,
is_simple, simple_value, is_simple_value, sourcetext,
depth, siblings,
Expand Down Expand Up @@ -233,6 +234,31 @@ that cannot have children (e.g. `Text`, `Comment`, `CData`).
"""
children(o::Node) = something(o.children, ())

"""
eachelement(node)

Lazy iterator over the child *elements* of `node`, skipping every other node type
(`Text`, `Comment`, `CData`, `ProcessingInstruction`, …). Since v0.4 preserves
inter-element whitespace, `children` on pretty-printed documents interleaves
whitespace `Text` nodes with elements — `eachelement` is the idiomatic way to
iterate the elements only. Works with both `Node` and `LazyNode`.

for el in eachelement(node)
# el isa Element node
end

See also [`elements`](@ref).
"""
eachelement(node) = Iterators.filter(n -> nodetype(n) === Element, children(node))

"""
elements(node) -> Vector

The child *elements* of `node` in document order — the collected counterpart of
[`eachelement`](@ref).
"""
elements(node) = collect(eachelement(node))

"""
is_simple(node) -> Bool

Expand Down
4 changes: 4 additions & 0 deletions src/lazynode.jl
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,10 @@ function eachchildnode(n::LazyNode{S}) where {S}
LazyChildIterator{S, typeof(iter)}(n.data, iter, Ref(true))
end

# eachelement's generic definition filters children(node); for LazyNode, build on the
# lazy child iterator instead so no intermediate Vector is materialized.
eachelement(node::LazyNode) = Iterators.filter(n -> nodetype(n) === Element, eachchildnode(node))

function Base.iterate(ci::LazyChildIterator, _ = nothing)
ci.done[] && return nothing
for tok in ci.iter
Expand Down
15 changes: 15 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,21 @@ end
@test value(doc[1][3][1]) == "y"
end

@testset "eachelement/elements skip non-element children" begin
xml = "<root>\n <a>x</a>\n <!-- note -->\n <b>y</b>\n</root>"
for T in (Node, LazyNode)
doc = parse(xml, T)
root = only(elements(doc))
@test tag(root) == "root"
@test length(children(root)) == 7 # 4 Text runs + Comment + 2 Elements
@test [tag(el) for el in eachelement(root)] == ["a", "b"]
@test elements(root) == collect(eachelement(root))
@test all(n -> nodetype(n) === XML.Element, elements(root))
a = first(eachelement(root))
@test isempty(elements(children(a)[1])) # Text leaf has no elements
end
end

@testset "xml:space attribute is preserved during parsing" begin
doc = parse("""<root xml:space="preserve"><child> text </child></root>""", Node)
@test doc[1]["xml:space"] == "preserve"
Expand Down
Loading