leptris 1.9.174.9 → 1.9.177.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: 73130c8bf549007d80bf610c22bc831df382f5fe972316835a6dc806885929c5
4
- data.tar.gz: 3b6bfb23c72a2ed3532eb64886c9f36603975b5d601aa693f6b3ba821bb19de2
3
+ metadata.gz: 9144461ce7cc58b12f57c1a5c497336f5712568654db476342b32b9d6d35dc93
4
+ data.tar.gz: a6fe5f35bf5891c3794f8bf5533c115b26b9eb19545d361aeafcb3b9b1935af0
5
5
  SHA512:
6
- metadata.gz: 621d0f62f7869c1a4b0c63212dce8694f51cc1914a3f115551b7e91c1f5b2bc0af9a33dcb2baa4d462fc5a12f72225a228dee59f7e4a7f70ed5173e0afe6932b
7
- data.tar.gz: 2a389319493ace9979ea863853b75c56149f73dcc5e204c362bd8bdbaacd335d94a58bd9bb3d4ed83f6c83df3331b53549bb462fa19980fb996a8c2798c3fbdc
6
+ metadata.gz: ee1362b97425fd76000f28d3232badcb7dd23d979e992cfd296ca17ee6bddeb716bd93b01830f22fd17103475cbfa4eccbcc918691625982fe4b79ca760de897
7
+ data.tar.gz: e1079b8c90d48d43e17d7ad3e7ebb55a8cbf87246cd61d0eed9eb2a44fc7c3cc91e5f2c034843ee1914f818d1c686b9596b433f96f2e22795fe02a005a75c21b
data/CHANGELOG.md CHANGED
@@ -5,6 +5,55 @@ 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.177.0] - 2026-09-16
9
+
10
+ ### Changed — libleptris 1.9.174 → 1.9.177 (lockstep)
11
+
12
+ - **Node-surface parity (upstream #1094, #212 — the big one)**:
13
+ entity references (`&name;` stays a first-class
14
+ `EntityReference` node under the new
15
+ `ParseOptions.keep_entity_refs` flag — text splits around them,
16
+ character references still expand, serialization is verbatim;
17
+ `Document#create_entity_reference`), the XML declaration
18
+ (`xml_version`/`xml_encoding`/`xml_standalone` readers and
19
+ writers — `#version` stays the mutation counter), programmatic
20
+ DOCTYPE (`Document#set_doctype` with PUBLIC/SYSTEM ids,
21
+ serializing in document position), and document-PI parity
22
+ (`add_pi` returns the PI node with identity and epilog
23
+ anchoring; `Document#remove_child` removes PIs/comments). The
24
+ ext's kind dispatch knows the new node kind.
25
+ - **C14N: redundant namespace redeclarations omitted** (upstream
26
+ #1096) — spec-pinned.
27
+ - **Document lifecycle** (upstream #1093): pool destroy parks the
28
+ arenas — the fresh-doc build row's teardown share should drop;
29
+ clean-host battery pending.
30
+ - XSLT/XPath/HTML perf + conformance run (1.9.175-177).
31
+
32
+ ### Added
33
+
34
+ - `Leptris::XML::EntityReference`, the ten new FFI faces, the
35
+ `KEEP_ENTITY_REFS` parse flag.
36
+
37
+ ## [1.9.174.10] - 2026-09-16
38
+
39
+ ### Changed
40
+
41
+ - **Status raises in C (TODO.perf/36)**: every C mutation face
42
+ raises `Leptris::XML::Error` with the exact `status_message`
43
+ format — the Ruby `check_status` dispatch leaves the fast
44
+ mutation paths (and the success/lift-marker collision it
45
+ briefly introduced — Qtrue on success now — was caught by the
46
+ bottom-up construction spec). `to_xml(encoding:)` rides the ext
47
+ serializer face (encoding VALUE, nil → NULL).
48
+ - **Document lazy Pointer (TODO.perf/37)**: `#c_ptr` materializes
49
+ once over `@c_address` (freed docs answer nil); the parse/create
50
+ faces skip the Pointer mint entirely. The ffi gem rejects
51
+ Integers for typedef'd pointer params — the search paths keep
52
+ the lazy Pointer.
53
+ - **Atomic bundle vendoring**: replacing a mapped `native.bundle`
54
+ in place gets the next loader SIGKILLed (CODESIGNING Invalid
55
+ Page) — the Rakefile now cp+mv's atomically.
56
+
8
57
  ## [1.9.174.9] - 2026-09-16
9
58
 
10
59
  ### Changed
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.174"
11
+ LIBLEPTRIS_VERSION = "1.9.177"
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
@@ -172,7 +172,12 @@ task :compile do
172
172
  # Build-log proof of the linkage contract: libSystem only.
173
173
  puts `otool -L #{bundle}`
174
174
  end
175
- cp(bundle, "lib/leptris/xml/#{File.basename(bundle)}")
175
+ # Atomic replace: overwriting a bundle another process still
176
+ # has mapped gets that loader SIGKILLed (CODESIGNING Invalid
177
+ # Page) — cp to a temp name, then mv.
178
+ dest = "lib/leptris/xml/#{File.basename(bundle)}"
179
+ cp(bundle, "#{dest}.new")
180
+ mv("#{dest}.new", dest)
176
181
  puts "Vendored native layer (#{File.basename(bundle)}) into lib/leptris/xml/"
177
182
  end
178
183
  end
@@ -0,0 +1,34 @@
1
+ # 36 — Status raises in C + encoding on the serializer faces
2
+
3
+ Status: DONE (1.9.174.10)
4
+
5
+ Every C mutation face returns INT2FIX(st) for a Ruby check_status
6
+ dispatch (~30ns per mutation, plus a whole class of
7
+ "forgot-to-check" bug the compiler cannot catch). With
8
+ leptris_status_string and leptris_last_error both dlsym'd, the
9
+ faces raise Leptris::XML::Error in C with the exact
10
+ status_message format ("base" or "base (detail)") — call sites
11
+ shrink to the bare face call.
12
+
13
+ The ext serializer faces skip the encoding-tagged shape entirely
14
+ (to_xml(encoding: ...) still builds the options struct through
15
+ FFI): the faces gain an encoding VALUE (nil -> NULL, the Ruby
16
+ string's bytes stay valid across the synchronous engine call).
17
+
18
+ Gates: every mutation's failure message byte-identical to
19
+ check_status; encoding-tagged to_xml output identical to the FFI
20
+ path (declaration, no anchor lifetime issues); suite both modes.
21
+
22
+ ## Outcome (1.9.174.10)
23
+
24
+ All seven mutation faces raise Leptris::XML::Error in C with the
25
+ exact status_message format (status_string + optional
26
+ " (last_error)") — the Ruby check_status dispatch disappears from
27
+ every fast mutation. TRAP FOUND: the faces' success return became
28
+ Qnil, colliding with the lift-needed marker — every fast mutation
29
+ fell through to the slow path and re-applied (order-corrupting
30
+ for inserts; the v180 bottom-up spec caught it). Success now
31
+ returns Qtrue. The serializer faces take an encoding VALUE (nil
32
+ -> NULL; the Ruby string's bytes live across the synchronous
33
+ call), and to_xml(encoding:) rides the ext face instead of the
34
+ options-struct FFI path.
@@ -0,0 +1,28 @@
1
+ # 37 — Document lazy Pointer + address-based xpath contexts
2
+
3
+ Status: DONE (1.9.174.10)
4
+
5
+ Documents still mint an FFI::Pointer eagerly (the parse/create
6
+ faces pay the funcall + one allocation + its GC visit per
7
+ document). Make #c_ptr lazy like Node's (attr_reader becomes a
8
+ materializer over @c_address), skip the Pointer in
9
+ build_binding_document, and feed the search paths Integer
10
+ addresses (the ffi gem converts Integers for :pointer params) so
11
+ per-xpath document Pointers never materialize at all.
12
+
13
+ Gates: c_ptr stability and public API unchanged; every FFI
14
+ document consumer still works (options paths, c14n, fragment
15
+ fallback); suite both modes.
16
+
17
+ ## Outcome (1.9.174.10)
18
+
19
+ Document#c_ptr materializes lazily over @c_address (freed docs
20
+ answer nil — never re-materialize); build_binding_document skips
21
+ the Pointer funcall (one alloc + ~200ns per document). TRAP
22
+ FOUND: this ffi version does NOT convert Integers for typedef'd
23
+ pointer params — passing c_address into the compiled-eval calls
24
+ failed with ":pointer argument is not a valid pointer"; the
25
+ search paths keep passing the (now lazy, once-per-document)
26
+ Pointer. Also: replacing a mapped native.bundle in place gets
27
+ the next loader SIGKILLed (CODESIGNING Invalid Page) — the
28
+ vendoring cp is now an atomic cp+mv.
@@ -97,6 +97,7 @@ typedef void *(*elem_copy_fn)(void *, void *);
97
97
  typedef void *(*parse_str_fn)(const char *, size_t, void *);
98
98
  typedef void *(*parse_frag_fn)(const char *, size_t, void *, int *);
99
99
  typedef const char *(*last_err_fn)(void);
100
+ typedef const char *(*status_str_fn)(int);
100
101
  static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
101
102
  static node_unlink_fn f_node_unlink;
102
103
  static traverse_fn f_node_traverse;
@@ -107,6 +108,7 @@ static elem_copy_fn f_elem_copy;
107
108
  static parse_str_fn f_parse_str;
108
109
  static parse_frag_fn f_parse_frag;
109
110
  static last_err_fn f_last_err;
111
+ static status_str_fn f_status_str;
110
112
  static set_root_fn f_set_root;
111
113
  static doc_free_fn f_doc_free;
112
114
 
@@ -125,8 +127,10 @@ static void resolve_binding_classes(void);
125
127
  static VALUE binding_cache_of(VALUE document);
126
128
  static VALUE binding_klass_for(int kind);
127
129
  static VALUE c_iteration_scope;
130
+ static VALUE c_leptris_error;
128
131
  static VALUE c_b_document, c_b_freed, c_ffi_pointer;
129
132
  static ID id_ptr_new, id_freed_new, id_alive;
133
+ static void check_status_c(int st);
130
134
 
131
135
  #define NT_ELEMENT 0
132
136
 
@@ -230,6 +234,7 @@ static void resolve_symbols(const char *lib_path)
230
234
  f_parse_str = (parse_str_fn)lib_sym(h, "leptris_parse_string");
231
235
  f_parse_frag = (parse_frag_fn)lib_sym(h, "leptris_parse_fragment");
232
236
  f_last_err = (last_err_fn)lib_sym(h, "leptris_last_error");
237
+ f_status_str = (status_str_fn)lib_sym(h, "leptris_status_string");
233
238
  f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
234
239
  f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
235
240
  if (!f_elem_name || !f_text_content || !f_attr ||
@@ -243,7 +248,7 @@ static void resolve_symbols(const char *lib_path)
243
248
  !f_text_set_content || !f_node_unlink ||
244
249
  !f_node_traverse || !f_node_visit || !f_free_str ||
245
250
  !f_node_xpath || !f_elem_copy || !f_parse_str ||
246
- !f_parse_frag || !f_last_err ||
251
+ !f_parse_frag || !f_last_err || !f_status_str ||
247
252
  !f_doc_free ||
248
253
  !f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
249
254
  !f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
@@ -640,7 +645,8 @@ static VALUE nf_append_binding_child(VALUE self, VALUE document,
640
645
  rb_ivar_set(document, id_iv_version,
641
646
  LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
642
647
  st = f_append_child(parent, child);
643
- return INT2FIX(st);
648
+ check_status_c(st);
649
+ return Qtrue;
644
650
  }
645
651
 
646
652
  /* C-bound set_attribute (TODO.perf/11, #204 gap row 0.25x): the
@@ -669,7 +675,8 @@ static VALUE nf_set_binding_attribute(VALUE self, VALUE document,
669
675
  * embedded-NUL value raises rather than truncates. */
670
676
  st = f_set_attr(node, RSTRING_PTR(StringValue(name)),
671
677
  StringValueCStr(value));
672
- return INT2FIX(st);
678
+ check_status_c(st);
679
+ return Qtrue;
673
680
  }
674
681
 
675
682
  /* C-bound value mutations (TODO.perf/23): the same gates + bump
@@ -692,7 +699,8 @@ static VALUE nf_set_binding_name(VALUE self, VALUE document,
692
699
  rb_ivar_set(document, id_iv_version,
693
700
  LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
694
701
  st = f_elem_set_name(node, StringValueCStr(name));
695
- return INT2FIX(st);
702
+ check_status_c(st);
703
+ return Qtrue;
696
704
  }
697
705
 
698
706
  /* Element text or text-node content — kind-dispatched in C. */
@@ -716,7 +724,8 @@ static VALUE nf_set_binding_text(VALUE self, VALUE document,
716
724
  st = f_node_type(node) == NT_ELEMENT
717
725
  ? f_elem_set_text(node, StringValueCStr(content))
718
726
  : f_text_set_content(node, StringValueCStr(content));
719
- return INT2FIX(st);
727
+ check_status_c(st);
728
+ return Qtrue;
720
729
  }
721
730
 
722
731
  static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
@@ -737,7 +746,8 @@ static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
737
746
  rb_ivar_set(document, id_iv_version,
738
747
  LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
739
748
  st = f_node_unlink(node);
740
- return INT2FIX(st);
749
+ check_status_c(st);
750
+ return Qtrue;
741
751
  }
742
752
 
743
753
  /* C-bound root= (TODO.perf/33): gates + bump + engine set_root
@@ -761,7 +771,8 @@ static VALUE nf_set_binding_root(VALUE self, VALUE document,
761
771
  rb_ivar_set(document, id_iv_version,
762
772
  LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
763
773
  st = f_set_root(doc_ptr_of(document), node);
764
- return INT2FIX(st);
774
+ check_status_c(st);
775
+ return Qtrue;
765
776
  }
766
777
 
767
778
  /* ---- C-yield traversal (TODO.perf/27) ----------------------------
@@ -1074,7 +1085,8 @@ static VALUE nf_insert_binding_child(VALUE self, VALUE document,
1074
1085
  rb_raise(rb_eArgError, "invalid insert mode %d", m);
1075
1086
  return Qnil;
1076
1087
  }
1077
- return INT2FIX(st);
1088
+ check_status_c(st);
1089
+ return Qtrue;
1078
1090
  }
1079
1091
 
1080
1092
  /* Builder factories (#149): create in C, wrap as NativeNode — no
@@ -1200,7 +1212,8 @@ static VALUE nn_address(VALUE self)
1200
1212
  * Init; the per-node Ruby frames (wrap/construct/memo/alive)
1201
1213
  * disappear. */
1202
1214
  static VALUE c_b_element = Qundef, c_b_text, c_b_comment, c_b_cdata,
1203
- c_b_pi, c_b_node, c_b_result_text, c_b_result_attr,
1215
+ c_b_pi, c_b_entity_ref, c_b_node, c_b_result_text,
1216
+ c_b_result_attr,
1204
1217
  c_b_attr, c_ffi_pointer, c_b_document, c_b_freed;
1205
1218
  static VALUE c_iteration_scope;
1206
1219
  static ID id_ptr_new;
@@ -1217,6 +1230,7 @@ static void resolve_binding_classes(void)
1217
1230
  c_b_comment = rb_path2class("Leptris::XML::Comment");
1218
1231
  c_b_cdata = rb_path2class("Leptris::XML::CDATA");
1219
1232
  c_b_pi = rb_path2class("Leptris::XML::ProcessingInstruction");
1233
+ c_b_entity_ref = rb_path2class("Leptris::XML::EntityReference");
1220
1234
  c_b_node = rb_path2class("Leptris::XML::Node");
1221
1235
  c_b_result_text = rb_path2class("Leptris::XML::ResultText");
1222
1236
  c_b_result_attr = rb_path2class("Leptris::XML::ResultAttr");
@@ -1225,12 +1239,14 @@ static void resolve_binding_classes(void)
1225
1239
  c_b_document = rb_path2class("Leptris::XML::Document");
1226
1240
  c_b_freed = rb_path2class("Leptris::XML::Document::Freed");
1227
1241
  c_iteration_scope = rb_path2class("Leptris::XML::IterationScope");
1242
+ c_leptris_error = rb_path2class("Leptris::XML::Error");
1228
1243
  id_ptr_new = rb_intern("new");
1229
1244
  rb_gc_register_mark_object(c_b_element);
1230
1245
  rb_gc_register_mark_object(c_b_text);
1231
1246
  rb_gc_register_mark_object(c_b_comment);
1232
1247
  rb_gc_register_mark_object(c_b_cdata);
1233
1248
  rb_gc_register_mark_object(c_b_pi);
1249
+ rb_gc_register_mark_object(c_b_entity_ref);
1234
1250
  rb_gc_register_mark_object(c_b_node);
1235
1251
  rb_gc_register_mark_object(c_b_result_text);
1236
1252
  rb_gc_register_mark_object(c_b_result_attr);
@@ -1239,6 +1255,7 @@ static void resolve_binding_classes(void)
1239
1255
  rb_gc_register_mark_object(c_b_document);
1240
1256
  rb_gc_register_mark_object(c_b_freed);
1241
1257
  rb_gc_register_mark_object(c_iteration_scope);
1258
+ rb_gc_register_mark_object(c_leptris_error);
1242
1259
  }
1243
1260
 
1244
1261
  static VALUE binding_klass_for(int kind)
@@ -1249,6 +1266,7 @@ static VALUE binding_klass_for(int kind)
1249
1266
  case 2: return c_b_comment;
1250
1267
  case 3: return c_b_cdata;
1251
1268
  case 4: return c_b_pi;
1269
+ case 10: return c_b_entity_ref; /* #212 / upstream #1094 */
1252
1270
  default: return c_b_node;
1253
1271
  }
1254
1272
  }
@@ -1635,7 +1653,8 @@ static char *ser_buf;
1635
1653
  static size_t ser_cap;
1636
1654
 
1637
1655
  static VALUE fast_serialize(serialize_into_fn fn, void *node,
1638
- int indent, int xml_declaration)
1656
+ int indent, int xml_declaration,
1657
+ VALUE encoding)
1639
1658
  {
1640
1659
  struct serialize_opts opts;
1641
1660
  char stack_buf[4096];
@@ -1646,7 +1665,11 @@ static VALUE fast_serialize(serialize_into_fn fn, void *node,
1646
1665
 
1647
1666
  opts.indent = indent;
1648
1667
  opts.xml_declaration = xml_declaration;
1649
- opts.encoding = NULL;
1668
+ /* The Ruby string's bytes stay valid across the synchronous
1669
+ * engine call (TODO.perf/36). */
1670
+ opts.encoding = NIL_P(encoding)
1671
+ ? NULL
1672
+ : StringValueCStr(encoding);
1650
1673
  /* Probe the largest buffer we own: a big scratch from a prior
1651
1674
  * call usually fits (single serialization); cold calls start
1652
1675
  * on the stack buffer. */
@@ -1672,21 +1695,25 @@ static VALUE fast_serialize(serialize_into_fn fn, void *node,
1672
1695
  }
1673
1696
 
1674
1697
  static VALUE nf_fast_document_xml(VALUE self, VALUE addr,
1675
- VALUE indent, VALUE decl)
1698
+ VALUE indent, VALUE decl,
1699
+ VALUE encoding)
1676
1700
  {
1677
1701
  (void)self;
1678
1702
  return fast_serialize(f_doc_serialize,
1679
1703
  (void *)(uintptr_t)NUM2ULL(addr),
1680
- NUM2INT(indent), RTEST(decl) ? 1 : 0);
1704
+ NUM2INT(indent), RTEST(decl) ? 1 : 0,
1705
+ encoding);
1681
1706
  }
1682
1707
 
1683
1708
  static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
1684
- VALUE indent, VALUE decl)
1709
+ VALUE indent, VALUE decl,
1710
+ VALUE encoding)
1685
1711
  {
1686
1712
  (void)self;
1687
1713
  return fast_serialize(f_elem_serialize,
1688
1714
  (void *)(uintptr_t)NUM2ULL(addr),
1689
- NUM2INT(indent), RTEST(decl) ? 1 : 0);
1715
+ NUM2INT(indent), RTEST(decl) ? 1 : 0,
1716
+ encoding);
1690
1717
  }
1691
1718
 
1692
1719
  /* ---- inner_html in one C pass (TODO.perf/18) --------------------
@@ -1861,8 +1888,6 @@ static VALUE nf_copy_binding_element(VALUE self, VALUE document,
1861
1888
  return Qnil;
1862
1889
  doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
1863
1890
  new_doc = rb_obj_alloc(c_b_document);
1864
- rb_iv_set(new_doc, "@c_ptr",
1865
- rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
1866
1891
  rb_iv_set(new_doc, "@c_address", doc_addr);
1867
1892
  freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
1868
1893
  rb_iv_set(new_doc, "@freed", freed);
@@ -1909,6 +1934,23 @@ static VALUE build_binding_document(void *doc)
1909
1934
  return new_doc;
1910
1935
  }
1911
1936
 
1937
+ /* check_status in C (TODO.perf/36): the exact status_message
1938
+ * format — "base" or "base (detail)" — so the mutation faces
1939
+ * raise instead of returning codes for a Ruby dispatch. */
1940
+ static void check_status_c(int st)
1941
+ {
1942
+ const char *base, *detail;
1943
+
1944
+ if (st == 0)
1945
+ return;
1946
+ base = f_status_str(st);
1947
+ detail = f_last_err();
1948
+ if (detail && *detail)
1949
+ rb_raise(c_leptris_error, "%s (%s)",
1950
+ base ? base : "?", detail);
1951
+ rb_raise(c_leptris_error, "%s", base ? base : "?");
1952
+ }
1953
+
1912
1954
  /* Document.parse default path in one dispatch (TODO.perf/35):
1913
1955
  * leptris_parse_string + wrapper + handle. Qnil = parse failure
1914
1956
  * (the caller raises with the same message shape). */
@@ -1974,7 +2016,7 @@ static VALUE nf_append_markup(VALUE self, VALUE document,
1974
2016
  next_child = f_next_sibling(child);
1975
2017
  status = f_append_child(parent, child);
1976
2018
  if (status != 0)
1977
- return INT2FIX(-1000 - status);
2019
+ check_status_c(status);
1978
2020
  count++;
1979
2021
  }
1980
2022
  return INT2FIX(count);
@@ -2231,7 +2273,7 @@ void Init_native(void)
2231
2273
  nf_bulk_element_children, 2);
2232
2274
  rb_define_module_function(m_native, "bulk_xpath", nf_bulk_xpath, 2);
2233
2275
  rb_define_module_function(m_native, "fast_document_xml",
2234
- nf_fast_document_xml, 3);
2276
+ nf_fast_document_xml, 4);
2235
2277
  rb_define_module_function(m_native, "fast_element_xml",
2236
- nf_fast_element_xml, 3);
2278
+ nf_fast_element_xml, 4);
2237
2279
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.174.9"
4
+ VERSION = "1.9.177.0"
5
5
  end
@@ -3,7 +3,6 @@
3
3
  require "ffi"
4
4
 
5
5
  class Leptris::XML::Document
6
- attr_reader :c_ptr
7
6
 
8
7
  # @api private
9
8
  # Internal flag container shared between the Document instance and its
@@ -57,6 +56,19 @@ class Leptris::XML::Document
57
56
  # document for trees that are freed before any wrap.
58
57
  end
59
58
 
59
+ # TODO.perf/37: the Integer address is canonical; the Pointer
60
+ # materializes only when read (parse/create faces skip it).
61
+ # A freed document answers nil — never re-materialize a stale
62
+ # or absent address.
63
+ def c_ptr
64
+ @c_ptr ||= (@c_address && ::FFI::Pointer.new(@c_address))
65
+ end
66
+
67
+ # The Integer twin of #c_ptr.
68
+ def c_address
69
+ @c_address
70
+ end
71
+
60
72
  def wrapper_cache
61
73
  @wrapper_cache ||= {}
62
74
  end
@@ -308,8 +320,7 @@ class Leptris::XML::Document
308
320
  # dispatch (the FFI call plus the c_ptr materialization fed
309
321
  # ~5% of a fresh-doc build).
310
322
  if defined?(Leptris::XML::NATIVE_FAST)
311
- Leptris::XML::FFI.check_status(
312
- Leptris::XML::Native.set_binding_root(self, element.c_address))
323
+ Leptris::XML::Native.set_binding_root(self, element.c_address)
313
324
  else
314
325
  Leptris::XML::FFI.check_status(
315
326
  Leptris::XML::FFI.leptris_document_set_root(c_ptr, element.c_ptr))
@@ -494,15 +505,82 @@ class Leptris::XML::Document
494
505
  Leptris::XML::Node.wrap(ptr, self)
495
506
  end
496
507
 
497
- # Append a document-level processing instruction. Returns self.
508
+ # Append a document-level processing instruction. Returns the PI
509
+ # node (epilog anchor — after the root; identity via
510
+ # leptris_document_append_pi, libleptris 1.9.176 / #212).
498
511
  def add_pi(target, data = "")
499
- witness = Leptris::XML::FFI.leptris_document_add_pi(
512
+ witness = Leptris::XML::FFI.leptris_document_append_pi(
500
513
  c_ptr, target.to_s, data.to_s)
501
- raise Leptris::XML::Error, "leptris_document_add_pi failed" if witness.null?
514
+ raise Leptris::XML::Error, "leptris_document_append_pi failed" if witness.null?
515
+ @version += 1
516
+ Leptris::XML::Node.wrap(witness, self)
517
+ end
518
+
519
+ # Remove a document-level PI or comment from the document
520
+ # children chain (libleptris 1.9.176 / #212). The node stays
521
+ # document-owned. Raises when the node is not in the chain.
522
+ def remove_child(node)
523
+ Leptris::XML::FFI.check_status(
524
+ Leptris::XML::FFI.leptris_document_remove_child(c_ptr, node.c_ptr))
502
525
  @version += 1
503
526
  self
504
527
  end
505
528
 
529
+ # Create an &name; entity reference node (libleptris 1.9.176 /
530
+ # #212): serializes back verbatim once attached.
531
+ def create_entity_reference(name)
532
+ ptr = Leptris::XML::FFI.leptris_entity_ref_node_create(
533
+ c_ptr, name.to_s)
534
+ raise Leptris::XML::Error, "leptris_entity_ref_node_create failed" if ptr.null?
535
+ Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_ENTITY_REF)
536
+ end
537
+
538
+ # The XML declaration (libleptris 1.9.176 / #212). `#version`
539
+ # stays the mutation counter — the declaration reads are
540
+ # xml_-prefixed.
541
+ def xml_version
542
+ Leptris::XML::FFI.leptris_document_version(c_ptr)
543
+ end
544
+
545
+ def xml_standalone
546
+ v = Leptris::XML::FFI.leptris_document_standalone(c_ptr)
547
+ v < 0 ? nil : (v == 1)
548
+ end
549
+
550
+ def xml_encoding
551
+ Leptris::XML::FFI.leptris_document_encoding(c_ptr)
552
+ end
553
+
554
+ def xml_version=(value)
555
+ Leptris::XML::FFI.check_status(
556
+ Leptris::XML::FFI.leptris_document_set_version(c_ptr, value.to_s))
557
+ value
558
+ end
559
+
560
+ def xml_encoding=(value)
561
+ Leptris::XML::FFI.check_status(
562
+ Leptris::XML::FFI.leptris_document_set_encoding(c_ptr, value.to_s))
563
+ value
564
+ end
565
+
566
+ def xml_standalone=(value)
567
+ Leptris::XML::FFI.check_status(
568
+ Leptris::XML::FFI.leptris_document_set_standalone(c_ptr,
569
+ value.nil? ? -1 : (value ? 1 : 0)))
570
+ value
571
+ end
572
+
573
+ # Programmatic DOCTYPE (libleptris 1.9.176 / #212): create and
574
+ # set the document's DOCTYPE (root name + optional PUBLIC/SYSTEM
575
+ # ids); serializes in document position. Returns the DocType.
576
+ def set_doctype(name, public_id: nil, system_id: nil)
577
+ dt = Leptris::XML::FFI.leptris_document_set_doctype(
578
+ c_ptr, name.to_s, public_id, system_id)
579
+ raise Leptris::XML::Error, "leptris_document_set_doctype failed" if dt.null?
580
+ @version += 1
581
+ Leptris::XML::DocType.new(dt, self)
582
+ end
583
+
506
584
  # Append a document-level comment (epilog position, after the
507
585
  # root element) — the add_pi twin (libleptris 1.9.160, #1032;
508
586
  # parsed document-level comments round-tripped since 1.9.3/#578,
@@ -25,9 +25,8 @@ class Leptris::XML::Element < Leptris::XML::Node
25
25
 
26
26
  def name=(new_name)
27
27
  if @native_fast
28
- Leptris::XML::FFI.check_status(
29
- Leptris::XML::Native.set_binding_name(
30
- @document, @c_address, new_name))
28
+ Leptris::XML::Native.set_binding_name(
29
+ @document, @c_address, new_name)
31
30
  return @name = new_name
32
31
  end
33
32
  ensure_writable!
@@ -54,9 +53,8 @@ class Leptris::XML::Element < Leptris::XML::Node
54
53
 
55
54
  def content=(new_content)
56
55
  if @native_fast
57
- Leptris::XML::FFI.check_status(
58
- Leptris::XML::Native.set_binding_text(
59
- @document, @c_address, new_content.to_s))
56
+ Leptris::XML::Native.set_binding_text(
57
+ @document, @c_address, new_content.to_s)
60
58
  @content = new_content.to_s
61
59
  @content_version = @document.version if @document
62
60
  return new_content
@@ -138,9 +136,8 @@ class Leptris::XML::Element < Leptris::XML::Node
138
136
  # set in one dispatch; the bump drops the version-stamped
139
137
  # attribute memos on both surfaces.
140
138
  if native_fast_children?
141
- Leptris::XML::FFI.check_status(
142
- Leptris::XML::Native.set_binding_attribute(
143
- @document, @c_address, key.to_s, value.to_s))
139
+ Leptris::XML::Native.set_binding_attribute(
140
+ @document, @c_address, key.to_s, value.to_s)
144
141
  seed_attribute_memo(key.to_s, value.to_s)
145
142
  return value
146
143
  end
@@ -374,10 +371,8 @@ class Leptris::XML::Element < Leptris::XML::Node
374
371
  # bump + engine insert in one dispatch; Qnil = the child needs
375
372
  # the namespace lift — the full path below handles it.
376
373
  if native_fast_children?
377
- st = Leptris::XML::Native.insert_binding_child(
378
- @document, @c_address, node.c_ptr.address, 1)
379
- unless st.nil?
380
- Leptris::XML::FFI.check_status(st)
374
+ unless Leptris::XML::Native.insert_binding_child(
375
+ @document, @c_address, node.c_ptr.address, 1).nil?
381
376
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
382
377
  return node
383
378
  end
@@ -397,10 +392,8 @@ class Leptris::XML::Element < Leptris::XML::Node
397
392
  # bump + engine insert in one dispatch; Qnil = the child needs
398
393
  # the namespace lift — the full path below handles it.
399
394
  if native_fast_children?
400
- st = Leptris::XML::Native.insert_binding_child(
401
- @document, @c_address, node.c_ptr.address, 2)
402
- unless st.nil?
403
- Leptris::XML::FFI.check_status(st)
395
+ unless Leptris::XML::Native.insert_binding_child(
396
+ @document, @c_address, node.c_ptr.address, 2).nil?
404
397
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
405
398
  return node
406
399
  end
@@ -420,10 +413,8 @@ class Leptris::XML::Element < Leptris::XML::Node
420
413
  # bump + engine insert in one dispatch; Qnil = the child needs
421
414
  # the namespace lift — the full path below handles it.
422
415
  if native_fast_children?
423
- st = Leptris::XML::Native.insert_binding_child(
424
- @document, @c_address, node.c_ptr.address, 3)
425
- unless st.nil?
426
- Leptris::XML::FFI.check_status(st)
416
+ unless Leptris::XML::Native.insert_binding_child(
417
+ @document, @c_address, node.c_ptr.address, 3).nil?
427
418
  Leptris::XML::Node.invalidate_cross_document!(node, @document)
428
419
  return node
429
420
  end
@@ -539,10 +530,10 @@ class Leptris::XML::Element < Leptris::XML::Node
539
530
  # means the child needs the namespace lift — fall through
540
531
  # to the full path.
541
532
  if native_fast_children?
542
- st = Leptris::XML::Native.append_binding_child(
543
- @document, @c_address, node_or_markup.c_ptr.address)
544
- unless st.nil?
545
- Leptris::XML::FFI.check_status(st)
533
+ # The face raises on failure (TODO.perf/36); Qnil = the
534
+ # child needs the lift — the full path handles it.
535
+ unless Leptris::XML::Native.append_binding_child(
536
+ @document, @c_address, node_or_markup.c_ptr.address).nil?
546
537
  Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
547
538
  return node_or_markup
548
539
  end
@@ -562,17 +553,15 @@ class Leptris::XML::Element < Leptris::XML::Node
562
553
  if native_fast_children?
563
554
  r = Leptris::XML::Native.append_markup(
564
555
  @document, @c_address, node_or_markup)
565
- if r.is_a?(Integer) && r >= 0
556
+ if r != -1
557
+ # Appends raise in C on status failures (TODO.perf/36);
558
+ # -1 alone means the parse failed.
566
559
  return Leptris::XML::NodeSet.new(@document, []) if r.zero?
567
560
  return Leptris::XML::NodeSet.new(@document, last_added(r))
568
561
  end
569
- if r == -1
570
- # parse failure: reproduce the exact error via the
571
- # legacy path (it re-fails fast)
572
- Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
573
- else
574
- Leptris::XML::FFI.check_status(-r - 1000)
575
- end
562
+ # parse failure: reproduce the exact error via the legacy
563
+ # path (it re-fails fast)
564
+ Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
576
565
  end
577
566
  frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
578
567
  added = []
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # An unexpanded &name; entity reference — a first-class node when
4
+ # parsing with ParseOptions.keep_entity_refs (libleptris 1.9.176,
5
+ # upstream #1094 / leptris-ruby#212), or created via
6
+ # Document#create_entity_reference. Serializes back verbatim as
7
+ # &name;; text runs around it split into their own Text siblings.
8
+ class Leptris::XML::EntityReference < Leptris::XML::Node
9
+ def name
10
+ Leptris::XML::FFI.leptris_entity_ref_node_name(c_ptr)
11
+ end
12
+ alias_method :node_name, :name
13
+ end
@@ -337,6 +337,29 @@ attach_function :leptris_parse_string,
337
337
  # node_first_child without the intermediate handle).
338
338
  attach_function :leptris_document_first_child,
339
339
  [:leptris_document], :leptris_node_ref
340
+ # Node-surface parity (libleptris 1.9.176, upstream #1094,
341
+ # leptris-ruby#212): entity references, the XML declaration,
342
+ # programmatic DOCTYPE, and document-PI identity/removal.
343
+ attach_function :leptris_entity_ref_node_create,
344
+ [:leptris_document, :string], :leptris_node_ref
345
+ attach_function :leptris_entity_ref_node_name,
346
+ [:leptris_node_ref], :string
347
+ attach_function :leptris_document_version,
348
+ [:leptris_document], :string
349
+ attach_function :leptris_document_standalone,
350
+ [:leptris_document], :int
351
+ attach_function :leptris_document_set_version,
352
+ [:leptris_document, :string], :leptris_status
353
+ attach_function :leptris_document_set_encoding,
354
+ [:leptris_document, :string], :leptris_status
355
+ attach_function :leptris_document_set_standalone,
356
+ [:leptris_document, :int], :leptris_status
357
+ attach_function :leptris_document_set_doctype,
358
+ [:leptris_document, :string, :string, :string], :leptris_doctype
359
+ attach_function :leptris_document_append_pi,
360
+ [:leptris_document, :string, :string], :leptris_node_ref
361
+ attach_function :leptris_document_remove_child,
362
+ [:leptris_document, :leptris_node_ref], :leptris_status
340
363
  # Wrap-free subtree visitation (1.9.20, upstream #645a):
341
364
  # enter/leave pairs for elements, depth from the walk's root,
342
365
  # one C call — no NodeSet/Array churn per level. The document
@@ -1070,11 +1093,15 @@ attach_function :leptris_parse_string,
1070
1093
  NODE_COMMENT = 2
1071
1094
  NODE_CDATA = 3
1072
1095
  NODE_PI = 4
1096
+ NODE_ENTITY_REF = 10
1073
1097
  # Internal XPath-synthetic text node (leptris/types.h): the
1074
1098
  # carrier for sequence/map/array items in result nodesets —
1075
1099
  # readable only through the result handle, so the binding
1076
1100
  # captures the value at materialization (ResultText).
1077
1101
  NODE_SYNTHETIC_TEXT = 8
1102
+ # #1094: &name; references stay unexpanded first-class nodes
1103
+ # when this flag parses (character references still expand).
1104
+ LEPTRIS_PARSE_KEEP_ENTITY_REFS = 4
1078
1105
  NODE_DOCTYPE = 5
1079
1106
  NODE_ATTRIBUTE = 6
1080
1107
 
@@ -132,6 +132,8 @@ class Leptris::XML::Node
132
132
  Leptris::XML::CDATA.new(c_ptr, document, parent: parent, node_type: node_type)
133
133
  when Leptris::XML::FFI::NODE_PI
134
134
  Leptris::XML::ProcessingInstruction.new(c_ptr, document, parent: parent, node_type: node_type)
135
+ when Leptris::XML::FFI::NODE_ENTITY_REF
136
+ Leptris::XML::EntityReference.new(c_ptr, document, parent: parent, node_type: node_type)
135
137
  else
136
138
  new(c_ptr, document, parent: parent, node_type: node_type)
137
139
  end
@@ -486,8 +488,7 @@ class Leptris::XML::Node
486
488
 
487
489
  def unlink
488
490
  if @native_fast
489
- Leptris::XML::FFI.check_status(
490
- Leptris::XML::Native.unlink_binding_node(@document, @c_address))
491
+ Leptris::XML::Native.unlink_binding_node(@document, @c_address)
491
492
  @parent = nil
492
493
  return self
493
494
  end
@@ -17,6 +17,13 @@ class Leptris::XML::ParseOptions
17
17
  # defaulted attributes.
18
18
  DTDATTR = Leptris::XML::FFI::LEPTRIS_PARSE_DTDATTR
19
19
 
20
+ # Keep &name; entity references unexpanded as first-class
21
+ # EntityReference nodes (libleptris 1.9.176, upstream #1094 /
22
+ # leptris-ruby#212): text runs split around them, character
23
+ # references still expand, and the references serialize back
24
+ # verbatim.
25
+ KEEP_ENTITY_REFS = Leptris::XML::FFI::LEPTRIS_PARSE_KEEP_ENTITY_REFS
26
+
20
27
  attr_reader :flags
21
28
 
22
29
  # Recover (libleptris 1.9.0, #547): a parse failure returns an
@@ -32,6 +39,10 @@ class Leptris::XML::ParseOptions
32
39
  @recover = recover ? true : false
33
40
  end
34
41
 
42
+ def self.keep_entity_refs
43
+ new(KEEP_ENTITY_REFS)
44
+ end
45
+
35
46
  def self.noblanks
36
47
  new(NOBLANKS)
37
48
  end
@@ -52,6 +63,10 @@ class Leptris::XML::ParseOptions
52
63
  @flags & DTDATTR != 0
53
64
  end
54
65
 
66
+ def keep_entity_refs?
67
+ @flags & KEEP_ENTITY_REFS != 0
68
+ end
69
+
55
70
  def dtdattr=(value)
56
71
  if value then @flags |= DTDATTR else @flags &= ~DTDATTR end
57
72
  end
@@ -55,6 +55,10 @@ module Leptris::XML::Searchable
55
55
  expr = paths.one? && paths.first.is_a?(String) ? paths.first
56
56
  : paths.join(" | ")
57
57
 
58
+ # TODO.perf/37: the document's Pointer is lazy (materializes
59
+ # once per document lifetime, not per parse); the ffi gem
60
+ # does NOT convert Integers for typedef'd pointer params, so
61
+ # the Pointer object is still what crosses here.
58
62
  doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
59
63
  context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
60
64
 
@@ -100,6 +104,10 @@ module Leptris::XML::Searchable
100
104
  expr = paths.one? && paths.first.is_a?(String) ? paths.first
101
105
  : paths.join(" | ")
102
106
 
107
+ # TODO.perf/37: the document's Pointer is lazy (materializes
108
+ # once per document lifetime, not per parse); the ffi gem
109
+ # does NOT convert Integers for typedef'd pointer params, so
110
+ # the Pointer object is still what crosses here.
103
111
  doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
104
112
  context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
105
113
 
@@ -32,16 +32,16 @@ module Leptris::XML::Serialization
32
32
  # cycle lives at the FFI seam (FFI.serialize_into_string); this
33
33
  # module owns only options selection and construction.
34
34
  def self.to_xml(ffi_function, c_ptr, indent: 0, no_decl: false, encoding: nil)
35
- # TODO.perf/04: the common shape (no encoding) runs the whole
36
- # sized-buffer cycle in the ext — one C dispatch, no options
37
- # marshaling, no Ruby buffer management.
38
- if defined?(Leptris::XML::NATIVE_FAST) && encoding.nil?
35
+ # TODO.perf/04+36: the whole sized-buffer cycle in the ext —
36
+ # one C dispatch, no options marshaling, no Ruby buffer
37
+ # management; encoding rides the face (nil -> no override).
38
+ if defined?(Leptris::XML::NATIVE_FAST)
39
39
  decl = !no_decl
40
40
  return Leptris::XML::Native.fast_element_xml(
41
- c_ptr.address, indent.to_i, decl) if
41
+ c_ptr.address, indent.to_i, decl, encoding) if
42
42
  ffi_function == ELEMENT_SERIALIZE_INTO
43
43
  return Leptris::XML::Native.fast_document_xml(
44
- c_ptr.address, indent.to_i, decl) if
44
+ c_ptr.address, indent.to_i, decl, encoding) if
45
45
  ffi_function == DOCUMENT_SERIALIZE_INTO
46
46
  end
47
47
  opts =
@@ -59,7 +59,8 @@ module Leptris::XML::Serialization
59
59
  # loop: no kwargs, no options rebuild, no method allocation —
60
60
  # one FFI fast-path dispatch per child.
61
61
  def self.element_xml_default(c_ptr)
62
- return Leptris::XML::Native.fast_element_xml(c_ptr.address, 0, true) if
62
+ return Leptris::XML::Native.fast_element_xml(c_ptr.address, 0, true,
63
+ nil) if
63
64
  defined?(Leptris::XML::NATIVE_FAST)
64
65
  Leptris::XML::FFI.serialize_into_string(
65
66
  ELEMENT_SERIALIZE_INTO, c_ptr, DEFAULT_OPTIONS.pointer)
@@ -20,9 +20,8 @@ class Leptris::XML::Text < Leptris::XML::Node
20
20
 
21
21
  def content=(new_content)
22
22
  if @native_fast
23
- Leptris::XML::FFI.check_status(
24
- Leptris::XML::Native.set_binding_text(
25
- @document, @c_address, new_content.to_s))
23
+ Leptris::XML::Native.set_binding_text(
24
+ @document, @c_address, new_content.to_s)
26
25
  return new_content
27
26
  end
28
27
  ensure_writable!
data/lib/leptris/xml.rb CHANGED
@@ -9,6 +9,7 @@ module Leptris
9
9
  autoload :Comment, "leptris/xml/comment"
10
10
  autoload :CDATA, "leptris/xml/cdata"
11
11
  autoload :ProcessingInstruction, "leptris/xml/processing_instruction"
12
+ autoload :EntityReference, "leptris/xml/entity_reference"
12
13
  autoload :Attr, "leptris/xml/attr"
13
14
  autoload :Namespace, "leptris/xml/namespace"
14
15
  autoload :Document, "leptris/xml/document"
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.174.9
4
+ version: 1.9.177.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -104,6 +104,8 @@ files:
104
104
  - TODO.perf/33-cbound-root-set.md
105
105
  - TODO.perf/34-fragment-fast-lane.md
106
106
  - TODO.perf/35-parse-default-c-face.md
107
+ - TODO.perf/36-raise-in-c-serializer-encoding.md
108
+ - TODO.perf/37-document-lazy-pointer.md
107
109
  - TODO.restructure/01-constraint-compliance-audit.md
108
110
  - TODO.restructure/02-deep-copy-seam.md
109
111
  - TODO.restructure/03-evaluation-context-seam.md
@@ -162,6 +164,7 @@ files:
162
164
  - lib/leptris/xml/document.rb
163
165
  - lib/leptris/xml/document_fragment.rb
164
166
  - lib/leptris/xml/element.rb
167
+ - lib/leptris/xml/entity_reference.rb
165
168
  - lib/leptris/xml/evaluation_context.rb
166
169
  - lib/leptris/xml/ffi.rb
167
170
  - lib/leptris/xml/iteration_scope.rb