dommy 0.7.0 → 0.8.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dommy/animation.rb +9 -1
  3. data/lib/dommy/attr.rb +192 -39
  4. data/lib/dommy/backend/nokogiri_adapter.rb +76 -0
  5. data/lib/dommy/backend/nokolexbor_adapter.rb +37 -0
  6. data/lib/dommy/backend.rb +46 -0
  7. data/lib/dommy/blob.rb +28 -9
  8. data/lib/dommy/bridge/constructor_registry.rb +28 -0
  9. data/lib/dommy/bridge/methods.rb +57 -0
  10. data/lib/dommy/bridge.rb +97 -0
  11. data/lib/dommy/callable_invoker.rb +36 -0
  12. data/lib/dommy/cookie_store.rb +3 -1
  13. data/lib/dommy/crypto.rb +7 -1
  14. data/lib/dommy/css.rb +46 -0
  15. data/lib/dommy/custom_elements.rb +27 -3
  16. data/lib/dommy/data_transfer.rb +4 -0
  17. data/lib/dommy/document.rb +615 -48
  18. data/lib/dommy/dom_parser.rb +28 -15
  19. data/lib/dommy/element.rb +999 -471
  20. data/lib/dommy/event.rb +260 -96
  21. data/lib/dommy/event_source.rb +6 -2
  22. data/lib/dommy/fetch.rb +505 -43
  23. data/lib/dommy/file_reader.rb +11 -3
  24. data/lib/dommy/form_data.rb +2 -0
  25. data/lib/dommy/history.rb +43 -8
  26. data/lib/dommy/html_collection.rb +55 -2
  27. data/lib/dommy/html_elements.rb +102 -1519
  28. data/lib/dommy/internal/css_pseudo_handlers.rb +109 -0
  29. data/lib/dommy/internal/global_functions.rb +26 -0
  30. data/lib/dommy/internal/idna.rb +16 -7
  31. data/lib/dommy/internal/ipv4_parser.rb +22 -7
  32. data/lib/dommy/internal/mutation_coordinator.rb +11 -2
  33. data/lib/dommy/internal/namespaces.rb +70 -0
  34. data/lib/dommy/internal/node_equality.rb +86 -0
  35. data/lib/dommy/internal/node_wrapper_cache.rb +62 -27
  36. data/lib/dommy/internal/observable_callback.rb +1 -5
  37. data/lib/dommy/internal/parent_node.rb +126 -0
  38. data/lib/dommy/internal/reflected_attributes.rb +103 -13
  39. data/lib/dommy/internal/selector_parser.rb +664 -0
  40. data/lib/dommy/internal/url_parser.rb +677 -0
  41. data/lib/dommy/intersection_observer.rb +2 -0
  42. data/lib/dommy/location.rb +2 -0
  43. data/lib/dommy/media_query_list.rb +7 -1
  44. data/lib/dommy/message_channel.rb +32 -2
  45. data/lib/dommy/mutation_observer.rb +55 -12
  46. data/lib/dommy/navigator.rb +26 -12
  47. data/lib/dommy/node.rb +158 -28
  48. data/lib/dommy/notification.rb +3 -1
  49. data/lib/dommy/performance.rb +4 -0
  50. data/lib/dommy/performance_observer.rb +2 -0
  51. data/lib/dommy/promise.rb +14 -14
  52. data/lib/dommy/range.rb +74 -5
  53. data/lib/dommy/resize_observer.rb +2 -0
  54. data/lib/dommy/scheduler.rb +34 -13
  55. data/lib/dommy/shadow_root.rb +23 -54
  56. data/lib/dommy/storage.rb +2 -0
  57. data/lib/dommy/streams.rb +18 -27
  58. data/lib/dommy/svg_elements.rb +204 -3606
  59. data/lib/dommy/text_codec.rb +174 -21
  60. data/lib/dommy/tree_walker.rb +255 -66
  61. data/lib/dommy/url.rb +287 -449
  62. data/lib/dommy/url_pattern.rb +2 -0
  63. data/lib/dommy/version.rb +1 -1
  64. data/lib/dommy/web_socket.rb +37 -7
  65. data/lib/dommy/window.rb +202 -213
  66. data/lib/dommy/worker.rb +7 -7
  67. data/lib/dommy/xml_http_request.rb +15 -5
  68. data/lib/dommy.rb +7 -0
  69. metadata +12 -3
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Internal
5
+ # Shared ParentNode tree-mutation surface for Element, Fragment, and
6
+ # ShadowRoot. Includers must expose `@__node__` (the backing Nokogiri
7
+ # node) and `@document` (the owning Dommy::Document).
8
+ #
9
+ # All child-list mutations funnel through `notify_child_list`, which
10
+ # forwards to MutationCoordinator#notify_child_list_mutation. That
11
+ # coordinator already no-ops on empty added/removed sets and on an
12
+ # unwrappable target, so callers may invoke it unconditionally.
13
+ module ParentNode
14
+ # `appendChild(child)` — detach the node(s) from any current parent
15
+ # and append to the end of this node's child list.
16
+ def append_child(child)
17
+ check_insertion!(child)
18
+ nodes = detach_dom_nodes(child)
19
+ nodes.each { |n| @__node__.add_child(n) }
20
+ notify_child_list(added: nodes)
21
+ child
22
+ end
23
+
24
+ # ParentNode#append — mixed Node/String args appended in order.
25
+ def append(*args)
26
+ nodes = args.flat_map { |arg| detach_dom_nodes(arg) }
27
+ nodes.each { |n| @__node__.add_child(n) }
28
+ notify_child_list(added: nodes)
29
+ nil
30
+ end
31
+
32
+ # ParentNode#prepend — insert before the current first child.
33
+ def prepend(*args)
34
+ nodes = args.flat_map { |arg| detach_dom_nodes(arg) }
35
+ anchor = @__node__.children.first
36
+ if anchor
37
+ nodes.reverse_each { |n| anchor.add_previous_sibling(n) }
38
+ else
39
+ nodes.each { |n| @__node__.add_child(n) }
40
+ end
41
+ notify_child_list(added: nodes)
42
+ nil
43
+ end
44
+
45
+ # ParentNode#replaceChildren — remove all existing children, then
46
+ # append the new set. One mutation record carries both sides.
47
+ def replace_children(*args)
48
+ removed = @__node__.children.to_a
49
+ removed.each(&:unlink)
50
+ nodes = args.flat_map { |arg| detach_dom_nodes(arg) }
51
+ nodes.each { |n| @__node__.add_child(n) }
52
+ notify_child_list(added: nodes, removed: removed)
53
+ nil
54
+ end
55
+
56
+ private
57
+
58
+ # Centralized MutationObserver childList notification. Defaults the
59
+ # target to this node; beforebegin/afterend/replaceWith/outerHTML
60
+ # callers pass the parent explicitly. The coordinator filters out
61
+ # empty added/removed sets, so this is always safe to call.
62
+ def notify_child_list(added: [], removed: [], target: @__node__)
63
+ @document.notify_child_list_mutation(
64
+ target_node: target,
65
+ added_nodes: added,
66
+ removed_nodes: removed
67
+ )
68
+ end
69
+
70
+ # Coerce an append/prepend/replaceChildren argument into raw Nokogiri
71
+ # node(s), detached from any current parent:
72
+ # - Element / TextNode / CommentNode → its backing node (unlinked)
73
+ # - Fragment → its extracted children
74
+ # - String → a fresh text node
75
+ # - anything else with a backing node → that node (unlinked)
76
+ #
77
+ # The class constants resolve at call time, so the mixin only needs to
78
+ # be defined before the including class bodies run.
79
+ def detach_dom_nodes(value)
80
+ case value
81
+ when Element, TextNode, CommentNode
82
+ [detach_with_notify(value.__dommy_backend_node__)]
83
+ when Fragment
84
+ value.extract_children
85
+ when String
86
+ [@document.create_text_node(value).__dommy_backend_node__]
87
+ else
88
+ node = value.respond_to?(:__dommy_backend_node__) ? value.__dommy_backend_node__ : nil
89
+ return [] unless node
90
+
91
+ [detach_with_notify(node)]
92
+ end
93
+ end
94
+
95
+ # Detach a node from its current parent, queuing a childList removal
96
+ # record on that old parent first (WHATWG "remove" runs before the
97
+ # subsequent insert, so moving a node yields a removal record + an addition
98
+ # record). Returns the raw node, ready to be re-linked.
99
+ def detach_with_notify(node)
100
+ old_parent = node.parent
101
+ return node unless old_parent
102
+
103
+ # Capture the position (as wrapped nodes — the coordinator records
104
+ # explicit siblings verbatim) before unlinking.
105
+ prev_sib = node.previous_sibling && @document.wrap_node(node.previous_sibling)
106
+ next_sib = node.next_sibling && @document.wrap_node(node.next_sibling)
107
+ node.unlink
108
+ @document.notify_child_list_mutation(
109
+ target_node: old_parent,
110
+ added_nodes: [],
111
+ removed_nodes: [node],
112
+ previous_sibling: prev_sib,
113
+ next_sibling: next_sib
114
+ )
115
+ node
116
+ end
117
+
118
+ # Hierarchy guard hook. Default no-op (Fragment / ShadowRoot stay
119
+ # permissive, matching current behavior). Element overrides this to
120
+ # call its `check_hierarchy!`.
121
+ def check_insertion!(_child)
122
+ nil
123
+ end
124
+ end
125
+ end
126
+ end
@@ -2,23 +2,113 @@
2
2
 
3
3
  module Dommy
4
4
  module Internal
5
- # IDL-attribute reflection helpers for `HTMLElement` and `SVGElement`
6
- # subclasses. Each helper delegates to the host element's standard
7
- # attribute API (`get_attribute` / `set_attribute` / `has_attribute?`
8
- # / `remove_attribute`), so case-sensitivity is naturally inherited
9
- # from the host element's namespace — HTML lowercases, SVG keeps
10
- # the spec name (`viewBox`).
5
+ # IDL-attribute reflection for `HTMLElement` and `SVGElement` subclasses.
11
6
  #
12
- # Two reflection styles:
7
+ # Two layers:
13
8
  #
14
- # - **String**: the property mirrors the attribute value verbatim.
15
- # Missing `""` (not nil), so subclasses don't need defensive
16
- # `.to_s`.
9
+ # 1. **Instance helpers** (`reflected_string` / `set_reflected_string` /
10
+ # `reflected_boolean` / `set_reflected_boolean`) delegate to the host
11
+ # element's standard attribute API (`get_attribute` / `set_attribute` /
12
+ # `has_attribute?` / `remove_attribute`), so case-sensitivity is
13
+ # inherited from the host's namespace — HTML lowercases, SVG keeps the
14
+ # spec name (`viewBox`).
17
15
  #
18
- # - **Boolean**: the property is true iff the attribute is present
19
- # (value ignored). Setting to true writes an empty string; setting
20
- # to false removes the attribute.
16
+ # - **String**: property mirrors the attribute value. Missing → `""`.
17
+ # - **Boolean**: property is true iff the attribute is present. Setting
18
+ # true writes `""`; setting false removes the attribute.
19
+ #
20
+ # 2. **A class-level DSL** (`reflect_string` / `reflect_boolean`) that
21
+ # declares reflected attributes once and generates BOTH the snake_case
22
+ # getter/setter pair AND a `js_key => ruby_name` registry entry. A shared
23
+ # `__js_get__` / `__js_set__` consults that registry, so bridge property
24
+ # access needs no hand-written `case` arm. This keeps the Ruby accessor,
25
+ # the JS getter, and the JS setter from drifting apart (the same class of
26
+ # bug the `js_methods` macro prevents for `__js_call__`).
27
+ #
28
+ # reflect_string :cx, :cy, :r
29
+ # reflect_string view_box: "viewBox", class_name: { attr: "class" }
30
+ # reflect_boolean :disabled, :required
31
+ #
32
+ # Identifier defaults (override via a String or Hash value):
33
+ # - js_key (camelCase IDL name) = camelize(ruby_name)
34
+ # - attr (content attribute) = camelize(ruby_name)
35
+ # - String value overrides attr only: text_anchor: "text-anchor"
36
+ # - Hash value overrides either: tabindex: { js: "tabIndex" }
21
37
  module ReflectedAttributes
38
+ def self.included(base)
39
+ base.extend(ClassMethods)
40
+ end
41
+
42
+ module ClassMethods
43
+ def reflect_string(*names, **mapped)
44
+ _reflect(:string, names, mapped)
45
+ end
46
+
47
+ def reflect_boolean(*names, **mapped)
48
+ _reflect(:boolean, names, mapped)
49
+ end
50
+
51
+ # Merged `js_key => ruby_name` map across the class ancestry (memoized).
52
+ # Recomputed lazily; `_reflect` invalidates the cache when called.
53
+ def reflected_property_map
54
+ @__reflected_map__ ||= begin
55
+ inherited = superclass.respond_to?(:reflected_property_map) ? superclass.reflected_property_map : {}
56
+ inherited.merge(@__reflected_props__ || {})
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def _reflect(type, names, mapped)
63
+ @__reflected_props__ ||= {}
64
+ @__reflected_map__ = nil # invalidate memoized merge
65
+
66
+ getter = type == :boolean ? :reflected_boolean : :reflected_string
67
+ setter = type == :boolean ? :set_reflected_boolean : :set_reflected_string
68
+
69
+ (names.map { |n| [n, nil] } + mapped.to_a).each do |ruby_name, override|
70
+ attr, js = _resolve_identifiers(ruby_name, override)
71
+ define_method(ruby_name) { __send__(getter, attr) }
72
+ define_method(:"#{ruby_name}=") { |value| __send__(setter, attr, value) }
73
+ @__reflected_props__[js] = ruby_name
74
+ end
75
+ end
76
+
77
+ def _resolve_identifiers(ruby_name, override)
78
+ default = _camelize(ruby_name)
79
+ case override
80
+ when nil
81
+ [default, default]
82
+ when String
83
+ [override, default]
84
+ when Hash
85
+ [override[:attr] || default, override[:js] || default]
86
+ else
87
+ raise ArgumentError, "reflect_*: unsupported mapping for #{ruby_name.inspect}: #{override.inspect}"
88
+ end
89
+ end
90
+
91
+ def _camelize(name)
92
+ name.to_s.gsub(/_([a-z0-9])/) { ::Regexp.last_match(1).upcase }
93
+ end
94
+ end
95
+
96
+ # Bridge property read: route reflected keys to their accessor (which a
97
+ # subclass may have overridden with coercion), else up the super chain.
98
+ def __js_get__(key)
99
+ prop = self.class.reflected_property_map[key]
100
+ return __send__(prop) if prop
101
+
102
+ super
103
+ end
104
+
105
+ def __js_set__(key, value)
106
+ prop = self.class.reflected_property_map[key]
107
+ return __send__(:"#{prop}=", value) if prop
108
+
109
+ super
110
+ end
111
+
22
112
  private
23
113
 
24
114
  def reflected_string(name)