dommy 0.5.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -13
  3. data/lib/dommy/animation.rb +288 -0
  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 +147 -0
  10. data/lib/dommy/cookie_store.rb +128 -0
  11. data/lib/dommy/crypto.rb +396 -0
  12. data/lib/dommy/css.rb +7 -7
  13. data/lib/dommy/custom_elements.rb +6 -6
  14. data/lib/dommy/document.rb +190 -32
  15. data/lib/dommy/dom_parser.rb +5 -4
  16. data/lib/dommy/element.rb +356 -53
  17. data/lib/dommy/event.rb +431 -25
  18. data/lib/dommy/event_source.rb +131 -0
  19. data/lib/dommy/fetch.rb +76 -6
  20. data/lib/dommy/file_reader.rb +176 -0
  21. data/lib/dommy/form_data.rb +1 -3
  22. data/lib/dommy/history.rb +82 -0
  23. data/lib/dommy/html_collection.rb +4 -4
  24. data/lib/dommy/html_elements.rb +130 -67
  25. data/lib/dommy/internal/cookie_jar.rb +2 -0
  26. data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
  27. data/lib/dommy/internal/dom_matching.rb +4 -4
  28. data/lib/dommy/internal/idna.rb +443 -0
  29. data/lib/dommy/internal/idna_data.rb +10379 -0
  30. data/lib/dommy/internal/ipv4_parser.rb +78 -0
  31. data/lib/dommy/internal/node_traversal.rb +1 -1
  32. data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
  33. data/lib/dommy/internal/observable_callback.rb +25 -0
  34. data/lib/dommy/internal/punycode.rb +202 -0
  35. data/lib/dommy/internal/range_text_serializer.rb +72 -0
  36. data/lib/dommy/internal/reflected_attributes.rb +45 -0
  37. data/lib/dommy/internal/template_content_registry.rb +6 -6
  38. data/lib/dommy/intersection_observer.rb +82 -0
  39. data/lib/dommy/{router.rb → location.rb} +8 -142
  40. data/lib/dommy/media_query_list.rb +118 -0
  41. data/lib/dommy/message_channel.rb +249 -0
  42. data/lib/dommy/{observer.rb → mutation_observer.rb} +21 -11
  43. data/lib/dommy/navigator.rb +365 -5
  44. data/lib/dommy/node.rb +12 -0
  45. data/lib/dommy/notification.rb +89 -0
  46. data/lib/dommy/parser.rb +13 -13
  47. data/lib/dommy/performance.rb +146 -0
  48. data/lib/dommy/performance_observer.rb +55 -0
  49. data/lib/dommy/range.rb +597 -0
  50. data/lib/dommy/resize_observer.rb +53 -0
  51. data/lib/dommy/shadow_root.rb +10 -8
  52. data/lib/dommy/streams.rb +386 -0
  53. data/lib/dommy/svg_elements.rb +3863 -0
  54. data/lib/dommy/text_codec.rb +175 -0
  55. data/lib/dommy/tree_walker.rb +21 -21
  56. data/lib/dommy/url.rb +274 -29
  57. data/lib/dommy/url_pattern.rb +144 -0
  58. data/lib/dommy/version.rb +1 -1
  59. data/lib/dommy/web_socket.rb +209 -0
  60. data/lib/dommy/window.rb +369 -0
  61. data/lib/dommy/worker.rb +143 -0
  62. data/lib/dommy/xml_http_request.rb +438 -0
  63. data/lib/dommy.rb +43 -5
  64. metadata +44 -29
  65. data/lib/dommy/world.rb +0 -209
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ # `Dommy::Backend` — pluggable HTML parser abstraction. Lets Dommy
5
+ # work with either Nokogiri (mature, full namespace support) or
6
+ # Nokolexbor (faster, HTML5-only). Internally, all DOM library
7
+ # code goes through this facade rather than referencing the parser
8
+ # directly.
9
+ #
10
+ # Defaults to Nokogiri if available, else Nokolexbor.
11
+ #
12
+ # Switching backends:
13
+ #
14
+ # require "dommy"
15
+ # Dommy::Backend.use(:nokolexbor)
16
+ #
17
+ # Or set directly:
18
+ #
19
+ # Dommy::Backend.current = Dommy::Backend::Nokolexbor
20
+ #
21
+ # All adapters must implement the same interface — see
22
+ # `Backend::Nokogiri` for the canonical reference.
23
+ module Backend
24
+ class BackendNotAvailable < StandardError
25
+ end
26
+
27
+ class << self
28
+ def current
29
+ @current ||= detect_default
30
+ end
31
+
32
+ attr_writer :current
33
+
34
+ def use(name)
35
+ @current = case name.to_sym
36
+ when :nokogiri
37
+ require_relative "backend/nokogiri_adapter"
38
+ Nokogiri
39
+ when :nokolexbor
40
+ require_relative "backend/nokolexbor_adapter"
41
+ Nokolexbor
42
+ else
43
+ raise ArgumentError, "Unknown backend: #{name.inspect}. Use :nokogiri or :nokolexbor."
44
+ end
45
+ end
46
+
47
+ # Delegate calls so internal code can use `Backend.parse(...)`.
48
+ def parse(html)
49
+ current.parse(html)
50
+ end
51
+
52
+ def fragment(html, owner_doc:)
53
+ current.fragment(html, owner_doc: owner_doc)
54
+ end
55
+
56
+ def create_element(name, doc)
57
+ current.create_element(name, doc)
58
+ end
59
+
60
+ def create_text(content, doc)
61
+ current.create_text(content, doc)
62
+ end
63
+
64
+ def create_comment(content, doc)
65
+ current.create_comment(content, doc)
66
+ end
67
+
68
+ def namespace_of(node)
69
+ current.namespace_of(node)
70
+ end
71
+
72
+ def add_namespace_definition(node, prefix, href)
73
+ current.add_namespace_definition(node, prefix, href)
74
+ end
75
+
76
+ # Type constants — proxy through to the current backend so
77
+ # `node.is_a?(Backend::Element)` resolves dynamically.
78
+ def element_class
79
+ current::Element
80
+ end
81
+
82
+ def document_class
83
+ current::Document
84
+ end
85
+
86
+ def text_class
87
+ current::Text
88
+ end
89
+
90
+ def comment_class
91
+ current::Comment
92
+ end
93
+
94
+ def document_fragment_class
95
+ current::DocumentFragment
96
+ end
97
+
98
+ def node_class
99
+ current::Node
100
+ end
101
+
102
+ private
103
+
104
+ def detect_default
105
+ try_nokogiri ||
106
+ try_nokolexbor ||
107
+ raise(BackendNotAvailable, "Dommy requires either 'nokogiri' or 'nokolexbor' gem to be installed.")
108
+ end
109
+
110
+ def try_nokogiri
111
+ require "nokogiri"
112
+
113
+ require_relative "backend/nokogiri_adapter"
114
+ Nokogiri
115
+ rescue LoadError
116
+ nil
117
+ end
118
+
119
+ def try_nokolexbor
120
+ require "nokolexbor"
121
+
122
+ require_relative "backend/nokolexbor_adapter"
123
+ Nokolexbor
124
+ rescue LoadError
125
+ nil
126
+ end
127
+ end
128
+ end
129
+ end
data/lib/dommy/blob.rb CHANGED
@@ -48,7 +48,7 @@ module Dommy
48
48
 
49
49
  # Raw binary bytes (Ruby ASCII-8BIT string). Used by FormData /
50
50
  # fetch when serializing multipart bodies.
51
- def __bytes__
51
+ def __dommy_bytes__
52
52
  @data
53
53
  end
54
54
 
@@ -79,7 +79,7 @@ module Dommy
79
79
  parts.each do |part|
80
80
  case part
81
81
  when Blob
82
- buf << part.__bytes__
82
+ buf << part.__dommy_bytes__
83
83
  when String
84
84
  buf << part.dup.force_encoding(Encoding::ASCII_8BIT)
85
85
  when Array
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "stringio"
5
+
6
+ module Dommy
7
+ # `CompressionStream` / `DecompressionStream` — wrap `TransformStream`
8
+ # over Ruby's `Zlib` to gzip/deflate/raw-deflate byte chunks. Each
9
+ # `write(chunk)` accumulates into an internal buffer; `close()`
10
+ # finalizes and emits the compressed/decompressed bytes downstream.
11
+ #
12
+ # Spec: https://wicg.github.io/compression/
13
+ class CompressionStream
14
+ SUPPORTED = %w[gzip deflate deflate-raw].freeze
15
+
16
+ attr_reader :readable, :writable
17
+
18
+ def initialize(window, format)
19
+ raise ArgumentError, "unsupported format #{format.inspect}" unless SUPPORTED.include?(format.to_s)
20
+
21
+ @buffer = +""
22
+ compressor = build_compressor(format.to_s)
23
+
24
+ @readable = ReadableStream.new(window)
25
+ controller = TransformStreamDefaultController.new(@readable)
26
+
27
+ @writable = WritableStream.new(
28
+ window,
29
+ {
30
+ "write" => proc { |chunk| @buffer << coerce(chunk) },
31
+ "close" => proc do
32
+ compressed = compressor.call(@buffer)
33
+ controller.enqueue(compressed)
34
+ @readable.__internal_close__
35
+ end,
36
+ "abort" => proc { |r| @readable.__internal_error__(r) }
37
+ }
38
+ )
39
+ end
40
+
41
+ def __js_get__(key)
42
+ case key
43
+ when "readable"
44
+ @readable
45
+ when "writable"
46
+ @writable
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def coerce(chunk)
53
+ chunk.is_a?(Array) ? chunk.pack("C*") : chunk.to_s
54
+ end
55
+
56
+ def build_compressor(format)
57
+ case format
58
+ when "gzip"
59
+ proc do |data|
60
+ io = StringIO.new
61
+ gz = Zlib::GzipWriter.new(io)
62
+ gz.write(data)
63
+ gz.close
64
+ io.string.bytes
65
+ end
66
+
67
+ when "deflate"
68
+ proc { |data| Zlib::Deflate.deflate(data).bytes }
69
+ when "deflate-raw"
70
+ proc do |data|
71
+ z = Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION, -Zlib::MAX_WBITS)
72
+ out = z.deflate(data, Zlib::FINISH)
73
+ z.close
74
+ out.bytes
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ class DecompressionStream
81
+ SUPPORTED = %w[gzip deflate deflate-raw].freeze
82
+
83
+ attr_reader :readable, :writable
84
+
85
+ def initialize(window, format)
86
+ raise ArgumentError, "unsupported format #{format.inspect}" unless SUPPORTED.include?(format.to_s)
87
+
88
+ @buffer = +""
89
+ decompressor = build_decompressor(format.to_s)
90
+
91
+ @readable = ReadableStream.new(window)
92
+ controller = TransformStreamDefaultController.new(@readable)
93
+
94
+ @writable = WritableStream.new(
95
+ window,
96
+ {
97
+ "write" => proc { |chunk| @buffer << coerce(chunk) },
98
+ "close" => proc do
99
+ plain = decompressor.call(@buffer)
100
+ controller.enqueue(plain)
101
+ @readable.__internal_close__
102
+ end,
103
+ "abort" => proc { |r| @readable.__internal_error__(r) }
104
+ }
105
+ )
106
+ end
107
+
108
+ def __js_get__(key)
109
+ case key
110
+ when "readable"
111
+ @readable
112
+ when "writable"
113
+ @writable
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def coerce(chunk)
120
+ chunk.is_a?(Array) ? chunk.pack("C*") : chunk.to_s
121
+ end
122
+
123
+ def build_decompressor(format)
124
+ case format
125
+ when "gzip"
126
+ proc do |data|
127
+ io = StringIO.new(data)
128
+ gz = Zlib::GzipReader.new(io)
129
+ out = gz.read
130
+ gz.close
131
+ out.bytes
132
+ end
133
+
134
+ when "deflate"
135
+ proc { |data| Zlib::Inflate.inflate(data).bytes }
136
+ when "deflate-raw"
137
+ proc do |data|
138
+ z = Zlib::Inflate.new(-Zlib::MAX_WBITS)
139
+ out = z.inflate(data)
140
+ z.finish
141
+ z.close
142
+ out.bytes
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ # `cookieStore` — the asynchronous Cookie Store API. Reads and
5
+ # writes go through the existing `Document#cookie_jar`, so values
6
+ # set via `document.cookie = ...` round-trip via `cookieStore.get`.
7
+ #
8
+ # Spec: https://wicg.github.io/cookie-store/
9
+ class CookieStore
10
+ include EventTarget
11
+
12
+ def initialize(window)
13
+ @window = window
14
+ end
15
+
16
+ def get(name_or_options)
17
+ name = name_or_options.is_a?(Hash) ? (name_or_options["name"] || name_or_options[:name]) : name_or_options
18
+ raw = cookie_jar.cookies[name.to_s]
19
+ raw ? PromiseValue.resolve(@window, build_record(name.to_s, raw)) : PromiseValue.resolve(@window, nil)
20
+ end
21
+
22
+ def get_all(name = nil)
23
+ records = cookie_jar.cookies.map { |k, v| build_record(k, v) }
24
+ records = records.select { |r| r["name"] == name.to_s } if name
25
+ PromiseValue.resolve(@window, records)
26
+ end
27
+
28
+ alias getAll get_all
29
+
30
+ def set(name_or_options, value = nil)
31
+ if name_or_options.is_a?(Hash)
32
+ opts = name_or_options.transform_keys(&:to_s)
33
+ name = opts["name"]
34
+ value = opts["value"]
35
+ else
36
+ name = name_or_options
37
+ end
38
+
39
+ cookie_jar.set_cookie("#{name}=#{value}")
40
+ dispatch_event(
41
+ CookieChangeEvent.new(
42
+ "change",
43
+ "changed" => [build_record(name.to_s, value.to_s)],
44
+ "deleted" => []
45
+ )
46
+ )
47
+ PromiseValue.resolve(@window, nil)
48
+ end
49
+
50
+ def delete(name_or_options)
51
+ name = name_or_options.is_a?(Hash) ? (name_or_options["name"] || name_or_options[:name]) : name_or_options
52
+ cookie_jar.cookies.delete(name.to_s)
53
+ dispatch_event(
54
+ CookieChangeEvent.new(
55
+ "change",
56
+ "changed" => [],
57
+ "deleted" => [build_record(name.to_s, "")]
58
+ )
59
+ )
60
+ PromiseValue.resolve(@window, nil)
61
+ end
62
+
63
+ def __js_call__(method, args)
64
+ case method
65
+ when "get"
66
+ get(args[0])
67
+ when "getAll"
68
+ get_all(args[0])
69
+ when "set"
70
+ set(args[0], args[1])
71
+ when "delete"
72
+ delete(args[0])
73
+ when "addEventListener"
74
+ add_event_listener(args[0], args[1], args[2])
75
+ when "removeEventListener"
76
+ remove_event_listener(args[0], args[1])
77
+ when "dispatchEvent"
78
+ dispatch_event(args[0])
79
+ end
80
+ end
81
+
82
+ def __internal_event_parent__
83
+ nil
84
+ end
85
+
86
+ private
87
+
88
+ def cookie_jar
89
+ # `document.cookie_jar` is private; we reach the same backing
90
+ # store via the public `document.cookie` round-trip. Easier: use
91
+ # instance_variable_get on the document.
92
+ @window.document.instance_variable_get(:@cookie_jar)
93
+ end
94
+
95
+ def build_record(name, value)
96
+ {
97
+ "name" => name.to_s,
98
+ "value" => value.to_s,
99
+ "domain" => nil,
100
+ "path" => "/",
101
+ "expires" => nil,
102
+ "secure" => false,
103
+ "sameSite" => "strict"
104
+ }
105
+ end
106
+ end
107
+
108
+ class CookieChangeEvent < Event
109
+ def initialize(type, init = nil)
110
+ super
111
+ @changed = Array(read_init(init, "changed") || [])
112
+ @deleted = Array(read_init(init, "deleted") || [])
113
+ end
114
+
115
+ attr_reader :changed, :deleted
116
+
117
+ def __js_get__(key)
118
+ case key
119
+ when "changed"
120
+ @changed
121
+ when "deleted"
122
+ @deleted
123
+ else
124
+ super
125
+ end
126
+ end
127
+ end
128
+ end