Skip to content

Commit 326f3ad

Browse files
committed
Implement Document#getElementsByTagName(NS)
1 parent a3fa27e commit 326f3ad

File tree

5 files changed

+305
-1
lines changed

5 files changed

+305
-1
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ This library omits properties and methods that exist mainly for web compatibilit
9090

9191
The following features are missing simply because I have not yet had a need for them. If you do need one, feel free to create a feature request issue or even submit a pull request.
9292

93-
- Older non-CSS, non-HTML query methods (`getElementsByTagName` / `getElementsByTagNameNS` on `Document`).
9493
- Iteration helpers (`NodeIterator` / `TreeWalker` / `NodeFilter`, and the `createNodeIterator` / `createTreeWalker` methods on `Document`).
9594
- DOM-modifying methods on Range (`deleteContents` / `extractContents` / `cloneContents` / `insertNode` / `surroundContents`).
9695
- `attributeFilter` for mutation observers.

api/slimdom.api.json

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2619,6 +2619,121 @@
26192619
},
26202620
"isStatic": false
26212621
},
2622+
{
2623+
"kind": "Method",
2624+
"canonicalReference": "slimdom!Document#getElementsByTagName:member(1)",
2625+
"docComment": "/**\n * Returns the list of elements with the given qualified name.\n *\n * @param qualifiedName - Qualified name of the elements to collect.\n *\n * @returns The list of elements with matching qualified name.\n */\n",
2626+
"excerptTokens": [
2627+
{
2628+
"kind": "Content",
2629+
"text": "getElementsByTagName(qualifiedName: "
2630+
},
2631+
{
2632+
"kind": "Content",
2633+
"text": "string"
2634+
},
2635+
{
2636+
"kind": "Content",
2637+
"text": "): "
2638+
},
2639+
{
2640+
"kind": "Reference",
2641+
"text": "Element",
2642+
"canonicalReference": "slimdom!default:class"
2643+
},
2644+
{
2645+
"kind": "Content",
2646+
"text": "[]"
2647+
},
2648+
{
2649+
"kind": "Content",
2650+
"text": ";"
2651+
}
2652+
],
2653+
"isOptional": false,
2654+
"isStatic": false,
2655+
"returnTypeTokenRange": {
2656+
"startIndex": 3,
2657+
"endIndex": 5
2658+
},
2659+
"releaseTag": "Public",
2660+
"overloadIndex": 1,
2661+
"parameters": [
2662+
{
2663+
"parameterName": "qualifiedName",
2664+
"parameterTypeTokenRange": {
2665+
"startIndex": 1,
2666+
"endIndex": 2
2667+
}
2668+
}
2669+
],
2670+
"name": "getElementsByTagName"
2671+
},
2672+
{
2673+
"kind": "Method",
2674+
"canonicalReference": "slimdom!Document#getElementsByTagNameNS:member(1)",
2675+
"docComment": "/**\n * Returns the list of elements with the given namespace and local name.\n *\n * @param namespace - Namespace URI of the elements to collect.\n *\n * @param localName - Local name of the elements to collect\n *\n * @returns The list of elements with matching namespace and local name.\n */\n",
2676+
"excerptTokens": [
2677+
{
2678+
"kind": "Content",
2679+
"text": "getElementsByTagNameNS(namespace: "
2680+
},
2681+
{
2682+
"kind": "Content",
2683+
"text": "string | null"
2684+
},
2685+
{
2686+
"kind": "Content",
2687+
"text": ", localName: "
2688+
},
2689+
{
2690+
"kind": "Content",
2691+
"text": "string"
2692+
},
2693+
{
2694+
"kind": "Content",
2695+
"text": "): "
2696+
},
2697+
{
2698+
"kind": "Reference",
2699+
"text": "Element",
2700+
"canonicalReference": "slimdom!default:class"
2701+
},
2702+
{
2703+
"kind": "Content",
2704+
"text": "[]"
2705+
},
2706+
{
2707+
"kind": "Content",
2708+
"text": ";"
2709+
}
2710+
],
2711+
"isOptional": false,
2712+
"isStatic": false,
2713+
"returnTypeTokenRange": {
2714+
"startIndex": 5,
2715+
"endIndex": 7
2716+
},
2717+
"releaseTag": "Public",
2718+
"overloadIndex": 1,
2719+
"parameters": [
2720+
{
2721+
"parameterName": "namespace",
2722+
"parameterTypeTokenRange": {
2723+
"startIndex": 1,
2724+
"endIndex": 2
2725+
}
2726+
},
2727+
{
2728+
"parameterName": "localName",
2729+
"parameterTypeTokenRange": {
2730+
"startIndex": 3,
2731+
"endIndex": 4
2732+
}
2733+
}
2734+
],
2735+
"name": "getElementsByTagNameNS"
2736+
},
26222737
{
26232738
"kind": "Property",
26242739
"canonicalReference": "slimdom!Document#implementation:member",

api/slimdom.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ export class Document extends Node implements NonElementParentNode, ParentNode {
126126
documentElement: Element | null;
127127
// (undocumented)
128128
firstElementChild: Element | null;
129+
getElementsByTagName(qualifiedName: string): Element[];
130+
getElementsByTagNameNS(namespace: string | null, localName: string): Element[];
129131
readonly implementation: DOMImplementation;
130132
importNode<TNode extends Node>(node: TNode, deep?: boolean): TNode;
131133
// (undocumented)

src/Document.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { adoptNode, appendNodes, prependNodes } from './util/mutationAlgorithms'
2222
import { NodeType, isNodeOfType } from './util/NodeType';
2323
import { matchesNameProduction, validateAndExtract } from './util/namespaceHelpers';
2424
import { asNullableString, asObject } from './util/typeHelpers';
25+
import { forEachInclusiveDescendant } from './util/treeHelpers';
2526

2627
/**
2728
* 3.5. Interface Document
@@ -134,6 +135,92 @@ export default class Document extends Node implements NonElementParentNode, Pare
134135
super();
135136
}
136137

138+
/**
139+
* Returns the list of elements with the given qualified name.
140+
*
141+
* @param qualifiedName - Qualified name of the elements to collect.
142+
*
143+
* @returns The list of elements with matching qualified name.
144+
*/
145+
public getElementsByTagName(qualifiedName: string): Element[] {
146+
expectArity(arguments, 1);
147+
qualifiedName = String(qualifiedName);
148+
149+
const elements: Element[] = [];
150+
forEachInclusiveDescendant(this, (node) => {
151+
if (node.nodeType !== NodeType.ELEMENT_NODE) {
152+
return;
153+
}
154+
const element = node as Element;
155+
156+
if (
157+
// 1. If qualifiedName is "*" (U+002A), return a HTMLCollection rooted at root,
158+
// whose filter matches only descendant elements.
159+
qualifiedName === '\u002a' ||
160+
// 2. Otherwise, if root’s node document is an HTML document,
161+
// return a HTMLCollection rooted at root, whose filter matches the following
162+
// descendant elements:
163+
// - Whose namespace is the HTML namespace and whose qualified name is qualifiedName,
164+
// in ASCII lowercase.
165+
// - Whose namespace is not the HTML namespace and whose qualified name is
166+
// qualifiedName.
167+
// (html documents not implemented)
168+
169+
// 3. Otherwise, return a HTMLCollection rooted at root, whose filter matches
170+
// descendant elements whose qualified name is qualifiedName.
171+
element.nodeName === qualifiedName
172+
) {
173+
elements.push(element);
174+
}
175+
});
176+
177+
return elements;
178+
}
179+
180+
/**
181+
* Returns the list of elements with the given namespace and local name.
182+
*
183+
* @param namespace - Namespace URI of the elements to collect.
184+
* @param localName - Local name of the elements to collect
185+
*
186+
* @returns The list of elements with matching namespace and local name.
187+
*/
188+
public getElementsByTagNameNS(namespace: string | null, localName: string): Element[] {
189+
expectArity(arguments, 2);
190+
namespace = asNullableString(namespace);
191+
localName = String(localName);
192+
193+
// 1. If namespace is the empty string, set it to null.
194+
if (namespace === '') {
195+
namespace = null;
196+
}
197+
198+
const elements: Element[] = [];
199+
forEachInclusiveDescendant(this, (node) => {
200+
if (node.nodeType !== NodeType.ELEMENT_NODE) {
201+
return;
202+
}
203+
const element = node as Element;
204+
205+
if (
206+
// 2. If both namespace and localName are "*" (U+002A), return a HTMLCollection
207+
// rooted at root, whose filter matches descendant elements.
208+
// 3. Otherwise, if namespace is "*" (U+002A), return a HTMLCollection rooted at
209+
// root, whose filter matches descendant elements whose local name is localName.
210+
// 4. Otherwise, if localName is "*" (U+002A), return a HTMLCollection rooted at
211+
// root, whose filter matches descendant elements whose namespace is namespace.
212+
// 5. Otherwise, return a HTMLCollection rooted at root, whose filter matches
213+
// descendant elements whose namespace is namespace and local name is localName.
214+
(namespace === '\u002a' || element.namespaceURI === namespace) &&
215+
(localName === '\u002a' || element.localName === localName)
216+
) {
217+
elements.push(element);
218+
}
219+
});
220+
221+
return elements;
222+
}
223+
137224
/**
138225
* Creates a new element in the null namespace.
139226
*

test/Document.tests.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,107 @@ describe('Document', () => {
119119
});
120120
});
121121

122+
describe('.getElementsByTagName', () => {
123+
it('can find all descendants matching the given qualifiedName', () => {
124+
const root = document.appendChild(document.createElement('root'));
125+
const e1 = root.appendChild(document.createElementNS('namespace', 'pre:elem'));
126+
const e2 = root.appendChild(document.createElementNS('namespace', 'pre:elem'));
127+
const other = root.appendChild(document.createElementNS(null, 'other'));
128+
const e3 = other.appendChild(document.createElementNS('otherns', 'pre:elem'));
129+
const e4 = other.appendChild(document.createElementNS('otherns', 'pre:elem'));
130+
const e5 = other.appendChild(document.createElementNS('otherns', 'x:elem'));
131+
const e6 = other.appendChild(document.createElementNS('otherns', 'x:elem'));
132+
133+
expect(document.getElementsByTagName('root')).toEqual([root]);
134+
expect(document.getElementsByTagName('pre:elem')).toEqual([e1, e2, e3, e4]);
135+
expect(document.getElementsByTagName('other')).toEqual([other]);
136+
expect(document.getElementsByTagName('x:elem')).toEqual([e5, e6]);
137+
});
138+
139+
it('can find all descendant elements using the special name "*"', () => {
140+
const root = document.appendChild(document.createElement('root'));
141+
const e1 = root.appendChild(document.createElementNS('namespace', 'pre:elem'));
142+
const e2 = root.appendChild(document.createElementNS('namespace', 'pre:elem'));
143+
const other = root.appendChild(document.createElementNS(null, 'other'));
144+
const e3 = other.appendChild(document.createElementNS('otherns', 'pre:elem'));
145+
const e4 = other.appendChild(document.createElementNS('otherns', 'pre:elem'));
146+
const e5 = other.appendChild(document.createElementNS('otherns', 'x:elem'));
147+
const e6 = other.appendChild(document.createElementNS('otherns', 'x:elem'));
148+
149+
expect(document.getElementsByTagName('*')).toEqual([
150+
root,
151+
e1,
152+
e2,
153+
other,
154+
e3,
155+
e4,
156+
e5,
157+
e6,
158+
]);
159+
});
160+
});
161+
162+
describe('.getElementsByTagNameNS', () => {
163+
it('can find all descendants matching the given namespace and localName', () => {
164+
const root = document.appendChild(document.createElement('root'));
165+
const e1 = root.appendChild(document.createElementNS('namespace', 'pre:elem'));
166+
const e2 = root.appendChild(document.createElementNS('namespace', 'pre:elem'));
167+
const other = root.appendChild(document.createElementNS(null, 'other'));
168+
const e3 = other.appendChild(document.createElementNS('otherns', 'pre:elem'));
169+
const e4 = other.appendChild(document.createElementNS('otherns', 'pre:elem'));
170+
const e5 = other.appendChild(document.createElementNS('otherns', 'x:elem'));
171+
const e6 = other.appendChild(document.createElementNS('otherns', 'x:elem'));
172+
173+
expect(document.getElementsByTagNameNS(null, 'root')).toEqual([root]);
174+
expect(document.getElementsByTagNameNS('zoinks', 'root')).toEqual([]);
175+
expect(document.getElementsByTagNameNS('namespace', 'elem')).toEqual([e1, e2]);
176+
expect(document.getElementsByTagNameNS('otherns', 'elem')).toEqual([e3, e4, e5, e6]);
177+
expect(document.getElementsByTagNameNS('', 'other')).toEqual([other]);
178+
});
179+
180+
it('can find all descendant elements using the special namespace "*"', () => {
181+
const root = document.appendChild(document.createElement('root'));
182+
const e1 = root.appendChild(document.createElementNS('namespace', 'pre:elem'));
183+
const e2 = root.appendChild(document.createElementNS('namespace', 'pre:elem'));
184+
const other = root.appendChild(document.createElementNS(null, 'other'));
185+
const e3 = other.appendChild(document.createElementNS('otherns', 'pre:elem'));
186+
const e4 = other.appendChild(document.createElementNS('otherns', 'pre:elem'));
187+
const e5 = other.appendChild(document.createElementNS('otherns', 'x:elem'));
188+
const e6 = other.appendChild(document.createElementNS('otherns', 'x:elem'));
189+
190+
expect(document.getElementsByTagNameNS('*', 'root')).toEqual([root]);
191+
expect(document.getElementsByTagNameNS('*', 'elem')).toEqual([e1, e2, e3, e4, e5, e6]);
192+
expect(document.getElementsByTagNameNS('*', 'other')).toEqual([other]);
193+
});
194+
195+
it('can find all descendant elements using the special localName "*"', () => {
196+
const root = document.appendChild(document.createElement('root'));
197+
const e1 = root.appendChild(document.createElementNS('namespace', 'pre:elem'));
198+
const e2 = root.appendChild(document.createElementNS('namespace', 'pre:elem'));
199+
const other = root.appendChild(document.createElementNS(null, 'other'));
200+
const e3 = other.appendChild(document.createElementNS('otherns', 'pre:elem'));
201+
const e4 = other.appendChild(document.createElementNS('otherns', 'pre:elem'));
202+
const e5 = other.appendChild(document.createElementNS('otherns', 'x:elem'));
203+
const e6 = other.appendChild(document.createElementNS('otherns', 'x:elem'));
204+
205+
expect(document.getElementsByTagNameNS(null, '*')).toEqual([root, other]);
206+
expect(document.getElementsByTagNameNS('', '*')).toEqual([root, other]);
207+
expect(document.getElementsByTagNameNS('zoinks', '*')).toEqual([]);
208+
expect(document.getElementsByTagNameNS('namespace', '*')).toEqual([e1, e2]);
209+
expect(document.getElementsByTagNameNS('otherns', '*')).toEqual([e3, e4, e5, e6]);
210+
expect(document.getElementsByTagNameNS('*', '*')).toEqual([
211+
root,
212+
e1,
213+
e2,
214+
other,
215+
e3,
216+
e4,
217+
e5,
218+
e6,
219+
]);
220+
});
221+
});
222+
122223
describe('.cloneNode', () => {
123224
beforeEach(() => {
124225
document.appendChild(document.createElement('root'));

0 commit comments

Comments
 (0)