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



74
75
76
77
78
79
# File 'lib/leptris/xml/document.rb', line 74

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) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/leptris/xml/document.rb', line 37

def self.parse(xml_or_io, options: nil, readonly: 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
  flags = resolve_flags(options)
  # 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 flags.zero?
      Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, nil)
    else
      Leptris::XML::FFI.leptris_parse_string_flags(
        xml, xml.bytesize, flags, nil)
    end
  if raw.null?
    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



61
62
63
64
65
66
67
68
69
# File 'lib/leptris/xml/document.rb', line 61

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.



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

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.



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

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



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

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



151
152
153
154
155
# File 'lib/leptris/xml/document.rb', line 151

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



145
146
147
148
149
# File 'lib/leptris/xml/document.rb', line 145

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



133
134
135
136
137
# File 'lib/leptris/xml/document.rb', line 133

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



157
158
159
160
161
# File 'lib/leptris/xml/document.rb', line 157

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



139
140
141
142
143
# File 'lib/leptris/xml/document.rb', line 139

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



174
175
176
177
178
# File 'lib/leptris/xml/document.rb', line 174

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



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

def document; self; end

#dupObject Also known as: clone



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

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



278
279
280
281
# File 'lib/leptris/xml/document.rb', line 278

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.



232
233
234
235
236
# File 'lib/leptris/xml/document.rb', line 232

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

#fragment(markup) ⇒ Object



163
164
165
# File 'lib/leptris/xml/document.rb', line 163

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

#freeObject



219
220
221
222
223
224
225
# File 'lib/leptris/xml/document.rb', line 219

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.



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

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

#nameObject



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

def name; "document"; end

#processing_instructionsObject

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



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

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.



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

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

#readonly?Boolean

Returns:

  • (Boolean)


266
267
268
# File 'lib/leptris/xml/document.rb', line 266

def readonly?
  @readonly == true
end

#rootObject



114
115
116
117
118
119
120
# File 'lib/leptris/xml/document.rb', line 114

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



126
127
128
129
130
131
# File 'lib/leptris/xml/document.rb', line 126

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



191
192
193
194
195
196
197
198
199
200
# File 'lib/leptris/xml/document.rb', line 191

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



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

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), @c_ptr,
    indent: indent, no_decl: no_decl, encoding: encoding)
end