leptris 1.9.208.0 → 1.9.209.0

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: 8c88c4d67f3b29d8e39348f96c9dfd6580563de8c1adcbb76a35a29a932a45dc
4
- data.tar.gz: '030917e922c2ae16f63d588378d7d5552e52d03cfc2b5e33043aaea39c067c78'
3
+ metadata.gz: 700c8b243ae83bca888161a3db72b2e3c6304023c2d6876716d63cbc77046469
4
+ data.tar.gz: 3f5e149db82b4dd010e7a485650321bc752d74ee198e06b5711736781eca3281
5
5
  SHA512:
6
- metadata.gz: '09ba6dc5293eb1d93becec0eef194a97466ae800017ba5e2d44169dc3912b9620398f090300147ed62a8ebcacce5b83a7165b133f9bb84771cbdefc0e4c8d303'
7
- data.tar.gz: 8b88a8828ce0bed5e3b7b8140bbb3f31238fc5e484737e6dcdc3a3f4171f3c36e768dd8245c3c7097657a7715add8afcf1c2f0a22d42c0f6fb5631b7d1771650
6
+ metadata.gz: 87f6bfc69bcabf29bef4721bddc2947311ba021146912be96f34d043123611f0bc5230fc027855d897d42b7979ff7bed97d0b6df749bcaaf9f7b62cc642c6d26
7
+ data.tar.gz: 022fa14f46227d54e5966be7f0b90da028a2ff4bed14e4c2f2f4e26343a4a684c43770637077ab31657efb6c7a63af357285558cc4d25550156c55742c39f83a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,87 @@ All notable changes to Leptris will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.209.0] - 2026-09-20
9
+
10
+ ### Performance — engine sync (1.9.209)
11
+
12
+ - Vendored libleptris 1.9.209: WHATWG HTML parse round 2
13
+ (#1218) — the per-close-tag implied-end/open-side name-list
14
+ scans (40 + 33 names, ~7% of parse self-time on table-heavy
15
+ pages) became membership bits on the first-char-bucketed tag
16
+ classifier; bench_html_parse 24.1 → 25.1-26.7 MB/s. No
17
+ binding-facing surface changes; audit holds.
18
+
19
+ ### Added — `Diff#summary` / `Diff#to_json` (the 1.9.200 output modes, #1184)
20
+
21
+ - The `leptris diff --summary` / `--json` CLI modes' library face:
22
+ per-kind op counts (`{update_attr: 1, insert: 1}`, `{}` when
23
+ identical) and the op list as JSON — pure Ruby over the existing
24
+ `#ops` face, no new engine symbols. Salvaged from #269's unique
25
+ half (its DTD half was superseded by the 1.9.204 wiring).
26
+
27
+ ## [1.9.208.0] - 2026-09-20
28
+
29
+ ### Fixed — engine sync: the #1242 TLS-memo corruption (1.9.208)
30
+
31
+ - Vendored libleptris 1.9.208: the TLS `(root, doc)` last-hit memo
32
+ in `leptris_element_get_document` trusted the root address and
33
+ was cleared only by document identity — an element-level
34
+ unregistration left `(freed_address, doc)` behind, and a
35
+ recycled address resolved the WRONG document, poisoning
36
+ attribute ownership (whole-run all-NULL
37
+ `leptris_element_attribute` reads; set_root's cross-document
38
+ check tripping EINVAL). The memo is now cleared whenever the
39
+ outgoing root address matches. `leptris_document_set_root` also
40
+ sets a fresh thread-local error on every rejection (no more
41
+ stale XPath messages), and `LEPTRIS_DEBUG_ATTR_MISS=1` dumps
42
+ attribute-chain state on lookup misses.
43
+ - **First IBM Z gem: `s390x-linux-musl`** ships from this release —
44
+ the 14th variant (qemu-built, in-container smoked). The release
45
+ build-musl containers needed `linux-headers` (the ffi gem's
46
+ bundled libffi includes `linux/limits.h` under musl; #283 fixed
47
+ the wrong apk line, #284 the one s390x runs). With #1197 fixed
48
+ (1.9.206) and strict s390x CI legs green since 1.9.207, the
49
+ big-endian platform is fully shippable.
50
+ - **CI un-quarantine where the evidence holds**: the s390x/ppc64le
51
+ qemu test legs are strict again, and so are the armv7/ppc64le/
52
+ s390x-musl release-build cells (#1174's ILP32 port shipped; arm
53
+ gems publish from every release since). The ubuntu-latest Ruby
54
+ 3.4 leg stays non-blocking: strikes 7-10 on 1.9.208 show the
55
+ engine's memo clear was incomplete — in-spec forensics captured
56
+ a direct `leptris_element_attribute` call returning NULL with an
57
+ EMPTY chain on a live yielded element, and windows-4.0 produced
58
+ the fresh `set_root: element belongs to a different document`
59
+ for a same-document element. One mechanism: the TLS memo
60
+ resolving elements to a wrong document under address-recycling
61
+ interleavings. Tracked as leptris-ruby#279 / leptris/leptris#1242
62
+ with the dump env + forensics armed on the leg until the
63
+ residual engine fix lands.
64
+ - ffi.rb dedupe: the 1.9.206-surface block re-attached ten
65
+ symbols the 1.9.204 wiring had already attached (audit counted
66
+ 345 attachments against 335 exports). The typed 1.9.206 block
67
+ is kept; audit 335/335.
68
+
69
+ ## [1.9.207.0] - 2026-09-19
70
+
71
+ ### Fixed — engine sync: big-endian close-tag parse (#1197 upstream)
72
+
73
+ - Vendored libleptris 1.9.207: the close-tag fast-path compare
74
+ masked the wrong end of its 8-byte name load on big-endian hosts,
75
+ rejecting ordinary paired-tag documents with "malformed input"
76
+ (434/727 binding-suite failures under qemu s390x). The mask now
77
+ follows memory order per byte dialect; the s390x-linux-musl
78
+ platform gem becomes viable with this release.
79
+
80
+ ### Added — FFI mirror adopts the 1.9.202–1.9.207 engine surface
81
+
82
+ - audit:symbols lockstep attachments: recover diagnostics
83
+ (leptris_document_parse_diag[_count]), the standalone DTD family
84
+ (leptris_dtd_*), declaration/doctype removal, engine-heap buffer
85
+ allocation, and one-shot SAX. leptris_set_error stays internal —
86
+ the engine now hides it on ELF too (leptris#1243), so the export
87
+ surface agrees across Linux/macOS/Windows.
88
+
8
89
  ## [1.9.204.0] - 2026-09-19
9
90
 
10
91
  ### Added — document-level mutation completed: #1229 (the 1.9.204 surface)
@@ -51,29 +132,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
51
132
  ppc64le qemu leg (ffi_spec:223) once the audit fix let the suite
52
133
  run; the spec now asserts JVM ≡ host tier.
53
134
 
54
- ### Not fixed despite upstream closes
135
+ ### Resolved upstream threads (final state)
55
136
 
137
+ - **#1220 (HTML mutations lost on serialization) — fixed in
138
+ 1.9.204** (engine commit 737fe977 / #1228). Verified from the
139
+ binding on 1.9.208: the original reopen repro serializes the
140
+ mutation on both the whatwg and html4 lanes.
56
141
  - **Iterparse attribute reads: all-nil runs on ubuntu-latest Ruby
57
- ## [1.9.207.0] - 2026-09-19
58
-
59
- ### Fixed engine sync: big-endian close-tag parse (#1197 upstream)
60
-
61
- - Vendored libleptris 1.9.207: the close-tag fast-path compare
62
- masked the wrong end of its 8-byte name load on big-endian hosts,
63
- rejecting ordinary paired-tag documents with "malformed input"
64
- (434/727 binding-suite failures under qemu s390x). The mask now
65
- follows memory order per byte dialect; the s390x-linux-musl
66
- platform gem becomes viable with this release.
67
-
68
- ### Added — FFI mirror adopts the 1.9.202–1.9.207 engine surface
69
-
70
- - audit:symbols lockstep attachments: recover diagnostics
71
- (leptris_document_parse_diag[_count]), the standalone DTD family
72
- (leptris_dtd_*), declaration/doctype removal, engine-heap buffer
73
- allocation, and one-shot SAX. leptris_set_error stays internal —
74
- the engine now hides it on ELF too (leptris#1243), so the export
75
- surface agrees across Linux/macOS/Windows.
76
-
142
+ 3.4 legs** — filed upstream as leptris/leptris#1242
143
+ (leptris-ruby#279 tracks the investigation). Originally
144
+ suspected binding-side, finally root-caused engine-side by
145
+ in-spec forensics (see the 1.9.208.0 entry); the engine's
146
+ 1.9.208 fix is incomplete and the flake persists. The leg is
147
+ gated with diagnostics armed until the residual engine fix.
77
148
  ## [1.9.201.3] - 2026-09-19
78
149
 
79
150
  ### Added — the HTML facade (Nokogiri-shaped entry points)
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.208"
11
+ LIBLEPTRIS_VERSION = "1.9.209"
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
@@ -77,13 +77,14 @@ task :compile do
77
77
  fetch_tarball.call(
78
78
  "https://api.github.com/repos/leptris/leptris/tarball/v#{version}", build)
79
79
 
80
- # NOTE: 32-bit ARM (armv7) is an ENGINE port task — the round-19
80
+ # NOTE: 32-bit ARM (armv7) WAS an engine port task — the round-19
81
81
  # ABI asserts pin layouts to 8-byte-pointer sizes and correctly
82
- # abort the build on 32-bit. Filed upstream as leptris/leptris
83
- # #1174; the release legs stay experimental (continue-on-error)
84
- # and flip green automatically when the port ships. We patch
85
- # nothing here the emulated-leg smoke validates whatever the
86
- # engine provides.
82
+ # aborted the build on 32-bit. Upstream shipped the ILP32 port
83
+ # (leptris/leptris#1174, closed; per-wordsize layout pins +
84
+ # inline-value gate landed in 1.9.199) and arm-linux /
85
+ # arm-linux-musl gems publish from every release since the
86
+ # emulated release legs are strict again. We patch nothing here;
87
+ # the emulated-leg smoke validates whatever the engine provides.
87
88
 
88
89
  # utf8proc: shared build only, @rpath install name, local prefix.
89
90
  u8_dir = File.expand_path("tmp/utf8proc-#{UTF8PROC_VERSION}", __dir__)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.208.0"
4
+ VERSION = "1.9.209.0"
5
5
  end
@@ -71,4 +71,18 @@ class Leptris::XML::Diff
71
71
  str_ptr = Leptris::XML::FFI.leptris_diff_serialize(@handle)
72
72
  Leptris::XML::FFI.read_owned_string(str_ptr)
73
73
  end
74
+
75
+ # Per-kind op counts (#1184): {update_attr: 1, insert: 1, ...} —
76
+ # {} when identical.
77
+ def summary
78
+ ops.each_with_object(Hash.new(0)) { |op, h| h[op[:type]] += 1 }
79
+ end
80
+
81
+ # The op list as JSON (#1184) — the same shape #ops yields, with
82
+ # stringified ops. Requires the json standard library lazily.
83
+ def to_json(*args)
84
+ require "json"
85
+ ops.map { |op| op.transform_values { |v| v.nil? ? nil : v.to_s } }
86
+ .to_json(*args)
87
+ end
74
88
  end
@@ -386,12 +386,6 @@ attach_function :leptris_parse_string,
386
386
  [:leptris_document, :int], :leptris_status
387
387
  attach_function :leptris_document_set_doctype,
388
388
  [:leptris_document, :string, :string, :string], :leptris_doctype
389
- # The #1229 remove-half (libleptris 1.9.204): un-set the
390
- # declaration; un-set the DOCTYPE (NOT_FOUND when none set).
391
- attach_function :leptris_document_clear_declaration,
392
- [:leptris_document], :leptris_status
393
- attach_function :leptris_document_remove_doctype,
394
- [:leptris_document], :leptris_status
395
389
  attach_function :leptris_document_append_pi,
396
390
  [:leptris_document, :string, :string], :leptris_node_ref
397
391
  attach_function :leptris_document_remove_child,
@@ -860,14 +854,6 @@ attach_function :leptris_parse_string,
860
854
  [:leptris_sax_parser], :void
861
855
  attach_function :leptris_sax_parser_set_streaming,
862
856
  [:leptris_sax_parser, :int], :int
863
- # One-shot input hint (libleptris 1.9.204): whole document in
864
- # a single feed(), alive for the parser's lifetime — enables
865
- # in-place scans. The binding's one-shot entry
866
- # (leptris_sax_parse) is one-shot by construction; the engine
867
- # sets this itself for pull's memory source. Attach-only
868
- # until a chunked-feed face wants to opt in.
869
- attach_function :leptris_sax_parser_set_one_shot,
870
- [:leptris_sax_parser, :int], :void
871
857
 
872
858
  # Pull parsing (v1.6.0): StAX-style cursor over a document.
873
859
  attach_function :leptris_pull_new,
@@ -1053,19 +1039,6 @@ attach_function :leptris_parse_string,
1053
1039
  :line, :int,
1054
1040
  :column, :int
1055
1041
  end
1056
- attach_function :leptris_dtd_parse,
1057
- [:string, :size_t], :pointer
1058
- # leptris_document_get_dtd attached above with the readers
1059
- attach_function :leptris_dtd_parse_external_subset,
1060
- [:pointer, :string, :size_t], :int
1061
- attach_function :leptris_dtd_set_pe_loader,
1062
- [:pointer, :pointer, :pointer], :void
1063
- attach_function :leptris_dtd_validate,
1064
- [:leptris_document, :pointer, :pointer], :int
1065
- attach_function :leptris_dtd_free,
1066
- [:pointer], :void
1067
- attach_function :leptris_dtd_error_free,
1068
- [:pointer], :void
1069
1042
 
1070
1043
  attach_function :leptris_rng_parse,
1071
1044
  [:string, :size_t, :pointer], :leptris_relaxng
@@ -1143,11 +1116,6 @@ attach_function :leptris_parse_string,
1143
1116
  [:leptris_doctype], :string
1144
1117
 
1145
1118
  attach_function :leptris_free_string, [:pointer], :void
1146
- # Engine-heap allocation (libleptris 1.9.204): buffers handed
1147
- # to engine callbacks (the DTD PE loader) so the engine frees
1148
- # through its matching deallocation path. Release with
1149
- # leptris_free_string when NOT consumed by the engine.
1150
- attach_function :leptris_alloc_buffer, [:size_t], :pointer
1151
1119
  attach_function :leptris_explicit_cleanup, [], :void
1152
1120
  attach_function :leptris_set_memory_management_functions,
1153
1121
  [:pointer, :pointer], :void
@@ -1,5 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.209] - 2026-09-20
4
+
5
+ ### Performance
6
+
7
+ - WHATWG HTML parse, #1218 round 2: the per-close-tag name-list
8
+ scans (a 40-name implied-end set and a 33-name open-side set,
9
+ ~7% of parse self-time on table-heavy pages) are now membership
10
+ bits on the first-char-bucketed tag classifier — both lookups
11
+ are one bucket walk, sets preserved bit-for-bit.
12
+ bench_html_parse plain fixture: 24.1 → 25.1-26.7 MB/s.
13
+
14
+
15
+
3
16
  ## [1.9.208] - 2026-09-19
4
17
 
5
18
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.208
6
+ VERSION 1.9.209
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -9,7 +9,7 @@ RED bench first where perf claims are made.
9
9
  | 1 | SIMD/memchr HTML tokenizer scans | perf | memchr ×2 (#1176/#1190) + LUT (#1199, +30%/+51%) | DONE v1.9.199 |
10
10
  | 7 | leptris validate CLI + bindings | feature | engine half merged #1198 (--dtd + DTD engine fixes); bindings remain | half done |
11
11
  | 8 | native XML diff consumer surface | feature | CLI diff text/JSON/--summary + engine API shipped | DONE (#1184 closed) |
12
- | 4 | compact-node micro-opts (non-layout) | perf | audited: TODOs 172/195/Lane18-P3 already in; residual = node-creation cost (node-layout fork, owner call) | parked on fork |
12
+ | 4 | compact-node micro-opts (non-layout) | perf | residual = node-creation cost; SUPERSEDED by the node-layout fork (TODO.max-perf/node-layout-fork.md, owner green-lit 2026-09-20) | folded into fork |
13
13
  | 2 | compiled-dispatch tokenizer | perf | slices 1+3 landed #1236; slice 2's class LUT + dense switch pre-existed; residual = iterparse consumer (row 3) | mostly done |
14
14
  | 5 | XSLT 2x per-bench profile rounds (#682) | perf | medium, ongoing | in progress |
15
15
  | 6 | language tails (QT3/dates/fn-items/lane 15) | correctness | medium-large | queued |
@@ -20,3 +20,7 @@ Portability wave 2026-09-18 (v1.9.199): ILP32 layout port #1193 —
20
20
  per-wordsize pins, SSE2 gate, xsl:number 64-bit chain, ILP32 +
21
21
  native-arm64 + Intel-macOS CI legs, concurrency groups everywhere.
22
22
  Next portability gate: s390x big-endian (#1194, PR #1202).
23
+
24
+ The node-layout fork (row 4 successor, owner green-lit 2026-09-20):
25
+ TODO.max-perf/node-layout-fork.md — 5 slices, addresses #1222 +
26
+ #1179 residual + #1238 parse side.
@@ -0,0 +1,70 @@
1
+ # The node-layout fork — pugixml-class allocation for the DOM
2
+
3
+ Status: DESIGN (2026-09-20). Owner green-light received. This is
4
+ the durable fix for #1222 (2-5.9x vs pugixml large docs), #1179's
5
+ residual node-creation cost, and the parse-side constant factor of
6
+ #1238. Measured basis: scaling is LINEAR on serialbench fixtures
7
+ (352-467 MB/s across 1.5-13MB) — the gap is a per-node constant
8
+ cost, not complexity.
9
+
10
+ ## Why the gap exists (measured)
11
+
12
+ Every element today costs: pool_alloc (block chain + freelist
13
+ recycler bookkeeping) + compact-edge encoding (cp16 + overflow
14
+ table on out-of-range pairs) + root-doc registration for detached
15
+ nodes + name materialization through the namebp/inline paths.
16
+ pugixml pays: one bump-pointer increment inside ONE document-sized
17
+ block, with node "pointers" as intra-block offsets and names
18
+ interned into a shared char arena. Constant-factor difference per
19
+ node, multiplied by millions of nodes.
20
+
21
+ ## Target layout (pugixml-class, adapted)
22
+
23
+ - ONE primary allocation per document (grown geometrically,
24
+ never per-node): node array with intra-block integer offsets for
25
+ parent/first-child/next-sibling/first-attr edges. No cp16
26
+ overflow table (offsets are block-relative and always
27
+ representable), no root-doc map for parse-created nodes (block
28
+ base implies the document).
29
+ - String arena beside the node array: element/attr names
30
+ interned by (ptr,len) hash into the block; parse buffers stay
31
+ immutable (#1125) and zero-copy views remain the primary
32
+ representation — interning only replaces the per-node copies.
33
+ - MUTATION-CREATED nodes keep today's paths initially (mut blocks
34
+ + root-doc map) behind the same public API; the internal
35
+ get_document walk resolves by block base first. Migration of
36
+ mutation paths is a later slice, gated by the same benches.
37
+ - ABI unchanged: every public handle is already opaque; the
38
+ compact-pointer exports and element-size check (VALIDATION.md)
39
+ get new expected values.
40
+
41
+ ## Slices (one PR each, RED bench first)
42
+
43
+ 1. **Document block allocator**: the growable block + offset-edge
44
+ node struct, PARALLEL to the pool (parse path only, behind a
45
+ flag). Gate: no test changes; element-size check documents both.
46
+ 2. **Parser wiring**: flat/direct_parse + html_parse build into
47
+ block storage. Gate: full ctest + html5lib corpus 1169 floor;
48
+ RED bench: serialbench shapes to >=350 MB/s at 13MB (close the
49
+ libxml2 gap, halve the pugixml gap).
50
+ 3. **Name interning**: shared arena replaces per-node name copies.
51
+ Gate: attr/name read benches (many-attrs, first-access) no
52
+ regression; memory RSS down on name-heavy docs.
53
+ 4. **Compact-edge retirement for parse trees**: drop overflow-table
54
+ traffic on the block path; mutation keeps the old encode until
55
+ slice 5.
56
+ 5. **Mutation unification** (optional, last): programmatic
57
+ create/append onto the block. Gate: append <= pugixml (the
58
+ lane-18 P1 row).
59
+
60
+ ## Non-goals / invariants
61
+
62
+ - No public API or ABI change (opaque handles).
63
+ - Digest (#869), diff, c14n correctness rides the same node
64
+ accessors — all read through the inline edge helpers, which
65
+ become offset arithmetic.
66
+ - The address-keyed TLS caches (root-doc map/memo) survive for
67
+ MUTATION nodes only; their invalidation contracts (#1242/#1038)
68
+ must be re-gated, not assumed.
69
+
70
+ Effort: medium-large. Every slice independently shippable.
@@ -2189,40 +2189,36 @@ static int h_closes_ww(const char* open, const char* start) {
2189
2189
  /* WHATWG: starts that close an open p in button scope
2190
2190
  * (13.2.6.4.7 "in body" block set + h/pre/form/li/dd/dt/plaintext/
2191
2191
  * hr/xmp). */
2192
+ typedef struct {
2193
+ char name[12];
2194
+ uint8_t len;
2195
+ uint8_t special; /* WHATWG 13.2.4 special set */
2196
+ uint8_t no_fmt; /* never reconstructed (formatting) */
2197
+ uint8_t closes; /* bit0: h_p_closes implied-end set; bit1:
2198
+ * k_p_closers — replaced the per-close-tag
2199
+ * strcmp scans (#1218 profile). */
2200
+ } HTagInfo;
2201
+
2202
+ static const HTagInfo* h_tag_lookup(const char* n);
2203
+
2204
+ static const HTagInfo* h_tag_lookup(const char* n);
2205
+
2192
2206
  static int h_p_closes(const char* start) {
2193
- static const char* const k[] = {
2194
- "address", "article", "aside", "blockquote", "center",
2195
- "details", "dialog", "dir", "div", "dl",
2196
- "fieldset", "figcaption", "figure", "footer", "header",
2197
- "hgroup", "main", "menu", "nav", "ol",
2198
- "p", "search", "section", "summary", "ul",
2199
- "h1", "h2", "h3", "h4", "h5",
2200
- "h6", "pre", "listing", "form", "li",
2201
- "dd", "dt", "plaintext", "hr", "xmp",
2202
- NULL};
2203
- for (int i = 0; k[i]; i++)
2204
- if (strcmp(start, k[i]) == 0) return 1;
2205
- return 0;
2207
+ /* Classifier hit (round: #1218 profile — the 40-name strcmp
2208
+ * scan was the top WHATWG self-time on table-heavy pages).
2209
+ * Sets preserved bit-for-bit from the old k[] list. */
2210
+ const HTagInfo* t = h_tag_lookup(start);
2211
+ return t && (t->closes & 1);
2206
2212
  }
2207
2213
 
2208
2214
  /* Implied-end sets: a start tag in `closes` closes any open
2209
2215
  * element named `name` (HTML4 §7.5.4 / table model). */
2210
- static const char* const k_p_closers[] = {
2211
- "address", "article", "aside", "blockquote", "details", "dialog",
2212
- "dir", "div", "dl", "fieldset", "figcaption", "figure", "footer",
2213
- "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hgroup",
2214
- "hr", "li", "main", "menu", "nav", "ol", "p", "pre", "section",
2215
- "table", "ul", NULL,
2216
- };
2217
- static int h_in_list(const char* const* list, const char* name) {
2218
- for (int i = 0; list[i]; i++)
2219
- if (strcmp(list[i], name) == 0) return 1;
2220
- return 0;
2221
- }
2222
2216
  /* Does starting `start` close an open element named `open`? */
2223
2217
  static int h_closes(const char* open, const char* start) {
2224
- if (strcmp(open, "p") == 0 && h_in_list(k_p_closers, start))
2225
- return 1;
2218
+ if (strcmp(open, "p") == 0) {
2219
+ const HTagInfo* t = h_tag_lookup(start);
2220
+ if (t && (t->closes & 2)) return 1;
2221
+ }
2226
2222
  if (strcmp(open, "li") == 0 && strcmp(start, "li") == 0)
2227
2223
  return 1;
2228
2224
  if ((strcmp(open, "dt") == 0 || strcmp(open, "dd") == 0) &&
@@ -3066,98 +3062,92 @@ static int h_is_formatting(const char* n) {
3066
3062
  * sampler pinned at ~21% of WHATWG parse (h_is_special_ww +
3067
3063
  * h_reconstructs). Keep in sync with the WHATWG special and
3068
3064
  * no-formatting sets. */
3069
- typedef struct {
3070
- char name[12];
3071
- uint8_t len;
3072
- uint8_t special; /* WHATWG 13.2.4 special set */
3073
- uint8_t no_fmt; /* never reconstructed (formatting) */
3074
- } HTagInfo;
3075
3065
 
3076
3066
  static const HTagInfo h_tag_infos[] = {
3077
- {"address", 7, 1, 1},
3078
- {"applet", 6, 1, 0},
3079
- {"area", 4, 1, 0},
3080
- {"article", 7, 1, 1},
3081
- {"aside", 5, 1, 1},
3082
- {"base", 4, 1, 1},
3083
- {"basefont", 8, 1, 1},
3084
- {"bgsound", 7, 1, 1},
3085
- {"blockquote", 10, 1, 1},
3086
- {"body", 4, 1, 1},
3087
- {"br", 2, 1, 0},
3088
- {"button", 6, 1, 0},
3089
- {"caption", 7, 1, 1},
3090
- {"center", 6, 1, 1},
3091
- {"col", 3, 1, 1},
3092
- {"colgroup", 8, 1, 1},
3093
- {"dd", 2, 1, 1},
3094
- {"details", 7, 1, 1},
3095
- {"dialog", 6, 1, 1},
3096
- {"dir", 3, 1, 1},
3097
- {"div", 3, 1, 1},
3098
- {"dl", 2, 1, 1},
3099
- {"dt", 2, 1, 1},
3100
- {"embed", 5, 1, 0},
3101
- {"fieldset", 8, 1, 1},
3102
- {"figcaption", 10, 1, 1},
3103
- {"figure", 6, 1, 1},
3104
- {"footer", 6, 1, 1},
3105
- {"form", 4, 1, 1},
3106
- {"frame", 5, 1, 1},
3107
- {"frameset", 8, 1, 1},
3108
- {"h1", 2, 1, 1},
3109
- {"h2", 2, 1, 1},
3110
- {"h3", 2, 1, 1},
3111
- {"h4", 2, 1, 1},
3112
- {"h5", 2, 1, 1},
3113
- {"h6", 2, 1, 1},
3114
- {"head", 4, 1, 1},
3115
- {"header", 6, 1, 1},
3116
- {"hgroup", 6, 1, 1},
3117
- {"hr", 2, 1, 1},
3118
- {"html", 4, 1, 1},
3119
- {"iframe", 6, 1, 0},
3120
- {"img", 3, 1, 0},
3121
- {"input", 5, 1, 0},
3122
- {"keygen", 6, 1, 0},
3123
- {"li", 2, 1, 1},
3124
- {"link", 4, 1, 1},
3125
- {"listing", 7, 1, 1},
3126
- {"main", 4, 1, 1},
3127
- {"marquee", 7, 1, 0},
3128
- {"menu", 4, 1, 1},
3129
- {"meta", 4, 1, 1},
3130
- {"nav", 3, 1, 1},
3131
- {"noembed", 7, 1, 0},
3132
- {"noframes", 8, 1, 1},
3133
- {"noscript", 8, 1, 0},
3134
- {"object", 6, 1, 0},
3135
- {"ol", 2, 1, 1},
3136
- {"p", 1, 1, 1},
3137
- {"param", 5, 1, 0},
3138
- {"plaintext", 9, 1, 1},
3139
- {"pre", 3, 1, 1},
3140
- {"script", 6, 1, 1},
3141
- {"search", 6, 1, 1},
3142
- {"section", 7, 1, 1},
3143
- {"select", 6, 1, 0},
3144
- {"source", 6, 1, 0},
3145
- {"style", 5, 1, 1},
3146
- {"summary", 7, 1, 1},
3147
- {"table", 5, 1, 1},
3148
- {"tbody", 5, 1, 1},
3149
- {"td", 2, 1, 1},
3150
- {"template", 8, 1, 1},
3151
- {"textarea", 8, 1, 1},
3152
- {"tfoot", 5, 1, 1},
3153
- {"th", 2, 1, 1},
3154
- {"thead", 5, 1, 1},
3155
- {"title", 5, 1, 1},
3156
- {"tr", 2, 1, 1},
3157
- {"track", 5, 1, 0},
3158
- {"ul", 2, 1, 1},
3159
- {"wbr", 3, 1, 0},
3160
- {"xmp", 3, 1, 0},
3067
+ {"address", 7, 1, 1, 3},
3068
+ {"applet", 6, 1, 0, 0},
3069
+ {"area", 4, 1, 0, 0},
3070
+ {"article", 7, 1, 1, 3},
3071
+ {"aside", 5, 1, 1, 3},
3072
+ {"base", 4, 1, 1, 0},
3073
+ {"basefont", 8, 1, 1, 0},
3074
+ {"bgsound", 7, 1, 1, 0},
3075
+ {"blockquote", 10, 1, 1, 3},
3076
+ {"body", 4, 1, 1, 0},
3077
+ {"br", 2, 1, 0, 0},
3078
+ {"button", 6, 1, 0, 0},
3079
+ {"caption", 7, 1, 1, 0},
3080
+ {"center", 6, 1, 1, 1},
3081
+ {"col", 3, 1, 1, 0},
3082
+ {"colgroup", 8, 1, 1, 0},
3083
+ {"dd", 2, 1, 1, 1},
3084
+ {"details", 7, 1, 1, 3},
3085
+ {"dialog", 6, 1, 1, 3},
3086
+ {"dir", 3, 1, 1, 3},
3087
+ {"div", 3, 1, 1, 3},
3088
+ {"dl", 2, 1, 1, 3},
3089
+ {"dt", 2, 1, 1, 1},
3090
+ {"embed", 5, 1, 0, 0},
3091
+ {"fieldset", 8, 1, 1, 3},
3092
+ {"figcaption", 10, 1, 1, 3},
3093
+ {"figure", 6, 1, 1, 3},
3094
+ {"footer", 6, 1, 1, 3},
3095
+ {"form", 4, 1, 1, 3},
3096
+ {"frame", 5, 1, 1, 0},
3097
+ {"frameset", 8, 1, 1, 0},
3098
+ {"h1", 2, 1, 1, 3},
3099
+ {"h2", 2, 1, 1, 3},
3100
+ {"h3", 2, 1, 1, 3},
3101
+ {"h4", 2, 1, 1, 3},
3102
+ {"h5", 2, 1, 1, 3},
3103
+ {"h6", 2, 1, 1, 3},
3104
+ {"head", 4, 1, 1, 0},
3105
+ {"header", 6, 1, 1, 3},
3106
+ {"hgroup", 6, 1, 1, 3},
3107
+ {"hr", 2, 1, 1, 3},
3108
+ {"html", 4, 1, 1, 0},
3109
+ {"iframe", 6, 1, 0, 0},
3110
+ {"img", 3, 1, 0, 0},
3111
+ {"input", 5, 1, 0, 0},
3112
+ {"keygen", 6, 1, 0, 0},
3113
+ {"li", 2, 1, 1, 3},
3114
+ {"link", 4, 1, 1, 0},
3115
+ {"listing", 7, 1, 1, 1},
3116
+ {"main", 4, 1, 1, 3},
3117
+ {"marquee", 7, 1, 0, 0},
3118
+ {"menu", 4, 1, 1, 3},
3119
+ {"meta", 4, 1, 1, 0},
3120
+ {"nav", 3, 1, 1, 3},
3121
+ {"noembed", 7, 1, 0, 0},
3122
+ {"noframes", 8, 1, 1, 0},
3123
+ {"noscript", 8, 1, 0, 0},
3124
+ {"object", 6, 1, 0, 0},
3125
+ {"ol", 2, 1, 1, 3},
3126
+ {"p", 1, 1, 1, 3},
3127
+ {"param", 5, 1, 0, 0},
3128
+ {"plaintext", 9, 1, 1, 1},
3129
+ {"pre", 3, 1, 1, 3},
3130
+ {"script", 6, 1, 1, 0},
3131
+ {"search", 6, 1, 1, 1},
3132
+ {"section", 7, 1, 1, 3},
3133
+ {"select", 6, 1, 0, 0},
3134
+ {"source", 6, 1, 0, 0},
3135
+ {"style", 5, 1, 1, 0},
3136
+ {"summary", 7, 1, 1, 1},
3137
+ {"table", 5, 1, 1, 2},
3138
+ {"tbody", 5, 1, 1, 0},
3139
+ {"td", 2, 1, 1, 0},
3140
+ {"template", 8, 1, 1, 0},
3141
+ {"textarea", 8, 1, 1, 0},
3142
+ {"tfoot", 5, 1, 1, 0},
3143
+ {"th", 2, 1, 1, 0},
3144
+ {"thead", 5, 1, 1, 0},
3145
+ {"title", 5, 1, 1, 0},
3146
+ {"tr", 2, 1, 1, 0},
3147
+ {"track", 5, 1, 0, 0},
3148
+ {"ul", 2, 1, 1, 3},
3149
+ {"wbr", 3, 1, 0, 0},
3150
+ {"xmp", 3, 1, 0, 1},
3161
3151
  };
3162
3152
 
3163
3153
  /* bucket[first_char] = start index; bucket[first_char+1] = end */
@@ -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.208",
4
+ "version": "1.9.209",
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.208.0
4
+ version: 1.9.209.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-19 00:00:00.000000000 Z
11
+ date: 2026-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi
@@ -154,6 +154,8 @@ files:
154
154
  - lib/leptris/vendor/arm64-darwin/libutf8proc.3.dylib
155
155
  - lib/leptris/vendor/ppc64le-linux/libleptris.so
156
156
  - lib/leptris/vendor/ppc64le-linux/libutf8proc.so.3
157
+ - lib/leptris/vendor/s390x-linux-musl/libleptris.so
158
+ - lib/leptris/vendor/s390x-linux-musl/libutf8proc.so.3
157
159
  - lib/leptris/vendor/x86_64-darwin/libleptris.dylib
158
160
  - lib/leptris/vendor/x86_64-darwin/libutf8proc.3.dylib
159
161
  - lib/leptris/vendor/x86_64-linux-musl/libleptris.so
@@ -246,6 +248,7 @@ files:
246
248
  - vendor-src/libleptris/TODO.max-perf/7-validate-bindings.md
247
249
  - vendor-src/libleptris/TODO.max-perf/8-native-xml-diff-consumer.md
248
250
  - vendor-src/libleptris/TODO.max-perf/9-prebuilt-gem-matrix.md
251
+ - vendor-src/libleptris/TODO.max-perf/node-layout-fork.md
249
252
  - vendor-src/libleptris/TODO.md
250
253
  - vendor-src/libleptris/TODO.transform/README.md
251
254
  - vendor-src/libleptris/TODO.validate/README.md