dommy 0.7.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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dommy/animation.rb +9 -1
  3. data/lib/dommy/attr.rb +192 -39
  4. data/lib/dommy/backend/nokogiri_adapter.rb +76 -0
  5. data/lib/dommy/backend/nokolexbor_adapter.rb +37 -0
  6. data/lib/dommy/backend.rb +46 -0
  7. data/lib/dommy/blob.rb +28 -9
  8. data/lib/dommy/bridge/constructor_registry.rb +28 -0
  9. data/lib/dommy/bridge/methods.rb +57 -0
  10. data/lib/dommy/bridge.rb +97 -0
  11. data/lib/dommy/callable_invoker.rb +36 -0
  12. data/lib/dommy/cookie_store.rb +3 -1
  13. data/lib/dommy/crypto.rb +7 -1
  14. data/lib/dommy/css.rb +46 -0
  15. data/lib/dommy/custom_elements.rb +27 -3
  16. data/lib/dommy/data_transfer.rb +4 -0
  17. data/lib/dommy/document.rb +615 -48
  18. data/lib/dommy/dom_parser.rb +28 -15
  19. data/lib/dommy/element.rb +999 -471
  20. data/lib/dommy/event.rb +260 -96
  21. data/lib/dommy/event_source.rb +6 -2
  22. data/lib/dommy/fetch.rb +505 -43
  23. data/lib/dommy/file_reader.rb +11 -3
  24. data/lib/dommy/form_data.rb +2 -0
  25. data/lib/dommy/history.rb +43 -8
  26. data/lib/dommy/html_collection.rb +55 -2
  27. data/lib/dommy/html_elements.rb +102 -1519
  28. data/lib/dommy/internal/css_pseudo_handlers.rb +109 -0
  29. data/lib/dommy/internal/global_functions.rb +26 -0
  30. data/lib/dommy/internal/idna.rb +16 -7
  31. data/lib/dommy/internal/ipv4_parser.rb +22 -7
  32. data/lib/dommy/internal/mutation_coordinator.rb +11 -2
  33. data/lib/dommy/internal/namespaces.rb +70 -0
  34. data/lib/dommy/internal/node_equality.rb +86 -0
  35. data/lib/dommy/internal/node_wrapper_cache.rb +62 -27
  36. data/lib/dommy/internal/observable_callback.rb +1 -5
  37. data/lib/dommy/internal/parent_node.rb +126 -0
  38. data/lib/dommy/internal/reflected_attributes.rb +103 -13
  39. data/lib/dommy/internal/selector_parser.rb +664 -0
  40. data/lib/dommy/internal/url_parser.rb +677 -0
  41. data/lib/dommy/intersection_observer.rb +2 -0
  42. data/lib/dommy/location.rb +2 -0
  43. data/lib/dommy/media_query_list.rb +7 -1
  44. data/lib/dommy/message_channel.rb +32 -2
  45. data/lib/dommy/mutation_observer.rb +55 -12
  46. data/lib/dommy/navigator.rb +26 -12
  47. data/lib/dommy/node.rb +158 -28
  48. data/lib/dommy/notification.rb +3 -1
  49. data/lib/dommy/performance.rb +4 -0
  50. data/lib/dommy/performance_observer.rb +2 -0
  51. data/lib/dommy/promise.rb +14 -14
  52. data/lib/dommy/range.rb +74 -5
  53. data/lib/dommy/resize_observer.rb +2 -0
  54. data/lib/dommy/scheduler.rb +34 -13
  55. data/lib/dommy/shadow_root.rb +23 -54
  56. data/lib/dommy/storage.rb +2 -0
  57. data/lib/dommy/streams.rb +18 -27
  58. data/lib/dommy/svg_elements.rb +204 -3606
  59. data/lib/dommy/text_codec.rb +174 -21
  60. data/lib/dommy/tree_walker.rb +255 -66
  61. data/lib/dommy/url.rb +287 -449
  62. data/lib/dommy/url_pattern.rb +2 -0
  63. data/lib/dommy/version.rb +1 -1
  64. data/lib/dommy/web_socket.rb +37 -7
  65. data/lib/dommy/window.rb +202 -213
  66. data/lib/dommy/worker.rb +7 -7
  67. data/lib/dommy/xml_http_request.rb +15 -5
  68. data/lib/dommy.rb +7 -0
  69. metadata +12 -3
@@ -45,7 +45,8 @@ module Dommy
45
45
  alias readAsDataURL read_as_data_url
46
46
 
47
47
  def read_as_array_buffer(blob)
48
- schedule_read(blob) { |raw| raw.bytes }
48
+ # readAsArrayBuffer's result is an ArrayBuffer — cross it as a bare one.
49
+ schedule_read(blob) { |raw| Bridge::ArrayBuffer.new(raw.bytes) }
49
50
  end
50
51
 
51
52
  alias readAsArrayBuffer read_as_array_buffer
@@ -89,10 +90,17 @@ module Dommy
89
90
 
90
91
  def __js_set__(key, value)
91
92
  event = inline_event_for(key)
92
- set_inline_handler(event, value) if event
93
+ return Bridge::UNHANDLED unless event
94
+
95
+ set_inline_handler(event, value)
93
96
  nil
94
97
  end
95
98
 
99
+ include Bridge::Methods
100
+ js_methods %w[
101
+ readAsText readAsDataURL readAsArrayBuffer readAsBinaryString abort addEventListener
102
+ removeEventListener dispatchEvent
103
+ ]
96
104
  def __js_call__(method, args)
97
105
  case method
98
106
  when "readAsText"
@@ -108,7 +116,7 @@ module Dommy
108
116
  when "addEventListener"
109
117
  add_event_listener(args[0], args[1], args[2])
110
118
  when "removeEventListener"
111
- remove_event_listener(args[0], args[1])
119
+ remove_event_listener(args[0], args[1], args[2])
112
120
  when "dispatchEvent"
113
121
  dispatch_event(args[0])
114
122
  end
@@ -108,6 +108,8 @@ module Dommy
108
108
  end
109
109
  end
110
110
 
111
+ include Bridge::Methods
112
+ js_methods %w[append set get getAll has delete keys values entries forEach]
111
113
  def __js_call__(method, args)
112
114
  case method
113
115
  when "append"
data/lib/dommy/history.rb CHANGED
@@ -9,9 +9,10 @@ module Dommy
9
9
  def initialize(window, location)
10
10
  @window = window
11
11
  @location = location
12
- # Initial entry mirrors the live Location. Bookmark URL is
13
- # resynthesized lazily from Location each time we read it.
14
- @stack = [{state: nil, url: nil}]
12
+ # Each entry records the full href it navigated to, so back/forward can
13
+ # restore Location to it (and fire popstate) a restoration that
14
+ # framework routers like Turbo's depend on to swap the cached snapshot.
15
+ @stack = [{state: nil, url: @location.href}]
15
16
  @cursor = 0
16
17
  @scroll_restoration = "auto"
17
18
  end
@@ -34,11 +35,15 @@ module Dommy
34
35
  # values silently retain the current value.
35
36
  v = value.to_s
36
37
  @scroll_restoration = v if %w[auto manual].include?(v)
38
+ else
39
+ return Bridge::UNHANDLED
37
40
  end
38
41
 
39
42
  nil
40
43
  end
41
44
 
45
+ include Bridge::Methods
46
+ js_methods %w[pushState replaceState back forward go]
42
47
  def __js_call__(method, args)
43
48
  case method
44
49
  when "pushState"
@@ -57,18 +62,43 @@ module Dommy
57
62
  private
58
63
 
59
64
  def push(state, url)
65
+ resolved = resolve_url!(url)
60
66
  @stack = @stack[0..@cursor]
61
- @location.__internal_set_url__(url.to_s) if url
67
+ @location.__internal_set_url__(resolved) if resolved
62
68
  # WHATWG: pushState serializes the state via structured-clone
63
69
  # so subsequent caller-side mutation of the original cannot
64
70
  # affect history.state.
65
- @stack << {state: Dommy.structured_clone(state), url: nil}
71
+ @stack << {state: Dommy.structured_clone(state), url: @location.href}
66
72
  @cursor = @stack.size - 1
67
73
  end
68
74
 
69
75
  def replace(state, url)
70
- @location.__internal_set_url__(url.to_s) if url
71
- @stack[@cursor] = {state: Dommy.structured_clone(state), url: nil}
76
+ resolved = resolve_url!(url)
77
+ @location.__internal_set_url__(resolved) if resolved
78
+ @stack[@cursor] = {state: Dommy.structured_clone(state), url: @location.href}
79
+ end
80
+
81
+ # WHATWG "URL and history update steps": resolve the given URL against the
82
+ # document URL; a parse failure or a cross-origin result is a SecurityError
83
+ # (same-document history entries must stay same-origin).
84
+ def resolve_url!(url)
85
+ return nil if url.nil? || url.equal?(Bridge::UNDEFINED)
86
+
87
+ current = @location.href.to_s
88
+ resolved =
89
+ begin
90
+ current.empty? ? URL.new(url.to_s) : URL.new(url.to_s, current)
91
+ rescue StandardError
92
+ raise DOMException::SecurityError, "A history state object with URL '#{url}' cannot be created in a document with URL '#{current}'."
93
+ end
94
+
95
+ unless current.empty?
96
+ if resolved.origin != URL.new(current).origin
97
+ raise DOMException::SecurityError, "A history state object with URL '#{resolved.href}' cannot be created in a document with origin '#{URL.new(current).origin}'."
98
+ end
99
+ end
100
+
101
+ resolved.href
72
102
  end
73
103
 
74
104
  def go(delta)
@@ -76,7 +106,12 @@ module Dommy
76
106
  return if target < 0 || target >= @stack.size
77
107
 
78
108
  @cursor = target
79
- @window.fire_popstate(@stack[@cursor][:state])
109
+ entry = @stack[@cursor]
110
+ # Restore Location to the target entry's URL BEFORE firing popstate, so a
111
+ # listener that reads `location` (Turbo's restoration visit, the WHATWG
112
+ # traversal steps) sees the destination, not the page we came from.
113
+ @location.__internal_set_url__(entry[:url]) if entry[:url]
114
+ @window.fire_popstate(entry[:state])
80
115
  end
81
116
  end
82
117
  end
@@ -22,6 +22,25 @@ module Dommy
22
22
  @compute = compute
23
23
  end
24
24
 
25
+ # Shared `getElementsByTagNameNS(namespace, localName)` — a live collection
26
+ # of descendants of `root` matching the (namespace, localName) filter, where
27
+ # "*" matches any. An empty-string namespace means the null namespace.
28
+ def self.elements_by_tag_name_ns(root, document, namespace, local_name)
29
+ ns = namespace.to_s
30
+ ns_filter = ns == "*" ? :any : (ns.empty? ? nil : ns)
31
+ local = local_name.to_s
32
+ new do
33
+ nodes = local == "*" ? root.css("*") : root.css(local)
34
+ nodes.filter_map do |node|
35
+ el = document.wrap_node(node)
36
+ next nil unless el
37
+
38
+ el_ns = el.respond_to?(:namespace_uri) ? el.namespace_uri : nil
39
+ (ns_filter == :any || el_ns == ns_filter) ? el : nil
40
+ end
41
+ end
42
+ end
43
+
25
44
  def length
26
45
  to_a.length
27
46
  end
@@ -42,7 +61,10 @@ module Dommy
42
61
  # `namedItem(name)` returns the first element whose `id` or
43
62
  # `name` attribute equals `name`. Returns nil if no match.
44
63
  def named_item(name)
45
- key = name.to_s
64
+ # A numeric argument (`namedItem(2147483648)`) crosses from JS as a Float
65
+ # for values past int32; format it as an integer string so it matches an
66
+ # `id`/`name` attribute like "2147483648" (not "2147483648.0").
67
+ key = (name.is_a?(Float) && name.finite? && name == name.to_i) ? name.to_i.to_s : name.to_s
46
68
  return nil if key.empty?
47
69
 
48
70
  to_a.find do |el|
@@ -91,14 +113,41 @@ module Dommy
91
113
  item(key)
92
114
  else
93
115
  s = key.to_s
94
- if s.match?(/\A\d+\z/)
116
+ if s.match?(/\A\d+\z/) && s.to_i < 4_294_967_295
117
+ # A valid array index (0 ≤ n < 2^32-1) is a pure indexed lookup — out
118
+ # of range yields nil (→ undefined), never a named fallback.
95
119
  item(s.to_i)
96
120
  else
121
+ # Non-array-index strings (negative, ≥ 2^32-1, or names) use the named
122
+ # getter.
97
123
  named_item(s) || (s == "length" ? length : nil)
98
124
  end
99
125
  end
100
126
  end
101
127
 
128
+ # WebIDL "supported property names" for HTMLCollection: in tree order, each
129
+ # element contributes its non-empty `id`, then (if it is in the HTML
130
+ # namespace) its non-empty `name` — ignoring duplicates.
131
+ def __js_named_props__
132
+ names = []
133
+ to_a.each do |el|
134
+ next unless el.respond_to?(:__dommy_backend_node__)
135
+
136
+ node = el.__dommy_backend_node__
137
+ id = node["id"].to_s
138
+ names << id if !id.empty? && !names.include?(id)
139
+
140
+ name = node["name"].to_s
141
+ next if name.empty? || names.include?(name)
142
+
143
+ html_ns = !el.respond_to?(:namespace_uri) || el.namespace_uri == "http://www.w3.org/1999/xhtml"
144
+ names << name if html_ns
145
+ end
146
+ names
147
+ end
148
+
149
+ include Bridge::Methods
150
+ js_methods %w[item namedItem]
102
151
  def __js_call__(method, args)
103
152
  case method
104
153
  when "item"
@@ -188,11 +237,15 @@ module Dommy
188
237
  self.selected_index = value
189
238
  when "length"
190
239
  self.length = value
240
+ else
241
+ return Bridge::UNHANDLED
191
242
  end
192
243
 
193
244
  nil
194
245
  end
195
246
 
247
+ # Adds add/remove on top of the inherited item/namedItem (else -> super).
248
+ js_methods %w[add remove]
196
249
  def __js_call__(method, args)
197
250
  case method
198
251
  when "add"