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.
- checksums.yaml +4 -4
- data/README.md +31 -13
- data/lib/dommy/animation.rb +288 -0
- data/lib/dommy/attr.rb +23 -11
- data/lib/dommy/backend/nokogiri_adapter.rb +51 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +80 -0
- data/lib/dommy/backend.rb +129 -0
- data/lib/dommy/blob.rb +2 -2
- data/lib/dommy/compression_streams.rb +147 -0
- data/lib/dommy/cookie_store.rb +128 -0
- data/lib/dommy/crypto.rb +396 -0
- data/lib/dommy/css.rb +7 -7
- data/lib/dommy/custom_elements.rb +6 -6
- data/lib/dommy/document.rb +190 -32
- data/lib/dommy/dom_parser.rb +5 -4
- data/lib/dommy/element.rb +356 -53
- data/lib/dommy/event.rb +431 -25
- data/lib/dommy/event_source.rb +131 -0
- data/lib/dommy/fetch.rb +76 -6
- data/lib/dommy/file_reader.rb +176 -0
- data/lib/dommy/form_data.rb +1 -3
- data/lib/dommy/history.rb +82 -0
- data/lib/dommy/html_collection.rb +4 -4
- data/lib/dommy/html_elements.rb +130 -67
- data/lib/dommy/internal/cookie_jar.rb +2 -0
- data/lib/dommy/internal/css_pseudo_handlers.rb +28 -0
- data/lib/dommy/internal/dom_matching.rb +4 -4
- data/lib/dommy/internal/idna.rb +443 -0
- data/lib/dommy/internal/idna_data.rb +10379 -0
- data/lib/dommy/internal/ipv4_parser.rb +78 -0
- data/lib/dommy/internal/node_traversal.rb +1 -1
- data/lib/dommy/internal/node_wrapper_cache.rb +23 -12
- data/lib/dommy/internal/observable_callback.rb +25 -0
- data/lib/dommy/internal/punycode.rb +202 -0
- data/lib/dommy/internal/range_text_serializer.rb +72 -0
- data/lib/dommy/internal/reflected_attributes.rb +45 -0
- data/lib/dommy/internal/template_content_registry.rb +6 -6
- data/lib/dommy/intersection_observer.rb +82 -0
- data/lib/dommy/{router.rb → location.rb} +8 -142
- data/lib/dommy/media_query_list.rb +118 -0
- data/lib/dommy/message_channel.rb +249 -0
- data/lib/dommy/{observer.rb → mutation_observer.rb} +21 -11
- data/lib/dommy/navigator.rb +365 -5
- data/lib/dommy/node.rb +12 -0
- data/lib/dommy/notification.rb +89 -0
- data/lib/dommy/parser.rb +13 -13
- data/lib/dommy/performance.rb +146 -0
- data/lib/dommy/performance_observer.rb +55 -0
- data/lib/dommy/range.rb +597 -0
- data/lib/dommy/resize_observer.rb +53 -0
- data/lib/dommy/shadow_root.rb +10 -8
- data/lib/dommy/streams.rb +386 -0
- data/lib/dommy/svg_elements.rb +3863 -0
- data/lib/dommy/text_codec.rb +175 -0
- data/lib/dommy/tree_walker.rb +21 -21
- data/lib/dommy/url.rb +274 -29
- data/lib/dommy/url_pattern.rb +144 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +209 -0
- data/lib/dommy/window.rb +369 -0
- data/lib/dommy/worker.rb +143 -0
- data/lib/dommy/xml_http_request.rb +438 -0
- data/lib/dommy.rb +43 -5
- metadata +44 -29
- 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
|
|
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.
|
|
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
|