diff --git a/.changeset/calm-types-erase.md b/.changeset/calm-types-erase.md new file mode 100644 index 00000000..dd377244 --- /dev/null +++ b/.changeset/calm-types-erase.md @@ -0,0 +1,5 @@ +--- +'@remote-dom/polyfill': patch +--- + +Make the Polyfill source compatible with type stripping by replacing TypeScript enums and a parameter property with erasable syntax. diff --git a/packages/polyfill/source/Attr.ts b/packages/polyfill/source/Attr.ts index a43d3660..650f0c93 100644 --- a/packages/polyfill/source/Attr.ts +++ b/packages/polyfill/source/Attr.ts @@ -4,15 +4,16 @@ import { VALUE, OWNER_ELEMENT, NAME, - NamespaceURI, - NodeType, + NODE_TYPE_ATTRIBUTE, + type NamespaceURI, + type NodeType, HOOKS, } from './constants.ts'; import {Node} from './Node.ts'; import type {Element} from './Element.ts'; export class Attr extends Node { - nodeType = NodeType.ATTRIBUTE_NODE; + nodeType: NodeType = NODE_TYPE_ATTRIBUTE; [NS]: NamespaceURI | null = null; [NEXT]: Attr | null = null; [VALUE]: string; diff --git a/packages/polyfill/source/Comment.ts b/packages/polyfill/source/Comment.ts index c3ca61b7..80d50109 100644 --- a/packages/polyfill/source/Comment.ts +++ b/packages/polyfill/source/Comment.ts @@ -1,7 +1,7 @@ -import {NAME} from './constants.ts'; +import {NAME, NODE_TYPE_COMMENT, type NodeType} from './constants.ts'; import {CharacterData} from './CharacterData.ts'; export class Comment extends CharacterData { - nodeType = 8; + nodeType: NodeType = NODE_TYPE_COMMENT; [NAME] = '#comment'; } diff --git a/packages/polyfill/source/Document.ts b/packages/polyfill/source/Document.ts index 625cde69..cabe2332 100644 --- a/packages/polyfill/source/Document.ts +++ b/packages/polyfill/source/Document.ts @@ -1,8 +1,10 @@ import { NS, NAME, - NamespaceURI, - NodeType, + NODE_TYPE_DOCUMENT, + SVG_NAMESPACE, + type NamespaceURI, + type NodeType, OWNER_DOCUMENT, HOOKS, IS_CONNECTED, @@ -28,7 +30,7 @@ import {HTMLHeadElement} from './HTMLHeadElement.ts'; import {HTMLHtmlElement} from './HTMLHtmlElement.ts'; export class Document extends ParentNode { - nodeType = NodeType.DOCUMENT_NODE; + nodeType: NodeType = NODE_TYPE_DOCUMENT; [NAME] = '#document'; body: HTMLBodyElement; head: HTMLHeadElement; @@ -115,7 +117,7 @@ export function createElement( let element: T; const lowerName = String(name).toLowerCase(); - if (namespace === NamespaceURI.SVG) { + if (namespace === SVG_NAMESPACE) { element = new SVGElement() as any; } else if (lowerName === 'template') { element = new HTMLTemplateElement() as any; diff --git a/packages/polyfill/source/DocumentFragment.ts b/packages/polyfill/source/DocumentFragment.ts index 0853da10..f9d2c25e 100644 --- a/packages/polyfill/source/DocumentFragment.ts +++ b/packages/polyfill/source/DocumentFragment.ts @@ -1,9 +1,14 @@ -import {NAME, OWNER_DOCUMENT, NodeType} from './constants.ts'; +import { + NAME, + OWNER_DOCUMENT, + NODE_TYPE_DOCUMENT_FRAGMENT, + type NodeType, +} from './constants.ts'; import {ParentNode} from './ParentNode.ts'; import {getElementById as findElementById} from './shared.ts'; export class DocumentFragment extends ParentNode { - nodeType = NodeType.DOCUMENT_FRAGMENT_NODE; + nodeType: NodeType = NODE_TYPE_DOCUMENT_FRAGMENT; [NAME] = '#document-fragment'; [OWNER_DOCUMENT] = (typeof window !== 'undefined' ? window.document diff --git a/packages/polyfill/source/Element.ts b/packages/polyfill/source/Element.ts index 8f6e3837..d4537cab 100644 --- a/packages/polyfill/source/Element.ts +++ b/packages/polyfill/source/Element.ts @@ -1,4 +1,11 @@ -import {NS, ATTRIBUTES, NamespaceURI, NodeType} from './constants.ts'; +import { + NS, + ATTRIBUTES, + HTML_NAMESPACE, + NODE_TYPE_ELEMENT, + type NamespaceURI, + type NodeType, +} from './constants.ts'; import {ParentNode} from './ParentNode.ts'; import {NamedNodeMap} from './NamedNodeMap.ts'; import {Attr} from './Attr.ts'; @@ -8,9 +15,9 @@ import {getElementsByTagName as findElementsByTagName} from './shared.ts'; export class Element extends ParentNode { static readonly observedAttributes?: string[]; - nodeType = NodeType.ELEMENT_NODE; + nodeType: NodeType = NODE_TYPE_ELEMENT; - [NS] = NamespaceURI.XHTML; + [NS]: NamespaceURI = HTML_NAMESPACE; get namespaceURI() { return this[NS]; } diff --git a/packages/polyfill/source/Event.ts b/packages/polyfill/source/Event.ts index e5793dc1..a2d2db75 100644 --- a/packages/polyfill/source/Event.ts +++ b/packages/polyfill/source/Event.ts @@ -6,14 +6,12 @@ import { } from './constants.ts'; import type {EventTarget} from './EventTarget.ts'; -// TODO: Replace this enum with one-way constants, matching the DOM and Web IDL. -// @ts-expect-error -- Legacy enum; keep this exception scoped to this declaration. -export const enum EventPhase { - NONE = 0, - CAPTURING_PHASE = 1, - AT_TARGET = 2, - BUBBLING_PHASE = 3, -} +export const EVENT_PHASE_NONE = 0; +export const EVENT_PHASE_CAPTURING = 1; +export const EVENT_PHASE_AT_TARGET = 2; +export const EVENT_PHASE_BUBBLING = 3; + +export type EventPhase = number; export const CAPTURE_MARKER = '@'; @@ -29,16 +27,17 @@ const now = : performance.now.bind(performance); export class Event { - static NONE = EventPhase.NONE; - static CAPTURING_PHASE = EventPhase.CAPTURING_PHASE; - static AT_TARGET = EventPhase.AT_TARGET; - static BUBBLING_PHASE = EventPhase.BUBBLING_PHASE; + static NONE: EventPhase = EVENT_PHASE_NONE; + static CAPTURING_PHASE: EventPhase = EVENT_PHASE_CAPTURING; + static AT_TARGET: EventPhase = EVENT_PHASE_AT_TARGET; + static BUBBLING_PHASE: EventPhase = EVENT_PHASE_BUBBLING; - // NONE = EventPhase.NONE; - // CAPTURING_PHASE = EventPhase.CAPTURING_PHASE; - // AT_TARGET = EventPhase.AT_TARGET; - // BUBBLING_PHASE = EventPhase.BUBBLING_PHASE; + // NONE = EVENT_PHASE_NONE; + // CAPTURING_PHASE = EVENT_PHASE_CAPTURING; + // AT_TARGET = EVENT_PHASE_AT_TARGET; + // BUBBLING_PHASE = EVENT_PHASE_BUBBLING; + type: string; timeStamp = now(); target: EventTarget | null = null; currentTarget: EventTarget | null = null; @@ -48,18 +47,15 @@ export class Event { composed = false; defaultPrevented = false; cancelBubble = false; - eventPhase: EventPhase = 0; + eventPhase: EventPhase = EVENT_PHASE_NONE; // private inPassiveListener = false; data?: any; [PATH]: EventTarget[] = []; [IS_TRUSTED]!: boolean; [STOP_IMMEDIATE_PROPAGATION] = false; - constructor( - // @ts-expect-error -- Legacy parameter property; keep this exception scoped to this declaration. - public type: string, - options?: EventInit, - ) { + constructor(type: string, options?: EventInit) { + this.type = type; Object.defineProperty(this, IS_TRUSTED, {writable: true, value: false}); if (options) { if (options.bubbles) this.bubbles = options.bubbles; @@ -108,20 +104,18 @@ export class Event { export function fireEvent( event: Event, currentTarget: EventTarget, - phase: EventPhase.BUBBLING_PHASE | EventPhase.CAPTURING_PHASE, + phase: typeof EVENT_PHASE_BUBBLING | typeof EVENT_PHASE_CAPTURING, ): void { const listeners = currentTarget[LISTENERS]; const list = listeners?.get( - `${event.type}${ - phase === EventPhase.CAPTURING_PHASE ? CAPTURE_MARKER : '' - }`, + `${event.type}${phase === EVENT_PHASE_CAPTURING ? CAPTURE_MARKER : ''}`, ); if (!list) return; for (const listener of list) { event.eventPhase = - event.target === currentTarget ? EventPhase.AT_TARGET : phase; + event.target === currentTarget ? EVENT_PHASE_AT_TARGET : phase; event.currentTarget = currentTarget; try { diff --git a/packages/polyfill/source/EventTarget.ts b/packages/polyfill/source/EventTarget.ts index ea448660..992ccb14 100644 --- a/packages/polyfill/source/EventTarget.ts +++ b/packages/polyfill/source/EventTarget.ts @@ -1,5 +1,9 @@ import {HOOKS, PATH, LISTENERS, OWNER_DOCUMENT} from './constants.ts'; -import {fireEvent, EventPhase} from './Event.ts'; +import { + EVENT_PHASE_BUBBLING, + EVENT_PHASE_CAPTURING, + fireEvent, +} from './Event.ts'; import {CAPTURE_MARKER, type Event} from './Event.ts'; import type {ChildNode} from './ChildNode.ts'; import type {Document} from './Document.ts'; @@ -114,14 +118,14 @@ export class EventTarget { event[PATH] = path; for (let i = path.length; i--; ) { - fireEvent(event, path[i]!, EventPhase.CAPTURING_PHASE); + fireEvent(event, path[i]!, EVENT_PHASE_CAPTURING); if (event.cancelBubble) return event.defaultPrevented; } const bubblePath = event.bubbles ? path : path.slice(0, 1); for (let i = 0; i < bubblePath.length; i++) { - fireEvent(event, bubblePath[i]!, EventPhase.BUBBLING_PHASE); + fireEvent(event, bubblePath[i]!, EVENT_PHASE_BUBBLING); if (event.cancelBubble) return event.defaultPrevented; } diff --git a/packages/polyfill/source/NamedNodeMap.ts b/packages/polyfill/source/NamedNodeMap.ts index d82eac9f..d039cf79 100644 --- a/packages/polyfill/source/NamedNodeMap.ts +++ b/packages/polyfill/source/NamedNodeMap.ts @@ -3,7 +3,7 @@ import { OWNER_ELEMENT, NS, NEXT, - NamespaceURI, + type NamespaceURI, HOOKS, } from './constants.ts'; import type {Attr} from './Attr.ts'; diff --git a/packages/polyfill/source/Node.ts b/packages/polyfill/source/Node.ts index af1a5705..4c2c1ce2 100644 --- a/packages/polyfill/source/Node.ts +++ b/packages/polyfill/source/Node.ts @@ -5,8 +5,9 @@ import { CHILD, PREV, NEXT, - NamespaceURI, - NodeType, + HTML_NAMESPACE, + NODE_TYPE_NODE, + type NodeType, HOOKS, IS_CONNECTED, } from './constants.ts'; @@ -22,7 +23,7 @@ import { } from './shared.ts'; export class Node extends EventTarget { - nodeType = NodeType.NODE; + nodeType: NodeType = NODE_TYPE_NODE; [OWNER_DOCUMENT]!: Document; [NAME] = ''; @@ -52,8 +53,8 @@ export class Node extends EventTarget { return this[IS_CONNECTED]; } - isDefaultNamespace(namespace: string) { - return namespace === NamespaceURI.XHTML; + isDefaultNamespace(namespace: string): namespace is typeof HTML_NAMESPACE { + return namespace === HTML_NAMESPACE; } get parentNode() { diff --git a/packages/polyfill/source/ParentNode.ts b/packages/polyfill/source/ParentNode.ts index 7ed9bd81..e16c1967 100644 --- a/packages/polyfill/source/ParentNode.ts +++ b/packages/polyfill/source/ParentNode.ts @@ -4,7 +4,8 @@ import { PREV, PARENT, OWNER_DOCUMENT, - NodeType, + NODE_TYPE_DOCUMENT_FRAGMENT, + NODE_TYPE_ELEMENT, HOOKS, IS_CONNECTED, } from './constants.ts'; @@ -81,7 +82,7 @@ export class ParentNode extends ChildNode { } } - if (this.nodeType === NodeType.ELEMENT_NODE) { + if (this.nodeType === NODE_TYPE_ELEMENT) { this[HOOKS].removeChild?.(this as any, child as any, childNodesIndex); } } @@ -105,7 +106,7 @@ export class ParentNode extends ChildNode { private insertInto(child: Node, before: Node | null) { // append the children of a DocumentFragment: - if (child.nodeType === NodeType.DOCUMENT_FRAGMENT_NODE) { + if (child.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT) { let node = child[CHILD]; while (node) { const next = node[NEXT]; @@ -144,7 +145,7 @@ export class ParentNode extends ChildNode { } const ownerDocument = this[OWNER_DOCUMENT]; - const isElement = child.nodeType === NodeType.ELEMENT_NODE; + const isElement = child.nodeType === NODE_TYPE_ELEMENT; child[PARENT] = this; child[OWNER_DOCUMENT] = ownerDocument; @@ -179,7 +180,7 @@ export class ParentNode extends ChildNode { } } - if (this.nodeType === NodeType.ELEMENT_NODE) { + if (this.nodeType === NODE_TYPE_ELEMENT) { this[HOOKS].insertChild?.(this as any, child as any, insertIndex); } } diff --git a/packages/polyfill/source/SVGElement.ts b/packages/polyfill/source/SVGElement.ts index 328b17f4..0ed95c52 100644 --- a/packages/polyfill/source/SVGElement.ts +++ b/packages/polyfill/source/SVGElement.ts @@ -1,8 +1,8 @@ -import {NS, NamespaceURI} from './constants.ts'; +import {NS, SVG_NAMESPACE, type NamespaceURI} from './constants.ts'; import {Element} from './Element.ts'; export class SVGElement extends Element { - [NS] = NamespaceURI.SVG; + [NS]: NamespaceURI = SVG_NAMESPACE; get ownerSVGElement() { let root: SVGElement | null = null; diff --git a/packages/polyfill/source/Text.ts b/packages/polyfill/source/Text.ts index fd64724d..47b39bd4 100644 --- a/packages/polyfill/source/Text.ts +++ b/packages/polyfill/source/Text.ts @@ -1,7 +1,7 @@ -import {NAME, NodeType} from './constants.ts'; +import {NAME, NODE_TYPE_TEXT, type NodeType} from './constants.ts'; import {CharacterData} from './CharacterData.ts'; export class Text extends CharacterData { - nodeType = NodeType.TEXT_NODE; + nodeType: NodeType = NODE_TYPE_TEXT; [NAME] = '#text'; } diff --git a/packages/polyfill/source/constants.ts b/packages/polyfill/source/constants.ts index f304bea9..8175a8df 100644 --- a/packages/polyfill/source/constants.ts +++ b/packages/polyfill/source/constants.ts @@ -18,26 +18,22 @@ export const CONTENT = Symbol('content'); export const HOOKS = Symbol('hooks'); export const IS_CONNECTED = Symbol('is_connected'); -// TODO: Replace this enum with one-way constants, matching the DOM and Web IDL. -// @ts-expect-error -- Legacy enum; keep this exception scoped to this declaration. -export const enum NodeType { - NODE = 0, - ELEMENT_NODE = 1, - ATTRIBUTE_NODE = 2, - TEXT_NODE = 3, - CDATA_SECTION_NODE = 4, - ENTITY_REFERENCE_NODE = 5, - ENTITY_NODE = 6, - PROCESSING_INSTRUCTION_NODE = 7, - COMMENT_NODE = 8, - DOCUMENT_NODE = 9, - DOCUMENT_TYPE_NODE = 10, - DOCUMENT_FRAGMENT_NODE = 11, -} +export const NODE_TYPE_NODE = 0; +export const NODE_TYPE_ELEMENT = 1; +export const NODE_TYPE_ATTRIBUTE = 2; +export const NODE_TYPE_TEXT = 3; +export const NODE_TYPE_CDATA_SECTION = 4; +export const NODE_TYPE_ENTITY_REFERENCE = 5; +export const NODE_TYPE_ENTITY = 6; +export const NODE_TYPE_PROCESSING_INSTRUCTION = 7; +export const NODE_TYPE_COMMENT = 8; +export const NODE_TYPE_DOCUMENT = 9; +export const NODE_TYPE_DOCUMENT_TYPE = 10; +export const NODE_TYPE_DOCUMENT_FRAGMENT = 11; -// TODO: Replace this enum with namespace string constants, matching the DOM. -// @ts-expect-error -- Legacy enum; keep this exception scoped to this declaration. -export const enum NamespaceURI { - XHTML = 'http://www.w3.org/1999/xhtml', - SVG = 'http://www.w3.org/2000/svg', -} +export type NodeType = number; + +export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; +export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; + +export type NamespaceURI = typeof HTML_NAMESPACE | typeof SVG_NAMESPACE; diff --git a/packages/polyfill/source/selectors.ts b/packages/polyfill/source/selectors.ts index 3d357247..be1d6d2c 100644 --- a/packages/polyfill/source/selectors.ts +++ b/packages/polyfill/source/selectors.ts @@ -1,31 +1,39 @@ -import {CHILD, NEXT, PARENT, PREV, NamespaceURI} from './constants.ts'; +import {CHILD, NEXT, PARENT, PREV, HTML_NAMESPACE} from './constants.ts'; import {isElementNode} from './shared.ts'; import type {Node} from './Node.ts'; import type {Element} from './Element.ts'; import type {ParentNode} from './ParentNode.ts'; -// TODO: Replace this enum with an erasable constant object. -// @ts-expect-error -- Legacy enum; keep this exception scoped to this declaration. -export const enum Combinator { - Descendant, - Child, - Sibling, - Adjacent, - Inner, -} +export const COMBINATOR_DESCENDANT = 0; +export const COMBINATOR_CHILD = 1; +export const COMBINATOR_SIBLING = 2; +export const COMBINATOR_ADJACENT = 3; +export const COMBINATOR_INNER = 4; -// TODO: Replace this enum with an erasable constant object. -// @ts-expect-error -- Legacy enum; keep this exception scoped to this declaration. -export const enum MatcherType { - Unknown, - Element, - Id, - Class, - Attribute, - Pseudo, - Function, -} +export type Combinator = + | typeof COMBINATOR_DESCENDANT + | typeof COMBINATOR_CHILD + | typeof COMBINATOR_SIBLING + | typeof COMBINATOR_ADJACENT + | typeof COMBINATOR_INNER; + +export const MATCHER_UNKNOWN = 0; +export const MATCHER_ELEMENT = 1; +export const MATCHER_ID = 2; +export const MATCHER_CLASS = 3; +export const MATCHER_ATTRIBUTE = 4; +export const MATCHER_PSEUDO = 5; +export const MATCHER_FUNCTION = 6; + +export type MatcherType = + | typeof MATCHER_UNKNOWN + | typeof MATCHER_ELEMENT + | typeof MATCHER_ID + | typeof MATCHER_CLASS + | typeof MATCHER_ATTRIBUTE + | typeof MATCHER_PSEUDO + | typeof MATCHER_FUNCTION; export interface Part { combinator: Combinator; @@ -44,10 +52,10 @@ export function querySelector( within: ParentNode, selector: string | Matcher[], ): Element | null { - const parts = + const parts: Part[] = typeof selector === 'string' ? parseSelector(selector) - : [{combinator: Combinator.Inner, matchers: selector}]; + : [{combinator: COMBINATOR_INNER, matchers: selector}]; let result: Element | null = null; const child = within[CHILD]; @@ -64,10 +72,10 @@ export function querySelectorAll( within: ParentNode, selector: string | Matcher[], ): Element[] { - const parts = + const parts: Part[] = typeof selector === 'string' ? parseSelector(selector) - : [{combinator: Combinator.Inner, matchers: selector}]; + : [{combinator: COMBINATOR_INNER, matchers: selector}]; const results: Element[] = []; const child = within[CHILD]; @@ -80,7 +88,7 @@ export function querySelectorAll( } export function parseSelector(selector: string) { - let part: Part = {combinator: Combinator.Inner, matchers: []}; + let part: Part = {combinator: COMBINATOR_INNER, matchers: []}; const parts = [part]; const tokenizer = /\s*?([>\s+~]?)\s*?(?:(?:\[\s*([^\]=]+)(?:=(['"])(.*?)\3)?\s*\])|([#.]?)([^\s#.[>:+~]+)|:(\w+)(?:\((.*?)\))?)/gi; @@ -95,27 +103,27 @@ export function parseSelector(selector: string) { // [8]: :function(argument) value if (token[1]) { // Update the combinator on the (now parent) Part: - if (token[1] === '>') part.combinator = Combinator.Child; - else if (token[1] === '+') part.combinator = Combinator.Adjacent; - else if (token[1] === '~') part.combinator = Combinator.Sibling; - else part.combinator = Combinator.Descendant; + if (token[1] === '>') part.combinator = COMBINATOR_CHILD; + else if (token[1] === '+') part.combinator = COMBINATOR_ADJACENT; + else if (token[1] === '~') part.combinator = COMBINATOR_SIBLING; + else part.combinator = COMBINATOR_DESCENDANT; // Add a new Part for the next selector parts: - part = {combinator: Combinator.Inner, matchers: []}; + part = {combinator: COMBINATOR_INNER, matchers: []}; parts.push(part); } - let type = MatcherType.Unknown; + let type: MatcherType = MATCHER_UNKNOWN; if (token[2]) { - type = MatcherType.Attribute; + type = MATCHER_ATTRIBUTE; } else if (token[5]) { - type = token[5] === '#' ? MatcherType.Id : MatcherType.Class; + type = token[5] === '#' ? MATCHER_ID : MATCHER_CLASS; } else if (token[7]) { - type = token[8] == null ? MatcherType.Pseudo : MatcherType.Function; + type = token[8] == null ? MATCHER_PSEUDO : MATCHER_FUNCTION; } else if (token[6]) { if (token[6] === '*') { - type = MatcherType.Unknown; // Universal selector matches all + type = MATCHER_UNKNOWN; // Universal selector matches all } else if (ELEMENT_SELECTOR_TEST.test(token[6])) { - type = MatcherType.Element; + type = MATCHER_ELEMENT; } } part.matchers.push({ @@ -159,21 +167,21 @@ function walkNodesForSelector( function matchesSelectorRecursive(element: Element, parts: Part[]): boolean { const {combinator, matchers} = parts[parts.length - 1]!; - if (combinator === Combinator.Inner) { + if (combinator === COMBINATOR_INNER) { if (!matchesSelectorMatcher(element, matchers)) return false; const pp = parts.slice(0, -1); return pp.length === 0 || matchesSelectorRecursive(element, pp); } const link = - combinator === Combinator.Child || combinator === Combinator.Descendant + combinator === COMBINATOR_CHILD || combinator === COMBINATOR_DESCENDANT ? PARENT : PREV; let ref = element[link]; if (!ref) return false; if ( - combinator === Combinator.Descendant || - combinator === Combinator.Sibling + combinator === COMBINATOR_DESCENDANT || + combinator === COMBINATOR_SIBLING ) { // For descendant/sibling combinators, search through all ancestors/siblings while (ref) { @@ -188,7 +196,7 @@ function matchesSelectorRecursive(element: Element, parts: Part[]): boolean { } else { // For child/adjacent combinators, check only the immediate parent/sibling // For sibling combinators, skip non-element siblings - if (combinator === Combinator.Adjacent && !isElementNode(ref)) { + if (combinator === COMBINATOR_ADJACENT && !isElementNode(ref)) { // Skip to next element sibling while (ref && !isElementNode(ref)) { ref = ref[link]; @@ -205,18 +213,18 @@ function matchesSelectorRecursive(element: Element, parts: Part[]): boolean { } function matchesSelectorPart(element: Element, {combinator, matchers}: Part) { - if (combinator === Combinator.Inner) { + if (combinator === COMBINATOR_INNER) { return matchesSelectorMatcher(element, matchers); } const link = - combinator === Combinator.Child || combinator === Combinator.Descendant + combinator === COMBINATOR_CHILD || combinator === COMBINATOR_DESCENDANT ? PARENT : PREV; let ref = element[link]; if (!ref) return false; // For sibling combinators, skip non-element siblings - if (combinator === Combinator.Adjacent && !isElementNode(ref)) { + if (combinator === COMBINATOR_ADJACENT && !isElementNode(ref)) { while (ref && !isElementNode(ref)) { ref = ref[link]; } @@ -228,8 +236,8 @@ function matchesSelectorPart(element: Element, {combinator, matchers}: Part) { } if ( - combinator === Combinator.Descendant || - combinator === Combinator.Sibling + combinator === COMBINATOR_DESCENDANT || + combinator === COMBINATOR_SIBLING ) { while ((ref = ref[link])) { if (isElementNode(ref) && matchesSelectorMatcher(ref, matchers)) @@ -252,28 +260,28 @@ function matchesSelectorMatcher( } const {type, name, value} = matcher; switch (type) { - case MatcherType.Unknown: + case MATCHER_UNKNOWN: return name === '*'; // Universal selector - case MatcherType.Element: - return element.namespaceURI === NamespaceURI.XHTML + case MATCHER_ELEMENT: + return element.namespaceURI === HTML_NAMESPACE ? element.localName.toLowerCase() === name.toLowerCase() : element.localName === name; - case MatcherType.Id: + case MATCHER_ID: return element.getAttribute('id') === name; - case MatcherType.Class: + case MATCHER_CLASS: const classAttr = element.getAttribute('class'); if (!classAttr) return false; return classAttr.split(/\s+/).includes(name); - case MatcherType.Attribute: + case MATCHER_ATTRIBUTE: return value == null ? element.hasAttribute(name) : element.getAttribute(name) === value; - case MatcherType.Pseudo: + case MATCHER_PSEUDO: switch (name) { default: throw Error(`Pseudo :${name} not implemented`); } - case MatcherType.Function: + case MATCHER_FUNCTION: switch (name) { case 'has': return matchesSelector(element, value || ''); diff --git a/packages/polyfill/source/serialization.ts b/packages/polyfill/source/serialization.ts index 16bfde89..45bf55ad 100644 --- a/packages/polyfill/source/serialization.ts +++ b/packages/polyfill/source/serialization.ts @@ -5,7 +5,9 @@ import { NAME, NEXT, VALUE, - NodeType, + NODE_TYPE_COMMENT, + NODE_TYPE_ELEMENT, + NODE_TYPE_TEXT, } from './constants.ts'; import type {Node} from './Node.ts'; import type {Text} from './Text.ts'; @@ -71,7 +73,7 @@ export function serializeChildren(parentNode: ParentNode) { export function serializeNode(node: Node) { switch (node.nodeType) { - case NodeType.ELEMENT_NODE: { + case NODE_TYPE_ELEMENT: { const el = node as Element; let out = `<${el[NAME]}`; let attr = el[ATTRIBUTES]?.[CHILD]; @@ -94,14 +96,14 @@ export function serializeNode(node: Node) { out += ``; return out; } - case NodeType.TEXT_NODE: { + case NODE_TYPE_TEXT: { const text = node as Text; return text[DATA].replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); } - case NodeType.COMMENT_NODE: { + case NODE_TYPE_COMMENT: { const text = node as Comment; return ``; } diff --git a/packages/polyfill/source/shared.ts b/packages/polyfill/source/shared.ts index 65396b0c..6a334119 100644 --- a/packages/polyfill/source/shared.ts +++ b/packages/polyfill/source/shared.ts @@ -2,7 +2,10 @@ import { DATA, OWNER_DOCUMENT, ATTRIBUTES, - NodeType, + NODE_TYPE_COMMENT, + NODE_TYPE_DOCUMENT_FRAGMENT, + NODE_TYPE_ELEMENT, + NODE_TYPE_TEXT, CHILD, NEXT, } from './constants.ts'; @@ -14,26 +17,32 @@ import type {ParentNode} from './ParentNode.ts'; import type {Element} from './Element.ts'; import type {CharacterData} from './CharacterData.ts'; import type {Text} from './Text.ts'; -import {querySelector, querySelectorAll, MatcherType} from './selectors.ts'; +import { + MATCHER_ELEMENT, + MATCHER_ID, + MATCHER_UNKNOWN, + querySelector, + querySelectorAll, +} from './selectors.ts'; export function isCharacterData(node: Node): node is CharacterData { return DATA in node; } export function isTextNode(node: Node): node is Text { - return node.nodeType === NodeType.TEXT_NODE; + return node.nodeType === NODE_TYPE_TEXT; } export function isCommentNode(node: Node): node is Comment { - return node.nodeType === NodeType.COMMENT_NODE; + return node.nodeType === NODE_TYPE_COMMENT; } export function isElementNode(node: Node): node is Element { - return node.nodeType === NodeType.ELEMENT_NODE; + return node.nodeType === NODE_TYPE_ELEMENT; } export function isDocumentFragmentNode(node: Node): node is DocumentFragment { - return node.nodeType === NodeType.DOCUMENT_FRAGMENT_NODE; + return node.nodeType === NODE_TYPE_DOCUMENT_FRAGMENT; } export function isParentNode(node: Node): node is ParentNode { @@ -91,7 +100,7 @@ export function getElementById(within: ParentNode, elementId: string) { const id = String(elementId); if (id === '') return null; - return querySelector(within, [{type: MatcherType.Id, name: id}]); + return querySelector(within, [{type: MATCHER_ID, name: id}]); } export function getElementsByTagName( @@ -101,7 +110,7 @@ export function getElementsByTagName( const name = String(qualifiedName); return querySelectorAll(within, [ - {type: name === '*' ? MatcherType.Unknown : MatcherType.Element, name}, + {type: name === '*' ? MATCHER_UNKNOWN : MATCHER_ELEMENT, name}, ]); } diff --git a/packages/polyfill/source/tests/constants.test.ts b/packages/polyfill/source/tests/constants.test.ts new file mode 100644 index 00000000..500a08bf --- /dev/null +++ b/packages/polyfill/source/tests/constants.test.ts @@ -0,0 +1,46 @@ +import {describe, expect, it} from 'vitest'; + +import {Window} from '../index.ts'; + +describe('DOM constants', () => { + it('exposes standard event phase constants', () => { + const {Event} = new Window(); + + expect(Event.NONE).toBe(0); + expect(Event.CAPTURING_PHASE).toBe(1); + expect(Event.AT_TARGET).toBe(2); + expect(Event.BUBBLING_PHASE).toBe(3); + }); + + it('initializes the event type', () => { + const {Event} = new Window(); + const event = new Event('click'); + + expect(event.type).toBe('click'); + }); + + it('uses standard node type values', () => { + const {document} = new Window(); + const element = document.createElement('div'); + element.setAttribute('example', 'value'); + + expect(element.nodeType).toBe(1); + expect(element.attributes.item(0)?.nodeType).toBe(2); + expect(document.createTextNode('').nodeType).toBe(3); + expect(document.createComment('').nodeType).toBe(8); + expect(document.nodeType).toBe(9); + expect(document.createDocumentFragment().nodeType).toBe(11); + }); + + it('uses standard namespace values', () => { + const {document} = new Window(); + + expect(document.createElement('div').namespaceURI).toBe( + 'http://www.w3.org/1999/xhtml', + ); + expect( + document.createElementNS('http://www.w3.org/2000/svg', 'svg') + .namespaceURI, + ).toBe('http://www.w3.org/2000/svg'); + }); +}); diff --git a/packages/polyfill/source/tests/get-elements-by-tag-name.test.ts b/packages/polyfill/source/tests/get-elements-by-tag-name.test.ts index 704d191b..2d8086af 100644 --- a/packages/polyfill/source/tests/get-elements-by-tag-name.test.ts +++ b/packages/polyfill/source/tests/get-elements-by-tag-name.test.ts @@ -1,4 +1,4 @@ -import {NamespaceURI} from '../constants.ts'; +import {SVG_NAMESPACE} from '../constants.ts'; import {Window} from '../index.ts'; import {beforeEach, describe, expect, it} from 'vitest'; @@ -26,11 +26,8 @@ describe('getElementsByTagName', () => { }); it('matches non-HTML tag names case-sensitively', () => { - const svg = document.createElementNS(NamespaceURI.SVG, 'svg'); - const gradient = document.createElementNS( - NamespaceURI.SVG, - 'linearGradient', - ); + const svg = document.createElementNS(SVG_NAMESPACE, 'svg'); + const gradient = document.createElementNS(SVG_NAMESPACE, 'linearGradient'); svg.appendChild(gradient); document.body.appendChild(svg); diff --git a/packages/polyfill/source/tests/selectors.test.ts b/packages/polyfill/source/tests/selectors.test.ts index bd9e5ff7..51303b93 100644 --- a/packages/polyfill/source/tests/selectors.test.ts +++ b/packages/polyfill/source/tests/selectors.test.ts @@ -1,12 +1,22 @@ import {Window} from '../index.ts'; import { + MATCHER_CLASS, + MATCHER_ELEMENT, + MATCHER_ID, + MATCHER_UNKNOWN, parseSelector, querySelector, querySelectorAll, - MatcherType, } from '../selectors.ts'; import type {ParentNode} from '../ParentNode.ts'; +const MatcherType = { + Unknown: MATCHER_UNKNOWN, + Element: MATCHER_ELEMENT, + Id: MATCHER_ID, + Class: MATCHER_CLASS, +} as const; + import {describe, it, expect, beforeEach} from 'vitest'; describe('selector parsing and matching', () => {