Skip to content

Streaming-split reader support + expand_to_document default-namespace fix (0.3.18) - #211

Merged
dginev merged 3 commits into
mainfrom
feat-reader-streaming-split
Aug 1, 2026
Merged

Streaming-split reader support + expand_to_document default-namespace fix (0.3.18)#211
dginev merged 3 commits into
mainfrom
feat-reader-streaming-split

Conversation

@dginev

@dginev dginev commented Aug 1, 2026

Copy link
Copy Markdown
Member

What

Three commits preparing 0.3.18, driven by latexml-oxide's streaming post-split (splitting a multi-GB XML into subdocuments without materializing large subtrees):

  1. TextReader additionsattributes_qname() (document-order attributes incl. namespace declarations, without expanding the subtree), value(), is_empty_element(), and outer_xml() (serialize the current subtree exactly as input). outer_xml deliberately does not use xmlTextReaderReadOuterXml: that API deep-copies the node parentless, and xmlNewReconciledNs then mints a default: prefix onto default-namespace content. Dumping the reader-owned node directly keeps ancestor declarations reachable, so nothing is fabricated.
  2. ReaderEvent — lossless xmlReaderTypes access. node_type() maps end-element and both whitespace events to None; a structure-reconstructing caller needs them distinguished.
  3. Fix: expand_to_document minted default: prefixes. xmlReconciliateNs turns a NULL-prefix namespace inherited from an un-copied ancestor into xmlns:default="…", so every element serialized as <default:x> — and a dumped subtree of the copy never re-parsed into the right namespace. Minted declaration prefixes are now restored to the source element's prefixes (clash-checked). Guard added to the reconciliation test.

Validation

  • Full crate suite green (120 tests incl. 12 reader tests; 3 new guards pin the exact serializations).
  • Downstream: latexml-oxide's streaming split ran a 2.68 GB document through these APIs — 115,519 subdocuments, byte-identical output vs its non-streaming path.
  • Version bumped to 0.3.18, changelog updated.

🤖 Generated with Claude Code

dginev and others added 3 commits July 31, 2026 21:00
…mpty_element, outer_xml

outer_xml dumps the reader-owned expanded node directly instead of going
through xmlTextReaderReadOuterXml: that API deep-copies the node parentless,
and xmlNewReconciledNs then mints a 'default:' prefix onto default-namespace
content. Guard test pins the exact serialization.

0.3.18 version bump + changelog.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
node_type() maps end-element and both whitespace events to None; a
structure-reconstructing caller needs them apart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xmlReconciliateNs mints xmlns:default= for a NULL-prefix namespace
inherited from an un-copied ancestor, making every element serialize
as <default:x> — and a dumped subtree of the copy never re-parses into
the right namespace. Restore minted declaration prefixes to the source
element's prefixes (clash-checked). Guard added to the reconciliation
test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dginev
dginev merged commit 7fceac4 into main Aug 1, 2026
16 of 18 checks passed
@dginev
dginev deleted the feat-reader-streaming-split branch August 1, 2026 14:20
dginev added a commit to dginev/latexml-oxide that referenced this pull request Aug 1, 2026
Published (KWARC/rust-libxml#211); cargo tree confirms the registry
resolution. Suite 1854/1854 against the published crate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dginev added a commit to dginev/latexml-oxide that referenced this pull request Aug 1, 2026
… commodity RAM (#477)

* feat(post): two-pass streaming split — huge documents split without the whole-DOM parse

The last whole-DOM stage of post (Split's one-time parse) OOM'd a 31 GB
laptop on the 131 MB witness's 2.68 GB core XML (>26.6 GB mid-parse, exit
137, zero pages). latexml_post::stream_split partitions the file with a
TextReader pull-parse: pages assembled as text and spilled at close, wrapper
subtrees (back-matter shells) via mini-DOM descent, then a pre-order Scan
sweep that preserves Scan's order-sensitive semantics. Auto-engages for file
input >= 1 GiB (LATEXML_POST_STREAM_SPLIT forces, LATEXML_POST_STREAM_THRESHOLD
tunes); oversized in-memory handoffs now spill at the same threshold so the
single-invocation .tex -> .htm flow streams too. Page spills live beside the
destination, not the (possibly tmpfs) system temp dir.

Parity gate 118_streaming_split_parity: byte-identical rendered pages vs the
DOM split across run adjacency, TOC suppression, inlist=toc lookahead,
wrapper descent, unnamed pages, template copies, inherited attributes. The
gate caught a latent DOM-split defect, also fixed: the inherited xml:lang
copy silently never fired (namespaced-attribute read; get_attribute returns
None for xml:* on parsed docs).

Requires libxml 0.3.18 (attributes_qname/value/is_empty_element/event/
outer_xml + the expand_to_document default:-prefix mint fix) — TEMPORARY
[patch.crates-io] path override in Cargo.toml until 0.3.18 is published;
REMOVE before merge. Vendored-native audit pins refreshed (libxml 0.3.18,
ar_archive_writer 0.5.2 — verdicts re-verified).

Suite 1853/1853, clippy -D warnings clean, rustdoc clean, lint.sh green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(stream-split): boundary-checked element probe + multiple-root guard

The descent probe matched '<index' inside '<indexmark' (and any name-prefix
collision), sending every \index-marked paragraph through a mini-DOM descent
— correct but a per-paragraph expand+copy storm on index-heavy documents.
The probe now requires a name-boundary character after the localname. A
second top-level element (recover-mode oddity) now fails loud instead of
clobbering the root spill slot.

Guards: element_probe_respects_name_boundaries; parity + handoff + post
suites green; A/B on a 300 MB slice of the Nasser witness: 892 pages
byte-identical between the DOM and streaming paths (diff -r clean), peak RSS
7.1 GB (DOM) vs 771 MB (streamed).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(streaming-post): record the libxslt page-size ceiling + A/B breadth results

--splitat A/B on a 300 MB witness slice: byte-identical DOM-vs-streaming
trees at subsubsection (892 pages), section (17) and chapter (5) — the
chapter case failing identically on both paths at a ~260 MB page (libxslt
ceiling, reproduced bit-for-bit on the pre-branch rc4 binary; not a
regression). Unsplit giant documents hit the same ceiling and always have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* build: libxml 0.3.18 from crates.io — drop the temporary path override

Published (KWARC/rust-libxml#211); cargo tree confirms the registry
resolution. Suite 1854/1854 against the published crate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* ci(windows): un-wedge the check lane (kpathsea toolchain check) + libxml 0.3.19 floor

The compile-only Windows check job runs on a TeX-less runner, so kpathsea's
build-script toolchain check ('no usable TeX backend') killed it before
checking anything — the lane has NEVER been green, including the PRs that
introduced it (#465/#466). Set KPATHSEA_SKIP_TOOLCHAIN_CHECK=1 there, which
is that check's documented bypass for build-without-TeX machines; the
build+test job installs TeX Live and keeps the guard armed.

libxml floor 0.3.18 → 0.3.19: 0.3.18 fails to LINK on windows-msvc (xmlFree
is not a linkable data symbol there; LNK2019) — fixed upstream via the
crate's per-target free shim (KWARC/rust-libxml#212), 0.3.19 published,
0.3.18 yanked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* tools(audit): an AUDITED entry may list multiple audited versions

The lockfile is gitignored, so CI and dev boxes can resolve different
compatible versions of the same crate during a release window —
ar_archive_writer flip-flopped 0.5.2/0.5.3 between this laptop and the CI
runner, each side failing the version-exact audit the other passed. An
entry's version field now accepts a tuple listing every audited version
(both 0.5.2 and 0.5.3 carry the same uncompiled reference/*.cpp verdict);
unaudited versions still fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant