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

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, needs_expand)
  markup = markup.gsub("&apos;", "'") if needs_apos
  if needs_expand
    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, options)
  needs_apos = xml.include?("&apos;")
  needs_expand = options[:expand_empty] && xml.include?("/>")
  return xml unless needs_apos || needs_expand

  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, needs_expand)
      break
    end

    out << normalize_markup(xml[pos...opener_at], needs_apos, needs_expand)
    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, options)
  # 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, options)
  end

  include_decl = options.fetch(:declaration) do
    options[:no_declaration] ? false : document_has_declaration?(node)
  end
  xml = node.to_xml(
    indent: options.fetch(:indent, 0),
    no_decl: !include_decl,
    encoding: options[: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, options = {})
  # 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, options), options)
  # The binding's FFI strings come back binary-tagged; the
  # engine encoded the bytes per this option, so tag them.
  xml.force_encoding(options[:encoding]) if options[:encoding]
  xml
end