dommy 0.5.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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +213 -0
  3. data/lib/dommy/attr.rb +200 -0
  4. data/lib/dommy/blob.rb +182 -0
  5. data/lib/dommy/bridge.rb +141 -0
  6. data/lib/dommy/css.rb +283 -0
  7. data/lib/dommy/custom_elements.rb +125 -0
  8. data/lib/dommy/data_transfer.rb +98 -0
  9. data/lib/dommy/document.rb +674 -0
  10. data/lib/dommy/dom_exception.rb +258 -0
  11. data/lib/dommy/dom_parser.rb +88 -0
  12. data/lib/dommy/element.rb +1975 -0
  13. data/lib/dommy/event.rb +589 -0
  14. data/lib/dommy/fetch.rb +241 -0
  15. data/lib/dommy/form_data.rb +208 -0
  16. data/lib/dommy/html_collection.rb +207 -0
  17. data/lib/dommy/html_elements.rb +4455 -0
  18. data/lib/dommy/internal/cookie_jar.rb +27 -0
  19. data/lib/dommy/internal/dom_matching.rb +141 -0
  20. data/lib/dommy/internal/mutation_coordinator.rb +172 -0
  21. data/lib/dommy/internal/node_traversal.rb +36 -0
  22. data/lib/dommy/internal/node_wrapper_cache.rb +179 -0
  23. data/lib/dommy/internal/observer_manager.rb +31 -0
  24. data/lib/dommy/internal/observer_matcher.rb +31 -0
  25. data/lib/dommy/internal/scope_resolution.rb +27 -0
  26. data/lib/dommy/internal/shadow_root_registry.rb +35 -0
  27. data/lib/dommy/internal/template_content_registry.rb +97 -0
  28. data/lib/dommy/minitest/assertions.rb +105 -0
  29. data/lib/dommy/minitest.rb +17 -0
  30. data/lib/dommy/navigator.rb +271 -0
  31. data/lib/dommy/node.rb +218 -0
  32. data/lib/dommy/observer.rb +199 -0
  33. data/lib/dommy/parser.rb +29 -0
  34. data/lib/dommy/promise.rb +199 -0
  35. data/lib/dommy/router.rb +275 -0
  36. data/lib/dommy/rspec/capy_style_matchers.rb +356 -0
  37. data/lib/dommy/rspec/matchers.rb +230 -0
  38. data/lib/dommy/rspec.rb +18 -0
  39. data/lib/dommy/scheduler.rb +135 -0
  40. data/lib/dommy/shadow_root.rb +255 -0
  41. data/lib/dommy/storage.rb +112 -0
  42. data/lib/dommy/test_helpers.rb +78 -0
  43. data/lib/dommy/tree_walker.rb +425 -0
  44. data/lib/dommy/url.rb +479 -0
  45. data/lib/dommy/version.rb +5 -0
  46. data/lib/dommy/world.rb +209 -0
  47. data/lib/dommy.rb +119 -0
  48. metadata +110 -0
@@ -0,0 +1,4455 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ # Base for specialized HTMLElement subclasses. Adds the reflected
5
+ # IDL boolean / string attribute helpers each subclass uses.
6
+ class HTMLElement < Element
7
+ private
8
+
9
+ def reflected_boolean(name)
10
+ @__node__.key?(name.to_s.downcase)
11
+ end
12
+
13
+ def set_reflected_boolean(name, value)
14
+ key = name.to_s.downcase
15
+ if value
16
+ set_attribute(key, "")
17
+ elsif @__node__.key?(key)
18
+ remove_attribute(key)
19
+ end
20
+ end
21
+
22
+ def reflected_string(name)
23
+ @__node__[name.to_s.downcase].to_s
24
+ end
25
+
26
+ def set_reflected_string(name, value)
27
+ set_attribute(name.to_s.downcase, value.to_s)
28
+ end
29
+ end
30
+
31
+ # `<a>` — exposes URL-component getters/setters via the `href`
32
+ # attribute, plus reflected `target` / `download` / `rel` / `type`.
33
+ class HTMLAnchorElement < HTMLElement
34
+ def target
35
+ reflected_string("target")
36
+ end
37
+
38
+ def target=(v)
39
+ set_reflected_string("target", v)
40
+ end
41
+
42
+ def download
43
+ reflected_string("download")
44
+ end
45
+
46
+ def download=(v)
47
+ set_reflected_string("download", v)
48
+ end
49
+
50
+ def rel
51
+ reflected_string("rel")
52
+ end
53
+
54
+ def rel=(v)
55
+ set_reflected_string("rel", v)
56
+ end
57
+
58
+ def hreflang
59
+ reflected_string("hreflang")
60
+ end
61
+
62
+ def type
63
+ reflected_string("type")
64
+ end
65
+
66
+ # URL-decomposition helpers. The anchor's `href` is resolved to
67
+ # an absolute URL (inherited from Element#anchor_href); break it
68
+ # into the standard components on demand.
69
+ def hash
70
+ uri_part(:fragment) ? "##{uri_part(:fragment)}" : ""
71
+ end
72
+
73
+ def host
74
+ uri.host ? "#{uri.host}#{port_suffix}" : ""
75
+ end
76
+
77
+ def hostname
78
+ uri.host || ""
79
+ end
80
+
81
+ def pathname
82
+ uri.path || "/"
83
+ end
84
+
85
+ def protocol
86
+ uri.scheme ? "#{uri.scheme}:" : ""
87
+ end
88
+
89
+ def search
90
+ uri.query ? "?#{uri.query}" : ""
91
+ end
92
+
93
+ def port
94
+ uri.port ? uri.port.to_s : ""
95
+ end
96
+
97
+ def origin
98
+ uri.scheme && uri.host ? "#{uri.scheme}://#{uri.host}#{port_suffix}" : ""
99
+ end
100
+
101
+ def __js_get__(key)
102
+ case key
103
+ when "target"
104
+ target
105
+ when "download"
106
+ download
107
+ when "rel"
108
+ rel
109
+ when "hreflang"
110
+ hreflang
111
+ when "type"
112
+ type
113
+ when "hash"
114
+ self.hash
115
+ when "host"
116
+ host
117
+ when "hostname"
118
+ hostname
119
+ when "pathname"
120
+ pathname
121
+ when "protocol"
122
+ protocol
123
+ when "search"
124
+ search
125
+ when "port"
126
+ port
127
+ when "origin"
128
+ origin
129
+ else
130
+ super
131
+ end
132
+ end
133
+
134
+ def __js_set__(key, value)
135
+ case key
136
+ when "target", "download", "rel", "hreflang"
137
+ set_reflected_string(key, value)
138
+ else
139
+ super
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def uri
146
+ require "uri"
147
+
148
+ URI(anchor_href)
149
+ rescue URI::InvalidURIError, ArgumentError
150
+ URI("")
151
+ end
152
+
153
+ def uri_part(part)
154
+ uri.send(part)
155
+ end
156
+
157
+ def port_suffix
158
+ return "" unless uri.port
159
+
160
+ default = uri.scheme == "https" ? 443 : 80
161
+ uri.port == default ? "" : ":#{uri.port}"
162
+ end
163
+ end
164
+
165
+ # `<form>` — element collection, submit/reset, and a stubbed
166
+ # validation surface.
167
+ class HTMLFormElement < HTMLElement
168
+ def name
169
+ reflected_string("name")
170
+ end
171
+
172
+ def name=(v)
173
+ set_reflected_string("name", v)
174
+ end
175
+
176
+ def action
177
+ reflected_string("action")
178
+ end
179
+
180
+ def action=(v)
181
+ set_reflected_string("action", v)
182
+ end
183
+
184
+ def method_attr
185
+ reflected_string("method")
186
+ end
187
+
188
+ def method_attr=(v)
189
+ set_reflected_string("method", v)
190
+ end
191
+
192
+ def enctype
193
+ reflected_string("enctype")
194
+ end
195
+
196
+ def target
197
+ reflected_string("target")
198
+ end
199
+
200
+ def autocomplete
201
+ reflected_string("autocomplete")
202
+ end
203
+
204
+ def accept_charset
205
+ reflected_string("accept-charset")
206
+ end
207
+
208
+ def no_validate
209
+ reflected_boolean("novalidate")
210
+ end
211
+
212
+ def no_validate=(v)
213
+ set_reflected_boolean("novalidate", v)
214
+ end
215
+
216
+ # `form.elements` — listed elements inside the form (excludes
217
+ # nested forms per spec; we approximate by walking
218
+ # input/select/textarea/button/output/fieldset). Returned as a
219
+ # live HTMLCollection so listening to `submit`/`reset` and
220
+ # adding fields between accesses works as expected.
221
+ def elements
222
+ el = self
223
+ HTMLCollection.new do
224
+ el
225
+ .__node__
226
+ .css("input, select, textarea, button, output, fieldset")
227
+ .map do |n|
228
+ el.document.wrap_node(n)
229
+ end
230
+ .compact
231
+ end
232
+ end
233
+
234
+ def length
235
+ elements.size
236
+ end
237
+
238
+ # Spec: `submit()` performs form submission directly WITHOUT
239
+ # firing a `submit` event. This is the JS-only entry point —
240
+ # browsers don't run constraint validation either. Dommy has no
241
+ # navigation engine, so this is effectively a no-op (returns nil).
242
+ def submit
243
+ nil
244
+ end
245
+
246
+ # Spec: `reset()` does fire a `reset` event; if the event is
247
+ # default-prevented, no reset happens. Dommy has no built-in
248
+ # control re-init logic, so we just dispatch the event.
249
+ def reset
250
+ dispatch_event(Event.new("reset", "bubbles" => true, "cancelable" => true))
251
+ end
252
+
253
+ # Spec: `requestSubmit(submitter?)` is the JS counterpart that
254
+ # MIRRORS user-initiated submission — it runs constraint validation
255
+ # and fires a `submit` event. Returns true if not default-prevented.
256
+ # `submitter` (if given) must be a button inside this form.
257
+ def request_submit(submitter = nil)
258
+ if submitter
259
+ unless submitter.respond_to?(:__node__) && submitter.__node__.ancestors.include?(@__node__)
260
+ raise DOMException::NotFoundError, "submitter is not a descendant of this form"
261
+ end
262
+
263
+ type = submitter.respond_to?(:type) ? submitter.type.to_s.downcase : ""
264
+ unless %w[submit image].include?(type)
265
+ raise TypeError, "submitter must be a submit button"
266
+ end
267
+ end
268
+
269
+ dispatch_event(Event.new("submit", "bubbles" => true, "cancelable" => true))
270
+ end
271
+
272
+ # Walk all listed elements; the form is "valid" iff every
273
+ # candidate control passes its own checkValidity. Dispatches a
274
+ # non-bubbling `invalid` event on each failing control.
275
+ def check_validity
276
+ ok = true
277
+ elements.each do |el|
278
+ next unless el.respond_to?(:will_validate)
279
+ next unless el.will_validate
280
+ next if el.validity.valid && (el.instance_variable_get(:@custom_validity_message) || "").empty?
281
+
282
+ # Fire invalid event on this control (matches spec).
283
+ el.dispatch_event(Event.new("invalid", "bubbles" => false, "cancelable" => true))
284
+ ok = false
285
+ end
286
+
287
+ ok
288
+ end
289
+
290
+ def report_validity
291
+ check_validity
292
+ end
293
+
294
+ def __js_get__(key)
295
+ case key
296
+ when "elements"
297
+ elements
298
+ when "length"
299
+ length
300
+ when "name"
301
+ name
302
+ when "action"
303
+ action
304
+ when "method"
305
+ method_attr
306
+ when "enctype"
307
+ enctype
308
+ when "target"
309
+ target
310
+ when "autocomplete"
311
+ autocomplete
312
+ when "acceptCharset"
313
+ accept_charset
314
+ when "noValidate"
315
+ no_validate
316
+ else
317
+ super
318
+ end
319
+ end
320
+
321
+ def __js_set__(key, value)
322
+ case key
323
+ when "name"
324
+ set_reflected_string("name", value)
325
+ when "action"
326
+ set_reflected_string("action", value)
327
+ when "method"
328
+ set_reflected_string("method", value)
329
+ when "enctype"
330
+ set_reflected_string("enctype", value)
331
+ when "target"
332
+ set_reflected_string("target", value)
333
+ when "noValidate"
334
+ set_reflected_boolean("novalidate", value)
335
+ else
336
+ super
337
+ end
338
+ end
339
+
340
+ def __js_call__(method, args)
341
+ case method
342
+ when "submit"
343
+ submit
344
+ when "reset"
345
+ reset
346
+ when "requestSubmit"
347
+ request_submit(args[0])
348
+ when "checkValidity"
349
+ check_validity
350
+ when "reportValidity"
351
+ report_validity
352
+ else
353
+ super
354
+ end
355
+ end
356
+ end
357
+
358
+ # `<input>` — covers the most-used form control surface.
359
+ class HTMLInputElement < HTMLElement
360
+ def type
361
+ raw = @__node__["type"].to_s
362
+ raw.empty? ? "text" : raw.downcase
363
+ end
364
+
365
+ def type=(v)
366
+ set_reflected_string("type", v)
367
+ end
368
+
369
+ def name
370
+ reflected_string("name")
371
+ end
372
+
373
+ def name=(v)
374
+ set_reflected_string("name", v)
375
+ end
376
+
377
+ def placeholder
378
+ reflected_string("placeholder")
379
+ end
380
+
381
+ def placeholder=(v)
382
+ set_reflected_string("placeholder", v)
383
+ end
384
+
385
+ def min
386
+ reflected_string("min")
387
+ end
388
+
389
+ def max
390
+ reflected_string("max")
391
+ end
392
+
393
+ def step
394
+ reflected_string("step")
395
+ end
396
+
397
+ def pattern
398
+ reflected_string("pattern")
399
+ end
400
+
401
+ def autocomplete
402
+ reflected_string("autocomplete")
403
+ end
404
+
405
+ def autofocus
406
+ reflected_boolean("autofocus")
407
+ end
408
+
409
+ def autofocus=(v)
410
+ set_reflected_boolean("autofocus", v)
411
+ end
412
+
413
+ def default_value
414
+ reflected_string("value")
415
+ end
416
+
417
+ def default_checked
418
+ reflected_boolean("checked")
419
+ end
420
+
421
+ # Runtime value/checked. Dommy has no UI, so the runtime state is
422
+ # initialized from the attribute on first access and tracked
423
+ # separately thereafter — matching browser semantics where the
424
+ # `value` IDL attribute can drift from the `value` content attr.
425
+ def value
426
+ sanitize_value(@__value.nil? ? reflected_string("value") : @__value)
427
+ end
428
+
429
+ def value=(v)
430
+ raw = v.to_s
431
+ @__raw_value = raw
432
+ @__value = raw
433
+ end
434
+
435
+ # `files` — for `<input type="file">`. Browsers populate this via
436
+ # user interaction; in tests, code uses `__set_files__` to seed it.
437
+ def files
438
+ @__files ||= FileList.new
439
+ end
440
+
441
+ # Test-only seam: set the input's file list directly.
442
+ # Accepts an array (wrapped in a FileList) or a FileList itself.
443
+ def __set_files__(files_input)
444
+ @__files = files_input.is_a?(FileList) ? files_input : FileList.new(Array(files_input))
445
+ end
446
+
447
+ # Spec: the "value sanitization algorithm" runs lazily on read.
448
+ # type=email/url trim leading/trailing ASCII whitespace; type=number
449
+ # rejects non-finite floats by returning "" (badInput stays true
450
+ # so validity surfaces the original raw value).
451
+ def sanitize_value(raw)
452
+ case type
453
+ when "email"
454
+ if @__node__.key?("multiple")
455
+ raw.to_s.split(",").map(&:strip).join(",")
456
+ else
457
+ raw.to_s.strip
458
+ end
459
+
460
+ when "url"
461
+ raw.to_s.strip
462
+ when "number", "range"
463
+ sanitize_number(raw)
464
+ when "color"
465
+ s = raw.to_s.strip.downcase
466
+ s.match?(/\A#[0-9a-f]{6}\z/) ? s : "#000000"
467
+ else
468
+ raw.to_s
469
+ end
470
+ end
471
+
472
+ def sanitize_number(raw)
473
+ s = raw.to_s
474
+ Float(s)
475
+ s
476
+ rescue ArgumentError, TypeError
477
+ ""
478
+ end
479
+
480
+ # Underlying string the user supplied to `value=`, before any
481
+ # sanitization. Used by ValidityState.badInput so a non-parseable
482
+ # number still trips constraint validation.
483
+ def raw_value
484
+ @__raw_value || @__value || reflected_string("value")
485
+ end
486
+
487
+ def checked
488
+ @__checked.nil? ? default_checked : @__checked
489
+ end
490
+
491
+ def checked=(v)
492
+ @__checked = !!v
493
+ end
494
+
495
+ def disabled
496
+ reflected_boolean("disabled")
497
+ end
498
+
499
+ def disabled=(v)
500
+ set_reflected_boolean("disabled", v)
501
+ end
502
+
503
+ def required
504
+ reflected_boolean("required")
505
+ end
506
+
507
+ def required=(v)
508
+ set_reflected_boolean("required", v)
509
+ end
510
+
511
+ def readonly
512
+ reflected_boolean("readonly")
513
+ end
514
+
515
+ def readonly=(v)
516
+ set_reflected_boolean("readonly", v)
517
+ end
518
+
519
+ def labels
520
+ return [] if id.empty?
521
+
522
+ @document.query_selector_all("label[for='#{id}']")
523
+ end
524
+
525
+ # Closest enclosing form (or nil if detached / not in a form).
526
+ def form
527
+ closest("form")
528
+ end
529
+
530
+ # No real text selection; method stubs let callers proceed.
531
+ def select
532
+ nil
533
+ end
534
+
535
+ def set_selection_range(_start, _end, _direction = nil)
536
+ nil
537
+ end
538
+
539
+ def set_range_text(_replacement, *_)
540
+ nil
541
+ end
542
+
543
+ def step_up(_n = 1)
544
+ nil
545
+ end
546
+
547
+ def step_down(_n = 1)
548
+ nil
549
+ end
550
+
551
+ def validity
552
+ @__validity ||= ValidityState.new(self)
553
+ end
554
+
555
+ # Whether this control participates in constraint validation.
556
+ # Disabled / hidden / button-type inputs return false.
557
+ def will_validate
558
+ return false if reflected_boolean("disabled")
559
+ return false if reflected_boolean("readonly")
560
+ return false if %w[hidden button submit reset image].include?(type)
561
+
562
+ true
563
+ end
564
+
565
+ def validation_message
566
+ return "" unless will_validate
567
+
568
+ msg = (@custom_validity_message || "").to_s
569
+ return msg unless msg.empty?
570
+ return "Please fill out this field." if validity.value_missing
571
+ return "Please match the requested format." if validity.pattern_mismatch
572
+ return "Please enter a valid email address." if validity.type_mismatch && type == "email"
573
+ return "Please enter a URL." if validity.type_mismatch && type == "url"
574
+
575
+ ""
576
+ end
577
+
578
+ def check_validity
579
+ ok = !will_validate || validity.valid
580
+ dispatch_event(Event.new("invalid", "bubbles" => false, "cancelable" => true)) unless ok
581
+ ok
582
+ end
583
+
584
+ def report_validity
585
+ check_validity
586
+ end
587
+
588
+ def set_custom_validity(msg)
589
+ @custom_validity_message = msg.to_s
590
+ nil
591
+ end
592
+
593
+ def __js_get__(key)
594
+ case key
595
+ when "type"
596
+ type
597
+ when "name"
598
+ name
599
+ when "placeholder"
600
+ placeholder
601
+ when "min"
602
+ min
603
+ when "max"
604
+ max
605
+ when "step"
606
+ step
607
+ when "pattern"
608
+ pattern
609
+ when "autocomplete"
610
+ autocomplete
611
+ when "autofocus"
612
+ autofocus
613
+ when "defaultValue"
614
+ default_value
615
+ when "defaultChecked"
616
+ default_checked
617
+ when "value"
618
+ value
619
+ when "checked"
620
+ checked
621
+ when "disabled"
622
+ disabled
623
+ when "required"
624
+ required
625
+ when "readonly", "readOnly"
626
+ readonly
627
+ when "labels"
628
+ labels
629
+ when "form"
630
+ form
631
+ when "validity"
632
+ validity
633
+ when "willValidate"
634
+ will_validate
635
+ when "validationMessage"
636
+ validation_message
637
+ when "files"
638
+ files
639
+ else
640
+ super
641
+ end
642
+ end
643
+
644
+ def __js_set__(key, value)
645
+ case key
646
+ when "type"
647
+ set_reflected_string("type", value)
648
+ when "name"
649
+ set_reflected_string("name", value)
650
+ when "placeholder"
651
+ set_reflected_string("placeholder", value)
652
+ when "min", "max", "step", "pattern", "autocomplete"
653
+ set_reflected_string(key, value)
654
+ when "autofocus"
655
+ set_reflected_boolean("autofocus", value)
656
+ when "value"
657
+ self.value = value
658
+ when "checked"
659
+ self.checked = value
660
+ when "disabled"
661
+ self.disabled = value
662
+ when "required"
663
+ self.required = value
664
+ when "readonly", "readOnly"
665
+ self.readonly = value
666
+ else
667
+ super
668
+ end
669
+ end
670
+
671
+ def __js_call__(method, args)
672
+ case method
673
+ when "select"
674
+ select
675
+ when "setSelectionRange"
676
+ set_selection_range(args[0], args[1], args[2])
677
+ when "setRangeText"
678
+ set_range_text(args[0])
679
+ when "stepUp"
680
+ step_up(args[0])
681
+ when "stepDown"
682
+ step_down(args[0])
683
+ when "checkValidity"
684
+ check_validity
685
+ when "reportValidity"
686
+ report_validity
687
+ when "setCustomValidity"
688
+ set_custom_validity(args[0])
689
+ else
690
+ super
691
+ end
692
+ end
693
+ end
694
+
695
+ # `<button>` — type defaults to "submit" per spec.
696
+ class HTMLButtonElement < HTMLElement
697
+ def type
698
+ raw = @__node__["type"].to_s.downcase
699
+ %w[submit reset button].include?(raw) ? raw : "submit"
700
+ end
701
+
702
+ def type=(v)
703
+ set_reflected_string("type", v)
704
+ end
705
+
706
+ def name
707
+ reflected_string("name")
708
+ end
709
+
710
+ def name=(v)
711
+ set_reflected_string("name", v)
712
+ end
713
+
714
+ def form_action
715
+ reflected_string("formaction")
716
+ end
717
+
718
+ def form_enctype
719
+ reflected_string("formenctype")
720
+ end
721
+
722
+ def form_method
723
+ reflected_string("formmethod")
724
+ end
725
+
726
+ def form_target
727
+ reflected_string("formtarget")
728
+ end
729
+
730
+ def form_no_validate
731
+ reflected_boolean("formnovalidate")
732
+ end
733
+
734
+ def form_no_validate=(v)
735
+ set_reflected_boolean("formnovalidate", v)
736
+ end
737
+
738
+ def form
739
+ closest("form")
740
+ end
741
+
742
+ def labels
743
+ return [] if id.empty?
744
+
745
+ @document.query_selector_all("label[for='#{id}']")
746
+ end
747
+
748
+ def validity
749
+ @__validity ||= ValidityState.new(self)
750
+ end
751
+
752
+ # Buttons don't participate in constraint validation (per spec).
753
+ def will_validate
754
+ false
755
+ end
756
+
757
+ def validation_message
758
+ ""
759
+ end
760
+
761
+ def check_validity
762
+ true
763
+ end
764
+
765
+ def report_validity
766
+ true
767
+ end
768
+
769
+ def set_custom_validity(msg)
770
+ @custom_validity_message = msg.to_s
771
+ nil
772
+ end
773
+
774
+ def __js_get__(key)
775
+ case key
776
+ when "type"
777
+ type
778
+ when "name"
779
+ name
780
+ when "formAction"
781
+ form_action
782
+ when "formEnctype"
783
+ form_enctype
784
+ when "formMethod"
785
+ form_method
786
+ when "formTarget"
787
+ form_target
788
+ when "formNoValidate"
789
+ form_no_validate
790
+ when "form"
791
+ form
792
+ when "labels"
793
+ labels
794
+ when "validity"
795
+ validity
796
+ when "willValidate"
797
+ will_validate
798
+ when "validationMessage"
799
+ validation_message
800
+ else
801
+ super
802
+ end
803
+ end
804
+
805
+ def __js_set__(key, value)
806
+ case key
807
+ when "type"
808
+ set_reflected_string("type", value)
809
+ when "name"
810
+ set_reflected_string("name", value)
811
+ when "formAction"
812
+ set_reflected_string("formaction", value)
813
+ when "formEnctype"
814
+ set_reflected_string("formenctype", value)
815
+ when "formMethod"
816
+ set_reflected_string("formmethod", value)
817
+ when "formTarget"
818
+ set_reflected_string("formtarget", value)
819
+ when "formNoValidate"
820
+ set_reflected_boolean("formnovalidate", value)
821
+ else
822
+ super
823
+ end
824
+ end
825
+ end
826
+
827
+ # `<img>` — reflected URL/dimension attributes. Dommy has no real
828
+ # image loading, so `complete`/`naturalWidth`/`naturalHeight` are
829
+ # static (complete=true, dimensions=0).
830
+ class HTMLImageElement < HTMLElement
831
+ def src
832
+ reflected_string("src")
833
+ end
834
+
835
+ def src=(v)
836
+ set_reflected_string("src", v)
837
+ end
838
+
839
+ def alt
840
+ reflected_string("alt")
841
+ end
842
+
843
+ def alt=(v)
844
+ set_reflected_string("alt", v)
845
+ end
846
+
847
+ def width
848
+ @__node__["width"].to_s.to_i
849
+ end
850
+
851
+ def width=(v)
852
+ set_reflected_string("width", v.to_s)
853
+ end
854
+
855
+ def height
856
+ @__node__["height"].to_s.to_i
857
+ end
858
+
859
+ def height=(v)
860
+ set_reflected_string("height", v.to_s)
861
+ end
862
+
863
+ def crossorigin
864
+ reflected_string("crossorigin")
865
+ end
866
+
867
+ def decoding
868
+ reflected_string("decoding")
869
+ end
870
+
871
+ def loading
872
+ reflected_string("loading")
873
+ end
874
+
875
+ def referrer_policy
876
+ reflected_string("referrerpolicy")
877
+ end
878
+
879
+ def sizes
880
+ reflected_string("sizes")
881
+ end
882
+
883
+ def srcset
884
+ reflected_string("srcset")
885
+ end
886
+
887
+ # No real loader → these are constants.
888
+ def natural_width
889
+ 0
890
+ end
891
+
892
+ def natural_height
893
+ 0
894
+ end
895
+
896
+ def complete
897
+ true
898
+ end
899
+
900
+ def current_src
901
+ src
902
+ end
903
+
904
+ def __js_get__(key)
905
+ case key
906
+ when "src"
907
+ src
908
+ when "alt"
909
+ alt
910
+ when "width"
911
+ width
912
+ when "height"
913
+ height
914
+ when "naturalWidth"
915
+ natural_width
916
+ when "naturalHeight"
917
+ natural_height
918
+ when "complete"
919
+ complete
920
+ when "currentSrc"
921
+ current_src
922
+ when "crossOrigin"
923
+ crossorigin
924
+ when "decoding"
925
+ decoding
926
+ when "loading"
927
+ loading
928
+ when "referrerPolicy"
929
+ referrer_policy
930
+ when "sizes"
931
+ sizes
932
+ when "srcset"
933
+ srcset
934
+ else
935
+ super
936
+ end
937
+ end
938
+
939
+ def __js_set__(key, value)
940
+ case key
941
+ when "src", "alt", "decoding", "loading", "sizes", "srcset"
942
+ set_reflected_string(key, value)
943
+ when "width", "height"
944
+ set_reflected_string(key, value.to_s)
945
+ when "crossOrigin"
946
+ set_reflected_string("crossorigin", value)
947
+ when "referrerPolicy"
948
+ set_reflected_string("referrerpolicy", value)
949
+ else
950
+ super
951
+ end
952
+ end
953
+ end
954
+
955
+ # `<script>` — `src` / `type` / `async` / `defer` / `text`.
956
+ class HTMLScriptElement < HTMLElement
957
+ def src
958
+ reflected_string("src")
959
+ end
960
+
961
+ def src=(v)
962
+ set_reflected_string("src", v)
963
+ end
964
+
965
+ def type
966
+ reflected_string("type")
967
+ end
968
+
969
+ def type=(v)
970
+ set_reflected_string("type", v)
971
+ end
972
+
973
+ def integrity
974
+ reflected_string("integrity")
975
+ end
976
+
977
+ def nonce
978
+ reflected_string("nonce")
979
+ end
980
+
981
+ def referrer_policy
982
+ reflected_string("referrerpolicy")
983
+ end
984
+
985
+ def async
986
+ reflected_boolean("async")
987
+ end
988
+
989
+ def async=(v)
990
+ set_reflected_boolean("async", v)
991
+ end
992
+
993
+ def defer
994
+ reflected_boolean("defer")
995
+ end
996
+
997
+ def defer=(v)
998
+ set_reflected_boolean("defer", v)
999
+ end
1000
+
1001
+ def no_module
1002
+ reflected_boolean("nomodule")
1003
+ end
1004
+
1005
+ def no_module=(v)
1006
+ set_reflected_boolean("nomodule", v)
1007
+ end
1008
+
1009
+ # `text` is an alias for textContent on <script>.
1010
+ def text
1011
+ text_content
1012
+ end
1013
+
1014
+ def text=(v)
1015
+ self.text_content = v
1016
+ end
1017
+
1018
+ def __js_get__(key)
1019
+ case key
1020
+ when "src"
1021
+ src
1022
+ when "type"
1023
+ type
1024
+ when "async"
1025
+ async
1026
+ when "defer"
1027
+ defer
1028
+ when "noModule"
1029
+ no_module
1030
+ when "integrity"
1031
+ integrity
1032
+ when "nonce"
1033
+ nonce
1034
+ when "referrerPolicy"
1035
+ referrer_policy
1036
+ when "text"
1037
+ text
1038
+ else
1039
+ super
1040
+ end
1041
+ end
1042
+
1043
+ def __js_set__(key, value)
1044
+ case key
1045
+ when "src", "type", "integrity", "nonce"
1046
+ set_reflected_string(key, value)
1047
+ when "async"
1048
+ set_reflected_boolean("async", value)
1049
+ when "defer"
1050
+ set_reflected_boolean("defer", value)
1051
+ when "noModule"
1052
+ set_reflected_boolean("nomodule", value)
1053
+ when "referrerPolicy"
1054
+ set_reflected_string("referrerpolicy", value)
1055
+ when "text"
1056
+ self.text_content = value
1057
+ else
1058
+ super
1059
+ end
1060
+ end
1061
+ end
1062
+
1063
+ # `<link>` — primarily for stylesheets, icons, preload, manifests.
1064
+ class HTMLLinkElement < HTMLElement
1065
+ def href
1066
+ reflected_string("href")
1067
+ end
1068
+
1069
+ def href=(v)
1070
+ set_reflected_string("href", v)
1071
+ end
1072
+
1073
+ def rel
1074
+ reflected_string("rel")
1075
+ end
1076
+
1077
+ def rel=(v)
1078
+ set_reflected_string("rel", v)
1079
+ end
1080
+
1081
+ def type
1082
+ reflected_string("type")
1083
+ end
1084
+
1085
+ def type=(v)
1086
+ set_reflected_string("type", v)
1087
+ end
1088
+
1089
+ def media
1090
+ reflected_string("media")
1091
+ end
1092
+
1093
+ def sizes
1094
+ reflected_string("sizes")
1095
+ end
1096
+
1097
+ def hreflang
1098
+ reflected_string("hreflang")
1099
+ end
1100
+
1101
+ def as_attr
1102
+ reflected_string("as")
1103
+ end
1104
+
1105
+ def crossorigin
1106
+ reflected_string("crossorigin")
1107
+ end
1108
+
1109
+ def integrity
1110
+ reflected_string("integrity")
1111
+ end
1112
+
1113
+ def referrer_policy
1114
+ reflected_string("referrerpolicy")
1115
+ end
1116
+
1117
+ # `link.sheet` — non-nil only when this link is a stylesheet
1118
+ # (`rel` contains "stylesheet"). The sheet itself is a stub:
1119
+ # Dommy doesn't fetch or parse CSS, but consumers can still
1120
+ # `insertRule` / `deleteRule` against the in-memory sheet.
1121
+ def sheet
1122
+ return nil unless rel.split(/\s+/).any? { |t| t.casecmp("stylesheet").zero? }
1123
+
1124
+ @__sheet ||= CSSStyleSheet.new(
1125
+ owner_node: self,
1126
+ href: href,
1127
+ media: media,
1128
+ title: @__node__["title"].to_s,
1129
+ type: (type.empty? ? "text/css" : type)
1130
+ )
1131
+ end
1132
+
1133
+ def __js_get__(key)
1134
+ case key
1135
+ when "href"
1136
+ href
1137
+ when "rel"
1138
+ rel
1139
+ when "type"
1140
+ type
1141
+ when "media"
1142
+ media
1143
+ when "sizes"
1144
+ sizes
1145
+ when "hreflang"
1146
+ hreflang
1147
+ when "as"
1148
+ as_attr
1149
+ when "crossOrigin"
1150
+ crossorigin
1151
+ when "integrity"
1152
+ integrity
1153
+ when "referrerPolicy"
1154
+ referrer_policy
1155
+ when "sheet"
1156
+ sheet
1157
+ else
1158
+ super
1159
+ end
1160
+ end
1161
+
1162
+ def __js_set__(key, value)
1163
+ case key
1164
+ when "href", "rel", "type", "media", "sizes", "hreflang", "as", "integrity"
1165
+ set_reflected_string(key, value)
1166
+ when "crossOrigin"
1167
+ set_reflected_string("crossorigin", value)
1168
+ when "referrerPolicy"
1169
+ set_reflected_string("referrerpolicy", value)
1170
+ else
1171
+ super
1172
+ end
1173
+ end
1174
+ end
1175
+
1176
+ # `ValidityState` — computes constraint-validation flags from the
1177
+ # host control's current attributes and value. Bound to a single
1178
+ # host control; reads dynamically on every access so attribute
1179
+ # changes between calls are reflected.
1180
+ #
1181
+ # Flags follow the HTML spec; `badInput` is always false (we'd need
1182
+ # the browser's number parser to detect "12abc" in a type=number).
1183
+ class ValidityState
1184
+ FLAGS = %w[
1185
+ valueMissing
1186
+ typeMismatch
1187
+ patternMismatch
1188
+ tooLong
1189
+ tooShort
1190
+ rangeUnderflow
1191
+ rangeOverflow
1192
+ stepMismatch
1193
+ badInput
1194
+ customError
1195
+ ]
1196
+ .freeze
1197
+
1198
+ EMAIL_RE = /\A[^@\s]+@[^@\s]+\.[^@\s]+\z/
1199
+ URL_SCHEMES = %w[http:// https:// ftp://].freeze
1200
+
1201
+ def initialize(host = nil)
1202
+ @host = host
1203
+ end
1204
+
1205
+ # ---- Computed flags ----
1206
+
1207
+ def value_missing
1208
+ return false unless @host && host_attr_present?("required")
1209
+
1210
+ case host_type
1211
+ when "checkbox", "radio"
1212
+ !host_attr_present?("checked")
1213
+ else
1214
+ host_value.to_s.empty?
1215
+ end
1216
+ end
1217
+
1218
+ def type_mismatch
1219
+ return false unless @host
1220
+
1221
+ v = host_value.to_s
1222
+ return false if v.empty?
1223
+
1224
+ case host_type
1225
+ when "email"
1226
+ !v.match?(EMAIL_RE)
1227
+ when "url"
1228
+ URL_SCHEMES.none? { |s| v.start_with?(s) }
1229
+ else
1230
+ false
1231
+ end
1232
+ end
1233
+
1234
+ def pattern_mismatch
1235
+ return false unless @host
1236
+
1237
+ pat = host_attr_value("pattern").to_s
1238
+ return false if pat.empty?
1239
+
1240
+ v = host_value.to_s
1241
+ return false if v.empty?
1242
+
1243
+ !Regexp.new("\\A(?:#{pat})\\z").match?(v)
1244
+ rescue RegexpError
1245
+ false
1246
+ end
1247
+
1248
+ def too_long
1249
+ return false unless @host
1250
+
1251
+ max = host_attr_value("maxlength").to_s
1252
+ return false if max.empty?
1253
+
1254
+ max_n = max.to_i
1255
+ return false if max_n < 0
1256
+
1257
+ host_value.to_s.length > max_n
1258
+ end
1259
+
1260
+ def too_short
1261
+ return false unless @host
1262
+
1263
+ min = host_attr_value("minlength").to_s
1264
+ return false if min.empty?
1265
+
1266
+ min_n = min.to_i
1267
+ return false if min_n < 0
1268
+
1269
+ v = host_value.to_s
1270
+ !v.empty? && v.length < min_n
1271
+ end
1272
+
1273
+ def range_underflow
1274
+ return false unless numeric_host?
1275
+
1276
+ min = host_attr_value("min").to_s
1277
+ return false if min.empty?
1278
+
1279
+ num = numeric_value
1280
+ num && num < min.to_f
1281
+ end
1282
+
1283
+ def range_overflow
1284
+ return false unless numeric_host?
1285
+
1286
+ max = host_attr_value("max").to_s
1287
+ return false if max.empty?
1288
+
1289
+ num = numeric_value
1290
+ num && num > max.to_f
1291
+ end
1292
+
1293
+ def step_mismatch
1294
+ return false unless numeric_host?
1295
+
1296
+ step = host_attr_value("step").to_s
1297
+ return false if step.empty? || step == "any"
1298
+
1299
+ step_n = step.to_f
1300
+ return false if step_n <= 0
1301
+
1302
+ num = numeric_value
1303
+ return false unless num
1304
+
1305
+ base = host_attr_value("min").to_s
1306
+ base_n = base.empty? ? 0.0 : base.to_f
1307
+ ((num - base_n) / step_n - ((num - base_n) / step_n).round).abs > 1e-9
1308
+ end
1309
+
1310
+ # `badInput` flags input that the user agent couldn't convert to
1311
+ # the host control's expected type. For Dommy this is meaningful
1312
+ # for type=number/range (raw string not a finite float) and
1313
+ # type=color (not a #rrggbb literal).
1314
+ def bad_input
1315
+ return false unless @host
1316
+
1317
+ raw = @host.respond_to?(:raw_value) ? @host.raw_value : host_value
1318
+ raw = raw.to_s
1319
+ return false if raw.empty?
1320
+
1321
+ case host_type
1322
+ when "number", "range"
1323
+ !valid_float?(raw)
1324
+ when "color"
1325
+ !raw.strip.downcase.match?(/\A#[0-9a-f]{6}\z/)
1326
+ else
1327
+ false
1328
+ end
1329
+ end
1330
+
1331
+ def valid_float?(s)
1332
+ Float(s)
1333
+ true
1334
+ rescue ArgumentError, TypeError
1335
+ false
1336
+ end
1337
+
1338
+ def custom_error
1339
+ !custom_message.empty?
1340
+ end
1341
+
1342
+ def valid
1343
+ !(value_missing ||
1344
+ type_mismatch ||
1345
+ pattern_mismatch ||
1346
+ too_long ||
1347
+ too_short ||
1348
+ range_underflow ||
1349
+ range_overflow ||
1350
+ step_mismatch ||
1351
+ bad_input ||
1352
+ custom_error)
1353
+ end
1354
+
1355
+ # ---- Bridge protocol ----
1356
+
1357
+ def __js_get__(key)
1358
+ case key
1359
+ when "valueMissing"
1360
+ value_missing
1361
+ when "typeMismatch"
1362
+ type_mismatch
1363
+ when "patternMismatch"
1364
+ pattern_mismatch
1365
+ when "tooLong"
1366
+ too_long
1367
+ when "tooShort"
1368
+ too_short
1369
+ when "rangeUnderflow"
1370
+ range_underflow
1371
+ when "rangeOverflow"
1372
+ range_overflow
1373
+ when "stepMismatch"
1374
+ step_mismatch
1375
+ when "badInput"
1376
+ bad_input
1377
+ when "customError"
1378
+ custom_error
1379
+ when "valid"
1380
+ valid
1381
+ end
1382
+ end
1383
+
1384
+ private
1385
+
1386
+ def host_value
1387
+ return "" unless @host
1388
+
1389
+ @host.respond_to?(:value) ? @host.value : @host.__js_get__("value")
1390
+ end
1391
+
1392
+ def host_attr_value(name)
1393
+ return "" unless @host
1394
+
1395
+ @host.__node__[name].to_s
1396
+ end
1397
+
1398
+ def host_attr_present?(name)
1399
+ return false unless @host
1400
+
1401
+ @host.__node__.key?(name.to_s)
1402
+ end
1403
+
1404
+ def host_type
1405
+ return nil unless @host
1406
+
1407
+ @host.respond_to?(:type) ? @host.type : ""
1408
+ end
1409
+
1410
+ def custom_message
1411
+ return "" unless @host
1412
+
1413
+ (@host.instance_variable_get(:@custom_validity_message) || "").to_s
1414
+ end
1415
+
1416
+ def numeric_host?
1417
+ @host.is_a?(HTMLInputElement) && %w[number range].include?(host_type)
1418
+ end
1419
+
1420
+ def numeric_value
1421
+ v = host_value.to_s
1422
+ return nil if v.empty?
1423
+
1424
+ Float(v)
1425
+ rescue ArgumentError
1426
+ nil
1427
+ end
1428
+
1429
+ def truthy?(value)
1430
+ v = value.to_s
1431
+ !v.empty? && v != "false" && v != "0"
1432
+ end
1433
+ end
1434
+
1435
+ # `<option>` — value, label, selected, disabled, text, index, form.
1436
+ class HTMLOptionElement < HTMLElement
1437
+ def value
1438
+ # Per spec, value defaults to text content if the `value`
1439
+ # attribute is absent.
1440
+ @__node__.key?("value") ? @__node__["value"].to_s : text_content
1441
+ end
1442
+
1443
+ def value=(v)
1444
+ set_reflected_string("value", v)
1445
+ end
1446
+
1447
+ def label
1448
+ @__node__.key?("label") ? @__node__["label"].to_s : text_content
1449
+ end
1450
+
1451
+ def label=(v)
1452
+ set_reflected_string("label", v)
1453
+ end
1454
+
1455
+ def selected
1456
+ reflected_boolean("selected")
1457
+ end
1458
+
1459
+ def selected=(v)
1460
+ set_reflected_boolean("selected", v)
1461
+ end
1462
+
1463
+ def default_selected
1464
+ selected
1465
+ end
1466
+
1467
+ def default_selected=(v)
1468
+ self.selected = v
1469
+ end
1470
+
1471
+ def disabled
1472
+ reflected_boolean("disabled")
1473
+ end
1474
+
1475
+ def disabled=(v)
1476
+ set_reflected_boolean("disabled", v)
1477
+ end
1478
+
1479
+ def text
1480
+ text_content
1481
+ end
1482
+
1483
+ def text=(v)
1484
+ self.text_content = v
1485
+ end
1486
+
1487
+ def form
1488
+ closest("form")
1489
+ end
1490
+
1491
+ # `index` — position within the containing select's options list.
1492
+ def index
1493
+ sel = closest("select")
1494
+ return 0 unless sel
1495
+
1496
+ sel.options.find_index { |o| o.__node__ == @__node__ } || 0
1497
+ end
1498
+
1499
+ def __js_get__(key)
1500
+ case key
1501
+ when "value"
1502
+ value
1503
+ when "label"
1504
+ label
1505
+ when "selected"
1506
+ selected
1507
+ when "defaultSelected"
1508
+ default_selected
1509
+ when "disabled"
1510
+ disabled
1511
+ when "text"
1512
+ text
1513
+ when "form"
1514
+ form
1515
+ when "index"
1516
+ index
1517
+ else
1518
+ super
1519
+ end
1520
+ end
1521
+
1522
+ def __js_set__(key, v)
1523
+ case key
1524
+ when "value"
1525
+ self.value = v
1526
+ when "label"
1527
+ self.label = v
1528
+ when "selected", "defaultSelected"
1529
+ self.selected = v
1530
+ when "disabled"
1531
+ self.disabled = v
1532
+ when "text"
1533
+ self.text = v
1534
+ else
1535
+ super
1536
+ end
1537
+ end
1538
+ end
1539
+
1540
+ # `<optgroup>` — label + disabled, container for options.
1541
+ class HTMLOptGroupElement < HTMLElement
1542
+ def label
1543
+ reflected_string("label")
1544
+ end
1545
+
1546
+ def label=(v)
1547
+ set_reflected_string("label", v)
1548
+ end
1549
+
1550
+ def disabled
1551
+ reflected_boolean("disabled")
1552
+ end
1553
+
1554
+ def disabled=(v)
1555
+ set_reflected_boolean("disabled", v)
1556
+ end
1557
+
1558
+ def __js_get__(key)
1559
+ case key
1560
+ when "label"
1561
+ label
1562
+ when "disabled"
1563
+ disabled
1564
+ else
1565
+ super
1566
+ end
1567
+ end
1568
+
1569
+ def __js_set__(key, v)
1570
+ case key
1571
+ when "label"
1572
+ self.label = v
1573
+ when "disabled"
1574
+ self.disabled = v
1575
+ else
1576
+ super
1577
+ end
1578
+ end
1579
+ end
1580
+
1581
+ # `<textarea>` — multi-line text input.
1582
+ class HTMLTextAreaElement < HTMLElement
1583
+ def value
1584
+ @__node__["value"] || text_content
1585
+ end
1586
+
1587
+ def value=(v)
1588
+ @__node__["value"] = v.to_s
1589
+ self.text_content = v.to_s
1590
+ end
1591
+
1592
+ def default_value
1593
+ text_content
1594
+ end
1595
+
1596
+ def default_value=(v)
1597
+ self.text_content = v
1598
+ end
1599
+
1600
+ def name
1601
+ reflected_string("name")
1602
+ end
1603
+
1604
+ def name=(v)
1605
+ set_reflected_string("name", v)
1606
+ end
1607
+
1608
+ def placeholder
1609
+ reflected_string("placeholder")
1610
+ end
1611
+
1612
+ def placeholder=(v)
1613
+ set_reflected_string("placeholder", v)
1614
+ end
1615
+
1616
+ def rows
1617
+ (@__node__["rows"] || "2").to_i
1618
+ end
1619
+
1620
+ def rows=(v)
1621
+ set_reflected_string("rows", v.to_s)
1622
+ end
1623
+
1624
+ def cols
1625
+ (@__node__["cols"] || "20").to_i
1626
+ end
1627
+
1628
+ def cols=(v)
1629
+ set_reflected_string("cols", v.to_s)
1630
+ end
1631
+
1632
+ def wrap
1633
+ reflected_string("wrap")
1634
+ end
1635
+
1636
+ def max_length
1637
+ (@__node__["maxlength"] || "-1").to_i
1638
+ end
1639
+
1640
+ def min_length
1641
+ (@__node__["minlength"] || "-1").to_i
1642
+ end
1643
+
1644
+ def text_length
1645
+ value.length
1646
+ end
1647
+
1648
+ def autocomplete
1649
+ reflected_string("autocomplete")
1650
+ end
1651
+
1652
+ def type
1653
+ "textarea"
1654
+ end
1655
+
1656
+ def form
1657
+ closest("form")
1658
+ end
1659
+
1660
+ def labels
1661
+ return [] if id.empty?
1662
+
1663
+ @document.query_selector_all("label[for='#{id}']")
1664
+ end
1665
+
1666
+ # No real selection — same stub story as input.
1667
+ def select
1668
+ nil
1669
+ end
1670
+
1671
+ def set_selection_range(_s, _e, _direction = nil)
1672
+ nil
1673
+ end
1674
+
1675
+ def set_range_text(_replacement, *_)
1676
+ nil
1677
+ end
1678
+
1679
+ def validity
1680
+ @__validity ||= ValidityState.new(self)
1681
+ end
1682
+
1683
+ def will_validate
1684
+ !reflected_boolean("disabled") && !reflected_boolean("readonly")
1685
+ end
1686
+
1687
+ def validation_message
1688
+ return "" unless will_validate
1689
+
1690
+ msg = (@custom_validity_message || "").to_s
1691
+ return msg unless msg.empty?
1692
+ return "Please fill out this field." if validity.value_missing
1693
+
1694
+ ""
1695
+ end
1696
+
1697
+ def check_validity
1698
+ ok = !will_validate || validity.valid
1699
+ dispatch_event(Event.new("invalid", "bubbles" => false, "cancelable" => true)) unless ok
1700
+ ok
1701
+ end
1702
+
1703
+ def report_validity
1704
+ check_validity
1705
+ end
1706
+
1707
+ def set_custom_validity(msg)
1708
+ @custom_validity_message = msg.to_s
1709
+ nil
1710
+ end
1711
+
1712
+ def __js_get__(key)
1713
+ case key
1714
+ when "value"
1715
+ value
1716
+ when "defaultValue"
1717
+ default_value
1718
+ when "name"
1719
+ name
1720
+ when "placeholder"
1721
+ placeholder
1722
+ when "rows"
1723
+ rows
1724
+ when "cols"
1725
+ cols
1726
+ when "wrap"
1727
+ wrap
1728
+ when "maxLength"
1729
+ max_length
1730
+ when "minLength"
1731
+ min_length
1732
+ when "textLength"
1733
+ text_length
1734
+ when "autocomplete"
1735
+ autocomplete
1736
+ when "type"
1737
+ type
1738
+ when "form"
1739
+ form
1740
+ when "labels"
1741
+ labels
1742
+ when "validity"
1743
+ validity
1744
+ when "willValidate"
1745
+ will_validate
1746
+ when "validationMessage"
1747
+ validation_message
1748
+ else
1749
+ super
1750
+ end
1751
+ end
1752
+
1753
+ def __js_set__(key, v)
1754
+ case key
1755
+ when "value"
1756
+ self.value = v
1757
+ when "defaultValue"
1758
+ self.default_value = v
1759
+ when "name"
1760
+ set_reflected_string("name", v)
1761
+ when "placeholder"
1762
+ set_reflected_string("placeholder", v)
1763
+ when "rows"
1764
+ self.rows = v
1765
+ when "cols"
1766
+ self.cols = v
1767
+ when "wrap"
1768
+ set_reflected_string("wrap", v)
1769
+ when "maxLength"
1770
+ set_reflected_string("maxlength", v.to_s)
1771
+ when "minLength"
1772
+ set_reflected_string("minlength", v.to_s)
1773
+ else
1774
+ super
1775
+ end
1776
+ end
1777
+
1778
+ def __js_call__(method, args)
1779
+ case method
1780
+ when "select"
1781
+ select
1782
+ when "setSelectionRange"
1783
+ set_selection_range(args[0], args[1], args[2])
1784
+ when "setRangeText"
1785
+ set_range_text(args[0])
1786
+ when "checkValidity"
1787
+ check_validity
1788
+ when "reportValidity"
1789
+ report_validity
1790
+ when "setCustomValidity"
1791
+ set_custom_validity(args[0])
1792
+ else
1793
+ super
1794
+ end
1795
+ end
1796
+ # end HTMLTextAreaElement
1797
+ end
1798
+
1799
+ # `<label>` — `htmlFor` IDL maps to the HTML `for` attribute;
1800
+ # `control` returns the labelled form control.
1801
+ class HTMLLabelElement < HTMLElement
1802
+ def html_for
1803
+ reflected_string("for")
1804
+ end
1805
+
1806
+ def html_for=(v)
1807
+ set_reflected_string("for", v)
1808
+ end
1809
+
1810
+ # `label.control` — the form control associated with this label.
1811
+ # Priority: explicit `for=`, then first form control descendant.
1812
+ def control
1813
+ target = html_for
1814
+ if !target.empty?
1815
+ @document.get_element_by_id(target)
1816
+ else
1817
+ query_selector("input, select, textarea, button, output, meter, progress")
1818
+ end
1819
+ end
1820
+
1821
+ def form
1822
+ closest("form")
1823
+ end
1824
+
1825
+ def __js_get__(key)
1826
+ case key
1827
+ when "htmlFor"
1828
+ html_for
1829
+ when "control"
1830
+ control
1831
+ when "form"
1832
+ form
1833
+ else
1834
+ super
1835
+ end
1836
+ end
1837
+
1838
+ def __js_set__(key, v)
1839
+ case key
1840
+ when "htmlFor"
1841
+ self.html_for = v
1842
+ else
1843
+ super
1844
+ end
1845
+ end
1846
+ end
1847
+
1848
+ # `<fieldset>` — disabled-state-propagating wrapper; exposes
1849
+ # `elements` collection like form.
1850
+ class HTMLFieldsetElement < HTMLElement
1851
+ def name
1852
+ reflected_string("name")
1853
+ end
1854
+
1855
+ def name=(v)
1856
+ set_reflected_string("name", v)
1857
+ end
1858
+
1859
+ def disabled
1860
+ reflected_boolean("disabled")
1861
+ end
1862
+
1863
+ def disabled=(v)
1864
+ set_reflected_boolean("disabled", v)
1865
+ end
1866
+
1867
+ def type
1868
+ "fieldset"
1869
+ end
1870
+
1871
+ def form
1872
+ closest("form")
1873
+ end
1874
+
1875
+ def elements
1876
+ el = self
1877
+ HTMLCollection.new do
1878
+ el
1879
+ .__node__
1880
+ .css("input, select, textarea, button, output, fieldset")
1881
+ .map do |n|
1882
+ el.document.wrap_node(n)
1883
+ end
1884
+ .compact
1885
+ end
1886
+ end
1887
+
1888
+ def validity
1889
+ ValidityState.new
1890
+ end
1891
+
1892
+ def check_validity
1893
+ true
1894
+ end
1895
+
1896
+ def report_validity
1897
+ true
1898
+ end
1899
+
1900
+ def __js_get__(key)
1901
+ case key
1902
+ when "name"
1903
+ name
1904
+ when "disabled"
1905
+ disabled
1906
+ when "type"
1907
+ type
1908
+ when "form"
1909
+ form
1910
+ when "elements"
1911
+ elements
1912
+ when "validity"
1913
+ validity
1914
+ else
1915
+ super
1916
+ end
1917
+ end
1918
+
1919
+ def __js_set__(key, v)
1920
+ case key
1921
+ when "name"
1922
+ self.name = v
1923
+ when "disabled"
1924
+ self.disabled = v
1925
+ else
1926
+ super
1927
+ end
1928
+ end
1929
+ end
1930
+
1931
+ # `<output>` — calculation result element.
1932
+ class HTMLOutputElement < HTMLElement
1933
+ def value
1934
+ text_content
1935
+ end
1936
+
1937
+ def value=(v)
1938
+ self.text_content = v
1939
+ end
1940
+
1941
+ def default_value
1942
+ text_content
1943
+ end
1944
+
1945
+ def default_value=(v)
1946
+ self.text_content = v
1947
+ end
1948
+
1949
+ def name
1950
+ reflected_string("name")
1951
+ end
1952
+
1953
+ def name=(v)
1954
+ set_reflected_string("name", v)
1955
+ end
1956
+
1957
+ # `for` attribute is a space-separated list of IDs.
1958
+ def html_for_tokens
1959
+ reflected_string("for").split(/\s+/).reject(&:empty?)
1960
+ end
1961
+
1962
+ def form
1963
+ closest("form")
1964
+ end
1965
+
1966
+ def labels
1967
+ return [] if id.empty?
1968
+
1969
+ @document.query_selector_all("label[for='#{id}']")
1970
+ end
1971
+
1972
+ def type
1973
+ "output"
1974
+ end
1975
+
1976
+ def validity
1977
+ ValidityState.new
1978
+ end
1979
+
1980
+ def check_validity
1981
+ true
1982
+ end
1983
+
1984
+ def report_validity
1985
+ true
1986
+ end
1987
+
1988
+ def __js_get__(key)
1989
+ case key
1990
+ when "value"
1991
+ value
1992
+ when "defaultValue"
1993
+ default_value
1994
+ when "name"
1995
+ name
1996
+ when "type"
1997
+ type
1998
+ when "form"
1999
+ form
2000
+ when "labels"
2001
+ labels
2002
+ when "validity"
2003
+ validity
2004
+ when "htmlFor"
2005
+ reflected_string("for")
2006
+ else
2007
+ super
2008
+ end
2009
+ end
2010
+
2011
+ def __js_set__(key, v)
2012
+ case key
2013
+ when "value"
2014
+ self.value = v
2015
+ when "defaultValue"
2016
+ self.default_value = v
2017
+ when "name"
2018
+ self.name = v
2019
+ when "htmlFor"
2020
+ set_reflected_string("for", v)
2021
+ else
2022
+ super
2023
+ end
2024
+ end
2025
+ end
2026
+
2027
+ # `<legend>` — primarily exposes its `form` back-ref.
2028
+ class HTMLLegendElement < HTMLElement
2029
+ def form
2030
+ fieldset = closest("fieldset")
2031
+ fieldset&.closest("form") || closest("form")
2032
+ end
2033
+
2034
+ def __js_get__(key)
2035
+ key == "form" ? form : super
2036
+ end
2037
+ end
2038
+
2039
+ # `<slot>` — composes light DOM into the shadow tree. Light DOM
2040
+ # children of the shadow's host get assigned to slots: those whose
2041
+ # `slot=name` attribute matches a named slot, or those without a
2042
+ # `slot` attribute go to the unnamed default slot. If nothing is
2043
+ # assigned, the slot's own children render as fallback content.
2044
+ class HTMLSlotElement < HTMLElement
2045
+ def name
2046
+ reflected_string("name")
2047
+ end
2048
+
2049
+ def name=(v)
2050
+ set_reflected_string("name", v)
2051
+ end
2052
+
2053
+ # `slot.assignedNodes({ flatten: true|false })` — returns the
2054
+ # light DOM children currently composed into this slot. With
2055
+ # `flatten: true` and no assigned nodes, falls back to the
2056
+ # slot's own children (the default content).
2057
+ def assigned_nodes(options = nil)
2058
+ flatten = options.is_a?(Hash) ? (options["flatten"] || options[:flatten]) : false
2059
+ nodes = matching_light_nodes
2060
+ if nodes.empty? && flatten
2061
+ @__node__.children.map { |n| @document.wrap_node(n) }.compact
2062
+ else
2063
+ nodes
2064
+ end
2065
+ end
2066
+
2067
+ def assigned_elements(options = nil)
2068
+ assigned_nodes(options).select { |n| n.is_a?(Element) }
2069
+ end
2070
+
2071
+ # `slot.assign(...)` — manual assignment (honored only when the
2072
+ # owning shadow uses `slotAssignment: "manual"`). We accept the
2073
+ # call and fire `slotchange` in both modes; named mode simply
2074
+ # ignores the override.
2075
+ def assign(*nodes)
2076
+ @__manual_assignment = nodes.flatten.select { |n| n.respond_to?(:__node__) }
2077
+ dispatch_event(Event.new("slotchange", "bubbles" => true))
2078
+ nil
2079
+ end
2080
+
2081
+ def __js_get__(key)
2082
+ case key
2083
+ when "name"
2084
+ name
2085
+ when "assignedNodes"
2086
+ assigned_nodes
2087
+ when "assignedElements"
2088
+ assigned_elements
2089
+ else
2090
+ super
2091
+ end
2092
+ end
2093
+
2094
+ def __js_set__(key, value)
2095
+ case key
2096
+ when "name"
2097
+ self.name = value
2098
+ else
2099
+ super
2100
+ end
2101
+ end
2102
+
2103
+ def __js_call__(method, args)
2104
+ case method
2105
+ when "assignedNodes"
2106
+ assigned_nodes(args[0])
2107
+ when "assignedElements"
2108
+ assigned_elements(args[0])
2109
+ when "assign"
2110
+ assign(*args)
2111
+ else
2112
+ super
2113
+ end
2114
+ end
2115
+
2116
+ private
2117
+
2118
+ def matching_light_nodes
2119
+ sr = @document.__shadow_root_containing__(@__node__)
2120
+ return [] unless sr
2121
+
2122
+ host = sr.host
2123
+ return [] unless host
2124
+
2125
+ slot_name = name
2126
+ # Manual mode honors the explicit list.
2127
+ if sr.slot_assignment == "manual" && @__manual_assignment
2128
+ return @__manual_assignment
2129
+ end
2130
+
2131
+ host
2132
+ .__node__
2133
+ .children
2134
+ .map do |child|
2135
+ wrapped = @document.wrap_node(child)
2136
+ next nil unless wrapped
2137
+
2138
+ attr_value = child.element? ? child["slot"].to_s : ""
2139
+ if slot_name.empty?
2140
+ attr_value.empty? ? wrapped : nil
2141
+ else
2142
+ (child.element? && attr_value == slot_name) ? wrapped : nil
2143
+ end
2144
+ end
2145
+ .compact
2146
+ end
2147
+ end
2148
+
2149
+ # `<select>` — exposes `value` (selected option's value), `options`,
2150
+ # `selectedIndex`, and dispatches change events. Minimal compared to
2151
+ # happy-dom's full HTMLSelectElement, but covers common test cases.
2152
+ class HTMLSelectElement < HTMLElement
2153
+ def name
2154
+ reflected_string("name")
2155
+ end
2156
+
2157
+ def name=(v)
2158
+ set_reflected_string("name", v)
2159
+ end
2160
+
2161
+ def multiple
2162
+ reflected_boolean("multiple")
2163
+ end
2164
+
2165
+ def multiple=(v)
2166
+ set_reflected_boolean("multiple", v)
2167
+ end
2168
+
2169
+ def size
2170
+ @__node__["size"].to_s.to_i
2171
+ end
2172
+
2173
+ # `options` — all <option> descendants (including those inside
2174
+ # <optgroup>). Live HTMLOptionsCollection (HTMLCollection +
2175
+ # add/remove/selectedIndex/length= helpers).
2176
+ def options
2177
+ el = self
2178
+ HTMLOptionsCollection.new(self) do
2179
+ el.__node__.css("option").map { |n| el.document.wrap_node(n) }.compact
2180
+ end
2181
+ end
2182
+
2183
+ # `selectedOptions` — live collection of options with `selected`
2184
+ # attribute. When nothing is explicitly selected, browsers fall
2185
+ # back to the first option for non-multiple selects.
2186
+ def selected_options
2187
+ el = self
2188
+ HTMLCollection.new do
2189
+ opts = el.__node__.css("option").map { |n| el.document.wrap_node(n) }.compact
2190
+ chosen = opts.select { |o| o.__node__.key?("selected") }
2191
+ next chosen unless chosen.empty?
2192
+ next [] if el.multiple
2193
+
2194
+ opts.first ? [opts.first] : []
2195
+ end
2196
+ end
2197
+
2198
+ def length
2199
+ options.size
2200
+ end
2201
+
2202
+ def form
2203
+ closest("form")
2204
+ end
2205
+
2206
+ # `selectedIndex` — first option with `selected`, or 0 if none and
2207
+ # not multiple, or -1 if multiple and none.
2208
+ def selected_index
2209
+ opts = options
2210
+ idx = opts.find_index { |o| o.__node__.key?("selected") }
2211
+ return idx if idx
2212
+
2213
+ multiple ? -1 : (opts.empty? ? -1 : 0)
2214
+ end
2215
+
2216
+ def selected_index=(i)
2217
+ opts = options
2218
+ opts.each_with_index do |o, idx|
2219
+ if idx == i.to_i
2220
+ o.set_attribute("selected", "")
2221
+ elsif o.__node__.key?("selected")
2222
+ o.remove_attribute("selected")
2223
+ end
2224
+ end
2225
+ end
2226
+
2227
+ # `value` of the select = value of the selected option, or "".
2228
+ def value
2229
+ opts = options
2230
+ sel = opts.find { |o| o.__node__.key?("selected") } || opts.first
2231
+ sel ? (sel.__node__["value"] || sel.text_content).to_s : ""
2232
+ end
2233
+
2234
+ def value=(new_value)
2235
+ target = options.find { |o| (o.__node__["value"] || o.text_content).to_s == new_value.to_s }
2236
+ return unless target
2237
+
2238
+ options.each { |o| o.remove_attribute("selected") if o.__node__.key?("selected") }
2239
+ target.set_attribute("selected", "")
2240
+ end
2241
+
2242
+ # `select.item(i)` — returns the option at index i.
2243
+ def item(i)
2244
+ options[i.to_i]
2245
+ end
2246
+
2247
+ # `select.add(option, before)` — appends or inserts before `before`.
2248
+ def add(option, before = nil)
2249
+ return nil unless option.respond_to?(:__node__)
2250
+
2251
+ if before.respond_to?(:__node__)
2252
+ insert_before(option, before)
2253
+ else
2254
+ append_child(option)
2255
+ end
2256
+
2257
+ nil
2258
+ end
2259
+
2260
+ # `select.remove(i)` — removes the option at index i. (Note: also
2261
+ # inherits `remove()` from ChildNode for self-removal; spec lets
2262
+ # both forms coexist via overloading.)
2263
+ def remove_option(i)
2264
+ target = options[i.to_i]
2265
+ target&.remove
2266
+ end
2267
+
2268
+ def labels
2269
+ return [] if id.empty?
2270
+
2271
+ @document.query_selector_all("label[for='#{id}']")
2272
+ end
2273
+
2274
+ def type
2275
+ multiple ? "select-multiple" : "select-one"
2276
+ end
2277
+
2278
+ def validity
2279
+ @__validity ||= ValidityState.new(self)
2280
+ end
2281
+
2282
+ def will_validate
2283
+ !reflected_boolean("disabled")
2284
+ end
2285
+
2286
+ def validation_message
2287
+ return "" unless will_validate
2288
+
2289
+ msg = (@custom_validity_message || "").to_s
2290
+ return msg unless msg.empty?
2291
+ return "Please select an item in the list." if validity.value_missing
2292
+
2293
+ ""
2294
+ end
2295
+
2296
+ def check_validity
2297
+ ok = !will_validate || validity.valid
2298
+ dispatch_event(Event.new("invalid", "bubbles" => false, "cancelable" => true)) unless ok
2299
+ ok
2300
+ end
2301
+
2302
+ def report_validity
2303
+ check_validity
2304
+ end
2305
+
2306
+ def set_custom_validity(msg)
2307
+ @custom_validity_message = msg.to_s
2308
+ nil
2309
+ end
2310
+
2311
+ def __js_get__(key)
2312
+ case key
2313
+ when "options"
2314
+ options
2315
+ when "length"
2316
+ length
2317
+ when "value"
2318
+ value
2319
+ when "name"
2320
+ name
2321
+ when "multiple"
2322
+ multiple
2323
+ when "size"
2324
+ size
2325
+ when "selectedIndex"
2326
+ selected_index
2327
+ when "form"
2328
+ form
2329
+ when "labels"
2330
+ labels
2331
+ when "type"
2332
+ type
2333
+ when "validity"
2334
+ validity
2335
+ when "willValidate"
2336
+ will_validate
2337
+ when "validationMessage"
2338
+ validation_message
2339
+ else
2340
+ super
2341
+ end
2342
+ end
2343
+
2344
+ def __js_set__(key, val)
2345
+ case key
2346
+ when "value"
2347
+ self.value = val
2348
+ when "name"
2349
+ set_reflected_string("name", val)
2350
+ when "multiple"
2351
+ set_reflected_boolean("multiple", val)
2352
+ when "selectedIndex"
2353
+ self.selected_index = val
2354
+ else
2355
+ super
2356
+ end
2357
+ end
2358
+
2359
+ def __js_call__(method, args)
2360
+ case method
2361
+ when "item"
2362
+ item(args[0])
2363
+ when "add"
2364
+ add(args[0], args[1])
2365
+ when "checkValidity"
2366
+ check_validity
2367
+ when "reportValidity"
2368
+ report_validity
2369
+ when "setCustomValidity"
2370
+ set_custom_validity(args[0])
2371
+ else
2372
+ super
2373
+ end
2374
+ end
2375
+ end
2376
+
2377
+ # `<dialog>` — `open` reflected boolean, `show()` / `showModal()` /
2378
+ # `close(returnValue?)`. Dommy has no modal stack, so showModal is
2379
+ # functionally identical to show (no backdrop, no escape-to-close).
2380
+ class HTMLDialogElement < HTMLElement
2381
+ def open
2382
+ reflected_boolean("open")
2383
+ end
2384
+
2385
+ def open=(v)
2386
+ set_reflected_boolean("open", v)
2387
+ end
2388
+
2389
+ def return_value
2390
+ @return_value ||= ""
2391
+ end
2392
+
2393
+ def return_value=(v)
2394
+ @return_value = v.to_s
2395
+ end
2396
+
2397
+ def show
2398
+ self.open = true
2399
+ nil
2400
+ end
2401
+
2402
+ def show_modal
2403
+ self.open = true
2404
+ nil
2405
+ end
2406
+
2407
+ def close(value = nil)
2408
+ self.open = false
2409
+ @return_value = value.to_s unless value.nil?
2410
+ dispatch_event(Event.new("close", "bubbles" => false, "cancelable" => false))
2411
+ nil
2412
+ end
2413
+
2414
+ def __js_get__(key)
2415
+ case key
2416
+ when "open"
2417
+ open
2418
+ when "returnValue"
2419
+ return_value
2420
+ else
2421
+ super
2422
+ end
2423
+ end
2424
+
2425
+ def __js_set__(key, value)
2426
+ case key
2427
+ when "open"
2428
+ self.open = value
2429
+ when "returnValue"
2430
+ self.return_value = value
2431
+ else
2432
+ super
2433
+ end
2434
+ end
2435
+
2436
+ def __js_call__(method, args)
2437
+ case method
2438
+ when "show"
2439
+ show
2440
+ when "showModal"
2441
+ show_modal
2442
+ when "close"
2443
+ close(args[0])
2444
+ else
2445
+ super
2446
+ end
2447
+ end
2448
+ end
2449
+
2450
+ # `<details>` — `open` reflected boolean. Toggling it fires a
2451
+ # `toggle` event (non-bubbling per spec).
2452
+ class HTMLDetailsElement < HTMLElement
2453
+ def open
2454
+ reflected_boolean("open")
2455
+ end
2456
+
2457
+ def open=(v)
2458
+ was = open
2459
+ set_reflected_boolean("open", v)
2460
+ now = open
2461
+ return if was == now
2462
+
2463
+ dispatch_event(Event.new("toggle", "bubbles" => false, "cancelable" => false))
2464
+ end
2465
+
2466
+ def __js_get__(key)
2467
+ key == "open" ? open : super
2468
+ end
2469
+
2470
+ def __js_set__(key, value)
2471
+ if key == "open"
2472
+ self.open = value
2473
+ else
2474
+ super
2475
+ end
2476
+ end
2477
+ end
2478
+
2479
+ # `<meter>` — gauge with `value` / `min` / `max` (default 0/0/1)
2480
+ # plus `low` / `high` / `optimum`. All numeric; `labels` via the
2481
+ # standard `<label for="...">` association.
2482
+ class HTMLMeterElement < HTMLElement
2483
+ def value
2484
+ numeric_attr("value", 0.0)
2485
+ end
2486
+
2487
+ def value=(v)
2488
+ set_reflected_string("value", v.to_s)
2489
+ end
2490
+
2491
+ def min
2492
+ numeric_attr("min", 0.0)
2493
+ end
2494
+
2495
+ def min=(v)
2496
+ set_reflected_string("min", v.to_s)
2497
+ end
2498
+
2499
+ def max
2500
+ numeric_attr("max", 1.0)
2501
+ end
2502
+
2503
+ def max=(v)
2504
+ set_reflected_string("max", v.to_s)
2505
+ end
2506
+
2507
+ def low
2508
+ numeric_attr("low", min)
2509
+ end
2510
+
2511
+ def low=(v)
2512
+ set_reflected_string("low", v.to_s)
2513
+ end
2514
+
2515
+ def high
2516
+ numeric_attr("high", max)
2517
+ end
2518
+
2519
+ def high=(v)
2520
+ set_reflected_string("high", v.to_s)
2521
+ end
2522
+
2523
+ def optimum
2524
+ numeric_attr("optimum", (min + max) / 2.0)
2525
+ end
2526
+
2527
+ def optimum=(v)
2528
+ set_reflected_string("optimum", v.to_s)
2529
+ end
2530
+
2531
+ def labels
2532
+ return [] if id.empty?
2533
+
2534
+ @document.query_selector_all("label[for='#{id}']")
2535
+ end
2536
+
2537
+ def __js_get__(key)
2538
+ case key
2539
+ when "value"
2540
+ value
2541
+ when "min"
2542
+ min
2543
+ when "max"
2544
+ max
2545
+ when "low"
2546
+ low
2547
+ when "high"
2548
+ high
2549
+ when "optimum"
2550
+ optimum
2551
+ when "labels"
2552
+ labels
2553
+ else
2554
+ super
2555
+ end
2556
+ end
2557
+
2558
+ def __js_set__(key, v)
2559
+ case key
2560
+ when "value", "min", "max", "low", "high", "optimum"
2561
+ set_reflected_string(key, v.to_s)
2562
+ else
2563
+ super
2564
+ end
2565
+ end
2566
+
2567
+ private
2568
+
2569
+ def numeric_attr(name, default)
2570
+ raw = @__node__[name].to_s
2571
+ raw.empty? ? default : Float(raw) rescue default
2572
+ end
2573
+ end
2574
+
2575
+ # `<progress>` — `value` and `max` (default max=1). `position`
2576
+ # returns `value / max` for a "determinate" progress bar, or -1
2577
+ # when no value is set ("indeterminate").
2578
+ class HTMLProgressElement < HTMLElement
2579
+ def value
2580
+ raw = @__node__["value"].to_s
2581
+ raw.empty? ? nil : Float(raw)
2582
+ rescue ArgumentError
2583
+ nil
2584
+ end
2585
+
2586
+ def value=(v)
2587
+ set_reflected_string("value", v.to_s)
2588
+ end
2589
+
2590
+ def max
2591
+ raw = @__node__["max"].to_s
2592
+ raw.empty? ? 1.0 : (Float(raw) rescue 1.0)
2593
+ end
2594
+
2595
+ def max=(v)
2596
+ set_reflected_string("max", v.to_s)
2597
+ end
2598
+
2599
+ # `position` = value/max for determinate progress; -1 if value
2600
+ # was never set (indeterminate).
2601
+ def position
2602
+ v = value
2603
+ return -1.0 if v.nil?
2604
+
2605
+ m = max
2606
+ m <= 0 ? 1.0 : (v / m)
2607
+ end
2608
+
2609
+ def labels
2610
+ return [] if id.empty?
2611
+
2612
+ @document.query_selector_all("label[for='#{id}']")
2613
+ end
2614
+
2615
+ def __js_get__(key)
2616
+ case key
2617
+ when "value"
2618
+ value
2619
+ when "max"
2620
+ max
2621
+ when "position"
2622
+ position
2623
+ when "labels"
2624
+ labels
2625
+ else
2626
+ super
2627
+ end
2628
+ end
2629
+
2630
+ def __js_set__(key, v)
2631
+ case key
2632
+ when "value", "max"
2633
+ set_reflected_string(key, v.to_s)
2634
+ else
2635
+ super
2636
+ end
2637
+ end
2638
+ end
2639
+
2640
+ # `<template>` — `content` returns the DocumentFragment that
2641
+ # owns the template's children. Reuses the document-level
2642
+ # template_content storage so existing template handling stays
2643
+ # consistent.
2644
+ class HTMLTemplateElement < HTMLElement
2645
+ def content
2646
+ @document.template_content_fragment(self)
2647
+ end
2648
+
2649
+ def __js_get__(key)
2650
+ case key
2651
+ when "content"
2652
+ content
2653
+ else
2654
+ super
2655
+ end
2656
+ end
2657
+ end
2658
+
2659
+ # `<td>` / `<th>` — single table cell. `cellIndex` is the
2660
+ # position within the parent row's cells collection.
2661
+ class HTMLTableCellElement < HTMLElement
2662
+ def cell_index
2663
+ row = closest("tr")
2664
+ return -1 unless row
2665
+
2666
+ row.cells.find_index { |c| c.__node__ == @__node__ } || -1
2667
+ end
2668
+
2669
+ def col_span
2670
+ (@__node__["colspan"] || "1").to_i
2671
+ end
2672
+
2673
+ def col_span=(v)
2674
+ set_reflected_string("colspan", v.to_s)
2675
+ end
2676
+
2677
+ def row_span
2678
+ (@__node__["rowspan"] || "1").to_i
2679
+ end
2680
+
2681
+ def row_span=(v)
2682
+ set_reflected_string("rowspan", v.to_s)
2683
+ end
2684
+
2685
+ def headers
2686
+ reflected_string("headers")
2687
+ end
2688
+
2689
+ def headers=(v)
2690
+ set_reflected_string("headers", v)
2691
+ end
2692
+
2693
+ # `scope` / `abbr` are only meaningful on `<th>`, but the IDL
2694
+ # exposes them on the cell element either way.
2695
+ def scope
2696
+ reflected_string("scope")
2697
+ end
2698
+
2699
+ def scope=(v)
2700
+ set_reflected_string("scope", v)
2701
+ end
2702
+
2703
+ def abbr
2704
+ reflected_string("abbr")
2705
+ end
2706
+
2707
+ def abbr=(v)
2708
+ set_reflected_string("abbr", v)
2709
+ end
2710
+
2711
+ def __js_get__(key)
2712
+ case key
2713
+ when "cellIndex"
2714
+ cell_index
2715
+ when "colSpan"
2716
+ col_span
2717
+ when "rowSpan"
2718
+ row_span
2719
+ when "headers"
2720
+ headers
2721
+ when "scope"
2722
+ scope
2723
+ when "abbr"
2724
+ abbr
2725
+ else
2726
+ super
2727
+ end
2728
+ end
2729
+
2730
+ def __js_set__(key, value)
2731
+ case key
2732
+ when "colSpan"
2733
+ self.col_span = value
2734
+ when "rowSpan"
2735
+ self.row_span = value
2736
+ when "headers"
2737
+ self.headers = value
2738
+ when "scope"
2739
+ self.scope = value
2740
+ when "abbr"
2741
+ self.abbr = value
2742
+ else
2743
+ super
2744
+ end
2745
+ end
2746
+ end
2747
+
2748
+ # `<tr>` — table row. `cells` are direct `<td>`/`<th>` children.
2749
+ # `rowIndex` walks the enclosing table; `sectionRowIndex` walks
2750
+ # the enclosing thead/tbody/tfoot.
2751
+ class HTMLTableRowElement < HTMLElement
2752
+ def cells
2753
+ el = self
2754
+ HTMLCollection.new do
2755
+ el
2756
+ .__node__
2757
+ .element_children
2758
+ .select { |n| %w[td th].include?(n.name) }
2759
+ .map { |n| el.document.wrap_node(n) }
2760
+ .compact
2761
+ end
2762
+ end
2763
+
2764
+ def row_index
2765
+ table = closest("table")
2766
+ return -1 unless table
2767
+
2768
+ table.rows.find_index { |r| r.__node__ == @__node__ } || -1
2769
+ end
2770
+
2771
+ def section_row_index
2772
+ section = @__node__.parent
2773
+ return -1 unless section && section.element? && %w[thead tbody tfoot].include?(section.name)
2774
+
2775
+ section.element_children.select { |n| n.name == "tr" }.find_index { |n| n == @__node__ } || -1
2776
+ end
2777
+
2778
+ # `insertCell(index)` — adds a `<td>` at the given index
2779
+ # (defaults to end). Returns the new cell.
2780
+ def insert_cell(index = -1)
2781
+ cell = @document.create_element("td")
2782
+ list = cells
2783
+ if index.to_i == -1 || index.to_i >= list.size
2784
+ append_child(cell)
2785
+ else
2786
+ insert_before(cell, list[index.to_i])
2787
+ end
2788
+
2789
+ cell
2790
+ end
2791
+
2792
+ def delete_cell(index)
2793
+ target = cells[index.to_i]
2794
+ target&.remove
2795
+ nil
2796
+ end
2797
+
2798
+ def __js_get__(key)
2799
+ case key
2800
+ when "cells"
2801
+ cells
2802
+ when "rowIndex"
2803
+ row_index
2804
+ when "sectionRowIndex"
2805
+ section_row_index
2806
+ else
2807
+ super
2808
+ end
2809
+ end
2810
+
2811
+ def __js_call__(method, args)
2812
+ case method
2813
+ when "insertCell"
2814
+ insert_cell(args[0] || -1)
2815
+ when "deleteCell"
2816
+ delete_cell(args[0])
2817
+ else
2818
+ super
2819
+ end
2820
+ end
2821
+ end
2822
+
2823
+ # `<thead>` / `<tbody>` / `<tfoot>` — share section-level row
2824
+ # collection + insertRow / deleteRow.
2825
+ class HTMLTableSectionElement < HTMLElement
2826
+ def rows
2827
+ el = self
2828
+ HTMLCollection.new do
2829
+ el
2830
+ .__node__
2831
+ .element_children
2832
+ .select { |n| n.name == "tr" }
2833
+ .map { |n| el.document.wrap_node(n) }
2834
+ .compact
2835
+ end
2836
+ end
2837
+
2838
+ def insert_row(index = -1)
2839
+ tr = @document.create_element("tr")
2840
+ list = rows
2841
+ if index.to_i == -1 || index.to_i >= list.size
2842
+ append_child(tr)
2843
+ else
2844
+ insert_before(tr, list[index.to_i])
2845
+ end
2846
+
2847
+ tr
2848
+ end
2849
+
2850
+ def delete_row(index)
2851
+ rows[index.to_i]&.remove
2852
+ nil
2853
+ end
2854
+
2855
+ def __js_get__(key)
2856
+ key == "rows" ? rows : super
2857
+ end
2858
+
2859
+ def __js_call__(method, args)
2860
+ case method
2861
+ when "insertRow"
2862
+ insert_row(args[0] || -1)
2863
+ when "deleteRow"
2864
+ delete_row(args[0])
2865
+ else
2866
+ super
2867
+ end
2868
+ end
2869
+ end
2870
+
2871
+ # `<caption>` — table caption, minimal subclass.
2872
+ class HTMLTableCaptionElement < HTMLElement
2873
+ end
2874
+
2875
+ # `<table>` — top-level table element. `rows` returns rows from
2876
+ # all sections (thead → tbody → tfoot); `tBodies` is a list of
2877
+ # tbody elements. `insertRow(-1)` appends to the last tbody (or
2878
+ # creates one); `deleteRow` works against the merged `rows` list.
2879
+ class HTMLTableElement < HTMLElement
2880
+ def caption
2881
+ @__node__.element_children.find { |n| n.name == "caption" }&.then { |n| @document.wrap_node(n) }
2882
+ end
2883
+
2884
+ def caption=(new_caption)
2885
+ delete_caption
2886
+ return unless new_caption.respond_to?(:__node__)
2887
+
2888
+ first = @__node__.children.first
2889
+ first ? first.add_previous_sibling(new_caption.__node__) : @__node__.add_child(new_caption.__node__)
2890
+ end
2891
+
2892
+ def t_head
2893
+ @__node__.element_children.find { |n| n.name == "thead" }&.then { |n| @document.wrap_node(n) }
2894
+ end
2895
+
2896
+ def t_foot
2897
+ @__node__.element_children.find { |n| n.name == "tfoot" }&.then { |n| @document.wrap_node(n) }
2898
+ end
2899
+
2900
+ def t_bodies
2901
+ el = self
2902
+ HTMLCollection.new do
2903
+ el
2904
+ .__node__
2905
+ .element_children
2906
+ .select { |n| n.name == "tbody" }
2907
+ .map { |n| el.document.wrap_node(n) }
2908
+ .compact
2909
+ end
2910
+ end
2911
+
2912
+ def rows
2913
+ el = self
2914
+ HTMLCollection.new do
2915
+ ordered = []
2916
+ head = el.__node__.element_children.find { |n| n.name == "thead" }
2917
+ bodies = el.__node__.element_children.select { |n| n.name == "tbody" }
2918
+ direct = el.__node__.element_children.select { |n| n.name == "tr" }
2919
+ foot = el.__node__.element_children.find { |n| n.name == "tfoot" }
2920
+ [head, *bodies, foot].compact.each do |sec|
2921
+ sec.element_children.select { |n| n.name == "tr" }.each { |n| ordered << n }
2922
+ end
2923
+
2924
+ direct.each { |n| ordered << n }
2925
+ ordered.map { |n| el.document.wrap_node(n) }.compact
2926
+ end
2927
+ end
2928
+
2929
+ def create_caption
2930
+ existing = caption
2931
+ return existing if existing
2932
+
2933
+ cap = @document.create_element("caption")
2934
+ first = @__node__.children.first
2935
+ first ? first.add_previous_sibling(cap.__node__) : @__node__.add_child(cap.__node__)
2936
+ cap
2937
+ end
2938
+
2939
+ def delete_caption
2940
+ cap = caption
2941
+ cap&.remove
2942
+ nil
2943
+ end
2944
+
2945
+ def create_t_head
2946
+ existing = t_head
2947
+ return existing if existing
2948
+
2949
+ head = @document.create_element("thead")
2950
+ cap = caption
2951
+ if cap
2952
+ cap.__node__.add_next_sibling(head.__node__)
2953
+ else
2954
+ first = @__node__.children.first
2955
+ first ? first.add_previous_sibling(head.__node__) : @__node__.add_child(head.__node__)
2956
+ end
2957
+
2958
+ head
2959
+ end
2960
+
2961
+ def delete_t_head
2962
+ t_head&.remove
2963
+ nil
2964
+ end
2965
+
2966
+ def create_t_foot
2967
+ existing = t_foot
2968
+ return existing if existing
2969
+
2970
+ foot = @document.create_element("tfoot")
2971
+ @__node__.add_child(foot.__node__)
2972
+ foot
2973
+ end
2974
+
2975
+ def delete_t_foot
2976
+ t_foot&.remove
2977
+ nil
2978
+ end
2979
+
2980
+ def create_t_body
2981
+ tb = @document.create_element("tbody")
2982
+ last_tbody = t_bodies.last
2983
+ if last_tbody
2984
+ last_tbody.__node__.add_next_sibling(tb.__node__)
2985
+ else
2986
+ @__node__.add_child(tb.__node__)
2987
+ end
2988
+
2989
+ tb
2990
+ end
2991
+
2992
+ # `table.insertRow(index)` — inserts a `<tr>` at the merged
2993
+ # index. Per spec, if no `<tbody>` exists and the table is
2994
+ # empty, the row is inserted directly; otherwise it goes into
2995
+ # the last `<tbody>`.
2996
+ def insert_row(index = -1)
2997
+ list = rows.to_a
2998
+ raw = index.to_i
2999
+ raise DOMException::IndexSizeError, "row index #{raw} out of range" if raw < -1 || raw > list.size
3000
+
3001
+ idx = raw == -1 ? list.size : raw
3002
+
3003
+ tr = @document.create_element("tr")
3004
+ if idx == list.size
3005
+ target_section = t_bodies.last || create_t_body
3006
+ target_section.append_child(tr)
3007
+ else
3008
+ anchor = list[idx]
3009
+ section = anchor.__node__.parent
3010
+ if section
3011
+ anchor.__node__.add_previous_sibling(tr.__node__)
3012
+ @document.notify_child_list_mutation(target_node: section, added_nodes: [tr.__node__], removed_nodes: [])
3013
+ end
3014
+ end
3015
+
3016
+ tr
3017
+ end
3018
+
3019
+ def delete_row(index)
3020
+ rows[index.to_i]&.remove
3021
+ nil
3022
+ end
3023
+
3024
+ def __js_get__(key)
3025
+ case key
3026
+ when "caption"
3027
+ caption
3028
+ when "tHead"
3029
+ t_head
3030
+ when "tFoot"
3031
+ t_foot
3032
+ when "tBodies"
3033
+ t_bodies
3034
+ when "rows"
3035
+ rows
3036
+ else
3037
+ super
3038
+ end
3039
+ end
3040
+
3041
+ def __js_set__(key, value)
3042
+ case key
3043
+ when "caption"
3044
+ self.caption = value
3045
+ else
3046
+ super
3047
+ end
3048
+ end
3049
+
3050
+ def __js_call__(method, args)
3051
+ case method
3052
+ when "insertRow"
3053
+ insert_row(args[0] || -1)
3054
+ when "deleteRow"
3055
+ delete_row(args[0])
3056
+ when "createCaption"
3057
+ create_caption
3058
+ when "deleteCaption"
3059
+ delete_caption
3060
+ when "createTHead"
3061
+ create_t_head
3062
+ when "deleteTHead"
3063
+ delete_t_head
3064
+ when "createTFoot"
3065
+ create_t_foot
3066
+ when "deleteTFoot"
3067
+ delete_t_foot
3068
+ when "createTBody"
3069
+ create_t_body
3070
+ else
3071
+ super
3072
+ end
3073
+ end
3074
+ end
3075
+
3076
+ # `<audio>` / `<video>` shared base. The actual media engine is
3077
+ # absent in Dommy — getters return inert values, `play()` returns
3078
+ # a resolved Promise, and `pause()` flips `paused` back to true.
3079
+ class HTMLMediaElement < HTMLElement
3080
+ NETWORK_EMPTY = 0
3081
+ NETWORK_IDLE = 1
3082
+ NETWORK_LOADING = 2
3083
+ NETWORK_NO_SOURCE = 3
3084
+
3085
+ HAVE_NOTHING = 0
3086
+ HAVE_METADATA = 1
3087
+ HAVE_CURRENT_DATA = 2
3088
+ HAVE_FUTURE_DATA = 3
3089
+ HAVE_ENOUGH_DATA = 4
3090
+
3091
+ def src
3092
+ reflected_string("src")
3093
+ end
3094
+
3095
+ def src=(v)
3096
+ set_reflected_string("src", v)
3097
+ end
3098
+
3099
+ def current_src
3100
+ src
3101
+ end
3102
+
3103
+ def preload
3104
+ reflected_string("preload")
3105
+ end
3106
+
3107
+ def preload=(v)
3108
+ set_reflected_string("preload", v)
3109
+ end
3110
+
3111
+ def crossorigin
3112
+ reflected_string("crossorigin")
3113
+ end
3114
+
3115
+ def autoplay
3116
+ reflected_boolean("autoplay")
3117
+ end
3118
+
3119
+ def autoplay=(v)
3120
+ set_reflected_boolean("autoplay", v)
3121
+ end
3122
+
3123
+ def loop_
3124
+ reflected_boolean("loop")
3125
+ end
3126
+
3127
+ def loop_=(v)
3128
+ set_reflected_boolean("loop", v)
3129
+ end
3130
+
3131
+ def controls
3132
+ reflected_boolean("controls")
3133
+ end
3134
+
3135
+ def controls=(v)
3136
+ set_reflected_boolean("controls", v)
3137
+ end
3138
+
3139
+ def muted
3140
+ @__muted == true || reflected_boolean("muted")
3141
+ end
3142
+
3143
+ def muted=(v)
3144
+ @__muted = !!v
3145
+ end
3146
+
3147
+ def default_muted
3148
+ reflected_boolean("muted")
3149
+ end
3150
+
3151
+ def default_muted=(v)
3152
+ set_reflected_boolean("muted", v)
3153
+ end
3154
+
3155
+ def paused
3156
+ @__paused.nil? ? true : @__paused
3157
+ end
3158
+
3159
+ def ended
3160
+ false
3161
+ end
3162
+
3163
+ def seeking
3164
+ false
3165
+ end
3166
+
3167
+ def volume
3168
+ @__volume.nil? ? 1.0 : @__volume
3169
+ end
3170
+
3171
+ def volume=(v)
3172
+ @__volume = v.to_f
3173
+ end
3174
+
3175
+ def playback_rate
3176
+ @__rate.nil? ? 1.0 : @__rate
3177
+ end
3178
+
3179
+ def playback_rate=(v)
3180
+ @__rate = v.to_f
3181
+ end
3182
+
3183
+ def default_playback_rate
3184
+ @__default_rate.nil? ? 1.0 : @__default_rate
3185
+ end
3186
+
3187
+ def default_playback_rate=(v)
3188
+ @__default_rate = v.to_f
3189
+ end
3190
+
3191
+ def current_time
3192
+ @__current_time.to_f
3193
+ end
3194
+
3195
+ def current_time=(v)
3196
+ @__current_time = v.to_f
3197
+ end
3198
+
3199
+ def duration
3200
+ Float::NAN
3201
+ end
3202
+
3203
+ def network_state
3204
+ NETWORK_EMPTY
3205
+ end
3206
+
3207
+ def ready_state
3208
+ HAVE_NOTHING
3209
+ end
3210
+
3211
+ def play
3212
+ @__paused = false
3213
+ promise = PromiseValue.new(@document.default_view)
3214
+ promise.fulfill(nil)
3215
+ promise
3216
+ end
3217
+
3218
+ def pause
3219
+ @__paused = true
3220
+ nil
3221
+ end
3222
+
3223
+ def load
3224
+ nil
3225
+ end
3226
+
3227
+ def can_play_type(_type)
3228
+ # spec: "" | "maybe" | "probably". We don't decode → "".
3229
+ ""
3230
+ end
3231
+
3232
+ def __js_get__(key)
3233
+ case key
3234
+ when "src"
3235
+ src
3236
+ when "currentSrc"
3237
+ current_src
3238
+ when "preload"
3239
+ preload
3240
+ when "crossOrigin"
3241
+ crossorigin
3242
+ when "autoplay"
3243
+ autoplay
3244
+ when "loop"
3245
+ loop_
3246
+ when "controls"
3247
+ controls
3248
+ when "muted"
3249
+ muted
3250
+ when "defaultMuted"
3251
+ default_muted
3252
+ when "paused"
3253
+ paused
3254
+ when "ended"
3255
+ ended
3256
+ when "seeking"
3257
+ seeking
3258
+ when "volume"
3259
+ volume
3260
+ when "playbackRate"
3261
+ playback_rate
3262
+ when "defaultPlaybackRate"
3263
+ default_playback_rate
3264
+ when "currentTime"
3265
+ current_time
3266
+ when "duration"
3267
+ duration
3268
+ when "networkState"
3269
+ network_state
3270
+ when "readyState"
3271
+ ready_state
3272
+ when "NETWORK_EMPTY"
3273
+ NETWORK_EMPTY
3274
+ when "NETWORK_IDLE"
3275
+ NETWORK_IDLE
3276
+ when "NETWORK_LOADING"
3277
+ NETWORK_LOADING
3278
+ when "NETWORK_NO_SOURCE"
3279
+ NETWORK_NO_SOURCE
3280
+ when "HAVE_NOTHING"
3281
+ HAVE_NOTHING
3282
+ when "HAVE_METADATA"
3283
+ HAVE_METADATA
3284
+ when "HAVE_CURRENT_DATA"
3285
+ HAVE_CURRENT_DATA
3286
+ when "HAVE_FUTURE_DATA"
3287
+ HAVE_FUTURE_DATA
3288
+ when "HAVE_ENOUGH_DATA"
3289
+ HAVE_ENOUGH_DATA
3290
+ else
3291
+ super
3292
+ end
3293
+ end
3294
+
3295
+ def __js_set__(key, value)
3296
+ case key
3297
+ when "src"
3298
+ self.src = value
3299
+ when "preload"
3300
+ self.preload = value
3301
+ when "autoplay"
3302
+ self.autoplay = value
3303
+ when "loop"
3304
+ self.loop_ = value
3305
+ when "controls"
3306
+ self.controls = value
3307
+ when "muted"
3308
+ self.muted = value
3309
+ when "defaultMuted"
3310
+ self.default_muted = value
3311
+ when "volume"
3312
+ self.volume = value
3313
+ when "playbackRate"
3314
+ self.playback_rate = value
3315
+ when "defaultPlaybackRate"
3316
+ self.default_playback_rate = value
3317
+ when "currentTime"
3318
+ self.current_time = value
3319
+ else
3320
+ super
3321
+ end
3322
+ end
3323
+
3324
+ def __js_call__(method, args)
3325
+ case method
3326
+ when "play"
3327
+ play
3328
+ when "pause"
3329
+ pause
3330
+ when "load"
3331
+ load
3332
+ when "canPlayType"
3333
+ can_play_type(args[0])
3334
+ else
3335
+ super
3336
+ end
3337
+ end
3338
+ end
3339
+
3340
+ class HTMLAudioElement < HTMLMediaElement
3341
+ end
3342
+
3343
+ class HTMLVideoElement < HTMLMediaElement
3344
+ def poster
3345
+ reflected_string("poster")
3346
+ end
3347
+
3348
+ def poster=(v)
3349
+ set_reflected_string("poster", v)
3350
+ end
3351
+
3352
+ def width
3353
+ @__node__["width"].to_s.to_i
3354
+ end
3355
+
3356
+ def width=(v)
3357
+ set_reflected_string("width", v.to_s)
3358
+ end
3359
+
3360
+ def height
3361
+ @__node__["height"].to_s.to_i
3362
+ end
3363
+
3364
+ def height=(v)
3365
+ set_reflected_string("height", v.to_s)
3366
+ end
3367
+
3368
+ def video_width
3369
+ width
3370
+ end
3371
+
3372
+ def video_height
3373
+ height
3374
+ end
3375
+
3376
+ def plays_inline
3377
+ reflected_boolean("playsinline")
3378
+ end
3379
+
3380
+ def plays_inline=(v)
3381
+ set_reflected_boolean("playsinline", v)
3382
+ end
3383
+
3384
+ def __js_get__(key)
3385
+ case key
3386
+ when "poster"
3387
+ poster
3388
+ when "width"
3389
+ width
3390
+ when "height"
3391
+ height
3392
+ when "videoWidth"
3393
+ video_width
3394
+ when "videoHeight"
3395
+ video_height
3396
+ when "playsInline"
3397
+ plays_inline
3398
+ else
3399
+ super
3400
+ end
3401
+ end
3402
+
3403
+ def __js_set__(key, value)
3404
+ case key
3405
+ when "poster"
3406
+ self.poster = value
3407
+ when "width"
3408
+ self.width = value
3409
+ when "height"
3410
+ self.height = value
3411
+ when "playsInline"
3412
+ self.plays_inline = value
3413
+ else
3414
+ super
3415
+ end
3416
+ end
3417
+ end
3418
+
3419
+ class HTMLSourceElement < HTMLElement
3420
+ def src
3421
+ reflected_string("src")
3422
+ end
3423
+
3424
+ def src=(v)
3425
+ set_reflected_string("src", v)
3426
+ end
3427
+
3428
+ def type
3429
+ reflected_string("type")
3430
+ end
3431
+
3432
+ def type=(v)
3433
+ set_reflected_string("type", v)
3434
+ end
3435
+
3436
+ def media
3437
+ reflected_string("media")
3438
+ end
3439
+
3440
+ def media=(v)
3441
+ set_reflected_string("media", v)
3442
+ end
3443
+
3444
+ def srcset
3445
+ reflected_string("srcset")
3446
+ end
3447
+
3448
+ def srcset=(v)
3449
+ set_reflected_string("srcset", v)
3450
+ end
3451
+
3452
+ def sizes
3453
+ reflected_string("sizes")
3454
+ end
3455
+
3456
+ def sizes=(v)
3457
+ set_reflected_string("sizes", v)
3458
+ end
3459
+
3460
+ def width
3461
+ @__node__["width"].to_s.to_i
3462
+ end
3463
+
3464
+ def width=(v)
3465
+ set_reflected_string("width", v.to_s)
3466
+ end
3467
+
3468
+ def height
3469
+ @__node__["height"].to_s.to_i
3470
+ end
3471
+
3472
+ def height=(v)
3473
+ set_reflected_string("height", v.to_s)
3474
+ end
3475
+
3476
+ def __js_get__(key)
3477
+ case key
3478
+ when "src"
3479
+ src
3480
+ when "type"
3481
+ type
3482
+ when "media"
3483
+ media
3484
+ when "srcset"
3485
+ srcset
3486
+ when "sizes"
3487
+ sizes
3488
+ when "width"
3489
+ width
3490
+ when "height"
3491
+ height
3492
+ else
3493
+ super
3494
+ end
3495
+ end
3496
+
3497
+ def __js_set__(key, value)
3498
+ case key
3499
+ when "src", "type", "media", "srcset", "sizes"
3500
+ set_reflected_string(key, value)
3501
+ when "width"
3502
+ self.width = value
3503
+ when "height"
3504
+ self.height = value
3505
+ else
3506
+ super
3507
+ end
3508
+ end
3509
+ end
3510
+
3511
+ class HTMLTrackElement < HTMLElement
3512
+ NONE = 0
3513
+ LOADING = 1
3514
+ LOADED = 2
3515
+ ERROR = 3
3516
+
3517
+ def kind
3518
+ reflected_string("kind")
3519
+ end
3520
+
3521
+ def kind=(v)
3522
+ set_reflected_string("kind", v)
3523
+ end
3524
+
3525
+ def src
3526
+ reflected_string("src")
3527
+ end
3528
+
3529
+ def src=(v)
3530
+ set_reflected_string("src", v)
3531
+ end
3532
+
3533
+ def srclang
3534
+ reflected_string("srclang")
3535
+ end
3536
+
3537
+ def srclang=(v)
3538
+ set_reflected_string("srclang", v)
3539
+ end
3540
+
3541
+ def label
3542
+ reflected_string("label")
3543
+ end
3544
+
3545
+ def label=(v)
3546
+ set_reflected_string("label", v)
3547
+ end
3548
+
3549
+ def default_
3550
+ reflected_boolean("default")
3551
+ end
3552
+
3553
+ def default_=(v)
3554
+ set_reflected_boolean("default", v)
3555
+ end
3556
+
3557
+ def ready_state
3558
+ NONE
3559
+ end
3560
+
3561
+ def __js_get__(key)
3562
+ case key
3563
+ when "kind"
3564
+ kind
3565
+ when "src"
3566
+ src
3567
+ when "srclang"
3568
+ srclang
3569
+ when "label"
3570
+ label
3571
+ when "default"
3572
+ default_
3573
+ when "readyState"
3574
+ ready_state
3575
+ else
3576
+ super
3577
+ end
3578
+ end
3579
+
3580
+ def __js_set__(key, value)
3581
+ case key
3582
+ when "kind", "src", "srclang", "label"
3583
+ set_reflected_string(key, value)
3584
+ when "default"
3585
+ self.default_ = value
3586
+ else
3587
+ super
3588
+ end
3589
+ end
3590
+ end
3591
+
3592
+ class HTMLIFrameElement < HTMLElement
3593
+ def src
3594
+ reflected_string("src")
3595
+ end
3596
+
3597
+ def src=(v)
3598
+ set_reflected_string("src", v)
3599
+ end
3600
+
3601
+ def srcdoc
3602
+ reflected_string("srcdoc")
3603
+ end
3604
+
3605
+ def srcdoc=(v)
3606
+ set_reflected_string("srcdoc", v)
3607
+ end
3608
+
3609
+ def name
3610
+ reflected_string("name")
3611
+ end
3612
+
3613
+ def name=(v)
3614
+ set_reflected_string("name", v)
3615
+ end
3616
+
3617
+ def sandbox
3618
+ reflected_string("sandbox")
3619
+ end
3620
+
3621
+ def sandbox=(v)
3622
+ set_reflected_string("sandbox", v)
3623
+ end
3624
+
3625
+ def allow
3626
+ reflected_string("allow")
3627
+ end
3628
+
3629
+ def allow=(v)
3630
+ set_reflected_string("allow", v)
3631
+ end
3632
+
3633
+ def allow_fullscreen
3634
+ reflected_boolean("allowfullscreen")
3635
+ end
3636
+
3637
+ def allow_fullscreen=(v)
3638
+ set_reflected_boolean("allowfullscreen", v)
3639
+ end
3640
+
3641
+ def referrer_policy
3642
+ reflected_string("referrerpolicy")
3643
+ end
3644
+
3645
+ def referrer_policy=(v)
3646
+ set_reflected_string("referrerpolicy", v)
3647
+ end
3648
+
3649
+ def loading
3650
+ reflected_string("loading")
3651
+ end
3652
+
3653
+ def loading=(v)
3654
+ set_reflected_string("loading", v)
3655
+ end
3656
+
3657
+ def width
3658
+ @__node__["width"].to_s
3659
+ end
3660
+
3661
+ def width=(v)
3662
+ set_reflected_string("width", v.to_s)
3663
+ end
3664
+
3665
+ def height
3666
+ @__node__["height"].to_s
3667
+ end
3668
+
3669
+ def height=(v)
3670
+ set_reflected_string("height", v.to_s)
3671
+ end
3672
+
3673
+ def content_document
3674
+ nil
3675
+ end
3676
+
3677
+ def content_window
3678
+ nil
3679
+ end
3680
+
3681
+ def __js_get__(key)
3682
+ case key
3683
+ when "src"
3684
+ src
3685
+ when "srcdoc"
3686
+ srcdoc
3687
+ when "name"
3688
+ name
3689
+ when "sandbox"
3690
+ sandbox
3691
+ when "allow"
3692
+ allow
3693
+ when "allowFullscreen"
3694
+ allow_fullscreen
3695
+ when "referrerPolicy"
3696
+ referrer_policy
3697
+ when "loading"
3698
+ loading
3699
+ when "width"
3700
+ width
3701
+ when "height"
3702
+ height
3703
+ when "contentDocument"
3704
+ content_document
3705
+ when "contentWindow"
3706
+ content_window
3707
+ else
3708
+ super
3709
+ end
3710
+ end
3711
+
3712
+ def __js_set__(key, value)
3713
+ case key
3714
+ when "src", "srcdoc", "name", "sandbox", "allow", "loading"
3715
+ set_reflected_string(key, value)
3716
+ when "allowFullscreen"
3717
+ self.allow_fullscreen = value
3718
+ when "referrerPolicy"
3719
+ set_reflected_string("referrerpolicy", value)
3720
+ when "width"
3721
+ self.width = value
3722
+ when "height"
3723
+ self.height = value
3724
+ else
3725
+ super
3726
+ end
3727
+ end
3728
+ end
3729
+
3730
+ class HTMLPictureElement < HTMLElement
3731
+ end
3732
+
3733
+ class HTMLOListElement < HTMLElement
3734
+ def start
3735
+ (@__node__["start"] || "1").to_i
3736
+ end
3737
+
3738
+ def start=(v)
3739
+ set_reflected_string("start", v.to_s)
3740
+ end
3741
+
3742
+ def reversed
3743
+ reflected_boolean("reversed")
3744
+ end
3745
+
3746
+ def reversed=(v)
3747
+ set_reflected_boolean("reversed", v)
3748
+ end
3749
+
3750
+ def type
3751
+ reflected_string("type")
3752
+ end
3753
+
3754
+ def type=(v)
3755
+ set_reflected_string("type", v)
3756
+ end
3757
+
3758
+ def __js_get__(key)
3759
+ case key
3760
+ when "start"
3761
+ start
3762
+ when "reversed"
3763
+ reversed
3764
+ when "type"
3765
+ type
3766
+ else
3767
+ super
3768
+ end
3769
+ end
3770
+
3771
+ def __js_set__(key, value)
3772
+ case key
3773
+ when "start"
3774
+ self.start = value
3775
+ when "reversed"
3776
+ self.reversed = value
3777
+ when "type"
3778
+ self.type = value
3779
+ else
3780
+ super
3781
+ end
3782
+ end
3783
+ end
3784
+
3785
+ class HTMLUListElement < HTMLElement
3786
+ end
3787
+
3788
+ class HTMLLIElement < HTMLElement
3789
+ def value
3790
+ @__node__["value"]&.to_i
3791
+ end
3792
+
3793
+ def value=(v)
3794
+ set_reflected_string("value", v.to_s)
3795
+ end
3796
+
3797
+ def __js_get__(key)
3798
+ key == "value" ? value : super
3799
+ end
3800
+
3801
+ def __js_set__(key, value)
3802
+ key == "value" ? (self.value = value) : super
3803
+ end
3804
+ end
3805
+
3806
+ class HTMLTimeElement < HTMLElement
3807
+ def date_time
3808
+ reflected_string("datetime")
3809
+ end
3810
+
3811
+ def date_time=(v)
3812
+ set_reflected_string("datetime", v)
3813
+ end
3814
+
3815
+ def __js_get__(key)
3816
+ key == "dateTime" ? date_time : super
3817
+ end
3818
+
3819
+ def __js_set__(key, value)
3820
+ key == "dateTime" ? (self.date_time = value) : super
3821
+ end
3822
+ end
3823
+
3824
+ class HTMLDataElement < HTMLElement
3825
+ def value
3826
+ reflected_string("value")
3827
+ end
3828
+
3829
+ def value=(v)
3830
+ set_reflected_string("value", v)
3831
+ end
3832
+
3833
+ def __js_get__(key)
3834
+ key == "value" ? value : super
3835
+ end
3836
+
3837
+ def __js_set__(key, value)
3838
+ key == "value" ? (self.value = value) : super
3839
+ end
3840
+ end
3841
+
3842
+ class HTMLAreaElement < HTMLElement
3843
+ def alt
3844
+ reflected_string("alt")
3845
+ end
3846
+
3847
+ def alt=(v)
3848
+ set_reflected_string("alt", v)
3849
+ end
3850
+
3851
+ def coords
3852
+ reflected_string("coords")
3853
+ end
3854
+
3855
+ def coords=(v)
3856
+ set_reflected_string("coords", v)
3857
+ end
3858
+
3859
+ def shape
3860
+ reflected_string("shape")
3861
+ end
3862
+
3863
+ def shape=(v)
3864
+ set_reflected_string("shape", v)
3865
+ end
3866
+
3867
+ def href
3868
+ reflected_string("href")
3869
+ end
3870
+
3871
+ def href=(v)
3872
+ set_reflected_string("href", v)
3873
+ end
3874
+
3875
+ def target
3876
+ reflected_string("target")
3877
+ end
3878
+
3879
+ def target=(v)
3880
+ set_reflected_string("target", v)
3881
+ end
3882
+
3883
+ def rel
3884
+ reflected_string("rel")
3885
+ end
3886
+
3887
+ def rel=(v)
3888
+ set_reflected_string("rel", v)
3889
+ end
3890
+
3891
+ def __js_get__(key)
3892
+ case key
3893
+ when "alt"
3894
+ alt
3895
+ when "coords"
3896
+ coords
3897
+ when "shape"
3898
+ shape
3899
+ when "href"
3900
+ href
3901
+ when "target"
3902
+ target
3903
+ when "rel"
3904
+ rel
3905
+ else
3906
+ super
3907
+ end
3908
+ end
3909
+
3910
+ def __js_set__(key, value)
3911
+ case key
3912
+ when "alt", "coords", "shape", "href", "target", "rel"
3913
+ set_reflected_string(key, value)
3914
+ else
3915
+ super
3916
+ end
3917
+ end
3918
+ end
3919
+
3920
+ class HTMLMapElement < HTMLElement
3921
+ def name
3922
+ reflected_string("name")
3923
+ end
3924
+
3925
+ def name=(v)
3926
+ set_reflected_string("name", v)
3927
+ end
3928
+
3929
+ def areas
3930
+ HTMLCollection.new do
3931
+ @__node__.css("area").map { |n| @document.wrap_node(n) }.compact
3932
+ end
3933
+ end
3934
+
3935
+ def __js_get__(key)
3936
+ case key
3937
+ when "name"
3938
+ name
3939
+ when "areas"
3940
+ areas
3941
+ else
3942
+ super
3943
+ end
3944
+ end
3945
+
3946
+ def __js_set__(key, value)
3947
+ key == "name" ? (self.name = value) : super
3948
+ end
3949
+ end
3950
+
3951
+ class HTMLObjectElement < HTMLElement
3952
+ def data
3953
+ reflected_string("data")
3954
+ end
3955
+
3956
+ def data=(v)
3957
+ set_reflected_string("data", v)
3958
+ end
3959
+
3960
+ def type
3961
+ reflected_string("type")
3962
+ end
3963
+
3964
+ def type=(v)
3965
+ set_reflected_string("type", v)
3966
+ end
3967
+
3968
+ def name
3969
+ reflected_string("name")
3970
+ end
3971
+
3972
+ def name=(v)
3973
+ set_reflected_string("name", v)
3974
+ end
3975
+
3976
+ def use_map
3977
+ reflected_string("usemap")
3978
+ end
3979
+
3980
+ def use_map=(v)
3981
+ set_reflected_string("usemap", v)
3982
+ end
3983
+
3984
+ def width
3985
+ @__node__["width"].to_s
3986
+ end
3987
+
3988
+ def width=(v)
3989
+ set_reflected_string("width", v.to_s)
3990
+ end
3991
+
3992
+ def height
3993
+ @__node__["height"].to_s
3994
+ end
3995
+
3996
+ def height=(v)
3997
+ set_reflected_string("height", v.to_s)
3998
+ end
3999
+
4000
+ def content_document
4001
+ nil
4002
+ end
4003
+
4004
+ def content_window
4005
+ nil
4006
+ end
4007
+
4008
+ def __js_get__(key)
4009
+ case key
4010
+ when "data"
4011
+ data
4012
+ when "type"
4013
+ type
4014
+ when "name"
4015
+ name
4016
+ when "useMap"
4017
+ use_map
4018
+ when "width"
4019
+ width
4020
+ when "height"
4021
+ height
4022
+ when "contentDocument"
4023
+ content_document
4024
+ when "contentWindow"
4025
+ content_window
4026
+ else
4027
+ super
4028
+ end
4029
+ end
4030
+
4031
+ def __js_set__(key, value)
4032
+ case key
4033
+ when "data", "type", "name"
4034
+ set_reflected_string(key, value)
4035
+ when "useMap"
4036
+ set_reflected_string("usemap", value)
4037
+ when "width"
4038
+ self.width = value
4039
+ when "height"
4040
+ self.height = value
4041
+ else
4042
+ super
4043
+ end
4044
+ end
4045
+ end
4046
+
4047
+ class HTMLEmbedElement < HTMLElement
4048
+ def src
4049
+ reflected_string("src")
4050
+ end
4051
+
4052
+ def src=(v)
4053
+ set_reflected_string("src", v)
4054
+ end
4055
+
4056
+ def type
4057
+ reflected_string("type")
4058
+ end
4059
+
4060
+ def type=(v)
4061
+ set_reflected_string("type", v)
4062
+ end
4063
+
4064
+ def width
4065
+ @__node__["width"].to_s
4066
+ end
4067
+
4068
+ def width=(v)
4069
+ set_reflected_string("width", v.to_s)
4070
+ end
4071
+
4072
+ def height
4073
+ @__node__["height"].to_s
4074
+ end
4075
+
4076
+ def height=(v)
4077
+ set_reflected_string("height", v.to_s)
4078
+ end
4079
+
4080
+ def __js_get__(key)
4081
+ case key
4082
+ when "src"
4083
+ src
4084
+ when "type"
4085
+ type
4086
+ when "width"
4087
+ width
4088
+ when "height"
4089
+ height
4090
+ else
4091
+ super
4092
+ end
4093
+ end
4094
+
4095
+ def __js_set__(key, value)
4096
+ case key
4097
+ when "src", "type"
4098
+ set_reflected_string(key, value)
4099
+ when "width"
4100
+ self.width = value
4101
+ when "height"
4102
+ self.height = value
4103
+ else
4104
+ super
4105
+ end
4106
+ end
4107
+ end
4108
+
4109
+ class HTMLBaseElement < HTMLElement
4110
+ def href
4111
+ reflected_string("href")
4112
+ end
4113
+
4114
+ def href=(v)
4115
+ set_reflected_string("href", v)
4116
+ end
4117
+
4118
+ def target
4119
+ reflected_string("target")
4120
+ end
4121
+
4122
+ def target=(v)
4123
+ set_reflected_string("target", v)
4124
+ end
4125
+
4126
+ def __js_get__(key)
4127
+ case key
4128
+ when "href"
4129
+ href
4130
+ when "target"
4131
+ target
4132
+ else
4133
+ super
4134
+ end
4135
+ end
4136
+
4137
+ def __js_set__(key, value)
4138
+ case key
4139
+ when "href", "target"
4140
+ set_reflected_string(key, value)
4141
+ else
4142
+ super
4143
+ end
4144
+ end
4145
+ end
4146
+
4147
+ class HTMLMetaElement < HTMLElement
4148
+ def name
4149
+ reflected_string("name")
4150
+ end
4151
+
4152
+ def name=(v)
4153
+ set_reflected_string("name", v)
4154
+ end
4155
+
4156
+ def content
4157
+ reflected_string("content")
4158
+ end
4159
+
4160
+ def content=(v)
4161
+ set_reflected_string("content", v)
4162
+ end
4163
+
4164
+ def charset
4165
+ reflected_string("charset")
4166
+ end
4167
+
4168
+ def charset=(v)
4169
+ set_reflected_string("charset", v)
4170
+ end
4171
+
4172
+ def http_equiv
4173
+ reflected_string("http-equiv")
4174
+ end
4175
+
4176
+ def http_equiv=(v)
4177
+ set_reflected_string("http-equiv", v)
4178
+ end
4179
+
4180
+ def __js_get__(key)
4181
+ case key
4182
+ when "name"
4183
+ name
4184
+ when "content"
4185
+ content
4186
+ when "charset"
4187
+ charset
4188
+ when "httpEquiv"
4189
+ http_equiv
4190
+ else
4191
+ super
4192
+ end
4193
+ end
4194
+
4195
+ def __js_set__(key, value)
4196
+ case key
4197
+ when "name", "content", "charset"
4198
+ set_reflected_string(key, value)
4199
+ when "httpEquiv"
4200
+ set_reflected_string("http-equiv", value)
4201
+ else
4202
+ super
4203
+ end
4204
+ end
4205
+ end
4206
+
4207
+ class HTMLStyleElement < HTMLElement
4208
+ def type
4209
+ reflected_string("type")
4210
+ end
4211
+
4212
+ def type=(v)
4213
+ set_reflected_string("type", v)
4214
+ end
4215
+
4216
+ def media
4217
+ reflected_string("media")
4218
+ end
4219
+
4220
+ def media=(v)
4221
+ set_reflected_string("media", v)
4222
+ end
4223
+
4224
+ def disabled
4225
+ @__disabled == true
4226
+ end
4227
+
4228
+ def disabled=(v)
4229
+ @__disabled = !!v
4230
+ end
4231
+
4232
+ # `style.sheet` — always non-nil for `<style>` (in browsers the
4233
+ # text content is parsed; we hand back an empty sheet stub that
4234
+ # consumers can manipulate via insertRule/deleteRule).
4235
+ def sheet
4236
+ @__sheet ||= CSSStyleSheet.new(
4237
+ owner_node: self,
4238
+ media: media,
4239
+ title: @__node__["title"].to_s,
4240
+ type: (type.empty? ? "text/css" : type)
4241
+ )
4242
+ end
4243
+
4244
+ def __js_get__(key)
4245
+ case key
4246
+ when "type"
4247
+ type
4248
+ when "media"
4249
+ media
4250
+ when "disabled"
4251
+ disabled
4252
+ when "sheet"
4253
+ sheet
4254
+ else
4255
+ super
4256
+ end
4257
+ end
4258
+
4259
+ def __js_set__(key, value)
4260
+ case key
4261
+ when "type", "media"
4262
+ set_reflected_string(key, value)
4263
+ when "disabled"
4264
+ self.disabled = value
4265
+ else
4266
+ super
4267
+ end
4268
+ end
4269
+ end
4270
+
4271
+ class HTMLTitleElement < HTMLElement
4272
+ def text
4273
+ text_content
4274
+ end
4275
+
4276
+ def text=(v)
4277
+ self.text_content = v.to_s
4278
+ end
4279
+
4280
+ def __js_get__(key)
4281
+ key == "text" ? text : super
4282
+ end
4283
+
4284
+ def __js_set__(key, value)
4285
+ key == "text" ? (self.text = value) : super
4286
+ end
4287
+ end
4288
+
4289
+ class HTMLQuoteElement < HTMLElement
4290
+ def cite
4291
+ reflected_string("cite")
4292
+ end
4293
+
4294
+ def cite=(v)
4295
+ set_reflected_string("cite", v)
4296
+ end
4297
+
4298
+ def __js_get__(key)
4299
+ key == "cite" ? cite : super
4300
+ end
4301
+
4302
+ def __js_set__(key, value)
4303
+ key == "cite" ? (self.cite = value) : super
4304
+ end
4305
+ end
4306
+
4307
+ class HTMLModElement < HTMLElement
4308
+ def cite
4309
+ reflected_string("cite")
4310
+ end
4311
+
4312
+ def cite=(v)
4313
+ set_reflected_string("cite", v)
4314
+ end
4315
+
4316
+ def date_time
4317
+ reflected_string("datetime")
4318
+ end
4319
+
4320
+ def date_time=(v)
4321
+ set_reflected_string("datetime", v)
4322
+ end
4323
+
4324
+ def __js_get__(key)
4325
+ case key
4326
+ when "cite"
4327
+ cite
4328
+ when "dateTime"
4329
+ date_time
4330
+ else
4331
+ super
4332
+ end
4333
+ end
4334
+
4335
+ def __js_set__(key, value)
4336
+ case key
4337
+ when "cite"
4338
+ self.cite = value
4339
+ when "dateTime"
4340
+ self.date_time = value
4341
+ else
4342
+ super
4343
+ end
4344
+ end
4345
+ end
4346
+
4347
+ # Identity-only subclasses — useful for `instanceof` / `is_a?` checks
4348
+ # in consumer code, even though they don't add reflected IDL attrs
4349
+ # beyond what HTMLElement already exposes.
4350
+ class HTMLDivElement < HTMLElement
4351
+ end
4352
+
4353
+ class HTMLSpanElement < HTMLElement
4354
+ end
4355
+
4356
+ class HTMLParagraphElement < HTMLElement
4357
+ end
4358
+
4359
+ class HTMLHeadingElement < HTMLElement
4360
+ end
4361
+
4362
+ class HTMLBRElement < HTMLElement
4363
+ end
4364
+
4365
+ class HTMLHRElement < HTMLElement
4366
+ end
4367
+
4368
+ class HTMLPreElement < HTMLElement
4369
+ end
4370
+
4371
+ class HTMLBodyElement < HTMLElement
4372
+ end
4373
+
4374
+ class HTMLHeadElement < HTMLElement
4375
+ end
4376
+
4377
+ class HTMLHtmlElement < HTMLElement
4378
+ end
4379
+
4380
+ # Look up the subclass for a given HTML tag. Document#wrap_node
4381
+ # consults this map; defaults to plain Element.
4382
+ HTML_ELEMENT_CLASSES = {
4383
+ "a" => HTMLAnchorElement,
4384
+ "form" => HTMLFormElement,
4385
+ "input" => HTMLInputElement,
4386
+ "button" => HTMLButtonElement,
4387
+ "img" => HTMLImageElement,
4388
+ "script" => HTMLScriptElement,
4389
+ "link" => HTMLLinkElement,
4390
+ "select" => HTMLSelectElement,
4391
+ "option" => HTMLOptionElement,
4392
+ "optgroup" => HTMLOptGroupElement,
4393
+ "textarea" => HTMLTextAreaElement,
4394
+ "label" => HTMLLabelElement,
4395
+ "fieldset" => HTMLFieldsetElement,
4396
+ "output" => HTMLOutputElement,
4397
+ "legend" => HTMLLegendElement,
4398
+ "slot" => HTMLSlotElement,
4399
+ "table" => HTMLTableElement,
4400
+ "thead" => HTMLTableSectionElement,
4401
+ "tbody" => HTMLTableSectionElement,
4402
+ "tfoot" => HTMLTableSectionElement,
4403
+ "tr" => HTMLTableRowElement,
4404
+ "td" => HTMLTableCellElement,
4405
+ "th" => HTMLTableCellElement,
4406
+ "caption" => HTMLTableCaptionElement,
4407
+ "dialog" => HTMLDialogElement,
4408
+ "details" => HTMLDetailsElement,
4409
+ "meter" => HTMLMeterElement,
4410
+ "progress" => HTMLProgressElement,
4411
+ "template" => HTMLTemplateElement,
4412
+ "audio" => HTMLAudioElement,
4413
+ "video" => HTMLVideoElement,
4414
+ "source" => HTMLSourceElement,
4415
+ "track" => HTMLTrackElement,
4416
+ "iframe" => HTMLIFrameElement,
4417
+ "picture" => HTMLPictureElement,
4418
+ "ol" => HTMLOListElement,
4419
+ "ul" => HTMLUListElement,
4420
+ "li" => HTMLLIElement,
4421
+ "time" => HTMLTimeElement,
4422
+ "data" => HTMLDataElement,
4423
+ "area" => HTMLAreaElement,
4424
+ "map" => HTMLMapElement,
4425
+ "object" => HTMLObjectElement,
4426
+ "embed" => HTMLEmbedElement,
4427
+ "base" => HTMLBaseElement,
4428
+ "meta" => HTMLMetaElement,
4429
+ "style" => HTMLStyleElement,
4430
+ "title" => HTMLTitleElement,
4431
+ "q" => HTMLQuoteElement,
4432
+ "blockquote" => HTMLQuoteElement,
4433
+ "ins" => HTMLModElement,
4434
+ "del" => HTMLModElement,
4435
+ "div" => HTMLDivElement,
4436
+ "span" => HTMLSpanElement,
4437
+ "p" => HTMLParagraphElement,
4438
+ "h1" => HTMLHeadingElement,
4439
+ "h2" => HTMLHeadingElement,
4440
+ "h3" => HTMLHeadingElement,
4441
+ "h4" => HTMLHeadingElement,
4442
+ "h5" => HTMLHeadingElement,
4443
+ "h6" => HTMLHeadingElement,
4444
+ "br" => HTMLBRElement,
4445
+ "hr" => HTMLHRElement,
4446
+ "pre" => HTMLPreElement,
4447
+ "body" => HTMLBodyElement,
4448
+ "head" => HTMLHeadElement,
4449
+ "html" => HTMLHtmlElement
4450
+ }.freeze
4451
+
4452
+ def self.element_class_for(tag_name)
4453
+ HTML_ELEMENT_CLASSES[tag_name.to_s.downcase] || Element
4454
+ end
4455
+ end