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_first_result, wrap_xpath_result, #xpath

Constructor Details

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

Returns a new instance of Document.



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

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



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

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



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/leptris/xml/document.rb', line 60

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



102
103
104
105
106
107
108
109
110
# File 'lib/leptris/xml/document.rb', line 102

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.



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

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.



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

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?
  @version += 1
  self
end

#advance_versionObject

Called by the mutation gates (Node#ensure_writable!, root=, add_pi) — every memo stamped with an older version discards.



29
30
31
# File 'lib/leptris/xml/document.rb', line 29

def advance_version
  @version += 1
end

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



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/leptris/xml/document.rb', line 235

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

#commentsObject

Document-level comments — parsed outside the root element, prolog then epilog, in document order (the companion reader to #processing_instructions; libleptris 1.9.3, upstream #578). Version-memoized like the PI list.



318
319
320
321
322
323
324
325
326
327
# File 'lib/leptris/xml/document.rb', line 318

def comments
  return @comments if @comments_version == @version
  count = Leptris::XML::FFI.leptris_document_comment_count(@c_ptr)
  result = Array.new(count) do |i|
    Leptris::XML::FFI.leptris_document_comment_content(@c_ptr, i)
  end
  @comments = result
  @comments_version = @version
  result
end

#create_cdata(content) ⇒ Object



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

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



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

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



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

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



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

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



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

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



207
208
209
210
211
# File 'lib/leptris/xml/document.rb', line 207

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



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

def document; self; end

#dupObject Also known as: clone



200
201
202
203
204
# File 'lib/leptris/xml/document.rb', line 200

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



353
354
355
356
# File 'lib/leptris/xml/document.rb', line 353

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.



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

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

#fragment(markup) ⇒ Object



196
197
198
# File 'lib/leptris/xml/document.rb', line 196

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

#freeObject



252
253
254
255
256
257
258
# File 'lib/leptris/xml/document.rb', line 252

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)


310
311
312
# File 'lib/leptris/xml/document.rb', line 310

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

#last_errorObject

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



346
347
348
349
# File 'lib/leptris/xml/document.rb', line 346

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.



332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/leptris/xml/document.rb', line 332

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



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

def name; "document"; end

#processing_instructionsObject

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



273
274
275
276
277
278
279
280
281
282
283
# File 'lib/leptris/xml/document.rb', line 273

def processing_instructions
  return @processing_instructions if @pi_version == @version
  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
  @pi_version = @version
  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.



298
299
300
301
302
# File 'lib/leptris/xml/document.rb', line 298

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

#readonly?Boolean

Returns:

  • (Boolean)


304
305
306
# File 'lib/leptris/xml/document.rb', line 304

def readonly?
  @readonly == true
end

#rootObject



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

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



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

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))
  @version += 1
  element
end

#save(path, **opts) ⇒ Object



224
225
226
227
228
229
230
231
232
233
# File 'lib/leptris/xml/document.rb', line 224

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



214
215
216
217
218
219
220
# File 'lib/leptris/xml/document.rb', line 214

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

#versionObject

Mutation version: advanced by every data mutation (via Node#ensure_writable!, root=, add_pi). Node memos stamp the version they were computed under and recompute after any bump — the invalidation that makes WRITABLE-document memoization sound. Readonly documents never advance it, so their memos are forever valid (ADR 0003 semantics, unchanged).



23
24
25
# File 'lib/leptris/xml/document.rb', line 23

def version
  @version
end

#wrapper_cacheObject



56
57
58
# File 'lib/leptris/xml/document.rb', line 56

def wrapper_cache
  @wrapper_cache ||= {}
end