Class: Leptris::XML::Document
- Inherits:
-
Object
- Object
- Leptris::XML::Document
- Includes:
- Searchable
- Defined in:
- lib/leptris/xml/document.rb
Defined Under Namespace
Classes: Freed
Instance Attribute Summary collapse
-
#c_ptr ⇒ Object
readonly
Returns the value of attribute c_ptr.
Class Method Summary collapse
-
.create ⇒ Object
Create an empty document (no root element) backed by its own memory pool.
- .parse(xml_or_io, options: nil, readonly: false, recover: false) ⇒ Object
- .parse_file(path, readonly: false) ⇒ Object
-
.wrap(raw_address) ⇒ Object
Convert a raw LeptrisDocument pointer into a Ruby Document with safe GC lifetime management.
Instance Method Summary collapse
-
#add_pi(target, data = "") ⇒ Object
Append a document-level processing instruction.
-
#advance_version ⇒ Object
Called by the mutation gates (Node#ensure_writable!, root=, add_pi) — every memo stamped with an older version discards.
- #canonicalize(version = Leptris::XML::FFI::C14N_1_0, inclusive_namespaces = nil, with_comments: false, exclusive: false, mode: nil) ⇒ Object (also: #c14n)
-
#children ⇒ Object
The document's children, via the document node: prolog comments/PIs, the root element, epilog comments/PIs, in document order (Nokogiri-parity shape).
-
#comments ⇒ Object
Document-level comments — parsed <!-- ...
- #create_cdata(content) ⇒ Object
- #create_comment(content) ⇒ Object
- #create_element(name) ⇒ Object
- #create_processing_instruction(target, data = "") ⇒ Object
- #create_text_node(content) ⇒ Object
- #doctype ⇒ Object (also: #internal_subset)
- #document ⇒ Object
- #dup ⇒ Object (also: #clone)
- #encoding ⇒ Object
-
#exslt ⇒ Object
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.
- #fragment(markup) ⇒ Object
- #free ⇒ Object
-
#freed? ⇒ Boolean
True once #free has run (or the GC finalizer fired) — borrowed handles check this before dereferencing their c_ptr.
-
#initialize(c_ptr = nil, freed = Freed.new(:alive)) ⇒ Document
constructor
A new instance of Document.
-
#last_error ⇒ Object
The most recent error recorded against this document, or nil.
-
#last_error_position ⇒ Object
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.
- #name ⇒ Object
-
#node ⇒ Object
The document node — navigation head over the whole tree chain [prolog comments/PIs, root element, epilog comments/PIs] in document order (libleptris 1.9.7, upstream #580: the libxml2 model; XPath /comment() and //processing-instruction() see the document-level nodes).
-
#processing_instructions ⇒ Object
Document-level processing instructions (not tree nodes): an array of [target, data] pairs in document order.
-
#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.
- #readonly? ⇒ Boolean
-
#remove_pi(target_or_index) ⇒ Object
Removes a document-level processing instruction — by target (String/Symbol) or by 0-based index among the document's PIs (Integer) (libleptris 1.9.9, upstream #612).
- #root ⇒ Object
-
#root=(element) ⇒ Object
Attach
elementas the document's root element. - #save(path, **opts) ⇒ Object
-
#to_xml(indent: 0, no_decl: false, encoding: nil, indent_text: false) ⇒ Object
(also: #to_s, #serialize)
indent_text selects the ext-serializer knob (libleptris 1.9.22): a STRING is the indent unit with Nokogiri's semantics — the unit replaces the default spaces, one copy per depth level, standard layout (requires indent > 0); true selects the display form (1.9.9 #129 — text and mixed content indent too; output is display-oriented and not round-trip-guaranteed).
-
#version ⇒ Object
Mutation version: advanced by every data mutation (via Node#ensure_writable!, root=, add_pi).
- #wrapper_cache ⇒ Object
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_ptr ⇒ Object (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
.create ⇒ Object
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 .nil? = Leptris::XML::ParseOptions.new(recover: recover) elsif recover && !.recover? = | Leptris::XML::ParseOptions.new(recover: true) elsif !.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 .struct_required? # Recover is a struct field, not a parse flag — the options # struct path (leptris_parse_string_ex) is the only carrier. = .to_c_struct Leptris::XML::FFI.leptris_parse_string_ex( xml, xml.bytesize, .pointer, nil) elsif .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? if .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.
376 377 378 379 380 381 382 |
# File 'lib/leptris/xml/document.rb', line 376 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_version ⇒ Object
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
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/leptris/xml/document.rb', line 305 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 |
#children ⇒ Object
The document's children, via the document node: prolog comments/PIs, the root element, epilog comments/PIs, in document order (Nokogiri-parity shape).
On programmatically built documents the C document node's chain misses a root attached via #root= until some other document mutation refreshes it (leptris-ruby#91 — libleptris's document_set_root does not register into the chain). The merge below splices the attached root in by document order whenever the chain lacks it, so parsed and built documents read the same. Prolog/epilog placement uses node_compare against the root.
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/leptris/xml/document.rb', line 178 def children doc_node = node return Leptris::XML::NodeSet.new(self, []) if doc_node.nil? kids = doc_node.children.to_a root_ptr = Leptris::XML::FFI.leptris_document_root(@c_ptr) return kids if root_ptr.null? return kids if kids.any? { |child| child.c_ptr == root_ptr } root = Leptris::XML::Node.wrap(root_ptr, self) # A replaced root stays in the C chain until another document # mutation refreshes it — the chain's element slots other than # the current root are stale old roots. The new root inherits # the first stale slot's position (the prolog/epilog split # follows where the old root sat); with no stale element # (programmatically built documents) placement falls back to # document-order comparison. stale = kids.select do |child| child.element? && child.c_ptr != root_ptr end if stale.any? kept = kids.reject { |child| stale.include?(child) } slot = kids.index(stale.first) kept.insert(slot, root) Leptris::XML::NodeSet.new(self, kept) else prolog, epilog = kids.partition do |child| Leptris::XML::FFI.leptris_node_compare(child.c_ptr, root_ptr).negative? end Leptris::XML::NodeSet.new(self, prolog + [root] + epilog) end end |
#comments ⇒ Object
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.
408 409 410 411 412 413 414 415 416 417 |
# File 'lib/leptris/xml/document.rb', line 408 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
239 240 241 242 243 |
# File 'lib/leptris/xml/document.rb', line 239 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
233 234 235 236 237 |
# File 'lib/leptris/xml/document.rb', line 233 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
221 222 223 224 225 |
# File 'lib/leptris/xml/document.rb', line 221 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
245 246 247 248 249 |
# File 'lib/leptris/xml/document.rb', line 245 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
227 228 229 230 231 |
# File 'lib/leptris/xml/document.rb', line 227 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 |
#doctype ⇒ Object Also known as: internal_subset
262 263 264 265 266 |
# File 'lib/leptris/xml/document.rb', line 262 def doctype ptr = Leptris::XML::FFI.leptris_document_internal_subset(@c_ptr) return nil if ptr.null? Leptris::XML::DocType.new(ptr, self) end |
#document ⇒ Object
442 |
# File 'lib/leptris/xml/document.rb', line 442 def document; self; end |
#dup ⇒ Object Also known as: clone
255 256 257 258 259 |
# File 'lib/leptris/xml/document.rb', line 255 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 |
#encoding ⇒ Object
443 444 445 446 |
# File 'lib/leptris/xml/document.rb', line 443 def encoding return nil if @c_ptr.nil? Leptris::XML::FFI.leptris_document_encoding(@c_ptr) end |
#exslt ⇒ Object
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.
335 336 337 338 339 |
# File 'lib/leptris/xml/document.rb', line 335 def exslt Leptris::XML::FFI.check_status( Leptris::XML::FFI.leptris_exslt_enable(@c_ptr)) self end |
#fragment(markup) ⇒ Object
251 252 253 |
# File 'lib/leptris/xml/document.rb', line 251 def fragment(markup) Leptris::XML::DocumentFragment.parse(markup, self) end |
#free ⇒ Object
322 323 324 325 326 327 328 |
# File 'lib/leptris/xml/document.rb', line 322 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.
400 401 402 |
# File 'lib/leptris/xml/document.rb', line 400 def freed? @freed.state == :freed || @c_ptr.nil? end |
#last_error ⇒ Object
The most recent error recorded against this document, or nil.
436 437 438 439 |
# File 'lib/leptris/xml/document.rb', line 436 def last_error msg = Leptris::XML::FFI.leptris_document_last_error(@c_ptr) msg.nil? || msg.empty? ? nil : msg end |
#last_error_position ⇒ Object
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.
422 423 424 425 426 427 428 429 430 431 432 433 |
# File 'lib/leptris/xml/document.rb', line 422 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 |
#name ⇒ Object
441 |
# File 'lib/leptris/xml/document.rb', line 441 def name; "document"; end |
#node ⇒ Object
The document node — navigation head over the whole tree chain [prolog comments/PIs, root element, epilog comments/PIs] in document order (libleptris 1.9.7, upstream #580: the libxml2 model; XPath /comment() and //processing-instruction() see the document-level nodes). A stable, document-owned singleton — Node.wrap's cache keeps the returned wrapper identical.
160 161 162 163 164 |
# File 'lib/leptris/xml/document.rb', line 160 def node raise Leptris::XML::UseAfterFreeError if @freed.state == :freed @node ||= Leptris::XML::Node.wrap( Leptris::XML::FFI.leptris_document_node(@c_ptr), self) end |
#processing_instructions ⇒ Object
Document-level processing instructions (not tree nodes): an array of [target, data] pairs in document order.
343 344 345 346 347 348 349 350 351 352 353 354 |
# File 'lib/leptris/xml/document.rb', line 343 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.read_pi_data( Leptris::XML::FFI.leptris_document_pi_data(@c_ptr, i)).to_s] 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.
388 389 390 391 392 |
# File 'lib/leptris/xml/document.rb', line 388 def readonly! Leptris::XML::FFI.leptris_document_freeze(@c_ptr) @readonly = true self end |
#readonly? ⇒ Boolean
394 395 396 |
# File 'lib/leptris/xml/document.rb', line 394 def readonly? @readonly == true end |
#remove_pi(target_or_index) ⇒ Object
Removes a document-level processing instruction — by target (String/Symbol) or by 0-based index among the document's PIs (Integer) (libleptris 1.9.9, upstream #612). Returns the removed PI — pool-owned and valid until #free — or nil when nothing matched.
361 362 363 364 365 366 367 368 369 370 371 372 373 |
# File 'lib/leptris/xml/document.rb', line 361 def remove_pi(target_or_index) raise Leptris::XML::UseAfterFreeError if @freed.state == :freed if target_or_index.is_a?(Integer) ptr = Leptris::XML::FFI.leptris_document_remove_pi( @c_ptr, nil, target_or_index) else ptr = Leptris::XML::FFI.leptris_document_remove_pi( @c_ptr, target_or_index.to_s, 0) end return nil if ptr.null? @version += 1 Leptris::XML::Node.wrap(ptr, self) end |
#root ⇒ Object
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).
213 214 215 216 217 218 219 |
# File 'lib/leptris/xml/document.rb', line 213 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
294 295 296 297 298 299 300 301 302 303 |
# File 'lib/leptris/xml/document.rb', line 294 def save(path, **opts) opts_struct, _encoding_anchor = Leptris::XML::Serialization.( 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, indent_text: false) ⇒ Object Also known as: to_s, serialize
indent_text selects the ext-serializer knob (libleptris 1.9.22): a STRING is the indent unit with Nokogiri's semantics — the unit replaces the default spaces, one copy per depth level, standard layout (requires indent > 0); true selects the display form (1.9.9 #129 — text and mixed content indent too; output is display-oriented and not round-trip-guaranteed).
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/leptris/xml/document.rb', line 275 def to_xml(indent: 0, no_decl: false, encoding: nil, indent_text: false) raise Leptris::XML::UseAfterFreeError if @freed.state == :freed return "" if @c_ptr.nil? case indent_text when String return Leptris::XML::Serialization.to_xml_indent_unit( @c_ptr, indent_text, indent: indent, no_decl: no_decl, encoding: encoding) when true return Leptris::XML::Serialization.to_xml_display( @c_ptr, indent: indent, no_decl: no_decl, encoding: encoding) end Leptris::XML::Serialization.to_xml( Leptris::XML::FFI.method(:leptris_document_serialize_into), @c_ptr, indent: indent, no_decl: no_decl, encoding: encoding) end |
#version ⇒ Object
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_cache ⇒ Object
56 57 58 |
# File 'lib/leptris/xml/document.rb', line 56 def wrapper_cache @wrapper_cache ||= {} end |