dommy 0.8.1 → 0.10.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 +57 -13
- data/lib/dommy/backend/makiri_adapter.rb +364 -0
- data/lib/dommy/backend.rb +143 -33
- data/lib/dommy/blob.rb +50 -6
- data/lib/dommy/bridge.rb +11 -0
- data/lib/dommy/browser.rb +420 -0
- data/lib/dommy/callable_invoker.rb +6 -2
- 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 +901 -275
- data/lib/dommy/dom_exception.rb +2 -0
- data/lib/dommy/dom_parser.rb +7 -17
- data/lib/dommy/element.rb +940 -353
- data/lib/dommy/event.rb +529 -17
- data/lib/dommy/fetch.rb +577 -53
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +26 -3
- data/lib/dommy/html_canvas_element.rb +230 -0
- data/lib/dommy/html_collection.rb +150 -27
- data/lib/dommy/html_elements.rb +1968 -282
- data/lib/dommy/interaction/debug.rb +35 -0
- data/lib/dommy/interaction/dom_summary.rb +131 -0
- data/lib/dommy/interaction/driver.rb +385 -0
- data/lib/dommy/interaction/event_synthesis.rb +164 -0
- data/lib/dommy/interaction/field_interactor.rb +177 -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/child_node.rb +199 -0
- data/lib/dommy/internal/css/calc.rb +242 -0
- data/lib/dommy/internal/css/cascade.rb +472 -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 +55 -9
- data/lib/dommy/internal/mutation_coordinator.rb +122 -11
- data/lib/dommy/internal/namespaces.rb +49 -5
- data/lib/dommy/internal/node_wrapper_cache.rb +217 -38
- data/lib/dommy/internal/observer_manager.rb +6 -0
- data/lib/dommy/internal/observer_matcher.rb +6 -8
- data/lib/dommy/internal/parent_node.rb +98 -57
- 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 +845 -0
- data/lib/dommy/internal/selector_parser.rb +321 -132
- 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 +101 -0
- data/lib/dommy/js/dom_interfaces.rb +177 -0
- data/lib/dommy/js/handle_table.rb +60 -0
- data/lib/dommy/js/host_bridge.rb +538 -0
- data/lib/dommy/js/host_runtime.js +2357 -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 +301 -0
- data/lib/dommy/js/wire_tags.rb +62 -0
- data/lib/dommy/location.rb +78 -13
- 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 -5
- data/lib/dommy/navigation.rb +263 -0
- data/lib/dommy/navigator.rb +34 -2
- data/lib/dommy/node.rb +198 -35
- 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 +187 -33
- data/lib/dommy/resources.rb +178 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +126 -0
- data/lib/dommy/scheduler.rb +165 -13
- data/lib/dommy/screen.rb +91 -0
- data/lib/dommy/shadow_root.rb +109 -13
- data/lib/dommy/storage.rb +63 -11
- data/lib/dommy/streams.rb +6 -0
- data/lib/dommy/text_codec.rb +7 -1
- data/lib/dommy/tree_walker.rb +51 -46
- data/lib/dommy/url.rb +17 -3
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +45 -2
- data/lib/dommy/window.rb +322 -15
- data/lib/dommy/worker.rb +8 -4
- data/lib/dommy/xml_http_request.rb +76 -9
- data/lib/dommy.rb +38 -1
- metadata +82 -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: 52933caebd00a8d1b7dcfa14421116be45617e04d53ee6f8d8771c5fc5ccefdc
|
|
4
|
+
data.tar.gz: da2372f52dac931a6e7ad4316ec4cb15efb0effa799229eae7bde1435b8aa3c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e05358f558f5e84fe6da7e7f705dbdc5bdd178646ef71a3ee9311a9d05893320cda1c0e6ca3596aa05e7e389e9112e96c8d44b640b7dea55480066af40e65eb7
|
|
7
|
+
data.tar.gz: ac0dea6fecd2fe7624f71cdef6f9eab786be60a584a4307d0fcac66540002de7cc1cc52812a638bfc8bb35afb53dd836ed7ebb2b007fee8fabfd172e62e8850a
|
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.
|
|
@@ -22,9 +22,12 @@ module Dommy
|
|
|
22
22
|
nil
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def initialize(name, owner: nil, value: "", namespace_uri: nil, prefix: nil, local_name: nil)
|
|
25
|
+
def initialize(name, owner: nil, value: "", namespace_uri: nil, prefix: nil, local_name: nil, document: nil)
|
|
26
26
|
qname = name.to_s
|
|
27
27
|
@owner = owner
|
|
28
|
+
# The node document (for baseURI/ownerDocument when detached from an
|
|
29
|
+
# element). Owned attrs derive it from their owner instead.
|
|
30
|
+
@document = document
|
|
28
31
|
@detached_value = value.to_s
|
|
29
32
|
if namespace_uri && !namespace_uri.to_s.empty?
|
|
30
33
|
# Namespaced attributes preserve case and carry prefix / localName.
|
|
@@ -33,11 +36,15 @@ module Dommy
|
|
|
33
36
|
@prefix = prefix
|
|
34
37
|
@local_name = (local_name || qname.split(":", 2).last).to_s
|
|
35
38
|
else
|
|
36
|
-
# Null-namespace
|
|
37
|
-
|
|
39
|
+
# Null-namespace attribute: use the qualified name verbatim. The casing is
|
|
40
|
+
# already decided by the caller — the backend stores `setAttribute` names
|
|
41
|
+
# lower-cased (HTML) but preserves `setAttributeNS("", "FOO")` as "FOO",
|
|
42
|
+
# and `createAttribute` lower-cases up front — so re-downcasing here would
|
|
43
|
+
# wrongly fold a case-preserved null-namespace attribute.
|
|
44
|
+
@name = qname
|
|
38
45
|
@namespace_uri = nil
|
|
39
46
|
@prefix = nil
|
|
40
|
-
@local_name = @name
|
|
47
|
+
@local_name = local_name ? local_name.to_s : @name
|
|
41
48
|
end
|
|
42
49
|
end
|
|
43
50
|
|
|
@@ -46,6 +53,22 @@ module Dommy
|
|
|
46
53
|
@owner
|
|
47
54
|
end
|
|
48
55
|
|
|
56
|
+
# Node.baseURI — the node document's base URL. Derived from the owner
|
|
57
|
+
# element when attached, else the document the attr was created in.
|
|
58
|
+
def base_uri
|
|
59
|
+
return @owner.base_uri if @owner.respond_to?(:base_uri)
|
|
60
|
+
|
|
61
|
+
@document&.base_uri
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Node.ownerDocument — the owner element's current document when attached (so
|
|
65
|
+
# it follows the element across adoptNode), else the creation document.
|
|
66
|
+
def owner_document
|
|
67
|
+
return @owner.document if @owner.respond_to?(:document)
|
|
68
|
+
|
|
69
|
+
@document
|
|
70
|
+
end
|
|
71
|
+
|
|
49
72
|
def value
|
|
50
73
|
if @owner
|
|
51
74
|
if @namespace_uri
|
|
@@ -85,6 +108,8 @@ module Dommy
|
|
|
85
108
|
value
|
|
86
109
|
when "ownerElement"
|
|
87
110
|
@owner
|
|
111
|
+
when "ownerDocument"
|
|
112
|
+
owner_document
|
|
88
113
|
when "localName"
|
|
89
114
|
@local_name
|
|
90
115
|
when "namespaceURI"
|
|
@@ -93,9 +118,13 @@ module Dommy
|
|
|
93
118
|
@prefix
|
|
94
119
|
when "nodeType"
|
|
95
120
|
2
|
|
121
|
+
when "baseURI"
|
|
122
|
+
base_uri
|
|
96
123
|
when "specified"
|
|
97
124
|
# Legacy/useless attribute — always true (WHATWG DOM).
|
|
98
125
|
true
|
|
126
|
+
else
|
|
127
|
+
Bridge::ABSENT
|
|
99
128
|
end
|
|
100
129
|
end
|
|
101
130
|
|
|
@@ -113,12 +142,20 @@ module Dommy
|
|
|
113
142
|
include Bridge::Methods
|
|
114
143
|
js_methods %w[cloneNode isSameNode getRootNode hasChildNodes normalize compareDocumentPosition
|
|
115
144
|
appendChild insertBefore removeChild replaceChild
|
|
145
|
+
lookupNamespaceURI lookupPrefix isDefaultNamespace
|
|
116
146
|
addEventListener removeEventListener dispatchEvent]
|
|
117
147
|
def __js_call__(method, args)
|
|
118
148
|
case method
|
|
149
|
+
when "lookupNamespaceURI"
|
|
150
|
+
lookup_namespace_uri(args[0])
|
|
151
|
+
when "lookupPrefix"
|
|
152
|
+
lookup_prefix(args[0])
|
|
153
|
+
when "isDefaultNamespace"
|
|
154
|
+
is_default_namespace(args[0])
|
|
119
155
|
when "cloneNode"
|
|
120
156
|
Attr.new(@name, owner: nil, value: value,
|
|
121
|
-
namespace_uri: @namespace_uri, prefix: @prefix, local_name: @local_name
|
|
157
|
+
namespace_uri: @namespace_uri, prefix: @prefix, local_name: @local_name,
|
|
158
|
+
document: @document || (@owner.respond_to?(:document) ? @owner.document : nil))
|
|
122
159
|
when "isSameNode"
|
|
123
160
|
is_same_node(args[0])
|
|
124
161
|
when "getRootNode"
|
|
@@ -162,7 +199,7 @@ module Dommy
|
|
|
162
199
|
# `.getNamedItem(name)`, `.removeNamedItem(name)`, `.setNamedItem(attr)`,
|
|
163
200
|
# plus property-style access (`attributes.id`, `attributes.class`).
|
|
164
201
|
#
|
|
165
|
-
# NamedNodeMap is *live* — it re-reads the element's
|
|
202
|
+
# NamedNodeMap is *live* — it re-reads the element's Makiri
|
|
166
203
|
# attributes on every access so DOM mutations are reflected.
|
|
167
204
|
class NamedNodeMap
|
|
168
205
|
include Enumerable
|
|
@@ -203,7 +240,9 @@ module Dommy
|
|
|
203
240
|
end
|
|
204
241
|
|
|
205
242
|
def get_named_item(name)
|
|
206
|
-
|
|
243
|
+
# getNamedItem / getAttribute lowercase the qualified name only for an HTML
|
|
244
|
+
# element in an HTML document; other elements match case-sensitively.
|
|
245
|
+
key = @element.__internal_normalize_attr_key__(name)
|
|
207
246
|
node = Backend.attribute_nodes(@element.__dommy_backend_node__).find do |a|
|
|
208
247
|
Backend.attribute_ns_info(a)[:qualified_name] == key
|
|
209
248
|
end
|
|
@@ -215,7 +254,7 @@ module Dommy
|
|
|
215
254
|
end
|
|
216
255
|
|
|
217
256
|
def remove_named_item(name)
|
|
218
|
-
key = name
|
|
257
|
+
key = @element.__internal_normalize_attr_key__(name)
|
|
219
258
|
node = Backend.attribute_nodes(@element.__dommy_backend_node__).find do |a|
|
|
220
259
|
Backend.attribute_ns_info(a)[:qualified_name] == key
|
|
221
260
|
end
|
|
@@ -323,12 +362,17 @@ module Dommy
|
|
|
323
362
|
end
|
|
324
363
|
end
|
|
325
364
|
|
|
326
|
-
# WebIDL "supported property names" for NamedNodeMap:
|
|
327
|
-
#
|
|
365
|
+
# WebIDL "supported property names" for NamedNodeMap: each attribute's
|
|
366
|
+
# qualified name, in order with duplicates omitted (the indexed names are
|
|
367
|
+
# reflected separately). For an HTML element in an HTML document, names
|
|
368
|
+
# containing an ASCII upper alpha are excluded (they can't be reached by the
|
|
369
|
+
# case-insensitive named getter).
|
|
328
370
|
def __js_named_props__
|
|
329
|
-
Backend.attribute_nodes(@element.__dommy_backend_node__).map do |a|
|
|
371
|
+
names = Backend.attribute_nodes(@element.__dommy_backend_node__).map do |a|
|
|
330
372
|
Backend.attribute_ns_info(a)[:qualified_name]
|
|
331
|
-
end
|
|
373
|
+
end.uniq
|
|
374
|
+
names.reject! { |n| n.match?(/[A-Z]/) } unless @element.__internal_case_sensitive_attribute_names__?
|
|
375
|
+
names
|
|
332
376
|
end
|
|
333
377
|
|
|
334
378
|
include Bridge::Methods
|
|
@@ -0,0 +1,364 @@
|
|
|
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
|
+
DocumentType = ::Makiri::DocumentType
|
|
25
|
+
Node = ::Makiri::Node
|
|
26
|
+
|
|
27
|
+
# A minimal namespace wrapper exposing the same `href` API that Nokogiri's
|
|
28
|
+
# Namespace object has, so calling code treats both backends uniformly.
|
|
29
|
+
Namespace = Struct.new(:href)
|
|
30
|
+
|
|
31
|
+
HTML_NAMESPACE_URI = "http://www.w3.org/1999/xhtml"
|
|
32
|
+
|
|
33
|
+
# Throwaway attribute used to bind `:scope` to a context element — Lexbor
|
|
34
|
+
# has no `:scope`, so a scoped query temporarily marks the element and
|
|
35
|
+
# rewrites `:scope` to an attribute selector, removing the mark after.
|
|
36
|
+
SCOPE_ATTR = "data-dommy-scope"
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# Makiri clones natively (import_node + template fixup), preserving the
|
|
41
|
+
# node's namespace and attributes and carrying <template> contents.
|
|
42
|
+
def clone_node(node, deep:)
|
|
43
|
+
node.clone_node(deep)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Makiri documents have no node-level clone; re-parsing the serialized
|
|
47
|
+
# document reproduces the full tree. Dispatch on the document kind so an XML
|
|
48
|
+
# document round-trips through the XML parser (case/namespaces/CDATA) and an
|
|
49
|
+
# HTML document through the HTML parser.
|
|
50
|
+
def clone_document(doc)
|
|
51
|
+
if doc.is_a?(::Makiri::XML::Document)
|
|
52
|
+
::Makiri::XML::Document.parse(doc.to_xml)
|
|
53
|
+
else
|
|
54
|
+
::Makiri::HTML::Document.parse(doc.to_html)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A fresh, empty HTML-backed document (children dropped so it starts with no
|
|
59
|
+
# documentElement). The backing for a shallow clone of an HTML document.
|
|
60
|
+
def empty_document
|
|
61
|
+
doc = ::Makiri::HTML::Document.parse("")
|
|
62
|
+
doc.children.to_a.each(&:unlink)
|
|
63
|
+
doc
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A fresh, empty XML-backed document — the backing for `new Document()` /
|
|
67
|
+
# createDocument, which the DOM defines as XML documents. An XML backing
|
|
68
|
+
# gives them case preservation, real CDATA nodes (nodeType 4) and namespace
|
|
69
|
+
# tracking. Makiri's cross-kind `import_node` translates between the HTML and
|
|
70
|
+
# XML node representations, so a node created here can still be adopted into
|
|
71
|
+
# the main HTML tree (and vice versa) — which is why an XML backing no longer
|
|
72
|
+
# blocks the cross-tree inserts WPT performs.
|
|
73
|
+
def empty_xml_document
|
|
74
|
+
::Makiri::XML::Document.new
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# An empty document matching `doc`'s kind, for a shallow document clone
|
|
78
|
+
# (cloneNode(false) on a document must keep the same flavor).
|
|
79
|
+
def empty_document_like(doc)
|
|
80
|
+
doc.is_a?(::Makiri::XML::Document) ? empty_xml_document : empty_document
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Lexbor seeds even an empty parse with an <html> shell and has no `root=`,
|
|
84
|
+
# so clear the existing children before adopting `node` as the root.
|
|
85
|
+
def set_document_root(doc, node)
|
|
86
|
+
doc.children.to_a.each(&:unlink)
|
|
87
|
+
doc.add_child(node)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Makiri can't move a node between document arenas, so adoption imports a
|
|
91
|
+
# detached copy owned by `target_doc`. The caller reseats the wrapper onto
|
|
92
|
+
# this returned node, preserving Dommy-level identity.
|
|
93
|
+
#
|
|
94
|
+
# Gap absorption: Lexbor's HTML serializer has no CDATA case (it errors on a
|
|
95
|
+
# CDATA node) and Lexbor is a pinned upstream submodule, so Makiri's
|
|
96
|
+
# cross-kind import_node fails closed when bringing an XML CDATASection into
|
|
97
|
+
# an HTML document. Rather than let that surface as an error, degrade the
|
|
98
|
+
# node to a text node carrying the same data — the data a spec-faithful HTML
|
|
99
|
+
# serializer emits anyway (CDATASection is a Text subtype). The caller
|
|
100
|
+
# reseats the Dommy CDATASection wrapper onto this node, so `nodeType`
|
|
101
|
+
# stays 4 at the DOM level; only the backing node (and thus serialization)
|
|
102
|
+
# is text. An XML target keeps a real CDATA node. This handles a directly
|
|
103
|
+
# adopted CDATA node (a CDATA descendant inside an adopted element subtree
|
|
104
|
+
# is rare, untested, and still fails closed in the backend).
|
|
105
|
+
def adopt(node, target_doc)
|
|
106
|
+
if node.is_a?(::Makiri::CDATASection) && !target_doc.is_a?(::Makiri::XML::Document)
|
|
107
|
+
return target_doc.create_text_node(node.text)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
target_doc.import_node(node, true)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Lexbor arenas can't move a node between documents — a foreign node must be
|
|
114
|
+
# imported (see #adopt) before insertion.
|
|
115
|
+
def moves_nodes_across_documents?
|
|
116
|
+
false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# CSS query honoring Dommy's custom pseudo-classes. Lexbor handles
|
|
120
|
+
# `:disabled`/`:enabled`/`:checked` natively, so only `:scope` needs help:
|
|
121
|
+
# when `scope_node` is given and the selector uses `:scope`, bind it to
|
|
122
|
+
# that element via a temporary attribute.
|
|
123
|
+
def select_all(node, selector, scope_node: nil)
|
|
124
|
+
with_scope(selector, scope_node) { |sel| node.css(sel) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def select_first(node, selector, scope_node: nil)
|
|
128
|
+
with_scope(selector, scope_node) { |sel| node.at_css(sel) }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def with_scope(selector, scope_node)
|
|
132
|
+
return yield(selector) unless scope_node && selector.include?(":scope")
|
|
133
|
+
|
|
134
|
+
scope_node[SCOPE_ATTR] = ""
|
|
135
|
+
begin
|
|
136
|
+
yield(selector.gsub(":scope", "[#{SCOPE_ATTR}]"))
|
|
137
|
+
ensure
|
|
138
|
+
scope_node.remove_attribute(SCOPE_ATTR)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Makiri hands out a fresh Ruby wrapper on each traversal, so object_id is
|
|
143
|
+
# not stable; pointer_id (the underlying lxb_dom_node_t pointer) is. Safe
|
|
144
|
+
# as an identity key because Makiri detaches but never frees nodes — the
|
|
145
|
+
# document arena owns them — so a live node's pointer is never recycled.
|
|
146
|
+
def identity_key(node)
|
|
147
|
+
node.pointer_id
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def parse(html)
|
|
151
|
+
::Makiri::HTML::Document.parse(html.to_s)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# XML parse (DOMParser `text/xml` / `application/xml`): a real XML document,
|
|
155
|
+
# so element/attribute case is preserved, namespaces are tracked, and CDATA
|
|
156
|
+
# round-trips. The parsed tree is self-contained (not mixed into the HTML
|
|
157
|
+
# tree), so the HTML/XML node-kind split doesn't bite here.
|
|
158
|
+
def parse_xml(xml)
|
|
159
|
+
::Makiri::XML::Document.parse(xml.to_s)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def fragment(html, owner_doc:)
|
|
163
|
+
::Makiri::DocumentFragment.parse(html.to_s)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def create_element(name, doc)
|
|
167
|
+
# Mint from the owning document so HTML docs lower-case the name and XML
|
|
168
|
+
# docs preserve its case.
|
|
169
|
+
doc.create_element(name)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# An XML-backed document rejects a qualified name that DOM allows (e.g.
|
|
173
|
+
# "f}oo" — an invalid char in the local part), so createElementNS uses
|
|
174
|
+
# Makiri's loose creator, which builds it verbatim (case/prefix preserved).
|
|
175
|
+
# nil for a non-XML backend → the caller uses the strict #create_element.
|
|
176
|
+
def create_element_loose(qualified_name, prefix, local, namespace, doc)
|
|
177
|
+
return nil unless doc.is_a?(::Makiri::XML::Document) && doc.respond_to?(:create_loose_dom_element)
|
|
178
|
+
|
|
179
|
+
doc.create_loose_dom_element(qualified_name, prefix, local, namespace)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# A detached DocumentType node owned by `doc`, for
|
|
183
|
+
# DOMImplementation.createDocumentType. Only the HTML backend ships the
|
|
184
|
+
# factory (`create_document_type`); nil signals the caller to fall back to a
|
|
185
|
+
# synthetic (non-tree) DocumentType. Raises ArgumentError for a name the
|
|
186
|
+
# factory rejects.
|
|
187
|
+
def create_document_type(name, public_id, system_id, doc)
|
|
188
|
+
return nil unless doc.respond_to?(:create_document_type)
|
|
189
|
+
|
|
190
|
+
doc.create_document_type(name.to_s, public_id.to_s, system_id.to_s)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# The parsed document's DocumentType node (`<!DOCTYPE …>`), or nil when the
|
|
194
|
+
# document declares none.
|
|
195
|
+
def internal_subset(doc)
|
|
196
|
+
doc.respond_to?(:internal_subset) ? doc.internal_subset : nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# The backend class for a DocumentType node, so the wrapper routes it to
|
|
200
|
+
# Dommy::DocumentType (node-backed).
|
|
201
|
+
def document_type_class
|
|
202
|
+
::Makiri::DocumentType
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def create_text(content, doc)
|
|
206
|
+
doc.create_text_node(content)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def create_comment(content, doc)
|
|
210
|
+
doc.create_comment(content)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# CDATASection (nodeType 4). A genuine XML document — including a
|
|
214
|
+
# `new Document()` / createDocument document, now XML-backed (see
|
|
215
|
+
# #empty_xml_document) — mints a real CDATA node and the XML serializer emits
|
|
216
|
+
# `<![CDATA[…]]>`. `Document#create_cdata_section` rejects HTML documents up
|
|
217
|
+
# front (NotSupportedError, per spec), so the text-node fallback below is a
|
|
218
|
+
# defensive guard for any non-XML backend that still slips through (Lexbor's
|
|
219
|
+
# HTML serializer raises on a native CDATA node, so a text node keeps
|
|
220
|
+
# serialization safe).
|
|
221
|
+
def create_cdata(content, doc)
|
|
222
|
+
if doc.is_a?(::Makiri::XML::Document)
|
|
223
|
+
doc.create_cdata(content)
|
|
224
|
+
else
|
|
225
|
+
doc.create_text_node(content)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# The backend class for a CDATA node, so the wrapper routes it to
|
|
230
|
+
# CDATASectionNode (it is a Text subtype, matched before Text).
|
|
231
|
+
def cdata_class
|
|
232
|
+
::Makiri::CDATASection
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Both Makiri document families (HTML and XML) mint a real PI node and
|
|
236
|
+
# serialize it (HTML as `<?target data>`, XML as `<?target data?>`), so PIs
|
|
237
|
+
# — unlike CDATA — need no HTML-document fallback.
|
|
238
|
+
def create_processing_instruction(target, data, doc)
|
|
239
|
+
doc.create_processing_instruction(target, data)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def processing_instruction_class
|
|
243
|
+
::Makiri::ProcessingInstruction
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Makiri doesn't track XML namespaces. We synthesize one for SVG by
|
|
247
|
+
# walking ancestors — necessary so `element_class_for` routes SVG
|
|
248
|
+
# tags to their specialized classes.
|
|
249
|
+
# The element's namespace, from Lexbor's own namespace tracking (HTML /
|
|
250
|
+
# SVG / MathML). nil for the HTML namespace, so Element#namespace_uri
|
|
251
|
+
# falls back to its HTML default (and the wrapper is allocated only for
|
|
252
|
+
# genuine foreign content).
|
|
253
|
+
def namespace_of(node)
|
|
254
|
+
return nil unless node.respond_to?(:namespace_uri)
|
|
255
|
+
|
|
256
|
+
uri = node.namespace_uri
|
|
257
|
+
return nil if uri.nil? || uri.empty? || uri == HTML_NAMESPACE_URI
|
|
258
|
+
|
|
259
|
+
Namespace.new(uri)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Bind a *prefixed* element's namespace so the prefix resolves. An XML
|
|
263
|
+
# document resolves an element's prefix from xmlns declarations at insertion
|
|
264
|
+
# time, so a prefixed element (createElementNS / createDocument with a
|
|
265
|
+
# qualified name like "foo:div") must carry an xmlns:prefix declaration or
|
|
266
|
+
# the insert fails with an unbound-prefix error. The namespaceURI itself is
|
|
267
|
+
# tracked on the Dommy wrapper, so the unprefixed case needs nothing here —
|
|
268
|
+
# and must add no attribute, lest a spurious xmlns surface in the DOM view
|
|
269
|
+
# (attributes/isEqualNode). An HTML (Lexbor) document tracks the namespace
|
|
270
|
+
# natively and needs no declaration either. (This was a blanket no-op back
|
|
271
|
+
# when new Document()/createDocument were HTML-backed; XML-backing them
|
|
272
|
+
# surfaced the prefixed-element gap.)
|
|
273
|
+
def add_namespace_definition(node, prefix, href)
|
|
274
|
+
return if prefix.nil? || prefix.empty?
|
|
275
|
+
return unless node.document.is_a?(::Makiri::XML::Document)
|
|
276
|
+
|
|
277
|
+
node["xmlns:#{prefix}"] = href.to_s
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def namespace_definitions(_node)
|
|
282
|
+
# Makiri tracks no XML namespace declarations.
|
|
283
|
+
[]
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Lexbor keeps <template> contents in a separate content fragment rather
|
|
287
|
+
# than the normal child chain.
|
|
288
|
+
def template_content_nodes(node)
|
|
289
|
+
cf = node.respond_to?(:content_fragment) ? node.content_fragment : nil
|
|
290
|
+
cf ? cf.children.to_a : []
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# ----- Namespaced attributes -----
|
|
294
|
+
# Lexbor (Makiri >= 0.2) tracks the attribute's own namespace: set_attribute_ns
|
|
295
|
+
# records it (splitting prefix/local), and the attr node reports
|
|
296
|
+
# namespace_uri/prefix/local_name. So *AttributeNS matches on (namespace,
|
|
297
|
+
# local name) faithfully.
|
|
298
|
+
|
|
299
|
+
def get_attribute_ns(node, namespace, local_name)
|
|
300
|
+
attr_by_ns(node, namespace, local_name)&.value
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def has_attribute_ns?(node, namespace, local_name)
|
|
304
|
+
!attr_by_ns(node, namespace, local_name).nil?
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def set_attribute_ns(node, namespace, _prefix, _local_name, qualified_name, value)
|
|
308
|
+
node.set_attribute_ns(presence(namespace), qualified_name.to_s, value.to_s)
|
|
309
|
+
value.to_s
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def remove_attribute_ns(node, namespace, local_name)
|
|
313
|
+
# Remove by (namespace, local name) — removing by qualified name is
|
|
314
|
+
# ambiguous once same-name/different-namespace attributes coexist.
|
|
315
|
+
node.remove_attribute_ns(presence(namespace), local_name.to_s)
|
|
316
|
+
nil
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def attribute_ns_info(attr_node)
|
|
320
|
+
{
|
|
321
|
+
namespace_uri: presence(attr_node.namespace_uri),
|
|
322
|
+
prefix: presence(attr_node.prefix),
|
|
323
|
+
local_name: attr_node.local_name,
|
|
324
|
+
qualified_name: attr_node.name,
|
|
325
|
+
value: attr_node.value,
|
|
326
|
+
}
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def attribute_nodes(node)
|
|
330
|
+
node.attribute_nodes
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Attribute node matching (namespace, local name) case-sensitively; a
|
|
334
|
+
# null/empty namespace matches a null-namespace attribute.
|
|
335
|
+
def attr_by_ns(node, namespace, local_name)
|
|
336
|
+
want_ns = presence(namespace)
|
|
337
|
+
want_local = local_name.to_s
|
|
338
|
+
node.attribute_nodes.find do |a|
|
|
339
|
+
a.local_name == want_local && presence(a.namespace_uri) == want_ns
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def presence(value)
|
|
344
|
+
return nil if value.nil?
|
|
345
|
+
|
|
346
|
+
s = value.to_s
|
|
347
|
+
s.empty? ? nil : s
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Internal helper — visible to allow testing.
|
|
351
|
+
def in_svg_subtree?(node)
|
|
352
|
+
return true if node.name.to_s.downcase == "svg"
|
|
353
|
+
|
|
354
|
+
current = node.parent
|
|
355
|
+
while current
|
|
356
|
+
return true if current.respond_to?(:name) && current.name.to_s.downcase == "svg"
|
|
357
|
+
current = current.respond_to?(:parent) ? current.parent : nil
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
false
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|