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
- #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
53 54 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 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 53 def add_document_child(doc, child) case child when CustomizedLeptris::Declaration child.parent_doc = doc .set(doc, :declaration, child) when CustomizedLeptris::Doctype child.parent_doc = doc .set(doc, :doctype, child) when ::Leptris::XML::DocType 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 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 23 def assemble_document_children(doc) children = [] native_doctype = doc.doctype children << native_doctype if native_doctype doctype_wrapper = .get(doc, :doctype) children << doctype_wrapper if doctype_wrapper # 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
293 294 295 296 297 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 293 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
299 300 301 302 303 304 305 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 299 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
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 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 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 148 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 ">".
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 236 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
307 308 309 310 311 312 313 314 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 307 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
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 275 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 |
#native_doctype_xml(doc) ⇒ Object
219 220 221 222 223 224 225 226 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 219 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.
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/moxml/adapter/leptris/document_parts.rb', line 95 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 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 |