dommy 0.13.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 +262 -197
- data/lib/dommy/blob.rb +7 -0
- data/lib/dommy/bridge/constructor.rb +72 -0
- data/lib/dommy/bridge/errors.rb +42 -0
- data/lib/dommy/bridge/methods.rb +31 -14
- data/lib/dommy/bridge/sentinels.rb +39 -0
- data/lib/dommy/bridge/values.rb +78 -0
- data/lib/dommy/{js → bridge}/wire_tags.rb +14 -11
- data/lib/dommy/bridge.rb +22 -250
- data/lib/dommy/browser.rb +115 -14
- data/lib/dommy/callable_invoker.rb +27 -31
- data/lib/dommy/character_data.rb +539 -0
- data/lib/dommy/class_list.rb +328 -0
- data/lib/dommy/crypto.rb +12 -1
- data/lib/dommy/css.rb +15 -0
- data/lib/dommy/custom_elements.rb +11 -15
- data/lib/dommy/data_transfer.rb +106 -0
- data/lib/dommy/document.rb +490 -511
- data/lib/dommy/dom_parser.rb +3 -0
- data/lib/dommy/element.rb +352 -2246
- data/lib/dommy/encodings/tables.rb +4 -4
- data/lib/dommy/encodings.rb +58 -27
- data/lib/dommy/event.rb +234 -30
- data/lib/dommy/event_source.rb +41 -0
- data/lib/dommy/fetch.rb +8 -12
- data/lib/dommy/form_data.rb +18 -73
- data/lib/dommy/form_entry_list.rb +227 -0
- data/lib/dommy/fragment.rb +317 -0
- data/lib/dommy/history.rb +3 -0
- data/lib/dommy/html_canvas_element.rb +1 -1
- data/lib/dommy/html_collection.rb +43 -26
- data/lib/dommy/html_elements/base.rb +602 -0
- data/lib/dommy/html_elements/embedded.rb +237 -0
- data/lib/dommy/html_elements/forms/form.rb +1090 -0
- data/lib/dommy/html_elements/forms/input.rb +645 -0
- data/lib/dommy/html_elements/forms/select.rb +536 -0
- data/lib/dommy/html_elements/forms.rb +8 -0
- data/lib/dommy/html_elements/interactive.rb +385 -0
- data/lib/dommy/html_elements/media.rb +329 -0
- data/lib/dommy/html_elements/metadata.rb +357 -0
- data/lib/dommy/html_elements/miscellaneous.rb +77 -0
- data/lib/dommy/html_elements/tables.rb +448 -0
- data/lib/dommy/html_elements.rb +16 -5324
- data/lib/dommy/hyperlink_utils.rb +171 -0
- data/lib/dommy/interaction/debug.rb +7 -6
- data/lib/dommy/interaction/dom_summary.rb +65 -40
- data/lib/dommy/interaction/driver.rb +29 -115
- data/lib/dommy/interaction/event_synthesis.rb +97 -4
- data/lib/dommy/interaction/field_interactor.rb +2 -14
- data/lib/dommy/interaction/form_submission.rb +90 -168
- data/lib/dommy/interaction/key_sender.rb +121 -0
- data/lib/dommy/interaction/mime_types.rb +25 -0
- data/lib/dommy/interaction.rb +2 -0
- data/lib/dommy/internal/accessibility_tree.rb +36 -13
- data/lib/dommy/internal/accessibility_visibility.rb +44 -0
- data/lib/dommy/internal/accessible_description.rb +2 -2
- data/lib/dommy/internal/accessible_name.rb +109 -94
- data/lib/dommy/internal/an_plus_b.rb +125 -0
- data/lib/dommy/internal/aria_role.rb +47 -23
- data/lib/dommy/internal/aria_snapshot.rb +5 -0
- data/lib/dommy/internal/aria_state.rb +31 -12
- data/lib/dommy/internal/autofill.rb +141 -0
- data/lib/dommy/internal/backend_prefilter.rb +177 -0
- data/lib/dommy/internal/bounded_cache.rb +50 -0
- data/lib/dommy/internal/child_list.rb +75 -0
- data/lib/dommy/internal/child_node.rb +9 -34
- data/lib/dommy/internal/complex_match.rb +127 -0
- data/lib/dommy/internal/css/calc.rb +143 -145
- data/lib/dommy/internal/css/cascade.rb +38 -393
- data/lib/dommy/internal/css/cascaded_declarations.rb +236 -0
- data/lib/dommy/internal/css/color.rb +61 -34
- data/lib/dommy/internal/css/computed_style_builder.rb +226 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +20 -4
- data/lib/dommy/internal/css/counters.rb +60 -37
- data/lib/dommy/internal/css/custom_properties.rb +97 -78
- data/lib/dommy/internal/css/media_query.rb +228 -203
- data/lib/dommy/internal/css/parser.rb +70 -41
- data/lib/dommy/internal/css/property_registry.rb +32 -56
- data/lib/dommy/internal/css/renderability.rb +61 -0
- data/lib/dommy/internal/css/rule_index.rb +40 -152
- data/lib/dommy/internal/css/selector_dependencies.rb +162 -0
- data/lib/dommy/internal/css/style_cache.rb +61 -0
- data/lib/dommy/internal/css/ua_stylesheet.rb +42 -2
- data/lib/dommy/internal/css/value_tokens.rb +60 -0
- data/lib/dommy/internal/css_source.rb +223 -0
- data/lib/dommy/internal/css_syntax.rb +153 -0
- data/lib/dommy/internal/directionality.rb +188 -0
- data/lib/dommy/internal/document_generations.rb +189 -0
- data/lib/dommy/internal/document_interaction_state.rb +47 -0
- data/lib/dommy/internal/document_live_ranges.rb +97 -0
- data/lib/dommy/internal/dom_matching.rb +32 -16
- data/lib/dommy/internal/element_aria.rb +199 -0
- 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_geometry.rb +69 -0
- data/lib/dommy/internal/element_matching.rb +5 -12
- 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 +120 -0
- data/lib/dommy/internal/element_state.rb +244 -0
- data/lib/dommy/internal/element_tasks.rb +19 -0
- data/lib/dommy/internal/element_top_layer.rb +19 -0
- data/lib/dommy/internal/enumerated_keyword_sets.rb +53 -0
- data/lib/dommy/internal/exception_report.rb +1 -1
- 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 +292 -0
- data/lib/dommy/internal/insertion_point.rb +62 -0
- data/lib/dommy/internal/literal_lookup.rb +103 -0
- data/lib/dommy/internal/mime_type.rb +74 -0
- data/lib/dommy/internal/mutation_coordinator.rb +60 -199
- data/lib/dommy/internal/namespaces.rb +41 -43
- data/lib/dommy/internal/node_adopter.rb +125 -98
- data/lib/dommy/internal/node_factory.rb +115 -0
- data/lib/dommy/internal/node_identity.rb +31 -0
- data/lib/dommy/internal/node_traversal.rb +12 -13
- data/lib/dommy/internal/node_wrapper_cache.rb +71 -299
- data/lib/dommy/internal/observable_callback.rb +2 -5
- data/lib/dommy/internal/observer_options.rb +93 -0
- data/lib/dommy/internal/parent_node.rb +29 -29
- data/lib/dommy/internal/post_insertion_steps.rb +213 -0
- data/lib/dommy/internal/reflected_attributes.rb +456 -24
- data/lib/dommy/internal/rendered_text/collector.rb +228 -0
- data/lib/dommy/internal/rendered_text/computed_box.rb +39 -0
- data/lib/dommy/internal/rendered_text/fragment.rb +55 -0
- data/lib/dommy/internal/rendered_text/items.rb +57 -0
- data/lib/dommy/internal/rendered_text/whitespace_collapser.rb +132 -0
- data/lib/dommy/internal/selector_arguments.rb +79 -0
- data/lib/dommy/internal/selector_index.rb +22 -11
- data/lib/dommy/internal/selector_match.rb +190 -0
- data/lib/dommy/internal/selector_matcher.rb +98 -628
- data/lib/dommy/internal/selector_parser.rb +164 -136
- 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 +112 -66
- data/lib/dommy/internal/text_flattening.rb +20 -0
- data/lib/dommy/internal/text_selection.rb +104 -0
- data/lib/dommy/internal/toggle_events.rb +56 -0
- data/lib/dommy/internal/toggle_task_tracker.rb +57 -0
- data/lib/dommy/internal/url_parser.rb +722 -569
- data/lib/dommy/internal/url_record_accessors.rb +90 -0
- data/lib/dommy/internal/web_idl.rb +11 -0
- data/lib/dommy/internal/window_constructors.rb +190 -0
- data/lib/dommy/internal/xml_serialization.rb +174 -122
- data/lib/dommy/js/crossing_profile.rb +75 -0
- data/lib/dommy/js/dom_interfaces.rb +23 -2
- data/lib/dommy/js/host_bridge.rb +43 -187
- data/lib/dommy/js/host_callbacks.rb +106 -0
- data/lib/dommy/js/host_runtime.js +863 -763
- data/lib/dommy/js/import_map.rb +10 -1
- data/lib/dommy/js/marshaller.rb +94 -65
- data/lib/dommy/js/module_loader.rb +12 -2
- data/lib/dommy/js/module_preload.rb +116 -0
- data/lib/dommy/js/observable_runtime.js +125 -146
- data/lib/dommy/js/runtime.rb +19 -43
- data/lib/dommy/js/runtime_registry.rb +46 -0
- data/lib/dommy/js/script_boot.rb +43 -74
- data/lib/dommy/js/webidl_tables.js +643 -0
- data/lib/dommy/js/window_wiring.rb +69 -0
- data/lib/dommy/location.rb +152 -103
- data/lib/dommy/mathml_elements.rb +13 -0
- data/lib/dommy/mutation_observer.rb +4 -64
- data/lib/dommy/navigation.rb +65 -12
- data/lib/dommy/navigator.rb +1 -5
- data/lib/dommy/node.rb +106 -51
- data/lib/dommy/notification.rb +1 -5
- data/lib/dommy/parser.rb +5 -16
- data/lib/dommy/promise.rb +55 -15
- data/lib/dommy/range.rb +15 -13
- data/lib/dommy/resources.rb +19 -1
- data/lib/dommy/shadow_root.rb +19 -11
- data/lib/dommy/streams.rb +2 -6
- data/lib/dommy/style_declaration.rb +300 -0
- data/lib/dommy/svg_elements.rb +26 -56
- data/lib/dommy/tree_walker.rb +2 -8
- data/lib/dommy/url.rb +21 -74
- data/lib/dommy/url_pattern/regexp_translator.rb +1 -6
- data/lib/dommy/url_pattern/tokenizer.rb +4 -2
- data/lib/dommy/url_pattern.rb +1 -9
- data/lib/dommy/validity_state.rb +383 -0
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/web_socket.rb +5 -5
- data/lib/dommy/window.rb +35 -171
- data/lib/dommy/xml_http_request.rb +35 -4
- data/lib/dommy.rb +31 -12
- metadata +91 -8
- data/lib/dommy/backend/makiri_adapter.rb +0 -374
- data/lib/dommy/bridge/constructor_registry.rb +0 -28
- data/lib/dommy/internal/css_pseudo_handlers.rb +0 -378
data/lib/dommy/backend.rb
CHANGED
|
@@ -1,297 +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:
|
|
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).
|
|
17
11
|
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
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
|
|
35
|
+
|
|
36
|
+
def document_type_class = ::Makiri::DocumentType
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
#
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# Whether the backend can move a node between documents in place or must adopt a copy first
|
|
83
|
-
# (Lexbor's arenas can't move a node, so inserting a foreign node requires importing
|
|
84
|
-
# it). Lets callers skip a needless — and on an empty target, root-less and
|
|
85
|
-
# therefore crashing — adoption on backends that don't need it.
|
|
86
|
-
def moves_nodes_across_documents?
|
|
87
|
-
current.respond_to?(:moves_nodes_across_documents?) ? current.moves_nodes_across_documents? : true
|
|
88
|
-
end
|
|
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
|
|
89
112
|
|
|
90
|
-
|
|
91
|
-
# (`:disabled`/`:enabled`/`:checked`/`:scope`). Each backend applies its
|
|
92
|
-
# own mechanism (Lexbor native pseudos plus a
|
|
93
|
-
# `:scope` rewrite). `scope_node` binds `:scope` to that element.
|
|
94
|
-
def select_all(node, selector, scope_node: nil)
|
|
95
|
-
current.select_all(node, selector, scope_node: scope_node)
|
|
113
|
+
target_doc.import_node(node, true)
|
|
96
114
|
end
|
|
97
115
|
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
# A copy of the element `node` alone, owned by `target_doc`: its name and
|
|
117
|
+
# attributes exactly as the backend holds them.
|
|
118
|
+
def import_element(node, target_doc)
|
|
119
|
+
target_doc.import_node(node, false)
|
|
100
120
|
end
|
|
101
121
|
|
|
102
|
-
# Delegate calls so internal code can use `Backend.parse(...)`.
|
|
103
122
|
def parse(html)
|
|
104
|
-
|
|
123
|
+
::Makiri::HTML::Document.parse(html.to_s)
|
|
105
124
|
end
|
|
106
125
|
|
|
107
|
-
#
|
|
108
|
-
#
|
|
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.
|
|
109
129
|
def parse_xml(xml)
|
|
110
|
-
|
|
130
|
+
::Makiri::XML::Document.parse(xml.to_s)
|
|
111
131
|
end
|
|
112
132
|
|
|
113
133
|
def fragment(html, owner_doc:)
|
|
114
|
-
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
|
|
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.
|
|
161
|
+
def create_element_ns(namespace, qualified_name, doc)
|
|
162
|
+
doc.create_element_ns(namespace, qualified_name.to_s)
|
|
163
|
+
end
|
|
164
|
+
|
|
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.
|
|
142
170
|
def create_document_type(name, public_id, system_id, doc)
|
|
143
|
-
|
|
171
|
+
doc.create_document_type(name.to_s, public_id.to_s, system_id.to_s)
|
|
144
172
|
end
|
|
145
173
|
|
|
146
|
-
# 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.
|
|
147
176
|
def internal_subset(doc)
|
|
148
|
-
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# The backend class for a DocumentType node (nil if unsupported), so the
|
|
152
|
-
# wrapper cache can route it to Dommy::DocumentType.
|
|
153
|
-
def document_type_class
|
|
154
|
-
current.respond_to?(:document_type_class) ? current.document_type_class : nil
|
|
177
|
+
doc.internal_subset
|
|
155
178
|
end
|
|
156
179
|
|
|
157
180
|
def create_text(content, doc)
|
|
158
|
-
|
|
181
|
+
doc.create_text_node(content)
|
|
159
182
|
end
|
|
160
183
|
|
|
161
184
|
def create_comment(content, doc)
|
|
162
|
-
|
|
185
|
+
doc.create_comment(content)
|
|
163
186
|
end
|
|
164
187
|
|
|
165
|
-
#
|
|
166
|
-
#
|
|
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).
|
|
167
196
|
def create_cdata(content, doc)
|
|
168
|
-
|
|
197
|
+
if doc.is_a?(::Makiri::XML::Document)
|
|
198
|
+
doc.create_cdata(content)
|
|
199
|
+
else
|
|
200
|
+
doc.create_text_node(content)
|
|
201
|
+
end
|
|
169
202
|
end
|
|
170
203
|
|
|
171
|
-
|
|
172
|
-
|
|
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)
|
|
173
209
|
end
|
|
174
210
|
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
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
|
|
180
215
|
end
|
|
181
216
|
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
184
222
|
end
|
|
185
223
|
|
|
186
|
-
|
|
187
|
-
|
|
224
|
+
# The element's own namespace prefix as the DOM reports it, nil for none.
|
|
225
|
+
def prefix(node)
|
|
226
|
+
node.prefix
|
|
188
227
|
end
|
|
189
228
|
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
|
|
193
|
-
|
|
229
|
+
# Makiri's own fragment holding a `<template>` element's contents
|
|
230
|
+
# (Lexbor keeps them off the child list), the same one every time; nil
|
|
231
|
+
# for a node that has none — any node of an XML document, whose contents
|
|
232
|
+
# the template-content registry keeps instead.
|
|
233
|
+
def template_contents(node)
|
|
234
|
+
node.respond_to?(:content_fragment) ? node.content_fragment : nil
|
|
194
235
|
end
|
|
195
236
|
|
|
196
|
-
#
|
|
197
|
-
#
|
|
198
|
-
#
|
|
199
|
-
|
|
200
|
-
|
|
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.
|
|
244
|
+
|
|
245
|
+
def get_attribute_ns(node, namespace, local_name)
|
|
246
|
+
node.attribute_value_ns(namespace, local_name.to_s)
|
|
201
247
|
end
|
|
202
248
|
|
|
203
|
-
def
|
|
204
|
-
|
|
249
|
+
def has_attribute_ns?(node, namespace, local_name)
|
|
250
|
+
!attr_by_ns(node, namespace, local_name).nil?
|
|
205
251
|
end
|
|
206
252
|
|
|
207
|
-
#
|
|
208
|
-
#
|
|
209
|
-
def
|
|
210
|
-
|
|
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)
|
|
211
257
|
end
|
|
212
258
|
|
|
213
|
-
def set_attribute_ns(node, namespace, prefix,
|
|
214
|
-
|
|
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
|
|
215
266
|
end
|
|
216
267
|
|
|
217
|
-
|
|
218
|
-
|
|
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
|
|
219
278
|
end
|
|
220
279
|
|
|
221
|
-
def
|
|
222
|
-
|
|
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
|
|
223
289
|
end
|
|
224
290
|
|
|
225
291
|
# Reads a backend attribute node into {namespace_uri:, prefix:,
|
|
226
292
|
# local_name:, qualified_name:, value:} (namespace-aware).
|
|
227
293
|
def attribute_ns_info(attr_node)
|
|
228
|
-
|
|
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
|
+
}
|
|
229
301
|
end
|
|
230
302
|
|
|
231
303
|
# The attribute node whose QUALIFIED name is `qualified_name`, or nil.
|
|
232
304
|
#
|
|
233
305
|
# WHATWG's by-name family ("get an attribute by name", `setAttribute`,
|
|
234
|
-
# `removeAttribute`) matches on the qualified name. A lookup by local
|
|
235
|
-
# confuses `b` with a prefixed `xml:b`, so those paths must come
|
|
236
|
-
# 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.
|
|
237
309
|
def attr_by_qualified_name(node, qualified_name)
|
|
238
|
-
|
|
310
|
+
node.attribute_by_qualified_name(qualified_name.to_s)
|
|
239
311
|
end
|
|
240
312
|
|
|
241
|
-
# That attribute's VALUE, or nil when there is no such attribute. The
|
|
242
|
-
# match without an attribute node in hand, for the two readers that
|
|
243
|
-
# ever wanted the value — `getAttribute` and `hasAttribute` — which
|
|
244
|
-
# 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.
|
|
245
317
|
def attr_value_by_qualified_name(node, qualified_name)
|
|
246
|
-
|
|
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)
|
|
247
328
|
end
|
|
248
329
|
|
|
249
330
|
# The element's attribute nodes (each readable via attribute_ns_info).
|
|
250
331
|
# The single choke point so DOM code doesn't touch parser internals.
|
|
251
332
|
def attribute_nodes(node)
|
|
252
|
-
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# Type constants — proxy through to the current backend so
|
|
256
|
-
# `node.is_a?(Backend::Element)` resolves dynamically.
|
|
257
|
-
def element_class
|
|
258
|
-
current::Element
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
def document_class
|
|
262
|
-
current::Document
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
def text_class
|
|
266
|
-
current::Text
|
|
267
|
-
end
|
|
268
|
-
|
|
269
|
-
def comment_class
|
|
270
|
-
current::Comment
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def document_fragment_class
|
|
274
|
-
current::DocumentFragment
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def node_class
|
|
278
|
-
current::Node
|
|
333
|
+
node.attribute_nodes
|
|
279
334
|
end
|
|
280
335
|
|
|
281
336
|
private
|
|
282
337
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
|
295
360
|
end
|
|
296
361
|
end
|
|
297
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 = "")
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Bridge
|
|
5
|
+
# A JS constructor function as Ruby sees it: `new X(...)` plus the static
|
|
6
|
+
# methods hanging off it, and the table Window looks names up in. The
|
|
7
|
+
# registry maps to these, so they share a file.
|
|
8
|
+
|
|
9
|
+
# Block-as-constructor adapter — invoking `__js_new__(args)`
|
|
10
|
+
# calls the wrapped block with `args` and returns whatever the
|
|
11
|
+
# block produces. Used by Window to wire up `new Event(init)`,
|
|
12
|
+
# `new CustomEvent(init)`, etc. without hand-rolling a class
|
|
13
|
+
# for each constructor.
|
|
14
|
+
class Constructor
|
|
15
|
+
def initialize(&block)
|
|
16
|
+
@block = block
|
|
17
|
+
@class_methods = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def __js_new__(args)
|
|
21
|
+
@block.call(args)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Register a class-level method (e.g. `URL.createObjectURL`)
|
|
25
|
+
# that JS bridges resolve via `__js_call__` on the constructor
|
|
26
|
+
# itself. Returns self for chaining.
|
|
27
|
+
def define_class_method(name, &block)
|
|
28
|
+
@class_methods[name.to_s] = block
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# A name outside the registered statics is not a method of this
|
|
33
|
+
# constructor, and answering nil would make `URL.notAThing()` read as a
|
|
34
|
+
# call that returned null rather than one that should not have compiled.
|
|
35
|
+
def __js_call__(method, args)
|
|
36
|
+
handler = @class_methods[method.to_s]
|
|
37
|
+
raise TypeError, "#{method} is not a function" unless handler
|
|
38
|
+
|
|
39
|
+
handler.call(args)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Names of the registered class-level (static) methods, so a JS host can
|
|
43
|
+
# expose them on the constructor function (e.g. `URL.createObjectURL`).
|
|
44
|
+
def __js_class_method_names__
|
|
45
|
+
@class_methods.keys
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Maps JS global constructor names (e.g. "Event", "URL", "XMLHttpRequest")
|
|
50
|
+
# to their `Bridge::Constructor` instances. Window builds one and routes
|
|
51
|
+
# `__js_get__` name lookups through it, instead of carrying one ivar plus
|
|
52
|
+
# one `when` arm per constructor.
|
|
53
|
+
class ConstructorRegistry
|
|
54
|
+
def initialize(map)
|
|
55
|
+
@map = map.freeze
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def [](name)
|
|
59
|
+
@map[name]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def key?(name)
|
|
63
|
+
@map.key?(name)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The set of constructor names, e.g. for host-side enumeration / tests.
|
|
67
|
+
def names
|
|
68
|
+
@map.keys
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|