Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/element-query-methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@remote-dom/polyfill': minor
---

Add `getElementById()` to `Document` and `DocumentFragment`, with reflected `Element.id` properties. Add `getElementsByTagName()` to `Document` and `Element`, supporting HTML, non-HTML, and wildcard descendant searches. `querySelector` and `querySelectorAll` accept a pre-parsed `Matcher[]` in addition to string selectors, with `MatcherType`, `Combinator`, `Matcher`, and `Part` exported from `selectors.ts`; `getElementById` and `getElementsByTagName` delegate to this shared selector engine instead of independent tree-walk implementations.

Fixed `insertBefore()` leaving the previous sibling pointing at the reference node when inserting before a middle child, causing `NEXT` traversals (including `getElementById`) to skip the inserted subtree even though `childNodes` contained it, and return the inserted child as required by the DOM specification. Fixed `appendChild()` to return the appended child and `NodeList.item()` to return `null` for out-of-range indexes. Fixed case-insensitive HTML tag-name matching in the selector engine so `querySelector('DIV')` now matches `<div>` per the CSS spec. Fixed CSS-escaping issues so `getElementById` matches ids containing special characters (`.`, `:`, `#`, etc.) literally instead of treating them as selector syntax.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages/wpt-runner/capabilities.tsv whitespace=-blank-at-eol
16 changes: 16 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,19 @@ jobs:
name: playwright-report
path: playwright-report/
retention-days: 30

wpt:
name: Web Platform Tests 🌐
runs-on: ubuntu-22.04
timeout-minutes: 15
env:
WPT_CACHE_DIR: ${{ github.workspace }}/.cache/wpt
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: ./.github/workflows/actions/prepare
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: .cache/wpt
key: ${{ runner.os }}-wpt-${{ hashFiles('packages/wpt-runner/wpt.lock.json') }}
- run: pnpm exec playwright install --with-deps chromium
- run: pnpm run test:wpt
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
build/
node_modules/
/.cache/wpt/
.DS_STORE
packages/*/bin/
*.log
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
build/
node_modules/
.cache/wpt/
pnpm-lock.yaml
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
"format": "prettier --write --cache .",
"lint": "prettier --check --cache .",
"test": "vitest",
"test:wpt": "pnpm --filter @remote-dom/wpt-runner wpt",
"type-check": "tsc --build --pretty",
"wpt:dev": "pnpm --filter @remote-dom/wpt-runner dev",
"wpt:format": "pnpm --filter @remote-dom/wpt-runner format:capabilities",
"wpt:prepare": "pnpm --filter @remote-dom/wpt-runner wpt:prepare",
"version-bump": "changeset version && pnpm install --no-frozen-lockfile",
"deploy": "pnpm publish -r",
"example:getting-started": "pnpm run --filter example-getting-started start",
Expand Down
15 changes: 14 additions & 1 deletion packages/polyfill/source/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import {Text} from './Text.ts';
import {Comment} from './Comment.ts';
import {DocumentFragment} from './DocumentFragment.ts';
import {HTMLTemplateElement} from './HTMLTemplateElement.ts';
import {isParentNode, cloneNode} from './shared.ts';
import {
isParentNode,
cloneNode,
getElementById as findElementById,
getElementsByTagName as findElementsByTagName,
} from './shared.ts';
import {HTMLBodyElement} from './HTMLBodyElement.ts';
import {HTMLHeadElement} from './HTMLHeadElement.ts';
import {HTMLHtmlElement} from './HTMLHtmlElement.ts';
Expand Down Expand Up @@ -70,6 +75,14 @@ export class Document extends ParentNode {
return new Event('');
}

getElementById(elementId: string) {
return findElementById(this, elementId);
}

getElementsByTagName(qualifiedName: string) {
return findElementsByTagName(this, qualifiedName);
}

importNode(node: Node, deep?: boolean) {
return cloneNode(node, deep, this);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/polyfill/source/DocumentFragment.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import {NAME, OWNER_DOCUMENT, 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;
[NAME] = '#document-fragment';
[OWNER_DOCUMENT] = (typeof window !== 'undefined'
? window.document
: null) as any;

getElementById(elementId: string) {
return findElementById(this, elementId);
}
}
13 changes: 13 additions & 0 deletions packages/polyfill/source/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {ParentNode} from './ParentNode.ts';
import {NamedNodeMap} from './NamedNodeMap.ts';
import {Attr} from './Attr.ts';
import {serializeNode, serializeChildren, parseHtml} from './serialization.ts';
import {getElementsByTagName as findElementsByTagName} from './shared.ts';

export class Element extends ParentNode {
static readonly observedAttributes?: string[];
Expand All @@ -22,6 +23,14 @@ export class Element extends ParentNode {

[anyProperty: string]: any;

get id() {
return this.getAttribute('id') ?? '';
}

set id(id: string) {
this.setAttribute('id', String(id));
}

get slot() {
return this.getAttribute('slot') ?? '';
}
Expand All @@ -47,6 +56,10 @@ export class Element extends ParentNode {
return [...this.attributes].map((attr) => attr.name);
}

getElementsByTagName(qualifiedName: string) {
return findElementsByTagName(this, qualifiedName);
}

get firstElementChild() {
return this.children[0] ?? null;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/polyfill/source/NodeList.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export class NodeList extends Array {
item(index: number) {
return this[index];
return this[index] ?? null;
}
}
12 changes: 8 additions & 4 deletions packages/polyfill/source/ParentNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ export class ParentNode extends ChildNode {
readonly childNodes = new NodeList();
readonly children = new NodeList();

appendChild(child: Node) {
appendChild<T extends Node>(child: T) {
this.insertInto(child, null);
return child;
}

insertBefore(child: Node, ref?: Node | null) {
insertBefore<T extends Node>(child: T, ref?: Node | null) {
this.insertInto(child, ref || null);
return child;
}

append(...nodes: (Node | string)[]) {
Expand Down Expand Up @@ -121,9 +123,11 @@ export class ParentNode extends ChildNode {
if (before.parentNode !== this) {
throw Error('reference node is not a child of this parent');
}
const previous = before[PREV];
child[NEXT] = before;
child[PREV] = before[PREV];
if (before[PREV] === null) this[CHILD] = child;
child[PREV] = previous;
if (previous) previous[NEXT] = child;
else this[CHILD] = child;
before[PREV] = child;
} else {
child[NEXT] = null;
Expand Down
36 changes: 25 additions & 11 deletions packages/polyfill/source/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import {CHILD, NEXT, PARENT, PREV} from './constants.ts';
import {CHILD, NEXT, PARENT, PREV, NamespaceURI} 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';

const enum Combinator {
export const enum Combinator {
Descendant,
Child,
Sibling,
Adjacent,
Inner,
}

const enum MatcherType {
export const enum MatcherType {
Unknown,
Element,
Id,
Expand All @@ -23,21 +23,27 @@ const enum MatcherType {
Function,
}

interface Part {
export interface Part {
combinator: Combinator;
matchers: Matcher[];
}

interface Matcher {
export interface Matcher {
type: MatcherType;
name: string;
value?: string;
}

const ELEMENT_SELECTOR_TEST = /[a-z]/;
const ELEMENT_SELECTOR_TEST = /[a-zA-Z]/;

export function querySelector(within: ParentNode, selector: string) {
const parts = parseSelector(selector);
export function querySelector(
within: ParentNode,
selector: string | Matcher[],
): Element | null {
const parts =
typeof selector === 'string'
? parseSelector(selector)
: [{combinator: Combinator.Inner, matchers: selector}];
let result: Element | null = null;

const child = within[CHILD];
Expand All @@ -50,8 +56,14 @@ export function querySelector(within: ParentNode, selector: string) {
return result;
}

export function querySelectorAll(within: ParentNode, selector: string) {
const parts = parseSelector(selector);
export function querySelectorAll(
within: ParentNode,
selector: string | Matcher[],
): Element[] {
const parts =
typeof selector === 'string'
? parseSelector(selector)
: [{combinator: Combinator.Inner, matchers: selector}];
const results: Element[] = [];

const child = within[CHILD];
Expand Down Expand Up @@ -239,7 +251,9 @@ function matchesSelectorMatcher(
case MatcherType.Unknown:
return name === '*'; // Universal selector
case MatcherType.Element:
return element.localName === name;
return element.namespaceURI === NamespaceURI.XHTML
? element.localName.toLowerCase() === name.toLowerCase()
: element.localName === name;
case MatcherType.Id:
return element.getAttribute('id') === name;
case MatcherType.Class:
Expand Down
24 changes: 24 additions & 0 deletions packages/polyfill/source/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ 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 {NodeList} from './NodeList.ts';
import {querySelector, querySelectorAll, MatcherType} from './selectors.ts';

export function isCharacterData(node: Node): node is CharacterData {
return DATA in node;
Expand Down Expand Up @@ -86,6 +88,28 @@ export function cloneNode(
}
}

export function getElementById(within: ParentNode, elementId: string) {
const id = String(elementId);
if (id === '') return null;

return querySelector(within, [{type: MatcherType.Id, name: id}]);
}

export function getElementsByTagName(
within: ParentNode,
qualifiedName: string,
) {
const name = String(qualifiedName);

const results = querySelectorAll(within, [
{type: name === '*' ? MatcherType.Unknown : MatcherType.Element, name},
]);

const matches = new NodeList();
matches.push(...results);
return matches;
}

export function descendants(node: Node) {
const nodes: Node[] = [];
const walk = (node: Node) => {
Expand Down
Loading
Loading