leptris 1.5.0-aarch64-linux → 1.6.1-aarch64-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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -0
  3. data/CLAUDE.md +76 -87
  4. data/Rakefile +1 -1
  5. data/lib/leptris/version.rb +1 -1
  6. data/lib/leptris/xml/cdata.rb +1 -0
  7. data/lib/leptris/xml/comment.rb +1 -0
  8. data/lib/leptris/xml/document.rb +47 -14
  9. data/lib/leptris/xml/element.rb +21 -3
  10. data/lib/leptris/xml/ffi.rb +70 -13
  11. data/lib/leptris/xml/iterparse.rb +57 -0
  12. data/lib/leptris/xml/node.rb +32 -10
  13. data/lib/leptris/xml/node_set.rb +6 -5
  14. data/lib/leptris/xml/processing_instruction.rb +2 -0
  15. data/lib/leptris/xml/pull.rb +123 -0
  16. data/lib/leptris/xml/searchable.rb +5 -2
  17. data/lib/leptris/xml/serialization.rb +20 -2
  18. data/lib/leptris/xml/text.rb +1 -0
  19. data/lib/leptris/xml/xpath.rb +80 -0
  20. data/lib/leptris/xml.rb +6 -2
  21. data/lib/libleptris.so +0 -0
  22. metadata +5 -37
  23. data/TODO.impl/01-architecture.md +0 -217
  24. data/TODO.impl/02-ffi-declarations.md +0 -236
  25. data/TODO.impl/03-document-node-element-nodeset.md +0 -382
  26. data/TODO.impl/04-sax-parser.md +0 -203
  27. data/TODO.impl/05-serialize-c14n-memory-specs-css.md +0 -276
  28. data/docs/ARCHITECTURE.adoc +0 -559
  29. data/docs/BUILD.md +0 -395
  30. data/docs/ERROR_MESSAGES.md +0 -458
  31. data/docs/FFI_ARCHITECTURE.md +0 -439
  32. data/docs/FUTURE_VISION.md +0 -303
  33. data/docs/GITHUB_ACTIONS.md +0 -293
  34. data/docs/OPTIMIZATIONS_IMPLEMENTED.adoc +0 -459
  35. data/docs/PERFORMANCE.adoc +0 -668
  36. data/docs/PERFORMANCE.md +0 -448
  37. data/docs/RELEASE_NOTES_v1.0.0.md +0 -515
  38. data/docs/XPATH_SPEC_COMPLIANCE.md +0 -298
  39. data/docs/completion/leptris.bash +0 -86
  40. data/docs/completion/leptris.zsh +0 -74
  41. data/docs/man/leptris-format.1 +0 -227
  42. data/docs/man/leptris-parse.1 +0 -178
  43. data/docs/man/leptris-xpath.1 +0 -312
  44. data/docs/man/leptris.1 +0 -160
  45. data/docs/v0.9.0_PERFORMANCE_IMPROVEMENTS.md +0 -217
  46. data/docs/v0.9.0_RELEASE_SUMMARY.md +0 -281
  47. data/docs/v1.0.0_CONTINUATION_PLAN.md +0 -172
  48. data/docs/v1.0.0_CONTINUATION_PROMPT.md +0 -382
  49. data/docs/v1.0.0_SESSION_6_CONTINUATION.md +0 -434
  50. data/docs/v1.0.0_SESSION_6_PROMPT.md +0 -231
  51. data/docs/v1.0.0_STATUS_TRACKER.md +0 -224
  52. data/docs/v1.1.0_CONTINUATION_PLAN.md +0 -299
  53. data/docs/v1.1.0_FINAL_CONTINUATION_PLAN.md +0 -201
  54. data/docs/v1.1.0_SESSION_3_PROMPT.md +0 -223
  55. data/docs/v1.1.0_STATUS_TRACKER.md +0 -355
  56. data/docs/xml-performance.adoc +0 -115
  57. data/docs/xpath-performance.adoc +0 -379
@@ -24,6 +24,9 @@ module Leptris
24
24
  typedef :pointer, :leptris_doctype
25
25
  typedef :pointer, :leptris_xpath_result
26
26
  typedef :pointer, :leptris_xpath_ns_set
27
+ typedef :pointer, :leptris_xpath_compiled
28
+ typedef :pointer, :leptris_pull_parser
29
+ typedef :pointer, :leptris_iterparse
27
30
  typedef :pointer, :leptris_xpath_var_set
28
31
  typedef :pointer, :leptris_sax_parser
29
32
  typedef :int, :leptris_status
@@ -73,6 +76,20 @@ module Leptris
73
76
  [:leptris_document, :leptris_element], :leptris_status
74
77
  attach_function :leptris_document_root,
75
78
  [:leptris_document], :leptris_element
79
+ # Document-level PIs (v1.6.0): not tree nodes — enumerate via
80
+ # these accessors only.
81
+ attach_function :leptris_document_pi_count,
82
+ [:leptris_document], :size_t
83
+ attach_function :leptris_document_pi_target,
84
+ [:leptris_document, :size_t], :string
85
+ attach_function :leptris_document_pi_data,
86
+ [:leptris_document, :size_t], :string
87
+ attach_function :leptris_document_add_pi,
88
+ [:leptris_document, :string, :string], :pointer
89
+ # Options-struct parse (v1.6.0): supersedes the flags variants;
90
+ # the flags path remains bound for compatibility.
91
+ attach_function :leptris_parse_string_ex,
92
+ [:string, :size_t, :pointer, :pointer], :leptris_document
76
93
  attach_function :leptris_document_encoding,
77
94
  [:leptris_document], :string
78
95
  attach_function :leptris_document_finalize_strings,
@@ -324,8 +341,6 @@ module Leptris
324
341
  [:leptris_xpath_result], :int
325
342
  attach_function :leptris_xpath_result_count,
326
343
  [:leptris_xpath_result], :size_t
327
- attach_function :leptris_xpath_result_get,
328
- [:leptris_xpath_result, :size_t], :leptris_element
329
344
  # Mixed nodesets (v1.1.0): unlike result_get (elements only),
330
345
  # get_node returns the node whatever its kind; node_kind reports
331
346
  # which (element / synthetic attribute / text / other).
@@ -342,10 +357,8 @@ module Leptris
342
357
  # a heap string the library frees.
343
358
  attach_function :leptris_xpath_register_function,
344
359
  [:leptris_document, :string, :pointer, :pointer], :leptris_status
345
- attach_function :leptris_xpath_result_get_nodes,
346
- [:leptris_xpath_result, :pointer, :size_t], :size_t
347
- # v1.3.0: copies ALL node kinds (the legacy accessor drops
348
- # non-element entries) and can also report per-entry kinds.
360
+ # v1.3.0: copies ALL node kinds in one call, optionally
361
+ # reporting per-entry kinds.
349
362
  attach_function :leptris_xpath_result_get_nodes_ex,
350
363
  [:leptris_xpath_result, :pointer, :pointer, :size_t], :size_t
351
364
  attach_function :leptris_xpath_result_boolean,
@@ -376,15 +389,27 @@ module Leptris
376
389
  [], :leptris_xpath_ns_set
377
390
  attach_function :leptris_xpath_ns_set_free,
378
391
  [:leptris_xpath_ns_set], :void
379
- attach_function :leptris_xpath_ns_set_add,
380
- [:leptris_xpath_ns_set, :string, :string], :leptris_status
381
392
  # One-call constructor: flat array of 2*pair_count alternating
382
- # prefix/URI strings (v1.3.0).
393
+ # prefix/URI strings.
383
394
  attach_function :leptris_xpath_ns_set_new_from_pairs,
384
395
  [:pointer, :size_t], :leptris_xpath_ns_set
385
396
  attach_function :leptris_xpath_eval_ns,
386
397
  [:leptris_document, :leptris_element, :string, :leptris_xpath_ns_set],
387
398
  :leptris_xpath_result
399
+ # Compiled expressions (v1.6.0): parse once, evaluate many.
400
+ attach_function :leptris_xpath_compile,
401
+ [:string], :leptris_xpath_compiled
402
+ attach_function :leptris_xpath_compiled_eval,
403
+ [:leptris_xpath_compiled, :leptris_document, :leptris_element],
404
+ :leptris_xpath_result
405
+ attach_function :leptris_xpath_compiled_eval_ns,
406
+ [:leptris_xpath_compiled, :leptris_document, :leptris_element,
407
+ :leptris_xpath_ns_set], :leptris_xpath_result
408
+ attach_function :leptris_xpath_compiled_eval_vars,
409
+ [:leptris_xpath_compiled, :leptris_document, :leptris_element,
410
+ :leptris_xpath_var_set], :leptris_xpath_result
411
+ attach_function :leptris_xpath_compiled_free,
412
+ [:leptris_xpath_compiled], :void
388
413
 
389
414
  attach_function :leptris_sax_parse,
390
415
  [:string, :size_t, :pointer, :pointer], :int
@@ -397,12 +422,35 @@ module Leptris
397
422
  attach_function :leptris_sax_parser_set_streaming,
398
423
  [:leptris_sax_parser, :int], :int
399
424
 
425
+ # Pull parsing (v1.6.0): StAX-style cursor over a document.
426
+ attach_function :leptris_pull_new,
427
+ [:string, :size_t], :leptris_pull_parser
428
+ attach_function :leptris_pull_new_file,
429
+ [:string], :leptris_pull_parser
430
+ attach_function :leptris_pull_next,
431
+ [:leptris_pull_parser], :pointer
432
+ attach_function :leptris_pull_attr_count,
433
+ [:leptris_pull_parser], :size_t
434
+ attach_function :leptris_pull_attr_name,
435
+ [:leptris_pull_parser, :size_t], :string
436
+ attach_function :leptris_pull_attr_value,
437
+ [:leptris_pull_parser, :size_t], :string
438
+ attach_function :leptris_pull_free,
439
+ [:leptris_pull_parser], :void
440
+
441
+ # Iterparse (v1.6.0): yields each completed top-level child
442
+ # element; the previous subtree is released on each next call.
443
+ attach_function :leptris_iterparse_new,
444
+ [:string, :size_t], :leptris_iterparse
445
+ attach_function :leptris_iterparse_new_file,
446
+ [:string], :leptris_iterparse
447
+ attach_function :leptris_iterparse_next,
448
+ [:leptris_iterparse], :leptris_element
449
+ attach_function :leptris_iterparse_free,
450
+ [:leptris_iterparse], :void
451
+
400
452
  attach_function :leptris_document_serialize,
401
453
  [:leptris_document, :pointer], :pointer
402
- # Legacy simple serializer, declared retroactively in v1.1.0.
403
- # Prefer leptris_document_serialize; bound for API completeness.
404
- attach_function :leptris_serialize_document,
405
- [:leptris_document], :pointer
406
454
  attach_function :leptris_document_get_dtd,
407
455
  [:leptris_document], :pointer
408
456
  attach_function :leptris_element_serialize,
@@ -491,6 +539,15 @@ module Leptris
491
539
  TRAVERSE_PRE_ORDER = 0
492
540
  TRAVERSE_POST_ORDER = 1
493
541
 
542
+ PULL_START_ELEMENT = 0
543
+ PULL_END_ELEMENT = 1
544
+ PULL_TEXT = 2
545
+ PULL_COMMENT = 3
546
+ PULL_CDATA = 4
547
+ PULL_PI = 5
548
+ PULL_END_DOCUMENT = 6
549
+ PULL_ERROR = 7
550
+
494
551
  LEPTRIS_PARSE_DEFAULT = 0
495
552
  LEPTRIS_PARSE_DROP_WS_TEXT = 1
496
553
 
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ # Incremental tree iteration (libleptris v1.6.0): yields each
6
+ # completed TOP-LEVEL child element of the document root as it
7
+ # parses. The previous element's whole subtree is released when the
8
+ # next is produced — memory stays bounded by the largest subtree,
9
+ # not the document.
10
+ #
11
+ # Leptris::XML::Iterparse.parse(huge_xml) do |element|
12
+ # process(element) # subtree valid until the next iteration
13
+ # end
14
+ #
15
+ # v1 limitation (upstream): element names are the QName as written;
16
+ # namespace prefixes are not re-resolved — use the DOM path when
17
+ # namespace URIs matter. The yielded elements have no parent
18
+ # Document; #document returns nil and they must not outlive the
19
+ # iteration.
20
+ class Leptris::XML::Iterparse
21
+ def self.parse(xml_or_io, &block)
22
+ xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
23
+ iterator = new(Leptris::XML::FFI.leptris_iterparse_new(xml, xml.bytesize))
24
+ iterator.run(&block)
25
+ end
26
+
27
+ def self.parse_file(path, &block)
28
+ iterator = new(Leptris::XML::FFI.leptris_iterparse_new_file(path))
29
+ iterator.run(&block)
30
+ end
31
+
32
+ def initialize(handle)
33
+ raise Leptris::XML::ParseError, "leptris_iterparse_new failed" if handle.null?
34
+ @handle = handle
35
+ end
36
+
37
+ def run
38
+ return enum_for(:run) unless block_given?
39
+ loop do
40
+ ptr = Leptris::XML::FFI.leptris_iterparse_next(@handle)
41
+ break if ptr.null?
42
+ # document: nil — the subtree belongs to the iterator's pool,
43
+ # not a long-lived Document, so wrapper caching is skipped and
44
+ # #document is nil on the yielded elements.
45
+ yield Leptris::XML::Node.wrap(ptr, nil)
46
+ end
47
+ self
48
+ ensure
49
+ free
50
+ end
51
+
52
+ def free
53
+ return if @handle.nil?
54
+ Leptris::XML::FFI.leptris_iterparse_free(@handle)
55
+ @handle = nil
56
+ end
57
+ end
@@ -3,10 +3,13 @@
3
3
  class Leptris::XML::Node
4
4
  attr_reader :c_ptr, :document
5
5
 
6
- def initialize(c_ptr, document, parent: nil)
6
+ def initialize(c_ptr, document, parent: nil, node_type: nil)
7
7
  @c_ptr = c_ptr
8
8
  @document = document
9
9
  @parent = parent
10
+ # wrap() already calls leptris_node_get_type for dispatch; reusing
11
+ # the result makes every predicate and #type call FFI-free.
12
+ @node_type = node_type
10
13
  end
11
14
 
12
15
  def self.wrap(c_ptr, document, parent: nil)
@@ -18,20 +21,21 @@ class Leptris::XML::Node
18
21
  return cached
19
22
  end
20
23
 
24
+ node_type = Leptris::XML::FFI.leptris_node_get_type(c_ptr)
21
25
  node =
22
- case Leptris::XML::FFI.leptris_node_get_type(c_ptr)
26
+ case node_type
23
27
  when Leptris::XML::FFI::NODE_ELEMENT
24
- Leptris::XML::Element.new(c_ptr, document, parent: parent)
28
+ Leptris::XML::Element.new(c_ptr, document, parent: parent, node_type: node_type)
25
29
  when Leptris::XML::FFI::NODE_TEXT
26
- Leptris::XML::Text.new(c_ptr, document, parent: parent)
30
+ Leptris::XML::Text.new(c_ptr, document, parent: parent, node_type: node_type)
27
31
  when Leptris::XML::FFI::NODE_COMMENT
28
- Leptris::XML::Comment.new(c_ptr, document, parent: parent)
32
+ Leptris::XML::Comment.new(c_ptr, document, parent: parent, node_type: node_type)
29
33
  when Leptris::XML::FFI::NODE_CDATA
30
- Leptris::XML::CDATA.new(c_ptr, document, parent: parent)
34
+ Leptris::XML::CDATA.new(c_ptr, document, parent: parent, node_type: node_type)
31
35
  when Leptris::XML::FFI::NODE_PI
32
- Leptris::XML::ProcessingInstruction.new(c_ptr, document, parent: parent)
36
+ Leptris::XML::ProcessingInstruction.new(c_ptr, document, parent: parent, node_type: node_type)
33
37
  else
34
- new(c_ptr, document, parent: parent)
38
+ new(c_ptr, document, parent: parent, node_type: node_type)
35
39
  end
36
40
 
37
41
  document.wrapper_cache[c_ptr.address] = node if document
@@ -58,7 +62,7 @@ class Leptris::XML::Node
58
62
  end
59
63
 
60
64
  def type
61
- Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
65
+ @node_type ||= Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
62
66
  end
63
67
  alias_method :node_type, :type
64
68
 
@@ -78,6 +82,14 @@ class Leptris::XML::Node
78
82
  Leptris::XML::Node.wrap(ptr, @document)
79
83
  end
80
84
 
85
+ # Raises ReadOnlyError when the owning document was marked readonly.
86
+ def ensure_writable!
87
+ if @document&.readonly?
88
+ raise Leptris::XML::ReadOnlyError,
89
+ "document is readonly — mutation attempted on #{inspect}"
90
+ end
91
+ end
92
+
81
93
  def line
82
94
  Leptris::XML::FFI.leptris_node_line(@c_ptr)
83
95
  end
@@ -95,13 +107,18 @@ class Leptris::XML::Node
95
107
  end
96
108
 
97
109
  def children
110
+ # Immutable in readonly mode: the walk (first_child + one
111
+ # next_sibling per child) plus wrapper construction is paid once.
112
+ return @children if readonly_cached?(:@children)
98
113
  nodes = []
99
114
  ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
100
115
  until ptr.nil? || ptr.null?
101
116
  nodes << Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
102
117
  ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
103
118
  end
104
- Leptris::XML::NodeSet.new(@document, nodes)
119
+ result = Leptris::XML::NodeSet.new(@document, nodes)
120
+ @children = result if @document&.readonly?
121
+ result
105
122
  end
106
123
 
107
124
  def next_sibling
@@ -150,6 +167,7 @@ class Leptris::XML::Node
150
167
  end
151
168
 
152
169
  def unlink
170
+ ensure_writable!
153
171
  Leptris::XML::FFI.check_status(
154
172
  Leptris::XML::FFI.leptris_node_unlink(@c_ptr))
155
173
  @parent = nil
@@ -212,6 +230,10 @@ class Leptris::XML::Node
212
230
 
213
231
  protected
214
232
 
233
+ def readonly_cached?(ivar)
234
+ @document&.readonly? && instance_variable_defined?(ivar)
235
+ end
236
+
215
237
  def as_element_or_self
216
238
  is_a?(Leptris::XML::Element) ? self : nil
217
239
  end
@@ -63,15 +63,16 @@ class Leptris::XML::NodeSet
63
63
  def each
64
64
  return enum_for(:each) unless block_given?
65
65
  if @result_ptr
66
- # Batch-fetch all node pointers in one FFI call (leptris_xpath_result_get_nodes,
67
- # libleptris v0.11.4) and wrap each. Saves N-1 FFI calls vs the per-index
68
- # leptris_xpath_result_get loop. Wrappers are still cached per-Document via
69
- # Node.wrap, so a re-iteration of the same NodeSet hits the cache.
66
+ # Batch-fetch all node pointers in one FFI call
67
+ # (leptris_xpath_result_get_nodes_ex copies every node kind, not
68
+ # just elements) and wrap each. Wrappers are cached per-Document
69
+ # via Node.wrap, so re-iteration of the same NodeSet hits it.
70
70
  n = length
71
71
  if n > 0
72
72
  buf = ::FFI::MemoryPointer.new(:pointer, n)
73
73
  begin
74
- copied = Leptris::XML::FFI.leptris_xpath_result_get_nodes(@result_ptr, buf, n)
74
+ copied = Leptris::XML::FFI.leptris_xpath_result_get_nodes_ex(
75
+ @result_ptr, buf, nil, n)
75
76
  copied.times do |i|
76
77
  ptr = buf.get_pointer(i * ::FFI.type_size(:pointer))
77
78
  next if ptr.null?
@@ -11,12 +11,14 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
11
11
  end
12
12
 
13
13
  def target=(new_target)
14
+ ensure_writable!
14
15
  Leptris::XML::FFI.check_status(
15
16
  Leptris::XML::FFI.leptris_pi_node_set_target(@c_ptr, new_target.to_s))
16
17
  new_target
17
18
  end
18
19
 
19
20
  def data=(new_data)
21
+ ensure_writable!
20
22
  Leptris::XML::FFI.check_status(
21
23
  Leptris::XML::FFI.leptris_pi_node_set_data(@c_ptr, new_data.to_s))
22
24
  new_data
@@ -0,0 +1,123 @@
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
+ Event = Struct.new(:type, :name, :text, :attrs, keyword_init: true)
18
+
19
+ TYPES = {
20
+ Leptris::XML::FFI::PULL_START_ELEMENT => :start_element,
21
+ Leptris::XML::FFI::PULL_END_ELEMENT => :end_element,
22
+ Leptris::XML::FFI::PULL_TEXT => :text,
23
+ Leptris::XML::FFI::PULL_COMMENT => :comment,
24
+ Leptris::XML::FFI::PULL_CDATA => :cdata,
25
+ Leptris::XML::FFI::PULL_PI => :pi,
26
+ Leptris::XML::FFI::PULL_END_DOCUMENT => :end_document,
27
+ Leptris::XML::FFI::PULL_ERROR => :error,
28
+ }.freeze
29
+
30
+ class Parser
31
+ def self.parse(xml_or_io)
32
+ xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
33
+ new(Leptris::XML::FFI.leptris_pull_new(xml, xml.bytesize))
34
+ end
35
+
36
+ def self.parse_file(path)
37
+ new(Leptris::XML::FFI.leptris_pull_new_file(path))
38
+ end
39
+
40
+ def initialize(handle)
41
+ raise Leptris::XML::ParseError, "leptris_pull_new failed" if handle.null?
42
+ @handle = handle
43
+ end
44
+
45
+ # Releases the C parser. Safe to call twice.
46
+ def free
47
+ return if @handle.nil?
48
+ Leptris::XML::FFI.leptris_pull_free(@handle)
49
+ @handle = nil
50
+ end
51
+
52
+ def each
53
+ return enum_for(:each) unless block_given?
54
+ loop do
55
+ event = next_event
56
+ break if event.nil?
57
+ yield event
58
+ break if event.type == :end_document || event.type == :error
59
+ end
60
+ self
61
+ end
62
+
63
+ # Advances the cursor and returns the next Event, or nil after the
64
+ # end of the document. Attribute values are captured during
65
+ # start_element because the C accessors are valid only for the
66
+ # most recently returned event.
67
+ def next_event
68
+ raw = Leptris::XML::FFI.leptris_pull_next(@handle)
69
+ return nil if raw.null?
70
+ type_code = raw.get_int(0)
71
+ type = TYPES[type_code]
72
+ name = read_string_at(raw, 8)
73
+ text_ptr = raw.get_pointer(16)
74
+ text = text_ptr.null? ? nil : text_ptr.read_string
75
+ attrs = type == :start_element ? capture_attrs : nil
76
+ Event.new(type: type, name: name, text: text, attrs: attrs)
77
+ end
78
+
79
+ private
80
+
81
+ # struct LeptrisPullEvent { int type; const char* name; const
82
+ # char* text; size_t text_len; } — pointer-sized fields follow the
83
+ # 4-byte enum on 64-bit ABIs (name at 8, text at 16).
84
+ def read_string_at(raw, offset)
85
+ ptr = raw.get_pointer(offset)
86
+ ptr.null? ? nil : ptr.read_string
87
+ end
88
+
89
+ def capture_attrs
90
+ count = Leptris::XML::FFI.leptris_pull_attr_count(@handle)
91
+ count.times.each_with_object({}) do |i, hash|
92
+ hash[Leptris::XML::FFI.leptris_pull_attr_name(@handle, i)] =
93
+ Leptris::XML::FFI.leptris_pull_attr_value(@handle, i)
94
+ end
95
+ end
96
+ end
97
+
98
+ def self.parse(xml_or_io, &block)
99
+ parser = Parser.parse(xml_or_io)
100
+ if block
101
+ begin
102
+ parser.each(&block)
103
+ ensure
104
+ parser.free
105
+ end
106
+ else
107
+ parser
108
+ end
109
+ end
110
+
111
+ def self.parse_file(path, &block)
112
+ parser = Parser.parse_file(path)
113
+ if block
114
+ begin
115
+ parser.each(&block)
116
+ ensure
117
+ parser.free
118
+ end
119
+ else
120
+ parser
121
+ end
122
+ end
123
+ end
@@ -20,7 +20,7 @@ module Leptris::XML::Searchable
20
20
  Leptris::XML::FFI.leptris_status_string(Leptris::XML::FFI::LEPTRIS_ERROR_XPATH)
21
21
  end
22
22
 
23
- wrap_xpath_result(result_ptr)
23
+ Leptris::XML::Searchable.wrap_xpath_result(document, result_ptr)
24
24
  end
25
25
 
26
26
  def at_xpath(*paths)
@@ -88,7 +88,10 @@ module Leptris::XML::Searchable
88
88
  %r{\A(\./|/|\.\.|\.)}.match?(str)
89
89
  end
90
90
 
91
- def wrap_xpath_result(result_ptr)
91
+ # Shared by Searchable#xpath and Leptris::XML::XPath (compiled
92
+ # expressions): wraps a raw XPathResult pointer into the
93
+ # Ruby-typed result and frees the C handle.
94
+ def self.wrap_xpath_result(document, result_ptr)
92
95
  type = Leptris::XML::FFI.leptris_xpath_result_type(result_ptr)
93
96
  case type
94
97
  when Leptris::XML::FFI::XPATH_NODESET
@@ -6,11 +6,29 @@
6
6
  # struct, the encoding anchor, the C call, and the read-and-free all
7
7
  # live here.
8
8
  module Leptris::XML::Serialization
9
+ # The default options struct (indent 0, declaration on, no encoding
10
+ # override) is constant: build it once and reuse its pointer on the
11
+ # fast path instead of allocating a fresh struct per call.
12
+ DEFAULT_OPTIONS, _default_anchor = begin
13
+ opts = Leptris::XML::FFI::SerializeOptions.new
14
+ opts[:indent] = 0
15
+ opts[:xml_declaration] = 1
16
+ opts[:encoding] = nil
17
+ [opts, nil]
18
+ end
19
+ private_constant :DEFAULT_OPTIONS
20
+
9
21
  # +ffi_function+ is a bound FFI function taking (c_ptr, opts_ptr):
10
22
  # leptris_document_serialize or leptris_element_serialize.
11
23
  def self.to_xml(ffi_function, c_ptr, indent: 0, no_decl: false, encoding: nil)
12
- opts, _encoding_anchor = build_options(
13
- indent: indent, no_decl: no_decl, encoding: encoding)
24
+ opts =
25
+ if indent.to_i.zero? && !no_decl && encoding.nil?
26
+ DEFAULT_OPTIONS
27
+ else
28
+ opts, _encoding_anchor = build_options(
29
+ indent: indent, no_decl: no_decl, encoding: encoding)
30
+ opts
31
+ end
14
32
  str_ptr = ffi_function.call(c_ptr, opts.pointer)
15
33
  Leptris::XML::FFI.read_owned_string(str_ptr)
16
34
  end
@@ -8,6 +8,7 @@ class Leptris::XML::Text < Leptris::XML::Node
8
8
  end
9
9
 
10
10
  def content=(new_content)
11
+ ensure_writable!
11
12
  Leptris::XML::FFI.check_status(
12
13
  Leptris::XML::FFI.leptris_text_node_set_content(@c_ptr, new_content.to_s))
13
14
  new_content
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ # Compiled XPath expression: parse once, evaluate many times against
6
+ # any document (libleptris v1.6.0). The C handle is released by GC.
7
+ #
8
+ # expr = Leptris::XML::XPath.compile("//item[@qty > 3]")
9
+ # expr.eval(doc_a).length
10
+ # expr.eval(doc_b, "p" => "urn:p").length
11
+ #
12
+ class Leptris::XML::XPath
13
+ class CompiledHandle < ::FFI::AutoPointer
14
+ def self.release(ptr)
15
+ Leptris::XML::FFI.leptris_xpath_compiled_free(ptr)
16
+ end
17
+ end
18
+
19
+ attr_reader :expression
20
+
21
+ def self.compile(expression)
22
+ raw = Leptris::XML::FFI.leptris_xpath_compile(expression.to_s)
23
+ if raw.null?
24
+ raise Leptris::XML::XPathError,
25
+ "invalid expression: #{Leptris::XML::FFI.leptris_last_error}"
26
+ end
27
+ new(expression.to_s, CompiledHandle.new(raw))
28
+ end
29
+
30
+ def initialize(expression, handle)
31
+ @expression = expression
32
+ @handle = handle
33
+ end
34
+
35
+ # Evaluates against +doc_or_element+. An optional trailing hash of
36
+ # namespace bindings ("prefix" => uri) routes the evaluation through
37
+ # the namespace-bound path, matching Searchable#xpath semantics.
38
+ def eval(doc_or_element, ns = nil)
39
+ context = resolve_context(doc_or_element)
40
+ document = context.document
41
+ result_ptr =
42
+ if ns && !ns.empty?
43
+ flat = ns.flat_map { |prefix, uri| [prefix.to_s, uri.to_s] }
44
+ buffer, _anchors = Leptris::XML::CStringArray.to_c(flat)
45
+ set = Leptris::XML::FFI.leptris_xpath_ns_set_new_from_pairs(
46
+ buffer, flat.length / 2)
47
+ raise Leptris::XML::Error, "leptris_xpath_ns_set_new_from_pairs failed" if set.null?
48
+ begin
49
+ Leptris::XML::FFI.leptris_xpath_compiled_eval_ns(
50
+ @handle, document.c_ptr, context_ptr(context), set)
51
+ ensure
52
+ Leptris::XML::FFI.leptris_xpath_ns_set_free(set)
53
+ end
54
+ else
55
+ Leptris::XML::FFI.leptris_xpath_compiled_eval(
56
+ @handle, document.c_ptr, context_ptr(context))
57
+ end
58
+ if result_ptr.null?
59
+ raise Leptris::XML::XPathError,
60
+ Leptris::XML::FFI.leptris_last_error.to_s
61
+ end
62
+ Leptris::XML::Searchable.wrap_xpath_result(document, result_ptr)
63
+ end
64
+
65
+ private
66
+
67
+ def resolve_context(doc_or_element)
68
+ case doc_or_element
69
+ when Leptris::XML::Document then doc_or_element
70
+ when Leptris::XML::Element then doc_or_element
71
+ else
72
+ raise ArgumentError,
73
+ "expected a Leptris::XML::Document or Element, got #{doc_or_element.class}"
74
+ end
75
+ end
76
+
77
+ def context_ptr(context)
78
+ context.is_a?(Leptris::XML::Document) ? nil : context.c_ptr
79
+ end
80
+ end
data/lib/leptris/xml.rb CHANGED
@@ -21,11 +21,14 @@ module Leptris
21
21
  autoload :CStringArray, "leptris/xml/c_string_array"
22
22
  autoload :Serialization, "leptris/xml/serialization"
23
23
  autoload :SAX, "leptris/xml/sax"
24
+ autoload :XPath, "leptris/xml/xpath"
25
+ autoload :Pull, "leptris/xml/pull"
26
+ autoload :Iterparse, "leptris/xml/iterparse"
24
27
 
25
28
  # Nokogiri-style top-level entry points (Nokogiri::XML(...) /
26
29
  # Nokogiri::XML.parse). Delegates to Document.parse.
27
- def self.parse(xml_or_io, options: nil)
28
- Document.parse(xml_or_io, options: options)
30
+ def self.parse(xml_or_io, options: nil, readonly: false)
31
+ Document.parse(xml_or_io, options: options, readonly: readonly)
29
32
  end
30
33
 
31
34
  def self.parse_file(path)
@@ -34,6 +37,7 @@ module Leptris
34
37
 
35
38
  class Error < StandardError; end
36
39
  class ParseError < Error; end
40
+ class ReadOnlyError < Error; end
37
41
  class XPathError < Error; end
38
42
  class UseAfterFreeError < Error; end
39
43
  end
data/lib/libleptris.so CHANGED
Binary file