dommy 0.13.0 → 0.15.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 (193) 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 +262 -197
  5. data/lib/dommy/blob.rb +7 -0
  6. data/lib/dommy/bridge/constructor.rb +72 -0
  7. data/lib/dommy/bridge/errors.rb +42 -0
  8. data/lib/dommy/bridge/methods.rb +31 -14
  9. data/lib/dommy/bridge/sentinels.rb +39 -0
  10. data/lib/dommy/bridge/values.rb +78 -0
  11. data/lib/dommy/{js → bridge}/wire_tags.rb +14 -11
  12. data/lib/dommy/bridge.rb +22 -250
  13. data/lib/dommy/browser.rb +115 -14
  14. data/lib/dommy/callable_invoker.rb +27 -31
  15. data/lib/dommy/character_data.rb +539 -0
  16. data/lib/dommy/class_list.rb +328 -0
  17. data/lib/dommy/crypto.rb +12 -1
  18. data/lib/dommy/css.rb +15 -0
  19. data/lib/dommy/custom_elements.rb +11 -15
  20. data/lib/dommy/data_transfer.rb +106 -0
  21. data/lib/dommy/document.rb +490 -511
  22. data/lib/dommy/dom_parser.rb +3 -0
  23. data/lib/dommy/element.rb +352 -2246
  24. data/lib/dommy/encodings/tables.rb +4 -4
  25. data/lib/dommy/encodings.rb +58 -27
  26. data/lib/dommy/event.rb +234 -30
  27. data/lib/dommy/event_source.rb +41 -0
  28. data/lib/dommy/fetch.rb +8 -12
  29. data/lib/dommy/form_data.rb +18 -73
  30. data/lib/dommy/form_entry_list.rb +227 -0
  31. data/lib/dommy/fragment.rb +317 -0
  32. data/lib/dommy/history.rb +3 -0
  33. data/lib/dommy/html_canvas_element.rb +1 -1
  34. data/lib/dommy/html_collection.rb +43 -26
  35. data/lib/dommy/html_elements/base.rb +602 -0
  36. data/lib/dommy/html_elements/embedded.rb +237 -0
  37. data/lib/dommy/html_elements/forms/form.rb +1090 -0
  38. data/lib/dommy/html_elements/forms/input.rb +645 -0
  39. data/lib/dommy/html_elements/forms/select.rb +536 -0
  40. data/lib/dommy/html_elements/forms.rb +8 -0
  41. data/lib/dommy/html_elements/interactive.rb +385 -0
  42. data/lib/dommy/html_elements/media.rb +329 -0
  43. data/lib/dommy/html_elements/metadata.rb +357 -0
  44. data/lib/dommy/html_elements/miscellaneous.rb +77 -0
  45. data/lib/dommy/html_elements/tables.rb +448 -0
  46. data/lib/dommy/html_elements.rb +16 -5324
  47. data/lib/dommy/hyperlink_utils.rb +171 -0
  48. data/lib/dommy/interaction/debug.rb +7 -6
  49. data/lib/dommy/interaction/dom_summary.rb +65 -40
  50. data/lib/dommy/interaction/driver.rb +29 -115
  51. data/lib/dommy/interaction/event_synthesis.rb +97 -4
  52. data/lib/dommy/interaction/field_interactor.rb +2 -14
  53. data/lib/dommy/interaction/form_submission.rb +90 -168
  54. data/lib/dommy/interaction/key_sender.rb +121 -0
  55. data/lib/dommy/interaction/mime_types.rb +25 -0
  56. data/lib/dommy/interaction.rb +2 -0
  57. data/lib/dommy/internal/accessibility_tree.rb +36 -13
  58. data/lib/dommy/internal/accessibility_visibility.rb +44 -0
  59. data/lib/dommy/internal/accessible_description.rb +2 -2
  60. data/lib/dommy/internal/accessible_name.rb +109 -94
  61. data/lib/dommy/internal/an_plus_b.rb +125 -0
  62. data/lib/dommy/internal/aria_role.rb +47 -23
  63. data/lib/dommy/internal/aria_snapshot.rb +5 -0
  64. data/lib/dommy/internal/aria_state.rb +31 -12
  65. data/lib/dommy/internal/autofill.rb +141 -0
  66. data/lib/dommy/internal/backend_prefilter.rb +177 -0
  67. data/lib/dommy/internal/bounded_cache.rb +50 -0
  68. data/lib/dommy/internal/child_list.rb +75 -0
  69. data/lib/dommy/internal/child_node.rb +9 -34
  70. data/lib/dommy/internal/complex_match.rb +127 -0
  71. data/lib/dommy/internal/css/calc.rb +143 -145
  72. data/lib/dommy/internal/css/cascade.rb +38 -393
  73. data/lib/dommy/internal/css/cascaded_declarations.rb +236 -0
  74. data/lib/dommy/internal/css/color.rb +61 -34
  75. data/lib/dommy/internal/css/computed_style_builder.rb +226 -0
  76. data/lib/dommy/internal/css/computed_style_declaration.rb +20 -4
  77. data/lib/dommy/internal/css/counters.rb +60 -37
  78. data/lib/dommy/internal/css/custom_properties.rb +97 -78
  79. data/lib/dommy/internal/css/media_query.rb +228 -203
  80. data/lib/dommy/internal/css/parser.rb +70 -41
  81. data/lib/dommy/internal/css/property_registry.rb +32 -56
  82. data/lib/dommy/internal/css/renderability.rb +61 -0
  83. data/lib/dommy/internal/css/rule_index.rb +40 -152
  84. data/lib/dommy/internal/css/selector_dependencies.rb +162 -0
  85. data/lib/dommy/internal/css/style_cache.rb +61 -0
  86. data/lib/dommy/internal/css/ua_stylesheet.rb +42 -2
  87. data/lib/dommy/internal/css/value_tokens.rb +60 -0
  88. data/lib/dommy/internal/css_source.rb +223 -0
  89. data/lib/dommy/internal/css_syntax.rb +153 -0
  90. data/lib/dommy/internal/directionality.rb +188 -0
  91. data/lib/dommy/internal/document_generations.rb +189 -0
  92. data/lib/dommy/internal/document_interaction_state.rb +47 -0
  93. data/lib/dommy/internal/document_live_ranges.rb +97 -0
  94. data/lib/dommy/internal/dom_matching.rb +32 -16
  95. data/lib/dommy/internal/element_aria.rb +199 -0
  96. data/lib/dommy/internal/element_css_inline_style.rb +26 -0
  97. data/lib/dommy/internal/element_editing.rb +41 -0
  98. data/lib/dommy/internal/element_geometry.rb +69 -0
  99. data/lib/dommy/internal/element_matching.rb +5 -12
  100. data/lib/dommy/internal/element_nonce.rb +28 -0
  101. data/lib/dommy/internal/element_popover.rb +109 -0
  102. data/lib/dommy/internal/element_shadow.rb +120 -0
  103. data/lib/dommy/internal/element_state.rb +244 -0
  104. data/lib/dommy/internal/element_tasks.rb +19 -0
  105. data/lib/dommy/internal/element_top_layer.rb +19 -0
  106. data/lib/dommy/internal/enumerated_keyword_sets.rb +53 -0
  107. data/lib/dommy/internal/exception_report.rb +1 -1
  108. data/lib/dommy/internal/html_or_svg_or_mathml_element.rb +75 -0
  109. data/lib/dommy/internal/infra.rb +27 -0
  110. data/lib/dommy/internal/input_type.rb +292 -0
  111. data/lib/dommy/internal/insertion_point.rb +62 -0
  112. data/lib/dommy/internal/literal_lookup.rb +103 -0
  113. data/lib/dommy/internal/mime_type.rb +74 -0
  114. data/lib/dommy/internal/mutation_coordinator.rb +60 -199
  115. data/lib/dommy/internal/namespaces.rb +41 -43
  116. data/lib/dommy/internal/node_adopter.rb +125 -98
  117. data/lib/dommy/internal/node_factory.rb +115 -0
  118. data/lib/dommy/internal/node_identity.rb +31 -0
  119. data/lib/dommy/internal/node_traversal.rb +12 -13
  120. data/lib/dommy/internal/node_wrapper_cache.rb +71 -299
  121. data/lib/dommy/internal/observable_callback.rb +2 -5
  122. data/lib/dommy/internal/observer_options.rb +93 -0
  123. data/lib/dommy/internal/parent_node.rb +29 -29
  124. data/lib/dommy/internal/post_insertion_steps.rb +213 -0
  125. data/lib/dommy/internal/reflected_attributes.rb +456 -24
  126. data/lib/dommy/internal/rendered_text/collector.rb +228 -0
  127. data/lib/dommy/internal/rendered_text/computed_box.rb +39 -0
  128. data/lib/dommy/internal/rendered_text/fragment.rb +55 -0
  129. data/lib/dommy/internal/rendered_text/items.rb +57 -0
  130. data/lib/dommy/internal/rendered_text/whitespace_collapser.rb +132 -0
  131. data/lib/dommy/internal/selector_arguments.rb +79 -0
  132. data/lib/dommy/internal/selector_index.rb +22 -11
  133. data/lib/dommy/internal/selector_match.rb +190 -0
  134. data/lib/dommy/internal/selector_matcher.rb +98 -628
  135. data/lib/dommy/internal/selector_parser.rb +164 -136
  136. data/lib/dommy/internal/shadow_root_registry.rb +7 -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 +112 -66
  140. data/lib/dommy/internal/text_flattening.rb +20 -0
  141. data/lib/dommy/internal/text_selection.rb +104 -0
  142. data/lib/dommy/internal/toggle_events.rb +56 -0
  143. data/lib/dommy/internal/toggle_task_tracker.rb +57 -0
  144. data/lib/dommy/internal/url_parser.rb +722 -569
  145. data/lib/dommy/internal/url_record_accessors.rb +90 -0
  146. data/lib/dommy/internal/web_idl.rb +11 -0
  147. data/lib/dommy/internal/window_constructors.rb +190 -0
  148. data/lib/dommy/internal/xml_serialization.rb +174 -122
  149. data/lib/dommy/js/crossing_profile.rb +75 -0
  150. data/lib/dommy/js/dom_interfaces.rb +23 -2
  151. data/lib/dommy/js/host_bridge.rb +43 -187
  152. data/lib/dommy/js/host_callbacks.rb +106 -0
  153. data/lib/dommy/js/host_runtime.js +863 -763
  154. data/lib/dommy/js/import_map.rb +10 -1
  155. data/lib/dommy/js/marshaller.rb +94 -65
  156. data/lib/dommy/js/module_loader.rb +12 -2
  157. data/lib/dommy/js/module_preload.rb +116 -0
  158. data/lib/dommy/js/observable_runtime.js +125 -146
  159. data/lib/dommy/js/runtime.rb +19 -43
  160. data/lib/dommy/js/runtime_registry.rb +46 -0
  161. data/lib/dommy/js/script_boot.rb +43 -74
  162. data/lib/dommy/js/webidl_tables.js +643 -0
  163. data/lib/dommy/js/window_wiring.rb +69 -0
  164. data/lib/dommy/location.rb +152 -103
  165. data/lib/dommy/mathml_elements.rb +13 -0
  166. data/lib/dommy/mutation_observer.rb +4 -64
  167. data/lib/dommy/navigation.rb +65 -12
  168. data/lib/dommy/navigator.rb +1 -5
  169. data/lib/dommy/node.rb +106 -51
  170. data/lib/dommy/notification.rb +1 -5
  171. data/lib/dommy/parser.rb +5 -16
  172. data/lib/dommy/promise.rb +55 -15
  173. data/lib/dommy/range.rb +15 -13
  174. data/lib/dommy/resources.rb +19 -1
  175. data/lib/dommy/shadow_root.rb +19 -11
  176. data/lib/dommy/streams.rb +2 -6
  177. data/lib/dommy/style_declaration.rb +300 -0
  178. data/lib/dommy/svg_elements.rb +26 -56
  179. data/lib/dommy/tree_walker.rb +2 -8
  180. data/lib/dommy/url.rb +21 -74
  181. data/lib/dommy/url_pattern/regexp_translator.rb +1 -6
  182. data/lib/dommy/url_pattern/tokenizer.rb +4 -2
  183. data/lib/dommy/url_pattern.rb +1 -9
  184. data/lib/dommy/validity_state.rb +383 -0
  185. data/lib/dommy/version.rb +1 -1
  186. data/lib/dommy/web_socket.rb +5 -5
  187. data/lib/dommy/window.rb +35 -171
  188. data/lib/dommy/xml_http_request.rb +35 -4
  189. data/lib/dommy.rb +31 -12
  190. metadata +91 -8
  191. data/lib/dommy/backend/makiri_adapter.rb +0 -374
  192. data/lib/dommy/bridge/constructor_registry.rb +0 -28
  193. data/lib/dommy/internal/css_pseudo_handlers.rb +0 -378
data/lib/dommy/backend.rb CHANGED
@@ -1,297 +1,362 @@
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
42
37
 
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.
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
43
+
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.respond_to?(:quirks_mode?) && 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.
70
87
  def empty_document_like(doc)
71
- current.empty_document_like(doc)
88
+ doc.is_a?(::Makiri::XML::Document) ? empty_xml_document : empty_document
72
89
  end
73
90
 
74
91
  # 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.
92
+ # returning the backend node now owned by `target_doc`. Lexbor's arenas
93
+ # can't move a node between documents, so adoption imports a detached
94
+ # copy — callers must reseat any wrapper onto the returned node.
95
+ #
96
+ # Gap absorption: Lexbor's HTML serializer has no CDATA case (it errors
97
+ # on a CDATA node) and Lexbor is a pinned upstream submodule, so Makiri's
98
+ # cross-kind import_node fails closed when bringing an XML CDATASection
99
+ # into an HTML document. Rather than let that surface as an error,
100
+ # degrade the node to a text node carrying the same data — the data a
101
+ # spec-faithful HTML serializer emits anyway (CDATASection is a Text
102
+ # subtype). The caller reseats the Dommy CDATASection wrapper onto this
103
+ # node, so `nodeType` stays 4 at the DOM level; only the backing node
104
+ # (and thus serialization) is text. An XML target keeps a real CDATA
105
+ # node. This handles a directly adopted CDATA node (a CDATA descendant
106
+ # inside an adopted element subtree is rare, untested, and still fails
107
+ # closed in the backend).
78
108
  def adopt(node, target_doc)
79
- current.adopt(node, target_doc)
80
- end
81
-
82
- # Whether the backend can move a node between documents in place or must adopt a copy first
83
- # (Lexbor's arenas can't move a node, so inserting a foreign node requires importing
84
- # it). Lets callers skip a needless — and on an empty target, root-less and
85
- # therefore crashing — adoption on backends that don't need it.
86
- def moves_nodes_across_documents?
87
- current.respond_to?(:moves_nodes_across_documents?) ? current.moves_nodes_across_documents? : true
88
- end
109
+ if node.is_a?(::Makiri::CDATASection) && !target_doc.is_a?(::Makiri::XML::Document)
110
+ return target_doc.create_text_node(node.text)
111
+ end
89
112
 
90
- # CSS query that honors Dommy's custom pseudo-classes
91
- # (`:disabled`/`:enabled`/`:checked`/`:scope`). Each backend applies its
92
- # own mechanism (Lexbor native pseudos plus a
93
- # `:scope` rewrite). `scope_node` binds `:scope` to that element.
94
- def select_all(node, selector, scope_node: nil)
95
- current.select_all(node, selector, scope_node: scope_node)
113
+ target_doc.import_node(node, true)
96
114
  end
97
115
 
98
- def select_first(node, selector, scope_node: nil)
99
- current.select_first(node, selector, scope_node: scope_node)
116
+ # A copy of the element `node` alone, owned by `target_doc`: its name and
117
+ # attributes exactly as the backend holds them.
118
+ def import_element(node, target_doc)
119
+ target_doc.import_node(node, false)
100
120
  end
101
121
 
102
- # Delegate calls so internal code can use `Backend.parse(...)`.
103
122
  def parse(html)
104
- current.parse(html)
123
+ ::Makiri::HTML::Document.parse(html.to_s)
105
124
  end
106
125
 
107
- # Parse XML input into an XML document.
108
- # fall back to the HTML parser.
126
+ # XML parse (DOMParser `text/xml` / `application/xml`): a real XML
127
+ # document, so element/attribute case is preserved, namespaces are
128
+ # tracked, and CDATA round-trips.
109
129
  def parse_xml(xml)
110
- current.parse_xml(xml)
130
+ ::Makiri::XML::Document.parse(xml.to_s)
111
131
  end
112
132
 
113
133
  def fragment(html, owner_doc:)
114
- current.fragment(html, owner_doc: owner_doc)
115
- end
116
-
117
- # Make `node` the sole document element of `doc` (used by
118
- # DOMImplementation.createDocument). Lexbor-backed documents are seeded with an <html> shell
119
- # that must be cleared first.
120
- def set_document_root(doc, node)
121
- current.set_document_root(doc, node)
122
- end
123
-
124
- def create_element(name, doc)
125
- current.create_element(name, doc)
126
- end
127
-
128
- # Create a namespaced element permitting a DOM-valid qualified name that a
129
- # strict XML backend would reject (an internal invalid char like "f}oo"),
130
- # preserving case/prefix. Returns nil when the backend has no loose path
131
- # (fall back to #create_element); raises ArgumentError for a genuinely
132
- # invalid name (the caller maps it to InvalidCharacterError).
133
- def create_element_loose(qualified_name, prefix, local, namespace, doc)
134
- current.create_element_loose(qualified_name, prefix, local, namespace, doc)
135
- end
136
-
137
- # A detached DocumentType node owned by `doc` (for
138
- # DOMImplementation.createDocumentType). Returns nil when the backend has no
139
- # doctype factory (the caller falls back to a synthetic DocumentType); raises
140
- # ArgumentError for a name the factory rejects (the caller then also falls
141
- # back, since createDocumentType is permissive).
134
+ ::Makiri::DocumentFragment.parse(html.to_s)
135
+ end
136
+
137
+ # The DOM's createElement: an element whose local name is `local_name`
138
+ # as written, colons included (`foo:` and `f::oo` are valid local names,
139
+ # not prefixed ones), in `namespace` — the HTML namespace in an HTML or
140
+ # XHTML document, nil in any other. An XML document's own creator parses
141
+ # a QName, so its element is built from the parts. An HTML-backed one —
142
+ # which a document typed as XHTML or XML after an HTML parse also is —
143
+ # makes it by createElementNS, which keeps the name's case and the
144
+ # namespace given; that splits a name at its colon, and refuses `xmlns`
145
+ # outside the XMLNS namespace, as createElementNS must — so a name with
146
+ # a colon, or `xmlns`, takes the HTML document's creator, which keeps
147
+ # the name as written but folds it into the HTML namespace in lower case.
148
+ def create_element(local_name, namespace, doc)
149
+ return doc.create_loose_dom_element(local_name, nil, local_name, namespace) unless doc.is_a?(::Makiri::HTML::Document)
150
+ return doc.create_element(local_name) if local_name.include?(":") || local_name == "xmlns"
151
+
152
+ doc.create_element_ns(namespace, local_name)
153
+ end
154
+
155
+ # The DOM's createElementNS, in an HTML or an XML document alike: the
156
+ # element is in `namespace` (nil for none) with `qualified_name` as
157
+ # written — an SVG `feGaussianBlur` keeps its case, a prefixed name its
158
+ # prefix. Makiri checks the name as the DOM does, so a name the caller
159
+ # has validated is never refused; it raises ArgumentError for one it
160
+ # would not have passed.
161
+ def create_element_ns(namespace, qualified_name, doc)
162
+ doc.create_element_ns(namespace, qualified_name.to_s)
163
+ end
164
+
165
+ # A detached DocumentType node owned by `doc`, HTML or XML (for
166
+ # DOMImplementation.createDocumentType). Makiri takes any name the DOM
167
+ # does but the empty one, which Lexbor reads as no name, and raises
168
+ # ArgumentError for it; the caller then falls back to a synthetic
169
+ # (non-tree) DocumentType, since createDocumentType is permissive.
142
170
  def create_document_type(name, public_id, system_id, doc)
143
- current.respond_to?(:create_document_type) ? current.create_document_type(name, public_id, system_id, doc) : nil
171
+ doc.create_document_type(name.to_s, public_id.to_s, system_id.to_s)
144
172
  end
145
173
 
146
- # The parsed document's DocumentType node, or nil when it declares none.
174
+ # The parsed document's DocumentType node (`<!DOCTYPE …>`), or nil when
175
+ # the document declares none.
147
176
  def internal_subset(doc)
148
- current.respond_to?(:internal_subset) ? current.internal_subset(doc) : nil
149
- end
150
-
151
- # The backend class for a DocumentType node (nil if unsupported), so the
152
- # wrapper cache can route it to Dommy::DocumentType.
153
- def document_type_class
154
- current.respond_to?(:document_type_class) ? current.document_type_class : nil
177
+ doc.internal_subset
155
178
  end
156
179
 
157
180
  def create_text(content, doc)
158
- current.create_text(content, doc)
181
+ doc.create_text_node(content)
159
182
  end
160
183
 
161
184
  def create_comment(content, doc)
162
- current.create_comment(content, doc)
185
+ doc.create_comment(content)
163
186
  end
164
187
 
165
- # CDATA section node (XML documents). Backends without CDATA fall back to a
166
- # text node.
188
+ # CDATASection (nodeType 4). A genuine XML document — including a
189
+ # `new Document()` / createDocument document, XML-backed (see
190
+ # #empty_xml_document) — mints a real CDATA node and the XML serializer
191
+ # emits `<![CDATA[…]]>`. `Document#create_cdata_section` rejects HTML
192
+ # documents up front (NotSupportedError, per spec), so the text-node
193
+ # fallback below is a defensive guard for an HTML document that still
194
+ # slips through (Lexbor's HTML serializer raises on a native CDATA node,
195
+ # so a text node keeps serialization safe).
167
196
  def create_cdata(content, doc)
168
- current.respond_to?(:create_cdata) ? current.create_cdata(content, doc) : current.create_text(content, doc)
197
+ if doc.is_a?(::Makiri::XML::Document)
198
+ doc.create_cdata(content)
199
+ else
200
+ doc.create_text_node(content)
201
+ end
169
202
  end
170
203
 
171
- def cdata_class
172
- current.respond_to?(:cdata_class) ? current.cdata_class : nil
204
+ # ProcessingInstruction node (`<?target data?>`). Both Makiri document
205
+ # families mint a real PI node and serialize it, so PIs — unlike CDATA —
206
+ # need no HTML-document fallback.
207
+ def create_processing_instruction(target, data, doc)
208
+ doc.create_processing_instruction(target, data)
173
209
  end
174
210
 
175
- # ProcessingInstruction node (`<?target data?>`). Supported by every
176
- # backend Dommy ships (both Makiri document families), so — unlike CDATA —
177
- # there is no text-node fallback.
178
- def create_processing_instruction(target, data, doc)
179
- current.create_processing_instruction(target, data, doc)
211
+ # DOM "child text content": the data of `node`'s Text (and CDATA
212
+ # section) children, in order — no deeper descendant's.
213
+ def child_text_content(node)
214
+ node.children.select { |c| c.text? || c.cdata? }.map(&:content).join
180
215
  end
181
216
 
182
- def processing_instruction_class
183
- current.processing_instruction_class
217
+ # The element's own namespace URI as the DOM reports it (Lexbor's HTML /
218
+ # SVG / MathML, an XML document's own), nil for none — Makiri answers
219
+ # nil, never "", for a node without one, parsed or created.
220
+ def namespace_uri(node)
221
+ node.namespace_uri
184
222
  end
185
223
 
186
- def namespace_of(node)
187
- current.namespace_of(node)
224
+ # The element's own namespace prefix as the DOM reports it, nil for none.
225
+ def prefix(node)
226
+ node.prefix
188
227
  end
189
228
 
190
- # The element's in-scope namespace declarations (each responds to
191
- # `prefix`/`href`). Empty on backends without an XML namespace model.
192
- def namespace_definitions(node)
193
- current.namespace_definitions(node)
229
+ # Makiri's own fragment holding a `<template>` element's contents
230
+ # (Lexbor keeps them off the child list), the same one every time; nil
231
+ # for a node that has none — any node of an XML document, whose contents
232
+ # the template-content registry keeps instead.
233
+ def template_contents(node)
234
+ node.respond_to?(:content_fragment) ? node.content_fragment : nil
194
235
  end
195
236
 
196
- # The content child nodes of a `<template>` element. HTML5 parsers model
197
- # template contents differently — Lexbor/Makiri in a separate content fragment — so reading them goes
198
- # through the backend. Used by the template-content registry's migration.
199
- def template_content_nodes(node)
200
- current.template_content_nodes(node)
237
+ # ----- Namespaced attributes -----
238
+ # Lexbor tracks the attribute's own namespace: set_attribute_ns records
239
+ # it (splitting prefix/local), and the attr node reports
240
+ # namespace_uri/prefix/local_name. So *AttributeNS matches on
241
+ # (namespace, local name) faithfully, and Makiri finds an attribute by
242
+ # that pair natively. `namespace` is an href String or nil throughout,
243
+ # and Makiri takes "" as none, as the DOM does.
244
+
245
+ def get_attribute_ns(node, namespace, local_name)
246
+ node.attribute_value_ns(namespace, local_name.to_s)
201
247
  end
202
248
 
203
- def add_namespace_definition(node, prefix, href)
204
- current.add_namespace_definition(node, prefix, href)
249
+ def has_attribute_ns?(node, namespace, local_name)
250
+ !attr_by_ns(node, namespace, local_name).nil?
205
251
  end
206
252
 
207
- # Namespaced attribute access (DOM *AttributeNS). `namespace` is an href
208
- # String or nil.
209
- def get_attribute_ns(node, namespace, local_name)
210
- current.get_attribute_ns(node, namespace, local_name)
253
+ # The attribute node in `namespace` with `local_name`, or nil — what
254
+ # getAttributeNodeNS and the NS removals find.
255
+ def attr_by_ns(node, namespace, local_name)
256
+ node.attribute_node_ns(namespace, local_name.to_s)
211
257
  end
212
258
 
213
- def set_attribute_ns(node, namespace, prefix, local_name, qualified_name, value)
214
- current.set_attribute_ns(node, namespace, prefix, local_name, qualified_name, value)
259
+ def set_attribute_ns(node, namespace, prefix, _local_name, qualified_name, value)
260
+ none = namespace.to_s.empty?
261
+ note_namespaced_unprefixed_attribute if prefix.to_s.empty? && !none
262
+ name = qualified_name.to_s
263
+ value = value.to_s
264
+ node.set_attribute_ns(namespace, name, value) unless none && set_null_namespace_attribute(node, name, value)
265
+ value
215
266
  end
216
267
 
217
- def remove_attribute_ns(node, namespace, local_name)
218
- current.remove_attribute_ns(node, namespace, local_name)
268
+ # Whether an attribute in a namespace but with no prefix has ever been
269
+ # made (setAttributeNS("urn:x", "id")). That is the only way an
270
+ # attribute whose qualified name is a plain `id` or `class` can be in a
271
+ # namespace — a parser's are either prefixed or, `xmlns` aside, in none —
272
+ # so until one is made, no element need be asked whether it has one
273
+ # (Element#namespaced_unprefixed_attribute?, which gates the JS bridge's
274
+ # attribute snapshot). Process-wide, as an attribute moves between
275
+ # documents with its node; it only ever decides how fast an answer comes.
276
+ def note_namespaced_unprefixed_attribute
277
+ @namespaced_unprefixed_attribute = true
219
278
  end
220
279
 
221
- def has_attribute_ns?(node, namespace, local_name)
222
- current.has_attribute_ns?(node, namespace, local_name)
280
+ def namespaced_unprefixed_attribute?
281
+ @namespaced_unprefixed_attribute == true
282
+ end
283
+
284
+ # Remove by (namespace, local name) — removing by qualified name is
285
+ # ambiguous once same-name/different-namespace attributes coexist.
286
+ def remove_attribute_ns(node, namespace, local_name)
287
+ node.remove_attribute_ns(namespace, local_name.to_s)
288
+ nil
223
289
  end
224
290
 
225
291
  # Reads a backend attribute node into {namespace_uri:, prefix:,
226
292
  # local_name:, qualified_name:, value:} (namespace-aware).
227
293
  def attribute_ns_info(attr_node)
228
- current.attribute_ns_info(attr_node)
294
+ {
295
+ namespace_uri: attr_node.namespace_uri,
296
+ prefix: attr_node.prefix,
297
+ local_name: attr_node.local_name,
298
+ qualified_name: attr_node.name,
299
+ value: attr_node.value,
300
+ }
229
301
  end
230
302
 
231
303
  # The attribute node whose QUALIFIED name is `qualified_name`, or nil.
232
304
  #
233
305
  # WHATWG's by-name family ("get an attribute by name", `setAttribute`,
234
- # `removeAttribute`) matches on the qualified name. A lookup by local name
235
- # confuses `b` with a prefixed `xml:b`, so those paths must come through
236
- # here rather than through `node[name]`.
306
+ # `removeAttribute`) matches on the qualified name. A lookup by local
307
+ # name confuses `b` with a prefixed `xml:b`, so those paths must come
308
+ # through here rather than through `node[name]`. Makiri scans natively.
237
309
  def attr_by_qualified_name(node, qualified_name)
238
- current.attr_by_qualified_name(node, qualified_name)
310
+ node.attribute_by_qualified_name(qualified_name.to_s)
239
311
  end
240
312
 
241
- # That attribute's VALUE, or nil when there is no such attribute. The same
242
- # match without an attribute node in hand, for the two readers that only
243
- # ever wanted the value — `getAttribute` and `hasAttribute` — which run on
244
- # every CSS match and every reflected IDL attribute.
313
+ # That attribute's VALUE, or nil when there is no such attribute. The
314
+ # same match without an attribute node in hand, for the two readers that
315
+ # only ever wanted the value — `getAttribute` and `hasAttribute` — which
316
+ # run on every CSS match and every reflected IDL attribute.
245
317
  def attr_value_by_qualified_name(node, qualified_name)
246
- current.attr_value_by_qualified_name(node, qualified_name)
318
+ node.attribute_value_by_qualified_name(qualified_name.to_s)
319
+ end
320
+
321
+ # The value of `node`'s attribute named `local_name` in no namespace, or
322
+ # nil — what a selector's `[att]` and HTML's id, class and name read, on
323
+ # the hottest path in the library. One native read, which finds the
324
+ # attribute by namespace and local name, so a namespaced one that shares
325
+ # the name (setAttributeNS("u", "att"), a parsed `xmlns`) is not it.
326
+ def no_namespace_attribute_value(node, local_name)
327
+ node.attribute_value_ns(nil, local_name)
247
328
  end
248
329
 
249
330
  # The element's attribute nodes (each readable via attribute_ns_info).
250
331
  # The single choke point so DOM code doesn't touch parser internals.
251
332
  def attribute_nodes(node)
252
- current.attribute_nodes(node)
253
- end
254
-
255
- # Type constants — proxy through to the current backend so
256
- # `node.is_a?(Backend::Element)` resolves dynamically.
257
- def element_class
258
- current::Element
259
- end
260
-
261
- def document_class
262
- current::Document
263
- end
264
-
265
- def text_class
266
- current::Text
267
- end
268
-
269
- def comment_class
270
- current::Comment
271
- end
272
-
273
- def document_fragment_class
274
- current::DocumentFragment
275
- end
276
-
277
- def node_class
278
- current::Node
333
+ node.attribute_nodes
279
334
  end
280
335
 
281
336
  private
282
337
 
283
- def detect_default
284
- try_makiri ||
285
- raise(BackendNotAvailable, "Dommy requires either 'makiri' gem to be installed.")
286
- end
287
-
288
- def try_makiri
289
- require "makiri"
290
-
291
- require_relative "backend/makiri_adapter"
292
- Makiri
293
- rescue LoadError
294
- nil
338
+ # A null-namespace name that only the DOM's `setAttribute` can make: one
339
+ # whose local name holds a colon ("xlink:href", "v-on:click") or is
340
+ # "xmlns". Makiri checks set_attribute_ns as the DOM's setAttributeNS
341
+ # does, which refuses these (a prefix needs a namespace).
342
+ def set_attribute_only_name?(name)
343
+ name == "xmlns" || name.include?(":")
344
+ end
345
+
346
+ # Makiri's `set_loose_dom_attribute`, the DOM's setAttribute, for the
347
+ # setAttribute-only names, which set_attribute_ns refuses: a plain
348
+ # attribute in no namespace (never a namespace declaration) whose local
349
+ # name is the whole name, colon included, lower-cased only on an
350
+ # HTML-namespace element of an HTML document. true when it made the
351
+ # attribute; false leaves every other name to set_attribute_ns, which
352
+ # matches an existing attribute by (namespace, local name) rather than
353
+ # by qualified name: `setAttributeNS("u", "a")` then a null-namespace
354
+ # "a" are two attributes.
355
+ def set_null_namespace_attribute(node, name, value)
356
+ return false unless set_attribute_only_name?(name)
357
+
358
+ node.set_loose_dom_attribute(name, value)
359
+ true
295
360
  end
296
361
  end
297
362
  end
data/lib/dommy/blob.rb CHANGED
@@ -36,6 +36,13 @@ 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
+
39
46
  # Return a new Blob over a byte range of this one.
40
47
  # Negative indices are treated as offsets from the end (per spec).
41
48
  def slice(start = 0, last = @size, content_type = "")
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Bridge
5
+ # A JS constructor function as Ruby sees it: `new X(...)` plus the static
6
+ # methods hanging off it, and the table Window looks names up in. The
7
+ # registry maps to these, so they share a file.
8
+
9
+ # Block-as-constructor adapter — invoking `__js_new__(args)`
10
+ # calls the wrapped block with `args` and returns whatever the
11
+ # block produces. Used by Window to wire up `new Event(init)`,
12
+ # `new CustomEvent(init)`, etc. without hand-rolling a class
13
+ # for each constructor.
14
+ class Constructor
15
+ def initialize(&block)
16
+ @block = block
17
+ @class_methods = {}
18
+ end
19
+
20
+ def __js_new__(args)
21
+ @block.call(args)
22
+ end
23
+
24
+ # Register a class-level method (e.g. `URL.createObjectURL`)
25
+ # that JS bridges resolve via `__js_call__` on the constructor
26
+ # itself. Returns self for chaining.
27
+ def define_class_method(name, &block)
28
+ @class_methods[name.to_s] = block
29
+ self
30
+ end
31
+
32
+ # A name outside the registered statics is not a method of this
33
+ # constructor, and answering nil would make `URL.notAThing()` read as a
34
+ # call that returned null rather than one that should not have compiled.
35
+ def __js_call__(method, args)
36
+ handler = @class_methods[method.to_s]
37
+ raise TypeError, "#{method} is not a function" unless handler
38
+
39
+ handler.call(args)
40
+ end
41
+
42
+ # Names of the registered class-level (static) methods, so a JS host can
43
+ # expose them on the constructor function (e.g. `URL.createObjectURL`).
44
+ def __js_class_method_names__
45
+ @class_methods.keys
46
+ end
47
+ end
48
+
49
+ # Maps JS global constructor names (e.g. "Event", "URL", "XMLHttpRequest")
50
+ # to their `Bridge::Constructor` instances. Window builds one and routes
51
+ # `__js_get__` name lookups through it, instead of carrying one ivar plus
52
+ # one `when` arm per constructor.
53
+ class ConstructorRegistry
54
+ def initialize(map)
55
+ @map = map.freeze
56
+ end
57
+
58
+ def [](name)
59
+ @map[name]
60
+ end
61
+
62
+ def key?(name)
63
+ @map.key?(name)
64
+ end
65
+
66
+ # The set of constructor names, e.g. for host-side enumeration / tests.
67
+ def names
68
+ @map.keys
69
+ end
70
+ end
71
+ end
72
+ end