leptris 1.9.197.2-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 (124) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3890 -0
  3. data/CLAUDE.md +115 -0
  4. data/CONTEXT.md +39 -0
  5. data/LICENSE.md +33 -0
  6. data/README.adoc +755 -0
  7. data/Rakefile +407 -0
  8. data/TODO.perf/01-ext-accelerated-default-reads.md +28 -0
  9. data/TODO.perf/02-auto-enable-native-layer.md +19 -0
  10. data/TODO.perf/03-bulk-xpath-result-materialization.md +18 -0
  11. data/TODO.perf/04-ext-bound-serialization.md +15 -0
  12. data/TODO.perf/05-native-surface-completion.md +19 -0
  13. data/TODO.perf/06-perf-battery-native-legs.md +16 -0
  14. data/TODO.perf/07-native-read-floor.md +31 -0
  15. data/TODO.perf/08-version-coherent-native-mutations.md +24 -0
  16. data/TODO.perf/09-adoption-lift-fast-path.md +38 -0
  17. data/TODO.perf/10-bulk-attribute-materialization.md +20 -0
  18. data/TODO.perf/11-moxml-gap-battery.md +18 -0
  19. data/TODO.perf/12-document-lifetime-in-c.md +47 -0
  20. data/TODO.perf/13-structural-memos.md +49 -0
  21. data/TODO.perf/14-insert-family-c-bound.md +31 -0
  22. data/TODO.perf/15-compiled-expression-cache.md +35 -0
  23. data/TODO.perf/16-at-xpath-single-result-seam.md +38 -0
  24. data/TODO.perf/17-precomputed-fast-path-flags.md +35 -0
  25. data/TODO.perf/18-inner-html-one-c-pass.md +32 -0
  26. data/TODO.perf/19-lazy-node-pointer.md +39 -0
  27. data/TODO.perf/20-css-translation-cache.md +24 -0
  28. data/TODO.perf/21-key-memo-consult.md +20 -0
  29. data/TODO.perf/22-eager-nodeset-materialization.md +30 -0
  30. data/TODO.perf/23-cbound-value-mutations.md +27 -0
  31. data/TODO.perf/24-immutable-read-lanes.md +22 -0
  32. data/TODO.perf/25-scope-owned-bulk-path.md +39 -0
  33. data/TODO.perf/26-post-mutation-memo-seeding.md +26 -0
  34. data/TODO.perf/27-c-yield-traversal.md +38 -0
  35. data/TODO.perf/28-address-fills.md +25 -0
  36. data/TODO.perf/29-ns-xpath-compiled.md +23 -0
  37. data/TODO.perf/30-copy-and-element-child-faces.md +31 -0
  38. data/TODO.perf/31-battery-rounds-4-8.md +23 -0
  39. data/TODO.perf/32-clean-host-floor-table.md +29 -0
  40. data/TODO.perf/33-cbound-root-set.md +29 -0
  41. data/TODO.perf/34-fragment-fast-lane.md +41 -0
  42. data/TODO.perf/35-parse-default-c-face.md +24 -0
  43. data/TODO.perf/36-raise-in-c-serializer-encoding.md +34 -0
  44. data/TODO.perf/37-document-lazy-pointer.md +28 -0
  45. data/TODO.perf/38-bulk-walk-and-bench-gate.md +23 -0
  46. data/TODO.restructure/01-constraint-compliance-audit.md +40 -0
  47. data/TODO.restructure/02-deep-copy-seam.md +21 -0
  48. data/TODO.restructure/03-evaluation-context-seam.md +18 -0
  49. data/TODO.restructure/04-spec-mece-restructure.md +20 -0
  50. data/TODO.restructure/05-memory-documentation.md +13 -0
  51. data/TODO.restructure/06-architecture-map-refresh.md +12 -0
  52. data/TODO.restructure/07-external-gates.md +16 -0
  53. data/TODO.restructure/08-new-code-audit-and-ext-memo.md +11 -0
  54. data/TODO.restructure/09-iteration-scope.md +29 -0
  55. data/TODO.restructure/10-cold-attr-read-diet.md +19 -0
  56. data/TODO.restructure/11-construction-factory-diet.md +16 -0
  57. data/TODO.restructure/12-resultattr-merge.md +12 -0
  58. data/TODO.restructure/13-spec-mece-followup.md +9 -0
  59. data/TODO.restructure/14-constraint-audit-refresh.md +15 -0
  60. data/TODO.restructure/15-readme-feature-documentation.md +10 -0
  61. data/TODO.restructure/16-claudemd-map-refresh.md +5 -0
  62. data/TODO.restructure/17-perf-drift-record.md +12 -0
  63. data/TODO.restructure/18-audit-spec-doubles-and-gem-contents.md +12 -0
  64. data/TODO.restructure/19-gitignore-hygiene.md +10 -0
  65. data/TODO.restructure/20-utf8proc-enablement.md +34 -0
  66. data/TODO.restructure/21-typeddata-variant-prototype.md +42 -0
  67. data/TODO.restructure/22-sax-drain-bulk-strip.md +28 -0
  68. data/docs/adr/0001-lockstep-mirror.md +17 -0
  69. data/docs/adr/0002-utf8-at-the-seam.md +19 -0
  70. data/docs/adr/0003-readonly-memoization-pattern.md +34 -0
  71. data/docs/adr/0004-lifetime-guard.md +21 -0
  72. data/docs/adr/0005-autoload-manifest-ordering.md +19 -0
  73. data/docs/adr/0006-ruby-variant-policy.md +25 -0
  74. data/ext/build_windows_native.rb +42 -0
  75. data/ext/leptris/native/extconf.rb +55 -0
  76. data/ext/leptris/native/native.c +2646 -0
  77. data/leptris.gemspec +43 -0
  78. data/lib/leptris/version.rb +5 -0
  79. data/lib/leptris/xml/attr.rb +76 -0
  80. data/lib/leptris/xml/c_string_array.rb +37 -0
  81. data/lib/leptris/xml/cdata.rb +27 -0
  82. data/lib/leptris/xml/comment.rb +27 -0
  83. data/lib/leptris/xml/css_to_xpath.rb +192 -0
  84. data/lib/leptris/xml/descriptor.rb +214 -0
  85. data/lib/leptris/xml/diff.rb +74 -0
  86. data/lib/leptris/xml/doc_type.rb +54 -0
  87. data/lib/leptris/xml/document.rb +716 -0
  88. data/lib/leptris/xml/document_fragment.rb +63 -0
  89. data/lib/leptris/xml/element.rb +771 -0
  90. data/lib/leptris/xml/entity_reference.rb +13 -0
  91. data/lib/leptris/xml/evaluation_context.rb +32 -0
  92. data/lib/leptris/xml/ffi.rb +1527 -0
  93. data/lib/leptris/xml/iteration_scope.rb +56 -0
  94. data/lib/leptris/xml/iterparse.rb +144 -0
  95. data/lib/leptris/xml/namespace.rb +43 -0
  96. data/lib/leptris/xml/native.so +0 -0
  97. data/lib/leptris/xml/native_layer.rb +92 -0
  98. data/lib/leptris/xml/node.rb +686 -0
  99. data/lib/leptris/xml/node_set.rb +212 -0
  100. data/lib/leptris/xml/parse_options.rb +97 -0
  101. data/lib/leptris/xml/plan_value.rb +121 -0
  102. data/lib/leptris/xml/processing_instruction.rb +90 -0
  103. data/lib/leptris/xml/pull.rb +212 -0
  104. data/lib/leptris/xml/relaxng.rb +126 -0
  105. data/lib/leptris/xml/result_attr.rb +39 -0
  106. data/lib/leptris/xml/result_text.rb +36 -0
  107. data/lib/leptris/xml/sax/document.rb +45 -0
  108. data/lib/leptris/xml/sax/dom_dispatch.rb +144 -0
  109. data/lib/leptris/xml/sax/parser.rb +326 -0
  110. data/lib/leptris/xml/sax/recorder.rb +374 -0
  111. data/lib/leptris/xml/sax.rb +14 -0
  112. data/lib/leptris/xml/schematron.rb +88 -0
  113. data/lib/leptris/xml/searchable.rb +361 -0
  114. data/lib/leptris/xml/serialization.rb +180 -0
  115. data/lib/leptris/xml/text.rb +32 -0
  116. data/lib/leptris/xml/xpath.rb +89 -0
  117. data/lib/leptris/xml/xquery.rb +77 -0
  118. data/lib/leptris/xml/xslt.rb +85 -0
  119. data/lib/leptris/xml.rb +108 -0
  120. data/lib/leptris.rb +53 -0
  121. data/lib/libleptris.so +0 -0
  122. data/lib/libutf8proc.so.3 +0 -0
  123. data/scripts/gem_smoke.rb +36 -0
  124. metadata +214 -0
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ # Pull parsing (libleptris v1.6.0): a StAX-style cursor over a
6
+ # document. Each #next advances one event; attributes are readable
7
+ # only during the start_element event just returned.
8
+ #
9
+ # Leptris::XML::Pull.parse(xml).each do |event|
10
+ # case event.type
11
+ # when :start_element then handle(event.name, event.attrs)
12
+ # when :text then handle(event.text)
13
+ # end
14
+ # end
15
+ #
16
+ module Leptris::XML::Pull
17
+ # Constructed positionally by the parser: the keyword-init
18
+ # variant cost ~220ns/event in batch drains (round XXI). Readers
19
+ # (event.type, event.attrs, ...) are unchanged.
20
+ Event = Struct.new(:type, :name, :text, :attrs)
21
+
22
+ TYPES = {
23
+ Leptris::XML::FFI::PULL_START_ELEMENT => :start_element,
24
+ Leptris::XML::FFI::PULL_END_ELEMENT => :end_element,
25
+ Leptris::XML::FFI::PULL_TEXT => :text,
26
+ Leptris::XML::FFI::PULL_COMMENT => :comment,
27
+ Leptris::XML::FFI::PULL_CDATA => :cdata,
28
+ Leptris::XML::FFI::PULL_PI => :pi,
29
+ Leptris::XML::FFI::PULL_END_DOCUMENT => :end_document,
30
+ Leptris::XML::FFI::PULL_ERROR => :error,
31
+ # Prefix events ride the stream ("" prefix = the default
32
+ # namespace): the engine has always delivered them; the map
33
+ # lacked the kinds, silently yielding nil-typed Events — which
34
+ # the staging guard then misread as corruption after the real
35
+ # engine fix landed (leptris/leptris#648's residual).
36
+ Leptris::XML::FFI::PULL_START_PREFIX => :start_prefix,
37
+ Leptris::XML::FFI::PULL_END_PREFIX => :end_prefix,
38
+ }.freeze
39
+
40
+ class Parser
41
+ def self.parse(xml_or_io)
42
+ xml = xml_or_io.is_a?(String) ? xml_or_io : xml_or_io.read
43
+ new(Leptris::XML::FFI.leptris_pull_new(xml, xml.bytesize))
44
+ end
45
+
46
+ def self.parse_file(path)
47
+ new(Leptris::XML::FFI.leptris_pull_new_file(path))
48
+ end
49
+
50
+ def initialize(handle)
51
+ raise Leptris::XML::ParseError, "leptris_pull_new failed" if handle.null?
52
+ @handle = handle
53
+ end
54
+
55
+ # Releases the C parser. Safe to call twice.
56
+ def free
57
+ return if @handle.nil?
58
+ Leptris::XML::FFI.leptris_pull_free(@handle)
59
+ @handle = nil
60
+ end
61
+
62
+ def each
63
+ return enum_for(:each) unless block_given?
64
+ loop do
65
+ event = next_event
66
+ break if event.nil?
67
+ yield event
68
+ break if event.type == :end_document || event.type == :error
69
+ end
70
+ self
71
+ end
72
+
73
+ # Bulk-delivers events (libleptris 1.9.7, upstream #589): ONE C
74
+ # call stages up to +max_count+ events — the per-event dispatch
75
+ # that made streaming ~145x slower than a DOM parse collapses to
76
+ # amortized noise. Per the engine's protocol, attributes are
77
+ # captured only for the LAST start_element of each batch
78
+ # (earlier starts in the same batch carry nil attrs — segment
79
+ # boundaries fall where they fall); use #each when every
80
+ # start's attrs must be captured.
81
+ def each_batch(max_count = 256)
82
+ return enum_for(:each_batch, max_count) unless block_given?
83
+ stride = Leptris::XML::FFI::PullEventStruct.size
84
+ type_off = 0
85
+ name_off = Leptris::XML::FFI::PullEventStruct.offset_of(:name)
86
+ text_off = Leptris::XML::FFI::PullEventStruct.offset_of(:text)
87
+ buffer = Leptris::XML::FFI.scratch_events(max_count)
88
+ while (n = Leptris::XML::FFI.leptris_pull_next_batch(
89
+ @handle, buffer, max_count)) > 0
90
+ events = Array.new(n) do |i|
91
+ base = i * stride
92
+ name_ptr = buffer.get_pointer(base + name_off)
93
+ text_ptr = buffer.get_pointer(base + text_off)
94
+ type = TYPES[buffer.get_int(base + type_off)]
95
+ name = name_ptr.null? ? nil :
96
+ name_ptr.read_string.force_encoding(Encoding::UTF_8)
97
+ # Staging-corruption guard (leptris/leptris#648): the C
98
+ # batch's staging arena misplaces record strings once the
99
+ # staged content crosses its block boundary (~185 bytes of
100
+ # attribute value under nested attr-carrying ancestors) —
101
+ # names come back empty, control-byte-laced, or
102
+ # invalid-encoding, and unknown type codes surface as nil.
103
+ # Element names can never look like that: fail loudly
104
+ # instead of delivering garbage, and point at the cursor
105
+ # path, which reads the same documents correctly.
106
+ if type.nil? ||
107
+ (name && (!name.valid_encoding? ||
108
+ name.match?(/[\x00-\x08\x0e-\x1f]/))) ||
109
+ (%i[start_element end_element pi].include?(type) &&
110
+ (name.nil? || name.empty?))
111
+ raise Leptris::XML::Error,
112
+ "pull batch staging corruption (leptris/leptris#648) — " \
113
+ "use Parser#each (cursor) for this document"
114
+ end
115
+ text = text_ptr.null? ? nil :
116
+ text_ptr.read_string.force_encoding(Encoding::UTF_8)
117
+ text = Leptris::XML::FFI.read_pi_data(text) if type == :pi
118
+ Event.new(type, name, text, nil)
119
+ end
120
+ # The attr mirror holds the batch's most recent start.
121
+ last_start = nil
122
+ events.each_with_index do |event, i|
123
+ last_start = i if event.type == :start_element
124
+ end
125
+ events[last_start].attrs = capture_attrs if last_start
126
+ events.each { |event| yield event }
127
+ tail = events.last.type
128
+ break if tail == :end_document || tail == :error
129
+ end
130
+ self
131
+ end
132
+
133
+ # Hot-loop offsets: derived from PullEventStruct's layout (the
134
+ # struct stays the single source of truth for the ABI), read via
135
+ # get_int/get_pointer so the per-event struct-wrapper allocation
136
+ # disappears from streaming loops.
137
+ NAME_OFFSET = Leptris::XML::FFI::PullEventStruct.offset_of(:name)
138
+ TEXT_OFFSET = Leptris::XML::FFI::PullEventStruct.offset_of(:text)
139
+ private_constant :NAME_OFFSET, :TEXT_OFFSET
140
+
141
+ # Advances the cursor and returns the next Event, or nil after the
142
+ # end of the document. Attribute values are captured during
143
+ # start_element because the C accessors are valid only for the
144
+ # most recently returned event.
145
+ def next_event
146
+ raw = Leptris::XML::FFI.leptris_pull_next(@handle)
147
+ return nil if raw.null?
148
+ type = TYPES[raw.get_int(0)]
149
+ name_ptr = raw.get_pointer(NAME_OFFSET)
150
+ text_ptr = raw.get_pointer(TEXT_OFFSET)
151
+ attrs = type == :start_element ? capture_attrs : nil
152
+ text = text_ptr.null? ? nil :
153
+ text_ptr.read_string.force_encoding(Encoding::UTF_8)
154
+ text = Leptris::XML::FFI.read_pi_data(text) if type == :pi
155
+ Event.new(
156
+ type,
157
+ name_ptr.null? ? nil : name_ptr.read_string.force_encoding(Encoding::UTF_8),
158
+ text, attrs
159
+ )
160
+ end
161
+
162
+ private
163
+
164
+ # One count-only query plus one flat copy (leptris_pull_attrs,
165
+ # 1.9.7) — the char** wire format the SAX callbacks already use —
166
+ # replaces the 2N per-index dispatches.
167
+ def capture_attrs
168
+ total = Leptris::XML::FFI.leptris_pull_attrs(@handle, nil, 0)
169
+ return nil if total.zero?
170
+ buffer = Leptris::XML::FFI.scratch_pointers(2 * total)
171
+ copied = Leptris::XML::FFI.leptris_pull_attrs(@handle, buffer, total)
172
+ ptr_size = ::FFI.type_size(:pointer)
173
+ hash = {}
174
+ i = 0
175
+ while i < copied
176
+ name = buffer.get_pointer(2 * i * ptr_size).read_string
177
+ .force_encoding(Encoding::UTF_8)
178
+ value = buffer.get_pointer((2 * i + 1) * ptr_size).read_string
179
+ .force_encoding(Encoding::UTF_8)
180
+ hash[name] = value
181
+ i += 1
182
+ end
183
+ hash
184
+ end
185
+ end
186
+
187
+ def self.parse(xml_or_io, &block)
188
+ parser = Parser.parse(xml_or_io)
189
+ if block
190
+ begin
191
+ parser.each(&block)
192
+ ensure
193
+ parser.free
194
+ end
195
+ else
196
+ parser
197
+ end
198
+ end
199
+
200
+ def self.parse_file(path, &block)
201
+ parser = Parser.parse_file(path)
202
+ if block
203
+ begin
204
+ parser.each(&block)
205
+ ensure
206
+ parser.free
207
+ end
208
+ else
209
+ parser
210
+ end
211
+ end
212
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module Leptris::XML::RelaxNG
6
+ module_function
7
+
8
+ def parse(schema_xml)
9
+ Schema.parse(schema_xml)
10
+ end
11
+
12
+ def parse_file(path)
13
+ Schema.parse_file(path)
14
+ end
15
+
16
+ # A compiled RELAX NG schema (libleptris >= 1.9.115): Jing-parity
17
+ # validation against the standard DOM — parse once, validate any
18
+ # number of documents. Schema errors publish to the thread-global
19
+ # error channel, so failures raise with the engine's detail
20
+ # ("unknown pattern element: ...", "cannot open schema file ...").
21
+ #
22
+ # rng = Leptris::XML::RelaxNG.parse(schema_xml)
23
+ # rng.valid?(doc) # => true/false
24
+ # rng.validate(doc) # => [] or ["1:0: error: element ..."]
25
+ # rng.validate_errors(doc) # => [] or [{line:, column:, message:}]
26
+ # rng.validate_report(doc) # => [] or [{kind:, message:, offender:, ...}]
27
+ #
28
+ class Schema
29
+ # GC-managed compiled handle.
30
+ class Handle < ::FFI::AutoPointer
31
+ def self.release(ptr)
32
+ Leptris::XML::FFI.leptris_rng_free(ptr)
33
+ end
34
+ end
35
+
36
+ # Compile a schema from XML text. Raises Error on a schema
37
+ # error (detail via leptris_last_error).
38
+ def self.parse(schema_xml)
39
+ xml = schema_xml.to_s
40
+ raw = Leptris::XML::FFI.leptris_rng_parse(xml, xml.bytesize, nil)
41
+ if raw.null?
42
+ raise Leptris::XML::Error,
43
+ "schema parse failed: #{Leptris::XML::FFI.leptris_last_error}"
44
+ end
45
+ new(Handle.new(raw))
46
+ end
47
+
48
+ # Compile from a file; <include href="..."> resolves relative to
49
+ # the file's directory (Jing's override rules apply).
50
+ def self.parse_file(path)
51
+ raw = Leptris::XML::FFI.leptris_rng_parse_file(path.to_s, nil)
52
+ if raw.null?
53
+ raise Leptris::XML::Error,
54
+ "schema parse failed: #{Leptris::XML::FFI.leptris_last_error}"
55
+ end
56
+ new(Handle.new(raw))
57
+ end
58
+
59
+ def initialize(handle)
60
+ @handle = handle
61
+ end
62
+
63
+ # Nokogiri shape: an array of error strings (the engine emits
64
+ # Jing's "line:col: error: message" form), empty when valid.
65
+ # libleptris 1.9.179 (#878): errors accumulate — every failure
66
+ # from the validate call is returned, not just the first.
67
+ def validate(document)
68
+ structured = validate_errors(document)
69
+ unless structured.empty?
70
+ return structured.map do |e|
71
+ e[:line] > 0 ? "#{e[:line]}:#{e[:column]}: error: #{e[:message]}"
72
+ : e[:message]
73
+ end
74
+ end
75
+ # Invalid verdict with nothing accumulated: the back-compat
76
+ # single-error accessor is the last resort (nil-safe).
77
+ return [] if valid?(document)
78
+ [Leptris::XML::FFI.leptris_rng_error(@handle)].compact
79
+ end
80
+
81
+ def valid?(document)
82
+ Leptris::XML::FFI.leptris_rng_validate(@handle,
83
+ document.c_ptr) != 0
84
+ end
85
+
86
+ # Structured errors: [{ line:, column:, message: }] — the fields
87
+ # callers need for log formatting, instead of re-parsing the
88
+ # Jing-form strings from #validate. Empty when valid. Message,
89
+ # line, and column carry Jing's exact attribution (libleptris
90
+ # >= 1.9.179, upstream #878). Rendered from #validate_report —
91
+ # the single enumeration of the error surface.
92
+ def validate_errors(document)
93
+ validate_report(document).map do |r|
94
+ { line: r[:line], column: r[:column], message: r[:message] }
95
+ end
96
+ end
97
+
98
+ # The whole validation report, one C call (libleptris >= 1.9.190):
99
+ # [{ kind:, message:, offender:, line:, column: }] — empty when
100
+ # valid. `kind` is the #1126 failure-class taxonomy
101
+ # ("missing-required-attr", "attr-not-allowed", ...), `offender`
102
+ # the attributed element/attribute name (nil when unknown).
103
+ # Strings are read into Ruby copies before returning.
104
+ def validate_report(document)
105
+ ok = Leptris::XML::FFI.leptris_rng_validate(@handle,
106
+ document.c_ptr)
107
+ return [] if ok != 0
108
+ out = ::FFI::MemoryPointer.new(:pointer)
109
+ count = Leptris::XML::FFI.leptris_rng_error_report(@handle, out)
110
+ return [] if count.zero?
111
+ base = out.read_pointer
112
+ return [] if base.null?
113
+ record = Leptris::XML::FFI::RngErrorRecord
114
+ Array.new(count) do |i|
115
+ r = record.new(base + i * record.size)
116
+ {
117
+ kind: r[:kind],
118
+ message: r[:message],
119
+ offender: r[:offender],
120
+ line: r[:line],
121
+ column: r[:column],
122
+ }
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Attribute-kind item from an XPath result set (ruby#153): the
4
+ # engine exposes synthetic attribute nodes whose name and value are
5
+ # readable only through the live result handle — the NodeSet
6
+ # captures both at materialization time and hands them to this
7
+ # wrapper, so the faces stay usable after the result is freed.
8
+ #
9
+ # doc.xpath("//item/@id").first.name # => "id"
10
+ # doc.xpath("//item/@id").first.value # => "1"
11
+ #
12
+ class Leptris::XML::ResultAttr < Leptris::XML::Node
13
+ def initialize(c_ptr, document, name, value, parent: nil, node_type: nil)
14
+ super(c_ptr, document, parent: parent, node_type: node_type)
15
+ @attr_name = name
16
+ @attr_value = value
17
+ end
18
+
19
+ def name
20
+ @attr_name
21
+ end
22
+
23
+ # Nokogiri's Attr#value is the attribute's string value.
24
+ def value
25
+ @attr_value
26
+ end
27
+
28
+ def content
29
+ @attr_value
30
+ end
31
+
32
+ def to_s
33
+ @attr_value
34
+ end
35
+
36
+ def attribute?
37
+ true
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Text-kind item from an XPath/XQuery sequence result: the engine
4
+ # materializes sequence, map, and array items as synthetic text
5
+ # nodes (leptris/types.h's internal kind 8) whose value is readable
6
+ # only through the live result handle — the node-pointer accessors
7
+ # return nothing for them. The NodeSet therefore captures the string
8
+ # value at materialization time and hands it to this wrapper.
9
+ #
10
+ # doc.xpath("for $w in //a return string($w/@v)").map(&:content)
11
+ # # => ["1", "2"]
12
+ #
13
+ class Leptris::XML::ResultText < Leptris::XML::Node
14
+ def initialize(c_ptr, document, value, parent: nil, node_type: nil)
15
+ super(c_ptr, document, parent: parent, node_type: node_type)
16
+ @value = value
17
+ end
18
+
19
+ def name
20
+ "text"
21
+ end
22
+
23
+ # The captured string: safe to read even after the owning result
24
+ # set has been collected (unlike pointer-backed reads).
25
+ def content
26
+ @value
27
+ end
28
+
29
+ def to_s
30
+ @value
31
+ end
32
+
33
+ def text?
34
+ true
35
+ end
36
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Leptris::XML::SAX::Document
4
+ # Called when an XML declaration is parsed.
5
+ def xmldecl(version, encoding, standalone)
6
+ end
7
+
8
+ def start_document
9
+ end
10
+
11
+ def end_document
12
+ end
13
+
14
+ # Called at the beginning of an element.
15
+ # +attrs+ is an Array of [name, value] pairs in source order.
16
+ def start_element(name, attrs = [])
17
+ end
18
+
19
+ def end_element(name)
20
+ end
21
+
22
+ def characters(string)
23
+ end
24
+
25
+ def comment(string)
26
+ end
27
+
28
+ def cdata_block(string)
29
+ end
30
+
31
+ def processing_instruction(name, content)
32
+ end
33
+
34
+ def start_prefix_mapping(prefix, uri)
35
+ end
36
+
37
+ def end_prefix_mapping(prefix)
38
+ end
39
+
40
+ def warning(string)
41
+ end
42
+
43
+ def error(message, line = 0, column = 0)
44
+ end
45
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ # DOM-backed SAX dispatch (leptris-ruby#95): the engine's streaming
4
+ # attribute buffer corrupts leading attribute pairs when several
5
+ # nested ancestor levels each carry attributes — the callback
6
+ # transport, the recorder's packed arena, AND the pull parser all
7
+ # deliver the loss on the issue fixture (verified), while the DOM
8
+ # parser reads the same bytes correctly. Until the engine fix
9
+ # lands, SAX::Parser delivers from a DOM parse: same call shapes as
10
+ # the streaming transports — attach-only-overridden kinds,
11
+ # one-argument start_element arity, QName element names ("p:a"),
12
+ # UTF-8 strings, PI-data normalization.
13
+ #
14
+ # The walk is lean by construction: elements wrap (identity, name
15
+ # and prefix memos); every other child kind is read straight from
16
+ # its pointer — content getters per node type, never a wrapper.
17
+ module Leptris::XML::SAX::DomDispatch
18
+ module_function
19
+
20
+ def parse(handler, dispatched, string)
21
+ doc = Leptris::XML::Document.parse(string)
22
+ begin
23
+ dispatch(handler, dispatched, doc)
24
+ ensure
25
+ doc.free
26
+ end
27
+ end
28
+
29
+ def dispatch(handler, dispatched, doc)
30
+ handler.start_document if dispatched[:start_document]
31
+ doc_node = doc.node
32
+ if doc_node
33
+ visit_children(doc_node.c_ptr, doc, handler, dispatched,
34
+ element_parent: false)
35
+ end
36
+ handler.end_document if dispatched[:end_document]
37
+ self
38
+ end
39
+
40
+ NODE = Leptris::XML::FFI
41
+ ELEMENT = Leptris::XML::FFI::NODE_ELEMENT
42
+ TEXT = Leptris::XML::FFI::NODE_TEXT
43
+ COMMENT = Leptris::XML::FFI::NODE_COMMENT
44
+ CDATA = Leptris::XML::FFI::NODE_CDATA
45
+ PI = Leptris::XML::FFI::NODE_PI
46
+
47
+ def visit_children(c_ptr, doc, handler, dispatched, element_parent: true)
48
+ wants_text_kinds = dispatched[:characters] ||
49
+ dispatched[:comment] ||
50
+ dispatched[:cdata] || dispatched[:pi]
51
+ if element_parent && !wants_text_kinds
52
+ # Element-only handlers under an element: the element batch
53
+ # hands out element pointers with no per-child get_type
54
+ # (round XXIII's path) — text/comments/PIs are never fetched.
55
+ # (The document node is not an element handle — the typed
56
+ # walk serves it.)
57
+ Leptris::XML::FFI.fetch_element_children(c_ptr).each do |child_ptr|
58
+ visit_element(
59
+ Leptris::XML::Node.wrap(child_ptr, doc, node_type: ELEMENT),
60
+ handler, dispatched)
61
+ end
62
+ return
63
+ end
64
+ pointers, kinds = Leptris::XML::FFI.fetch_children(c_ptr)
65
+ pointers.each_with_index do |child_ptr, i|
66
+ case kinds[i]
67
+ when ELEMENT
68
+ visit_element(
69
+ Leptris::XML::Node.wrap(child_ptr, doc, node_type: ELEMENT),
70
+ handler, dispatched)
71
+ when TEXT
72
+ if dispatched[:characters]
73
+ handler.characters(utf8(NODE.leptris_text_node_get_content(child_ptr)))
74
+ end
75
+ when CDATA
76
+ if dispatched[:cdata]
77
+ handler.cdata_block(utf8(NODE.leptris_cdata_node_get_content(child_ptr)))
78
+ end
79
+ when COMMENT
80
+ if dispatched[:comment]
81
+ handler.comment(utf8(NODE.leptris_comment_node_get_content(child_ptr)))
82
+ end
83
+ when PI
84
+ if dispatched[:pi]
85
+ handler.processing_instruction(
86
+ utf8(NODE.leptris_pi_node_get_target(child_ptr)),
87
+ Leptris::XML::FFI.read_pi_data(
88
+ utf8(NODE.leptris_pi_node_get_data(child_ptr))))
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def visit_element(element, handler, dispatched)
95
+ qname = element.prefix ? "#{element.prefix}:#{element.name}" : element.name
96
+ if dispatched[:start_prefix]
97
+ element.namespace_definitions.each do |ns|
98
+ handler.start_prefix_mapping(ns.prefix.to_s, ns.href.to_s)
99
+ end
100
+ end
101
+ if dispatched[:start_element]
102
+ if dispatched[:start_element] == :one_arg
103
+ handler.start_element(qname)
104
+ else
105
+ handler.start_element(qname, declared_pairs(element))
106
+ end
107
+ end
108
+ visit_children(element.c_ptr, element.document, handler, dispatched)
109
+ handler.end_element(qname) if dispatched[:end_element]
110
+ if dispatched[:end_prefix]
111
+ element.namespace_definitions.reverse_each do |ns|
112
+ handler.end_prefix_mapping(ns.prefix.to_s)
113
+ end
114
+ end
115
+ end
116
+
117
+ # start_element's pairs with xmlns declarations carried — the
118
+ # engine's streaming contract reports declarations among the
119
+ # attribute pairs at their byte positions. Since libleptris
120
+ # 1.9.18 (upstream #635, closing leptris-ruby#99's documented
121
+ # difference), the raw qname-ordered list reproduces the exact
122
+ # interleave.
123
+ def declared_pairs(element)
124
+ Leptris::XML::FFI.fetch_attributes_raw(element.c_ptr)
125
+ end
126
+
127
+ # [name, value] pairs in source order, read straight from the
128
+ # attribute chain — no Attr objects, no hash.
129
+ def attr_pairs(c_ptr)
130
+ pairs = []
131
+ attr = Leptris::XML::FFI.leptris_element_first_attribute(c_ptr)
132
+ until attr.nil? || attr.null?
133
+ pairs << [utf8(Leptris::XML::FFI.leptris_attribute_get_name(attr)),
134
+ utf8(Leptris::XML::FFI.leptris_attribute_get_value(
135
+ c_ptr, attr))]
136
+ attr = Leptris::XML::FFI.leptris_attribute_next(attr)
137
+ end
138
+ pairs
139
+ end
140
+
141
+ def utf8(str)
142
+ str.nil? ? nil : str.force_encoding(Encoding::UTF_8)
143
+ end
144
+ end