leptris 1.9.209.1-arm-linux → 1.9.210.1-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: 9ce1d65e6aeb91514ba27063f13dd3d31113fcdf298fac6bf8010536032c5836
4
- data.tar.gz: dc150276080b58313d34a05e78e3457d5ae675585b824fdd187608b4e786ca25
3
+ metadata.gz: d1ea51f9fab577cb642f3d97041e358f9cd7bb24683f00efeda95d030d23f271
4
+ data.tar.gz: e5daf20fd58e184304892ae0524609a1f75421fcbd1b281c7b8b1b14b1434d14
5
5
  SHA512:
6
- metadata.gz: 20baf0ac1501f9c6b057d95993329c33b1fa126dd7024e6291d453f6cd1efd66b363886693f65d98b291e0df5a41214e20cadc08e20ec684171f3fd471003ea6
7
- data.tar.gz: fee9695415f16f8666748e3e108fcd9c2204aab15dcbdffd4521813b459453bea0f6247cf9295161d84dc95a01ff2f416c9a387ade147b5064a43c02d17042aa
6
+ metadata.gz: 748f76cccbf61b98d4c8c59f9a9c334496945e148a54421cdcbacbc0fd0e065a6d8174ffb535bd2f698aea595ce401dbef3dff5ba1ad183689665d6ffa101230
7
+ data.tar.gz: a95bccff70898f0ebe1879d9d3d6a25216e4e64eeec499671cf5fb87ba8a7e00852bbd926ed7e01b26eab0c268f19e43a46f01569f9124ef4ebfdb660f6753b0
data/CHANGELOG.md CHANGED
@@ -5,6 +5,50 @@ 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
+ ## [Unreleased]
9
+
10
+ ### Added — Descriptor typed scalars + the fused materialize entry (#230's consumer contract)
11
+
12
+ - **Typed scalars**: attribute and child plan rows accept
13
+ `type: :string | :integer | :float | :boolean`. The tag travels
14
+ through the plan ABI (host-defined, echoed verbatim) and
15
+ `PlanValue#to_ruby` returns the cast value — Integer / Float /
16
+ true / false — on element scalars AND collected attributes, so
17
+ per-value cast probes disappear from the consumer's loop
18
+ (lutaml-model's `cast_element_present`/`apply_value_map` class
19
+ of cost). Lenient on unparseable input (raw String wins);
20
+ `#string_value` stays the raw escape hatch regardless of tag.
21
+ - **`Descriptor#materialize(source)`** — the fused loop, pointed
22
+ at the parse: source bytes → typed rows in ONE call (parse,
23
+ plan walk, document free; the standalone PlanValue tree is all
24
+ that surfaces). Byte-parity with parse + `#walk(root)`.
25
+ - Mirrors the yeptris schema-ABI shape (leptris/yeptris#238:
26
+ SCALAR + type_tag STR|INT|FLOAT|BOOL, whole-document results)
27
+ so a framework compiles one descriptor vocabulary and runs it
28
+ on every engine. Native in-pass int/float parsing (no string
29
+ round-trip) is the recorded engine-side headroom.
30
+
31
+ ## [1.9.210.0] - 2026-09-20
32
+
33
+ ### Performance — engine sync (1.9.210)
34
+
35
+ - Vendored libleptris 1.9.210: the node-layout fork's document
36
+ block (slice 1, #1253) — two contiguous geometrically-grown
37
+ regions per document (dense node array + string arena) with
38
+ region-relative byte-offset edges, so growth never invalidates
39
+ an offset; and the parse-arena retain ceiling raised 32 → 96MB
40
+ with a thread-cleanup drain (#1222/#1238 lane): repeated large
41
+ parses stop freeing/re-mallocing tens of MB per cycle. Measured
42
+ engine-side: 13MB attr shape 341 → 472 MB/s (+38%); the text
43
+ shape's size-decline eliminated (~605-627 flat across
44
+ 1.5-13MB). No binding-facing surface changes; audit holds.
45
+ - Note: #1242 was closed upstream before our strikes 7-10
46
+ forensics landed; reopened with the post-fix evidence (the
47
+ 1.9.208 memo clear is incomplete — direct-call NULL + empty
48
+ chain on a live element; set_root cross-document false
49
+ positive on windows-4.0). The ubuntu-3.4 gate and armed
50
+ forensics stay until the engine ships the residual fix.
51
+
8
52
  ## [1.9.209.0] - 2026-09-20
9
53
 
10
54
  ### Performance — engine sync (1.9.209)
data/README.adoc CHANGED
@@ -67,8 +67,11 @@ vendors precompiled `libleptris` + `libutf8proc` binaries for the
67
67
  common engine platforms under `lib/leptris/vendor/`:
68
68
 
69
69
  - `arm64-darwin`, `x86_64-darwin`
70
- - `x86_64-linux`, `aarch64-linux` (glibc; the musl variants are
71
- tried as fallback for Alpine hosts)
70
+ - `x86_64-linux`, `aarch64-linux` (glibc) and `x86_64-linux-musl`,
71
+ `aarch64-linux-musl` (tried as fallback for Alpine hosts)
72
+ - `arm-linux`, `arm-linux-musl` (32-bit ARM EABI5)
73
+ - `ppc64le-linux`, `s390x-linux`, `s390x-linux-musl` (POWER8+ LE
74
+ and IBM Z, qemu-built)
72
75
 
73
76
  `gem install leptris` just works on both engines — the FFI layer
74
77
  selects the matching vendored binary at require time (the binding
@@ -127,7 +130,10 @@ Leptris::XML::FFI.leptris_last_error # => "..."
127
130
  `Leptris::XML.parse_html(html)` parses tolerant HTML4/5 into a
128
131
  standard Document — the same nodes, pool, serializer, and
129
132
  XPath/XSLT/XQuery machinery as XML (libleptris 1.9.75, the last
130
- Nokogiri capability gap):
133
+ Nokogiri capability gap). The `Leptris::HTML`, `Leptris::HTML4`,
134
+ and `Leptris::HTML5` module facades provide the
135
+ Nokogiri-shaped entry points over the same engine
136
+ (`Leptris::HTML5.parse(html)`, `Leptris::HTML(html)`):
131
137
 
132
138
  [source,ruby]
133
139
  ----
@@ -345,18 +351,45 @@ puts doc.to_xml
345
351
  `Element#replace(node)` / `#swap(node)` :: replace in parent.
346
352
  `Element#wrap(node_or_markup)` :: wrap this element in a new one.
347
353
  `Node#unlink` :: detach from the tree.
354
+ `Element#attribute_pairs` :: bulk read-only attribute listing — `[[name, value], ...]` in document order, duplicates included; one C crossing on the native layer (libleptris 1.9.210 era).
355
+
356
+ Document diffing (`Leptris::XML.diff(a, b)`) yields an op list with
357
+ `#ops`, per-kind counts via `#summary`
358
+ (`{update_attr: 1, insert: 1}`, `{}` when identical), and `#to_json`
359
+ for the op list — the `leptris diff --summary/--json` CLI modes'
360
+ library face.
348
361
 
349
362
  === Building from scratch (no parse)
350
363
 
364
+ `Document.create` starts an empty document; build up from there
365
+ (document-level prolog parts included — PIs, comments, the XML
366
+ declaration, and the DOCTYPE are all first-class):
367
+
351
368
  [source,ruby]
352
369
  ----
353
- # Create an empty Document by parsing a sentinel and replacing the root,
354
- # or build incrementally on a one-element seed.
355
- doc = Leptris::XML.parse("<root/>")
356
- doc.root.name = "catalog"
357
- # ... then create_element / add_child as above.
370
+ doc = Leptris::XML::Document.create
371
+ book = doc.create_element("book")
372
+ book[:id] = "b3"
373
+ book << doc.create_element("title")
374
+ doc.root = doc.create_element("catalog")
375
+ doc.root << book
376
+ doc.add_comment("built programmatically") # epilog comment
377
+ doc.set_doctype("catalog", system_id: "catalog.dtd")
378
+
379
+ puts doc.to_xml
358
380
  ----
359
381
 
382
+ [horizontal]
383
+ `Document.create` :: empty document (no root yet).
384
+ `Document#root=` :: attach the root element (bottom-up construction).
385
+ `Document#set_doctype(name, public_id:, system_id:)` :: programmatic DOCTYPE.
386
+ `Document#doctype` (alias `#internal_subset`) :: the `DocType` reader (name/root_name/public_id/system_id/internal_subset).
387
+ `Document#clear_declaration` :: un-set the XML declaration (serialize as if the input had none; idempotent).
388
+ `Document#remove_doctype` :: un-set the DOCTYPE (true when removed; the DocType stays readable until `#free`).
389
+ `Document#add_pi(target, data)` / `#remove_pi(target_or_index)` :: document-level processing instructions.
390
+ `Document#add_comment(str)` :: epilog comment.
391
+ `Document#create_entity_reference(name)` :: `&name;` node, serializes verbatim once attached.
392
+
360
393
  == Namespaces
361
394
 
362
395
  [horizontal]
@@ -711,11 +744,16 @@ Notable differences:
711
744
  past Document disposal will see the error; silence-replace-UAF patterns
712
745
  from Nokogiri do not apply.
713
746
  * No `Nokogiri::CSS` parser. HTML parsing is supported
714
- (`Leptris::XML.parse_html`, libleptris 1.9.75); Nokogiri's
747
+ (`Leptris::HTML` / `Leptris::HTML4` / `Leptris::HTML5` facades
748
+ over `Leptris::XML.parse_html`, libleptris 1.9.75); Nokogiri's
715
749
  HTML-specific node subclasses have no equivalent.
716
- * No RelaxNG / DTD validation, no schema caching (XSLT 1.0–3.0
750
+ * RelaxNG and DTD validation ARE supported (`Leptris::XML::RelaxNG`,
751
+ `Leptris::XML::DTD` — parse once, validate many, structured
752
+ errors). No W3C XML Schema and no schema caching (XSLT 1.0–3.0
717
753
  and an XQuery 1.0 core are supported — see above).
718
- * No built-in JRuby / TruffleRuby support — only CRuby via `ffi`.
754
+ * TruffleRuby and JRuby ARE supported out of the box (#160): the
755
+ `ruby`-platform variant vendors precompiled binaries and the
756
+ binding is FFI-based (no MRI C extension required to load).
719
757
 
720
758
  == Performance
721
759
 
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.209"
11
+ LIBLEPTRIS_VERSION = "1.9.210"
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
@@ -117,6 +117,35 @@ task :compile do
117
117
  rm_rf([File.join(dest, "build"), File.join(dest, "prefix")])
118
118
  end
119
119
 
120
+ # Rebuild notes ride every gem beside the sources (packaging
121
+ # doctrine): the exact pins and the commands to reproduce the
122
+ # vendored libraries. The ruby variant's extconf runs these
123
+ # trees automatically at install time.
124
+ File.write(File.join(vsrc, "README.md"), <<~README)
125
+ Sources vendored in this gem (recompile rights; the ruby
126
+ variant builds them at install via extconf.rb):
127
+
128
+ - libleptris v#{LIBLEPTRIS_VERSION}
129
+ - utf8proc v#{UTF8PROC_VERSION}
130
+
131
+ Rebuild by hand:
132
+
133
+ cmake -B vendor-src/utf8proc/build -S vendor-src/utf8proc \
134
+ -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \
135
+ -DUTF8PROC_ENABLE_TESTING=OFF
136
+ cmake --build vendor-src/utf8proc/build --config Release
137
+ cmake --install vendor-src/utf8proc/build --prefix <prefix>
138
+
139
+ cmake -B vendor-src/libleptris/build -S vendor-src/libleptris \
140
+ -DCMAKE_BUILD_TYPE=Release \
141
+ -DLEPTRIS_BUILD_SHARED=ON -DLEPTRIS_BUILD_STATIC=OFF \
142
+ -DBUILD_TESTING=OFF -DLEPTRIS_BUILD_CLI=OFF \
143
+ -DLEPTRIS_BUILD_BENCHMARKS=OFF -DLEPTRIS_BUILD_MAN_PAGES=OFF \
144
+ -DLEPTRIS_ENABLE_UTF8PROC=ON \
145
+ -DCMAKE_PREFIX_PATH=<utf8proc-prefix>
146
+ cmake --build vendor-src/libleptris/build --config Release
147
+ README
148
+
120
149
  # libleptris 1.9.18's xslt_functions.c:270 assigns LeptrisElement
121
150
  # to LeptrisNodeRef — GCC 14 (Alpine/musl) makes incompatible
122
151
  # pointer types an error by default and the musl platform gems
@@ -357,6 +386,46 @@ namespace :audit do
357
386
  end
358
387
  end
359
388
 
389
+ # Packaging doctrine (#271): every BUILT gem carries the engine
390
+ # source (vendor-src/libleptris build inputs) and the ext sources;
391
+ # platform gems additionally carry the prebuilt libraries and must
392
+ # NOT carry extensions; the pure ruby gem compiles at install
393
+ # (extensions present). Run over pkg/*.gem before publishing —
394
+ # the gate is what keeps the doctrine from regressing silently.
395
+ desc "Audit built gems for the packaging doctrine (source + binaries + extension policy)"
396
+ task :doctrine do
397
+ gems = Dir.glob("pkg/*.gem")
398
+ abort "audit:doctrine: no gems in pkg/ — build first (rake gem:native:...)" if gems.empty?
399
+
400
+ gems.each do |path|
401
+ spec = Gem::Package.new(path).spec
402
+ files = spec.files.map { |f| f.gsub("\\", "/") }
403
+
404
+ missing = []
405
+ missing << "vendor-src/libleptris/CMakeLists.txt" unless
406
+ files.include?("vendor-src/libleptris/CMakeLists.txt")
407
+ missing << "vendor-src/README.md" unless files.include?("vendor-src/README.md")
408
+ missing << "ext/leptris/native/extconf.rb" unless
409
+ files.include?("ext/leptris/native/extconf.rb")
410
+
411
+ if spec.platform.to_s == "ruby"
412
+ missing << "extensions (compile-at-install)" if spec.extensions.empty?
413
+ else
414
+ binary = files.any? { |f| f =~ %r{\Alib/libleptris\.(so|dylib|dll)\z} } ||
415
+ files.any? { |f| f.start_with?("lib/leptris/vendor/") }
416
+ missing << "prebuilt engine binary" unless binary
417
+ missing << "extensions (platform gems never rebuild)" unless spec.extensions.empty?
418
+ end
419
+
420
+ if missing.empty?
421
+ puts "audit:doctrine: #{File.basename(path)} OK " \
422
+ "(#{spec.platform}, #{files.length} files)"
423
+ else
424
+ abort "audit:doctrine: #{File.basename(path)} MISSING: #{missing.join(', ')}"
425
+ end
426
+ end
427
+ end
428
+
360
429
  # @api private
361
430
  def nm_available?
362
431
  system("nm", "--version", out: File::NULL, err: File::NULL)
@@ -10,3 +10,18 @@ Status: DONE
10
10
  parse 10–12×, serialize 2.9–3.2×, inner_html 4–5×, attr reads
11
11
  6–9×, text 9–11×, element_children walks at parity. Will
12
12
  re-record on the next clean window (load < 15).
13
+
14
+ ## Re-recorded 2026-09-21 — clean window (load ~12, 1.9.210.0, 84k-element/1.3MB catalog, interleaved best-of-5, nokogiri 1.19.4)
15
+
16
+ - [x] Standing harness (MEDIUM fixture): parse medium **11.1x** /
17
+ small 1.88x; xpath 2.4-20x (predicate 15-20x, complex
18
+ 11-14x); traverse 1.5-1.7x; serialize 2.25-3.13x
19
+ - [x] Warm `children` walks **2.06-2.08x** (was 1.86x at 1.9.152)
20
+ - [x] Cold parse+first-touch walks **0.58-0.74x (avg ~0.66x)** —
21
+ the loss nearly HALVED from 0.34x; the engine's
22
+ document-block/arena work (1.9.210) shows here
23
+ - [x] Attr cold row (fresh wrappers) 1.18x
24
+ - [ ] **Open front: cold TEXT row 0.66x** — fresh text-node
25
+ materialization (`element_children.each(&:content)` cold)
26
+ loses where warm text aggregation was 6.18x. Next perf
27
+ round's target.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.209.1"
4
+ VERSION = "1.9.210.1"
5
5
  end
@@ -28,7 +28,33 @@ require "ffi"
28
28
  # Namespace binding per plan: +ns:+ is :none (default), :any, or
29
29
  # { exact: "urn:..." }; +flags: [:mixed_content, :ordered, :cdata,
30
30
  # :ns_lenient].
31
+ #
32
+ # == Typed scalars (#230's fused-consumer contract, mirroring the
33
+ # yeptris schema ABI)
34
+ #
35
+ # Attribute and child rows accept +type: :string (default),
36
+ # :integer, :float, or :boolean+. The tag travels through the plan
37
+ # ABI (host-defined, echoed verbatim) and PlanValue#to_ruby returns
38
+ # the cast value — Integer / Float / true / false — so per-value
39
+ # cast probes disappear from the consumer. Unparseable values fall
40
+ # back to the raw String (lenient, first-wins house style);
41
+ # #string_value always returns the raw String regardless of tag.
42
+ #
43
+ # descriptor = Leptris::XML::Descriptor.build(
44
+ # name: "catalog",
45
+ # children: [
46
+ # { name: "price", kind: :scalar, type: :float },
47
+ # { name: "in_stock", kind: :scalar, type: :boolean },
48
+ # ])
49
+ # descriptor.materialize(xml_source).to_ruby # typed, one call
31
50
  class Leptris::XML::Descriptor
51
+ TYPE_TAGS = {
52
+ string: 0,
53
+ integer: 1,
54
+ float: 2,
55
+ boolean: 3,
56
+ }.freeze
57
+ private_constant :TYPE_TAGS
32
58
  class Handle < ::FFI::AutoPointer
33
59
  def self.release(ptr)
34
60
  Leptris::XML::FFI.leptris_plan_free(ptr)
@@ -136,7 +162,7 @@ class Leptris::XML::Descriptor
136
162
  ap = Leptris::XML::FFI::AttrPlan.new(attr_memory[j])
137
163
  ap[:wire_name] = anchor_string(anchors, row.fetch(:name))
138
164
  ap[:kind] = kind_code(row[:kind] || :scalar)
139
- ap[:type_tag] = row[:type_tag] || 0
165
+ ap[:type_tag] = type_tag_code(row)
140
166
  end
141
167
  ep[:attribute_count] = attrs.size
142
168
  ep[:attribute_plans] = attr_memory
@@ -148,7 +174,7 @@ class Leptris::XML::Descriptor
148
174
  cp = Leptris::XML::FFI::ChildPlan.new(child_memory[j])
149
175
  cp[:wire_name] = anchor_string(anchors, row.fetch(:name))
150
176
  cp[:kind] = kind_code(row[:kind] || :scalar)
151
- cp[:type_tag] = row[:type_tag] || 0
177
+ cp[:type_tag] = type_tag_code(row)
152
178
  cp[:child_plan_index] = row[:child_plan_index] || -1
153
179
  # Rule-level ns form (libleptris 1.9.178, #1115): siblings
154
180
  # under one parent can require different URIs when set;
@@ -189,6 +215,20 @@ class Leptris::XML::Descriptor
189
215
  end
190
216
  private_class_method :kind_code
191
217
 
218
+ # +type: :string/:integer/:float/:boolean+ (or a raw numeric
219
+ # type_tag passthrough for hosts with their own vocabulary).
220
+ # Public: PlanValue's attribute casts share the vocabulary.
221
+ def self.type_tag_code(row)
222
+ if row.key?(:type)
223
+ TYPE_TAGS.fetch(row[:type]) do
224
+ raise ArgumentError,
225
+ "type must be one of #{TYPE_TAGS.keys.inspect}, got #{row[:type].inspect}"
226
+ end
227
+ else
228
+ row[:type_tag] || 0
229
+ end
230
+ end
231
+
192
232
  def initialize(handle, plans, root_index)
193
233
  @handle = handle
194
234
  @plans = plans
@@ -211,4 +251,21 @@ class Leptris::XML::Descriptor
211
251
  Leptris::XML::PlanValue.new(ResultHandle.new(raw),
212
252
  owner: true, plans: @plans, plan: @plans[0])
213
253
  end
254
+
255
+ # The fused loop (#230, pointed at the parse): source bytes →
256
+ # typed rows in ONE call — parse, walk the compiled plan, free
257
+ # the document; the returned PlanValue tree is standalone and
258
+ # the Document never surfaces. Byte-for-byte parity with
259
+ # #walk on the same source's root element.
260
+ def materialize(source)
261
+ document = Leptris::XML::Document.parse(source)
262
+ begin
263
+ root = document.root
264
+ raise Leptris::XML::Error,
265
+ "materialize: document has no root element" if root.nil?
266
+ walk(root)
267
+ ensure
268
+ document.free
269
+ end
270
+ end
214
271
  end
@@ -94,12 +94,40 @@ class Leptris::XML::PlanValue
94
94
  when :callback
95
95
  { value: string_value, position: position, type_tag: type_tag }
96
96
  else
97
- string_value
97
+ typed_string_value
98
98
  end
99
99
  end
100
100
 
101
101
  private
102
102
 
103
+ # SCALAR honoring the plan row's type tag (#230's typed-scalars
104
+ # contract): 1=Integer, 2=Float, 3=boolean. Lenient on
105
+ # unparseable input — the raw String wins (first-wins house
106
+ # style; strict validation belongs to the consumer's callback
107
+ # rows). #string_value stays the raw escape.
108
+ def typed_string_value
109
+ case type_tag
110
+ when 1
111
+ s = string_value
112
+ Integer(s, 10) rescue s
113
+ when 2
114
+ s = string_value
115
+ begin
116
+ Float(s)
117
+ rescue ArgumentError, TypeError
118
+ s
119
+ end
120
+ when 3
121
+ case string_value
122
+ when "true", "1" then true
123
+ when "false", "0" then false
124
+ else string_value
125
+ end
126
+ else
127
+ string_value
128
+ end
129
+ end
130
+
103
131
  # The element plan that a child row with this wire_name recurses
104
132
  # into (nil for non-nested rows — their values carry no plan).
105
133
  def child_row_plan(child_wire_name)
@@ -114,8 +142,31 @@ class Leptris::XML::PlanValue
114
142
  result = {}
115
143
  ((@plan && @plan[:attributes]) || []).each do |row|
116
144
  v = attribute(row[:name])
117
- result[row[:name]] = v unless v.nil?
145
+ result[row[:name]] = cast_attribute(v, row) unless v.nil?
118
146
  end
119
147
  result
120
148
  end
149
+
150
+ # Attribute values come back as plain Strings from the walk;
151
+ # apply the row's type tag with the same lenient semantics as
152
+ # typed_string_value.
153
+ def cast_attribute(value, row)
154
+ tag = Leptris::XML::Descriptor.type_tag_code(row)
155
+ case tag
156
+ when 1 then Integer(value, 10) rescue value
157
+ when 2
158
+ begin
159
+ Float(value)
160
+ rescue ArgumentError, TypeError
161
+ value
162
+ end
163
+ when 3
164
+ case value
165
+ when "true", "1" then true
166
+ when "false", "0" then false
167
+ else value
168
+ end
169
+ else value
170
+ end
171
+ end
121
172
  end
data/lib/leptris.rb CHANGED
@@ -29,10 +29,13 @@ begin
29
29
  rescue LoadError => e
30
30
  raise LoadError, <<~MSG
31
31
  Leptris could not load the native libleptris library.
32
- The ruby-platform gem is a fallback variant and ships no
33
- binary; install the platform-specific variant for your system
34
- (e.g. `gem install leptris --platform=arm64-darwin`), or set
35
- LEPTRIS_LIB_PATH to a libleptris.{so,dylib,dll} file.
32
+ The ruby-platform gem carries the engine sources under
33
+ vendor-src/ and builds them at install time — a failed build
34
+ here usually means cmake or a C toolchain is missing (check
35
+ the gem install output). Alternatively install the
36
+ platform-specific variant for your system (e.g. `gem install
37
+ leptris --platform=arm64-darwin`), or set LEPTRIS_LIB_PATH to
38
+ a libleptris.{so,dylib,dll} file.
36
39
  (Underlying error: #{e.message})
37
40
  MSG
38
41
  end
data/lib/libleptris.so CHANGED
Binary file
@@ -0,0 +1,14 @@
1
+ Sources vendored in this gem (recompile rights; the ruby
2
+ variant builds them at install via extconf.rb):
3
+
4
+ - libleptris v1.9.210
5
+ - utf8proc v2.11.0
6
+
7
+ Rebuild by hand:
8
+
9
+ cmake -B vendor-src/utf8proc/build -S vendor-src/utf8proc -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON -DUTF8PROC_ENABLE_TESTING=OFF
10
+ cmake --build vendor-src/utf8proc/build --config Release
11
+ cmake --install vendor-src/utf8proc/build --prefix <prefix>
12
+
13
+ cmake -B vendor-src/libleptris/build -S vendor-src/libleptris -DCMAKE_BUILD_TYPE=Release -DLEPTRIS_BUILD_SHARED=ON -DLEPTRIS_BUILD_STATIC=OFF -DBUILD_TESTING=OFF -DLEPTRIS_BUILD_CLI=OFF -DLEPTRIS_BUILD_BENCHMARKS=OFF -DLEPTRIS_BUILD_MAN_PAGES=OFF -DLEPTRIS_ENABLE_UTF8PROC=ON -DCMAKE_PREFIX_PATH=<utf8proc-prefix>
14
+ cmake --build vendor-src/libleptris/build --config Release
@@ -1,5 +1,28 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.210] - 2026-09-20
4
+
5
+ ### Added
6
+
7
+ - The node-layout fork's document block (slice 1, #1253): two
8
+ contiguous geometrically-grown regions per document (dense node
9
+ array + string arena) with region-relative byte-offset edges —
10
+ growth never invalidates an offset. Foundation for the pugixml-
11
+ class parse layout (TODO.max-perf/node-layout-fork.md).
12
+
13
+ ### Performance
14
+
15
+ - Large-document parse (#1222/#1238 lane, #1255): the parse arena
16
+ (~4x input) exceeded the arena retain cache's 32MB ceiling, so
17
+ every repeated-parse cycle freed and re-malloc'd tens of MB.
18
+ Ceiling raised to 96MB (one document's worth of parking) with a
19
+ thread-cleanup drain and ASAN free-for-real. Measured: 13MB
20
+ attr shape 341 -> 472 MB/s (+38%); the text shape's size-decline
21
+ eliminated (465 -> 163 MB/s falling across 1.5-13MB, now
22
+ ~605-627 flat).
23
+
24
+
25
+
3
26
  ## [1.9.209] - 2026-09-20
4
27
 
5
28
  ### Performance
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.209
6
+ VERSION 1.9.210
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -47,6 +47,7 @@ set(LEPTRIS_SOURCES
47
47
 
48
48
  # Memory management
49
49
  leptris/memory/pool.c
50
+ leptris/memory/doc_block.c
50
51
  leptris/memory/arena.c
51
52
  leptris/dom/compact.c
52
53
  leptris/dom/root_doc_map.c
@@ -74,9 +74,11 @@ LEPTRIS_API void leptris_thread_cleanup(void) {
74
74
  extern void leptris_xpath_drain_thread_caches(void);
75
75
  extern void leptris_root_doc_drain_thread_caches(void);
76
76
  extern void leptris_pool_drain_recycler(void);
77
+ extern void leptris_arena_retain_drain(void);
77
78
  leptris_xpath_drain_thread_caches();
78
79
  leptris_root_doc_drain_thread_caches();
79
80
  leptris_pool_drain_recycler();
81
+ leptris_arena_retain_drain();
80
82
  }
81
83
 
82
84
  /* ---- Memory management API ---- */
@@ -51,7 +51,20 @@ static inline size_t align_up(size_t n) {
51
51
  * twice per document lifetime, so contention is irrelevant. */
52
52
  #define ARENA_RETAIN_MIN (256u * 1024u)
53
53
  #define ARENA_RETAIN_MAX_BLOCKS 4u
54
- #define ARENA_RETAIN_MAX_BYTES (32u * 1024u * 1024u)
54
+ /* 96MB: a large-document parse arena (~4x input with the
55
+ * pre-sized estimate) must survive the free to be reused by the
56
+ * next parse — at the old 32MB ceiling, every big-doc cycle
57
+ * freed and re-malloc'd tens of MB (libc madvise showed ~2% of
58
+ * repeated-parse wall time). One document's worth of parking. */
59
+ #define ARENA_RETAIN_MAX_BYTES (96u * 1024u * 1024u)
60
+
61
+ #if defined(__has_feature)
62
+ #if __has_feature(address_sanitizer)
63
+ #define ARENA_RETAIN_ASAN 1
64
+ #endif
65
+ #elif defined(__SANITIZE_ADDRESS__)
66
+ #define ARENA_RETAIN_ASAN 1
67
+ #endif
55
68
 
56
69
  static struct {
57
70
  char* base[ARENA_RETAIN_MAX_BLOCKS];
@@ -106,6 +119,14 @@ static char* retain_take(size_t request, size_t* capacity) {
106
119
  }
107
120
 
108
121
  static void retain_give(char* base, size_t size) {
122
+ #ifdef ARENA_RETAIN_ASAN
123
+ /* ASAN: retained blocks have no TLS destructor and read as
124
+ * LSan leaks — free for real (the recycle win is measurable
125
+ * only in repeated-parse loops, which ASAN runs don't gate). */
126
+ (void)size;
127
+ leptris_free_hook(base);
128
+ return;
129
+ #endif
109
130
  if (size < ARENA_RETAIN_MIN) {
110
131
  leptris_free_hook(base);
111
132
  return;
@@ -124,6 +145,18 @@ static void retain_give(char* base, size_t size) {
124
145
  retain_unlock();
125
146
  }
126
147
 
148
+ /* Host-invoked drain (leptris_thread_cleanup): the retained
149
+ * blocks are process-lifetime parking by design; release them so
150
+ * embedded hosts and leak checkers see a clean exit. */
151
+ void leptris_arena_retain_drain(void) {
152
+ retain_lock();
153
+ for (size_t i = 0; i < g_retain.count; i++)
154
+ leptris_free_hook(g_retain.base[i]);
155
+ g_retain.count = 0;
156
+ g_retain.bytes = 0;
157
+ retain_unlock();
158
+ }
159
+
127
160
  char* leptris_arena_buffer_alloc(size_t size) {
128
161
  size_t capacity = size;
129
162
  char* base = retain_take(size, &capacity);
@@ -55,6 +55,9 @@ void leptris_arena_reset(LeptrisArena* arena);
55
55
  * All pointers handed out by the arena become invalid. */
56
56
  void leptris_arena_destroy(LeptrisArena* arena);
57
57
 
58
+ /* Release retained arena blocks (leptris_thread_cleanup drain). */
59
+ void leptris_arena_retain_drain(void);
60
+
58
61
  /* Bump-allocate `size` bytes, 8-byte aligned. NOT zeroed.
59
62
  * Returns NULL when the request does not fit in remaining space —
60
63
  * no fallback allocation, ever (see the contract above). */
@@ -0,0 +1,108 @@
1
+ /* doc_block.c — the node-layout fork's document block (slice 1).
2
+ * See doc_block.h for the design contract. */
3
+
4
+ #include "doc_block.h"
5
+ #include <stdlib.h>
6
+ #include <string.h>
7
+
8
+ #define DOC_BLOCK_MIN_CAP ((size_t)4096)
9
+ #define DOC_BLOCK_ALIGN ((size_t)8)
10
+
11
+ struct LeptrisDocBlock {
12
+ char* nodes; /* node region base (8-aligned) */
13
+ size_t nodes_cap;
14
+ size_t nodes_used;
15
+ char* strs; /* string region base */
16
+ size_t strs_cap;
17
+ size_t strs_used;
18
+ };
19
+
20
+ static char* region_grow(char* p, size_t* cap, size_t need) {
21
+ size_t want = *cap ? *cap : DOC_BLOCK_MIN_CAP;
22
+ while (want < need) want *= 2;
23
+ char* grown = (char*)realloc(p, want);
24
+ if (grown) *cap = want;
25
+ return grown;
26
+ }
27
+
28
+ LeptrisDocBlock* leptris_doc_block_create(size_t initial_capacity) {
29
+ LeptrisDocBlock* b =
30
+ (LeptrisDocBlock*)calloc(1, sizeof(*b));
31
+ if (!b) return NULL;
32
+ if (initial_capacity && initial_capacity < DOC_BLOCK_MIN_CAP)
33
+ initial_capacity = DOC_BLOCK_MIN_CAP;
34
+ if (initial_capacity) {
35
+ b->nodes = (char*)malloc(initial_capacity);
36
+ b->strs = (char*)malloc(initial_capacity);
37
+ if (!b->nodes || !b->strs) {
38
+ free(b->nodes);
39
+ free(b->strs);
40
+ free(b);
41
+ return NULL;
42
+ }
43
+ b->nodes_cap = b->strs_cap = initial_capacity;
44
+ }
45
+ return b;
46
+ }
47
+
48
+ void leptris_doc_block_free(LeptrisDocBlock* b) {
49
+ if (!b) return;
50
+ free(b->nodes);
51
+ free(b->strs);
52
+ free(b);
53
+ }
54
+
55
+ size_t leptris_doc_block_node_alloc(LeptrisDocBlock* b, size_t sz) {
56
+ if (!b || sz == 0 || sz > (size_t)-1 / 2) return (size_t)-1;
57
+ size_t off = (b->nodes_used + DOC_BLOCK_ALIGN - 1) &
58
+ ~(DOC_BLOCK_ALIGN - 1);
59
+ size_t need = off + sz;
60
+ if (need > b->nodes_cap) {
61
+ char* grown = region_grow(b->nodes, &b->nodes_cap, need);
62
+ if (!grown) return (size_t)-1;
63
+ b->nodes = grown;
64
+ }
65
+ b->nodes_used = need;
66
+ return off;
67
+ }
68
+
69
+ size_t leptris_doc_block_str(LeptrisDocBlock* b, const char* s,
70
+ size_t len) {
71
+ if (!b || (!s && len)) return (size_t)-1;
72
+ size_t need = b->strs_used + len + 1;
73
+ if (need > b->strs_cap) {
74
+ char* grown = region_grow(b->strs, &b->strs_cap, need);
75
+ if (!grown) return (size_t)-1;
76
+ b->strs = grown;
77
+ }
78
+ size_t off = b->strs_used;
79
+ if (len) memcpy(b->strs + off, s, len);
80
+ b->strs[off + len] = '\0';
81
+ b->strs_used = need;
82
+ return off;
83
+ }
84
+
85
+ void* leptris_doc_block_node(const LeptrisDocBlock* b, size_t off) {
86
+ return (b && off < b->nodes_used) ? b->nodes + off : NULL;
87
+ }
88
+
89
+ void* leptris_doc_block_nodes_base(const LeptrisDocBlock* b) {
90
+ return b ? b->nodes : NULL;
91
+ }
92
+
93
+ size_t leptris_doc_block_nodes_used(const LeptrisDocBlock* b) {
94
+ return b ? b->nodes_used : 0;
95
+ }
96
+
97
+ const char* leptris_doc_block_str_at(const LeptrisDocBlock* b,
98
+ size_t off) {
99
+ return (b && off < b->strs_used) ? b->strs + off : NULL;
100
+ }
101
+
102
+ size_t leptris_doc_block_bytes_used(const LeptrisDocBlock* b) {
103
+ return b ? b->nodes_used + b->strs_used : 0;
104
+ }
105
+
106
+ size_t leptris_doc_block_capacity(const LeptrisDocBlock* b) {
107
+ return b ? b->nodes_cap + b->strs_cap : 0;
108
+ }
@@ -0,0 +1,61 @@
1
+ /* lib/src/memory/doc_block.h — the node-layout fork's document
2
+ * block (TODO.max-perf/node-layout-fork, slice 1).
3
+ *
4
+ * Two contiguous, geometrically-grown regions per document — a
5
+ * dense node array and a string arena. Every reference to either
6
+ * is a byte OFFSET within its region, so growth (realloc) never
7
+ * invalidates an edge: callers hold offsets, not pointers, and
8
+ * refetch region bases after any allocation.
9
+ *
10
+ * Differences vs memory/pool.c (which stays for mutation paths):
11
+ * the pool chains fixed 32KB pages, scattering one tree across
12
+ * pages; each parse node pays compact-edge encoding and detached
13
+ * nodes pay root-doc registration. Here the tree is contiguous
14
+ * (walk locality), edges are plain offsets (no cp16 overflow
15
+ * table), and the owning document is implied by the region base.
16
+ */
17
+ #ifndef LEPTRIS_MEMORY_DOC_BLOCK_H
18
+ #define LEPTRIS_MEMORY_DOC_BLOCK_H
19
+
20
+ #include <stddef.h>
21
+ #include <stdint.h>
22
+
23
+ #ifdef __cplusplus
24
+ extern "C" {
25
+ #endif
26
+
27
+ typedef struct LeptrisDocBlock LeptrisDocBlock;
28
+
29
+ /* Create with an initial byte capacity per region (grown
30
+ * geometrically on demand; 0 = lazy). NULL on failure. */
31
+ LeptrisDocBlock* leptris_doc_block_create(size_t initial_capacity);
32
+
33
+ void leptris_doc_block_free(LeptrisDocBlock* b);
34
+
35
+ /* Node region: bump-allocate sz bytes, 8-byte aligned. Returns
36
+ * the node's offset in the region (stable across growth) — never
37
+ * a pointer; resolve via leptris_doc_block_node. (size_t)-1 on
38
+ * failure. Growth may realloc: refetch any held pointers. */
39
+ size_t leptris_doc_block_node_alloc(LeptrisDocBlock* b, size_t sz);
40
+
41
+ /* String region: append len bytes plus a NUL. Returns the
42
+ * string's offset in ITS region (stable across growth) or
43
+ * (size_t)-1 on failure. */
44
+ size_t leptris_doc_block_str(LeptrisDocBlock* b, const char* s,
45
+ size_t len);
46
+
47
+ /* Resolve an offset (region base + off, one add). */
48
+ void* leptris_doc_block_node(const LeptrisDocBlock* b, size_t off);
49
+
50
+ void* leptris_doc_block_nodes_base(const LeptrisDocBlock* b);
51
+ size_t leptris_doc_block_nodes_used(const LeptrisDocBlock* b);
52
+ const char* leptris_doc_block_str_at(const LeptrisDocBlock* b,
53
+ size_t off);
54
+ size_t leptris_doc_block_bytes_used(const LeptrisDocBlock* b);
55
+ size_t leptris_doc_block_capacity(const LeptrisDocBlock* b);
56
+
57
+ #ifdef __cplusplus
58
+ }
59
+ #endif
60
+
61
+ #endif /* LEPTRIS_MEMORY_DOC_BLOCK_H */
@@ -116,6 +116,7 @@ leptris_add_test(test_simd_spans common/test_simd_spans.cpp)
116
116
  leptris_add_test(test_pool memory/test_pool.cpp)
117
117
  leptris_add_test(test_oom memory/test_oom_injection.cpp)
118
118
  leptris_add_test(test_arena memory/test_arena.cpp)
119
+ leptris_add_test(test_doc_block memory/test_doc_block.cpp)
119
120
  leptris_add_test(test_pool_arena memory/test_pool_arena.cpp)
120
121
  leptris_add_test(test_xpath xpath/test_xpath.cpp)
121
122
  leptris_add_test(test_exslt xpath/test_exslt.cpp)
@@ -192,11 +192,25 @@ TEST(Arena, AllPointersWithinSpanForCompactEncoding) {
192
192
 
193
193
  /* ---- Retained-block free list (parse fault fix) ---------------------- */
194
194
 
195
+ #if defined(__has_feature)
196
+ #if __has_feature(address_sanitizer)
197
+ #define ARENA_TESTS_ASAN 1
198
+ #endif
199
+ #elif defined(__SANITIZE_ADDRESS__)
200
+ #define ARENA_TESTS_ASAN 1
201
+ #endif
202
+
195
203
  TEST(LeptrisArena, RetainsAndReusesLargeBlocks) {
196
204
  /* Blocks >= 256 KB must round-trip through the free list: the
197
205
  * next same-size create gets the SAME mapping back (no munmap,
198
206
  * no page faults). Machine-independent by construction: it
199
207
  * asserts pointer identity, not timing. */
208
+ /* Under ASAN the retain cache frees for real (no TLS
209
+ * destructor; parked blocks read as LSan leaks) — the reuse
210
+ * contract is deliberately disabled there. */
211
+ #if defined(ARENA_TESTS_ASAN)
212
+ GTEST_SKIP() << "retain cache disabled under ASAN";
213
+ #endif
200
214
  const size_t big = 512u * 1024u;
201
215
  LeptrisArena* a1 = leptris_arena_create(big);
202
216
  ASSERT_NE(a1, nullptr);
@@ -239,6 +253,13 @@ TEST(LeptrisArena, SmallBlocksStillMallocSemantics) {
239
253
  }
240
254
 
241
255
  TEST(LeptrisArena, BufferRoundTripReusesMapping) {
256
+ /* Under ASAN the retain cache frees for real (no TLS
257
+ * destructor; parked blocks read as LSan leaks) — the reuse
258
+ * contract is deliberately disabled there. */
259
+ #if defined(ARENA_TESTS_ASAN)
260
+ GTEST_SKIP() << "retain cache disabled under ASAN";
261
+ #endif
262
+
242
263
  char* b1 = leptris_arena_buffer_alloc(512u * 1024u);
243
264
  ASSERT_NE(b1, nullptr);
244
265
  memset(b1, 1, 512u * 1024u);
@@ -0,0 +1,94 @@
1
+ // test/memory/test_doc_block.cpp - the node-layout fork's document
2
+ // block (slice 1). Falsifiable contract: offsets stay valid across
3
+ // geometric growth, regions stay dense/aligned, boundary inputs
4
+ // are refused.
5
+
6
+ #include <gtest/gtest.h>
7
+ #include <cstring>
8
+ #include <cstdio>
9
+ #include <vector>
10
+ #include "../../src/leptris/memory/doc_block.h"
11
+
12
+ typedef struct {
13
+ uint32_t first_child;
14
+ uint32_t next;
15
+ uint32_t name_off;
16
+ } TNode;
17
+
18
+ TEST(DocBlock, OffsetsSurviveGrowth) {
19
+ LeptrisDocBlock* b = leptris_doc_block_create(0);
20
+ ASSERT_NE(b, nullptr);
21
+ std::vector<uint32_t> offs;
22
+ const int N = 20000;
23
+ for (int i = 0; i < N; i++) {
24
+ size_t off = leptris_doc_block_node_alloc(b, sizeof(TNode));
25
+ ASSERT_NE(off, (size_t)-1);
26
+ ASSERT_EQ(off % 8u, 0u);
27
+ TNode* n = (TNode*)leptris_doc_block_node(b, off);
28
+ ASSERT_NE(n, nullptr);
29
+ n->first_child = 0;
30
+ n->next = i > 0 ? offs[i - 1] : 0;
31
+ n->name_off = 0;
32
+ offs.push_back((uint32_t)off);
33
+ if (i % 1000 == 0) {
34
+ for (size_t k = 1; k < offs.size(); k++) {
35
+ const TNode* p =
36
+ (const TNode*)leptris_doc_block_node(b, offs[k]);
37
+ ASSERT_NE(p, nullptr);
38
+ ASSERT_EQ(p->next, offs[k - 1]);
39
+ }
40
+ }
41
+ }
42
+ EXPECT_GT(leptris_doc_block_capacity(b), (size_t)0);
43
+ leptris_doc_block_free(b);
44
+ }
45
+
46
+ TEST(DocBlock, StringRegionStableAcrossGrowth) {
47
+ LeptrisDocBlock* b = leptris_doc_block_create(64);
48
+ ASSERT_NE(b, nullptr);
49
+ std::vector<size_t> offs;
50
+ char buf[32];
51
+ for (int i = 0; i < 5000; i++) {
52
+ int n = snprintf(buf, sizeof(buf), "name-%05d", i);
53
+ size_t off = leptris_doc_block_str(b, buf, (size_t)n);
54
+ ASSERT_NE(off, (size_t)-1);
55
+ offs.push_back(off);
56
+ }
57
+ for (int i = 0; i < 5000; i += 500) {
58
+ int n = snprintf(buf, sizeof(buf), "name-%05d", i);
59
+ const char* s = leptris_doc_block_str_at(b, offs[i]);
60
+ ASSERT_NE(s, nullptr);
61
+ EXPECT_EQ(std::memcmp(s, buf, (size_t)n), 0);
62
+ EXPECT_EQ(s[n], '\0');
63
+ }
64
+ leptris_doc_block_free(b);
65
+ }
66
+
67
+ TEST(DocBlock, BoundaryInputsRefused) {
68
+ LeptrisDocBlock* b = leptris_doc_block_create(0);
69
+ ASSERT_NE(b, nullptr);
70
+ EXPECT_EQ(leptris_doc_block_node_alloc(b, 0), (size_t)-1);
71
+ EXPECT_EQ(leptris_doc_block_node_alloc(nullptr, 8), (size_t)-1);
72
+ EXPECT_EQ(leptris_doc_block_str(b, nullptr, 3), (size_t)-1);
73
+ size_t off = leptris_doc_block_str(b, "", 0);
74
+ EXPECT_NE(off, (size_t)-1);
75
+ EXPECT_STREQ(leptris_doc_block_str_at(b, off), "");
76
+ EXPECT_EQ(leptris_doc_block_node(b, 1 << 20), nullptr);
77
+ leptris_doc_block_free(b);
78
+ leptris_doc_block_free(nullptr);
79
+ }
80
+
81
+ TEST(DocBlock, NodesAndStringsIndependent) {
82
+ LeptrisDocBlock* b = leptris_doc_block_create(0);
83
+ ASSERT_NE(b, nullptr);
84
+ size_t n1 = leptris_doc_block_node_alloc(b, 16);
85
+ size_t s1 = leptris_doc_block_str(b, "abc", 3);
86
+ size_t n2 = leptris_doc_block_node_alloc(b, 16);
87
+ size_t s2 = leptris_doc_block_str(b, "defg", 4);
88
+ EXPECT_NE(n1, n2);
89
+ EXPECT_NE(s1, s2);
90
+ EXPECT_STREQ(leptris_doc_block_str_at(b, s1), "abc");
91
+ EXPECT_STREQ(leptris_doc_block_str_at(b, s2), "defg");
92
+ EXPECT_EQ(leptris_doc_block_nodes_used(b), 32u);
93
+ leptris_doc_block_free(b);
94
+ }
@@ -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.209",
4
+ "version": "1.9.210",
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.209.1
4
+ version: 1.9.210.1
5
5
  platform: arm-linux
6
6
  authors:
7
7
  - Ribose Inc.
@@ -187,6 +187,7 @@ files:
187
187
  - lib/libleptris.so
188
188
  - lib/libutf8proc.so.3
189
189
  - scripts/gem_smoke.rb
190
+ - vendor-src/README.md
190
191
  - vendor-src/libleptris/CHANGELOG.md
191
192
  - vendor-src/libleptris/CLAUDE.md
192
193
  - vendor-src/libleptris/CMakeLists.txt
@@ -644,6 +645,8 @@ files:
644
645
  - vendor-src/libleptris/src/leptris/leptris_parse.h
645
646
  - vendor-src/libleptris/src/leptris/memory/arena.c
646
647
  - vendor-src/libleptris/src/leptris/memory/arena.h
648
+ - vendor-src/libleptris/src/leptris/memory/doc_block.c
649
+ - vendor-src/libleptris/src/leptris/memory/doc_block.h
647
650
  - vendor-src/libleptris/src/leptris/memory/pool.c
648
651
  - vendor-src/libleptris/src/leptris/memory/pool.h
649
652
  - vendor-src/libleptris/src/leptris/rng/parse.c
@@ -798,6 +801,7 @@ files:
798
801
  - vendor-src/libleptris/test/html/test_html.cpp
799
802
  - vendor-src/libleptris/test/html/test_html5lib.cpp
800
803
  - vendor-src/libleptris/test/memory/test_arena.cpp
804
+ - vendor-src/libleptris/test/memory/test_doc_block.cpp
801
805
  - vendor-src/libleptris/test/memory/test_oom_injection.cpp
802
806
  - vendor-src/libleptris/test/memory/test_pool.cpp
803
807
  - vendor-src/libleptris/test/memory/test_pool_arena.cpp