dommy 0.12.0 → 0.13.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/bridge.rb +16 -3
- data/lib/dommy/browser.rb +43 -47
- data/lib/dommy/callable_invoker.rb +16 -0
- data/lib/dommy/compression_streams.rb +12 -62
- data/lib/dommy/document.rb +5 -5
- data/lib/dommy/element.rb +2 -3
- data/lib/dommy/encodings/tables.rb +751 -0
- data/lib/dommy/encodings.rb +299 -0
- data/lib/dommy/event.rb +4 -5
- data/lib/dommy/fetch.rb +60 -49
- data/lib/dommy/form_data.rb +10 -0
- data/lib/dommy/html_elements.rb +136 -62
- data/lib/dommy/internal/exception_report.rb +123 -0
- data/lib/dommy/internal/idna.rb +4 -4
- data/lib/dommy/internal/idna_data.rb +513 -100
- data/lib/dommy/internal/url_parser.rb +70 -18
- data/lib/dommy/js/dom_interfaces.rb +1 -0
- data/lib/dommy/js/error_log.rb +137 -0
- data/lib/dommy/js/host_bridge.rb +13 -0
- data/lib/dommy/js/host_runtime.js +107 -17
- data/lib/dommy/js/marshaller.rb +21 -2
- data/lib/dommy/js/runtime.rb +8 -1
- data/lib/dommy/js/script_boot.rb +58 -14
- data/lib/dommy/js/wire_tags.rb +8 -0
- data/lib/dommy/location.rb +14 -4
- data/lib/dommy/navigator.rb +3 -1
- data/lib/dommy/promise.rb +4 -4
- data/lib/dommy/scheduler.rb +35 -6
- data/lib/dommy/streams.rb +1200 -187
- data/lib/dommy/text_codec.rb +110 -213
- data/lib/dommy/url.rb +34 -32
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +162 -34
- data/lib/dommy/xml_http_request.rb +18 -4
- data/lib/dommy.rb +2 -0
- metadata +5 -1
data/lib/dommy/html_elements.rb
CHANGED
|
@@ -241,107 +241,169 @@ module Dommy
|
|
|
241
241
|
end
|
|
242
242
|
end
|
|
243
243
|
|
|
244
|
-
# The HTMLHyperlinkElementUtils IDL mixin, shared by <a> and <area>.
|
|
245
|
-
#
|
|
246
|
-
#
|
|
247
|
-
#
|
|
244
|
+
# The HTMLHyperlinkElementUtils IDL mixin, shared by <a> and <area>. The
|
|
245
|
+
# `href` content attribute, parsed against the document base URL, is the
|
|
246
|
+
# element's URL; every getter reads a component of it and every setter
|
|
247
|
+
# changes that component the way the URL API does, then writes the
|
|
248
|
+
# serialization back into the attribute. Without an href there is no URL
|
|
249
|
+
# and the getters return the empty string (":" for protocol); an href that
|
|
250
|
+
# does not parse reads back as written.
|
|
251
|
+
#
|
|
252
|
+
# Spec: https://html.spec.whatwg.org/multipage/links.html#htmlhyperlinkelementutils
|
|
248
253
|
module HyperlinkUtils
|
|
254
|
+
URL_COMPONENTS = %w[origin protocol username password host hostname port pathname search hash].freeze
|
|
255
|
+
|
|
249
256
|
# WebIDL stringifier: `String(link)` / `link.toString()` is its href (the
|
|
250
257
|
# resolved absolute URL), not the element's serialization.
|
|
251
258
|
def to_s
|
|
252
|
-
|
|
259
|
+
href
|
|
253
260
|
end
|
|
254
261
|
|
|
255
262
|
def href
|
|
256
|
-
|
|
263
|
+
raw = @__node__["href"]
|
|
264
|
+
return "" if raw.nil?
|
|
265
|
+
|
|
266
|
+
url = hyperlink_url
|
|
267
|
+
url ? url.href : raw
|
|
257
268
|
end
|
|
258
269
|
|
|
259
270
|
def href=(value)
|
|
260
271
|
set_attribute("href", value.to_s)
|
|
261
272
|
end
|
|
262
273
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
274
|
+
def origin
|
|
275
|
+
url = hyperlink_url
|
|
276
|
+
url ? url.origin : ""
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def protocol
|
|
280
|
+
url = hyperlink_url
|
|
281
|
+
url ? url.protocol : ":"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def protocol=(value)
|
|
285
|
+
change_url { |url| url.protocol = value }
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def username
|
|
289
|
+
hyperlink_url&.username.to_s
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def username=(value)
|
|
293
|
+
change_url { |url| url.username = value }
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def password
|
|
297
|
+
hyperlink_url&.password.to_s
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def password=(value)
|
|
301
|
+
change_url { |url| url.password = value }
|
|
267
302
|
end
|
|
268
303
|
|
|
269
304
|
def host
|
|
270
|
-
|
|
305
|
+
hyperlink_url&.host.to_s
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def host=(value)
|
|
309
|
+
change_url { |url| url.host = value }
|
|
271
310
|
end
|
|
272
311
|
|
|
273
312
|
def hostname
|
|
274
|
-
|
|
313
|
+
hyperlink_url&.hostname.to_s
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def hostname=(value)
|
|
317
|
+
change_url { |url| url.hostname = value }
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def port
|
|
321
|
+
hyperlink_url&.port.to_s
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def port=(value)
|
|
325
|
+
change_url { |url| url.port = value }
|
|
275
326
|
end
|
|
276
327
|
|
|
277
328
|
def pathname
|
|
278
|
-
|
|
329
|
+
hyperlink_url&.pathname.to_s
|
|
279
330
|
end
|
|
280
331
|
|
|
281
|
-
def
|
|
282
|
-
|
|
332
|
+
def pathname=(value)
|
|
333
|
+
change_url { |url| url.pathname = value }
|
|
283
334
|
end
|
|
284
335
|
|
|
285
336
|
def search
|
|
286
|
-
|
|
337
|
+
hyperlink_url&.search.to_s
|
|
287
338
|
end
|
|
288
339
|
|
|
289
|
-
def
|
|
290
|
-
|
|
340
|
+
def search=(value)
|
|
341
|
+
change_url { |url| url.search = value }
|
|
291
342
|
end
|
|
292
343
|
|
|
293
|
-
def
|
|
294
|
-
|
|
344
|
+
def hash
|
|
345
|
+
hyperlink_url&.hash.to_s
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def hash=(value)
|
|
349
|
+
change_url { |url| url.hash = value }
|
|
295
350
|
end
|
|
296
351
|
|
|
297
352
|
def __js_get__(key)
|
|
298
353
|
case key
|
|
299
|
-
when "href"
|
|
300
|
-
|
|
301
|
-
when "
|
|
302
|
-
|
|
303
|
-
when "
|
|
304
|
-
|
|
305
|
-
when "hostname"
|
|
306
|
-
|
|
307
|
-
when "pathname"
|
|
308
|
-
|
|
309
|
-
when "
|
|
310
|
-
|
|
311
|
-
when "search"
|
|
312
|
-
search
|
|
313
|
-
when "port"
|
|
314
|
-
port
|
|
315
|
-
when "origin"
|
|
316
|
-
origin
|
|
317
|
-
else
|
|
318
|
-
super
|
|
354
|
+
when "href" then href
|
|
355
|
+
when "origin" then origin
|
|
356
|
+
when "protocol" then protocol
|
|
357
|
+
when "username" then username
|
|
358
|
+
when "password" then password
|
|
359
|
+
when "host" then host
|
|
360
|
+
when "hostname" then hostname
|
|
361
|
+
when "port" then port
|
|
362
|
+
when "pathname" then pathname
|
|
363
|
+
when "search" then search
|
|
364
|
+
when "hash" then self.hash
|
|
365
|
+
else super
|
|
319
366
|
end
|
|
320
367
|
end
|
|
321
368
|
|
|
322
369
|
def __js_set__(key, value)
|
|
323
|
-
key
|
|
370
|
+
case key
|
|
371
|
+
when "href" then self.href = value
|
|
372
|
+
when "protocol" then self.protocol = value
|
|
373
|
+
when "username" then self.username = value
|
|
374
|
+
when "password" then self.password = value
|
|
375
|
+
when "host" then self.host = value
|
|
376
|
+
when "hostname" then self.hostname = value
|
|
377
|
+
when "port" then self.port = value
|
|
378
|
+
when "pathname" then self.pathname = value
|
|
379
|
+
when "search" then self.search = value
|
|
380
|
+
when "hash" then self.hash = value
|
|
381
|
+
else super
|
|
382
|
+
end
|
|
324
383
|
end
|
|
325
384
|
|
|
326
385
|
private
|
|
327
386
|
|
|
328
|
-
|
|
329
|
-
|
|
387
|
+
# HTML "reinitialize url": the href attribute parsed against the
|
|
388
|
+
# document base URL, or nil when there is none or it does not parse.
|
|
389
|
+
def hyperlink_url
|
|
390
|
+
raw = @__node__["href"]
|
|
391
|
+
return nil if raw.nil?
|
|
330
392
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def uri_part(part)
|
|
337
|
-
uri.send(part)
|
|
393
|
+
base = @document.base_uri.to_s
|
|
394
|
+
URL.new(raw, base.empty? ? nil : base)
|
|
395
|
+
rescue Bridge::TypeError
|
|
396
|
+
nil
|
|
338
397
|
end
|
|
339
398
|
|
|
340
|
-
|
|
341
|
-
|
|
399
|
+
# A setter: nothing without a URL; otherwise the change, then "update
|
|
400
|
+
# href" with the serialization.
|
|
401
|
+
def change_url
|
|
402
|
+
url = hyperlink_url
|
|
403
|
+
return unless url
|
|
342
404
|
|
|
343
|
-
|
|
344
|
-
|
|
405
|
+
yield url
|
|
406
|
+
set_attribute("href", url.href)
|
|
345
407
|
end
|
|
346
408
|
end
|
|
347
409
|
|
|
@@ -747,12 +809,12 @@ module Dommy
|
|
|
747
809
|
str.gsub(/[\r\n]/, "")
|
|
748
810
|
end
|
|
749
811
|
|
|
812
|
+
# HTML's value sanitization for number and range: anything that is not a
|
|
813
|
+
# valid floating-point number, or is out of the finite range, is the
|
|
814
|
+
# empty string. Ruby's Float() is wider than the spec (" 1", "+1", "1.").
|
|
750
815
|
def sanitize_number(raw)
|
|
751
816
|
s = raw.to_s
|
|
752
|
-
|
|
753
|
-
s
|
|
754
|
-
rescue ArgumentError, TypeError
|
|
755
|
-
""
|
|
817
|
+
parse_valid_float(s).finite? ? s : ""
|
|
756
818
|
end
|
|
757
819
|
|
|
758
820
|
# Underlying string the user supplied to `value=`, before any
|
|
@@ -1068,10 +1130,15 @@ module Dommy
|
|
|
1068
1130
|
return if mn && mx && mn > mx
|
|
1069
1131
|
|
|
1070
1132
|
before = value_as_number
|
|
1071
|
-
|
|
1133
|
+
# The arithmetic runs on the decimal values the attributes spell, not on
|
|
1134
|
+
# their nearest doubles: 0.1 + 0.1 + 0.1 is 0.3, not 0.30000000000000004.
|
|
1135
|
+
allowed = decimal(allowed)
|
|
1136
|
+
mn = decimal(mn) if mn
|
|
1137
|
+
mx = decimal(mx) if mx
|
|
1138
|
+
base = before.nan? ? (mn || 0r) : decimal(before)
|
|
1072
1139
|
result = base + count * allowed
|
|
1073
1140
|
|
|
1074
|
-
step_base = mn ||
|
|
1141
|
+
step_base = mn || 0r
|
|
1075
1142
|
result = mx - (mx - step_base) % allowed if mx && result > mx
|
|
1076
1143
|
result = mn + (step_base - mn) % allowed if mn && result < mn
|
|
1077
1144
|
|
|
@@ -1082,10 +1149,16 @@ module Dommy
|
|
|
1082
1149
|
return if count.negative? && result > before
|
|
1083
1150
|
end
|
|
1084
1151
|
|
|
1085
|
-
self.value_as_number = result
|
|
1152
|
+
self.value_as_number = result.to_f
|
|
1086
1153
|
nil
|
|
1087
1154
|
end
|
|
1088
1155
|
|
|
1156
|
+
# The decimal number a double stands for: 0.1 is 1/10, not the binary
|
|
1157
|
+
# fraction nearest to it.
|
|
1158
|
+
def decimal(number)
|
|
1159
|
+
number.rationalize(Rational(1, 10**12))
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1089
1162
|
# The scale that turns one declared step into valueAsNumber units (ms for the
|
|
1090
1163
|
# date/time types, natural units for number/range/month).
|
|
1091
1164
|
def step_scale_factor
|
|
@@ -1132,7 +1205,8 @@ module Dommy
|
|
|
1132
1205
|
# WHATWG "valid floating-point number": no surrounding whitespace (unlike
|
|
1133
1206
|
# Ruby's Float()), optional sign, digits with optional fraction, optional
|
|
1134
1207
|
# exponent. Anything else — including " 1 " or "1e" — yields NaN.
|
|
1135
|
-
|
|
1208
|
+
# A fraction needs a digit after the dot, so "1." is not a number.
|
|
1209
|
+
VALID_FLOAT_RE = /\A-?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][-+]?\d+)?\z/
|
|
1136
1210
|
|
|
1137
1211
|
def parse_valid_float(str)
|
|
1138
1212
|
s = str.to_s
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Internal
|
|
5
|
+
# Shared shaping for WHATWG "report an exception": turn whatever a throwing
|
|
6
|
+
# entry point handed us into the pair the report needs — the value to expose
|
|
7
|
+
# as `ErrorEvent#error` and the string to expose as `ErrorEvent#message`.
|
|
8
|
+
#
|
|
9
|
+
# Two shapes arrive. A JS callback's throw comes wrapped in a
|
|
10
|
+
# `Bridge::ThrowValue` carrying the thrown JS value with its identity intact
|
|
11
|
+
# (so `event.error === thrown` holds in the page); everything else is a plain
|
|
12
|
+
# Ruby exception raised by the host or the engine. Unwrapping in one place
|
|
13
|
+
# keeps every report site (event dispatch, script evaluation, a timer
|
|
14
|
+
# callback, `reportError`) agreeing on what the page sees.
|
|
15
|
+
module ExceptionReport
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# The `error` value a report should expose: a ThrowValue's payload keeps
|
|
19
|
+
# JS identity, anything else reports itself.
|
|
20
|
+
def error_value(error)
|
|
21
|
+
error.is_a?(Bridge::ThrowValue) ? error.value : error
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The `message` a report should expose for `error`. A JS Error proxy and a
|
|
25
|
+
# Ruby exception both answer `message`; a thrown non-Error (a string, an
|
|
26
|
+
# object literal) has only its string form.
|
|
27
|
+
def message_for(error)
|
|
28
|
+
value = error_value(error)
|
|
29
|
+
return value.message.to_s if value.respond_to?(:message)
|
|
30
|
+
|
|
31
|
+
value.to_s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Both halves at once, for a caller that needs the pair.
|
|
35
|
+
def describe(error)
|
|
36
|
+
[error_value(error), message_for(error)]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# One JS stack frame, in either of the two forms an engine writes:
|
|
40
|
+
# `at name (file:line:col)` and the anonymous `at file:line:col`.
|
|
41
|
+
STACK_FRAME = /\Aat\s+(?:\S+\s+\()?(?<file>.+?):(?<line>\d+):(?<column>\d+)\)?\z/
|
|
42
|
+
|
|
43
|
+
# Frames inside Dommy's own JS plumbing. The page did not write them, so
|
|
44
|
+
# they must not be reported as where its error happened.
|
|
45
|
+
INTERNAL_SOURCES = %w[host_runtime.js observable_runtime.js].freeze
|
|
46
|
+
|
|
47
|
+
# Where the error happened, as `ErrorEvent`'s [filename, lineno, colno].
|
|
48
|
+
# A JS engine puts its frames on the raised exception's backtrace, so the
|
|
49
|
+
# topmost frame the PAGE owns is the position to report. Zeroes when there
|
|
50
|
+
# is no usable frame, which is what the spec says to expose when the
|
|
51
|
+
# position is unknown.
|
|
52
|
+
def source_position(error)
|
|
53
|
+
Array(error.backtrace).each do |frame|
|
|
54
|
+
match = STACK_FRAME.match(frame.to_s.strip)
|
|
55
|
+
next unless match
|
|
56
|
+
next if INTERNAL_SOURCES.any? { |source| match[:file].end_with?(source) }
|
|
57
|
+
|
|
58
|
+
return [match[:file], match[:line].to_i, match[:column].to_i]
|
|
59
|
+
end
|
|
60
|
+
["", 0, 0]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# WHATWG "report an exception" at `window`, shaping the thrown value, its
|
|
64
|
+
# message and its source position from whatever the entry point caught.
|
|
65
|
+
# Every entry point reports through here, so none of them can drift from
|
|
66
|
+
# the others in what the page gets to see. Returns whether the page
|
|
67
|
+
# handled it.
|
|
68
|
+
# `value` overrides what the page sees as `event.error`, for a caller that
|
|
69
|
+
# can supply a better one than the raw catch: an engine that converted the
|
|
70
|
+
# throw to a host exception can rebuild the JS Error the page threw, which
|
|
71
|
+
# is the difference between a handler reading `.message` and one crashing
|
|
72
|
+
# on `undefined`.
|
|
73
|
+
def report_at(window, error, value: nil)
|
|
74
|
+
value ||= error_value(error)
|
|
75
|
+
message = message_for(error)
|
|
76
|
+
file, line, column = source_position(error)
|
|
77
|
+
window.__internal_report_exception__(value, message,
|
|
78
|
+
filename: document_source(file, window), lineno: line, colno: column, host_error: error)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# What an engine calls source it was handed with no name of its own — an
|
|
82
|
+
# inline `<script>`, an `eval`. A browser reports the document's URL for
|
|
83
|
+
# those, so the placeholder is swapped for it rather than leaking an
|
|
84
|
+
# engine-internal name into `ErrorEvent#filename`.
|
|
85
|
+
ANONYMOUS_SOURCES = ["<code>", "<input>", "<eval>", "<anonymous>"].freeze
|
|
86
|
+
|
|
87
|
+
def document_source(file, window)
|
|
88
|
+
return file unless ANONYMOUS_SOURCES.include?(file)
|
|
89
|
+
|
|
90
|
+
location = window.location if window.respond_to?(:location)
|
|
91
|
+
location.respond_to?(:href) ? location.href.to_s : file
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# A host-loggable exception for a value that came straight off the bridge
|
|
95
|
+
# with no exception attached — a rejection reason, which the engine hands
|
|
96
|
+
# over as the JS value itself. `Bridge::ThrowValue` is that shape already:
|
|
97
|
+
# it is raisable, keeps the value, and takes the JS frames as its backtrace
|
|
98
|
+
# so the report can still say where the page failed.
|
|
99
|
+
def thrown_host_error(value)
|
|
100
|
+
return value if value.is_a?(::Exception)
|
|
101
|
+
|
|
102
|
+
name = value.js_name if value.respond_to?(:js_name)
|
|
103
|
+
text = value.to_s
|
|
104
|
+
error = Bridge::ThrowValue.new(value, name && name != "Object" ? "#{name}: #{text}" : text)
|
|
105
|
+
frames = value.respond_to?(:stack_frames) ? value.stack_frames : nil
|
|
106
|
+
error.set_backtrace(frames) if frames && !frames.empty?
|
|
107
|
+
error
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The form the HOST logs. A report's `error` value is whatever the page
|
|
111
|
+
# threw, which for JS code is an opaque `Bridge::JSValue` with no `class` /
|
|
112
|
+
# `message` / backtrace — everything an error log wants. A caller that has
|
|
113
|
+
# the original Ruby exception passes it as `host_error`; otherwise the bare
|
|
114
|
+
# value is wrapped, so the log never has to special-case what it holds.
|
|
115
|
+
def host_form(error_value, host_error)
|
|
116
|
+
return host_error if host_error
|
|
117
|
+
return error_value if error_value.is_a?(::Exception)
|
|
118
|
+
|
|
119
|
+
Bridge::ThrowValue.new(error_value)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/lib/dommy/internal/idna.rb
CHANGED
|
@@ -7,7 +7,7 @@ module Dommy
|
|
|
7
7
|
# IDNA ToASCII / ToUnicode for domain names. Built on
|
|
8
8
|
# `Internal::Punycode` plus the Unicode tables in
|
|
9
9
|
# `Internal::IDNAData` (generated by `script/build_idna_tables.rb`
|
|
10
|
-
# from Unicode
|
|
10
|
+
# from Unicode 17.0 source files in `vendor/unicode/`).
|
|
11
11
|
#
|
|
12
12
|
# Conforms (approximately) to UTS #46 with WHATWG URL parameters:
|
|
13
13
|
#
|
|
@@ -201,12 +201,12 @@ module Dommy
|
|
|
201
201
|
end
|
|
202
202
|
end
|
|
203
203
|
|
|
204
|
+
# UTS #46 V6: General_Category Mark (Mn, Mc or Me), which is wider than
|
|
205
|
+
# the Bidi class NSM; a spacing mark such as U+1B44 is L.
|
|
204
206
|
def self.validate_no_leading_combining_mark(label)
|
|
205
207
|
first_cp = label.codepoints.first
|
|
206
208
|
return unless first_cp
|
|
207
|
-
|
|
208
|
-
bidi_class = bidi_class_of(first_cp)
|
|
209
|
-
return unless bidi_class == :NSM
|
|
209
|
+
return unless IDNAData.lookup(IDNAData::MARK, first_cp)
|
|
210
210
|
|
|
211
211
|
raise Error, "label starts with combining mark: #{label.inspect}"
|
|
212
212
|
end
|