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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -38
  3. data/lib/dommy/animation.rb +1 -1
  4. data/lib/dommy/attr.rb +23 -11
  5. data/lib/dommy/backend/nokogiri_adapter.rb +51 -0
  6. data/lib/dommy/backend/nokolexbor_adapter.rb +80 -0
  7. data/lib/dommy/backend.rb +129 -0
  8. data/lib/dommy/blob.rb +2 -2
  9. data/lib/dommy/compression_streams.rb +4 -4
  10. data/lib/dommy/cookie_store.rb +1 -1
  11. data/lib/dommy/crypto.rb +9 -8
  12. data/lib/dommy/css.rb +7 -7
  13. data/lib/dommy/custom_elements.rb +6 -6
  14. data/lib/dommy/document.rb +98 -32
  15. data/lib/dommy/dom_parser.rb +5 -4
  16. data/lib/dommy/element.rb +231 -50
  17. data/lib/dommy/event.rb +61 -25
  18. data/lib/dommy/event_source.rb +8 -8
  19. data/lib/dommy/fetch.rb +14 -6
  20. data/lib/dommy/file_reader.rb +3 -3
  21. data/lib/dommy/form_data.rb +1 -3
  22. data/lib/dommy/history.rb +7 -4
  23. data/lib/dommy/html_collection.rb +4 -4
  24. data/lib/dommy/html_elements.rb +110 -42
  25. data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
  26. data/lib/dommy/internal/dom_matching.rb +3 -3
  27. data/lib/dommy/internal/node_traversal.rb +1 -1
  28. data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
  29. data/lib/dommy/internal/template_content_registry.rb +6 -6
  30. data/lib/dommy/intersection_observer.rb +2 -2
  31. data/lib/dommy/location.rb +8 -4
  32. data/lib/dommy/media_query_list.rb +3 -3
  33. data/lib/dommy/message_channel.rb +9 -9
  34. data/lib/dommy/mutation_observer.rb +21 -11
  35. data/lib/dommy/navigator.rb +12 -12
  36. data/lib/dommy/node.rb +12 -0
  37. data/lib/dommy/notification.rb +3 -3
  38. data/lib/dommy/parser.rb +13 -13
  39. data/lib/dommy/performance_observer.rb +2 -2
  40. data/lib/dommy/range.rb +2 -2
  41. data/lib/dommy/resize_observer.rb +2 -2
  42. data/lib/dommy/shadow_root.rb +10 -8
  43. data/lib/dommy/streams.rb +22 -22
  44. data/lib/dommy/text_codec.rb +4 -4
  45. data/lib/dommy/tree_walker.rb +21 -21
  46. data/lib/dommy/url.rb +25 -8
  47. data/lib/dommy/version.rb +1 -1
  48. data/lib/dommy/web_socket.rb +13 -13
  49. data/lib/dommy/window.rb +14 -1
  50. data/lib/dommy/worker.rb +5 -5
  51. data/lib/dommy/xml_http_request.rb +19 -4
  52. data/lib/dommy.rb +12 -2
  53. metadata +12 -26
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ce9aec097da24f1f6a44792f44615605a1405483b6c6c5183031757d11a0853
4
- data.tar.gz: cf7c0e05839c313763590fcdb4d134459f5b1fe91ed1b479f819b18181cf1058
3
+ metadata.gz: ccbe61a606e042968621f7fa81588634f2cdae566d96cef137bfefdfb6d38dd6
4
+ data.tar.gz: 4358d9b68082608b7e78971be2d2818fb35200b17bec1819ce21a520b3552d7c
5
5
  SHA512:
6
- metadata.gz: 0db6e083fb62dd609fd719264a4d35e6bcadd6b97ce8515d08a8a8a53028e1f3f870224ef40ca33240688ddaf782d41e09d261c079c145144fe7b47b83288ef6
7
- data.tar.gz: d33e89e420bf0f93c2e478c93dd9241643ee47c40efa40927ff77714f20b92af561330b2cbedcaa287951cd07aab4ecfb899e24371fe8d6e2b20f289f6973b53
6
+ metadata.gz: a8c9a3b7bbfacb6f58440cbfec8884cc79c8690beda03225b229ca935f1b6d1ce0778c83a9253f2996198e46c8d8b85f9df53ccedb1ffa4e942a5b9841c051e4
7
+ data.tar.gz: f428a160a4c2f0180e7fa01f8cb1e6ec31dc9855f2bd7b43f1be6abd7a752a6b93926c1e57791a1c8b51a22e62631c997c4fbe8b55ea38fed5748f62cc874033
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Dommy
2
2
 
3
- A pure-Ruby DOM polyfill on top of [Nokogiri::HTML5](https://nokogiri.org/) a Ruby-side analogue to happy-dom / jsdom.
4
- Dommy exposes browser-like DOM semantics (events, MutationObserver, Custom Elements, Shadow DOM, File API, timers, Storage) so view / component / request specs can verify DOM structure and behavior without spinning up a real browser.
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 = dom.query_selector("input[type='file']")
81
- input.__set_files__([file])
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(dom.query_selector("form"))
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
- dropzone.dispatch_event(ev)
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`, `Response#text`, `localStorage.get_item`) return synchronous Ruby values — not Promises. `.await` is for the JS-bridged async surface.
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
- - 26 specialized HTMLElement subclasses
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 (WHATWG-leaning parsing with whitespace/tab/newline stripping, percent-encoding of unsafe path chars, `\` → `/` conversion for special schemes, `./` and `../` resolution, IPv4 number forms normalization, ws/wss default port stripping, Punycode hostname encoding, full UTS #46 IDNA with RFC 5893 Bidi and RFC 5892 ContextJ/ContextO)
189
+ - Location / History / URL
189
190
  - Storage
190
- - fetch (stub) / XMLHttpRequest (stub-driven, sync + async, shares the `__fetchy_stub__` fixture map)
191
- - WebSocket / EventSource — test seams (`__simulate_open__` / `__simulate_message__` / `__simulate_close__`) drive the streams
192
- - MessageChannel / MessagePort / BroadcastChannel — in-process pub/sub with `structuredClone`-on-transfer
193
- - FileReader (`readAsText` / `readAsDataURL` / `readAsArrayBuffer` / `readAsBinaryString`)
194
- - Notification (permission settable via `Notification.__set_permission__`)
195
- - Geolocation (mock position via `navigator.geolocation.__set_position__`)
196
- - `window.matchMedia` returning a `MediaQueryList` (`__set_matches__` flips and fires `change`)
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
- - Web Crypto (`crypto.randomUUID`, `getRandomValues`) / TextEncoder / TextDecoder
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; lifecycle via scheduler, finished/ready Promises)
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
- > - requires a layout / CSS engine or media subsystems: real `getBoundingClientRect` / scroll metrics
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 (`getBoundingClientRect` returns zero rects)
227
- > - SVG-specific value types (SVGAnimatedLength, SVGTransform, SVGMatrix)
228
- > - Web Animations: no actual value interpolation — `Animation` is a state machine (`idle` / `running` / `paused` / `finished`) for testing lifecycle and event wiring
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
 
@@ -244,7 +244,7 @@ module Dommy
244
244
  end
245
245
 
246
246
  # Event bubbling stops at Animation — it isn't part of the DOM tree.
247
- def __event_parent__
247
+ def __internal_event_parent__
248
248
  nil
249
249
  end
250
250
 
data/lib/dommy/attr.rb CHANGED
@@ -28,7 +28,7 @@ module Dommy
28
28
 
29
29
  def value
30
30
  if @owner
31
- @owner.__node__[@name].to_s
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 __attach__(element)
90
+ def __internal_attach__(element)
85
91
  @owner = element
86
92
  @detached_value = ""
87
93
  nil
88
94
  end
89
95
 
90
- def __detach__
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.__node__.attribute_nodes.size
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.__node__.attribute_nodes[index.to_i]&.name
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.__node__.key?(key)
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.__attach__(@element)
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.__node__.key?(key)
147
+ return nil unless @element.__dommy_backend_node__.key?(key)
142
148
 
143
- attr = Attr.new(key, owner: nil, value: @element.__node__[key].to_s)
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.__node__.attribute_nodes.each do |a|
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.__node__.key?(name.to_s.downcase) || super
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 __bytes__
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.__bytes__
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.__close__
34
+ @readable.__internal_close__
35
35
  end,
36
- "abort" => proc { |r| @readable.__error__(r) }
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.__close__
101
+ @readable.__internal_close__
102
102
  end,
103
- "abort" => proc { |r| @readable.__error__(r) }
103
+ "abort" => proc { |r| @readable.__internal_error__(r) }
104
104
  }
105
105
  )
106
106
  end
@@ -79,7 +79,7 @@ module Dommy
79
79
  end
80
80
  end
81
81
 
82
- def __event_parent__
82
+ def __internal_event_parent__
83
83
  nil
84
84
  end
85
85
 
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.__bytes__, coerce_bytes(data)).bytes
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.__bytes__, coerce_bytes(data))
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.__bytes__ : coerce_bytes(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
- # `__bytes__` accessor stays internal-only (`__double_underscore__`
364
- # convention) so production code paths can't read the raw bytes.
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
- # Test / internal seam production callers should not reach in.
378
- def __bytes__
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.__insert__(idx, CSSRule.new(rule_text.to_s, self))
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.__delete_at__(idx)
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.__clear__
81
+ @css_rules.__internal_clear__
82
82
  return nil if text.to_s.empty?
83
83
 
84
- @css_rules.__insert__(0, CSSRule.new(text.to_s, self))
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 __insert__(index, rule)
176
+ def __internal_insert__(index, rule)
177
177
  @rules.insert(index, rule)
178
178
  end
179
179
 
180
- def __delete_at__(index)
180
+ def __internal_delete_at__(index)
181
181
  @rules.delete_at(index)
182
182
  end
183
183
 
184
- def __clear__
184
+ def __internal_clear__
185
185
  @rules.clear
186
186
  end
187
187