leptris 1.9.222.2-arm-linux → 1.9.225.0-arm-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/Rakefile +1 -1
  4. data/ext/leptris/native/native.c +51 -44
  5. data/lib/leptris/version.rb +1 -1
  6. data/lib/leptris/xml/document.rb +12 -4
  7. data/lib/leptris/xml/element.rb +1 -1
  8. data/lib/leptris/xml/ffi.rb +27 -1
  9. data/lib/leptris/xml/native.so +0 -0
  10. data/lib/leptris/xml/serialization.rb +27 -0
  11. data/lib/leptris/xml.rb +0 -1
  12. data/lib/libleptris.so +0 -0
  13. data/vendor-src/README.md +1 -1
  14. data/vendor-src/libleptris/CHANGELOG.md +73 -0
  15. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  16. data/vendor-src/libleptris/src/CMakeLists.txt +2 -0
  17. data/vendor-src/libleptris/src/include/leptris/sax/sax.h +87 -0
  18. data/vendor-src/libleptris/src/include/leptris/types.h +7 -1
  19. data/vendor-src/libleptris/src/include/leptris.h +49 -2
  20. data/vendor-src/libleptris/src/leptris/dom/digest.c +11 -0
  21. data/vendor-src/libleptris/src/leptris/dom/elem_pos.c +88 -0
  22. data/vendor-src/libleptris/src/leptris/dom/element.c +2 -1
  23. data/vendor-src/libleptris/src/leptris/dom/element.h +89 -56
  24. data/vendor-src/libleptris/src/leptris/dom/element_query.c +6 -5
  25. data/vendor-src/libleptris/src/leptris/dom/node_public.c +10 -23
  26. data/vendor-src/libleptris/src/leptris/dom/text.c +14 -79
  27. data/vendor-src/libleptris/src/leptris/dom/text.h +21 -33
  28. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +150 -58
  29. data/vendor-src/libleptris/src/leptris/flat/il_scan.h +69 -0
  30. data/vendor-src/libleptris/src/leptris/html/html_parse.c +3 -0
  31. data/vendor-src/libleptris/src/leptris/leptris.c +10 -0
  32. data/vendor-src/libleptris/src/leptris/leptris_internal.h +9 -0
  33. data/vendor-src/libleptris/src/leptris/sax/records.c +135 -0
  34. data/vendor-src/libleptris/src/leptris/serialize/serialize.c +61 -47
  35. data/vendor-src/libleptris/src/leptris/xslt/xslt_exec.c +3 -8
  36. data/vendor-src/libleptris/test/CMakeLists.txt +2 -0
  37. data/vendor-src/libleptris/test/dom/test_digest.cpp +49 -0
  38. data/vendor-src/libleptris/test/dom/test_text_borrowed.cpp +51 -40
  39. data/vendor-src/libleptris/test/html/test_html.cpp +23 -9
  40. data/vendor-src/libleptris/test/html/test_html_builder.cpp +190 -0
  41. data/vendor-src/libleptris/test/perf/test_perf.cpp +28 -17
  42. data/vendor-src/libleptris/test/sax/test_records.cpp +196 -0
  43. data/vendor-src/libleptris/vcpkg.json +1 -1
  44. metadata +7 -3
  45. data/lib/leptris/xml/html_serialize.rb +0 -97
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2da4a19cbca71331b31eb5eacc93924035fc1f4051bde8361b127c318354cb63
4
- data.tar.gz: c794ac2ce4219f5b97ea0264b766c6e2caa9fb7acf35a3254eaa4bc62744ae35
3
+ metadata.gz: 3ddb9c850703c6cc75a0d22649d3065ed7535d15e3b0b66db6a7aa8af4d73286
4
+ data.tar.gz: bcf50f7e810b2caa0405f5e9e0043f4682e50c7bf7c03897a74c8731caf05c6a
5
5
  SHA512:
6
- metadata.gz: 7a7acc7d9a8828a9d4bf441240bfb0312f464e5e3556c554af119a91626373babd03fa9ab7087433936f6fdd2a0c0c648c9dd1f4d8507ef5a8b72abc36c9530a
7
- data.tar.gz: 24a2ba0806bd7fcc23dfac774b6c90c6ff9ef93082140421070fc4210abde3835c4133615726a37efda58753ae678c8760b74932cdb0bbd068479727de6c1b85
6
+ metadata.gz: 7c463cf68fb31d8ab1a1b02ec65a15190008d27700326f0b30698268a97fca76176516ef9e5f92a891b24f92b607a6c10d6a8a764e122151c976f1fc54e737ec
7
+ data.tar.gz: cb509afb2b2be7edcdf3502289865febbc607a938e07ce35a1449a047c71240c4c587fe8e6150051bbba6f271526205be0681ea2a14b8ff32f1e7437b98320fa
data/CHANGELOG.md CHANGED
@@ -5,6 +5,33 @@ 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.225.0] - 2026-09-22
9
+
10
+ ### Added — native HTML serialization, construction, and the bulk SAX drain (engine 1.9.223–225)
11
+
12
+ - **`to_html` re-pointed at the engine's native HTML mode**
13
+ (1.9.225, upstream #1309/#1311 — our filing answered in one
14
+ release): `Document#to_html` → `leptris_document_serialize_html`,
15
+ `Element#to_html` → the ext serializer with `html_method: 1`.
16
+ Byte-parity with the interim Ruby walk is pinned by the #309
17
+ spec battery; the native path also fixes the walk's PI branch
18
+ (the Ruby walk crashed on processing instructions — target/data
19
+ readers don't exist on the wrapper). The Ruby walk is removed.
20
+ - **`Document#save(path, mode: :html)`** → `leptris_document_save_html`.
21
+ - **`leptris_document_create_html` attached** (native HTML-mode
22
+ documents — serialize AS HTML by default, Nokogiri behavior);
23
+ the facade's `create` stays on the XML base + doctype preset so
24
+ `to_xml` output on facade docs is unchanged. `html_method`
25
+ (tri-state) rides `SerializeExtStruct` — `to_xml` on an HTML
26
+ document can force XML with `-1` when the face needs it.
27
+ - **Bulk SAX record drain attached** (1.9.223, upstream #1298,
28
+ Canon-reported): `leptris_sax_records_*` — one call yields the
29
+ whole document as a flat record table (tree edges, source
30
+ offsets, flat attribute table with §3.3.3 normalization flags).
31
+ Attach-only until the SAX face wants the bulk lane.
32
+ - 1.9.224 rides along (LeakSanitizer fix on the direct-parse
33
+ failure path). Audit 351/351, suite 780/0.
34
+
8
35
  ## [1.9.222.0] - 2026-09-22
9
36
 
10
37
  ### Fixed — engine sync (1.9.222): the interleaved lane's namespace regression
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.222"
11
+ LIBLEPTRIS_VERSION = "1.9.225"
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
@@ -135,6 +135,9 @@ static VALUE c_iteration_scope;
135
135
  static VALUE c_leptris_error;
136
136
  static VALUE c_b_document, c_b_freed, c_ffi_pointer;
137
137
  static ID id_ptr_new, id_freed_new, id_alive;
138
+ static ID id_iv_c_address, id_iv_document, id_iv_parent;
139
+ static ID id_iv_structure_memoizable, id_iv_native_fast;
140
+ static ID id_iv_pub_document, id_iv_addr_reads_fast, id_iv_node_type;
138
141
  static void check_status_c(int st);
139
142
 
140
143
  #define NT_ELEMENT 0
@@ -839,8 +842,38 @@ struct trav_state {
839
842
  VALUE err;
840
843
  void *self_ptr;
841
844
  int for_visit;
845
+ /* Hoisted per-walk state (was re-resolved per node): the
846
+ * document's wrapper cache and the iteration-scope class
847
+ * check. #312 item 1. */
848
+ VALUE cache;
849
+ VALUE klass_memo_flag;
842
850
  };
843
851
 
852
+ /* Construct-or-fetch the binding wrapper for node_ptr against the
853
+ * per-walk hoisted cache (#312 item 1): static-ID ivar writes (no
854
+ * per-call string interning), the scope class check precomputed. */
855
+ static VALUE trav_wrapper_for(struct trav_state *st, void *node_ptr,
856
+ int kind)
857
+ {
858
+ VALUE key = ULL2NUM((uint64_t)(uintptr_t)node_ptr);
859
+ VALUE node = rb_hash_aref(st->cache, key);
860
+ if (!NIL_P(node)) return node;
861
+
862
+ node = rb_obj_alloc(binding_klass_for(kind));
863
+ rb_ivar_set(node, id_iv_c_address, key);
864
+ rb_ivar_set(node, id_iv_document, st->document);
865
+ rb_ivar_set(node, id_iv_parent, Qnil);
866
+ VALUE fast = st->klass_memo_flag;
867
+ rb_ivar_set(node, id_iv_structure_memoizable, fast);
868
+ rb_ivar_set(node, id_iv_native_fast, fast);
869
+ rb_ivar_set(node, id_iv_pub_document,
870
+ fast == Qtrue ? st->document : Qnil);
871
+ rb_ivar_set(node, id_iv_addr_reads_fast, Qtrue);
872
+ rb_ivar_set(node, id_iv_node_type, INT2FIX(kind));
873
+ rb_hash_aset(st->cache, key, node);
874
+ return node;
875
+ }
876
+
844
877
  static VALUE trav_yield_one(VALUE arg)
845
878
  {
846
879
  return rb_yield(arg);
@@ -849,33 +882,13 @@ static VALUE trav_yield_one(VALUE arg)
849
882
  static int trav_cb(void *node_ptr, void *user)
850
883
  {
851
884
  struct trav_state *st = user;
852
- VALUE cache, key, node;
885
+ VALUE node;
853
886
  int kind, state;
854
887
 
855
888
  if (st->err != Qnil)
856
889
  return 1;
857
890
  kind = f_node_type(node_ptr);
858
- cache = binding_cache_of(st->document);
859
- key = ULL2NUM((uint64_t)(uintptr_t)node_ptr);
860
- node = rb_hash_aref(cache, key);
861
- if (NIL_P(node)) {
862
- node = rb_obj_alloc(binding_klass_for(kind));
863
- rb_iv_set(node, "@c_address", key);
864
- rb_iv_set(node, "@document", st->document);
865
- rb_iv_set(node, "@parent", Qnil);
866
- if (rb_obj_class(st->document) == c_iteration_scope) {
867
- rb_iv_set(node, "@structure_memoizable", Qfalse);
868
- rb_iv_set(node, "@native_fast", Qfalse);
869
- rb_iv_set(node, "@pub_document", Qnil);
870
- } else {
871
- rb_iv_set(node, "@structure_memoizable", Qtrue);
872
- rb_iv_set(node, "@native_fast", Qtrue);
873
- rb_iv_set(node, "@pub_document", st->document);
874
- }
875
- rb_iv_set(node, "@addr_reads_fast", Qtrue);
876
- rb_iv_set(node, "@node_type", INT2FIX(kind));
877
- rb_hash_aset(cache, key, node);
878
- }
891
+ node = trav_wrapper_for(st, node_ptr, kind);
879
892
  rb_protect(trav_yield_one, node, &state);
880
893
  if (state) {
881
894
  st->err = rb_errinfo();
@@ -894,6 +907,9 @@ static VALUE nf_traverse_binding(VALUE self, VALUE document,
894
907
  st.document = document;
895
908
  st.err = Qnil;
896
909
  st.self_ptr = (void *)(uintptr_t)NUM2ULL(addr);
910
+ st.cache = binding_cache_of(document);
911
+ st.klass_memo_flag =
912
+ rb_obj_class(document) == c_iteration_scope ? Qfalse : Qtrue;
897
913
  st.for_visit = 0;
898
914
  f_node_traverse(st.self_ptr, 1 /* TRAVERSE_POST_ORDER */,
899
915
  trav_cb, &st);
@@ -922,33 +938,13 @@ static void visit_cb(void *user, void *node_ptr, int entering,
922
938
  * happens after the walk, same observable outcome. */
923
939
  struct trav_state *st = user;
924
940
  struct visit_yield_args va;
925
- VALUE cache, key, node;
941
+ VALUE node;
926
942
  int kind, state;
927
943
 
928
944
  if (st->err != Qnil)
929
945
  return;
930
946
  kind = f_node_type(node_ptr);
931
- cache = binding_cache_of(st->document);
932
- key = ULL2NUM((uint64_t)(uintptr_t)node_ptr);
933
- node = rb_hash_aref(cache, key);
934
- if (NIL_P(node)) {
935
- node = rb_obj_alloc(binding_klass_for(kind));
936
- rb_iv_set(node, "@c_address", key);
937
- rb_iv_set(node, "@document", st->document);
938
- rb_iv_set(node, "@parent", Qnil);
939
- if (rb_obj_class(st->document) == c_iteration_scope) {
940
- rb_iv_set(node, "@structure_memoizable", Qfalse);
941
- rb_iv_set(node, "@native_fast", Qfalse);
942
- rb_iv_set(node, "@pub_document", Qnil);
943
- } else {
944
- rb_iv_set(node, "@structure_memoizable", Qtrue);
945
- rb_iv_set(node, "@native_fast", Qtrue);
946
- rb_iv_set(node, "@pub_document", st->document);
947
- }
948
- rb_iv_set(node, "@addr_reads_fast", Qtrue);
949
- rb_iv_set(node, "@node_type", INT2FIX(kind));
950
- rb_hash_aset(cache, key, node);
951
- }
947
+ node = trav_wrapper_for(st, node_ptr, kind);
952
948
  va.node = node;
953
949
  va.entering = entering ? Qtrue : Qfalse;
954
950
  va.depth = INT2NUM(depth);
@@ -966,6 +962,9 @@ static VALUE nf_visit_binding(VALUE self, VALUE document, VALUE addr)
966
962
  st.document = document;
967
963
  st.err = Qnil;
968
964
  st.self_ptr = (void *)(uintptr_t)NUM2ULL(addr);
965
+ st.cache = binding_cache_of(document);
966
+ st.klass_memo_flag =
967
+ rb_obj_class(document) == c_iteration_scope ? Qfalse : Qtrue;
969
968
  st.for_visit = 1;
970
969
  f_node_visit(st.self_ptr, visit_cb, &st);
971
970
  if (st.err != Qnil)
@@ -2648,6 +2647,14 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
2648
2647
  id_iv_c_address = rb_intern("@c_address");
2649
2648
  id_iv_native_cache = rb_intern("@native_cache");
2650
2649
  id_iv_binding_cache = rb_intern("@wrapper_cache");
2650
+ id_iv_c_address = rb_intern("@c_address");
2651
+ id_iv_document = rb_intern("@document");
2652
+ id_iv_parent = rb_intern("@parent");
2653
+ id_iv_structure_memoizable = rb_intern("@structure_memoizable");
2654
+ id_iv_native_fast = rb_intern("@native_fast");
2655
+ id_iv_pub_document = rb_intern("@pub_document");
2656
+ id_iv_addr_reads_fast = rb_intern("@addr_reads_fast");
2657
+ id_iv_node_type = rb_intern("@node_type");
2651
2658
  id_iv_version = rb_intern("@version");
2652
2659
  id_iv_readonly = rb_intern("@readonly");
2653
2660
  id_address = rb_intern("address");
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.222.2"
4
+ VERSION = "1.9.225.0"
5
5
  end
@@ -481,16 +481,24 @@ class Leptris::XML::Document
481
481
  # the perf headroom this face gates on.
482
482
  def to_html
483
483
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
484
- Leptris::XML::HTMLSerialize.document(self)
484
+ Leptris::XML::Serialization.document_html(c_ptr)
485
485
  end
486
486
 
487
- def save(path, **opts)
487
+ def save(path, mode: :xml, **opts)
488
488
  opts_struct, _encoding_anchor = Leptris::XML::Serialization.build_options(
489
489
  indent: opts.fetch(:indent, 0),
490
490
  no_decl: opts.fetch(:no_decl, false),
491
491
  encoding: opts[:encoding])
492
- status = Leptris::XML::FFI.leptris_document_save_file(
493
- c_ptr, path, opts_struct.pointer)
492
+ status =
493
+ if mode == :html
494
+ Leptris::XML::FFI.leptris_document_save_html(
495
+ c_ptr, path, opts_struct.pointer)
496
+ elsif mode != :xml
497
+ raise ArgumentError, "mode must be :xml or :html, got #{mode.inspect}"
498
+ else
499
+ Leptris::XML::FFI.leptris_document_save_file(
500
+ c_ptr, path, opts_struct.pointer)
501
+ end
494
502
  Leptris::XML::FFI.check_status(status)
495
503
  self
496
504
  end
@@ -784,7 +784,7 @@ class Leptris::XML::Element < Leptris::XML::Node
784
784
  # Document#to_html for the semantics).
785
785
  def to_html
786
786
  ensure_alive!
787
- Leptris::XML::HTMLSerialize.element(self)
787
+ Leptris::XML::Serialization.element_html(c_ptr)
788
788
  end
789
789
 
790
790
  def canonicalize(version = Leptris::XML::FFI::C14N_1_0,
@@ -169,7 +169,11 @@ module Leptris
169
169
  class SerializeExtStruct < ::FFI::Struct
170
170
  layout :indent_text, :int,
171
171
  :indent_unit, :pointer,
172
- :expand_empty, :int
172
+ :expand_empty, :int,
173
+ # libleptris 1.9.225 (#1311): tri-state — 0 = the
174
+ # document's default method, 1 = force HTML, -1 =
175
+ # force XML (to_xml on an HTML document).
176
+ :html_method, :int
173
177
  end
174
178
 
175
179
  # Mirrors LeptrisParseOptions (libleptris >= 1.9.0 carries the
@@ -992,6 +996,28 @@ attach_function :leptris_parse_string,
992
996
  [:leptris_element, :pointer, :size_t, :pointer, :pointer], :size_t
993
997
  attach_function :leptris_document_save_file,
994
998
  [:leptris_document, :string, :pointer], :leptris_status
999
+ # HTML construction + serialization (libleptris 1.9.225,
1000
+ # upstream #1309/#1311 — the engine side of #309): native HTML
1001
+ # documents carry the html serialization mode (voids unclosed,
1002
+ # raw-text script/style, explicit closes, no declaration).
1003
+ attach_function :leptris_document_create_html, [], :leptris_document
1004
+ attach_function :leptris_document_serialize_html,
1005
+ [:leptris_document, :pointer], :pointer
1006
+ attach_function :leptris_document_save_html,
1007
+ [:leptris_document, :string, :pointer], :leptris_status
1008
+ # Bulk SAX record drain (libleptris 1.9.223, upstream #1298):
1009
+ # ONE call parses the document into a flat record table —
1010
+ # tree edges, source offsets, and a flat attribute table, all
1011
+ # offset-views into a parser-owned buffer. Attach-only until
1012
+ # the SAX face wants the bulk lane.
1013
+ attach_function :leptris_sax_records_parse,
1014
+ [:string, :size_t, :uint, :pointer], :leptris_status
1015
+ attach_function :leptris_sax_records_count, [:pointer], :size_t
1016
+ attach_function :leptris_sax_records_data, [:pointer], :pointer
1017
+ attach_function :leptris_sax_records_attrs,
1018
+ [:pointer, :pointer], :pointer
1019
+ attach_function :leptris_sax_records_buffer, [:pointer], :string
1020
+ attach_function :leptris_sax_records_free, [:pointer], :void
995
1021
  # XSLT 1.0 engine (libleptris 1.9.1): compile once, apply many.
996
1022
  # XQuery 1.0 core (libleptris 1.9.64-1.9.66): orchestration
997
1023
  # over the XPath engine — prolog (declare variable/namespace/
Binary file
@@ -58,6 +58,33 @@ module Leptris::XML::Serialization
58
58
  # Default-options element serialization for inner_html's per-child
59
59
  # loop: no kwargs, no options rebuild, no method allocation —
60
60
  # one FFI fast-path dispatch per child.
61
+ # Document HTML serialization (libleptris 1.9.225, #1311): the
62
+ # native HTML mode — voids unclosed, raw-text script/style,
63
+ # explicit closes, no declaration, DOCTYPE first.
64
+ def self.document_html(c_ptr)
65
+ Leptris::XML::FFI.read_owned_string(
66
+ Leptris::XML::FFI.leptris_document_serialize_html(c_ptr, nil))
67
+ end
68
+
69
+ # Element-level HTML mode through the ext serializer (the
70
+ # html_method tri-state forces HTML regardless of the document
71
+ # flavor).
72
+ HTML_METHOD_EXT = begin
73
+ ext = Leptris::XML::FFI::SerializeExtStruct.new
74
+ ext[:indent_text] = 0
75
+ ext[:indent_unit] = nil
76
+ ext[:expand_empty] = 0
77
+ ext[:html_method] = 1
78
+ ext
79
+ end
80
+ private_constant :HTML_METHOD_EXT
81
+
82
+ def self.element_html(c_ptr)
83
+ Leptris::XML::FFI.read_owned_string(
84
+ Leptris::XML::FFI.leptris_element_serialize_ext(
85
+ c_ptr, nil, HTML_METHOD_EXT.pointer))
86
+ end
87
+
61
88
  def self.element_xml_default(c_ptr)
62
89
  return Leptris::XML::Native.fast_element_xml(c_ptr.address, 0, true,
63
90
  nil) if
data/lib/leptris/xml.rb CHANGED
@@ -21,7 +21,6 @@ module Leptris
21
21
  autoload :CssToXPath, "leptris/xml/css_to_xpath"
22
22
  autoload :CStringArray, "leptris/xml/c_string_array"
23
23
  autoload :Serialization, "leptris/xml/serialization"
24
- autoload :HTMLSerialize, "leptris/xml/html_serialize"
25
24
  autoload :SAX, "leptris/xml/sax"
26
25
  autoload :XPath, "leptris/xml/xpath"
27
26
  autoload :EvaluationContext, "leptris/xml/evaluation_context"
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.222
4
+ - libleptris v1.9.225
5
5
  - utf8proc v2.11.0
6
6
 
7
7
  Rebuild by hand:
@@ -1,5 +1,78 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.225] - 2026-09-22
4
+
5
+ ### Added
6
+
7
+ - **html: public HTML construction + serialization surface (#1309,
8
+ #1311).** HTML documents now serialize AS HTML by default
9
+ (libxml2/Nokogiri behavior): void elements unclosed (`<br>`,
10
+ `<meta charset="utf-8">`), raw-text script/style verbatim,
11
+ childless non-voids explicitly closed, no XML declaration. New:
12
+ `leptris_document_create_html()`, `leptris_document_serialize_html()`,
13
+ `leptris_document_save_html()`, and `LeptrisSerializeExtOptions.html_method`
14
+ (tri-state: 0 = document default, 1 = force HTML, -1 = force XML —
15
+ to_xml on an HTML doc). Element-level `html_method` maps through the
16
+ ext serializer too. HTML-parsed documents (whatwg + html4 modes)
17
+ carry the mode automatically; XSLT `method="html"` output unchanged
18
+ (#1312).
19
+
20
+
21
+ ## [1.9.224] - 2026-09-22
22
+
23
+ ### Fixed
24
+
25
+ - **mem:** the element source-offset side table (new in this
26
+ release) is now also freed on the direct-parse failure path,
27
+ which bypasses leptris_document_free — caught by CI LeakSanitizer.
28
+ - **test:** the runner-contention perf gate
29
+ (SmallDocumentParseIsFast) retries across settling windows instead
30
+ of failing on a single contended measurement (three CI flakes in
31
+ one day, budget unchanged).
32
+
33
+ ### Performance
34
+
35
+ - **dom:** elements shrink **72 -> 64 bytes** (ILP32 56 -> 48;
36
+ #1285 slice 3a): the namespace side-cache pointer became a
37
+ self-relative int32 offset (the attrs' round-19 pattern), and the
38
+ #1124 source offsets moved to a document-owned side table — cold
39
+ diagnostics-only data no longer costs hot-path cache lines.
40
+ Position accessors are unchanged (#1124 specs green). Part of the
41
+ pugi-style node-storage lane (#1222/#1238) (#1308).
42
+
43
+
44
+
45
+ ## [1.9.223] - 2026-09-22
46
+
47
+ ### Added
48
+
49
+ - **sax:** `leptris_sax_records_parse` — a bulk record drain for hot SAX
50
+ loops (#1298, reported by Canon): ONE call returns the whole document
51
+ as a flat record table (element tree edges, start/end offsets, flat
52
+ attribute table with XML 1.0 §3.3.3 normalization flags), all
53
+ NUL-terminated views into a parser-owned buffer. Unsupported
54
+ constructs (comments/PIs/CDATA/DOCTYPE/entities/xmlns) return
55
+ `LEPTRIS_ERROR_NOT_SUPPORTED` — fall back to the pull API. New
56
+ additive status code (#1305).
57
+
58
+ ### Fixed
59
+
60
+ - **dom:** `LEPTRIS_DIGEST_ATTR_ORDER` is now actually honored — the
61
+ 1.9.205 flag shipped only as a C enum with no env reader and no
62
+ binding carrier, so `strings` never found it in shipped bundles
63
+ (#1297, reported by Canon). `leptris_node_digest` reads the env
64
+ per call and ORs it into the passed flags (#1304).
65
+
66
+ ### Performance
67
+
68
+ - **dom:** text nodes shrink **64 → 48 bytes** (#1285 slice 4): the
69
+ `pool`/`borrowed` fields are gone — every parse lane terminates
70
+ text in place and entities decode eagerly at parse time, so
71
+ `leptris_text_get_content` is a plain field read. Content is
72
+ always NUL-terminated; `content_len` is authoritative (#1303).
73
+
74
+
75
+
3
76
  ## [1.9.222] - 2026-09-22
4
77
 
5
78
  ### Fixed
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.222
6
+ VERSION 1.9.225
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -35,6 +35,7 @@ set(LEPTRIS_SOURCES
35
35
  leptris/dom/node_public.c
36
36
  leptris/descriptor/plan.c
37
37
  leptris/dom/element.c
38
+ leptris/dom/elem_pos.c
38
39
  leptris/dom/element_query.c
39
40
  leptris/dom/element_modify.c
40
41
  leptris/dom/element_index.c
@@ -112,6 +113,7 @@ set(LEPTRIS_SOURCES
112
113
  leptris/sax/iterparse.c
113
114
  leptris/sax/streaming.c
114
115
  leptris/sax/recorder.c
116
+ leptris/sax/records.c
115
117
 
116
118
  # DTD
117
119
  leptris/dtd/model.c
@@ -596,6 +596,93 @@ LEPTRIS_API const char* leptris_iterparse_error(LeptrisIterparse it);
596
596
 
597
597
  LEPTRIS_API void leptris_iterparse_free(LeptrisIterparse it);
598
598
 
599
+
600
+ /* ============================================================================
601
+ * Bulk SAX records (issue #1298) — one-crossing whole-document drain
602
+ * ============================================================================ */
603
+
604
+ /** Record kinds (LeptrisSaxRecord.kind). */
605
+ enum {
606
+ LEPTRIS_SAX_REC_ELEMENT = 0, /**< start+end collapsed; tree via parent/next_sib */
607
+ LEPTRIS_SAX_REC_TEXT = 1 /**< character data run (entity-free docs only) */
608
+ };
609
+
610
+ /** One element or text record. Offsets are byte positions into the
611
+ * buffer returned by leptris_sax_records_buffer; every view is
612
+ * NUL-terminated at off+len (names and text both). */
613
+ typedef struct LeptrisSaxRecord {
614
+ uint32_t kind; /**< LEPTRIS_SAX_REC_* */
615
+ uint32_t parent; /**< record index, 0xFFFFFFFF for the root */
616
+ uint32_t next_sib; /**< record index, 0xFFFFFFFF = last sibling */
617
+ uint32_t off; /**< element: name; text: content */
618
+ uint32_t len; /**< byte length at off (excl. NUL) */
619
+ uint32_t line; /**< byte offset of '<' (or text start) + 1 (#1124 dp semantics) */
620
+ uint32_t start_tag_end; /**< byte offset after the open '>' (elements; 0 when unknown) */
621
+ uint32_t elem_end; /**< byte offset after the close '>' (self-close == start) */
622
+ uint32_t attr_first; /**< index into the flat attr table, 0xFFFFFFFF = none */
623
+ uint32_t attr_count; /**< number of attributes */
624
+ uint8_t self_closing; /**< element written <a/> */
625
+ } LeptrisSaxRecord;
626
+
627
+ /** One attribute: flat (off,len) views + 3.3.3 normalization need. */
628
+ typedef struct LeptrisSaxAttr {
629
+ uint32_t name_off, name_len; /**< attribute name (qname, NUL-terminated at [name_len]) */
630
+ uint32_t value_off, value_len; /**< raw value (NUL-terminated at [value_len]) */
631
+ uint8_t value_has_ws; /**< literal \t/\n/\r present — apply XML 1.0 3.3.3 normalization */
632
+ } LeptrisSaxAttr;
633
+
634
+ /** Opaque whole-document record table. */
635
+ typedef struct LeptrisSaxRecords LeptrisSaxRecords;
636
+
637
+ /**
638
+ * Parse a whole document into a flat record table (ONE call)
639
+ *
640
+ * The engine-side surface for #1298: hosts with hot SAX loops drain
641
+ * the entire document with a single crossing and then walk plain C
642
+ * structs — no per-event callback dispatch, no per-event string
643
+ * minting C-side. Records are (off,len) views into the parser's
644
+ * private copy of the input; every view is NUL-terminated in place.
645
+ *
646
+ * Coverage: the interleaved lane's scannable subset — elements,
647
+ * text, attributes; well-formed entity-free documents without
648
+ * comments/PIs/CDATA/DOCTYPE/xmlns declarations. Anything outside
649
+ * that subset returns LEPTRIS_ERROR_NOT_SUPPORTED (zero allocation);
650
+ * hosts fall back to the pull API (leptris_pull_new) for those
651
+ * documents. Malformed input returns LEPTRIS_ERROR_PARSE.
652
+ *
653
+ * @param xml Input (copied; the caller's buffer is untouched)
654
+ * @param len Input length in bytes (< 2 GiB)
655
+ * @param flags Reserved; pass 0
656
+ * @param out Receives the table handle
657
+ * @return LEPTRIS_OK, LEPTRIS_ERROR_NULL_ARG, LEPTRIS_ERROR_NOT_SUPPORTED,
658
+ * LEPTRIS_ERROR_PARSE, or LEPTRIS_ERROR_MEMORY
659
+ *
660
+ * Memory: free with leptris_sax_records_free; all views die with it.
661
+ */
662
+ LEPTRIS_API LeptrisStatus leptris_sax_records_parse(const char* xml,
663
+ size_t len,
664
+ unsigned flags,
665
+ LeptrisSaxRecords** out);
666
+
667
+ /** Number of records. */
668
+ LEPTRIS_API size_t leptris_sax_records_count(const LeptrisSaxRecords* recs);
669
+
670
+ /** Record array (count entries; valid until free). */
671
+ LEPTRIS_API const LeptrisSaxRecord* leptris_sax_records_data(
672
+ const LeptrisSaxRecords* recs);
673
+
674
+ /** Flat attribute table + its length (valid until free). */
675
+ LEPTRIS_API const LeptrisSaxAttr* leptris_sax_records_attrs(
676
+ const LeptrisSaxRecords* recs, size_t* out_attr_count);
677
+
678
+ /** The buffer all record/attr offsets index into (NUL-padded past
679
+ * len). */
680
+ LEPTRIS_API const char* leptris_sax_records_buffer(
681
+ const LeptrisSaxRecords* recs);
682
+
683
+ /** Free the table (invalidates every view). */
684
+ LEPTRIS_API void leptris_sax_records_free(LeptrisSaxRecords* recs);
685
+
599
686
  #ifdef __cplusplus
600
687
  }
601
688
  #endif
@@ -49,7 +49,8 @@ typedef enum {
49
49
  LEPTRIS_ERROR_INVALID_ARG = -5, /* Invalid argument */
50
50
  LEPTRIS_ERROR_NOT_FOUND = -6, /* Resource not found */
51
51
  LEPTRIS_ERROR_IO = -7, /* I/O error (file not found, etc.) */
52
- LEPTRIS_ERROR_NOT_IMPLEMENTED = -8 /* Feature not yet implemented */
52
+ LEPTRIS_ERROR_NOT_IMPLEMENTED = -8, /* Feature not yet implemented */
53
+ LEPTRIS_ERROR_NOT_SUPPORTED = -9 /* Construct outside this API's subset (additive, #1298) */
53
54
  } LeptrisStatus;
54
55
 
55
56
  /* ============================================================================
@@ -342,6 +343,11 @@ typedef struct {
342
343
  * Applies to the XML method only — HTML void elements keep
343
344
  * their HTML shapes. */
344
345
  int expand_empty;
346
+ /* §16.2 HTML method control (#1309): 0 = document default
347
+ * (HTML-parsed and create_html documents serialize as HTML),
348
+ * 1 = force HTML on any document, -1 = force XML (to_xml on an
349
+ * HTML document). XSLT method="html" maps to 1. */
350
+ int html_method;
345
351
  } LeptrisSerializeExtOptions;
346
352
 
347
353
  /* ============================================================================
@@ -191,9 +191,14 @@ LEPTRIS_API size_t leptris_node_child_count(LeptrisNodeRef node);
191
191
  *
192
192
  * @param node Subtree root (any node type; NULL hashes to 0)
193
193
  * @param flags LEPTRIS_DIGEST_DROP_WS_TEXT skips whitespace-only
194
- * text nodes
194
+ * text nodes; LEPTRIS_DIGEST_ATTR_ORDER makes attribute
195
+ * ORDER identity (no sort)
195
196
  * @return 64-bit content digest
196
- */
197
+ *
198
+ * Environment opt-in (#1297): setting LEPTRIS_DIGEST_ATTR_ORDER=1 in
199
+ * the process environment ORs LEPTRIS_DIGEST_ATTR_ORDER into every
200
+ * call — the carrier for hosts whose binding exposes no flags
201
+ * parameter. "0" or an empty value leaves the passed flags alone. */
197
202
  LEPTRIS_API uint64_t leptris_node_digest(LeptrisNodeRef node,
198
203
  LeptrisDigestFlags flags);
199
204
 
@@ -773,6 +778,19 @@ LEPTRIS_API const char* leptris_pi_node_get_data(LeptrisNodeRef node);
773
778
  */
774
779
  LEPTRIS_API LeptrisDocument leptris_document_create(void);
775
780
 
781
+ /**
782
+ * Create an empty HTML-mode document (#1309)
783
+ *
784
+ * Identical to leptris_document_create except the document carries
785
+ * html_mode: the default serializer emits §16.2 HTML output (void
786
+ * elements unclosed, raw-text script/style, HTML attribute
787
+ * escaping). Build with the regular element APIs.
788
+ *
789
+ * @return New empty HTML document, or NULL on failure
790
+ * Memory: free with leptris_document_free.
791
+ */
792
+ LEPTRIS_API LeptrisDocument leptris_document_create_html(void);
793
+
776
794
  /**
777
795
  * Attach an element as the document's root element
778
796
  *
@@ -2451,6 +2469,35 @@ LEPTRIS_API char* leptris_document_serialize_ext_sized(LeptrisDocument doc,
2451
2469
  const LeptrisSerializeExtOptions* ext,
2452
2470
  size_t ext_size);
2453
2471
 
2472
+ /**
2473
+ * Serialize a document with the §16.2 HTML method (#1309)
2474
+ *
2475
+ * Forces the HTML serialization rules (void elements unclosed,
2476
+ * raw-text script/style, HTML attribute escaping) regardless of the
2477
+ * document's mode — the XSLT method="html" semantic, public.
2478
+ * HTML-parsed and create_html documents get this behavior by
2479
+ * default; this entry forces it on any document.
2480
+ *
2481
+ * @param doc Document
2482
+ * @param options Frozen serialize options (NULL = defaults)
2483
+ * @return Serialized HTML string
2484
+ * Memory: Caller must call leptris_free_string() when done.
2485
+ */
2486
+ LEPTRIS_API char* leptris_document_serialize_html(
2487
+ LeptrisDocument doc, const LeptrisSerializeOptions* options);
2488
+
2489
+ /**
2490
+ * Save a document as HTML to a file (#1309)
2491
+ *
2492
+ * @param doc Document
2493
+ * @param filepath Destination path
2494
+ * @param options Frozen serialize options (NULL = defaults)
2495
+ * @return LEPTRIS_OK (0), or a negative LeptrisStatus on failure
2496
+ */
2497
+ LEPTRIS_API int leptris_document_save_html(LeptrisDocument doc,
2498
+ const char* filepath,
2499
+ const LeptrisSerializeOptions* options);
2500
+
2454
2501
  /**
2455
2502
  * Extended element serialization (issue #882): the same option
2456
2503
  * surface as leptris_document_serialize_ext — indent_text,