Skip to content

Commit 056b97f

Browse files
authored
Merge pull request #76 from JuliaData/v0.4-dev
v0.4: token-based parser, Cursor streaming API, XPath, well-formedness levels
2 parents 2c869e3 + f55d422 commit 056b97f

39 files changed

Lines changed: 13092 additions & 2037 deletions

.github/workflows/CI.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ jobs:
4040
${{ runner.os }}-test-${{ env.cache-name }}-
4141
${{ runner.os }}-test-
4242
${{ runner.os }}-
43+
- uses: actions/cache@v5
44+
with:
45+
path: test/data/w3c
46+
key: w3c-xmlconf-v20130923
4347
- uses: julia-actions/julia-buildpkg@v1
4448
- uses: julia-actions/julia-runtest@v1
4549
- uses: julia-actions/julia-processcoverage@v1

.github/workflows/Downstream.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@ jobs:
1212
strategy:
1313
fail-fast: false
1414
matrix:
15-
package:
16-
- "XLSX"
15+
include:
16+
# XLSX's XML-v0.4 adaptation (JuliaData/XLSX.jl#415). The *registered* XLSX
17+
# pins XML 0.3, so testing it against this branch fails by design; test the
18+
# adaptation PR instead. Once #415 merges and releases, point this back at
19+
# the registered package (PackageSpec(name=...) as before).
20+
- package: XLSX
21+
repository: JuliaData/XLSX.jl
22+
ref: refs/pull/415/head
1723
steps:
1824
- uses: actions/checkout@v7
1925
- uses: julia-actions/setup-julia@v3
@@ -22,17 +28,21 @@ jobs:
2228
arch: x64
2329
show-versioninfo: true
2430
- uses: julia-actions/julia-buildpkg@latest
31+
- uses: actions/checkout@v7
32+
with:
33+
repository: ${{ matrix.repository }}
34+
ref: ${{ matrix.ref }}
35+
path: downstream
2536
- name: Load this and run the downstream tests
2637
shell: julia --color=yes {0}
2738
run: |
2839
using Pkg
2940
Pkg.Registry.update()
3041
Pkg.activate(;temp=true)
31-
# force it to use this PR's version of the package
32-
ENV["JULIA_PKG_DEVDIR"]= mktempdir()
42+
# this branch's XML + the downstream package's adaptation branch
3343
Pkg.develop([
3444
PackageSpec(path="."),
35-
PackageSpec(name="${{ matrix.package }}"),
45+
PackageSpec(path="downstream"),
3646
])
3747
Pkg.update()
3848
Pkg.test("${{ matrix.package }}")

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
*Manifest.toml
2-
*generated_xsd.jl
3-
*.xml
42
*.gz
3+
*.tar
54
*.DS_Store
5+
*.claude
6+
test/data/w3c/
7+
benchmarks/data/

CHANGELOG.md

Lines changed: 238 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,96 @@
11
# Changelog
22

3-
Itemised from v0.3.9 onward; for earlier releases see the
4-
[GitHub releases](https://github.com/JuliaData/XML.jl/releases) and git tags.
5-
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
3+
All notable changes to XML.jl will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.4.0] - 2026-07-03
9+
10+
> **Upgrading from 0.3.x?** See the standalone [v0.4 migration guide](MIGRATING_TO_v0.4.md).
11+
12+
### Added
13+
- New streaming tokenizer (`XMLTokenizer` module) for fine-grained XML token iteration.
14+
- Pull/cursor streaming API — `Cursor` with `next!`, `for_each_child` / `@for_each_child`,
15+
`skip_element!`, and `eof` for forward, allocation-light traversal of large documents ([#8], [#61]).
16+
- XPath support via `xpath(node, path)` — an experimental subset of XPath 1.0 ([#30]).
17+
- Configurable well-formedness: `parse`/`read` accept `wellformed = :lenient | :structural | :strict`
18+
(default `:structural`).
19+
- `get(node, key, default)` accessor, matching `getindex` ([#50]).
20+
- `test/test_libxml2_testcases.jl`: 243 test cases borrowed from the [libxml2](https://github.com/GNOME/libxml2) test suite.
21+
- `AbstractTrees` package extension (`print_tree`, `PreOrderDFS`, `Leaves`, … on `Node` and `LazyNode`).
22+
23+
### Changed
24+
- **`Node` is now parametric `Node{S}`** (storage type `S`, typically `String` or `SubString{String}`)
25+
and is no longer a subtype of `AbstractXMLNode`. `::Node` annotations continue to work.
26+
- **A `Node`'s `attributes` field is now a `Vector{Pair{S,S}}`** (was `OrderedDict{String,String}`).
27+
Use the `attributes(node)` accessor — which returns an ordered `Attributes <: AbstractDict`, or
28+
`nothing` — instead of indexing the field directly.
29+
- **`LazyNode` is now an immutable `LazyNode{S}`** constructed with `parse(x, LazyNode)` /
30+
`read(file, LazyNode)`; the `.raw` field is removed and accessors return `SubString` views.
31+
- **`XML.write` now escapes text and attribute values automatically**, and `parse`/`read` unescape
32+
into values — so `value(node)` returns decoded text (`&`, not `&amp;`). Round-trips are preserved
33+
because `write` re-escapes.
34+
- **Duplicate attribute names now raise an error** during parsing.
35+
- **`parse`/`read` reject malformed documents by default** (`wellformed = :structural`): multiple
36+
root elements, a document with no root element, non-whitespace text outside the root, empty/invalid
37+
element names, a literal `<` in an attribute value, and a misplaced/duplicate/nested DOCTYPE or XML
38+
declaration. `:strict` additionally rejects `--` (or a trailing `-`) in comments, empty/invalid PI
39+
targets, and characters — numeric references *and* raw literal characters — outside the XML §2.2
40+
`Char` range. Pass `wellformed = :lenient` to restore the previous permissive behavior; note that a
41+
standalone DTD file or a prolog-only fragment (no root element) now needs `:lenient`.
42+
- **`Node` constructors validate names and content.** Element/PI names must be valid XML names, and
43+
`Comment`/`CData`/`ProcessingInstruction` content may not contain its own close delimiter (`-->`,
44+
`]]>`, `?>`), which would otherwise split the node on write. (Content is not otherwise validated —
45+
e.g. a `Comment` whose text contains `--` is constructed as-is and is rejected only on re-parse at
46+
`:strict`.)
47+
- **`escape` is no longer idempotent** — every `&` is escaped, so `escape("&amp;") == "&amp;amp;"`;
48+
call it only on raw, unescaped text ([#52]).
49+
- **`read` no longer memory-maps** the input file.
50+
- **Minimum Julia version is now 1.10.**
51+
52+
### Deprecated
53+
- `simplevalue` — already a deprecated alias of `simple_value` *before* 0.4 — is **no longer exported** (it stays
54+
reachable as `XML.simplevalue`, still warning). Use `simple_value`. *(Not a new 0.4 deprecation: the rename to the
55+
snake_case `simple_value`, matching `is_simple` / `is_simple_value`, predates this release; 0.4 only un-exports the
56+
old alias.)*
57+
58+
### Removed
59+
- **`XML.Raw`** and the Raw/LazyNode streaming internals — use `parse(x, Node)` / `read(file, Node)`
60+
for an in-memory tree, or the new `Cursor` API for streaming.
61+
- **`next` / `prev`** (LazyNode traversal), and **`prev!`** (the 0.3.x in-place `LazyNode` advance) —
62+
parse to a `Node` and use `children` / integer indexing, or the `Cursor` API. (`next!` still exists
63+
but now advances a `Cursor`, not a `LazyNode`; there is no `prev!``Cursor` is forward-only.)
64+
- **Single-argument `parent(node)` / `depth(node)`** — use `parent(child, root)` / `depth(child, root)`.
65+
- **`nodes_equal(a, b)`** — use `a == b`.
66+
- **`escape!` / `unescape!`** — escaping/unescaping now happens automatically in `write` / `parse`.
67+
- **`DTDBody`** — use `parse_dtd` or the `DTD` node.
68+
69+
(Calling a removed function throws an error whose message names the replacement; `DTDBody` is
70+
removed outright and raises `UndefVarError`. These removals are all consequences of the v0.4 internals
71+
rewrite ([#54]); none has a dedicated issue to link individually.)
72+
73+
### Fixed
74+
- **Tokenizer: multi-byte UTF-8 in attribute values** — values like `<doc city="東京"/>` no longer
75+
raise `StringIndexError` (`attr_value()` used byte arithmetic instead of `prevind`).
76+
- **Tokenizer: quotes inside DTD comments** — a `"`/`'` inside a `<!-- -->` comment in a DTD internal
77+
subset no longer triggers an "Unterminated quoted string" error.
78+
- Numeric character references are now escaped/unescaped correctly ([#17]).
79+
- `unescape` no longer double-unescapes; each reference is processed exactly once ([#53]). It is now
80+
single-pass, so a numeric reference resolving to `&` is never re-scanned as a named entity —
81+
`unescape("&#38;amp;")` is `"&amp;"`, not `"&"`.
82+
- A leading U+FEFF byte-order-mark character in an in-memory string is now stripped by
83+
`parse(_, LazyNode)` and `Cursor` as well as `parse(_, Node)`, so all three readers agree.
84+
- A truncated comment/CDATA/PI/DOCTYPE at end of input raises a clear "unterminated …" error instead
85+
of being silently accepted (`Node`) or crashing `value()` (`LazyNode`/`Cursor`).
86+
- Odd-length UTF-16 input that begins with a BOM raises a clear error instead of an opaque
87+
`reinterpret` failure.
88+
- Processing-instruction content keeps its trailing whitespace on round-trip (only the leading
89+
separator after the target is dropped, per §2.6).
90+
- `parse_dtd` reports a clear error on parameter-entity references (`%name;`) instead of an opaque
91+
internal error.
92+
- XPath: unsupported axis syntax (`child::`, `descendant::`, …) now raises instead of silently
93+
returning the wrong result; dead scaffolding in `xpath` was removed.
694

795
## [0.3.9] - 2026-06-20
896

@@ -19,8 +107,155 @@ First release since XML.jl moved to [JuliaData](https://github.com/JuliaData/XML
19107
### Changed
20108
- Relaxed the OrderedCollections.jl compat bound to include v2 ([#64]).
21109

110+
## [0.3.8]
111+
112+
### Fixed
113+
- `XML.write` now respects `xml:space="preserve"` and suppresses indentation for elements with this attribute ([#49]).
114+
115+
## [0.3.7]
116+
117+
### Fixed
118+
- Resolved remaining issues from [#45] and fixed [#46] (whitespace preservation edge cases) ([#47]).
119+
120+
## [0.3.6]
121+
122+
### Added
123+
- `XML.write` respects `xml:space="preserve"` on elements, suppressing automatic indentation ([#45]).
124+
125+
### Fixed
126+
- `String` type ambiguity on Julia nightly resolved ([#38]).
127+
128+
## [0.3.5]
129+
130+
### Fixed
131+
- `depth` and `parent` functions corrected to work properly with the DOM tree API ([#37]).
132+
- `escape` updated to no longer be idempotent — every `&` is now escaped, matching spec behavior ([#32], addressing [#31]).
133+
- `pushfirst!` support added for `Node` children ([#29]).
134+
135+
## [0.3.4]
136+
137+
### Fixed
138+
- Fixed [#26].
139+
- CI updated to use `julia-actions/cache@v4` and `lts` Julia version.
140+
141+
## [0.3.3]
142+
143+
### Added
144+
- `h` constructor for concise element creation (e.g., `h.div("hello"; class="main")`).
145+
146+
### Fixed
147+
- Path definition error in README example ([#20]).
148+
149+
## [0.3.2]
150+
151+
### Fixed
152+
- Minor typos.
153+
154+
## [0.3.1]
155+
156+
### Added
157+
- Julia 1.6 compatibility ([#16]).
158+
159+
### Changed
160+
- Smarter escaping logic.
161+
162+
## [0.3.0]
163+
164+
### Changed
165+
- Attribute internal representation changed from `Dict` to `OrderedDict` (later reverted to `Vector{Pair}`).
166+
167+
## [0.2.3]
168+
169+
### Fixed
170+
- Parse method fix.
171+
172+
## [0.2.2]
173+
174+
### Added
175+
- DTD parsing via `parse_dtd`.
176+
- `is_simple` and `simple_value` exports.
177+
- `setindex!` methods for modifying attributes.
178+
- `unescape` function.
179+
180+
### Fixed
181+
- DOCTYPE parsing made case-insensitive.
182+
183+
## [0.2.1]
184+
185+
### Fixed
186+
- Write output fixes.
187+
188+
## [0.2.0]
189+
190+
### Changed
191+
- Major rewrite: introduced `NodeType` enum, `Node{S}` parametric struct, callable `NodeType` constructors, and `XML.write`.
192+
- Processing instruction support.
193+
- Benchmarks added.
194+
195+
## [0.1.3]
196+
197+
### Changed
198+
- Improved print output for `AbstractXMLNode`.
199+
200+
## [0.1.2]
201+
202+
### Added
203+
- AbstractTrees 0.4 compatibility ([#5]).
204+
205+
## [0.1.1]
206+
207+
### Added
208+
- `Node` implementation with `print_tree`.
209+
- Color output in REPL display.
210+
- Stopped stripping whitespace from text nodes.
211+
212+
## [0.1.0]
213+
214+
- Initial release.
215+
216+
[Unreleased]: https://github.com/JuliaData/XML.jl/compare/v0.3.9...HEAD
22217
[0.3.9]: https://github.com/JuliaData/XML.jl/compare/v0.3.8...v0.3.9
218+
[0.3.8]: https://github.com/JuliaData/XML.jl/compare/v0.3.7...v0.3.8
219+
[0.3.7]: https://github.com/JuliaData/XML.jl/compare/v0.3.6...v0.3.7
220+
[0.3.6]: https://github.com/JuliaData/XML.jl/compare/v0.3.5...v0.3.6
221+
[0.3.5]: https://github.com/JuliaData/XML.jl/compare/v0.3.4...v0.3.5
222+
[0.3.4]: https://github.com/JuliaData/XML.jl/compare/v0.3.3...v0.3.4
223+
[0.3.3]: https://github.com/JuliaData/XML.jl/compare/v0.3.2...v0.3.3
224+
[0.3.2]: https://github.com/JuliaData/XML.jl/compare/v0.3.1...v0.3.2
225+
[0.3.1]: https://github.com/JuliaData/XML.jl/compare/v0.3.0...v0.3.1
226+
[0.3.0]: https://github.com/JuliaData/XML.jl/compare/v0.2.3...v0.3.0
227+
[0.2.3]: https://github.com/JuliaData/XML.jl/compare/v0.2.2...v0.2.3
228+
[0.2.2]: https://github.com/JuliaData/XML.jl/compare/v0.2.1...v0.2.2
229+
[0.2.1]: https://github.com/JuliaData/XML.jl/compare/v0.2.0...v0.2.1
230+
[0.2.0]: https://github.com/JuliaData/XML.jl/compare/v0.1.3...v0.2.0
231+
[0.1.3]: https://github.com/JuliaData/XML.jl/compare/v0.1.2...v0.1.3
232+
[0.1.2]: https://github.com/JuliaData/XML.jl/compare/v0.1.1...v0.1.2
233+
[0.1.1]: https://github.com/JuliaData/XML.jl/compare/v0.1.0...v0.1.1
234+
[0.1.0]: https://github.com/JuliaData/XML.jl/releases/tag/v0.1.0
235+
236+
[#5]: https://github.com/JuliaData/XML.jl/pull/5
237+
[#16]: https://github.com/JuliaData/XML.jl/pull/16
238+
[#20]: https://github.com/JuliaData/XML.jl/pull/20
239+
[#26]: https://github.com/JuliaData/XML.jl/issues/26
240+
[#29]: https://github.com/JuliaData/XML.jl/pull/29
241+
[#31]: https://github.com/JuliaData/XML.jl/issues/31
242+
[#32]: https://github.com/JuliaData/XML.jl/pull/32
243+
[#37]: https://github.com/JuliaData/XML.jl/pull/37
244+
[#38]: https://github.com/JuliaData/XML.jl/pull/38
245+
[#43]: https://github.com/JuliaData/XML.jl/issues/43
246+
[#45]: https://github.com/JuliaData/XML.jl/pull/45
247+
[#46]: https://github.com/JuliaData/XML.jl/issues/46
248+
[#47]: https://github.com/JuliaData/XML.jl/pull/47
249+
[#49]: https://github.com/JuliaData/XML.jl/pull/49
23250
[#56]: https://github.com/JuliaData/XML.jl/pull/56
24251
[#59]: https://github.com/JuliaData/XML.jl/pull/59
25252
[#60]: https://github.com/JuliaData/XML.jl/pull/60
26253
[#64]: https://github.com/JuliaData/XML.jl/pull/64
254+
[#8]: https://github.com/JuliaData/XML.jl/issues/8
255+
[#17]: https://github.com/JuliaData/XML.jl/issues/17
256+
[#30]: https://github.com/JuliaData/XML.jl/issues/30
257+
[#50]: https://github.com/JuliaData/XML.jl/issues/50
258+
[#52]: https://github.com/JuliaData/XML.jl/issues/52
259+
[#53]: https://github.com/JuliaData/XML.jl/issues/53
260+
[#54]: https://github.com/JuliaData/XML.jl/pull/54
261+
[#61]: https://github.com/JuliaData/XML.jl/issues/61

0 commit comments

Comments
 (0)