Module: Moxml::Adapter::Leptris::Serialize
- Included in:
- Moxml::Adapter::Leptris
- Defined in:
- lib/moxml/adapter/leptris/serialize.rb
Overview
Wire-format knowledge: the C serializer entry, the canonicalization pass (apostrophes stay literal, optional empty-element expansion), and the literal-region scanner that keeps both from touching CDATA/comment/PI content.
Constant Summary collapse
- LITERAL_REGIONS =
moxml canonical serialization: apostrophes stay literal (only & < > " are escaped) and empty elements expand when the caller asked for it — matching the other adapters' contract. Runs segment-aware: CDATA content is literal and must not be touched. Regions whose content is literal and must never be rewritten. Scanned positionally (String#index), not with a regex: the scan is linear in the input length, so pathological document content cannot blow up the serializer.
{ "<!--" => "-->", "<![CDATA[" => "]]>", "<?" => "?>", }.freeze
- EMPTY_ELEMENT_RE =
All quantifiers are possessive and the bare-part class excludes "/" and ">": the possessive groups can never consume the closing delimiter, so no backtracking is possible and the match is linear even on malformed tags.
%r{<([A-Za-z_][\w.:-]*+)((?:"[^"]*+"|'[^']*+'|[^<>"'/]++)*+)/>}
Instance Method Summary collapse
-
#next_literal_region(xml, from) ⇒ Object
Nearest literal region at/after from: [position, terminator].
- #normalize_markup(markup, needs_apos, needs_expand) ⇒ Object
- #normalize_serialization(xml, options) ⇒ Object
-
#opener_at_offset(terminator) ⇒ Object
Search for a terminator past its opener's overlap-safe offset ("-->" cannot start inside "<!--").
- #raw_serialize(node, options) ⇒ Object
- #serialize(node, options = {}) ⇒ Object
Instance Method Details
#next_literal_region(xml, from) ⇒ Object
Nearest literal region at/after from: [position, terminator].
102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/moxml/adapter/leptris/serialize.rb', line 102 def next_literal_region(xml, from) best = nil best_terminator = nil LITERAL_REGIONS.each do |opener, terminator| idx = xml.index(opener, from) next if idx.nil? if best.nil? || idx < best best = idx best_terminator = terminator end end best.nil? ? nil : [best, best_terminator] end |
#normalize_markup(markup, needs_apos, needs_expand) ⇒ Object
123 124 125 126 127 128 129 130 131 |
# File 'lib/moxml/adapter/leptris/serialize.rb', line 123 def normalize_markup(markup, needs_apos, ) markup = markup.gsub("'", "'") if needs_apos if markup = markup.gsub(EMPTY_ELEMENT_RE) do "<#{Regexp.last_match(1)}#{Regexp.last_match(2)}></#{Regexp.last_match(1)}>" end end markup end |
#normalize_serialization(xml, options) ⇒ Object
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/moxml/adapter/leptris/serialize.rb', line 77 def normalize_serialization(xml, ) needs_apos = xml.include?("'") = [:expand_empty] && xml.include?("/>") return xml unless needs_apos || out = +"" pos = 0 while pos < xml.length opener_at, terminator = next_literal_region(xml, pos) if opener_at.nil? out << normalize_markup(xml[pos..], needs_apos, ) break end out << normalize_markup(xml[pos...opener_at], needs_apos, ) search_from = opener_at + opener_at_offset(terminator) close = xml.index(terminator, search_from) close_end = close.nil? ? xml.length : close + terminator.length out << xml[opener_at...close_end] pos = close_end end out end |
#opener_at_offset(terminator) ⇒ Object
Search for a terminator past its opener's overlap-safe offset ("-->" cannot start inside "<!--").
119 120 121 |
# File 'lib/moxml/adapter/leptris/serialize.rb', line 119 def opener_at_offset(terminator) terminator == "-->" ? 4 : 0 end |
#raw_serialize(node, options) ⇒ Object
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/moxml/adapter/leptris/serialize.rb', line 43 def raw_serialize(node, ) # CDATA must precede Text in this chain: CDATA < Text in the # binding, so a Text branch first would swallow CDATA nodes. case node when CustomizedLeptris::Declaration, CustomizedLeptris::Doctype, CustomizedLeptris::EntityReference, CustomizedLeptris::DocumentPI return node.to_xml when ::Leptris::XML::CDATA return XmlEmitter.cdata(node.content) when ::Leptris::XML::Comment return "<!--#{node.content}-->" when ::Leptris::XML::ProcessingInstruction content = node.content.to_s return content.empty? ? "<?#{node.target}?>" : "<?#{node.target} #{content}?>" when ::Leptris::XML::Text, CustomizedLeptris::TextSegment return XmlEmitter.escape_text(node.content.to_s) when ::Leptris::XML::Document return serialize_document(node, ) end include_decl = .fetch(:declaration) do [:no_declaration] ? false : document_has_declaration?(node) end xml = node.to_xml( indent: .fetch(:indent, 0), no_decl: !include_decl, encoding: [:encoding], ) # Element output always ends with the close tag — but the # engine's serializer appends a stray trailing newline when # the element's last text child is non-ASCII. xml.sub(/\n+\z/, "") end |
#serialize(node, options = {}) ⇒ Object
32 33 34 35 36 37 38 39 40 41 |
# File 'lib/moxml/adapter/leptris/serialize.rb', line 32 def serialize(node, = {}) # Entity restoration belongs to the wrapper layer # (Node#to_xml runs adapter.restore_entities for every # adapter); doing it here scanned the output a second time. xml = normalize_serialization(raw_serialize(node, ), ) # The binding's FFI strings come back binary-tagged; the # engine encoded the bytes per this option, so tag them. xml.force_encoding([:encoding]) if [:encoding] xml end |