leptris 1.9.216.0-aarch64-linux → 1.9.221.0-aarch64-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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.adoc +63 -4
- data/Rakefile +1 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/libleptris.so +0 -0
- data/vendor-src/README.md +1 -1
- data/vendor-src/libleptris/CHANGELOG.md +68 -0
- data/vendor-src/libleptris/CMakeLists.txt +1 -1
- data/vendor-src/libleptris/TODO.max-perf/7-validate-bindings.md +14 -4
- data/vendor-src/libleptris/benchmarks/html/bench_html_parse.c +44 -0
- data/vendor-src/libleptris/src/leptris/dom/compact.c +40 -17
- data/vendor-src/libleptris/src/leptris/dom/compact.h +3 -0
- data/vendor-src/libleptris/src/leptris/dom/element.c +10 -2
- data/vendor-src/libleptris/src/leptris/dom/text.c +10 -0
- data/vendor-src/libleptris/src/leptris/html/html_parse.c +318 -266
- data/vendor-src/libleptris/src/leptris/xslt/xslt_exec.c +24 -0
- data/vendor-src/libleptris/vcpkg.json +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c56cdcd6847369fa4c7ab56ae64331b2759cf56e10ecf2697a4a387218f341f1
|
|
4
|
+
data.tar.gz: 23c44ac793d90e6443c2d701b481cd8b4bdb6c50c1d5ba2654fa178ee53e8600
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48a54a10ab8125023c2945bb95ac1f75f0cd00d24d55094649ea7f05ebd413588979c1a8bcd5c97c779a0b69f352bd3964ad7c073a185629c6dbf05004edc92f
|
|
7
|
+
data.tar.gz: b7d81c9d46758b795a36ebfcbf5c0a2e6ad68f4586d0f036cf6e1aaabe882b5a979e5daec03e63dd8415f96d94315e59671368ff78cf9e220bdaef28dfb8c0ed
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ 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.221.0] - 2026-09-22
|
|
9
|
+
|
|
10
|
+
### Performance — engine sync (1.9.221)
|
|
11
|
+
|
|
12
|
+
- Vendored libleptris 1.9.221: per-document overflow teardown in
|
|
13
|
+
the dispatch row (#682). No binding-facing surface changes;
|
|
14
|
+
audit 342/342, suite 770/0.
|
|
15
|
+
|
|
16
|
+
## [1.9.220.0] - 2026-09-22
|
|
17
|
+
|
|
18
|
+
### Performance — engine sync (1.9.217–1.9.220)
|
|
19
|
+
|
|
20
|
+
- Vendored libleptris 1.9.220: four perf releases — the HTML
|
|
21
|
+
parse tag-id classifier stack + in-place borrowed text runs
|
|
22
|
+
(#1218 slices 1-2), the authoritative content_len text-join
|
|
23
|
+
pass (#1285 slice 1), and borrowed string-values for
|
|
24
|
+
`value-of select="."` (#682). No binding-facing surface
|
|
25
|
+
changes; audit 342/342, suite 770/0.
|
|
26
|
+
|
|
8
27
|
## [1.9.216.0] - 2026-09-21
|
|
9
28
|
|
|
10
29
|
### Added — the plan-ABI consumer contract (#298 fully bound; engine 1.9.214–216)
|
data/README.adoc
CHANGED
|
@@ -504,10 +504,69 @@ Row kinds: `:scalar`, `:collection`, `:nested` (via `plan:`), `:raw`
|
|
|
504
504
|
offset + `type_tag` echo, matching `Node#byte_offset`). Namespace
|
|
505
505
|
binding lives on plans: `ns:` is `:none` (default), `:any`, or
|
|
506
506
|
`{ exact: "urn:..." }`; `flags:` also accepts `:cdata`, `:ordered`,
|
|
507
|
-
`:ns_lenient` (#754 out-of-namespace adoption)
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
507
|
+
`:ns_lenient` (#754 out-of-namespace adoption), and `:order_spine`
|
|
508
|
+
(#1273 — unmatched sibling text/comment/PI runs emit as positioned
|
|
509
|
+
scalars so ordered hosts rebuild element order without re-parsing).
|
|
510
|
+
Children the plan does not describe are skipped; undescribed document
|
|
511
|
+
order within a row is preserved. `#walk` returns a lazy `PlanValue`
|
|
512
|
+
tree (the result outlives the document); `#to_ruby` materializes it.
|
|
513
|
+
|
|
514
|
+
=== Typed scalars and the fused entry (#230/#298)
|
|
515
|
+
|
|
516
|
+
Plan rows accept `type: :integer | :float | :boolean` — the walk
|
|
517
|
+
parses the values natively (in-pass, libleptris #1269a) and
|
|
518
|
+
`#to_ruby` / the bulk face return `Integer` / `Float` / `true` /
|
|
519
|
+
`false`. Unparseable values fall back to the raw String;
|
|
520
|
+
`PlanValue#string_value` stays the raw escape, and
|
|
521
|
+
`#int_value` / `#float_value` / `#bool_value` expose the parsed
|
|
522
|
+
accessors directly.
|
|
523
|
+
|
|
524
|
+
`Descriptor#materialize(source)` is the fused entry: source bytes →
|
|
525
|
+
typed rows in ONE C call — parse, walk, and free happen natively
|
|
526
|
+
(#1269b); the standalone `PlanValue` tree is all that surfaces.
|
|
527
|
+
|
|
528
|
+
[source,ruby]
|
|
529
|
+
----
|
|
530
|
+
descriptor = Leptris::XML::Descriptor.build(
|
|
531
|
+
name: "iso",
|
|
532
|
+
children: [{ name: "row", kind: :collection, plan: {
|
|
533
|
+
name: "row",
|
|
534
|
+
attributes: [{ name: "id", kind: :scalar, type: :integer }],
|
|
535
|
+
children: [
|
|
536
|
+
{ name: "price", kind: :scalar, type: :float },
|
|
537
|
+
{ name: "active", kind: :scalar, type: :boolean },
|
|
538
|
+
] } },
|
|
539
|
+
])
|
|
540
|
+
|
|
541
|
+
rows = descriptor.materialize(xml_source) # one C call
|
|
542
|
+
rows.as_kwarg_hash[:children]["row"] # typed, hydrator-ready
|
|
543
|
+
----
|
|
544
|
+
|
|
545
|
+
=== Predicate rows (#1272) and order identity (#1273)
|
|
546
|
+
|
|
547
|
+
Same-wire-name rows can partition on attribute values with `when:` —
|
|
548
|
+
AND across pairs, exclusive routing (first matching row wins per
|
|
549
|
+
occurrence):
|
|
550
|
+
|
|
551
|
+
[source,ruby]
|
|
552
|
+
----
|
|
553
|
+
Leptris::XML::Descriptor.build(
|
|
554
|
+
name: "r",
|
|
555
|
+
children: [
|
|
556
|
+
{ name: "item", kind: :collection, when: { "kind" => "a" },
|
|
557
|
+
plan: { name: "item", children: [{ name: "name", kind: :scalar }] } },
|
|
558
|
+
{ name: "item", kind: :collection, when: { "kind" => "b" },
|
|
559
|
+
plan: { name: "item", children: [{ name: "name", kind: :scalar }] } },
|
|
560
|
+
])
|
|
561
|
+
----
|
|
562
|
+
|
|
563
|
+
`PlanValue#node_kind` and `#order_index` carry document-order
|
|
564
|
+
identity for matched rows (and, under `:order_spine`, for the
|
|
565
|
+
interleaved text runs) — ordered/mixed content reconstructs
|
|
566
|
+
without fragment re-parsing. `PlanValue#as_kwarg_hash` is the
|
|
567
|
+
bulk hydrator face: one Ruby call per node returns the typed
|
|
568
|
+
attribute/children hash (the crossings floor on the 5k-row ISO
|
|
569
|
+
fixture is ~4 FFI crossings per row, pinned by spec).
|
|
511
570
|
|
|
512
571
|
== XQuery
|
|
513
572
|
|
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.
|
|
11
|
+
LIBLEPTRIS_VERSION = "1.9.221"
|
|
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
|
data/lib/leptris/version.rb
CHANGED
data/lib/libleptris.so
CHANGED
|
Binary file
|
data/vendor-src/README.md
CHANGED
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.9.221] - 2026-09-22
|
|
4
|
+
|
|
5
|
+
### Performance
|
|
6
|
+
|
|
7
|
+
- per-document overflow teardown (#682 dispatch row) (dom)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## [1.9.220] - 2026-09-21
|
|
12
|
+
|
|
13
|
+
### Performance
|
|
14
|
+
|
|
15
|
+
- in-place borrowed text runs (#1218 slice 2) (html)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## [1.9.219] - 2026-09-21
|
|
20
|
+
|
|
21
|
+
### Performance
|
|
22
|
+
|
|
23
|
+
- authoritative content_len in the text-join passes (#1285 slice 1) (dom)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## [1.9.218] - 2026-09-21
|
|
28
|
+
|
|
29
|
+
### Performance
|
|
30
|
+
|
|
31
|
+
- borrowed string-value for value-of select='.' (#682) (xslt)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## [1.9.217] - 2026-09-21
|
|
36
|
+
|
|
37
|
+
### Performance
|
|
38
|
+
|
|
39
|
+
- tag-id classifier stack + single-lookup start tags (#1218) (html)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## [1.9.217] - 2026-09-21
|
|
44
|
+
|
|
45
|
+
### Added — plan-ABI additive v1 (descriptor materialization, PR #1275)
|
|
46
|
+
|
|
47
|
+
Closes #1272, #1273, #1269 — the binding-facing plan upgrades:
|
|
48
|
+
|
|
49
|
+
- **#1272 attribute-predicate rows**: child rows and attribute plans
|
|
50
|
+
accept `leptris_attr_predicate` pairs; same-wire siblings partition
|
|
51
|
+
by predicate (AND across pairs, exclusive routing only when
|
|
52
|
+
predicates are present — the #1115 multi-namespace behavior is
|
|
53
|
+
untouched).
|
|
54
|
+
- **#1273 document-order identity**: every result value carries an
|
|
55
|
+
`order_index` cumulative across rows (strictly increasing in
|
|
56
|
+
document order); `LEPTRIS_PLAN_FLAG_EMIT_ORDER_SPINE` optionally
|
|
57
|
+
appends unmatched text runs so interleavings reconstruct without
|
|
58
|
+
fragment re-parses.
|
|
59
|
+
- **#1269 in-pass types + fusion**: rows carry a `type_tag`
|
|
60
|
+
(1=int / 2=float / 3=bool) executed during the walk with soft-fail
|
|
61
|
+
to string; `leptris_plan_materialize` fuses parse→walk→free into
|
|
62
|
+
one call for the binding hot path.
|
|
63
|
+
- New accessors: `leptris_plan_value_node_kind/_order_index/_int/
|
|
64
|
+
_float/_bool`. `LEPTRIS_PLAN_ABI_VERSION` stays 1 — all additions
|
|
65
|
+
are trailing fields, aggregate initializers keep compiling.
|
|
66
|
+
|
|
67
|
+
Five new specs in `test/abi/test_descriptor.cpp` (Plan1272/1273/
|
|
68
|
+
1269a/1269b); full ctest green across all 24 CI legs.
|
|
69
|
+
|
|
70
|
+
|
|
3
71
|
## [1.9.216] - 2026-09-21
|
|
4
72
|
|
|
5
73
|
### Fixed
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# Lever 7: Lane 16.6 — leptris validate CLI + bindings
|
|
2
2
|
|
|
3
|
-
Status:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Status: DONE (issue #1183). All three legs shipped and verified on
|
|
4
|
+
main:
|
|
5
|
+
|
|
6
|
+
- **CLI**: `cli/commands/validate.c` (286 lines) — RELAX NG +
|
|
7
|
+
Schematron validation, stdin/file input; registered in
|
|
8
|
+
`cli/main.c:97`, listed in the help text (`leptris validate`).
|
|
9
|
+
- **leptris-ruby**: `RelaxNG#validate/#valid?/#validate_errors/
|
|
10
|
+
#validate_report`, `Schematron#validate/#valid?`,
|
|
11
|
+
`DTD#validate/#valid?` (lib/leptris/xml/{relaxng,schematron,dtd}.rb).
|
|
12
|
+
- **leptris-py**: `RelaxNG`, `DTD`, `Schematron` exported from the
|
|
13
|
+
package root (leptris/__init__.py).
|
|
14
|
+
|
|
15
|
+
Well-formedness validation is `leptris_parse_string` itself (line:col
|
|
16
|
+
error contract); no separate wrapper required.
|
|
@@ -67,7 +67,28 @@ static size_t gen_entities(char* out, int paras) {
|
|
|
67
67
|
return len;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/* Table-of-rows shape (#1218): well-formed medium HTML — one big
|
|
71
|
+
* tbody, no error-recovery cases, matching the serialbench
|
|
72
|
+
* medium.html fixture class the issue reports 3.4-7x on. */
|
|
73
|
+
static size_t gen_table_rows(char* out, int rows) {
|
|
74
|
+
size_t len = 0;
|
|
75
|
+
len += (size_t)sprintf(out + len,
|
|
76
|
+
"<!DOCTYPE html><html lang='en'><head><meta charset='utf-8'>"
|
|
77
|
+
"<title>Report</title></head><body>"
|
|
78
|
+
"<table class='data'><thead><tr><th>Id</th><th>Name</th>"
|
|
79
|
+
"<th>Value</th></tr></thead><tbody>");
|
|
80
|
+
for (int r = 0; r < rows; r++)
|
|
81
|
+
len += (size_t)sprintf(out + len,
|
|
82
|
+
"<tr class='r%d'><td>%d</td><td>row-%d</td>"
|
|
83
|
+
"<td data-v='%d'>%d.%02d</td></tr>",
|
|
84
|
+
r & 1, r, r, r, r, r & 99);
|
|
85
|
+
len += (size_t)sprintf(out + len,
|
|
86
|
+
"</tbody></table></body></html>");
|
|
87
|
+
return len;
|
|
88
|
+
}
|
|
89
|
+
|
|
70
90
|
int main(void) {
|
|
91
|
+
const char* only = getenv("HTML_BENCH_ONLY"); /* page|entities|table */
|
|
71
92
|
size_t cap = 1u << 22;
|
|
72
93
|
char* html = (char*)malloc(cap);
|
|
73
94
|
if (!html) return 1;
|
|
@@ -80,6 +101,7 @@ int main(void) {
|
|
|
80
101
|
|
|
81
102
|
int reps = getenv("HTML_BENCH_REPS") ? atoi(getenv("HTML_BENCH_REPS")) : 9;
|
|
82
103
|
double best = 1e18;
|
|
104
|
+
if (!only || strcmp(only, "page") == 0) {
|
|
83
105
|
for (int i = 0; i < reps; i++) {
|
|
84
106
|
double t0 = benchmark_time_us();
|
|
85
107
|
LeptrisDocument d = leptris_parse_html_string(html, len, &st);
|
|
@@ -89,8 +111,10 @@ int main(void) {
|
|
|
89
111
|
}
|
|
90
112
|
printf("leptris html parse: %zu bytes, %.0f us, %.1f MB/s (best of %d)\n",
|
|
91
113
|
len, best, (double)len / best, reps);
|
|
114
|
+
}
|
|
92
115
|
|
|
93
116
|
/* Entity-laden second shape (nokogiri reference: same page). */
|
|
117
|
+
if (!only || strcmp(only, "entities") == 0) {
|
|
94
118
|
len = gen_entities(html, 20000);
|
|
95
119
|
warm = leptris_parse_html_string(html, len, &st);
|
|
96
120
|
if (!warm) { printf("entity parse failed\n"); return 1; }
|
|
@@ -106,6 +130,26 @@ int main(void) {
|
|
|
106
130
|
printf("leptris html parse (entity-laden): %zu bytes, %.0f us, "
|
|
107
131
|
"%.1f MB/s (best of %d)\n",
|
|
108
132
|
len, best, (double)len / best, reps);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Table-of-rows ~2MB shape (#1218, serialbench medium class). */
|
|
136
|
+
if (!only || strcmp(only, "table") == 0) {
|
|
137
|
+
len = gen_table_rows(html, 32000);
|
|
138
|
+
warm = leptris_parse_html_string(html, len, &st);
|
|
139
|
+
if (!warm) { printf("table parse failed\n"); return 1; }
|
|
140
|
+
leptris_document_free(warm);
|
|
141
|
+
best = 1e18;
|
|
142
|
+
for (int i = 0; i < reps; i++) {
|
|
143
|
+
double t0 = benchmark_time_us();
|
|
144
|
+
LeptrisDocument d = leptris_parse_html_string(html, len, &st);
|
|
145
|
+
double us = benchmark_time_us() - t0;
|
|
146
|
+
leptris_document_free(d);
|
|
147
|
+
if (us < best) best = us;
|
|
148
|
+
}
|
|
149
|
+
printf("leptris html parse (table-rows): %zu bytes, %.0f us, "
|
|
150
|
+
"%.1f MB/s (best of %d)\n",
|
|
151
|
+
len, best, (double)len / best, reps);
|
|
152
|
+
}
|
|
109
153
|
free(html);
|
|
110
154
|
return 0;
|
|
111
155
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
#include "compact.h"
|
|
12
|
+
#include "../leptris_internal.h" /* struct leptris_document (overflow_entries) */
|
|
12
13
|
#include "../common/port.h"
|
|
13
14
|
#include <stdlib.h>
|
|
14
15
|
#include <string.h>
|
|
@@ -158,6 +159,14 @@ int leptris_compact_overflow_set(LeptrisCompactOverflowTable* table,
|
|
|
158
159
|
table->buckets[index] = entry;
|
|
159
160
|
table->entry_count++;
|
|
160
161
|
|
|
162
|
+
/* #682: parallel per-document chain — teardown walks exactly
|
|
163
|
+
* this doc's entries instead of scanning the whole table. */
|
|
164
|
+
if (doc) {
|
|
165
|
+
entry->doc_next =
|
|
166
|
+
(struct leptris_compact_overflow_entry*)doc->overflow_entries;
|
|
167
|
+
doc->overflow_entries = entry;
|
|
168
|
+
}
|
|
169
|
+
|
|
161
170
|
if (table->entry_count >= table->bucket_count) {
|
|
162
171
|
overflow_table_grow(table);
|
|
163
172
|
}
|
|
@@ -208,26 +217,40 @@ void leptris_compact_cleanup(void) {
|
|
|
208
217
|
void leptris_compact_cleanup_document(struct leptris_document* doc) {
|
|
209
218
|
if (!doc || !g_overflow_table) return;
|
|
210
219
|
|
|
211
|
-
|
|
220
|
+
/* #682: walk THIS document's entry chain (linked at set time)
|
|
221
|
+
* instead of scanning every bucket of the global table — the
|
|
222
|
+
* old full-table sweep ran on every document free and dominated
|
|
223
|
+
* apply-heavy XSLT profiles (result docs register thousands of
|
|
224
|
+
* cross-block edge entries; the sampler pinned 35% of the
|
|
225
|
+
* dispatch bench in this loop). */
|
|
226
|
+
LeptrisCompactOverflowEntry* e =
|
|
227
|
+
(LeptrisCompactOverflowEntry*)doc->overflow_entries;
|
|
228
|
+
doc->overflow_entries = NULL;
|
|
212
229
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
LeptrisCompactOverflowEntry*
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
size_t removed_count = 0;
|
|
231
|
+
while (e) {
|
|
232
|
+
LeptrisCompactOverflowEntry* next = e->doc_next;
|
|
233
|
+
if (e->doc == doc) {
|
|
234
|
+
/* Stale chain links (entry re-registered under another
|
|
235
|
+
* doc) are skipped — the ->doc check keeps them alive
|
|
236
|
+
* for their true owner. */
|
|
237
|
+
size_t index = hash_pointer(e->key,
|
|
238
|
+
g_overflow_table->bucket_count);
|
|
239
|
+
LeptrisCompactOverflowEntry** entry_ptr =
|
|
240
|
+
&g_overflow_table->buckets[index];
|
|
241
|
+
while (*entry_ptr) {
|
|
242
|
+
if (*entry_ptr == e) {
|
|
243
|
+
/* Unlink only — entries live in SLABS owned by
|
|
244
|
+
* the table; free()ing them corrupted the heap
|
|
245
|
+
* (#450 sibling-edge follow-up). */
|
|
246
|
+
*entry_ptr = e->next;
|
|
247
|
+
removed_count++;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
entry_ptr = &(*entry_ptr)->next;
|
|
229
251
|
}
|
|
230
252
|
}
|
|
253
|
+
e = next;
|
|
231
254
|
}
|
|
232
255
|
|
|
233
256
|
g_overflow_table->entry_count -= removed_count;
|
|
@@ -60,6 +60,9 @@ typedef struct leptris_compact_overflow_entry {
|
|
|
60
60
|
void* value; /* Actual pointer value */
|
|
61
61
|
struct leptris_document* doc; /* Document that owns this entry */
|
|
62
62
|
struct leptris_compact_overflow_entry* next;
|
|
63
|
+
/* #682: per-document chain — teardown walks THIS doc's entries
|
|
64
|
+
* instead of scanning every bucket of the global table. */
|
|
65
|
+
struct leptris_compact_overflow_entry* doc_next;
|
|
63
66
|
} LeptrisCompactOverflowEntry;
|
|
64
67
|
|
|
65
68
|
/**
|
|
@@ -979,7 +979,12 @@ static size_t calculate_text_length_recursive(LeptrisNode* node) {
|
|
|
979
979
|
* text->content_len directly. */
|
|
980
980
|
const char* content = leptris_text_get_content(text);
|
|
981
981
|
if (content) {
|
|
982
|
-
|
|
982
|
+
/* #1285 slice 1: frozen (parse-created,
|
|
983
|
+
* immutable) texts carry an authoritative
|
|
984
|
+
* content_len — skip the scan. */
|
|
985
|
+
size_t clen = child->frozen ? text->content_len
|
|
986
|
+
: strlen(content);
|
|
987
|
+
len += clen;
|
|
983
988
|
}
|
|
984
989
|
} else if (child->type == LEPTRIS_NODE_TYPE_CDATA) {
|
|
985
990
|
LeptrisCDATANode* cdata = (LeptrisCDATANode*)child;
|
|
@@ -1013,7 +1018,10 @@ static void copy_text_content_recursive(LeptrisNode* node, char* result, size_t*
|
|
|
1013
1018
|
LeptrisTextNode* text = (LeptrisTextNode*)child;
|
|
1014
1019
|
const char* content = leptris_text_get_content(text);
|
|
1015
1020
|
if (content) {
|
|
1016
|
-
|
|
1021
|
+
/* #1285 slice 1: frozen texts — authoritative
|
|
1022
|
+
* content_len (mirrors the length pass). */
|
|
1023
|
+
size_t clen = child->frozen ? text->content_len
|
|
1024
|
+
: strlen(content);
|
|
1017
1025
|
memcpy(result + *offset, content, clen);
|
|
1018
1026
|
*offset += clen;
|
|
1019
1027
|
}
|
|
@@ -102,6 +102,16 @@ const char* leptris_text_get_content(LeptrisTextNode* text) {
|
|
|
102
102
|
if (!text) return NULL;
|
|
103
103
|
|
|
104
104
|
if (text->borrowed && text->pool) {
|
|
105
|
+
/* #1218 slice 2: an in-place terminated run (the parser
|
|
106
|
+
* wrote the NUL into the doc-owned input copy) is already
|
|
107
|
+
* materialized — serve it directly when it carries no
|
|
108
|
+
* entity. A run WITH '&' still takes the expansion path
|
|
109
|
+
* below (dp-borrowed nodes rely on it). */
|
|
110
|
+
if (text->content_len > 0 &&
|
|
111
|
+
text->content[text->content_len] == '\0' &&
|
|
112
|
+
memchr(text->content, '&', text->content_len) == NULL) {
|
|
113
|
+
return text->content;
|
|
114
|
+
}
|
|
105
115
|
/* Entity expansion: if the borrowed content contains '&',
|
|
106
116
|
* expand predefined XML entities (&, <, etc.) and
|
|
107
117
|
* numeric character references (A, B) into a new
|