dommy 0.6.0 → 0.7.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 +1 -1
- data/lib/dommy/attr.rb +23 -11
- data/lib/dommy/backend/nokogiri_adapter.rb +51 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +80 -0
- data/lib/dommy/backend.rb +129 -0
- data/lib/dommy/blob.rb +2 -2
- data/lib/dommy/compression_streams.rb +4 -4
- data/lib/dommy/cookie_store.rb +1 -1
- data/lib/dommy/crypto.rb +9 -8
- data/lib/dommy/css.rb +7 -7
- data/lib/dommy/custom_elements.rb +6 -6
- data/lib/dommy/document.rb +98 -32
- data/lib/dommy/dom_parser.rb +5 -4
- data/lib/dommy/element.rb +231 -50
- data/lib/dommy/event.rb +61 -25
- data/lib/dommy/event_source.rb +8 -8
- data/lib/dommy/fetch.rb +14 -6
- data/lib/dommy/file_reader.rb +3 -3
- data/lib/dommy/form_data.rb +1 -3
- data/lib/dommy/history.rb +7 -4
- data/lib/dommy/html_collection.rb +4 -4
- data/lib/dommy/html_elements.rb +110 -42
- data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
- data/lib/dommy/internal/dom_matching.rb +3 -3
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/intersection_observer.rb +2 -2
- data/lib/dommy/location.rb +8 -4
- data/lib/dommy/media_query_list.rb +3 -3
- data/lib/dommy/message_channel.rb +9 -9
- data/lib/dommy/mutation_observer.rb +21 -11
- data/lib/dommy/navigator.rb +12 -12
- data/lib/dommy/node.rb +12 -0
- data/lib/dommy/notification.rb +3 -3
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance_observer.rb +2 -2
- data/lib/dommy/range.rb +2 -2
- data/lib/dommy/resize_observer.rb +2 -2
- data/lib/dommy/shadow_root.rb +10 -8
- data/lib/dommy/streams.rb +22 -22
- data/lib/dommy/text_codec.rb +4 -4
- data/lib/dommy/tree_walker.rb +21 -21
- data/lib/dommy/url.rb +25 -8
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +13 -13
- data/lib/dommy/window.rb +14 -1
- data/lib/dommy/worker.rb +5 -5
- data/lib/dommy/xml_http_request.rb +19 -4
- data/lib/dommy.rb +12 -2
- metadata +12 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ccbe61a606e042968621f7fa81588634f2cdae566d96cef137bfefdfb6d38dd6
|
|
4
|
+
data.tar.gz: 4358d9b68082608b7e78971be2d2818fb35200b17bec1819ce21a520b3552d7c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8c9a3b7bbfacb6f58440cbfec8884cc79c8690beda03225b229ca935f1b6d1ce0778c83a9253f2996198e46c8d8b85f9df53ccedb1ffa4e942a5b9841c051e4
|
|
7
|
+
data.tar.gz: f428a160a4c2f0180e7fa01f8cb1e6ec31dc9855f2bd7b43f1be6abd7a752a6b93926c1e57791a1c8b51a22e62631c997c4fbe8b55ea38fed5748f62cc874033
|
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
data/lib/dommy/attr.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Dommy
|
|
|
28
28
|
|
|
29
29
|
def value
|
|
30
30
|
if @owner
|
|
31
|
-
@owner.
|
|
31
|
+
@owner.__dommy_backend_node__[@name].to_s
|
|
32
32
|
else
|
|
33
33
|
@detached_value
|
|
34
34
|
end
|
|
@@ -72,6 +72,12 @@ module Dommy
|
|
|
72
72
|
nil
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
76
|
+
JS_METHOD_NAMES = %w[cloneNode].freeze
|
|
77
|
+
def __js_method_names__
|
|
78
|
+
JS_METHOD_NAMES
|
|
79
|
+
end
|
|
80
|
+
|
|
75
81
|
def __js_call__(method, _args)
|
|
76
82
|
case method
|
|
77
83
|
when "cloneNode"
|
|
@@ -81,13 +87,13 @@ module Dommy
|
|
|
81
87
|
|
|
82
88
|
# Internal: called by Element when the attr is being transferred
|
|
83
89
|
# to (or detached from) an Element.
|
|
84
|
-
def
|
|
90
|
+
def __internal_attach__(element)
|
|
85
91
|
@owner = element
|
|
86
92
|
@detached_value = ""
|
|
87
93
|
nil
|
|
88
94
|
end
|
|
89
95
|
|
|
90
|
-
def
|
|
96
|
+
def __internal_detach__
|
|
91
97
|
cached = value
|
|
92
98
|
@owner = nil
|
|
93
99
|
@detached_value = cached
|
|
@@ -109,19 +115,19 @@ module Dommy
|
|
|
109
115
|
end
|
|
110
116
|
|
|
111
117
|
def length
|
|
112
|
-
@element.
|
|
118
|
+
@element.__dommy_backend_node__.attribute_nodes.size
|
|
113
119
|
end
|
|
114
120
|
|
|
115
121
|
alias size length
|
|
116
122
|
|
|
117
123
|
def item(index)
|
|
118
|
-
name = @element.
|
|
124
|
+
name = @element.__dommy_backend_node__.attribute_nodes[index.to_i]&.name
|
|
119
125
|
name && Attr.new(name, owner: @element)
|
|
120
126
|
end
|
|
121
127
|
|
|
122
128
|
def get_named_item(name)
|
|
123
129
|
key = name.to_s.downcase
|
|
124
|
-
return nil unless @element.
|
|
130
|
+
return nil unless @element.__dommy_backend_node__.key?(key)
|
|
125
131
|
|
|
126
132
|
Attr.new(key, owner: @element)
|
|
127
133
|
end
|
|
@@ -131,22 +137,22 @@ module Dommy
|
|
|
131
137
|
|
|
132
138
|
key = attr.name
|
|
133
139
|
val = attr.value
|
|
134
|
-
attr.
|
|
140
|
+
attr.__internal_attach__(@element)
|
|
135
141
|
@element.set_attribute(key, val)
|
|
136
142
|
attr
|
|
137
143
|
end
|
|
138
144
|
|
|
139
145
|
def remove_named_item(name)
|
|
140
146
|
key = name.to_s.downcase
|
|
141
|
-
return nil unless @element.
|
|
147
|
+
return nil unless @element.__dommy_backend_node__.key?(key)
|
|
142
148
|
|
|
143
|
-
attr = Attr.new(key, owner: nil, value: @element.
|
|
149
|
+
attr = Attr.new(key, owner: nil, value: @element.__dommy_backend_node__[key].to_s)
|
|
144
150
|
@element.remove_attribute(key)
|
|
145
151
|
attr
|
|
146
152
|
end
|
|
147
153
|
|
|
148
154
|
def each(&blk)
|
|
149
|
-
@element.
|
|
155
|
+
@element.__dommy_backend_node__.attribute_nodes.each do |a|
|
|
150
156
|
yield Attr.new(a.name, owner: @element)
|
|
151
157
|
end
|
|
152
158
|
end
|
|
@@ -175,6 +181,12 @@ module Dommy
|
|
|
175
181
|
end
|
|
176
182
|
end
|
|
177
183
|
|
|
184
|
+
# Methods routed through __js_call__ (keep in sync with its when-arms).
|
|
185
|
+
JS_METHOD_NAMES = %w[item getNamedItem setNamedItem removeNamedItem].freeze
|
|
186
|
+
def __js_method_names__
|
|
187
|
+
JS_METHOD_NAMES
|
|
188
|
+
end
|
|
189
|
+
|
|
178
190
|
def __js_call__(method, args)
|
|
179
191
|
case method
|
|
180
192
|
when "item"
|
|
@@ -194,7 +206,7 @@ module Dommy
|
|
|
194
206
|
end
|
|
195
207
|
|
|
196
208
|
def respond_to_missing?(name, include_private = false)
|
|
197
|
-
@element.
|
|
209
|
+
@element.__dommy_backend_node__.key?(name.to_s.downcase) || super
|
|
198
210
|
end
|
|
199
211
|
end
|
|
200
212
|
end
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
def fragment(html, owner_doc:)
|
|
25
|
+
# owner_doc is unused by Nokogiri — the fragment carries its
|
|
26
|
+
# own document. The Parser layer copies nodes into the target.
|
|
27
|
+
::Nokogiri::HTML5.fragment(html.to_s, max_errors: 0)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_element(name, doc)
|
|
31
|
+
::Nokogiri::XML::Node.new(name, doc)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create_text(content, doc)
|
|
35
|
+
::Nokogiri::XML::Text.new(content, doc)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def create_comment(content, doc)
|
|
39
|
+
::Nokogiri::XML::Comment.new(doc, content)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def namespace_of(node)
|
|
43
|
+
node.namespace
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def add_namespace_definition(node, prefix, href)
|
|
47
|
+
node.add_namespace_definition(prefix, href)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokolexbor"
|
|
4
|
+
|
|
5
|
+
module Dommy
|
|
6
|
+
module Backend
|
|
7
|
+
# Nokolexbor (Lexbor-based) backend. 3-7× faster CSS selectors,
|
|
8
|
+
# 6× faster HTML5 parsing. HTML-only — XML namespaces are not
|
|
9
|
+
# tracked, so SVG detection falls back to ancestor walking.
|
|
10
|
+
module Nokolexbor
|
|
11
|
+
# Class references for `is_a?` / type-checking.
|
|
12
|
+
Element = ::Nokolexbor::Element
|
|
13
|
+
Document = ::Nokolexbor::Document
|
|
14
|
+
Text = ::Nokolexbor::Text
|
|
15
|
+
Comment = ::Nokolexbor::Comment
|
|
16
|
+
DocumentFragment = ::Nokolexbor::DocumentFragment
|
|
17
|
+
Node = ::Nokolexbor::Node
|
|
18
|
+
|
|
19
|
+
# Fake "namespace" wrapper returned for nodes inside <svg> subtrees.
|
|
20
|
+
# Provides the same `href` API that Nokogiri's Namespace object has,
|
|
21
|
+
# so calling code can treat them uniformly.
|
|
22
|
+
SvgNamespace = Struct.new(:href) do
|
|
23
|
+
def initialize
|
|
24
|
+
super("http://www.w3.org/2000/svg")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
SVG_NAMESPACE = SvgNamespace.new.freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def parse(html)
|
|
33
|
+
::Nokolexbor.parse(html.to_s)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fragment(html, owner_doc:)
|
|
37
|
+
::Nokolexbor::DocumentFragment.parse(html.to_s)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_element(name, doc)
|
|
41
|
+
::Nokolexbor::Node.new(name, doc)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def create_text(content, doc)
|
|
45
|
+
::Nokolexbor::Text.new(content, doc)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def create_comment(content, doc)
|
|
49
|
+
# Nokolexbor's argument order is (content, doc).
|
|
50
|
+
::Nokolexbor::Comment.new(content, doc)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Nokolexbor doesn't track XML namespaces. We synthesize one for
|
|
54
|
+
# SVG by walking ancestors — necessary so `element_class_for`
|
|
55
|
+
# routes SVG tags to their specialized classes.
|
|
56
|
+
def namespace_of(node)
|
|
57
|
+
return nil unless node.respond_to?(:name)
|
|
58
|
+
|
|
59
|
+
in_svg_subtree?(node) ? SVG_NAMESPACE : nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def add_namespace_definition(_node, _prefix, _href)
|
|
63
|
+
# No-op: Nokolexbor doesn't support XML namespaces.
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Internal helper — visible to allow testing.
|
|
67
|
+
def in_svg_subtree?(node)
|
|
68
|
+
return true if node.name.to_s.downcase == "svg"
|
|
69
|
+
|
|
70
|
+
current = node.parent
|
|
71
|
+
while current
|
|
72
|
+
return true if current.respond_to?(:name) && current.name.to_s.downcase == "svg"
|
|
73
|
+
current = current.respond_to?(:parent) ? current.parent : nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# `Dommy::Backend` — pluggable HTML parser abstraction. Lets Dommy
|
|
5
|
+
# work with either Nokogiri (mature, full namespace support) or
|
|
6
|
+
# Nokolexbor (faster, HTML5-only). Internally, all DOM library
|
|
7
|
+
# code goes through this facade rather than referencing the parser
|
|
8
|
+
# directly.
|
|
9
|
+
#
|
|
10
|
+
# Defaults to Nokogiri if available, else Nokolexbor.
|
|
11
|
+
#
|
|
12
|
+
# Switching backends:
|
|
13
|
+
#
|
|
14
|
+
# require "dommy"
|
|
15
|
+
# Dommy::Backend.use(:nokolexbor)
|
|
16
|
+
#
|
|
17
|
+
# Or set directly:
|
|
18
|
+
#
|
|
19
|
+
# Dommy::Backend.current = Dommy::Backend::Nokolexbor
|
|
20
|
+
#
|
|
21
|
+
# All adapters must implement the same interface — see
|
|
22
|
+
# `Backend::Nokogiri` for the canonical reference.
|
|
23
|
+
module Backend
|
|
24
|
+
class BackendNotAvailable < StandardError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
def current
|
|
29
|
+
@current ||= detect_default
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
attr_writer :current
|
|
33
|
+
|
|
34
|
+
def use(name)
|
|
35
|
+
@current = case name.to_sym
|
|
36
|
+
when :nokogiri
|
|
37
|
+
require_relative "backend/nokogiri_adapter"
|
|
38
|
+
Nokogiri
|
|
39
|
+
when :nokolexbor
|
|
40
|
+
require_relative "backend/nokolexbor_adapter"
|
|
41
|
+
Nokolexbor
|
|
42
|
+
else
|
|
43
|
+
raise ArgumentError, "Unknown backend: #{name.inspect}. Use :nokogiri or :nokolexbor."
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Delegate calls so internal code can use `Backend.parse(...)`.
|
|
48
|
+
def parse(html)
|
|
49
|
+
current.parse(html)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def fragment(html, owner_doc:)
|
|
53
|
+
current.fragment(html, owner_doc: owner_doc)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def create_element(name, doc)
|
|
57
|
+
current.create_element(name, doc)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def create_text(content, doc)
|
|
61
|
+
current.create_text(content, doc)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def create_comment(content, doc)
|
|
65
|
+
current.create_comment(content, doc)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def namespace_of(node)
|
|
69
|
+
current.namespace_of(node)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def add_namespace_definition(node, prefix, href)
|
|
73
|
+
current.add_namespace_definition(node, prefix, href)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Type constants — proxy through to the current backend so
|
|
77
|
+
# `node.is_a?(Backend::Element)` resolves dynamically.
|
|
78
|
+
def element_class
|
|
79
|
+
current::Element
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def document_class
|
|
83
|
+
current::Document
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def text_class
|
|
87
|
+
current::Text
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def comment_class
|
|
91
|
+
current::Comment
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def document_fragment_class
|
|
95
|
+
current::DocumentFragment
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def node_class
|
|
99
|
+
current::Node
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def detect_default
|
|
105
|
+
try_nokogiri ||
|
|
106
|
+
try_nokolexbor ||
|
|
107
|
+
raise(BackendNotAvailable, "Dommy requires either 'nokogiri' or 'nokolexbor' gem to be installed.")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def try_nokogiri
|
|
111
|
+
require "nokogiri"
|
|
112
|
+
|
|
113
|
+
require_relative "backend/nokogiri_adapter"
|
|
114
|
+
Nokogiri
|
|
115
|
+
rescue LoadError
|
|
116
|
+
nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def try_nokolexbor
|
|
120
|
+
require "nokolexbor"
|
|
121
|
+
|
|
122
|
+
require_relative "backend/nokolexbor_adapter"
|
|
123
|
+
Nokolexbor
|
|
124
|
+
rescue LoadError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/dommy/blob.rb
CHANGED
|
@@ -48,7 +48,7 @@ module Dommy
|
|
|
48
48
|
|
|
49
49
|
# Raw binary bytes (Ruby ASCII-8BIT string). Used by FormData /
|
|
50
50
|
# fetch when serializing multipart bodies.
|
|
51
|
-
def
|
|
51
|
+
def __dommy_bytes__
|
|
52
52
|
@data
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -79,7 +79,7 @@ module Dommy
|
|
|
79
79
|
parts.each do |part|
|
|
80
80
|
case part
|
|
81
81
|
when Blob
|
|
82
|
-
buf << part.
|
|
82
|
+
buf << part.__dommy_bytes__
|
|
83
83
|
when String
|
|
84
84
|
buf << part.dup.force_encoding(Encoding::ASCII_8BIT)
|
|
85
85
|
when Array
|
|
@@ -31,9 +31,9 @@ module Dommy
|
|
|
31
31
|
"close" => proc do
|
|
32
32
|
compressed = compressor.call(@buffer)
|
|
33
33
|
controller.enqueue(compressed)
|
|
34
|
-
@readable.
|
|
34
|
+
@readable.__internal_close__
|
|
35
35
|
end,
|
|
36
|
-
"abort" => proc { |r| @readable.
|
|
36
|
+
"abort" => proc { |r| @readable.__internal_error__(r) }
|
|
37
37
|
}
|
|
38
38
|
)
|
|
39
39
|
end
|
|
@@ -98,9 +98,9 @@ module Dommy
|
|
|
98
98
|
"close" => proc do
|
|
99
99
|
plain = decompressor.call(@buffer)
|
|
100
100
|
controller.enqueue(plain)
|
|
101
|
-
@readable.
|
|
101
|
+
@readable.__internal_close__
|
|
102
102
|
end,
|
|
103
|
-
"abort" => proc { |r| @readable.
|
|
103
|
+
"abort" => proc { |r| @readable.__internal_error__(r) }
|
|
104
104
|
}
|
|
105
105
|
)
|
|
106
106
|
end
|
data/lib/dommy/cookie_store.rb
CHANGED
data/lib/dommy/crypto.rb
CHANGED
|
@@ -182,7 +182,7 @@ module Dommy
|
|
|
182
182
|
raise ArgumentError, "HMAC key required" unless key.is_a?(CryptoKey) && key.algorithm_name == "HMAC"
|
|
183
183
|
raise ArgumentError, "key.usages must include 'sign'" unless key.usages.include?("sign")
|
|
184
184
|
|
|
185
|
-
OpenSSL::HMAC.digest(openssl_digest_name(key.hash_name), key.
|
|
185
|
+
OpenSSL::HMAC.digest(openssl_digest_name(key.hash_name), key.__dommy_bytes__, coerce_bytes(data)).bytes
|
|
186
186
|
end
|
|
187
187
|
end
|
|
188
188
|
|
|
@@ -192,7 +192,7 @@ module Dommy
|
|
|
192
192
|
raise ArgumentError, "HMAC key required" unless key.is_a?(CryptoKey) && key.algorithm_name == "HMAC"
|
|
193
193
|
raise ArgumentError, "key.usages must include 'verify'" unless key.usages.include?("verify")
|
|
194
194
|
|
|
195
|
-
expected = OpenSSL::HMAC.digest(openssl_digest_name(key.hash_name), key.
|
|
195
|
+
expected = OpenSSL::HMAC.digest(openssl_digest_name(key.hash_name), key.__dommy_bytes__, coerce_bytes(data))
|
|
196
196
|
sig_bytes = coerce_bytes(signature)
|
|
197
197
|
if expected.bytesize == sig_bytes.bytesize
|
|
198
198
|
OpenSSL.fixed_length_secure_compare(expected, sig_bytes)
|
|
@@ -327,7 +327,7 @@ module Dommy
|
|
|
327
327
|
end
|
|
328
328
|
|
|
329
329
|
def build_gcm_cipher(direction, algorithm, key)
|
|
330
|
-
raw_key = key.is_a?(CryptoKey) ? key.
|
|
330
|
+
raw_key = key.is_a?(CryptoKey) ? key.__dommy_bytes__ : coerce_bytes(key)
|
|
331
331
|
raise ArgumentError, "AES-GCM key must be 16/24/32 bytes" unless [16, 24, 32].include?(raw_key.bytesize)
|
|
332
332
|
|
|
333
333
|
iv = algorithm.is_a?(Hash) ? (algorithm["iv"] || algorithm[:iv]) : nil
|
|
@@ -359,9 +359,9 @@ module Dommy
|
|
|
359
359
|
end
|
|
360
360
|
|
|
361
361
|
# `CryptoKey` — opaque key handle returned by SubtleCrypto.
|
|
362
|
-
# `extractable: false` keys reject export attempts; the
|
|
363
|
-
#
|
|
364
|
-
#
|
|
362
|
+
# `extractable: false` keys reject export attempts; the raw bytes are
|
|
363
|
+
# reachable only through the `__dommy_bytes__` ecosystem accessor, never
|
|
364
|
+
# the public (Web-mirroring) API.
|
|
365
365
|
class CryptoKey
|
|
366
366
|
attr_reader :type, :algorithm_name, :hash_name, :usages, :extractable
|
|
367
367
|
|
|
@@ -374,8 +374,9 @@ module Dommy
|
|
|
374
374
|
@usages = usages.map(&:to_s).freeze
|
|
375
375
|
end
|
|
376
376
|
|
|
377
|
-
#
|
|
378
|
-
|
|
377
|
+
# Low-level ecosystem accessor (see __dommy_ convention) — the public
|
|
378
|
+
# Web API never exposes raw key bytes.
|
|
379
|
+
def __dommy_bytes__
|
|
379
380
|
@bytes
|
|
380
381
|
end
|
|
381
382
|
|
data/lib/dommy/css.rb
CHANGED
|
@@ -63,7 +63,7 @@ module Dommy
|
|
|
63
63
|
idx = index.nil? ? @css_rules.length : index.to_i
|
|
64
64
|
raise DOMException::IndexSizeError, "out of range" if idx < 0 || idx > @css_rules.length
|
|
65
65
|
|
|
66
|
-
@css_rules.
|
|
66
|
+
@css_rules.__internal_insert__(idx, CSSRule.new(rule_text.to_s, self))
|
|
67
67
|
idx
|
|
68
68
|
end
|
|
69
69
|
|
|
@@ -71,17 +71,17 @@ module Dommy
|
|
|
71
71
|
idx = index.to_i
|
|
72
72
|
raise DOMException::IndexSizeError, "out of range" if idx < 0 || idx >= @css_rules.length
|
|
73
73
|
|
|
74
|
-
@css_rules.
|
|
74
|
+
@css_rules.__internal_delete_at__(idx)
|
|
75
75
|
nil
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
# `replaceSync(text)` — replace all rules with a single rule blob
|
|
79
79
|
# (no parsing — we keep it as one opaque entry).
|
|
80
80
|
def replace_sync(text)
|
|
81
|
-
@css_rules.
|
|
81
|
+
@css_rules.__internal_clear__
|
|
82
82
|
return nil if text.to_s.empty?
|
|
83
83
|
|
|
84
|
-
@css_rules.
|
|
84
|
+
@css_rules.__internal_insert__(0, CSSRule.new(text.to_s, self))
|
|
85
85
|
nil
|
|
86
86
|
end
|
|
87
87
|
|
|
@@ -173,15 +173,15 @@ module Dommy
|
|
|
173
173
|
@rules.dup
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
-
def
|
|
176
|
+
def __internal_insert__(index, rule)
|
|
177
177
|
@rules.insert(index, rule)
|
|
178
178
|
end
|
|
179
179
|
|
|
180
|
-
def
|
|
180
|
+
def __internal_delete_at__(index)
|
|
181
181
|
@rules.delete_at(index)
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
-
def
|
|
184
|
+
def __internal_clear__
|
|
185
185
|
@rules.clear
|
|
186
186
|
end
|
|
187
187
|
|