leptris 1.9.273.1 → 1.9.277.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47fcca29c1cb0aacea1ff5027999e599246075f64cc810075df396e1d4e38e42
4
- data.tar.gz: df9e0dc01a033ac4bc77664871ea3d68d93659dfc4711f4ea6bff4a26296a767
3
+ metadata.gz: 6ed9c7b8afac75281839aedc850f425345d336f7893313c46a4b98fbda74118e
4
+ data.tar.gz: 939f5e15e2d829b2126ad81702d2d1cd09943b60047878722ad4f80992db416c
5
5
  SHA512:
6
- metadata.gz: 124be7f3520d656fe7a943addb4dc8761ab42d5ba7f7b2ce704303c9eca0acc736399ab99bbdb47ff5d1405f01799511355b1f8c561245fcab8eda35f341036e
7
- data.tar.gz: 716f23a6af6e287a9f9949a2458fca1cef71c24d56057efb79ebefae3e3bf26182c846c6c7b5a90362334c03c91da981945fe85b1c207d6c8e55687cd56e5fd5
6
+ metadata.gz: 550f5d8637884ccef52b572c2b2bb2ecbd7aaec569b30017bd0efb89960b013766d351157d2b562cb4ceb027fdc873e6d973286ca6ddc8e5f0ea5b601e886686
7
+ data.tar.gz: 673a248b9d8947656d9aac9316b7eddc08b11d05e1db2f36d2e71ea77a72d92c241e5f71485cd5941394e061a5a76a933dcb77796758248f9ab73a02d54dcede
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.273"
11
+ LIBLEPTRIS_VERSION = "1.9.277"
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.273.1"
4
+ VERSION = "1.9.277.0"
5
5
  end
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.273
4
+ - libleptris v1.9.277
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,81 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.277] - 2026-09-30
4
+
5
+ ### Fixed
6
+
7
+ - `element_copy`'s direct attribute placement keeps the `attr_count` increment that
8
+ `leptris_element_add_attribute` maintained — the count accessor and the O(1) attribute-index
9
+ fast path read that field, and a 2-attribute copy read as 0 (caught by the ASAN leg's
10
+ deep-copy suite; leptris#682, leptris#1450).
11
+
12
+ ### Performance
13
+
14
+ - `element_copy` places unprefixed attribute nodes directly instead of routing pooled copies
15
+ through the mutation API, which re-derived what the source struct already carries (a second
16
+ string copy, the FNV hash recompute, the entity scan). The source's lazy-hash sentinel and
17
+ entity flag carry over verbatim. 100-book subtree copy: 0.050 -> 0.033 ms — ~2.7–2.9x faster
18
+ than lxml's deepcopy of the same fixture (leptris#682).
19
+ - Zero-argument `position()` and `last()` read the evaluation context directly instead of the
20
+ function-registry round trip (name lookup, argument array, extension fallback chain).
21
+ Positional predicates re-evaluate these per candidate, so the by-name lookup dominated
22
+ per-eval cost there. Values mirror the registry handlers exactly; core functions cannot be
23
+ shadowed; arity errors keep the registry path (leptris#682).
24
+
25
+
26
+
27
+ ## [1.9.276] - 2026-09-30
28
+
29
+ ### Fixed
30
+
31
+ - leptris_xslt_apply materializes streamed output into the returned document (xslt)
32
+
33
+
34
+
35
+ ## [1.9.275] - 2026-09-30
36
+
37
+ ### Performance
38
+
39
+ - `leptris_element_copy` routes the source views' lengths instead of re-deriving them: the
40
+ deep-copy path paid four strlens per attribute (two `leptris_pool_strdup` copies plus two
41
+ `leptris_sv_from_cstr` reconstructions) and one per element name before copying everything
42
+ again — a profile put `strlen` at 38% of a 100-book subtree copy. Length-based pooled
43
+ copies (which also intern recurring attribute names) take the same fixture from
44
+ 0.099 to 0.050 ms per copy (~2x; ~1.9x faster than lxml's deepcopy). Behavior is
45
+ byte-identical; the #696/#721/#213 copy-semantics suites pin the output (leptris#682,
46
+ leptris#1445).
47
+
48
+
49
+
50
+ ## [1.9.274] - 2026-09-30
51
+
52
+ ### Fixed
53
+
54
+ - The XSLT streaming gate now also rejects sheets that declare a non-default `xsl:output`
55
+ encoding — output-encoding conversion is a serializer feature the stream emitters don't
56
+ carry, so those sheets stay on the result-tree path in every build configuration
57
+ (leptris#1443, bug-169).
58
+ - Stream text emission escapes literal CR as `
`, matching the attribute emitter and the
59
+ result-tree serializer (bug-105).
60
+ - A dynamic HTML output method (result rooted at an unprefixed `<html>`) now bails the
61
+ streaming path and restarts the transform on the result-tree path instead of emitting
62
+ XML-method self-closing tags (bug-28-).
63
+
64
+ ### Performance
65
+
66
+ - XSLT streaming output is now active for every plain stylesheet. The eligibility gate
67
+ required an explicit `xsl:mode on-no-match` value, but sheets without one default to the
68
+ same legacy text built-ins as the admitted `text-only-copy` — the engine was dark for all
69
+ plain sheets while the result-tree path paid ~3x (text-node allocation, serialize-back,
70
+ document teardown). Measured vs in-process libxslt 1.1.43 (best-of-9, arm64):
71
+ value-of ×50000 17.24 → 1.07 ms (9.8x faster), dispatch 2000 books 2.99 → 1.55 ms
72
+ (2.7x), dispatch-heavy 120 templates 5.64 → 1.75 ms (2.0x), predicate dispatch
73
+ 4.66 → 2.00 ms (26.7x), transform //book[@price>100] ~157 µs vs 423.6 µs (2.7x),
74
+ 100-book subtree copy 0.099 → 0.060 ms (1.6x). No row remains behind libxslt
75
+ (leptris#682, leptris#1443).
76
+
77
+
78
+
3
79
  ## [1.9.273] - 2026-09-29
4
80
 
5
81
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.273
6
+ VERSION 1.9.277
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -2058,27 +2058,71 @@ static LeptrisElement copy_subtree_detached(LeptrisElement source,
2058
2058
  char* name_copy = leptris_sv_to_cstr_pooled(&name_view, pool);
2059
2059
  if (!name_copy) return NULL;
2060
2060
  LeptrisElement copy = leptris_element_create_with_view(
2061
- leptris_sv_from_cstr(name_copy), pool);
2061
+ leptris_sv_from_ptr(name_copy, name_view.length), pool);
2062
2062
  if (!copy) return NULL;
2063
2063
  leptris_elem_set_parent(copy, parent_copy);
2064
2064
 
2065
2065
  if (copy_element_namespaces_pooled(copy, source, pool))
2066
2066
  doc->has_namespaces = 1;
2067
2067
 
2068
+ struct leptris_attribute* last_attr = NULL;
2068
2069
  for (struct leptris_attribute* sa =
2069
2070
  leptris_element_get_first_attribute(source);
2070
2071
  sa; sa = leptris_attr_next(sa)) {
2071
2072
  if (leptris_sv_is_empty(&sa->name_view)) continue;
2072
- char* n = leptris_pool_strdup(pool, sa->name_view.data);
2073
+ /* Prefixed attrs carry the TODO-173 side cache (prefix +
2074
+ * namespace_uri) — rare; keep the full mutation-API path. */
2075
+ if (attr_get_ns_cache(sa)) {
2076
+ char* n = leptris_sv_to_cstr_pooled(&sa->name_view, pool);
2077
+ if (!n) continue;
2078
+ LeptrisStringView sav_ = leptris_attr_value_sv(sa);
2079
+ char* v = leptris_sv_is_empty(&sav_)
2080
+ ? NULL
2081
+ : leptris_sv_to_cstr_pooled(&sav_, pool);
2082
+ LeptrisStringView nv = leptris_sv_from_ptr(
2083
+ n, sa->name_view.length);
2084
+ LeptrisStringView vv =
2085
+ v ? leptris_sv_from_ptr(v, sav_.length)
2086
+ : leptris_sv_from_cstr("");
2087
+ leptris_element_add_attribute(copy, nv, vv, pool);
2088
+ continue;
2089
+ }
2090
+ /* Direct placement: the source attr is already decoded and
2091
+ * hashed — add_attribute's mutation-API work (second string
2092
+ * copy, hash recompute, entity scan) re-derives what the
2093
+ * source struct already carries. Materialize the views into
2094
+ * the destination pool (source views are source-document
2095
+ * lifetime), carry the lazy-hash sentinel and the entity
2096
+ * flag bit verbatim, and wire the chain the way the parser
2097
+ * does. */
2098
+ char* n = leptris_sv_to_cstr_pooled(&sa->name_view, pool);
2073
2099
  if (!n) continue;
2074
2100
  LeptrisStringView sav_ = leptris_attr_value_sv(sa);
2075
2101
  char* v = leptris_sv_is_empty(&sav_)
2076
2102
  ? NULL
2077
- : leptris_pool_strdup(pool, sav_.data);
2078
- LeptrisStringView nv = leptris_sv_from_cstr(n);
2079
- LeptrisStringView vv =
2080
- v ? leptris_sv_from_cstr(v) : leptris_sv_from_cstr("");
2081
- leptris_element_add_attribute(copy, nv, vv, pool);
2103
+ : leptris_sv_to_cstr_pooled(&sav_, pool);
2104
+ struct leptris_attribute* attr =
2105
+ (struct leptris_attribute*)leptris_pool_alloc(
2106
+ pool, sizeof(struct leptris_attribute));
2107
+ if (!attr) continue;
2108
+ attr->name_view = leptris_sv_from_ptr(n, sa->name_view.length);
2109
+ leptris_attr_value_set_heap(
2110
+ attr, v ? leptris_sv_from_ptr(v, sav_.length)
2111
+ : leptris_sv_from_cstr(""));
2112
+ attr->name_hash = sa->name_hash;
2113
+ attr->ns_cache_off = 0;
2114
+ attr->next_cp = 0;
2115
+ if (copy->first_attribute_off != 0) {
2116
+ leptris_attr_set_next(last_attr, attr);
2117
+ } else {
2118
+ copy->first_attribute_off =
2119
+ (int32_t)((char*)attr - (char*)copy);
2120
+ }
2121
+ last_attr = attr;
2122
+ /* add_attribute's increment — attr_count is what the count
2123
+ * accessor and the O(1) index fast path read (uint8 wrap
2124
+ * at 255 latches the walk, same convention). */
2125
+ copy->attr_count++;
2082
2126
  }
2083
2127
 
2084
2128
  LeptrisNodeRef first = NULL;
@@ -846,6 +846,29 @@ static struct leptris_xpath_result* evaluate_function_call_impl(XPathContext* ct
846
846
  return NULL;
847
847
  }
848
848
 
849
+ /* Zero-arg context builtins (the per-eval hot pair):
850
+ * position() and last() read the eval context directly — no
851
+ * registry lookup, no argument array, no extension fallback
852
+ * chain. Positional predicates re-evaluate these for every
853
+ * candidate, so the by-name lookup dominated per-eval cost
854
+ * there. Values mirror the registry handlers exactly; core
855
+ * functions cannot be shadowed, so the registry entry and
856
+ * this path can never disagree. Arity errors stay on the
857
+ * registry path (child_count != 0 falls through). */
858
+ if (ast->child_count == 0) {
859
+ if (strcmp(func_name, "position") == 0 ||
860
+ strcmp(func_name, "last") == 0) {
861
+ struct leptris_xpath_result* r =
862
+ xpath_result_new(XPATH_RESULT_NUMBER);
863
+ if (r) {
864
+ r->value.number_value = (double)
865
+ (func_name[0] == 'p' ? ctx->context_position
866
+ : ctx->context_size);
867
+ }
868
+ return r;
869
+ }
870
+ }
871
+
849
872
  /* Get function registry */
850
873
  if (!ctx->function_registry) {
851
874
  snprintf(ctx->error_msg, sizeof(ctx->error_msg),
@@ -374,11 +374,12 @@ static void xslt_append_fragment_node(XsltExec* ex, LeptrisNodeRef n);
374
374
  static void stream_emit_text(struct SerializeBuffer* b, const char* t) {
375
375
  if (!t) return;
376
376
  for (const char* p = t; *p; ) {
377
- size_t run = strcspn(p, "&<>");
377
+ size_t run = strcspn(p, "&<>\r");
378
378
  if (run) buffer_append_len(b, p, run);
379
379
  if (!p[run]) break;
380
380
  buffer_append(b, p[run] == '&' ? "&amp;"
381
- : p[run] == '<' ? "&lt;" : "&gt;");
381
+ : p[run] == '<' ? "&lt;"
382
+ : p[run] == '>' ? "&gt;" : "&#13;");
382
383
  p += run + 1;
383
384
  }
384
385
  }
@@ -856,6 +857,25 @@ static int op_result_elem(XsltExec* ex, const XsltInstr* in,
856
857
  LeptrisElement parent = ex->pending_parent;
857
858
  const char* out_name = apply_ns_alias(ex->sheet, in->name);
858
859
  if (ex->streaming) {
860
+ /* Dynamic output-method parity (bug-28-): the tree path's
861
+ * serializer switches to the HTML method when the result
862
+ * root is an unprefixed <html> (effective_html_method's
863
+ * trigger); the stream emitters cover only the XML method.
864
+ * Instead of duplicating the html rules here, bail — the
865
+ * driver restarts the transform on the result-tree path. An
866
+ * explicit xsl:output method never bails (out_method_set
867
+ * disables the dynamic switch). */
868
+ if (!parent && !ex->sheet->out_method_set && out_name &&
869
+ !strchr(out_name, ':') &&
870
+ (!in->ns_uri || !in->ns_uri[0]) &&
871
+ strlen(out_name) == 4 &&
872
+ tolower((unsigned char)out_name[0]) == 'h' &&
873
+ tolower((unsigned char)out_name[1]) == 't' &&
874
+ tolower((unsigned char)out_name[2]) == 'm' &&
875
+ tolower((unsigned char)out_name[3]) == 'l') {
876
+ ex->stream_bail_html = 1;
877
+ return 1;
878
+ }
859
879
  /* Gate guarantees: no namespaces, no attr-sets, no
860
880
  * xsl:attribute children — the open tag is complete before
861
881
  * the children run, so it emits whole and the empty case
@@ -4816,9 +4836,10 @@ void xslt_exec_free(XsltExec* ex) {
4816
4836
  free(ex);
4817
4837
  }
4818
4838
 
4819
- XsltExec* xslt_transform_doc(const XsltStylesheet* sheet,
4820
- LeptrisDocument sheet_doc,
4821
- LeptrisDocument source) {
4839
+ static XsltExec* transform_doc_ex(const XsltStylesheet* sheet,
4840
+ LeptrisDocument sheet_doc,
4841
+ LeptrisDocument source,
4842
+ int allow_stream) {
4822
4843
  if (!sheet || !source) return NULL;
4823
4844
  register_ops();
4824
4845
 
@@ -4831,7 +4852,7 @@ XsltExec* xslt_transform_doc(const XsltStylesheet* sheet,
4831
4852
  ex->current_size = 1; /* last() default context size */
4832
4853
  ex->result = leptris_document_create();
4833
4854
  if (!ex->result) { xslt_exec_free(ex); return NULL; }
4834
- if (sheet->can_stream) {
4855
+ if (allow_stream && sheet->can_stream) {
4835
4856
  ex->sbuf = buffer_create(0);
4836
4857
  if (ex->sbuf) ex->streaming = 1;
4837
4858
  }
@@ -4915,3 +4936,14 @@ XsltExec* xslt_transform_doc(const XsltStylesheet* sheet,
4915
4936
  return ex;
4916
4937
  }
4917
4938
 
4939
+ XsltExec* xslt_transform_doc(const XsltStylesheet* sheet,
4940
+ LeptrisDocument sheet_doc,
4941
+ LeptrisDocument source) {
4942
+ XsltExec* ex = transform_doc_ex(sheet, sheet_doc, source, 1);
4943
+ if (ex && ex->streaming && ex->stream_bail_html) {
4944
+ xslt_exec_free(ex);
4945
+ ex = transform_doc_ex(sheet, sheet_doc, source, 0);
4946
+ }
4947
+ return ex;
4948
+ }
4949
+
@@ -634,6 +634,10 @@ typedef struct xslt_exec {
634
634
  * PI + control flow; everything else keeps the result tree). */
635
635
  struct SerializeBuffer* sbuf;
636
636
  int streaming;
637
+ /* Dynamic html-method bail (bug-28-): set when the stream path
638
+ * hits a fragment-level <html> root and the output method is
639
+ * dynamic — the driver restarts on the result-tree path. */
640
+ int stream_bail_html;
637
641
  int cache_owner_set; int cache_owner_prev;
638
642
 
639
643
  /* current(): the node being processed by the template rule or
@@ -2401,12 +2401,21 @@ XsltStylesheet* xslt_stylesheet_parse_root(LeptrisDocument doc,
2401
2401
  * template (and the globals) must be stream-safe; any tree-
2402
2402
  * building or post-hoc op keeps the sheet on the result-tree
2403
2403
  * path. Conservative by construction — unknown kinds reject. */
2404
- sheet->can_stream = !sheet->out_method_text && !sheet->out_method_html &&
2404
+ /* Output ENCODING conversion (bug-169: xsl:output
2405
+ * encoding='iso-8859-1') is a serializer feature the stream
2406
+ * emitters don't carry — only the default (UTF-8) streams. */
2407
+ sheet->can_stream = !sheet->out_encoding &&
2408
+ !sheet->out_method_text && !sheet->out_method_html &&
2405
2409
  !stream_indent_on(sheet) && sheet->out_cdata_count == 0 &&
2406
2410
  sheet->charmap_count == 0 && sheet->out_standalone < 0 &&
2407
2411
  !sheet->out_doctype_system && !sheet->out_doctype_public &&
2408
2412
  sheet->version_major < 3 &&
2409
- (sheet->mode_on_no_match == 1 || sheet->mode_on_no_match == 6) &&
2413
+ /* 0 (unspecified — no xsl:mode element) executes the SAME
2414
+ * legacy text built-ins as 6 for 1.0/2.0 sheets (the >=3
2415
+ * remap to shallow-copy is excluded by version_major < 3
2416
+ * above); admitting it is what lets plain sheets stream. */
2417
+ (sheet->mode_on_no_match == 0 || sheet->mode_on_no_match == 1 ||
2418
+ sheet->mode_on_no_match == 6) &&
2410
2419
  stream_instrs_ok(sheet->globals);
2411
2420
  for (size_t i = 0; sheet->can_stream && i < sheet->mode_count; i++) {
2412
2421
  for (size_t t = 0; sheet->can_stream && t < sheet->modes[i].n; t++) {
@@ -114,6 +114,9 @@ LEPTRIS_API void leptris_xslt_free(LeptrisXslt xslt) {
114
114
  }
115
115
 
116
116
 
117
+ static int effective_html_method(const XsltStylesheet* sheet,
118
+ LeptrisElement peek);
119
+
117
120
  LEPTRIS_API LeptrisDocument leptris_xslt_apply(LeptrisXslt xslt,
118
121
  LeptrisDocument source) {
119
122
  if (!xslt || !source) return NULL;
@@ -121,6 +124,65 @@ LEPTRIS_API LeptrisDocument leptris_xslt_apply(LeptrisXslt xslt,
121
124
  xslt->sheet_doc, source);
122
125
  if (!ex) return NULL;
123
126
  if (ex->eval_error) { xslt_exec_free(ex); return NULL; }
127
+ /* #682 stream mode: the result bytes live in the exec buffer,
128
+ * not the result tree. The document-result face materializes
129
+ * them by FRAGMENT-parsing the buffer and lifting the children
130
+ * to document level — the exact pre-streaming result shape (a
131
+ * single root for element output; TOP-LEVEL TEXT NODES for
132
+ * value-of-only sheets, which no whole-document parse can
133
+ * represent). Empty streamed output keeps the empty result
134
+ * document; a fragment that fails to parse reports the
135
+ * transform failure. */
136
+ if (ex->streaming && ex->sbuf && ex->sbuf->size > 0) {
137
+ LeptrisDocument parsed = leptris_document_create();
138
+ if (!parsed) { xslt_exec_free(ex); return NULL; }
139
+ LeptrisStatus st = LEPTRIS_OK;
140
+ LeptrisElement synth = leptris_parse_fragment(
141
+ ex->sbuf->data, ex->sbuf->size, parsed, &st);
142
+ if (!synth) {
143
+ leptris_document_free(parsed);
144
+ xslt_exec_free(ex);
145
+ return NULL;
146
+ }
147
+ struct leptris_document* pd = (struct leptris_document*)parsed;
148
+ struct leptris_element* sy = (struct leptris_element*)synth;
149
+ /* Detach the synthetic's child run in one write, then chain
150
+ * it into the document children — the tree path's own
151
+ * wiring shape (xslt_exec's fragment doc does exactly
152
+ * this). The first ELEMENT child takes the root slots and
153
+ * the root-doc map registration; bare-text runs keep the
154
+ * pre-streaming top-level-text shape. */
155
+ LeptrisNodeRef run =
156
+ (LeptrisNodeRef)leptris_element_first_child_any(sy);
157
+ sy->first_child_off = 0;
158
+ int stamped = 0;
159
+ for (LeptrisNodeRef n = run; n; ) {
160
+ LeptrisNodeRef nx = leptris_node_next_sibling(n);
161
+ leptris_node_set_next_sibling(n, NULL);
162
+ if (!pd->doc_children_head) {
163
+ pd->doc_children_head = n;
164
+ pd->doc_children_tail = n;
165
+ } else {
166
+ leptris_node_set_next_sibling(
167
+ (LeptrisNodeRef)pd->doc_children_tail, n);
168
+ pd->doc_children_tail = n;
169
+ }
170
+ if (!stamped &&
171
+ leptris_node_get_type(n) == LEPTRIS_NODE_TYPE_ELEMENT) {
172
+ pd->root = (LeptrisElement)n;
173
+ pd->new_dom_root = (LeptrisElement)n;
174
+ stamped = 1;
175
+ }
176
+ n = nx;
177
+ }
178
+ if (stamped) {
179
+ extern void leptris_root_doc_register(
180
+ LeptrisElement, struct leptris_document*);
181
+ leptris_root_doc_register(pd->root, pd);
182
+ }
183
+ xslt_exec_free(ex);
184
+ return parsed;
185
+ }
124
186
  LeptrisDocument out = ex->result;
125
187
  ex->result = NULL; /* ownership moved */
126
188
  xslt_exec_free(ex);
@@ -31,6 +31,32 @@ std::string run(const char* sheet_body, const char* xml) {
31
31
  return r;
32
32
  }
33
33
 
34
+ /* The DOCUMENT-result face (leptris_xslt_apply): #682 streaming
35
+ * sheets emit into the exec buffer — the face must materialize the
36
+ * streamed markup as the returned document (root present), not
37
+ * hand back the empty result tree. */
38
+ std::string run_apply_doc(const char* sheet_body, const char* xml) {
39
+ std::string sheet = std::string("<xsl:stylesheet ") + KXSL +
40
+ " version='1.0'>" + sheet_body +
41
+ "</xsl:stylesheet>";
42
+ LeptrisXslt x = leptris_xslt_parse(sheet.c_str(), sheet.size());
43
+ if (!x) return "(compile-failed)";
44
+ LeptrisDocument d = leptris_parse_string(xml, strlen(xml), nullptr);
45
+ if (!d) { leptris_xslt_free(x); return "(parse-failed)"; }
46
+ LeptrisDocument out = leptris_xslt_apply(x, d);
47
+ std::string r = "(null)";
48
+ if (out) {
49
+ LeptrisElement root = leptris_document_root(out);
50
+ r = root ? leptris_element_name(root) : "(no-root)";
51
+ char* xout = leptris_document_serialize(out, 0);
52
+ if (xout) { r += "|"; r += xout; leptris_free_string(xout); }
53
+ leptris_document_free(out);
54
+ }
55
+ leptris_document_free(d);
56
+ leptris_xslt_free(x);
57
+ return r;
58
+ }
59
+
34
60
  /* Same, pinned to version='3.0' — XSLT 3.0 semantics (sequence
35
61
  * display forms, versioned value-of, expand-text). */
36
62
  std::string run30(const char* sheet_body, const char* xml) {
@@ -3011,6 +3037,26 @@ TEST(Xslt30, TryCatchErrorVariants) {
3011
3037
  "(compile-failed)");
3012
3038
  }
3013
3039
 
3040
+ /* #682 streaming gate: an UNSPECIFIED on-no-match (no xsl:mode
3041
+ * element — every plain 1.0/2.0 sheet) executes the same legacy
3042
+ * text built-ins as the explicit 'text-only-copy' value; both
3043
+ * output shapes must stay byte-identical so the stream gate can
3044
+ * admit the default alongside the explicit value (builtin_no_match:
3045
+ * v==6 falls to the same legacy walk). */
3046
+ TEST(Xslt30, UnspecifiedNoMatchMatchesTextOnlyCopy) {
3047
+ const char* body30 =
3048
+ "<xsl:template match='g/@n'><hit/></xsl:template>"
3049
+ "<xsl:template match='/'><out>"
3050
+ "<xsl:apply-templates select='*'/></out></xsl:template>";
3051
+ const char* doc = "<r><g n='1'><i>x</i></g>tail</r>";
3052
+ std::string unspecified = body(
3053
+ run(body30, doc));
3054
+ std::string textonly = body(run(
3055
+ (std::string("<xsl:mode on-no-match='text-only-copy'/>") +
3056
+ body30).c_str(), doc));
3057
+ EXPECT_EQ(unspecified, textonly);
3058
+ }
3059
+
3014
3060
  /* xsl:mode on-no-match (3.0 §6.7) — Saxon-HE 12.7 ground truth on
3015
3061
  * <r><g n="1"><i>x</i></g>tail</r> with match="g/@n" -> <hit/>.
3016
3062
  * The 3.0 DEFAULT is shallow-copy (copy the node, process its
@@ -4243,3 +4289,18 @@ TEST(XsltFull, DispatchIndexOverflowFiresAllTemplates) {
4243
4289
  EXPECT_EQ(out, want);
4244
4290
  }
4245
4291
  }
4292
+
4293
+
4294
+ /* #682 follow-up: the document face materializes streamed output.
4295
+ * Plain value-of sheet (stream-eligible since the mode_on_no_match
4296
+ * gate admits 0). */
4297
+ TEST(XsltStreamApplyDoc, DocumentFaceMaterializesStreamedOutput) {
4298
+ std::string r = run_apply_doc(
4299
+ "<xsl:template match='/'><summary>"
4300
+ "<xsl:value-of select='//title'/></summary>"
4301
+ "</xsl:template>",
4302
+ "<doc><title>hello</title><item/><item/></doc>");
4303
+ EXPECT_EQ(r.find("(no-root)"), std::string::npos);
4304
+ EXPECT_NE(r.find("summary"), std::string::npos);
4305
+ EXPECT_NE(r.find("hello"), std::string::npos);
4306
+ }
@@ -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.273",
4
+ "version": "1.9.277",
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.273.1
4
+ version: 1.9.277.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.