dommy 0.8.0 → 0.9.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/lib/dommy/animation.rb +4 -0
  4. data/lib/dommy/attr.rb +11 -5
  5. data/lib/dommy/backend/makiri_adapter.rb +330 -0
  6. data/lib/dommy/backend.rb +114 -33
  7. data/lib/dommy/blob.rb +2 -0
  8. data/lib/dommy/bridge.rb +11 -0
  9. data/lib/dommy/browser.rb +217 -0
  10. data/lib/dommy/compression_streams.rb +4 -0
  11. data/lib/dommy/crypto.rb +4 -0
  12. data/lib/dommy/css.rb +487 -50
  13. data/lib/dommy/custom_elements.rb +2 -2
  14. data/lib/dommy/data_transfer.rb +2 -0
  15. data/lib/dommy/data_uri.rb +35 -0
  16. data/lib/dommy/deferred_response.rb +59 -0
  17. data/lib/dommy/document.rb +386 -228
  18. data/lib/dommy/dom_exception.rb +2 -0
  19. data/lib/dommy/dom_parser.rb +7 -17
  20. data/lib/dommy/element.rb +502 -155
  21. data/lib/dommy/event.rb +240 -9
  22. data/lib/dommy/fetch.rb +152 -34
  23. data/lib/dommy/form_data.rb +2 -0
  24. data/lib/dommy/history.rb +2 -0
  25. data/lib/dommy/html_canvas_element.rb +230 -0
  26. data/lib/dommy/html_collection.rb +5 -6
  27. data/lib/dommy/html_elements.rb +304 -27
  28. data/lib/dommy/interaction/debug.rb +35 -0
  29. data/lib/dommy/interaction/dom_summary.rb +131 -0
  30. data/lib/dommy/interaction/driver.rb +244 -0
  31. data/lib/dommy/interaction/event_synthesis.rb +56 -0
  32. data/lib/dommy/interaction/field_interactor.rb +117 -0
  33. data/lib/dommy/interaction/form_submission.rb +268 -0
  34. data/lib/dommy/interaction/locator.rb +158 -0
  35. data/lib/dommy/interaction/role_query.rb +58 -0
  36. data/lib/dommy/interaction.rb +32 -0
  37. data/lib/dommy/internal/accessibility_tree.rb +215 -0
  38. data/lib/dommy/internal/accessible_description.rb +38 -0
  39. data/lib/dommy/internal/accessible_name.rb +301 -0
  40. data/lib/dommy/internal/aria_role.rb +252 -0
  41. data/lib/dommy/internal/aria_snapshot.rb +64 -0
  42. data/lib/dommy/internal/aria_state.rb +151 -0
  43. data/lib/dommy/internal/css/calc.rb +242 -0
  44. data/lib/dommy/internal/css/cascade.rb +430 -0
  45. data/lib/dommy/internal/css/color.rb +381 -0
  46. data/lib/dommy/internal/css/computed_style_declaration.rb +130 -0
  47. data/lib/dommy/internal/css/counters.rb +227 -0
  48. data/lib/dommy/internal/css/custom_properties.rb +183 -0
  49. data/lib/dommy/internal/css/media_query.rb +302 -0
  50. data/lib/dommy/internal/css/parser.rb +265 -0
  51. data/lib/dommy/internal/css/property_registry.rb +512 -0
  52. data/lib/dommy/internal/css/rule_index.rb +494 -0
  53. data/lib/dommy/internal/css/supports.rb +158 -0
  54. data/lib/dommy/internal/css/ua_stylesheet.rb +53 -0
  55. data/lib/dommy/internal/css_pseudo_handlers.rb +283 -42
  56. data/lib/dommy/internal/css_rule_text.rb +160 -0
  57. data/lib/dommy/internal/dom_matching.rb +80 -9
  58. data/lib/dommy/internal/element_matching.rb +109 -0
  59. data/lib/dommy/internal/global_functions.rb +33 -0
  60. data/lib/dommy/internal/mutation_coordinator.rb +95 -4
  61. data/lib/dommy/internal/namespaces.rb +49 -5
  62. data/lib/dommy/internal/node_wrapper_cache.rb +163 -26
  63. data/lib/dommy/internal/parent_node.rb +82 -5
  64. data/lib/dommy/internal/selector_ast.rb +124 -0
  65. data/lib/dommy/internal/selector_index.rb +146 -0
  66. data/lib/dommy/internal/selector_matcher.rb +756 -0
  67. data/lib/dommy/internal/selector_parser.rb +283 -131
  68. data/lib/dommy/internal/shadow_root_registry.rb +9 -2
  69. data/lib/dommy/internal/template_content_registry.rb +26 -18
  70. data/lib/dommy/internal/xml_serialization.rb +344 -0
  71. data/lib/dommy/intersection_observer.rb +2 -0
  72. data/lib/dommy/js/bridge_conformance.rb +80 -0
  73. data/lib/dommy/js/constructor_resolver.rb +44 -0
  74. data/lib/dommy/js/custom_element_bridge.rb +90 -0
  75. data/lib/dommy/js/dom_interfaces.rb +162 -0
  76. data/lib/dommy/js/handle_table.rb +60 -0
  77. data/lib/dommy/js/host_bridge.rb +517 -0
  78. data/lib/dommy/js/host_runtime.js +1495 -0
  79. data/lib/dommy/js/import_map.rb +58 -0
  80. data/lib/dommy/js/marshaller.rb +240 -0
  81. data/lib/dommy/js/module_loader.rb +99 -0
  82. data/lib/dommy/js/observable_runtime.js +742 -0
  83. data/lib/dommy/js/runtime.rb +115 -0
  84. data/lib/dommy/js/script_boot.rb +221 -0
  85. data/lib/dommy/js/wire_tags.rb +62 -0
  86. data/lib/dommy/location.rb +2 -0
  87. data/lib/dommy/media_query_list.rb +50 -14
  88. data/lib/dommy/message_channel.rb +22 -6
  89. data/lib/dommy/minitest/assertions.rb +27 -0
  90. data/lib/dommy/mutation_observer.rb +89 -4
  91. data/lib/dommy/navigator.rb +34 -2
  92. data/lib/dommy/node.rb +24 -14
  93. data/lib/dommy/notification.rb +2 -0
  94. data/lib/dommy/parser.rb +1 -1
  95. data/lib/dommy/performance.rb +21 -1
  96. data/lib/dommy/promise.rb +94 -10
  97. data/lib/dommy/range.rb +173 -31
  98. data/lib/dommy/resources.rb +178 -0
  99. data/lib/dommy/rspec/capy_style_matchers.rb +126 -0
  100. data/lib/dommy/scheduler.rb +149 -13
  101. data/lib/dommy/screen.rb +91 -0
  102. data/lib/dommy/shadow_root.rb +76 -13
  103. data/lib/dommy/storage.rb +2 -1
  104. data/lib/dommy/streams.rb +6 -0
  105. data/lib/dommy/text_codec.rb +7 -1
  106. data/lib/dommy/tree_walker.rb +33 -10
  107. data/lib/dommy/url.rb +13 -1
  108. data/lib/dommy/version.rb +1 -1
  109. data/lib/dommy/window.rb +199 -11
  110. data/lib/dommy/worker.rb +8 -4
  111. data/lib/dommy/xml_http_request.rb +47 -6
  112. data/lib/dommy.rb +36 -1
  113. metadata +96 -10
  114. data/lib/dommy/backend/nokogiri_adapter.rb +0 -127
  115. data/lib/dommy/backend/nokolexbor_adapter.rb +0 -117
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dom_matching"
4
+
5
+ module Dommy
6
+ module Internal
7
+ # Element-type-specific finders for links, forms, selects, and
8
+ # checkable fields. Lives in dommy core so that companion gems
9
+ # (currently dommy-rails) share a single finder implementation
10
+ # instead of re-implementing the selector / filter logic per gem.
11
+ #
12
+ # Attribute criteria (`href:`, `action:`, `name:`) accept a String
13
+ # (exact match), a Regexp, or any object responding to `matches?` —
14
+ # the extension point dommy-rails uses to inject URL normalization.
15
+ module ElementMatching
16
+ module_function
17
+
18
+ # Find <a> elements matching the given criteria.
19
+ def find_links(scope, text: nil, href: nil)
20
+ links = scope.query_selector_all("a[href]").to_a
21
+ links = links.select { |el| DomMatching.text_matches?(el.text_content, text) } if text
22
+ links = links.select { |el| attribute_matches?(el, "href", href) } if href
23
+ links
24
+ end
25
+
26
+ # Find <form> elements matching the given criteria. `method:` is
27
+ # matched against the effective method, honoring a hidden
28
+ # `_method` override field inside POST forms.
29
+ def find_forms(scope, action: nil, method: nil)
30
+ forms = scope.query_selector_all("form").to_a
31
+ forms = forms.select { |el| attribute_matches?(el, "action", action) } if action
32
+ forms = forms.select { |el| form_method_matches?(el, method) } if method
33
+ forms
34
+ end
35
+
36
+ # Find <select> elements matching the given criteria.
37
+ def find_selects(scope, name: nil, label: nil)
38
+ selects = scope.query_selector_all("select").to_a
39
+ selects = selects.select { |el| attribute_matches?(el, "name", name) } if name
40
+ selects = selects.select { |el| field_label_matches?(el, label) } if label
41
+ selects
42
+ end
43
+
44
+ # Find checkable fields (input[type=checkbox|radio]) matching the
45
+ # given criteria.
46
+ def find_checkable_fields(scope, name: nil, checked: nil)
47
+ fields = scope.query_selector_all("input[type='checkbox'], input[type='radio']").to_a
48
+ fields = fields.select { |el| attribute_matches?(el, "name", name) } if name
49
+ fields = fields.select { |el| el.get_attribute("checked") } if checked == true
50
+ fields = fields.reject { |el| el.get_attribute("checked") } if checked == false
51
+ fields
52
+ end
53
+
54
+ def attribute_matches?(element, attr_name, expected)
55
+ actual = element.get_attribute(attr_name).to_s
56
+ case expected
57
+ when Regexp
58
+ actual.match?(expected)
59
+ else
60
+ if expected.respond_to?(:matches?)
61
+ expected.matches?(actual)
62
+ else
63
+ actual == expected.to_s
64
+ end
65
+ end
66
+ end
67
+
68
+ # Labels associated with a field: a <label for=...> pointing at
69
+ # its id, and/or the nearest <label> ancestor wrapping it.
70
+ # Scans label elements instead of interpolating the id into a
71
+ # selector, so ids containing quotes cannot break the query.
72
+ def field_labels(field)
73
+ labels = []
74
+
75
+ id = field.get_attribute("id").to_s
76
+ unless id.empty?
77
+ for_label = field.owner_document.query_selector_all("label").to_a.find do |label|
78
+ label.get_attribute("for") == id
79
+ end
80
+ labels << for_label if for_label
81
+ end
82
+
83
+ parent = field.parent_node
84
+ while parent
85
+ if parent.respond_to?(:tag_name) && parent.tag_name == "LABEL"
86
+ labels << parent
87
+ break
88
+ end
89
+ parent = parent.respond_to?(:parent_node) ? parent.parent_node : nil
90
+ end
91
+
92
+ labels
93
+ end
94
+
95
+ def field_label_matches?(field, expected_label)
96
+ field_labels(field).any? { |label| DomMatching.text_matches?(label.text_content, expected_label) }
97
+ end
98
+
99
+ def form_method_matches?(form, expected_method)
100
+ actual_method = form.get_attribute("method").to_s.downcase
101
+ if actual_method == "post"
102
+ hidden_method = form.query_selector("input[type='hidden'][name='_method']")
103
+ actual_method = hidden_method.get_attribute("value").to_s.downcase if hidden_method
104
+ end
105
+ actual_method == expected_method.to_s.downcase
106
+ end
107
+ end
108
+ end
109
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "cgi"
4
4
  require "erb"
5
+ require "base64"
5
6
 
6
7
  module Dommy
7
8
  module Internal
@@ -21,6 +22,38 @@ module Dommy
21
22
  def decode_uri_component(value)
22
23
  CGI.unescape(value.to_s)
23
24
  end
25
+
26
+ # JS `btoa`: base64-encode a binary (Latin1) string. Each code unit must be
27
+ # 0..255; anything beyond Latin1 is an InvalidCharacterError (per spec).
28
+ def btoa(value)
29
+ codepoints = value.to_s.codepoints
30
+ if codepoints.any? { |c| c > 0xFF }
31
+ raise DOMException::InvalidCharacterError.new(
32
+ "Failed to execute 'btoa': characters outside the Latin1 range cannot be base64-encoded."
33
+ )
34
+ end
35
+
36
+ Base64.strict_encode64(codepoints.pack("C*"))
37
+ end
38
+
39
+ # JS `atob`: decode base64 to a binary (Latin1) string. ASCII whitespace is
40
+ # ignored; an invalid alphabet/length is an InvalidCharacterError. The
41
+ # decoded bytes are returned as a string whose code units equal the bytes.
42
+ def atob(value)
43
+ data = value.to_s.gsub(/[\t\n\f\r ]/, "")
44
+ invalid_atob! if data.length % 4 == 1 || !data.match?(%r{\A[A-Za-z0-9+/]*={0,2}\z})
45
+
46
+ padded = data + ("=" * ((-data.length) % 4))
47
+ Base64.strict_decode64(padded).bytes.map { |byte| byte.chr(Encoding::UTF_8) }.join
48
+ rescue ArgumentError
49
+ invalid_atob!
50
+ end
51
+
52
+ def invalid_atob!
53
+ raise DOMException::InvalidCharacterError.new(
54
+ "Failed to execute 'atob': the string to be decoded is not correctly base64-encoded."
55
+ )
56
+ end
24
57
  end
25
58
  end
26
59
  end
@@ -43,12 +43,96 @@ module Dommy
43
43
 
44
44
  if nk.element?
45
45
  wrapped = @document.wrap_node(nk)
46
- notify_connected(wrapped) if wrapped
46
+ if wrapped
47
+ notify_connected(wrapped)
48
+ run_connected_script(wrapped)
49
+ fire_blank_iframe_load(wrapped)
50
+ end
47
51
  end
48
52
 
49
53
  nk.children.each { |c| notify_connected_subtree(c) } if nk.respond_to?(:children)
50
54
  end
51
55
 
56
+ # A srcless ("blank"/about:blank) `<iframe>` connected to the document gets
57
+ # an empty nested browsing context (a real, complete content document) and
58
+ # fires its `load` event ASYNCHRONOUSLY (a microtask), like a real browser —
59
+ # handlers are commonly attached after insertion (`appendChild(f); f.onload
60
+ # = …`). Without this, code that awaits a blank iframe's load and then reads
61
+ # `iframe.contentWindow.document` hangs: FingerprintJS's `withIframe` (its
62
+ # font sources) does exactly that, which hung note.com's tracking plugin and
63
+ # its whole Nuxt hydration. A `src` iframe is left to the integration layer.
64
+ BLANK_IFRAME_SRCS = ["", "about:blank"].freeze
65
+
66
+ def fire_blank_iframe_load(element)
67
+ return unless element.respond_to?(:local_name) && element.local_name == "iframe"
68
+ return unless element.respond_to?(:is_connected?) && element.is_connected?
69
+ return unless element.respond_to?(:src) && BLANK_IFRAME_SRCS.include?(element.src.to_s.strip)
70
+
71
+ ensure_blank_content_document(element)
72
+ fire = proc { element.dispatch_event(Event.new("load")) rescue nil }
73
+ scheduler = (@document.default_view&.scheduler if @document.respond_to?(:default_view))
74
+ scheduler ? scheduler.queue_microtask(fire) : fire.call
75
+ rescue StandardError
76
+ nil
77
+ end
78
+
79
+ # Give a blank iframe a fresh empty document (or its `srcdoc`) so
80
+ # `contentWindow` / `contentDocument` resolve and DOM ops + measurement
81
+ # inside it work (readyState defaults to "complete"). No-op if it already
82
+ # has one.
83
+ def ensure_blank_content_document(element)
84
+ return unless element.respond_to?(:__internal_set_content_document__)
85
+ return if element.respond_to?(:content_document) && element.content_document
86
+
87
+ srcdoc = (element.srcdoc.to_s if element.respond_to?(:srcdoc))
88
+ html = srcdoc.nil? || srcdoc.empty? ? "<html><head></head><body></body></html>" : srcdoc
89
+ win = Dommy::Window.new(backend_doc: Dommy::Backend.parse(html))
90
+ element.__internal_set_content_document__(win.document)
91
+ end
92
+
93
+ # A classic <script> that's now genuinely connected to this document runs:
94
+ # an inline body through the document's script_runner (wired by the JS
95
+ # bridge), an external `src` through external_script_runner (wired by the
96
+ # integration layer, which fetches + runs it — webpack/Vite load on-demand
97
+ # chunks by injecting `<script src>` this way). Gated on is_connected?
98
+ # because this walk also fires for additions to a still-detached subtree.
99
+ def run_connected_script(element)
100
+ return unless element.respond_to?(:__internal_take_pending_script__) # a <script>
101
+ return unless element.respond_to?(:is_connected?) && element.is_connected?
102
+
103
+ if (runner = @document.script_runner) && (source = element.__internal_take_pending_script__)
104
+ # A script-inserted INLINE classic script runs synchronously on insertion.
105
+ runner.call(source)
106
+ elsif @document.external_script_runner &&
107
+ element.respond_to?(:__internal_take_pending_src__) &&
108
+ (src = element.__internal_take_pending_src__)
109
+ run_external_connected_script(element, src)
110
+ end
111
+ rescue StandardError
112
+ nil
113
+ end
114
+
115
+ # A script-inserted EXTERNAL `<script src>` loads and runs ASYNCHRONOUSLY
116
+ # (per HTML spec), unlike an inline one. Running it synchronously inside the
117
+ # insertion steps would (a) execute it mid-render and, worse, (b) make the
118
+ # engine drain its microtask queue while JS is still on the stack — running
119
+ # an unrelated queued microtask (e.g. Vue's `nextTick` scheduler flush)
120
+ # re-entrantly and patching a half-built component tree (note.com's
121
+ # RecommendTemplate crashed Vue's `isPatchable` this way). Defer to a
122
+ # microtask so it runs at a proper checkpoint, after the current task
123
+ # unwinds. Re-check connectedness then (the node may have been removed).
124
+ def run_external_connected_script(element, src)
125
+ run = proc do
126
+ next unless element.respond_to?(:is_connected?) && element.is_connected?
127
+
128
+ @document.external_script_runner.call(element, src)
129
+ rescue StandardError
130
+ nil
131
+ end
132
+ scheduler = (@document.default_view&.scheduler if @document.respond_to?(:default_view))
133
+ scheduler ? scheduler.queue_microtask(run) : run.call
134
+ end
135
+
52
136
  def notify_disconnected_subtree(nk)
53
137
  return unless nk.respond_to?(:element?)
54
138
 
@@ -80,6 +164,7 @@ module Dommy
80
164
  previous_sibling: nil,
81
165
  next_sibling: nil
82
166
  )
167
+ @document.__internal_bump_style_generation__
83
168
  target = @document.wrap_node(target_node)
84
169
  return nil unless target
85
170
  return nil if added_nodes.empty? && removed_nodes.empty?
@@ -111,12 +196,16 @@ module Dommy
111
196
  )
112
197
  # Only observers whose matching registration requested childList get the
113
198
  # record (an `attributes`/`characterData`-only observer must not — e.g.
114
- # `observe(t, {childList: false, attributes: true})`).
199
+ # `observe(t, {childList: false, attributes: true})`). A subtree
200
+ # registration that matched ALSO gains a transient registered observer
201
+ # for each removed node, so mutations within the just-removed subtree
202
+ # (before the next microtask checkpoint) are still observed.
115
203
  @observer_manager.observers_matching(target).each do |observer|
116
204
  entry = observer.find_matching_entry(target)
117
- next unless entry && entry[:child_list]
205
+ next unless entry
118
206
 
119
- observer.enqueue(record)
207
+ observer.enqueue(record) if entry[:child_list]
208
+ wrapped_removed.each { |removed| observer.add_transient(removed, entry) } if entry[:subtree]
120
209
  end
121
210
 
122
211
  nil
@@ -124,6 +213,7 @@ module Dommy
124
213
 
125
214
  # Fire MutationObserver attribute records
126
215
  def notify_attribute_mutation(target_node:, attribute_name:, old_value:, namespace: nil)
216
+ @document.__internal_bump_style_generation__
127
217
  target = @document.wrap_node(target_node)
128
218
  return nil unless target
129
219
 
@@ -158,6 +248,7 @@ module Dommy
158
248
 
159
249
  # Fire MutationObserver characterData records
160
250
  def notify_character_data_mutation(target_node:, old_value:)
251
+ @document.__internal_bump_style_generation__
161
252
  target = @document.wrap_node(target_node)
162
253
  return nil unless target
163
254
 
@@ -25,31 +25,75 @@ module Dommy
25
25
 
26
26
  # The full Name production (NameStartChar additionally includes ":").
27
27
  NAME = Regexp.new("\\A[:#{NC_START}][:#{NC_START}#{NC_EXTRA}]*\\z")
28
+ # `createElement` / `setAttribute` name validation. Browsers (and the WPT
29
+ # tests that pin web reality) are far more lenient than the XML Name
30
+ # production: any non-empty string with no ASCII whitespace or ">", whose
31
+ # first character is not a digit, ".", "-", "<", ">", or "}". (Names like
32
+ # "f}oo", "f<oo" or a leading combining mark are valid here but not under
33
+ # the strict QName production the *AttributeNS family still uses.)
34
+ HTML_NAME = /\A(?![\s0-9.\-<>}])[^\s>]+\z/
28
35
  # PrefixedName | UnprefixedName.
29
36
  QNAME = Regexp.new(
30
37
  "(?:\\A#{NCSTART}#{NCCHAR}*:#{NCSTART}#{NCCHAR}*\\z)|(?:\\A#{NCSTART}#{NCCHAR}*\\z)"
31
38
  )
32
39
 
40
+ # Code points forbidden anywhere in a "valid local name" / "valid namespace
41
+ # prefix" per the modern WHATWG algorithm: ASCII whitespace (TAB, LF, FF,
42
+ # CR, SPACE), NULL, U+002F (/), U+003E (>).
43
+ LOCAL_FORBIDDEN = Regexp.new("[\\u0000\\u0009\\u000A\\u000C\\u000D\\u0020/>]")
44
+ # Valid first code point of an element local name: ASCII alpha, U+003A (:),
45
+ # U+005F (_), or any code point U+0080 and above.
46
+ ELEMENT_LOCAL_START = Regexp.new("\\A[A-Za-z:_\\u0080-\\u{10FFFF}]")
47
+
33
48
  module_function
34
49
 
50
+ def valid_namespace_prefix?(str)
51
+ !str.empty? && !str.match?(LOCAL_FORBIDDEN)
52
+ end
53
+
54
+ def valid_element_local_name?(str)
55
+ return false if str.empty?
56
+ return false unless str.match?(ELEMENT_LOCAL_START)
57
+
58
+ rest = str[1..]
59
+ rest.nil? || rest.empty? || !rest.match?(LOCAL_FORBIDDEN)
60
+ end
61
+
35
62
  # https://dom.spec.whatwg.org/#validate-and-extract
36
63
  # Returns [namespace_or_nil, prefix_or_nil, local_name]. Raises
37
64
  # DOMException (InvalidCharacterError / NamespaceError) on bad input.
38
- def validate_and_extract(namespace, qualified_name)
65
+ #
66
+ # `context: :element` applies the modern WHATWG "validate" character rules
67
+ # (lenient: a restricted first code point then any non-forbidden code
68
+ # points, multiple colons allowed when namespaced). `context: :attribute`
69
+ # (the default) keeps the strict XML QName production used historically by
70
+ # the *AttributeNS family.
71
+ def validate_and_extract(namespace, qualified_name, context: :attribute)
39
72
  ns = namespace.to_s
40
73
  ns = nil if ns.empty?
41
74
  qname = qualified_name.to_s
42
75
 
43
- unless qname.match?(QNAME)
44
- raise DOMException::InvalidCharacterError, "invalid qualified name: #{qname.inspect}"
45
- end
46
-
47
76
  prefix = nil
48
77
  local = qname
49
78
  if qname.include?(":")
79
+ # Split on the FIRST colon: any further colons stay in the local part
80
+ # (e.g. "f:o:o" → prefix "f", local "o:o").
50
81
  prefix, local = qname.split(":", 2)
51
82
  end
52
83
 
84
+ if context == :element
85
+ if prefix && !valid_namespace_prefix?(prefix)
86
+ raise DOMException::InvalidCharacterError, "invalid namespace prefix: #{prefix.inspect}"
87
+ end
88
+ unless valid_element_local_name?(local)
89
+ raise DOMException::InvalidCharacterError, "invalid local name: #{local.inspect}"
90
+ end
91
+ else
92
+ unless qname.match?(QNAME)
93
+ raise DOMException::InvalidCharacterError, "invalid qualified name: #{qname.inspect}"
94
+ end
95
+ end
96
+
53
97
  if prefix && ns.nil?
54
98
  raise DOMException::NamespaceError, "prefix #{prefix.inspect} with null namespace"
55
99
  end