Module: Dommy::Internal::XmlSerialization
- Defined in:
- lib/dommy/internal/xml_serialization.rb
Overview
WHATWG "XML serialization" algorithm (DOM Parsing & Serialization), run over
Dommy node wrappers so it is backend-agnostic. Produces namespace-correct
XML — default-namespace inheritance/reset, dropping redundant/inconsistent
xmlns, generating ns1/ns2 prefixes — which a backend's own to_xml
does not. Namespace declarations are read via Backend.namespace_definitions
(libxml2 models them as namespace nodes, not attributes) and presented to
the algorithm as the xmlns/xmlns:* attributes the spec expects.
Defined Under Namespace
Classes: Attr
Constant Summary collapse
- XML_NS =
"http://www.w3.org/XML/1998/namespace"- XMLNS_NS =
"http://www.w3.org/2000/xmlns/"- HTML_NS =
"http://www.w3.org/1999/xhtml"- VOID_ELEMENTS =
HTML void elements: when empty and in the HTML namespace they self-close with a trailing space in XML serialization (
<br />). %w[ area base basefont bgsound br col embed frame hr img input keygen link meta param source track wbr ].freeze
Class Method Summary collapse
-
.already_emitted_prefix?(_attr) ⇒ Boolean
An xmlns:foo declaration is re-emitted by serialize_attributes unless the element start tag already wrote it (when it adopted that prefix).
- .attr_struct(attr) ⇒ Object
- .backend_namespace_attrs(node) ⇒ Object
- .child_nodes(node) ⇒ Object
- .copy_map(map) ⇒ Object
-
.created_namespace(node) ⇒ Object
The explicit createElementNS [namespace, prefix, local] when the backend (lexbor) didn't retain it, else nil.
-
.element_attributes(node) ⇒ Object
Regular attributes plus xmlns / xmlns:* declarations synthesized from the backend's namespace definitions (which libxml2 keeps off the attribute list).
-
.element_namespace(node) ⇒ Object
The element's TRUE namespace from the backend (nil when none) — not the wrapper's #namespace_uri, which defaults to the HTML namespace for HTML documents and isn't right for an XML serialization.
- .element_prefix(node) ⇒ Object
-
.empty_element_close(ns, node, qualified) ⇒ Object
WHATWG XML serialization of an empty element: an HTML-namespace void element self-closes with a space (
<br />); any other HTML-namespace element gets an explicit end tag (<div></div>); a non-HTML element self-closes (<foo/>). - .escape_attr(str) ⇒ Object
- .escape_text(str) ⇒ Object
- .generate_prefix(map, ns, prefix_index) ⇒ Object
-
.local_name(node) ⇒ Object
The backend node name is the local part, case-preserved (the wrapper's #local_name lower-cases for HTML).
-
.node_type(node) ⇒ Object
---- node data access (backend-agnostic, via Dommy wrappers) ----.
- .presence(value) ⇒ Object
-
.record_namespace_information(attrs, map, local_prefixes) ⇒ Object
https://w3c.github.io/DOM-Parsing/#recording-the-namespace Updates
map/local_prefixesfrom the element's xmlns declarations and returns the default-namespace value declared on the element (or nil). - .retrieve_preferred_prefix(map, ns, preferred) ⇒ Object
- .serialize(node) ⇒ Object
- .serialize_attributes(attrs, map, prefix_index, local_prefixes, ignore_ns_def) ⇒ Object
- .serialize_children(node, context_ns, map, prefix_index) ⇒ Object
- .serialize_doctype(node) ⇒ Object
- .serialize_element(node, context_ns, map, prefix_index) ⇒ Object
- .serialize_node(node, context_ns, map, prefix_index) ⇒ Object
- .string_data(node) ⇒ Object
Class Method Details
.already_emitted_prefix?(_attr) ⇒ Boolean
An xmlns:foo declaration is re-emitted by serialize_attributes unless the element start tag already wrote it (when it adopted that prefix). We keep it simple: the spec drops a redundant declaration whose (prefix, value) is already in the local prefixes map, which record_namespace_information set.
203 204 205 |
# File 'lib/dommy/internal/xml_serialization.rb', line 203 def already_emitted_prefix?(_attr) false end |
.attr_struct(attr) ⇒ Object
294 295 296 297 298 299 300 301 |
# File 'lib/dommy/internal/xml_serialization.rb', line 294 def attr_struct(attr) Attr.new( presence(attr.respond_to?(:namespace_uri) ? attr.namespace_uri : nil), presence(attr.respond_to?(:prefix) ? attr.prefix : nil), attr.respond_to?(:local_name) ? attr.local_name : attr.name, attr.respond_to?(:value) ? attr.value.to_s : "" ) end |
.backend_namespace_attrs(node) ⇒ Object
279 280 281 282 283 284 285 286 287 288 289 290 291 292 |
# File 'lib/dommy/internal/xml_serialization.rb', line 279 def backend_namespace_attrs(node) backend = node.respond_to?(:__dommy_backend_node__) ? node.__dommy_backend_node__ : nil return [] unless backend Backend.namespace_definitions(backend).map do |defn| pfx = defn.respond_to?(:prefix) ? defn.prefix : defn.first href = defn.respond_to?(:href) ? defn.href : defn.last if pfx.nil? || pfx.to_s.empty? Attr.new(XMLNS_NS, nil, "xmlns", href.to_s) else Attr.new(XMLNS_NS, "xmlns", pfx.to_s, href.to_s) end end end |
.child_nodes(node) ⇒ Object
260 261 262 263 264 |
# File 'lib/dommy/internal/xml_serialization.rb', line 260 def child_nodes(node) return node.child_nodes.to_a if node.respond_to?(:child_nodes) [] end |
.copy_map(map) ⇒ Object
317 318 319 |
# File 'lib/dommy/internal/xml_serialization.rb', line 317 def copy_map(map) map.each_with_object({}) { |(k, v), out| out[k] = v.dup } end |
.created_namespace(node) ⇒ Object
The explicit createElementNS [namespace, prefix, local] when the backend (lexbor) didn't retain it, else nil.
256 257 258 |
# File 'lib/dommy/internal/xml_serialization.rb', line 256 def created_namespace(node) node.respond_to?(:__internal_created_namespace__) ? node.__internal_created_namespace__ : nil end |
.element_attributes(node) ⇒ Object
Regular attributes plus xmlns / xmlns:* declarations synthesized from the backend's namespace definitions (which libxml2 keeps off the attribute list). xmlns declarations come first so they populate the prefix map.
273 274 275 276 277 |
# File 'lib/dommy/internal/xml_serialization.rb', line 273 def element_attributes(node) ns_attrs = backend_namespace_attrs(node) regular = node.respond_to?(:attributes) ? node.attributes.to_a.map { |a| attr_struct(a) } : [] ns_attrs + regular end |
.element_namespace(node) ⇒ Object
The element's TRUE namespace from the backend (nil when none) — not the wrapper's #namespace_uri, which defaults to the HTML namespace for HTML documents and isn't right for an XML serialization.
225 226 227 228 229 230 231 232 233 234 |
# File 'lib/dommy/internal/xml_serialization.rb', line 225 def element_namespace(node) created = created_namespace(node) return presence(created[0]) if created # The backend's raw namespace — the HTML namespace for an HTML element # (which an XML serialization DOES declare as xmlns="…xhtml"), or the # parsed namespace for an XML element. backend = node.respond_to?(:__dommy_backend_node__) ? node.__dommy_backend_node__ : nil presence(backend.respond_to?(:namespace_uri) ? backend.namespace_uri : nil) end |
.element_prefix(node) ⇒ Object
236 237 238 239 240 241 242 |
# File 'lib/dommy/internal/xml_serialization.rb', line 236 def element_prefix(node) created = created_namespace(node) return presence(created[1]) if created backend = node.respond_to?(:__dommy_backend_node__) ? node.__dommy_backend_node__ : nil presence(backend.respond_to?(:prefix) ? backend.prefix : nil) end |
.empty_element_close(ns, node, qualified) ⇒ Object
WHATWG XML serialization of an empty element: an HTML-namespace void
element self-closes with a space (<br />); any other HTML-namespace
element gets an explicit end tag (<div></div>); a non-HTML element
self-closes (<foo/>).
117 118 119 120 121 |
# File 'lib/dommy/internal/xml_serialization.rb', line 117 def empty_element_close(ns, node, qualified) return "/>" unless ns == HTML_NS VOID_ELEMENTS.include?(local_name(node).downcase) ? " />" : "></#{qualified}>" end |
.escape_attr(str) ⇒ Object
332 333 334 335 336 337 338 339 340 341 |
# File 'lib/dommy/internal/xml_serialization.rb', line 332 def escape_attr(str) str.to_s .gsub("&", "&") .gsub('"', """) .gsub("<", "<") .gsub(">", ">") .gsub("\t", "	") .gsub("\n", " ") .gsub("\r", " ") end |
.escape_text(str) ⇒ Object
328 329 330 |
# File 'lib/dommy/internal/xml_serialization.rb', line 328 def escape_text(str) str.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">") end |
.generate_prefix(map, ns, prefix_index) ⇒ Object
159 160 161 162 163 164 |
# File 'lib/dommy/internal/xml_serialization.rb', line 159 def generate_prefix(map, ns, prefix_index) generated = "ns#{prefix_index[0]}" prefix_index[0] += 1 (map[ns] ||= []) << generated generated end |
.local_name(node) ⇒ Object
The backend node name is the local part, case-preserved (the wrapper's #local_name lower-cases for HTML).
246 247 248 249 250 251 252 |
# File 'lib/dommy/internal/xml_serialization.rb', line 246 def local_name(node) created = created_namespace(node) return created[2] if created && presence(created[2]) backend = node.respond_to?(:__dommy_backend_node__) ? node.__dommy_backend_node__ : nil backend ? backend.name.split(":", 2).last : node.__js_get__("nodeName") end |
.node_type(node) ⇒ Object
---- node data access (backend-agnostic, via Dommy wrappers) ----
209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/dommy/internal/xml_serialization.rb', line 209 def node_type(node) return 1 if node.is_a?(Dommy::Element) return 4 if defined?(Dommy::CDATASectionNode) && node.is_a?(Dommy::CDATASectionNode) return 3 if node.is_a?(Dommy::TextNode) return 8 if node.is_a?(Dommy::CommentNode) return 7 if node.is_a?(Dommy::ProcessingInstructionNode) return 10 if node.is_a?(Dommy::DocumentType) return 11 if node.is_a?(Dommy::Fragment) return 9 if node.is_a?(Dommy::Document) 0 end |
.presence(value) ⇒ Object
321 322 323 324 325 326 |
# File 'lib/dommy/internal/xml_serialization.rb', line 321 def presence(value) return nil if value.nil? s = value.to_s s.empty? ? nil : s end |
.record_namespace_information(attrs, map, local_prefixes) ⇒ Object
https://w3c.github.io/DOM-Parsing/#recording-the-namespace
Updates map/local_prefixes from the element's xmlns declarations and
returns the default-namespace value declared on the element (or nil).
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/dommy/internal/xml_serialization.rb', line 126 def record_namespace_information(attrs, map, local_prefixes) default_ns = nil attrs.each do |attr| next unless attr.namespace == XMLNS_NS if attr.prefix.nil? # xmlns="..." — a default namespace declaration. default_ns = attr.value next end prefix_def = attr.local_name ns_def = attr.value next if ns_def == XML_NS # An already-recorded (prefix → namespace) pairing is redundant. next if (map[ns_def] || []).include?(prefix_def) (map[ns_def] ||= []) << prefix_def local_prefixes[prefix_def] = ns_def end default_ns end |
.retrieve_preferred_prefix(map, ns, preferred) ⇒ Object
150 151 152 153 154 155 156 |
# File 'lib/dommy/internal/xml_serialization.rb', line 150 def retrieve_preferred_prefix(map, ns, preferred) candidates = map[ns.to_s] || map[ns] return nil if candidates.nil? || candidates.empty? return preferred if preferred && candidates.include?(preferred) candidates.last end |
.serialize(node) ⇒ Object
29 30 31 32 |
# File 'lib/dommy/internal/xml_serialization.rb', line 29 def serialize(node) prefix_index = [1] serialize_node(node, nil, { XML_NS => ["xml"] }, prefix_index) end |
.serialize_attributes(attrs, map, prefix_index, local_prefixes, ignore_ns_def) ⇒ Object
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 |
# File 'lib/dommy/internal/xml_serialization.rb', line 167 def serialize_attributes(attrs, map, prefix_index, local_prefixes, ignore_ns_def) result = +"" attrs.each do |attr| ns = presence(attr.namespace) prefix = nil if ns if ns == XMLNS_NS # A default-namespace declaration already emitted by the element. next if attr.prefix.nil? && ignore_ns_def # A prefixed declaration that the element already wrote out. next if attr.prefix && local_prefixes[attr.local_name] == attr.value && already_emitted_prefix?(attr) prefix = attr.prefix # "xmlns" for xmlns:foo, nil for xmlns elsif ns == XML_NS prefix = "xml" else candidate = retrieve_preferred_prefix(map, ns, presence(attr.prefix)) if candidate.nil? candidate = generate_prefix(map, ns, prefix_index) result << %( xmlns:#{candidate}="#{escape_attr(ns)}") end prefix = candidate end end result << " " result << "#{prefix}:" if prefix result << %(#{attr.local_name}="#{escape_attr(attr.value)}") end result end |
.serialize_children(node, context_ns, map, prefix_index) ⇒ Object
47 48 49 |
# File 'lib/dommy/internal/xml_serialization.rb', line 47 def serialize_children(node, context_ns, map, prefix_index) child_nodes(node).map { |c| serialize_node(c, context_ns, map, prefix_index) }.join end |
.serialize_doctype(node) ⇒ Object
303 304 305 306 307 308 309 310 311 312 313 314 315 |
# File 'lib/dommy/internal/xml_serialization.rb', line 303 def serialize_doctype(node) name = node.respond_to?(:name) ? node.name : node.__js_get__("name") public_id = node.__js_get__("publicId").to_s system_id = node.__js_get__("systemId").to_s out = +"<!DOCTYPE #{name}" if !public_id.empty? out << %( PUBLIC "#{public_id}") out << %( "#{system_id}") unless system_id.empty? elsif !system_id.empty? out << %( SYSTEM "#{system_id}") end out << ">" end |
.serialize_element(node, context_ns, map, prefix_index) ⇒ Object
52 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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/dommy/internal/xml_serialization.rb', line 52 def serialize_element(node, context_ns, map, prefix_index) map = copy_map(map) local_prefixes = {} attrs = element_attributes(node) local_default_ns = record_namespace_information(attrs, map, local_prefixes) inherited_ns = context_ns ns = presence(element_namespace(node)) ignore_ns_def = false markup = +"<" qualified = nil if inherited_ns == ns ignore_ns_def = true unless local_default_ns.nil? qualified = (ns == XML_NS ? "xml:" : "") + local_name(node) markup << qualified else prefix = presence(element_prefix(node)) candidate = retrieve_preferred_prefix(map, ns, prefix) if prefix == "xmlns" candidate = "xmlns" end if candidate && candidate != "xmlns" qualified = "#{candidate}:#{local_name(node)}" if local_default_ns && local_default_ns != XML_NS inherited_ns = local_default_ns.empty? ? nil : local_default_ns end markup << qualified elsif prefix prefix = generate_prefix(map, ns, prefix_index) if local_prefixes.key?(prefix) (map[ns] ||= []) << prefix qualified = "#{prefix}:#{local_name(node)}" markup << qualified << %( xmlns:#{prefix}="#{escape_attr(ns)}") inherited_ns = (local_default_ns.nil? || local_default_ns.empty? ? nil : local_default_ns) unless local_default_ns.nil? elsif local_default_ns.nil? || local_default_ns != ns.to_s ignore_ns_def = true qualified = local_name(node) inherited_ns = ns markup << qualified << %( xmlns="#{escape_attr(ns.to_s)}") else qualified = local_name(node) inherited_ns = ns markup << qualified end end markup << serialize_attributes(attrs, map, prefix_index, local_prefixes, ignore_ns_def) children = child_nodes(node) if children.empty? markup << empty_element_close(ns, node, qualified) return markup end markup << ">" children.each { |c| markup << serialize_node(c, inherited_ns, map, prefix_index) } markup << "</#{qualified}>" markup end |
.serialize_node(node, context_ns, map, prefix_index) ⇒ Object
34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/dommy/internal/xml_serialization.rb', line 34 def serialize_node(node, context_ns, map, prefix_index) case node_type(node) when 1 then serialize_element(node, context_ns, map, prefix_index) when 3 then escape_text(string_data(node)) when 4 then "<![CDATA[#{string_data(node)}]]>" when 7 then "<?#{node.target} #{string_data(node)}?>" when 8 then "<!--#{string_data(node)}-->" when 9, 11 then serialize_children(node, context_ns, map, prefix_index) when 10 then serialize_doctype(node) else "" end end |
.string_data(node) ⇒ Object
266 267 268 |
# File 'lib/dommy/internal/xml_serialization.rb', line 266 def string_data(node) node.respond_to?(:data) ? node.data.to_s : node.__js_get__("data").to_s end |