dommy 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/lib/dommy/animation.rb +4 -0
- data/lib/dommy/attr.rb +11 -5
- data/lib/dommy/backend/makiri_adapter.rb +330 -0
- data/lib/dommy/backend.rb +114 -33
- data/lib/dommy/blob.rb +2 -0
- data/lib/dommy/bridge.rb +11 -0
- data/lib/dommy/browser.rb +217 -0
- data/lib/dommy/compression_streams.rb +4 -0
- data/lib/dommy/crypto.rb +4 -0
- data/lib/dommy/css.rb +487 -50
- data/lib/dommy/custom_elements.rb +2 -2
- data/lib/dommy/data_transfer.rb +2 -0
- data/lib/dommy/data_uri.rb +35 -0
- data/lib/dommy/deferred_response.rb +59 -0
- data/lib/dommy/document.rb +386 -228
- data/lib/dommy/dom_exception.rb +2 -0
- data/lib/dommy/dom_parser.rb +7 -17
- data/lib/dommy/element.rb +502 -155
- data/lib/dommy/event.rb +240 -9
- data/lib/dommy/fetch.rb +152 -34
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +2 -0
- data/lib/dommy/html_canvas_element.rb +230 -0
- data/lib/dommy/html_collection.rb +5 -6
- data/lib/dommy/html_elements.rb +304 -27
- data/lib/dommy/interaction/debug.rb +35 -0
- data/lib/dommy/interaction/dom_summary.rb +131 -0
- data/lib/dommy/interaction/driver.rb +244 -0
- data/lib/dommy/interaction/event_synthesis.rb +56 -0
- data/lib/dommy/interaction/field_interactor.rb +117 -0
- data/lib/dommy/interaction/form_submission.rb +268 -0
- data/lib/dommy/interaction/locator.rb +158 -0
- data/lib/dommy/interaction/role_query.rb +58 -0
- data/lib/dommy/interaction.rb +32 -0
- data/lib/dommy/internal/accessibility_tree.rb +215 -0
- data/lib/dommy/internal/accessible_description.rb +38 -0
- data/lib/dommy/internal/accessible_name.rb +301 -0
- data/lib/dommy/internal/aria_role.rb +252 -0
- data/lib/dommy/internal/aria_snapshot.rb +64 -0
- data/lib/dommy/internal/aria_state.rb +151 -0
- data/lib/dommy/internal/css/calc.rb +242 -0
- data/lib/dommy/internal/css/cascade.rb +430 -0
- data/lib/dommy/internal/css/color.rb +381 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +130 -0
- data/lib/dommy/internal/css/counters.rb +227 -0
- data/lib/dommy/internal/css/custom_properties.rb +183 -0
- data/lib/dommy/internal/css/media_query.rb +302 -0
- data/lib/dommy/internal/css/parser.rb +265 -0
- data/lib/dommy/internal/css/property_registry.rb +512 -0
- data/lib/dommy/internal/css/rule_index.rb +494 -0
- data/lib/dommy/internal/css/supports.rb +158 -0
- data/lib/dommy/internal/css/ua_stylesheet.rb +53 -0
- data/lib/dommy/internal/css_pseudo_handlers.rb +283 -42
- data/lib/dommy/internal/css_rule_text.rb +160 -0
- data/lib/dommy/internal/dom_matching.rb +80 -9
- data/lib/dommy/internal/element_matching.rb +109 -0
- data/lib/dommy/internal/global_functions.rb +33 -0
- data/lib/dommy/internal/mutation_coordinator.rb +95 -4
- data/lib/dommy/internal/namespaces.rb +49 -5
- data/lib/dommy/internal/node_wrapper_cache.rb +163 -26
- data/lib/dommy/internal/parent_node.rb +82 -5
- data/lib/dommy/internal/selector_ast.rb +124 -0
- data/lib/dommy/internal/selector_index.rb +146 -0
- data/lib/dommy/internal/selector_matcher.rb +756 -0
- data/lib/dommy/internal/selector_parser.rb +283 -131
- data/lib/dommy/internal/shadow_root_registry.rb +9 -2
- data/lib/dommy/internal/template_content_registry.rb +26 -18
- data/lib/dommy/internal/xml_serialization.rb +344 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/js/bridge_conformance.rb +80 -0
- data/lib/dommy/js/constructor_resolver.rb +44 -0
- data/lib/dommy/js/custom_element_bridge.rb +90 -0
- data/lib/dommy/js/dom_interfaces.rb +162 -0
- data/lib/dommy/js/handle_table.rb +60 -0
- data/lib/dommy/js/host_bridge.rb +517 -0
- data/lib/dommy/js/host_runtime.js +1495 -0
- data/lib/dommy/js/import_map.rb +58 -0
- data/lib/dommy/js/marshaller.rb +240 -0
- data/lib/dommy/js/module_loader.rb +99 -0
- data/lib/dommy/js/observable_runtime.js +742 -0
- data/lib/dommy/js/runtime.rb +115 -0
- data/lib/dommy/js/script_boot.rb +221 -0
- data/lib/dommy/js/wire_tags.rb +62 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +50 -14
- data/lib/dommy/message_channel.rb +22 -6
- data/lib/dommy/minitest/assertions.rb +27 -0
- data/lib/dommy/mutation_observer.rb +89 -4
- data/lib/dommy/navigator.rb +34 -2
- data/lib/dommy/node.rb +24 -14
- data/lib/dommy/notification.rb +2 -0
- data/lib/dommy/parser.rb +1 -1
- data/lib/dommy/performance.rb +21 -1
- data/lib/dommy/promise.rb +94 -10
- data/lib/dommy/range.rb +173 -31
- data/lib/dommy/resources.rb +178 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +126 -0
- data/lib/dommy/scheduler.rb +149 -13
- data/lib/dommy/screen.rb +91 -0
- data/lib/dommy/shadow_root.rb +76 -13
- data/lib/dommy/storage.rb +2 -1
- data/lib/dommy/streams.rb +6 -0
- data/lib/dommy/text_codec.rb +7 -1
- data/lib/dommy/tree_walker.rb +33 -10
- data/lib/dommy/url.rb +13 -1
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +199 -11
- data/lib/dommy/worker.rb +8 -4
- data/lib/dommy/xml_http_request.rb +47 -6
- data/lib/dommy.rb +36 -1
- metadata +86 -14
- data/lib/dommy/backend/nokogiri_adapter.rb +0 -127
- data/lib/dommy/backend/nokolexbor_adapter.rb +0 -117
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab840b2c863131b5f20d7be40f28028bec227fb490688b4b0f26157058c0ea68
|
|
4
|
+
data.tar.gz: a87acf4824167a08eae696de0ffe504cdf808d9085176e084b3e4f3d225e5c7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db29082c6b8b686eb762aa53f3ae213b40da69210732c99a53f15a1f18318a326fd6bcf4c65fcfa4abc27dc41bebff0b16164682b7d18e7fed7a44415a9bb7e6
|
|
7
|
+
data.tar.gz: 71669401a09832106ec301e0b2d4747fb7782b2ac2514fbf4cfbf625358695f0c498561cbd3efb565b7b2abbaf43b4b2068dbe5f1831523a74ec25dc2c2e1435
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Dommy
|
|
2
2
|
|
|
3
|
-
Dommy is a pure Ruby DOM polyfill built on [
|
|
3
|
+
Dommy is a pure Ruby DOM polyfill built on [Makiri](https://github.com/takahashim/makiri), inspired by happy-dom and jsdom.
|
|
4
4
|
It gives Ruby tests a browser style DOM with events, MutationObserver, Custom Elements, Shadow DOM, the File API, timers, and Storage, without requiring a real browser.
|
|
5
5
|
|
|
6
6
|
## Quick start
|
|
@@ -170,8 +170,8 @@ end
|
|
|
170
170
|
|
|
171
171
|
Supported Capybara-style options: `text:` / `exact:` / `count:` (Integer or Range) / `visible:` / `href:` / `with:` / `type:`. `wait:` is accepted and ignored (Dommy is synchronous).
|
|
172
172
|
|
|
173
|
-
> [!
|
|
174
|
-
> `:visible` is
|
|
173
|
+
> [!NOTE]
|
|
174
|
+
> `:visible` understands CSS stylesheets when the `makiri` gem is available (its bundled lexbor provides the CSS parser — the default backend already is makiri): `display: none` / `visibility: hidden` / `opacity: 0` set via a class in a `<style>` sheet is detected, on the element or an ancestor, alongside the HTML-level signals (`hidden` attribute, `<input type=hidden>`, non-rendering ancestors, inline styles). There is still **no layout engine**: geometry-dependent invisibility (zero size, off-screen, overlap) and media-query-conditional rules are not evaluated, and `getComputedStyle` returns computed (not used) values — `width: auto` stays `"auto"`. Without makiri, detection falls back to the HTML-level signals only.
|
|
175
175
|
|
|
176
176
|
## What's in scope
|
|
177
177
|
|
data/lib/dommy/animation.rb
CHANGED
data/lib/dommy/attr.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Dommy
|
|
|
8
8
|
#
|
|
9
9
|
# We represent two states:
|
|
10
10
|
# - "owned" — the Attr is attached to an Element. value reads/writes
|
|
11
|
-
# go through the element's
|
|
11
|
+
# go through the element's Makiri attribute slot.
|
|
12
12
|
# - "detached" — created via `document.createAttribute(name)` but
|
|
13
13
|
# not yet attached. Value is stored locally; `setAttributeNode`
|
|
14
14
|
# transfers it to an element.
|
|
@@ -33,11 +33,15 @@ module Dommy
|
|
|
33
33
|
@prefix = prefix
|
|
34
34
|
@local_name = (local_name || qname.split(":", 2).last).to_s
|
|
35
35
|
else
|
|
36
|
-
# Null-namespace
|
|
37
|
-
|
|
36
|
+
# Null-namespace attribute: use the qualified name verbatim. The casing is
|
|
37
|
+
# already decided by the caller — the backend stores `setAttribute` names
|
|
38
|
+
# lower-cased (HTML) but preserves `setAttributeNS("", "FOO")` as "FOO",
|
|
39
|
+
# and `createAttribute` lower-cases up front — so re-downcasing here would
|
|
40
|
+
# wrongly fold a case-preserved null-namespace attribute.
|
|
41
|
+
@name = qname
|
|
38
42
|
@namespace_uri = nil
|
|
39
43
|
@prefix = nil
|
|
40
|
-
@local_name = @name
|
|
44
|
+
@local_name = local_name ? local_name.to_s : @name
|
|
41
45
|
end
|
|
42
46
|
end
|
|
43
47
|
|
|
@@ -96,6 +100,8 @@ module Dommy
|
|
|
96
100
|
when "specified"
|
|
97
101
|
# Legacy/useless attribute — always true (WHATWG DOM).
|
|
98
102
|
true
|
|
103
|
+
else
|
|
104
|
+
Bridge::ABSENT
|
|
99
105
|
end
|
|
100
106
|
end
|
|
101
107
|
|
|
@@ -162,7 +168,7 @@ module Dommy
|
|
|
162
168
|
# `.getNamedItem(name)`, `.removeNamedItem(name)`, `.setNamedItem(attr)`,
|
|
163
169
|
# plus property-style access (`attributes.id`, `attributes.class`).
|
|
164
170
|
#
|
|
165
|
-
# NamedNodeMap is *live* — it re-reads the element's
|
|
171
|
+
# NamedNodeMap is *live* — it re-reads the element's Makiri
|
|
166
172
|
# attributes on every access so DOM mutations are reflected.
|
|
167
173
|
class NamedNodeMap
|
|
168
174
|
include Enumerable
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "makiri"
|
|
4
|
+
|
|
5
|
+
module Dommy
|
|
6
|
+
module Backend
|
|
7
|
+
# Makiri (Lexbor-based) backend. HTML5 parsing and CSS selectors via
|
|
8
|
+
# Lexbor, plus a native XPath 1.0 engine, with no libxml2 dependency.
|
|
9
|
+
# Makiri splits its document model into `Makiri::HTML::Document` (case-folding,
|
|
10
|
+
# html/head/body) and `Makiri::XML::Document` (case-preserving, namespaces,
|
|
11
|
+
# CDATA); both share the `Makiri::Document` / `Makiri::Node` bases used here
|
|
12
|
+
# for `is_a?` checks. HTML parses go through HTML::Document; `new Document()` /
|
|
13
|
+
# createDocument go through XML::Document.
|
|
14
|
+
module Makiri
|
|
15
|
+
# Class references for `is_a?` / type-checking (the shared bases, so both
|
|
16
|
+
# HTML and XML node subclasses match).
|
|
17
|
+
Element = ::Makiri::Element
|
|
18
|
+
Document = ::Makiri::Document
|
|
19
|
+
Text = ::Makiri::Text
|
|
20
|
+
Comment = ::Makiri::Comment
|
|
21
|
+
CDATASection = ::Makiri::CDATASection
|
|
22
|
+
ProcessingInstruction = ::Makiri::ProcessingInstruction
|
|
23
|
+
DocumentFragment = ::Makiri::DocumentFragment
|
|
24
|
+
Node = ::Makiri::Node
|
|
25
|
+
|
|
26
|
+
# A minimal namespace wrapper exposing the same `href` API that Nokogiri's
|
|
27
|
+
# Namespace object has, so calling code treats both backends uniformly.
|
|
28
|
+
Namespace = Struct.new(:href)
|
|
29
|
+
|
|
30
|
+
HTML_NAMESPACE_URI = "http://www.w3.org/1999/xhtml"
|
|
31
|
+
|
|
32
|
+
# Throwaway attribute used to bind `:scope` to a context element — Lexbor
|
|
33
|
+
# has no `:scope`, so a scoped query temporarily marks the element and
|
|
34
|
+
# rewrites `:scope` to an attribute selector, removing the mark after.
|
|
35
|
+
SCOPE_ATTR = "data-dommy-scope"
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# Makiri clones natively (import_node + template fixup), preserving the
|
|
40
|
+
# node's namespace and attributes and carrying <template> contents.
|
|
41
|
+
def clone_node(node, deep:)
|
|
42
|
+
node.clone_node(deep)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Makiri documents have no node-level clone; re-parsing the serialized
|
|
46
|
+
# document reproduces the full tree. Dispatch on the document kind so an XML
|
|
47
|
+
# document round-trips through the XML parser (case/namespaces/CDATA) and an
|
|
48
|
+
# HTML document through the HTML parser.
|
|
49
|
+
def clone_document(doc)
|
|
50
|
+
if doc.is_a?(::Makiri::XML::Document)
|
|
51
|
+
::Makiri::XML::Document.parse(doc.to_xml)
|
|
52
|
+
else
|
|
53
|
+
::Makiri::HTML::Document.parse(doc.to_html)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A fresh, empty HTML-backed document (children dropped so it starts with no
|
|
58
|
+
# documentElement). The backing for a shallow clone of an HTML document.
|
|
59
|
+
def empty_document
|
|
60
|
+
doc = ::Makiri::HTML::Document.parse("")
|
|
61
|
+
doc.children.to_a.each(&:unlink)
|
|
62
|
+
doc
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# A fresh, empty XML-backed document — the backing for `new Document()` /
|
|
66
|
+
# createDocument, which the DOM defines as XML documents. An XML backing
|
|
67
|
+
# gives them case preservation, real CDATA nodes (nodeType 4) and namespace
|
|
68
|
+
# tracking. Makiri's cross-kind `import_node` translates between the HTML and
|
|
69
|
+
# XML node representations, so a node created here can still be adopted into
|
|
70
|
+
# the main HTML tree (and vice versa) — which is why an XML backing no longer
|
|
71
|
+
# blocks the cross-tree inserts WPT performs.
|
|
72
|
+
def empty_xml_document
|
|
73
|
+
::Makiri::XML::Document.new
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# An empty document matching `doc`'s kind, for a shallow document clone
|
|
77
|
+
# (cloneNode(false) on a document must keep the same flavor).
|
|
78
|
+
def empty_document_like(doc)
|
|
79
|
+
doc.is_a?(::Makiri::XML::Document) ? empty_xml_document : empty_document
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Lexbor seeds even an empty parse with an <html> shell and has no `root=`,
|
|
83
|
+
# so clear the existing children before adopting `node` as the root.
|
|
84
|
+
def set_document_root(doc, node)
|
|
85
|
+
doc.children.to_a.each(&:unlink)
|
|
86
|
+
doc.add_child(node)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Makiri can't move a node between document arenas, so adoption imports a
|
|
90
|
+
# detached copy owned by `target_doc`. The caller reseats the wrapper onto
|
|
91
|
+
# this returned node, preserving Dommy-level identity.
|
|
92
|
+
#
|
|
93
|
+
# Gap absorption: Lexbor's HTML serializer has no CDATA case (it errors on a
|
|
94
|
+
# CDATA node) and Lexbor is a pinned upstream submodule, so Makiri's
|
|
95
|
+
# cross-kind import_node fails closed when bringing an XML CDATASection into
|
|
96
|
+
# an HTML document. Rather than let that surface as an error, degrade the
|
|
97
|
+
# node to a text node carrying the same data — the data a spec-faithful HTML
|
|
98
|
+
# serializer emits anyway (CDATASection is a Text subtype). The caller
|
|
99
|
+
# reseats the Dommy CDATASection wrapper onto this node, so `nodeType`
|
|
100
|
+
# stays 4 at the DOM level; only the backing node (and thus serialization)
|
|
101
|
+
# is text. An XML target keeps a real CDATA node. This handles a directly
|
|
102
|
+
# adopted CDATA node (a CDATA descendant inside an adopted element subtree
|
|
103
|
+
# is rare, untested, and still fails closed in the backend).
|
|
104
|
+
def adopt(node, target_doc)
|
|
105
|
+
if node.is_a?(::Makiri::CDATASection) && !target_doc.is_a?(::Makiri::XML::Document)
|
|
106
|
+
return target_doc.create_text_node(node.text)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
target_doc.import_node(node, true)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Lexbor arenas can't move a node between documents — a foreign node must be
|
|
113
|
+
# imported (see #adopt) before insertion.
|
|
114
|
+
def moves_nodes_across_documents?
|
|
115
|
+
false
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# CSS query honoring Dommy's custom pseudo-classes. Lexbor handles
|
|
119
|
+
# `:disabled`/`:enabled`/`:checked` natively, so only `:scope` needs help:
|
|
120
|
+
# when `scope_node` is given and the selector uses `:scope`, bind it to
|
|
121
|
+
# that element via a temporary attribute.
|
|
122
|
+
def select_all(node, selector, scope_node: nil)
|
|
123
|
+
with_scope(selector, scope_node) { |sel| node.css(sel) }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def select_first(node, selector, scope_node: nil)
|
|
127
|
+
with_scope(selector, scope_node) { |sel| node.at_css(sel) }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def with_scope(selector, scope_node)
|
|
131
|
+
return yield(selector) unless scope_node && selector.include?(":scope")
|
|
132
|
+
|
|
133
|
+
scope_node[SCOPE_ATTR] = ""
|
|
134
|
+
begin
|
|
135
|
+
yield(selector.gsub(":scope", "[#{SCOPE_ATTR}]"))
|
|
136
|
+
ensure
|
|
137
|
+
scope_node.remove_attribute(SCOPE_ATTR)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Makiri hands out a fresh Ruby wrapper on each traversal, so object_id is
|
|
142
|
+
# not stable; pointer_id (the underlying lxb_dom_node_t pointer) is. Safe
|
|
143
|
+
# as an identity key because Makiri detaches but never frees nodes — the
|
|
144
|
+
# document arena owns them — so a live node's pointer is never recycled.
|
|
145
|
+
def identity_key(node)
|
|
146
|
+
node.pointer_id
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def parse(html)
|
|
150
|
+
::Makiri::HTML::Document.parse(html.to_s)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# XML parse (DOMParser `text/xml` / `application/xml`): a real XML document,
|
|
154
|
+
# so element/attribute case is preserved, namespaces are tracked, and CDATA
|
|
155
|
+
# round-trips. The parsed tree is self-contained (not mixed into the HTML
|
|
156
|
+
# tree), so the HTML/XML node-kind split doesn't bite here.
|
|
157
|
+
def parse_xml(xml)
|
|
158
|
+
::Makiri::XML::Document.parse(xml.to_s)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def fragment(html, owner_doc:)
|
|
162
|
+
::Makiri::DocumentFragment.parse(html.to_s)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def create_element(name, doc)
|
|
166
|
+
# Mint from the owning document so HTML docs lower-case the name and XML
|
|
167
|
+
# docs preserve its case.
|
|
168
|
+
doc.create_element(name)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def create_text(content, doc)
|
|
172
|
+
doc.create_text_node(content)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def create_comment(content, doc)
|
|
176
|
+
doc.create_comment(content)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# CDATASection (nodeType 4). A genuine XML document — including a
|
|
180
|
+
# `new Document()` / createDocument document, now XML-backed (see
|
|
181
|
+
# #empty_xml_document) — mints a real CDATA node and the XML serializer emits
|
|
182
|
+
# `<![CDATA[…]]>`. `Document#create_cdata_section` rejects HTML documents up
|
|
183
|
+
# front (NotSupportedError, per spec), so the text-node fallback below is a
|
|
184
|
+
# defensive guard for any non-XML backend that still slips through (Lexbor's
|
|
185
|
+
# HTML serializer raises on a native CDATA node, so a text node keeps
|
|
186
|
+
# serialization safe).
|
|
187
|
+
def create_cdata(content, doc)
|
|
188
|
+
if doc.is_a?(::Makiri::XML::Document)
|
|
189
|
+
doc.create_cdata(content)
|
|
190
|
+
else
|
|
191
|
+
doc.create_text_node(content)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# The backend class for a CDATA node, so the wrapper routes it to
|
|
196
|
+
# CDATASectionNode (it is a Text subtype, matched before Text).
|
|
197
|
+
def cdata_class
|
|
198
|
+
::Makiri::CDATASection
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Both Makiri document families (HTML and XML) mint a real PI node and
|
|
202
|
+
# serialize it (HTML as `<?target data>`, XML as `<?target data?>`), so PIs
|
|
203
|
+
# — unlike CDATA — need no HTML-document fallback.
|
|
204
|
+
def create_processing_instruction(target, data, doc)
|
|
205
|
+
doc.create_processing_instruction(target, data)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def processing_instruction_class
|
|
209
|
+
::Makiri::ProcessingInstruction
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Makiri doesn't track XML namespaces. We synthesize one for SVG by
|
|
213
|
+
# walking ancestors — necessary so `element_class_for` routes SVG
|
|
214
|
+
# tags to their specialized classes.
|
|
215
|
+
# The element's namespace, from Lexbor's own namespace tracking (HTML /
|
|
216
|
+
# SVG / MathML). nil for the HTML namespace, so Element#namespace_uri
|
|
217
|
+
# falls back to its HTML default (and the wrapper is allocated only for
|
|
218
|
+
# genuine foreign content).
|
|
219
|
+
def namespace_of(node)
|
|
220
|
+
return nil unless node.respond_to?(:namespace_uri)
|
|
221
|
+
|
|
222
|
+
uri = node.namespace_uri
|
|
223
|
+
return nil if uri.nil? || uri.empty? || uri == HTML_NAMESPACE_URI
|
|
224
|
+
|
|
225
|
+
Namespace.new(uri)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Bind a *prefixed* element's namespace so the prefix resolves. An XML
|
|
229
|
+
# document resolves an element's prefix from xmlns declarations at insertion
|
|
230
|
+
# time, so a prefixed element (createElementNS / createDocument with a
|
|
231
|
+
# qualified name like "foo:div") must carry an xmlns:prefix declaration or
|
|
232
|
+
# the insert fails with an unbound-prefix error. The namespaceURI itself is
|
|
233
|
+
# tracked on the Dommy wrapper, so the unprefixed case needs nothing here —
|
|
234
|
+
# and must add no attribute, lest a spurious xmlns surface in the DOM view
|
|
235
|
+
# (attributes/isEqualNode). An HTML (Lexbor) document tracks the namespace
|
|
236
|
+
# natively and needs no declaration either. (This was a blanket no-op back
|
|
237
|
+
# when new Document()/createDocument were HTML-backed; XML-backing them
|
|
238
|
+
# surfaced the prefixed-element gap.)
|
|
239
|
+
def add_namespace_definition(node, prefix, href)
|
|
240
|
+
return if prefix.nil? || prefix.empty?
|
|
241
|
+
return unless node.document.is_a?(::Makiri::XML::Document)
|
|
242
|
+
|
|
243
|
+
node["xmlns:#{prefix}"] = href.to_s
|
|
244
|
+
nil
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def namespace_definitions(_node)
|
|
248
|
+
# Makiri tracks no XML namespace declarations.
|
|
249
|
+
[]
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Lexbor keeps <template> contents in a separate content fragment rather
|
|
253
|
+
# than the normal child chain.
|
|
254
|
+
def template_content_nodes(node)
|
|
255
|
+
cf = node.respond_to?(:content_fragment) ? node.content_fragment : nil
|
|
256
|
+
cf ? cf.children.to_a : []
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# ----- Namespaced attributes -----
|
|
260
|
+
# Lexbor (Makiri >= 0.2) tracks the attribute's own namespace: set_attribute_ns
|
|
261
|
+
# records it (splitting prefix/local), and the attr node reports
|
|
262
|
+
# namespace_uri/prefix/local_name. So *AttributeNS matches on (namespace,
|
|
263
|
+
# local name) faithfully.
|
|
264
|
+
|
|
265
|
+
def get_attribute_ns(node, namespace, local_name)
|
|
266
|
+
attr_by_ns(node, namespace, local_name)&.value
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def has_attribute_ns?(node, namespace, local_name)
|
|
270
|
+
!attr_by_ns(node, namespace, local_name).nil?
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def set_attribute_ns(node, namespace, _prefix, _local_name, qualified_name, value)
|
|
274
|
+
node.set_attribute_ns(presence(namespace), qualified_name.to_s, value.to_s)
|
|
275
|
+
value.to_s
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def remove_attribute_ns(node, namespace, local_name)
|
|
279
|
+
# Remove by (namespace, local name) — removing by qualified name is
|
|
280
|
+
# ambiguous once same-name/different-namespace attributes coexist.
|
|
281
|
+
node.remove_attribute_ns(presence(namespace), local_name.to_s)
|
|
282
|
+
nil
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def attribute_ns_info(attr_node)
|
|
286
|
+
{
|
|
287
|
+
namespace_uri: presence(attr_node.namespace_uri),
|
|
288
|
+
prefix: presence(attr_node.prefix),
|
|
289
|
+
local_name: attr_node.local_name,
|
|
290
|
+
qualified_name: attr_node.name,
|
|
291
|
+
value: attr_node.value,
|
|
292
|
+
}
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def attribute_nodes(node)
|
|
296
|
+
node.attribute_nodes
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Attribute node matching (namespace, local name) case-sensitively; a
|
|
300
|
+
# null/empty namespace matches a null-namespace attribute.
|
|
301
|
+
def attr_by_ns(node, namespace, local_name)
|
|
302
|
+
want_ns = presence(namespace)
|
|
303
|
+
want_local = local_name.to_s
|
|
304
|
+
node.attribute_nodes.find do |a|
|
|
305
|
+
a.local_name == want_local && presence(a.namespace_uri) == want_ns
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def presence(value)
|
|
310
|
+
return nil if value.nil?
|
|
311
|
+
|
|
312
|
+
s = value.to_s
|
|
313
|
+
s.empty? ? nil : s
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Internal helper — visible to allow testing.
|
|
317
|
+
def in_svg_subtree?(node)
|
|
318
|
+
return true if node.name.to_s.downcase == "svg"
|
|
319
|
+
|
|
320
|
+
current = node.parent
|
|
321
|
+
while current
|
|
322
|
+
return true if current.respond_to?(:name) && current.name.to_s.downcase == "svg"
|
|
323
|
+
current = current.respond_to?(:parent) ? current.parent : nil
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
false
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
data/lib/dommy/backend.rb
CHANGED
|
@@ -2,24 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module Dommy
|
|
4
4
|
# `Dommy::Backend` — pluggable HTML parser abstraction. Lets Dommy
|
|
5
|
-
# work with
|
|
6
|
-
# Nokolexbor (faster, HTML5-only). Internally, all DOM library
|
|
5
|
+
# work with Makiri (Lexbor-based, HTML5-only). Internally, all DOM library
|
|
7
6
|
# code goes through this facade rather than referencing the parser
|
|
8
7
|
# directly.
|
|
9
8
|
#
|
|
10
|
-
# Defaults to
|
|
9
|
+
# Defaults to Makiri.
|
|
11
10
|
#
|
|
12
11
|
# Switching backends:
|
|
13
12
|
#
|
|
14
13
|
# require "dommy"
|
|
15
|
-
# Dommy::Backend.use(:
|
|
14
|
+
# Dommy::Backend.use(:makiri)
|
|
16
15
|
#
|
|
17
16
|
# Or set directly:
|
|
18
17
|
#
|
|
19
|
-
# Dommy::Backend.current = Dommy::Backend::
|
|
18
|
+
# Dommy::Backend.current = Dommy::Backend::Makiri
|
|
20
19
|
#
|
|
21
20
|
# All adapters must implement the same interface — see
|
|
22
|
-
# `Backend::
|
|
21
|
+
# `Backend::Makiri` for the canonical reference.
|
|
23
22
|
module Backend
|
|
24
23
|
class BackendNotAvailable < StandardError
|
|
25
24
|
end
|
|
@@ -33,32 +32,100 @@ module Dommy
|
|
|
33
32
|
|
|
34
33
|
def use(name)
|
|
35
34
|
@current = case name.to_sym
|
|
36
|
-
when :
|
|
37
|
-
require_relative "backend/
|
|
38
|
-
|
|
39
|
-
when :nokolexbor
|
|
40
|
-
require_relative "backend/nokolexbor_adapter"
|
|
41
|
-
Nokolexbor
|
|
35
|
+
when :makiri
|
|
36
|
+
require_relative "backend/makiri_adapter"
|
|
37
|
+
Makiri
|
|
42
38
|
else
|
|
43
|
-
raise ArgumentError, "Unknown backend: #{name.inspect}. Use :
|
|
39
|
+
raise ArgumentError, "Unknown backend: #{name.inspect}. Use :makiri."
|
|
44
40
|
end
|
|
45
41
|
end
|
|
46
42
|
|
|
43
|
+
# Stable per-document identity key for a backend node, used to cache DOM
|
|
44
|
+
# wrappers and key per-node side tables. Makiri mints a fresh wrapper per traversal
|
|
45
|
+
# but never frees nodes (arena-owned), so it keys on the stable node pointer.
|
|
46
|
+
def identity_key(node)
|
|
47
|
+
current.identity_key(node)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Deep (or shallow) copy of an element/node, detached and owned by the
|
|
51
|
+
# same document — the backing for DOM cloneNode.
|
|
52
|
+
def clone_node(node, deep:)
|
|
53
|
+
current.clone_node(node, deep: deep)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Deep copy of a whole document (DOM cloneNode on the document).
|
|
57
|
+
def clone_document(doc)
|
|
58
|
+
current.clone_document(doc)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A fresh, empty HTML-backed document.
|
|
62
|
+
def empty_document
|
|
63
|
+
current.empty_document
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A fresh, empty XML-backed document — for `new Document()` / createDocument,
|
|
67
|
+
# which the DOM defines as XML documents (case-preserving, real CDATA
|
|
68
|
+
# nodeType, namespaces).
|
|
69
|
+
def empty_xml_document
|
|
70
|
+
current.empty_xml_document
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# An empty backing document matching `doc`'s kind (HTML stays HTML, XML stays
|
|
74
|
+
# XML) — for a shallow document clone, whose result keeps the source flavor.
|
|
75
|
+
def empty_document_like(doc)
|
|
76
|
+
current.empty_document_like(doc)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Bring `node` (already detached from its old tree) into `target_doc`,
|
|
80
|
+
# returning the backend node now owned by `target_doc`. Makiri can't move a node
|
|
81
|
+
# between arenas, so it imports a copy — callers must reseat any wrapper
|
|
82
|
+
# onto the returned node.
|
|
83
|
+
def adopt(node, target_doc)
|
|
84
|
+
current.adopt(node, target_doc)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Whether the backend can move a node between documents in place or must adopt a copy first
|
|
88
|
+
# (Lexbor's arenas can't move a node, so inserting a foreign node requires importing
|
|
89
|
+
# it). Lets callers skip a needless — and on an empty target, root-less and
|
|
90
|
+
# therefore crashing — adoption on backends that don't need it.
|
|
91
|
+
def moves_nodes_across_documents?
|
|
92
|
+
current.respond_to?(:moves_nodes_across_documents?) ? current.moves_nodes_across_documents? : true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# CSS query that honors Dommy's custom pseudo-classes
|
|
96
|
+
# (`:disabled`/`:enabled`/`:checked`/`:scope`). Each backend applies its
|
|
97
|
+
# own mechanism (Lexbor native pseudos plus a
|
|
98
|
+
# `:scope` rewrite). `scope_node` binds `:scope` to that element.
|
|
99
|
+
def select_all(node, selector, scope_node: nil)
|
|
100
|
+
current.select_all(node, selector, scope_node: scope_node)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def select_first(node, selector, scope_node: nil)
|
|
104
|
+
current.select_first(node, selector, scope_node: scope_node)
|
|
105
|
+
end
|
|
106
|
+
|
|
47
107
|
# Delegate calls so internal code can use `Backend.parse(...)`.
|
|
48
108
|
def parse(html)
|
|
49
109
|
current.parse(html)
|
|
50
110
|
end
|
|
51
111
|
|
|
52
|
-
# Parse XML input into an XML document.
|
|
53
|
-
#
|
|
112
|
+
# Parse XML input into an XML document.
|
|
113
|
+
# fall back to the HTML parser.
|
|
54
114
|
def parse_xml(xml)
|
|
55
|
-
current.
|
|
115
|
+
current.parse_xml(xml)
|
|
56
116
|
end
|
|
57
117
|
|
|
58
118
|
def fragment(html, owner_doc:)
|
|
59
119
|
current.fragment(html, owner_doc: owner_doc)
|
|
60
120
|
end
|
|
61
121
|
|
|
122
|
+
# Make `node` the sole document element of `doc` (used by
|
|
123
|
+
# DOMImplementation.createDocument). Lexbor-backed documents are seeded with an <html> shell
|
|
124
|
+
# that must be cleared first.
|
|
125
|
+
def set_document_root(doc, node)
|
|
126
|
+
current.set_document_root(doc, node)
|
|
127
|
+
end
|
|
128
|
+
|
|
62
129
|
def create_element(name, doc)
|
|
63
130
|
current.create_element(name, doc)
|
|
64
131
|
end
|
|
@@ -81,16 +148,40 @@ module Dommy
|
|
|
81
148
|
current.respond_to?(:cdata_class) ? current.cdata_class : nil
|
|
82
149
|
end
|
|
83
150
|
|
|
151
|
+
# ProcessingInstruction node (`<?target data?>`). Supported by every
|
|
152
|
+
# backend Dommy ships (both Makiri document families), so — unlike CDATA —
|
|
153
|
+
# there is no text-node fallback.
|
|
154
|
+
def create_processing_instruction(target, data, doc)
|
|
155
|
+
current.create_processing_instruction(target, data, doc)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def processing_instruction_class
|
|
159
|
+
current.processing_instruction_class
|
|
160
|
+
end
|
|
161
|
+
|
|
84
162
|
def namespace_of(node)
|
|
85
163
|
current.namespace_of(node)
|
|
86
164
|
end
|
|
87
165
|
|
|
166
|
+
# The element's in-scope namespace declarations (each responds to
|
|
167
|
+
# `prefix`/`href`). Empty on backends without an XML namespace model.
|
|
168
|
+
def namespace_definitions(node)
|
|
169
|
+
current.namespace_definitions(node)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The content child nodes of a `<template>` element. HTML5 parsers model
|
|
173
|
+
# template contents differently — Lexbor/Makiri in a separate content fragment — so reading them goes
|
|
174
|
+
# through the backend. Used by the template-content registry's migration.
|
|
175
|
+
def template_content_nodes(node)
|
|
176
|
+
current.template_content_nodes(node)
|
|
177
|
+
end
|
|
178
|
+
|
|
88
179
|
def add_namespace_definition(node, prefix, href)
|
|
89
180
|
current.add_namespace_definition(node, prefix, href)
|
|
90
181
|
end
|
|
91
182
|
|
|
92
183
|
# Namespaced attribute access (DOM *AttributeNS). `namespace` is an href
|
|
93
|
-
# String or nil.
|
|
184
|
+
# String or nil.
|
|
94
185
|
def get_attribute_ns(node, namespace, local_name)
|
|
95
186
|
current.get_attribute_ns(node, namespace, local_name)
|
|
96
187
|
end
|
|
@@ -148,25 +239,15 @@ module Dommy
|
|
|
148
239
|
private
|
|
149
240
|
|
|
150
241
|
def detect_default
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
raise(BackendNotAvailable, "Dommy requires either 'nokogiri' or 'nokolexbor' gem to be installed.")
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def try_nokogiri
|
|
157
|
-
require "nokogiri"
|
|
158
|
-
|
|
159
|
-
require_relative "backend/nokogiri_adapter"
|
|
160
|
-
Nokogiri
|
|
161
|
-
rescue LoadError
|
|
162
|
-
nil
|
|
242
|
+
try_makiri ||
|
|
243
|
+
raise(BackendNotAvailable, "Dommy requires either 'makiri' gem to be installed.")
|
|
163
244
|
end
|
|
164
245
|
|
|
165
|
-
def
|
|
166
|
-
require "
|
|
246
|
+
def try_makiri
|
|
247
|
+
require "makiri"
|
|
167
248
|
|
|
168
|
-
require_relative "backend/
|
|
169
|
-
|
|
249
|
+
require_relative "backend/makiri_adapter"
|
|
250
|
+
Makiri
|
|
170
251
|
rescue LoadError
|
|
171
252
|
nil
|
|
172
253
|
end
|
data/lib/dommy/blob.rb
CHANGED
data/lib/dommy/bridge.rb
CHANGED
|
@@ -37,6 +37,17 @@ module Dommy
|
|
|
37
37
|
def UNDEFINED.inspect = "#<Dommy::Bridge::UNDEFINED>"
|
|
38
38
|
UNDEFINED.freeze
|
|
39
39
|
|
|
40
|
+
# The sentinel a `__js_get__` returns for a GENUINELY-ABSENT property (a key
|
|
41
|
+
# the object does not have), as distinct from a present property whose value
|
|
42
|
+
# is `nil`/JS null or `UNDEFINED`/JS undefined. It marshals to JS `undefined`
|
|
43
|
+
# for the value, but the proxy reports `("x" in obj) === false` for it — so
|
|
44
|
+
# feature detection like `isUndefined(window.Vue)` AND `"Vue" in window` are
|
|
45
|
+
# both correct. (UNDEFINED means present-but-undefined → reported present.)
|
|
46
|
+
ABSENT = Object.new
|
|
47
|
+
def ABSENT.to_s = "undefined"
|
|
48
|
+
def ABSENT.inspect = "#<Dommy::Bridge::ABSENT>"
|
|
49
|
+
ABSENT.freeze
|
|
50
|
+
|
|
40
51
|
# An opaque handle to a JS-side value that Ruby only stores and hands back
|
|
41
52
|
# (an AbortSignal's reason, a CustomEvent's detail). A non-plain JS object
|
|
42
53
|
# (Error, class instance, …) crosses as one of these instead of being
|