dommy 0.14.0 → 0.16.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 +15 -1
- data/lib/dommy/attr.rb +56 -32
- data/lib/dommy/backend.rb +251 -208
- data/lib/dommy/blob.rb +24 -0
- data/lib/dommy/bridge/methods.rb +12 -2
- data/lib/dommy/bridge/values.rb +29 -2
- data/lib/dommy/bridge/wire_tags.rb +8 -4
- data/lib/dommy/browser.rb +328 -53
- data/lib/dommy/callable_invoker.rb +27 -31
- data/lib/dommy/character_data.rb +21 -28
- data/lib/dommy/class_list.rb +55 -23
- data/lib/dommy/cookie_jar.rb +408 -0
- data/lib/dommy/cookie_store.rb +104 -49
- data/lib/dommy/css.rb +37 -8
- data/lib/dommy/custom_elements.rb +360 -116
- data/lib/dommy/data_transfer.rb +22 -2
- data/lib/dommy/document.rb +1258 -346
- data/lib/dommy/dom_exception.rb +6 -0
- data/lib/dommy/dom_parser.rb +46 -14
- data/lib/dommy/dom_string_list.rb +36 -0
- data/lib/dommy/element.rb +408 -569
- data/lib/dommy/element_internals.rb +215 -0
- data/lib/dommy/encodings.rb +10 -0
- data/lib/dommy/event.rb +382 -148
- data/lib/dommy/event_source.rb +8 -34
- data/lib/dommy/fetch.rb +8 -12
- data/lib/dommy/file_reader.rb +5 -29
- data/lib/dommy/form_data.rb +53 -16
- data/lib/dommy/form_entry_list.rb +107 -138
- data/lib/dommy/fragment.rb +14 -16
- data/lib/dommy/history.rb +247 -90
- data/lib/dommy/html_canvas_element.rb +15 -35
- data/lib/dommy/html_collection.rb +76 -50
- data/lib/dommy/html_elements/base.rb +377 -71
- data/lib/dommy/html_elements/embedded.rb +350 -106
- data/lib/dommy/html_elements/forms/form.rb +424 -383
- data/lib/dommy/html_elements/forms/input.rb +351 -189
- data/lib/dommy/html_elements/forms/select.rb +97 -81
- data/lib/dommy/html_elements/interactive.rb +285 -72
- data/lib/dommy/html_elements/media.rb +73 -32
- data/lib/dommy/html_elements/metadata.rb +226 -78
- data/lib/dommy/html_elements/miscellaneous.rb +58 -14
- data/lib/dommy/html_elements/tables.rb +20 -42
- data/lib/dommy/html_elements.rb +28 -3
- data/lib/dommy/hyperlink_utils.rb +19 -8
- data/lib/dommy/interaction/drag_and_drop.rb +187 -0
- data/lib/dommy/interaction/event_synthesis.rb +182 -24
- data/lib/dommy/interaction/field_interactor.rb +18 -6
- data/lib/dommy/interaction/form_submission.rb +12 -6
- data/lib/dommy/interaction/key_sender.rb +102 -17
- data/lib/dommy/interaction.rb +1 -0
- data/lib/dommy/internal/accessibility_tree.rb +19 -7
- data/lib/dommy/internal/accessibility_visibility.rb +2 -2
- data/lib/dommy/internal/accessible_description.rb +3 -3
- data/lib/dommy/internal/accessible_name.rb +33 -45
- data/lib/dommy/internal/an_plus_b.rb +125 -0
- data/lib/dommy/internal/aria_role.rb +27 -18
- data/lib/dommy/internal/aria_state.rb +7 -7
- data/lib/dommy/internal/autofill.rb +2 -2
- data/lib/dommy/internal/backend_prefilter.rb +21 -18
- data/lib/dommy/internal/ce_reactions.rb +285 -0
- data/lib/dommy/internal/ce_reactions_table.rb +296 -0
- data/lib/dommy/internal/child_list.rb +39 -0
- data/lib/dommy/internal/child_navigable.rb +155 -0
- data/lib/dommy/internal/child_node.rb +44 -41
- data/lib/dommy/internal/close_watcher_manager.rb +238 -0
- data/lib/dommy/internal/complex_match.rb +129 -0
- data/lib/dommy/internal/constraint_validation.rb +130 -0
- data/lib/dommy/internal/css/ancestor_filter.rb +107 -0
- data/lib/dommy/internal/css/cascade.rb +7 -7
- data/lib/dommy/internal/css/cascaded_declarations.rb +14 -20
- data/lib/dommy/internal/css/computed_style_builder.rb +30 -3
- data/lib/dommy/internal/css/custom_properties.rb +63 -80
- data/lib/dommy/internal/css/parser.rb +53 -63
- data/lib/dommy/internal/css/renderability.rb +6 -0
- data/lib/dommy/internal/css/rule_index.rb +170 -108
- data/lib/dommy/internal/css/rule_index_inputs.rb +135 -0
- data/lib/dommy/internal/css/selector_dependencies.rb +8 -2
- data/lib/dommy/internal/css/style_cache.rb +9 -6
- data/lib/dommy/internal/css/ua_stylesheet.rb +37 -2
- data/lib/dommy/internal/css_source.rb +223 -0
- data/lib/dommy/internal/css_syntax.rb +153 -0
- data/lib/dommy/internal/declarative_shadow_roots.rb +182 -0
- data/lib/dommy/internal/directionality.rb +2 -2
- data/lib/dommy/internal/document_generations.rb +91 -18
- data/lib/dommy/internal/document_interaction_state.rb +293 -9
- data/lib/dommy/internal/dom_matching.rb +7 -7
- data/lib/dommy/internal/element_aria.rb +65 -23
- data/lib/dommy/internal/element_css_inline_style.rb +26 -0
- data/lib/dommy/internal/element_editing.rb +41 -0
- data/lib/dommy/internal/element_nonce.rb +28 -0
- data/lib/dommy/internal/element_popover.rb +310 -0
- data/lib/dommy/internal/element_shadow.rb +107 -33
- data/lib/dommy/internal/element_state.rb +131 -26
- data/lib/dommy/internal/element_top_layer.rb +21 -51
- data/lib/dommy/internal/enumerated_keyword_sets.rb +40 -0
- data/lib/dommy/internal/event_handler_tables.rb +241 -0
- data/lib/dommy/internal/event_handlers.rb +149 -0
- data/lib/dommy/internal/exception_report.rb +1 -1
- data/lib/dommy/internal/focusability.rb +267 -0
- data/lib/dommy/internal/form_associated_custom_elements.rb +127 -0
- data/lib/dommy/internal/html_or_svg_or_mathml_element.rb +86 -0
- data/lib/dommy/internal/html_serialization.rb +292 -0
- data/lib/dommy/internal/idl_reflection.rb +202 -0
- data/lib/dommy/internal/idl_reflection_table.rb +589 -0
- data/lib/dommy/internal/infra.rb +36 -0
- data/lib/dommy/internal/input_type.rb +90 -20
- data/lib/dommy/internal/insertion_point.rb +27 -3
- data/lib/dommy/internal/js_number.rb +60 -0
- data/lib/dommy/internal/light_dismiss.rb +178 -0
- data/lib/dommy/internal/literal_lookup.rb +103 -0
- data/lib/dommy/internal/mime_type.rb +8 -0
- data/lib/dommy/internal/mutation_coordinator.rb +94 -60
- data/lib/dommy/internal/node_adopter.rb +165 -97
- data/lib/dommy/internal/node_factory.rb +111 -49
- data/lib/dommy/internal/node_identity.rb +1 -1
- data/lib/dommy/internal/node_traversal.rb +30 -13
- data/lib/dommy/internal/node_wrapper_cache.rb +34 -148
- data/lib/dommy/internal/observable_callback.rb +2 -5
- data/lib/dommy/internal/observer_options.rb +24 -14
- data/lib/dommy/internal/origin.rb +86 -0
- data/lib/dommy/internal/parent_node.rb +12 -10
- data/lib/dommy/internal/popover_invoker_element.rb +67 -0
- data/lib/dommy/internal/popover_stack.rb +120 -0
- data/lib/dommy/internal/post_insertion_steps.rb +146 -77
- data/lib/dommy/internal/reflected_attributes.rb +173 -43
- data/lib/dommy/internal/rendered_text/collector.rb +12 -9
- data/lib/dommy/internal/rendered_text/fragment.rb +6 -2
- data/lib/dommy/internal/selector_arguments.rb +2 -16
- data/lib/dommy/internal/selector_index.rb +19 -10
- data/lib/dommy/internal/selector_match.rb +190 -0
- data/lib/dommy/internal/selector_matcher.rb +91 -256
- data/lib/dommy/internal/selector_parser.rb +176 -100
- data/lib/dommy/internal/sequential_focus_navigation.rb +210 -0
- data/lib/dommy/internal/shadow_root_registry.rb +14 -6
- data/lib/dommy/internal/sibling_positions.rb +59 -0
- data/lib/dommy/internal/svg_uri_reference.rb +33 -0
- data/lib/dommy/internal/template_content_registry.rb +11 -5
- data/lib/dommy/internal/text_selection.rb +185 -32
- data/lib/dommy/internal/toggle_events.rb +6 -4
- data/lib/dommy/internal/unsafe_html.rb +107 -0
- data/lib/dommy/internal/url_parser.rb +84 -8
- data/lib/dommy/internal/user_activation.rb +192 -0
- data/lib/dommy/internal/web_idl.rb +160 -16
- data/lib/dommy/internal/window_constructors.rb +34 -14
- data/lib/dommy/internal/xml_serialization.rb +9 -45
- data/lib/dommy/js/constructor_resolver.rb +18 -0
- data/lib/dommy/js/custom_element_bridge.rb +57 -82
- data/lib/dommy/js/dom_interfaces.rb +51 -15
- data/lib/dommy/js/host_bridge.rb +189 -40
- data/lib/dommy/js/host_callbacks.rb +10 -0
- data/lib/dommy/js/host_runtime.js +1921 -416
- data/lib/dommy/js/marshaller.rb +13 -4
- data/lib/dommy/js/module_loader.rb +46 -5
- data/lib/dommy/js/module_preload.rb +194 -0
- data/lib/dommy/js/runtime.rb +9 -0
- data/lib/dommy/js/script_boot.rb +200 -114
- data/lib/dommy/js/serialized_record.rb +32 -0
- data/lib/dommy/js/webidl_event_handlers.js +45 -0
- data/lib/dommy/js/webidl_members.js +196 -0
- data/lib/dommy/js/webidl_signatures.js +2055 -0
- data/lib/dommy/js/webidl_tables.js +72 -101
- data/lib/dommy/js/window_wiring.rb +9 -5
- data/lib/dommy/location.rb +129 -76
- data/lib/dommy/mathml_elements.rb +13 -0
- data/lib/dommy/media_query_list.rb +4 -13
- data/lib/dommy/message_channel.rb +137 -64
- data/lib/dommy/mutation_observer.rb +3 -7
- data/lib/dommy/navigation.rb +61 -11
- data/lib/dommy/navigator.rb +132 -10
- data/lib/dommy/node.rb +81 -44
- data/lib/dommy/notification.rb +10 -6
- data/lib/dommy/parser.rb +5 -16
- data/lib/dommy/promise.rb +20 -26
- data/lib/dommy/range.rb +20 -47
- data/lib/dommy/resources.rb +38 -3
- data/lib/dommy/scheduler.rb +176 -48
- data/lib/dommy/shadow_root.rb +125 -25
- data/lib/dommy/storage.rb +135 -25
- data/lib/dommy/streams.rb +2 -6
- data/lib/dommy/structured_data.rb +45 -0
- data/lib/dommy/style_declaration.rb +38 -6
- data/lib/dommy/svg_elements.rb +25 -11
- data/lib/dommy/text_codec.rb +2 -14
- data/lib/dommy/tree_walker.rb +12 -26
- data/lib/dommy/url.rb +12 -9
- data/lib/dommy/url_pattern.rb +1 -9
- data/lib/dommy/validity_state.rb +109 -107
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +10 -33
- data/lib/dommy/window.rb +817 -105
- data/lib/dommy/worker.rb +4 -28
- data/lib/dommy/xml_http_request.rb +32 -45
- data/lib/dommy.rb +43 -10
- metadata +52 -5
- data/lib/dommy/backend/makiri_adapter.rb +0 -408
- data/lib/dommy/internal/cookie_jar.rb +0 -29
|
@@ -13,8 +13,8 @@ module Dommy
|
|
|
13
13
|
# mid-week, so steps measure from 1970-W01) lived three methods away from
|
|
14
14
|
# the code that converts a week string. Here a type is one class.
|
|
15
15
|
#
|
|
16
|
-
# Only the numeric surface lives here. Value sanitization stays on
|
|
17
|
-
# element, where it reads other attributes (`multiple` for email).
|
|
16
|
+
# Only the numeric and Date surface lives here. Value sanitization stays on
|
|
17
|
+
# the element, where it reads other attributes (`multiple` for email).
|
|
18
18
|
class InputType
|
|
19
19
|
# The type `name` stands for. An unknown or non-numeric type answers the
|
|
20
20
|
# one that has no numbers, so callers never branch on nil. REGISTRY and
|
|
@@ -25,6 +25,10 @@ module Dommy
|
|
|
25
25
|
# Whether valueAsNumber / stepUp / stepDown apply at all.
|
|
26
26
|
def numeric? = true
|
|
27
27
|
|
|
28
|
+
# Whether valueAsDate applies: date, month, week and time only. A
|
|
29
|
+
# datetime-local value names no instant (it has no time zone).
|
|
30
|
+
def date? = false
|
|
31
|
+
|
|
28
32
|
# How many of the number's units one step is worth.
|
|
29
33
|
def scale_factor = 1
|
|
30
34
|
|
|
@@ -51,6 +55,14 @@ module Dommy
|
|
|
51
55
|
# the one that knows which type to name in the error.
|
|
52
56
|
def from_number(_number) = raise(NotImplementedError)
|
|
53
57
|
|
|
58
|
+
# The value string as a Date's time value (ms since the epoch), or NaN
|
|
59
|
+
# when it is not one. The number valueAsNumber reads, for every date type
|
|
60
|
+
# but month, which counts months.
|
|
61
|
+
def to_date_value(text) = to_number(text)
|
|
62
|
+
|
|
63
|
+
# A Date's time value as the value string.
|
|
64
|
+
def from_date_value(time_value) = from_number(time_value)
|
|
65
|
+
|
|
54
66
|
# A `min`/`max` attribute as a number, or nil when it is not one. The
|
|
55
67
|
# non-numeric type parses nothing, so this is nil for it too.
|
|
56
68
|
def boundary(text) = nan_to_nil(to_number(text))
|
|
@@ -74,9 +86,21 @@ module Dommy
|
|
|
74
86
|
|
|
75
87
|
VALID_FLOAT = /\A-?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][-+]?\d+)?\z/
|
|
76
88
|
|
|
89
|
+
# Float(string), raising ArgumentError for what is no number. A number
|
|
90
|
+
# past a double's range comes out as ±Infinity, which every caller
|
|
91
|
+
# already reads as no valid number; Ruby warns about it in verbose mode,
|
|
92
|
+
# so the warning is held off for the conversion.
|
|
93
|
+
def self.float(string)
|
|
94
|
+
verbose = $VERBOSE
|
|
95
|
+
$VERBOSE = nil
|
|
96
|
+
Float(string)
|
|
97
|
+
ensure
|
|
98
|
+
$VERBOSE = verbose
|
|
99
|
+
end
|
|
100
|
+
|
|
77
101
|
def to_number(text)
|
|
78
102
|
string = text.to_s
|
|
79
|
-
VALID_FLOAT.match?(string) ? (
|
|
103
|
+
VALID_FLOAT.match?(string) ? NumberInputType.float(string) : ::Float::NAN
|
|
80
104
|
end
|
|
81
105
|
|
|
82
106
|
def from_number(number)
|
|
@@ -85,7 +109,7 @@ module Dommy
|
|
|
85
109
|
number == number.to_i ? number.to_i.to_s : number.to_s
|
|
86
110
|
end
|
|
87
111
|
|
|
88
|
-
def boundary(text) = (
|
|
112
|
+
def boundary(text) = (NumberInputType.float(text) rescue nil)
|
|
89
113
|
end
|
|
90
114
|
|
|
91
115
|
# A range always reads as a number: an unparseable value is the midpoint of
|
|
@@ -94,24 +118,46 @@ module Dommy
|
|
|
94
118
|
|
|
95
119
|
def value_of(text, element)
|
|
96
120
|
number = to_number(text)
|
|
97
|
-
low = (
|
|
98
|
-
high = (
|
|
99
|
-
number =
|
|
100
|
-
number
|
|
101
|
-
number
|
|
121
|
+
low = finite_attribute(element, "min") || 0.0
|
|
122
|
+
high = finite_attribute(element, "max") || 100.0
|
|
123
|
+
number = high < low ? low : low / 2.0 + high / 2.0 unless number.finite?
|
|
124
|
+
number = [number, low].max
|
|
125
|
+
number = [number, high].min if high >= low
|
|
126
|
+
return number if element.__internal_attribute_value__("step") == "any"
|
|
127
|
+
|
|
128
|
+
step = finite_attribute(element, "step")
|
|
129
|
+
step = default_step unless step&.positive?
|
|
130
|
+
base = finite_attribute(element, "min") || finite_attribute(element, "value") || step_base
|
|
131
|
+
# Round in decimal arithmetic so a valid 0.6 at step 0.1 does not
|
|
132
|
+
# become 0.6000000000000001, and tiny steps cannot overflow the ratio.
|
|
133
|
+
number = number.to_s.to_r
|
|
134
|
+
base = base.to_s.to_r
|
|
135
|
+
step = step.to_s.to_r
|
|
136
|
+
offset = (number - base) / step
|
|
137
|
+
candidates = [offset.floor, offset.ceil].map { |n| base + n * step }
|
|
138
|
+
.select { |n| n >= low && (high < low || n <= high) }
|
|
139
|
+
(candidates.min_by { |n| [(n - number).abs, -n] } || number).to_f
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def finite_attribute(element, name)
|
|
145
|
+
number = to_number(element.__internal_attribute_value__(name).to_s)
|
|
146
|
+
number if number.finite?
|
|
102
147
|
end
|
|
103
148
|
end
|
|
104
149
|
|
|
105
150
|
class DateInputType < InputType
|
|
106
151
|
|
|
107
152
|
def scale_factor = 86_400_000
|
|
153
|
+
def date? = true
|
|
108
154
|
|
|
109
155
|
def to_number(text)
|
|
110
|
-
match = /\A(\d{4,})-(\d{2})-(\d{2})\z/.match(text.to_s
|
|
156
|
+
match = /\A(\d{4,})-(\d{2})-(\d{2})\z/.match(text.to_s)
|
|
111
157
|
return ::Float::NAN unless match
|
|
112
158
|
|
|
113
159
|
year, month, day = match[1].to_i, match[2].to_i, match[3].to_i
|
|
114
|
-
return ::Float::NAN if year < 1 || !::Date.valid_date?(year, month, day)
|
|
160
|
+
return ::Float::NAN if year < 1 || !::Date.valid_date?(year, month, day, ::Date::GREGORIAN)
|
|
115
161
|
|
|
116
162
|
::Time.utc(year, month, day).to_i * 1000.0
|
|
117
163
|
end
|
|
@@ -127,13 +173,15 @@ module Dommy
|
|
|
127
173
|
|
|
128
174
|
end
|
|
129
175
|
|
|
176
|
+
# A time is the time of day; as a Date it falls on 1970-01-01 UTC.
|
|
130
177
|
class TimeInputType < InputType
|
|
131
178
|
|
|
132
179
|
def scale_factor = 1000
|
|
180
|
+
def date? = true
|
|
133
181
|
def default_step = 60.0
|
|
134
182
|
|
|
135
183
|
def to_number(text)
|
|
136
|
-
match = /\A(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?\z/.match(text.to_s
|
|
184
|
+
match = /\A(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?\z/.match(text.to_s)
|
|
137
185
|
return ::Float::NAN unless match
|
|
138
186
|
|
|
139
187
|
hour, minute, second = match[1].to_i, match[2].to_i, match[3].to_i
|
|
@@ -171,12 +219,12 @@ module Dommy
|
|
|
171
219
|
# The date/time separator may be "T" or a space (the "parse a local date
|
|
172
220
|
# and time string" algorithm accepts both).
|
|
173
221
|
match = /\A(\d{4,})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?\z/
|
|
174
|
-
.match(text.to_s
|
|
222
|
+
.match(text.to_s)
|
|
175
223
|
return ::Float::NAN unless match
|
|
176
224
|
|
|
177
225
|
year, month, day = match[1].to_i, match[2].to_i, match[3].to_i
|
|
178
226
|
hour, minute, second = match[4].to_i, match[5].to_i, match[6].to_i
|
|
179
|
-
return ::Float::NAN if year < 1 || !::Date.valid_date?(year, month, day)
|
|
227
|
+
return ::Float::NAN if year < 1 || !::Date.valid_date?(year, month, day, ::Date::GREGORIAN)
|
|
180
228
|
return ::Float::NAN if hour > 23 || minute > 59 || second > 59
|
|
181
229
|
|
|
182
230
|
fraction = match[7] ? match[7].ljust(3, "0").to_i : 0
|
|
@@ -184,10 +232,10 @@ module Dommy
|
|
|
184
232
|
end
|
|
185
233
|
|
|
186
234
|
def from_number(number)
|
|
187
|
-
return ""
|
|
235
|
+
return "" unless number.finite? && number.abs <= Bridge::Date::MAX_TIME_VALUE
|
|
188
236
|
|
|
189
237
|
time = utc_time_from_ms(number)
|
|
190
|
-
return "" if time.year < 1
|
|
238
|
+
return "" if time.year < 1
|
|
191
239
|
|
|
192
240
|
base = format("%04d-%02d-%02dT%02d:%02d", time.year, time.month, time.day, time.hour, time.min)
|
|
193
241
|
fraction = (number % 1000).to_i
|
|
@@ -196,7 +244,7 @@ module Dommy
|
|
|
196
244
|
elsif fraction.zero?
|
|
197
245
|
base + format(":%02d", time.sec)
|
|
198
246
|
else
|
|
199
|
-
base + format(":%02d.%03d", time.sec, fraction)
|
|
247
|
+
base + format(":%02d.%03d", time.sec, fraction).sub(/0+\z/, "")
|
|
200
248
|
end
|
|
201
249
|
rescue ::RangeError, ::ArgumentError, ::FloatDomainError
|
|
202
250
|
""
|
|
@@ -208,8 +256,10 @@ module Dommy
|
|
|
208
256
|
# its scale is 1.
|
|
209
257
|
class MonthInputType < InputType
|
|
210
258
|
|
|
259
|
+
def date? = true
|
|
260
|
+
|
|
211
261
|
def to_number(text)
|
|
212
|
-
match = /\A(\d{4,})-(\d{2})\z/.match(text.to_s
|
|
262
|
+
match = /\A(\d{4,})-(\d{2})\z/.match(text.to_s)
|
|
213
263
|
return ::Float::NAN unless match
|
|
214
264
|
|
|
215
265
|
year, month = match[1].to_i, match[2].to_i
|
|
@@ -226,11 +276,31 @@ module Dommy
|
|
|
226
276
|
format("%04d-%02d", year, (months % 12) + 1)
|
|
227
277
|
end
|
|
228
278
|
|
|
279
|
+
# As a Date, a month is the UTC midnight that starts it.
|
|
280
|
+
def to_date_value(text)
|
|
281
|
+
months = to_number(text)
|
|
282
|
+
return months if months.nan?
|
|
283
|
+
|
|
284
|
+
months = months.to_i
|
|
285
|
+
::Time.utc(1970 + months.fdiv(12).floor, (months % 12) + 1).to_i * 1000.0
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def from_date_value(time_value)
|
|
289
|
+
return "" unless time_value.finite?
|
|
290
|
+
|
|
291
|
+
time = utc_time_from_ms(time_value)
|
|
292
|
+
format("%04d-%02d", time.year, time.month)
|
|
293
|
+
rescue ::RangeError, ::ArgumentError, ::FloatDomainError
|
|
294
|
+
""
|
|
295
|
+
end
|
|
296
|
+
|
|
229
297
|
end
|
|
230
298
|
|
|
299
|
+
# As a Date, a week is the UTC midnight of its Monday.
|
|
231
300
|
class WeekInputType < InputType
|
|
232
301
|
|
|
233
302
|
def scale_factor = 604_800_000
|
|
303
|
+
def date? = true
|
|
234
304
|
|
|
235
305
|
# The epoch falls mid-week, so a week control aligns to the Monday of
|
|
236
306
|
# 1970-W01; measuring from 0 would report every whole week as a step
|
|
@@ -238,14 +308,14 @@ module Dommy
|
|
|
238
308
|
def step_base = to_number("1970-W01")
|
|
239
309
|
|
|
240
310
|
def to_number(text)
|
|
241
|
-
match = /\A(\d{4,})-W(\d{2})\z/.match(text.to_s
|
|
311
|
+
match = /\A(\d{4,})-W(\d{2})\z/.match(text.to_s)
|
|
242
312
|
return ::Float::NAN unless match
|
|
243
313
|
|
|
244
314
|
year, week = match[1].to_i, match[2].to_i
|
|
245
315
|
return ::Float::NAN if year < 1 || week < 1
|
|
246
316
|
|
|
247
317
|
# Date.commercial raises for a week beyond the ISO year's 52/53 weeks.
|
|
248
|
-
date = ::Date.commercial(year, week, 1)
|
|
318
|
+
date = ::Date.commercial(year, week, 1, ::Date::GREGORIAN)
|
|
249
319
|
::Time.utc(date.year, date.month, date.day).to_i * 1000.0
|
|
250
320
|
rescue ::ArgumentError
|
|
251
321
|
::Float::NAN
|
|
@@ -13,6 +13,30 @@ module Dommy
|
|
|
13
13
|
module InsertionPoint
|
|
14
14
|
module_function
|
|
15
15
|
|
|
16
|
+
# WHATWG "convert nodes into a node" in `document`, for the variadic
|
|
17
|
+
# methods (ChildNode's, ParentNode's and the Document's own): two or
|
|
18
|
+
# more arguments are appended, in order, to a new DocumentFragment —
|
|
19
|
+
# strings as new Text nodes — and the fragment stands for them. That
|
|
20
|
+
# append moves each node out of wherever it was and checks it as any
|
|
21
|
+
# append does, so it can fail half way (a doctype or a Document among
|
|
22
|
+
# the arguments): the nodes appended by then stay in the fragment, out of
|
|
23
|
+
# the tree, as the spec has it. One argument is left as it is: a string
|
|
24
|
+
# becomes a Text node where it is inserted, and a node moves only then.
|
|
25
|
+
#
|
|
26
|
+
# Spec: https://dom.spec.whatwg.org/#converting-nodes-into-a-node
|
|
27
|
+
def convert_nodes_into_a_node(document, args)
|
|
28
|
+
return args if args.size < 2
|
|
29
|
+
|
|
30
|
+
fragment = document.create_document_fragment
|
|
31
|
+
args.each do |arg|
|
|
32
|
+
case arg
|
|
33
|
+
when Dommy::Node then fragment.append_child(arg)
|
|
34
|
+
when String then fragment.append_child(document.create_text_node(arg))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
[fragment]
|
|
38
|
+
end
|
|
39
|
+
|
|
16
40
|
# Insert step 1's count: how many nodes `args` will contribute once
|
|
17
41
|
# converted. A DocumentFragment expands to its children, a String becomes
|
|
18
42
|
# one Text node, and anything without a backing node contributes nothing.
|
|
@@ -21,9 +45,9 @@ module Dommy
|
|
|
21
45
|
def count(args)
|
|
22
46
|
args.sum do |arg|
|
|
23
47
|
case arg
|
|
24
|
-
when Fragment then arg.__dommy_backend_node__.
|
|
48
|
+
when Fragment then arg.__dommy_backend_node__.child_count
|
|
25
49
|
when String then 1
|
|
26
|
-
else arg.
|
|
50
|
+
else arg.is_a?(Node) && arg.__dommy_backend_node__ ? 1 : 0
|
|
27
51
|
end
|
|
28
52
|
end
|
|
29
53
|
end
|
|
@@ -32,7 +56,7 @@ module Dommy
|
|
|
32
56
|
# or the parent's last child when appending. Measured before anything
|
|
33
57
|
# moves. Returns the backend node; the caller wraps it.
|
|
34
58
|
def previous_sibling(parent, ref)
|
|
35
|
-
ref ? ref.previous_sibling : parent.
|
|
59
|
+
ref ? ref.previous_sibling : parent.last_child
|
|
36
60
|
end
|
|
37
61
|
|
|
38
62
|
# Advance `ref` past any node that is itself being inserted: it is about
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
# ECMAScript's Number::toString(x) with radix 10 — what JS's `String(x)`
|
|
6
|
+
# gives, and what HTML calls "the best representation of the number as a
|
|
7
|
+
# floating-point number" (the string a double reflection, a meter or a
|
|
8
|
+
# number input writes). Ruby's Float#to_s picks the same shortest
|
|
9
|
+
# round-trip digits but lays them out differently ("1.0e-10", "5.0",
|
|
10
|
+
# "1.0e+16"), so this re-lays them out by the ECMAScript rules:
|
|
11
|
+
#
|
|
12
|
+
# to_string(5.0) # => "5"
|
|
13
|
+
# to_string(-0.0) # => "0"
|
|
14
|
+
# to_string(1e21) # => "1e+21"
|
|
15
|
+
# to_string(1e-7) # => "1e-7"
|
|
16
|
+
# to_string(1.5e-6) # => "0.0000015"
|
|
17
|
+
# to_string(1e25) # => "1e+25"
|
|
18
|
+
#
|
|
19
|
+
# Spec: https://tc39.es/ecma262/#sec-numeric-types-number-tostring
|
|
20
|
+
module JsNumber
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def to_string(number)
|
|
24
|
+
x = Float(number)
|
|
25
|
+
return "NaN" if x.nan?
|
|
26
|
+
return x.positive? ? "Infinity" : "-Infinity" if x.infinite?
|
|
27
|
+
return "0" if x.zero? # +0 and -0 alike
|
|
28
|
+
return "-#{to_string(-x)}" if x.negative?
|
|
29
|
+
|
|
30
|
+
digits, n = shortest_digits(x)
|
|
31
|
+
k = digits.length
|
|
32
|
+
if k <= n && n <= 21
|
|
33
|
+
digits + ("0" * (n - k))
|
|
34
|
+
elsif n.positive? && n <= 21
|
|
35
|
+
"#{digits[0, n]}.#{digits[n..]}"
|
|
36
|
+
elsif n > -6 && n <= 0
|
|
37
|
+
"0.#{"0" * -n}#{digits}"
|
|
38
|
+
else
|
|
39
|
+
exponent = n - 1
|
|
40
|
+
sign = exponent.negative? ? "-" : "+"
|
|
41
|
+
mantissa = k == 1 ? digits : "#{digits[0]}.#{digits[1..]}"
|
|
42
|
+
"#{mantissa}e#{sign}#{exponent.abs}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# [s, n] for a finite positive x: the shortest digit string s (no leading
|
|
47
|
+
# or trailing zeros) with x = 0.s × 10^n — ECMAScript's k digits and n.
|
|
48
|
+
# Float#to_s already chose the digits; this only reads them back out.
|
|
49
|
+
def shortest_digits(x)
|
|
50
|
+
mantissa, exponent = x.to_s.split("e")
|
|
51
|
+
int_part, frac_part = mantissa.split(".")
|
|
52
|
+
all = int_part + frac_part.to_s
|
|
53
|
+
point = int_part.length + exponent.to_i
|
|
54
|
+
stripped = all.sub(/\A0+/, "")
|
|
55
|
+
point -= all.length - stripped.length
|
|
56
|
+
[stripped.sub(/0+\z/, ""), point]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
# HTML's light dismiss: "run light dismiss activities" for a trusted
|
|
6
|
+
# pointerdown or pointerup — "light dismiss open popovers" (§6.12) and
|
|
7
|
+
# "light dismiss open dialogs" (the dialog element). Pointer Events calls
|
|
8
|
+
# it as the user presses and releases the pointer, before the pointer
|
|
9
|
+
# event is dispatched; Dommy's Interaction layer does the same for its
|
|
10
|
+
# clicks.
|
|
11
|
+
#
|
|
12
|
+
# A press records the topmost popover (and the nearest open dialog) the
|
|
13
|
+
# pointer went down in; the release, when it lands in the same one,
|
|
14
|
+
# closes the auto and hint popovers above it, and requests to close the
|
|
15
|
+
# topmost `closedby=any` dialog when that is not the dialog clicked.
|
|
16
|
+
module LightDismiss
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Run light dismiss activities for `event` (a trusted PointerEvent of
|
|
20
|
+
# type pointerdown or pointerup) aimed at `target`. `backdrop` says the
|
|
21
|
+
# pointer hit a modal dialog's ::backdrop rather than its box — the
|
|
22
|
+
# coordinates were outside the dialog.
|
|
23
|
+
def run(event, target, backdrop: false)
|
|
24
|
+
return unless event.__js_get__("isTrusted") == true
|
|
25
|
+
return unless target.is_a?(Node)
|
|
26
|
+
|
|
27
|
+
light_dismiss_open_popovers(event.type, target)
|
|
28
|
+
light_dismiss_open_dialogs(event.type, target, backdrop)
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# HTML "light dismiss open popovers".
|
|
33
|
+
def light_dismiss_open_popovers(type, target)
|
|
34
|
+
document = target.owner_document
|
|
35
|
+
stack = document.__internal_popover_stack__
|
|
36
|
+
return if stack.topmost_auto_or_hint.nil?
|
|
37
|
+
|
|
38
|
+
case type
|
|
39
|
+
when "pointerdown"
|
|
40
|
+
document.__internal_popover_pointerdown_target__ = topmost_clicked_popover(target)
|
|
41
|
+
when "pointerup"
|
|
42
|
+
ancestor = topmost_clicked_popover(target)
|
|
43
|
+
same_target = ancestor.equal?(document.__internal_popover_pointerdown_target__)
|
|
44
|
+
document.__internal_popover_pointerdown_target__ = nil
|
|
45
|
+
return unless same_target
|
|
46
|
+
|
|
47
|
+
stack.hide_popovers_until(ancestor, false, true)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# HTML "light dismiss open dialogs".
|
|
52
|
+
def light_dismiss_open_dialogs(type, target, backdrop)
|
|
53
|
+
document = target.owner_document
|
|
54
|
+
open_dialogs = document.__internal_open_dialogs__
|
|
55
|
+
return if open_dialogs.empty?
|
|
56
|
+
|
|
57
|
+
ancestor = nearest_clicked_dialog(target, backdrop)
|
|
58
|
+
case type
|
|
59
|
+
when "pointerdown"
|
|
60
|
+
document.__internal_dialog_pointerdown_target__ = ancestor
|
|
61
|
+
when "pointerup"
|
|
62
|
+
same_target = ancestor.equal?(document.__internal_dialog_pointerdown_target__)
|
|
63
|
+
document.__internal_dialog_pointerdown_target__ = nil
|
|
64
|
+
return unless same_target
|
|
65
|
+
|
|
66
|
+
topmost = open_dialogs.last
|
|
67
|
+
return if ancestor.equal?(topmost)
|
|
68
|
+
return unless topmost.closed_by == "any"
|
|
69
|
+
|
|
70
|
+
topmost.__internal_request_close_watcher__(false)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# HTML "nearest clicked dialog": nil for a hit on an open modal
|
|
75
|
+
# dialog's backdrop, else the nearest open dialog among the target's
|
|
76
|
+
# flat-tree inclusive ancestors.
|
|
77
|
+
def nearest_clicked_dialog(target, backdrop)
|
|
78
|
+
return nil if backdrop && open_dialog?(target) && target.__internal_modal__?
|
|
79
|
+
|
|
80
|
+
node = target
|
|
81
|
+
until node.nil?
|
|
82
|
+
return node if open_dialog?(node)
|
|
83
|
+
|
|
84
|
+
node = flat_tree_parent(node)
|
|
85
|
+
end
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# HTML "topmost clicked popover".
|
|
90
|
+
def topmost_clicked_popover(node)
|
|
91
|
+
clicked = nearest_inclusive_open_popover(node)
|
|
92
|
+
target = nearest_inclusive_target_popover(node)
|
|
93
|
+
stack_position(clicked) > stack_position(target) ? clicked : target
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# HTML "nearest inclusive open popover".
|
|
97
|
+
def nearest_inclusive_open_popover(node)
|
|
98
|
+
current = node
|
|
99
|
+
until current.nil?
|
|
100
|
+
if current.respond_to?(:__internal_popover_opened_mode__) && current.__internal_popover_opened_mode__ &&
|
|
101
|
+
current.__internal_popover_showing__?
|
|
102
|
+
return current
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
current = flat_tree_parent(current)
|
|
106
|
+
end
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# HTML "nearest inclusive target popover".
|
|
111
|
+
def nearest_inclusive_target_popover(node)
|
|
112
|
+
current = node
|
|
113
|
+
until current.nil?
|
|
114
|
+
popover = target_popover(current)
|
|
115
|
+
if popover && %w[auto hint].include?(popover.popover) && popover.__internal_popover_showing__?
|
|
116
|
+
return popover
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
current = flat_tree_parent(current)
|
|
120
|
+
end
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# HTML "get the target popover": what a button's commandfor names, when
|
|
125
|
+
# its command is a popover one, else its popover target element.
|
|
126
|
+
def target_popover(node)
|
|
127
|
+
return nil unless node.is_a?(HTMLElement)
|
|
128
|
+
|
|
129
|
+
popover_target = node.respond_to?(:__internal_popover_target_element__) ? node.__internal_popover_target_element__ : nil
|
|
130
|
+
return popover_target unless node.is_a?(HTMLButtonElement)
|
|
131
|
+
return nil if node.__internal_actually_disabled__
|
|
132
|
+
|
|
133
|
+
target = node.command_for_element
|
|
134
|
+
return popover_target if target.nil?
|
|
135
|
+
|
|
136
|
+
if node.form
|
|
137
|
+
return nil if node.__internal_submit_button_state__?
|
|
138
|
+
return nil if %w[reset auto].include?(node.type_state)
|
|
139
|
+
end
|
|
140
|
+
return nil unless HTMLButtonElement::POPOVER_COMMANDS.include?(node.command)
|
|
141
|
+
return nil unless target.is_a?(HTMLElement) && !target.popover.nil?
|
|
142
|
+
|
|
143
|
+
target
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# HTML "get the popover stack position".
|
|
147
|
+
def stack_position(popover)
|
|
148
|
+
return 0 if popover.nil?
|
|
149
|
+
|
|
150
|
+
stack = popover.owner_document.__internal_popover_stack__
|
|
151
|
+
hint = stack.list("hint")
|
|
152
|
+
auto = stack.list("auto")
|
|
153
|
+
if (index = hint.index { |p| p.equal?(popover) })
|
|
154
|
+
return index + auto.size + 1
|
|
155
|
+
end
|
|
156
|
+
if (index = auto.index { |p| p.equal?(popover) })
|
|
157
|
+
return index + 1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
0
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def open_dialog?(node)
|
|
164
|
+
node.is_a?(HTMLDialogElement) && node.__internal_has_attribute__?("open")
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# A node's parent in the flat tree, up to the document element.
|
|
168
|
+
def flat_tree_parent(node)
|
|
169
|
+
parent = Focusability.flat_tree_parent(node)
|
|
170
|
+
parent.is_a?(Element) ? parent : nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private_class_method :light_dismiss_open_popovers, :light_dismiss_open_dialogs, :nearest_clicked_dialog,
|
|
174
|
+
:topmost_clicked_popover, :nearest_inclusive_open_popover, :nearest_inclusive_target_popover,
|
|
175
|
+
:target_popover, :stack_position, :open_dialog?, :flat_tree_parent
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "infra"
|
|
4
|
+
|
|
5
|
+
module Dommy
|
|
6
|
+
module Internal
|
|
7
|
+
# getElementById, getElementsByClassName and getElementsByName: each
|
|
8
|
+
# compares an attribute with a string, and none of them takes a CSS
|
|
9
|
+
# selector. A value that would be special in one — a digit first, `.`, `:`,
|
|
10
|
+
# a quote, a backslash, a newline, NUL — must still be found and never
|
|
11
|
+
# raise, so these read the backend tree rather than compose a selector
|
|
12
|
+
# string from their argument (getElementById uses the backend's id
|
|
13
|
+
# selector as an index, and checks what it answers).
|
|
14
|
+
module LiteralLookup
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# The first backend element under `bnode` whose `id` is `id`, in tree
|
|
18
|
+
# order. An id with selector-special characters (e.g. React's `useId`
|
|
19
|
+
# values like `:rjm:`) is escaped into a valid id-selector ident for the
|
|
20
|
+
# backend's engine (#selector_ident). In a quirks-mode document that
|
|
21
|
+
# engine matches an id selector ASCII case-insensitively, as CSS asks,
|
|
22
|
+
# while getElementById still compares case-sensitively: its answer is
|
|
23
|
+
# kept only when the id is exactly `id`, and otherwise the exact one is
|
|
24
|
+
# looked for among the rest it matches. No escape spells U+0000 (it
|
|
25
|
+
# reads as U+FFFD), so an id holding it is found by comparing the
|
|
26
|
+
# attribute itself. An element's id, like its classes and its name, is
|
|
27
|
+
# the attribute of that name in no namespace (#attribute).
|
|
28
|
+
def element_by_id(bnode, id)
|
|
29
|
+
return bnode.css("[id]").find { |n| attribute(n, "id") == id } if id.include?("\u0000")
|
|
30
|
+
|
|
31
|
+
selector = "##{selector_ident(id)}"
|
|
32
|
+
first = bnode.at_css(selector)
|
|
33
|
+
return first if first.nil? || attribute(first, "id") == id
|
|
34
|
+
|
|
35
|
+
bnode.css(selector).find { |n| attribute(n, "id") == id }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The class tokens getElementsByClassName looks for in `names`.
|
|
39
|
+
def class_tokens(names) = Infra.split_on_ascii_whitespace(names)
|
|
40
|
+
|
|
41
|
+
# The backend elements under `root`, a node of `document`, whose class
|
|
42
|
+
# list contains every one of `tokens`, compared directly: a token may
|
|
43
|
+
# hold anything but ASCII whitespace (`1`, `a.b`, `a:b`, `[x]`, a quote,
|
|
44
|
+
# NUL). In a quirks-mode document they compare ASCII case-insensitively.
|
|
45
|
+
# The backend's own class selector narrows the candidates first (see
|
|
46
|
+
# #class_candidates).
|
|
47
|
+
def elements_with_classes(document, root, tokens)
|
|
48
|
+
quirks = document.quirks_mode?
|
|
49
|
+
wanted = quirks ? tokens.map { |t| t.downcase(:ascii) } : tokens
|
|
50
|
+
class_candidates(document, root, tokens).select do |n|
|
|
51
|
+
classes = Infra.split_on_ascii_whitespace(attribute(n, "class"))
|
|
52
|
+
classes = classes.map { |c| c.downcase(:ascii) } if quirks
|
|
53
|
+
wanted.all? { |t| classes.include?(t) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# A superset of the elements with every one of `tokens`: what the
|
|
58
|
+
# backend's class selector, the tokens escaped by #selector_ident,
|
|
59
|
+
# matches — natively, where the comparison above runs in Ruby. That
|
|
60
|
+
# selector compares exactly where the backend parsed a document in
|
|
61
|
+
# no-quirks mode and ASCII case-insensitively where it parsed one in
|
|
62
|
+
# quirks mode; Dommy takes the document's mode from that parse, so the
|
|
63
|
+
# selector is never stricter than the comparison above — but a superset
|
|
64
|
+
# must not rest on that, so a quirks-mode `document` over a backend that
|
|
65
|
+
# does not fold scans instead. It cannot spell U+0000 (an escape of it
|
|
66
|
+
# reads as U+FFFD); then too every element with a class is a candidate.
|
|
67
|
+
def class_candidates(document, root, tokens)
|
|
68
|
+
backend_folds = !document.quirks_mode? || Backend.quirks_mode?(document.backend_doc)
|
|
69
|
+
return root.css("[class]") if !backend_folds || tokens.any? { |t| t.include?("\u0000") }
|
|
70
|
+
|
|
71
|
+
root.css(tokens.map { |t| ".#{selector_ident(t)}" }.join)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# `value` as an ident a selector can carry whatever it holds: every code
|
|
75
|
+
# point but an ASCII letter, an underscore, and (after the first) a
|
|
76
|
+
# digit or U+002D written as a hex escape. CSS.escape is not enough —
|
|
77
|
+
# it leaves a non-ASCII code point as it is, and css-syntax-3 counts
|
|
78
|
+
# only some of them as ident code points (§4.2), so a class or an id of
|
|
79
|
+
# U+00A0 or U+3000 would make an invalid selector.
|
|
80
|
+
def selector_ident(value)
|
|
81
|
+
value.each_char.with_index.map do |c, i|
|
|
82
|
+
plain = i.zero? ? c.match?(/[A-Za-z_]/) : c.match?(/[A-Za-z0-9_-]/)
|
|
83
|
+
plain ? c : "\\#{c.ord.to_s(16)} "
|
|
84
|
+
end.join
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The HTML elements under `bnode` whose `name` is `name`. Only elements
|
|
88
|
+
# in the HTML namespace count: an SVG or MathML element with a `name`
|
|
89
|
+
# attribute is not one getElementsByName finds.
|
|
90
|
+
#
|
|
91
|
+
# Spec: https://html.spec.whatwg.org/multipage/dom.html#dom-document-getelementsbyname
|
|
92
|
+
def elements_named(bnode, name)
|
|
93
|
+
bnode.css("[name]").select { |n| Backend.namespace_uri(n) == Namespaces::HTML && attribute(n, "name") == name }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The value of `node`'s attribute `name` in no namespace: an `id`,
|
|
97
|
+
# `class` or `name` set with setAttributeNS("urn:x", …) is none of them.
|
|
98
|
+
def attribute(node, name) = Backend.no_namespace_attribute_value(node, name)
|
|
99
|
+
|
|
100
|
+
private_class_method :class_candidates, :selector_ident, :attribute
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -19,6 +19,14 @@ module Dommy
|
|
|
19
19
|
|
|
20
20
|
module_function
|
|
21
21
|
|
|
22
|
+
# Whether the essence names an XML MIME type: text/xml, application/xml,
|
|
23
|
+
# or any subtype ending in "+xml" (image/svg+xml, application/xhtml+xml).
|
|
24
|
+
# https://mimesniff.spec.whatwg.org/#xml-mime-type
|
|
25
|
+
def xml?(essence)
|
|
26
|
+
essence = essence.to_s
|
|
27
|
+
essence == "text/xml" || essence == "application/xml" || essence.end_with?("+xml")
|
|
28
|
+
end
|
|
29
|
+
|
|
22
30
|
# The value of `name` among the MIME type's parameters, or nil. The name
|
|
23
31
|
# is ASCII case-insensitive; a quoted value comes back unquoted.
|
|
24
32
|
def parameter(mime_type, name)
|