dommy 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +15 -1
- data/lib/dommy/attr.rb +56 -32
- data/lib/dommy/backend.rb +255 -213
- data/lib/dommy/blob.rb +7 -0
- data/lib/dommy/bridge/methods.rb +12 -2
- data/lib/dommy/browser.rb +4 -4
- data/lib/dommy/callable_invoker.rb +27 -31
- data/lib/dommy/character_data.rb +1 -1
- data/lib/dommy/class_list.rb +31 -14
- data/lib/dommy/css.rb +3 -0
- data/lib/dommy/custom_elements.rb +11 -15
- data/lib/dommy/document.rb +199 -158
- data/lib/dommy/element.rb +223 -499
- data/lib/dommy/event.rb +7 -29
- data/lib/dommy/event_source.rb +4 -4
- data/lib/dommy/fetch.rb +8 -12
- data/lib/dommy/form_data.rb +13 -10
- data/lib/dommy/form_entry_list.rb +5 -5
- data/lib/dommy/fragment.rb +3 -6
- data/lib/dommy/html_canvas_element.rb +1 -1
- data/lib/dommy/html_collection.rb +27 -20
- data/lib/dommy/html_elements/base.rb +279 -15
- data/lib/dommy/html_elements/embedded.rb +2 -2
- data/lib/dommy/html_elements/forms/form.rb +25 -23
- data/lib/dommy/html_elements/forms/input.rb +21 -18
- data/lib/dommy/html_elements/forms/select.rb +7 -7
- data/lib/dommy/html_elements/interactive.rb +11 -8
- data/lib/dommy/html_elements/media.rb +2 -2
- data/lib/dommy/html_elements/metadata.rb +14 -10
- data/lib/dommy/html_elements/tables.rb +13 -33
- data/lib/dommy/html_elements.rb +5 -2
- data/lib/dommy/hyperlink_utils.rb +4 -4
- data/lib/dommy/interaction/field_interactor.rb +1 -1
- data/lib/dommy/interaction/form_submission.rb +2 -2
- data/lib/dommy/interaction/key_sender.rb +1 -1
- data/lib/dommy/internal/accessibility_tree.rb +4 -4
- data/lib/dommy/internal/accessibility_visibility.rb +2 -2
- data/lib/dommy/internal/accessible_description.rb +2 -2
- data/lib/dommy/internal/accessible_name.rb +23 -28
- data/lib/dommy/internal/an_plus_b.rb +125 -0
- data/lib/dommy/internal/aria_role.rb +26 -17
- data/lib/dommy/internal/aria_state.rb +7 -7
- data/lib/dommy/internal/backend_prefilter.rb +20 -13
- data/lib/dommy/internal/child_list.rb +75 -0
- data/lib/dommy/internal/complex_match.rb +127 -0
- data/lib/dommy/internal/css/cascaded_declarations.rb +10 -19
- data/lib/dommy/internal/css/computed_style_builder.rb +22 -1
- data/lib/dommy/internal/css/custom_properties.rb +22 -51
- data/lib/dommy/internal/css/parser.rb +53 -41
- data/lib/dommy/internal/css/rule_index.rb +23 -12
- data/lib/dommy/internal/css/style_cache.rb +2 -2
- data/lib/dommy/internal/css/ua_stylesheet.rb +3 -2
- data/lib/dommy/internal/css_source.rb +223 -0
- data/lib/dommy/internal/css_syntax.rb +153 -0
- data/lib/dommy/internal/directionality.rb +2 -2
- data/lib/dommy/internal/document_generations.rb +38 -18
- data/lib/dommy/internal/dom_matching.rb +5 -5
- data/lib/dommy/internal/element_aria.rb +59 -23
- data/lib/dommy/internal/element_css_inline_style.rb +26 -0
- data/lib/dommy/internal/element_editing.rb +41 -0
- data/lib/dommy/internal/element_nonce.rb +28 -0
- data/lib/dommy/internal/element_popover.rb +109 -0
- data/lib/dommy/internal/element_shadow.rb +13 -10
- data/lib/dommy/internal/element_state.rb +35 -23
- data/lib/dommy/internal/element_top_layer.rb +6 -50
- data/lib/dommy/internal/html_or_svg_or_mathml_element.rb +75 -0
- data/lib/dommy/internal/infra.rb +27 -0
- data/lib/dommy/internal/input_type.rb +16 -4
- data/lib/dommy/internal/literal_lookup.rb +103 -0
- data/lib/dommy/internal/mutation_coordinator.rb +3 -1
- data/lib/dommy/internal/node_adopter.rb +113 -93
- data/lib/dommy/internal/node_factory.rb +7 -49
- data/lib/dommy/internal/node_traversal.rb +12 -13
- data/lib/dommy/internal/node_wrapper_cache.rb +29 -111
- data/lib/dommy/internal/observable_callback.rb +2 -5
- data/lib/dommy/internal/parent_node.rb +2 -2
- data/lib/dommy/internal/post_insertion_steps.rb +7 -7
- data/lib/dommy/internal/reflected_attributes.rb +45 -25
- data/lib/dommy/internal/rendered_text/collector.rb +11 -4
- data/lib/dommy/internal/selector_arguments.rb +0 -15
- data/lib/dommy/internal/selector_index.rb +19 -10
- data/lib/dommy/internal/selector_match.rb +190 -0
- data/lib/dommy/internal/selector_matcher.rb +80 -250
- data/lib/dommy/internal/selector_parser.rb +162 -99
- data/lib/dommy/internal/shadow_root_registry.rb +7 -6
- data/lib/dommy/internal/sibling_positions.rb +59 -0
- data/lib/dommy/internal/svg_uri_reference.rb +33 -0
- data/lib/dommy/internal/template_content_registry.rb +4 -4
- data/lib/dommy/internal/web_idl.rb +11 -0
- data/lib/dommy/internal/window_constructors.rb +8 -8
- data/lib/dommy/internal/xml_serialization.rb +6 -42
- data/lib/dommy/js/dom_interfaces.rb +1 -0
- data/lib/dommy/js/host_callbacks.rb +10 -0
- data/lib/dommy/js/host_runtime.js +24 -1
- data/lib/dommy/js/module_loader.rb +12 -2
- data/lib/dommy/js/module_preload.rb +116 -0
- data/lib/dommy/js/runtime.rb +9 -0
- data/lib/dommy/js/script_boot.rb +6 -2
- data/lib/dommy/js/webidl_tables.js +25 -1
- data/lib/dommy/mathml_elements.rb +13 -0
- data/lib/dommy/mutation_observer.rb +1 -7
- data/lib/dommy/navigation.rb +2 -2
- data/lib/dommy/navigator.rb +1 -5
- data/lib/dommy/node.rb +63 -34
- data/lib/dommy/notification.rb +1 -5
- data/lib/dommy/parser.rb +5 -16
- data/lib/dommy/promise.rb +12 -26
- data/lib/dommy/range.rb +15 -13
- data/lib/dommy/shadow_root.rb +8 -6
- data/lib/dommy/streams.rb +2 -6
- data/lib/dommy/style_declaration.rb +22 -6
- data/lib/dommy/svg_elements.rb +25 -11
- data/lib/dommy/tree_walker.rb +2 -8
- data/lib/dommy/url.rb +1 -5
- data/lib/dommy/url_pattern.rb +1 -9
- data/lib/dommy/validity_state.rb +1 -1
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +5 -5
- data/lib/dommy/window.rb +6 -6
- data/lib/dommy.rb +8 -9
- metadata +20 -4
- data/lib/dommy/backend/makiri_adapter.rb +0 -408
data/lib/dommy/backend.rb
CHANGED
|
@@ -1,320 +1,362 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "makiri"
|
|
4
|
+
|
|
3
5
|
module Dommy
|
|
4
|
-
# `Dommy::Backend` —
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# Defaults to Makiri.
|
|
10
|
-
#
|
|
11
|
-
# Switching backends:
|
|
12
|
-
#
|
|
13
|
-
# require "dommy"
|
|
14
|
-
# Dommy::Backend.use(:makiri)
|
|
15
|
-
#
|
|
16
|
-
# Or set directly:
|
|
17
|
-
#
|
|
18
|
-
# Dommy::Backend.current = Dommy::Backend::Makiri
|
|
6
|
+
# `Dommy::Backend` — what the DOM asks of Makiri, the Lexbor-based parser
|
|
7
|
+
# that holds every tree. All DOM library code comes through here rather
|
|
8
|
+
# than reading Makiri's API directly, so what Dommy relies on of it is in
|
|
9
|
+
# one place: the node classes, the creators, the namespaced attribute
|
|
10
|
+
# model, and the few normalizations (an empty namespace is none).
|
|
19
11
|
#
|
|
20
|
-
#
|
|
21
|
-
# `
|
|
12
|
+
# Makiri splits its document model into `Makiri::HTML::Document`
|
|
13
|
+
# (case-folding, html/head/body) and `Makiri::XML::Document`
|
|
14
|
+
# (case-preserving, namespaces, CDATA); both share the `Makiri::Document` /
|
|
15
|
+
# `Makiri::Node` bases used here for `is_a?` checks. HTML parses go through
|
|
16
|
+
# HTML::Document; `new Document()` / createDocument go through XML::Document.
|
|
22
17
|
module Backend
|
|
23
|
-
class BackendNotAvailable < StandardError
|
|
24
|
-
end
|
|
25
|
-
|
|
26
18
|
class << self
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
# The node classes (the shared bases, so both HTML and XML node
|
|
20
|
+
# subclasses match), so the wrapper cache can route each node.
|
|
21
|
+
def element_class = ::Makiri::Element
|
|
30
22
|
|
|
31
|
-
|
|
23
|
+
def document_class = ::Makiri::Document
|
|
32
24
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
25
|
+
def text_class = ::Makiri::Text
|
|
26
|
+
|
|
27
|
+
def comment_class = ::Makiri::Comment
|
|
28
|
+
|
|
29
|
+
# A Text subtype, matched before Text.
|
|
30
|
+
def cdata_class = ::Makiri::CDATASection
|
|
31
|
+
|
|
32
|
+
def processing_instruction_class = ::Makiri::ProcessingInstruction
|
|
33
|
+
|
|
34
|
+
def document_fragment_class = ::Makiri::DocumentFragment
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
def document_type_class = ::Makiri::DocumentType
|
|
37
|
+
|
|
38
|
+
def node_class = ::Makiri::Node
|
|
39
|
+
|
|
40
|
+
# What Makiri raises for markup that is not well-formed XML (an XML
|
|
41
|
+
# document's fragment parse included).
|
|
42
|
+
def xml_syntax_error_class = ::Makiri::XML::SyntaxError
|
|
43
|
+
|
|
44
|
+
# Stable per-document identity key for a backend node, used to key
|
|
45
|
+
# per-node side tables. pointer_id is the underlying lxb_dom_node_t
|
|
46
|
+
# pointer: Makiri detaches but never frees nodes — the document arena
|
|
47
|
+
# owns them — so a live node's pointer is never recycled; a freed
|
|
48
|
+
# document's may be, so a table keyed by it is a document's own.
|
|
46
49
|
def identity_key(node)
|
|
47
|
-
|
|
50
|
+
node.pointer_id
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
# Deep (or shallow) copy of an element/node, detached and owned by the
|
|
51
|
-
# same document — the backing for DOM cloneNode.
|
|
54
|
+
# same document — the backing for DOM cloneNode. Makiri clones natively
|
|
55
|
+
# (import_node + template fixup), preserving the node's namespace and
|
|
56
|
+
# attributes and carrying <template> contents.
|
|
52
57
|
def clone_node(node, deep:)
|
|
53
|
-
|
|
58
|
+
node.clone_node(deep)
|
|
54
59
|
end
|
|
55
60
|
|
|
56
|
-
# A fresh, empty HTML-backed document.
|
|
61
|
+
# A fresh, empty HTML-backed document — no children, no-quirks. The
|
|
62
|
+
# backing for a shallow clone of an HTML document.
|
|
57
63
|
def empty_document
|
|
58
|
-
|
|
64
|
+
::Makiri::HTML::Document.new
|
|
59
65
|
end
|
|
60
66
|
|
|
61
|
-
# A fresh, empty XML-backed document — for `new Document()` /
|
|
62
|
-
# which the DOM defines as XML documents
|
|
63
|
-
#
|
|
67
|
+
# A fresh, empty XML-backed document — the backing for `new Document()` /
|
|
68
|
+
# createDocument, which the DOM defines as XML documents. An XML backing
|
|
69
|
+
# gives them case preservation, real CDATA nodes (nodeType 4) and
|
|
70
|
+
# namespace tracking. Makiri's cross-kind `import_node` translates
|
|
71
|
+
# between the HTML and XML node representations, so a node created here
|
|
72
|
+
# can still be adopted into the main HTML tree (and vice versa).
|
|
64
73
|
def empty_xml_document
|
|
65
|
-
|
|
74
|
+
::Makiri::XML::Document.new
|
|
66
75
|
end
|
|
67
76
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
77
|
+
# Whether the HTML parser left `doc` in quirks mode (not limited-quirks,
|
|
78
|
+
# which matches no-quirks everywhere Dommy asks). An XML document is
|
|
79
|
+
# never in it.
|
|
80
|
+
def quirks_mode?(doc)
|
|
81
|
+
doc.respond_to?(:quirks_mode?) && doc.quirks_mode?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# An empty backing document matching `doc`'s kind (HTML stays HTML, XML
|
|
85
|
+
# stays XML) — for a shallow document clone, whose result keeps the
|
|
86
|
+
# source flavor.
|
|
70
87
|
def empty_document_like(doc)
|
|
71
|
-
|
|
88
|
+
doc.is_a?(::Makiri::XML::Document) ? empty_xml_document : empty_document
|
|
72
89
|
end
|
|
73
90
|
|
|
74
91
|
# Bring `node` (already detached from its old tree) into `target_doc`,
|
|
75
|
-
# returning the backend node now owned by `target_doc`.
|
|
76
|
-
# between
|
|
77
|
-
# onto the returned node.
|
|
92
|
+
# returning the backend node now owned by `target_doc`. Lexbor's arenas
|
|
93
|
+
# can't move a node between documents, so adoption imports a detached
|
|
94
|
+
# copy — callers must reseat any wrapper onto the returned node.
|
|
95
|
+
#
|
|
96
|
+
# Gap absorption: Lexbor's HTML serializer has no CDATA case (it errors
|
|
97
|
+
# on a CDATA node) and Lexbor is a pinned upstream submodule, so Makiri's
|
|
98
|
+
# cross-kind import_node fails closed when bringing an XML CDATASection
|
|
99
|
+
# into an HTML document. Rather than let that surface as an error,
|
|
100
|
+
# degrade the node to a text node carrying the same data — the data a
|
|
101
|
+
# spec-faithful HTML serializer emits anyway (CDATASection is a Text
|
|
102
|
+
# subtype). The caller reseats the Dommy CDATASection wrapper onto this
|
|
103
|
+
# node, so `nodeType` stays 4 at the DOM level; only the backing node
|
|
104
|
+
# (and thus serialization) is text. An XML target keeps a real CDATA
|
|
105
|
+
# node. This handles a directly adopted CDATA node (a CDATA descendant
|
|
106
|
+
# inside an adopted element subtree is rare, untested, and still fails
|
|
107
|
+
# closed in the backend).
|
|
78
108
|
def adopt(node, target_doc)
|
|
79
|
-
|
|
109
|
+
if node.is_a?(::Makiri::CDATASection) && !target_doc.is_a?(::Makiri::XML::Document)
|
|
110
|
+
return target_doc.create_text_node(node.text)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
target_doc.import_node(node, true)
|
|
80
114
|
end
|
|
81
115
|
|
|
82
116
|
# A copy of the element `node` alone, owned by `target_doc`: its name and
|
|
83
117
|
# attributes exactly as the backend holds them.
|
|
84
118
|
def import_element(node, target_doc)
|
|
85
|
-
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Whether the backend can move a node between documents in place or must adopt a copy first
|
|
89
|
-
# (Lexbor's arenas can't move a node, so inserting a foreign node requires importing
|
|
90
|
-
# it). Lets callers skip a needless — and on an empty target, root-less and
|
|
91
|
-
# therefore crashing — adoption on backends that don't need it.
|
|
92
|
-
def moves_nodes_across_documents?
|
|
93
|
-
current.respond_to?(:moves_nodes_across_documents?) ? current.moves_nodes_across_documents? : true
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
# CSS query that honors Dommy's custom pseudo-classes
|
|
97
|
-
# (`:disabled`/`:enabled`/`:checked`/`:scope`). Each backend applies its
|
|
98
|
-
# own mechanism (Lexbor native pseudos plus a
|
|
99
|
-
# `:scope` rewrite). `scope_node` binds `:scope` to that element.
|
|
100
|
-
def select_all(node, selector, scope_node: nil)
|
|
101
|
-
current.select_all(node, selector, scope_node: scope_node)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def select_first(node, selector, scope_node: nil)
|
|
105
|
-
current.select_first(node, selector, scope_node: scope_node)
|
|
119
|
+
target_doc.import_node(node, false)
|
|
106
120
|
end
|
|
107
121
|
|
|
108
|
-
# Delegate calls so internal code can use `Backend.parse(...)`.
|
|
109
122
|
def parse(html)
|
|
110
|
-
|
|
123
|
+
::Makiri::HTML::Document.parse(html.to_s)
|
|
111
124
|
end
|
|
112
125
|
|
|
113
|
-
#
|
|
114
|
-
#
|
|
126
|
+
# XML parse (DOMParser `text/xml` / `application/xml`): a real XML
|
|
127
|
+
# document, so element/attribute case is preserved, namespaces are
|
|
128
|
+
# tracked, and CDATA round-trips.
|
|
115
129
|
def parse_xml(xml)
|
|
116
|
-
|
|
130
|
+
::Makiri::XML::Document.parse(xml.to_s)
|
|
117
131
|
end
|
|
118
132
|
|
|
119
133
|
def fragment(html, owner_doc:)
|
|
120
|
-
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
#
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
#
|
|
144
|
-
#
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Create an element in `namespace` in an HTML document, so the backend
|
|
150
|
-
# node carries the namespace and the name's case itself. Returns nil for
|
|
151
|
-
# an XML document (fall back to #create_element); raises ArgumentError
|
|
152
|
-
# for an invalid name (the caller maps it to InvalidCharacterError).
|
|
134
|
+
::Makiri::DocumentFragment.parse(html.to_s)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# The DOM's createElement: an element whose local name is `local_name`
|
|
138
|
+
# as written, colons included (`foo:` and `f::oo` are valid local names,
|
|
139
|
+
# not prefixed ones), in `namespace` — the HTML namespace in an HTML or
|
|
140
|
+
# XHTML document, nil in any other. An XML document's own creator parses
|
|
141
|
+
# a QName, so its element is built from the parts. An HTML-backed one —
|
|
142
|
+
# which a document typed as XHTML or XML after an HTML parse also is —
|
|
143
|
+
# makes it by createElementNS, which keeps the name's case and the
|
|
144
|
+
# namespace given; that splits a name at its colon, and refuses `xmlns`
|
|
145
|
+
# outside the XMLNS namespace, as createElementNS must — so a name with
|
|
146
|
+
# a colon, or `xmlns`, takes the HTML document's creator, which keeps
|
|
147
|
+
# the name as written but folds it into the HTML namespace in lower case.
|
|
148
|
+
def create_element(local_name, namespace, doc)
|
|
149
|
+
return doc.create_loose_dom_element(local_name, nil, local_name, namespace) unless doc.is_a?(::Makiri::HTML::Document)
|
|
150
|
+
return doc.create_element(local_name) if local_name.include?(":") || local_name == "xmlns"
|
|
151
|
+
|
|
152
|
+
doc.create_element_ns(namespace, local_name)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The DOM's createElementNS, in an HTML or an XML document alike: the
|
|
156
|
+
# element is in `namespace` (nil for none) with `qualified_name` as
|
|
157
|
+
# written — an SVG `feGaussianBlur` keeps its case, a prefixed name its
|
|
158
|
+
# prefix. Makiri checks the name as the DOM does, so a name the caller
|
|
159
|
+
# has validated is never refused; it raises ArgumentError for one it
|
|
160
|
+
# would not have passed.
|
|
153
161
|
def create_element_ns(namespace, qualified_name, doc)
|
|
154
|
-
|
|
162
|
+
doc.create_element_ns(namespace, qualified_name.to_s)
|
|
155
163
|
end
|
|
156
164
|
|
|
157
|
-
# A detached DocumentType node owned by `doc
|
|
158
|
-
# DOMImplementation.createDocumentType).
|
|
159
|
-
#
|
|
160
|
-
# ArgumentError for
|
|
161
|
-
#
|
|
165
|
+
# A detached DocumentType node owned by `doc`, HTML or XML (for
|
|
166
|
+
# DOMImplementation.createDocumentType). Makiri takes any name the DOM
|
|
167
|
+
# does but the empty one, which Lexbor reads as no name, and raises
|
|
168
|
+
# ArgumentError for it; the caller then falls back to a synthetic
|
|
169
|
+
# (non-tree) DocumentType, since createDocumentType is permissive.
|
|
162
170
|
def create_document_type(name, public_id, system_id, doc)
|
|
163
|
-
|
|
171
|
+
doc.create_document_type(name.to_s, public_id.to_s, system_id.to_s)
|
|
164
172
|
end
|
|
165
173
|
|
|
166
|
-
# The parsed document's DocumentType node, or nil when
|
|
174
|
+
# The parsed document's DocumentType node (`<!DOCTYPE …>`), or nil when
|
|
175
|
+
# the document declares none.
|
|
167
176
|
def internal_subset(doc)
|
|
168
|
-
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# The backend class for a DocumentType node (nil if unsupported), so the
|
|
172
|
-
# wrapper cache can route it to Dommy::DocumentType.
|
|
173
|
-
def document_type_class
|
|
174
|
-
current.respond_to?(:document_type_class) ? current.document_type_class : nil
|
|
177
|
+
doc.internal_subset
|
|
175
178
|
end
|
|
176
179
|
|
|
177
180
|
def create_text(content, doc)
|
|
178
|
-
|
|
181
|
+
doc.create_text_node(content)
|
|
179
182
|
end
|
|
180
183
|
|
|
181
184
|
def create_comment(content, doc)
|
|
182
|
-
|
|
185
|
+
doc.create_comment(content)
|
|
183
186
|
end
|
|
184
187
|
|
|
185
|
-
#
|
|
186
|
-
#
|
|
188
|
+
# CDATASection (nodeType 4). A genuine XML document — including a
|
|
189
|
+
# `new Document()` / createDocument document, XML-backed (see
|
|
190
|
+
# #empty_xml_document) — mints a real CDATA node and the XML serializer
|
|
191
|
+
# emits `<![CDATA[…]]>`. `Document#create_cdata_section` rejects HTML
|
|
192
|
+
# documents up front (NotSupportedError, per spec), so the text-node
|
|
193
|
+
# fallback below is a defensive guard for an HTML document that still
|
|
194
|
+
# slips through (Lexbor's HTML serializer raises on a native CDATA node,
|
|
195
|
+
# so a text node keeps serialization safe).
|
|
187
196
|
def create_cdata(content, doc)
|
|
188
|
-
|
|
197
|
+
if doc.is_a?(::Makiri::XML::Document)
|
|
198
|
+
doc.create_cdata(content)
|
|
199
|
+
else
|
|
200
|
+
doc.create_text_node(content)
|
|
201
|
+
end
|
|
189
202
|
end
|
|
190
203
|
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
# ProcessingInstruction node (`<?target data?>`). Both Makiri document
|
|
205
|
+
# families mint a real PI node and serialize it, so PIs — unlike CDATA —
|
|
206
|
+
# need no HTML-document fallback.
|
|
207
|
+
def create_processing_instruction(target, data, doc)
|
|
208
|
+
doc.create_processing_instruction(target, data)
|
|
193
209
|
end
|
|
194
210
|
|
|
195
|
-
#
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
current.create_processing_instruction(target, data, doc)
|
|
211
|
+
# DOM "child text content": the data of `node`'s Text (and CDATA
|
|
212
|
+
# section) children, in order — no deeper descendant's.
|
|
213
|
+
def child_text_content(node)
|
|
214
|
+
node.children.select { |c| c.text? || c.cdata? }.map(&:content).join
|
|
200
215
|
end
|
|
201
216
|
|
|
202
|
-
|
|
203
|
-
|
|
217
|
+
# The element's own namespace URI as the DOM reports it (Lexbor's HTML /
|
|
218
|
+
# SVG / MathML, an XML document's own), nil for none — Makiri answers
|
|
219
|
+
# nil, never "", for a node without one, parsed or created.
|
|
220
|
+
def namespace_uri(node)
|
|
221
|
+
node.namespace_uri
|
|
204
222
|
end
|
|
205
223
|
|
|
206
|
-
# The element's
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
current.namespace_definitions(node)
|
|
224
|
+
# The element's own namespace prefix as the DOM reports it, nil for none.
|
|
225
|
+
def prefix(node)
|
|
226
|
+
node.prefix
|
|
210
227
|
end
|
|
211
228
|
|
|
212
|
-
#
|
|
229
|
+
# Makiri's own fragment holding a `<template>` element's contents
|
|
213
230
|
# (Lexbor keeps them off the child list), the same one every time; nil
|
|
214
231
|
# for a node that has none — any node of an XML document, whose contents
|
|
215
232
|
# the template-content registry keeps instead.
|
|
216
233
|
def template_contents(node)
|
|
217
|
-
|
|
234
|
+
node.respond_to?(:content_fragment) ? node.content_fragment : nil
|
|
218
235
|
end
|
|
219
236
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
237
|
+
# ----- Namespaced attributes -----
|
|
238
|
+
# Lexbor tracks the attribute's own namespace: set_attribute_ns records
|
|
239
|
+
# it (splitting prefix/local), and the attr node reports
|
|
240
|
+
# namespace_uri/prefix/local_name. So *AttributeNS matches on
|
|
241
|
+
# (namespace, local name) faithfully, and Makiri finds an attribute by
|
|
242
|
+
# that pair natively. `namespace` is an href String or nil throughout,
|
|
243
|
+
# and Makiri takes "" as none, as the DOM does.
|
|
223
244
|
|
|
224
|
-
# Namespaced attribute access (DOM *AttributeNS). `namespace` is an href
|
|
225
|
-
# String or nil.
|
|
226
245
|
def get_attribute_ns(node, namespace, local_name)
|
|
227
|
-
|
|
246
|
+
node.attribute_value_ns(namespace, local_name.to_s)
|
|
228
247
|
end
|
|
229
248
|
|
|
230
|
-
def
|
|
231
|
-
|
|
249
|
+
def has_attribute_ns?(node, namespace, local_name)
|
|
250
|
+
!attr_by_ns(node, namespace, local_name).nil?
|
|
232
251
|
end
|
|
233
252
|
|
|
234
|
-
|
|
235
|
-
|
|
253
|
+
# The attribute node in `namespace` with `local_name`, or nil — what
|
|
254
|
+
# getAttributeNodeNS and the NS removals find.
|
|
255
|
+
def attr_by_ns(node, namespace, local_name)
|
|
256
|
+
node.attribute_node_ns(namespace, local_name.to_s)
|
|
236
257
|
end
|
|
237
258
|
|
|
238
|
-
def
|
|
239
|
-
|
|
259
|
+
def set_attribute_ns(node, namespace, prefix, _local_name, qualified_name, value)
|
|
260
|
+
none = namespace.to_s.empty?
|
|
261
|
+
note_namespaced_unprefixed_attribute if prefix.to_s.empty? && !none
|
|
262
|
+
name = qualified_name.to_s
|
|
263
|
+
value = value.to_s
|
|
264
|
+
node.set_attribute_ns(namespace, name, value) unless none && set_null_namespace_attribute(node, name, value)
|
|
265
|
+
value
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Whether an attribute in a namespace but with no prefix has ever been
|
|
269
|
+
# made (setAttributeNS("urn:x", "id")). That is the only way an
|
|
270
|
+
# attribute whose qualified name is a plain `id` or `class` can be in a
|
|
271
|
+
# namespace — a parser's are either prefixed or, `xmlns` aside, in none —
|
|
272
|
+
# so until one is made, no element need be asked whether it has one
|
|
273
|
+
# (Element#namespaced_unprefixed_attribute?, which gates the JS bridge's
|
|
274
|
+
# attribute snapshot). Process-wide, as an attribute moves between
|
|
275
|
+
# documents with its node; it only ever decides how fast an answer comes.
|
|
276
|
+
def note_namespaced_unprefixed_attribute
|
|
277
|
+
@namespaced_unprefixed_attribute = true
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def namespaced_unprefixed_attribute?
|
|
281
|
+
@namespaced_unprefixed_attribute == true
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Remove by (namespace, local name) — removing by qualified name is
|
|
285
|
+
# ambiguous once same-name/different-namespace attributes coexist.
|
|
286
|
+
def remove_attribute_ns(node, namespace, local_name)
|
|
287
|
+
node.remove_attribute_ns(namespace, local_name.to_s)
|
|
288
|
+
nil
|
|
240
289
|
end
|
|
241
290
|
|
|
242
291
|
# Reads a backend attribute node into {namespace_uri:, prefix:,
|
|
243
292
|
# local_name:, qualified_name:, value:} (namespace-aware).
|
|
244
293
|
def attribute_ns_info(attr_node)
|
|
245
|
-
|
|
294
|
+
{
|
|
295
|
+
namespace_uri: attr_node.namespace_uri,
|
|
296
|
+
prefix: attr_node.prefix,
|
|
297
|
+
local_name: attr_node.local_name,
|
|
298
|
+
qualified_name: attr_node.name,
|
|
299
|
+
value: attr_node.value,
|
|
300
|
+
}
|
|
246
301
|
end
|
|
247
302
|
|
|
248
303
|
# The attribute node whose QUALIFIED name is `qualified_name`, or nil.
|
|
249
304
|
#
|
|
250
305
|
# WHATWG's by-name family ("get an attribute by name", `setAttribute`,
|
|
251
|
-
# `removeAttribute`) matches on the qualified name. A lookup by local
|
|
252
|
-
# confuses `b` with a prefixed `xml:b`, so those paths must come
|
|
253
|
-
# here rather than through `node[name]`.
|
|
306
|
+
# `removeAttribute`) matches on the qualified name. A lookup by local
|
|
307
|
+
# name confuses `b` with a prefixed `xml:b`, so those paths must come
|
|
308
|
+
# through here rather than through `node[name]`. Makiri scans natively.
|
|
254
309
|
def attr_by_qualified_name(node, qualified_name)
|
|
255
|
-
|
|
310
|
+
node.attribute_by_qualified_name(qualified_name.to_s)
|
|
256
311
|
end
|
|
257
312
|
|
|
258
|
-
# That attribute's VALUE, or nil when there is no such attribute. The
|
|
259
|
-
# match without an attribute node in hand, for the two readers that
|
|
260
|
-
# ever wanted the value — `getAttribute` and `hasAttribute` — which
|
|
261
|
-
# every CSS match and every reflected IDL attribute.
|
|
313
|
+
# That attribute's VALUE, or nil when there is no such attribute. The
|
|
314
|
+
# same match without an attribute node in hand, for the two readers that
|
|
315
|
+
# only ever wanted the value — `getAttribute` and `hasAttribute` — which
|
|
316
|
+
# run on every CSS match and every reflected IDL attribute.
|
|
262
317
|
def attr_value_by_qualified_name(node, qualified_name)
|
|
263
|
-
|
|
318
|
+
node.attribute_value_by_qualified_name(qualified_name.to_s)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# The value of `node`'s attribute named `local_name` in no namespace, or
|
|
322
|
+
# nil — what a selector's `[att]` and HTML's id, class and name read, on
|
|
323
|
+
# the hottest path in the library. One native read, which finds the
|
|
324
|
+
# attribute by namespace and local name, so a namespaced one that shares
|
|
325
|
+
# the name (setAttributeNS("u", "att"), a parsed `xmlns`) is not it.
|
|
326
|
+
def no_namespace_attribute_value(node, local_name)
|
|
327
|
+
node.attribute_value_ns(nil, local_name)
|
|
264
328
|
end
|
|
265
329
|
|
|
266
330
|
# The element's attribute nodes (each readable via attribute_ns_info).
|
|
267
331
|
# The single choke point so DOM code doesn't touch parser internals.
|
|
268
332
|
def attribute_nodes(node)
|
|
269
|
-
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# Type constants — proxy through to the current backend so
|
|
273
|
-
# `node.is_a?(Backend::Element)` resolves dynamically.
|
|
274
|
-
def element_class
|
|
275
|
-
current::Element
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
def document_class
|
|
279
|
-
current::Document
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# What the backend raises for markup that is not well-formed XML (an XML
|
|
283
|
-
# document's fragment parse included).
|
|
284
|
-
def xml_syntax_error_class
|
|
285
|
-
current::XMLSyntaxError
|
|
286
|
-
end
|
|
287
|
-
|
|
288
|
-
def text_class
|
|
289
|
-
current::Text
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
def comment_class
|
|
293
|
-
current::Comment
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def document_fragment_class
|
|
297
|
-
current::DocumentFragment
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def node_class
|
|
301
|
-
current::Node
|
|
333
|
+
node.attribute_nodes
|
|
302
334
|
end
|
|
303
335
|
|
|
304
336
|
private
|
|
305
337
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
# A null-namespace name that only the DOM's `setAttribute` can make: one
|
|
339
|
+
# whose local name holds a colon ("xlink:href", "v-on:click") or is
|
|
340
|
+
# "xmlns". Makiri checks set_attribute_ns as the DOM's setAttributeNS
|
|
341
|
+
# does, which refuses these (a prefix needs a namespace).
|
|
342
|
+
def set_attribute_only_name?(name)
|
|
343
|
+
name == "xmlns" || name.include?(":")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Makiri's `set_loose_dom_attribute`, the DOM's setAttribute, for the
|
|
347
|
+
# setAttribute-only names, which set_attribute_ns refuses: a plain
|
|
348
|
+
# attribute in no namespace (never a namespace declaration) whose local
|
|
349
|
+
# name is the whole name, colon included, lower-cased only on an
|
|
350
|
+
# HTML-namespace element of an HTML document. true when it made the
|
|
351
|
+
# attribute; false leaves every other name to set_attribute_ns, which
|
|
352
|
+
# matches an existing attribute by (namespace, local name) rather than
|
|
353
|
+
# by qualified name: `setAttributeNS("u", "a")` then a null-namespace
|
|
354
|
+
# "a" are two attributes.
|
|
355
|
+
def set_null_namespace_attribute(node, name, value)
|
|
356
|
+
return false unless set_attribute_only_name?(name)
|
|
357
|
+
|
|
358
|
+
node.set_loose_dom_attribute(name, value)
|
|
359
|
+
true
|
|
318
360
|
end
|
|
319
361
|
end
|
|
320
362
|
end
|
data/lib/dommy/blob.rb
CHANGED
|
@@ -36,6 +36,13 @@ module Dommy
|
|
|
36
36
|
@window = window
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# A File of the same bytes and type named `name`, as FormData's "create an
|
|
40
|
+
# entry" makes of a Blob it is given (named "blob") or of a File given a
|
|
41
|
+
# filename.
|
|
42
|
+
def __internal_file_named__(name)
|
|
43
|
+
File.new([@data], name, {"type" => @type}, @window)
|
|
44
|
+
end
|
|
45
|
+
|
|
39
46
|
# Return a new Blob over a byte range of this one.
|
|
40
47
|
# Negative indices are treated as offsets from the end (per spec).
|
|
41
48
|
def slice(start = 0, last = @size, content_type = "")
|
data/lib/dommy/bridge/methods.rb
CHANGED
|
@@ -19,7 +19,9 @@ module Dommy
|
|
|
19
19
|
#
|
|
20
20
|
# The per-class `JS_METHOD_NAMES` constant holds the class's OWN names; the
|
|
21
21
|
# suite asserts it matches the class's own `__js_call__` `when` arms — see
|
|
22
|
-
# test/test_js_call_dispatch_invariant.rb.
|
|
22
|
+
# test/test_js_call_dispatch_invariant.rb. A mixin module that answers
|
|
23
|
+
# methods of its own (HTMLOrSVGOrMathMLElement's focus / blur) sets the
|
|
24
|
+
# same constant on itself, and the classes it is included in expose them.
|
|
23
25
|
module Methods
|
|
24
26
|
def self.included(base)
|
|
25
27
|
base.extend(ClassMethods)
|
|
@@ -49,7 +51,15 @@ module Dommy
|
|
|
49
51
|
def js_method_names
|
|
50
52
|
@js_method_names ||= begin
|
|
51
53
|
inherited_names = superclass.respond_to?(:js_method_names) ? superclass.js_method_names : []
|
|
52
|
-
(inherited_names + own_js_methods).uniq.freeze
|
|
54
|
+
(inherited_names + mixin_js_methods + own_js_methods).uniq.freeze
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The names the modules included in this class itself (not in an
|
|
59
|
+
# ancestor) declare.
|
|
60
|
+
def mixin_js_methods
|
|
61
|
+
(ancestors - superclass.ancestors).grep_v(Class).flat_map do |mod|
|
|
62
|
+
mod.const_defined?(:JS_METHOD_NAMES, false) ? mod::JS_METHOD_NAMES : []
|
|
53
63
|
end
|
|
54
64
|
end
|
|
55
65
|
|
data/lib/dommy/browser.rb
CHANGED
|
@@ -298,7 +298,7 @@ module Dommy
|
|
|
298
298
|
@runtime&.dispose
|
|
299
299
|
# The JS engine is pluggable: `@backend` selects a registered runtime
|
|
300
300
|
# (nil → the configured default, QuickJS when dommy-js-quickjs is loaded).
|
|
301
|
-
runtime = Js.build_runtime(@backend)
|
|
301
|
+
runtime = Js::ModulePreload.build_runtime(window.document, @backend)
|
|
302
302
|
# Every uncaught error the page produces goes through the window's
|
|
303
303
|
# "report an exception" / "notify about rejected promises" funnel, and only
|
|
304
304
|
# what the page left unhandled lands here. A page that installs its own
|
|
@@ -409,7 +409,7 @@ module Dommy
|
|
|
409
409
|
return nil if name.empty? || RESERVED_TARGETS.include?(name.downcase)
|
|
410
410
|
|
|
411
411
|
@window.document.query_selector_all("iframe").find do |frame|
|
|
412
|
-
frame.
|
|
412
|
+
frame.__internal_attribute_value__("name").to_s == name
|
|
413
413
|
end
|
|
414
414
|
end
|
|
415
415
|
|
|
@@ -463,9 +463,9 @@ module Dommy
|
|
|
463
463
|
# reloads and is left alone to avoid a busy loop).
|
|
464
464
|
def meta_refresh_target(document)
|
|
465
465
|
document.query_selector_all("meta").each do |meta|
|
|
466
|
-
next unless meta.
|
|
466
|
+
next unless meta.__internal_attribute_value__("http-equiv").to_s.casecmp?("refresh")
|
|
467
467
|
|
|
468
|
-
_delay, separator, rest = meta.
|
|
468
|
+
_delay, separator, rest = meta.__internal_attribute_value__("content").to_s.partition(";")
|
|
469
469
|
next if separator.empty?
|
|
470
470
|
|
|
471
471
|
url = rest.strip.sub(/\Aurl\s*=\s*/i, "").gsub(/\A["']|["']\z/, "").strip
|