dommy 0.16.0 → 0.17.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dommy/animation.rb +1 -0
  3. data/lib/dommy/backend.rb +3 -0
  4. data/lib/dommy/blob.rb +6 -0
  5. data/lib/dommy/browser.rb +12 -4
  6. data/lib/dommy/character_data.rb +1 -13
  7. data/lib/dommy/cookie_store.rb +2 -1
  8. data/lib/dommy/document.rb +67 -30
  9. data/lib/dommy/element.rb +11 -0
  10. data/lib/dommy/element_internals.rb +12 -0
  11. data/lib/dommy/encodings/legacy_multibyte.rb +800 -0
  12. data/lib/dommy/encodings/multibyte_overrides.rb +76 -0
  13. data/lib/dommy/encodings/prescan.rb +234 -0
  14. data/lib/dommy/encodings.rb +121 -57
  15. data/lib/dommy/event.rb +107 -54
  16. data/lib/dommy/event_source.rb +1 -0
  17. data/lib/dommy/fetch.rb +244 -53
  18. data/lib/dommy/file_reader.rb +1 -0
  19. data/lib/dommy/form_data.rb +22 -9
  20. data/lib/dommy/form_entry_list.rb +3 -3
  21. data/lib/dommy/html_canvas_element.rb +85 -12
  22. data/lib/dommy/html_elements/base.rb +1 -0
  23. data/lib/dommy/html_elements/forms/input.rb +3 -0
  24. data/lib/dommy/html_elements/interactive.rb +56 -48
  25. data/lib/dommy/html_elements/metadata.rb +23 -0
  26. data/lib/dommy/interaction/form_submission.rb +24 -42
  27. data/lib/dommy/internal/accessibility_tree.rb +149 -162
  28. data/lib/dommy/internal/accessibility_visibility.rb +16 -17
  29. data/lib/dommy/internal/accessible_name.rb +231 -246
  30. data/lib/dommy/internal/aria_role.rb +182 -195
  31. data/lib/dommy/internal/aria_snapshot.rb +35 -38
  32. data/lib/dommy/internal/aria_state.rb +102 -118
  33. data/lib/dommy/internal/autofill.rb +75 -75
  34. data/lib/dommy/internal/backend_prefilter.rb +127 -130
  35. data/lib/dommy/internal/child_navigable.rb +86 -83
  36. data/lib/dommy/internal/close_watcher_manager.rb +1 -0
  37. data/lib/dommy/internal/css/ancestor_filter.rb +64 -64
  38. data/lib/dommy/internal/css/calc.rb +90 -71
  39. data/lib/dommy/internal/css/cascade.rb +69 -67
  40. data/lib/dommy/internal/css/color.rb +249 -187
  41. data/lib/dommy/internal/css/computed_style_builder.rb +34 -0
  42. data/lib/dommy/internal/css/computed_style_declaration.rb +50 -11
  43. data/lib/dommy/internal/css/counters.rb +67 -67
  44. data/lib/dommy/internal/css/custom_properties.rb +114 -114
  45. data/lib/dommy/internal/css/parser.rb +277 -239
  46. data/lib/dommy/internal/css/property_registry.rb +321 -302
  47. data/lib/dommy/internal/css/renderability.rb +37 -35
  48. data/lib/dommy/internal/css/rule_index.rb +2 -2
  49. data/lib/dommy/internal/css/rule_index_inputs.rb +2 -2
  50. data/lib/dommy/internal/css/supports.rb +115 -113
  51. data/lib/dommy/internal/css/ua_stylesheet.rb +55 -53
  52. data/lib/dommy/internal/css_rule_text.rb +121 -119
  53. data/lib/dommy/internal/css_source.rb +1 -3
  54. data/lib/dommy/internal/css_syntax.rb +157 -92
  55. data/lib/dommy/internal/declarative_shadow_roots.rb +1 -0
  56. data/lib/dommy/internal/directionality.rb +120 -123
  57. data/lib/dommy/internal/document_live_ranges.rb +26 -1
  58. data/lib/dommy/internal/dom_matching.rb +185 -184
  59. data/lib/dommy/internal/element_geometry.rb +8 -1
  60. data/lib/dommy/internal/element_matching.rb +71 -69
  61. data/lib/dommy/internal/element_shadow.rb +1 -13
  62. data/lib/dommy/internal/element_state.rb +255 -265
  63. data/lib/dommy/internal/event_handlers.rb +66 -64
  64. data/lib/dommy/internal/exception_report.rb +88 -86
  65. data/lib/dommy/internal/focusability.rb +205 -205
  66. data/lib/dommy/internal/global_functions.rb +49 -47
  67. data/lib/dommy/internal/idl_reflection.rb +99 -97
  68. data/lib/dommy/internal/js_number.rb +35 -33
  69. data/lib/dommy/internal/light_dismiss.rb +130 -132
  70. data/lib/dommy/internal/literal_lookup.rb +75 -75
  71. data/lib/dommy/internal/mime_type.rb +54 -49
  72. data/lib/dommy/internal/mutation_coordinator.rb +46 -0
  73. data/lib/dommy/internal/mutation_observer_microtask.rb +60 -0
  74. data/lib/dommy/internal/namespaces.rb +51 -49
  75. data/lib/dommy/internal/node_adopter.rb +25 -1
  76. data/lib/dommy/internal/node_equality.rb +61 -59
  77. data/lib/dommy/internal/post_insertion_steps.rb +1 -0
  78. data/lib/dommy/internal/referrer.rb +144 -0
  79. data/lib/dommy/internal/rendered_text/collector.rb +22 -3
  80. data/lib/dommy/internal/rendered_text/computed_box.rb +3 -2
  81. data/lib/dommy/internal/rendered_text/fragment.rb +37 -38
  82. data/lib/dommy/internal/rendered_text/items.rb +7 -5
  83. data/lib/dommy/internal/selector_matcher.rb +361 -359
  84. data/lib/dommy/internal/selector_parser.rb +74 -62
  85. data/lib/dommy/internal/sequential_focus_navigation.rb +150 -151
  86. data/lib/dommy/internal/shadow_root_registry.rb +7 -0
  87. data/lib/dommy/internal/slot_assignment.rb +117 -0
  88. data/lib/dommy/internal/slot_signals.rb +125 -0
  89. data/lib/dommy/internal/unsafe_html.rb +79 -77
  90. data/lib/dommy/internal/url_parser.rb +5 -29
  91. data/lib/dommy/internal/web_idl.rb +158 -153
  92. data/lib/dommy/internal/window_bound.rb +14 -0
  93. data/lib/dommy/internal/window_constructors.rb +3 -0
  94. data/lib/dommy/internal/xml_serialization.rb +298 -296
  95. data/lib/dommy/js/dom_interfaces.rb +13 -1
  96. data/lib/dommy/js/host_bridge.rb +23 -1
  97. data/lib/dommy/js/host_callbacks.rb +7 -0
  98. data/lib/dommy/js/host_runtime.js +625 -61
  99. data/lib/dommy/js/import_map.rb +1 -1
  100. data/lib/dommy/js/observable_runtime.js +295 -128
  101. data/lib/dommy/js/webidl_members.js +1 -1
  102. data/lib/dommy/js/webidl_signatures.js +35 -6
  103. data/lib/dommy/media_query_list.rb +1 -0
  104. data/lib/dommy/message_channel.rb +2 -0
  105. data/lib/dommy/mutation_observer.rb +25 -29
  106. data/lib/dommy/navigator.rb +4 -1
  107. data/lib/dommy/node.rb +4 -0
  108. data/lib/dommy/notification.rb +1 -0
  109. data/lib/dommy/promise.rb +12 -0
  110. data/lib/dommy/range.rb +20 -3
  111. data/lib/dommy/shadow_root.rb +25 -1
  112. data/lib/dommy/streams.rb +18 -0
  113. data/lib/dommy/style_declaration.rb +15 -4
  114. data/lib/dommy/text_codec.rb +7 -5
  115. data/lib/dommy/tree_walker.rb +68 -61
  116. data/lib/dommy/version.rb +1 -1
  117. data/lib/dommy/web_socket.rb +1 -0
  118. data/lib/dommy/window.rb +22 -4
  119. data/lib/dommy/worker.rb +1 -0
  120. data/lib/dommy/xml_http_request.rb +12 -2
  121. data/lib/dommy.rb +17 -2
  122. metadata +11 -3
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by script/build_encoding_tables.rb from the Encoding Standard's
4
+ # index-{jis0208,jis0212,big5,euc-kr,gb18030}.txt, against the Ruby
5
+ # encodings LegacyIndexes.ruby_lookup reads. Do not edit by hand.
6
+ #
7
+ # Spec: https://encoding.spec.whatwg.org/#indexes
8
+
9
+ module Dommy
10
+ module Encodings
11
+ # Pointer => code point, where the spec's index and Ruby's table differ.
12
+ MULTIBYTE_INDEX_OVERRIDES = {
13
+ jis0208: {
14
+ }.freeze,
15
+ jis0212: {
16
+ 116 => 0xFF5E,
17
+ }.freeze,
18
+ big5: {
19
+ 1000 => 0x3875, 1001 => 0x21D53, 1002 => 0x2369E, 1003 => 0x26021, 1004 => 0x3EEC, 1005 => 0x258DE,
20
+ 1006 => 0x3AF5, 1007 => 0x7AFC, 1008 => 0x9F97, 1009 => 0x24161, 1010 => 0x2890D, 1011 => 0x231EA,
21
+ 1012 => 0x20A8A, 1013 => 0x2325E, 1014 => 0x430A, 1015 => 0x8484, 1016 => 0x9F96, 1017 => 0x942F,
22
+ 1018 => 0x4930, 1019 => 0x8613, 1020 => 0x5896, 1021 => 0x974A, 1022 => 0x9218, 1023 => 0x79D0,
23
+ 1024 => 0x7A32, 1025 => 0x6660, 1026 => 0x6A29, 1027 => 0x889D, 1028 => 0x744C, 1029 => 0x7BC5,
24
+ 1030 => 0x6782, 1031 => 0x7A2C, 1032 => 0x524F, 1033 => 0x9046, 1034 => 0x34E6, 1035 => 0x73C4,
25
+ 1036 => 0x25DB9, 1037 => 0x74C6, 1038 => 0x9FC7, 1039 => 0x57B3, 1040 => 0x492F, 1041 => 0x544C,
26
+ 1042 => 0x4131, 1043 => 0x2368E, 1044 => 0x5818, 1045 => 0x7A72, 1046 => 0x27B65, 1047 => 0x8B8F,
27
+ 1048 => 0x46AE, 1049 => 0x26E88, 1050 => 0x4181, 1051 => 0x25D99, 1052 => 0x7BAE, 1053 => 0x224BC,
28
+ 1054 => 0x9FC8, 1055 => 0x224C1, 1056 => 0x224C9, 1057 => 0x224CC, 1058 => 0x9FC9, 1059 => 0x8504,
29
+ 1060 => 0x235BB, 1061 => 0x40B4, 1062 => 0x9FCA, 1063 => 0x44E1, 1064 => 0x2ADFF, 1065 => 0x62C1,
30
+ 1066 => 0x706E, 1067 => 0x9FCB, 2082 => 0x7BB8, 2088 => 0x7C06, 2103 => 0x7CCE, 2114 => 0x7DD2,
31
+ 2123 => 0x7E1D, 2148 => 0x8005, 2151 => 0x8028, 2221 => 0x83C1, 2239 => 0x84A8, 2244 => 0x840F,
32
+ 2303 => 0x89A6, 2304 => 0x89A9, 2354 => 0x8D77, 2400 => 0x90FD, 2413 => 0x92B9, 2477 => 0x975C,
33
+ 2498 => 0x97FF, 2605 => 0x9F16, 2673 => 0x8503, 2746 => 0x5159, 2747 => 0x515B, 2748 => 0x515D,
34
+ 2749 => 0x515E, 2771 => 0x936E, 2780 => 0x7479, 2990 => 0x6D67, 3087 => 0x799B, 3259 => 0x9097,
35
+ 3301 => 0x975D, 3436 => 0x701E, 3451 => 0x5B28, 4136 => 0x7201, 4138 => 0x77D7, 4141 => 0x7E87,
36
+ 4182 => 0x99D6, 4206 => 0x91D4, 4220 => 0x60DE, 4230 => 0x6FB6, 4241 => 0x8F36, 4258 => 0x4FBB,
37
+ 4273 => 0x71DF, 4279 => 0x9104, 4282 => 0x9DF0, 4294 => 0x83CF, 4329 => 0x5C10, 4330 => 0x79E3,
38
+ 4349 => 0x5A67, 4419 => 0x8F0B, 4422 => 0x7B51, 4494 => 0x62D0, 4624 => 0x6062, 4694 => 0x75F9,
39
+ 4708 => 0x6C4A, 4742 => 0x9B2E, 4748 => 0x9F17, 4815 => 0x50ED, 4828 => 0x5F0C, 4902 => 0x880F,
40
+ 4922 => 0x62CE, 4982 => 0x7468, 4992 => 0x7162, 4997 => 0x7250, 5029 => 0x2027, 5038 => 0xFE51,
41
+ 5050 => 0x2574, 5120 => 0x00AF, 5121 => 0xFFE3, 5123 => 0x02CD, 5153 => 0xFF5E, 5168 => 0x2295,
42
+ 5169 => 0x2299, 5180 => 0xFF0F, 5181 => 0xFF3C, 5182 => 0x2215, 5183 => 0xFE68, 5185 => 0xFFE5,
43
+ 5187 => 0xFFE0, 5188 => 0xFFE1, 5287 => 0x5341, 5289 => 0x5345, 5432 => 0x2400, 5433 => 0x2401,
44
+ 5434 => 0x2402, 5435 => 0x2403, 5436 => 0x2404, 5437 => 0x2405, 5438 => 0x2406, 5439 => 0x2407,
45
+ 5440 => 0x2408, 5441 => 0x2409, 5442 => 0x240A, 5443 => 0x240B, 5444 => 0x240C, 5445 => 0x240D,
46
+ 5446 => 0x240E, 5447 => 0x240F, 5448 => 0x2410, 5449 => 0x2411, 5450 => 0x2412, 5451 => 0x2413,
47
+ 5452 => 0x2414, 5453 => 0x2415, 5454 => 0x2416, 5455 => 0x2417, 5456 => 0x2418, 5457 => 0x2419,
48
+ 5458 => 0x241A, 5459 => 0x241B, 5460 => 0x241C, 5461 => 0x241D, 5462 => 0x241E, 5463 => 0x241F,
49
+ 5464 => 0x2421, 5465 => 0x20AC, 10942 => 0x5EF4, 10946 => 0x65E0, 10948 => 0x7676, 10950 => 0x96B6,
50
+ 10957 => 0x3003, 10958 => 0x4EDD, 18975 => 0x255E, 18976 => 0x256A, 18977 => 0x2561, 18991 => 0x2550,
51
+ 18992 => 0x256D, 18993 => 0x256E, 18994 => 0x2570, 18995 => 0x256F, 19028 => 0x5029, 19035 => 0x507D,
52
+ 19088 => 0x5305, 19096 => 0x5344, 19112 => 0x537F, 19162 => 0x5605, 19240 => 0x5A77, 19299 => 0x5E75,
53
+ 19305 => 0x5ED0, 19326 => 0x5F58, 19355 => 0x60A4, 19398 => 0x6490, 19439 => 0x6674, 19454 => 0x675E,
54
+ 19553 => 0x6C9C, 19554 => 0x6E1D, 19557 => 0x6E2F, 19611 => 0x716E, 19643 => 0x732A, 19672 => 0x745C,
55
+ 19697 => 0x74E9, 19748 => 0x7809,
56
+ }.freeze,
57
+ euc_kr: {
58
+ }.freeze,
59
+ gb18030: {
60
+ 6555 => 0x3000, 7182 => 0xFE10, 7183 => 0xFE12, 7184 => 0xFE11, 7185 => 0xFE13, 7186 => 0xFE14,
61
+ 7187 => 0xFE15, 7188 => 0xFE16, 7201 => 0xFE17, 7202 => 0xFE18, 7208 => 0xFE19, 7533 => 0x1E3F,
62
+ 23775 => 0x9FB4, 23783 => 0x9FB5, 23788 => 0x9FB6, 23789 => 0x9FB7, 23795 => 0x9FB8, 23812 => 0x9FB9,
63
+ 23829 => 0x9FBA, 23845 => 0x9FBB,
64
+ }.freeze,
65
+ }.freeze
66
+
67
+ # Pointers Ruby's table maps that the spec's index leaves empty.
68
+ MULTIBYTE_INDEX_NULLS = {
69
+ jis0208: [8836..10715].freeze,
70
+ jis0212: [].freeze,
71
+ big5: [].freeze,
72
+ euc_kr: [].freeze,
73
+ gb18030: [].freeze,
74
+ }.freeze
75
+ end
76
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dommy
4
+ module Encodings
5
+ # HTML's "prescan a byte stream to determine its encoding" over the first
6
+ # 1024 bytes: a walk over the bytes that skips comments and other tags as
7
+ # the tokenizer would, and reads each `<meta>`'s attributes (quoted values
8
+ # included) for a `charset`, or an `http-equiv="content-type"` with a
9
+ # `content` naming one. Running out of bytes ends it with what a leading
10
+ # `<?xml … encoding="…"?>` declares, if anything.
11
+ #
12
+ # Spec: https://html.spec.whatwg.org/#prescan-a-byte-stream-to-determine-its-encoding
13
+ class Prescan
14
+ LIMIT = 1024
15
+ # ASCII whitespace, as bytes: TAB, LF, FF, CR, SPACE.
16
+ SPACE = [0x09, 0x0A, 0x0C, 0x0D, 0x20].freeze
17
+ SPACE_OR_SLASH = (SPACE + [0x2F]).freeze
18
+ SPACE_OR_GT = (SPACE + [0x3E]).freeze
19
+ private_constant :LIMIT, :SPACE, :SPACE_OR_SLASH, :SPACE_OR_GT
20
+
21
+ def self.call(source) = new(source.to_s.b.byteslice(0, LIMIT)).run
22
+
23
+ def initialize(bytes)
24
+ @bytes = bytes
25
+ @position = 0
26
+ end
27
+
28
+ def run
29
+ utf16 = utf16_xml_declaration
30
+ return utf16 if utf16
31
+
32
+ catch(:end) do
33
+ # Only a `<` starts anything; every other byte is passed over.
34
+ while (@position = @bytes.index("<", @position))
35
+ found =
36
+ if starts_with?("<!--") then skip_comment
37
+ elsif meta_tag? then meta
38
+ elsif tag? then skip_tag
39
+ elsif starts_with?("<!") || starts_with?("</") || starts_with?("<?") then skip_to(0x3E)
40
+ end
41
+ return found if found.is_a?(String)
42
+
43
+ @position += 1
44
+ end
45
+ end
46
+ xml_encoding
47
+ end
48
+
49
+ private
50
+
51
+ # "Prescan for UTF-16 XML declarations": `<?x` in UTF-16 is UTF-16.
52
+ def utf16_xml_declaration
53
+ return "UTF-16LE" if @bytes.start_with?("<\0?\0x\0".b)
54
+
55
+ "UTF-16BE" if @bytes.start_with?("\0<\0?\0x".b)
56
+ end
57
+
58
+ def byte
59
+ @bytes.getbyte(@position) || throw(:end)
60
+ end
61
+
62
+ def starts_with?(text) = @bytes.byteslice(@position, text.bytesize) == text
63
+
64
+ def letter?(value) = value && ((0x41..0x5A).cover?(value) || (0x61..0x7A).cover?(value))
65
+
66
+ # `<meta` (ASCII case-insensitive) followed by whitespace or `/`.
67
+ def meta_tag?
68
+ @bytes.byteslice(@position, 5)&.casecmp?("<meta") && SPACE_OR_SLASH.include?(@bytes.getbyte(@position + 5))
69
+ end
70
+
71
+ # `<` or `</` followed by an ASCII letter.
72
+ def tag?
73
+ return false unless @bytes.getbyte(@position) == 0x3C
74
+
75
+ letter?(@bytes.getbyte(@position + 1)) || (@bytes.getbyte(@position + 1) == 0x2F && letter?(@bytes.getbyte(@position + 2)))
76
+ end
77
+
78
+ # To the first `>` preceded by two `-` after the `<`; those may be the
79
+ # `<!--`'s own, so `<!-->` and `<!--->` are closed comments.
80
+ def skip_comment
81
+ index = @bytes.index("-->".b, @position + 2) || throw(:end)
82
+ @position = index + 2
83
+ end
84
+
85
+ def skip_to(value)
86
+ index = @bytes.index(value.chr, @position + 1) || throw(:end)
87
+ @position = index
88
+ end
89
+
90
+ # A tag that is not a `<meta>`: past its name, then its attributes, so a
91
+ # `>` inside a quoted value does not end it.
92
+ def skip_tag
93
+ @position += 1
94
+ @position += 1 until SPACE_OR_GT.include?(byte)
95
+ nil while attribute
96
+ end
97
+
98
+ # The `<meta>` steps: its encoding when it declares one, else nil.
99
+ def meta
100
+ @position += 5
101
+ names = []
102
+ got_pragma = false
103
+ need_pragma = nil
104
+ charset = nil
105
+ while (name, value = attribute)
106
+ next if names.include?(name)
107
+
108
+ names << name
109
+ case name
110
+ when "http-equiv"
111
+ got_pragma = true if value == "content-type"
112
+ when "content"
113
+ encoding = encoding_from_content(value)
114
+ if encoding && charset.nil?
115
+ charset = encoding
116
+ need_pragma = true
117
+ end
118
+ when "charset"
119
+ charset = Encodings.get(value) || false
120
+ need_pragma = false
121
+ end
122
+ end
123
+ return nil if need_pragma.nil? || (need_pragma && !got_pragma) || !charset
124
+ return "UTF-8" if charset.start_with?("UTF-16")
125
+
126
+ charset == "x-user-defined" ? "windows-1252" : charset
127
+ end
128
+
129
+ # "Get an attribute": the next attribute's name and value (ASCII
130
+ # lowercased, as bytes), or nil at the tag's `>`.
131
+ def attribute
132
+ @position += 1 while SPACE_OR_SLASH.include?(byte)
133
+ return nil if byte == 0x3E
134
+
135
+ name = +"".b
136
+ value = +"".b
137
+ loop do
138
+ current = byte
139
+ if current == 0x3D && !name.empty?
140
+ @position += 1
141
+ break
142
+ elsif SPACE.include?(current)
143
+ @position += 1 while SPACE.include?(byte)
144
+ return [name, value] unless byte == 0x3D
145
+
146
+ @position += 1
147
+ break
148
+ elsif current == 0x2F || current == 0x3E
149
+ return [name, value]
150
+ end
151
+ name << lower(current)
152
+ @position += 1
153
+ end
154
+
155
+ @position += 1 while SPACE.include?(byte)
156
+ current = byte
157
+ if current == 0x22 || current == 0x27
158
+ loop do
159
+ @position += 1
160
+ if byte == current
161
+ @position += 1
162
+ return [name, value]
163
+ end
164
+ value << lower(byte)
165
+ end
166
+ end
167
+ return [name, value] if current == 0x3E
168
+
169
+ loop do
170
+ current = byte
171
+ return [name, value] if SPACE.include?(current) || current == 0x3E
172
+
173
+ value << lower(current)
174
+ @position += 1
175
+ end
176
+ end
177
+
178
+ def lower(value) = (0x41..0x5A).cover?(value) ? value + 0x20 : value
179
+
180
+ # HTML's "extracting a character encoding from a meta element", for a
181
+ # `content` attribute's value: the encoding its `charset=` names, or nil.
182
+ #
183
+ # Spec: https://html.spec.whatwg.org/#algorithm-for-extracting-a-character-encoding-from-a-meta-element
184
+ def encoding_from_content(content)
185
+ position = 0
186
+ loop do
187
+ index = content.index(/charset/i, position) or return nil
188
+ position = index + 7
189
+ position += 1 while SPACE.include?(content.getbyte(position))
190
+ next unless content.getbyte(position) == 0x3D
191
+
192
+ position += 1
193
+ position += 1 while SPACE.include?(content.getbyte(position))
194
+ first = content.getbyte(position) or return nil
195
+ if first == 0x22 || first == 0x27
196
+ close = content.index(first.chr, position + 1) or return nil
197
+ return Encodings.get(content.byteslice(position + 1...close))
198
+ end
199
+ return Encodings.get(content.byteslice(position..)[/\A[^\t\n\f\r ;]*/n])
200
+ end
201
+ end
202
+
203
+ # "Get an XML encoding": the encoding a leading `<?xml` declaration's
204
+ # `encoding="…"` (before its `>`, quoted, with no byte at or below 0x20)
205
+ # names, with UTF-16 read as UTF-8, or nil.
206
+ def xml_encoding
207
+ return nil unless @bytes.start_with?("<?xml")
208
+
209
+ finish = @bytes.index(">") or return nil
210
+ position = @bytes.index("encoding")
211
+ return nil if position.nil? || position >= finish
212
+
213
+ position = skip_controls(position + 8)
214
+ return nil unless @bytes.getbyte(position) == 0x3D
215
+
216
+ position = skip_controls(position + 1)
217
+ quote = @bytes.getbyte(position)
218
+ return nil unless quote == 0x22 || quote == 0x27
219
+
220
+ close = @bytes.index(quote.chr, position + 1) or return nil
221
+ label = @bytes.byteslice(position + 1...close)
222
+ return nil if label.each_byte.any? { |value| value <= 0x20 }
223
+
224
+ name = Encodings.get(label)
225
+ name&.start_with?("UTF-16") ? "UTF-8" : name
226
+ end
227
+
228
+ def skip_controls(position)
229
+ position += 1 while (value = @bytes.getbyte(position)) && value <= 0x20
230
+ position
231
+ end
232
+ end
233
+ end
234
+ end
@@ -8,27 +8,14 @@ module Dommy
8
8
  # each encoding that turns bytes into code points, streaming, with the
9
9
  # replacement-or-throw error mode TextDecoder exposes as `fatal`.
10
10
  #
11
- # UTF-8, UTF-16 and the legacy single-byte encodings are decoded here, from
12
- # the spec's own algorithms and index tables. The legacy multi-byte
13
- # encodings (Big5, EUC-JP, ISO-2022-JP, Shift_JIS, EUC-KR, GBK, gb18030) go
14
- # through Ruby's converters for the encoding closest to the spec's index,
15
- # which differ from it in a few code points and in how many U+FFFD a broken
16
- # sequence yields.
11
+ # Every decoder is the spec's own algorithm: UTF-8, UTF-16 and the legacy
12
+ # single-byte encodings here, with their index tables, and the legacy
13
+ # multi-byte ones (Big5, EUC-JP, ISO-2022-JP, Shift_JIS, EUC-KR, GBK,
14
+ # gb18030) in encodings/legacy_multibyte, which read their indexes through
15
+ # the Ruby encoding that matches each.
17
16
  #
18
17
  # Spec: https://encoding.spec.whatwg.org/
19
18
  module Encodings
20
- # The Ruby encoding whose mapping is closest to the spec's index for each
21
- # legacy multi-byte encoding.
22
- RUBY_ENCODINGS = {
23
- "Big5" => "Big5-HKSCS",
24
- "EUC-JP" => "EUC-JP",
25
- "ISO-2022-JP" => "ISO-2022-JP",
26
- "Shift_JIS" => "Windows-31J",
27
- "EUC-KR" => "CP949",
28
- "GBK" => "GBK",
29
- "gb18030" => "GB18030"
30
- }.freeze
31
-
32
19
  REPLACEMENT_CHARACTER = 0xFFFD
33
20
 
34
21
  # The name of the encoding `label` stands for, or nil. Only ASCII
@@ -36,7 +23,24 @@ module Dommy
36
23
  #
37
24
  # Spec: https://encoding.spec.whatwg.org/#concept-encoding-get
38
25
  def self.get(label)
39
- LABELS[label.gsub(/\A[\t\n\f\r ]+|[\t\n\f\r ]+\z/, "").downcase]
26
+ LABELS[label.gsub(/\A[\t\n\f\r ]+|[\t\n\f\r ]+\z/, "").downcase(:ascii)]
27
+ end
28
+
29
+ # The encoding `encoding` names, given as an Encoding Standard name or
30
+ # label; an ArgumentError for anything else.
31
+ def self.name_for(encoding)
32
+ text = encoding.to_s
33
+ return text if LABELS.value?(text)
34
+
35
+ get(text) or raise ArgumentError, "unknown encoding: #{encoding.inspect}"
36
+ end
37
+
38
+ # The encoding text is encoded in when `name` is the one asked for: UTF-8
39
+ # in place of replacement, UTF-16BE and UTF-16LE, which have no encoder.
40
+ #
41
+ # Spec: https://encoding.spec.whatwg.org/#get-an-output-encoding
42
+ def self.output_encoding(name)
43
+ name.start_with?("UTF-16") || name == "replacement" ? "UTF-8" : name
40
44
  end
41
45
 
42
46
  # The byte-order marks that name their own encoding, longest first.
@@ -67,16 +71,98 @@ module Dommy
67
71
  valid_utf8(bytes, name) || decoder_for(name).decode(bytes, flush: true).pack("U*")
68
72
  end
69
73
 
70
- # A document's bytes as text, in the encoding its response declares: a
71
- # byte-order mark first, then the Content-Type's `charset`, else UTF-8.
72
- # (HTML's prescan of a `<meta charset>` is not done.)
74
+ # A document's bytes as text, and the encoding they were decoded with
75
+ # (::document_encoding), which is the document's: a caller hands both to
76
+ # the document, so that its URLs encode a query as its bytes were.
77
+ # `html: false` (an XML or a text document) skips the `<meta>` prescan.
78
+ def self.decode_document(bytes, content_type, html: true)
79
+ encoding = document_encoding(bytes, content_type, html: html)
80
+ [decode(bytes.to_s, encoding), encoding]
81
+ end
82
+
83
+ # The encoding an HTML document's bytes are in: a byte-order mark's, else
84
+ # the Content-Type's `charset`, else what a `<meta>` in the first 1024
85
+ # bytes declares (the prescan), else UTF-8.
73
86
  #
74
87
  # Spec: https://html.spec.whatwg.org/#determining-the-character-encoding
75
- def self.decode_document(bytes, content_type)
88
+ def self.document_encoding(bytes, content_type, html: true)
89
+ bytes = bytes.to_s.b
90
+ BOMS.each { |mark, name| return name if bytes.start_with?(mark) }
76
91
  label = Internal::MimeType.charset_of(content_type.to_s)
77
- decode(bytes.to_s, (label && get(label)) || "UTF-8")
92
+ (label && get(label)) || (html && prescan(bytes)) || "UTF-8"
78
93
  end
79
94
 
95
+ # HTML's "prescan a byte stream to determine its encoding" (Prescan): what
96
+ # the first 1024 bytes' `<meta charset>` or `<meta http-equiv=content-type
97
+ # content="…; charset=…">` declares, else what a leading `<?xml … encoding=…?>`
98
+ # does, or nil. A declared UTF-16 is UTF-8 (the bytes are ASCII-compatible
99
+ # if the tag was read), and x-user-defined windows-1252; `<?x` in UTF-16
100
+ # itself is that UTF-16.
101
+ def self.prescan(source) = Prescan.call(source)
102
+
103
+ # The bytes `string` encodes to in the encoding named `name` (as `get`
104
+ # returns it); a code point the encoding cannot represent is the bytes the
105
+ # block returns for it. The legacy multi-byte encoders and the single-byte
106
+ # ones (an index read backwards) are the spec's own; Ruby encodes the rest,
107
+ # and UTF-8 is the string's own bytes.
108
+ #
109
+ # Spec: https://encoding.spec.whatwg.org/#encode
110
+ def self.encode(name, string, &unmappable)
111
+ return LegacyEncoders.encode(name, string, &unmappable).pack("C*") if LegacyEncoders.handles?(name)
112
+ return single_byte_encode(single_byte_pointers(name), string, &unmappable) if SINGLE_BYTE_INDEXES.key?(name)
113
+ return x_user_defined_encode(string, &unmappable) if name == "x-user-defined"
114
+
115
+ ruby = begin
116
+ ::Encoding.find(name.to_s)
117
+ rescue ArgumentError
118
+ nil
119
+ end
120
+ return string.b if ruby.nil? || ruby == ::Encoding::UTF_8
121
+
122
+ begin
123
+ string.encode(ruby).b
124
+ rescue ::Encoding::UndefinedConversionError, ::Encoding::InvalidByteSequenceError
125
+ string.each_char.each_with_object(+"".b) do |char, out|
126
+ out << char.encode(ruby).b
127
+ rescue ::Encoding::UndefinedConversionError, ::Encoding::InvalidByteSequenceError
128
+ out << unmappable.call(char.ord).pack("C*")
129
+ end
130
+ end
131
+ end
132
+
133
+ # A single-byte index read backwards, code point => byte, made once per
134
+ # encoding.
135
+ @single_byte_pointers = {}
136
+ def self.single_byte_pointers(name)
137
+ @single_byte_pointers[name] ||= SINGLE_BYTE_INDEXES[name].each_with_index.each_with_object({}) do |(code_point, pointer), map|
138
+ map[code_point] ||= pointer + 0x80 if code_point
139
+ end.freeze
140
+ end
141
+ private_class_method :single_byte_pointers
142
+
143
+ # Spec: https://encoding.spec.whatwg.org/#single-byte-encoder
144
+ def self.single_byte_encode(pointers, string, &unmappable)
145
+ string.each_codepoint.each_with_object(+"".b) do |code_point, out|
146
+ byte = code_point <= 0x7F ? code_point : pointers[code_point]
147
+ byte ? out << byte : out << unmappable.call(code_point).pack("C*")
148
+ end
149
+ end
150
+ private_class_method :single_byte_encode
151
+
152
+ # Spec: https://encoding.spec.whatwg.org/#x-user-defined-encoder
153
+ def self.x_user_defined_encode(string, &unmappable)
154
+ string.each_codepoint.each_with_object(+"".b) do |code_point, out|
155
+ if code_point <= 0x7F
156
+ out << code_point
157
+ elsif code_point.between?(0xF780, 0xF7FF)
158
+ out << (code_point - 0xF780 + 0x80)
159
+ else
160
+ out << unmappable.call(code_point).pack("C*")
161
+ end
162
+ end
163
+ end
164
+ private_class_method :x_user_defined_encode
165
+
80
166
  # Already-valid UTF-8 needs no decoding: the spec's decoder would walk it
81
167
  # byte by byte and rebuild it code point by code point, which measures ~40x
82
168
  # the cost of asking Ruby, on the whole-buffer path an XHR response takes.
@@ -101,17 +187,21 @@ module Dommy
101
187
  when "UTF-16BE" then Utf16Decoder.new(fatal, big_endian: true)
102
188
  when "x-user-defined" then XUserDefinedDecoder.new(fatal)
103
189
  when "replacement" then ReplacementDecoder.new(fatal)
190
+ when "ISO-2022-JP" then Iso2022JpDecoder.new(fatal)
191
+ when "Shift_JIS" then ShiftJisDecoder.new(fatal)
192
+ when "EUC-JP" then EucJpDecoder.new(fatal)
193
+ when "Big5" then Big5Decoder.new(fatal)
194
+ when "EUC-KR" then EucKrDecoder.new(fatal)
195
+ # GBK's decoder is gb18030's.
196
+ when "GBK", "gb18030" then Gb18030Decoder.new(fatal)
104
197
  else
105
198
  index = SINGLE_BYTE_INDEXES[name]
106
199
  return SingleByteDecoder.new(fatal, index) if index
107
200
 
108
- ruby_encoding = RUBY_ENCODINGS[name]
109
201
  # `name` comes from ::get, so every spec encoding is covered above. A
110
202
  # name from anywhere else is the caller's mistake, and says so rather
111
203
  # than surfacing a Hash's KeyError.
112
- raise ArgumentError, "not an encoding the standard names: #{name.inspect}" unless ruby_encoding
113
-
114
- ConverterDecoder.new(fatal, ruby_encoding)
204
+ raise ArgumentError, "not an encoding the standard names: #{name.inspect}"
115
205
  end
116
206
  end
117
207
 
@@ -307,34 +397,8 @@ module Dommy
307
397
  [replacement!]
308
398
  end
309
399
  end
310
-
311
- # A legacy multi-byte encoding, through Ruby's converter. Partial input is
312
- # held between calls; at a flush what is left over is an error.
313
- class ConverterDecoder < Decoder
314
- def initialize(fatal, ruby_encoding)
315
- super(fatal)
316
- options = fatal ? {} : {invalid: :replace, undef: :replace, replace: "\uFFFD"}
317
- @converter = ::Encoding::Converter.new(ruby_encoding, "UTF-8", **options)
318
- end
319
-
320
- def decode(bytes, flush:)
321
- output = +""
322
- result = @converter.primitive_convert(bytes.dup, output, nil, nil, partial_input: !flush)
323
- code_points = output.codepoints
324
- case result
325
- when :finished, :source_buffer_empty
326
- code_points
327
- when :incomplete_input, :invalid_byte_sequence, :undefined_conversion
328
- # Only reached in fatal mode, or by an incomplete sequence at a
329
- # flush; either way the stream is over.
330
- code_points << replacement!
331
- else
332
- # :destination_buffer_full and :after_output need a byte limit or an
333
- # output filter to occur, and this call passes neither. Reaching here
334
- # would mean the converter contract changed under us.
335
- raise "unexpected Encoding::Converter result: #{result.inspect}"
336
- end
337
- end
338
- end
339
400
  end
340
401
  end
402
+
403
+ require_relative "encodings/legacy_multibyte"
404
+ require_relative "encodings/prescan"