leptris 1.9.372.0-arm-linux → 1.9.374.0-arm-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75f85d625ceea40a5ae60dfe1729942b3c2d0e9978ad424f48d967ef1fb377b7
4
- data.tar.gz: 6b1adb445caad6e67bea400fd1c5addfcf96637cfac5ed757009a480bd4d070b
3
+ metadata.gz: d18d23c755cec08cc9356c522e9157e2e5b06a11f3447d6e395e3e6ace07fe2d
4
+ data.tar.gz: 50961a2bd05dce8f36bb5ed61980b39d997237222733a806fbdaef8571db054a
5
5
  SHA512:
6
- metadata.gz: c68a4d4a1efb6fcb311b57a3581c53a2433d36523f328d05d527fb52d6d7b7534cb2f843f31356132844f6cb53aa0e198b3236bb2b7649f4bdd604819619b5ab
7
- data.tar.gz: 4a10168f55c0aa208aeaa9544d2510ed2e75a03e4afa1320b47448abb41fb415a2f1c220441fa3b7fec56132b03c577a2bf90697f9a5cf5c3846302abf899f1a
6
+ metadata.gz: cac0f820beec4f23cf3f4dafe2bd07a69cd4d412d37b2c9b33c8d78b32e1f09b5389517f405d305f592ddd735fb3b8dcfd80d837a655602389cdb9485343ea5a
7
+ data.tar.gz: 661c7595d582b237677cfb65e0266a79e2433467e5ba6f561d4177e771e6ce25cb694d34c6db34cecdb096e517cac996a12c5f105475fe467c6e00e96f479fc1
data/Rakefile CHANGED
@@ -8,7 +8,7 @@ RSpec::Core::RakeTask.new(:spec)
8
8
  # Pin for `rake compile` and the platform-gem builds. Keep in lockstep
9
9
  # with .github/workflows/build.yml (which calls `rake compile`) and the
10
10
  # CHANGELOG when libleptris releases.
11
- LIBLEPTRIS_VERSION = "1.9.372"
11
+ LIBLEPTRIS_VERSION = "1.9.374"
12
12
  # Vendored alongside libleptris for fn:normalize-unicode (TODO
13
13
  # .restructure/20): built per platform with a RELOCATABLE @rpath
14
14
  # install name, loaded by ffi.rb before libleptris so the
@@ -1414,20 +1414,30 @@ static VALUE nf_insert_binding_child(VALUE self, VALUE document,
1414
1414
  static VALUE nn_create_element(VALUE klass, VALUE document, VALUE name)
1415
1415
  {
1416
1416
  void *ptr;
1417
+ VALUE mk = doc_klass_for_kind(document, 0);
1417
1418
  (void)klass;
1418
1419
  ptr = f_elem_create(doc_ptr_of(document), StringValueCStr(name));
1419
1420
  if (!ptr)
1420
1421
  rb_raise(c_runtime_error, "leptris_element_create failed");
1422
+ /* #340: registered documents mint their element klass here too —
1423
+ * the unattached create factories are the build path (moxml's
1424
+ * create_element per record); plain NativeNode here forced the
1425
+ * consumer into wrapper-mode bridging for the whole document. */
1426
+ if (!NIL_P(mk))
1427
+ return mint_consumer_node(mk, document, ptr);
1421
1428
  return wrap_new(document, ptr);
1422
1429
  }
1423
1430
 
1424
1431
  static VALUE nn_create_text(VALUE klass, VALUE document, VALUE content)
1425
1432
  {
1426
1433
  void *ptr;
1434
+ VALUE mk = doc_klass_for_kind(document, 1);
1427
1435
  (void)klass;
1428
1436
  ptr = f_text_create(doc_ptr_of(document), StringValueCStr(content));
1429
1437
  if (!ptr)
1430
1438
  rb_raise(c_runtime_error, "leptris_text_node_create failed");
1439
+ if (!NIL_P(mk))
1440
+ return mint_consumer_node(mk, document, ptr);
1431
1441
  return wrap_new(document, ptr);
1432
1442
  }
1433
1443
 
@@ -3548,6 +3558,7 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
3548
3558
  rb_define_method(c_native_node, "create_child", nn_create_child, 1);
3549
3559
  rb_define_method(c_native_node, "append_child", nn_append_child, 1);
3550
3560
  rb_define_method(c_native_node, "add_child", nn_append_child, 1);
3561
+ rb_define_alias(c_native_node, "add_child_native", "add_child");
3551
3562
  rb_define_method(c_native_node, "address", nn_address, 0);
3552
3563
  rb_define_method(c_native_node, "document", nn_document, 0);
3553
3564
  rb_define_method(c_native_node, "attributes", nn_attributes, 0);
@@ -3566,6 +3577,7 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
3566
3577
  rb_define_alias(c_native_node, "text_read", "content");
3567
3578
  rb_define_alias(c_native_node, "name_read", "name");
3568
3579
  rb_define_method(c_native_node, "children", nn_children, 0);
3580
+ rb_define_alias(c_native_node, "children_read", "children");
3569
3581
  rb_define_method(c_native_node, "element_children", nn_element_children, 0);
3570
3582
  rb_define_method(c_native_node, "next_sibling", nn_next_sibling, 0);
3571
3583
  rb_define_method(c_native_node, "parent", nn_parent, 0);
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.372.0"
4
+ VERSION = "1.9.374.0"
5
5
  end
@@ -27,6 +27,15 @@ class Leptris::XML::DocumentFragment
27
27
  end
28
28
 
29
29
  def self.parse(xml, document)
30
+ # leptris#1781: the second argument is the HOST document (the
31
+ # fragment borrows its lifetime). A nil or wrong-typed argument
32
+ # used to surface from the native face as "document has been
33
+ # freed" — misread as a GC/lifecycle bug. Name the contract.
34
+ unless document.is_a?(Leptris::XML::Document)
35
+ raise ArgumentError,
36
+ "second argument must be the host Leptris::XML::Document " \
37
+ "(got #{document.inspect})"
38
+ end
30
39
  # TODO.perf/34: the C face answers the fragment address with
31
40
  # no status MemoryPointer and no Pointer mint; failures fall
32
41
  # back to the FFI pair for the exact error.
@@ -60,4 +69,33 @@ class Leptris::XML::DocumentFragment
60
69
  def name
61
70
  "#document-fragment"
62
71
  end
72
+
73
+ # The serialization face (leptris#1781): a fragment serializes as
74
+ # its children, concatenated — the synthetic "#document-fragment"
75
+ # wrapper never emits (Nokogiri's contract). One C pass over the
76
+ # child chain — the same pass Element#inner_html uses; the FFI
77
+ # fallback walks the children per node.
78
+ def to_html
79
+ ensure_host_alive!
80
+ return Leptris::XML::Native.fast_inner_xml(@c_address) if
81
+ defined?(Leptris::XML::NATIVE_FAST)
82
+ children.map(&:to_html).join
83
+ end
84
+
85
+ def to_xml(*, **)
86
+ to_html
87
+ end
88
+
89
+ def to_s
90
+ to_html
91
+ end
92
+
93
+ private
94
+
95
+ def ensure_host_alive!
96
+ # the document's own convention (UseAfterFreeError) — the
97
+ # fragment borrows the host's lifetime
98
+ raise Leptris::XML::UseAfterFreeError, "document has been freed" if
99
+ @document.freed?
100
+ end
63
101
  end
Binary file
Binary file
data/lib/libleptris.so CHANGED
Binary file
data/vendor-src/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  Sources vendored in this gem (recompile rights; the ruby
2
2
  variant builds them at install via extconf.rb):
3
3
 
4
- - libleptris v1.9.372
4
+ - libleptris v1.9.374
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,48 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.374] - 2026-10-19
4
+
5
+ ### Fixed
6
+
7
+ - **dom: detached text mutations resolve their document
8
+ (moxml#401, PR #1785).** `Text#content=` on a detached text node
9
+ — subtree removed, node unlinked, or document-level stray text —
10
+ failed with `Invalid argument`. Three composed gaps: the
11
+ document resolver's parent walk stopped at a detached ancestor
12
+ instead of falling through to the node's #1320 owner stamp;
13
+ parse-carved text nodes were never stamped at all (only the
14
+ public creator stamped); and lazily-promoted documents had no
15
+ pool at mutation time. All parsed text stamps at carve now; a
16
+ detached mutation promotes the document. Red-first specs
17
+ (`DomTextMutate.*`); 100% of 2066 ctest.
18
+
19
+
20
+
21
+ ## [1.9.373] - 2026-10-19
22
+
23
+ ### Added
24
+
25
+ - **html: the #1771 verdict-parity gate (PRs #1778, #1779, #1780,
26
+ #1782, #1783).** The HTML5 corpus lane now gates on the §13.2.5
27
+ parse-error stream, not just tree shape: html5lib's own #errors
28
+ verdict vs `LEPTRIS_PARSE_COLLECT_HTML_ERRORS` per case. New
29
+ emission sites — the doctype-less initial insertion-mode family
30
+ (chars/start-tag/end-tag/EOF), EOF with open elements, the
31
+ raw-text EOF variants (named vs generic per the corpus's own
32
+ mapping), frameset-after-body-content, and the fenced
33
+ any-other-end-tag walk. Verdict parity 190 → 1394 of 1555 (floor
34
+ pinned); code-set overlap 975/1231, 417 exact; trees
35
+ byte-identical throughout (1555/1555).
36
+
37
+ ### Changed
38
+
39
+ - **docs: the mission and the mission plan (#1776, #1777).** The
40
+ two-sided bar — conformance suites as the gate, benchmark
41
+ supremacy as the goal — now lives in CLAUDE.md, the README, and
42
+ `docs/MISSION-PLAN.md` (four falsifiable gates, phase
43
+ sequencing, invariants).
44
+
45
+
3
46
  ## [1.9.372] - 2026-10-19
4
47
 
5
48
  ### Changed
@@ -6,6 +6,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
6
6
 
7
7
  **libleptris** — pure C99 XML 1.0 parser, XPath 1.0 engine (W3C-conformant), and SAX parser. Ships as a static/shared library plus a `leptris` CLI. Zero required runtime deps; `utf8proc` (Unicode) and `iconv` (encoding conversion) are optional.
8
8
 
9
+ **Mission.** Replace the libxml2 era with a clean-room, dependency-free XML stack that is simultaneously more conformant and faster than the incumbents, delivered natively to every language it reaches. The bar is two-sided:
10
+
11
+ - **Conformance suites are the gate.** W3C XPath (438/438), libxslt (205/205), QT3, the WHATWG/html5lib corpus — a feature isn't done until the suite says so.
12
+ - **Benchmark supremacy is the goal.** No published scoreboard row — parse, serialize, XPath, walk+read, build — may trail nokogiri, libxml2, or pugixml.
13
+
14
+ One engine, four runtime bindings — leptris-ruby (Nokogiri-compatible), leptris-py, leptris-ts (WebAssembly), leptris-rs — same engine, same numbers everywhere.
15
+
9
16
  ## Build & Test
10
17
 
11
18
  CMake-based (CMake ≥ 3.20). The default build is a static lib + CLI + tests:
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.372
6
+ VERSION 1.9.374
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -17,6 +17,28 @@ canonicalization (C14N). It is designed for low-latency,
17
17
  memory-efficient workloads where predictable performance and safe
18
18
  memory handling matter.
19
19
 
20
+ == Mission
21
+
22
+ Replace the libxml2 era with a clean-room, dependency-free XML stack
23
+ that is simultaneously more conformant and faster than the incumbents,
24
+ delivered natively to every language it reaches.
25
+
26
+ The bar is two-sided:
27
+
28
+ * **Conformance suites are the gate.** W3C XPath (438/438), libxslt
29
+ (205/205), QT3, the WHATWG/html5lib corpus — a feature isn't done
30
+ until the suite says so.
31
+ * **Benchmark supremacy is the goal.** No published scoreboard row —
32
+ parse, serialize, XPath, walk+read, build — may trail nokogiri,
33
+ libxml2, or pugixml.
34
+
35
+ One engine, four runtime bindings —
36
+ https://github.com/leptris/leptris-ruby[leptris-ruby]
37
+ (Nokogiri-compatible), https://github.com/leptris/leptris-py[leptris-py],
38
+ https://github.com/leptris/leptris-ts[leptris-ts] (WebAssembly), and
39
+ https://github.com/leptris/leptris-rs[leptris-rs] — same engine, same
40
+ numbers everywhere.
41
+
20
42
  == Highlights
21
43
 
22
44
  * **XML 1.0 parsing** with UTF-8 validation and optional encoding
@@ -0,0 +1,163 @@
1
+ # The Mission Plan
2
+
3
+ How the mission — *replace the libxml2 era with a clean-room,
4
+ dependency-free XML stack that is simultaneously more conformant and
5
+ faster than the incumbents, delivered natively to every language it
6
+ reaches* — is fully achieved.
7
+
8
+ "Fully achieved" is not a feeling. The mission decomposes into four
9
+ falsifiable gates. Each gate has a measurement command whose output is
10
+ the verdict. Every number below is **last-known (date)** — Phase 0
11
+ re-baselines everything before any work is sequenced against it.
12
+
13
+ ---
14
+
15
+ ## Gate A — Conformance: no suite red anywhere
16
+
17
+ A feature isn't done until the suite says so. Conformance is the
18
+ moat: it is the one axis where "clean-room" must prove it gave up
19
+ nothing to libxml2.
20
+
21
+ | # | Suite | Exit | Last known |
22
+ |---|---|---|---|
23
+ | A1 | W3C XPath 1.0 | 438/438 pinned in CI | **done** — 438/438 |
24
+ | A2 | libxslt (XSLT 1.0) | 205/205 pinned (Linux-authoritative; macOS skips bug-169 iconv-encoding) | **done** — 205/205 (v1.9.19) |
25
+ | A3 | XsltFull | 27/27 + full suite green; XSLT gate (v1.10.0) stays met | **done** — gate MET (#1761) |
26
+ | A4 | QT3 / XQuery 3.1 | 0 unexplained pins; every pin has a recorded disposition | pins ~41–99 by slice (2026-09/10) — **re-pin fresh** |
27
+ | A5 | WHATWG/html5lib (#659) | 0 reds on the tree gate; verdict parity climbing on the #1771 error-verdict gate | **tree lane CLOSED** — 1555/1555, redlist retired (found during Phase 0); verdict gate flipped 2026-10-19: 1312/1555, floor pinned |
28
+ | A6 | XSD conformance lane | suite-complete per the lane's own queue | 769→795 (2026-10-14, queue #1691) |
29
+ | A7 | DTD phase 8 (TODO 91) | ENTITY/ENTITIES attribute resolution, parameter entities, choice-model backtracking | open |
30
+ | A8 | XInclude parse="xml" + xpointer (TODO 92) | spec-complete | open (parse="text" + fallback ship) |
31
+ | A9 | RelaxNG / C14N / SAX | parity maintained, no reds | green |
32
+
33
+ **Working rule (proven across A1–A3):** suite-driven fixes only —
34
+ read the failing case, name the root cause, red-first spec, fix,
35
+ re-run the whole suite. The recurring root causes are already
36
+ documented (RTF binding, foreign-doc patterns, §3.4 ordering,
37
+ xmlXPathFormatNumber, define-body sibling lists, carrier-by-pointer).
38
+ Every engine fix lands with its regression spec before the next case
39
+ is opened.
40
+
41
+ **Exit:** `scripts/validate.sh` green + each suite's command in
42
+ `VALIDATION.md` green + zero pins without a disposition file.
43
+
44
+ ---
45
+
46
+ ## Gate B — Performance: no published row trails
47
+
48
+ The scoreboard is published pairs — CPU-time medians, 31
49
+ interleaved reps, fresh build dirs, identical `CMAKE_BUILD_TYPE`
50
+ on both sides. The discipline *is* the result; phantom wins come
51
+ from stale build dirs, so the harness protocol is part of the gate.
52
+
53
+ | Row | Bar | Last known |
54
+ |---|---|---|
55
+ | parse | ≥ nokogiri/libxml2/pugixml | 2.6x — ahead |
56
+ | serialize | ≥ all | 2.6x — ahead |
57
+ | XPath | ≥ all | 18x — ahead |
58
+ | attr listing | ≥ all | 99x — ahead |
59
+ | build | ≥ all | 0.87x — ahead |
60
+ | walk+read (moxml) | ≥ 0.95x nokogiri | **done 2026-10-19** — ~195ns name / ~193ns content, 0.90–0.95x |
61
+ | append / attr-parse (pugixml lane 18) | ≥ parity | **RED 2.0x / 5.9x** — hash-prefilter lever named |
62
+ | DOM mutation rows | ≥ parity | RED — node-layout redesign, owner decision pending |
63
+ | zero-copy text (#1218 lever) | adopt if it wins any row | lever named, unmeasured |
64
+
65
+ **Working rules:** algorithms before struct bytes (the attr-parse and
66
+ append rounds proved it); measure deltas, not absolutes (spill and
67
+ extension-block effects are invisible in means — count events);
68
+ one local build dir at -j4, CI is the gate; macro-benchmarks claim
69
+ nothing without interleaved A/B medians.
70
+
71
+ **Exit:** every published row ≥ 1.00x median-of-runs on the published
72
+ harnesses + chokepoint regression gates in CI (the write-path and
73
+ attr-path chokepoints get the same treatment conformance got).
74
+
75
+ ---
76
+
77
+ ## Gate C — Delivery: the engine reaches every language, natively
78
+
79
+ The engine is nothing until a consumer can install it. Lockstep law:
80
+ runtime versions are `{engine-semver}.{binding-patch}`; every engine
81
+ release gets its runtime ride; ride branches push only after the tag
82
+ lands (TAG-RACE).
83
+
84
+ | # | Surface | Exit | Last known |
85
+ |---|---|---|---|
86
+ | C1 | leptris-ruby / -py / -ts rides | automated, proven (1.9.372 train: rubygems full matrix, PyPI, npm+provenance) | **done**, keep green |
87
+ | C2 | leptris-rs descriptor crate | `leptris-descriptor` published; safe-wrapper crate rides lockstep | feat/294 in flight, not published |
88
+ | C3 | moxml gem | cut (owner-dispatched) carries the walk+read adoption | merged on main, release pending owner |
89
+ | C4 | Prebuilt binaries everywhere consumers lack compilers | rubygems platform matrix ✓, PyPI wheels ✓, npm WASM ✓ | **done**, keep green |
90
+ | C5 | System packaging | vcpkg ✓; add homebrew / conan / deb formulas so "install libleptris" is one command per platform | open |
91
+ | C6 | Cargo publish lane | OIDC-free trusted publishing (crates-io-auth-action@v1) wired into the rust-release workflow; CARGO_REGISTRY_TOKEN secret set once | workflow shipped; secret/token state to verify in Phase 0 |
92
+
93
+ **Exit:** a consumer on any of the four runtimes installs a prebuilt
94
+ binary with one command; the descriptor ABI crate is consumable from
95
+ Rust with no engine checkout.
96
+
97
+ ---
98
+
99
+ ## Gate D — Adoption & durability: the libxml2 era actually ends
100
+
101
+ Replacing an era is an ecosystem event, not a benchmark. This gate is
102
+ the proof that downstream can and does move.
103
+
104
+ | # | Proof | Exit |
105
+ |---|---|---|
106
+ | D1 | Downstream corpus compiles end-to-end on leptris — metanorma/lutaml document sets parse, validate, transform, serialize with libxml2 removed from the path | green corpus runs recorded per release |
107
+ | D2 | Migration surface documented — nokogiri-compat face, per-adapter notes in moxml, a libxml2→libleptris migration guide in `docs/guide/` | guide ships |
108
+ | D3 | Security posture — nightly fuzzing ✓, ASAN CI ✓, ASAN Linux-only leak gate ✓; plus a documented advisory/reporting channel and a CVE-response note | security.md ships |
109
+ | D4 | 1.0 — semver freeze, ABI stability policy (export-surface gate stays green), documented platform matrix (ILP32/arm64/Intel legs, s390x gate) | v1.0.0 tagged through the release workflow, never by hand |
110
+ | D5 | Durability gates in CI — conformance suites and perf chokepoints fail the build on regression, so the mission cannot silently erode | gates wired into test.yml/asan.yml/fuzz-nightly.yml siblings |
111
+
112
+ **Exit:** D1 green + v1.0.0 shipped. That is the day the era ends.
113
+
114
+ ---
115
+
116
+ ## Phase sequencing
117
+
118
+ **Phase 0 — Re-baseline (first, everything depends on it).**
119
+ Run every Gate A command and every Gate B harness fresh. Produce one
120
+ numbered table dated today. Nothing in this plan is sequenced against
121
+ a stale number. (The #659 and QT3 figures above are already stale.)
122
+
123
+ **Phase 1 — Conformance closure (A4–A8).** Largest and longest. Order
124
+ by leverage: A5 (corpus tail — user-visible HTML correctness), A4
125
+ (QT3 pins — the XQuery story), A6 (XSD lane), A7/A8 (DTD/XInclude
126
+ completeness). One PR per fix, red-first spec with every fix,
127
+ suite-wide re-run per landing.
128
+
129
+ **Phase 2 — Performance closure (B reds).** Parallelizable with
130
+ Phase 1's tail: lane-18 hash prefilter first (named lever, biggest
131
+ gap), zero-copy text next, node-layout/mutation rows behind the
132
+ owner decision.
133
+
134
+ **Phase 3 — Delivery completion (C2/C3/C5/C6).** Descriptor crate
135
+ publish, moxml gem cut, packaging formulas. Small, mechanical, can
136
+ ride any release train.
137
+
138
+ **Phase 4 — Durability & adoption (D).** Migration guide, security
139
+ note, CI durability gates, then the 1.0 freeze. 1.0 ships **only
140
+ after** Gates A and B report zero reds — it is the seal, not a date.
141
+
142
+ ## Invariants (the operating system)
143
+
144
+ These laws produced every number above; the plan executes inside
145
+ them, never around them:
146
+
147
+ - Falsifiability — red-first specs; a test that passes both sides is
148
+ not a regression test.
149
+ - Lockstep + TAG-RACE — runtime versions ride engine semver; ride
150
+ branches push only after the tag lands.
151
+ - Release workflow owns versions and tags; humans never tag by hand.
152
+ - Benchmark discipline — fresh build dirs, explicit build types,
153
+ medians of interleaved reps, or the number does not exist.
154
+ - One PR per work item; branch → tests → ASAN → benchmark →
155
+ rebase-merge; verify content on main after every merge.
156
+ - Binding repos release only through their own automated flows.
157
+
158
+ ## The scoreboard of record
159
+
160
+ One table, updated at every release train: gate, command, last run,
161
+ verdict. When every row of Gate A and Gate B reads "green (dated)"
162
+ and Gate D1 is green, the mission is achieved — and v1.0.0 is the
163
+ tag that says so.
@@ -294,7 +294,13 @@ static LeptrisDocument node_public_document(LeptrisNodeRef node) {
294
294
  return leptris_element_get_document((LeptrisElement)node);
295
295
  }
296
296
  LeptrisElement parent = leptris_node_parent(node);
297
- if (parent) return leptris_element_get_document(parent);
297
+ if (parent) {
298
+ /* moxml#401: a DETACHED ancestor resolves to nothing — fall
299
+ * through to the node's own #1320 stamp instead of
300
+ * returning NULL from the parent's failed walk. */
301
+ LeptrisDocument pd = leptris_element_get_document(parent);
302
+ if (pd) return pd;
303
+ }
298
304
  /* Issue #519: detached non-element nodes carry their owning
299
305
  * document — mutations must work before any attach. Text was the
300
306
  * one kind #519 skipped (its pool backref covered it until #1285
@@ -413,7 +419,12 @@ LEPTRIS_API LeptrisStatus leptris_text_node_set_content(LeptrisNodeRef node,
413
419
  /* #1285 slice 4: the pool field is gone — allocate the
414
420
  * NUL-terminated copy from the owning document's pool. */
415
421
  LeptrisDocument doc = node_public_document(node);
416
- if (!doc || !doc->pool) return LEPTRIS_ERROR_INVALID_ARG;
422
+ if (!doc) return LEPTRIS_ERROR_INVALID_ARG;
423
+ /* TODO 139: lazily-promoted documents carry no pool until the
424
+ * promote runs — a detached mutation is the promote trigger
425
+ * (moxml#401). */
426
+ leptris_document_ensure_promoted(doc);
427
+ if (!doc->pool) return LEPTRIS_ERROR_INVALID_ARG;
417
428
  char* copy = node_public_pool_strdup(doc->pool, content, len);
418
429
  if (!copy) return LEPTRIS_ERROR_MEMORY;
419
430
  leptris_textnode_set_content_ptr_doc(t, copy, len, doc);
@@ -33,7 +33,15 @@ LeptrisTextNode* leptris_text_create(const char* content,
33
33
  node->base.raw = 0; /* uninitialized: see create_borrowed */
34
34
  node->parent_off = 0;
35
35
  node->next_sibling_off = 0;
36
- node->owner_doc_off = 0; /* #1320: only the public creator stamps */
36
+ /* moxml#401: stamp here when the doc is in hand — the #1320
37
+ * "only the public creator stamps" rule left every parse-carved
38
+ * and builder-created text unstamped, so a DETACHED text (its
39
+ * subtree removed) could not resolve its document for mutations
40
+ * (content= -> INVALID_ARG). */
41
+ node->owner_doc_off =
42
+ doc ? leptris_compact_int32_encode_doc(node, doc,
43
+ &node->owner_doc_off, doc)
44
+ : 0;
37
45
 
38
46
  if (content && content_len > 0) {
39
47
  memcpy(content_storage, content, content_len);
@@ -82,7 +90,13 @@ LeptrisTextNode* leptris_text_create_borrowed(const char* content,
82
90
  node->content_len = (uint32_t)content_len;
83
91
  node->parent_off = 0;
84
92
  node->next_sibling_off = 0;
85
- node->owner_doc_off = 0; /* #1320: only the public creator stamps */
93
+ /* moxml#401: same stamp — the borrowed flavor's doc tag is
94
+ * MANDATORY (its content pointer needs the doc-tagged fallback
95
+ * anyway), so the owner resolution must have it too. */
96
+ node->owner_doc_off =
97
+ doc ? leptris_compact_int32_encode_doc(node, doc,
98
+ &node->owner_doc_off, doc)
99
+ : 0;
86
100
 
87
101
  return node;
88
102
  }
@@ -1326,7 +1326,15 @@ static inline LeptrisTextNode* dp_text_create(DParser* p,
1326
1326
  tn->content_len = (uint32_t)content_len;
1327
1327
  tn->parent_off = 0;
1328
1328
  tn->next_sibling_off = 0;
1329
- tn->owner_doc_off = 0; /* #1320: parse-carved nodes attach */
1329
+ /* moxml#401: carve-time stamp — "attach will fix it" left every
1330
+ * parse-carved text unstamped forever (nothing re-stamps at
1331
+ * attach), so a DETACHED subtree's text could not resolve its
1332
+ * document for mutations. */
1333
+ tn->owner_doc_off =
1334
+ p->doc ? leptris_compact_int32_encode_doc(tn, p->doc,
1335
+ &tn->owner_doc_off,
1336
+ p->doc)
1337
+ : 0;
1330
1338
  return tn;
1331
1339
  }
1332
1340
 
@@ -3227,7 +3235,12 @@ static struct leptris_document* dp_il_build(
3227
3235
  * run is already NUL-terminated at carve) */
3228
3236
  tn->parent_off = 0;
3229
3237
  tn->next_sibling_off = 0;
3230
- tn->owner_doc_off = 0; /* #1320: parse-carved nodes attach */
3238
+ /* moxml#401: carve-time stamp (see the text carve). */
3239
+ tn->owner_doc_off =
3240
+ doc ? leptris_compact_int32_encode_doc(tn, doc,
3241
+ &tn->owner_doc_off,
3242
+ doc)
3243
+ : 0;
3231
3244
  smap[i] = (LeptrisNode*)tn;
3232
3245
  pp.depth = (int)rdepth[r->parent] + 1;
3233
3246
  pp.last_child_stack[pp.depth - 1] = lc[r->parent];
@@ -3139,6 +3139,8 @@ static uint8_t h_id_p, h_id_li, h_id_dd, h_id_dt, h_id_ol, h_id_ul,
3139
3139
  h_id_frameset, h_id_button, h_id_applet, h_id_caption,
3140
3140
  h_id_colgroup, h_id_marquee, h_id_object, h_id_html, h_id_rb,
3141
3141
  h_id_rt, h_id_rp, h_id_rtc, h_id_listing, h_id_plaintext;
3142
+ static uint8_t h_id_textarea, h_id_title, h_id_xmp, h_id_iframe,
3143
+ h_id_noembed, h_id_noframes;
3142
3144
  static uint8_t h_heading_lut[256], h_ruby_lut[256], h_p_fence_lut[256],
3143
3145
  h_li_fence_lut[256], h_dd_fence_lut[256];
3144
3146
  /* #1218 slice 6: the per-name strcmp chains the sampler pinned at
@@ -3186,6 +3188,12 @@ static void h_id_luts_init(void) {
3186
3188
  h_id_rtc = h_tag_id("rtc");
3187
3189
  h_id_listing = h_tag_id("listing");
3188
3190
  h_id_plaintext = h_tag_id("plaintext");
3191
+ h_id_textarea = h_tag_id("textarea");
3192
+ h_id_title = h_tag_id("title");
3193
+ h_id_xmp = h_tag_id("xmp");
3194
+ h_id_iframe = h_tag_id("iframe");
3195
+ h_id_noembed = h_tag_id("noembed");
3196
+ h_id_noframes = h_tag_id("noframes");
3189
3197
  h_id_head = h_tag_id("head");
3190
3198
  h_id_body = h_tag_id("body");
3191
3199
  h_id_noscript = h_tag_id("noscript");
@@ -5584,6 +5592,22 @@ static void h_diag(HBuilder* b, const char* code, size_t off,
5584
5592
  code, line, col, "%s", detail);
5585
5593
  }
5586
5594
 
5595
+ /* #1771 verdict slice: a non-whitespace character run while the
5596
+ * doctype-less initial insertion mode is still open is the spec's
5597
+ * expected-doctype-but-got-chars (13.2.6.4.1). Judged once per run
5598
+ * here — ahead of the per-path flush/drop decisions, so dropped or
5599
+ * re-parented text still reports. */
5600
+ static void h_initial_chars_diag(HBuilder* b, const char* from,
5601
+ const char* to) {
5602
+ for (const char* c = from; c < to; c++)
5603
+ if (*c && !h_is_ws(*c)) {
5604
+ h_diag(b, "expected-doctype-but-got-chars",
5605
+ (size_t)(from - b->input_base),
5606
+ "characters in the initial insertion mode");
5607
+ return;
5608
+ }
5609
+ }
5610
+
5587
5611
  static LeptrisDocument html_parse_shared(
5588
5612
  const char* buf, size_t len, LeptrisStatus* status, int whatwg,
5589
5613
  LeptrisParseFlags flags) {
@@ -5653,6 +5677,12 @@ static LeptrisDocument html_parse_shared(
5653
5677
  if (p + 1 >= end) { p++; continue; }
5654
5678
  char k1 = p[1];
5655
5679
 
5680
+ /* #1771: judge a pending character run against the
5681
+ * doctype-less initial mode before any branch flushes or
5682
+ * drops it (expected-doctype-but-got-chars). */
5683
+ if (b.whatwg && !b.left_initial && !doc->doctype && text < p)
5684
+ h_initial_chars_diag(&b, text, p);
5685
+
5656
5686
  if (k1 == '!') {
5657
5687
  /* Flush pending text first. */
5658
5688
  if (text < p) {
@@ -6033,6 +6063,10 @@ static LeptrisDocument html_parse_shared(
6033
6063
  continue;
6034
6064
  }
6035
6065
  /* End tag: ends the initial insertion mode too. */
6066
+ if (b.whatwg && !b.left_initial && !doc->doctype)
6067
+ h_diag(&b, "expected-doctype-but-got-end-tag",
6068
+ (size_t)(p - b.input_base),
6069
+ "end tag in the initial insertion mode");
6036
6070
  if (b.whatwg && !b.left_initial && text < p) {
6037
6071
  int ws_only = 1;
6038
6072
  for (const char* c = text; c < p; c++)
@@ -6559,7 +6593,21 @@ static LeptrisDocument html_parse_shared(
6559
6593
  (h_ieq_raw(on, "ul") ||
6560
6594
  h_ieq_raw(on, "ol")));
6561
6595
  }
6562
- if (fenced) break;
6596
+ if (fenced) {
6597
+ /* #1771: html5lib's walk reports
6598
+ * unexpected-end-tag when a special
6599
+ * element stops the search before
6600
+ * the target is found (endTagOther);
6601
+ * the list-scope fences map to its
6602
+ * endTagListItem not-in-scope
6603
+ * error. */
6604
+ h_diag(&b, "unexpected-end-tag",
6605
+ (size_t)(p - b.input_base),
6606
+ "</%s> ignored: a special "
6607
+ "element fences the target",
6608
+ lname);
6609
+ break;
6610
+ }
6563
6611
  }
6564
6612
  if (on && tag_match) {
6565
6613
  /* Scope guard (WHATWG): a foreign
@@ -6816,6 +6864,13 @@ static LeptrisDocument html_parse_shared(
6816
6864
  if (*c != 0 && !h_is_ws(*c)) { ws_only = 0; break; }
6817
6865
  if (ws_only) text = p;
6818
6866
  }
6867
+ /* #1771: a start tag in the doctype-less initial insertion
6868
+ * mode is expected-doctype-but-got-start-tag (13.2.6.4.1) —
6869
+ * even when the tag itself is a dropped structural one. */
6870
+ if (b.whatwg && !b.left_initial && !doc->doctype)
6871
+ h_diag(&b, "expected-doctype-but-got-start-tag",
6872
+ (size_t)(p - b.input_base),
6873
+ "start tag in the initial insertion mode");
6819
6874
  b.left_initial = 1;
6820
6875
  /* 13.2.5.4.4: non-whitespace body text (NUL ignored — an
6821
6876
  * ignored token cannot clear the flag) clears frameset-ok;
@@ -7022,6 +7077,18 @@ static LeptrisDocument html_parse_shared(
7022
7077
  /* frameset is NOT in the 13.2.6.5 breakout list: inside
7023
7078
  * foreign content it is a plain foreign element
7024
7079
  * (plain-text-unsafe:16-23). */
7080
+ /* #1771: html5lib's verdict — a <frameset> token that
7081
+ * reaches the body phase after body content is
7082
+ * unexpected-start-tag whether it then converts or is
7083
+ * ignored (webkit01:51); head-phase converters and
7084
+ * in-template framesets stay clean. body_seen is not
7085
+ * set by the frameset token itself (it is in the
7086
+ * head-only list), so it reads PRIOR content only. */
7087
+ if (b.body_seen && !b.frameset &&
7088
+ h_template_idx(&b) < 0)
7089
+ h_diag(&b, "unexpected-start-tag",
7090
+ (size_t)(p - b.input_base),
7091
+ "frameset after body content");
7025
7092
  /* depth<=1: at most the (explicit or synthesized) html
7026
7093
  * element is open; frameset-ok is the spec gate and the
7027
7094
  * body is removed wholesale when it converts. */
@@ -8121,6 +8188,27 @@ static LeptrisDocument html_parse_shared(
8121
8188
  cq++;
8122
8189
  }
8123
8190
  p = (cq < end) ? cq + 1 : end;
8191
+ /* #1771: the element never closed at EOF — either the
8192
+ * raw scan consumed to EOF without a close tag, or the
8193
+ * close tag itself is unterminated (</script foo= —
8194
+ * tests16:11-60). html5lib's named variant for the
8195
+ * strict containers, the generic one for the RCDATA
8196
+ * pair (mapping mined from the corpus: script/style/
8197
+ * xmp/noframes/noembed/iframe -> named; textarea/
8198
+ * title -> generic; plaintext has no close form and
8199
+ * never reports). Judged here, before the pop. */
8200
+ if (b.whatwg && cq >= end &&
8201
+ strcmp(name, "plaintext") != 0) {
8202
+ int named = strcmp(name, "textarea") != 0 &&
8203
+ strcmp(name, "title") != 0;
8204
+ h_diag(&b,
8205
+ named ? "expected-named-closing-tag-but-got-eof"
8206
+ : "expected-closing-tag-but-got-eof",
8207
+ (size_t)(end - b.input_base),
8208
+ "raw-text element <%s> left open at end of "
8209
+ "input",
8210
+ name);
8211
+ }
8124
8212
  b.depth--; /* the raw element is complete */
8125
8213
  text = p;
8126
8214
  continue;
@@ -8201,6 +8289,11 @@ static LeptrisDocument html_parse_shared(
8201
8289
  }
8202
8290
 
8203
8291
  /* Trailing text. */
8292
+ /* #1771: a trailing character run in the doctype-less initial
8293
+ * insertion mode is expected-doctype-but-got-chars too — the
8294
+ * loop-internal judge only runs at markup boundaries. */
8295
+ if (b.whatwg && !b.left_initial && !doc->doctype && text < end)
8296
+ h_initial_chars_diag(&b, text, end);
8204
8297
  if (text < end) {
8205
8298
  LeptrisTextNode* t = h_text_node(&b, text, end);
8206
8299
  if (t) {
@@ -8230,6 +8323,30 @@ done:
8230
8323
  /* Nothing appended (empty input, stray end tags only, doctype
8231
8324
  * only) is NOT an error in lenient HTML mode: the wrapper
8232
8325
  * synthesis below yields the empty Nokogiri shape. */
8326
+ /* #1771: elements still open at EOF are
8327
+ * expected-closing-tag-but-got-eof (html5lib's verdict; the
8328
+ * implied html shell does not count). Judged BEFORE the
8329
+ * wrapper synthesis, which tears the open stack down. Our
8330
+ * stack carries no implied html/body, so the html5lib
8331
+ * top-of-stack rule maps to: any real element open, gated on
8332
+ * </body>/</html> never having been seen — after-body EOF is
8333
+ * clean whatever remains (tests1:34). A raw-text container
8334
+ * got its own variant at the raw branch already (that branch
8335
+ * pops the element, so the top here is whatever wraps it). */
8336
+ if (b.whatwg && b.depth > 0) {
8337
+ uint8_t top = b.open_id[b.depth - 1];
8338
+ if (top != h_id_html && top != h_id_head &&
8339
+ top != h_id_body)
8340
+ h_diag(&b,
8341
+ (top == h_id_script || top == h_id_style ||
8342
+ top == h_id_listing || top == h_id_plaintext ||
8343
+ top == h_id_textarea || top == h_id_title ||
8344
+ top == h_id_xmp || top == h_id_iframe ||
8345
+ top == h_id_noembed || top == h_id_noframes)
8346
+ ? "expected-named-closing-tag-but-got-eof"
8347
+ : "expected-closing-tag-but-got-eof",
8348
+ b.input_len, "elements left open at end of input");
8349
+ }
8233
8350
  /* Nokogiri document shape (#659): with no explicit <html>,
8234
8351
  * synthesize <html><head></head><body>content</body></html> —
8235
8352
  * the document model is single-rooted, and libxml2's
@@ -8361,6 +8478,12 @@ done:
8361
8478
  b.body_attr_n);
8362
8479
  }
8363
8480
  }
8481
+ /* #1771: EOF in the doctype-less initial insertion mode is
8482
+ * expected-doctype-but-got-eof (13.2.6.4.1) — empty and
8483
+ * whitespace-only documents included. */
8484
+ if (b.whatwg && !b.left_initial && !doc->doctype)
8485
+ h_diag(&b, "expected-doctype-but-got-eof", b.input_len,
8486
+ "document ended without a DOCTYPE");
8364
8487
  doc->new_dom_root = b.root;
8365
8488
  leptris_root_doc_register(b.root, doc);
8366
8489
  if (b.prolog_head) {
@@ -3386,3 +3386,47 @@ TEST(DomBasics, AttributesSnapshotEmptyAndNullContracts) {
3386
3386
 
3387
3387
  leptris_document_free(doc);
3388
3388
  }
3389
+
3390
+ /* moxml#401: Text#content= on a DETACHED text node (its subtree
3391
+ * removed, or the node itself unlinked) failed with INVALID_ARG —
3392
+ * the resolver's parent walk stopped at the detached ancestor and
3393
+ * the #1320 owner stamp was only set by the public creator, never
3394
+ * by the parse paths. Detached text mutations must reach the
3395
+ * owning document's pool. */
3396
+ TEST(DomTextMutate, DetachedSubtreeTextSetContent) {
3397
+ LeptrisStatus st;
3398
+ LeptrisDocument doc = leptris_parse_string(
3399
+ "<r><a>hello world</a></r>", 25, &st);
3400
+ ASSERT_NE(doc, nullptr);
3401
+ LeptrisNodeRef root = (LeptrisNodeRef)leptris_document_root(doc);
3402
+ ASSERT_NE(root, nullptr);
3403
+ LeptrisElement a = (LeptrisElement)leptris_node_first_child(root);
3404
+ ASSERT_NE(a, nullptr);
3405
+ LeptrisNodeRef t = leptris_node_first_child((LeptrisNodeRef)a);
3406
+ ASSERT_EQ(leptris_node_get_type(t), LEPTRIS_NODE_TYPE_TEXT);
3407
+
3408
+ /* detach the element subtree carrying the text */
3409
+ ASSERT_EQ(leptris_element_remove_child(
3410
+ (LeptrisElement)root, a),
3411
+ LEPTRIS_OK);
3412
+ EXPECT_EQ(leptris_text_node_set_content(t, "changed"), LEPTRIS_OK);
3413
+ const char* got = leptris_text_node_get_content(t);
3414
+ EXPECT_STREQ(got, "changed");
3415
+ leptris_document_free(doc);
3416
+ }
3417
+
3418
+ TEST(DomTextMutate, DetachedTextNodeSetContent) {
3419
+ LeptrisStatus st;
3420
+ LeptrisDocument doc = leptris_parse_string(
3421
+ "<r><a>hello world</a></r>", 25, &st);
3422
+ ASSERT_NE(doc, nullptr);
3423
+ LeptrisNodeRef root = (LeptrisNodeRef)leptris_document_root(doc);
3424
+ LeptrisElement a = (LeptrisElement)leptris_node_first_child(root);
3425
+ LeptrisNodeRef t = leptris_node_first_child((LeptrisNodeRef)a);
3426
+ /* unlink JUST the text from its parent */
3427
+ ASSERT_EQ(leptris_element_remove_child(
3428
+ a, (LeptrisElement)t),
3429
+ LEPTRIS_OK);
3430
+ EXPECT_EQ(leptris_text_node_set_content(t, "solo"), LEPTRIS_OK);
3431
+ leptris_document_free(doc);
3432
+ }
@@ -21,6 +21,8 @@
21
21
  #include <gtest/gtest.h>
22
22
  extern "C" {
23
23
  #include "leptris.h"
24
+ #include "leptris/html.h"
25
+ #include "leptris/error.h"
24
26
  /* Internal (non-exported) accessor needed for the ns comparison;
25
27
  * the public surface exposes attribute/namespace-node URIs only. */
26
28
  const char* leptris_element_get_namespace_uri(LeptrisElement elem);
@@ -50,6 +52,10 @@ struct XCase {
50
52
  bool skip = false; /* fragment pairs / script-on */
51
53
  std::string skip_why;
52
54
  XNode doc; /* synthesized document level */
55
+ /* the #errors section, verbatim "(line,col): code" entries —
56
+ * the html5lib suite's own parse-error verdict (leptris#1771
57
+ * verdict-parity gate) */
58
+ std::vector<std::string> errors;
53
59
  XCase() { doc.kind = XNode::DOC; }
54
60
  };
55
61
 
@@ -327,7 +333,11 @@ std::vector<XCase> ScanFile(const std::string& path, const std::string& fname,
327
333
  } else if (l.compare(0, 7, "#errors") == 0) {
328
334
  i++;
329
335
  while (i < lines.size() &&
330
- !(lines[i].size() && lines[i][0] == '#')) i++;
336
+ !(lines[i].size() && lines[i][0] == '#')) {
337
+ if (!lines[i].empty())
338
+ c.errors.push_back(lines[i]);
339
+ i++;
340
+ }
331
341
  } else if (l.compare(0, 18, "#document-fragment") == 0) {
332
342
  c.skip = true;
333
343
  c.skip_why = "fragment mode";
@@ -829,3 +839,147 @@ TEST(Html5LibCorpus, TreeConstruction) {
829
839
  EXPECT_GE(ppassed, (size_t)784);
830
840
  }
831
841
  }
842
+
843
+ /* ---- #1771 VERDICT PARITY (leptris#1771) ----
844
+ *
845
+ * The tree gate proves WHAT we build; this gate proves we JUDGE the
846
+ * same inputs the same way. The corpus's #errors section is
847
+ * html5lib's own parse-error verdict for every case; ours is the
848
+ * §13.2.5 token stream recorded through
849
+ * LEPTRIS_PARSE_COLLECT_HTML_ERRORS (leptris_parse_html_string_flags).
850
+ * The gate is VERDICT parity — did we error where they errored —
851
+ * per case; the code sets are compared too and dumped to the
852
+ * red-list for the tail. Positions are not compared (offset
853
+ * conventions differ legitimately). */
854
+ TEST(Html5LibCorpus, ErrorVerdictParity) {
855
+ const char* srcdir = getenv("HTML5LIB_SRC");
856
+ std::string dir = srcdir && *srcdir
857
+ ? srcdir
858
+ : LEPTRIS_HTML5LIB_DIR;
859
+ std::vector<std::string> files(kHtml5libDatFiles,
860
+ kHtml5libDatFiles +
861
+ sizeof(kHtml5libDatFiles) /
862
+ sizeof(kHtml5libDatFiles[0]));
863
+ size_t total = 0, parity = 0, skipped = 0, vfail = 0;
864
+ size_t code_both = 0, code_overlap = 0, code_equal = 0;
865
+ std::vector<std::string> failures;
866
+ std::string err;
867
+ for (const std::string& fn : files) {
868
+ std::vector<XCase> cs = ScanFile(dir + "/" + fn, fn, &err, 0, 1);
869
+ if (!err.empty() && cs.empty()) {
870
+ ADD_FAILURE() << fn << ": " << err;
871
+ continue;
872
+ }
873
+ for (auto& c : cs) {
874
+ total++;
875
+ if (c.skip) { skipped++; continue; }
876
+ LeptrisStatus st = LEPTRIS_OK;
877
+ LeptrisDocument d = leptris_parse_html_string_flags(
878
+ c.data.c_str(), c.data.size(),
879
+ LEPTRIS_PARSE_COLLECT_HTML_ERRORS, &st);
880
+ if (!d) {
881
+ vfail++;
882
+ failures.push_back(c.id + ": parse failed");
883
+ continue;
884
+ }
885
+ size_t n = leptris_document_parse_diag_count(d);
886
+ bool ours_err = n > 0;
887
+ bool want_err = !c.errors.empty();
888
+ std::string ours_codes, want_codes;
889
+ size_t shown = 0;
890
+ for (size_t i = 0; i < n && shown < 6; i++) {
891
+ char code[64] = {0};
892
+ if (leptris_document_parse_diag_code(d, i, code,
893
+ sizeof(code),
894
+ nullptr, nullptr) &&
895
+ code[0]) {
896
+ if (shown++) ours_codes += ",";
897
+ ours_codes += code;
898
+ }
899
+ }
900
+ for (size_t i = 0; i < c.errors.size(); i++) {
901
+ const std::string& e = c.errors[i];
902
+ /* corpus formats: "(l,c): code", "(l,c) code",
903
+ * "(l:c) code" — strip the "(...)" position. */
904
+ std::string code = e;
905
+ if (e.size() > 1 && e[0] == '(') {
906
+ size_t close = e.find(')');
907
+ if (close != std::string::npos) {
908
+ size_t cs = close + 1;
909
+ while (cs < e.size() &&
910
+ (e[cs] == ':' || e[cs] == ' '))
911
+ cs++;
912
+ code = e.substr(cs);
913
+ }
914
+ }
915
+ if (!want_codes.empty()) want_codes += ",";
916
+ want_codes += code;
917
+ }
918
+ leptris_document_free(d);
919
+
920
+ if (ours_err == want_err) {
921
+ parity++;
922
+ if (ours_err) {
923
+ /* secondary: code-set comparison (reported,
924
+ * not gated — the verdict is the flip) */
925
+ code_both++;
926
+ bool overlap = false, equal = false;
927
+ /* both sides are small comma lists; exact-set
928
+ * comparison on the shown prefixes */
929
+ equal = ours_codes == want_codes;
930
+ for (size_t a = 0, b = a; !overlap &&
931
+ a < ours_codes.size(); ) {
932
+ size_t ca = ours_codes.find(',', a);
933
+ std::string oc = ours_codes.substr(
934
+ a, ca == std::string::npos
935
+ ? std::string::npos : ca - a);
936
+ for (b = 0; b < want_codes.size(); ) {
937
+ size_t cb = want_codes.find(',', b);
938
+ std::string wc = want_codes.substr(
939
+ b, cb == std::string::npos
940
+ ? std::string::npos : cb - b);
941
+ if (oc == wc) { overlap = true; break; }
942
+ if (cb == std::string::npos) break;
943
+ b = cb + 1;
944
+ }
945
+ if (ca == std::string::npos) break;
946
+ a = ca + 1;
947
+ }
948
+ if (overlap) code_overlap++;
949
+ if (equal) code_equal++;
950
+ if (!overlap)
951
+ failures.push_back(c.id + ": code-set divergence want["
952
+ + want_codes + "] ours["
953
+ + ours_codes + "]");
954
+ }
955
+ } else {
956
+ vfail++;
957
+ failures.push_back(
958
+ c.id + ": verdict mismatch want"
959
+ + (want_err ? "=errors[" + want_codes + "]"
960
+ : "=clean")
961
+ + " ours"
962
+ + (ours_err ? "=errors[" + ours_codes + "]"
963
+ : "=clean"));
964
+ }
965
+ }
966
+ }
967
+ FILE* rl = fopen("html5lib-verdict-redlist.txt", "w");
968
+ if (rl) {
969
+ for (auto& f : failures) fprintf(rl, "%s\n", f.c_str());
970
+ fclose(rl);
971
+ }
972
+ printf("[verdict-parity] total=%zu parity=%zu verdict-fail=%zu "
973
+ "skip=%zu | both-error=%zu code-overlap=%zu code-equal=%zu "
974
+ "(red-list: html5lib-verdict-redlist.txt)\n",
975
+ total, parity, vfail, skipped,
976
+ code_both, code_overlap, code_equal);
977
+ for (size_t i = 0; i < failures.size() && i < 20; i++)
978
+ printf(" VERDICT %s\n", failures[i].c_str());
979
+ EXPECT_GT(total, (size_t)1500);
980
+ /* Verdict-parity floor — set at the slice-2 baseline
981
+ * (1394/1555; was 190 before the #1771 emission slices —
982
+ * the doctype-less initial-mode family, trailing chars, EOF with
983
+ * open elements; slice 3 adds the raw-text EOF variants). Only raises. */
984
+ EXPECT_GE(parity, (size_t)1394);
985
+ }
@@ -70,10 +70,16 @@ TEST(HtmlParseErrors, EofInTagCarriesSpecCode) {
70
70
  xml, strlen(xml), LEPTRIS_PARSE_COLLECT_HTML_ERRORS, &st);
71
71
  ASSERT_NE(d, nullptr);
72
72
  auto diags = drain(d);
73
- ASSERT_GE(diags.size(), 1u);
74
- EXPECT_EQ(diags[0].code, "eof-in-tag");
73
+ /* The doctype-less initial mode fires first (the <a> token),
74
+ * then the unterminated tag at EOF — html5lib's verdict for
75
+ * this input is both codes, in token order. */
76
+ ASSERT_GE(diags.size(), 2u);
77
+ EXPECT_EQ(diags[0].code, "expected-doctype-but-got-start-tag");
75
78
  EXPECT_EQ(diags[0].line, 1);
76
- EXPECT_EQ(diags[0].col, 8);
79
+ EXPECT_EQ(diags[0].col, 1);
80
+ EXPECT_EQ(diags[1].code, "eof-in-tag");
81
+ EXPECT_EQ(diags[1].line, 1);
82
+ EXPECT_EQ(diags[1].col, 8);
77
83
  leptris_document_free(d);
78
84
  }
79
85
 
@@ -84,8 +90,11 @@ TEST(HtmlParseErrors, DoctypeOutsideInitialIgnored) {
84
90
  xml, strlen(xml), LEPTRIS_PARSE_COLLECT_HTML_ERRORS, &st);
85
91
  ASSERT_NE(d, nullptr);
86
92
  auto diags = drain(d);
87
- ASSERT_GE(diags.size(), 1u);
88
- EXPECT_EQ(diags[0].code, "unexpected-doctype");
93
+ /* The <div> opens the doctype-less initial mode; the DOCTYPE
94
+ * token inside the body is unexpected-doctype (13.2.6.4.73). */
95
+ ASSERT_GE(diags.size(), 2u);
96
+ EXPECT_EQ(diags[0].code, "expected-doctype-but-got-start-tag");
97
+ EXPECT_EQ(diags[1].code, "unexpected-doctype");
89
98
  leptris_document_free(d);
90
99
  }
91
100
 
@@ -96,7 +105,15 @@ TEST(HtmlParseErrors, LineColumnTrackRealLines) {
96
105
  xml, strlen(xml), LEPTRIS_PARSE_COLLECT_HTML_ERRORS, &st);
97
106
  ASSERT_NE(d, nullptr);
98
107
  auto diags = drain(d);
99
- ASSERT_GE(diags.size(), 1u);
100
- EXPECT_EQ(diags[0].line, 3);
108
+ ASSERT_GE(diags.size(), 2u);
109
+ EXPECT_EQ(diags[0].code, "expected-doctype-but-got-start-tag");
110
+ EXPECT_EQ(diags[0].line, 1);
111
+ bool dup_found = false;
112
+ for (auto& dd : diags)
113
+ if (dd.code == "duplicate-attribute") {
114
+ dup_found = true;
115
+ EXPECT_EQ(dd.line, 3);
116
+ }
117
+ EXPECT_TRUE(dup_found);
101
118
  leptris_document_free(d);
102
119
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg/master/scripts/vcpkg.schema.json",
3
3
  "name": "leptris",
4
- "version": "1.9.372",
4
+ "version": "1.9.374",
5
5
  "description": "High-performance XML parser and XPath 1.0 engine in C (libleptris)",
6
6
  "homepage": "https://github.com/leptris/leptris",
7
7
  "license": "MIT",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.372.0
4
+ version: 1.9.374.0
5
5
  platform: arm-linux
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-19 00:00:00.000000000 Z
11
+ date: 2026-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -530,6 +530,7 @@ files:
530
530
  - vendor-src/libleptris/docs/ARCHITECTURE_REVIEW.md
531
531
  - vendor-src/libleptris/docs/Doxyfile
532
532
  - vendor-src/libleptris/docs/FFI.md
533
+ - vendor-src/libleptris/docs/MISSION-PLAN.md
533
534
  - vendor-src/libleptris/docs/README.md
534
535
  - vendor-src/libleptris/docs/ROADMAP-SAXON-PARITY.md
535
536
  - vendor-src/libleptris/docs/guide/building.md