leptris 1.0.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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +8 -0
  4. data/CHANGELOG.md +529 -0
  5. data/CLAUDE.md +104 -0
  6. data/LICENSE.md +33 -0
  7. data/README.adoc +405 -0
  8. data/Rakefile +7 -0
  9. data/TODO.impl/01-architecture.md +217 -0
  10. data/TODO.impl/02-ffi-declarations.md +236 -0
  11. data/TODO.impl/03-document-node-element-nodeset.md +382 -0
  12. data/TODO.impl/04-sax-parser.md +203 -0
  13. data/TODO.impl/05-serialize-c14n-memory-specs-css.md +276 -0
  14. data/benchmark/README.md +168 -0
  15. data/benchmark/leptris_vs_nokogiri.rb +105 -0
  16. data/docs/ARCHITECTURE.adoc +559 -0
  17. data/docs/BUILD.md +395 -0
  18. data/docs/ERROR_MESSAGES.md +458 -0
  19. data/docs/FFI_ARCHITECTURE.md +439 -0
  20. data/docs/FUTURE_VISION.md +303 -0
  21. data/docs/GITHUB_ACTIONS.md +293 -0
  22. data/docs/OPTIMIZATIONS_IMPLEMENTED.adoc +459 -0
  23. data/docs/PERFORMANCE.adoc +668 -0
  24. data/docs/PERFORMANCE.md +448 -0
  25. data/docs/RELEASE_NOTES_v1.0.0.md +515 -0
  26. data/docs/XPATH_SPEC_COMPLIANCE.md +298 -0
  27. data/docs/completion/leptris.bash +86 -0
  28. data/docs/completion/leptris.zsh +74 -0
  29. data/docs/man/leptris-format.1 +227 -0
  30. data/docs/man/leptris-parse.1 +178 -0
  31. data/docs/man/leptris-xpath.1 +312 -0
  32. data/docs/man/leptris.1 +160 -0
  33. data/docs/v0.9.0_PERFORMANCE_IMPROVEMENTS.md +217 -0
  34. data/docs/v0.9.0_RELEASE_SUMMARY.md +281 -0
  35. data/docs/v1.0.0_CONTINUATION_PLAN.md +172 -0
  36. data/docs/v1.0.0_CONTINUATION_PROMPT.md +382 -0
  37. data/docs/v1.0.0_SESSION_6_CONTINUATION.md +434 -0
  38. data/docs/v1.0.0_SESSION_6_PROMPT.md +231 -0
  39. data/docs/v1.0.0_STATUS_TRACKER.md +224 -0
  40. data/docs/v1.1.0_CONTINUATION_PLAN.md +299 -0
  41. data/docs/v1.1.0_FINAL_CONTINUATION_PLAN.md +201 -0
  42. data/docs/v1.1.0_SESSION_3_PROMPT.md +223 -0
  43. data/docs/v1.1.0_STATUS_TRACKER.md +355 -0
  44. data/docs/xml-performance.adoc +115 -0
  45. data/docs/xpath-performance.adoc +379 -0
  46. data/leptris.gemspec +42 -0
  47. data/lib/leptris/version.rb +5 -0
  48. data/lib/leptris/xml/attr.rb +43 -0
  49. data/lib/leptris/xml/c14n.rb +23 -0
  50. data/lib/leptris/xml/cdata.rb +16 -0
  51. data/lib/leptris/xml/comment.rb +16 -0
  52. data/lib/leptris/xml/css_to_xpath.rb +177 -0
  53. data/lib/leptris/xml/doc_type.rb +54 -0
  54. data/lib/leptris/xml/document.rb +202 -0
  55. data/lib/leptris/xml/document_fragment.rb +42 -0
  56. data/lib/leptris/xml/element.rb +278 -0
  57. data/lib/leptris/xml/ffi.rb +420 -0
  58. data/lib/leptris/xml/namespace.rb +43 -0
  59. data/lib/leptris/xml/node.rb +221 -0
  60. data/lib/leptris/xml/node_set.rb +143 -0
  61. data/lib/leptris/xml/parse_options.rb +19 -0
  62. data/lib/leptris/xml/processing_instruction.rb +26 -0
  63. data/lib/leptris/xml/sax/document.rb +45 -0
  64. data/lib/leptris/xml/sax/parser.rb +148 -0
  65. data/lib/leptris/xml/sax.rb +12 -0
  66. data/lib/leptris/xml/searchable.rb +93 -0
  67. data/lib/leptris/xml/text.rb +16 -0
  68. data/lib/leptris/xml.rb +39 -0
  69. data/lib/leptris.rb +7 -0
  70. metadata +157 -0
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ class Leptris::XML::NodeSet
6
+ include Enumerable
7
+ include Leptris::XML::Searchable
8
+
9
+ attr_reader :document
10
+
11
+ # Two construction modes:
12
+ # - eager: pass an Array of Nodes (e.g. Element#children builds one)
13
+ # - lazy: pass an FFI::Pointer to a LeptrisXPathResult that this NodeSet
14
+ # will keep alive and free on GC. Each [i] / each call goes
15
+ # through leptris_xpath_result_get instead of pre-materializing.
16
+ def initialize(document, source = nil)
17
+ @document = document
18
+ case source
19
+ when ::FFI::Pointer
20
+ if source.null?
21
+ @result_ptr = nil
22
+ @array = []
23
+ else
24
+ # Wrap in AutoPointer for automatic GC-time cleanup. NodeSet has no
25
+ # explicit #free method (Nokogiri doesn't either), so the AutoPointer
26
+ # double-free risk that Document#free hit doesn't apply here.
27
+ @result_ptr = ::FFI::AutoPointer.new(source, Leptris::XML::FFI.method(:leptris_xpath_result_free))
28
+ @array = nil
29
+ end
30
+ when nil
31
+ @result_ptr = nil
32
+ @array = []
33
+ else
34
+ @result_ptr = nil
35
+ @array = source.to_a
36
+ end
37
+ end
38
+
39
+ def self.from_result(document, result_ptr)
40
+ new(document, result_ptr)
41
+ end
42
+
43
+ def length
44
+ @result_ptr ? Leptris::XML::FFI.leptris_xpath_result_count(@result_ptr) : @array.length
45
+ end
46
+ alias_method :size, :length
47
+
48
+ def empty?
49
+ length == 0
50
+ end
51
+
52
+ def [](idx)
53
+ if @result_ptr
54
+ return nil if idx < 0 || idx >= length
55
+ ptr = Leptris::XML::FFI.leptris_xpath_result_get(@result_ptr, idx)
56
+ return nil if ptr.null?
57
+ Leptris::XML::Node.wrap(ptr, @document)
58
+ else
59
+ @array[idx]
60
+ end
61
+ end
62
+
63
+ def each
64
+ return enum_for(:each) unless block_given?
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.
70
+ n = length
71
+ if n > 0
72
+ buf = ::FFI::MemoryPointer.new(:pointer, n)
73
+ begin
74
+ copied = Leptris::XML::FFI.leptris_xpath_result_get_nodes(@result_ptr, buf, n)
75
+ copied.times do |i|
76
+ ptr = buf.get_pointer(i * ::FFI.type_size(:pointer))
77
+ next if ptr.null?
78
+ yield Leptris::XML::Node.wrap(ptr, @document)
79
+ end
80
+ ensure
81
+ buf.free
82
+ end
83
+ end
84
+ else
85
+ @array.each { |n| yield n }
86
+ end
87
+ self
88
+ end
89
+
90
+ def to_a
91
+ return @array if @array
92
+ return [] if @result_ptr.nil?
93
+ out = []
94
+ each { |n| out << n }
95
+ @array = out # memoize so subsequent to_a / each avoids re-fetch
96
+ out
97
+ end
98
+ alias_method :to_ary, :to_a
99
+
100
+ def first(n = nil)
101
+ return self[0] if n.nil?
102
+ n.times.map { |i| self[i] }.take_while { |x| !x.nil? }
103
+ end
104
+
105
+ def last
106
+ if @result_ptr
107
+ self[length - 1]
108
+ else
109
+ @array.last
110
+ end
111
+ end
112
+
113
+ def inner_text
114
+ map(&:content).join
115
+ end
116
+ alias_method :text, :inner_text
117
+
118
+ def xpath(*paths)
119
+ handler, _ns, _vars = parse_search_args(paths)
120
+ raise ArgumentError, "custom XPath handlers not supported" if handler
121
+ expr = paths.join(" | ")
122
+ accumulated = Leptris::XML::NodeSet.new(@document)
123
+ each do |node|
124
+ next unless node.is_a?(Leptris::XML::Element)
125
+ result_ptr = Leptris::XML::FFI.leptris_xpath_eval(
126
+ @document.c_ptr, node.c_ptr, expr)
127
+ next if result_ptr.null?
128
+ sub = Leptris::XML::NodeSet.send(:from_result, @document, result_ptr)
129
+ accumulated = merge_node_sets(accumulated, sub)
130
+ end
131
+ accumulated
132
+ end
133
+
134
+ def inspect
135
+ to_a.inspect
136
+ end
137
+
138
+ private
139
+
140
+ def merge_node_sets(a, b)
141
+ Leptris::XML::NodeSet.new(@document, a.to_a + b.to_a)
142
+ end
143
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Leptris::XML::ParseOptions
4
+ DEFAULT_XML = 0
5
+ RECOVER = 1 << 0
6
+ NOERROR = 1 << 5
7
+ NOWARNING = 1 << 6
8
+ NOCDATA = 1 << 8
9
+ STRICT = 1 << 18
10
+
11
+ attr_reader :options
12
+
13
+ def initialize(options = DEFAULT_XML)
14
+ @options = options.to_i
15
+ end
16
+
17
+ def strict?; !@options.zero?; end
18
+ def recover?; @options & RECOVER != 0; end
19
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
4
+ def name
5
+ Leptris::XML::FFI.leptris_pi_node_get_target(@c_ptr)
6
+ end
7
+ alias_method :target, :name
8
+
9
+ def content
10
+ Leptris::XML::FFI.leptris_pi_node_get_data(@c_ptr).to_s
11
+ end
12
+
13
+ def target=(new_target)
14
+ status = Leptris::XML::FFI.leptris_pi_node_set_target(@c_ptr, new_target.to_s)
15
+ raise Leptris::XML::Error,
16
+ Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
17
+ new_target
18
+ end
19
+
20
+ def data=(new_data)
21
+ status = Leptris::XML::FFI.leptris_pi_node_set_data(@c_ptr, new_data.to_s)
22
+ raise Leptris::XML::Error,
23
+ Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
24
+ new_data
25
+ end
26
+ 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,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ class Leptris::XML::SAX::Parser
6
+ CHUNK_SIZE = 4096
7
+ private_constant :CHUNK_SIZE
8
+
9
+ attr_accessor :document, :encoding
10
+
11
+ def initialize(handler = Leptris::XML::SAX::Document.new, encoding = nil)
12
+ @document = handler
13
+ @encoding = encoding
14
+ end
15
+
16
+ # Parse a string, IO, or file path. Dispatches to parse_memory /
17
+ # parse_io / parse_file based on the argument type.
18
+ def parse(input)
19
+ case input
20
+ when String then parse_memory(input)
21
+ when ->(x) { x.respond_to?(:read) } then parse_io(input)
22
+ else
23
+ raise ArgumentError, "SAX parser expects a String or IO, got #{input.class}"
24
+ end
25
+ end
26
+
27
+ def parse_memory(string)
28
+ string = string.dup.force_encoding("UTF-8")
29
+ handler_struct = build_handler_struct
30
+ rc = Leptris::XML::FFI.leptris_sax_parse(
31
+ string, string.bytesize, handler_struct.pointer, nil)
32
+ if rc != 0
33
+ raise Leptris::XML::ParseError,
34
+ "leptris_sax_parse failed (rc=#{rc})"
35
+ end
36
+ self
37
+ end
38
+
39
+ def parse_io(io)
40
+ handler_struct = build_handler_struct
41
+ parser_ptr = Leptris::XML::FFI.leptris_sax_parser_create(
42
+ handler_struct.pointer, nil)
43
+ if parser_ptr.null?
44
+ raise Leptris::XML::Error, "leptris_sax_parser_create failed"
45
+ end
46
+ begin
47
+ while (chunk = io.read(CHUNK_SIZE))
48
+ rc = Leptris::XML::FFI.leptris_sax_parser_feed(
49
+ parser_ptr, chunk, chunk.bytesize, 0)
50
+ if rc != 0
51
+ raise Leptris::XML::ParseError, "leptris_sax_parser_feed failed (rc=#{rc})"
52
+ end
53
+ end
54
+ # Final flush
55
+ rc = Leptris::XML::FFI.leptris_sax_parser_feed(parser_ptr, "", 0, 1)
56
+ if rc != 0
57
+ raise Leptris::XML::ParseError, "leptris_sax_parser_feed (final) failed (rc=#{rc})"
58
+ end
59
+ ensure
60
+ Leptris::XML::FFI.leptris_sax_parser_free(parser_ptr)
61
+ end
62
+ self
63
+ end
64
+
65
+ def parse_file(path)
66
+ File.open(path, "r") { |f| parse_io(f) }
67
+ end
68
+
69
+ private
70
+
71
+ # Build a LeptrisSAXHandler struct populated with FFI::Function callbacks
72
+ # that dispatch to the Ruby handler. The struct (and its callbacks) are
73
+ # anchored against GC via the local variable for the duration of the
74
+ # synchronous parse call.
75
+ def build_handler_struct
76
+ s = Leptris::XML::FFI::SAXHandler.new
77
+ handler = @document # capture in closures
78
+
79
+ s[:start_document] = callback(:void, [:pointer]) do |_|
80
+ handler.start_document
81
+ end
82
+
83
+ s[:end_document] = callback(:void, [:pointer]) do |_|
84
+ handler.end_document
85
+ end
86
+
87
+ s[:start_element] = callback(:void, [:pointer, :string, :pointer]) do |_, name, attrs_ptr|
88
+ attrs = walk_attr_array(attrs_ptr)
89
+ handler.start_element(name, attrs)
90
+ end
91
+
92
+ s[:end_element] = callback(:void, [:pointer, :string]) do |_, name|
93
+ handler.end_element(name)
94
+ end
95
+
96
+ s[:characters] = callback(:void, [:pointer, :pointer, :size_t]) do |_, text_ptr, len|
97
+ handler.characters(text_ptr.read_bytes(len).force_encoding("UTF-8"))
98
+ end
99
+
100
+ s[:comment] = callback(:void, [:pointer, :string]) do |_, comment|
101
+ handler.comment(comment)
102
+ end
103
+
104
+ s[:cdata] = callback(:void, [:pointer, :string]) do |_, cdata|
105
+ handler.cdata_block(cdata)
106
+ end
107
+
108
+ s[:processing_instruction] = callback(:void, [:pointer, :string, :string]) do |_, target, data|
109
+ handler.processing_instruction(target, data)
110
+ end
111
+
112
+ s[:start_prefix_mapping] = callback(:void, [:pointer, :string, :string]) do |_, prefix, uri|
113
+ handler.start_prefix_mapping(prefix, uri)
114
+ end
115
+
116
+ s[:end_prefix_mapping] = callback(:void, [:pointer, :string]) do |_, prefix|
117
+ handler.end_prefix_mapping(prefix)
118
+ end
119
+
120
+ s[:error] = callback(:void, [:pointer, :string, :int, :int]) do |_, msg, line, col|
121
+ handler.error(msg, line, col)
122
+ end
123
+
124
+ s
125
+ end
126
+
127
+ def callback(return_type, params, blocking: true, &block)
128
+ ::FFI::Function.new(return_type, params, blocking: blocking, &block)
129
+ end
130
+
131
+ # The C `const char** attrs` is a NULL-terminated flat array of name/value
132
+ # pairs: [name1, value1, name2, value2, ..., NULL]. Walk into pairs.
133
+ def walk_attr_array(attrs_ptr)
134
+ return [] if attrs_ptr.null?
135
+ ptr_size = ::FFI.type_size(:pointer)
136
+ result = []
137
+ offset = 0
138
+ loop do
139
+ name_ptr = attrs_ptr.get_pointer(offset)
140
+ break if name_ptr.null?
141
+ value_ptr = attrs_ptr.get_pointer(offset + ptr_size)
142
+ break if value_ptr.null?
143
+ result << [name_ptr.read_string, value_ptr.read_string]
144
+ offset += 2 * ptr_size
145
+ end
146
+ result
147
+ end
148
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module Leptris
6
+ module XML
7
+ module SAX
8
+ autoload :Document, "leptris/xml/sax/document"
9
+ autoload :Parser, "leptris/xml/sax/parser"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leptris::XML::Searchable
4
+ def xpath(*paths)
5
+ handler, _ns, _vars = parse_search_args(paths)
6
+ raise ArgumentError, "custom XPath handlers not supported" if handler
7
+ expr = paths.join(" | ")
8
+
9
+ doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
10
+ context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
11
+
12
+ result_ptr = Leptris::XML::FFI.leptris_xpath_eval(doc_ptr, context_ptr, expr)
13
+ if result_ptr.null?
14
+ raise Leptris::XML::XPathError,
15
+ Leptris::XML::FFI.leptris_status_string(Leptris::XML::FFI::LEPTRIS_ERROR_XPATH)
16
+ end
17
+
18
+ wrap_xpath_result(result_ptr)
19
+ end
20
+
21
+ def at_xpath(*paths)
22
+ result = xpath(*paths)
23
+ result.is_a?(Leptris::XML::NodeSet) ? result.first : result
24
+ end
25
+
26
+ def search(*args)
27
+ paths = args.first.is_a?(Array) ? args.first : [args.first]
28
+ paths.map(&:to_s).all? { |p| looks_like_xpath?(p) } ? xpath(*paths) : css(*paths)
29
+ end
30
+ alias_method :/, :search
31
+
32
+ def at(*args)
33
+ result = search(*args)
34
+ result.is_a?(Leptris::XML::NodeSet) ? result.first : result
35
+ end
36
+ alias_method :%, :at
37
+
38
+ def css(*args)
39
+ handler, ns, _ = parse_search_args(args)
40
+ raise ArgumentError, "namespace bindings not supported in css" if ns && !ns.empty?
41
+ raise ArgumentError, "custom CSS handlers not supported" if handler
42
+ expr = args.map { |r| Leptris::XML::CssToXPath.convert(r) }.join(" | ")
43
+ xpath(expr)
44
+ end
45
+
46
+ def at_css(*args)
47
+ result = css(*args)
48
+ result.is_a?(Leptris::XML::NodeSet) ? result.first : result
49
+ end
50
+
51
+ protected
52
+
53
+ def parse_search_args(args)
54
+ handler = args.find { |a| !a.is_a?(String) && !a.is_a?(Hash) && !a.is_a?(Symbol) }
55
+ args = args - [handler] if handler
56
+ hashes = []
57
+ while args.last.is_a?(Hash) || args.last.nil?
58
+ hashes << args.pop
59
+ break if args.empty?
60
+ end
61
+ ns, vars = hashes.reverse
62
+ [handler, ns, vars]
63
+ end
64
+
65
+ def looks_like_xpath?(str)
66
+ %r{\A(\./|/|\.\.|\.)}.match?(str)
67
+ end
68
+
69
+ def wrap_xpath_result(result_ptr)
70
+ type = Leptris::XML::FFI.leptris_xpath_result_type(result_ptr)
71
+ case type
72
+ when Leptris::XML::FFI::XPATH_NODESET
73
+ Leptris::XML::NodeSet.send(:from_result, document, result_ptr)
74
+ when Leptris::XML::FFI::XPATH_BOOLEAN
75
+ v = Leptris::XML::FFI.leptris_xpath_result_boolean(result_ptr) != 0
76
+ Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
77
+ v
78
+ when Leptris::XML::FFI::XPATH_NUMBER
79
+ v = Leptris::XML::FFI.leptris_xpath_result_number(result_ptr)
80
+ Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
81
+ v
82
+ when Leptris::XML::FFI::XPATH_STRING
83
+ str_ptr = Leptris::XML::FFI.leptris_xpath_result_string(result_ptr)
84
+ v = str_ptr.null? ? "" : str_ptr.read_string
85
+ Leptris::XML::FFI.leptris_free_string(str_ptr) unless str_ptr.null?
86
+ Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
87
+ v
88
+ else
89
+ Leptris::XML::FFI.leptris_xpath_result_free(result_ptr)
90
+ raise Leptris::XML::XPathError, "unknown xpath result type #{type}"
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Leptris::XML::Text < Leptris::XML::Node
4
+ def name; "text"; end
5
+
6
+ def content
7
+ Leptris::XML::FFI.leptris_text_node_get_content(@c_ptr)
8
+ end
9
+
10
+ def content=(new_content)
11
+ status = Leptris::XML::FFI.leptris_text_node_set_content(@c_ptr, new_content.to_s)
12
+ raise Leptris::XML::Error,
13
+ Leptris::XML::FFI.leptris_status_string(status) unless status == Leptris::XML::FFI::LEPTRIS_OK
14
+ new_content
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Leptris
4
+ module XML
5
+ autoload :FFI, "leptris/xml/ffi"
6
+ autoload :Node, "leptris/xml/node"
7
+ autoload :Element, "leptris/xml/element"
8
+ autoload :Text, "leptris/xml/text"
9
+ autoload :Comment, "leptris/xml/comment"
10
+ autoload :CDATA, "leptris/xml/cdata"
11
+ autoload :ProcessingInstruction, "leptris/xml/processing_instruction"
12
+ autoload :Attr, "leptris/xml/attr"
13
+ autoload :Namespace, "leptris/xml/namespace"
14
+ autoload :Document, "leptris/xml/document"
15
+ autoload :DocumentFragment, "leptris/xml/document_fragment"
16
+ autoload :DocType, "leptris/xml/doc_type"
17
+ autoload :NodeSet, "leptris/xml/node_set"
18
+ autoload :Searchable, "leptris/xml/searchable"
19
+ autoload :ParseOptions, "leptris/xml/parse_options"
20
+ autoload :CssToXPath, "leptris/xml/css_to_xpath"
21
+ autoload :SAX, "leptris/xml/sax"
22
+ require "leptris/xml/c14n" # module-level helper, eager load
23
+
24
+ # Nokogiri-style top-level entry points (Nokogiri::XML(...) /
25
+ # Nokogiri::XML.parse). Delegates to Document.parse.
26
+ def self.parse(xml_or_io)
27
+ Document.parse(xml_or_io)
28
+ end
29
+
30
+ def self.parse_file(path)
31
+ Document.parse_file(path)
32
+ end
33
+
34
+ class Error < StandardError; end
35
+ class ParseError < Error; end
36
+ class XPathError < Error; end
37
+ class UseAfterFreeError < Error; end
38
+ end
39
+ end
data/lib/leptris.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "leptris/version"
4
+
5
+ module Leptris
6
+ autoload :XML, "leptris/xml"
7
+ end