Module: Dommy::Js::DomInterfaces

Defined in:
lib/dommy/js/dom_interfaces.rb

Overview

Derives WebIDL interface metadata for a Dommy DOM object: the most-derived interface name and the single-inheritance chain up to the root (EventTarget for nodes). This mirrors the JS prototype chain the bridge builds so ‘instanceof` / Object.prototype.toString resolve correctly.

Engine-agnostic, and the single home for DOM interface hierarchy knowledge: BASE_CHAINS (seeded eagerly on the JS side) must stay consistent with what #chain_for derives from real objects.

Constant Summary collapse

NAME_OVERRIDES =

Dommy class basename -> WebIDL interface name, where they diverge. Anything not listed uses the class basename verbatim (HTMLDivElement, …).

{
  "TextNode" => "Text",
  "CommentNode" => "Comment",
  "CharacterDataNode" => "CharacterData",
  "Fragment" => "DocumentFragment",
  "ClassList" => "DOMTokenList",
  "DatasetMap" => "DOMStringMap",
  "StyleDeclaration" => "CSSStyleDeclaration",
  "LiveNodeList" => "NodeList",
  "StandaloneEventTarget" => "EventTarget"
}.freeze
HTML_LEAF_INTERFACES =

Concrete HTML element interfaces. A browser exposes every one of these as a global constructor whether or not an instance exists, so a framework’s bare ‘instanceof HTMLInputElement` feature check resolves regardless of page content (idiomorph, Turbo’s morph engine, probes ‘instanceof HTMLInputElement`/`HTMLTextAreaElement` during focus restoration even when the page has no such element). Each is a direct HTMLElement subclass except the two media leaves, appended with their chains below. Mirrors the `class HTMLxxxElement < HTMLElement` set in the dommy gem’s html_elements.

%w[
  HTMLAnchorElement HTMLAreaElement HTMLBaseElement HTMLBodyElement
  HTMLBRElement HTMLButtonElement HTMLDataElement HTMLDetailsElement
  HTMLDialogElement HTMLDivElement HTMLEmbedElement HTMLFieldsetElement
  HTMLFormElement HTMLHeadElement HTMLHeadingElement HTMLHRElement
  HTMLHtmlElement HTMLIFrameElement HTMLImageElement HTMLInputElement
  HTMLLabelElement HTMLLegendElement HTMLLIElement HTMLLinkElement
  HTMLMapElement HTMLMetaElement HTMLMeterElement HTMLModElement
  HTMLObjectElement HTMLOListElement HTMLOptGroupElement HTMLOptionElement
  HTMLOutputElement HTMLParagraphElement HTMLPictureElement HTMLPreElement
  HTMLProgressElement HTMLQuoteElement HTMLScriptElement HTMLSelectElement
  HTMLSlotElement HTMLSourceElement HTMLSpanElement HTMLStyleElement
  HTMLTableCaptionElement HTMLTableCellElement HTMLTableElement
  HTMLTableRowElement HTMLTableSectionElement HTMLTemplateElement
  HTMLTextAreaElement HTMLTimeElement HTMLTitleElement HTMLTrackElement
  HTMLUListElement
].freeze
BASE_CHAINS =

Base interface chains seeded eagerly on the JS side so ‘instanceof Node` / `typeof HTMLElement` resolve before an instance of that exact type has crossed. Concrete leaves (HTMLButtonElement, …) are built lazily from #chain_for when an instance crosses. Keep consistent with #chain_for.

[
  %w[Node EventTarget],
  %w[Element Node EventTarget],
  %w[HTMLElement Element Node EventTarget],
  %w[SVGElement Element Node EventTarget],
  %w[CharacterData Node EventTarget],
  %w[Text CharacterData Node EventTarget],
  %w[Comment CharacterData Node EventTarget],
  %w[Document Node EventTarget],
  %w[DocumentFragment Node EventTarget],
  %w[DocumentType Node EventTarget],
  %w[Attr Node EventTarget],
  %w[Event],
  %w[CustomEvent Event],
  %w[MessageEvent Event],
  %w[PopStateEvent Event],
  %w[CloseEvent Event],
  %w[MouseEvent Event],
  %w[KeyboardEvent Event],
  %w[DOMException],
  # Window-exposed constructors that frameworks call bare (new X(...)).
  # Seeding them creates the global; construction routes to the window.
  %w[MutationObserver], %w[IntersectionObserver], %w[ResizeObserver],
  %w[PerformanceObserver], %w[AbortController], %w[AbortSignal EventTarget],
  %w[FormData], %w[URL], %w[URLSearchParams], %w[Headers], %w[Request], %w[Response],
  %w[Blob], %w[File], %w[FileList], %w[FileReader], %w[XMLHttpRequest],
  %w[TextEncoder], %w[TextDecoder], %w[DOMParser], %w[XMLSerializer],
  %w[MessageChannel], %w[BroadcastChannel], %w[WebSocket], %w[EventSource],
  %w[Notification], %w[Worker], %w[DataTransfer],
  %w[ReadableStream], %w[WritableStream], %w[TransformStream],
  %w[Range],
  # Collection interfaces, seeded so `result instanceof NodeList` /
  # `instanceof HTMLCollection` resolve (querySelectorAll, children, …).
  %w[NodeList], %w[HTMLCollection],
  # Traversal: NodeFilter exposes only [Constant]s (NodeFilter.SHOW_ELEMENT,
  # .FILTER_ACCEPT, …); TreeWalker/NodeIterator are instances.
  %w[NodeFilter], %w[TreeWalker], %w[NodeIterator],
  # Concrete HTML element interfaces (see HTML_LEAF_INTERFACES) + the media
  # subtree, so bare `instanceof HTMLInputElement` always resolves.
  *HTML_LEAF_INTERFACES.map { |n| [n, "HTMLElement", "Element", "Node", "EventTarget"] },
  %w[HTMLMediaElement HTMLElement Element Node EventTarget],
  %w[HTMLAudioElement HTMLMediaElement HTMLElement Element Node EventTarget],
  %w[HTMLVideoElement HTMLMediaElement HTMLElement Element Node EventTarget]
].freeze

Class Method Summary collapse

Class Method Details

.chain_for(obj) ⇒ Object

Walk the Dommy class superclass chain (HTMLDivElement < HTMLElement < Element), then append the module-provided base interfaces (Node -> EventTarget) for nodes, since Dommy models Node as a mixin rather than a superclass. Stops at the first foreign superclass (Object, or StandardError for DOMException) so non-DOM ancestors stay out.



116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/dommy/js/dom_interfaces.rb', line 116

def chain_for(obj)
  names = []
  klass = obj.class
  while klass && klass.name&.start_with?("Dommy::")
    name = name_for(klass)
    names << name if name && !names.include?(name)
    klass = klass.superclass
  end
  if defined?(Dommy::Node) && obj.is_a?(Dommy::Node)
    names << "Node" unless names.include?("Node")
    names << "EventTarget" unless names.include?("EventTarget")
  end
  names
end

.info(obj) ⇒ Object

{ “name” => most-derived interface, “chain” => […] } for a host object.



106
107
108
109
# File 'lib/dommy/js/dom_interfaces.rb', line 106

def info(obj)
  chain = chain_for(obj)
  {"name" => chain.first, "chain" => chain}
end

.name_for(klass) ⇒ Object



131
132
133
134
135
136
# File 'lib/dommy/js/dom_interfaces.rb', line 131

def name_for(klass)
  base = klass.name&.split("::")&.last
  return nil unless base

  NAME_OVERRIDES.fetch(base, base)
end