leptris 1.9.216.0-x86_64-linux → 1.9.220.0-x86_64-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: bd31f0587ebb9098ed318c732ffb69d6b38c4476b5e16c4dd8fc78bcc9f1eba2
4
- data.tar.gz: cd6c4d000d2cff045c16ee809e5b359e0f344334e8c567d798d409857687b083
3
+ metadata.gz: 18237585fdb14e605fffad88e641b4030aee2d7db1f4e8bc3594077da61e090c
4
+ data.tar.gz: ad306389acd63683fd14fda7e39c874611812956631a85e852aafd1606aca2ae
5
5
  SHA512:
6
- metadata.gz: '002850579eb358499474e6f8420046962b03d3ac2dd574251dec77928da4685c66e17bd08f0dbda8d4130cf350fb860c90813cdd3d35196e98db5bff1f45279b'
7
- data.tar.gz: 3ccce56b40ee17ac63de60655ccc2972d463699a92f6500eb60e55d1dbf2d026604ef86ef30f205a591ba5006657035b55229f8da3d2e1134209581c9088d58a
6
+ metadata.gz: 807ceb316a225c9f832f080e998c3ddd1e2d1f2e2d9ccc0e042feaf20179b1fcf71157cfd8931747c28cc41f80781390df70c4e31d4d5db53822d9cd1535aa76
7
+ data.tar.gz: 7cb5959381e7a27940d4a3eb67d86416d2eabb41cd666bd25c38c0fa42ba9c73f5c5011fa1abb4c09f2155323b56502596d8366d829d749358e7f5a933f20caa
data/CHANGELOG.md CHANGED
@@ -5,6 +5,17 @@ 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.220.0] - 2026-09-22
9
+
10
+ ### Performance — engine sync (1.9.217–1.9.220)
11
+
12
+ - Vendored libleptris 1.9.220: four perf releases — the HTML
13
+ parse tag-id classifier stack + in-place borrowed text runs
14
+ (#1218 slices 1-2), the authoritative content_len text-join
15
+ pass (#1285 slice 1), and borrowed string-values for
16
+ `value-of select="."` (#682). No binding-facing surface
17
+ changes; audit 342/342, suite 770/0.
18
+
8
19
  ## [1.9.216.0] - 2026-09-21
9
20
 
10
21
  ### Added — the plan-ABI consumer contract (#298 fully bound; engine 1.9.214–216)
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.216"
11
+ LIBLEPTRIS_VERSION = "1.9.220"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.216.0"
4
+ VERSION = "1.9.220.0"
5
5
  end
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.216
4
+ - libleptris v1.9.220
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,65 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.220] - 2026-09-21
4
+
5
+ ### Performance
6
+
7
+ - in-place borrowed text runs (#1218 slice 2) (html)
8
+
9
+
10
+
11
+ ## [1.9.219] - 2026-09-21
12
+
13
+ ### Performance
14
+
15
+ - authoritative content_len in the text-join passes (#1285 slice 1) (dom)
16
+
17
+
18
+
19
+ ## [1.9.218] - 2026-09-21
20
+
21
+ ### Performance
22
+
23
+ - borrowed string-value for value-of select='.' (#682) (xslt)
24
+
25
+
26
+
27
+ ## [1.9.217] - 2026-09-21
28
+
29
+ ### Performance
30
+
31
+ - tag-id classifier stack + single-lookup start tags (#1218) (html)
32
+
33
+
34
+
35
+ ## [1.9.217] - 2026-09-21
36
+
37
+ ### Added — plan-ABI additive v1 (descriptor materialization, PR #1275)
38
+
39
+ Closes #1272, #1273, #1269 — the binding-facing plan upgrades:
40
+
41
+ - **#1272 attribute-predicate rows**: child rows and attribute plans
42
+ accept `leptris_attr_predicate` pairs; same-wire siblings partition
43
+ by predicate (AND across pairs, exclusive routing only when
44
+ predicates are present — the #1115 multi-namespace behavior is
45
+ untouched).
46
+ - **#1273 document-order identity**: every result value carries an
47
+ `order_index` cumulative across rows (strictly increasing in
48
+ document order); `LEPTRIS_PLAN_FLAG_EMIT_ORDER_SPINE` optionally
49
+ appends unmatched text runs so interleavings reconstruct without
50
+ fragment re-parses.
51
+ - **#1269 in-pass types + fusion**: rows carry a `type_tag`
52
+ (1=int / 2=float / 3=bool) executed during the walk with soft-fail
53
+ to string; `leptris_plan_materialize` fuses parse→walk→free into
54
+ one call for the binding hot path.
55
+ - New accessors: `leptris_plan_value_node_kind/_order_index/_int/
56
+ _float/_bool`. `LEPTRIS_PLAN_ABI_VERSION` stays 1 — all additions
57
+ are trailing fields, aggregate initializers keep compiling.
58
+
59
+ Five new specs in `test/abi/test_descriptor.cpp` (Plan1272/1273/
60
+ 1269a/1269b); full ctest green across all 24 CI legs.
61
+
62
+
3
63
  ## [1.9.216] - 2026-09-21
4
64
 
5
65
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.216
6
+ VERSION 1.9.220
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -1,6 +1,16 @@
1
1
  # Lever 7: Lane 16.6 — leptris validate CLI + bindings
2
2
 
3
- Status: TODO. `leptris validate` subcommand (XML well-formedness
4
- + DTD + RNG paths through the public API) and the binding-side
5
- validate wrappers. Small, closed scope; CLI command registration
6
- follows cli/CLI_ARCHITECTURE.md (new file in cli/commands/).
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
  }
@@ -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
- len += strlen(content);
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
- size_t clen = strlen(content);
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 (&amp;, &lt;, etc.) and
107
117
  * numeric character references (&#65;, &#x42;) into a new
@@ -2121,17 +2121,8 @@ static int h_isalnum(char c) {
2121
2121
  return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
2122
2122
  (c >= '0' && c <= '9');
2123
2123
  }
2124
- /* Void elements: never take children; their start tag is complete. */
2125
- static const char* const k_void[] = {
2126
- "area", "base", "basefont", "bgsound", "br", "col", "embed",
2127
- "frame", "hr", "img", "input", "keygen", "link", "meta", "param",
2128
- "source", "track", "wbr", NULL,
2129
- };
2130
- static int h_is_void(const char* name) {
2131
- for (int i = 0; k_void[i]; i++)
2132
- if (strcmp(name, k_void[i]) == 0) return 1;
2133
- return 0;
2134
- }
2124
+ /* Void / frameset-ok-clear classifications live in HTagInfo.
2125
+ * fo_void (bit0 void, bit1 frameset-ok clear) — see h_tag_infos. */
2135
2126
 
2136
2127
  /* Raw-text elements: content runs to the case-insensitive close
2137
2128
  * tag, no markup inside. script/style take entities verbatim. */
@@ -2197,6 +2188,10 @@ typedef struct {
2197
2188
  uint8_t closes; /* bit0: h_p_closes implied-end set; bit1:
2198
2189
  * k_p_closers — replaced the per-close-tag
2199
2190
  * strcmp scans (#1218 profile). */
2191
+ uint8_t fo_void; /* bit0: void element; bit1: start tag
2192
+ * clears frameset-ok (13.2.5.4.4) —
2193
+ * replaced the k_void/k_fo_clear strcmp
2194
+ * scans (#1218 profile). */
2200
2195
  } HTagInfo;
2201
2196
 
2202
2197
  static const HTagInfo* h_tag_lookup(const char* n);
@@ -2316,6 +2311,21 @@ static char* h_decode_ex(LeptrisMemoryPool* pool, const char* s,
2316
2311
  * returned length is the compacted one; the string is NUL-free). */
2317
2312
  static char* h_decode_body(LeptrisMemoryPool* pool, const char* s,
2318
2313
  const char* e, int whatwg, size_t* out_len) {
2314
+ size_t len = (size_t)(e - s);
2315
+ /* #1218: entity-free, NUL-free runs — the dominant case on
2316
+ * well-formed pages — skip the entity state machine and the
2317
+ * NUL-strip pass: one bump alloc + memcpy. Identical output
2318
+ * (decode_ex passes non-'&' bytes verbatim; the strip is a
2319
+ * no-op without NULs). */
2320
+ if (len && !memchr(s, '&', len) && !memchr(s, '\0', len)) {
2321
+ char* fast = (char*)leptris_pool_alloc(pool, len + 1);
2322
+ if (!fast) return NULL;
2323
+ memcpy(fast, s, len);
2324
+ fast[len] = 0;
2325
+ if (out_len) *out_len = len;
2326
+ return fast;
2327
+ }
2328
+ {
2319
2329
  size_t n = 0;
2320
2330
  char* d = h_decode_ex(pool, s, e, 0, whatwg, &n);
2321
2331
  if (!d) return NULL;
@@ -2327,6 +2337,7 @@ static char* h_decode_body(LeptrisMemoryPool* pool, const char* s,
2327
2337
  d[w] = 0;
2328
2338
  if (out_len) *out_len = w;
2329
2339
  return d;
2340
+ }
2330
2341
  }
2331
2342
  static size_t h_utf8_encode(uint32_t cp, char* out) {
2332
2343
  if (cp < 0x80) { out[0] = (char)cp; return 1; }
@@ -2725,6 +2736,15 @@ typedef struct {
2725
2736
  * whole child chain, which is quadratic on table/text-heavy
2726
2737
  * pages. Slots are refreshed on push (NULL) and by hb_put. */
2727
2738
  LeptrisNodeRef open_tail[256];
2739
+ /* #1218 slice 2: the doc-owned input copy — borrowed text
2740
+ * nodes point into it. */
2741
+ char* owned;
2742
+ /* #1218: per-slot tag id (h_tag_infos index, 255 unknown) —
2743
+ * the in-select / template-fence / table-context stack walks
2744
+ * become integer compares instead of per-element name fetch +
2745
+ * strcmp chains. Set at push; pops truncate depth, so the
2746
+ * parallel array needs no pop logic. */
2747
+ uint8_t open_id[256];
2728
2748
  size_t depth;
2729
2749
  LeptrisElement root; /* first top-level element */
2730
2750
  LeptrisNodeRef top_head; /* top-level chain: text, comments, root */
@@ -3064,90 +3084,90 @@ static int h_is_formatting(const char* n) {
3064
3084
  * no-formatting sets. */
3065
3085
 
3066
3086
  static const HTagInfo h_tag_infos[] = {
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},
3087
+ {"address", 7, 1, 1, 3, 0},
3088
+ {"applet", 6, 1, 0, 0, 2},
3089
+ {"area", 4, 1, 0, 0, 3},
3090
+ {"article", 7, 1, 1, 3, 0},
3091
+ {"aside", 5, 1, 1, 3, 0},
3092
+ {"base", 4, 1, 1, 0, 1},
3093
+ {"basefont", 8, 1, 1, 0, 1},
3094
+ {"bgsound", 7, 1, 1, 0, 1},
3095
+ {"blockquote", 10, 1, 1, 3, 0},
3096
+ {"body", 4, 1, 1, 0, 0},
3097
+ {"br", 2, 1, 0, 0, 3},
3098
+ {"button", 6, 1, 0, 0, 2},
3099
+ {"caption", 7, 1, 1, 0, 0},
3100
+ {"center", 6, 1, 1, 1, 0},
3101
+ {"col", 3, 1, 1, 0, 1},
3102
+ {"colgroup", 8, 1, 1, 0, 0},
3103
+ {"dd", 2, 1, 1, 1, 2},
3104
+ {"details", 7, 1, 1, 3, 0},
3105
+ {"dialog", 6, 1, 1, 3, 0},
3106
+ {"dir", 3, 1, 1, 3, 0},
3107
+ {"div", 3, 1, 1, 3, 0},
3108
+ {"dl", 2, 1, 1, 3, 0},
3109
+ {"dt", 2, 1, 1, 1, 2},
3110
+ {"embed", 5, 1, 0, 0, 3},
3111
+ {"fieldset", 8, 1, 1, 3, 0},
3112
+ {"figcaption", 10, 1, 1, 3, 0},
3113
+ {"figure", 6, 1, 1, 3, 0},
3114
+ {"footer", 6, 1, 1, 3, 0},
3115
+ {"form", 4, 1, 1, 3, 0},
3116
+ {"frame", 5, 1, 1, 0, 1},
3117
+ {"frameset", 8, 1, 1, 0, 0},
3118
+ {"h1", 2, 1, 1, 3, 0},
3119
+ {"h2", 2, 1, 1, 3, 0},
3120
+ {"h3", 2, 1, 1, 3, 0},
3121
+ {"h4", 2, 1, 1, 3, 0},
3122
+ {"h5", 2, 1, 1, 3, 0},
3123
+ {"h6", 2, 1, 1, 3, 0},
3124
+ {"head", 4, 1, 1, 0, 0},
3125
+ {"header", 6, 1, 1, 3, 0},
3126
+ {"hgroup", 6, 1, 1, 3, 0},
3127
+ {"hr", 2, 1, 1, 3, 3},
3128
+ {"html", 4, 1, 1, 0, 0},
3129
+ {"iframe", 6, 1, 0, 0, 2},
3130
+ {"img", 3, 1, 0, 0, 3},
3131
+ {"input", 5, 1, 0, 0, 3},
3132
+ {"keygen", 6, 1, 0, 0, 3},
3133
+ {"li", 2, 1, 1, 3, 2},
3134
+ {"link", 4, 1, 1, 0, 1},
3135
+ {"listing", 7, 1, 1, 1, 2},
3136
+ {"main", 4, 1, 1, 3, 0},
3137
+ {"marquee", 7, 1, 0, 0, 2},
3138
+ {"menu", 4, 1, 1, 3, 0},
3139
+ {"meta", 4, 1, 1, 0, 1},
3140
+ {"nav", 3, 1, 1, 3, 0},
3141
+ {"noembed", 7, 1, 0, 0, 2},
3142
+ {"noframes", 8, 1, 1, 0, 2},
3143
+ {"noscript", 8, 1, 0, 0, 0},
3144
+ {"object", 6, 1, 0, 0, 2},
3145
+ {"ol", 2, 1, 1, 3, 0},
3146
+ {"p", 1, 1, 1, 3, 0},
3147
+ {"param", 5, 1, 0, 0, 1},
3148
+ {"plaintext", 9, 1, 1, 1, 2},
3149
+ {"pre", 3, 1, 1, 3, 2},
3150
+ {"script", 6, 1, 1, 0, 0},
3151
+ {"search", 6, 1, 1, 1, 0},
3152
+ {"section", 7, 1, 1, 3, 0},
3153
+ {"select", 6, 1, 0, 0, 2},
3154
+ {"source", 6, 1, 0, 0, 1},
3155
+ {"style", 5, 1, 1, 0, 0},
3156
+ {"summary", 7, 1, 1, 1, 0},
3157
+ {"table", 5, 1, 1, 2, 2},
3158
+ {"tbody", 5, 1, 1, 0, 0},
3159
+ {"td", 2, 1, 1, 0, 0},
3160
+ {"template", 8, 1, 1, 0, 0},
3161
+ {"textarea", 8, 1, 1, 0, 2},
3162
+ {"tfoot", 5, 1, 1, 0, 0},
3163
+ {"th", 2, 1, 1, 0, 0},
3164
+ {"thead", 5, 1, 1, 0, 0},
3165
+ {"title", 5, 1, 1, 0, 0},
3166
+ {"tr", 2, 1, 1, 0, 0},
3167
+ {"track", 5, 1, 0, 0, 1},
3168
+ {"ul", 2, 1, 1, 3, 0},
3169
+ {"wbr", 3, 1, 0, 0, 3},
3170
+ {"xmp", 3, 1, 0, 1, 2},
3151
3171
  };
3152
3172
 
3153
3173
  /* bucket[first_char] = start index; bucket[first_char+1] = end */
@@ -3157,7 +3177,11 @@ static const uint8_t h_tag_bucket[27] = {
3157
3177
  63, 70, 81, 82, 82, 83, 84, 84, 84,
3158
3178
  };
3159
3179
 
3160
- /* Resolve a (lowercased) tag name's flags; NULL when unknown. */
3180
+ /* Resolve a (lowercased) tag name's flags; NULL when unknown.
3181
+ * strncmp first: it stops at n's terminator, so the n[t->len]
3182
+ * terminator probe below only runs when n really has t->len
3183
+ * matching bytes (the old n[t->len]-first form read past short
3184
+ * names — UB clang exploited under new inlining contexts). */
3161
3185
  static const HTagInfo* h_tag_lookup(const char* n) {
3162
3186
  if (!n) return NULL;
3163
3187
  unsigned char c = (unsigned char)n[0];
@@ -3165,13 +3189,47 @@ static const HTagInfo* h_tag_lookup(const char* n) {
3165
3189
  for (int i = h_tag_bucket[c - 'a'];
3166
3190
  i < h_tag_bucket[c - 'a' + 1]; i++) {
3167
3191
  const HTagInfo* t = &h_tag_infos[i];
3168
- if ((uint8_t)n[t->len] == 0 && t->name[t->len - 1] == n[t->len - 1] &&
3169
- memcmp(n, t->name, t->len) == 0)
3192
+ if (t->name[1] == n[1] &&
3193
+ strncmp(n, t->name, t->len) == 0 &&
3194
+ (uint8_t)n[t->len] == 0)
3170
3195
  return t;
3171
3196
  }
3172
3197
  return NULL;
3173
3198
  }
3174
3199
 
3200
+ /* #1218: tag ids + per-id LUTs. The sampler pinned the strcmp
3201
+ * scans (h_is_void 18 names, h_clears_frameset_ok 26 names) and
3202
+ * the open-stack walks (name fetch + strcmp per element) at the
3203
+ * top of WHATWG self-time on table-heavy pages. ids are
3204
+ * h_tag_infos indexes, so one first-char bucket walk per start
3205
+ * tag serves every later classification of that name. */
3206
+ #define H_ID_UNKNOWN 255u
3207
+ static uint8_t h_tag_id(const char* n) {
3208
+ const HTagInfo* t = h_tag_lookup(n);
3209
+ return t ? (uint8_t)(t - h_tag_infos) : (uint8_t)H_ID_UNKNOWN;
3210
+ }
3211
+
3212
+ static uint8_t h_id_select, h_id_template, h_id_table, h_id_tbody,
3213
+ h_id_thead, h_id_tfoot, h_id_tr;
3214
+
3215
+ static void h_id_luts_init(void) {
3216
+ static int done;
3217
+ if (done) return;
3218
+ done = 1;
3219
+ h_id_select = h_tag_id("select");
3220
+ h_id_template = h_tag_id("template");
3221
+ h_id_table = h_tag_id("table");
3222
+ h_id_tbody = h_tag_id("tbody");
3223
+ h_id_thead = h_tag_id("thead");
3224
+ h_id_tfoot = h_tag_id("tfoot");
3225
+ h_id_tr = h_tag_id("tr");
3226
+ }
3227
+
3228
+ static int h_is_void(const char* name) {
3229
+ const HTagInfo* t = h_tag_lookup(name);
3230
+ return t && (t->fo_void & 1);
3231
+ }
3232
+
3175
3233
  static int h_is_special_ww(const char* n) {
3176
3234
  const HTagInfo* t = h_tag_lookup(n);
3177
3235
  return t && t->special;
@@ -3287,16 +3345,11 @@ static char* h_decode_foreign(LeptrisMemoryPool* pool, const char* s,
3287
3345
  }
3288
3346
 
3289
3347
 
3290
- /* 13.2.5.4.4: start tags that set frameset-ok to false. */
3348
+ /* 13.2.5.4.4: start tags that set frameset-ok to false — HTagInfo
3349
+ * bit (see h_tag_infos). */
3291
3350
  static int h_clears_frameset_ok(const char* n) {
3292
- static const char* const yes[] = {
3293
- "pre", "listing", "li", "dd", "dt", "plaintext", "button",
3294
- "applet", "marquee", "object", "table", "area", "br",
3295
- "embed", "img", "keygen", "wbr", "input", "hr", "textarea",
3296
- "xmp", "iframe", "noembed", "noframes", "select", NULL};
3297
- for (int i = 0; yes[i]; i++)
3298
- if (strcmp(n, yes[i]) == 0) return 1;
3299
- return 0;
3351
+ const HTagInfo* t = h_tag_lookup(n);
3352
+ return t && (t->fo_void & 2);
3300
3353
  }
3301
3354
 
3302
3355
  /* 13.2.5.4.4 exception: <input> clears frameset-ok UNLESS its type
@@ -3552,22 +3605,20 @@ static int h_cur_ns(HBuilder* b) {
3552
3605
 
3553
3606
  /* An open <select> swallows svg/math start tags (in-select mode
3554
3607
  * ignores unknown start tags; the html4 entry keeps libxml2's
3555
- * keep-everything shape). */
3608
+ * keep-everything shape). #1218: open_id integer walk. */
3556
3609
  static int h_in_select(HBuilder* b) {
3557
- for (size_t i = b->depth; i > 0; i--) {
3558
- const char* on = leptris_element_name(b->open[i - 1]);
3559
- if (on && strcmp(on, "select") == 0) return 1;
3560
- }
3610
+ h_id_luts_init();
3611
+ for (size_t i = b->depth; i > 0; i--)
3612
+ if (b->open_id[i - 1] == h_id_select) return 1;
3561
3613
  return 0;
3562
3614
  }
3563
3615
 
3564
3616
  /* #659: index of the nearest open <template> on the stack, or
3565
3617
  * -1. The in-template insertion rules fence on it. */
3566
3618
  static int h_template_idx(HBuilder* b) {
3567
- for (size_t i = b->depth; i > 0; i--) {
3568
- const char* n = leptris_element_name(b->open[i - 1]);
3569
- if (n && h_ieq_raw(n, "template")) return (int)(i - 1);
3570
- }
3619
+ h_id_luts_init();
3620
+ for (size_t i = b->depth; i > 0; i--)
3621
+ if (b->open_id[i - 1] == h_id_template) return (int)(i - 1);
3571
3622
  return -1;
3572
3623
  }
3573
3624
 
@@ -3713,6 +3764,15 @@ static int h_is_table_context(LeptrisElement e) {
3713
3764
  h_ieq_raw(n, "tr");
3714
3765
  }
3715
3766
 
3767
+ /* #1218: stack-slot flavor — integer compare on the push-time id
3768
+ * (identical membership: ids were resolved from the same stored
3769
+ * lowercase names). */
3770
+ static int h_id_table_ctx(uint8_t id) {
3771
+ h_id_luts_init();
3772
+ return id == h_id_table || id == h_id_tbody ||
3773
+ id == h_id_thead || id == h_id_tfoot || id == h_id_tr;
3774
+ }
3775
+
3716
3776
  /* Insert n into parent's child chain BEFORE `before`. Same surgery
3717
3777
  * pattern as the head-content lift (first_child + sibling links +
3718
3778
  * element child_count). */
@@ -3944,7 +4004,7 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
3944
4004
  * table" insertion mode - ordinary content still
3945
4005
  * fosters, hidden inputs insert at spot
3946
4006
  * (html5test-com:20). */
3947
- int tbl_ctx = h_is_table_context(top);
4007
+ int tbl_ctx = h_id_table_ctx(b->open_id[b->depth - 1]);
3948
4008
  if (!tbl_ctx && b->depth >= 2 &&
3949
4009
  b->open_ns[b->depth - 1] == H_NS_HTML &&
3950
4010
  h_ieq_raw(leptris_element_name(top), "form") &&
@@ -3969,7 +4029,7 @@ static void h_append(HBuilder* b, LeptrisNodeRef n) {
3969
4029
  int oi = -1;
3970
4030
  for (int k = (int)b->depth - 1; k >= 0; k--)
3971
4031
  if (b->open_ns[k] == H_NS_HTML &&
3972
- h_is_table_context(b->open[k])) {
4032
+ h_id_table_ctx(b->open_id[k])) {
3973
4033
  oi = k;
3974
4034
  }
3975
4035
  if (oi >= 0) {
@@ -4102,12 +4162,16 @@ static LeptrisElement h_open_foreign(HBuilder* b, const char* name,
4102
4162
  b->open[b->depth] = e;
4103
4163
  b->open_tail[b->depth] = NULL;
4104
4164
  b->open_ns[b->depth] = (uint8_t)ns;
4165
+ b->open_id[b->depth] = h_tag_id(store);
4105
4166
  b->depth++;
4106
4167
  }
4107
4168
  return e;
4108
4169
  }
4109
4170
 
4110
- static LeptrisElement h_open_element(HBuilder* b, const char* name) {
4171
+ /* Hot-path variant (#1218): the start-tag section resolves the
4172
+ * HTagInfo once per token and passes the id down. */
4173
+ static LeptrisElement h_open_element_id(HBuilder* b, const char* name,
4174
+ uint8_t tid) {
4111
4175
  LeptrisStringView nv = leptris_sv_from_cstr(name);
4112
4176
  LeptrisElement e = leptris_element_create_with_view(nv, b->pool);
4113
4177
  if (!e) return NULL;
@@ -4140,11 +4204,16 @@ static LeptrisElement h_open_element(HBuilder* b, const char* name) {
4140
4204
  b->open[b->depth] = e;
4141
4205
  b->open_tail[b->depth] = NULL;
4142
4206
  b->open_ns[b->depth] = H_NS_HTML;
4207
+ b->open_id[b->depth] = tid;
4143
4208
  b->depth++;
4144
4209
  }
4145
4210
  return e;
4146
4211
  }
4147
4212
 
4213
+ static LeptrisElement h_open_element(HBuilder* b, const char* name) {
4214
+ return h_open_element_id(b, name, h_tag_id(name));
4215
+ }
4216
+
4148
4217
  /* Case-insensitive C-string compare (ASCII). */
4149
4218
  static int h_ieq_raw(const char* a, const char* bname) {
4150
4219
  if (!a) return 0;
@@ -4341,6 +4410,7 @@ static LeptrisElement h_afe_open_clone(HBuilder* b, LeptrisElement src) {
4341
4410
  b->open[b->depth] = c;
4342
4411
  b->open_tail[b->depth] = NULL;
4343
4412
  b->open_ns[b->depth] = H_NS_HTML;
4413
+ b->open_id[b->depth] = h_tag_id(leptris_element_name(c));
4344
4414
  b->depth++;
4345
4415
  }
4346
4416
  return c;
@@ -4624,6 +4694,7 @@ static LeptrisElement h_open_named(HBuilder* b, const char* name,
4624
4694
  b->open[b->depth] = e;
4625
4695
  b->open_tail[b->depth] = NULL;
4626
4696
  b->open_ns[b->depth] = H_NS_HTML;
4697
+ b->open_id[b->depth] = h_tag_id(name);
4627
4698
  b->depth++;
4628
4699
  }
4629
4700
  return e;
@@ -5303,6 +5374,43 @@ head_end = leptris_node_get_next_sibling(head_end);
5303
5374
  }
5304
5375
 
5305
5376
  /* ---- tokenizer ---- */
5377
+ /* #1218 slice 2: build a text node for the run [s, e). Clean runs
5378
+ * (no '&', no NUL) borrow the doc-owned input copy in place — the
5379
+ * terminator byte (the '<' the dispatcher already consumed, or the
5380
+ * buffer's own EOF NUL) becomes the content terminator: zero
5381
+ * copies. Entity- or NUL-bearing runs take the decode path. */
5382
+ static LeptrisTextNode* h_text_node(HBuilder* b, const char* s,
5383
+ const char* e) {
5384
+ size_t len = (size_t)(e - s);
5385
+ if (b->owned && len && !memchr(s, '&', len) &&
5386
+ !memchr(s, '\0', len)) {
5387
+ /* 13.2.6.4.7: non-whitespace text clears frameset-ok —
5388
+ * the decode path scans the decoded string; the borrow
5389
+ * path scans the raw run (identical for entity-free
5390
+ * runs; FFFD in body counts as non-ws, :21). */
5391
+ if (b->whatwg && b->frameset_ok) {
5392
+ for (const char* c = s; c < e; c++) {
5393
+ if ((unsigned char)c[0] == 0xEF && c + 2 < e &&
5394
+ (unsigned char)c[1] == 0xBF &&
5395
+ (unsigned char)c[2] == 0xBD) {
5396
+ b->frameset_ok = 0;
5397
+ break;
5398
+ }
5399
+ if (!h_is_ws(*c)) {
5400
+ b->frameset_ok = 0;
5401
+ break;
5402
+ }
5403
+ }
5404
+ }
5405
+ *(char*)e = '\0';
5406
+ return leptris_text_create_borrowed(s, len, b->pool);
5407
+ }
5408
+ size_t dlen = 0;
5409
+ char* dec = h_decode_text(b, s, e, &dlen);
5410
+ if (!dec || !*dec) return NULL;
5411
+ return leptris_text_create(dec, dlen, b->pool);
5412
+ }
5413
+
5306
5414
  static LeptrisDocument html_parse_shared(
5307
5415
  const char* buf, size_t len, LeptrisStatus* status, int whatwg) {
5308
5416
  if (status) *status = LEPTRIS_OK;
@@ -5325,9 +5433,23 @@ static LeptrisDocument html_parse_shared(
5325
5433
  b.whatwg_head_set = whatwg;
5326
5434
  b.whatwg_foster = whatwg;
5327
5435
  b.whatwg_adopt = whatwg;
5436
+ /* #1218 slice 2: one copy of the input into document memory.
5437
+ * Clean text runs borrow this copy in place (the terminator
5438
+ * byte becomes their NUL) instead of a pool alloc + memcpy
5439
+ * per text node — the pugi-style in-place model. */
5440
+ char* owned = (char*)leptris_pool_alloc(b.pool, len + 1);
5441
+ if (!owned) {
5442
+ if (status) *status = LEPTRIS_ERROR_MEMORY;
5443
+ leptris_document_free(doc);
5444
+ return NULL;
5445
+ }
5446
+ memcpy(owned, buf, len);
5447
+ owned[len] = '\0';
5448
+ b.owned = owned;
5449
+ h_id_luts_init();
5328
5450
 
5329
- const char* p = buf;
5330
- const char* end = buf + len;
5451
+ const char* p = owned;
5452
+ const char* end = owned + len;
5331
5453
  const char* text = p; /* pending text run start */
5332
5454
 
5333
5455
  while (p < end) {
@@ -5345,14 +5467,10 @@ static LeptrisDocument html_parse_shared(
5345
5467
  /* Flush pending text first. */
5346
5468
  if (text < p) {
5347
5469
  size_t dlen = 0;
5348
- char* dec = h_decode_text(&b, text, p, &dlen);
5349
- if (dec && *dec) {
5350
- LeptrisTextNode* t = leptris_text_create(
5351
- dec, dlen, b.pool);
5352
- if (t) {
5353
- h_reconstruct(&b);
5354
- h_append(&b, (LeptrisNodeRef)t);
5355
- }
5470
+ LeptrisTextNode* t = h_text_node(&b, text, p);
5471
+ if (t) {
5472
+ h_reconstruct(&b);
5473
+ h_append(&b, (LeptrisNodeRef)t);
5356
5474
  }
5357
5475
  }
5358
5476
  if (p + 3 < end && p[2] == '-' && p[3] == '-') {
@@ -5677,13 +5795,9 @@ static LeptrisDocument html_parse_shared(
5677
5795
  const char* ns2 = p + 2;
5678
5796
  if (b.whatwg && ns2 >= end) {
5679
5797
  if (text < p) {
5680
- size_t dlen = 0;
5681
- char* dec = h_decode_text(&b, text, p, &dlen);
5682
- if (dec && *dec) {
5683
- LeptrisTextNode* t = leptris_text_create(
5684
- dec, dlen, b.pool);
5685
- if (t) h_append(&b, (LeptrisNodeRef)t);
5686
- }
5798
+ LeptrisTextNode* t =
5799
+ h_text_node(&b, text, p);
5800
+ if (t) h_append(&b, (LeptrisNodeRef)t);
5687
5801
  }
5688
5802
  LeptrisTextNode* t =
5689
5803
  leptris_text_create("</", 2, b.pool);
@@ -5695,13 +5809,9 @@ static LeptrisDocument html_parse_shared(
5695
5809
  if (b.whatwg && !((*ns2 >= 'a' && *ns2 <= 'z') ||
5696
5810
  (*ns2 >= 'A' && *ns2 <= 'Z'))) {
5697
5811
  if (text < p) {
5698
- size_t dlen = 0;
5699
- char* dec = h_decode_text(&b, text, p, &dlen);
5700
- if (dec && *dec) {
5701
- LeptrisTextNode* t = leptris_text_create(
5702
- dec, dlen, b.pool);
5703
- if (t) h_append(&b, (LeptrisNodeRef)t);
5704
- }
5812
+ LeptrisTextNode* t =
5813
+ h_text_node(&b, text, p);
5814
+ if (t) h_append(&b, (LeptrisNodeRef)t);
5705
5815
  }
5706
5816
  const char* q2 = ns2;
5707
5817
  while (q2 < end && *q2 != '>') q2++;
@@ -5715,13 +5825,8 @@ static LeptrisDocument html_parse_shared(
5715
5825
  * dropped too (same 13.2.5.44 rule). */
5716
5826
  if (b.whatwg && !memchr(p, '>', (size_t)(end - p))) {
5717
5827
  if (text < p) {
5718
- size_t dlen = 0;
5719
- char* dec = h_decode_text(&b, text, p, &dlen);
5720
- if (dec && *dec) {
5721
- LeptrisTextNode* t =
5722
- leptris_text_create(dec, dlen, b.pool);
5723
- if (t) h_append(&b, (LeptrisNodeRef)t);
5724
- }
5828
+ LeptrisTextNode* t = h_text_node(&b, text, p);
5829
+ if (t) h_append(&b, (LeptrisNodeRef)t);
5725
5830
  }
5726
5831
  p = end;
5727
5832
  text = p;
@@ -5746,26 +5851,22 @@ static LeptrisDocument html_parse_shared(
5746
5851
  * whitespace-only run in a table context skips the
5747
5852
  * reconstruct (13.2.6.4.9 - tricky01:6). */
5748
5853
  if (text < p) {
5749
- size_t dlen = 0;
5750
- char* dec = h_decode_text(&b, text, p, &dlen);
5751
- if (dec && *dec) {
5854
+ LeptrisTextNode* t = h_text_node(&b, text, p);
5855
+ if (t) {
5856
+ const char* c = leptris_text_get_content(t);
5752
5857
  int tbl_ws = b.whatwg_foster &&
5753
5858
  b.depth > 0 &&
5754
- h_is_table_context(
5755
- b.open[b.depth - 1]);
5859
+ h_id_table_ctx(
5860
+ b.open_id[b.depth - 1]);
5756
5861
  if (tbl_ws)
5757
- for (const char* w2 = dec; *w2; w2++)
5862
+ for (const char* w2 = c; *w2; w2++)
5758
5863
  if (*w2 != ' ' && *w2 != '\t' &&
5759
5864
  *w2 != '\n' && *w2 != '\r') {
5760
5865
  tbl_ws = 0;
5761
5866
  break;
5762
5867
  }
5763
- LeptrisTextNode* t = leptris_text_create(
5764
- dec, dlen, b.pool);
5765
- if (t) {
5766
- if (!tbl_ws) h_reconstruct(&b);
5767
- h_append(&b, (LeptrisNodeRef)t);
5768
- }
5868
+ if (!tbl_ws) h_reconstruct(&b);
5869
+ h_append(&b, (LeptrisNodeRef)t);
5769
5870
  }
5770
5871
  }
5771
5872
  /* #659 tests26:17-20 - an HTML end tag with the foreign
@@ -6106,17 +6207,11 @@ static LeptrisDocument html_parse_shared(
6106
6207
  if (!head_found2 && !b.head_tag_seen &&
6107
6208
  !b.body_seen && !b.frameset) {
6108
6209
  if (text < p) {
6109
- size_t dlen = 0;
6110
- char* dec =
6111
- h_decode_text(&b, text, p, &dlen);
6112
- if (dec && *dec) {
6113
- LeptrisTextNode* t =
6114
- leptris_text_create(
6115
- dec, dlen, b.pool);
6116
- if (t)
6117
- h_append(&b,
6118
- (LeptrisNodeRef)t);
6119
- }
6210
+ LeptrisTextNode* t =
6211
+ h_text_node(&b, text, p);
6212
+ if (t)
6213
+ h_append(&b,
6214
+ (LeptrisNodeRef)t);
6120
6215
  }
6121
6216
  b.head_tag_seen = 1;
6122
6217
  b.head_end_seen = 1;
@@ -6446,14 +6541,10 @@ static LeptrisDocument html_parse_shared(
6446
6541
  * trailing '?' — content runs to the first '>'. */
6447
6542
  if (text < p) {
6448
6543
  size_t dlen = 0;
6449
- char* dec = h_decode_text(&b, text, p, &dlen);
6450
- if (dec && *dec) {
6451
- LeptrisTextNode* t = leptris_text_create(
6452
- dec, dlen, b.pool);
6453
- if (t) {
6454
- h_reconstruct(&b);
6455
- h_append(&b, (LeptrisNodeRef)t);
6456
- }
6544
+ LeptrisTextNode* t = h_text_node(&b, text, p);
6545
+ if (t) {
6546
+ h_reconstruct(&b);
6547
+ h_append(&b, (LeptrisNodeRef)t);
6457
6548
  }
6458
6549
  }
6459
6550
  const char* ts = p + 2; /* skip "<?" */
@@ -6531,13 +6622,8 @@ static LeptrisDocument html_parse_shared(
6531
6622
  * parsing ends (webkit01:4: '<di' -> empty body). */
6532
6623
  if (b.whatwg && !memchr(p, '>', (size_t)(end - p))) {
6533
6624
  if (text < p) {
6534
- size_t dlen = 0;
6535
- char* dec = h_decode_text(&b, text, p, &dlen);
6536
- if (dec && *dec) {
6537
- LeptrisTextNode* t =
6538
- leptris_text_create(dec, dlen, b.pool);
6539
- if (t) h_append(&b, (LeptrisNodeRef)t);
6540
- }
6625
+ LeptrisTextNode* t = h_text_node(&b, text, p);
6626
+ if (t) h_append(&b, (LeptrisNodeRef)t);
6541
6627
  }
6542
6628
  p = end;
6543
6629
  text = p;
@@ -6576,13 +6662,9 @@ static LeptrisDocument html_parse_shared(
6576
6662
  }
6577
6663
  if (!closed) {
6578
6664
  if (text < p) {
6579
- size_t dlen = 0;
6580
- char* dec = h_decode_text(&b, text, p, &dlen);
6581
- if (dec && *dec) {
6582
- LeptrisTextNode* t = leptris_text_create(
6583
- dec, dlen, b.pool);
6584
- if (t) h_append(&b, (LeptrisNodeRef)t);
6585
- }
6665
+ LeptrisTextNode* t =
6666
+ h_text_node(&b, text, p);
6667
+ if (t) h_append(&b, (LeptrisNodeRef)t);
6586
6668
  }
6587
6669
  p = end;
6588
6670
  text = p;
@@ -6598,12 +6680,19 @@ static LeptrisDocument html_parse_shared(
6598
6680
  * reprocessed (tests1:90). */
6599
6681
  if (b.whatwg && strcmp(name, "image") == 0)
6600
6682
  name = h_pooled_lower(b.pool, "img", 3);
6683
+ /* #1218: ONE classification per start tag — every
6684
+ * downstream consumer (frameset-ok, void, open) reads the
6685
+ * resolved entry instead of re-walking the bucket. */
6686
+ const HTagInfo* tinfo = h_tag_lookup(name);
6687
+ uint8_t tid = tinfo ? (uint8_t)(tinfo - h_tag_infos)
6688
+ : (uint8_t)H_ID_UNKNOWN;
6601
6689
  /* 13.2.5.4.4: the enumerated start tags clear frameset-ok;
6602
6690
  * <input type=hidden> does not (webkit01:51). */
6603
6691
  b.input_hidden_tag =
6604
6692
  b.whatwg && nlen == 5 && strcmp(name, "input") == 0 &&
6605
6693
  h_input_type_hidden(q, end);
6606
- if (b.whatwg && b.frameset_ok && h_clears_frameset_ok(name) &&
6694
+ if (b.whatwg && b.frameset_ok && tinfo &&
6695
+ (tinfo->fo_void & 2) &&
6607
6696
  !(nlen == 5 && strcmp(name, "input") == 0 &&
6608
6697
  h_input_type_hidden(q, end))) {
6609
6698
  b.frameset_ok = 0;
@@ -6676,13 +6765,8 @@ static LeptrisDocument html_parse_shared(
6676
6765
  * after-</head> whitespace must land on the chain to
6677
6766
  * become an html child (webkit01:35). */
6678
6767
  if (text < p) {
6679
- size_t dlen = 0;
6680
- char* dec = h_decode_text(&b, text, p, &dlen);
6681
- if (dec && *dec) {
6682
- LeptrisTextNode* t =
6683
- leptris_text_create(dec, dlen, b.pool);
6684
- if (t) h_append(&b, (LeptrisNodeRef)t);
6685
- }
6768
+ LeptrisTextNode* t = h_text_node(&b, text, p);
6769
+ if (t) h_append(&b, (LeptrisNodeRef)t);
6686
6770
  }
6687
6771
  if (strcmp(name, "body") == 0) {
6688
6772
  b.body_tag_seen = 1;
@@ -6750,13 +6834,9 @@ static LeptrisDocument html_parse_shared(
6750
6834
  } else if (!(b.frameset && b.depth > 0)) {
6751
6835
  /* Dropped token: flush pending text first. */
6752
6836
  if (text < p) {
6753
- size_t dlen = 0;
6754
- char* dec = h_decode_text(&b, text, p, &dlen);
6755
- if (dec && *dec) {
6756
- LeptrisTextNode* t = leptris_text_create(
6757
- dec, dlen, b.pool);
6758
- if (t) h_append(&b, (LeptrisNodeRef)t);
6759
- }
6837
+ LeptrisTextNode* t =
6838
+ h_text_node(&b, text, p);
6839
+ if (t) h_append(&b, (LeptrisNodeRef)t);
6760
6840
  }
6761
6841
  while (q < end && *q != '>') q++;
6762
6842
  p = (q < end) ? q + 1 : end;
@@ -6782,13 +6862,8 @@ static LeptrisDocument html_parse_shared(
6782
6862
  * (html5lib template.dat:64-67 — attrs do NOT merge
6783
6863
  * onto the outer elements). */
6784
6864
  if (text < p) {
6785
- size_t dlen = 0;
6786
- char* dec = h_decode_text(&b, text, p, &dlen);
6787
- if (dec && *dec) {
6788
- LeptrisTextNode* t = leptris_text_create(
6789
- dec, dlen, b.pool);
6790
- if (t) h_append(&b, (LeptrisNodeRef)t);
6791
- }
6865
+ LeptrisTextNode* t = h_text_node(&b, text, p);
6866
+ if (t) h_append(&b, (LeptrisNodeRef)t);
6792
6867
  }
6793
6868
  while (q < end && *q != '>') q++;
6794
6869
  p = (q < end) ? q + 1 : end;
@@ -6808,21 +6883,16 @@ static LeptrisDocument html_parse_shared(
6808
6883
  strcmp(name, "noframes") != 0 &&
6809
6884
  strcmp(name, "html") != 0) {
6810
6885
  if (text < p) {
6811
- size_t dlen = 0;
6812
- char* dec = h_decode_text(&b, text, p, &dlen);
6813
- if (dec && *dec) {
6886
+ LeptrisTextNode* t = h_text_node(&b, text, p);
6887
+ if (t) {
6888
+ const char* c = leptris_text_get_content(t);
6814
6889
  int ws = 1;
6815
- for (const char* q2 = dec; *q2; q2++)
6890
+ for (const char* q2 = c; *q2; q2++)
6816
6891
  if (!h_is_ws(*q2)) {
6817
6892
  ws = 0;
6818
6893
  break;
6819
6894
  }
6820
- if (ws) {
6821
- LeptrisTextNode* t = leptris_text_create(
6822
- dec, dlen, b.pool);
6823
- if (t)
6824
- h_append(&b, (LeptrisNodeRef)t);
6825
- }
6895
+ if (ws) h_append(&b, (LeptrisNodeRef)t);
6826
6896
  }
6827
6897
  }
6828
6898
  while (q < end && *q != '>') q++;
@@ -6833,31 +6903,27 @@ static LeptrisDocument html_parse_shared(
6833
6903
 
6834
6904
  /* Flush pending text before the element. */
6835
6905
  if (text < p) {
6836
- size_t dlen = 0;
6837
- char* dec = h_decode_text(&b, text, p, &dlen);
6838
- if (dec && *dec) {
6906
+ LeptrisTextNode* t = h_text_node(&b, text, p);
6907
+ if (t) {
6839
6908
  /* 13.2.6.4.9: a WHITESPACE-ONLY run in a table
6840
6909
  * context inserts into the current node WITHOUT
6841
6910
  * the in-body reconstruct - no formatting clone
6842
6911
  * wraps it (tricky01:6: the ws between
6843
6912
  * </center> and <img> goes into the table). */
6913
+ const char* c = leptris_text_get_content(t);
6844
6914
  int tbl_ws = b.whatwg_foster && b.depth > 0 &&
6845
- h_is_table_context(
6846
- b.open[b.depth - 1]);
6915
+ h_id_table_ctx(
6916
+ b.open_id[b.depth - 1]);
6847
6917
  if (tbl_ws) {
6848
- for (const char* w2 = dec; *w2; w2++)
6918
+ for (const char* w2 = c; *w2; w2++)
6849
6919
  if (*w2 != ' ' && *w2 != '\t' &&
6850
6920
  *w2 != '\n' && *w2 != '\r') {
6851
6921
  tbl_ws = 0;
6852
6922
  break;
6853
6923
  }
6854
6924
  }
6855
- LeptrisTextNode* t =
6856
- leptris_text_create(dec, dlen, b.pool);
6857
- if (t) {
6858
- if (!tbl_ws) h_reconstruct(&b);
6859
- h_append(&b, (LeptrisNodeRef)t);
6860
- }
6925
+ if (!tbl_ws) h_reconstruct(&b);
6926
+ h_append(&b, (LeptrisNodeRef)t);
6861
6927
  }
6862
6928
  }
6863
6929
 
@@ -7015,13 +7081,8 @@ static LeptrisDocument html_parse_shared(
7015
7081
  * template.dat:64-67) and in-frameset. */
7016
7082
  if (b.whatwg && strcmp(name, "html") == 0 && b.html_seen) {
7017
7083
  if (text < p) {
7018
- size_t dlen = 0;
7019
- char* dec = h_decode_text(&b, text, p, &dlen);
7020
- if (dec && *dec) {
7021
- LeptrisTextNode* t = leptris_text_create(dec, dlen,
7022
- b.pool);
7023
- if (t) h_append(&b, (LeptrisNodeRef)t);
7024
- }
7084
+ LeptrisTextNode* t = h_text_node(&b, text, p);
7085
+ if (t) h_append(&b, (LeptrisNodeRef)t);
7025
7086
  }
7026
7087
  h_stash_attrs(&b, q, end, b.html_attrs, &b.html_attr_n);
7027
7088
  while (q < end && *q != '>') q++;
@@ -7367,13 +7428,8 @@ static LeptrisDocument html_parse_shared(
7367
7428
  }
7368
7429
  if (tmpl_act == 1) {
7369
7430
  if (text < p) {
7370
- size_t dlen = 0;
7371
- char* dec = h_decode_text(&b, text, p, &dlen);
7372
- if (dec && *dec) {
7373
- LeptrisTextNode* t = leptris_text_create(
7374
- dec, dlen, b.pool);
7375
- if (t) h_append(&b, (LeptrisNodeRef)t);
7376
- }
7431
+ LeptrisTextNode* t = h_text_node(&b, text, p);
7432
+ if (t) h_append(&b, (LeptrisNodeRef)t);
7377
7433
  }
7378
7434
  while (q < end && *q != '>') q++;
7379
7435
  p = (q < end) ? q + 1 : end;
@@ -7640,7 +7696,7 @@ static LeptrisDocument html_parse_shared(
7640
7696
 
7641
7697
  LeptrisElement e = (elem_ns != H_NS_HTML)
7642
7698
  ? h_open_foreign(&b, name, elem_ns)
7643
- : h_open_element(&b, name);
7699
+ : h_open_element_id(&b, name, tid);
7644
7700
  if (!e) goto done;
7645
7701
  if (strcmp(name, "html") == 0) {
7646
7702
  b.html_seen = 1;
@@ -7867,7 +7923,7 @@ static LeptrisDocument html_parse_shared(
7867
7923
  * their entry; applet/object/marquee/td/th/caption open a
7868
7924
  * marker scope (13.2.4.3). */
7869
7925
  if (b.whatwg_adopt && elem_ns == H_NS_HTML &&
7870
- !(self_closing || h_is_void(name))) {
7926
+ !(self_closing || (tinfo && (tinfo->fo_void & 1)))) {
7871
7927
  if (h_is_formatting(name)) {
7872
7928
  h_afe_push(&b, e);
7873
7929
  } else if (strcmp(name, "applet") == 0 ||
@@ -7884,7 +7940,7 @@ static LeptrisDocument html_parse_shared(
7884
7940
  * HTML elements (parse error, div stays open -
7885
7941
  * webkit01:46). Only foreign self-closing elements pop.
7886
7942
  * html4 keeps libxml2's honored-slash shape. */
7887
- if (h_is_void(name) ||
7943
+ if ((tinfo && (tinfo->fo_void & 1)) ||
7888
7944
  (self_closing &&
7889
7945
  (elem_ns != H_NS_HTML || !b.whatwg)))
7890
7946
  b.depth--;
@@ -7939,16 +7995,16 @@ static LeptrisDocument html_parse_shared(
7939
7995
 
7940
7996
  /* Trailing text. */
7941
7997
  if (text < end) {
7942
- size_t dlen = 0;
7943
- char* dec = h_decode_text(&b, text, end, &dlen);
7944
- if (dec && *dec) {
7998
+ LeptrisTextNode* t = h_text_node(&b, text, end);
7999
+ if (t) {
7945
8000
  /* #659 after </html> with a frameset body, non-ws text
7946
8001
  * reprocesses into the frameset and drops (13.2.6.4.18,
7947
8002
  * tests19:41/42). Whitespace stays an html child after
7948
8003
  * the frameset (13.2.6.4.19, tests6:46). */
8004
+ const char* c = leptris_text_get_content(t);
7949
8005
  int drop = 0;
7950
8006
  if (b.whatwg && b.after_html && b.frameset) {
7951
- for (const char* w = dec; *w; w++)
8007
+ for (const char* w = c; *w; w++)
7952
8008
  if (*w != ' ' && *w != '\t' &&
7953
8009
  *w != '\n' && *w != '\r') {
7954
8010
  drop = 1;
@@ -7956,12 +8012,8 @@ static LeptrisDocument html_parse_shared(
7956
8012
  }
7957
8013
  }
7958
8014
  if (!drop) {
7959
- LeptrisTextNode* t =
7960
- leptris_text_create(dec, dlen, b.pool);
7961
- if (t) {
7962
- if (!b.post_table_text) h_reconstruct(&b);
7963
- h_append(&b, (LeptrisNodeRef)t);
7964
- }
8015
+ if (!b.post_table_text) h_reconstruct(&b);
8016
+ h_append(&b, (LeptrisNodeRef)t);
7965
8017
  }
7966
8018
  }
7967
8019
  }
@@ -1209,6 +1209,30 @@ static int op_value_of(XsltExec* ex, const XsltInstr* in,
1209
1209
  * kind, markers stripped). */
1210
1210
  char* sv = NULL;
1211
1211
  if (in->select_is_dot) {
1212
+ /* #682 borrow fast path: a TEXT context node, or an ELEMENT
1213
+ * whose entire content is one text child, has its string-
1214
+ * value ALREADY materialized — append that pointer instead
1215
+ * of get_node_text's malloc + two-pass join + copy. op_text
1216
+ * copies synchronously, so the borrow never outlives this
1217
+ * call. */
1218
+ int ty = ((LeptrisNode*)node)->type;
1219
+ const char* borrowed = NULL;
1220
+ if (ty == LEPTRIS_NODE_TYPE_TEXT) {
1221
+ borrowed = leptris_text_get_content((LeptrisTextNode*)node);
1222
+ } else if (ty == LEPTRIS_NODE_TYPE_ELEMENT) {
1223
+ LeptrisNodeRef fc = leptris_elem_first_child(node);
1224
+ if (fc && ((LeptrisNode*)fc)->type == LEPTRIS_NODE_TYPE_TEXT &&
1225
+ !leptris_node_get_next_sibling(fc))
1226
+ borrowed =
1227
+ leptris_text_get_content((LeptrisTextNode*)fc);
1228
+ }
1229
+ if (borrowed) {
1230
+ op_text(ex, &(XsltInstr){ .kind = XSLT_INSTR_TEXT,
1231
+ .text = (char*)borrowed,
1232
+ .doe = in->doe },
1233
+ node);
1234
+ return 0;
1235
+ }
1212
1236
  extern char* get_node_text(void* n);
1213
1237
  sv = get_node_text(node);
1214
1238
  } else if (in->select_attr_name) {
@@ -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.216",
4
+ "version": "1.9.220",
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.216.0
4
+ version: 1.9.220.0
5
5
  platform: x86_64-linux
6
6
  authors:
7
7
  - Ribose Inc.