leptris 1.9.201.4 → 1.9.207.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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/Rakefile +8 -2
  4. data/lib/leptris/vendor/aarch64-linux/libleptris.so +0 -0
  5. data/lib/leptris/vendor/aarch64-linux-musl/libleptris.so +0 -0
  6. data/lib/leptris/vendor/arm-linux/libleptris.so +0 -0
  7. data/lib/leptris/vendor/arm-linux-musl/libleptris.so +0 -0
  8. data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
  9. data/lib/leptris/vendor/ppc64le-linux/libleptris.so +0 -0
  10. data/lib/leptris/vendor/x86_64-darwin/libleptris.dylib +0 -0
  11. data/lib/leptris/vendor/x86_64-linux/libleptris.so +0 -0
  12. data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
  13. data/lib/leptris/version.rb +1 -1
  14. data/lib/leptris/xml/document.rb +25 -0
  15. data/lib/leptris/xml/dtd.rb +113 -0
  16. data/lib/leptris/xml/ffi.rb +75 -0
  17. data/lib/leptris/xml.rb +1 -0
  18. data/vendor-src/libleptris/CHANGELOG.md +79 -0
  19. data/vendor-src/libleptris/CLAUDE.md +1 -1
  20. data/vendor-src/libleptris/CMakeLists.txt +1 -1
  21. data/vendor-src/libleptris/README.adoc +26 -0
  22. data/vendor-src/libleptris/TODO.max-perf/2-3-oneshot-tokenizer-design.md +98 -0
  23. data/vendor-src/libleptris/cli/commands/validate.c +4 -2
  24. data/vendor-src/libleptris/docs/guide/parsing.md +30 -0
  25. data/vendor-src/libleptris/scripts/check_export_surface.py +20 -0
  26. data/vendor-src/libleptris/src/CMakeLists.txt +44 -1
  27. data/vendor-src/libleptris/src/include/leptris/descriptor.h +7 -1
  28. data/vendor-src/libleptris/src/include/leptris/dom/document.h +7 -1
  29. data/vendor-src/libleptris/src/include/leptris/dom/element.h +7 -1
  30. data/vendor-src/libleptris/src/include/leptris/dom/serialize.h +7 -1
  31. data/vendor-src/libleptris/src/include/leptris/dtd.h +46 -9
  32. data/vendor-src/libleptris/src/include/leptris/error.h +63 -1
  33. data/vendor-src/libleptris/src/include/leptris/html.h +79 -0
  34. data/vendor-src/libleptris/src/include/leptris/sax/sax.h +20 -1
  35. data/vendor-src/libleptris/src/include/leptris/types.h +29 -1
  36. data/vendor-src/libleptris/src/include/leptris/xpath/xpath.h +7 -1
  37. data/vendor-src/libleptris/src/include/leptris.h +59 -37
  38. data/vendor-src/libleptris/src/leptris/core.c +28 -0
  39. data/vendor-src/libleptris/src/leptris/dom/diag.h +2 -13
  40. data/vendor-src/libleptris/src/leptris/dom/digest.c +5 -1
  41. data/vendor-src/libleptris/src/leptris/dom/doctype.c +27 -0
  42. data/vendor-src/libleptris/src/leptris/dom/element.c +54 -9
  43. data/vendor-src/libleptris/src/leptris/dom/element.h +5 -0
  44. data/vendor-src/libleptris/src/leptris/dom/element_modify.c +46 -6
  45. data/vendor-src/libleptris/src/leptris/dtd/model.h +6 -0
  46. data/vendor-src/libleptris/src/leptris/dtd/parser.c +49 -12
  47. data/vendor-src/libleptris/src/leptris/error.c +22 -0
  48. data/vendor-src/libleptris/src/leptris/flat/direct_parse.c +61 -6
  49. data/vendor-src/libleptris/src/leptris/html/html_parse.c +156 -42
  50. data/vendor-src/libleptris/src/leptris/leptris.c +11 -2
  51. data/vendor-src/libleptris/src/leptris/leptris_internal.h +23 -1
  52. data/vendor-src/libleptris/src/leptris/sax/pull.c +62 -18
  53. data/vendor-src/libleptris/src/leptris/sax/sax_internal.h +7 -0
  54. data/vendor-src/libleptris/src/leptris/sax/streaming.c +155 -4
  55. data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +19 -2
  56. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +126 -0
  57. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +653 -151
  58. data/vendor-src/libleptris/src/leptris/xpath/parser.c +4 -2
  59. data/vendor-src/libleptris/src/leptris/xslt/xslt_functions.c +9 -5
  60. data/vendor-src/libleptris/test/dom/test_digest.cpp +36 -0
  61. data/vendor-src/libleptris/test/dom/test_dom.cpp +24 -0
  62. data/vendor-src/libleptris/test/dom/test_node_surface_parity.cpp +81 -0
  63. data/vendor-src/libleptris/test/dtd/test_dtd_validate.cpp +113 -0
  64. data/vendor-src/libleptris/test/html/test_html.cpp +51 -0
  65. data/vendor-src/libleptris/test/parser/test_parser.cpp +43 -0
  66. data/vendor-src/libleptris/test/sax/test_pull.cpp +32 -0
  67. data/vendor-src/libleptris/test/xquery/qt3/fn/replace.xml +865 -0
  68. data/vendor-src/libleptris/test/xquery/qt3/fn/string-join.xml +449 -0
  69. data/vendor-src/libleptris/test/xquery/qt3/fn/tokenize.xml +639 -0
  70. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +74 -6
  71. data/vendor-src/libleptris/vcpkg.json +1 -1
  72. metadata +7 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 230f192458c26ad52475b9ef659644858d264777896cc197482205ac32972a01
4
- data.tar.gz: 2013be07ce60da7b35b5564b6d18b3905dd7809d08acabb2780211323a6f6781
3
+ metadata.gz: b10f689b2142684316157264cfd935433d6d0d22db8cf90b4a27e9c4ba79bd88
4
+ data.tar.gz: f3e625c375d995087bec8cd6c058a7a9c900791125f1422f6da8b2436e90047b
5
5
  SHA512:
6
- metadata.gz: 979d1f1a2e9d5815a6502c1990e171ff6e0e1df90751121bf96eacab517f7510b0eb30efca09e848fb3ab4c27e33fba4f2fbcf3f977811a786b766a23fa58545
7
- data.tar.gz: 3df36dfd59e33291acad1ad240541d12d2f1fb0cd7a4e4590ff1bf7569e30ce6b639fbbc6aaffc469bf49716dc914c9d5ef42dafada61da49c0f408711ed2949
6
+ metadata.gz: ddf28875bb34e2eb14074c51834614f5cb5acddf0a33c58f55a50d9e6970a065eb70cb6f3da51c1114cad3a8c062a6dec98d6ce271ff27a81a77033f92e774ea
7
+ data.tar.gz: b1c7bc0629b2840a7a23d71bd27b08303426ab7b013d8a272d9a1fedc82b8f40ba8fdd6805724f878fbf4374752912658ea5922e79548f16a53a859cf4de3164
data/CHANGELOG.md CHANGED
@@ -5,6 +5,75 @@ 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.204.0] - 2026-09-19
9
+
10
+ ### Added — document-level mutation completed: #1229 (the 1.9.204 surface)
11
+
12
+ - **`Document#clear_declaration`** — un-sets the XML declaration
13
+ (clears version/encoding and the standalone marker); the
14
+ document serializes exactly as if the input had none.
15
+ Idempotent; returns self.
16
+ - **`Document#remove_doctype`** — un-sets the DOCTYPE; it leaves
17
+ the child chain and the serializer's view while the DocType
18
+ stays pool-owned and readable (`doc.doctype.name` et al.) until
19
+ `#free`. Returns true when removed, false when the document had
20
+ none. Rounds out the write-half next to `#set_doctype` /
21
+ `doc.doctype` (the read/create faces wired earlier).
22
+ - Completes upstream leptris/leptris#1229 and, with it, the
23
+ leptris-ruby#275 umbrella (PI removal 1.9.9, declaration clear +
24
+ DOCTYPE unset 1.9.204). Spec battery in
25
+ `document_construction_spec.rb`.
26
+ - Engine adoption: 1.9.203 (`leptris_dtd_parse` honors the
27
+ NULL-on-error contract, upstream #1211) and 1.9.204 (html.h
28
+ subsystem header, one-shot name fast paths, one-call buffer-mode
29
+ pull feeds). #1229's two exports are wired as the
30
+ `Document#clear_declaration` / `#remove_doctype` faces; the
31
+ other two 1.9.204 exports attach for lockstep parity
32
+ (`leptris_sax_parser_set_one_shot` — the binding's one-shot SAX
33
+ entry is one-shot by construction and pull's memory source sets
34
+ it engine-side; `leptris_alloc_buffer` — engine-heap allocation
35
+ for callback-owned buffers like the DTD PE loader, released via
36
+ the already-bound `leptris_free_string`). Audit 333/333.
37
+
38
+ ### Fixed — portability (audit:symbols + POWER vendor mapping)
39
+
40
+ - `nm -gU` → `nm -g`: GNU binutils nm (Debian bookworm 2.40)
41
+ accepts `-U` but then ignores the file operand entirely
42
+ (`nm: 'a.out': No such file`, empty export list, guaranteed
43
+ false drift). Undefined externals print two columns, so the
44
+ existing `split[2]` + `compact` already skips them on every nm
45
+ flavor. The engine build itself succeeded on ppc64le this round
46
+ — #1196's LTO crash did not reproduce.
47
+ - `vendor_platforms_for` normalizes RbConfig's `powerpc64le` to
48
+ the gem-tier spelling `ppc64le`: the JVM-host rebuild ("java" →
49
+ host cpu-os) matched no tier on POWER hosts, so JRuby/
50
+ TruffleRuby there missed their vendored binary. Surfaced by the
51
+ ppc64le qemu leg (ffi_spec:223) once the audit fix let the suite
52
+ run; the spec now asserts JVM ≡ host tier.
53
+
54
+ ### Not fixed despite upstream closes
55
+
56
+ - **Iterparse attribute reads: all-nil runs on ubuntu-latest Ruby
57
+ ## [1.9.207.0] - 2026-09-19
58
+
59
+ ### Fixed — engine sync: big-endian close-tag parse (#1197 upstream)
60
+
61
+ - Vendored libleptris 1.9.207: the close-tag fast-path compare
62
+ masked the wrong end of its 8-byte name load on big-endian hosts,
63
+ rejecting ordinary paired-tag documents with "malformed input"
64
+ (434/727 binding-suite failures under qemu s390x). The mask now
65
+ follows memory order per byte dialect; the s390x-linux-musl
66
+ platform gem becomes viable with this release.
67
+
68
+ ### Added — FFI mirror adopts the 1.9.202–1.9.207 engine surface
69
+
70
+ - audit:symbols lockstep attachments: recover diagnostics
71
+ (leptris_document_parse_diag[_count]), the standalone DTD family
72
+ (leptris_dtd_*), declaration/doctype removal, engine-heap buffer
73
+ allocation, and one-shot SAX. leptris_set_error stays internal —
74
+ the engine now hides it on ELF too (leptris#1243), so the export
75
+ surface agrees across Linux/macOS/Windows.
76
+
8
77
  ## [1.9.201.3] - 2026-09-19
9
78
 
10
79
  ### Added — the HTML facade (Nokogiri-shaped entry points)
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.201"
11
+ LIBLEPTRIS_VERSION = "1.9.207"
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
@@ -326,7 +326,13 @@ namespace :audit do
326
326
  unless lib
327
327
  abort "audit:symbols: vendored library not found — run rake compile"
328
328
  end
329
- exported = `nm -gU #{lib}`
329
+ # Plain -g, not -U: GNU binutils nm (Debian bookworm 2.40)
330
+ # accepts -U but then ignores the file operand entirely
331
+ # ("nm: 'a.out': No such file", empty export list, guaranteed
332
+ # false drift — the ppc64le leg). Undefined externals print two
333
+ # columns (type, name), so split[2] is nil for them and the
334
+ # compact below already drops them on every nm flavor.
335
+ exported = `nm -g #{lib}`
330
336
  .lines.map { |l| l.split[2] }.compact
331
337
  .map { |n| n.sub(/\A_/, "") }
332
338
  .select { |n| n.start_with?("leptris_") }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.201.4"
4
+ VERSION = "1.9.207.0"
5
5
  end
@@ -635,6 +635,31 @@ class Leptris::XML::Document
635
635
  Leptris::XML::DocType.new(dt, self)
636
636
  end
637
637
 
638
+ # Un-set the XML declaration (libleptris 1.9.204, upstream
639
+ # #1229): the document stops being declaration-bearing and
640
+ # serialization emits none — exactly as if the input had none.
641
+ # Clears version/encoding and the standalone marker. Idempotent.
642
+ # Returns self.
643
+ def clear_declaration
644
+ Leptris::XML::FFI.check_status(
645
+ Leptris::XML::FFI.leptris_document_clear_declaration(c_ptr))
646
+ @version += 1
647
+ self
648
+ end
649
+
650
+ # Un-set the document's DOCTYPE (libleptris 1.9.204, upstream
651
+ # #1229). The DocType stays pool-owned and readable via #doctype
652
+ # until #free; it leaves the child chain and the serializer's
653
+ # view. Returns true when removed, false when the document had
654
+ # none.
655
+ def remove_doctype
656
+ rc = Leptris::XML::FFI.leptris_document_remove_doctype(c_ptr)
657
+ return false if rc == Leptris::XML::FFI::LEPTRIS_ERROR_NOT_FOUND
658
+ Leptris::XML::FFI.check_status(rc)
659
+ @version += 1
660
+ true
661
+ end
662
+
638
663
  # Append a document-level comment (epilog position, after the
639
664
  # root element) — the add_pi twin (libleptris 1.9.160, #1032;
640
665
  # parsed document-level comments round-tripped since 1.9.3/#578,
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module Leptris::XML
6
+ # DTD validation (the libleptris 1.9.202-exported surface, the
7
+ # leptris validate --dtd CLI's library face): parse an internal
8
+ # subset, optionally merge an external subset (the application
9
+ # owns I/O — read the resource named by the DOCTYPE system id
10
+ # and hand the bytes over), then validate any number of documents.
11
+ # The RelaxNG shape: parse once, validate many; errors carry
12
+ # message/element/line/column.
13
+ #
14
+ # dtd = Leptris::XML::DTD.parse(<<~DTD)
15
+ # <!ELEMENT book (title, author+)>
16
+ # <!ATTLIST book id ID #REQUIRED>
17
+ # DTD
18
+ # dtd.valid?(doc)
19
+ # dtd.validate(doc) # => [{message:, element:, line:, column:}]
20
+ #
21
+ module DTD
22
+ module_function
23
+
24
+ def parse(dtd_content)
25
+ DTDHandle.parse(dtd_content)
26
+ end
27
+
28
+ # The document's own internal subset (from its DOCTYPE), as a
29
+ # validatable handle: document-owned, never freed by the caller.
30
+ # Extend it with an external subset via #merge_external_subset.
31
+ def from_document(document)
32
+ DTDHandle.wrap_owned(
33
+ Leptris::XML::FFI.leptris_document_get_dtd(document.c_ptr))
34
+ end
35
+
36
+ # One struct-free convenience: validate +first_error. Returns
37
+ # nil when valid.
38
+ def first_error(document, dtd_content)
39
+ DTDHandle.parse(dtd_content).first_error(document)
40
+ end
41
+
42
+ class DTDHandle
43
+ def self.parse(dtd_content)
44
+ content = dtd_content.to_s
45
+ raw = Leptris::XML::FFI.leptris_dtd_parse(
46
+ content, content.bytesize)
47
+ if raw.null?
48
+ raise Leptris::XML::Error,
49
+ "DTD parse failed: #{Leptris::XML::FFI.leptris_last_error}"
50
+ end
51
+ new(OwnedHandle.new(raw))
52
+ end
53
+
54
+ def self.wrap_owned(ptr)
55
+ new(nil, ptr)
56
+ end
57
+
58
+ class OwnedHandle < ::FFI::AutoPointer
59
+ def self.release(ptr)
60
+ Leptris::XML::FFI.leptris_dtd_free(ptr)
61
+ end
62
+ end
63
+
64
+ def initialize(owned, doc_owned_ptr = nil)
65
+ @owned = owned
66
+ @ptr = doc_owned_ptr || owned
67
+ end
68
+
69
+ def c_ptr
70
+ @ptr
71
+ end
72
+
73
+ # Merge an external subset (the application reads the resource;
74
+ # the library never does I/O). First declaration of a name wins,
75
+ # so the internal subset is never overridden.
76
+ def merge_external_subset(content)
77
+ c = content.to_s
78
+ rc = Leptris::XML::FFI.leptris_dtd_parse_external_subset(
79
+ c_ptr, c, c.bytesize)
80
+ raise Leptris::XML::Error, "external subset merge failed" if rc.negative?
81
+ self
82
+ end
83
+
84
+ def valid?(document)
85
+ validate(document).empty?
86
+ end
87
+
88
+ # [{ message:, element:, line:, column: }] — empty when valid.
89
+ def validate(document)
90
+ err = Leptris::XML::FFI::DTDErrorStruct.new
91
+ rc = Leptris::XML::FFI.leptris_dtd_validate(
92
+ document.c_ptr, c_ptr, err)
93
+ return [] if rc == 1
94
+ return [{ message: "internal validation error" }] if rc.negative?
95
+ message = err[:message]
96
+ return [{ message: "validation failed" }] if message.nil?
97
+ [{
98
+ message: message,
99
+ element: err[:element_name],
100
+ line: err[:line],
101
+ column: err[:column],
102
+ }]
103
+ ensure
104
+ Leptris::XML::FFI.leptris_dtd_error_free(err) unless rc == 1
105
+ end
106
+
107
+ # nil when valid — the single-error convenience.
108
+ def first_error(document)
109
+ validate(document).first
110
+ end
111
+ end
112
+ end
113
+ end
@@ -26,6 +26,9 @@ module Leptris
26
26
  end
27
27
  cpu = RbConfig::CONFIG["host_cpu"].to_s
28
28
  cpu = "aarch64" if cpu.empty?
29
+ # RbConfig says "powerpc64le"; the gem/vendor tier is
30
+ # "ppc64le-linux" (the gem platform spelling).
31
+ cpu = "ppc64le" if cpu == "powerpc64le"
29
32
  os = host_os =~ /darwin/ ? "darwin" : "linux"
30
33
  ruby_platform = "#{cpu}-#{os}"
31
34
  end
@@ -118,6 +121,7 @@ module Leptris
118
121
  typedef :pointer, :leptris_node_ref
119
122
  typedef :pointer, :leptris_attribute
120
123
  typedef :pointer, :leptris_doctype
124
+ typedef :pointer, :leptris_dtd
121
125
  typedef :pointer, :leptris_xpath_result
122
126
  typedef :pointer, :leptris_xpath_ns_set
123
127
  typedef :pointer, :leptris_xpath_compiled
@@ -133,6 +137,7 @@ module Leptris
133
137
  typedef :pointer, :leptris_iterparse
134
138
  typedef :pointer, :leptris_xpath_var_set
135
139
  typedef :pointer, :leptris_sax_parser
140
+ typedef :pointer, :leptris_dtd
136
141
  typedef :int, :leptris_status
137
142
 
138
143
  class SAXHandler < ::FFI::Struct
@@ -381,6 +386,12 @@ attach_function :leptris_parse_string,
381
386
  [:leptris_document, :int], :leptris_status
382
387
  attach_function :leptris_document_set_doctype,
383
388
  [:leptris_document, :string, :string, :string], :leptris_doctype
389
+ # The #1229 remove-half (libleptris 1.9.204): un-set the
390
+ # declaration; un-set the DOCTYPE (NOT_FOUND when none set).
391
+ attach_function :leptris_document_clear_declaration,
392
+ [:leptris_document], :leptris_status
393
+ attach_function :leptris_document_remove_doctype,
394
+ [:leptris_document], :leptris_status
384
395
  attach_function :leptris_document_append_pi,
385
396
  [:leptris_document, :string, :string], :leptris_node_ref
386
397
  attach_function :leptris_document_remove_child,
@@ -849,6 +860,14 @@ attach_function :leptris_parse_string,
849
860
  [:leptris_sax_parser], :void
850
861
  attach_function :leptris_sax_parser_set_streaming,
851
862
  [:leptris_sax_parser, :int], :int
863
+ # One-shot input hint (libleptris 1.9.204): whole document in
864
+ # a single feed(), alive for the parser's lifetime — enables
865
+ # in-place scans. The binding's one-shot entry
866
+ # (leptris_sax_parse) is one-shot by construction; the engine
867
+ # sets this itself for pull's memory source. Attach-only
868
+ # until a chunked-feed face wants to opt in.
869
+ attach_function :leptris_sax_parser_set_one_shot,
870
+ [:leptris_sax_parser, :int], :void
852
871
 
853
872
  # Pull parsing (v1.6.0): StAX-style cursor over a document.
854
873
  attach_function :leptris_pull_new,
@@ -1024,6 +1043,30 @@ attach_function :leptris_parse_string,
1024
1043
  attach_function :leptris_str_has_nonstandard_entity,
1025
1044
  [:string, :size_t], :int
1026
1045
 
1046
+ # DTD validation (exported 1.9.202; the surface existed
1047
+ # since the CLI work): parse an internal subset, merge an
1048
+ # external subset (application-owned I/O), validate a
1049
+ # document. Errors carry message/element/line/column.
1050
+ class DTDErrorStruct < ::FFI::Struct
1051
+ layout :message, :string,
1052
+ :element_name, :string,
1053
+ :line, :int,
1054
+ :column, :int
1055
+ end
1056
+ attach_function :leptris_dtd_parse,
1057
+ [:string, :size_t], :pointer
1058
+ # leptris_document_get_dtd attached above with the readers
1059
+ attach_function :leptris_dtd_parse_external_subset,
1060
+ [:pointer, :string, :size_t], :int
1061
+ attach_function :leptris_dtd_set_pe_loader,
1062
+ [:pointer, :pointer, :pointer], :void
1063
+ attach_function :leptris_dtd_validate,
1064
+ [:leptris_document, :pointer, :pointer], :int
1065
+ attach_function :leptris_dtd_free,
1066
+ [:pointer], :void
1067
+ attach_function :leptris_dtd_error_free,
1068
+ [:pointer], :void
1069
+
1027
1070
  attach_function :leptris_rng_parse,
1028
1071
  [:string, :size_t, :pointer], :leptris_relaxng
1029
1072
  attach_function :leptris_rng_parse_file,
@@ -1100,6 +1143,11 @@ attach_function :leptris_parse_string,
1100
1143
  [:leptris_doctype], :string
1101
1144
 
1102
1145
  attach_function :leptris_free_string, [:pointer], :void
1146
+ # Engine-heap allocation (libleptris 1.9.204): buffers handed
1147
+ # to engine callbacks (the DTD PE loader) so the engine frees
1148
+ # through its matching deallocation path. Release with
1149
+ # leptris_free_string when NOT consumed by the engine.
1150
+ attach_function :leptris_alloc_buffer, [:size_t], :pointer
1103
1151
  attach_function :leptris_explicit_cleanup, [], :void
1104
1152
  attach_function :leptris_set_memory_management_functions,
1105
1153
  [:pointer, :pointer], :void
@@ -1109,6 +1157,33 @@ attach_function :leptris_parse_string,
1109
1157
  [:leptris_document, :pointer, :pointer], :leptris_status
1110
1158
  attach_function :leptris_status_string, [:leptris_status], :string
1111
1159
  attach_function :leptris_error_message, [:leptris_status], :string
1160
+ # Engine surface adopted with libleptris 1.9.206 (the FFI
1161
+ # mirror audit keeps attachments and exports in lockstep):
1162
+ # recover diagnostics (#1200), the standalone DTD family
1163
+ # (leptris_dtd_*), declaration/doctype removal (#1094), the
1164
+ # engine-heap buffer (#1219), one-shot SAX (#1178), and the
1165
+ # exported error-recorder entry point.
1166
+ attach_function :leptris_alloc_buffer, [:size_t], :pointer
1167
+ attach_function :leptris_document_clear_declaration,
1168
+ [:leptris_document], :leptris_status
1169
+ attach_function :leptris_document_remove_doctype,
1170
+ [:leptris_document], :leptris_status
1171
+ attach_function :leptris_document_parse_diag_count,
1172
+ [:leptris_document], :size_t
1173
+ attach_function :leptris_document_parse_diag,
1174
+ [:leptris_document, :size_t, :pointer, :pointer, :size_t], :int
1175
+ attach_function :leptris_dtd_parse,
1176
+ [:string, :size_t], :leptris_dtd
1177
+ attach_function :leptris_dtd_free, [:leptris_dtd], :void
1178
+ attach_function :leptris_dtd_parse_external_subset,
1179
+ [:leptris_dtd, :string, :size_t], :int
1180
+ attach_function :leptris_dtd_set_pe_loader,
1181
+ [:leptris_dtd, :pointer, :pointer], :void
1182
+ attach_function :leptris_dtd_validate,
1183
+ [:leptris_document, :leptris_dtd, :pointer], :int
1184
+ attach_function :leptris_dtd_error_free, [:pointer], :void
1185
+ attach_function :leptris_sax_parser_set_one_shot,
1186
+ [:leptris_sax_parser, :int], :void
1112
1187
  # Thread-global last-failure position (error.h): companion to
1113
1188
  # leptris_last_error; populated by recover parses (#547).
1114
1189
  attach_function :leptris_last_error_position,
data/lib/leptris/xml.rb CHANGED
@@ -34,6 +34,7 @@ module Leptris
34
34
  autoload :XSLT, "leptris/xml/xslt"
35
35
  autoload :XQuery, "leptris/xml/xquery"
36
36
  autoload :RelaxNG, "leptris/xml/relaxng"
37
+ autoload :DTD, "leptris/xml/dtd"
37
38
  autoload :Pull, "leptris/xml/pull"
38
39
  autoload :Iterparse, "leptris/xml/iterparse"
39
40
 
@@ -1,5 +1,84 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.207] - 2026-09-19
4
+
5
+ ### Fixed
6
+
7
+ - stop leptris_set_error leaking into the ELF dynamic table (build)
8
+
9
+
10
+
11
+ ## [1.9.206] - 2026-09-19
12
+
13
+ ### Fixed
14
+
15
+ - endian-neutral close-tag name compare (#1197) (parse)
16
+
17
+ ### Performance
18
+
19
+ - memchr finds the text span end (TODO.max-perf/2-3 slice 3) (sax)
20
+ - zero-copy slice emission for one-shot pulls (#1178 tail) (sax)
21
+
22
+
23
+
24
+ ## [1.9.205] - 2026-09-19
25
+
26
+ ### Added
27
+
28
+ - Parse recover diagnostics (#1200): the DOM parse now records
29
+ duplicate-attribute events on the document — `LeptrisDiagKind`
30
+ (with the new `LEPTRIS_DIAG_RECOVER` kind) is public in
31
+ types.h, and the diag list reads through
32
+ `leptris_document_parse_diag_count` /
33
+ `leptris_document_parse_diag` (error.h). First definition wins,
34
+ matching the SAX lane's libxml2-recover surface; the digest
35
+ gate no longer needs a SAX probe pass.
36
+ - `LEPTRIS_DIGEST_ATTR_ORDER` (#1200 part 1): opt-in digest flag
37
+ that makes attribute order identity-bearing instead of sorted
38
+ away.
39
+
40
+ ### Fixed
41
+
42
+ - MSVC C2027 in `leptris_internal.h`: the `LeptrisDiag` forward
43
+ declaration moved to file scope (broke the windows-dll-exports
44
+ leg).
45
+
46
+ ### Performance
47
+
48
+ - First-char-bucketed tag classifier for the WHATWG HTML path:
49
+ the tokenizer's name-list strcmp scans become one-load bucket
50
+ lookups — parse +31-37% on the #1218 bench shapes.
51
+
52
+
53
+
54
+ ## [1.9.204] - 2026-09-19
55
+
56
+ ### Added
57
+
58
+ - leptris/html.h subsystem header; one-shot name fast paths (slice 1); document HTML parsing (api+perf)
59
+
60
+ ### Performance
61
+
62
+ - buffer-mode pull feeds in ONE call (slice-1 prerequisite) (sax)
63
+
64
+
65
+
66
+ ## [1.9.203] - 2026-09-19
67
+
68
+ ### Fixed
69
+
70
+ - leptris_dtd_parse honors the NULL-on-error contract (#1211) (dtd)
71
+
72
+
73
+
74
+ ## [1.9.202] - 2026-09-18
75
+
76
+ ### Fixed
77
+
78
+ - export the DTD validation surface; gate unmarked public declarations (api)
79
+
80
+
81
+
3
82
  ## [1.9.201] - 2026-09-18
4
83
 
5
84
  <!-- Edit this section with the actual release notes. -->
@@ -69,7 +69,7 @@ Core (src/leptris/) → DOM, parser, XPath engine, SAX, DTD, encoding, memor
69
69
  - `xinclude/` — built and shipping. `parse="text"` with `xi:fallback` works end-to-end. `parse="xml"` and xpointer are TODO 92.
70
70
 
71
71
  ### Public API surface (`src/include/leptris/`)
72
- The public contract. Don't break ABI without a major bump. Headers split by subsystem: `dom/{document,element,serialize}.h`, `xpath/xpath.h`, `sax/sax.h`, plus `types.h`, `error.h`, `dtd.h` at the top level. All handles are opaque typedefs (`LeptrisDocument`, `LeptrisElement`, `LeptrisNodeRef`, …); callers never see struct definitions.
72
+ The public contract. Don't break ABI without a major bump. Headers split by subsystem: `dom/{document,element,serialize}.h`, `xpath/xpath.h`, `sax/sax.h`, `html.h`, plus `types.h`, `error.h`, `dtd.h` at the top level. All handles are opaque typedefs (`LeptrisDocument`, `LeptrisElement`, `LeptrisNodeRef`, …); callers never see struct definitions.
73
73
 
74
74
  ### Memory model
75
75
  - Pool allocator is the dominant allocation path — node creation should go through `leptris_node_create_pooled`.
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(leptris
6
- VERSION 1.9.201
6
+ VERSION 1.9.207
7
7
  DESCRIPTION "Fast XML parser and XPath evaluator in pure C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -320,6 +320,32 @@ if (r) {
320
320
  Supported: all 13 axes, all 27 functions, all 15 operators, full
321
321
  predicate syntax. See <<api,xpath-coverage>> for details.
322
322
 
323
+ ==== HTML parsing (WHATWG + libxml2-compat)
324
+
325
+ HTML is a first-class input — two tolerant parsers, both declared in
326
+ `leptris/html.h`:
327
+
328
+ [source,c]
329
+ ----
330
+ LeptrisStatus st;
331
+ /* WHATWG-conformant tree construction (html5lib corpus green). */
332
+ LeptrisDocument doc =
333
+ leptris_parse_html_string("<p>Hello <b>world", 18, &st);
334
+
335
+ /* Nokogiri byte-for-byte shape: no synthesized <html>/<head>/<body>,
336
+ * no implied <tbody>, leading script/style content stays in <body>. */
337
+ LeptrisDocument frag =
338
+ leptris_parse_html4_string("<p>Hello <b>world", 18, &st);
339
+ ----
340
+
341
+ Both handle implied end tags (`p`/`li`/`td`/`tr`/`th`/`dt`/`dd`/
342
+ `option`/...), void elements, raw-text `script`/`style`, minimized
343
+ and unquoted attribute values, case-insensitive tag/attribute names,
344
+ and the HTML named-entity table. Malformed input never fails the
345
+ parse — it degrades to text; the result serializes and queries
346
+ (XPath/XSLT) like any XML document. Memory contract identical to
347
+ `leptris_parse_string`.
348
+
323
349
  ==== Parsing models: DOM, SAX, StAX, iterparse
324
350
 
325
351
  One engine, four consumption models. Pick by how you want to pay:
@@ -0,0 +1,98 @@
1
+ # Levers 2+3 — compiled-dispatch + zero-copy one-shot emission (design)
2
+
3
+ Status: DESIGNED (2026-09-19). Implementation slices below.
4
+ Owners: engine repo. Bench: `benchmarks/sax/bench_iterparse.c` (#1195).
5
+
6
+ ## Problem
7
+
8
+ The pull/iterparse path costs 4.4x the compact DOM parse over the
9
+ same bytes (10KB medium: pull 54.7us vs DOM one-shot 12.3us).
10
+ Root cause (measured, #1178): every event string is copied twice —
11
+
12
+ 1. streaming.c copies names/text/attrs into per-frame storage
13
+ (scratch arena + inline name bufs), because a streaming feed()
14
+ chunk may be freed once the call returns;
15
+ 2. pull.c's cb_* strdups them again into queue events, freed at the
16
+ next _next.
17
+
18
+ A falsified attempt (per-call arena staging in _next) ADDED a third
19
+ copy layer and measured slower (54.7 -> 67.6us) — churn-shaving the
20
+ wrong layer cannot win; the copies must not happen at all.
21
+
22
+ ## Insight
23
+
24
+ One-shot parses (leptris_sax_parse / leptris_pull_new over an
25
+ in-memory buffer) have no interruption: the input buffer is
26
+ immutable (#1125), caller-owned, and alive for the whole iteration
27
+ (the pull API already documents input-lifetime for buffer mode).
28
+ Names and attribute names cannot contain entity references (XML
29
+ names are name-chars only), so in one-shot mode they are always
30
+ contiguous slices of the input — borrowable with zero copies.
31
+
32
+ Only entity-expanded payloads (attribute values, text with &...;)
33
+ must materialize; the common case has none.
34
+
35
+ ## Design
36
+
37
+ ### Slice events (one-shot only)
38
+
39
+ ```
40
+ typedef struct {
41
+ const char* ptr; /* points INTO the input buffer */
42
+ size_t len;
43
+ } LeptrisSlice;
44
+
45
+ typedef struct {
46
+ LeptrisPullEventType type;
47
+ LeptrisSlice name; /* element / PI target */
48
+ LeptrisSlice text; /* ONLY if no entities: borrowed */
49
+ char* text_owned; /* materialized when entities */
50
+ LeptrisSlice attr_flat[]; /* name,value pairs, names always
51
+ borrowed; values borrowed when
52
+ entity-free, else owned */
53
+ } OneShotEvent;
54
+ ```
55
+
56
+ Rule: a payload is borrowed iff the engine can prove (single scan,
57
+ memchr '&') that its span contains no '&'. One memchr per payload
58
+ replaces one malloc+memcpy+free — and '&' is absent from the vast
59
+ majority of real payloads.
60
+
61
+ ### Compiled dispatch (lever 2)
62
+
63
+ The streaming machine's per-state if-chains (peek '<', '/', '!',
64
+ name-start, ws...) become a 256-entry class table + a state-indexed
65
+ goto/switch jump table:
66
+
67
+ - class LUT: 4 bits (WS, NAME_START, NAME_CHAR, SPECIAL masks) —
68
+ one load classifies each byte (the #1177 LUT result generalized);
69
+ - state dispatch: `switch (state)` compiled to a jump table by the
70
+ compiler once states are a dense enum — keep cases small and
71
+ branchless-friendly (the hot tokenizer states are ~20).
72
+
73
+ The SAME token loop serves both modes: streaming keeps frames/carry
74
+ (one_shot=0); one-shot mode (one_shot=1) skips carry and frame
75
+ copies, emits slices, and materializes only entity spans.
76
+
77
+ ### Consumer adaptation
78
+
79
+ pull.c gains a one-shot queue variant: cb_* check parser->one_shot
80
+ and enqueue slice events (no strdup); queue_reset_event frees only
81
+ owned pointers. Batch staging (#589) copies out of slices on
82
+ demand — the existing stage chain unchanged.
83
+
84
+ ## Slices
85
+
86
+ 1. **Slice events + one-shot flag** in streaming.c; pull one-shot
87
+ queue. Acceptance: bench_iterparse full-doc 0.47us/event ->
88
+ <=0.15us/event; no test changes (contracts identical).
89
+ 2. **Class LUT + dense-state dispatch** in the tokenizer loop.
90
+ Acceptance: bench_sax +5% minimum; no behavioral delta.
91
+ 3. **memchr fast paths** for text/attr-value spans feeding the
92
+ borrow check. Acceptance: entity-free docs near-memcpy speed.
93
+
94
+ ## Non-goals
95
+
96
+ - No API changes; event lifetime contracts unchanged.
97
+ - Streaming feed() semantics untouched (frames/carry stay).
98
+ - No new allocation strategies in pull.c (the falsified lesson).