Module: Moxml::Adapter::Leptris::DocumentParts
- Included in:
- Moxml::Adapter::Leptris
- Defined in:
- lib/moxml/adapter/leptris/document_parts.rb
Constant Summary collapse
- DOCUMENT_ATTACHMENT_KEYS =
Issue #134: deterministic release of the C tree. The binding clears its wrapper cache and raises UseAfterFreeError on later access; moxml-side attachments for the document are swept too (the context wrapper identity map self-cleans via its size valve).
%i[ entity_markers declaration doctype had_source_declaration document_text ].freeze
- DOCUMENT_CHILD_PTRS =
Issue #158: the engine's whole-document serializer is one C call; the composed path serializes the root through the element face, which copies the subtree into a fresh document on every call (~4.5x slower end to end). The engine's output is byte-identical to the composed one EXCEPT epilog parts glue directly to the root — so the fast path declines whenever anything follows the root, any attachment overrides a part, or the engine's declaration line differs from the facade's canonical form (checked post-hoc: a source declaration carrying standalone or another version would diverge). 1.9.174 attached leptris_document_first_child (libleptris 1.9.174): pointer probe for the single-child document shape without wrapping the child chain.
::Leptris::XML::FFI.respond_to?(:leptris_document_first_child)
Instance Method Summary collapse
- #add_document_child(doc, child) ⇒ Object
- #assemble_document_children(doc) ⇒ Object
- #default_declaration_xml(doc, options) ⇒ Object
- #document_has_declaration?(native) ⇒ Boolean
- #fast_document_output(doc, options) ⇒ Object
-
#format_internal_subset(subset) ⇒ Object
libxml2's DTD dump layout (leptris/leptris#636): newline after "[", one after every markup declaration, none after comments (they glue to both neighbors); an empty subset drops the brackets. Returns the INNER text for XmlEmitter.doctype_xml, nil when there is nothing to emit. The engine reports internal_subset as raw source text, so the declarations are re-tokenized — quote-aware, since an attribute default can contain ">"..
- #free_document(native) ⇒ Object
- #marker_text_for(parent, name) ⇒ Object
- #markup_decl_end(subset, start) ⇒ Object
-
#mirror_declaration_native(doc, child) ⇒ Object
Write a created declaration through to the engine's document state (setters, libleptris 1.9.176 / #1094) so native reads and serialization see the same truth the facade does.
- #native_doctype_xml(doc) ⇒ Object
-
#serialize_document(doc, options) ⇒ Object
Documents compose from their parts: the native serializer only walks the root subtree, so declaration, DOCTYPE, PIs and document-level text are assembled around it explicitly.
Instance Method Details
#add_document_child(doc, child) ⇒ Object
55 56 57 58 59 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 101 102 103 104 105 106 107 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 55 def add_document_child(doc, child) case child when CustomizedLeptris::Declaration child.parent_doc = doc .set(doc, :declaration, child) mirror_declaration_native(doc, child) if NATIVE_DOC_PARTS when CustomizedLeptris::Doctype if NATIVE_DOC_PARTS dt = doc.set_doctype(child.name, public_id: child.external_id, system_id: child.system_id) .set(doc, :doctype, dt) return dt end child.parent_doc = doc .set(doc, :doctype, child) when ::Leptris::XML::DocType if NATIVE_DOC_PARTS dt = doc.set_doctype(child.root_name, public_id: child.public_id, system_id: child.system_id) .set(doc, :doctype, dt) return dt end raise Moxml::DocumentStructureError.new( "libleptris does not support attaching a native DocType to a document", ) when ::Leptris::XML::Element doc.root = child when ::Leptris::XML::ProcessingInstruction doc.add_pi(child.target, child.content.to_s) when CustomizedLeptris::DocumentPI doc.add_pi(child.target, child.data) when ::Leptris::XML::Text texts = .get(doc, :document_text) || [] texts << child .set(doc, :document_text, texts) child when ::Leptris::XML::Comment # The tree model supports document comments (they parse # and serialize, libleptris 1.9.3 #578) but the engine # has no add entry yet (leptris/leptris#1032). raise Moxml::NotImplementedError.new( "Adding document-level comments requires an engine entry (leptris/leptris#1032)", feature: "add_document_child", adapter: "Leptris", ) else raise Moxml::DocumentStructureError.new( "Unsupported document child: #{child.class}", ) end child end |
#assemble_document_children(doc) ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 23 def assemble_document_children(doc) children = [] # Attached DOCTYPEs (NATIVE_DOC_PARTS) list the STORED # native — the same object the attaching wrapper was # refreshed onto — so wrapper identity holds; doc.doctype # mints a fresh DocType per call and would fork wrappers. doctype_native = .get(doc, :doctype) doctype_native = doc.doctype unless doctype_native.is_a?(::Leptris::XML::DocType) children << doctype_native if doctype_native # The libxml2-model document node lists prolog PIs/comments, # the root, and epilog PIs/comments in document order — the # Nokogiri-shaped contract, epilog anchoring included (issue # #130). Built documents reflect their parts immediately # since leptris-ruby 1.9.32 (leptris-ruby#91). children.concat(doc.children.to_a) texts = .get(doc, :document_text) children.concat(texts) if texts # Canonicalize through the doc's address-keyed native # cache: #root mints a NativeNode for the document # element, and an uncanonicalized list would hand the # binding twin instead — two wrappers over one node, and # equal?-based exclusion (canon's document-element skip) # silently breaks (issue #219). Mint-on-miss converges # both accessors on the same native object. children.map! { |child| canonical_native(doc, child) } if NATIVE_READ_LAYER children end |
#default_declaration_xml(doc, options) ⇒ Object
310 311 312 313 314 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 310 def default_declaration_xml(doc, ) encoding = [:encoding] || doc.encoding encoding = "UTF-8" if encoding.to_s.empty? XmlEmitter.declaration_xml("1.0", encoding, nil) end |
#document_has_declaration?(native) ⇒ Boolean
316 317 318 319 320 321 322 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 316 def document_has_declaration?(native) return false unless native.is_a?(::Leptris::XML::Document) return true if .get(native, :declaration) .get(native, :had_source_declaration) ? true : false end |
#fast_document_output(doc, options) ⇒ Object
165 166 167 168 169 170 171 172 173 174 175 176 177 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 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 165 def fast_document_output(doc, ) return nil unless LIBXML2_LAYOUT_PARITY return nil unless .none_set?( doc, %i[declaration doctype document_text entity_markers] ) include_decl = ![:no_declaration] && .fetch(:declaration) do document_has_declaration?(doc) end # Exactly one document child — the root — is the common # parsed shape; pointer probes decide it without wrapping # the child chain. Anything else (prolog/epilog parts, # multi-root) falls back to the scan. root = doc.root single_child = DOCUMENT_CHILD_PTRS && root && ::Leptris::XML::FFI.leptris_document_first_child(doc.c_ptr) .address == root.c_ptr.address && root.next_sibling.nil? unless single_child root_seen = false doc.children.each do |child| if child.is_a?(::Leptris::XML::Element) return nil if root_seen root_seen = true elsif root_seen # Epilog parts glue directly to the root in the engine's # document output — compose those. return nil end end end # The engine's subset serializer mangles every declaration # after the first (leptris/leptris#687) — moxml's own # formatter is correct, so those compose until the probe # says the engine is fixed. dt = doc.doctype if !ENGINE_MULTI_DECL_SUBSET_OK && dt&.class&.method_defined?(:internal_subset) && (subset = dt.internal_subset) && subset.scan("<!").size > 1 return nil end # Passing encoding when it equals the document's own is a # no-op conversion the serializer still pays (~20% of a # 31KB document serialize); nil skips it. Byte-equality of # both forms verified on 1.9.163.2 — gate to those builds. encoding = [:encoding] if NATIVE_STRINGS_UTF8 && encoding.to_s.casecmp?("UTF-8") encoding = nil end kwargs = { indent: .fetch(:indent, 0), no_decl: !include_decl, encoding: encoding, } if INDENT_UNIT_SUPPORTED && [:indent_text].is_a?(String) kwargs[:indent_text] = [:indent_text] end output = doc.to_xml(**kwargs) return nil if output.nil? || output.empty? if include_decl && !output.start_with?(default_declaration_xml(doc, )) return nil end output << "\n" unless output.end_with?("\n") output end |
#format_internal_subset(subset) ⇒ Object
libxml2's DTD dump layout (leptris/leptris#636): newline after "[", one after every markup declaration, none after comments (they glue to both neighbors); an empty subset drops the brackets. Returns the INNER text for XmlEmitter.doctype_xml, nil when there is nothing to emit. The engine reports internal_subset as raw source text, so the declarations are re-tokenized — quote-aware, since an attribute default can contain ">".
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 253 def format_internal_subset(subset) return nil if subset.nil? || subset.empty? out = +"\n" pos = 0 length = subset.length while pos < length start = subset.index("<", pos) break if start.nil? terminator, skip = if subset[start, 4] == "<!--" ["-->", 4] elsif subset[start, 2] == "<?" ["?>", 2] else [nil, 0] end if terminator stop = subset.index(terminator, start + skip) break if stop.nil? item_end = stop + terminator.length else item_end = markup_decl_end(subset, start) break if item_end.nil? end out << subset[start...item_end] # Comments carry no trailing newline; declarations do. out << "\n" unless subset[start, 4] == "<!--" pos = item_end end out == "\n" ? nil : out end |
#free_document(native) ⇒ Object
17 18 19 20 21 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 17 def free_document(native) DOCUMENT_ATTACHMENT_KEYS.each { |key| .delete(native, key) } native.free nil end |
#marker_text_for(parent, name) ⇒ Object
358 359 360 361 362 363 364 365 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 358 def marker_text_for(parent, name) return nil unless parent.is_a?(::Leptris::XML::Element) marker = "#{Entity::MARKER}#{name};" parent.children.to_a.find do |child| child.is_a?(::Leptris::XML::Text) && child.content == marker end end |
#markup_decl_end(subset, start) ⇒ Object
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 292 def markup_decl_end(subset, start) quote = nil i = start length = subset.length while i < length ch = subset[i] if quote quote = nil if ch == quote elsif QUOTE_CHARS.include?(ch) quote = ch elsif ch == ">" return i + 1 end i += 1 end nil end |
#mirror_declaration_native(doc, child) ⇒ Object
Write a created declaration through to the engine's document state (setters, libleptris 1.9.176 / #1094) so native reads and serialization see the same truth the facade does. Removal clears it (clear_declaration).
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 328 def mirror_declaration_native(doc, child) # The engine setters reject empty values; the facade's # minimal declarations may carry them (serializer formats # what it gets). Mirror only non-empty parts — the wrapper # remains the record for what the facade shows. unless child.version.to_s.empty? ::Leptris::XML::FFI.check_status( ::Leptris::XML::FFI.leptris_document_set_version( doc.c_ptr, child.version.to_s ), ) end unless child.encoding.to_s.empty? ::Leptris::XML::FFI.check_status( ::Leptris::XML::FFI.leptris_document_set_encoding( doc.c_ptr, child.encoding.to_s ), ) end case child.standalone.to_s when "yes" then sa = 1 when "no" then sa = 0 end ::Leptris::XML::FFI.check_status( ::Leptris::XML::FFI.leptris_document_set_standalone( doc.c_ptr, sa || -1 ), ) end |
#native_doctype_xml(doc) ⇒ Object
236 237 238 239 240 241 242 243 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 236 def native_doctype_xml(doc) dt = doc.doctype return nil unless dt subset = dt.internal_subset if dt.class.method_defined?(:internal_subset) subset = format_internal_subset(subset) if LIBXML2_LAYOUT_PARITY XmlEmitter.doctype_xml(dt.root_name, dt.public_id, dt.system_id, subset) end |
#serialize_document(doc, options) ⇒ Object
Documents compose from their parts: the native serializer only walks the root subtree, so declaration, DOCTYPE, PIs and document-level text are assembled around it explicitly.
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 112 def serialize_document(doc, ) fast = fast_document_output(doc, ) return fast if fast # Nokogiri's document shape: every top-level part is # newline-terminated, at any indent — declaration, DOCTYPE, # document PIs, the root element, trailing newline after it. # Document-level text is content, not structure: no added # newline. parts = [] include_decl = ![:no_declaration] && .fetch(:declaration) do document_has_declaration?(doc) end if include_decl declaration = .get(doc, :declaration) parts << (declaration ? declaration.to_xml : default_declaration_xml(doc, )) << "\n" end doctype = .get(doc, :doctype) parts << doctype.to_xml << "\n" if doctype.is_a?(CustomizedLeptris::Doctype) native = native_doctype_xml(doc) parts << native << "\n" if native # The libxml2-model document node: prolog PIs/comments, the # root, epilog PIs/comments — in document order, so epilog # parts serialize after the root (issue #130). doc.children.each { |child| parts << raw_serialize(child, ) << "\n" } texts = .get(doc, :document_text) texts&.each { |text| parts << XmlEmitter.escape_text(text.content.to_s) } parts.join end |