dommy 0.6.0 → 0.8.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 +30 -38
- data/lib/dommy/animation.rb +10 -2
- data/lib/dommy/attr.rb +197 -32
- data/lib/dommy/backend/nokogiri_adapter.rb +127 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +117 -0
- data/lib/dommy/backend.rb +175 -0
- data/lib/dommy/blob.rb +30 -11
- data/lib/dommy/bridge/constructor_registry.rb +28 -0
- data/lib/dommy/bridge/methods.rb +57 -0
- data/lib/dommy/bridge.rb +97 -0
- data/lib/dommy/callable_invoker.rb +36 -0
- data/lib/dommy/compression_streams.rb +4 -4
- data/lib/dommy/cookie_store.rb +4 -2
- data/lib/dommy/crypto.rb +16 -9
- data/lib/dommy/css.rb +53 -7
- data/lib/dommy/custom_elements.rb +33 -9
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +693 -60
- data/lib/dommy/dom_parser.rb +29 -15
- data/lib/dommy/element.rb +1147 -438
- data/lib/dommy/event.rb +279 -79
- data/lib/dommy/event_source.rb +14 -10
- data/lib/dommy/fetch.rb +509 -39
- data/lib/dommy/file_reader.rb +14 -6
- data/lib/dommy/form_data.rb +3 -3
- data/lib/dommy/history.rb +46 -8
- data/lib/dommy/html_collection.rb +59 -6
- data/lib/dommy/html_elements.rb +153 -1502
- data/lib/dommy/internal/css_pseudo_handlers.rb +137 -0
- data/lib/dommy/internal/dom_matching.rb +3 -3
- data/lib/dommy/internal/global_functions.rb +26 -0
- data/lib/dommy/internal/idna.rb +16 -7
- data/lib/dommy/internal/ipv4_parser.rb +22 -7
- data/lib/dommy/internal/mutation_coordinator.rb +11 -2
- data/lib/dommy/internal/namespaces.rb +70 -0
- data/lib/dommy/internal/node_equality.rb +86 -0
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +77 -31
- data/lib/dommy/internal/observable_callback.rb +1 -5
- data/lib/dommy/internal/parent_node.rb +126 -0
- data/lib/dommy/internal/reflected_attributes.rb +103 -13
- data/lib/dommy/internal/selector_parser.rb +664 -0
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +4 -2
- data/lib/dommy/location.rb +10 -4
- data/lib/dommy/media_query_list.rb +10 -4
- data/lib/dommy/message_channel.rb +41 -11
- data/lib/dommy/mutation_observer.rb +76 -23
- data/lib/dommy/navigator.rb +38 -24
- data/lib/dommy/node.rb +158 -16
- data/lib/dommy/notification.rb +6 -4
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +4 -2
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +4 -2
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +31 -60
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +40 -49
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +178 -25
- data/lib/dommy/tree_walker.rb +270 -81
- data/lib/dommy/url.rb +305 -450
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +49 -19
- data/lib/dommy/window.rb +205 -203
- data/lib/dommy/worker.rb +12 -12
- data/lib/dommy/xml_http_request.rb +32 -7
- data/lib/dommy.rb +19 -2
- metadata +22 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae1783921d75a534741bfa2de3be6ddd7492f01a6d63d4a3102b85411518e64d
|
|
4
|
+
data.tar.gz: 3022f80e295f06db17782b034d252e8529435ccae9e7ce7283e936da2cea8804
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3ccc2658999f785bab29f01e94bca1d7c0505370256fb55687cca8822b0e4dcdd8efb1d64d172a3a7dced5a3a4224612bc38d8df48f943088d56bd81bc85cd9
|
|
7
|
+
data.tar.gz: dc65af8b414579d71d72601e1c1f3ac070ed6e4a67a7385cbc17222ed9b896687a494709f974f75af192d2441f12de3cc6be0f4eaaa263198f5bc868335193ab
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Dommy
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Dommy is a pure Ruby DOM polyfill built on [Nokogiri::HTML5](https://nokogiri.org/), inspired by happy-dom and jsdom.
|
|
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
|
|
7
7
|
|
|
@@ -74,22 +74,24 @@ input.validation_message #=> "Please fill out this field."
|
|
|
74
74
|
### File API (Blob / File / FormData / DataTransfer)
|
|
75
75
|
|
|
76
76
|
```ruby
|
|
77
|
+
win = Dommy.parse("<form><input type='file' name='attachment'></form>")
|
|
77
78
|
file = Dommy::File.new(["pdf body"], "doc.pdf", "type" => "application/pdf")
|
|
78
79
|
|
|
79
80
|
# Seed a file input for tests
|
|
80
|
-
input =
|
|
81
|
-
input.
|
|
81
|
+
input = win.document.query_selector("input[type='file']")
|
|
82
|
+
input.__driver_set_files__([file])
|
|
82
83
|
|
|
83
84
|
# FormData picks it up
|
|
84
|
-
fd = Dommy::FormData.new(
|
|
85
|
+
fd = Dommy::FormData.new(win.document.query_selector("form"))
|
|
85
86
|
fd.entries.to_a #=> [["attachment", #<Dommy::File doc.pdf>]]
|
|
86
87
|
|
|
87
88
|
# Drag-and-drop simulation
|
|
88
89
|
dt = Dommy::DataTransfer.new(files: [file])
|
|
89
90
|
ev = Dommy::DragEvent.new("drop", "dataTransfer" => dt, "bubbles" => true)
|
|
90
|
-
|
|
91
|
+
win.document.body.dispatch_event(ev)
|
|
91
92
|
|
|
92
93
|
# Blob URLs
|
|
94
|
+
blob = Dommy::Blob.new(["blob body"], "type" => "text/plain")
|
|
93
95
|
url = Dommy::URL.create_object_url(blob) # "blob:dommy/..."
|
|
94
96
|
```
|
|
95
97
|
|
|
@@ -102,7 +104,7 @@ response = win.__js_call__("fetch", ["/api"]).await
|
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
> [!WARNING]
|
|
105
|
-
> Most Dommy accessors (`Blob#text`, `
|
|
107
|
+
> Most Dommy accessors (`Blob#text`, `localStorage.get_item`) return synchronous Ruby values — not Promises. `.await` is only for the JS-bridged async surface (e.g., `fetch()`, `window.__js_call__`). Methods like `Response#text()` are Promise-returning and require `.await`.
|
|
106
108
|
|
|
107
109
|
## Test helpers
|
|
108
110
|
|
|
@@ -176,8 +178,7 @@ Supported Capybara-style options: `text:` / `exact:` / `count:` (Integer or Rang
|
|
|
176
178
|
Implemented:
|
|
177
179
|
|
|
178
180
|
- Core DOM (Document, Element, Text/Comment/Fragment, NodeList, Attr)
|
|
179
|
-
-
|
|
180
|
-
- SVG: SVGElement base + ~63 specialized subclasses covering shapes, gradients, marker / mask, the full set of standard filter primitives (Gaussian blur, offset, blend, color matrix, flood, composite, merge, component transfer, tile, morphology, image, drop shadow, turbulence, displacement map, convolve matrix, diffuse / specular lighting + light sources), `<a>` / `<textPath>` / `<view>` / `<switch>` / `<metadata>`, and SMIL animation (`<animate>` / `<animateTransform>` / `<animateMotion>` / `<set>` / `<mpath>` / `<discard>`) — with case-sensitive attribute round-trip
|
|
181
|
+
- Specialized HTML and SVG element classes
|
|
181
182
|
- events with composedPath / AbortSignal
|
|
182
183
|
- MutationObserver (childList / attributes / characterData / subtree)
|
|
183
184
|
- Custom Elements lifecycle
|
|
@@ -185,47 +186,38 @@ Implemented:
|
|
|
185
186
|
- form validation
|
|
186
187
|
- Scheduler (timers + microtasks with `advance_time`)
|
|
187
188
|
- Promise
|
|
188
|
-
- Location / History / URL
|
|
189
|
+
- Location / History / URL
|
|
189
190
|
- Storage
|
|
190
|
-
- fetch
|
|
191
|
-
- WebSocket / EventSource
|
|
192
|
-
-
|
|
193
|
-
-
|
|
194
|
-
-
|
|
195
|
-
-
|
|
196
|
-
-
|
|
197
|
-
- `requestIdleCallback` / `cancelIdleCallback` (modelled on scheduler), `structuredClone` global
|
|
198
|
-
- `crypto.subtle.digest` (SHA-1/256/384/512), HMAC sign/verify/import/generateKey, and AES-GCM encrypt/decrypt (128/256-bit with additionalData and tagLength options)
|
|
199
|
-
- Streams API (`ReadableStream` / `WritableStream` / `TransformStream` + `TextEncoderStream` / `TextDecoderStream`)
|
|
200
|
-
- `CompressionStream` / `DecompressionStream` (gzip / deflate / deflate-raw via Ruby `Zlib`)
|
|
201
|
-
- Worker (inline-emulated — same-process message round-trip; tests register handlers via `worker.__on_message__`)
|
|
202
|
-
- Performance User Timing (`performance.mark` / `measure` / `getEntriesByName`)
|
|
203
|
-
- `cookieStore` (async Cookie Store API, backed by the same jar `document.cookie` uses)
|
|
204
|
-
- Navigator extras: `share` / `vibrate` / `wakeLock.request` / `getBattery` / `locks` (Web Locks) / `storage.estimate` / `persist` / `persisted` (StorageManager)
|
|
205
|
-
- Layout-adjacent stubs: `element.scrollIntoView` / `scrollTo` / scroll & client / offset metrics (return 0), `getComputedStyle` (inline style passthrough)
|
|
206
|
-
- Popover API (`showPopover` / `hidePopover` / `togglePopover` with `beforetoggle` / `toggle` events)
|
|
207
|
-
- Fullscreen API (`element.requestFullscreen` / `document.exitFullscreen` / `fullscreenchange`)
|
|
208
|
-
- `URLPattern` (pattern matching for URL components — protocol, username, password, hostname, port, pathname, search, hash — with named capture groups and modifiers: `:id`, `*`, `:version+`, `:version?`)
|
|
209
|
-
- `document.startViewTransition` (View Transitions API stub)
|
|
191
|
+
- fetch / XMLHttpRequest stubs
|
|
192
|
+
- WebSocket / EventSource / MessageChannel / BroadcastChannel test doubles
|
|
193
|
+
- FileReader / Notification / Geolocation / `matchMedia`
|
|
194
|
+
- `requestIdleCallback`, `structuredClone`, `URLPattern`
|
|
195
|
+
- Web Crypto, Streams, Compression Streams, Worker
|
|
196
|
+
- `performance`, `cookieStore`, Navigator extras
|
|
197
|
+
- Popover API, Fullscreen API, View Transitions API stub
|
|
210
198
|
- Navigator / Clipboard
|
|
211
199
|
- TreeWalker / NodeIterator / NodeFilter
|
|
212
200
|
- File API (Blob / File / FileList / FormData / DataTransfer)
|
|
213
|
-
-
|
|
214
|
-
- IntersectionObserver / ResizeObserver / PerformanceObserver (test-driven `__trigger__`)
|
|
201
|
+
- IntersectionObserver / ResizeObserver / PerformanceObserver (test-driven `__test_trigger__`)
|
|
215
202
|
- Range / Selection (DOM-level only, no layout)
|
|
216
|
-
- Web Animations API (Animation / KeyframeEffect
|
|
203
|
+
- Web Animations API (Animation / KeyframeEffect)
|
|
217
204
|
- Extended events: Touch / Clipboard / Composition / Wheel / Focus / BeforeUnload / Input / Pointer / Progress / Drag
|
|
218
205
|
|
|
206
|
+
For implementation notes and tradeoffs, see [design.md](./design.md).
|
|
207
|
+
|
|
219
208
|
> [!IMPORTANT]
|
|
220
209
|
> Out of scope:
|
|
221
210
|
>
|
|
222
|
-
> -
|
|
223
|
-
> - CSS scoping (`:host`, `::slotted`, computed styles)
|
|
211
|
+
> - layout and CSS-engine behavior
|
|
224
212
|
> - JS evaluation
|
|
225
213
|
> - Canvas / WebGL / media playback
|
|
226
|
-
> - layout-dependent Range / Selection geometry
|
|
227
|
-
> - SVG-specific value types
|
|
228
|
-
> -
|
|
214
|
+
> - layout-dependent Range / Selection geometry
|
|
215
|
+
> - SVG-specific value types
|
|
216
|
+
> - animation value interpolation
|
|
217
|
+
|
|
218
|
+
## Method naming conventions
|
|
219
|
+
|
|
220
|
+
See [docs/development.md](./docs/development.md) for method-naming conventions and internal protocols.
|
|
229
221
|
|
|
230
222
|
## Running the tests
|
|
231
223
|
|
data/lib/dommy/animation.rb
CHANGED
|
@@ -48,6 +48,8 @@ module Dommy
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
include Bridge::Methods
|
|
52
|
+
js_methods %w[getTiming updateTiming]
|
|
51
53
|
def __js_call__(method, args)
|
|
52
54
|
case method
|
|
53
55
|
when "getTiming"
|
|
@@ -217,11 +219,17 @@ module Dommy
|
|
|
217
219
|
@playback_rate = value.to_f
|
|
218
220
|
when "id"
|
|
219
221
|
@id = value.to_s
|
|
222
|
+
else
|
|
223
|
+
return Bridge::UNHANDLED
|
|
220
224
|
end
|
|
221
225
|
|
|
222
226
|
nil
|
|
223
227
|
end
|
|
224
228
|
|
|
229
|
+
include Bridge::Methods
|
|
230
|
+
js_methods %w[
|
|
231
|
+
play pause cancel finish reverse addEventListener removeEventListener dispatchEvent
|
|
232
|
+
]
|
|
225
233
|
def __js_call__(method, args)
|
|
226
234
|
case method
|
|
227
235
|
when "play"
|
|
@@ -237,14 +245,14 @@ module Dommy
|
|
|
237
245
|
when "addEventListener"
|
|
238
246
|
add_event_listener(args[0], args[1], args[2])
|
|
239
247
|
when "removeEventListener"
|
|
240
|
-
remove_event_listener(args[0], args[1])
|
|
248
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
241
249
|
when "dispatchEvent"
|
|
242
250
|
dispatch_event(args[0])
|
|
243
251
|
end
|
|
244
252
|
end
|
|
245
253
|
|
|
246
254
|
# Event bubbling stops at Animation — it isn't part of the DOM tree.
|
|
247
|
-
def
|
|
255
|
+
def __internal_event_parent__
|
|
248
256
|
nil
|
|
249
257
|
end
|
|
250
258
|
|
data/lib/dommy/attr.rb
CHANGED
|
@@ -13,12 +13,32 @@ module Dommy
|
|
|
13
13
|
# not yet attached. Value is stored locally; `setAttributeNode`
|
|
14
14
|
# transfers it to an element.
|
|
15
15
|
class Attr
|
|
16
|
-
|
|
16
|
+
include Node
|
|
17
|
+
include EventTarget
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
attr_reader :name, :namespace_uri, :prefix, :local_name
|
|
20
|
+
|
|
21
|
+
def __internal_event_parent__
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def initialize(name, owner: nil, value: "", namespace_uri: nil, prefix: nil, local_name: nil)
|
|
26
|
+
qname = name.to_s
|
|
20
27
|
@owner = owner
|
|
21
28
|
@detached_value = value.to_s
|
|
29
|
+
if namespace_uri && !namespace_uri.to_s.empty?
|
|
30
|
+
# Namespaced attributes preserve case and carry prefix / localName.
|
|
31
|
+
@name = qname
|
|
32
|
+
@namespace_uri = namespace_uri.to_s
|
|
33
|
+
@prefix = prefix
|
|
34
|
+
@local_name = (local_name || qname.split(":", 2).last).to_s
|
|
35
|
+
else
|
|
36
|
+
# Null-namespace (HTML) attributes are lower-cased, as before.
|
|
37
|
+
@name = qname.downcase
|
|
38
|
+
@namespace_uri = nil
|
|
39
|
+
@prefix = nil
|
|
40
|
+
@local_name = @name
|
|
41
|
+
end
|
|
22
42
|
end
|
|
23
43
|
|
|
24
44
|
# The Element this attr is on, or nil if detached.
|
|
@@ -28,7 +48,11 @@ module Dommy
|
|
|
28
48
|
|
|
29
49
|
def value
|
|
30
50
|
if @owner
|
|
31
|
-
@
|
|
51
|
+
if @namespace_uri
|
|
52
|
+
Backend.get_attribute_ns(@owner.__dommy_backend_node__, @namespace_uri, @local_name).to_s
|
|
53
|
+
else
|
|
54
|
+
@owner.__dommy_backend_node__[@name].to_s
|
|
55
|
+
end
|
|
32
56
|
else
|
|
33
57
|
@detached_value
|
|
34
58
|
end
|
|
@@ -36,7 +60,11 @@ module Dommy
|
|
|
36
60
|
|
|
37
61
|
def value=(new_value)
|
|
38
62
|
if @owner
|
|
39
|
-
@
|
|
63
|
+
if @namespace_uri
|
|
64
|
+
@owner.set_attribute_ns(@namespace_uri, @name, new_value.to_s)
|
|
65
|
+
else
|
|
66
|
+
@owner.set_attribute(@name, new_value.to_s)
|
|
67
|
+
end
|
|
40
68
|
else
|
|
41
69
|
@detached_value = new_value.to_s
|
|
42
70
|
end
|
|
@@ -52,42 +80,77 @@ module Dommy
|
|
|
52
80
|
@name
|
|
53
81
|
when "nodeValue"
|
|
54
82
|
value
|
|
83
|
+
when "textContent"
|
|
84
|
+
# Node.textContent for an Attr returns its value (WHATWG DOM).
|
|
85
|
+
value
|
|
55
86
|
when "ownerElement"
|
|
56
87
|
@owner
|
|
57
88
|
when "localName"
|
|
58
|
-
@
|
|
89
|
+
@local_name
|
|
59
90
|
when "namespaceURI"
|
|
60
|
-
|
|
91
|
+
@namespace_uri
|
|
92
|
+
when "prefix"
|
|
93
|
+
@prefix
|
|
61
94
|
when "nodeType"
|
|
62
95
|
2
|
|
96
|
+
when "specified"
|
|
97
|
+
# Legacy/useless attribute — always true (WHATWG DOM).
|
|
98
|
+
true
|
|
63
99
|
end
|
|
64
100
|
end
|
|
65
101
|
|
|
66
102
|
def __js_set__(key, val)
|
|
67
103
|
case key
|
|
68
|
-
when "value", "nodeValue"
|
|
104
|
+
when "value", "nodeValue", "textContent"
|
|
69
105
|
self.value = val
|
|
106
|
+
else
|
|
107
|
+
return Bridge::UNHANDLED
|
|
70
108
|
end
|
|
71
109
|
|
|
72
110
|
nil
|
|
73
111
|
end
|
|
74
112
|
|
|
75
|
-
|
|
113
|
+
include Bridge::Methods
|
|
114
|
+
js_methods %w[cloneNode isSameNode getRootNode hasChildNodes normalize compareDocumentPosition
|
|
115
|
+
appendChild insertBefore removeChild replaceChild
|
|
116
|
+
addEventListener removeEventListener dispatchEvent]
|
|
117
|
+
def __js_call__(method, args)
|
|
76
118
|
case method
|
|
77
119
|
when "cloneNode"
|
|
78
|
-
Attr.new(@name, owner: nil, value: value
|
|
120
|
+
Attr.new(@name, owner: nil, value: value,
|
|
121
|
+
namespace_uri: @namespace_uri, prefix: @prefix, local_name: @local_name)
|
|
122
|
+
when "isSameNode"
|
|
123
|
+
is_same_node(args[0])
|
|
124
|
+
when "getRootNode"
|
|
125
|
+
get_root_node(args[0])
|
|
126
|
+
when "compareDocumentPosition"
|
|
127
|
+
compare_document_position(args[0])
|
|
128
|
+
when "appendChild", "insertBefore"
|
|
129
|
+
raise DOMException::HierarchyRequestError, "an Attr may not have children"
|
|
130
|
+
when "removeChild", "replaceChild"
|
|
131
|
+
raise DOMException::NotFoundError, "the node to be removed is not a child of this node"
|
|
132
|
+
when "hasChildNodes"
|
|
133
|
+
false
|
|
134
|
+
when "normalize"
|
|
135
|
+
nil
|
|
136
|
+
when "addEventListener"
|
|
137
|
+
add_event_listener(args[0], args[1], args[2])
|
|
138
|
+
when "removeEventListener"
|
|
139
|
+
remove_event_listener(args[0], args[1], args[2])
|
|
140
|
+
when "dispatchEvent"
|
|
141
|
+
dispatch_event(args[0])
|
|
79
142
|
end
|
|
80
143
|
end
|
|
81
144
|
|
|
82
145
|
# Internal: called by Element when the attr is being transferred
|
|
83
146
|
# to (or detached from) an Element.
|
|
84
|
-
def
|
|
147
|
+
def __internal_attach__(element)
|
|
85
148
|
@owner = element
|
|
86
149
|
@detached_value = ""
|
|
87
150
|
nil
|
|
88
151
|
end
|
|
89
152
|
|
|
90
|
-
def
|
|
153
|
+
def __internal_detach__
|
|
91
154
|
cached = value
|
|
92
155
|
@owner = nil
|
|
93
156
|
@detached_value = cached
|
|
@@ -106,49 +169,134 @@ module Dommy
|
|
|
106
169
|
|
|
107
170
|
def initialize(element)
|
|
108
171
|
@element = element
|
|
172
|
+
# Attr-node identity cache, keyed by [namespace_or_nil, localName].
|
|
173
|
+
# Every accessor (item / index / getNamedItem(NS)) returns the SAME
|
|
174
|
+
# Attr object for a given underlying attribute, per the DOM.
|
|
175
|
+
@attrs = {}
|
|
109
176
|
end
|
|
110
177
|
|
|
111
178
|
def length
|
|
112
|
-
@element.
|
|
179
|
+
Backend.attribute_nodes(@element.__dommy_backend_node__).size
|
|
113
180
|
end
|
|
114
181
|
|
|
115
182
|
alias size length
|
|
116
183
|
|
|
117
184
|
def item(index)
|
|
118
|
-
|
|
119
|
-
|
|
185
|
+
node = Backend.attribute_nodes(@element.__dommy_backend_node__)[index.to_i]
|
|
186
|
+
node && attr_for(node)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Return the cached Attr for a backend attribute node, creating (and
|
|
190
|
+
# caching) one on first access so DOM node identity holds.
|
|
191
|
+
def attr_for(attr_node)
|
|
192
|
+
info = Backend.attribute_ns_info(attr_node)
|
|
193
|
+
key = [info[:namespace_uri], info[:local_name]]
|
|
194
|
+
cached = @attrs[key]
|
|
195
|
+
return cached if cached && cached.owner_element.equal?(@element)
|
|
196
|
+
|
|
197
|
+
attr = Attr.new(info[:qualified_name], owner: @element,
|
|
198
|
+
namespace_uri: info[:namespace_uri],
|
|
199
|
+
prefix: info[:prefix],
|
|
200
|
+
local_name: info[:local_name])
|
|
201
|
+
@attrs[key] = attr
|
|
202
|
+
attr
|
|
120
203
|
end
|
|
121
204
|
|
|
122
205
|
def get_named_item(name)
|
|
123
206
|
key = name.to_s.downcase
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
207
|
+
node = Backend.attribute_nodes(@element.__dommy_backend_node__).find do |a|
|
|
208
|
+
Backend.attribute_ns_info(a)[:qualified_name] == key
|
|
209
|
+
end
|
|
210
|
+
node && attr_for(node)
|
|
127
211
|
end
|
|
128
212
|
|
|
129
213
|
def set_named_item(attr)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
key = attr.name
|
|
133
|
-
val = attr.value
|
|
134
|
-
attr.__attach__(@element)
|
|
135
|
-
@element.set_attribute(key, val)
|
|
136
|
-
attr
|
|
214
|
+
set_attribute_node(attr)
|
|
137
215
|
end
|
|
138
216
|
|
|
139
217
|
def remove_named_item(name)
|
|
140
218
|
key = name.to_s.downcase
|
|
141
|
-
|
|
219
|
+
node = Backend.attribute_nodes(@element.__dommy_backend_node__).find do |a|
|
|
220
|
+
Backend.attribute_ns_info(a)[:qualified_name] == key
|
|
221
|
+
end
|
|
222
|
+
return nil unless node
|
|
142
223
|
|
|
143
|
-
|
|
224
|
+
removed = attr_for(node)
|
|
144
225
|
@element.remove_attribute(key)
|
|
145
|
-
|
|
226
|
+
removed
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def each
|
|
230
|
+
Backend.attribute_nodes(@element.__dommy_backend_node__).each do |a|
|
|
231
|
+
yield attr_for(a)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# WHATWG "set an attribute" / "set attribute node". Adopts `attr` (the
|
|
236
|
+
# exact object — identity is preserved), replacing any attribute with the
|
|
237
|
+
# same (namespace, localName) and returning the previous Attr (detached),
|
|
238
|
+
# or nil. Throws InUseAttributeError if `attr` is bound to another element.
|
|
239
|
+
def set_attribute_node(attr)
|
|
240
|
+
return nil unless attr.is_a?(Attr)
|
|
241
|
+
|
|
242
|
+
owner = attr.owner_element
|
|
243
|
+
if owner && !owner.equal?(@element)
|
|
244
|
+
raise DOMException::InUseAttributeError, "attribute is in use by another element"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
ns = attr.namespace_uri
|
|
248
|
+
local = attr.local_name
|
|
249
|
+
old = get_named_item_ns(ns, local)
|
|
250
|
+
return attr if old && old.equal?(attr)
|
|
251
|
+
|
|
252
|
+
value = attr.value
|
|
253
|
+
key = [ns, local]
|
|
254
|
+
if old
|
|
255
|
+
old.__internal_detach__
|
|
256
|
+
@attrs.delete(key)
|
|
257
|
+
end
|
|
258
|
+
attr.__internal_attach__(@element)
|
|
259
|
+
if ns
|
|
260
|
+
@element.set_attribute_ns(ns, attr.name, value)
|
|
261
|
+
else
|
|
262
|
+
@element.set_attribute(attr.name, value)
|
|
263
|
+
end
|
|
264
|
+
@attrs[key] = attr
|
|
265
|
+
old
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Detach and evict the cached Attr for (namespace, localName), if any —
|
|
269
|
+
# called by Element after the underlying attribute is removed so a held
|
|
270
|
+
# reference reports `ownerElement === null`.
|
|
271
|
+
def __internal_evict__(namespace, local_name)
|
|
272
|
+
key = [namespace.to_s.empty? ? nil : namespace.to_s, local_name.to_s]
|
|
273
|
+
attr = @attrs.delete(key)
|
|
274
|
+
attr&.__internal_detach__
|
|
275
|
+
nil
|
|
146
276
|
end
|
|
147
277
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
278
|
+
# ----- Namespaced named-item access (getNamedItemNS etc.) -----
|
|
279
|
+
|
|
280
|
+
def get_named_item_ns(namespace, local_name)
|
|
281
|
+
node = Backend.attribute_nodes(@element.__dommy_backend_node__).find do |a|
|
|
282
|
+
info = Backend.attribute_ns_info(a)
|
|
283
|
+
info[:local_name] == local_name.to_s &&
|
|
284
|
+
(info[:namespace_uri] || nil) == (namespace.to_s.empty? ? nil : namespace.to_s)
|
|
151
285
|
end
|
|
286
|
+
node && attr_for(node)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# setNamedItemNS shares the "set an attribute" algorithm with setNamedItem.
|
|
290
|
+
def set_named_item_ns(attr)
|
|
291
|
+
set_attribute_node(attr)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def remove_named_item_ns(namespace, local_name)
|
|
295
|
+
existing = get_named_item_ns(namespace, local_name)
|
|
296
|
+
return nil unless existing
|
|
297
|
+
|
|
298
|
+
@element.remove_attribute_ns(namespace, local_name)
|
|
299
|
+
existing
|
|
152
300
|
end
|
|
153
301
|
|
|
154
302
|
# Property-style access — `el.attributes.id`, `el.attributes["class"]`.
|
|
@@ -175,6 +323,17 @@ module Dommy
|
|
|
175
323
|
end
|
|
176
324
|
end
|
|
177
325
|
|
|
326
|
+
# WebIDL "supported property names" for NamedNodeMap: the qualified name of
|
|
327
|
+
# each attribute, in order (the indexed names are reflected separately).
|
|
328
|
+
def __js_named_props__
|
|
329
|
+
Backend.attribute_nodes(@element.__dommy_backend_node__).map do |a|
|
|
330
|
+
Backend.attribute_ns_info(a)[:qualified_name]
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
include Bridge::Methods
|
|
335
|
+
js_methods %w[item getNamedItem setNamedItem removeNamedItem
|
|
336
|
+
getNamedItemNS setNamedItemNS removeNamedItemNS]
|
|
178
337
|
def __js_call__(method, args)
|
|
179
338
|
case method
|
|
180
339
|
when "item"
|
|
@@ -185,6 +344,12 @@ module Dommy
|
|
|
185
344
|
set_named_item(args[0])
|
|
186
345
|
when "removeNamedItem"
|
|
187
346
|
remove_named_item(args[0])
|
|
347
|
+
when "getNamedItemNS"
|
|
348
|
+
get_named_item_ns(args[0], args[1])
|
|
349
|
+
when "setNamedItemNS"
|
|
350
|
+
set_named_item_ns(args[0])
|
|
351
|
+
when "removeNamedItemNS"
|
|
352
|
+
remove_named_item_ns(args[0], args[1])
|
|
188
353
|
end
|
|
189
354
|
end
|
|
190
355
|
|
|
@@ -194,7 +359,7 @@ module Dommy
|
|
|
194
359
|
end
|
|
195
360
|
|
|
196
361
|
def respond_to_missing?(name, include_private = false)
|
|
197
|
-
@element.
|
|
362
|
+
@element.__dommy_backend_node__.key?(name.to_s.downcase) || super
|
|
198
363
|
end
|
|
199
364
|
end
|
|
200
365
|
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Dommy
|
|
6
|
+
module Backend
|
|
7
|
+
# Nokogiri (libxml2-based) backend. Mature, full XML namespace
|
|
8
|
+
# support. Default backend.
|
|
9
|
+
module Nokogiri
|
|
10
|
+
# Class references for `is_a?` / type-checking from Dommy internals.
|
|
11
|
+
Element = ::Nokogiri::XML::Element
|
|
12
|
+
Document = ::Nokogiri::XML::Document
|
|
13
|
+
Text = ::Nokogiri::XML::Text
|
|
14
|
+
Comment = ::Nokogiri::XML::Comment
|
|
15
|
+
DocumentFragment = ::Nokogiri::XML::DocumentFragment
|
|
16
|
+
Node = ::Nokogiri::XML::Node
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def parse(html)
|
|
21
|
+
::Nokogiri::HTML5(html.to_s, max_errors: 0)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Parse an XML string into an XML document (DOMParser "text/xml" etc.).
|
|
25
|
+
def parse_xml(xml)
|
|
26
|
+
::Nokogiri::XML(xml.to_s)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def fragment(html, owner_doc:)
|
|
30
|
+
# owner_doc is unused by Nokogiri — the fragment carries its
|
|
31
|
+
# own document. The Parser layer copies nodes into the target.
|
|
32
|
+
::Nokogiri::HTML5.fragment(html.to_s, max_errors: 0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def create_element(name, doc)
|
|
36
|
+
::Nokogiri::XML::Node.new(name, doc)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_text(content, doc)
|
|
40
|
+
::Nokogiri::XML::Text.new(content, doc)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_comment(content, doc)
|
|
44
|
+
::Nokogiri::XML::Comment.new(doc, content)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def create_cdata(content, doc)
|
|
48
|
+
::Nokogiri::XML::CDATA.new(doc, content.to_s)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def cdata_class = ::Nokogiri::XML::CDATA
|
|
52
|
+
|
|
53
|
+
def namespace_of(node)
|
|
54
|
+
node.namespace
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_namespace_definition(node, prefix, href)
|
|
58
|
+
node.add_namespace_definition(prefix, href)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# ----- Namespaced attributes -----
|
|
62
|
+
|
|
63
|
+
def get_attribute_ns(node, namespace, local_name)
|
|
64
|
+
find_attr_ns(node, namespace, local_name)&.value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def has_attribute_ns?(node, namespace, local_name)
|
|
68
|
+
!find_attr_ns(node, namespace, local_name).nil?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def set_attribute_ns(node, namespace, prefix, local_name, qualified_name, value)
|
|
72
|
+
# WHATWG "set an attribute value": when an attribute with this
|
|
73
|
+
# (namespace, localName) already exists, only its value changes —
|
|
74
|
+
# the existing prefix/qualified name is preserved, not replaced by
|
|
75
|
+
# the one in this call.
|
|
76
|
+
existing = find_attr_ns(node, namespace, local_name)
|
|
77
|
+
if existing
|
|
78
|
+
existing.value = value.to_s
|
|
79
|
+
return value.to_s
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
if namespace.nil? || namespace.to_s.empty?
|
|
83
|
+
node[qualified_name] = value.to_s
|
|
84
|
+
return value.to_s
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Defining the namespace before the qualified-name assignment lets
|
|
88
|
+
# libxml2 bind the prefix to it (verified behavior). Reuse an existing
|
|
89
|
+
# matching definition so repeated sets don't pile up declarations.
|
|
90
|
+
node.namespace_definitions.find { |d| d.href == namespace.to_s && d.prefix == prefix } ||
|
|
91
|
+
node.add_namespace_definition(prefix, namespace.to_s)
|
|
92
|
+
node[qualified_name] = value.to_s
|
|
93
|
+
value.to_s
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def remove_attribute_ns(node, namespace, local_name)
|
|
97
|
+
find_attr_ns(node, namespace, local_name)&.remove
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def attribute_ns_info(attr_node)
|
|
102
|
+
ns = attr_node.namespace
|
|
103
|
+
{
|
|
104
|
+
namespace_uri: ns&.href,
|
|
105
|
+
prefix: ns&.prefix,
|
|
106
|
+
local_name: attr_node.name,
|
|
107
|
+
qualified_name: ns&.prefix ? "#{ns.prefix}:#{attr_node.name}" : attr_node.name,
|
|
108
|
+
value: attr_node.value,
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def attribute_nodes(node)
|
|
113
|
+
node.attribute_nodes
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Finds the attribute node matching (namespace, localName). A null
|
|
117
|
+
# namespace matches only the un-namespaced attribute of that local name.
|
|
118
|
+
def find_attr_ns(node, namespace, local_name)
|
|
119
|
+
if namespace.nil? || namespace.to_s.empty?
|
|
120
|
+
node.attribute_nodes.find { |a| a.namespace.nil? && a.name == local_name.to_s }
|
|
121
|
+
else
|
|
122
|
+
node.attribute_with_ns(local_name.to_s, namespace.to_s)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|