dommy 0.12.0 → 0.14.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/lib/dommy/backend/makiri_adapter.rb +63 -29
- data/lib/dommy/backend.rb +32 -9
- data/lib/dommy/bridge/constructor.rb +72 -0
- data/lib/dommy/bridge/errors.rb +42 -0
- data/lib/dommy/bridge/methods.rb +21 -14
- data/lib/dommy/bridge/sentinels.rb +39 -0
- data/lib/dommy/bridge/values.rb +78 -0
- data/lib/dommy/{js → bridge}/wire_tags.rb +22 -11
- data/lib/dommy/bridge.rb +22 -237
- data/lib/dommy/browser.rb +155 -58
- data/lib/dommy/callable_invoker.rb +16 -0
- data/lib/dommy/character_data.rb +539 -0
- data/lib/dommy/class_list.rb +311 -0
- data/lib/dommy/compression_streams.rb +12 -62
- data/lib/dommy/crypto.rb +12 -1
- data/lib/dommy/css.rb +12 -0
- data/lib/dommy/data_transfer.rb +106 -0
- data/lib/dommy/document.rb +312 -374
- data/lib/dommy/dom_parser.rb +3 -0
- data/lib/dommy/element.rb +201 -1820
- data/lib/dommy/encodings/tables.rb +751 -0
- data/lib/dommy/encodings.rb +330 -0
- data/lib/dommy/event.rb +231 -6
- data/lib/dommy/event_source.rb +41 -0
- data/lib/dommy/fetch.rb +60 -49
- data/lib/dommy/form_data.rb +16 -64
- data/lib/dommy/form_entry_list.rb +227 -0
- data/lib/dommy/fragment.rb +320 -0
- data/lib/dommy/history.rb +3 -0
- data/lib/dommy/html_collection.rb +18 -8
- data/lib/dommy/html_elements/base.rb +338 -0
- data/lib/dommy/html_elements/embedded.rb +237 -0
- data/lib/dommy/html_elements/forms/form.rb +1088 -0
- data/lib/dommy/html_elements/forms/input.rb +642 -0
- data/lib/dommy/html_elements/forms/select.rb +536 -0
- data/lib/dommy/html_elements/forms.rb +8 -0
- data/lib/dommy/html_elements/interactive.rb +382 -0
- data/lib/dommy/html_elements/media.rb +329 -0
- data/lib/dommy/html_elements/metadata.rb +353 -0
- data/lib/dommy/html_elements/miscellaneous.rb +77 -0
- data/lib/dommy/html_elements/tables.rb +468 -0
- data/lib/dommy/html_elements.rb +11 -5248
- data/lib/dommy/hyperlink_utils.rb +171 -0
- data/lib/dommy/interaction/debug.rb +7 -6
- data/lib/dommy/interaction/dom_summary.rb +65 -40
- data/lib/dommy/interaction/driver.rb +29 -115
- data/lib/dommy/interaction/event_synthesis.rb +97 -4
- data/lib/dommy/interaction/field_interactor.rb +1 -13
- data/lib/dommy/interaction/form_submission.rb +89 -167
- data/lib/dommy/interaction/key_sender.rb +121 -0
- data/lib/dommy/interaction/mime_types.rb +25 -0
- data/lib/dommy/interaction.rb +2 -0
- data/lib/dommy/internal/accessibility_tree.rb +32 -9
- data/lib/dommy/internal/accessibility_visibility.rb +44 -0
- data/lib/dommy/internal/accessible_name.rb +85 -65
- data/lib/dommy/internal/aria_role.rb +21 -6
- data/lib/dommy/internal/aria_snapshot.rb +5 -0
- data/lib/dommy/internal/aria_state.rb +24 -5
- data/lib/dommy/internal/autofill.rb +141 -0
- data/lib/dommy/internal/backend_prefilter.rb +170 -0
- data/lib/dommy/internal/bounded_cache.rb +50 -0
- data/lib/dommy/internal/child_node.rb +9 -34
- data/lib/dommy/internal/css/calc.rb +143 -145
- data/lib/dommy/internal/css/cascade.rb +38 -393
- data/lib/dommy/internal/css/cascaded_declarations.rb +245 -0
- data/lib/dommy/internal/css/color.rb +61 -34
- data/lib/dommy/internal/css/computed_style_builder.rb +205 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +20 -4
- data/lib/dommy/internal/css/counters.rb +60 -37
- data/lib/dommy/internal/css/custom_properties.rb +87 -39
- data/lib/dommy/internal/css/media_query.rb +228 -203
- data/lib/dommy/internal/css/parser.rb +29 -12
- data/lib/dommy/internal/css/property_registry.rb +32 -56
- data/lib/dommy/internal/css/renderability.rb +61 -0
- data/lib/dommy/internal/css/rule_index.rb +17 -140
- data/lib/dommy/internal/css/selector_dependencies.rb +162 -0
- data/lib/dommy/internal/css/style_cache.rb +61 -0
- data/lib/dommy/internal/css/ua_stylesheet.rb +40 -1
- data/lib/dommy/internal/css/value_tokens.rb +60 -0
- data/lib/dommy/internal/directionality.rb +188 -0
- data/lib/dommy/internal/document_generations.rb +169 -0
- data/lib/dommy/internal/document_interaction_state.rb +47 -0
- data/lib/dommy/internal/document_live_ranges.rb +97 -0
- data/lib/dommy/internal/dom_matching.rb +28 -12
- data/lib/dommy/internal/element_aria.rb +163 -0
- data/lib/dommy/internal/element_geometry.rb +69 -0
- data/lib/dommy/internal/element_matching.rb +5 -12
- data/lib/dommy/internal/element_shadow.rb +117 -0
- data/lib/dommy/internal/element_state.rb +232 -0
- data/lib/dommy/internal/element_tasks.rb +19 -0
- data/lib/dommy/internal/element_top_layer.rb +63 -0
- data/lib/dommy/internal/enumerated_keyword_sets.rb +53 -0
- data/lib/dommy/internal/exception_report.rb +123 -0
- data/lib/dommy/internal/idna.rb +4 -4
- data/lib/dommy/internal/idna_data.rb +513 -100
- data/lib/dommy/internal/input_type.rb +280 -0
- data/lib/dommy/internal/insertion_point.rb +62 -0
- data/lib/dommy/internal/mime_type.rb +74 -0
- data/lib/dommy/internal/mutation_coordinator.rb +57 -198
- data/lib/dommy/internal/namespaces.rb +41 -43
- data/lib/dommy/internal/node_adopter.rb +84 -77
- data/lib/dommy/internal/node_factory.rb +157 -0
- data/lib/dommy/internal/node_identity.rb +31 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +61 -207
- data/lib/dommy/internal/observer_options.rb +93 -0
- data/lib/dommy/internal/parent_node.rb +27 -27
- data/lib/dommy/internal/post_insertion_steps.rb +213 -0
- data/lib/dommy/internal/reflected_attributes.rb +425 -13
- data/lib/dommy/internal/rendered_text/collector.rb +221 -0
- data/lib/dommy/internal/rendered_text/computed_box.rb +39 -0
- data/lib/dommy/internal/rendered_text/fragment.rb +55 -0
- data/lib/dommy/internal/rendered_text/items.rb +57 -0
- data/lib/dommy/internal/rendered_text/whitespace_collapser.rb +132 -0
- data/lib/dommy/internal/selector_arguments.rb +94 -0
- data/lib/dommy/internal/selector_index.rb +3 -1
- data/lib/dommy/internal/selector_matcher.rb +23 -383
- data/lib/dommy/internal/selector_parser.rb +2 -37
- data/lib/dommy/internal/template_content_registry.rb +112 -66
- data/lib/dommy/internal/text_flattening.rb +20 -0
- data/lib/dommy/internal/text_selection.rb +104 -0
- data/lib/dommy/internal/toggle_events.rb +56 -0
- data/lib/dommy/internal/toggle_task_tracker.rb +57 -0
- data/lib/dommy/internal/url_parser.rb +732 -527
- data/lib/dommy/internal/url_record_accessors.rb +90 -0
- data/lib/dommy/internal/window_constructors.rb +190 -0
- data/lib/dommy/internal/xml_serialization.rb +168 -80
- data/lib/dommy/js/crossing_profile.rb +75 -0
- data/lib/dommy/js/dom_interfaces.rb +23 -2
- data/lib/dommy/js/error_log.rb +137 -0
- data/lib/dommy/js/host_bridge.rb +56 -187
- data/lib/dommy/js/host_callbacks.rb +96 -0
- data/lib/dommy/js/host_runtime.js +896 -729
- data/lib/dommy/js/import_map.rb +10 -1
- data/lib/dommy/js/marshaller.rb +111 -63
- data/lib/dommy/js/observable_runtime.js +125 -146
- data/lib/dommy/js/runtime.rb +17 -43
- data/lib/dommy/js/runtime_registry.rb +46 -0
- data/lib/dommy/js/script_boot.rb +94 -85
- data/lib/dommy/js/webidl_tables.js +619 -0
- data/lib/dommy/js/window_wiring.rb +69 -0
- data/lib/dommy/location.rb +161 -102
- data/lib/dommy/mutation_observer.rb +3 -57
- data/lib/dommy/navigation.rb +65 -12
- data/lib/dommy/navigator.rb +3 -1
- data/lib/dommy/node.rb +46 -20
- data/lib/dommy/promise.rb +62 -8
- data/lib/dommy/resources.rb +19 -1
- data/lib/dommy/scheduler.rb +35 -6
- data/lib/dommy/shadow_root.rb +11 -5
- data/lib/dommy/streams.rb +1200 -187
- data/lib/dommy/style_declaration.rb +284 -0
- data/lib/dommy/svg_elements.rb +1 -45
- data/lib/dommy/text_codec.rb +110 -213
- data/lib/dommy/url.rb +32 -79
- data/lib/dommy/url_pattern/regexp_translator.rb +1 -6
- data/lib/dommy/url_pattern/tokenizer.rb +4 -2
- data/lib/dommy/validity_state.rb +383 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +184 -192
- data/lib/dommy/xml_http_request.rb +52 -7
- data/lib/dommy.rb +25 -3
- metadata +78 -7
- data/lib/dommy/bridge/constructor_registry.rb +0 -28
- data/lib/dommy/internal/css_pseudo_handlers.rb +0 -378
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../css/cascade"
|
|
4
|
+
require_relative "../css/renderability"
|
|
5
|
+
require_relative "../css/ua_stylesheet"
|
|
6
|
+
require_relative "computed_box"
|
|
7
|
+
require_relative "items"
|
|
8
|
+
require_relative "whitespace_collapser"
|
|
9
|
+
|
|
10
|
+
module Dommy
|
|
11
|
+
module Internal
|
|
12
|
+
module RenderedText
|
|
13
|
+
# The `innerText` / `outerText` getter: HTML's "rendered text collection
|
|
14
|
+
# steps", from
|
|
15
|
+
# https://html.spec.whatwg.org/multipage/dom.html#the-innertext-and-outertext-properties
|
|
16
|
+
#
|
|
17
|
+
# Dommy lays nothing out, so the layout-derived parts — soft line
|
|
18
|
+
# wrapping, `::first-line` / `::first-letter`, multicol — are out of
|
|
19
|
+
# reach. Everything that follows from the computed style and the tree is
|
|
20
|
+
# implemented: display:none / contents, visibility, white-space,
|
|
21
|
+
# text-transform, <br>, block and <p> line breaks, table cells and rows,
|
|
22
|
+
# and the select / optgroup / option box rules.
|
|
23
|
+
#
|
|
24
|
+
# Whether the element itself is rendered is asked once, up the tree.
|
|
25
|
+
# Below it the walk goes top-down and never looks up again: a subtree
|
|
26
|
+
# without a box is skipped where it starts (see #collect_element and
|
|
27
|
+
# #rendered_children).
|
|
28
|
+
class Collector
|
|
29
|
+
# Elements that render as a replaced box, whose children are not
|
|
30
|
+
# rendered at all.
|
|
31
|
+
REPLACED = %w[area audio canvas embed iframe img input object textarea video].freeze
|
|
32
|
+
|
|
33
|
+
# The elements the spec gives a special box for inside <select>.
|
|
34
|
+
OPTION_LIKE = %w[optgroup option].freeze
|
|
35
|
+
|
|
36
|
+
# Displays that make an atomic inline-level box: it establishes its own
|
|
37
|
+
# inline formatting context, so its leading/trailing whitespace is
|
|
38
|
+
# trimmed and the whitespace around it does not merge with its content.
|
|
39
|
+
ATOMIC_INLINE_DISPLAYS = %w[inline-block inline-flex inline-grid inline-table].freeze
|
|
40
|
+
|
|
41
|
+
def initialize(element)
|
|
42
|
+
@element = element
|
|
43
|
+
@boxes = {}.compare_by_identity
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# HTML's "get the text steps": textContent for an element that is not
|
|
47
|
+
# being rendered, the rendered text otherwise.
|
|
48
|
+
def text
|
|
49
|
+
return @element.text_content.to_s unless being_rendered?(@element)
|
|
50
|
+
|
|
51
|
+
WhitespaceCollapser.collapse(collect_children(@element))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Rendered by tree position (connected, in the flat tree, in a rendered
|
|
57
|
+
# frame), and reached from the root only through boxes that render it:
|
|
58
|
+
# no display:none on the way, and each step down one that
|
|
59
|
+
# #rendered_children keeps — which is what rules out a replaced
|
|
60
|
+
# ancestor and the hidden content of a closed <details>.
|
|
61
|
+
def being_rendered?(element)
|
|
62
|
+
return false if CSS::Renderability.not_rendered?(element, method(:style))
|
|
63
|
+
|
|
64
|
+
node = element
|
|
65
|
+
while node
|
|
66
|
+
return false if box(node).none?
|
|
67
|
+
|
|
68
|
+
parent = node.parent_element
|
|
69
|
+
return false if parent && rendered_children(parent).none? { |child| child.equal?(node) }
|
|
70
|
+
|
|
71
|
+
node = parent
|
|
72
|
+
end
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# --- the rendered text collection steps -----------------------------
|
|
77
|
+
|
|
78
|
+
def collect(node)
|
|
79
|
+
case node
|
|
80
|
+
when TextNode then collect_text(node)
|
|
81
|
+
when Element then collect_element(node)
|
|
82
|
+
else [] # a Comment / ProcessingInstruction carries no rendered text
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def collect_text(node)
|
|
87
|
+
box = box(node.parent_element)
|
|
88
|
+
return [] unless box.visible?
|
|
89
|
+
|
|
90
|
+
[TextRun.new(node.data.to_s, box.white_space, box.text_transform)]
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def collect_element(element)
|
|
94
|
+
box = box(element)
|
|
95
|
+
# Nothing inside a display:none box is rendered.
|
|
96
|
+
return [] if box.none?
|
|
97
|
+
|
|
98
|
+
items = collect_children(element)
|
|
99
|
+
# Invisible, or no box of its own (display:contents): the children's
|
|
100
|
+
# items alone.
|
|
101
|
+
return items if !box.visible? || box.contents?
|
|
102
|
+
# An atomic inline renders as one unbreakable box: a replaced element
|
|
103
|
+
# contributes nothing, an inline-block contributes its own (already
|
|
104
|
+
# collapsed) text, and whitespace on either side does not merge with
|
|
105
|
+
# what is inside. Block-level replaced elements still get line breaks
|
|
106
|
+
# below.
|
|
107
|
+
return [Atomic.new("")] if replaced?(element) && box.inline_level?
|
|
108
|
+
return [Atomic.new(WhitespaceCollapser.collapse(items))] if atomic_inline?(element, box)
|
|
109
|
+
|
|
110
|
+
items << Literal.new("\n") if tag(element) == "br"
|
|
111
|
+
items << Literal.new("\t") if table_cell_not_last?(element, box)
|
|
112
|
+
items << Literal.new("\n") if table_row_not_last?(element, box)
|
|
113
|
+
breaks = required_breaks(element, box)
|
|
114
|
+
breaks ? [RequiredBreak.new(breaks), *items, RequiredBreak.new(breaks)] : items
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def collect_children(element)
|
|
118
|
+
rendered_children(element).flat_map { |child| collect(child) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The child nodes that get a box, where the element restricts them: a
|
|
122
|
+
# replaced element renders none, a <select> only its optgroup / option
|
|
123
|
+
# children, a closed <details> only its first <summary>, and a shadow
|
|
124
|
+
# host only the children assigned to a slot.
|
|
125
|
+
def rendered_children(element)
|
|
126
|
+
children = element.child_nodes.to_a
|
|
127
|
+
if replaced?(element)
|
|
128
|
+
[]
|
|
129
|
+
elsif tag(element) == "select"
|
|
130
|
+
children.select { |child| option_like?(child) }
|
|
131
|
+
elsif closed_details?(element)
|
|
132
|
+
summary = children.grep(Element).find { |child| tag(child) == "summary" }
|
|
133
|
+
summary ? [summary] : []
|
|
134
|
+
elsif element.shadow_root
|
|
135
|
+
children.select { |child| child.respond_to?(:assigned_slot) && child.assigned_slot }
|
|
136
|
+
else
|
|
137
|
+
children
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# A <p> gets two required line breaks whatever its display, any other
|
|
142
|
+
# block-level box one.
|
|
143
|
+
def required_breaks(element, box)
|
|
144
|
+
if tag(element) == "p" then 2
|
|
145
|
+
elsif block_level?(element, box) then 1
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# optgroup / option act as block-level boxes whatever their computed
|
|
150
|
+
# display; table cells and rows are separated by tabs and newlines
|
|
151
|
+
# instead. With no computed display, the UA-default box decides.
|
|
152
|
+
def block_level?(element, box)
|
|
153
|
+
return true if option_like?(element)
|
|
154
|
+
return CSS::UAStylesheet::BLOCK_LEVEL_TAGS.include?(tag(element)) if box.display.nil?
|
|
155
|
+
return false if box.display == "table-cell" || box.display == "table-row"
|
|
156
|
+
|
|
157
|
+
!box.inline_level?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# A <p> keeps its two line breaks whatever its display.
|
|
161
|
+
def atomic_inline?(element, box)
|
|
162
|
+
tag(element) != "p" && ATOMIC_INLINE_DISPLAYS.include?(box.display)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def table_cell_not_last?(element, box)
|
|
166
|
+
return false unless box.display == "table-cell"
|
|
167
|
+
|
|
168
|
+
row = element.parent_element
|
|
169
|
+
return false unless row
|
|
170
|
+
|
|
171
|
+
cells = element_children(row).select { |child| box(child).display == "table-cell" }
|
|
172
|
+
!cells.last.equal?(element)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def table_row_not_last?(element, box)
|
|
176
|
+
return false unless box.display == "table-row"
|
|
177
|
+
|
|
178
|
+
table = element.parent_element
|
|
179
|
+
table = table.parent_element while table && box(table).display == "table-row-group"
|
|
180
|
+
return false unless table
|
|
181
|
+
|
|
182
|
+
!table_rows(table).last.equal?(element)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Every table-row box of a table, in tree order, descending through the
|
|
186
|
+
# row groups (tbody / thead / tfoot) but not into a nested table.
|
|
187
|
+
def table_rows(parent)
|
|
188
|
+
element_children(parent).flat_map do |child|
|
|
189
|
+
case box(child).display
|
|
190
|
+
when "table-row" then [child]
|
|
191
|
+
when "table-row-group" then table_rows(child)
|
|
192
|
+
else []
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# --- element facts --------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def tag(element) = element.local_name.to_s.downcase
|
|
200
|
+
|
|
201
|
+
def replaced?(element) = REPLACED.include?(tag(element))
|
|
202
|
+
|
|
203
|
+
def option_like?(node) = node.is_a?(Element) && OPTION_LIKE.include?(tag(node))
|
|
204
|
+
|
|
205
|
+
def closed_details?(element) = tag(element) == "details" && !element.has_attribute?("open")
|
|
206
|
+
|
|
207
|
+
def element_children(element) = element.child_nodes.to_a.grep(Element)
|
|
208
|
+
|
|
209
|
+
def box(element)
|
|
210
|
+
@boxes[element] ||= ComputedBox.from(style(element))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def style(element)
|
|
214
|
+
CSS::Cascade.computed_style(element)
|
|
215
|
+
rescue CSS::Parser::Unavailable
|
|
216
|
+
{}
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
module RenderedText
|
|
6
|
+
# The computed properties the collection steps read from one element.
|
|
7
|
+
#
|
|
8
|
+
# An empty computed style (no CSS layer to ask) falls back to each
|
|
9
|
+
# property's initial value. `display` alone has no usable one — `inline`
|
|
10
|
+
# would make every <div> inline — so it is nil there, and the caller
|
|
11
|
+
# falls back to the element's UA-default box instead.
|
|
12
|
+
ComputedBox = Data.define(:display, :visibility, :white_space, :text_transform) do
|
|
13
|
+
def self.from(style)
|
|
14
|
+
new(
|
|
15
|
+
display: value_of(style, "display"),
|
|
16
|
+
visibility: value_of(style, "visibility") || "visible",
|
|
17
|
+
white_space: value_of(style, "white-space") || "normal",
|
|
18
|
+
text_transform: value_of(style, "text-transform") || "none"
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.value_of(style, property)
|
|
23
|
+
value = style[property].to_s
|
|
24
|
+
value unless value.empty?
|
|
25
|
+
end
|
|
26
|
+
private_class_method :value_of
|
|
27
|
+
|
|
28
|
+
def visible? = visibility == "visible"
|
|
29
|
+
|
|
30
|
+
def none? = display == "none"
|
|
31
|
+
|
|
32
|
+
def contents? = display == "contents"
|
|
33
|
+
|
|
34
|
+
# Inline-level, counting an unknown display as the inline default.
|
|
35
|
+
def inline_level? = display.nil? || display.start_with?("inline")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
module RenderedText
|
|
6
|
+
# The `innerText` / `outerText` setters, both built on HTML's "rendered
|
|
7
|
+
# text fragment": the value split on line breaks, a Text node for each
|
|
8
|
+
# run of text between them and a <br> for each break (CRLF counts once).
|
|
9
|
+
module Fragment
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# The innerText setter: "set the inner text steps".
|
|
13
|
+
def set_inner(element, value)
|
|
14
|
+
element.__internal_replace_all__(fragment(element.document, value.to_s).map(&:__dommy_backend_node__))
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The outerText setter: the element is replaced by the fragment (an
|
|
19
|
+
# empty Text node when there is none), and the Text nodes it lands
|
|
20
|
+
# between merge with it.
|
|
21
|
+
def set_outer(element, value)
|
|
22
|
+
raise DOMException::NoModificationAllowedError, "outerText requires a parent" if element.parent_node.nil?
|
|
23
|
+
|
|
24
|
+
following = element.next_sibling
|
|
25
|
+
preceding = element.previous_sibling
|
|
26
|
+
nodes = fragment(element.document, value.to_s)
|
|
27
|
+
nodes = [element.document.create_text_node("")] if nodes.empty?
|
|
28
|
+
element.replace_with(*nodes)
|
|
29
|
+
merge_next_text_node(following)
|
|
30
|
+
merge_next_text_node(preceding) if preceding.is_a?(TextNode)
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fragment(document, value)
|
|
35
|
+
value.scan(/[^\r\n]+|\r\n|\r|\n/).map do |piece|
|
|
36
|
+
piece.start_with?("\r", "\n") ? document.create_element("br") : document.create_text_node(piece)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def merge_next_text_node(node)
|
|
41
|
+
return unless node.is_a?(TextNode)
|
|
42
|
+
|
|
43
|
+
following = node.next_sibling
|
|
44
|
+
return unless following.is_a?(TextNode)
|
|
45
|
+
|
|
46
|
+
node.data = node.data + following.data
|
|
47
|
+
following.remove
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The two setters are the module; the rest is how.
|
|
51
|
+
private_class_method :fragment, :merge_next_text_node
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
module RenderedText
|
|
6
|
+
# The white-space values under which a run's spaces, tabs and newlines
|
|
7
|
+
# collapse into one space.
|
|
8
|
+
COLLAPSIBLE_WHITE_SPACE = %w[normal nowrap pre-line].freeze
|
|
9
|
+
# The values under which a newline in the source stays a newline.
|
|
10
|
+
NEWLINE_PRESERVING_WHITE_SPACE = %w[pre pre-wrap pre-line break-spaces].freeze
|
|
11
|
+
# The values under which spaces and tabs stay as written.
|
|
12
|
+
SPACE_PRESERVING_WHITE_SPACE = %w[pre pre-wrap break-spaces].freeze
|
|
13
|
+
|
|
14
|
+
# The item list the collection steps produce holds four kinds of item.
|
|
15
|
+
|
|
16
|
+
# A run of text as it appears in the item list, before the block-wide
|
|
17
|
+
# white-space pass: the raw data plus the two computed properties that
|
|
18
|
+
# decide how it renders.
|
|
19
|
+
TextRun = Data.define(:text, :white_space, :text_transform) do
|
|
20
|
+
def empty? = text.empty?
|
|
21
|
+
|
|
22
|
+
def collapsible? = COLLAPSIBLE_WHITE_SPACE.include?(white_space)
|
|
23
|
+
|
|
24
|
+
def preserves_newlines? = NEWLINE_PRESERVING_WHITE_SPACE.include?(white_space)
|
|
25
|
+
|
|
26
|
+
def preserves_spaces? = SPACE_PRESERVING_WHITE_SPACE.include?(white_space)
|
|
27
|
+
|
|
28
|
+
# Collapsible whitespace and nothing else: beside a block break it
|
|
29
|
+
# neither shows nor separates anything.
|
|
30
|
+
def collapsible_whitespace_only? = collapsible? && text.match?(/\A[ \t\n\r\f]*\z/)
|
|
31
|
+
|
|
32
|
+
# The text with `text-transform` applied.
|
|
33
|
+
def transformed_text
|
|
34
|
+
case text_transform
|
|
35
|
+
when "uppercase" then text.upcase
|
|
36
|
+
when "lowercase" then text.downcase
|
|
37
|
+
when "capitalize" then text.gsub(/\b\p{L}/) { |c| c.upcase }
|
|
38
|
+
else text
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# An atomic inline box (an inline-block or a replaced element): its own
|
|
44
|
+
# rendered text, already collapsed and trimmed, which the surrounding
|
|
45
|
+
# block treats as a single unbreakable unit.
|
|
46
|
+
Atomic = Data.define(:text)
|
|
47
|
+
|
|
48
|
+
# A string the collection steps append as is: "\n" for a <br>, "\t"
|
|
49
|
+
# between table cells, "\n" between table rows.
|
|
50
|
+
Literal = Data.define(:text)
|
|
51
|
+
|
|
52
|
+
# A required line break count: the breaks around a block-level box (1)
|
|
53
|
+
# or a <p> (2). Adjacent counts collapse to the largest.
|
|
54
|
+
RequiredBreak = Data.define(:count)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "items"
|
|
4
|
+
|
|
5
|
+
module Dommy
|
|
6
|
+
module Internal
|
|
7
|
+
module RenderedText
|
|
8
|
+
# The block-wide white-space pass: turns the item list into the final
|
|
9
|
+
# string. The list is first normalized (see #normalize), then each item
|
|
10
|
+
# is appended in turn. Its three pieces of state are the whole algorithm:
|
|
11
|
+
# * `pending_space` — a collapsible space seen but not yet emitted (it is
|
|
12
|
+
# dropped if nothing follows on the line);
|
|
13
|
+
# * `line_start` — suppresses that space at the start of a line;
|
|
14
|
+
# * `after_atomic` — makes the space following an atomic inline literal,
|
|
15
|
+
# so whitespace on either side of an inline-block does not merge.
|
|
16
|
+
class WhitespaceCollapser
|
|
17
|
+
def self.collapse(items) = new.collapse(items)
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@out = +""
|
|
21
|
+
@pending_space = false
|
|
22
|
+
@line_start = true
|
|
23
|
+
@after_atomic = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def collapse(items)
|
|
27
|
+
normalize(items).each { |item| append(item) }
|
|
28
|
+
@out
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Drop empty runs, drop a collapsible whitespace-only run beside a
|
|
34
|
+
# required break (the whitespace around a block box neither shows nor
|
|
35
|
+
# separates the two breaks), trim the leading/trailing required breaks,
|
|
36
|
+
# and collapse each run of them to the largest count.
|
|
37
|
+
def normalize(items)
|
|
38
|
+
items = items.reject { |item| item.is_a?(TextRun) && item.empty? }
|
|
39
|
+
items = items.reject.with_index do |item, i|
|
|
40
|
+
item.is_a?(TextRun) && item.collapsible_whitespace_only? &&
|
|
41
|
+
(required_break_at?(items, i - 1) || required_break_at?(items, i + 1))
|
|
42
|
+
end
|
|
43
|
+
items.shift while items.first.is_a?(RequiredBreak)
|
|
44
|
+
items.pop while items.last.is_a?(RequiredBreak)
|
|
45
|
+
|
|
46
|
+
items.chunk_while { |a, b| a.is_a?(RequiredBreak) && b.is_a?(RequiredBreak) }.map do |chunk|
|
|
47
|
+
chunk.first.is_a?(RequiredBreak) ? RequiredBreak.new(chunk.map(&:count).max) : chunk.first
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def required_break_at?(items, index)
|
|
52
|
+
index >= 0 && items[index].is_a?(RequiredBreak)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def append(item)
|
|
56
|
+
case item
|
|
57
|
+
when RequiredBreak then append_breaks(item.count)
|
|
58
|
+
when Literal then append_literal(item.text)
|
|
59
|
+
when Atomic then append_atomic(item.text)
|
|
60
|
+
when TextRun then append_run(item)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Emit that many newlines and start a line.
|
|
65
|
+
def append_breaks(count)
|
|
66
|
+
@out << ("\n" * count)
|
|
67
|
+
@pending_space = false
|
|
68
|
+
@line_start = true
|
|
69
|
+
@after_atomic = false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def append_literal(text)
|
|
73
|
+
@out << text
|
|
74
|
+
@pending_space = false
|
|
75
|
+
@line_start = text == "\n"
|
|
76
|
+
@after_atomic = false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def append_atomic(text)
|
|
80
|
+
@out << " " if @pending_space
|
|
81
|
+
@out << text
|
|
82
|
+
@pending_space = false
|
|
83
|
+
@line_start = false
|
|
84
|
+
@after_atomic = true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def append_run(run)
|
|
88
|
+
run.transformed_text.each_char do |char|
|
|
89
|
+
case char
|
|
90
|
+
when "\n", "\r" then append_newline(run)
|
|
91
|
+
when " ", "\t", "\f" then append_space(char, run)
|
|
92
|
+
else append_char(char)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def append_newline(run)
|
|
98
|
+
if run.preserves_newlines?
|
|
99
|
+
@out << "\n"
|
|
100
|
+
@pending_space = false
|
|
101
|
+
@line_start = true
|
|
102
|
+
elsif run.collapsible?
|
|
103
|
+
@pending_space = true
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def append_space(char, run)
|
|
108
|
+
if run.preserves_spaces?
|
|
109
|
+
@out << char
|
|
110
|
+
@pending_space = false
|
|
111
|
+
@line_start = false
|
|
112
|
+
elsif @after_atomic
|
|
113
|
+
@out << " "
|
|
114
|
+
@after_atomic = false
|
|
115
|
+
else
|
|
116
|
+
@pending_space = true
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def append_char(char)
|
|
121
|
+
if @pending_space && !@line_start && !@out.end_with?(" ", "\n", "\t")
|
|
122
|
+
@out << " "
|
|
123
|
+
end
|
|
124
|
+
@pending_space = false
|
|
125
|
+
@after_atomic = false
|
|
126
|
+
@out << char
|
|
127
|
+
@line_start = false
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
# What every query method does with its selector argument before anything
|
|
6
|
+
# matches: coerce it per WebIDL, validate it against the grammar, and map
|
|
7
|
+
# the errors a backend raises onto the DOM ones.
|
|
8
|
+
#
|
|
9
|
+
# Matching itself is SelectorMatcher's, and the state pseudo-classes it
|
|
10
|
+
# cannot answer from the tree alone (`:checked`, `:target`) are here because
|
|
11
|
+
# they read live DOM state rather than the selector.
|
|
12
|
+
|
|
13
|
+
# The complete set of CSS pseudo-classes (+ the four legacy single-colon
|
|
14
|
+
# pseudo-elements). A `:identifier` outside this set is an unknown selector
|
|
15
|
+
# token → SyntaxError, whereas a known-but-unimplemented one (`:hover`) is a
|
|
16
|
+
# valid selector that simply matches nothing.
|
|
17
|
+
KNOWN_PSEUDOS = %w[
|
|
18
|
+
active any-link autofill blank checked current default defined disabled empty
|
|
19
|
+
enabled first first-child first-of-type focus focus-visible focus-within
|
|
20
|
+
fullscreen future has host hover in-range indeterminate invalid is lang
|
|
21
|
+
last-child last-of-type left link local-link modal not nth-child nth-col
|
|
22
|
+
nth-last-child nth-last-col nth-last-of-type nth-of-type only-child
|
|
23
|
+
only-of-type optional out-of-range past placeholder-shown playing paused
|
|
24
|
+
read-only read-write required right root scope target target-within
|
|
25
|
+
user-invalid user-valid valid visited where dir
|
|
26
|
+
before after first-line first-letter
|
|
27
|
+
].to_set.freeze
|
|
28
|
+
|
|
29
|
+
# Validate a non-null CSS selector for `querySelector`/`matches`/`closest`,
|
|
30
|
+
# raising SyntaxError for syntactically invalid selectors. Delegates to the
|
|
31
|
+
# full grammar parser (SelectorParser), which catches the whole Selectors
|
|
32
|
+
# grammar — `[*=v]`, `..x`, `div % p`, unknown pseudo-elements, undeclared
|
|
33
|
+
# namespaces — not just the obvious cases.
|
|
34
|
+
def self.validate_selector!(selector)
|
|
35
|
+
SelectorParser.validate!(selector.to_s)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Coerce the JS argument of a query method (querySelector/All) per WebIDL: the
|
|
39
|
+
# selector is a *non-nullable* DOMString, so JS `null` → "null" and
|
|
40
|
+
# `undefined` → "undefined" (which then match `<null>` / `<undefined>` typed
|
|
41
|
+
# elements rather than returning nothing), while a missing argument is a
|
|
42
|
+
# TypeError. Used at every JS dispatch site so the behaviour is uniform.
|
|
43
|
+
def self.css_query_arg!(args)
|
|
44
|
+
raise ::Dommy::Bridge::TypeError, "1 argument required, but only 0 present" if args.empty?
|
|
45
|
+
|
|
46
|
+
value = args[0]
|
|
47
|
+
return "null" if value.nil?
|
|
48
|
+
return "undefined" if defined?(::Dommy::Bridge::UNDEFINED) && value.equal?(::Dommy::Bridge::UNDEFINED)
|
|
49
|
+
|
|
50
|
+
value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Map a backend's selector complaints onto the DOM's:
|
|
54
|
+
# - an "Unregistered function" means a valid pseudo the backend compiled
|
|
55
|
+
# but can't evaluate (`:active`, `:invalid`, …) → degrade to no match
|
|
56
|
+
# (returns []),
|
|
57
|
+
# - a backend syntax complaint becomes a DOMException::SyntaxError,
|
|
58
|
+
# - anything else propagates.
|
|
59
|
+
def self.with_selector_errors(selector)
|
|
60
|
+
yield
|
|
61
|
+
rescue ::StandardError => e
|
|
62
|
+
return [] if e.message.include?("Unregistered function")
|
|
63
|
+
raise DOMException::SyntaxError, "'#{selector}' is not a valid selector." if e.message.include?("unexpected")
|
|
64
|
+
|
|
65
|
+
raise
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# `:checked`'s checkedness/selectedness is live state, not the attribute:
|
|
69
|
+
# checkbox/radio inputs match on the checked property (which defaults to
|
|
70
|
+
# the attribute), <option> on selectedness.
|
|
71
|
+
def self.checked_state?(element)
|
|
72
|
+
return false unless element
|
|
73
|
+
|
|
74
|
+
case element.tag_name
|
|
75
|
+
when "INPUT"
|
|
76
|
+
%w[checkbox radio].include?(element.respond_to?(:type) ? element.type.to_s : "") &&
|
|
77
|
+
element.respond_to?(:checked) && !!element.checked
|
|
78
|
+
when "OPTION"
|
|
79
|
+
element.respond_to?(:selected) && !!element.selected
|
|
80
|
+
else
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# The id referenced by the document's URL fragment (`:target`), or nil when
|
|
86
|
+
# there is no fragment.
|
|
87
|
+
def self.target_id(document)
|
|
88
|
+
view = document.respond_to?(:default_view) ? document.default_view : nil
|
|
89
|
+
loc = view.respond_to?(:location) ? view.location : nil if view
|
|
90
|
+
hash = loc&.__js_get__("hash").to_s
|
|
91
|
+
hash.start_with?("#") && hash.length > 1 ? hash[1..] : nil
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -141,7 +141,9 @@ module Dommy
|
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
def record(bnode, enter)
|
|
144
|
-
name
|
|
144
|
+
# By local name, as a type selector compares: a prefixed element parsed
|
|
145
|
+
# from XML (`cp:coreProperties`) is found by `coreProperties`.
|
|
146
|
+
name = bnode.local_name
|
|
145
147
|
(@by_tag[name.downcase] ||= []) << [enter, bnode] if name && !name.empty?
|
|
146
148
|
|
|
147
149
|
id = bnode["id"]
|