dommy 0.15.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 (207) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dommy/animation.rb +1 -0
  3. data/lib/dommy/backend.rb +19 -15
  4. data/lib/dommy/blob.rb +23 -0
  5. data/lib/dommy/bridge/values.rb +29 -2
  6. data/lib/dommy/bridge/wire_tags.rb +8 -4
  7. data/lib/dommy/browser.rb +335 -52
  8. data/lib/dommy/character_data.rb +21 -40
  9. data/lib/dommy/class_list.rb +24 -9
  10. data/lib/dommy/cookie_jar.rb +408 -0
  11. data/lib/dommy/cookie_store.rb +105 -49
  12. data/lib/dommy/css.rb +34 -8
  13. data/lib/dommy/custom_elements.rb +362 -114
  14. data/lib/dommy/data_transfer.rb +22 -2
  15. data/lib/dommy/document.rb +1114 -206
  16. data/lib/dommy/dom_exception.rb +6 -0
  17. data/lib/dommy/dom_parser.rb +46 -14
  18. data/lib/dommy/dom_string_list.rb +36 -0
  19. data/lib/dommy/element.rb +228 -102
  20. data/lib/dommy/element_internals.rb +227 -0
  21. data/lib/dommy/encodings/legacy_multibyte.rb +800 -0
  22. data/lib/dommy/encodings/multibyte_overrides.rb +76 -0
  23. data/lib/dommy/encodings/prescan.rb +234 -0
  24. data/lib/dommy/encodings.rb +126 -52
  25. data/lib/dommy/event.rb +472 -163
  26. data/lib/dommy/event_source.rb +5 -30
  27. data/lib/dommy/fetch.rb +244 -53
  28. data/lib/dommy/file_reader.rb +6 -29
  29. data/lib/dommy/form_data.rb +62 -15
  30. data/lib/dommy/form_entry_list.rb +108 -139
  31. data/lib/dommy/fragment.rb +11 -10
  32. data/lib/dommy/history.rb +247 -90
  33. data/lib/dommy/html_canvas_element.rb +95 -42
  34. data/lib/dommy/html_collection.rb +49 -30
  35. data/lib/dommy/html_elements/base.rb +112 -69
  36. data/lib/dommy/html_elements/embedded.rb +349 -105
  37. data/lib/dommy/html_elements/forms/form.rb +407 -368
  38. data/lib/dommy/html_elements/forms/input.rb +336 -174
  39. data/lib/dommy/html_elements/forms/select.rb +90 -74
  40. data/lib/dommy/html_elements/interactive.rb +329 -111
  41. data/lib/dommy/html_elements/media.rb +71 -30
  42. data/lib/dommy/html_elements/metadata.rb +238 -71
  43. data/lib/dommy/html_elements/miscellaneous.rb +58 -14
  44. data/lib/dommy/html_elements/tables.rb +7 -9
  45. data/lib/dommy/html_elements.rb +23 -1
  46. data/lib/dommy/hyperlink_utils.rb +15 -4
  47. data/lib/dommy/interaction/drag_and_drop.rb +187 -0
  48. data/lib/dommy/interaction/event_synthesis.rb +182 -24
  49. data/lib/dommy/interaction/field_interactor.rb +17 -5
  50. data/lib/dommy/interaction/form_submission.rb +31 -43
  51. data/lib/dommy/interaction/key_sender.rb +101 -16
  52. data/lib/dommy/interaction.rb +1 -0
  53. data/lib/dommy/internal/accessibility_tree.rb +149 -150
  54. data/lib/dommy/internal/accessibility_visibility.rb +16 -17
  55. data/lib/dommy/internal/accessible_description.rb +1 -1
  56. data/lib/dommy/internal/accessible_name.rb +231 -253
  57. data/lib/dommy/internal/aria_role.rb +182 -195
  58. data/lib/dommy/internal/aria_snapshot.rb +35 -38
  59. data/lib/dommy/internal/aria_state.rb +102 -118
  60. data/lib/dommy/internal/autofill.rb +75 -75
  61. data/lib/dommy/internal/backend_prefilter.rb +126 -133
  62. data/lib/dommy/internal/ce_reactions.rb +285 -0
  63. data/lib/dommy/internal/ce_reactions_table.rb +296 -0
  64. data/lib/dommy/internal/child_list.rb +7 -43
  65. data/lib/dommy/internal/child_navigable.rb +158 -0
  66. data/lib/dommy/internal/child_node.rb +44 -41
  67. data/lib/dommy/internal/close_watcher_manager.rb +239 -0
  68. data/lib/dommy/internal/complex_match.rb +3 -1
  69. data/lib/dommy/internal/constraint_validation.rb +130 -0
  70. data/lib/dommy/internal/css/ancestor_filter.rb +107 -0
  71. data/lib/dommy/internal/css/calc.rb +90 -71
  72. data/lib/dommy/internal/css/cascade.rb +72 -70
  73. data/lib/dommy/internal/css/cascaded_declarations.rb +4 -1
  74. data/lib/dommy/internal/css/color.rb +249 -187
  75. data/lib/dommy/internal/css/computed_style_builder.rb +43 -3
  76. data/lib/dommy/internal/css/computed_style_declaration.rb +50 -11
  77. data/lib/dommy/internal/css/counters.rb +67 -67
  78. data/lib/dommy/internal/css/custom_properties.rb +115 -103
  79. data/lib/dommy/internal/css/parser.rb +273 -257
  80. data/lib/dommy/internal/css/property_registry.rb +321 -302
  81. data/lib/dommy/internal/css/renderability.rb +39 -31
  82. data/lib/dommy/internal/css/rule_index.rb +151 -100
  83. data/lib/dommy/internal/css/rule_index_inputs.rb +135 -0
  84. data/lib/dommy/internal/css/selector_dependencies.rb +8 -2
  85. data/lib/dommy/internal/css/style_cache.rb +7 -4
  86. data/lib/dommy/internal/css/supports.rb +115 -113
  87. data/lib/dommy/internal/css/ua_stylesheet.rb +60 -24
  88. data/lib/dommy/internal/css_rule_text.rb +121 -119
  89. data/lib/dommy/internal/css_source.rb +1 -3
  90. data/lib/dommy/internal/css_syntax.rb +157 -92
  91. data/lib/dommy/internal/declarative_shadow_roots.rb +183 -0
  92. data/lib/dommy/internal/directionality.rb +120 -123
  93. data/lib/dommy/internal/document_generations.rb +55 -2
  94. data/lib/dommy/internal/document_interaction_state.rb +293 -9
  95. data/lib/dommy/internal/document_live_ranges.rb +26 -1
  96. data/lib/dommy/internal/dom_matching.rb +185 -184
  97. data/lib/dommy/internal/element_aria.rb +25 -19
  98. data/lib/dommy/internal/element_geometry.rb +8 -1
  99. data/lib/dommy/internal/element_matching.rb +71 -69
  100. data/lib/dommy/internal/element_popover.rb +226 -25
  101. data/lib/dommy/internal/element_shadow.rb +98 -39
  102. data/lib/dommy/internal/element_state.rb +264 -181
  103. data/lib/dommy/internal/element_top_layer.rb +15 -1
  104. data/lib/dommy/internal/enumerated_keyword_sets.rb +40 -0
  105. data/lib/dommy/internal/event_handler_tables.rb +241 -0
  106. data/lib/dommy/internal/event_handlers.rb +151 -0
  107. data/lib/dommy/internal/exception_report.rb +89 -87
  108. data/lib/dommy/internal/focusability.rb +267 -0
  109. data/lib/dommy/internal/form_associated_custom_elements.rb +127 -0
  110. data/lib/dommy/internal/global_functions.rb +49 -47
  111. data/lib/dommy/internal/html_or_svg_or_mathml_element.rb +34 -23
  112. data/lib/dommy/internal/html_serialization.rb +292 -0
  113. data/lib/dommy/internal/idl_reflection.rb +204 -0
  114. data/lib/dommy/internal/idl_reflection_table.rb +589 -0
  115. data/lib/dommy/internal/infra.rb +9 -0
  116. data/lib/dommy/internal/input_type.rb +76 -18
  117. data/lib/dommy/internal/insertion_point.rb +27 -3
  118. data/lib/dommy/internal/js_number.rb +62 -0
  119. data/lib/dommy/internal/light_dismiss.rb +176 -0
  120. data/lib/dommy/internal/literal_lookup.rb +75 -75
  121. data/lib/dommy/internal/mime_type.rb +55 -42
  122. data/lib/dommy/internal/mutation_coordinator.rb +140 -62
  123. data/lib/dommy/internal/mutation_observer_microtask.rb +60 -0
  124. data/lib/dommy/internal/namespaces.rb +51 -49
  125. data/lib/dommy/internal/node_adopter.rb +77 -5
  126. data/lib/dommy/internal/node_equality.rb +61 -59
  127. data/lib/dommy/internal/node_factory.rb +111 -7
  128. data/lib/dommy/internal/node_identity.rb +1 -1
  129. data/lib/dommy/internal/node_traversal.rb +22 -4
  130. data/lib/dommy/internal/node_wrapper_cache.rb +6 -38
  131. data/lib/dommy/internal/observer_options.rb +24 -14
  132. data/lib/dommy/internal/origin.rb +86 -0
  133. data/lib/dommy/internal/parent_node.rb +10 -8
  134. data/lib/dommy/internal/popover_invoker_element.rb +67 -0
  135. data/lib/dommy/internal/popover_stack.rb +120 -0
  136. data/lib/dommy/internal/post_insertion_steps.rb +141 -71
  137. data/lib/dommy/internal/referrer.rb +144 -0
  138. data/lib/dommy/internal/reflected_attributes.rb +128 -18
  139. data/lib/dommy/internal/rendered_text/collector.rb +23 -8
  140. data/lib/dommy/internal/rendered_text/computed_box.rb +3 -2
  141. data/lib/dommy/internal/rendered_text/fragment.rb +37 -34
  142. data/lib/dommy/internal/rendered_text/items.rb +7 -5
  143. data/lib/dommy/internal/selector_arguments.rb +2 -1
  144. data/lib/dommy/internal/selector_match.rb +1 -1
  145. data/lib/dommy/internal/selector_matcher.rb +361 -354
  146. data/lib/dommy/internal/selector_parser.rb +88 -63
  147. data/lib/dommy/internal/sequential_focus_navigation.rb +209 -0
  148. data/lib/dommy/internal/shadow_root_registry.rb +14 -0
  149. data/lib/dommy/internal/slot_assignment.rb +117 -0
  150. data/lib/dommy/internal/slot_signals.rb +125 -0
  151. data/lib/dommy/internal/template_content_registry.rb +7 -1
  152. data/lib/dommy/internal/text_selection.rb +185 -32
  153. data/lib/dommy/internal/toggle_events.rb +6 -4
  154. data/lib/dommy/internal/unsafe_html.rb +109 -0
  155. data/lib/dommy/internal/url_parser.rb +89 -37
  156. data/lib/dommy/internal/user_activation.rb +192 -0
  157. data/lib/dommy/internal/web_idl.rb +176 -38
  158. data/lib/dommy/internal/window_bound.rb +14 -0
  159. data/lib/dommy/internal/window_constructors.rb +29 -6
  160. data/lib/dommy/internal/xml_serialization.rb +298 -296
  161. data/lib/dommy/js/constructor_resolver.rb +18 -0
  162. data/lib/dommy/js/custom_element_bridge.rb +57 -82
  163. data/lib/dommy/js/dom_interfaces.rb +63 -16
  164. data/lib/dommy/js/host_bridge.rb +212 -41
  165. data/lib/dommy/js/host_callbacks.rb +7 -0
  166. data/lib/dommy/js/host_runtime.js +2494 -448
  167. data/lib/dommy/js/import_map.rb +1 -1
  168. data/lib/dommy/js/marshaller.rb +13 -4
  169. data/lib/dommy/js/module_loader.rb +37 -6
  170. data/lib/dommy/js/module_preload.rb +107 -29
  171. data/lib/dommy/js/observable_runtime.js +295 -128
  172. data/lib/dommy/js/script_boot.rb +197 -115
  173. data/lib/dommy/js/serialized_record.rb +32 -0
  174. data/lib/dommy/js/webidl_event_handlers.js +45 -0
  175. data/lib/dommy/js/webidl_members.js +196 -0
  176. data/lib/dommy/js/webidl_signatures.js +2084 -0
  177. data/lib/dommy/js/webidl_tables.js +47 -100
  178. data/lib/dommy/js/window_wiring.rb +9 -5
  179. data/lib/dommy/location.rb +129 -76
  180. data/lib/dommy/media_query_list.rb +5 -13
  181. data/lib/dommy/message_channel.rb +139 -64
  182. data/lib/dommy/mutation_observer.rb +27 -29
  183. data/lib/dommy/navigation.rb +59 -9
  184. data/lib/dommy/navigator.rb +135 -6
  185. data/lib/dommy/node.rb +23 -11
  186. data/lib/dommy/notification.rb +10 -1
  187. data/lib/dommy/promise.rb +20 -0
  188. data/lib/dommy/range.rb +25 -37
  189. data/lib/dommy/resources.rb +38 -3
  190. data/lib/dommy/scheduler.rb +176 -48
  191. data/lib/dommy/shadow_root.rb +141 -19
  192. data/lib/dommy/storage.rb +135 -25
  193. data/lib/dommy/streams.rb +18 -0
  194. data/lib/dommy/structured_data.rb +45 -0
  195. data/lib/dommy/style_declaration.rb +31 -4
  196. data/lib/dommy/text_codec.rb +9 -19
  197. data/lib/dommy/tree_walker.rb +78 -79
  198. data/lib/dommy/url.rb +11 -4
  199. data/lib/dommy/validity_state.rb +109 -107
  200. data/lib/dommy/version.rb +1 -1
  201. data/lib/dommy/web_socket.rb +6 -28
  202. data/lib/dommy/window.rb +833 -103
  203. data/lib/dommy/worker.rb +5 -28
  204. data/lib/dommy/xml_http_request.rb +43 -46
  205. data/lib/dommy.rb +52 -3
  206. metadata +45 -6
  207. data/lib/dommy/internal/cookie_jar.rb +0 -29
@@ -36,8 +36,6 @@ module Dommy
36
36
  # Where a call of the function #next_function is asked for may start, or
37
37
  # an atom: one pattern per function name, built on its first use.
38
38
  FUNCTION_STOP = Hash.new { |stops, name| stops[name] = Regexp.union(ATOM_START, /#{Regexp.escape(name[0])}/i) }
39
- # The bracket that closes a block each opening one starts.
40
- CLOSER = {"(" => ")", "[" => "]", "{" => "}"}.freeze
41
39
 
42
40
  # `bytes:` is a binary String this CssSource may keep as its copy, in
43
41
  # place of `text` — a reader handing on what it has already made.
@@ -195,7 +193,7 @@ module Dommy
195
193
  # closing bracket closes the innermost only when it is that block's own
196
194
  # kind (:closed); any other closing bracket is a token like any other.
197
195
  def step_block(blocks, c)
198
- if (closer = CLOSER[c])
196
+ if (closer = CssSyntax::CLOSER[c])
199
197
  blocks.push(closer)
200
198
  :opened
201
199
  elsif !blocks.empty? && blocks.last == c
@@ -19,6 +19,8 @@ module Dommy
19
19
  module CssSyntax
20
20
  OPENING_BRACKETS = "([{"
21
21
  CLOSING_BRACKETS = ")]}"
22
+ # The mirror each opening bracket's block (or function) closes at.
23
+ CLOSER = {"(" => ")", "[" => "]", "{" => "}"}.freeze
22
24
  # css-syntax-3's whitespace (§4.2) — before §3.3's preprocessing folds CR
23
25
  # and FF into LF, the same set as Infra's ASCII whitespace.
24
26
  WHITESPACE = Infra::ASCII_WHITESPACE_CHARS
@@ -35,119 +37,182 @@ module Dommy
35
37
  STRING_STOP = {'"' => /["\\\n]/, "'" => /['\\\n]/}.freeze
36
38
  # What can end an unquoted url, or must be stepped over in one.
37
39
  URL_STOP = /[)\\]/
40
+ # What makes an unquoted url bad (§4.3.6), whitespace and escapes aside:
41
+ # a quote, a `(`, or a non-printable code point (§4.2).
42
+ BAD_URL_CODE_POINT = /["'(\u0000-\u0008\u000B\u000E-\u001F\u007F]/
38
43
  # css-syntax-3 §3.3: the newline forms and NULL the tokenizer never sees.
39
44
  NEEDS_FILTERING = /[\r\f\u0000]/
40
45
 
41
- module_function
46
+ class << self
47
+ # css-syntax-3 §3.3 "filter code points", the pass that runs before the
48
+ # tokenizer sees anything: the three newline forms become U+000A, and
49
+ # U+0000 becomes U+FFFD. Without it, a backslash before a CRLF escapes
50
+ # the CR and leaves the LF to end the string, as no browser reads it.
51
+ # The replacement character is itself an ident code point, so `.a<NUL>b`
52
+ # names the class `a<U+FFFD>b` rather than being a syntax error.
53
+ def preprocess(text)
54
+ return text unless text.match?(NEEDS_FILTERING)
55
+
56
+ text.gsub(/\r\n|[\r\f]/, "\n").gsub("\u0000", "\uFFFD")
57
+ end
42
58
 
43
- # css-syntax-3 §3.3 "filter code points", the pass that runs before the
44
- # tokenizer sees anything: the three newline forms become U+000A, and
45
- # U+0000 becomes U+FFFD. Without it, a backslash before a CRLF escapes
46
- # the CR and leaves the LF to end the string, as no browser reads it.
47
- # The replacement character is itself an ident code point, so `.a<NUL>b`
48
- # names the class `a<U+FFFD>b` rather than being a syntax error.
49
- def preprocess(text)
50
- return text unless text.match?(NEEDS_FILTERING)
59
+ # §4.3.8: a backslash at `j` starts a valid escape unless a newline
60
+ # follows it. Every other pair counts, the end of the input included.
61
+ def valid_escape_at?(source, j)
62
+ source[j] == "\\" && source[j + 1] != "\n"
63
+ end
51
64
 
52
- text.gsub(/\r\n|[\r\f]/, "\n").gsub("\u0000", "\uFFFD")
53
- end
65
+ # §4.2 ident-start code point: a letter, an underscore, or a non-ASCII
66
+ # ident code point. nil (past the end) is none.
67
+ def ident_start_code_point?(c)
68
+ return false if c.nil?
54
69
 
55
- # §4.3.8: a backslash at `j` starts a valid escape unless a newline
56
- # follows it. Every other pair counts, the end of the input included.
57
- def valid_escape_at?(source, j)
58
- source[j] == "\\" && source[j + 1] != "\n"
59
- end
70
+ c.match?(/[A-Za-z_]/) || non_ascii_ident_code_point?(c)
71
+ end
60
72
 
61
- # §4.2 ident-start code point: a letter, an underscore, or a non-ASCII
62
- # ident code point. nil (past the end) is none.
63
- def ident_start_code_point?(c)
64
- return false if c.nil?
73
+ # §4.2 ident code point: an ident-start one, a digit, or U+002D.
74
+ def name_code_point?(c)
75
+ return false if c.nil?
65
76
 
66
- c.match?(/[A-Za-z_]/) || non_ascii_ident_code_point?(c)
67
- end
77
+ c.match?(/[A-Za-z0-9_\-]/) || non_ascii_ident_code_point?(c)
78
+ end
68
79
 
69
- # §4.2 ident code point: an ident-start one, a digit, or U+002D.
70
- def name_code_point?(c)
71
- return false if c.nil?
80
+ # Index just past the escape, string, unquoted url or comment starting
81
+ # at `j`, or nil if none starts there. A string ends at its closing
82
+ # quote, or before a newline or the end of the input; a url at its `)`
83
+ # or the end of the input; a comment at its `*/` or the end of the input.
84
+ def atom_end(source, j)
85
+ c = source[j]
86
+ if (c == "u" || c == "U") && (k = url_contents_start(source, j))
87
+ url_end(source, k)
88
+ elsif c == "\\"
89
+ return nil unless valid_escape_at?(source, j)
90
+
91
+ [j + 2, source.length].min
92
+ elsif c == '"' || c == "'"
93
+ string_end(source, j + 1, c)
94
+ elsif c == "/" && source[j + 1] == "*"
95
+ close = source.index("*/", j + 2)
96
+ close ? close + 2 : source.length
97
+ end
98
+ end
72
99
 
73
- c.match?(/[A-Za-z0-9_\-]/) || non_ascii_ident_code_point?(c)
74
- end
100
+ # Whether the string or unquoted url starting at `j` is a bad one: a
101
+ # <bad-string-token>, which a newline ends unclosed (§4.3.5), or a
102
+ # <bad-url-token> (§4.3.6). No grammar takes either, not even
103
+ # <any-value>.
104
+ def bad_token_at?(source, j)
105
+ c = source[j]
106
+ if (c == "u" || c == "U") && (k = url_contents_start(source, j))
107
+ bad_url?(source, k)
108
+ elsif c == '"' || c == "'"
109
+ k = string_stop(source, j + 1, c)
110
+ !k.nil? && source[k] == "\n"
111
+ else
112
+ false
113
+ end
114
+ end
75
115
 
76
- # A single byte of a binary String at or above 0x80 is part of a
77
- # non-ASCII character that a byte-level reader cannot see whole; it is
78
- # taken for a name code point, which only decides whether a `url(` or
79
- # `var(` after it is the tail of a longer name.
80
- def non_ascii_ident_code_point?(c)
81
- codepoint = c.ord
82
- return false if codepoint < 0x80
83
- return true if c.bytesize == 1 || codepoint >= 0x10000
116
+ private
84
117
 
85
- NON_ASCII_IDENT_RANGES.any? { |range| range.cover?(codepoint) }
86
- end
118
+ # A single byte of a binary String at or above 0x80 is part of a
119
+ # non-ASCII character that a byte-level reader cannot see whole; it is
120
+ # taken for a name code point, which only decides whether a `url(` or
121
+ # `var(` after it is the tail of a longer name.
122
+ def non_ascii_ident_code_point?(c)
123
+ codepoint = c.ord
124
+ return false if codepoint < 0x80
125
+ return true if c.bytesize == 1 || codepoint >= 0x10000
87
126
 
88
- # Index just past the escape, string, unquoted url or comment starting
89
- # at `j`, or nil if none starts there. A string ends at its closing
90
- # quote, or before a newline or the end of the input; a url at its `)`
91
- # or the end of the input; a comment at its `*/` or the end of the input.
92
- def atom_end(source, j)
93
- c = source[j]
94
- if (c == "u" || c == "U") && (k = url_contents_start(source, j))
95
- url_end(source, k)
96
- elsif c == "\\"
97
- return nil unless valid_escape_at?(source, j)
98
-
99
- [j + 2, source.length].min
100
- elsif c == '"' || c == "'"
101
- string_end(source, j + 1, c)
102
- elsif c == "/" && source[j + 1] == "*"
103
- close = source.index("*/", j + 2)
104
- close ? close + 2 : source.length
127
+ NON_ASCII_IDENT_RANGES.any? { |range| range.cover?(codepoint) }
105
128
  end
106
- end
107
129
 
108
- # Index just past the `quote` closing a string whose contents start at
109
- # `k`, or of the newline that ends it unclosed, or the end of the input.
110
- # A backslash takes the code point after it (§4.3.5).
111
- def string_end(source, k, quote)
112
- stop = STRING_STOP[quote]
113
- while (k = source.index(stop, k))
114
- d = source[k]
115
- return k + 1 if d == quote
116
- return k if d == "\n"
117
-
118
- k += 2
130
+ # Index just past the `quote` closing a string whose contents start at
131
+ # `k`, or of the newline that ends it unclosed, or the end of the input.
132
+ # A backslash takes the code point after it (§4.3.5).
133
+ def string_end(source, k, quote)
134
+ k = string_stop(source, k, quote)
135
+ return source.length if k.nil?
136
+
137
+ source[k] == quote ? k + 1 : k
119
138
  end
120
- source.length
121
- end
122
139
 
123
- # Where an unquoted url token's contents start, if the ident `url`
124
- # (ASCII case-insensitive, not the tail of a longer name) followed by
125
- # `(` starts at `j` — after the whitespace the url skips. A quote there
126
- # makes it a `url(` function holding a string instead (§4.3.4).
127
- def url_contents_start(source, j)
128
- return nil unless source[j, 4]&.casecmp?("url(")
129
- return nil if j.positive? && name_code_point?(source[j - 1])
130
-
131
- k = j + 4
132
- k += 1 while k < source.length && WHITESPACE.include?(source[k])
133
- q = source[k]
134
- q == '"' || q == "'" ? nil : k
135
- end
140
+ # Index of the `quote` closing a string whose contents start at `k`, or
141
+ # of the newline that ends it unclosed; nil if the input ends first.
142
+ def string_stop(source, k, quote)
143
+ stop = STRING_STOP[quote]
144
+ while (k = source.index(stop, k))
145
+ return k unless source[k] == "\\"
136
146
 
137
- # Index just past the `)` closing an unquoted url whose contents start
138
- # at `k`, or the end of the input. An escape is stepped over; whatever
139
- # else would make the url bad (§4.3.6's consume the remnants of a bad
140
- # url) still ends at the same `)`.
141
- def url_end(source, k)
142
- while (k = source.index(URL_STOP, k))
143
- return k + 1 if source[k] == ")"
147
+ k += 2
148
+ end
149
+ nil
150
+ end
144
151
 
145
- k += valid_escape_at?(source, k) ? 2 : 1
152
+ # Where an unquoted url token's contents start, if the ident `url`
153
+ # (ASCII case-insensitive, not the tail of a longer name) followed by
154
+ # `(` starts at `j` — after the whitespace the url skips. A quote there
155
+ # makes it a `url(` function holding a string instead (§4.3.4).
156
+ def url_contents_start(source, j)
157
+ return nil unless source[j, 4]&.casecmp?("url(")
158
+ return nil if j.positive? && name_code_point?(source[j - 1])
159
+
160
+ k = j + 4
161
+ k += 1 while k < source.length && WHITESPACE.include?(source[k])
162
+ q = source[k]
163
+ q == '"' || q == "'" ? nil : k
164
+ end
165
+
166
+ # Index just past the `)` closing an unquoted url whose contents start
167
+ # at `k`, or the end of the input. An escape is stepped over; whatever
168
+ # else would make the url bad (§4.3.6's consume the remnants of a bad
169
+ # url) still ends at the same `)`.
170
+ def url_end(source, k)
171
+ while (k = source.index(URL_STOP, k))
172
+ return k + 1 if source[k] == ")"
173
+
174
+ k += valid_escape_at?(source, k) ? 2 : 1
175
+ end
176
+ source.length
177
+ end
178
+
179
+ # Whether the unquoted url whose contents start at `k` is bad (§4.3.6):
180
+ # whitespace before anything but its `)` or the end of the input, a
181
+ # BAD_URL_CODE_POINT, or a backslash that starts no valid escape.
182
+ def bad_url?(source, k)
183
+ while k < source.length
184
+ c = source[k]
185
+ return false if c == ")"
186
+
187
+ if WHITESPACE.include?(c)
188
+ k += 1 while k < source.length && WHITESPACE.include?(source[k])
189
+ return k < source.length && source[k] != ")"
190
+ end
191
+ return true if c.match?(BAD_URL_CODE_POINT)
192
+
193
+ if c == "\\"
194
+ return true unless valid_escape_at?(source, k)
195
+
196
+ k = escape_end(source, k)
197
+ else
198
+ k += 1
199
+ end
200
+ end
201
+ false
146
202
  end
147
- source.length
148
- end
149
203
 
150
- private_class_method :non_ascii_ident_code_point?, :string_end, :url_contents_start, :url_end
204
+ # Index just past the valid escape whose backslash is at `j` (§4.3.7):
205
+ # up to six hex digits and one whitespace after them, or else the one
206
+ # code point after the backslash.
207
+ def escape_end(source, j)
208
+ hex = source[j + 1, 6].to_s[/\A\h+/]
209
+ return [j + 2, source.length].min if hex.nil?
210
+
211
+ k = j + 1 + hex.length
212
+ k += 1 if k < source.length && WHITESPACE.include?(source[k])
213
+ k
214
+ end
215
+ end
151
216
  end
152
217
  end
153
218
  end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Internal
5
+ # Declarative shadow roots, as a pass over what the parser built.
6
+ #
7
+ # The backend's HTML parser has no declarative shadow DOM: it parses a
8
+ # `<template shadowrootmode>` as an ordinary template, with its children
9
+ # in the template contents. HTML's tree builder, when its "allow
10
+ # declarative shadow roots" is on, does instead — at the template start
11
+ # tag ("in head" insertion mode) — attach a shadow root to the adjusted
12
+ # current node and parse the template's children into it, never
13
+ # inserting the template. This pass reproduces that result: it walks the
14
+ # parsed nodes in tree order, which is the order the parser met the start
15
+ # tags, and for each such template
16
+ #
17
+ # - takes its parent as the declarative shadow host (the adjusted
18
+ # current node the template was inserted under). A template whose
19
+ # parent is not an element — the top of template contents (the
20
+ # adjusted current node was the enclosing template), or of a fragment
21
+ # parse (see below) — is left alone;
22
+ # - leaves the template in place when the host is already a shadow host
23
+ # (the first declarative template wins), or when attaching a shadow
24
+ # root throws (not a valid shadow host, a definition that disables
25
+ # shadow);
26
+ # - otherwise attaches one with the template's shadowrootmode,
27
+ # shadowrootdelegatesfocus, shadowrootserializable,
28
+ # shadowrootslotassignment and shadowrootclonable, and a null registry
29
+ # with shadowrootcustomelementregistry (else the host's document's);
30
+ # sets it declarative and available to element internals (and keep
31
+ # custom element registry null with that attribute), moves the template
32
+ # contents into it and drops the template.
33
+ #
34
+ # Template contents, and the shadow trees the pass makes, are walked too:
35
+ # the parser attaches declarative shadow roots wherever it meets one.
36
+ #
37
+ # The fragment case: HTML's fragment parsing algorithm puts only its own
38
+ # `html` root on the stack of open elements, so for a top-level template
39
+ # the adjusted current node is the context element, and the spec text
40
+ # would attach a shadow root to it. Browsers (and the WPT expectations)
41
+ # leave such a template in place, and so does this pass, unless the
42
+ # caller names a `top_host` — document.write, whose parser is the
43
+ # document's own, where the template's adjusted current node really is
44
+ # the element the markup goes into.
45
+ #
46
+ # Moving the contents is a representation fix-up, not a DOM mutation: in
47
+ # the parser's tree they were never anywhere else. So it uses raw backend
48
+ # moves, and queues no mutation records. What the parser does at the
49
+ # time — a script's access mid-parse, the moment a MutationObserver sees
50
+ # the host — cannot be reproduced by a pass that runs once parsing ends.
51
+ #
52
+ # Spec: https://html.spec.whatwg.org/multipage/parsing.html#parsing-main-inhead
53
+ class DeclarativeShadowRoots
54
+ MODES = %w[open closed].freeze
55
+
56
+ # Run the pass over `root_bn`'s subtree (an element, a fragment, a
57
+ # document). Returns the shadow roots it attached, in tree order.
58
+ def self.attach(document, root_bn, top_host: nil)
59
+ new(document).run(root_bn, top_host)
60
+ end
61
+
62
+ def initialize(document)
63
+ @document = document
64
+ @templates = document.__internal_template_registry__
65
+ @attached = []
66
+ end
67
+
68
+ def run(root_bn, top_host)
69
+ walk(root_bn, top_host) if candidate?(root_bn)
70
+ @attached
71
+ end
72
+
73
+ private
74
+
75
+ # Whether a `<template shadowrootmode>` is anywhere in the subtree,
76
+ # template contents included — found by native queries, so a page with
77
+ # none (most) is not walked.
78
+ def candidate?(root_bn)
79
+ return false unless root_bn.respond_to?(:css)
80
+ return true if root_bn.element? && declarative_mode(root_bn)
81
+ return true unless root_bn.css("template[shadowrootmode]").empty?
82
+
83
+ templates = root_bn.css("template").to_a
84
+ templates.unshift(root_bn) if root_bn.element? && @templates.template_node?(root_bn)
85
+ templates.any? do |template|
86
+ contents = @templates.existing_contents(template)
87
+ contents && candidate?(contents)
88
+ end
89
+ end
90
+
91
+ def walk(parent, host_for_children = nil)
92
+ host_for_children ||= parent if parent.element?
93
+ parent.children.to_a.each do |child|
94
+ next unless child.element?
95
+
96
+ if host_for_children && (mode = declarative_mode(child))
97
+ shadow = attach(host_for_children, child, mode)
98
+ if shadow
99
+ walk(shadow.__dommy_backend_node__)
100
+ next
101
+ end
102
+ end
103
+ walk(child)
104
+ next unless @templates.template_node?(child)
105
+
106
+ contents = @templates.existing_contents(child)
107
+ walk(contents) if contents
108
+ end
109
+ end
110
+
111
+ # The template's shadowrootmode state, when it is open or closed.
112
+ def declarative_mode(node)
113
+ return nil unless @templates.template_node?(node)
114
+
115
+ value = Backend.no_namespace_attribute_value(node, "shadowrootmode")
116
+ return nil unless value
117
+
118
+ mode = value.downcase(:ascii)
119
+ mode if MODES.include?(mode)
120
+ end
121
+
122
+ def attach(host_bn, template, mode)
123
+ host = @document.wrap_node(host_bn)
124
+ return nil unless host.respond_to?(:__internal_attach_shadow_root__)
125
+ return nil if host.__internal_shadow_root__
126
+
127
+ keep_null = attribute?(template, "shadowrootcustomelementregistry")
128
+ slot = Backend.no_namespace_attribute_value(template, "shadowrootslotassignment")
129
+ begin
130
+ shadow = host.__internal_attach_shadow_root__(
131
+ mode: mode,
132
+ delegates_focus: attribute?(template, "shadowrootdelegatesfocus"),
133
+ serializable: attribute?(template, "shadowrootserializable"),
134
+ slot_assignment: slot&.downcase(:ascii) == "manual" ? "manual" : "named",
135
+ clonable: attribute?(template, "shadowrootclonable"),
136
+ registry: keep_null ? nil : :document
137
+ )
138
+ rescue DOMException
139
+ return nil
140
+ end
141
+ return nil unless shadow
142
+
143
+ shadow.__internal_available_to_internals__ = true
144
+ shadow.__internal_declarative__ = true
145
+ shadow.__internal_keep_registry_null__ = keep_null
146
+ shadow.__internal_parsed_before__ = template.next_sibling
147
+ # Where the merge below leaves the text that followed the template.
148
+ shadow.__internal_parsed_before__ = template.previous_sibling if text?(template.previous_sibling) && text?(template.next_sibling)
149
+ move_contents(template, shadow.__dommy_backend_node__)
150
+ shadow.__internal_first_slots__ = SlotAssignment.first_slots_by_name(shadow)
151
+ drop_template(template)
152
+ @attached << shadow
153
+ shadow
154
+ end
155
+
156
+ # The template was never in the tree, so the parser appended the text
157
+ # on either side of it to one Text node.
158
+ def drop_template(template)
159
+ before = template.previous_sibling
160
+ after = template.next_sibling
161
+ template.unlink
162
+ return unless text?(before) && text?(after)
163
+
164
+ before.content = before.content + after.content
165
+ after.unlink
166
+ end
167
+
168
+ def text?(node) = node.is_a?(Backend.text_class) && !node.is_a?(Backend.cdata_class)
169
+
170
+ def attribute?(node, name) = !Backend.no_namespace_attribute_value(node, name).nil?
171
+
172
+ def move_contents(template, fragment)
173
+ contents = @templates.existing_contents(template)
174
+ return unless contents
175
+
176
+ contents.children.to_a.each do |child|
177
+ child.unlink
178
+ fragment.add_child(child)
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end