Module: Dommy::Backend::Makiri
- Defined in:
- lib/dommy/backend/makiri_adapter.rb
Overview
Makiri (Lexbor-based) backend. HTML5 parsing and CSS selectors via
Lexbor, plus a native XPath 1.0 engine, with no libxml2 dependency.
Makiri splits its document model into Makiri::HTML::Document (case-folding,
html/head/body) and Makiri::XML::Document (case-preserving, namespaces,
CDATA); both share the Makiri::Document / Makiri::Node bases used here
for is_a? checks. HTML parses go through HTML::Document; new Document() /
createDocument go through XML::Document.
Defined Under Namespace
Classes: Namespace
Constant Summary collapse
- Element =
Class references for
is_a?/ type-checking (the shared bases, so both HTML and XML node subclasses match). ::Makiri::Element
- Document =
::Makiri::Document
- Text =
::Makiri::Text
- Comment =
::Makiri::Comment
- CDATASection =
::Makiri::CDATASection
- ProcessingInstruction =
::Makiri::ProcessingInstruction
- DocumentFragment =
::Makiri::DocumentFragment
- DocumentType =
::Makiri::DocumentType
- Node =
::Makiri::Node
- HTML_NAMESPACE_URI =
"http://www.w3.org/1999/xhtml"- SCOPE_ATTR =
Throwaway attribute used to bind
:scopeto a context element — Lexbor has no:scope, so a scoped query temporarily marks the element and rewrites:scopeto an attribute selector, removing the mark after. "data-dommy-scope"
Class Method Summary collapse
-
.add_namespace_definition(node, prefix, href) ⇒ Object
Bind a prefixed element's namespace so the prefix resolves.
-
.adopt(node, target_doc) ⇒ Object
Makiri can't move a node between document arenas, so adoption imports a detached copy owned by
target_doc. -
.attr_by_ns(node, namespace, local_name) ⇒ Object
Attribute node matching (namespace, local name) case-sensitively; a null/empty namespace matches a null-namespace attribute.
- .attribute_nodes(node) ⇒ Object
- .attribute_ns_info(attr_node) ⇒ Object
-
.cdata_class ⇒ Object
The backend class for a CDATA node, so the wrapper routes it to CDATASectionNode (it is a Text subtype, matched before Text).
-
.clone_document(doc) ⇒ Object
Makiri documents have no node-level clone; re-parsing the serialized document reproduces the full tree.
-
.clone_node(node, deep:) ⇒ Object
Makiri clones natively (import_node + template fixup), preserving the node's namespace and attributes and carrying contents.
-
.create_cdata(content, doc) ⇒ Object
CDATASection (nodeType 4).
- .create_comment(content, doc) ⇒ Object
-
.create_document_type(name, public_id, system_id, doc) ⇒ Object
A detached DocumentType node owned by
doc, for DOMImplementation.createDocumentType. - .create_element(name, doc) ⇒ Object
-
.create_element_loose(qualified_name, prefix, local, namespace, doc) ⇒ Object
An XML-backed document rejects a qualified name that DOM allows (e.g. "f}oo" — an invalid char in the local part), so createElementNS uses Makiri's loose creator, which builds it verbatim (case/prefix preserved).
-
.create_processing_instruction(target, data, doc) ⇒ Object
Both Makiri document families (HTML and XML) mint a real PI node and serialize it (HTML as
<?target data>, XML as<?target data?>), so PIs — unlike CDATA — need no HTML-document fallback. - .create_text(content, doc) ⇒ Object
-
.document_type_class ⇒ Object
The backend class for a DocumentType node, so the wrapper routes it to Dommy::DocumentType (node-backed).
-
.empty_document ⇒ Object
A fresh, empty HTML-backed document (children dropped so it starts with no documentElement).
-
.empty_document_like(doc) ⇒ Object
An empty document matching
doc's kind, for a shallow document clone (cloneNode(false) on a document must keep the same flavor). -
.empty_xml_document ⇒ Object
A fresh, empty XML-backed document — the backing for
new Document()/ createDocument, which the DOM defines as XML documents. - .fragment(html, owner_doc:) ⇒ Object
-
.get_attribute_ns(node, namespace, local_name) ⇒ Object
----- Namespaced attributes ----- Lexbor (Makiri >= 0.2) tracks the attribute's own namespace: set_attribute_ns records it (splitting prefix/local), and the attr node reports namespace_uri/prefix/local_name.
- .has_attribute_ns?(node, namespace, local_name) ⇒ Boolean
-
.identity_key(node) ⇒ Object
Makiri hands out a fresh Ruby wrapper on each traversal, so object_id is not stable; pointer_id (the underlying lxb_dom_node_t pointer) is.
-
.in_svg_subtree?(node) ⇒ Boolean
Internal helper — visible to allow testing.
-
.internal_subset(doc) ⇒ Object
The parsed document's DocumentType node (
<!DOCTYPE …>), or nil when the document declares none. -
.moves_nodes_across_documents? ⇒ Boolean
Lexbor arenas can't move a node between documents — a foreign node must be imported (see #adopt) before insertion.
- .namespace_definitions(_node) ⇒ Object
-
.namespace_of(node) ⇒ Object
Makiri doesn't track XML namespaces.
- .parse(html) ⇒ Object
-
.parse_xml(xml) ⇒ Object
XML parse (DOMParser
text/xml/application/xml): a real XML document, so element/attribute case is preserved, namespaces are tracked, and CDATA round-trips. - .presence(value) ⇒ Object
- .processing_instruction_class ⇒ Object
- .remove_attribute_ns(node, namespace, local_name) ⇒ Object
-
.select_all(node, selector, scope_node: nil) ⇒ Object
CSS query honoring Dommy's custom pseudo-classes.
- .select_first(node, selector, scope_node: nil) ⇒ Object
- .set_attribute_ns(node, namespace, _prefix, _local_name, qualified_name, value) ⇒ Object
-
.set_document_root(doc, node) ⇒ Object
Lexbor seeds even an empty parse with an shell and has no
root=, so clear the existing children before adoptingnodeas the root. -
.template_content_nodes(node) ⇒ Object
Lexbor keeps contents in a separate content fragment rather than the normal child chain.
- .with_scope(selector, scope_node) ⇒ Object
Class Method Details
.add_namespace_definition(node, prefix, href) ⇒ Object
Bind a prefixed element's namespace so the prefix resolves. An XML document resolves an element's prefix from xmlns declarations at insertion time, so a prefixed element (createElementNS / createDocument with a qualified name like "foo:div") must carry an xmlns:prefix declaration or the insert fails with an unbound-prefix error. The namespaceURI itself is tracked on the Dommy wrapper, so the unprefixed case needs nothing here — and must add no attribute, lest a spurious xmlns surface in the DOM view (attributes/isEqualNode). An HTML (Lexbor) document tracks the namespace natively and needs no declaration either. (This was a blanket no-op back when new Document()/createDocument were HTML-backed; XML-backing them surfaced the prefixed-element gap.)
273 274 275 276 277 278 279 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 273 def add_namespace_definition(node, prefix, href) return if prefix.nil? || prefix.empty? return unless node.document.is_a?(::Makiri::XML::Document) node["xmlns:#{prefix}"] = href.to_s nil end |
.adopt(node, target_doc) ⇒ Object
Makiri can't move a node between document arenas, so adoption imports a
detached copy owned by target_doc. The caller reseats the wrapper onto
this returned node, preserving Dommy-level identity.
Gap absorption: Lexbor's HTML serializer has no CDATA case (it errors on a
CDATA node) and Lexbor is a pinned upstream submodule, so Makiri's
cross-kind import_node fails closed when bringing an XML CDATASection into
an HTML document. Rather than let that surface as an error, degrade the
node to a text node carrying the same data — the data a spec-faithful HTML
serializer emits anyway (CDATASection is a Text subtype). The caller
reseats the Dommy CDATASection wrapper onto this node, so nodeType
stays 4 at the DOM level; only the backing node (and thus serialization)
is text. An XML target keeps a real CDATA node. This handles a directly
adopted CDATA node (a CDATA descendant inside an adopted element subtree
is rare, untested, and still fails closed in the backend).
105 106 107 108 109 110 111 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 105 def adopt(node, target_doc) if node.is_a?(::Makiri::CDATASection) && !target_doc.is_a?(::Makiri::XML::Document) return target_doc.create_text_node(node.text) end target_doc.import_node(node, true) end |
.attr_by_ns(node, namespace, local_name) ⇒ Object
Attribute node matching (namespace, local name) case-sensitively; a null/empty namespace matches a null-namespace attribute.
335 336 337 338 339 340 341 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 335 def attr_by_ns(node, namespace, local_name) want_ns = presence(namespace) want_local = local_name.to_s node.attribute_nodes.find do |a| a.local_name == want_local && presence(a.namespace_uri) == want_ns end end |
.attribute_nodes(node) ⇒ Object
329 330 331 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 329 def attribute_nodes(node) node.attribute_nodes end |
.attribute_ns_info(attr_node) ⇒ Object
319 320 321 322 323 324 325 326 327 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 319 def attribute_ns_info(attr_node) { namespace_uri: presence(attr_node.namespace_uri), prefix: presence(attr_node.prefix), local_name: attr_node.local_name, qualified_name: attr_node.name, value: attr_node.value, } end |
.cdata_class ⇒ Object
The backend class for a CDATA node, so the wrapper routes it to CDATASectionNode (it is a Text subtype, matched before Text).
231 232 233 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 231 def cdata_class ::Makiri::CDATASection end |
.clone_document(doc) ⇒ Object
Makiri documents have no node-level clone; re-parsing the serialized document reproduces the full tree. Dispatch on the document kind so an XML document round-trips through the XML parser (case/namespaces/CDATA) and an HTML document through the HTML parser.
50 51 52 53 54 55 56 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 50 def clone_document(doc) if doc.is_a?(::Makiri::XML::Document) ::Makiri::XML::Document.parse(doc.to_xml) else ::Makiri::HTML::Document.parse(doc.to_html) end end |
.clone_node(node, deep:) ⇒ Object
Makiri clones natively (import_node + template fixup), preserving the node's namespace and attributes and carrying contents.
42 43 44 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 42 def clone_node(node, deep:) node.clone_node(deep) end |
.create_cdata(content, doc) ⇒ Object
CDATASection (nodeType 4). A genuine XML document — including a
new Document() / createDocument document, now XML-backed (see
#empty_xml_document) — mints a real CDATA node and the XML serializer emits
<![CDATA[…]]>. Document#create_cdata_section rejects HTML documents up
front (NotSupportedError, per spec), so the text-node fallback below is a
defensive guard for any non-XML backend that still slips through (Lexbor's
HTML serializer raises on a native CDATA node, so a text node keeps
serialization safe).
221 222 223 224 225 226 227 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 221 def create_cdata(content, doc) if doc.is_a?(::Makiri::XML::Document) doc.create_cdata(content) else doc.create_text_node(content) end end |
.create_comment(content, doc) ⇒ Object
209 210 211 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 209 def create_comment(content, doc) doc.create_comment(content) end |
.create_document_type(name, public_id, system_id, doc) ⇒ Object
A detached DocumentType node owned by doc, for
DOMImplementation.createDocumentType. Only the HTML backend ships the
factory (create_document_type); nil signals the caller to fall back to a
synthetic (non-tree) DocumentType. Raises ArgumentError for a name the
factory rejects.
187 188 189 190 191 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 187 def create_document_type(name, public_id, system_id, doc) return nil unless doc.respond_to?(:create_document_type) doc.create_document_type(name.to_s, public_id.to_s, system_id.to_s) end |
.create_element(name, doc) ⇒ Object
166 167 168 169 170 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 166 def create_element(name, doc) # Mint from the owning document so HTML docs lower-case the name and XML # docs preserve its case. doc.create_element(name) end |
.create_element_loose(qualified_name, prefix, local, namespace, doc) ⇒ Object
An XML-backed document rejects a qualified name that DOM allows (e.g. "f}oo" — an invalid char in the local part), so createElementNS uses Makiri's loose creator, which builds it verbatim (case/prefix preserved). nil for a non-XML backend → the caller uses the strict #create_element.
176 177 178 179 180 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 176 def create_element_loose(qualified_name, prefix, local, namespace, doc) return nil unless doc.is_a?(::Makiri::XML::Document) && doc.respond_to?(:create_loose_dom_element) doc.create_loose_dom_element(qualified_name, prefix, local, namespace) end |
.create_processing_instruction(target, data, doc) ⇒ Object
Both Makiri document families (HTML and XML) mint a real PI node and
serialize it (HTML as <?target data>, XML as <?target data?>), so PIs
— unlike CDATA — need no HTML-document fallback.
238 239 240 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 238 def create_processing_instruction(target, data, doc) doc.create_processing_instruction(target, data) end |
.create_text(content, doc) ⇒ Object
205 206 207 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 205 def create_text(content, doc) doc.create_text_node(content) end |
.document_type_class ⇒ Object
The backend class for a DocumentType node, so the wrapper routes it to Dommy::DocumentType (node-backed).
201 202 203 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 201 def document_type_class ::Makiri::DocumentType end |
.empty_document ⇒ Object
A fresh, empty HTML-backed document (children dropped so it starts with no documentElement). The backing for a shallow clone of an HTML document.
60 61 62 63 64 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 60 def empty_document doc = ::Makiri::HTML::Document.parse("") doc.children.to_a.each(&:unlink) doc end |
.empty_document_like(doc) ⇒ Object
An empty document matching doc's kind, for a shallow document clone
(cloneNode(false) on a document must keep the same flavor).
79 80 81 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 79 def empty_document_like(doc) doc.is_a?(::Makiri::XML::Document) ? empty_xml_document : empty_document end |
.empty_xml_document ⇒ Object
A fresh, empty XML-backed document — the backing for new Document() /
createDocument, which the DOM defines as XML documents. An XML backing
gives them case preservation, real CDATA nodes (nodeType 4) and namespace
tracking. Makiri's cross-kind import_node translates between the HTML and
XML node representations, so a node created here can still be adopted into
the main HTML tree (and vice versa) — which is why an XML backing no longer
blocks the cross-tree inserts WPT performs.
73 74 75 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 73 def empty_xml_document ::Makiri::XML::Document.new end |
.fragment(html, owner_doc:) ⇒ Object
162 163 164 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 162 def fragment(html, owner_doc:) ::Makiri::DocumentFragment.parse(html.to_s) end |
.get_attribute_ns(node, namespace, local_name) ⇒ Object
----- Namespaced attributes ----- Lexbor (Makiri >= 0.2) tracks the attribute's own namespace: set_attribute_ns records it (splitting prefix/local), and the attr node reports namespace_uri/prefix/local_name. So *AttributeNS matches on (namespace, local name) faithfully.
299 300 301 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 299 def get_attribute_ns(node, namespace, local_name) attr_by_ns(node, namespace, local_name)&.value end |
.has_attribute_ns?(node, namespace, local_name) ⇒ Boolean
303 304 305 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 303 def has_attribute_ns?(node, namespace, local_name) !attr_by_ns(node, namespace, local_name).nil? end |
.identity_key(node) ⇒ Object
Makiri hands out a fresh Ruby wrapper on each traversal, so object_id is not stable; pointer_id (the underlying lxb_dom_node_t pointer) is. Safe as an identity key because Makiri detaches but never frees nodes — the document arena owns them — so a live node's pointer is never recycled.
146 147 148 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 146 def identity_key(node) node.pointer_id end |
.in_svg_subtree?(node) ⇒ Boolean
Internal helper — visible to allow testing.
351 352 353 354 355 356 357 358 359 360 361 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 351 def in_svg_subtree?(node) return true if node.name.to_s.downcase == "svg" current = node.parent while current return true if current.respond_to?(:name) && current.name.to_s.downcase == "svg" current = current.respond_to?(:parent) ? current.parent : nil end false end |
.internal_subset(doc) ⇒ Object
The parsed document's DocumentType node (<!DOCTYPE …>), or nil when the
document declares none.
195 196 197 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 195 def internal_subset(doc) doc.respond_to?(:internal_subset) ? doc.internal_subset : nil end |
.moves_nodes_across_documents? ⇒ Boolean
Lexbor arenas can't move a node between documents — a foreign node must be imported (see #adopt) before insertion.
115 116 117 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 115 def moves_nodes_across_documents? false end |
.namespace_definitions(_node) ⇒ Object
281 282 283 284 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 281 def namespace_definitions(_node) # Makiri tracks no XML namespace declarations. [] end |
.namespace_of(node) ⇒ Object
Makiri doesn't track XML namespaces. We synthesize one for SVG by
walking ancestors — necessary so element_class_for routes SVG
tags to their specialized classes.
The element's namespace, from Lexbor's own namespace tracking (HTML /
SVG / MathML). nil for the HTML namespace, so Element#namespace_uri
falls back to its HTML default (and the wrapper is allocated only for
genuine foreign content).
253 254 255 256 257 258 259 260 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 253 def namespace_of(node) return nil unless node.respond_to?(:namespace_uri) uri = node.namespace_uri return nil if uri.nil? || uri.empty? || uri == HTML_NAMESPACE_URI Namespace.new(uri) end |
.parse(html) ⇒ Object
150 151 152 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 150 def parse(html) ::Makiri::HTML::Document.parse(html.to_s) end |
.parse_xml(xml) ⇒ Object
XML parse (DOMParser text/xml / application/xml): a real XML document,
so element/attribute case is preserved, namespaces are tracked, and CDATA
round-trips. The parsed tree is self-contained (not mixed into the HTML
tree), so the HTML/XML node-kind split doesn't bite here.
158 159 160 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 158 def parse_xml(xml) ::Makiri::XML::Document.parse(xml.to_s) end |
.presence(value) ⇒ Object
343 344 345 346 347 348 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 343 def presence(value) return nil if value.nil? s = value.to_s s.empty? ? nil : s end |
.processing_instruction_class ⇒ Object
242 243 244 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 242 def processing_instruction_class ::Makiri::ProcessingInstruction end |
.remove_attribute_ns(node, namespace, local_name) ⇒ Object
312 313 314 315 316 317 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 312 def remove_attribute_ns(node, namespace, local_name) # Remove by (namespace, local name) — removing by qualified name is # ambiguous once same-name/different-namespace attributes coexist. node.remove_attribute_ns(presence(namespace), local_name.to_s) nil end |
.select_all(node, selector, scope_node: nil) ⇒ Object
CSS query honoring Dommy's custom pseudo-classes. Lexbor handles
:disabled/:enabled/:checked natively, so only :scope needs help:
when scope_node is given and the selector uses :scope, bind it to
that element via a temporary attribute.
123 124 125 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 123 def select_all(node, selector, scope_node: nil) with_scope(selector, scope_node) { |sel| node.css(sel) } end |
.select_first(node, selector, scope_node: nil) ⇒ Object
127 128 129 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 127 def select_first(node, selector, scope_node: nil) with_scope(selector, scope_node) { |sel| node.at_css(sel) } end |
.set_attribute_ns(node, namespace, _prefix, _local_name, qualified_name, value) ⇒ Object
307 308 309 310 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 307 def set_attribute_ns(node, namespace, _prefix, _local_name, qualified_name, value) node.set_attribute_ns(presence(namespace), qualified_name.to_s, value.to_s) value.to_s end |
.set_document_root(doc, node) ⇒ Object
Lexbor seeds even an empty parse with an shell and has no root=,
so clear the existing children before adopting node as the root.
85 86 87 88 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 85 def set_document_root(doc, node) doc.children.to_a.each(&:unlink) doc.add_child(node) end |
.template_content_nodes(node) ⇒ Object
Lexbor keeps contents in a separate content fragment rather than the normal child chain.
288 289 290 291 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 288 def template_content_nodes(node) cf = node.respond_to?(:content_fragment) ? node.content_fragment : nil cf ? cf.children.to_a : [] end |
.with_scope(selector, scope_node) ⇒ Object
131 132 133 134 135 136 137 138 139 140 |
# File 'lib/dommy/backend/makiri_adapter.rb', line 131 def with_scope(selector, scope_node) return yield(selector) unless scope_node && selector.include?(":scope") scope_node[SCOPE_ATTR] = "" begin yield(selector.gsub(":scope", "[#{SCOPE_ATTR}]")) ensure scope_node.remove_attribute(SCOPE_ATTR) end end |