dommy 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -1
  3. data/lib/dommy/attr.rb +56 -32
  4. data/lib/dommy/backend.rb +251 -208
  5. data/lib/dommy/blob.rb +24 -0
  6. data/lib/dommy/bridge/methods.rb +12 -2
  7. data/lib/dommy/bridge/values.rb +29 -2
  8. data/lib/dommy/bridge/wire_tags.rb +8 -4
  9. data/lib/dommy/browser.rb +328 -53
  10. data/lib/dommy/callable_invoker.rb +27 -31
  11. data/lib/dommy/character_data.rb +21 -28
  12. data/lib/dommy/class_list.rb +55 -23
  13. data/lib/dommy/cookie_jar.rb +408 -0
  14. data/lib/dommy/cookie_store.rb +104 -49
  15. data/lib/dommy/css.rb +37 -8
  16. data/lib/dommy/custom_elements.rb +360 -116
  17. data/lib/dommy/data_transfer.rb +22 -2
  18. data/lib/dommy/document.rb +1258 -346
  19. data/lib/dommy/dom_exception.rb +6 -0
  20. data/lib/dommy/dom_parser.rb +46 -14
  21. data/lib/dommy/dom_string_list.rb +36 -0
  22. data/lib/dommy/element.rb +408 -569
  23. data/lib/dommy/element_internals.rb +215 -0
  24. data/lib/dommy/encodings.rb +10 -0
  25. data/lib/dommy/event.rb +382 -148
  26. data/lib/dommy/event_source.rb +8 -34
  27. data/lib/dommy/fetch.rb +8 -12
  28. data/lib/dommy/file_reader.rb +5 -29
  29. data/lib/dommy/form_data.rb +53 -16
  30. data/lib/dommy/form_entry_list.rb +107 -138
  31. data/lib/dommy/fragment.rb +14 -16
  32. data/lib/dommy/history.rb +247 -90
  33. data/lib/dommy/html_canvas_element.rb +15 -35
  34. data/lib/dommy/html_collection.rb +76 -50
  35. data/lib/dommy/html_elements/base.rb +377 -71
  36. data/lib/dommy/html_elements/embedded.rb +350 -106
  37. data/lib/dommy/html_elements/forms/form.rb +424 -383
  38. data/lib/dommy/html_elements/forms/input.rb +351 -189
  39. data/lib/dommy/html_elements/forms/select.rb +97 -81
  40. data/lib/dommy/html_elements/interactive.rb +285 -72
  41. data/lib/dommy/html_elements/media.rb +73 -32
  42. data/lib/dommy/html_elements/metadata.rb +226 -78
  43. data/lib/dommy/html_elements/miscellaneous.rb +58 -14
  44. data/lib/dommy/html_elements/tables.rb +20 -42
  45. data/lib/dommy/html_elements.rb +28 -3
  46. data/lib/dommy/hyperlink_utils.rb +19 -8
  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 +18 -6
  50. data/lib/dommy/interaction/form_submission.rb +12 -6
  51. data/lib/dommy/interaction/key_sender.rb +102 -17
  52. data/lib/dommy/interaction.rb +1 -0
  53. data/lib/dommy/internal/accessibility_tree.rb +19 -7
  54. data/lib/dommy/internal/accessibility_visibility.rb +2 -2
  55. data/lib/dommy/internal/accessible_description.rb +3 -3
  56. data/lib/dommy/internal/accessible_name.rb +33 -45
  57. data/lib/dommy/internal/an_plus_b.rb +125 -0
  58. data/lib/dommy/internal/aria_role.rb +27 -18
  59. data/lib/dommy/internal/aria_state.rb +7 -7
  60. data/lib/dommy/internal/autofill.rb +2 -2
  61. data/lib/dommy/internal/backend_prefilter.rb +21 -18
  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 +39 -0
  65. data/lib/dommy/internal/child_navigable.rb +155 -0
  66. data/lib/dommy/internal/child_node.rb +44 -41
  67. data/lib/dommy/internal/close_watcher_manager.rb +238 -0
  68. data/lib/dommy/internal/complex_match.rb +129 -0
  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/cascade.rb +7 -7
  72. data/lib/dommy/internal/css/cascaded_declarations.rb +14 -20
  73. data/lib/dommy/internal/css/computed_style_builder.rb +30 -3
  74. data/lib/dommy/internal/css/custom_properties.rb +63 -80
  75. data/lib/dommy/internal/css/parser.rb +53 -63
  76. data/lib/dommy/internal/css/renderability.rb +6 -0
  77. data/lib/dommy/internal/css/rule_index.rb +170 -108
  78. data/lib/dommy/internal/css/rule_index_inputs.rb +135 -0
  79. data/lib/dommy/internal/css/selector_dependencies.rb +8 -2
  80. data/lib/dommy/internal/css/style_cache.rb +9 -6
  81. data/lib/dommy/internal/css/ua_stylesheet.rb +37 -2
  82. data/lib/dommy/internal/css_source.rb +223 -0
  83. data/lib/dommy/internal/css_syntax.rb +153 -0
  84. data/lib/dommy/internal/declarative_shadow_roots.rb +182 -0
  85. data/lib/dommy/internal/directionality.rb +2 -2
  86. data/lib/dommy/internal/document_generations.rb +91 -18
  87. data/lib/dommy/internal/document_interaction_state.rb +293 -9
  88. data/lib/dommy/internal/dom_matching.rb +7 -7
  89. data/lib/dommy/internal/element_aria.rb +65 -23
  90. data/lib/dommy/internal/element_css_inline_style.rb +26 -0
  91. data/lib/dommy/internal/element_editing.rb +41 -0
  92. data/lib/dommy/internal/element_nonce.rb +28 -0
  93. data/lib/dommy/internal/element_popover.rb +310 -0
  94. data/lib/dommy/internal/element_shadow.rb +107 -33
  95. data/lib/dommy/internal/element_state.rb +131 -26
  96. data/lib/dommy/internal/element_top_layer.rb +21 -51
  97. data/lib/dommy/internal/enumerated_keyword_sets.rb +40 -0
  98. data/lib/dommy/internal/event_handler_tables.rb +241 -0
  99. data/lib/dommy/internal/event_handlers.rb +149 -0
  100. data/lib/dommy/internal/exception_report.rb +1 -1
  101. data/lib/dommy/internal/focusability.rb +267 -0
  102. data/lib/dommy/internal/form_associated_custom_elements.rb +127 -0
  103. data/lib/dommy/internal/html_or_svg_or_mathml_element.rb +86 -0
  104. data/lib/dommy/internal/html_serialization.rb +292 -0
  105. data/lib/dommy/internal/idl_reflection.rb +202 -0
  106. data/lib/dommy/internal/idl_reflection_table.rb +589 -0
  107. data/lib/dommy/internal/infra.rb +36 -0
  108. data/lib/dommy/internal/input_type.rb +90 -20
  109. data/lib/dommy/internal/insertion_point.rb +27 -3
  110. data/lib/dommy/internal/js_number.rb +60 -0
  111. data/lib/dommy/internal/light_dismiss.rb +178 -0
  112. data/lib/dommy/internal/literal_lookup.rb +103 -0
  113. data/lib/dommy/internal/mime_type.rb +8 -0
  114. data/lib/dommy/internal/mutation_coordinator.rb +94 -60
  115. data/lib/dommy/internal/node_adopter.rb +165 -97
  116. data/lib/dommy/internal/node_factory.rb +111 -49
  117. data/lib/dommy/internal/node_identity.rb +1 -1
  118. data/lib/dommy/internal/node_traversal.rb +30 -13
  119. data/lib/dommy/internal/node_wrapper_cache.rb +34 -148
  120. data/lib/dommy/internal/observable_callback.rb +2 -5
  121. data/lib/dommy/internal/observer_options.rb +24 -14
  122. data/lib/dommy/internal/origin.rb +86 -0
  123. data/lib/dommy/internal/parent_node.rb +12 -10
  124. data/lib/dommy/internal/popover_invoker_element.rb +67 -0
  125. data/lib/dommy/internal/popover_stack.rb +120 -0
  126. data/lib/dommy/internal/post_insertion_steps.rb +146 -77
  127. data/lib/dommy/internal/reflected_attributes.rb +173 -43
  128. data/lib/dommy/internal/rendered_text/collector.rb +12 -9
  129. data/lib/dommy/internal/rendered_text/fragment.rb +6 -2
  130. data/lib/dommy/internal/selector_arguments.rb +2 -16
  131. data/lib/dommy/internal/selector_index.rb +19 -10
  132. data/lib/dommy/internal/selector_match.rb +190 -0
  133. data/lib/dommy/internal/selector_matcher.rb +91 -256
  134. data/lib/dommy/internal/selector_parser.rb +176 -100
  135. data/lib/dommy/internal/sequential_focus_navigation.rb +210 -0
  136. data/lib/dommy/internal/shadow_root_registry.rb +14 -6
  137. data/lib/dommy/internal/sibling_positions.rb +59 -0
  138. data/lib/dommy/internal/svg_uri_reference.rb +33 -0
  139. data/lib/dommy/internal/template_content_registry.rb +11 -5
  140. data/lib/dommy/internal/text_selection.rb +185 -32
  141. data/lib/dommy/internal/toggle_events.rb +6 -4
  142. data/lib/dommy/internal/unsafe_html.rb +107 -0
  143. data/lib/dommy/internal/url_parser.rb +84 -8
  144. data/lib/dommy/internal/user_activation.rb +192 -0
  145. data/lib/dommy/internal/web_idl.rb +160 -16
  146. data/lib/dommy/internal/window_constructors.rb +34 -14
  147. data/lib/dommy/internal/xml_serialization.rb +9 -45
  148. data/lib/dommy/js/constructor_resolver.rb +18 -0
  149. data/lib/dommy/js/custom_element_bridge.rb +57 -82
  150. data/lib/dommy/js/dom_interfaces.rb +51 -15
  151. data/lib/dommy/js/host_bridge.rb +189 -40
  152. data/lib/dommy/js/host_callbacks.rb +10 -0
  153. data/lib/dommy/js/host_runtime.js +1921 -416
  154. data/lib/dommy/js/marshaller.rb +13 -4
  155. data/lib/dommy/js/module_loader.rb +46 -5
  156. data/lib/dommy/js/module_preload.rb +194 -0
  157. data/lib/dommy/js/runtime.rb +9 -0
  158. data/lib/dommy/js/script_boot.rb +200 -114
  159. data/lib/dommy/js/serialized_record.rb +32 -0
  160. data/lib/dommy/js/webidl_event_handlers.js +45 -0
  161. data/lib/dommy/js/webidl_members.js +196 -0
  162. data/lib/dommy/js/webidl_signatures.js +2055 -0
  163. data/lib/dommy/js/webidl_tables.js +72 -101
  164. data/lib/dommy/js/window_wiring.rb +9 -5
  165. data/lib/dommy/location.rb +129 -76
  166. data/lib/dommy/mathml_elements.rb +13 -0
  167. data/lib/dommy/media_query_list.rb +4 -13
  168. data/lib/dommy/message_channel.rb +137 -64
  169. data/lib/dommy/mutation_observer.rb +3 -7
  170. data/lib/dommy/navigation.rb +61 -11
  171. data/lib/dommy/navigator.rb +132 -10
  172. data/lib/dommy/node.rb +81 -44
  173. data/lib/dommy/notification.rb +10 -6
  174. data/lib/dommy/parser.rb +5 -16
  175. data/lib/dommy/promise.rb +20 -26
  176. data/lib/dommy/range.rb +20 -47
  177. data/lib/dommy/resources.rb +38 -3
  178. data/lib/dommy/scheduler.rb +176 -48
  179. data/lib/dommy/shadow_root.rb +125 -25
  180. data/lib/dommy/storage.rb +135 -25
  181. data/lib/dommy/streams.rb +2 -6
  182. data/lib/dommy/structured_data.rb +45 -0
  183. data/lib/dommy/style_declaration.rb +38 -6
  184. data/lib/dommy/svg_elements.rb +25 -11
  185. data/lib/dommy/text_codec.rb +2 -14
  186. data/lib/dommy/tree_walker.rb +12 -26
  187. data/lib/dommy/url.rb +12 -9
  188. data/lib/dommy/url_pattern.rb +1 -9
  189. data/lib/dommy/validity_state.rb +109 -107
  190. data/lib/dommy/version.rb +1 -1
  191. data/lib/dommy/web_socket.rb +10 -33
  192. data/lib/dommy/window.rb +817 -105
  193. data/lib/dommy/worker.rb +4 -28
  194. data/lib/dommy/xml_http_request.rb +32 -45
  195. data/lib/dommy.rb +43 -10
  196. metadata +52 -5
  197. data/lib/dommy/backend/makiri_adapter.rb +0 -408
  198. data/lib/dommy/internal/cookie_jar.rb +0 -29
data/lib/dommy/backend.rb CHANGED
@@ -1,320 +1,363 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "makiri"
4
+
3
5
  module Dommy
4
- # `Dommy::Backend` — pluggable HTML parser abstraction. Lets Dommy
5
- # work with Makiri (Lexbor-based, HTML5-only). Internally, all DOM library
6
- # code goes through this facade rather than referencing the parser
7
- # directly.
8
- #
9
- # Defaults to Makiri.
10
- #
11
- # Switching backends:
12
- #
13
- # require "dommy"
14
- # Dommy::Backend.use(:makiri)
15
- #
16
- # Or set directly:
6
+ # `Dommy::Backend` — what the DOM asks of Makiri, the Lexbor-based parser
7
+ # that holds every tree. All DOM library code comes through here rather
8
+ # than reading Makiri's API directly, so what Dommy relies on of it is in
9
+ # one place: the node classes, the creators, the namespaced attribute
10
+ # model, and the few normalizations (an empty namespace is none).
17
11
  #
18
- # Dommy::Backend.current = Dommy::Backend::Makiri
19
- #
20
- # All adapters must implement the same interface — see
21
- # `Backend::Makiri` for the canonical reference.
12
+ # Makiri splits its document model into `Makiri::HTML::Document`
13
+ # (case-folding, html/head/body) and `Makiri::XML::Document`
14
+ # (case-preserving, namespaces, CDATA); both share the `Makiri::Document` /
15
+ # `Makiri::Node` bases used here for `is_a?` checks. HTML parses go through
16
+ # HTML::Document; `new Document()` / createDocument go through XML::Document.
22
17
  module Backend
23
- class BackendNotAvailable < StandardError
24
- end
25
-
26
18
  class << self
27
- def current
28
- @current ||= detect_default
29
- end
19
+ # The node classes (the shared bases, so both HTML and XML node
20
+ # subclasses match), so the wrapper cache can route each node.
21
+ def element_class = ::Makiri::Element
30
22
 
31
- attr_writer :current
23
+ def document_class = ::Makiri::Document
32
24
 
33
- def use(name)
34
- @current = case name.to_sym
35
- when :makiri
36
- require_relative "backend/makiri_adapter"
37
- Makiri
38
- else
39
- raise ArgumentError, "Unknown backend: #{name.inspect}. Use :makiri."
40
- end
41
- end
25
+ def text_class = ::Makiri::Text
26
+
27
+ def comment_class = ::Makiri::Comment
28
+
29
+ # A Text subtype, matched before Text.
30
+ def cdata_class = ::Makiri::CDATASection
31
+
32
+ def processing_instruction_class = ::Makiri::ProcessingInstruction
33
+
34
+ def document_fragment_class = ::Makiri::DocumentFragment
35
+
36
+ def document_type_class = ::Makiri::DocumentType
37
+
38
+ def node_class = ::Makiri::Node
39
+
40
+ # What Makiri raises for markup that is not well-formed XML (an XML
41
+ # document's fragment parse included).
42
+ def xml_syntax_error_class = ::Makiri::XML::SyntaxError
42
43
 
43
- # Stable per-document identity key for a backend node, used to cache DOM
44
- # wrappers and key per-node side tables. Makiri mints a fresh wrapper per traversal
45
- # but never frees nodes (arena-owned), so it keys on the stable node pointer.
44
+ # Stable per-document identity key for a backend node, used to key
45
+ # per-node side tables. pointer_id is the underlying lxb_dom_node_t
46
+ # pointer: Makiri detaches but never frees nodes — the document arena
47
+ # owns them — so a live node's pointer is never recycled; a freed
48
+ # document's may be, so a table keyed by it is a document's own.
46
49
  def identity_key(node)
47
- current.identity_key(node)
50
+ node.pointer_id
48
51
  end
49
52
 
50
53
  # Deep (or shallow) copy of an element/node, detached and owned by the
51
- # same document — the backing for DOM cloneNode.
54
+ # same document — the backing for DOM cloneNode. Makiri clones natively
55
+ # (import_node + template fixup), preserving the node's namespace and
56
+ # attributes and carrying <template> contents.
52
57
  def clone_node(node, deep:)
53
- current.clone_node(node, deep: deep)
58
+ node.clone_node(deep)
54
59
  end
55
60
 
56
- # A fresh, empty HTML-backed document.
61
+ # A fresh, empty HTML-backed document — no children, no-quirks. The
62
+ # backing for a shallow clone of an HTML document.
57
63
  def empty_document
58
- current.empty_document
64
+ ::Makiri::HTML::Document.new
59
65
  end
60
66
 
61
- # A fresh, empty XML-backed document — for `new Document()` / createDocument,
62
- # which the DOM defines as XML documents (case-preserving, real CDATA
63
- # nodeType, namespaces).
67
+ # A fresh, empty XML-backed document — the backing for `new Document()` /
68
+ # createDocument, which the DOM defines as XML documents. An XML backing
69
+ # gives them case preservation, real CDATA nodes (nodeType 4) and
70
+ # namespace tracking. Makiri's cross-kind `import_node` translates
71
+ # between the HTML and XML node representations, so a node created here
72
+ # can still be adopted into the main HTML tree (and vice versa).
64
73
  def empty_xml_document
65
- current.empty_xml_document
74
+ ::Makiri::XML::Document.new
66
75
  end
67
76
 
68
- # An empty backing document matching `doc`'s kind (HTML stays HTML, XML stays
69
- # XML) — for a shallow document clone, whose result keeps the source flavor.
77
+ # Whether the HTML parser left `doc` in quirks mode (not limited-quirks,
78
+ # which matches no-quirks everywhere Dommy asks). An XML document is
79
+ # never in it.
80
+ def quirks_mode?(doc)
81
+ doc.quirks_mode?
82
+ end
83
+
84
+ # An empty backing document matching `doc`'s kind (HTML stays HTML, XML
85
+ # stays XML) — for a shallow document clone, whose result keeps the
86
+ # source flavor.
87
+ # Whether `doc` is an HTML-backed document (the HTML parser built it).
88
+ def html_backed?(doc) = !doc.is_a?(::Makiri::XML::Document)
89
+
70
90
  def empty_document_like(doc)
71
- current.empty_document_like(doc)
91
+ doc.is_a?(::Makiri::XML::Document) ? empty_xml_document : empty_document
72
92
  end
73
93
 
74
94
  # Bring `node` (already detached from its old tree) into `target_doc`,
75
- # returning the backend node now owned by `target_doc`. Makiri can't move a node
76
- # between arenas, so it imports a copy — callers must reseat any wrapper
77
- # onto the returned node.
95
+ # returning the backend node now owned by `target_doc`. Lexbor's arenas
96
+ # can't move a node between documents, so adoption imports a detached
97
+ # copy — callers must reseat any wrapper onto the returned node.
98
+ #
99
+ # Gap absorption: Lexbor's HTML serializer has no CDATA case (it errors
100
+ # on a CDATA node) and Lexbor is a pinned upstream submodule, so Makiri's
101
+ # cross-kind import_node fails closed when bringing an XML CDATASection
102
+ # into an HTML document. Rather than let that surface as an error,
103
+ # degrade the node to a text node carrying the same data — the data a
104
+ # spec-faithful HTML serializer emits anyway (CDATASection is a Text
105
+ # subtype). The caller reseats the Dommy CDATASection wrapper onto this
106
+ # node, so `nodeType` stays 4 at the DOM level; only the backing node
107
+ # (and thus serialization) is text. An XML target keeps a real CDATA
108
+ # node. This handles a directly adopted CDATA node (a CDATA descendant
109
+ # inside an adopted element subtree is rare, untested, and still fails
110
+ # closed in the backend).
78
111
  def adopt(node, target_doc)
79
- current.adopt(node, target_doc)
112
+ if node.is_a?(::Makiri::CDATASection) && !target_doc.is_a?(::Makiri::XML::Document)
113
+ return target_doc.create_text_node(node.text)
114
+ end
115
+
116
+ target_doc.import_node(node, true)
80
117
  end
81
118
 
82
119
  # A copy of the element `node` alone, owned by `target_doc`: its name and
83
120
  # attributes exactly as the backend holds them.
84
121
  def import_element(node, target_doc)
85
- current.import_element(node, target_doc)
86
- end
87
-
88
- # Whether the backend can move a node between documents in place or must adopt a copy first
89
- # (Lexbor's arenas can't move a node, so inserting a foreign node requires importing
90
- # it). Lets callers skip a needless — and on an empty target, root-less and
91
- # therefore crashing — adoption on backends that don't need it.
92
- def moves_nodes_across_documents?
93
- current.respond_to?(:moves_nodes_across_documents?) ? current.moves_nodes_across_documents? : true
94
- end
95
-
96
- # CSS query that honors Dommy's custom pseudo-classes
97
- # (`:disabled`/`:enabled`/`:checked`/`:scope`). Each backend applies its
98
- # own mechanism (Lexbor native pseudos plus a
99
- # `:scope` rewrite). `scope_node` binds `:scope` to that element.
100
- def select_all(node, selector, scope_node: nil)
101
- current.select_all(node, selector, scope_node: scope_node)
122
+ target_doc.import_node(node, false)
102
123
  end
103
124
 
104
- def select_first(node, selector, scope_node: nil)
105
- current.select_first(node, selector, scope_node: scope_node)
106
- end
107
-
108
- # Delegate calls so internal code can use `Backend.parse(...)`.
109
125
  def parse(html)
110
- current.parse(html)
126
+ ::Makiri::HTML::Document.parse(html.to_s)
111
127
  end
112
128
 
113
- # Parse XML input into an XML document.
114
- # fall back to the HTML parser.
129
+ # XML parse (DOMParser `text/xml` / `application/xml`): a real XML
130
+ # document, so element/attribute case is preserved, namespaces are
131
+ # tracked, and CDATA round-trips.
115
132
  def parse_xml(xml)
116
- current.parse_xml(xml)
133
+ ::Makiri::XML::Document.parse(xml.to_s)
117
134
  end
118
135
 
119
136
  def fragment(html, owner_doc:)
120
- current.fragment(html, owner_doc: owner_doc)
121
- end
122
-
123
- # Make `node` the sole document element of `doc` (used by
124
- # DOMImplementation.createDocument). Lexbor-backed documents are seeded with an <html> shell
125
- # that must be cleared first.
126
- def set_document_root(doc, node)
127
- current.set_document_root(doc, node)
128
- end
129
-
130
- def create_element(name, doc)
131
- current.create_element(name, doc)
137
+ ::Makiri::DocumentFragment.parse(html.to_s)
132
138
  end
133
139
 
134
- # Create a namespaced element permitting a DOM-valid qualified name that a
135
- # strict XML backend would reject (an internal invalid char like "f}oo"),
136
- # preserving case/prefix. Returns nil when the backend has no loose path
137
- # (fall back to #create_element); raises ArgumentError for a genuinely
138
- # invalid name (the caller maps it to InvalidCharacterError).
139
- def create_element_loose(qualified_name, prefix, local, namespace, doc)
140
- current.create_element_loose(qualified_name, prefix, local, namespace, doc)
140
+ # The DOM's createElement: an element whose local name is `local_name`
141
+ # as written, colons and case included (`foo:` and `f::oo` are valid
142
+ # local names, not prefixed ones), in `namespace` — the HTML namespace
143
+ # in an HTML or XHTML document, nil in any other. Makiri builds it from
144
+ # the parts in an HTML and an XML document alike, without parsing the
145
+ # name as a QName.
146
+ def create_element(local_name, namespace, doc)
147
+ doc.create_loose_dom_element(local_name, nil, local_name, namespace)
141
148
  end
142
149
 
143
- # The element's own namespace URI as the DOM reports it: nil for no
144
- # namespace.
145
- def namespace_uri(node)
146
- current.namespace_uri(node)
147
- end
148
-
149
- # Create an element in `namespace` in an HTML document, so the backend
150
- # node carries the namespace and the name's case itself. Returns nil for
151
- # an XML document (fall back to #create_element); raises ArgumentError
152
- # for an invalid name (the caller maps it to InvalidCharacterError).
150
+ # The DOM's createElementNS, in an HTML or an XML document alike: the
151
+ # element is in `namespace` (nil for none) with `qualified_name` as
152
+ # written — an SVG `feGaussianBlur` keeps its case, a prefixed name its
153
+ # prefix. Makiri checks the name as the DOM does, so a name the caller
154
+ # has validated is never refused; it raises ArgumentError for one it
155
+ # would not have passed.
153
156
  def create_element_ns(namespace, qualified_name, doc)
154
- current.create_element_ns(namespace, qualified_name, doc)
157
+ doc.create_element_ns(namespace, qualified_name.to_s)
155
158
  end
156
159
 
157
- # A detached DocumentType node owned by `doc` (for
158
- # DOMImplementation.createDocumentType). Returns nil when the backend has no
159
- # doctype factory (the caller falls back to a synthetic DocumentType); raises
160
- # ArgumentError for a name the factory rejects (the caller then also falls
161
- # back, since createDocumentType is permissive).
160
+ # A detached DocumentType node owned by `doc`, HTML or XML (for
161
+ # DOMImplementation.createDocumentType). Makiri takes any name the DOM
162
+ # does but the empty one, which Lexbor reads as no name, and raises
163
+ # ArgumentError for it; the caller then falls back to a synthetic
164
+ # (non-tree) DocumentType, since createDocumentType is permissive.
162
165
  def create_document_type(name, public_id, system_id, doc)
163
- current.respond_to?(:create_document_type) ? current.create_document_type(name, public_id, system_id, doc) : nil
166
+ doc.create_document_type(name.to_s, public_id.to_s, system_id.to_s)
164
167
  end
165
168
 
166
- # The parsed document's DocumentType node, or nil when it declares none.
169
+ # The parsed document's DocumentType node (`<!DOCTYPE …>`), or nil when
170
+ # the document declares none.
167
171
  def internal_subset(doc)
168
- current.respond_to?(:internal_subset) ? current.internal_subset(doc) : nil
169
- end
170
-
171
- # The backend class for a DocumentType node (nil if unsupported), so the
172
- # wrapper cache can route it to Dommy::DocumentType.
173
- def document_type_class
174
- current.respond_to?(:document_type_class) ? current.document_type_class : nil
172
+ doc.internal_subset
175
173
  end
176
174
 
177
175
  def create_text(content, doc)
178
- current.create_text(content, doc)
176
+ doc.create_text_node(content)
179
177
  end
180
178
 
181
179
  def create_comment(content, doc)
182
- current.create_comment(content, doc)
180
+ doc.create_comment(content)
183
181
  end
184
182
 
185
- # CDATA section node (XML documents). Backends without CDATA fall back to a
186
- # text node.
183
+ # CDATASection (nodeType 4). A genuine XML document — including a
184
+ # `new Document()` / createDocument document, XML-backed (see
185
+ # #empty_xml_document) — mints a real CDATA node and the XML serializer
186
+ # emits `<![CDATA[…]]>`. `Document#create_cdata_section` rejects HTML
187
+ # documents up front (NotSupportedError, per spec), so the text-node
188
+ # fallback below is a defensive guard for an HTML document that still
189
+ # slips through (Lexbor's HTML serializer raises on a native CDATA node,
190
+ # so a text node keeps serialization safe).
187
191
  def create_cdata(content, doc)
188
- current.respond_to?(:create_cdata) ? current.create_cdata(content, doc) : current.create_text(content, doc)
192
+ if doc.is_a?(::Makiri::XML::Document)
193
+ doc.create_cdata(content)
194
+ else
195
+ doc.create_text_node(content)
196
+ end
189
197
  end
190
198
 
191
- def cdata_class
192
- current.respond_to?(:cdata_class) ? current.cdata_class : nil
199
+ # ProcessingInstruction node (`<?target data?>`). Both Makiri document
200
+ # families mint a real PI node and serialize it, so PIs — unlike CDATA —
201
+ # need no HTML-document fallback.
202
+ def create_processing_instruction(target, data, doc)
203
+ doc.create_processing_instruction(target, data)
193
204
  end
194
205
 
195
- # ProcessingInstruction node (`<?target data?>`). Supported by every
196
- # backend Dommy ships (both Makiri document families), so — unlike CDATA —
197
- # there is no text-node fallback.
198
- def create_processing_instruction(target, data, doc)
199
- current.create_processing_instruction(target, data, doc)
206
+ # DOM "child text content": the data of `node`'s Text (and CDATA
207
+ # section) children, in order — no deeper descendant's.
208
+ def child_text_content(node)
209
+ node.children.select { |c| c.text? || c.cdata? }.map(&:content).join
200
210
  end
201
211
 
202
- def processing_instruction_class
203
- current.processing_instruction_class
212
+ # The element's own namespace URI as the DOM reports it (Lexbor's HTML /
213
+ # SVG / MathML, an XML document's own), nil for none — Makiri answers
214
+ # nil, never "", for a node without one, parsed or created.
215
+ def namespace_uri(node)
216
+ node.namespace_uri
204
217
  end
205
218
 
206
- # The element's in-scope namespace declarations (each responds to
207
- # `prefix`/`href`). Empty on backends without an XML namespace model.
208
- def namespace_definitions(node)
209
- current.namespace_definitions(node)
219
+ # The element's own namespace prefix as the DOM reports it, nil for none.
220
+ def prefix(node)
221
+ node.prefix
210
222
  end
211
223
 
212
- # The backend's own fragment holding a `<template>` element's contents
224
+ # Makiri's own fragment holding a `<template>` element's contents
213
225
  # (Lexbor keeps them off the child list), the same one every time; nil
214
226
  # for a node that has none — any node of an XML document, whose contents
215
227
  # the template-content registry keeps instead.
216
228
  def template_contents(node)
217
- current.template_contents(node)
229
+ node.respond_to?(:content_fragment) ? node.content_fragment : nil
218
230
  end
219
231
 
220
- def add_namespace_definition(node, prefix, href)
221
- current.add_namespace_definition(node, prefix, href)
222
- end
232
+ # ----- Namespaced attributes -----
233
+ # Lexbor tracks the attribute's own namespace: set_attribute_ns records
234
+ # it (splitting prefix/local), and the attr node reports
235
+ # namespace_uri/prefix/local_name. So *AttributeNS matches on
236
+ # (namespace, local name) faithfully, and Makiri finds an attribute by
237
+ # that pair natively. `namespace` is an href String or nil throughout,
238
+ # and Makiri takes "" as none, as the DOM does.
223
239
 
224
- # Namespaced attribute access (DOM *AttributeNS). `namespace` is an href
225
- # String or nil.
226
240
  def get_attribute_ns(node, namespace, local_name)
227
- current.get_attribute_ns(node, namespace, local_name)
241
+ node.attribute_value_ns(namespace, local_name.to_s)
228
242
  end
229
243
 
230
- def set_attribute_ns(node, namespace, prefix, local_name, qualified_name, value)
231
- current.set_attribute_ns(node, namespace, prefix, local_name, qualified_name, value)
244
+ def has_attribute_ns?(node, namespace, local_name)
245
+ !attr_by_ns(node, namespace, local_name).nil?
232
246
  end
233
247
 
234
- def remove_attribute_ns(node, namespace, local_name)
235
- current.remove_attribute_ns(node, namespace, local_name)
248
+ # The attribute node in `namespace` with `local_name`, or nil — what
249
+ # getAttributeNodeNS and the NS removals find.
250
+ def attr_by_ns(node, namespace, local_name)
251
+ node.attribute_node_ns(namespace, local_name.to_s)
236
252
  end
237
253
 
238
- def has_attribute_ns?(node, namespace, local_name)
239
- current.has_attribute_ns?(node, namespace, local_name)
254
+ def set_attribute_ns(node, namespace, prefix, _local_name, qualified_name, value)
255
+ none = namespace.to_s.empty?
256
+ note_namespaced_unprefixed_attribute if prefix.to_s.empty? && !none
257
+ name = qualified_name.to_s
258
+ value = value.to_s
259
+ node.set_attribute_ns(namespace, name, value) unless none && set_null_namespace_attribute(node, name, value)
260
+ value
261
+ end
262
+
263
+ # Whether an attribute in a namespace but with no prefix has ever been
264
+ # made (setAttributeNS("urn:x", "id")). That is the only way an
265
+ # attribute whose qualified name is a plain `id` or `class` can be in a
266
+ # namespace — a parser's are either prefixed or, `xmlns` aside, in none —
267
+ # so until one is made, no element need be asked whether it has one
268
+ # (Element#namespaced_unprefixed_attribute?, which gates the JS bridge's
269
+ # attribute snapshot). Process-wide, as an attribute moves between
270
+ # documents with its node; it only ever decides how fast an answer comes.
271
+ def note_namespaced_unprefixed_attribute
272
+ @namespaced_unprefixed_attribute = true
273
+ end
274
+
275
+ def namespaced_unprefixed_attribute?
276
+ @namespaced_unprefixed_attribute == true
277
+ end
278
+
279
+ # Remove by (namespace, local name) — removing by qualified name is
280
+ # ambiguous once same-name/different-namespace attributes coexist.
281
+ def remove_attribute_ns(node, namespace, local_name)
282
+ node.remove_attribute_ns(namespace, local_name.to_s)
283
+ nil
240
284
  end
241
285
 
242
286
  # Reads a backend attribute node into {namespace_uri:, prefix:,
243
287
  # local_name:, qualified_name:, value:} (namespace-aware).
244
288
  def attribute_ns_info(attr_node)
245
- current.attribute_ns_info(attr_node)
289
+ {
290
+ namespace_uri: attr_node.namespace_uri,
291
+ prefix: attr_node.prefix,
292
+ local_name: attr_node.local_name,
293
+ qualified_name: attr_node.name,
294
+ value: attr_node.value,
295
+ }
246
296
  end
247
297
 
248
298
  # The attribute node whose QUALIFIED name is `qualified_name`, or nil.
249
299
  #
250
300
  # WHATWG's by-name family ("get an attribute by name", `setAttribute`,
251
- # `removeAttribute`) matches on the qualified name. A lookup by local name
252
- # confuses `b` with a prefixed `xml:b`, so those paths must come through
253
- # here rather than through `node[name]`.
301
+ # `removeAttribute`) matches on the qualified name. A lookup by local
302
+ # name confuses `b` with a prefixed `xml:b`, so those paths must come
303
+ # through here rather than through `node[name]`. Makiri scans natively.
254
304
  def attr_by_qualified_name(node, qualified_name)
255
- current.attr_by_qualified_name(node, qualified_name)
305
+ node.attribute_by_qualified_name(qualified_name.to_s)
256
306
  end
257
307
 
258
- # That attribute's VALUE, or nil when there is no such attribute. The same
259
- # match without an attribute node in hand, for the two readers that only
260
- # ever wanted the value — `getAttribute` and `hasAttribute` — which run on
261
- # every CSS match and every reflected IDL attribute.
308
+ # That attribute's VALUE, or nil when there is no such attribute. The
309
+ # same match without an attribute node in hand, for the two readers that
310
+ # only ever wanted the value — `getAttribute` and `hasAttribute` — which
311
+ # run on every CSS match and every reflected IDL attribute.
262
312
  def attr_value_by_qualified_name(node, qualified_name)
263
- current.attr_value_by_qualified_name(node, qualified_name)
313
+ node.attribute_value_by_qualified_name(qualified_name.to_s)
264
314
  end
265
315
 
266
- # The element's attribute nodes (each readable via attribute_ns_info).
267
- # The single choke point so DOM code doesn't touch parser internals.
268
- def attribute_nodes(node)
269
- current.attribute_nodes(node)
270
- end
271
-
272
- # Type constants — proxy through to the current backend so
273
- # `node.is_a?(Backend::Element)` resolves dynamically.
274
- def element_class
275
- current::Element
276
- end
277
-
278
- def document_class
279
- current::Document
280
- end
281
-
282
- # What the backend raises for markup that is not well-formed XML (an XML
283
- # document's fragment parse included).
284
- def xml_syntax_error_class
285
- current::XMLSyntaxError
286
- end
287
-
288
- def text_class
289
- current::Text
316
+ # The value of `node`'s attribute named `local_name` in no namespace, or
317
+ # nil — what a selector's `[att]` and HTML's id, class and name read, on
318
+ # the hottest path in the library. One native read, which finds the
319
+ # attribute by namespace and local name, so a namespaced one that shares
320
+ # the name (setAttributeNS("u", "att"), a parsed `xmlns`) is not it.
321
+ def no_namespace_attribute_value(node, local_name)
322
+ node.attribute_value_ns(nil, local_name)
290
323
  end
291
324
 
292
- def comment_class
293
- current::Comment
325
+ # The qualified names of the element's attributes, in order, without
326
+ # materializing an attribute node for each (Element#getAttributeNames).
327
+ def attribute_names(node)
328
+ node.keys
294
329
  end
295
330
 
296
- def document_fragment_class
297
- current::DocumentFragment
298
- end
299
-
300
- def node_class
301
- current::Node
331
+ # The element's attribute nodes (each readable via attribute_ns_info).
332
+ # The single choke point so DOM code doesn't touch parser internals.
333
+ def attribute_nodes(node)
334
+ node.attribute_nodes
302
335
  end
303
336
 
304
337
  private
305
338
 
306
- def detect_default
307
- try_makiri ||
308
- raise(BackendNotAvailable, "Dommy requires either 'makiri' gem to be installed.")
309
- end
310
-
311
- def try_makiri
312
- require "makiri"
313
-
314
- require_relative "backend/makiri_adapter"
315
- Makiri
316
- rescue LoadError
317
- nil
339
+ # A null-namespace name that only the DOM's `setAttribute` can make: one
340
+ # whose local name holds a colon ("xlink:href", "v-on:click") or is
341
+ # "xmlns". Makiri checks set_attribute_ns as the DOM's setAttributeNS
342
+ # does, which refuses these (a prefix needs a namespace).
343
+ def set_attribute_only_name?(name)
344
+ name == "xmlns" || name.include?(":")
345
+ end
346
+
347
+ # Makiri's `set_loose_dom_attribute`, the DOM's setAttribute, for the
348
+ # setAttribute-only names, which set_attribute_ns refuses: a plain
349
+ # attribute in no namespace (never a namespace declaration) whose local
350
+ # name is the whole name, colon included, lower-cased only on an
351
+ # HTML-namespace element of an HTML document. true when it made the
352
+ # attribute; false leaves every other name to set_attribute_ns, which
353
+ # matches an existing attribute by (namespace, local name) rather than
354
+ # by qualified name: `setAttributeNS("u", "a")` then a null-namespace
355
+ # "a" are two attributes.
356
+ def set_null_namespace_attribute(node, name, value)
357
+ return false unless set_attribute_only_name?(name)
358
+
359
+ node.set_loose_dom_attribute(name, value)
360
+ true
318
361
  end
319
362
  end
320
363
  end
data/lib/dommy/blob.rb CHANGED
@@ -36,6 +36,19 @@ module Dommy
36
36
  @window = window
37
37
  end
38
38
 
39
+ # A File of the same bytes and type named `name`, as FormData's "create an
40
+ # entry" makes of a Blob it is given (named "blob") or of a File given a
41
+ # filename.
42
+ def __internal_file_named__(name)
43
+ File.new([@data], name, {"type" => @type}, @window)
44
+ end
45
+
46
+ # Blob's serialization + deserialization steps (structured clone): a new
47
+ # Blob over the same bytes and type.
48
+ def __internal_structured_clone__(_for_storage = false)
49
+ Blob.new([@data], {"type" => @type}, @window)
50
+ end
51
+
39
52
  # Return a new Blob over a byte range of this one.
40
53
  # Negative indices are treated as offsets from the end (per spec).
41
54
  def slice(start = 0, last = @size, content_type = "")
@@ -174,6 +187,12 @@ module Dommy
174
187
  @last_modified = (raw_lm || (Time.now.to_f * 1000)).to_i
175
188
  end
176
189
 
190
+ # File's serialization + deserialization steps: bytes, type, name and
191
+ # lastModified.
192
+ def __internal_structured_clone__(_for_storage = false)
193
+ File.new([@data], @name, {"type" => @type, "lastModified" => @last_modified}, @window)
194
+ end
195
+
177
196
  def __js_get__(key)
178
197
  case key
179
198
  when "name"
@@ -203,6 +222,11 @@ module Dommy
203
222
 
204
223
  alias size length
205
224
 
225
+ # FileList's serialization + deserialization steps: each File cloned.
226
+ def __internal_structured_clone__(for_storage = false)
227
+ FileList.new(@files.map { |file| file.__internal_structured_clone__(for_storage) })
228
+ end
229
+
206
230
  def item(index)
207
231
  @files[index.to_i]
208
232
  end
@@ -19,7 +19,9 @@ module Dommy
19
19
  #
20
20
  # The per-class `JS_METHOD_NAMES` constant holds the class's OWN names; the
21
21
  # suite asserts it matches the class's own `__js_call__` `when` arms — see
22
- # test/test_js_call_dispatch_invariant.rb.
22
+ # test/test_js_call_dispatch_invariant.rb. A mixin module that answers
23
+ # methods of its own (HTMLOrSVGOrMathMLElement's focus / blur) sets the
24
+ # same constant on itself, and the classes it is included in expose them.
23
25
  module Methods
24
26
  def self.included(base)
25
27
  base.extend(ClassMethods)
@@ -49,7 +51,15 @@ module Dommy
49
51
  def js_method_names
50
52
  @js_method_names ||= begin
51
53
  inherited_names = superclass.respond_to?(:js_method_names) ? superclass.js_method_names : []
52
- (inherited_names + own_js_methods).uniq.freeze
54
+ (inherited_names + mixin_js_methods + own_js_methods).uniq.freeze
55
+ end
56
+ end
57
+
58
+ # The names the modules included in this class itself (not in an
59
+ # ancestor) declare.
60
+ def mixin_js_methods
61
+ (ancestors - superclass.ancestors).grep_v(Class).flat_map do |mod|
62
+ mod.const_defined?(:JS_METHOD_NAMES, false) ? mod::JS_METHOD_NAMES : []
53
63
  end
54
64
  end
55
65