dommy 0.13.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.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dommy/backend/makiri_adapter.rb +63 -29
  3. data/lib/dommy/backend.rb +32 -9
  4. data/lib/dommy/bridge/constructor.rb +72 -0
  5. data/lib/dommy/bridge/errors.rb +42 -0
  6. data/lib/dommy/bridge/methods.rb +21 -14
  7. data/lib/dommy/bridge/sentinels.rb +39 -0
  8. data/lib/dommy/bridge/values.rb +78 -0
  9. data/lib/dommy/{js → bridge}/wire_tags.rb +14 -11
  10. data/lib/dommy/bridge.rb +22 -250
  11. data/lib/dommy/browser.rb +112 -11
  12. data/lib/dommy/character_data.rb +539 -0
  13. data/lib/dommy/class_list.rb +311 -0
  14. data/lib/dommy/crypto.rb +12 -1
  15. data/lib/dommy/css.rb +12 -0
  16. data/lib/dommy/data_transfer.rb +106 -0
  17. data/lib/dommy/document.rb +307 -369
  18. data/lib/dommy/dom_parser.rb +3 -0
  19. data/lib/dommy/element.rb +199 -1817
  20. data/lib/dommy/encodings/tables.rb +4 -4
  21. data/lib/dommy/encodings.rb +58 -27
  22. data/lib/dommy/event.rb +227 -1
  23. data/lib/dommy/event_source.rb +41 -0
  24. data/lib/dommy/form_data.rb +6 -64
  25. data/lib/dommy/form_entry_list.rb +227 -0
  26. data/lib/dommy/fragment.rb +320 -0
  27. data/lib/dommy/history.rb +3 -0
  28. data/lib/dommy/html_collection.rb +18 -8
  29. data/lib/dommy/html_elements/base.rb +338 -0
  30. data/lib/dommy/html_elements/embedded.rb +237 -0
  31. data/lib/dommy/html_elements/forms/form.rb +1088 -0
  32. data/lib/dommy/html_elements/forms/input.rb +642 -0
  33. data/lib/dommy/html_elements/forms/select.rb +536 -0
  34. data/lib/dommy/html_elements/forms.rb +8 -0
  35. data/lib/dommy/html_elements/interactive.rb +382 -0
  36. data/lib/dommy/html_elements/media.rb +329 -0
  37. data/lib/dommy/html_elements/metadata.rb +353 -0
  38. data/lib/dommy/html_elements/miscellaneous.rb +77 -0
  39. data/lib/dommy/html_elements/tables.rb +468 -0
  40. data/lib/dommy/html_elements.rb +11 -5322
  41. data/lib/dommy/hyperlink_utils.rb +171 -0
  42. data/lib/dommy/interaction/debug.rb +7 -6
  43. data/lib/dommy/interaction/dom_summary.rb +65 -40
  44. data/lib/dommy/interaction/driver.rb +29 -115
  45. data/lib/dommy/interaction/event_synthesis.rb +97 -4
  46. data/lib/dommy/interaction/field_interactor.rb +1 -13
  47. data/lib/dommy/interaction/form_submission.rb +89 -167
  48. data/lib/dommy/interaction/key_sender.rb +121 -0
  49. data/lib/dommy/interaction/mime_types.rb +25 -0
  50. data/lib/dommy/interaction.rb +2 -0
  51. data/lib/dommy/internal/accessibility_tree.rb +32 -9
  52. data/lib/dommy/internal/accessibility_visibility.rb +44 -0
  53. data/lib/dommy/internal/accessible_name.rb +85 -65
  54. data/lib/dommy/internal/aria_role.rb +21 -6
  55. data/lib/dommy/internal/aria_snapshot.rb +5 -0
  56. data/lib/dommy/internal/aria_state.rb +24 -5
  57. data/lib/dommy/internal/autofill.rb +141 -0
  58. data/lib/dommy/internal/backend_prefilter.rb +170 -0
  59. data/lib/dommy/internal/bounded_cache.rb +50 -0
  60. data/lib/dommy/internal/child_node.rb +9 -34
  61. data/lib/dommy/internal/css/calc.rb +143 -145
  62. data/lib/dommy/internal/css/cascade.rb +38 -393
  63. data/lib/dommy/internal/css/cascaded_declarations.rb +245 -0
  64. data/lib/dommy/internal/css/color.rb +61 -34
  65. data/lib/dommy/internal/css/computed_style_builder.rb +205 -0
  66. data/lib/dommy/internal/css/computed_style_declaration.rb +20 -4
  67. data/lib/dommy/internal/css/counters.rb +60 -37
  68. data/lib/dommy/internal/css/custom_properties.rb +87 -39
  69. data/lib/dommy/internal/css/media_query.rb +228 -203
  70. data/lib/dommy/internal/css/parser.rb +29 -12
  71. data/lib/dommy/internal/css/property_registry.rb +32 -56
  72. data/lib/dommy/internal/css/renderability.rb +61 -0
  73. data/lib/dommy/internal/css/rule_index.rb +17 -140
  74. data/lib/dommy/internal/css/selector_dependencies.rb +162 -0
  75. data/lib/dommy/internal/css/style_cache.rb +61 -0
  76. data/lib/dommy/internal/css/ua_stylesheet.rb +40 -1
  77. data/lib/dommy/internal/css/value_tokens.rb +60 -0
  78. data/lib/dommy/internal/directionality.rb +188 -0
  79. data/lib/dommy/internal/document_generations.rb +169 -0
  80. data/lib/dommy/internal/document_interaction_state.rb +47 -0
  81. data/lib/dommy/internal/document_live_ranges.rb +97 -0
  82. data/lib/dommy/internal/dom_matching.rb +28 -12
  83. data/lib/dommy/internal/element_aria.rb +163 -0
  84. data/lib/dommy/internal/element_geometry.rb +69 -0
  85. data/lib/dommy/internal/element_matching.rb +5 -12
  86. data/lib/dommy/internal/element_shadow.rb +117 -0
  87. data/lib/dommy/internal/element_state.rb +232 -0
  88. data/lib/dommy/internal/element_tasks.rb +19 -0
  89. data/lib/dommy/internal/element_top_layer.rb +63 -0
  90. data/lib/dommy/internal/enumerated_keyword_sets.rb +53 -0
  91. data/lib/dommy/internal/exception_report.rb +1 -1
  92. data/lib/dommy/internal/input_type.rb +280 -0
  93. data/lib/dommy/internal/insertion_point.rb +62 -0
  94. data/lib/dommy/internal/mime_type.rb +74 -0
  95. data/lib/dommy/internal/mutation_coordinator.rb +57 -198
  96. data/lib/dommy/internal/namespaces.rb +41 -43
  97. data/lib/dommy/internal/node_adopter.rb +84 -77
  98. data/lib/dommy/internal/node_factory.rb +157 -0
  99. data/lib/dommy/internal/node_identity.rb +31 -0
  100. data/lib/dommy/internal/node_wrapper_cache.rb +61 -207
  101. data/lib/dommy/internal/observer_options.rb +93 -0
  102. data/lib/dommy/internal/parent_node.rb +27 -27
  103. data/lib/dommy/internal/post_insertion_steps.rb +213 -0
  104. data/lib/dommy/internal/reflected_attributes.rb +425 -13
  105. data/lib/dommy/internal/rendered_text/collector.rb +221 -0
  106. data/lib/dommy/internal/rendered_text/computed_box.rb +39 -0
  107. data/lib/dommy/internal/rendered_text/fragment.rb +55 -0
  108. data/lib/dommy/internal/rendered_text/items.rb +57 -0
  109. data/lib/dommy/internal/rendered_text/whitespace_collapser.rb +132 -0
  110. data/lib/dommy/internal/selector_arguments.rb +94 -0
  111. data/lib/dommy/internal/selector_index.rb +3 -1
  112. data/lib/dommy/internal/selector_matcher.rb +23 -383
  113. data/lib/dommy/internal/selector_parser.rb +2 -37
  114. data/lib/dommy/internal/template_content_registry.rb +112 -66
  115. data/lib/dommy/internal/text_flattening.rb +20 -0
  116. data/lib/dommy/internal/text_selection.rb +104 -0
  117. data/lib/dommy/internal/toggle_events.rb +56 -0
  118. data/lib/dommy/internal/toggle_task_tracker.rb +57 -0
  119. data/lib/dommy/internal/url_parser.rb +722 -569
  120. data/lib/dommy/internal/url_record_accessors.rb +90 -0
  121. data/lib/dommy/internal/window_constructors.rb +190 -0
  122. data/lib/dommy/internal/xml_serialization.rb +168 -80
  123. data/lib/dommy/js/crossing_profile.rb +75 -0
  124. data/lib/dommy/js/dom_interfaces.rb +22 -2
  125. data/lib/dommy/js/host_bridge.rb +43 -187
  126. data/lib/dommy/js/host_callbacks.rb +96 -0
  127. data/lib/dommy/js/host_runtime.js +840 -763
  128. data/lib/dommy/js/import_map.rb +10 -1
  129. data/lib/dommy/js/marshaller.rb +94 -65
  130. data/lib/dommy/js/observable_runtime.js +125 -146
  131. data/lib/dommy/js/runtime.rb +10 -43
  132. data/lib/dommy/js/runtime_registry.rb +46 -0
  133. data/lib/dommy/js/script_boot.rb +38 -73
  134. data/lib/dommy/js/webidl_tables.js +619 -0
  135. data/lib/dommy/js/window_wiring.rb +69 -0
  136. data/lib/dommy/location.rb +152 -103
  137. data/lib/dommy/mutation_observer.rb +3 -57
  138. data/lib/dommy/navigation.rb +65 -12
  139. data/lib/dommy/node.rb +46 -20
  140. data/lib/dommy/promise.rb +58 -4
  141. data/lib/dommy/resources.rb +19 -1
  142. data/lib/dommy/shadow_root.rb +11 -5
  143. data/lib/dommy/style_declaration.rb +284 -0
  144. data/lib/dommy/svg_elements.rb +1 -45
  145. data/lib/dommy/url.rb +20 -69
  146. data/lib/dommy/url_pattern/regexp_translator.rb +1 -6
  147. data/lib/dommy/url_pattern/tokenizer.rb +4 -2
  148. data/lib/dommy/validity_state.rb +383 -0
  149. data/lib/dommy/version.rb +1 -1
  150. data/lib/dommy/window.rb +30 -166
  151. data/lib/dommy/xml_http_request.rb +35 -4
  152. data/lib/dommy.rb +23 -3
  153. metadata +74 -7
  154. data/lib/dommy/bridge/constructor_registry.rb +0 -28
  155. data/lib/dommy/internal/css_pseudo_handlers.rb +0 -378
@@ -15,8 +15,9 @@ module Dommy
15
15
  # - every live wrapper in the subtree, so a reference page script is
16
16
  # holding (an aria element reference, a queued MutationRecord's target)
17
17
  # keeps pointing at a node in the right document;
18
- # - a `<template>`'s contents, which are not in its child list and so are
19
- # reachable only through the source document's registry.
18
+ # - a `<template>`'s contents, which are not in its child list, and which
19
+ # the import copies in a shape of its own between an HTML and an XML
20
+ # document.
20
21
  #
21
22
  # Spec: https://dom.spec.whatwg.org/#concept-node-adopt
22
23
  class NodeAdopter
@@ -55,50 +56,10 @@ module Dommy
55
56
  return node if node.document == backend_doc
56
57
 
57
58
  adopted = Backend.adopt(node, backend_doc)
58
- if source_document && !source_document.equal?(@document)
59
- reseat_wrapper(node, adopted, source_document)
60
- reseat_descendant_wrappers(node, adopted, source_document)
61
- adopt_template_contents(node, adopted, source_document)
62
- end
59
+ carry_over(node, adopted, source_document) if source_document && !source_document.equal?(@document)
63
60
  adopted
64
61
  end
65
62
 
66
- # Move each live wrapper for a descendant of `src_root` onto the matching
67
- # node in `dst_root` (the imported copy), pruning it from the source
68
- # document. `include_root` also re-binds the root pair — #adopt re-binds
69
- # its own root wrapper by hand, but a template's content fragment has no
70
- # such caller.
71
- def reseat_descendant_wrappers(src_root, dst_root, src_doc, include_root: false)
72
- return unless src_doc.respond_to?(:__internal_peek_wrapper__)
73
-
74
- each_node_pair(src_root, dst_root) do |orig, copy|
75
- next if orig.equal?(src_root) && !include_root
76
-
77
- reseat_wrapper(orig, copy, src_doc)
78
- end
79
- end
80
-
81
- # HTML: adopting a `<template>` adopts its template contents
82
- # DocumentFragment along with it — the SAME fragment object, so
83
- # `template.content` keeps both its identity and its children across the
84
- # move. The contents are not in the template's child list, so neither the
85
- # backend's adopt nor the descendant walk above ever reaches them; without
86
- # this the adopted template comes out empty. Recurses, since a template's
87
- # contents can hold further templates.
88
- # Spec: https://html.spec.whatwg.org/#the-template-element (adopting steps)
89
- def adopt_template_contents(src_root, dst_root, src_doc)
90
- return if src_doc.nil? || src_doc.equal?(@document)
91
- return unless src_doc.respond_to?(:__internal_template_registry__)
92
-
93
- src_registry = src_doc.__internal_template_registry__
94
- each_node_pair(src_root, dst_root) do |orig, copy|
95
- src_frag = src_registry.raw_fragment_for(orig)
96
- next unless src_frag
97
-
98
- adopt_one_template_content(src_frag, copy, src_doc)
99
- end
100
- end
101
-
102
63
  private
103
64
 
104
65
  def backend_doc = @document.backend_doc
@@ -108,15 +69,13 @@ module Dommy
108
69
  # publicId / systemId (as createDocument does), then re-bind the caller's
109
70
  # wrapper onto the new node so JS identity survives the move.
110
71
  def adopt_doctype(node, src)
111
- adopted = begin
112
- Backend.create_document_type(node.name, node.public_id, node.system_id, backend_doc)
113
- rescue StandardError
114
- nil
115
- end
116
- return node unless adopted
117
-
72
+ adopted = Backend.create_document_type(node.name, node.public_id, node.system_id, backend_doc)
118
73
  reseat_known_wrapper(node, src, adopted, node.document)
119
74
  node
75
+ rescue StandardError => e
76
+ # Returning the node unchanged would read as a successful adoption while
77
+ # its ownerDocument never moved, so the caller hears about it instead.
78
+ raise DOMException::NotSupportedError, "Cannot adopt this doctype: #{e.message}"
120
79
  end
121
80
 
122
81
  # Hand the detached source to the backend, which returns the node now
@@ -127,42 +86,90 @@ module Dommy
127
86
  adopted = Backend.adopt(src, backend_doc)
128
87
 
129
88
  reseat_known_wrapper(node, src, adopted, src_doc)
130
- # A deep adopt imports a fresh copy of the whole subtree, so any live
131
- # descendant wrapper must be re-bound onto its corresponding copy —
132
- # otherwise it stays bound to the old document. Import preserves document
133
- # order, so walk both subtrees in lockstep.
134
- reseat_descendant_wrappers(src, adopted, src_doc)
135
- adopt_template_contents(src, adopted, src_doc)
89
+ carry_over(src, adopted, src_doc)
136
90
  node
137
91
  end
138
92
 
139
- def adopt_one_template_content(src_frag, template_copy, src_doc)
140
- frag = Parser.fragment("", owner_doc: backend_doc)
141
- # Snapshot before moving: the backend either relocates each node in place
142
- # (Nokogiri) or hands back an imported copy (Makiri, which cannot move a
143
- # node between arenas), and the pairs drive the wrapper re-bind either way.
144
- src_frag.children.to_a.each do |child|
145
- moved = Backend.adopt(child, backend_doc)
146
- adopt_template_contents(child, moved, src_doc)
147
- reseat_wrapper(child, moved, src_doc)
148
- reseat_descendant_wrappers(child, moved, src_doc)
149
- frag.add_child(moved)
93
+ # A deep adopt imports a fresh copy of the whole subtree, so each live
94
+ # wrapper in it must be re-bound onto its copy — otherwise it stays bound
95
+ # to the old document. Import preserves document order, so the two
96
+ # subtrees are walked together.
97
+ def carry_over(orig, copy, src_doc)
98
+ reseat_wrapper(orig, copy, src_doc)
99
+ if registry.template_node?(orig)
100
+ carry_over_template(orig, copy, src_doc)
101
+ elsif orig.respond_to?(:children)
102
+ carry_over_each(orig.children.to_a, copy.children.to_a, src_doc)
103
+ end
104
+ end
105
+
106
+ # A length mismatch means the copies are not the import of the originals,
107
+ # so nothing is paired at all rather than paired wrongly.
108
+ def carry_over_each(origs, copies, src_doc)
109
+ return unless origs.length == copies.length
110
+
111
+ origs.zip(copies).each { |orig, copy| carry_over(orig, copy, src_doc) }
112
+ end
113
+
114
+ # HTML: adopting a `<template>` adopts its template contents
115
+ # DocumentFragment along with it — the SAME fragment object, so
116
+ # `template.content` keeps both its identity and its children across the
117
+ # move. An HTML document's contents are the backend's own, which its
118
+ # import copies as contents into an HTML document and as the leading
119
+ # children into an XML one (which has no contents); an XML document's
120
+ # live apart from the tree and move here one by one. Whatever the import
121
+ # made of them, they end up as the destination keeps contents, and the
122
+ # template's own children as its children.
123
+ # Spec: https://html.spec.whatwg.org/#the-template-element (adopting steps)
124
+ def carry_over_template(orig, copy, src_doc)
125
+ src_registry = src_doc.__internal_template_registry__
126
+ src_frag = src_registry.existing_contents(orig)
127
+ contents = src_frag ? src_frag.children.to_a : []
128
+ children = orig.children.to_a
129
+ dst_frag = registry.contents(copy)
130
+ from_html = !Backend.template_contents(orig).nil?
131
+ to_html = !Backend.template_contents(copy).nil?
132
+
133
+ if from_html && to_html
134
+ carry_over_each(contents, dst_frag.children.to_a, src_doc)
135
+ carry_over_each(children, copy.children.to_a, src_doc)
136
+ elsif from_html
137
+ copied = copy.children.to_a
138
+ copied.first(contents.length).each { |node| move_node(node, dst_frag) }
139
+ carry_over_each(contents, copied.first(contents.length), src_doc)
140
+ carry_over_each(children, copied.drop(contents.length), src_doc)
141
+ else
142
+ if to_html
143
+ # The import made the XML template's own children the contents.
144
+ copied = dst_frag.children.to_a
145
+ copied.each { |node| move_node(node, copy) }
146
+ end
147
+ carry_over_each(children, copy.children.to_a, src_doc)
148
+ adopt_detached_contents(contents, dst_frag, src_doc)
149
+ src_registry.release(orig)
150
150
  end
151
- @document.__internal_template_registry__.store(template_copy, frag)
152
- reseat_wrapper(src_frag, frag, src_doc)
151
+ reseat_wrapper(src_frag, dst_frag, src_doc) if src_frag
153
152
  end
154
153
 
155
- # Each (original, copy) pair of the two subtrees, in document order. A
156
- # length mismatch means the copy is not the import of the original, so
157
- # nothing is paired at all rather than paired wrongly.
158
- def each_node_pair(src_root, dst_root)
159
- src_nodes = NodeTraversal.subtree_nodes(src_root)
160
- dst_nodes = NodeTraversal.subtree_nodes(dst_root)
161
- return if src_nodes.length != dst_nodes.length
154
+ # An XML document's contents, which no import reached, moved one by one.
155
+ def adopt_detached_contents(nodes, dst_frag, src_doc)
156
+ nodes.each do |node|
157
+ moved = Backend.adopt(node, backend_doc)
158
+ carry_over(node, moved, src_doc)
159
+ dst_frag.add_child(moved)
160
+ end
161
+ end
162
162
 
163
- src_nodes.zip(dst_nodes).each { |pair| yield(*pair) }
163
+ # Put a copy the import placed where the destination does not keep it
164
+ # into place. Not a DOM mutation — the node is where the adopt means it to
165
+ # be — so a raw unlink.
166
+ def move_node(node, parent)
167
+ node.unlink
168
+ parent.add_child(node)
164
169
  end
165
170
 
171
+ def registry = @document.__internal_template_registry__
172
+
166
173
  # Look up the live wrapper for `orig` in the source document, if it has
167
174
  # one, and move it onto `copy`.
168
175
  def reseat_wrapper(orig, copy, src_doc)
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Internal
5
+ # Document's createElement / createTextNode / createAttribute family: mint a
6
+ # backend node, then hand back the wrapper for it.
7
+ #
8
+ # A factory is not a cache. These lived in NodeWrapperCache, which made that
9
+ # class's name describe about a third of it; it keeps the identity table the
10
+ # wrappers come from, and this asks it for them.
11
+ class NodeFactory
12
+ def initialize(document, wrappers)
13
+ @document = document
14
+ @wrappers = wrappers
15
+ end
16
+
17
+ def create_element(name)
18
+ str = domstring(name)
19
+ raise DOMException::InvalidCharacterError, "name must not be empty" if str.empty?
20
+ raise DOMException::InvalidCharacterError, "invalid element name: #{str.inspect}" unless Namespaces.valid_element_local_name?(str)
21
+
22
+ # WHATWG createElement: lowercase (ASCII) the name only in an HTML
23
+ # document; the namespace is the HTML namespace for HTML/XHTML documents
24
+ # and null for a non-XHTML XML document. Record the metadata so the
25
+ # element's localName/tagName/namespaceURI getters report it faithfully
26
+ # (in particular case preservation for XML/XHTML).
27
+ if @document.html_document?
28
+ local = str.downcase(:ascii)
29
+ namespace = Element::HTML_NAMESPACE
30
+ else
31
+ local = str
32
+ namespace = @document.content_type == "application/xhtml+xml" ? Element::HTML_NAMESPACE : nil
33
+ end
34
+
35
+ # createElement validates against the XML *Name* production, which is
36
+ # looser than the QName an XML backend insists on: ":", "foo:", "f::oo"
37
+ # and a local part with a combining char are all valid element names the
38
+ # backend would reject. The loose creator builds those verbatim; anything
39
+ # it (or the strict path) still refuses is an InvalidCharacterError.
40
+ node =
41
+ begin
42
+ Backend.create_element_loose(local, nil, local, namespace, @document.backend_doc) ||
43
+ Backend.create_element(local, @document.backend_doc)
44
+ rescue ArgumentError
45
+ raise DOMException::InvalidCharacterError, "invalid element name: #{str.inspect}"
46
+ end
47
+
48
+ wrapper = @wrappers.wrap(node)
49
+ wrapper.__internal_set_namespace__(namespace, nil, local, local)
50
+ @document.__internal_note_namespaced_element__(namespace, nil)
51
+ wrapper
52
+ end
53
+
54
+ def create_text_node(text)
55
+ @wrappers.wrap(Backend.create_text(text.to_s, @document.backend_doc))
56
+ end
57
+
58
+ def create_cdata_section(text)
59
+ @wrappers.wrap(Backend.create_cdata(text.to_s, @document.backend_doc))
60
+ end
61
+
62
+ def create_comment(text)
63
+ @wrappers.wrap(Backend.create_comment(text.to_s, @document.backend_doc))
64
+ end
65
+
66
+ # WHATWG Document.createProcessingInstruction: the target must be a valid
67
+ # XML Name and the data must not contain the PI close delimiter "?>", else
68
+ # InvalidCharacterError. The result is a real backend-backed PI node.
69
+ def create_processing_instruction(target, data)
70
+ t = domstring(target)
71
+ d = domstring(data)
72
+ raise DOMException::InvalidCharacterError, "invalid processing instruction target: #{t.inspect}" unless t.match?(Namespaces::NAME)
73
+ raise DOMException::InvalidCharacterError, "processing instruction data must not contain '?>'" if d.include?("?>")
74
+
75
+ @wrappers.wrap(Backend.create_processing_instruction(t, d, @document.backend_doc))
76
+ end
77
+
78
+ def create_document_fragment
79
+ @wrappers.wrap(Parser.fragment("", owner_doc: @document.backend_doc))
80
+ end
81
+
82
+ def create_attribute(name)
83
+ str = domstring(name)
84
+ raise DOMException::InvalidCharacterError, "name must not be empty" if str.empty?
85
+ raise DOMException::InvalidCharacterError, "invalid attribute name: #{str.inspect}" unless Namespaces.valid_attribute_local_name?(str)
86
+
87
+ # WHATWG createAttribute: an HTML document lower-cases the name (an XML
88
+ # document preserves it). Attr.new no longer folds case, so do it here.
89
+ str = str.downcase if @document.html_document?
90
+ Attr.new(str, document: @document)
91
+ end
92
+
93
+ def create_attribute_ns(namespace_uri, qualified_name)
94
+ namespace_uri = nil if namespace_uri.equal?(Bridge::UNDEFINED)
95
+ qualified_name = domstring(qualified_name)
96
+ ns, prefix, local = Namespaces.validate_and_extract(namespace_uri, qualified_name)
97
+ Attr.new(qualified_name, namespace_uri: ns, prefix: prefix, local_name: local, document: @document)
98
+ end
99
+
100
+ def create_element_ns(namespace_uri, qualified_name)
101
+ # WHATWG "validate and extract": QName-validate the qualifiedName
102
+ # (InvalidCharacterError) and apply the prefix/namespace rules
103
+ # (NamespaceError), then build the element with its prefix bound.
104
+ # namespace is nullable (undefined → null); qualifiedName is a plain
105
+ # DOMString (undefined → "undefined", null → "null").
106
+ namespace_uri = nil if namespace_uri.equal?(Bridge::UNDEFINED)
107
+ qualified_name = domstring(qualified_name)
108
+ ns, prefix, local = Namespaces.validate_and_extract(namespace_uri, qualified_name, context: :element)
109
+
110
+ # An XML backend rejects some DOM-valid qualified names (an invalid char
111
+ # in the local part, which DOM permits): the loose creator builds them
112
+ # verbatim. An HTML backend builds the element in its namespace. A
113
+ # genuinely invalid name it (or the strict path) rejects with an
114
+ # ArgumentError becomes an InvalidCharacterError, per DOM.
115
+ el =
116
+ begin
117
+ Backend.create_element_loose(qualified_name, prefix, local, ns, @document.backend_doc) ||
118
+ Backend.create_element_ns(ns, qualified_name, @document.backend_doc) ||
119
+ Backend.create_element(qualified_name, @document.backend_doc)
120
+ rescue ArgumentError
121
+ raise DOMException::InvalidCharacterError, "'#{qualified_name}' is not a valid element name"
122
+ end
123
+ Backend.add_namespace_definition(el, prefix, ns) if ns
124
+
125
+ wrapper = @wrappers.build_element_wrapper(el, namespace: ns, local_name: local)
126
+ wrapper.__internal_set_namespace__(ns, prefix, local, qualified_name)
127
+ @document.__internal_note_namespaced_element__(ns, prefix)
128
+ wrapper
129
+ end
130
+
131
+ # Wrap a freshly-cloned backend element whose original was built via
132
+ # createElementNS: route the wrapper class by the known local name (the
133
+ # backend node name may be the full qualified name, e.g. "foo:div", which
134
+ # would otherwise resolve to HTMLUnknownElement) and reapply its namespace
135
+ # metadata (namespaceURI / prefix / localName / tagName).
136
+ def wrap_cloned_element_ns(node, namespace, prefix, local, qualified_name)
137
+ @wrappers.reset_wrapper(node)
138
+ wrapper = @wrappers.build_element_wrapper(node, namespace: namespace, local_name: local)
139
+ wrapper.__internal_set_namespace__(namespace, prefix, local, qualified_name)
140
+ wrapper
141
+ end
142
+
143
+ # Query methods
144
+
145
+ private
146
+
147
+ # WebIDL DOMString coercion for a name/qualifiedName argument: JS
148
+ # `undefined` → "undefined", JS `null` (Ruby nil) → "null", else #to_s.
149
+ def domstring(value)
150
+ return "undefined" if value.equal?(Bridge::UNDEFINED)
151
+ return "null" if value.nil?
152
+
153
+ value.to_s
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Internal
5
+ # Whether two DOM references stand for the same node.
6
+ #
7
+ # Comparing the backend nodes with `==` is not it: a backend may mint a
8
+ # fresh wrapper per traversal, and one that frees nodes may hand the same
9
+ # pointer to a later node. Backend.identity_key is what each backend says
10
+ # its stable key is (see NodeWrapperCache#identity_key), so that is what
11
+ # this asks — in one place, because three callers used to answer it three
12
+ # ways.
13
+ module NodeIdentity
14
+ module_function
15
+
16
+ # `first` and `second` may be wrappers or backend nodes, in any mix.
17
+ def same_node?(first, second)
18
+ return false unless first && second
19
+
20
+ left = key_for(first)
21
+ right = key_for(second)
22
+ !left.nil? && left == right
23
+ end
24
+
25
+ def key_for(node)
26
+ backend = node.respond_to?(:__dommy_backend_node__) ? node.__dommy_backend_node__ : node
27
+ backend && Backend.identity_key(backend)
28
+ end
29
+ end
30
+ end
31
+ end