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, #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
# File 'lib/leptris/xml/document.rb', line 17

def initialize(c_ptr = nil, freed = Freed.new(:alive))
  @c_ptr = c_ptr
  @freed = freed
  # 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=.



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

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



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

def self.parse(xml_or_io, options: nil)
  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)
  status_ptr = ::FFI::MemoryPointer.new(:int)
  raw =
    if flags.zero?
      Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, status_ptr)
    else
      Leptris::XML::FFI.leptris_parse_string_flags(
        xml, xml.bytesize, flags, status_ptr)
    end
  if raw.null?
    status = status_ptr.read_int
    raise Leptris::XML::ParseError,
      "leptris_parse_string failed (status=#{status}): " +
      Leptris::XML::FFI.status_message(status)
  end
  wrap(raw)
end

.parse_file(path) ⇒ Object



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

def self.parse_file(path)
  status_ptr = ::FFI::MemoryPointer.new(:int)
  raw = Leptris::XML::FFI.leptris_parse_file(path, status_ptr)
  if raw.null?
    status = status_ptr.read_int
    raise Leptris::XML::ParseError,
      "leptris_parse_file failed (status=#{status})"
  end
  wrap(raw)
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.



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

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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

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



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

def document; self; end

#dupObject Also known as: clone



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

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



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

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

#fragment(markup) ⇒ Object



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

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

#freeObject



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

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

#nameObject



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

def name; "document"; end

#rootObject



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

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



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

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



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

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



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

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