From 860279ccbe3061dc03a674d9ebcba491b145f761 Mon Sep 17 00:00:00 2001 From: Mathias Stang Date: Thu, 5 Sep 2024 11:39:35 +0200 Subject: [PATCH 1/4] fix: add missing type definitions (#721) Adds types for properties that are exported, but are missing in index.d.ts: - `Document.domElement` - `Element.tagName` Fixes #720 --- examples/typescript-node-es6/src/index.ts | 3 +++ index.d.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/examples/typescript-node-es6/src/index.ts b/examples/typescript-node-es6/src/index.ts index b497e4e6b..b4e3a03a1 100644 --- a/examples/typescript-node-es6/src/index.ts +++ b/examples/typescript-node-es6/src/index.ts @@ -23,6 +23,7 @@ import { ParseError, Text, XMLSerializer, + Element, } from '@xmldom/xmldom'; const failedAssertions: Error[] = []; @@ -85,6 +86,8 @@ assert(doc1.DOCUMENT_POSITION_CONTAINS, 8); assert(doc1 instanceof Node, true); assert(doc1 instanceof Document, true); assert(doc1.childNodes instanceof NodeList, true); +assert(doc1.documentElement instanceof Element, true); +assert(doc1.documentElement?.tagName, 'qualifiedName'); assert(doc1.getElementsByClassName('hide') instanceof LiveNodeList, true); const attr = doc1.createAttribute('attr'); diff --git a/index.d.ts b/index.d.ts index 2b931a0fc..4fb91bb2c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -770,6 +770,13 @@ declare module '@xmldom/xmldom' { /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/attributes) */ readonly attributes: NamedNodeMap; + /** + * Returns the HTML-uppercased qualified name. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/tagName) + */ + readonly tagName: string; + /** * Returns element's first attribute whose qualified name is qualifiedName, and null if there * is no such attribute otherwise. @@ -1067,6 +1074,12 @@ declare module '@xmldom/xmldom' { readonly nodeName: '#document'; readonly nodeType: typeof Node.DOCUMENT_NODE; readonly doctype: DocumentType | null; + /** + * Gets a reference to the root node of the document. + * + * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/documentElement) + */ + readonly documentElement: Element | null; /** * Creates an attribute object with a specified name. From 5e692844cfb85bb36ed9d5186396c4108297662e Mon Sep 17 00:00:00 2001 From: Christian Bewernitz Date: Thu, 5 Sep 2024 14:11:00 +0200 Subject: [PATCH 2/4] feat: add Element.getElementsByClassName (#722) the method was previously implemented on the Document interface. https://developer.mozilla.org/en-US/docs/Web/API/Element/getElementsByClassName supersedes #584 fixes #582 --- index.d.ts | 36 ++++++++++------ lib/dom.js | 91 ++++++++++++++++++++------------------- test/dom/document.test.js | 2 +- test/dom/element.test.js | 91 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+), 60 deletions(-) diff --git a/index.d.ts b/index.d.ts index 4fb91bb2c..643a35aa2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -804,7 +804,22 @@ declare module '@xmldom/xmldom' { namespace: string | null, localName: string ): Attr | null; - /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Element/getBoundingClientRect) */ + /** + * Returns a LiveNodeList of all child elements which have **all** of the given class + * name(s). + * + * Returns an empty list if `classNames` is an empty string or only contains HTML white space + * characters. + * + * Warning: This returns a live LiveNodeList. + * Changes in the DOM will reflect in the array as the changes occur. + * If an element selected by this array no longer qualifies for the selector, + * it will automatically be removed. Be aware of this for iteration purposes. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getElementsByClassName + * @see https://dom.spec.whatwg.org/#concept-getelementsbyclassname + */ + getElementsByClassName(classNames: string): LiveNodeList; /** * Returns a LiveNodeList of elements with the given qualifiedName. @@ -1118,7 +1133,6 @@ declare module '@xmldom/xmldom' { */ createDocumentFragment(): DocumentFragment; - createElement(tagName: string): Element; /** @@ -1141,10 +1155,7 @@ declare module '@xmldom/xmldom' { * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/Document/createElementNS) */ - createElementNS( - namespace: string | null, - qualifiedName: string, - ): Element; + createElementNS(namespace: string | null, qualifiedName: string): Element; /** * Returns a ProcessingInstruction node whose target is target and data is data. If target does @@ -1178,20 +1189,17 @@ declare module '@xmldom/xmldom' { getElementById(elementId: string): Element | null; /** - * The `getElementsByClassName` method of `Document` interface returns an array-like object - * of all child elements which have **all** of the given class name(s). + * Returns a LiveNodeList of all child elements which have **all** of the given class + * name(s). * - * Returns an empty list if `classeNames` is an empty string or only contains HTML white - * space characters. + * Returns an empty list if `classNames` is an empty string or only contains HTML white space + * characters. * - * Warning: This is a live LiveNodeList. + * Warning: This returns a live LiveNodeList. * Changes in the DOM will reflect in the array as the changes occur. * If an element selected by this array no longer qualifies for the selector, * it will automatically be removed. Be aware of this for iteration purposes. * - * @param {string} classNames - * Is a string representing the class name(s) to match; multiple class names are separated by - * (ASCII-)whitespace. * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName * @see https://dom.spec.whatwg.org/#concept-getelementsbyclassname */ diff --git a/lib/dom.js b/lib/dom.js index d9a42f79c..135b70cb3 100644 --- a/lib/dom.js +++ b/lib/dom.js @@ -2003,51 +2003,6 @@ Document.prototype = { return rtv; }, - /** - * The `getElementsByClassName` method of `Document` interface returns an array-like object of - * all child elements which have **all** of the given class name(s). - * - * Returns an empty list if `classeNames` is an empty string or only contains HTML white space - * characters. - * - * Warning: This is a live LiveNodeList. - * Changes in the DOM will reflect in the array as the changes occur. - * If an element selected by this array no longer qualifies for the selector, - * it will automatically be removed. Be aware of this for iteration purposes. - * - * @param {string} classNames - * Is a string representing the class name(s) to match; multiple class names are separated by - * (ASCII-)whitespace. - * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName - * @see https://dom.spec.whatwg.org/#concept-getelementsbyclassname - */ - getElementsByClassName: function (classNames) { - var classNamesSet = toOrderedSet(classNames); - return new LiveNodeList(this, function (base) { - var ls = []; - if (classNamesSet.length > 0) { - _visitNode(base.documentElement, function (node) { - if (node !== base && node.nodeType === ELEMENT_NODE) { - var nodeClassNames = node.getAttribute('class'); - // can be null if the attribute does not exist - if (nodeClassNames) { - // before splitting and iterating just compare them for the most common case - var matches = classNames === nodeClassNames; - if (!matches) { - var nodeClassNamesSet = toOrderedSet(nodeClassNames); - matches = classNamesSet.every(arrayIncludes(nodeClassNamesSet)); - } - if (matches) { - ls.push(node); - } - } - } - }); - } - return ls; - }); - }, - /** * Creates a new `Element` that is owned by this `Document`. * In HTML Documents `localName` is the lower cased `tagName`, @@ -2302,6 +2257,51 @@ Element.prototype = { return this.attributes.getNamedItemNS(namespaceURI, localName); }, + /** + * Returns a LiveNodeList of all child elements which have **all** of the given class name(s). + * + * Returns an empty list if `classNames` is an empty string or only contains HTML white space + * characters. + * + * Warning: This returns a live LiveNodeList. + * Changes in the DOM will reflect in the array as the changes occur. + * If an element selected by this array no longer qualifies for the selector, + * it will automatically be removed. Be aware of this for iteration purposes. + * + * @param {string} classNames + * Is a string representing the class name(s) to match; multiple class names are separated by + * (ASCII-)whitespace. + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getElementsByClassName + * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName + * @see https://dom.spec.whatwg.org/#concept-getelementsbyclassname + */ + getElementsByClassName: function (classNames) { + var classNamesSet = toOrderedSet(classNames); + return new LiveNodeList(this, function (base) { + var ls = []; + if (classNamesSet.length > 0) { + _visitNode(base, function (node) { + if (node !== base && node.nodeType === ELEMENT_NODE) { + var nodeClassNames = node.getAttribute('class'); + // can be null if the attribute does not exist + if (nodeClassNames) { + // before splitting and iterating just compare them for the most common case + var matches = classNames === nodeClassNames; + if (!matches) { + var nodeClassNamesSet = toOrderedSet(nodeClassNames); + matches = classNamesSet.every(arrayIncludes(nodeClassNamesSet)); + } + if (matches) { + ls.push(node); + } + } + } + }); + } + return ls; + }); + }, + /** * Returns a LiveNodeList of elements with the given qualifiedName. * Searching for all descendants can be done by passing `*` as `qualifiedName`. @@ -2365,6 +2365,7 @@ Element.prototype = { }); }, }; +Document.prototype.getElementsByClassName = Element.prototype.getElementsByClassName; Document.prototype.getElementsByTagName = Element.prototype.getElementsByTagName; Document.prototype.getElementsByTagNameNS = Element.prototype.getElementsByTagNameNS; diff --git a/test/dom/document.test.js b/test/dom/document.test.js index 22d8f0284..07c5f0ded 100644 --- a/test/dom/document.test.js +++ b/test/dom/document.test.js @@ -92,7 +92,7 @@ describe('Document.prototype', () => { expect(doc.getElementsByClassName(' \f\n\r\t')).toHaveLength(0); }); - test('should return only the case insensitive matching names', () => { + test('should return only the case sensitive matching names', () => { const MIXED_CASES = ['AAA', 'AAa', 'AaA', 'aAA']; const doc = getTestParser().parser.parseFromString(INPUT(...MIXED_CASES), MIME_TYPE.XML_TEXT); diff --git a/test/dom/element.test.js b/test/dom/element.test.js index 6cd63b025..3d2b82814 100644 --- a/test/dom/element.test.js +++ b/test/dom/element.test.js @@ -1,6 +1,7 @@ 'use strict'; const { describe, expect, test } = require('@jest/globals'); +const { getTestParser } = require('../get-test-parser'); const { DOMParser, DOMImplementation, XMLSerializer } = require('../../lib'); const { MIME_TYPE, NAMESPACE } = require('../../lib/conventions'); const { Element, Node } = require('../../lib/dom'); @@ -128,6 +129,29 @@ describe('documentElement', () => { xit('self append failed', () => {}); }); +const INPUT = (first = '', second = '', third = '', fourth = '') => ` + + +

Lorem ipsum

+

Lorem ipsum

+

Lorem ipsum

+

Lorem ipsum

+ + +`; + +/** + * Whitespace that can be part of classnames. + * Some characters (like `\u2028`) will be normalized when parsing, + * but they can still be added to the dom after parsing. + * + * @see https://www.w3.org/TR/html52/infrastructure.html#set-of-space-separated-tokens + * @see {@link normalizeLineEndings} + * @see https://www.w3.org/TR/xml11/#sec-line-ends + */ +const NON_HTML_WHITESPACE = + '\v\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff'; + describe('Element', () => { const ATTR_MIXED_CASE = 'AttR'; const ATTR_LOWER_CASE = 'attr'; @@ -141,6 +165,73 @@ describe('Element', () => { const doc = new DOMImplementation().createDocument(null, 'xml'); expect(doc.documentElement.getAttribute('no')).toBeNull(); }); + describe('getElementsByClassName', () => { + test('should be able to resolve [] as a class name', () => { + const doc = getTestParser().parser.parseFromString(INPUT('[]'), MIME_TYPE.XML_TEXT); + const body = doc.getElementsByTagName('body')[0]; + expect(body.getElementsByClassName('[]')).toHaveLength(1); + }); + test('should be able to resolve [ as a class name', () => { + const doc = getTestParser().parser.parseFromString(INPUT('['), MIME_TYPE.XML_TEXT); + const body = doc.getElementsByTagName('body')[0]; + expect(body.getElementsByClassName('[')).toHaveLength(1); + }); + test('should be able to resolve multiple class names in a different order', () => { + const doc = getTestParser().parser.parseFromString(INPUT(), MIME_TYPE.XML_TEXT); + const body = doc.getElementsByTagName('body')[0]; + expect(body.getElementsByClassName('odd quote')).toHaveLength(2); + }); + test('should be able to resolve non html whitespace as classname', () => { + const doc = getTestParser().parser.parseFromString(INPUT(), MIME_TYPE.XML_TEXT); + const body = doc.getElementsByTagName('body')[0]; + const firstP = body.getElementsByTagName('p')[0]; + expect(firstP).toBeDefined(); + + firstP.setAttribute('class', firstP.getAttribute('class') + ' ' + NON_HTML_WHITESPACE); + + expect(body.getElementsByClassName(`quote ${NON_HTML_WHITESPACE}`)).toHaveLength(1); + }); + test('should not allow regular expression in argument', () => { + const search = '(((a||||)+)+)+'; + const matching = 'aaaaa'; + expect(new RegExp(search).test(matching)).toBe(true); + + const doc = getTestParser().parser.parseFromString(INPUT(search, matching, search), MIME_TYPE.XML_TEXT); + const body = doc.getElementsByTagName('body')[0]; + + expect(body.getElementsByClassName(search)).toHaveLength(2); + }); + test('should return an empty collection when no class names or are passed', () => { + const doc = getTestParser().parser.parseFromString(INPUT(), MIME_TYPE.XML_TEXT); + const body = doc.getElementsByTagName('body')[0]; + + expect(body.getElementsByClassName('')).toHaveLength(0); + }); + test('should return only children not the element itself', () => { + const doc = getTestParser().parser.parseFromString(INPUT(), MIME_TYPE.XML_TEXT); + const body = doc.getElementsByTagName('body')[0]; + body.setAttribute('class', 'quote'); + + expect(body.getElementsByClassName('quote')).toHaveLength(4); + }); + test('should return an empty collection when only spaces are passed', () => { + const doc = getTestParser().parser.parseFromString( + INPUT(' \f\n\r\t', ' \f\n\r\t', ' \f\n\r\t', ' \f\n\r\t'), + MIME_TYPE.XML_TEXT + ); + const body = doc.getElementsByTagName('body')[0]; + + expect(body.getElementsByClassName(' \f\n\r\t')).toHaveLength(0); + }); + test('should return only the case sensitive matching names', () => { + const MIXED_CASES = ['AAA', 'AAa', 'AaA', 'aAA']; + const doc = getTestParser().parser.parseFromString(INPUT(...MIXED_CASES), MIME_TYPE.XML_TEXT); + + MIXED_CASES.forEach((className) => { + expect(doc.getElementsByClassName(className)).toHaveLength(1); + }); + }); + }); describe('setAttribute', () => { test.each([null, NAMESPACE.HTML])('should set attribute as is in XML document with namespace %s', (ns) => { const doc = new DOMImplementation().createDocument(ns, 'xml'); From 9ec1fda8be7993a710004df845625872f418b8a4 Mon Sep 17 00:00:00 2001 From: Christian Bewernitz Date: Thu, 5 Sep 2024 14:28:02 +0200 Subject: [PATCH 3/4] docs: prepare 0.9.2 (#723) --- CHANGELOG.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b30664165..c0bc0af74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.2](https://github.com/xmldom/xmldom/compare/0.9.1...0.9.2) + +### Feature + +- add `Element.getElementsByClassName` [`#722`](https://github.com/xmldom/xmldom/pull/722) + +### Fixed + +- add missing types for `Document.documentElement` and `Element.tagName` [`#721`](https://github.com/xmldom/xmldom/pull/721) [`#720`](https://github.com/xmldom/xmldom/issues/720) + +Thank you, [@censujiang](https://github.com/censujiang), [@Mathias-S](https://github.com/Mathias-S), for your contributions + + ## [0.9.1](https://github.com/xmldom/xmldom/compare/0.9.0...0.9.1) ### Fixed @@ -16,8 +29,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - minimum tested node version is 14 [`#710`](https://github.com/xmldom/xmldom/pull/710) -Thank you, [@krystofwoldrich](https://github.com/krystofwoldrich), [@marvinruder](https://github.com/marvinruder), -[@amacneil](https://github.com/amacneil), [@defunctzombie](https://github.com/defunctzombie), +Thank you, [@krystofwoldrich](https://github.com/krystofwoldrich), [@marvinruder](https://github.com/marvinruder), [@amacneil](https://github.com/amacneil), [@defunctzombie](https://github.com/defunctzombie), [@tjhorner](https://github.com/tjhorner), [@danon](https://github.com/danon), for your contributions From b6d02cf7a948979a03c8383eb5a0dde2bb1003cf Mon Sep 17 00:00:00 2001 From: Christian Bewernitz Date: Thu, 5 Sep 2024 14:30:01 +0200 Subject: [PATCH 4/4] 0.9.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 492e7364f..caa9da061 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@xmldom/xmldom", - "version": "0.9.1", + "version": "0.9.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@xmldom/xmldom", - "version": "0.9.1", + "version": "0.9.2", "license": "MIT", "devDependencies": { "@homer0/prettier-plugin-jsdoc": "9.0.2", diff --git a/package.json b/package.json index f31917143..76f8c3bbd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xmldom/xmldom", - "version": "0.9.1", + "version": "0.9.2", "description": "A pure JavaScript W3C standard-based (XML DOM Level 2 Core) DOMParser and XMLSerializer module.", "keywords": [ "w3c",