leptris 1.9.201.4 → 1.9.204.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 (62) 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 +47 -0
  17. data/lib/leptris/xml.rb +1 -0
  18. data/vendor-src/libleptris/CHANGELOG.md +28 -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 +7 -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/xpath/xpath.h +7 -1
  36. data/vendor-src/libleptris/src/include/leptris.h +59 -37
  37. data/vendor-src/libleptris/src/leptris/core.c +28 -0
  38. data/vendor-src/libleptris/src/leptris/dom/doctype.c +27 -0
  39. data/vendor-src/libleptris/src/leptris/dom/element.c +54 -9
  40. data/vendor-src/libleptris/src/leptris/dom/element.h +5 -0
  41. data/vendor-src/libleptris/src/leptris/dom/element_modify.c +46 -6
  42. data/vendor-src/libleptris/src/leptris/dtd/model.h +6 -0
  43. data/vendor-src/libleptris/src/leptris/dtd/parser.c +49 -12
  44. data/vendor-src/libleptris/src/leptris/html/html_parse.c +31 -3
  45. data/vendor-src/libleptris/src/leptris/leptris.c +4 -2
  46. data/vendor-src/libleptris/src/leptris/sax/pull.c +10 -1
  47. data/vendor-src/libleptris/src/leptris/sax/sax_internal.h +7 -0
  48. data/vendor-src/libleptris/src/leptris/sax/streaming.c +66 -0
  49. data/vendor-src/libleptris/src/leptris/xpath/evaluator.c +19 -2
  50. data/vendor-src/libleptris/src/leptris/xpath/evaluator_operators.c +126 -0
  51. data/vendor-src/libleptris/src/leptris/xpath/functions_ext31.c +653 -151
  52. data/vendor-src/libleptris/src/leptris/xpath/parser.c +4 -2
  53. data/vendor-src/libleptris/src/leptris/xslt/xslt_functions.c +9 -5
  54. data/vendor-src/libleptris/test/dom/test_node_surface_parity.cpp +81 -0
  55. data/vendor-src/libleptris/test/dtd/test_dtd_validate.cpp +113 -0
  56. data/vendor-src/libleptris/test/html/test_html.cpp +51 -0
  57. data/vendor-src/libleptris/test/xquery/qt3/fn/replace.xml +865 -0
  58. data/vendor-src/libleptris/test/xquery/qt3/fn/string-join.xml +449 -0
  59. data/vendor-src/libleptris/test/xquery/qt3/fn/tokenize.xml +639 -0
  60. data/vendor-src/libleptris/test/xquery/test_qt3.cpp +74 -6
  61. data/vendor-src/libleptris/vcpkg.json +1 -1
  62. 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: 0facebf2d44046c5a2c7ddaca5fdc8d2b5aea2b92017f6f1de8844122ec0eda4
4
+ data.tar.gz: f0f09d7786bd551d6f7077e1112e499ad25d6236a379d10607f59aeb2ddef02e
5
5
  SHA512:
6
- metadata.gz: 979d1f1a2e9d5815a6502c1990e171ff6e0e1df90751121bf96eacab517f7510b0eb30efca09e848fb3ab4c27e33fba4f2fbcf3f977811a786b766a23fa58545
7
- data.tar.gz: 3df36dfd59e33291acad1ad240541d12d2f1fb0cd7a4e4590ff1bf7569e30ce6b639fbbc6aaffc469bf49716dc914c9d5ef42dafada61da49c0f408711ed2949
6
+ metadata.gz: cbb0f3100ba9c705da214daafe066dcdbd973e41abb08d04e4b6db69a18091976b0083e223c9e888829fc46e5b6c4fe49ddf7d3defa380e62631fdc2d3897f7b
7
+ data.tar.gz: e11a0141773522b79b5ddc5217aa706b1ef01517e8981924842e0f1b7d272499601b3723d6df23c2741dea1e5e5733818a8b743a67af19b63be0f4ce9a554c10
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
+ 3.4 CI legs** — filed upstream as leptris/leptris#1242
58
+ (leptris-ruby#279 tracks the investigation). Six strikes; the
59
+ last three were consecutive reruns of one job, suggesting
60
+ per-machine determinism. Unreproducible across ~75k local reads
61
+ on darwin/arm64 (native + FFI, GC.stress) and x86_64 linux
62
+ (qemu; native + FFI; threaded and not). The leg sets
63
+ `LEPTRIS_SKIP_1242=1` to quarantine exactly that one spec on
64
+ that one leg until the engine ships the fix; every other leg
65
+ keeps the full guard.
66
+
67
+ - **#1220 reopened with the 1.9.203 repro**: HTML-document
68
+ mutations are still lost on serialization (the exact original
69
+ repro stands; node reads back mutated, serializer sees the old
70
+ tree). Tracking continues upstream.
71
+ - **#1197 reopened with fresh 1.9.203 evidence**: the s390x qemu
72
+ leg failed 442/742 with the identical `malformed input`
73
+ signature at ASCII offsets; the reopen quotes the close
74
+ comment's own terms. (1.9.204's notes carry no BE fix; the leg
75
+ stays experimental until the engine ships one.)
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.204"
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.204.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
@@ -381,6 +385,12 @@ attach_function :leptris_parse_string,
381
385
  [:leptris_document, :int], :leptris_status
382
386
  attach_function :leptris_document_set_doctype,
383
387
  [:leptris_document, :string, :string, :string], :leptris_doctype
388
+ # The #1229 remove-half (libleptris 1.9.204): un-set the
389
+ # declaration; un-set the DOCTYPE (NOT_FOUND when none set).
390
+ attach_function :leptris_document_clear_declaration,
391
+ [:leptris_document], :leptris_status
392
+ attach_function :leptris_document_remove_doctype,
393
+ [:leptris_document], :leptris_status
384
394
  attach_function :leptris_document_append_pi,
385
395
  [:leptris_document, :string, :string], :leptris_node_ref
386
396
  attach_function :leptris_document_remove_child,
@@ -849,6 +859,14 @@ attach_function :leptris_parse_string,
849
859
  [:leptris_sax_parser], :void
850
860
  attach_function :leptris_sax_parser_set_streaming,
851
861
  [:leptris_sax_parser, :int], :int
862
+ # One-shot input hint (libleptris 1.9.204): whole document in
863
+ # a single feed(), alive for the parser's lifetime — enables
864
+ # in-place scans. The binding's one-shot entry
865
+ # (leptris_sax_parse) is one-shot by construction; the engine
866
+ # sets this itself for pull's memory source. Attach-only
867
+ # until a chunked-feed face wants to opt in.
868
+ attach_function :leptris_sax_parser_set_one_shot,
869
+ [:leptris_sax_parser, :int], :void
852
870
 
853
871
  # Pull parsing (v1.6.0): StAX-style cursor over a document.
854
872
  attach_function :leptris_pull_new,
@@ -1024,6 +1042,30 @@ attach_function :leptris_parse_string,
1024
1042
  attach_function :leptris_str_has_nonstandard_entity,
1025
1043
  [:string, :size_t], :int
1026
1044
 
1045
+ # DTD validation (exported 1.9.202; the surface existed
1046
+ # since the CLI work): parse an internal subset, merge an
1047
+ # external subset (application-owned I/O), validate a
1048
+ # document. Errors carry message/element/line/column.
1049
+ class DTDErrorStruct < ::FFI::Struct
1050
+ layout :message, :string,
1051
+ :element_name, :string,
1052
+ :line, :int,
1053
+ :column, :int
1054
+ end
1055
+ attach_function :leptris_dtd_parse,
1056
+ [:string, :size_t], :pointer
1057
+ # leptris_document_get_dtd attached above with the readers
1058
+ attach_function :leptris_dtd_parse_external_subset,
1059
+ [:pointer, :string, :size_t], :int
1060
+ attach_function :leptris_dtd_set_pe_loader,
1061
+ [:pointer, :pointer, :pointer], :void
1062
+ attach_function :leptris_dtd_validate,
1063
+ [:leptris_document, :pointer, :pointer], :int
1064
+ attach_function :leptris_dtd_free,
1065
+ [:pointer], :void
1066
+ attach_function :leptris_dtd_error_free,
1067
+ [:pointer], :void
1068
+
1027
1069
  attach_function :leptris_rng_parse,
1028
1070
  [:string, :size_t, :pointer], :leptris_relaxng
1029
1071
  attach_function :leptris_rng_parse_file,
@@ -1100,6 +1142,11 @@ attach_function :leptris_parse_string,
1100
1142
  [:leptris_doctype], :string
1101
1143
 
1102
1144
  attach_function :leptris_free_string, [:pointer], :void
1145
+ # Engine-heap allocation (libleptris 1.9.204): buffers handed
1146
+ # to engine callbacks (the DTD PE loader) so the engine frees
1147
+ # through its matching deallocation path. Release with
1148
+ # leptris_free_string when NOT consumed by the engine.
1149
+ attach_function :leptris_alloc_buffer, [:size_t], :pointer
1103
1150
  attach_function :leptris_explicit_cleanup, [], :void
1104
1151
  attach_function :leptris_set_memory_management_functions,
1105
1152
  [:pointer, :pointer], :void
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,33 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.9.204] - 2026-09-19
4
+
5
+ ### Added
6
+
7
+ - leptris/html.h subsystem header; one-shot name fast paths (slice 1); document HTML parsing (api+perf)
8
+
9
+ ### Performance
10
+
11
+ - buffer-mode pull feeds in ONE call (slice-1 prerequisite) (sax)
12
+
13
+
14
+
15
+ ## [1.9.203] - 2026-09-19
16
+
17
+ ### Fixed
18
+
19
+ - leptris_dtd_parse honors the NULL-on-error contract (#1211) (dtd)
20
+
21
+
22
+
23
+ ## [1.9.202] - 2026-09-18
24
+
25
+ ### Fixed
26
+
27
+ - export the DTD validation surface; gate unmarked public declarations (api)
28
+
29
+
30
+
3
31
  ## [1.9.201] - 2026-09-18
4
32
 
5
33
  <!-- 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.204
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).
@@ -77,8 +77,10 @@ static void print_element_text(LeptrisElement e) {
77
77
  fputs(t, stdout);
78
78
  return;
79
79
  }
80
- for (LeptrisNodeRef c = leptris_element_first_child_any(e); c;
81
- c = leptris_element_next_sibling_any(c)) {
80
+ for (LeptrisNodeRef c = (LeptrisNodeRef)leptris_element_first_child_any(e);
81
+ c;
82
+ c = (LeptrisNodeRef)leptris_element_next_sibling_any(
83
+ (LeptrisElement)c)) {
82
84
  const char* txt = leptris_text_node_get_content(c);
83
85
  if (txt) fputs(txt, stdout);
84
86
  }
@@ -303,6 +303,36 @@ void find_by_attribute(LeptrisElement parent,
303
303
  }
304
304
  ```
305
305
 
306
+ ## Parsing HTML
307
+
308
+ HTML is a first-class input: the WHATWG-conformant tolerant parser
309
+ (`leptris_parse_html_string`) plus a libxml2/Nokogiri-compatibility
310
+ mode (`leptris_parse_html4_string`), both declared in
311
+ `leptris/html.h`:
312
+
313
+ ```c
314
+ #include "leptris/html.h"
315
+
316
+ LeptrisStatus st = LEPTRIS_OK;
317
+ LeptrisDocument doc =
318
+ leptris_parse_html_string("<p>Hello <b>world", 18, &st);
319
+ /* Implied end tags, void elements, raw-text <script>/<style>,
320
+ * minimized + unquoted attribute values, case-insensitive names and
321
+ * the HTML named-entity table are all handled. Malformed input
322
+ * never fails the parse — it degrades to text. The result
323
+ * serializes and queries (XPath/XSLT) like any XML document. */
324
+ ```
325
+
326
+ Choose the mode by contract:
327
+
328
+ | Function | Mode | Shape |
329
+ |----------|------|-------|
330
+ | `leptris_parse_html_string` | WHATWG | full html5lib-conformant tree construction |
331
+ | `leptris_parse_html4_string` | libxml2/Nokogiri | fragment shape: no synthesized `<html>/<head>/<body>`, no implied `<tbody>`; leading `<script>/<style>` content stays in `<body>` |
332
+
333
+ Memory: identical to XML — `leptris_document_free` releases
334
+ everything; input must be valid UTF-8.
335
+
306
336
  ## Performance Tips
307
337
 
308
338
  1. **Use in-place parsing** for string data when possible
@@ -31,6 +31,22 @@ def declared_public_symbols(root: Path):
31
31
  return names
32
32
 
33
33
 
34
+ def unmarked_public_declarations(root: Path):
35
+ """Public-header leptris_* function declarations WITHOUT
36
+ LEPTRIS_API: they link fine into the static lib (the CLI works)
37
+ but never export for FFI consumers — the dtd.h class caught by
38
+ the ruby binding (#1183)."""
39
+ pat = re.compile(
40
+ r"^(?!LEPTRIS_API|static|typedef|struct|#|//|\*|/\*)(?:const\s+|unsigned\s+|struct\s+)*"
41
+ r"[A-Za-z_][\w\s\*]*?\b(leptris_[a-z_0-9]+)\s*\(",
42
+ flags=re.M,
43
+ )
44
+ found = set()
45
+ for h in (root / "src" / "include").rglob("*.h"):
46
+ for m in pat.finditer(h.read_text()):
47
+ found.add(m.group(1))
48
+ return found
49
+
34
50
  def exported_symbols(lib: Path):
35
51
  out = subprocess.run(
36
52
  ["nm", "-gU", str(lib)], capture_output=True, text=True, check=True
@@ -65,11 +81,15 @@ def main():
65
81
  undeclared = sorted(exported - declared)
66
82
  missing = sorted(declared - exported)
67
83
 
84
+ unmarked = sorted(unmarked_public_declarations(root) - declared)
85
+
68
86
  failures = []
69
87
  for n in undeclared:
70
88
  failures.append(f"UNDECLARED EXPORT {n}: exported but absent from src/include/")
71
89
  for n in missing:
72
90
  failures.append(f"MISSING EXPORT {n}: declared public but not exported")
91
+ for n in unmarked:
92
+ failures.append(f"UNMARKED PUBLIC {n}: declared in a public header without LEPTRIS_API")
73
93
 
74
94
  if failures:
75
95
  print("EXPORT SURFACE DRIFT:")