dommy 0.6.0 → 0.7.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -38
  3. data/lib/dommy/animation.rb +1 -1
  4. data/lib/dommy/attr.rb +23 -11
  5. data/lib/dommy/backend/nokogiri_adapter.rb +51 -0
  6. data/lib/dommy/backend/nokolexbor_adapter.rb +80 -0
  7. data/lib/dommy/backend.rb +129 -0
  8. data/lib/dommy/blob.rb +2 -2
  9. data/lib/dommy/compression_streams.rb +4 -4
  10. data/lib/dommy/cookie_store.rb +1 -1
  11. data/lib/dommy/crypto.rb +9 -8
  12. data/lib/dommy/css.rb +7 -7
  13. data/lib/dommy/custom_elements.rb +6 -6
  14. data/lib/dommy/document.rb +98 -32
  15. data/lib/dommy/dom_parser.rb +5 -4
  16. data/lib/dommy/element.rb +231 -50
  17. data/lib/dommy/event.rb +61 -25
  18. data/lib/dommy/event_source.rb +8 -8
  19. data/lib/dommy/fetch.rb +14 -6
  20. data/lib/dommy/file_reader.rb +3 -3
  21. data/lib/dommy/form_data.rb +1 -3
  22. data/lib/dommy/history.rb +7 -4
  23. data/lib/dommy/html_collection.rb +4 -4
  24. data/lib/dommy/html_elements.rb +110 -42
  25. data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
  26. data/lib/dommy/internal/dom_matching.rb +3 -3
  27. data/lib/dommy/internal/node_traversal.rb +1 -1
  28. data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
  29. data/lib/dommy/internal/template_content_registry.rb +6 -6
  30. data/lib/dommy/intersection_observer.rb +2 -2
  31. data/lib/dommy/location.rb +8 -4
  32. data/lib/dommy/media_query_list.rb +3 -3
  33. data/lib/dommy/message_channel.rb +9 -9
  34. data/lib/dommy/mutation_observer.rb +21 -11
  35. data/lib/dommy/navigator.rb +12 -12
  36. data/lib/dommy/node.rb +12 -0
  37. data/lib/dommy/notification.rb +3 -3
  38. data/lib/dommy/parser.rb +13 -13
  39. data/lib/dommy/performance_observer.rb +2 -2
  40. data/lib/dommy/range.rb +2 -2
  41. data/lib/dommy/resize_observer.rb +2 -2
  42. data/lib/dommy/shadow_root.rb +10 -8
  43. data/lib/dommy/streams.rb +22 -22
  44. data/lib/dommy/text_codec.rb +4 -4
  45. data/lib/dommy/tree_walker.rb +21 -21
  46. data/lib/dommy/url.rb +25 -8
  47. data/lib/dommy/version.rb +1 -1
  48. data/lib/dommy/web_socket.rb +13 -13
  49. data/lib/dommy/window.rb +14 -1
  50. data/lib/dommy/worker.rb +5 -5
  51. data/lib/dommy/xml_http_request.rb +19 -4
  52. data/lib/dommy.rb +12 -2
  53. metadata +12 -26
@@ -64,16 +64,16 @@ module Dommy
64
64
  # registered; fires `connectedCallback` for each upgraded node
65
65
  # that's currently attached to a document tree.
66
66
  def upgrade(root)
67
- return nil unless root.respond_to?(:__node__)
67
+ return nil unless root.respond_to?(:__dommy_backend_node__)
68
68
 
69
- walk_descendants(root.__node__) do |nk|
69
+ walk_descendants(root.__dommy_backend_node__) do |nk|
70
70
  next unless nk.element?
71
71
  next unless @definitions.key?(nk.name)
72
72
 
73
73
  # Force re-wrap by clearing the document's cached wrapper.
74
- @window.document.__reset_wrapper__(nk)
74
+ @window.document.__internal_reset_wrapper__(nk)
75
75
  wrapped = @window.document.wrap_node(nk)
76
- @window.document.__notify_connected__(wrapped) if wrapped
76
+ @window.document.__internal_notify_connected__(wrapped) if wrapped
77
77
  end
78
78
 
79
79
  nil
@@ -109,9 +109,9 @@ module Dommy
109
109
  def upgrade_existing(name)
110
110
  doc = @window.document
111
111
  doc.nokogiri_doc.css(name).each do |nk|
112
- doc.__reset_wrapper__(nk)
112
+ doc.__internal_reset_wrapper__(nk)
113
113
  wrapped = doc.wrap_node(nk)
114
- doc.__notify_connected__(wrapped) if wrapped
114
+ doc.__internal_notify_connected__(wrapped) if wrapped
115
115
  end
116
116
  end
117
117
 
@@ -46,6 +46,9 @@ module Dommy
46
46
 
47
47
  attr_reader :body, :nokogiri_doc
48
48
  attr_accessor :default_view
49
+ # content_type defaults to "text/html"; settable so an integration layer
50
+ # can reflect the response Content-Type. Read-only over the JS bridge.
51
+ attr_accessor :content_type
49
52
 
50
53
  def initialize(host = nil, nokogiri_doc: nil, default_view: nil)
51
54
  @host = host
@@ -56,9 +59,10 @@ module Dommy
56
59
  @cookie_jar = Internal::CookieJar.new
57
60
  @template_content_registry = Internal::TemplateContentRegistry.new(self)
58
61
  @mutation_coordinator = Internal::MutationCoordinator.new(self, @observer_manager)
59
- @nokogiri_doc = nokogiri_doc || Nokogiri::HTML5("<!doctype html><html><head></head><body></body></html>")
62
+ @nokogiri_doc = nokogiri_doc || Backend.parse("<!doctype html><html><head></head><body></body></html>")
60
63
  body_node = @nokogiri_doc.at_css("body")
61
64
  @body = wrap_node(body_node) if body_node
65
+ @content_type = "text/html"
62
66
  end
63
67
 
64
68
  # ----- Public Ruby API (snake_case) -----
@@ -79,6 +83,21 @@ module Dommy
79
83
  wrap_node(@nokogiri_doc.at_css("head"))
80
84
  end
81
85
 
86
+ # Serialize the whole document to HTML (including the doctype).
87
+ def to_html
88
+ @nokogiri_doc.to_html
89
+ end
90
+
91
+ # XPath queries returning wrapped nodes (Element / TextNode / etc).
92
+ def at_xpath(expression)
93
+ node = @nokogiri_doc.at_xpath(expression)
94
+ node && wrap_node(node)
95
+ end
96
+
97
+ def xpath(expression)
98
+ @nokogiri_doc.xpath(expression).map { |node| wrap_node(node) }
99
+ end
100
+
82
101
  # `document.URL` / `documentURI` — both return location.href in
83
102
  # real browsers (legacy aliases of the same field).
84
103
  def url
@@ -116,6 +135,15 @@ module Dommy
116
135
  view.location.__js_get__("hostname").to_s
117
136
  end
118
137
 
138
+ # `document.origin` — serialized origin of the document URL, mirroring
139
+ # `window.location.origin`. Empty when there is no associated window.
140
+ def origin
141
+ view = @default_view
142
+ return "" unless view&.location
143
+
144
+ view.location.__js_get__("origin").to_s
145
+ end
146
+
119
147
  # `document.referrer` — Dommy never has a referring page, so this
120
148
  # is always empty.
121
149
  def referrer
@@ -175,7 +203,7 @@ module Dommy
175
203
  @active_element || @body
176
204
  end
177
205
 
178
- def __set_active_element__(el)
206
+ def __internal_set_active_element__(el)
179
207
  @active_element = el
180
208
  end
181
209
 
@@ -202,9 +230,9 @@ module Dommy
202
230
  # wrapper is owned by `this`. Per spec, the source node is left
203
231
  # in place. `deep: true` copies the entire subtree.
204
232
  def import_node(node, deep = false)
205
- return nil unless node.respond_to?(:__node__)
233
+ return nil unless node.respond_to?(:__dommy_backend_node__)
206
234
 
207
- copy = clone_into_doc(node.__node__, deep)
235
+ copy = clone_into_doc(node.__dommy_backend_node__, deep)
208
236
  wrap_node(copy)
209
237
  end
210
238
 
@@ -212,17 +240,32 @@ module Dommy
212
240
  # node is detached from its previous owner and its ownerDocument
213
241
  # becomes this. Returns the (possibly re-wrapped) node.
214
242
  def adopt_node(node)
215
- return nil unless node.respond_to?(:__node__)
243
+ return nil unless node.respond_to?(:__dommy_backend_node__)
216
244
 
217
- src = node.__node__
245
+ src = node.__dommy_backend_node__
218
246
  src.unlink if src.parent
219
- moved = if src.document == @nokogiri_doc
220
- src
221
- else
222
- clone_into_doc(src, true)
223
- end
224
247
 
225
- wrap_node(moved)
248
+ # Same document: just return the wrapper after the detach above.
249
+ return wrap_node(src) if src.document == @nokogiri_doc
250
+
251
+ # Cross-document: Nokogiri reassigns `src.document` when src is
252
+ # added under a node owned by another document. We transiently
253
+ # attach to our root, then unlink so src ends up free-floating
254
+ # but now belongs to @nokogiri_doc. The underlying Ruby object
255
+ # identity is preserved.
256
+ src_doc_wrapper = node.instance_variable_get(:@document)
257
+ @nokogiri_doc.root.add_child(src)
258
+ src.unlink
259
+
260
+ # Move the caller's Dommy wrapper from the source document's
261
+ # wrapper cache into ours, and re-point its @document. This
262
+ # keeps `adopt_node(x).equal?(x)` true across documents.
263
+ node.instance_variable_set(:@document, self)
264
+ if src_doc_wrapper.respond_to?(:__internal_reset_wrapper__)
265
+ src_doc_wrapper.__internal_reset_wrapper__(src)
266
+ end
267
+ @node_wrapper_cache.register(src, node)
268
+ node
226
269
  end
227
270
 
228
271
  # Legacy `document.createEvent("EventName")` factory. Returns an
@@ -268,7 +311,7 @@ module Dommy
268
311
  # is the read side.
269
312
  attr_reader :fullscreen_element
270
313
 
271
- def __set_fullscreen_element__(element)
314
+ def __internal_set_fullscreen_element__(element)
272
315
  previous = @fullscreen_element
273
316
  @fullscreen_element = element
274
317
  return if previous == element
@@ -339,8 +382,8 @@ module Dommy
339
382
  fragment = Parser.fragment(html, owner_doc: @nokogiri_doc)
340
383
  removed = []
341
384
  added = fragment.children.to_a
342
- added.each { |node| @body.__node__.add_child(node) }
343
- notify_child_list_mutation(target_node: @body.__node__, added_nodes: added, removed_nodes: removed)
385
+ added.each { |node| @body.__dommy_backend_node__.add_child(node) }
386
+ notify_child_list_mutation(target_node: @body.__dommy_backend_node__, added_nodes: added, removed_nodes: removed)
344
387
  nil
345
388
  end
346
389
 
@@ -408,6 +451,10 @@ module Dommy
408
451
  base_uri
409
452
  when "domain"
410
453
  domain
454
+ when "origin"
455
+ origin
456
+ when "contentType"
457
+ content_type
411
458
  when "referrer"
412
459
  referrer
413
460
  when "links"
@@ -444,6 +491,19 @@ module Dommy
444
491
  nil
445
492
  end
446
493
 
494
+ # Methods routed through __js_call__ (keep in sync with its when-arms).
495
+ JS_METHOD_NAMES = %w[
496
+ exitFullscreen startViewTransition createElement createElementNS createTextNode
497
+ createComment createDocumentFragment querySelector querySelectorAll getElementById
498
+ getElementsByClassName getElementsByTagName getElementsByName createAttribute
499
+ createAttributeNS createTreeWalker createNodeIterator createEvent importNode adoptNode
500
+ hasFocus getSelection elementFromPoint queryCommandSupported addEventListener
501
+ removeEventListener dispatchEvent write open close
502
+ ].freeze
503
+ def __js_method_names__
504
+ JS_METHOD_NAMES
505
+ end
506
+
447
507
  def __js_call__(method, args)
448
508
  case method
449
509
  when "exitFullscreen"
@@ -521,7 +581,7 @@ module Dommy
521
581
  end
522
582
  end
523
583
 
524
- def __event_parent__
584
+ def __internal_event_parent__
525
585
  @default_view
526
586
  end
527
587
 
@@ -533,7 +593,7 @@ module Dommy
533
593
  # Clear the cached wrapper so the next `wrap_node` creates a new
534
594
  # one. Used by `customElements.define` to upgrade nodes that were
535
595
  # constructed before the registration landed.
536
- def __reset_wrapper__(nokogiri_node)
596
+ def __internal_reset_wrapper__(nokogiri_node)
537
597
  @node_wrapper_cache.reset_wrapper(nokogiri_node)
538
598
  end
539
599
 
@@ -543,15 +603,15 @@ module Dommy
543
603
  # node back to its shadow boundary.
544
604
  # Delegate to ShadowRootRegistry
545
605
 
546
- def __register_shadow_fragment__(fragment_node, shadow_root)
606
+ def __internal_register_shadow_fragment__(fragment_node, shadow_root)
547
607
  @shadow_registry.register(fragment_node, shadow_root)
548
608
  end
549
609
 
550
- def __shadow_root_for_fragment__(fragment_node)
610
+ def __internal_shadow_root_for_fragment__(fragment_node)
551
611
  @shadow_registry.find_for_fragment(fragment_node)
552
612
  end
553
613
 
554
- def __shadow_root_containing__(node)
614
+ def __internal_shadow_root_containing__(node)
555
615
  @shadow_registry.find_enclosing(node)
556
616
  end
557
617
 
@@ -560,23 +620,23 @@ module Dommy
560
620
  # break the whole mutation pipeline.
561
621
  # Delegate to MutationCoordinator
562
622
 
563
- def __notify_connected__(element)
623
+ def __internal_notify_connected__(element)
564
624
  @mutation_coordinator.notify_connected(element)
565
625
  end
566
626
 
567
- def __notify_disconnected__(element)
627
+ def __internal_notify_disconnected__(element)
568
628
  @mutation_coordinator.notify_disconnected(element)
569
629
  end
570
630
 
571
- def __notify_connected_subtree__(nk)
631
+ def __internal_notify_connected_subtree__(nk)
572
632
  @mutation_coordinator.notify_connected_subtree(nk)
573
633
  end
574
634
 
575
- def __notify_disconnected_subtree__(nk)
635
+ def __internal_notify_disconnected_subtree__(nk)
576
636
  @mutation_coordinator.notify_disconnected_subtree(nk)
577
637
  end
578
638
 
579
- def __notify_attribute_changed__(element, name, old_value, new_value)
639
+ def __internal_notify_attribute_changed__(element, name, old_value, new_value)
580
640
  @mutation_coordinator.notify_attribute_changed(element, name, old_value, new_value)
581
641
  end
582
642
 
@@ -675,17 +735,17 @@ module Dommy
675
735
  # adoptNode for cross-document transfer.
676
736
  def clone_into_doc(source, deep)
677
737
  copy = if source.element?
678
- new_el = Nokogiri::XML::Node.new(source.name, @nokogiri_doc)
738
+ new_el = Backend.create_element(source.name, @nokogiri_doc)
679
739
  source.attribute_nodes.each { |a| new_el[a.name] = a.value }
680
740
  new_el
681
741
  elsif source.text?
682
- Nokogiri::XML::Text.new(source.content, @nokogiri_doc)
683
- elsif source.is_a?(Nokogiri::XML::Comment)
684
- Nokogiri::XML::Comment.new(@nokogiri_doc, source.content)
742
+ Backend.create_text(source.content, @nokogiri_doc)
743
+ elsif source.is_a?(Backend.comment_class)
744
+ Backend.create_comment(source.content, @nokogiri_doc)
685
745
  else
686
746
  # Fallback: serialize + reparse via fragment for unusual types.
687
747
  fragment = Parser.fragment(source.to_html, owner_doc: @nokogiri_doc)
688
- fragment.children.first || Nokogiri::XML::Text.new("", @nokogiri_doc)
748
+ fragment.children.first || Backend.create_text("", @nokogiri_doc)
689
749
  end
690
750
 
691
751
  if deep && source.respond_to?(:children)
@@ -709,12 +769,12 @@ module Dommy
709
769
 
710
770
  title = head.at_css("title")
711
771
  unless title
712
- title = Nokogiri::XML::Node.new("title", @nokogiri_doc)
772
+ title = Backend.create_element("title", @nokogiri_doc)
713
773
  head.add_child(title)
714
774
  end
715
775
 
716
776
  title.children.each(&:unlink)
717
- title.add_child(Nokogiri::XML::Text.new(value, @nokogiri_doc))
777
+ title.add_child(Backend.create_text(value, @nokogiri_doc))
718
778
  end
719
779
 
720
780
  end
@@ -756,6 +816,12 @@ module Dommy
756
816
  end
757
817
  end
758
818
 
819
+ # Methods routed through __js_call__ (keep in sync with its when-arms).
820
+ JS_METHOD_NAMES = %w[skipTransition].freeze
821
+ def __js_method_names__
822
+ JS_METHOD_NAMES
823
+ end
824
+
759
825
  def __js_call__(method, _args)
760
826
  case method
761
827
  when "skipTransition"
@@ -44,12 +44,13 @@ module Dommy
44
44
  private
45
45
 
46
46
  def parse_html(str)
47
- nokogiri_doc = Nokogiri::HTML5(str.empty? ? "<html><body></body></html>" : str, max_errors: 0)
47
+ nokogiri_doc = Backend.parse(str.empty? ? "<html><body></body></html>" : str)
48
48
  Document.new(nil, nokogiri_doc: nokogiri_doc)
49
49
  end
50
50
 
51
51
  def parse_xml(str)
52
- nokogiri_doc = Nokogiri::XML(str)
52
+ # Backends are HTML-only; parse XML input as HTML for now.
53
+ nokogiri_doc = Backend.parse(str.empty? ? "<html><body></body></html>" : str)
53
54
  Document.new(nil, nokogiri_doc: nokogiri_doc)
54
55
  end
55
56
  end
@@ -63,8 +64,8 @@ module Dommy
63
64
 
64
65
  if node.respond_to?(:outer_html)
65
66
  node.outer_html
66
- elsif node.respond_to?(:__node__)
67
- node.__node__.to_xml
67
+ elsif node.respond_to?(:__dommy_backend_node__)
68
+ node.__dommy_backend_node__.to_xml
68
69
  elsif node.respond_to?(:to_xml)
69
70
  node.to_xml
70
71
  else