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.
- checksums.yaml +4 -4
- data/lib/dommy/animation.rb +9 -1
- data/lib/dommy/attr.rb +192 -39
- data/lib/dommy/backend/nokogiri_adapter.rb +76 -0
- data/lib/dommy/backend/nokolexbor_adapter.rb +37 -0
- data/lib/dommy/backend.rb +46 -0
- data/lib/dommy/blob.rb +28 -9
- data/lib/dommy/bridge/constructor_registry.rb +28 -0
- data/lib/dommy/bridge/methods.rb +57 -0
- data/lib/dommy/bridge.rb +97 -0
- data/lib/dommy/callable_invoker.rb +36 -0
- data/lib/dommy/cookie_store.rb +3 -1
- data/lib/dommy/crypto.rb +7 -1
- data/lib/dommy/css.rb +46 -0
- data/lib/dommy/custom_elements.rb +27 -3
- data/lib/dommy/data_transfer.rb +4 -0
- data/lib/dommy/document.rb +615 -48
- data/lib/dommy/dom_parser.rb +28 -15
- data/lib/dommy/element.rb +999 -471
- data/lib/dommy/event.rb +260 -96
- data/lib/dommy/event_source.rb +6 -2
- data/lib/dommy/fetch.rb +505 -43
- data/lib/dommy/file_reader.rb +11 -3
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +43 -8
- data/lib/dommy/html_collection.rb +55 -2
- data/lib/dommy/html_elements.rb +102 -1519
- data/lib/dommy/internal/css_pseudo_handlers.rb +109 -0
- data/lib/dommy/internal/global_functions.rb +26 -0
- data/lib/dommy/internal/idna.rb +16 -7
- data/lib/dommy/internal/ipv4_parser.rb +22 -7
- data/lib/dommy/internal/mutation_coordinator.rb +11 -2
- data/lib/dommy/internal/namespaces.rb +70 -0
- data/lib/dommy/internal/node_equality.rb +86 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +62 -27
- data/lib/dommy/internal/observable_callback.rb +1 -5
- data/lib/dommy/internal/parent_node.rb +126 -0
- data/lib/dommy/internal/reflected_attributes.rb +103 -13
- data/lib/dommy/internal/selector_parser.rb +664 -0
- data/lib/dommy/internal/url_parser.rb +677 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +7 -1
- data/lib/dommy/message_channel.rb +32 -2
- data/lib/dommy/mutation_observer.rb +55 -12
- data/lib/dommy/navigator.rb +26 -12
- data/lib/dommy/node.rb +158 -28
- data/lib/dommy/notification.rb +3 -1
- data/lib/dommy/performance.rb +4 -0
- data/lib/dommy/performance_observer.rb +2 -0
- data/lib/dommy/promise.rb +14 -14
- data/lib/dommy/range.rb +74 -5
- data/lib/dommy/resize_observer.rb +2 -0
- data/lib/dommy/scheduler.rb +34 -13
- data/lib/dommy/shadow_root.rb +23 -54
- data/lib/dommy/storage.rb +2 -0
- data/lib/dommy/streams.rb +18 -27
- data/lib/dommy/svg_elements.rb +204 -3606
- data/lib/dommy/text_codec.rb +174 -21
- data/lib/dommy/tree_walker.rb +255 -66
- data/lib/dommy/url.rb +287 -449
- data/lib/dommy/url_pattern.rb +2 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +37 -7
- data/lib/dommy/window.rb +202 -213
- data/lib/dommy/worker.rb +7 -7
- data/lib/dommy/xml_http_request.rb +15 -5
- data/lib/dommy.rb +7 -0
- metadata +12 -3
data/lib/dommy/text_codec.rb
CHANGED
|
@@ -10,17 +10,20 @@ module Dommy
|
|
|
10
10
|
"utf-8"
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
# encode(string) →
|
|
14
|
-
#
|
|
15
|
-
#
|
|
13
|
+
# encode(string) → Uint8Array (UTF-8 bytes). Lone surrogates in the input
|
|
14
|
+
# have already been replaced with U+FFFD when the JS string crossed into Ruby
|
|
15
|
+
# (Ruby strings can't hold them), matching the spec's USVString conversion.
|
|
16
16
|
def encode(input = "")
|
|
17
|
-
input.
|
|
17
|
+
str = input.equal?(Bridge::UNDEFINED) ? "" : input.to_s
|
|
18
|
+
Bridge::Bytes.new(str.encode(Encoding::UTF_8, invalid: :replace, undef: :replace).bytes)
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def __js_get__(key)
|
|
21
22
|
key == "encoding" ? encoding : nil
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
include Bridge::Methods
|
|
26
|
+
js_methods %w[encode]
|
|
24
27
|
def __js_call__(method, args)
|
|
25
28
|
case method
|
|
26
29
|
when "encode"
|
|
@@ -34,35 +37,50 @@ module Dommy
|
|
|
34
37
|
#
|
|
35
38
|
# Spec: https://encoding.spec.whatwg.org/#textdecoder
|
|
36
39
|
class TextDecoder
|
|
37
|
-
def initialize(label = "utf-8",
|
|
40
|
+
def initialize(label = "utf-8", options = nil)
|
|
41
|
+
label = "utf-8" if label.nil? || label.equal?(Bridge::UNDEFINED)
|
|
38
42
|
@encoding = normalize_encoding(label.to_s)
|
|
43
|
+
opts = options.is_a?(Hash) ? options : {}
|
|
44
|
+
@fatal = truthy?(opts["fatal"] || opts[:fatal])
|
|
45
|
+
@ignore_bom = truthy?(opts["ignoreBOM"] || opts[:ignoreBOM])
|
|
39
46
|
end
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
attr_reader :encoding
|
|
49
|
+
|
|
50
|
+
def fatal? = @fatal
|
|
51
|
+
def ignore_bom? = @ignore_bom
|
|
44
52
|
|
|
45
|
-
# decode(bytes) → String. Accepts
|
|
46
|
-
# a binary String
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
# decode(bytes, {stream}) → String. Accepts a Bytes buffer (JS ArrayBuffer /
|
|
54
|
+
# TypedArray), an Array<Integer>, or a binary String. With `fatal: true` an
|
|
55
|
+
# invalid sequence throws a TypeError; otherwise it is replaced with U+FFFD.
|
|
56
|
+
# A leading BOM is stripped unless `ignoreBOM` was set.
|
|
57
|
+
def decode(input = nil, options = nil)
|
|
58
|
+
stream = options.is_a?(Hash) && truthy?(options["stream"] || options[:stream])
|
|
59
|
+
bytes = extract_bytes(input)
|
|
49
60
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
input.b
|
|
53
|
-
when Array
|
|
54
|
-
input.pack("C*")
|
|
61
|
+
if @encoding == "utf-8"
|
|
62
|
+
decode_utf8(bytes, stream)
|
|
55
63
|
else
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
# Non-UTF-8 (utf-16le/be, iso-8859-1): no streaming/exact-FFFD semantics,
|
|
65
|
+
# best-effort via Ruby's transcoder.
|
|
66
|
+
bytes = strip_bom(bytes) unless @ignore_bom
|
|
67
|
+
raw = bytes.force_encoding(ruby_encoding)
|
|
68
|
+
raise Bridge::TypeError, "decode failed" if @fatal && !raw.valid_encoding?
|
|
58
69
|
|
|
59
|
-
|
|
70
|
+
raw.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
71
|
+
end
|
|
60
72
|
end
|
|
61
73
|
|
|
62
74
|
def __js_get__(key)
|
|
63
|
-
key
|
|
75
|
+
case key
|
|
76
|
+
when "encoding" then @encoding
|
|
77
|
+
when "fatal" then @fatal
|
|
78
|
+
when "ignoreBOM" then @ignore_bom
|
|
79
|
+
end
|
|
64
80
|
end
|
|
65
81
|
|
|
82
|
+
include Bridge::Methods
|
|
83
|
+
js_methods %w[decode]
|
|
66
84
|
def __js_call__(method, args)
|
|
67
85
|
case method
|
|
68
86
|
when "decode"
|
|
@@ -72,6 +90,141 @@ module Dommy
|
|
|
72
90
|
|
|
73
91
|
private
|
|
74
92
|
|
|
93
|
+
def extract_bytes(input)
|
|
94
|
+
return "".b if input.nil? || input.equal?(Bridge::UNDEFINED)
|
|
95
|
+
|
|
96
|
+
case input
|
|
97
|
+
when Bridge::Bytes then input.pack_bytes
|
|
98
|
+
when String then input.b
|
|
99
|
+
when Array then input.pack("C*")
|
|
100
|
+
else input.respond_to?(:to_a) ? input.to_a.pack("C*") : input.to_s.b
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# WHATWG "UTF-8 decoder" — a stateful machine so streaming chunks, exact
|
|
105
|
+
# U+FFFD placement, and end-of-queue flushing match the spec (Ruby's
|
|
106
|
+
# transcoder replaces per-byte, which is wrong at sequence boundaries).
|
|
107
|
+
# State persists on the decoder across `{stream: true}` calls.
|
|
108
|
+
def decode_utf8(bytes, stream)
|
|
109
|
+
@u8_needed ||= 0
|
|
110
|
+
@u8_seen ||= 0
|
|
111
|
+
@u8_cp ||= 0
|
|
112
|
+
@u8_lower ||= 0x80
|
|
113
|
+
@u8_upper ||= 0xBF
|
|
114
|
+
|
|
115
|
+
out = []
|
|
116
|
+
queue = bytes.bytes
|
|
117
|
+
@u8_started = true
|
|
118
|
+
|
|
119
|
+
until queue.empty?
|
|
120
|
+
byte = queue.shift
|
|
121
|
+
if @u8_needed.zero?
|
|
122
|
+
if byte <= 0x7F
|
|
123
|
+
out << byte
|
|
124
|
+
elsif byte.between?(0xC2, 0xDF)
|
|
125
|
+
@u8_needed = 1
|
|
126
|
+
@u8_cp = byte & 0x1F
|
|
127
|
+
elsif byte.between?(0xE0, 0xEF)
|
|
128
|
+
@u8_lower = 0xA0 if byte == 0xE0
|
|
129
|
+
@u8_upper = 0x9F if byte == 0xED
|
|
130
|
+
@u8_needed = 2
|
|
131
|
+
@u8_cp = byte & 0x0F
|
|
132
|
+
elsif byte.between?(0xF0, 0xF4)
|
|
133
|
+
@u8_lower = 0x90 if byte == 0xF0
|
|
134
|
+
@u8_upper = 0x8F if byte == 0xF4
|
|
135
|
+
@u8_needed = 3
|
|
136
|
+
@u8_cp = byte & 0x07
|
|
137
|
+
else
|
|
138
|
+
out << utf8_error
|
|
139
|
+
end
|
|
140
|
+
next
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
unless byte.between?(@u8_lower, @u8_upper)
|
|
144
|
+
# Invalid continuation: emit an error and REPROCESS this byte.
|
|
145
|
+
reset_utf8_state
|
|
146
|
+
out << utf8_error
|
|
147
|
+
queue.unshift(byte)
|
|
148
|
+
next
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
@u8_lower = 0x80
|
|
152
|
+
@u8_upper = 0xBF
|
|
153
|
+
@u8_cp = (@u8_cp << 6) | (byte & 0x3F)
|
|
154
|
+
@u8_seen += 1
|
|
155
|
+
next unless @u8_seen == @u8_needed
|
|
156
|
+
|
|
157
|
+
out << @u8_cp
|
|
158
|
+
reset_utf8_state
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
unless stream
|
|
162
|
+
if @u8_needed != 0
|
|
163
|
+
reset_utf8_state
|
|
164
|
+
out << utf8_error
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Strip a leading byte-order mark at the code-point level, not the byte
|
|
169
|
+
# level: a BOM split across streaming chunks (EF BB in one decode() call,
|
|
170
|
+
# BF in the next) only surfaces as a single U+FEFF once the full sequence
|
|
171
|
+
# decodes, which a byte-prefix check would miss. The "BOM seen" flag
|
|
172
|
+
# persists across `{stream: true}` calls and is reset on flush.
|
|
173
|
+
unless @ignore_bom || @u8_bom_seen
|
|
174
|
+
if out[0] == 0xFEFF
|
|
175
|
+
out.shift
|
|
176
|
+
@u8_bom_seen = true
|
|
177
|
+
elsif !out.empty?
|
|
178
|
+
@u8_bom_seen = true
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
unless stream
|
|
183
|
+
@u8_started = false
|
|
184
|
+
@u8_bom_seen = false
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
out.pack("U*")
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# In fatal mode an error throws a TypeError; otherwise it is the U+FFFD
|
|
191
|
+
# replacement code point.
|
|
192
|
+
def utf8_error
|
|
193
|
+
raise Bridge::TypeError, "The encoded data was not valid for encoding utf-8" if @fatal
|
|
194
|
+
|
|
195
|
+
0xFFFD
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def reset_utf8_state
|
|
199
|
+
@u8_needed = 0
|
|
200
|
+
@u8_seen = 0
|
|
201
|
+
@u8_cp = 0
|
|
202
|
+
@u8_lower = 0x80
|
|
203
|
+
@u8_upper = 0xBF
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# JS ToBoolean for an option-bag value (false/nil/undefined/0/"" are falsy).
|
|
207
|
+
def truthy?(value)
|
|
208
|
+
return false if value.nil? || value == false || value == 0 || value == ""
|
|
209
|
+
return false if defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED)
|
|
210
|
+
|
|
211
|
+
true
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Remove a leading byte-order mark matching this decoder's encoding.
|
|
215
|
+
def strip_bom(bytes)
|
|
216
|
+
case @encoding
|
|
217
|
+
when "utf-8"
|
|
218
|
+
bytes.start_with?("\xEF\xBB\xBF".b) ? bytes.byteslice(3..) : bytes
|
|
219
|
+
when "utf-16le"
|
|
220
|
+
bytes.start_with?("\xFF\xFE".b) ? bytes.byteslice(2..) : bytes
|
|
221
|
+
when "utf-16be"
|
|
222
|
+
bytes.start_with?("\xFE\xFF".b) ? bytes.byteslice(2..) : bytes
|
|
223
|
+
else
|
|
224
|
+
bytes
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
75
228
|
def normalize_encoding(label)
|
|
76
229
|
l = label.downcase.strip
|
|
77
230
|
case l
|
data/lib/dommy/tree_walker.rb
CHANGED
|
@@ -21,23 +21,17 @@ module Dommy
|
|
|
21
21
|
|
|
22
22
|
# Map a wrapped Dommy node to its NodeFilter bitmask. Returns 0
|
|
23
23
|
# for unknown node types (effectively "doesn't pass any filter").
|
|
24
|
+
# whatToShow bit for a node: WHATWG defines it as `1 << (nodeType - 1)`,
|
|
25
|
+
# which covers every node type (Element, Text, CDATASection, PI, Comment,
|
|
26
|
+
# Document, DocumentType, DocumentFragment, Attribute) uniformly.
|
|
24
27
|
def self.bitmask_for(node)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
when Fragment
|
|
33
|
-
SHOW_DOCUMENT_FRAGMENT
|
|
34
|
-
when Document
|
|
35
|
-
SHOW_DOCUMENT
|
|
36
|
-
when DocumentType
|
|
37
|
-
SHOW_DOCUMENT_TYPE
|
|
38
|
-
else
|
|
39
|
-
0
|
|
40
|
-
end
|
|
28
|
+
nt =
|
|
29
|
+
if node.respond_to?(:node_type) then node.node_type
|
|
30
|
+
elsif node.respond_to?(:__js_get__) then node.__js_get__("nodeType")
|
|
31
|
+
end
|
|
32
|
+
return 0 unless nt.is_a?(Integer) && nt >= 1
|
|
33
|
+
|
|
34
|
+
1 << (nt - 1)
|
|
41
35
|
end
|
|
42
36
|
end
|
|
43
37
|
|
|
@@ -58,15 +52,40 @@ module Dommy
|
|
|
58
52
|
end
|
|
59
53
|
|
|
60
54
|
def invoke_filter(node)
|
|
61
|
-
return NodeFilter::FILTER_ACCEPT if @filter.nil?
|
|
55
|
+
return NodeFilter::FILTER_ACCEPT if @filter.nil? || (defined?(Bridge::UNDEFINED) && @filter.equal?(Bridge::UNDEFINED))
|
|
56
|
+
|
|
57
|
+
cb = filter_callback
|
|
58
|
+
# A non-null filter with no callable acceptNode is a TypeError when invoked.
|
|
59
|
+
raise Bridge::TypeError, "NodeFilter is not callable" unless cb
|
|
62
60
|
|
|
63
|
-
if
|
|
64
|
-
|
|
65
|
-
elsif @filter.respond_to?(:call)
|
|
66
|
-
@filter.call(node)
|
|
61
|
+
result = if cb.respond_to?(:__js_call__)
|
|
62
|
+
cb.__js_call__("call", [node])
|
|
67
63
|
else
|
|
68
|
-
|
|
64
|
+
cb.call(node)
|
|
69
65
|
end
|
|
66
|
+
# WebIDL coerces the filter return (an `unsigned short`): booleans and
|
|
67
|
+
# null become 0/1, everything else ToInteger.
|
|
68
|
+
return 1 if result == true
|
|
69
|
+
return 0 if result == false || result.nil?
|
|
70
|
+
|
|
71
|
+
result.to_i
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The actual filter callback: a function filter is called directly; an object
|
|
75
|
+
# filter's `acceptNode` is used (WebIDL callback interface).
|
|
76
|
+
def filter_callback
|
|
77
|
+
return @filter if callable?(@filter)
|
|
78
|
+
|
|
79
|
+
accept_node =
|
|
80
|
+
if @filter.respond_to?(:accept_node) then @filter.method(:accept_node)
|
|
81
|
+
elsif @filter.is_a?(Hash) then (@filter["acceptNode"] || @filter[:acceptNode])
|
|
82
|
+
elsif @filter.respond_to?(:__js_get__) then @filter.__js_get__("acceptNode")
|
|
83
|
+
end
|
|
84
|
+
callable?(accept_node) ? accept_node : nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def callable?(value)
|
|
88
|
+
value && (value.respond_to?(:__js_call__) || value.respond_to?(:call))
|
|
70
89
|
end
|
|
71
90
|
end
|
|
72
91
|
|
|
@@ -90,32 +109,55 @@ module Dommy
|
|
|
90
109
|
end
|
|
91
110
|
|
|
92
111
|
def next_node
|
|
93
|
-
node =
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
node = @current_node
|
|
113
|
+
result = NodeFilter::FILTER_ACCEPT
|
|
114
|
+
loop do
|
|
115
|
+
while result != NodeFilter::FILTER_REJECT && (child = first_wrapped_child(node))
|
|
116
|
+
node = child
|
|
117
|
+
result = accept(node)
|
|
118
|
+
return @current_node = node if result == NodeFilter::FILTER_ACCEPT
|
|
99
119
|
end
|
|
100
120
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
121
|
+
sibling = nil
|
|
122
|
+
temp = node
|
|
123
|
+
while temp
|
|
124
|
+
return nil if temp == @root
|
|
125
|
+
|
|
126
|
+
sibling = next_sibling_wrapped(temp)
|
|
127
|
+
if sibling
|
|
128
|
+
node = sibling
|
|
129
|
+
break
|
|
130
|
+
end
|
|
131
|
+
temp = wrapped_parent(temp)
|
|
132
|
+
end
|
|
133
|
+
return nil unless sibling
|
|
105
134
|
|
|
106
|
-
|
|
135
|
+
result = accept(node)
|
|
136
|
+
return @current_node = node if result == NodeFilter::FILTER_ACCEPT
|
|
137
|
+
end
|
|
107
138
|
end
|
|
108
139
|
|
|
109
140
|
def previous_node
|
|
110
|
-
node =
|
|
111
|
-
while node
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
node = @current_node
|
|
142
|
+
while node != @root
|
|
143
|
+
sibling = previous_sibling_wrapped(node)
|
|
144
|
+
while sibling
|
|
145
|
+
node = sibling
|
|
146
|
+
result = accept(node)
|
|
147
|
+
while result != NodeFilter::FILTER_REJECT && (child = last_wrapped_child(node))
|
|
148
|
+
node = child
|
|
149
|
+
result = accept(node)
|
|
150
|
+
end
|
|
151
|
+
return @current_node = node if result == NodeFilter::FILTER_ACCEPT
|
|
152
|
+
|
|
153
|
+
sibling = previous_sibling_wrapped(node)
|
|
116
154
|
end
|
|
117
155
|
|
|
118
|
-
|
|
156
|
+
parent = wrapped_parent(node)
|
|
157
|
+
return nil if node == @root || parent.nil?
|
|
158
|
+
|
|
159
|
+
node = parent
|
|
160
|
+
return @current_node = node if accept(node) == NodeFilter::FILTER_ACCEPT
|
|
119
161
|
end
|
|
120
162
|
|
|
121
163
|
nil
|
|
@@ -133,21 +175,19 @@ module Dommy
|
|
|
133
175
|
end
|
|
134
176
|
|
|
135
177
|
def first_child
|
|
136
|
-
|
|
137
|
-
walk_siblings(first, :next_sibling_wrapped)
|
|
178
|
+
traverse_children(:first_wrapped_child, :next_sibling_wrapped)
|
|
138
179
|
end
|
|
139
180
|
|
|
140
181
|
def last_child
|
|
141
|
-
|
|
142
|
-
walk_siblings(last, :previous_sibling_wrapped)
|
|
182
|
+
traverse_children(:last_wrapped_child, :previous_sibling_wrapped)
|
|
143
183
|
end
|
|
144
184
|
|
|
145
185
|
def next_sibling
|
|
146
|
-
|
|
186
|
+
traverse_siblings(:next_sibling_wrapped, :first_wrapped_child)
|
|
147
187
|
end
|
|
148
188
|
|
|
149
189
|
def previous_sibling
|
|
150
|
-
|
|
190
|
+
traverse_siblings(:previous_sibling_wrapped, :last_wrapped_child)
|
|
151
191
|
end
|
|
152
192
|
|
|
153
193
|
def __js_get__(key)
|
|
@@ -164,10 +204,17 @@ module Dommy
|
|
|
164
204
|
end
|
|
165
205
|
|
|
166
206
|
def __js_set__(key, value)
|
|
167
|
-
|
|
207
|
+
return Bridge::UNHANDLED unless key == "currentNode"
|
|
208
|
+
|
|
209
|
+
# currentNode is a non-null `Node`; non-Node values are a TypeError.
|
|
210
|
+
raise Bridge::TypeError, "currentNode must be a Node" unless value.is_a?(Dommy::Node)
|
|
211
|
+
|
|
212
|
+
@current_node = value
|
|
168
213
|
nil
|
|
169
214
|
end
|
|
170
215
|
|
|
216
|
+
include Bridge::Methods
|
|
217
|
+
js_methods %w[nextNode previousNode parentNode firstChild lastChild nextSibling previousSibling]
|
|
171
218
|
def __js_call__(method, _args)
|
|
172
219
|
case method
|
|
173
220
|
when "nextNode"
|
|
@@ -189,18 +236,62 @@ module Dommy
|
|
|
189
236
|
|
|
190
237
|
private
|
|
191
238
|
|
|
192
|
-
|
|
193
|
-
|
|
239
|
+
# WHATWG "traverse children": a SKIP node is transparent (descend into its
|
|
240
|
+
# children); a REJECT node is opaque (skip it and its subtree, advance to the
|
|
241
|
+
# next sibling, climbing toward currentNode as needed).
|
|
242
|
+
def traverse_children(descend, sibling_dir)
|
|
243
|
+
node = send(descend, @current_node)
|
|
194
244
|
while node
|
|
195
245
|
v = accept(node)
|
|
196
246
|
return @current_node = node if v == NodeFilter::FILTER_ACCEPT
|
|
197
247
|
|
|
198
|
-
|
|
248
|
+
if v == NodeFilter::FILTER_SKIP
|
|
249
|
+
child = send(descend, node)
|
|
250
|
+
if child
|
|
251
|
+
node = child
|
|
252
|
+
next
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
loop do
|
|
257
|
+
sib = send(sibling_dir, node)
|
|
258
|
+
if sib
|
|
259
|
+
node = sib
|
|
260
|
+
break
|
|
261
|
+
end
|
|
262
|
+
parent = wrapped_parent(node)
|
|
263
|
+
return nil if parent.nil? || parent == @root || parent == @current_node
|
|
264
|
+
|
|
265
|
+
node = parent
|
|
266
|
+
end
|
|
199
267
|
end
|
|
200
268
|
|
|
201
269
|
nil
|
|
202
270
|
end
|
|
203
271
|
|
|
272
|
+
# WHATWG "traverse siblings": walk siblings of currentNode; a SKIP sibling's
|
|
273
|
+
# children are searched (descend), a REJECT sibling's subtree is skipped.
|
|
274
|
+
def traverse_siblings(sibling_dir, child_dir)
|
|
275
|
+
node = @current_node
|
|
276
|
+
return nil if node == @root
|
|
277
|
+
|
|
278
|
+
loop do
|
|
279
|
+
sib = send(sibling_dir, node)
|
|
280
|
+
while sib
|
|
281
|
+
node = sib
|
|
282
|
+
v = accept(node)
|
|
283
|
+
return @current_node = node if v == NodeFilter::FILTER_ACCEPT
|
|
284
|
+
|
|
285
|
+
sib = send(child_dir, node)
|
|
286
|
+
sib = send(sibling_dir, node) if v == NodeFilter::FILTER_REJECT || sib.nil?
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
node = wrapped_parent(node)
|
|
290
|
+
return nil if node.nil? || node == @root
|
|
291
|
+
return nil if accept(node) == NodeFilter::FILTER_ACCEPT
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
204
295
|
def first_descendant_or_following(node)
|
|
205
296
|
child = first_wrapped_child(node)
|
|
206
297
|
return child if child
|
|
@@ -253,23 +344,46 @@ module Dommy
|
|
|
253
344
|
doc.wrap_node(parent_nk)
|
|
254
345
|
end
|
|
255
346
|
|
|
347
|
+
# The Nokogiri node backing a wrapper — an element's `__dommy_backend_node__`
|
|
348
|
+
# or, for a Document (which isn't a wrapped node), its `nokogiri_doc`. Lets a
|
|
349
|
+
# walker rooted at the document descend into its children.
|
|
350
|
+
def backend_node_of(node)
|
|
351
|
+
if node.respond_to?(:__dommy_backend_node__)
|
|
352
|
+
node.__dommy_backend_node__
|
|
353
|
+
elsif node.respond_to?(:nokogiri_doc)
|
|
354
|
+
node.nokogiri_doc
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Wrap the first Nokogiri node in `list` that has a wrapper, skipping ones
|
|
359
|
+
# that don't (e.g. the DTD, which Dommy doesn't model as a child node).
|
|
360
|
+
def first_wrappable(list, ctx)
|
|
361
|
+
list.each do |nk|
|
|
362
|
+
w = document_for(ctx).wrap_node(nk)
|
|
363
|
+
return w if w
|
|
364
|
+
end
|
|
365
|
+
nil
|
|
366
|
+
end
|
|
367
|
+
|
|
256
368
|
def first_wrapped_child(node)
|
|
257
|
-
|
|
258
|
-
|
|
369
|
+
bn = backend_node_of(node)
|
|
370
|
+
bn ? first_wrappable(bn.children, node) : nil
|
|
259
371
|
end
|
|
260
372
|
|
|
261
373
|
def last_wrapped_child(node)
|
|
262
|
-
|
|
263
|
-
|
|
374
|
+
bn = backend_node_of(node)
|
|
375
|
+
bn ? first_wrappable(bn.children.reverse, node) : nil
|
|
264
376
|
end
|
|
265
377
|
|
|
266
378
|
def next_sibling_wrapped(node)
|
|
267
379
|
n = node.respond_to?(:__dommy_backend_node__) ? node.__dommy_backend_node__.next : nil
|
|
380
|
+
n = n.next while n && document_for(node).wrap_node(n).nil?
|
|
268
381
|
n && document_for(node).wrap_node(n)
|
|
269
382
|
end
|
|
270
383
|
|
|
271
384
|
def previous_sibling_wrapped(node)
|
|
272
385
|
n = node.respond_to?(:__dommy_backend_node__) ? node.__dommy_backend_node__.previous : nil
|
|
386
|
+
n = n.previous while n && document_for(node).wrap_node(n).nil?
|
|
273
387
|
n && document_for(node).wrap_node(n)
|
|
274
388
|
end
|
|
275
389
|
|
|
@@ -294,35 +408,45 @@ module Dommy
|
|
|
294
408
|
@pointer_before_reference = true
|
|
295
409
|
end
|
|
296
410
|
|
|
411
|
+
# WHATWG "traverse" (direction=next). referenceNode / pointerBeforeReferenceNode
|
|
412
|
+
# are committed only when a node is accepted; if no node matches we return null
|
|
413
|
+
# and leave the iterator's position untouched.
|
|
297
414
|
def next_node
|
|
415
|
+
node = @reference_node
|
|
416
|
+
before = @pointer_before_reference
|
|
298
417
|
loop do
|
|
299
|
-
|
|
300
|
-
|
|
418
|
+
if before
|
|
419
|
+
before = false
|
|
301
420
|
else
|
|
302
|
-
next_in_document_order(
|
|
421
|
+
node = next_in_document_order(node)
|
|
422
|
+
return nil unless node
|
|
303
423
|
end
|
|
304
424
|
|
|
305
|
-
|
|
425
|
+
next unless accept(node) == NodeFilter::FILTER_ACCEPT
|
|
306
426
|
|
|
307
427
|
@reference_node = node
|
|
308
|
-
@pointer_before_reference =
|
|
309
|
-
return node
|
|
428
|
+
@pointer_before_reference = before
|
|
429
|
+
return node
|
|
310
430
|
end
|
|
311
431
|
end
|
|
312
432
|
|
|
433
|
+
# WHATWG "traverse" (direction=previous).
|
|
313
434
|
def previous_node
|
|
435
|
+
node = @reference_node
|
|
436
|
+
before = @pointer_before_reference
|
|
314
437
|
loop do
|
|
315
|
-
|
|
316
|
-
previous_in_document_order(
|
|
438
|
+
if before
|
|
439
|
+
node = previous_in_document_order(node)
|
|
440
|
+
return nil unless node
|
|
317
441
|
else
|
|
318
|
-
|
|
442
|
+
before = true
|
|
319
443
|
end
|
|
320
444
|
|
|
321
|
-
|
|
445
|
+
next unless accept(node) == NodeFilter::FILTER_ACCEPT
|
|
322
446
|
|
|
323
447
|
@reference_node = node
|
|
324
|
-
@pointer_before_reference =
|
|
325
|
-
return node
|
|
448
|
+
@pointer_before_reference = before
|
|
449
|
+
return node
|
|
326
450
|
end
|
|
327
451
|
end
|
|
328
452
|
|
|
@@ -330,6 +454,30 @@ module Dommy
|
|
|
330
454
|
nil
|
|
331
455
|
end
|
|
332
456
|
|
|
457
|
+
# WHATWG "NodeIterator pre-removing steps". `removed` is the wrapped node
|
|
458
|
+
# about to be detached (still attached when this runs).
|
|
459
|
+
def pre_remove(removed)
|
|
460
|
+
# Terminate unless `removed` is in this iterator's collection and an
|
|
461
|
+
# inclusive ancestor of the reference node: skip if it is an inclusive
|
|
462
|
+
# ancestor of root (never in the collection) or not one of referenceNode.
|
|
463
|
+
return if inclusive_ancestor?(removed, @root)
|
|
464
|
+
return unless inclusive_ancestor?(removed, @reference_node)
|
|
465
|
+
|
|
466
|
+
unless @pointer_before_reference
|
|
467
|
+
@reference_node = tree_preceding(removed)
|
|
468
|
+
return
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
following = tree_next_descendants(removed)
|
|
472
|
+
if following
|
|
473
|
+
@reference_node = following
|
|
474
|
+
return
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
@reference_node = tree_preceding(removed)
|
|
478
|
+
@pointer_before_reference = false
|
|
479
|
+
end
|
|
480
|
+
|
|
333
481
|
def __js_get__(key)
|
|
334
482
|
case key
|
|
335
483
|
when "root"
|
|
@@ -345,6 +493,8 @@ module Dommy
|
|
|
345
493
|
end
|
|
346
494
|
end
|
|
347
495
|
|
|
496
|
+
include Bridge::Methods
|
|
497
|
+
js_methods %w[nextNode previousNode detach]
|
|
348
498
|
def __js_call__(method, _args)
|
|
349
499
|
case method
|
|
350
500
|
when "nextNode"
|
|
@@ -421,5 +571,44 @@ module Dommy
|
|
|
421
571
|
def document_for(node)
|
|
422
572
|
node.instance_variable_get(:@document) || @root.instance_variable_get(:@document) || @root
|
|
423
573
|
end
|
|
574
|
+
|
|
575
|
+
def same_node?(node_a, node_b)
|
|
576
|
+
return false unless node_a && node_b
|
|
577
|
+
return true if node_a.equal?(node_b)
|
|
578
|
+
|
|
579
|
+
node_a.respond_to?(:__dommy_backend_node__) && node_b.respond_to?(:__dommy_backend_node__) &&
|
|
580
|
+
node_a.__dommy_backend_node__.equal?(node_b.__dommy_backend_node__)
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def inclusive_ancestor?(ancestor, descendant)
|
|
584
|
+
current = descendant
|
|
585
|
+
while current
|
|
586
|
+
return true if same_node?(current, ancestor)
|
|
587
|
+
|
|
588
|
+
current = parent_node_of(current)
|
|
589
|
+
end
|
|
590
|
+
false
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# The last node preceding `node` in tree order (common.js `previousNode`).
|
|
594
|
+
def tree_preceding(node)
|
|
595
|
+
sib = previous_sibling_node(node)
|
|
596
|
+
if sib
|
|
597
|
+
node = sib
|
|
598
|
+
while (last = last_child_node(node))
|
|
599
|
+
node = last
|
|
600
|
+
end
|
|
601
|
+
return node
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
parent_node_of(node)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# The first node following `node` and all its descendants in tree order
|
|
608
|
+
# (common.js `nextNodeDescendants`).
|
|
609
|
+
def tree_next_descendants(node)
|
|
610
|
+
node = parent_node_of(node) while node && next_sibling_node(node).nil?
|
|
611
|
+
node && next_sibling_node(node)
|
|
612
|
+
end
|
|
424
613
|
end
|
|
425
614
|
end
|