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
# 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.
  @wrapper_cache = {}
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

#wrapper_cacheObject (readonly)

Returns the value of attribute wrapper_cache.



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

def wrapper_cache
  @wrapper_cache
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=.



92
93
94
95
96
97
# File 'lib/leptris/xml/document.rb', line 92

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



37
38
39
40
41
42
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
# File 'lib/leptris/xml/document.rb', line 37

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



79
80
81
82
83
84
85
86
87
# File 'lib/leptris/xml/document.rb', line 79

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.



105
106
107
108
109
110
111
112
# File 'lib/leptris/xml/document.rb', line 105

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.



258
259
260
261
262
263
# File 'lib/leptris/xml/document.rb', line 258

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



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/leptris/xml/document.rb', line 211

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



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

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



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

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



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

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



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

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



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

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



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

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



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

def document; self; end

#dupObject Also known as: clone



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

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



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

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.



241
242
243
244
245
# File 'lib/leptris/xml/document.rb', line 241

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

#fragment(markup) ⇒ Object



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

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

#freeObject



228
229
230
231
232
233
234
# File 'lib/leptris/xml/document.rb', line 228

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

#last_errorObject

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



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

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

#nameObject



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

def name; "document"; end

#processing_instructionsObject

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



249
250
251
252
253
254
255
# File 'lib/leptris/xml/document.rb', line 249

def processing_instructions
  count = Leptris::XML::FFI.leptris_document_pi_count(@c_ptr)
  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
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.



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

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

#readonly?Boolean

Returns:

  • (Boolean)


275
276
277
# File 'lib/leptris/xml/document.rb', line 275

def readonly?
  @readonly == true
end

#rootObject



123
124
125
126
127
128
129
# File 'lib/leptris/xml/document.rb', line 123

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).



135
136
137
138
139
140
# File 'lib/leptris/xml/document.rb', line 135

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



200
201
202
203
204
205
206
207
208
209
# File 'lib/leptris/xml/document.rb', line 200

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



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

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