Class: Leptris::XML::Document

Inherits:
Object
  • Object
show all
Includes:
Searchable
Defined in:
lib/leptris/xml/document.rb

Defined Under Namespace

Classes: Freed

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Searchable

#at, #at_css, #at_xpath, #css, #search, wrap_xpath_result, #xpath

Constructor Details

#initialize(c_ptr = nil, freed = Freed.new(:alive)) ⇒ Document

Returns a new instance of Document.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/leptris/xml/document.rb', line 17

def initialize(c_ptr = nil, freed = Freed.new(:alive))
  @c_ptr = c_ptr
  @freed = freed
  @readonly = false
  # Per-document STRONG cache for Node wrappers, keyed on c_ptr
  # address. Every wrapper is created through Node.wrap, which is the
  # single construction path, so the same C node always yields the
  # same Ruby object. Cleared when the Document is freed — no stale
  # entries.
  #
  # Deliberately NOT ObjectSpace::WeakMap: a weak cache makes wrapper
  # identity a GC race. `doc.root.equal?(doc.root)` failed on the
  # Windows CI matrix (188 examples, the 4 identity specs) because
  # between the two calls the first wrapper was referenced only by
  # the weak map — any GC sweep evicted it and the second call built
  # a fresh object. A strong cache costs at most one wrapper per node
  # actually visited, held until the document dies.
  #
  # Allocated lazily: parse-heavy loops stop paying one Hash per
  # document for trees that are freed before any wrap.
end

Instance Attribute Details

#c_ptrObject (readonly)

Returns the value of attribute c_ptr.



6
7
8
# File 'lib/leptris/xml/document.rb', line 6

def c_ptr
  @c_ptr
end

Class Method Details

.createObject

Create an empty document (no root element) backed by its own memory pool. Elements for the tree are created against it via #create_element and friends, then attached with #root=.



98
99
100
101
102
103
# File 'lib/leptris/xml/document.rb', line 98

def self.create
  raw = Leptris::XML::FFI.leptris_document_create
  raise Leptris::XML::Error,
    "leptris_document_create failed" if raw.null?
  wrap(raw)
end

.parse(xml_or_io, options: nil, readonly: false, recover: false) ⇒ Object



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/leptris/xml/document.rb', line 43

def self.parse(xml_or_io, options: nil, readonly: false, recover: false)
  xml = xml_or_io.respond_to?(:read) ? xml_or_io.read : xml_or_io.to_s
  if xml.empty?
    raise Leptris::XML::ParseError, "empty input"
  end
  if options.nil?
    options = Leptris::XML::ParseOptions.new(recover: recover)
  elsif recover && !options.recover?
    options = options | Leptris::XML::ParseOptions.new(recover: true)
  elsif !options.is_a?(Leptris::XML::ParseOptions)
    raise ArgumentError, "options must be a Leptris::XML::ParseOptions"
  end
  # The status out-param is nullable; the thread-local last error
  # carries failure detail, and skipping the per-parse MemoryPointer
  # is measurable on small documents.
  raw =
    if options.struct_required?
      # Recover is a struct field, not a parse flag — the options
      # struct path (leptris_parse_string_ex) is the only carrier.
      options_struct = options.to_c_struct
      Leptris::XML::FFI.leptris_parse_string_ex(
        xml, xml.bytesize, options_struct.pointer, nil)
    elsif options.flags.zero?
      Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, nil)
    else
      Leptris::XML::FFI.leptris_parse_string_flags(
        xml, xml.bytesize, options.flags, nil)
    end
  if raw.null?
    if options.recover?
      # Unreachable in practice: recover returns an empty document
      # rather than NULL; kept so a contract change fails loudly.
      raise Leptris::XML::Error,
        "leptris_parse_string_ex returned NULL under recover"
    end
    raise Leptris::XML::ParseError,
      "leptris_parse_string failed: " +
      Leptris::XML::FFI.leptris_last_error.to_s
  end
  wrap(raw).tap { |doc| doc.readonly! if readonly }
end

.parse_file(path, readonly: false) ⇒ Object



85
86
87
88
89
90
91
92
93
# File 'lib/leptris/xml/document.rb', line 85

def self.parse_file(path, readonly: false)
  raw = Leptris::XML::FFI.leptris_parse_file(path, nil)
  if raw.null?
    raise Leptris::XML::ParseError,
      "leptris_parse_file failed: " +
      Leptris::XML::FFI.leptris_last_error.to_s
  end
  wrap(raw).tap { |doc| doc.readonly! if readonly }
end

.wrap(raw_address) ⇒ Object

Convert a raw LeptrisDocument pointer into a Ruby Document with safe GC lifetime management. The finalizer captures the raw address integer (not the Document or Pointer object — those would prevent GC) and shares a one-shot flag with the instance so explicit #free and the GC finalizer can never both call leptris_document_free on the same address.



111
112
113
114
115
116
117
118
# File 'lib/leptris/xml/document.rb', line 111

def self.wrap(raw_address)
  addr = raw_address.is_a?(::FFI::Pointer) ? raw_address.address : raw_address
  ptr = ::FFI::Pointer.new(addr)
  freed = Freed.new(:alive)
  doc = new(ptr, freed)
  ObjectSpace.define_finalizer(doc, finalizer(addr, freed))
  doc
end

Instance Method Details

#add_pi(target, data = "") ⇒ Object

Append a document-level processing instruction. Returns self.



269
270
271
272
273
274
# File 'lib/leptris/xml/document.rb', line 269

def add_pi(target, data = "")
  witness = Leptris::XML::FFI.leptris_document_add_pi(
    @c_ptr, target.to_s, data.to_s)
  raise Leptris::XML::Error, "leptris_document_add_pi failed" if witness.null?
  self
end

#canonicalize(version = Leptris::XML::FFI::C14N_1_0, inclusive_namespaces = nil, with_comments: false, exclusive: false, mode: nil) ⇒ Object Also known as: c14n



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/leptris/xml/document.rb', line 217

def canonicalize(version = Leptris::XML::FFI::C14N_1_0,
                 inclusive_namespaces = nil,
                 with_comments: false,
                 exclusive: false,
                 mode: nil)
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
  return "" if @c_ptr.nil?
  resolved_mode = mode || (exclusive ? Leptris::XML::FFI::C14N_MODE_EXCLUSIVE
                                     : Leptris::XML::FFI::C14N_MODE_CANONICAL)
  Leptris::XML::Serialization.canonicalize(
    Leptris::XML::FFI.method(:leptris_c14n_canonicalize_ex), @c_ptr,
    version: version, mode: resolved_mode,
    inclusive_namespaces: inclusive_namespaces,
    with_comments: with_comments)
end

#create_cdata(content) ⇒ Object



166
167
168
169
170
# File 'lib/leptris/xml/document.rb', line 166

def create_cdata(content)
  ptr = Leptris::XML::FFI.leptris_cdata_node_create(@c_ptr, content.to_s)
  raise Leptris::XML::Error, "leptris_cdata_node_create failed" if ptr.null?
  Leptris::XML::Node.wrap(ptr, self)
end

#create_comment(content) ⇒ Object



160
161
162
163
164
# File 'lib/leptris/xml/document.rb', line 160

def create_comment(content)
  ptr = Leptris::XML::FFI.leptris_comment_node_create(@c_ptr, content.to_s)
  raise Leptris::XML::Error, "leptris_comment_node_create failed" if ptr.null?
  Leptris::XML::Node.wrap(ptr, self)
end

#create_element(name) ⇒ Object



148
149
150
151
152
# File 'lib/leptris/xml/document.rb', line 148

def create_element(name)
  ptr = Leptris::XML::FFI.leptris_element_create(@c_ptr, name)
  raise Leptris::XML::Error, "leptris_element_create failed" if ptr.null?
  Leptris::XML::Node.wrap(ptr, self)
end

#create_processing_instruction(target, data = "") ⇒ Object



172
173
174
175
176
# File 'lib/leptris/xml/document.rb', line 172

def create_processing_instruction(target, data = "")
  ptr = Leptris::XML::FFI.leptris_pi_node_create(@c_ptr, target.to_s, data.to_s)
  raise Leptris::XML::Error, "leptris_pi_node_create failed" if ptr.null?
  Leptris::XML::Node.wrap(ptr, self)
end

#create_text_node(content) ⇒ Object



154
155
156
157
158
# File 'lib/leptris/xml/document.rb', line 154

def create_text_node(content)
  ptr = Leptris::XML::FFI.leptris_text_node_create(@c_ptr, content.to_s)
  raise Leptris::XML::Error, "leptris_text_node_create failed" if ptr.null?
  Leptris::XML::Node.wrap(ptr, self)
end

#doctypeObject Also known as: internal_subset



189
190
191
192
193
# File 'lib/leptris/xml/document.rb', line 189

def doctype
  ptr = Leptris::XML::FFI.leptris_document_internal_subset(@c_ptr)
  return nil if ptr.null?
  Leptris::XML::DocType.new(ptr, self)
end

#documentObject



319
# File 'lib/leptris/xml/document.rb', line 319

def document; self; end

#dupObject Also known as: clone



182
183
184
185
186
# File 'lib/leptris/xml/document.rb', line 182

def dup
  raw = Leptris::XML::FFI.leptris_document_copy(@c_ptr)
  raise Leptris::XML::Error, "leptris_document_copy failed" if raw.null?
  self.class.wrap(raw)
end

#encodingObject



320
321
322
323
# File 'lib/leptris/xml/document.rb', line 320

def encoding
  return nil if @c_ptr.nil?
  Leptris::XML::FFI.leptris_document_encoding(@c_ptr)
end

#exsltObject

Enable the first-party EXSLT-style extension pack on this document: str:/set:/math: prefixed functions (replace, tokenize, split, concat, padding; distinct, intersection, difference, leading, trailing; max, min, abs, sqrt, power) as native C handlers. Returns self for chaining.



247
248
249
250
251
# File 'lib/leptris/xml/document.rb', line 247

def exslt
  Leptris::XML::FFI.check_status(
    Leptris::XML::FFI.leptris_exslt_enable(@c_ptr))
  self
end

#fragment(markup) ⇒ Object



178
179
180
# File 'lib/leptris/xml/document.rb', line 178

def fragment(markup)
  Leptris::XML::DocumentFragment.parse(markup, self)
end

#freeObject



234
235
236
237
238
239
240
# File 'lib/leptris/xml/document.rb', line 234

def free
  return if @freed.state == :freed
  @freed.state = :freed
  Leptris::XML::FFI.leptris_document_free(@c_ptr) unless @c_ptr.nil?
  @c_ptr = nil
  @wrapper_cache&.clear
end

#freed?Boolean

True once #free has run (or the GC finalizer fired) — borrowed handles check this before dereferencing their c_ptr.

Returns:

  • (Boolean)


292
293
294
# File 'lib/leptris/xml/document.rb', line 292

def freed?
  @freed.state == :freed || @c_ptr.nil?
end

#last_errorObject

The most recent error recorded against this document, or nil.



313
314
315
316
# File 'lib/leptris/xml/document.rb', line 313

def last_error
  msg = Leptris::XML::FFI.leptris_document_last_error(@c_ptr)
  msg.nil? || msg.empty? ? nil : msg
end

#last_error_positionObject

The thread-global last-failure [line, column] (1-based), or nil when no error is recorded — the position companion to Document#last_error; populated by recover parses.



299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/leptris/xml/document.rb', line 299

def last_error_position
  line = ::FFI::MemoryPointer.new(:int)
  column = ::FFI::MemoryPointer.new(:int)
  begin
    Leptris::XML::FFI.leptris_last_error_position(line, column)
    line.read_int.zero? && column.read_int.zero? ? nil :
      [line.read_int, column.read_int]
  ensure
    line.free
    column.free
  end
end

#nameObject



318
# File 'lib/leptris/xml/document.rb', line 318

def name; "document"; end

#processing_instructionsObject

Document-level processing instructions (not tree nodes): an array of [target, data] pairs in document order.



255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/leptris/xml/document.rb', line 255

def processing_instructions
  if readonly? && instance_variable_defined?(:@processing_instructions)
    return @processing_instructions
  end
  count = Leptris::XML::FFI.leptris_document_pi_count(@c_ptr)
  result = count.times.map do |i|
    [Leptris::XML::FFI.leptris_document_pi_target(@c_ptr, i),
     Leptris::XML::FFI.leptris_document_pi_data(@c_ptr, i)]
  end
  @processing_instructions = result if readonly?
  result
end

#readonly!Object

Marks the document read-only: tree mutations raise Leptris::XML::ReadOnlyError, and read paths memoize aggressively (names, content, children, attributes) since they can never go stale. The C document is also frozen (advisory upstream). One-way.



280
281
282
283
284
# File 'lib/leptris/xml/document.rb', line 280

def readonly!
  Leptris::XML::FFI.leptris_document_freeze(@c_ptr)
  @readonly = true
  self
end

#readonly?Boolean

Returns:

  • (Boolean)


286
287
288
# File 'lib/leptris/xml/document.rb', line 286

def readonly?
  @readonly == true
end

#rootObject



129
130
131
132
133
134
135
# File 'lib/leptris/xml/document.rb', line 129

def root
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
  return nil if @c_ptr.nil?
  ptr = Leptris::XML::FFI.leptris_document_root(@c_ptr)
  return nil if ptr.null?
  Leptris::XML::Node.wrap(ptr, self)
end

#root=(element) ⇒ Object

Attach element as the document's root element. The element must have been created against this document and must not already have a parent. Any previous root is left detached (still owned by the document's pool until #free).



141
142
143
144
145
146
# File 'lib/leptris/xml/document.rb', line 141

def root=(element)
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
  Leptris::XML::FFI.check_status(
    Leptris::XML::FFI.leptris_document_set_root(@c_ptr, element.c_ptr))
  element
end

#save(path, **opts) ⇒ Object



206
207
208
209
210
211
212
213
214
215
# File 'lib/leptris/xml/document.rb', line 206

def save(path, **opts)
  opts_struct, _encoding_anchor = Leptris::XML::Serialization.build_options(
    indent: opts.fetch(:indent, 0),
    no_decl: opts.fetch(:no_decl, false),
    encoding: opts[:encoding])
  status = Leptris::XML::FFI.leptris_document_save_file(
    @c_ptr, path, opts_struct.pointer)
  Leptris::XML::FFI.check_status(status)
  self
end

#to_xml(indent: 0, no_decl: false, encoding: nil) ⇒ Object Also known as: to_s, serialize



196
197
198
199
200
201
202
# File 'lib/leptris/xml/document.rb', line 196

def to_xml(indent: 0, no_decl: false, encoding: nil)
  raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
  return "" if @c_ptr.nil?
  Leptris::XML::Serialization.to_xml(
    Leptris::XML::FFI.method(:leptris_document_serialize_into), @c_ptr,
    indent: indent, no_decl: no_decl, encoding: encoding)
end

#wrapper_cacheObject



39
40
41
# File 'lib/leptris/xml/document.rb', line 39

def wrapper_cache
  @wrapper_cache ||= {}
end