dommy 0.16.0 → 0.17.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dommy/animation.rb +1 -0
  3. data/lib/dommy/backend.rb +3 -0
  4. data/lib/dommy/blob.rb +6 -0
  5. data/lib/dommy/browser.rb +12 -4
  6. data/lib/dommy/character_data.rb +1 -13
  7. data/lib/dommy/cookie_store.rb +2 -1
  8. data/lib/dommy/document.rb +67 -30
  9. data/lib/dommy/element.rb +11 -0
  10. data/lib/dommy/element_internals.rb +12 -0
  11. data/lib/dommy/encodings/legacy_multibyte.rb +800 -0
  12. data/lib/dommy/encodings/multibyte_overrides.rb +76 -0
  13. data/lib/dommy/encodings/prescan.rb +234 -0
  14. data/lib/dommy/encodings.rb +121 -57
  15. data/lib/dommy/event.rb +107 -54
  16. data/lib/dommy/event_source.rb +1 -0
  17. data/lib/dommy/fetch.rb +244 -53
  18. data/lib/dommy/file_reader.rb +1 -0
  19. data/lib/dommy/form_data.rb +22 -9
  20. data/lib/dommy/form_entry_list.rb +3 -3
  21. data/lib/dommy/html_canvas_element.rb +85 -12
  22. data/lib/dommy/html_elements/base.rb +1 -0
  23. data/lib/dommy/html_elements/forms/input.rb +3 -0
  24. data/lib/dommy/html_elements/interactive.rb +56 -48
  25. data/lib/dommy/html_elements/metadata.rb +23 -0
  26. data/lib/dommy/interaction/form_submission.rb +24 -42
  27. data/lib/dommy/internal/accessibility_tree.rb +149 -162
  28. data/lib/dommy/internal/accessibility_visibility.rb +16 -17
  29. data/lib/dommy/internal/accessible_name.rb +231 -246
  30. data/lib/dommy/internal/aria_role.rb +182 -195
  31. data/lib/dommy/internal/aria_snapshot.rb +35 -38
  32. data/lib/dommy/internal/aria_state.rb +102 -118
  33. data/lib/dommy/internal/autofill.rb +75 -75
  34. data/lib/dommy/internal/backend_prefilter.rb +127 -130
  35. data/lib/dommy/internal/child_navigable.rb +86 -83
  36. data/lib/dommy/internal/close_watcher_manager.rb +1 -0
  37. data/lib/dommy/internal/css/ancestor_filter.rb +64 -64
  38. data/lib/dommy/internal/css/calc.rb +90 -71
  39. data/lib/dommy/internal/css/cascade.rb +69 -67
  40. data/lib/dommy/internal/css/color.rb +249 -187
  41. data/lib/dommy/internal/css/computed_style_builder.rb +34 -0
  42. data/lib/dommy/internal/css/computed_style_declaration.rb +50 -11
  43. data/lib/dommy/internal/css/counters.rb +67 -67
  44. data/lib/dommy/internal/css/custom_properties.rb +114 -114
  45. data/lib/dommy/internal/css/parser.rb +277 -239
  46. data/lib/dommy/internal/css/property_registry.rb +321 -302
  47. data/lib/dommy/internal/css/renderability.rb +37 -35
  48. data/lib/dommy/internal/css/rule_index.rb +2 -2
  49. data/lib/dommy/internal/css/rule_index_inputs.rb +2 -2
  50. data/lib/dommy/internal/css/supports.rb +115 -113
  51. data/lib/dommy/internal/css/ua_stylesheet.rb +55 -53
  52. data/lib/dommy/internal/css_rule_text.rb +121 -119
  53. data/lib/dommy/internal/css_source.rb +1 -3
  54. data/lib/dommy/internal/css_syntax.rb +157 -92
  55. data/lib/dommy/internal/declarative_shadow_roots.rb +1 -0
  56. data/lib/dommy/internal/directionality.rb +120 -123
  57. data/lib/dommy/internal/document_live_ranges.rb +26 -1
  58. data/lib/dommy/internal/dom_matching.rb +185 -184
  59. data/lib/dommy/internal/element_geometry.rb +8 -1
  60. data/lib/dommy/internal/element_matching.rb +71 -69
  61. data/lib/dommy/internal/element_shadow.rb +1 -13
  62. data/lib/dommy/internal/element_state.rb +255 -265
  63. data/lib/dommy/internal/event_handlers.rb +66 -64
  64. data/lib/dommy/internal/exception_report.rb +88 -86
  65. data/lib/dommy/internal/focusability.rb +205 -205
  66. data/lib/dommy/internal/global_functions.rb +49 -47
  67. data/lib/dommy/internal/idl_reflection.rb +99 -97
  68. data/lib/dommy/internal/js_number.rb +35 -33
  69. data/lib/dommy/internal/light_dismiss.rb +130 -132
  70. data/lib/dommy/internal/literal_lookup.rb +75 -75
  71. data/lib/dommy/internal/mime_type.rb +54 -49
  72. data/lib/dommy/internal/mutation_coordinator.rb +46 -0
  73. data/lib/dommy/internal/mutation_observer_microtask.rb +60 -0
  74. data/lib/dommy/internal/namespaces.rb +51 -49
  75. data/lib/dommy/internal/node_adopter.rb +25 -1
  76. data/lib/dommy/internal/node_equality.rb +61 -59
  77. data/lib/dommy/internal/post_insertion_steps.rb +1 -0
  78. data/lib/dommy/internal/referrer.rb +144 -0
  79. data/lib/dommy/internal/rendered_text/collector.rb +22 -3
  80. data/lib/dommy/internal/rendered_text/computed_box.rb +3 -2
  81. data/lib/dommy/internal/rendered_text/fragment.rb +37 -38
  82. data/lib/dommy/internal/rendered_text/items.rb +7 -5
  83. data/lib/dommy/internal/selector_matcher.rb +361 -359
  84. data/lib/dommy/internal/selector_parser.rb +74 -62
  85. data/lib/dommy/internal/sequential_focus_navigation.rb +150 -151
  86. data/lib/dommy/internal/shadow_root_registry.rb +7 -0
  87. data/lib/dommy/internal/slot_assignment.rb +117 -0
  88. data/lib/dommy/internal/slot_signals.rb +125 -0
  89. data/lib/dommy/internal/unsafe_html.rb +79 -77
  90. data/lib/dommy/internal/url_parser.rb +5 -29
  91. data/lib/dommy/internal/web_idl.rb +158 -153
  92. data/lib/dommy/internal/window_bound.rb +14 -0
  93. data/lib/dommy/internal/window_constructors.rb +3 -0
  94. data/lib/dommy/internal/xml_serialization.rb +298 -296
  95. data/lib/dommy/js/dom_interfaces.rb +13 -1
  96. data/lib/dommy/js/host_bridge.rb +23 -1
  97. data/lib/dommy/js/host_callbacks.rb +7 -0
  98. data/lib/dommy/js/host_runtime.js +625 -61
  99. data/lib/dommy/js/import_map.rb +1 -1
  100. data/lib/dommy/js/observable_runtime.js +295 -128
  101. data/lib/dommy/js/webidl_members.js +1 -1
  102. data/lib/dommy/js/webidl_signatures.js +35 -6
  103. data/lib/dommy/media_query_list.rb +1 -0
  104. data/lib/dommy/message_channel.rb +2 -0
  105. data/lib/dommy/mutation_observer.rb +25 -29
  106. data/lib/dommy/navigator.rb +4 -1
  107. data/lib/dommy/node.rb +4 -0
  108. data/lib/dommy/notification.rb +1 -0
  109. data/lib/dommy/promise.rb +12 -0
  110. data/lib/dommy/range.rb +20 -3
  111. data/lib/dommy/shadow_root.rb +25 -1
  112. data/lib/dommy/streams.rb +18 -0
  113. data/lib/dommy/style_declaration.rb +15 -4
  114. data/lib/dommy/text_codec.rb +7 -5
  115. data/lib/dommy/tree_walker.rb +68 -61
  116. data/lib/dommy/version.rb +1 -1
  117. data/lib/dommy/web_socket.rb +1 -0
  118. data/lib/dommy/window.rb +22 -4
  119. data/lib/dommy/worker.rb +1 -0
  120. data/lib/dommy/xml_http_request.rb +12 -2
  121. data/lib/dommy.rb +17 -2
  122. metadata +11 -3
@@ -58,193 +58,180 @@ module Dommy
58
58
 
59
59
  extend TextFlattening
60
60
 
61
- module_function
62
-
63
- # Build the tree for `scope` (a Document — started at its <body> — or an
64
- # Element). Returns a synthetic `:root` Node whose children are the
65
- # accessible nodes the scope contributes (so a Document has no document
66
- # line, matching Playwright).
67
- def build(scope)
68
- start = scope.respond_to?(:body) ? scope.body : scope
69
- root = Node.new(role: :root, element: scope)
70
- root.children = start ? nodes_for(start) : []
71
- # Text is carried raw through construction so adjacent runs concatenate
72
- # with correct (block vs inline) spacing; collapse it to its final form
73
- # once, here.
74
- finalize_text(root)
75
- root
76
- end
61
+ class << self
62
+ # Build the tree for `scope` (a Document — started at its <body> — or an
63
+ # Element). Returns a synthetic `:root` Node whose children are the
64
+ # accessible nodes the scope contributes (so a Document has no document
65
+ # line, matching Playwright).
66
+ def build(scope)
67
+ start = scope.respond_to?(:body) ? scope.body : scope
68
+ root = Node.new(role: :root, element: scope)
69
+ root.children = start ? nodes_for(start) : []
70
+ # Text is carried raw through construction so adjacent runs concatenate
71
+ # with correct (block vs inline) spacing; collapse it to its final form
72
+ # once, here.
73
+ finalize_text(root)
74
+ root
75
+ end
77
76
 
78
- # Squish each text node's accumulated content and drop the empties,
79
- # depth-first. Coalescing already merged adjacent runs during build.
80
- def finalize_text(node)
81
- node.children = node.children.filter_map do |child|
82
- if child.text?
83
- text = squish(child.name)
84
- Node.new(role: :text, name: text) unless text.empty?
85
- else
86
- finalize_text(child)
87
- child
77
+ private
78
+
79
+ # Squish each text node's accumulated content and drop the empties,
80
+ # depth-first. Coalescing already merged adjacent runs during build.
81
+ def finalize_text(node)
82
+ node.children = node.children.filter_map do |child|
83
+ if child.text?
84
+ text = squish(child.name)
85
+ Node.new(role: :text, name: text) unless text.empty?
86
+ else
87
+ finalize_text(child)
88
+ child
89
+ end
88
90
  end
91
+ node
89
92
  end
90
- node
91
- end
92
93
 
93
- # The accessible nodes an element contributes to its parent: 0 (excluded),
94
- # 1 (a real node), or many (its promoted children when it is
95
- # presentational / generic).
96
- def nodes_for(element)
97
- return [] if excluded?(element)
98
- return [] if lone_unscoped_th?(element)
99
-
100
- role = AriaRole.compute(element)
101
- children = build_children(element)
102
-
103
- # Presentational and generic/roleless containers drop out; their
104
- # children are promoted to the parent. A block-level box separates its
105
- # text from siblings, so its promoted run is padded with whitespace.
106
- if role == "none" || role == "" || role == "generic"
107
- return AccessibleName.block_level?(element) ? [text_node(" "), *children, text_node(" ")] : children
108
- end
109
-
110
- node = Node.new(
111
- role: role,
112
- name: AccessibleName.compute(element),
113
- states: AriaState.compute(element, role),
114
- element: element
115
- )
116
- # Name-from-content roles fold their descendant text into the name, so
117
- # those text nodes are not emitted again; descendant roled nodes stay.
118
- node.children = AccessibleName::NAME_FROM_CONTENT.include?(role) ? children.reject(&:text?) : children
119
- # When a node's sole content is text that just repeats its (author-
120
- # supplied) accessible name, that text is not exposed again
121
- # (aria-label="X">X is `banner "X"`, but a text sibling keeps it).
122
- node.children = [] if sole_name_text?(node)
123
- # A native range / number / color control shows its value as inline text.
124
- value = native_widget_value(element)
125
- node.children << text_node(value) if value
126
- [node]
127
- end
94
+ # The accessible nodes an element contributes to its parent: 0 (excluded),
95
+ # 1 (a real node), or many (its promoted children when it is
96
+ # presentational / generic).
97
+ def nodes_for(element)
98
+ return [] if excluded?(element)
99
+ return [] if lone_unscoped_th?(element)
100
+
101
+ role = AriaRole.compute(element)
102
+ children = build_children(element)
103
+
104
+ # Presentational and generic/roleless containers drop out; their
105
+ # children are promoted to the parent. A block-level box separates its
106
+ # text from siblings, so its promoted run is padded with whitespace.
107
+ if role == "none" || role == "" || role == "generic"
108
+ return AccessibleName.block_level?(element) ? [text_node(" "), *children, text_node(" ")] : children
109
+ end
128
110
 
129
- def sole_name_text?(node)
130
- return false unless node.children.size == 1 && !node.name.to_s.empty?
111
+ node = Node.new(
112
+ role: role,
113
+ name: AccessibleName.compute(element),
114
+ states: AriaState.compute(element, role),
115
+ element: element
116
+ )
117
+ # Name-from-content roles fold their descendant text into the name, so
118
+ # those text nodes are not emitted again; descendant roled nodes stay.
119
+ node.children = AccessibleName::NAME_FROM_CONTENT.include?(role) ? children.reject(&:text?) : children
120
+ # When a node's sole content is text that just repeats its (author-
121
+ # supplied) accessible name, that text is not exposed again
122
+ # (aria-label="X">X is `banner "X"`, but a text sibling keeps it).
123
+ node.children = [] if sole_name_text?(node)
124
+ # A native range / number / color control shows its value as inline text.
125
+ value = native_widget_value(element)
126
+ node.children << text_node(value) if value
127
+ [node]
128
+ end
131
129
 
132
- child = node.children.first
133
- child.text? && squish(child.name) == node.name
134
- end
130
+ def sole_name_text?(node)
131
+ return false unless node.children.size == 1 && !node.name.to_s.empty?
135
132
 
136
- # Walk an element's child nodes in document order: significant text
137
- # becomes a :text node; elements recurse (and may promote).
138
- def build_children(element)
139
- out = []
140
- element.child_nodes.each do |child|
141
- if child.is_a?(Dommy::Element)
142
- out.concat(nodes_for(child))
143
- elsif child.is_a?(Dommy::TextNode)
144
- # Keep the text raw (whitespace and all); spacing is resolved when
145
- # adjacent runs are coalesced and finally squished.
146
- out << text_node(child.text_content.to_s)
147
- end
133
+ child = node.children.first
134
+ child.text? && squish(child.name) == node.name
148
135
  end
149
- coalesce_text(out)
150
- end
151
136
 
152
- # Merge runs of adjacent text nodes into one by direct concatenation —
153
- # inline content glues ("Save" + "Save" -> "SaveSave"); the whitespace that
154
- # separates block-level content comes from the padding added when a
155
- # block box is promoted (see nodes_for).
156
- def coalesce_text(nodes)
157
- nodes.each_with_object([]) do |node, out|
158
- if node.text? && out.last&.text?
159
- out[-1] = text_node("#{out.last.name}#{node.name}")
160
- else
161
- out << node
137
+ # Walk an element's child nodes in document order: significant text
138
+ # becomes a :text node; elements recurse (and may promote).
139
+ def build_children(element)
140
+ out = []
141
+ element.child_nodes.each do |child|
142
+ if child.is_a?(Dommy::Element)
143
+ out.concat(nodes_for(child))
144
+ elsif child.is_a?(Dommy::TextNode)
145
+ # Keep the text raw (whitespace and all); spacing is resolved when
146
+ # adjacent runs are coalesced and finally squished.
147
+ out << text_node(child.text_content.to_s)
148
+ end
162
149
  end
150
+ coalesce_text(out)
163
151
  end
164
- end
165
-
166
- def text_node(text) = Node.new(role: :text, name: text)
167
152
 
168
- # The displayed value of a native range / number / color input (the only
169
- # widgets whose value Chromium puts in the snapshot — ARIA aria-valuenow /
170
- # progress / meter show nothing). A range always shows a value (defaulting
171
- # to the midpoint of its min/max); color defaults to "#000000"; a number
172
- # shows one only when non-empty.
173
- def native_widget_value(element)
174
- return nil unless element.local_name.to_s.casecmp?("input")
153
+ # Merge runs of adjacent text nodes into one by direct concatenation —
154
+ # inline content glues ("Save" + "Save" -> "SaveSave"); the whitespace that
155
+ # separates block-level content comes from the padding added when a
156
+ # block box is promoted (see nodes_for).
157
+ def coalesce_text(nodes)
158
+ nodes.each_with_object([]) do |node, out|
159
+ if node.text? && out.last&.text?
160
+ out[-1] = text_node("#{out.last.name}#{node.name}")
161
+ else
162
+ out << node
163
+ end
164
+ end
165
+ end
175
166
 
176
- value = element.value.to_s
177
- case element.__internal_attribute_value__("type").to_s.downcase
178
- when "range" then value.empty? ? range_default(element) : value
179
- when "color" then value.empty? ? "#000000" : value
180
- when "number" then value.empty? ? nil : value
167
+ def text_node(text) = Node.new(role: :text, name: text)
168
+
169
+ # The displayed value of a native range / number / color input (the only
170
+ # widgets whose value Chromium puts in the snapshot — ARIA aria-valuenow /
171
+ # progress / meter show nothing). A range always shows a value (defaulting
172
+ # to the midpoint of its min/max); color defaults to "#000000"; a number
173
+ # shows one only when non-empty.
174
+ def native_widget_value(element)
175
+ return nil unless element.local_name.to_s.casecmp?("input")
176
+
177
+ value = element.value.to_s
178
+ case element.__internal_attribute_value__("type").to_s.downcase
179
+ when "range" then value.empty? ? range_default(element) : value
180
+ when "color" then value.empty? ? "#000000" : value
181
+ when "number" then value.empty? ? nil : value
182
+ end
181
183
  end
182
- end
183
184
 
184
- def range_default(element)
185
- min = numeric(element.__internal_attribute_value__("min"), 0.0)
186
- max = numeric(element.__internal_attribute_value__("max"), 100.0)
187
- format_number(min + ((max - min) / 2.0))
188
- end
185
+ def range_default(element)
186
+ min = numeric(element.__internal_attribute_value__("min"), 0.0)
187
+ max = numeric(element.__internal_attribute_value__("max"), 100.0)
188
+ format_number(min + ((max - min) / 2.0))
189
+ end
189
190
 
190
- def numeric(value, fallback)
191
- Float(value)
192
- rescue ArgumentError, TypeError
193
- fallback
194
- end
191
+ def numeric(value, fallback)
192
+ Float(value)
193
+ rescue ArgumentError, TypeError
194
+ fallback
195
+ end
195
196
 
196
- def format_number(number)
197
- number == number.to_i ? number.to_i.to_s : number.to_s
198
- end
197
+ def format_number(number)
198
+ number == number.to_i ? number.to_i.to_s : number.to_s
199
+ end
199
200
 
200
- # An element (and its whole subtree) is excluded from the tree when
201
- # `aria-hidden="true"` or when it is not visually rendered. `visible?`
202
- # deliberately ignores aria-hidden, so it is checked here. An element
203
- # with the inert attribute is excluded too (HTML: inert nodes are not
204
- # exposed to accessibility APIs) — unless the modal dialog that
205
- # escapes its inertness sits inside it.
206
- def excluded?(element)
207
- AccessibilityVisibility.hidden?(element) || inert_subtree?(element)
208
- end
201
+ # An element (and its whole subtree) is excluded from the tree when
202
+ # `aria-hidden="true"` or when it is not visually rendered. `visible?`
203
+ # deliberately ignores aria-hidden, so it is checked here. An element
204
+ # with the inert attribute is excluded too (HTML: inert nodes are not
205
+ # exposed to accessibility APIs) — unless the modal dialog that
206
+ # escapes its inertness sits inside it.
207
+ def excluded?(element)
208
+ AccessibilityVisibility.hidden?(element) || inert_subtree?(element)
209
+ end
209
210
 
210
- def inert_subtree?(element)
211
- return false unless element.namespace_uri == Namespaces::HTML && element.__internal_has_attribute__?("inert")
211
+ def inert_subtree?(element)
212
+ return false unless element.namespace_uri == Namespaces::HTML && element.__internal_has_attribute__?("inert")
212
213
 
213
- blocker = element.owner_document.__internal_blocking_modal_dialog__
214
- !(blocker && element.contains?(blocker))
215
- end
214
+ blocker = element.owner_document.__internal_blocking_modal_dialog__
215
+ !(blocker && element.contains?(blocker))
216
+ end
216
217
 
217
- # A lone, unscoped <th> that is the only cell of the only row of its table
218
- # is not emitted as a header cell — Chromium folds it into the row's
219
- # accessible name. An explicit `scope`, a sibling cell, or a second row all
220
- # make it a real header.
221
- def lone_unscoped_th?(element)
222
- return false unless element.local_name.to_s.casecmp?("th")
223
- return false unless element.__internal_attribute_value__("scope").to_s.empty?
218
+ # A lone, unscoped <th> that is the only cell of the only row of its table
219
+ # is not emitted as a header cell — Chromium folds it into the row's
220
+ # accessible name. An explicit `scope`, a sibling cell, or a second row all
221
+ # make it a real header.
222
+ def lone_unscoped_th?(element)
223
+ return false unless element.local_name.to_s.casecmp?("th")
224
+ return false unless element.__internal_attribute_value__("scope").to_s.empty?
224
225
 
225
- table = element.respond_to?(:closest) ? element.closest("table") : nil
226
- return false unless table
226
+ table = element.respond_to?(:closest) ? element.closest("table") : nil
227
+ return false unless table
227
228
 
228
- rows = table.query_selector_all("tr").to_a
229
- return false unless rows.size == 1
229
+ rows = table.query_selector_all("tr").to_a
230
+ return false unless rows.size == 1
230
231
 
231
- rows.first.query_selector_all("td, th").to_a.size == 1
232
+ rows.first.query_selector_all("td, th").to_a.size == 1
233
+ end
232
234
  end
233
-
234
-
235
- # Everything below compute is how it is computed, not API.
236
- private_class_method :finalize_text
237
- private_class_method :nodes_for
238
- private_class_method :sole_name_text?
239
- private_class_method :build_children
240
- private_class_method :coalesce_text
241
- private_class_method :text_node
242
- private_class_method :native_widget_value
243
- private_class_method :range_default
244
- private_class_method :numeric
245
- private_class_method :format_number
246
- private_class_method :excluded?, :inert_subtree?
247
- private_class_method :lone_unscoped_th?
248
235
  end
249
236
  end
250
237
  end
@@ -16,29 +16,28 @@ module Dommy
16
16
  # `display: none` by the UA sheet, so the two agree wherever CSS is
17
17
  # available and this covers the case where it is not.
18
18
  module AccessibilityVisibility
19
- module_function
19
+ class << self
20
+ def hidden?(element)
21
+ return true if aria_hidden?(element)
20
22
 
21
- def hidden?(element)
22
- return true if aria_hidden?(element)
23
+ !DomMatching.visible?(element)
24
+ end
23
25
 
24
- !DomMatching.visible?(element)
25
- end
26
+ # The name-from-content variant. A node the author pointed at directly
27
+ # with aria-labelledby is still named even when hidden — this only governs
28
+ # the traversal INTO a subtree.
29
+ def hidden_for_name?(element)
30
+ return true if element.respond_to?(:__internal_has_attribute__?) && element.__internal_has_attribute__?("hidden")
26
31
 
27
- # The name-from-content variant. A node the author pointed at directly
28
- # with aria-labelledby is still named even when hidden — this only governs
29
- # the traversal INTO a subtree.
30
- def hidden_for_name?(element)
31
- return true if element.respond_to?(:__internal_has_attribute__?) && element.__internal_has_attribute__?("hidden")
32
+ hidden?(element)
33
+ end
32
34
 
33
- hidden?(element)
34
- end
35
+ private
35
36
 
36
- def aria_hidden?(element)
37
- element.__internal_attribute_value__("aria-hidden").to_s.casecmp?("true")
37
+ def aria_hidden?(element)
38
+ element.__internal_attribute_value__("aria-hidden").to_s.casecmp?("true")
39
+ end
38
40
  end
39
-
40
- # Everything above is the module; everything below is how.
41
- private_class_method :aria_hidden?
42
41
  end
43
42
  end
44
43
  end