From 90d1f629f458662c9c6f6d1ecd5e1ad876194d97 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Thu, 18 May 2017 14:37:42 +0200 Subject: [PATCH 01/34] Align implementation with current DOM spec. --- rollup.config.js | 36 +- src/Attr.ts | 105 +++ src/CDATASection.ts | 39 + src/CharacterData.ts | 238 +++--- src/Comment.ts | 48 +- src/DOMImplementation.ts | 77 +- src/Document.ts | 281 ++++++-- src/DocumentType.ts | 64 +- src/Element.ts | 675 +++++++++++++----- src/Node.ts | 629 ++++++---------- src/ProcessingInstruction.ts | 52 +- src/Range.ts | 543 ++++++++++++++ src/Text.ts | 191 +++-- src/XMLDocument.ts | 17 + src/globals.ts | 3 - src/index.ts | 56 +- src/mixins.ts | 109 +++ src/mutation-observer/MutationObserver.ts | 179 +++++ src/mutation-observer/MutationRecord.ts | 82 +++ src/mutation-observer/NotifyList.ts | 94 +++ src/mutation-observer/RegisteredObserver.ts | 110 +++ src/mutation-observer/RegisteredObservers.ts | 178 +++++ src/mutation-observer/queueMutationRecord.ts | 86 +++ src/mutations/MutationObserver.ts | 120 ---- src/mutations/MutationRecord.ts | 54 -- src/mutations/NotifyList.ts | 105 --- src/mutations/RegisteredObserver.ts | 89 --- src/mutations/RegisteredObservers.ts | 133 ---- src/mutations/queueMutationRecord.ts | 14 - src/selections/Range.ts | 242 ------- src/util.ts | 106 --- src/util/NodeType.ts | 28 + src/util/attrMutations.ts | 129 ++++ src/util/cloneNode.ts | 55 ++ src/util/createElementNS.ts | 33 + src/util/errorHelpers.ts | 31 + src/util/mutationAlgorithms.ts | 550 ++++++++++++++ src/util/namespaceHelpers.ts | 70 ++ src/util/treeHelpers.ts | 101 +++ src/util/treeMutations.ts | 150 ++++ test/Comment.tests.ts | 2 +- test/Document.tests.ts | 8 +- test/DocumentType.tests.ts | 2 +- test/Element.tests.ts | 52 +- .../{mutations => }/MutationObserver.tests.ts | 63 +- test/ProcessingInstruction.tests.ts | 2 +- test/{selections => }/Range.tests.ts | 16 +- test/Text.tests.ts | 76 +- test/tsconfig.json | 9 +- tsconfig.json | 6 +- 50 files changed, 4185 insertions(+), 1953 deletions(-) create mode 100644 src/Attr.ts create mode 100644 src/CDATASection.ts create mode 100644 src/Range.ts create mode 100644 src/XMLDocument.ts delete mode 100644 src/globals.ts create mode 100644 src/mixins.ts create mode 100644 src/mutation-observer/MutationObserver.ts create mode 100644 src/mutation-observer/MutationRecord.ts create mode 100644 src/mutation-observer/NotifyList.ts create mode 100644 src/mutation-observer/RegisteredObserver.ts create mode 100644 src/mutation-observer/RegisteredObservers.ts create mode 100644 src/mutation-observer/queueMutationRecord.ts delete mode 100644 src/mutations/MutationObserver.ts delete mode 100644 src/mutations/MutationRecord.ts delete mode 100644 src/mutations/NotifyList.ts delete mode 100644 src/mutations/RegisteredObserver.ts delete mode 100644 src/mutations/RegisteredObservers.ts delete mode 100644 src/mutations/queueMutationRecord.ts delete mode 100644 src/selections/Range.ts delete mode 100644 src/util.ts create mode 100644 src/util/NodeType.ts create mode 100644 src/util/attrMutations.ts create mode 100644 src/util/cloneNode.ts create mode 100644 src/util/createElementNS.ts create mode 100644 src/util/errorHelpers.ts create mode 100644 src/util/mutationAlgorithms.ts create mode 100644 src/util/namespaceHelpers.ts create mode 100644 src/util/treeHelpers.ts create mode 100644 src/util/treeMutations.ts rename test/{mutations => }/MutationObserver.tests.ts (84%) rename test/{selections => }/Range.tests.ts (96%) diff --git a/rollup.config.js b/rollup.config.js index 843d2f8..704092a 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -3,18 +3,26 @@ import babili from 'rollup-plugin-babili'; const { main: MAIN_DEST_FILE, module: MODULE_DEST_FILE } = require('./package.json'); export default { - entry: 'lib/index.js', - targets: [ - { dest: MAIN_DEST_FILE, format: 'umd' }, - { dest: MODULE_DEST_FILE, format: 'es' }, - ], - moduleName: 'slimdom', - exports: 'default', - sourceMap: true, - plugins: [ - babili({ - comments: false, - sourceMap: true - }) - ] + entry: 'lib/index.js', + targets: [ + { dest: MAIN_DEST_FILE, format: 'umd' }, + { dest: MODULE_DEST_FILE, format: 'es' }, + ], + moduleName: 'slimdom', + exports: 'named', + sourceMap: true, + onwarn (warning) { + // Ignore "this is undefined" warning triggered by typescript's __extends helper + if (warning.code === 'THIS_IS_UNDEFINED') { + return; + } + + console.error(warning.message); + }, + plugins: [ + babili({ + comments: false, + sourceMap: true + }) + ] } diff --git a/src/Attr.ts b/src/Attr.ts new file mode 100644 index 0000000..9811af2 --- /dev/null +++ b/src/Attr.ts @@ -0,0 +1,105 @@ +import Document from './Document'; +import Element from './Element'; +import Node from './Node'; +import { changeAttribute } from './util/attrMutations'; +import { NodeType } from './util/NodeType'; + +/** + * 3.9.2. Interface Attr + */ +export default class Attr extends Node { + // Node + + public get nodeType (): number { + return NodeType.ATTRIBUTE_NODE; + } + + public get nodeName (): string { + // Return the qualified name + return this.name; + } + + public get nodeValue (): string | null { + return this._value; + } + + public set nodeValue (newValue: string | null) { + // if the new value is null, act as if it was the empty string instead + if (newValue === null) { + newValue = ''; + } + + // Set an existing attribute value with context object and new value. + setExistingAttributeValue(this, newValue); + } + + // Attr + + public readonly namespaceURI: string | null; + public readonly prefix: string | null; + public readonly localName: string; + public readonly name: string; + + private _value: string; + + public get value (): string { + return this._value; + } + + public set value (value: string) { + setExistingAttributeValue(this, value); + } + + public ownerElement: Element | null; + + /** + * (non-standard) use Document#createAttribute(NS) or Element#setAttribute(NS) to create attribute nodes + * + * @param document The node document to associate with the attribute + * @param namespace The namespace URI for the attribute + * @param prefix The prefix for the attribute + * @param localName The local name for the attribute + * @param value The value for the attribute + * @param element The element for the attribute, or null if the attribute is not attached to an element + */ + constructor (document: Document, namespace: string | null, prefix: string | null, localName: string, value: string, element: Element | null) { + super(document); + this.namespaceURI = namespace; + this.prefix = prefix; + this.localName = localName; + this.name = prefix === null ? localName : `${prefix}:${localName}`; + this._value = value; + this.ownerElement = element; + this.ownerDocument = document; + } + + /** + * (non-standard) Creates a copy of the context object, not including its children. + * + * @param document The node document to associate with the copy + * + * @return A shallow copy of the context object + */ + public _copy (document: Document): Attr { + // Set copy’s namespace, namespace prefix, local name, and value, to those of node. + return new Attr(document, this.namespaceURI, this.prefix, this.localName, this.value, this.ownerElement); + } +} + +/** + * To set an existing attribute value, given an attribute attribute and string value, run these steps: + * + * @param attribute The attribute to set the value of + * @param value The new value for attribute + */ +function setExistingAttributeValue (attribute: Attr, value: string) { + // 1. If attribute’s element is null, then set attribute’s value to value. + const element = attribute.ownerElement; + if (element === null) { + (attribute as any)._value = value; + } + // 2. Otherwise, change attribute from attribute’s element to value. + else { + changeAttribute(attribute, element, value); + } +} diff --git a/src/CDATASection.ts b/src/CDATASection.ts new file mode 100644 index 0000000..7d24cc4 --- /dev/null +++ b/src/CDATASection.ts @@ -0,0 +1,39 @@ +import Document from './Document'; +import Text from './Text'; +import { NodeType } from './util/NodeType'; + +export default class CDATASection extends Text { + // Node + + public get nodeType (): number { + return NodeType.CDATA_SECTION_NODE; + } + + public get nodeName (): string { + return '#cdata-section'; + } + + // CDATASection + + /** + * (non-standard) use Document#createCDATASection to create a CDATA section. + * + * @param document (non-standard) The node document to associate with the node + * @param data The data for the node + */ + constructor (document: Document, data: string) { + super(document, data); + } + + /** + * (non-standard) Creates a copy of the context object, not including its children. + * + * @param document The node document to associate with the copy + * + * @return A shallow copy of the context object + */ + public _copy (document: Document): CDATASection { + // Set copy’s data, to that of node. + return new CDATASection(document, this.data); + } +} diff --git a/src/CharacterData.ts b/src/CharacterData.ts index 092819f..8aea980 100644 --- a/src/CharacterData.ts +++ b/src/CharacterData.ts @@ -1,143 +1,213 @@ +import { NonDocumentTypeChildNode, ChildNode } from './mixins'; import Document from './Document'; +import Element from './Element'; import Node from './Node'; +import { ranges } from './Range'; +import queueMutationRecord from './mutation-observer/queueMutationRecord'; +import { throwIndexSizeError } from './util/errorHelpers'; -import MutationRecord from './mutations/MutationRecord'; -import queueMutationRecord from './mutations/queueMutationRecord'; +function asUnsignedLong (number: number): number { + return number >>> 0; +} /** - * The CharacterData abstract interface represents a Node object that contains characters. This is an abstract - * interface, meaning there aren't any object of type CharacterData: it is implemented by other interfaces, - * like Text, Comment, or ProcessingInstruction which aren't abstract. + * 3.10. Interface CharacterData */ -export default abstract class CharacterData extends Node { - private _data: string; +export default abstract class CharacterData extends Node implements NonDocumentTypeChildNode, ChildNode { + // Node - public get data (): string { + public get nodeValue (): string | null { return this._data; } - public set data (newValue: string) { - this.replaceData(0, this._data.length, newValue); + public set nodeValue (newValue: string | null) { + // if the new value is null, act as if it was the empty string instead + if (newValue === null) { + newValue = ''; + } + + // Set an existing attribute value with context object and new value. + replaceData(this, 0, this.length, newValue); } + // NonDocumentTypeChildNode + + previousElementSibling: Element | null = null; + nextElementSibling: Element | null = null; + + // CharacterData + /** - * Alias for data. + * Each node inheriting from the CharacterData interface has an associated mutable string called data. */ - public get nodeValue (): string { + protected _data: string; + + public get data (): string { return this._data; } - /** - * The length of the string used as textual data for this CharacterData node. - */ + public set data (data: string) { + replaceData(this, 0, this.length, data); + } + public get length (): number { - return this._data.length; + return this.data.length; } /** - * @param type Node type - * @param data Content of the node + * @param document The node document to associate with the node + * @param data The data to associate with the node */ - constructor (type: number, data: string) { - super(type); - + protected constructor (document: Document, data: string) { + super(document); this._data = data; } /** - * Returns a string containing the part of CharacterData.data of the specified length and starting at the - * specified offset. + * Returns a substring of the node's data. * * @param offset Offset at which to start the substring - * @param count Number of characters to return. If omitted, returns all data starting at offset. + * @param count The number of code units to return * - * @return The specified substring of the current content + * @return The specified substring */ - public substringData (offset: number, count?: number): string { - return this._data.substr(offset, count); + public substringData (offset: number, count: number): string { + return substringData(this, offset, count); } /** - * Appends the given string to the CharacterData.data string; when this method returns, data contains the - * concatenated string. + * Appends data to the node's data. * - * @param data Content to add to the end of the current content + * @param data Data to append */ - public appendData (data: string) { - this.replaceData(this.length, 0, data); + public appendData (data: string): void { + replaceData(this, this.length, 0, data); } /** - * Inserts the specified characters, at the specified offset, in the CharacterData.data string; when this method - * returns, data contains the modified string. + * Inserts data at the specified position in the node's data. * - * @param offset Offset at which to insert data - * @param data Content to insert + * @param offset Offset at which to insert + * @param data Data to insert */ - public insertData (offset: number, data: string) { - this.replaceData(offset, 0, data); + public insertData (offset: number, data: string): void { + replaceData(this, offset, 0, data); } /** - * Removes the specified amount of characters, starting at the specified offset, from the CharacterData.data - * string; when this method returns, data contains the shortened string. + * Deletes data from the specified position. * - * @param offset Offset at which to start removing content - * @param count Number of characters to remove. Omitting count deletes from offset to the end of data. + * @param offset Offset at which to delete + * @param count Number of code units to delete */ - public deleteData (offset: number, count: number = this.length) { - this.replaceData(offset, count, ''); + public deleteData (offset: number, count: number): void { + replaceData(this, offset, count, ''); } /** - * Replaces the specified amount of characters, starting at the specified offset, with the specified string; - * when this method returns, data contains the modified string. + * Replaces data at the specified position. * - * @param offset Offset at which to remove and then insert content - * @param count Number of characters to remove - * @param data Content to insert + * @param offset Offset at which to replace + * @param count Number of code units to remove + * @param data Data to insert */ - public replaceData (offset: number, count: number, data: string) { - const length = this.length; - if (offset > length) { - offset = length; - } + public replaceData (offset: number, count: number, data: string): void { + replaceData(this, offset, count, data); + } +} - if (offset + count > length) { - count = length - offset; +/** + * To replace data of node node with offset offset, count count, and data data, run these steps: + * + * @param node The node to replace data on + * @param offset The offset at which to start replacing + * @param count The number of code units to replace + * @param data The data to insert in place of the removed data + */ +export function replaceData (node: CharacterData, offset: number, count: number, data: string): void { + // Match spec data type + offset = asUnsignedLong(offset); + + // 1. Let length be node’s length. + const length = node.length; + + // 2. If offset is greater than length, then throw an IndexSizeError. + if (offset > length) { + throwIndexSizeError('can not replace data past the node\'s length'); + } + + // 3. If offset plus count is greater than length, then set count to length minus offset. + if (offset + count > length) { + count = length - offset; + } + + // 4. Queue a mutation record of "characterData" for node with oldValue node’s data. + queueMutationRecord('characterData', node, { + oldValue: node.data + }); + + // 5. Insert data into node’s data after offset code units. + // 6. Let delete offset be offset plus the number of code units in data. + // 7. Starting from delete offset code units, remove count code units from node’s data. + const nodeData = node.data; + const newData = nodeData.substring(0, offset) + data + nodeData.substring(offset + count); + (node as any)._data = newData; + + ranges.forEach(range => { + // 8. For each range whose start node is node and start offset is greater than offset but less than or equal to + // offset plus count, set its start offset to offset. + if (range.startContainer === node && range.startOffset > offset && range.startOffset <= offset + count) { + range.startOffset = offset; } - const before = this.substringData(0, offset); - const after = this.substringData(offset + count); - const newData = before + data + after; + // 9. For each range whose end node is node and end offset is greater than offset but less than or equal to + // offset plus count, set its end offset to offset. + if (range.endContainer === node && range.endOffset > offset && range.endOffset <= offset + count) { + range.endOffset = offset; + } - if (newData !== this._data) { - // Queue mutation record - var record = new MutationRecord('characterData', this); - record.oldValue = this._data; - queueMutationRecord(record); + // 10. For each range whose start node is node and start offset is greater than offset plus count, increase its + // start offset by the number of code units in data, then decrease it by count. + if (range.startContainer === node && range.startOffset > offset + count) { + range.startOffset = range.startOffset + data.length - count; + } - // Replace data - this._data = newData; + // 11. For each range whose end node is node and end offset is greater than offset plus count, increase its end + // offset by the number of code units in data, then decrease it by count. + if (range.endContainer === node && range.endOffset > offset + count) { + range.endOffset = range.endOffset + data.length - count; } + }); +} + +/** + * To substring data with node node, offset offset, and count count, run these steps: + * + * @param node The node to get data from + * @param offset The offset at which to start the substring + * @param count The number of code units to include in the substring + * + * @return The requested substring + */ +export function substringData (node: CharacterData, offset: number, count: number): string { + // Match spec data type + offset = asUnsignedLong(offset); - // Update ranges - var document = this.ownerDocument as Document; - document._ranges.forEach(range => { - if (range.startContainer === this && range.startOffset > offset && range.startOffset <= offset + count) { - range.setStart(range.startContainer, offset); - } - if (range.endContainer === this && range.endOffset > offset && range.endOffset <= offset + count) { - range.setEnd(range.endContainer, offset); - } - const startOffset = range.startOffset; - const endOffset = range.endOffset; - if (range.startContainer === this && startOffset > offset + count) { - range.setStart(range.startContainer, startOffset - count + data.length); - } - if (range.endContainer === this && endOffset > offset + count) { - range.setEnd(range.endContainer, endOffset - count + data.length); - } - }); + // 1. Let length be node’s length. + const length = node.length; + + // 2. If offset is greater than length, then throw an IndexSizeError. + if (offset > length) { + throwIndexSizeError('can not substring data past the node\'s length'); + } + + // 3. If offset plus count is greater than length, return a string whose value is the code units from the offsetth + // code unit to the end of node’s data, and then return. + if (offset + count > length) { + return node.data.substring(offset); } + + // 4. Return a string whose value is the code units from the offsetth code unit to the offset+countth code unit in + // node’s data. + return node.data.substring(offset, offset + count); } diff --git a/src/Comment.ts b/src/Comment.ts index 38f3a29..fc2849b 100644 --- a/src/Comment.ts +++ b/src/Comment.ts @@ -1,22 +1,42 @@ import CharacterData from './CharacterData'; -import Node from './Node'; +import Document from './Document'; +import { NodeType } from './util/NodeType'; -/** - * The Comment interface represents textual notations within markup; although it is generally not visually - * shown, such comments are available to be read in the source view. Comments are represented in HTML and - * XML as content between '<!--' and '-->'. In XML, the character sequence '--' cannot be used within - * a comment. - */ export default class Comment extends CharacterData { - /** - * @param data Text of the comment + // Node + + public get nodeType (): number { + return NodeType.COMMENT_NODE; + } + + public get nodeName (): string { + return '#comment'; + } + + // Comment + + /** + * Returns a new Comment node whose data is data. + * + * Non-standard: as this implementation does not have a document associated with the global object, it is required + * to pass a document to this constructor. + * + * @param document (non-standard) The node document to associate with the new comment + * @param data The data for the new comment */ - constructor (data: string = '') { - super(Node.COMMENT_NODE, data); + constructor (document: Document, data: string = '') { + super(document, data); } - public cloneNode (deep: boolean = true, copy?: Comment): Comment { - copy = copy || new Comment(this.data); - return super.cloneNode(deep, copy) as Comment; + /** + * (non-standard) Creates a copy of the context object, not including its children. + * + * @param document The node document to associate with the copy + * + * @return A shallow copy of the context object + */ + public _copy (document: Document): Comment { + // Set copy’s data, to that of node. + return new Comment(document, this.data); } } diff --git a/src/DOMImplementation.ts b/src/DOMImplementation.ts index bc54702..6f166eb 100644 --- a/src/DOMImplementation.ts +++ b/src/DOMImplementation.ts @@ -1,52 +1,81 @@ -import Document from './Document'; import DocumentType from './DocumentType'; +import XMLDocument from './XMLDocument'; + +import createElementNS from './util/createElementNS'; +import { validateQualifiedName } from './util/namespaceHelpers'; -/** - * The DOMImplementation interface represents an object providing methods which are not dependent on any - * particular document. Such an object is returned by the Document.implementation property. - */ export default class DOMImplementation { /** - * Returns a DocumentType object which can either be used with DOMImplementation.createDocument upon document - * creation or can be put into the document via methods like Node.insertBefore() or Node.replaceChild(). + * Returns a doctype, with the given qualifiedName, publicId, and systemId. + * + * (Non-standard) As this implementation does not associate a document with the global object, the returned + * doctype does not have an associated node document until it is inserted in one. * - * @param qualifiedName The name of the doctype - * @param publicId The public identifier of the doctype - * @param systemId The system identifier of the doctype + * @param qualifiedName Qualified name for the doctype + * @param publicId Public ID for the doctype + * @param systemId System ID for the doctype * - * @return The new doctype + * @return The new doctype node */ - public createDocumentType (qualifiedName: string, publicId: string, systemId: string): DocumentType { - return new DocumentType(qualifiedName, publicId, systemId); + createDocumentType (qualifiedName: string, publicId: string, systemId: string): DocumentType { + // 1. Validate qualifiedName. + validateQualifiedName(qualifiedName); + + // 2. Return a new doctype, with qualifiedName as its name, publicId as its public ID, and systemId as its + // system ID, and with its node document set to the associated document of the context object. + return new DocumentType(null, qualifiedName, publicId, systemId); } /** - * Creates and returns a new Document. + * Returns an XMLDocument, with a document element whose local name is qualifiedName and whose namespace is + * namespace (unless qualifiedName is the empty string), and with doctype, if it is given, as its doctype. * - * Note that namespaces are not currently supported; namespace and any prefix in qualifiedName will be ignored + * @param namespace The namespace for the root element + * @param qualifiedName The qualified name for the root element, or empty string to not create a root element + * @param doctype The doctype for the new document, or null to not add a doctype * - * @param namespace Namespace URI for the new document's root element, not currently supported - * @param qualifiedName Qualified name for the new document's root element, currently interpreted as local name - * @param doctype Document type for the new document, or null to omit - * - * @return The new Document, with optional doctype and/or root element + * @return The new XMLDocument */ - public createDocument (namespace: string | null, qualifiedName: string, doctype: DocumentType | null = null) { - const document = new Document(); + createDocument (namespace: string | null, qualifiedName: string | null, doctype: DocumentType | null = null): XMLDocument { + // [TreatNullAs=EmptyString] for qualifiedName + if (qualifiedName === null) { + qualifiedName = ''; + } + + // 1. Let document be a new XMLDocument. + const document = new XMLDocument(); + + // 2. Let element be null. let element = null; + + // 3. If qualifiedName is not the empty string, then set element to the result of running the internal + // createElementNS steps, given document, namespace, qualifiedName, and an empty dictionary. if (qualifiedName !== '') { - // TODO: use createElementNS once it is supported - element = document.createElement(qualifiedName); + element = createElementNS(document, namespace, qualifiedName); } + // 4. If doctype is non-null, append doctype to document. if (doctype) { document.appendChild(doctype); } + // 5. If element is non-null, append element to document. if (element) { document.appendChild(element); } + // 6. document’s origin is context object’s associated document’s origin. + // (origin not implemented) + + // 7. document’s content type is determined by namespace: + // HTML namespace: application/xhtml+xml + // SVG namespace: image/svg+xml + // Any other namespace: application/xml + // (content type not implemented) + + // 8. Return document. return document; } } + +export const implementation = new DOMImplementation(); diff --git a/src/Document.ts b/src/Document.ts index 45ede35..602019b 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -1,147 +1,270 @@ +import { NonElementParentNode, ParentNode, getChildren } from './mixins'; +import Attr from './Attr'; +import CDATASection from './CDATASection'; import Comment from './Comment'; import DocumentType from './DocumentType'; -import DOMImplementation from './DOMImplementation'; -import Element from './Element'; +import { implementation, default as DOMImplementation } from './DOMImplementation'; +import { createElement, default as Element } from './Element'; import Node from './Node'; import ProcessingInstruction from './ProcessingInstruction'; import Text from './Text'; +import Range from './Range'; -import Range from './selections/Range'; +import cloneNode from './util/cloneNode'; +import createElementNS from './util/createElementNS'; +import { throwNotSupportedError } from './util/errorHelpers'; +import { adoptNode } from './util/mutationAlgorithms'; +import { NodeType, isNodeOfType } from './util/NodeType'; +import { validateAndExtract } from './util/namespaceHelpers'; -import { implementation } from './globals'; +/** + * 3.5. Interface Document + */ +export default class Document extends Node implements NonElementParentNode, ParentNode { + // Node + + public get nodeType (): number { + return NodeType.DOCUMENT_NODE; + } + + public get nodeName (): string { + return '#document'; + } + + public get nodeValue (): string | null { + return null; + } + + public set nodeValue (newValue: string | null) { + // Do nothing. + } + + // ParentNode + + public get children (): Element[] { + return getChildren(this); + } + + public firstElementChild: Element | null = null; + public lastElementChild: Element | null = null; + public childElementCount: number = 0; + + // Document + + /** + * Returns a reference to the DOMImplementation object associated with the document. + */ + public get implementation (): DOMImplementation { + return implementation; + } -export default class Document extends Node { /** - * The DocumentType that is a direct child of the current document, or null if there is none. + * The doctype, or null if there is none. */ public doctype: DocumentType | null = null; /** - * The Element that is a direct child of the current document, or null if there is none. + * The document element, or null if there is none. */ public documentElement: Element | null = null; /** - * Returns a reference to the DOMImplementation object which created the document. + * Creates a new Document. + * + * Note: Unlike DOMImplementation#createDocument(), this constructor does not return an XMLDocument object, but a + * document (Document object). */ - public implementation: DOMImplementation = implementation; + constructor() { + super(null); + } /** - * (internal) The ranges that are active on the current document. + * Creates a new element in the null namespace. + * + * @param localName Local name of the element + * + * @return The new element */ - public _ranges: Range[] = []; - - constructor () { - super(Node.DOCUMENT_NODE); + public createElement (localName: string): Element { + // 1. If localName does not match the Name production, then throw an InvalidCharacterError. - // Non-standard: should be null for Document nodes. - this.ownerDocument = this; - } - - // Override insertBefore to update the documentElement reference. - public insertBefore (newNode: Node, referenceNode: Node | null, suppressObservers: boolean = false): Node | null { - // Document can not have more than one child element node - if (newNode.nodeType === Node.ELEMENT_NODE && this.documentElement) { - return this.documentElement === newNode ? newNode : null; - } + // 2. If the context object is an HTML document, then set localName to localName in ASCII lowercase. + // (html documents not implemented) - // Document can not have more than one child doctype node - if (newNode.nodeType === Node.DOCUMENT_TYPE_NODE && this.doctype) { - return this.doctype === newNode ? newNode : null; - } + // 3. Let is be the value of is member of options, or null if no such member exists. + // (custom elements not implemented) - const result = super.insertBefore(newNode, referenceNode, suppressObservers); + // 4. Let namespace be the HTML namespace, if the context object is an HTML document or context object’s content + // type is "application/xhtml+xml", and null otherwise. + // (html documents not implemented) + const namespace: string | null = null; - // Update document element - if (result && result.nodeType === Node.ELEMENT_NODE) { - this.documentElement = result as Element; - } + // 5. Let element be the result of creating an element given the context object, localName, namespace, null, is, + // and with the synchronous custom elements flag set. + const element = createElement(this, localName, namespace, null); - // Update doctype - if (result && result.nodeType === Node.DOCUMENT_TYPE_NODE) { - this.doctype = result as DocumentType; - } + // 6. If is is non-null, then set an attribute value for element using "is" and is. + // (custom elements not implemented) - return result; + // 7. Return element. + return element; } - // Override removeChild to keep the documentElement property in sync. - public removeChild (childNode: Node, suppressObservers: boolean = false): Node | null { - var result = Node.prototype.removeChild.call(this, childNode, suppressObservers); - if (result === this.documentElement) { - this.documentElement = null; - } - else if (result === this.doctype) { - this.doctype = null; - } + /** + * Creates a new element in the given namespace. + * + * @param namespace Namespace URI for the new element + * @param qualifiedName Qualified name for the new element + * + * @return The new element + */ + public createElementNS (namespace: string | null, qualifiedName: string): Element { + // return the result of running the internal createElementNS steps, given context object, namespace, + // qualifiedName, and options. + return createElementNS(this, namespace, qualifiedName); + } - return result; + /** + * Creates a new text node with the given data. + * + * @param data Data for the new text node + * + * @return The new text node + */ + public createTextNode (data: string): Text { + return new Text(this, data); } /** - * Creates a new Element node with the given tag name. + * Creates a new CDATA section with the given data. * - * @param name NodeName of the new Element + * @param data Data for the new CDATA section * - * @return The new Element + * @return The new CDATA section */ - public createElement (name: string): Element { - const node = new Element(name); - node.ownerDocument = this; - return node; + public createCDATASection (data: string): CDATASection { + return new CDATASection(this, data); } /** - * Creates a new Text node with the given content. + * Creates a new comment node with the given data. * - * @param content Content for the new text node + * @param data Data for the new comment * - * @return The new text node + * @return The new comment node */ - public createTextNode (content: string): Text { - const node = new Text(content); - node.ownerDocument = this; - return node; + public createComment (data: string): Comment { + return new Comment(this, data); } /** - * Creates a new ProcessingInstruction node with a given target and given data. + * Creates a new processing instruction. * - * @param target Target of the processing instruction - * @param data Content of the processing instruction + * @param target Target for the new processing instruction + * @param data Data for the new processing instruction * * @return The new processing instruction */ public createProcessingInstruction (target: string, data: string): ProcessingInstruction { - const node = new ProcessingInstruction(target, data); - node.ownerDocument = this; - return node; + return new ProcessingInstruction(this, target, data); } /** - * Creates a new Comment node with the given data. + * Creates a copy of a node from an external document that can be inserted into the current document. * - * @param data Content of the comment + * @param node The node to import + * @param deep Whether to also import node's children + */ + public importNode (node: Node, deep: boolean = false): Node { + // 1. If node is a document or shadow root, then throw a NotSupportedError. + if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { + throwNotSupportedError('importing a Document node is not supported'); + } + + // 2. Return a clone of node, with context object and the clone children flag set if deep is true. + return cloneNode(node, deep, this); + } + + /** + * Adopts a node. The node and its subtree is removed from the document it's in (if any), and its ownerDocument is + * changed to the current document. The node can then be inserted into the current document. * - * @return The new comment node + * @param node The node to adopt */ - public createComment (data: string): Comment { - const node = new Comment(data); - node.ownerDocument = this; + public adoptNode (node: Node): Node { + // 1. If node is a document, then throw a NotSupportedError. + if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { + throwNotSupportedError('adopting a Document node is not supported'); + } + + // 2. If node is a shadow root, then throw a HierarchyRequestError. + // (shadow dom not implemented) + + // 3. Adopt node into the context object. + adoptNode(node, this); + + // 4. Return node. return node; } /** - * Creates a selection range within the current document. + * Creates a new attribute node with the null namespace and given local name. * - * @return The new range, positioned just inside the root of the document + * @param localName The local name of the attribute + * + * @return The new attribute node + */ + public createAttribute (localName: string): Attr { + // 1. If localName does not match the Name production in XML, then throw an InvalidCharacterError. + + // 2. If the context object is an HTML document, then set localName to localName in ASCII lowercase. + // (html documents not implemented) + + // 3. Return a new attribute whose local name is localName and node document is context object. + return new Attr(this, null, null, localName, '', null); + } + + /** + * Creates a new attribute node with the given namespace and qualified name. + * + * @param namespace Namespace URI for the new attribute, or null for the null namespace + * @param qualifiedName Qualified name for the new attribute + * + * @return The new attribute node + */ + public createAttributeNS (namespace: string | null, qualifiedName: string): Attr { + // 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName to validate and + // extract. + const { namespace: validatedNamespace, prefix, localName } = validateAndExtract(namespace, qualifiedName); + + // 2. Return a new attribute whose namespace is namespace, namespace prefix is prefix, local name is localName, + // and node document is context object. + return new Attr(this, validatedNamespace, prefix, localName, '', null); + } + + /** + * Creates a new Range, initially positioned at the root of this document. + * + * Note: although the spec encourages use of the Range() constructor, this implementation does not associate any + * Document with the global object, preventing implementation of that constructor. + * + * @return The new Range */ public createRange (): Range { return new Range(this); } - public cloneNode (deep: boolean = true, copy?: Document): Document { - copy = copy || new Document(); - return super.cloneNode(deep, copy) as Document; + /** + * (non-standard) Creates a copy of the context object, not including its children. + * + * @param document The node document to associate with the copy + * + * @return A shallow copy of the context object + */ + public _copy (document: Document): Document { + // Set copy’s encoding, content type, URL, origin, type, and mode, to those of node. + // (properties not implemented) + + return new Document(); } } diff --git a/src/DocumentType.ts b/src/DocumentType.ts index 8e313bc..c729edb 100644 --- a/src/DocumentType.ts +++ b/src/DocumentType.ts @@ -1,28 +1,68 @@ +import { ChildNode } from './mixins'; +import Document from './Document'; import Node from './Node'; +import { NodeType } from './util/NodeType'; -/** - * The DocumentType interface represents a Node containing a doctype. - */ -export default class DocumentType extends Node { +export default class DocumentType extends Node implements ChildNode { + // Node + + public get nodeType (): number { + return NodeType.DOCUMENT_TYPE_NODE; + } + + public get nodeName (): string { + return this.name; + } + + public get nodeValue (): string | null { + return null; + } + + public set nodeValue (newValue: string | null) { + // Do nothing. + } + + // DocumentType + + /** + * The name of the doctype. + */ public name: string; + + /** + * The public ID of the doctype. + */ public publicId: string; + + /** + * The system ID of the doctype. + */ public systemId: string; /** - * @param name The name of the document type - * @param publicId The public identifier of the doctype - * @param systemId The system identifier of the doctype + * (non-standard) Use DOMImplementation#createDocumentType instead. + * + * @param name The name of the doctype + * @param publicId The public ID of the doctype + * @param systemId The system ID of the doctype */ - constructor (name: string, publicId: string, systemId: string) { - super(Node.DOCUMENT_TYPE_NODE); + constructor (document: Document | null, name: string, publicId: string = '', systemId: string = '') { + super(document); this.name = name; this.publicId = publicId; this.systemId = systemId; } - public cloneNode (deep: boolean = true, copy?: DocumentType): DocumentType { - copy = copy || new DocumentType(this.name, this.publicId, this.systemId); - return super.cloneNode(deep, copy) as DocumentType; + /** + * (non-standard) Creates a copy of the context object, not including its children. + * + * @param document The node document to associate with the copy + * + * @return A shallow copy of the context object + */ + public _copy (document: Document): DocumentType { + // Set copy’s name, public ID, and system ID, to those of node. + return new DocumentType(document, this.name, this.publicId, this.systemId); } } diff --git a/src/Element.ts b/src/Element.ts index a11cdd7..2809944 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -1,250 +1,573 @@ +import { ParentNode, NonDocumentTypeChildNode, ChildNode } from './mixins'; +import { getChildren, getPreviousElementSibling, getNextElementSibling } from './mixins'; +import Attr from './Attr'; +import Document from './Document'; import Node from './Node'; -import MutationRecord from './mutations/MutationRecord'; -import queueMutationRecord from './mutations/queueMutationRecord'; - -export interface Attr { - name: string; - value: string; -} +import { appendAttribute, changeAttribute, removeAttribute, replaceAttribute } from './util/attrMutations'; +import { throwInUseAttributeError, throwNotFoundError } from './util/errorHelpers'; +import { validateAndExtract } from './util/namespaceHelpers'; +import { NodeType } from './util/NodeType'; /** - * Internal helper used to check if the given node is an Element object. - * - * @param node Node to check - * - * @return Whether node is an element + * 3.9. Interface Element */ -function isElement (node?: Node | null): boolean { - return !!node && node.nodeType === Node.ELEMENT_NODE; -} +export default class Element extends Node implements ParentNode, NonDocumentTypeChildNode, ChildNode { + // Node -/** - * Returns the first element sibling in the given direction: if it's backwards it's the first previousSibling - * node starting from the given node that's an Element, if it's forwards it's the first nextSibling node that's - * an Element. - * - * @param node Node to start from - * @param backwards Whether to look at node's preceding rather than its following siblings - * - * @return The element if found, or null otherwise - */ -function findNextElementSibling (node: Node | null, backwards: boolean): Element | null { - while (node) { - node = backwards ? node.previousSibling : node.nextSibling; - if (isElement(node)) { - break; - } + public get nodeType (): number { + return NodeType.ELEMENT_NODE; } - return node as Element; -} + public get nodeName (): string { + return this.tagName; + } + + public get nodeValue (): string | null { + return null; + } + + public set nodeValue (newValue: string | null) { + // Do nothing. + } + + // ParentNode + + public get children (): Element[] { + return getChildren(this); + } + + public firstElementChild: Element | null = null; + public lastElementChild: Element | null = null; + public childElementCount: number = 0; + + // NonDocumentTypeChildNode + + public get previousElementSibling (): Element | null { + return getPreviousElementSibling(this); + } + + public get nextElementSibling (): Element | null { + return getNextElementSibling(this); + } + + // Element + + public readonly namespaceURI: string | null; + public readonly prefix: string | null; + public readonly localName: string; + public readonly tagName: string; -/** - * The Element interface represents part of the document. This interface describes methods and properties common - * to each kind of elements. Specific behaviors are described in the specific interfaces, inheriting from - * Element: the HTMLElement interface for HTML elements, or the SVGElement interface for SVG elements. - */ -export default class Element extends Node { /** - * The name of the element. + * (non-standard) Use Document#createElement or Document#createElementNS to create an Element. + * + * @param document Node document for the element + * @param namespace Namespace for the element + * @param prefix Prefix for the element + * @param localName Local name for the element */ - public nodeName: string; + constructor (document: Document, namespace: string | null, prefix: string | null, localName: string) { + super(document); + this.namespaceURI = namespace; + this.prefix = prefix; + this.localName = localName; + this.tagName = prefix === null ? localName : `${prefix}:${localName}`; + } /** - * The attributes as an array of Attr objects, having name and value. + * Returns whether the element has any attributes. + * + * @return True if the element has attributes, otherwise false */ - public attributes: Attr[] = []; + public hasAttributes (): boolean { + return this.attributes.length > 0; + } /** - * Internal lookup of Attr objects by their name. + * The attributes for the element. + * + * Non-standard: the spec defines this as a NamedNodeMap, while this implementation uses an array. */ - private _attrByName: { [key: string]: Attr } = {}; + public readonly attributes: Attr[] = []; /** - * The first child node of the current element that's an Element node. + * Get the value of the specified attribute. + * + * @param qualifiedName The qualified name of the attribute + * + * @return The value of the attribute, or null if no such attribute exists */ - public firstElementChild: Element | null = null; + public getAttribute (qualifiedName: string): string | null { + // 1. Let attr be the result of getting an attribute given qualifiedName and the context object. + const attr = getAttributeByName(qualifiedName, this); + + // 2. If attr is null, return null. + if (attr === null) { + return null; + } + + // 3. Return attr’s value. + return attr.value; + } /** - * The last child node of the current element that's an Element node. + * Get the value of the specified attribute. + * + * @param namespace The namespace of the attribute + * @param localName The local name of the attribute + * + * @return The value of the attribute, or null if no such attribute exists */ - public lastElementChild: Element | null = null; + public getAttributeNS (namespace: string | null, localName: string): string | null { + // 1. Let attr be the result of getting an attribute given namespace, localName, and the context object. + const attr = getAttributeByNamespaceAndLocalName(namespace, localName, this); + + // 2. If attr is null, return null. + if (attr === null) { + return null; + } + + // 3. Return attr’s value. + return attr.value; + } /** - * The previous sibling node of the current element that's an Element node. + * Sets the value of the specified attribute. + * + * @param qualifiedName The qualified name of the attribute + * @param value The new value for the attribute */ - public previousElementSibling: Element | null = null; + public setAttribute (qualifiedName: string, value: string): void { + // 1. If qualifiedName does not match the Name production in XML, then throw an InvalidCharacterError. + + // 2. If the context object is in the HTML namespace and its node document is an HTML document, then set + // qualifiedName to qualifiedName in ASCII lowercase. + // (html documents not implemented) + + // 3. Let attribute be the first attribute in context object’s attribute list whose qualified name is + // qualifiedName, and null otherwise. + const attribute = getAttributeByName(qualifiedName, this); + + // 4. If attribute is null, create an attribute whose local name is qualifiedName, value is value, and node + // document is context object’s node document, then append this attribute to context object, and then return. + if (attribute === null) { + const attribute = new Attr(this.ownerDocument!, null, null, qualifiedName, value, this); + appendAttribute(attribute, this); + return; + } + + // 5. Change attribute from context object to value. + changeAttribute(attribute, this, value); + } /** - * The next sibling node of the current element that's an Element node. + * Sets the value of the specified attribute. + * + * @param namespace The namespace of the attribute + * @param qualifiedName The qualified name of the attribute + * @param value The value for the attribute */ - public nextElementSibling: Element | null = null; + public setAttributeNS (namespace: string | null, qualifiedName: string, value: string): void { + // 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName to validate and + // extract. + const { namespace: validatedNamespace, prefix, localName } = validateAndExtract(namespace, qualifiedName); + + // 2. Set an attribute value for the context object using localName, value, and also prefix and namespace. + setAttributeValue(this, localName, value, prefix, validatedNamespace); + } /** - * The number of child nodes of the current element that are Element nodes. + * Removes the specified attribute. + * + * @param qualifiedName The qualified name of the attribute */ - public childElementCount: number = 0; + public removeAttribute (qualifiedName: string): void { + removeAttributeByName(qualifiedName, this); + } /** - * @param name The NodeName for the Element + * Removes the specified attribute. + * + * @param namespace The namespace of the attribute + * @param localName The local name of the attribute */ - constructor (name: string) { - super(Node.ELEMENT_NODE); - - this.nodeName = name; + public removeAttributeNS (namespace: string | null, localName: string): void { + removeAttributeByNamespaceAndLocalName(namespace, localName, this); } - // Override insertBefore to update element-specific properties - public insertBefore (newNode: Node, referenceNode: Node | null, suppressObservers: boolean = false): Node | null { - // Already there? - if (newNode.parentNode === this && (newNode === referenceNode || newNode.nextSibling === referenceNode)) { - return newNode; - } - - const result = super.insertBefore(newNode, referenceNode, suppressObservers); - - if (isElement(newNode) && newNode.parentNode === this) { - const newElement = newNode as Element; - // Update child references - this.firstElementChild = findNextElementSibling(this.firstElementChild, true) || this.firstElementChild || newElement; - this.lastElementChild = findNextElementSibling(this.lastElementChild, false) || this.lastElementChild || newElement; - - // Update sibling references - newElement.previousElementSibling = findNextElementSibling(newNode, true); - if (newElement.previousElementSibling) { - newElement.previousElementSibling.nextElementSibling = newElement; - } - newElement.nextElementSibling = findNextElementSibling(newNode, false); - if (newElement.nextElementSibling) { - newElement.nextElementSibling.previousElementSibling = newElement; - } - - // Update element count - this.childElementCount += 1; - } - - return result; - } - - // Override removeChild to update element-specific properties - public removeChild (childNode: Node, suppressObservers: boolean = false): Node | null { - if (isElement(childNode) && childNode.parentNode === this) { - const childElement = childNode as Element; - // Update child references - if (childNode === this.firstElementChild) { - this.firstElementChild = findNextElementSibling(childNode, false); - } - if (childNode === this.lastElementChild) { - this.lastElementChild = findNextElementSibling(childNode, true); - } - - // Update sibling references - if (childElement.previousElementSibling) { - childElement.previousElementSibling.nextElementSibling = childElement.nextElementSibling; - } - if (childElement.nextElementSibling) { - childElement.nextElementSibling.previousElementSibling = childElement.previousElementSibling; - } - - // Update element count - this.childElementCount -= 1; - } + /** + * Returns true if the specified attribute exists and false otherwise. + * + * @param qualifiedName The qualified name of the attribute + */ + public hasAttribute (qualifiedName: string): boolean { + // 1. If the context object is in the HTML namespace and its node document is an HTML document, then set + // qualifiedName to qualifiedName in ASCII lowercase. + // (html documents not implemented) + + // 2. Return true if the context object has an attribute whose qualified name is qualifiedName, and false + // otherwise. + return getAttributeByName(qualifiedName, this) !== null; + } - return super.removeChild(childNode, suppressObservers); + /** + * Returns true if the specified attribute exists and false otherwise. + * + * @param namespace The namespace of the attribute + * @param localName The local name of the attribute + */ + public hasAttributeNS (namespace: string | null, localName: string): boolean { + // 1. If namespace is the empty string, set it to null. + // (handled by getAttributeByNamespaceAndLocalName, called below) + // 2. Return true if the context object has an attribute whose namespace is namespace and local name is + // localName, and false otherwise. + return getAttributeByNamespaceAndLocalName(namespace, localName, this) !== null; } /** - * Returns whether or not the element has an attribute with the given name. + * Returns the specified attribute node, or null if no such attribute exists. * - * @param name Name of the attribute + * @param qualifiedName The qualified name of the attribute * - * @return Whether the attribute exists on the current element + * @return The attribute, or null if no such attribute exists */ - public hasAttribute (name: string): boolean { - return !!this._attrByName[name]; + public getAttributeNode (qualifiedName: string): Attr | null { + return getAttributeByName(qualifiedName, this); } /** - * Returns the value of the attribute with the given name for the current element or null if the attribute - * doesn't exist. + * Returns the specified attribute node, or null if no such attribute exists. * - * @param name Name of the attribute + * @param namespace The namespace of the attribute + * @param localName The local name of the attribute * - * @return The value of the attribute, or null of no such attribute exists on the current element + * @return The attribute, or null if no such attribute exists */ - public getAttribute (name: string): string | null { - const attr = this._attrByName[name]; - return attr ? attr.value : null; + public getAttributeNodeNS (namespace: string | null, localName: string): Attr | null { + return getAttributeByNamespaceAndLocalName(namespace, localName, this); } /** - * Sets the value of the attribute with the given name to the given value. + * Sets an attribute given its node * - * @param name Name of the attribute - * @param value New value for the attribute + * @param attr The attribute node to set + * + * @return The previous attribute node for the attribute */ - public setAttribute (name: string, value: string) { - // Coerce the value to a string for consistency - value = '' + value; + public setAttributeNode (attr: Attr): Attr | null { + return setAttribute(attr, this); + } - const oldAttr = this._attrByName[name]; - const newAttr = { - name: name, - value: value - }; - const oldValue = oldAttr ? oldAttr.value : null; + /** + * Sets an attribute given its node + * + * @param attr The attribute node to set + * + * @return The previous attribute node for the attribute + */ + public setAttributeNodeNS (attr: Attr): Attr | null { + return setAttribute(attr, this); + } - // No need to trigger observers if the value doesn't actually change - if (value === oldValue) { - return; + /** + * Removes an attribute given its node + * + * @param attr The attribute node to remove + * + * @return The removed attribute node + */ + public removeAttributeNode (attr: Attr): Attr { + // 1. If context object’s attribute list does not contain attr, then throw a NotFoundError. + if (this.attributes.indexOf(attr) < 0) { + throwNotFoundError('the specified attribute does not exist') } - // Queue a mutation record - const record = new MutationRecord('attributes', this); - record.attributeName = name; - record.oldValue = oldValue; - queueMutationRecord(record); + // 2. Remove attr from context object. + removeAttribute(attr, this); - // Set value - if (oldAttr) { - oldAttr.value = value; - } - else { - this._attrByName[name] = newAttr; - this.attributes.push(newAttr); - } + // 3. Return attr. + return attr; } /** - * Removes the attribute with the given name. + * (non-standard) Creates a copy of the given node + * + * @param document The node document to associate with the copy + * @param other The node to copy * - * @param name Name of the attribute + * @return A shallow copy of the node */ - public removeAttribute (name: string) { - const attr = this._attrByName[name]; - if (!attr) { - return; + public _copy (document: Document): Element { + // 2.1. Let copy be the result of creating an element, given document, node’s local name, node’s namespace, + // node’s namespace prefix, and the value of node’s is attribute if present (or null if not). The synchronous + // custom elements flag should be unset. + const copyElement = createElement(document, this.localName, this.namespaceURI, this.prefix); + + // 2.2. For each attribute in node’s attribute list: + for (const attr of this.attributes) { + // 2.2.1. Let copyAttribute be a clone of attribute. + const copyAttribute = attr._copy(document); + + // 2.2.2. Append copyAttribute to copy. + copyElement.setAttributeNode(copyAttribute); } - // Queue mutation record - const record = new MutationRecord('attributes', this); - record.attributeName = attr.name; - record.oldValue = attr.value; - queueMutationRecord(record); + return copyElement; + } +} - // Remove the attribute - delete this._attrByName[name]; - const attrIndex = this.attributes.indexOf(attr); - this.attributes.splice(attrIndex, 1); +/** + * To create an element, given a document, localName, namespace, and optional prefix, is, and synchronous custom + * elements flag, run these steps: + * + * @param document The node document for the new element + * @param localName The local name for the new element + * @param namespace The namespace URI for the new element, or null for the null namespace + * @param prefix The prefix for the new element, or null for no prefix + * + * @return The new element + */ +export function createElement (document: Document, localName: string, namespace: string | null, prefix: string | null = null): Element { + // 1. If prefix was not given, let prefix be null. + // (handled by default) + + // 2. If is was not given, let is be null. + // (custom elements not implemented) + + // 3. Let result be null. + let result = null; + + // 4. Let definition be the result of looking up a custom element definition given document, namespace, localName, + // and is. + // (custom elements not implemented) + + // 5. If definition is non-null, and definition’s name is not equal to its local name (i.e., definition represents a + // customized built-in element), then: + // 5.1. Let interface be the element interface for localName and the HTML namespace. + // 5.2. Set result to a new element that implements interface, with no attributes, namespace set to the HTML + // namespace, namespace prefix set to prefix, local name set to localName, custom element state set to "undefined", + // custom element definition set to null, is value set to is, and node document set to document. + // 5.3. If the synchronous custom elements flag is set, upgrade element using definition. + // 5.4. Otherwise, enqueue a custom element upgrade reaction given result and definition. + // (custom elements not implemented) + + // 6. Otherwise, if definition is non-null, then: + // 6.1. If the synchronous custom elements flag is set, then run these steps while catching any exceptions: + // 6.1.1. Let C be definition’s constructor. + // 6.1.2. Set result to the result of constructing C, with no arguments. + // 6.1.3. If result does not implement the HTMLElement interface, then throw a TypeError. + // This is meant to be a brand check to ensure that the object was allocated by the HTML element constructor. See + // webidl #97 about making this more precise. + // If this check passes, then result will already have its custom element state and custom element definition + // initialized. + // 6.1.4. If result’s attribute list is not empty, then throw a NotSupportedError. + // 6.1.5. If result has children, then throw a NotSupportedError. + // 6.1.6. If result’s parent is not null, then throw a NotSupportedError. + // 6.1.7. If result’s node document is not document, then throw a NotSupportedError. + // 6.1.8. If result’s namespace is not the HTML namespace, then throw a NotSupportedError. + // As of the time of this writing, every element that implements the HTMLElement interface is also in the HTML + // namespace, so this check is currently redundant with the above brand check. However, this is not guaranteed to be + // true forever in the face of potential specification changes, such as converging certain SVG and HTML interfaces. + // 6.1.9. If result’s local name is not equal to localName, then throw a NotSupportedError. + // 6.1.10. Set result’s namespace prefix to prefix. + // 6.1.11. Set result’s is value to null. + // If any of these steps threw an exception, then: + // 6.1.catch.1. Report the exception. + // 6.1.catch.2. Set result to a new element that implements the HTMLUnknownElement interface, with no attributes, + // namespace set to the HTML namespace, namespace prefix set to prefix, local name set to localName, custom element + // state set to "failed", custom element definition set to null, is value set to null, and node document set to + // document. + // 6.2. Otherwise: + // 6.2.1. Set result to a new element that implements the HTMLElement interface, with no attributes, namespace set + // to the HTML namespace, namespace prefix set to prefix, local name set to localName, custom element state set to + // "undefined", custom element definition set to null, is value set to null, and node document set to document. + // 6.2.2. Enqueue a custom element upgrade reaction given result and definition. + // (custom elements not implemented) + + // 7. Otherwise: + // 7.1. Let interface be the element interface for localName and namespace. + // (interfaces other than Element not implemented) + + // 7.2. Set result to a new element that implements interface, with no attributes, namespace set to namespace, + // namespace prefix set to prefix, local name set to localName, custom element state set to "uncustomized", custom + // element definition set to null, is value set to is, and node document set to document. + result = new Element(document, namespace, prefix, localName); + + // If namespace is the HTML namespace, and either localName is a valid custom element name or is is non-null, then + // set result’s custom element state to "undefined". + // (custom elements not implemented) + + // Return result. + return result; +} + +/** + * To get an attribute by name given a qualifiedName and element element, run these steps: + * + * @param qualifiedName The qualified name of the attribute to get + * @param element The element to get the attribute on + * + * @return The first matching attribute, or null otherwise + */ +function getAttributeByName (qualifiedName: string, element: Element): Attr | null { + // 1. If element is in the HTML namespace and its node document is an HTML document, then set qualifiedName to + // qualifiedName in ASCII lowercase. + // (html documents not implemented) + + // 2. Return the first attribute in element’s attribute list whose qualified name is qualifiedName, and null + // otherwise. + return element.attributes.find(attr => attr.name === qualifiedName) || null; +} + +/** + * To get an attribute by namespace and local name given a namespace, localName, and element element, run these steps: + * + * @param namespace Namespace for the attribute + * @param localName Local name for the attribute + * @param element The element to get the attribute on + * + * @return The first matching attribute, or null otherwise + */ +function getAttributeByNamespaceAndLocalName (namespace: string | null, localName: string, element: Element): Attr | null { + // 1. If namespace is the empty string, set it to null. + if (namespace === '') { + namespace = null; } - public cloneNode (deep: boolean = true, copy?: Element): Element { - const copyElement = copy as Element || new Element(this.nodeName); + // 2. Return the attribute in element’s attribute list whose namespace is namespace and local name is localName, if + // any, and null otherwise. + return element.attributes.find(attr => attr.namespaceURI === namespace && attr.localName === localName) || null; +} - // Copy attributes - this.attributes.forEach(attr => copyElement.setAttribute(attr.name, attr.value)); +/** + * To get an attribute value given an element element, localName, and optionally a namespace (null unless stated + * otherwise), run these steps: + * + * @param element The element to get the attribute on + * @param localName The local name of the attribute + * @param namespace The namespace of the attribute + * + * @return The value of the first matching attribute, or the empty string if no such attribute exists + */ +function getAttributeValue (element: Element, localName: string, namespace: string | null = null): string { + // 1. Let attr be the result of getting an attribute given namespace, localName, and element. + const attr = getAttributeByNamespaceAndLocalName(namespace, localName, element); - return super.cloneNode(deep, copyElement) as Element; + // 2. If attr is null, then return the empty string. + if (attr === null) { + return ''; } + + // 3. Return attr’s value. + return attr.value; +} + +/** + * To set an attribute given an attr and element, run these steps: + * + * @param attr The new attribute to set + * @param element The element to set attr on + * + * @return The previous attribute with attr's namespace and local name, or null if there was no such attribute + */ +function setAttribute (attr: Attr, element: Element): Attr | null { + // 1. If attr’s element is neither null nor element, throw an InUseAttributeError. + if (attr.ownerElement !== null && attr.ownerElement !== element) { + throwInUseAttributeError('attribute is in use by another element'); + } + + // 2. Let oldAttr be the result of getting an attribute given attr’s namespace, attr’s local name, and element. + const oldAttr = getAttributeByNamespaceAndLocalName(attr.namespaceURI, attr.localName, element); + + // 3. If oldAttr is attr, return attr. + if (oldAttr === attr) { + return attr; + } + + // 4. If oldAttr is non-null, replace it by attr in element. + if (oldAttr !== null) { + replaceAttribute(oldAttr, attr, element); + } + // 5. Otherwise, append attr to element. + else { + appendAttribute(attr, element); + } + + // 6. Return oldAttr. + return oldAttr; +} + +/** + * To set an attribute value for an element element using a localName and value, and an optional prefix, and namespace, + * run these steps: + * + * @param element Element to set the attribute value on + * @param localName Local name of the attribute + * @param value New value of the attribute + * @param prefix Prefix of the attribute + * @param namespace Namespace of the attribute + */ +function setAttributeValue (element: Element, localName: string, value: string, prefix: string | null = null, namespace: string | null = null): void { + // 1. If prefix is not given, set it to null. + // 2. If namespace is not given, set it to null. + // (handled by default values) + + // 3. Let attribute be the result of getting an attribute given namespace, localName, and element. + const attribute = getAttributeByNamespaceAndLocalName(namespace, localName, element); + + // 4. If attribute is null, create an attribute whose namespace is namespace, namespace prefix is prefix, local name + // is localName, value is value, and node document is element’s node document, then append this attribute to + // element, and then return. + if (attribute === null) { + const attribute = new Attr(element.ownerDocument!, namespace, prefix, localName, value, element); + appendAttribute(attribute, element); + return; + } + + // 5. Change attribute from element to value. + changeAttribute(attribute, element, value); +} + +/** + * To remove an attribute by name given a qualifiedName and element element, run these steps: + * + * @param qualifiedName Qualified name of the attribute + * @param element The element to remove the attribute from + * + * @return The removed attribute, or null if no matching attribute exists + */ +function removeAttributeByName (qualifiedName: string, element: Element): Attr | null { + // 1. Let attr be the result of getting an attribute given qualifiedName and element. + const attr = getAttributeByName(qualifiedName, element); + + // 2. If attr is non-null, remove it from element. + if (attr !== null) { + removeAttribute(attr, element); + } + + // 3. Return attr. + return attr; +} + +/** + * To remove an attribute by namespace and local name given a namespace, localName, and element element, run these + * steps: + * + * @param namespace The namespace of the attribute + * @param localName The local name of the attribute + * @param element The element to remove the attribute from + * + * @return The removed attribute, or null if no matching attribute exists + */ +function removeAttributeByNamespaceAndLocalName (namespace: string | null, localName: string, element: Element): Attr | null { + // 1. Let attr be the result of getting an attribute given namespace, localName, and element. + const attr = getAttributeByNamespaceAndLocalName(namespace, localName, element); + + // 2. If attr is non-null, remove it from element. + if (attr !== null) { + removeAttribute(attr, element); + } + + // 3. Return attr. + return attr; } diff --git a/src/Node.ts b/src/Node.ts index aa96838..9475335 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -1,44 +1,44 @@ +import Element from './Element'; import Document from './Document'; import Text from './Text'; - -import MutationRecord from './mutations/MutationRecord'; -import RegisteredObservers from './mutations/RegisteredObservers'; -import queueMutationRecord from './mutations/queueMutationRecord'; - -import { getNodeIndex } from './util'; +import { ranges } from './Range'; +import RegisteredObservers from './mutation-observer/RegisteredObservers'; +import cloneNode from './util/cloneNode'; +import { preInsertNode, appendNode, replaceChildWithNode, preRemoveChild, removeNode } from './util/mutationAlgorithms'; +import { NodeType, isNodeOfType } from './util/NodeType'; +import { getNodeDocument } from './util/treeHelpers'; /** - * Internal helper used to adopt a given node into a given document. - * - * @param node Node to adopt - * @param document Document to adopt node into + * 3.4. Interface Node */ -function adopt (node: Node, document: Document) { - node.ownerDocument = document; - node.childNodes.forEach(child => adopt(child, document)); -} +export default abstract class Node { + static ELEMENT_NODE: number = NodeType.ELEMENT_NODE; + static ATTRIBUTE_NODE: number = NodeType.ATTRIBUTE_NODE; + static TEXT_NODE: number = NodeType.TEXT_NODE; + static CDATA_SECTION_NODE: number = NodeType.CDATA_SECTION_NODE; + static ENTITY_REFERENCE_NODE: number = NodeType.ENTITY_REFERENCE_NODE; // historical + static ENTITY_NODE: number = NodeType.ENTITY_NODE; // historical + static PROCESSING_INSTRUCTION_NODE: number = NodeType.PROCESSING_INSTRUCTION_NODE; + static COMMENT_NODE: number = NodeType.COMMENT_NODE; + static DOCUMENT_NODE: number = NodeType.DOCUMENT_NODE; + static DOCUMENT_TYPE_NODE: number = NodeType.DOCUMENT_TYPE_NODE; + static DOCUMENT_FRAGMENT_NODE: number = NodeType.DOCUMENT_FRAGMENT_NODE; + static NOTATION_NODE: number = NodeType.NOTATION_NODE; // historical -interface UserDataEntry { - name: string, - value: any -} + /** + * Returns the type of node, represented by a number. + */ + public abstract get nodeType (): number; -/** - * A Node is a class from which a number of DOM types inherit, and allows these various types to be treated - * (or tested) similarly. - */ -export default abstract class Node { - static ELEMENT_NODE = 1; - static TEXT_NODE = 3; - static PROCESSING_INSTRUCTION_NODE = 7; - static COMMENT_NODE = 8; - static DOCUMENT_NODE = 9; - static DOCUMENT_TYPE_NODE = 10; + /** + * Returns a string appropriate for the type of node. + */ + public abstract get nodeName (): string; /** - * An integer representing the type of the node. + * A reference to the Document node in which the current node resides. */ - public nodeType: number; + public ownerDocument: Document | null = null; /** * The parent node of the current node. @@ -46,493 +46,262 @@ export default abstract class Node { public parentNode: Node | null = null; /** - * The next sibling node of the current node (on the right, could be a Text node). + * The parent if it is an element, or null otherwise. */ - public nextSibling: Node | null = null; + public get parentElement (): Element | null { + return this.parentNode && isNodeOfType(this.parentNode, NodeType.ELEMENT_NODE) ? this.parentNode as Element : null; + } /** - * The next sibling node of the current node (on the left, could be a Text node). + * Returns true if the context object has children, and false otherwise. */ - public previousSibling: Node | null = null; + public hasChildNodes (): boolean { + return !!this.childNodes.length; + } /** - * A list of childNodes (including Text nodes) of this node. + * The node's children. + * + * Non-standard: implemented as an array rather than a NodeList. */ public childNodes: Node[] = []; /** - * The first child node of the current node. + * The first child node of the current node, or null if it has no children. */ public firstChild: Node | null = null; /** - * The last child node of the current node. + * The last child node of the current node, or null if it has no children. */ public lastChild: Node | null = null; /** - * A reference to the Document node in which the current node resides. + * The first preceding sibling of the current node, or null if it has none. */ - public ownerDocument: Document | null = null; - - // User data, use get/setUserData to access - private _userData: UserDataEntry[] = []; - private _userDataByKey: { [key: string]: UserDataEntry } = {}; - - // (internal) Registered mutation observers, use MutationObserver interface to manipulate - public _registeredObservers: RegisteredObservers; - - /** - * @param type NodeType for the node - */ - constructor (type: number) { - this.nodeType = type; - this._registeredObservers = new RegisteredObservers(this); - } + public previousSibling: Node | null = null; /** - * Internal helper used to update the firstChild and lastChild references. + * The first following sibling of the current node, or null if it has none. */ - private _updateFirstLast () { - this.firstChild = this.childNodes[0] || null; - this.lastChild = this.childNodes[this.childNodes.length - 1] || null; - } + public nextSibling: Node | null = null; /** - * Internal helper used to update the nextSibling and previousSibling references. + * The value of the node. */ - private _updateSiblings (index: number) { - if (!this.parentNode) { - // Node has been removed - if (this.nextSibling) { - this.nextSibling.previousSibling = this.previousSibling; - } - if (this.previousSibling) { - this.previousSibling.nextSibling = this.nextSibling; - } - this.nextSibling = null; - this.previousSibling = null; - return; - } - - this.nextSibling = this.parentNode.childNodes[index + 1] || null; - this.previousSibling = this.parentNode.childNodes[index - 1] || null; - - if (this.nextSibling) { - this.nextSibling.previousSibling = this; - } - if (this.previousSibling) { - this.previousSibling.nextSibling = this; - } - } + public abstract get nodeValue (): string | null; + public abstract set nodeValue (value: string | null); /** - * Adds a node to the end of the list of children of a specified parent node. - * If the node already exists it is removed from current parent node, then added to new parent node. - * - * @param childNode Node to append - * - * @return The node that was inserted + * (non-standard) Each node has an associated list of registered observers. */ - public appendChild (childNode: Node): Node | null { - return this.insertBefore(childNode, null); - } + public _registeredObservers: RegisteredObservers = new RegisteredObservers(this); /** - * Indicates whether the given node is a descendant of the current node. - * - * @param childNode Node to check + * (non-standard) Node should never be instantiated directly. * - * @return Whether childNode is an inclusive descendant of the current node + * @param document The node document to associate with the node */ - public contains (childNode: Node | null): boolean { - while (childNode && childNode != this) { - childNode = childNode.parentNode; - } - return childNode === this; + constructor (document: Document | null) { + this.ownerDocument = document; } /** - * Inserts the specified node before a reference node as a child of the current node. - * If referenceNode is null, the new node is appended after the last child node of the current node. - * - * @param newNode Node to insert - * @param referenceNode Childnode of the current node before which to insert, or null to append at the end - * @param suppressObservers (non-standard) Whether to enqueue a mutation record for the mutation - * - * @return The node that was inserted + * Puts the specified node and all of its subtree into a "normalized" form. In a normalized subtree, no text nodes + * in the subtree are empty and there are no adjacent text nodes. */ - public insertBefore (newNode: Node, referenceNode: Node | null, suppressObservers: boolean = false): Node | null { - // Check if referenceNode is a child - if (referenceNode && referenceNode.parentNode !== this) { - return null; - } - - // Fix using the new node as a reference - if (referenceNode === newNode) { - referenceNode = newNode.nextSibling; - } - - // Already there? - if (newNode.parentNode === this && newNode.nextSibling === referenceNode) { - return newNode; - } - - // Detach from old parent - if (newNode.parentNode) { - // This removal is never suppressed - newNode.parentNode.removeChild(newNode, false); - } - - // Adopt nodes into document - const ownerDocument = this instanceof Document ? this : this.ownerDocument as Document; - if (newNode.ownerDocument !== ownerDocument) { - adopt(newNode, ownerDocument); - } + public normalize (): void { + // for each descendant exclusive Text node node of context object: + let node = this.firstChild; + let index = 0; + const document = getNodeDocument(this); + while (node) { + let nextNode = node.nextSibling; + if (!isNodeOfType(node, NodeType.TEXT_NODE)) { + // Process descendants + node.normalize(); + node = nextNode; + continue; + } - // Check index of reference node - const index = referenceNode ? getNodeIndex(referenceNode) : this.childNodes.length; - if (index < 0) { - return null; - } + const textNode = node as Text; + // 1. Let length be node’s length. + let length = textNode.length; - // Update ranges - ownerDocument._ranges.forEach(range => { - if (range.startContainer === this && range.startOffset > index) { - range.startOffset += 1; + // 2. If length is zero, then remove node and continue with the next exclusive Text node, if any. + if (length === 0) { + removeNode(node, this); + --index; + node = nextNode; + continue; } - if (range.endContainer === this && range.endOffset > index) { - range.endOffset += 1; + + // 3. Let data be the concatenation of the data of node’s contiguous exclusive Text nodes (excluding + // itself), in tree order. + let data = ''; + const siblingsToRemove = []; + for (let sibling = textNode.nextSibling; + sibling && isNodeOfType(sibling, NodeType.TEXT_NODE); + sibling = sibling.nextSibling + ) { + data += (sibling as Text).data; + siblingsToRemove.push(sibling); } - }); - - // Queue mutation record - if (!suppressObservers) { - const record = new MutationRecord('childList', this); - record.addedNodes.push(newNode); - record.nextSibling = referenceNode; - record.previousSibling = referenceNode ? referenceNode.previousSibling : this.lastChild; - queueMutationRecord(record); - } - // Insert the node - newNode.parentNode = this; - this.childNodes.splice(index, 0, newNode); - this._updateFirstLast(); - newNode._updateSiblings(index); + // 4. Replace data with node node, offset length, count 0, and data data. + if (data) { + textNode.replaceData(length, 0, data); + } - return newNode; - } + // 5. Let currentNode be node’s next sibling. + // 6. While currentNode is an exclusive Text node: + for (let i = 0, l = siblingsToRemove.length; i < l; ++i) { + const currentNode = siblingsToRemove[i]; + const currentNodeIndex = index + i + 1; + + ranges.forEach(range => { + // 6.1. For each range whose start node is currentNode, add length to its start offset and set its + // start node to node. + if (range.startContainer === currentNode) { + range.startOffset += length; + range.startContainer = textNode; + } - /** - * Puts the specified node and all of its subtree into a "normalized" form. - * In a normalized subtree, no text nodes in the subtree are empty and there are no adjacent text nodes. - * - * @param recurse Whether to also normalize all descendants of the current node - */ - public normalize (recurse: boolean = true) { - let childNode = this.firstChild; - let index = 0; - const document = this instanceof Document ? this : this.ownerDocument as Document; - while (childNode) { - let nextNode = childNode.nextSibling; - if (childNode.nodeType === Node.TEXT_NODE) { - const textChildNode = childNode as Text; - - // Delete empty text nodes - let length = textChildNode.length; - if (!length) { - this.removeChild(childNode); - --index; - } - else { - // Concatenate and collect childNode's contiguous text nodes (excluding current) - let data = ''; - const siblingsToRemove = []; - let siblingIndex: number; - let sibling: Node | null; - for (sibling = childNode.nextSibling, siblingIndex = index; - sibling && sibling.nodeType == Node.TEXT_NODE; - sibling = sibling.nextSibling, ++siblingIndex - ) { - data += (sibling as Text).data; - siblingsToRemove.push(sibling); + // 6.2. For each range whose end node is currentNode, add length to its end offset and set its end + // node to node. + if (range.endContainer === currentNode) { + range.endOffset += length; + range.endContainer = textNode; } - // Append concatenated data, if any - if (data) { - textChildNode.appendData(data); + // 6.3. For each range whose start node is currentNode’s parent and start offset is currentNode’s + // index, set its start node to node and its start offset to length. + if (range.startContainer === this && range.startOffset === currentNodeIndex) { + range.startContainer = textNode; + range.startOffset = length; } - // Fix ranges - for (sibling = childNode.nextSibling, siblingIndex = index + 1; - sibling && sibling.nodeType == Node.TEXT_NODE; - sibling = sibling.nextSibling, ++siblingIndex) { - - document._ranges.forEach(range => { - if (range.startContainer === sibling) { - range.setStart(childNode as Node, length + range.startOffset); - } - if (range.startContainer === this && range.startOffset == siblingIndex) { - range.setStart(childNode as Node, length); - } - if (range.endContainer === sibling) { - range.setEnd(childNode as Node, length + range.endOffset); - } - if (range.endContainer === this && range.endOffset == siblingIndex) { - range.setEnd(childNode as Node, length); - } - }); - - length += (sibling as Text).length; - }; - - // Remove contiguous text nodes (excluding current) in tree order - while (siblingsToRemove.length) { - this.removeChild(siblingsToRemove.shift() as Node); + // 6.4. For each range whose end node is currentNode’s parent and end offset is currentNode’s index, + // set its end node to node and its end offset to length. + if (range.endContainer === this && range.endOffset === currentNodeIndex) { + range.endContainer = textNode; + range.endOffset = length; } + }); - // Update next node to process - nextNode = childNode.nextSibling; - } + // 6.5. Add currentNode’s length to length. + length += (currentNode as Text).length; + + // 6.6. Set currentNode to its next sibling. + // (see for-loop increment) } - else if (recurse) { - // Recurse - childNode.normalize(); + + // 7. Remove node’s contiguous exclusive Text nodes (excluding itself), in tree order. + while (siblingsToRemove.length) { + removeNode(siblingsToRemove.shift() as Node, this); } // Move to next node - childNode = nextNode; + node = node.nextSibling; ++index; } } /** - * Removes a child node from the DOM and returns the removed node. + * Returns a copy of the current node. * - * @param childNode Child of the current node to remove - * @param suppressObservers (non-standard) Whether to enqueue a mutation record for the mutation + * @param deep Whether to also clone the node's descendants * - * @return The node that was removed + * @return A copy of the current node */ - public removeChild (childNode: Node, suppressObservers: boolean = false): Node | null { - // Check if childNode is a child - if (childNode.parentNode !== this) { - return null; - } - - // Check index of node - const index = getNodeIndex(childNode); - if (index < 0) { - return null; - } - - // Update ranges - const document = this instanceof Document ? this : this.ownerDocument as Document; - document._ranges.forEach(range => { - if (childNode.contains(range.startContainer)) { - range.setStart(this, index); - } - if (childNode.contains(range.endContainer)) { - range.setEnd(this, index); - } - if (range.startContainer === this && range.startOffset > index) { - range.startOffset -= 1; - } - if (range.endContainer === this && range.endOffset > index) { - range.endOffset -= 1; - } - }); - - // Queue mutation record - if (!suppressObservers) { - const record = new MutationRecord('childList', this); - record.removedNodes.push(childNode); - record.nextSibling = childNode.nextSibling; - record.previousSibling = childNode.previousSibling; - queueMutationRecord(record); - } - - // Add transient registered observers to detect changes in the removed subtree - for (let ancestor: Node | null = this; ancestor; ancestor = ancestor.parentNode) { - childNode._registeredObservers.appendTransientsForAncestor(ancestor._registeredObservers); - } - - // Remove the node - childNode.parentNode = null; - this.childNodes.splice(index, 1); - this._updateFirstLast(); - childNode._updateSiblings(index); - - return childNode; + public cloneNode (deep: boolean = false): Node { + return cloneNode(this, deep); } /** - * Replaces the given oldChild node with the given newChild node and returns the node that was replaced - * (i.e. oldChild). + * Returns true if other is an inclusive descendant of context object, and false otherwise (including when other is + * null). * - * @param newChild Node to insert - * @param oldChild Node to remove + * @param childNode Node to check * - * @return The node that was removed + * @return Whether childNode is an inclusive descendant of the current node */ - public replaceChild (newChild: Node, oldChild: Node): Node | null { - // Check if oldChild is a child - if (oldChild.parentNode !== this) { - return null; + public contains (other: Node | null): boolean { + while (other && other != this) { + other = other.parentNode; } - - // Already there? - if (newChild === oldChild) { - return oldChild; - } - - // Get reference node for insert - let referenceNode = oldChild.nextSibling; - if (referenceNode === newChild) { - referenceNode = newChild.nextSibling; - } - - // Detach from old parent - if (newChild.parentNode) { - // This removal is never suppressed - newChild.parentNode.removeChild(newChild, false); - } - - // Adopt nodes into document - const ownerDocument = this instanceof Document ? this : this.ownerDocument as Document; - if (newChild.ownerDocument !== ownerDocument) { - adopt(newChild, ownerDocument); - } - - // Create mutation record - const record = new MutationRecord('childList', this); - record.addedNodes.push(newChild); - record.removedNodes.push(oldChild); - record.nextSibling = referenceNode; - record.previousSibling = oldChild.previousSibling; - - // Remove old child - this.removeChild(oldChild, true); - - // Insert new child - this.insertBefore(newChild, referenceNode, true); - - // Queue mutation record - queueMutationRecord(record); - - return oldChild; + return other === this; } /** - * Retrieves the object associated to a key on this node. + * Inserts the specified node before child within context object. * - * @param key Key under which the value is stored + * If child is null, the new node is appended after the last child node of the current node. * - * @return The associated value, or null of none exists + * @param node Node to insert + * @param child Childnode of the current node before which to insert, or null to append newNode at the end + * + * @return The node that was inserted */ - public getUserData (key: string): any | null { - const data = this._userDataByKey[key]; - if (data === undefined) { - return null; - } - - return data.value; + public insertBefore (node: Node, child: Node | null): Node { + return preInsertNode(node, this, child); } /** - * Retrieves the object associated to a key on this node. User data allows a user to attach (or remove) data to - * an element, without needing to modify the DOM. Note that such data will not be preserved when imported via - * Node.importNode, as with Node.cloneNode() and Node.renameNode() operations (though Node.adoptNode does - * preserve the information), and equality tests in Node.isEqualNode() do not consider user data in making the - * assessment. + * Adds node to the end of the list of children of the context object. * - * This method offers the convenience of associating data with specific nodes without needing to alter the - * structure of a document and in a standard fashion, but it also means that extra steps may need to be taken - * if one wishes to serialize the information or include the information upon clone, import, or rename - * operations. + * If the node already exists it is removed from its current parent node, then added. * - * @param key Key under which the value is stored - * @param data Data to store + * @param node Node to append * - * @return Previous data associated with the key, or null if none existed + * @return The node that was inserted */ - public setUserData (key: string, data: any = undefined) { - const oldData = this._userDataByKey[key]; - const newData = { - name: key, - value: data - }; - let oldValue = null; - - // No need to trigger observers if the value doesn't actually change - if (oldData) { - oldValue = oldData.value; - if (oldValue === data) { - return oldValue; - } - - if (data === undefined || data === null) { - // Remove user data - delete this._userDataByKey[key]; - const oldDataIndex = this._userData.indexOf(oldData); - this._userData.splice(oldDataIndex, 1); - } - else { - // Overwrite data - oldData.value = data; - } - } - else { - this._userDataByKey[key] = newData; - this._userData.push(newData); - } - - // Queue a mutation record (non-standard, but useful) - const record = new MutationRecord('userData', this); - record.attributeName = key; - record.oldValue = oldValue; - queueMutationRecord(record); - - return oldValue; + public appendChild (node: Node): Node { + return appendNode(node, this); } /** - * Returns a copy of the current node. - * Override on subclasses and pass a shallow copy of the node in the 'copy' parameter (I.e. they create a new - * instance of their class with their specific constructor parameters.) + * Replaces child with node within context object and returns child. * - * @param deep Whether to also clone the node's descendants - * @param copy (non-standard) Copy to populate + * @param node Node to insert + * @param child Node to remove * - * @return A copy of the current node + * @return The node that was removed */ - public cloneNode (deep: boolean = true, copy?: Node): Node | null { - if (!copy) { - return null; - } - - // Set owner document - if (copy.nodeType !== Node.DOCUMENT_NODE) { - copy.ownerDocument = this.ownerDocument; - } - - // User data is not copied, it is assumed to apply only to the original instance - - // Recurse if required - if (deep) { - for (let child = this.firstChild; child; child = child.nextSibling) { - copy.appendChild(child.cloneNode(true) as Node); - } - } + public replaceChild (node: Node, child: Node): Node { + return replaceChildWithNode(child, node, this); + } - return copy; + /** + * Removes child from context object and returns the removed node. + * + * @param child Child of the current node to remove + * + * @return The node that was removed + */ + public removeChild (child: Node): Node { + return preRemoveChild(child, this); } + + /** + * (non-standard) Creates a copy of the context object, not including its children. + * + * @param document The node document to associate with the copy + * + * @return A shallow copy of the context object + */ + public abstract _copy (document: Document): Node; } -(Node.prototype as any).ELEMENT_NODE = 1; -(Node.prototype as any).TEXT_NODE = 3; -(Node.prototype as any).PROCESSING_INSTRUCTION_NODE = 7; -(Node.prototype as any).COMMENT_NODE = 8; -(Node.prototype as any).DOCUMENT_NODE = 9; -(Node.prototype as any).DOCUMENT_TYPE_NODE = 10; +(Node.prototype as any).ELEMENT_NODE = NodeType.ELEMENT_NODE; +(Node.prototype as any).ATTRIBUTE_NODE = NodeType.ATTRIBUTE_NODE; +(Node.prototype as any).TEXT_NODE = NodeType.TEXT_NODE; +(Node.prototype as any).CDATA_SECTION_NODE = NodeType.CDATA_SECTION_NODE; +(Node.prototype as any).ENTITY_REFERENCE_NODE = NodeType.ENTITY_REFERENCE_NODE; // historical +(Node.prototype as any).ENTITY_NODE = NodeType.ENTITY_NODE; // historical +(Node.prototype as any).PROCESSING_INSTRUCTION_NODE = NodeType.PROCESSING_INSTRUCTION_NODE; +(Node.prototype as any).COMMENT_NODE = NodeType.COMMENT_NODE; +(Node.prototype as any).DOCUMENT_NODE = NodeType.DOCUMENT_NODE; +(Node.prototype as any).DOCUMENT_TYPE_NODE = NodeType.DOCUMENT_TYPE_NODE; +(Node.prototype as any).DOCUMENT_FRAGMENT_NODE = NodeType.DOCUMENT_FRAGMENT_NODE; +(Node.prototype as any).NOTATION_NODE = NodeType.NOTATION_NODE; // historical diff --git a/src/ProcessingInstruction.ts b/src/ProcessingInstruction.ts index d57bb6e..bcbd4b7 100644 --- a/src/ProcessingInstruction.ts +++ b/src/ProcessingInstruction.ts @@ -1,35 +1,45 @@ import CharacterData from './CharacterData'; -import Node from './Node'; +import Document from './Document'; +import { NodeType } from './util/NodeType'; /** - * A processing instruction provides an opportunity for application-specific instructions to be embedded within - * XML and which can be ignored by XML processors which do not support processing their instructions (outside - * of their having a place in the DOM). - * - * A Processing instruction is distinct from a XML Declaration which is used for other information about the - * document such as encoding and which appear (if it does) as the first item in the document. - * - * User-defined processing instructions cannot begin with 'xml', as these are reserved (e.g., as used in - * ). + * 3.13. Interface ProcessingInstruction */ export default class ProcessingInstruction extends CharacterData { - /** - * The string that goes after the determineLengthOfNode(node)) { + throwIndexSizeError('Can not set a range past the end of the node'); + } + + // 3. Let bp be the boundary point (node, offset). + // 4.a. If these steps were invoked as "set the start" + // 4.a.1. If bp is after the range’s end, or if range’s root is not equal to node’s root, set range’s end to bp. + const rootOfNode = getRootOfNode(node); + const rootOfRange = getRootOfRange(this); + if ( + rootOfNode !== rootOfRange || + compareBoundaryPointPositions(node, offset, this.endContainer, this.endOffset) === POSITION_AFTER + ) { + this.endContainer = node; + this.endOffset = offset; + } + // 4.a.2. Set range’s start to bp. + this.startContainer = node; + this.startOffset = offset; + + // 4.b. If these steps were invoked as "set the end" + // 4.b.1. If bp is before the range’s start, or if range’s root is not equal to node’s root, set range’s start + // to bp. + // 4.b.2. Set range’s end to bp. + // (see Range#setEnd for this branch) + } + + /** + * Sets the end boundary point of the range. + * + * @param node The new end container + * @param offset The new end offset + */ + setEnd (node: Node, offset: number): void { + // 1. If node is a doctype, then throw an InvalidNodeTypeError. + if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { + throwInvalidNodeTypeError('Can not set a range under a doctype node'); + } + + // 2. If offset is greater than node’s length, then throw an IndexSizeError. + if (offset > determineLengthOfNode(node)) { + throwIndexSizeError('Can not set a range past the end of the node'); + } + + // 3. Let bp be the boundary point (node, offset). + // 4.a. If these steps were invoked as "set the start" + // 4.a.1. If bp is after the range’s end, or if range’s root is not equal to node’s root, set range’s end to bp. + // 4.a.2. Set range’s start to bp. + // (see Range#setStart for this branch) + + // 4.b. If these steps were invoked as "set the end" + // 4.b.1. If bp is before the range’s start, or if range’s root is not equal to node’s root, set range’s start + // to bp. + const rootOfNode = getRootOfNode(node); + const rootOfRange = getRootOfRange(this); + if ( + rootOfNode !== rootOfRange || + compareBoundaryPointPositions(node, offset, this.endContainer, this.endOffset) === POSITION_BEFORE + ) { + this.startContainer = node; + this.startOffset = offset; + } + // 4.b.2. Set range’s end to bp. + this.endContainer = node; + this.endOffset = offset; + } + + /** + * Sets the start boundary point of the range to the position just before the given node. + * + * @param node The node to set the range's start before + */ + setStartBefore (node: Node): void { + // 1. Let parent be node’s parent. + const parent = node.parentNode; + + // 2. If parent is null, then throw an InvalidNodeTypeError. + if (parent === null) { + return throwInvalidNodeTypeError('Can not set range before node without a parent'); + } + + // 3. Set the start of the context object to boundary point (parent, node’s index). + this.setStart(parent, getNodeIndex(node)); + } + + /** + * Sets the start boundary point of the range to the position just after the given node. + * + * @param node The node to set the range's start before + */ + setStartAfter (node: Node): void { + // 1. Let parent be node’s parent. + const parent = node.parentNode; + + // 2. If parent is null, then throw an InvalidNodeTypeError. + if (parent === null) { + return throwInvalidNodeTypeError('Can not set range before node without a parent'); + } + + // 3. Set the start of the context object to boundary point (parent, node’s index plus one). + this.setStart(parent, getNodeIndex(node) + 1); + } + + /** + * Sets the end boundary point of the range to the position just before the given node. + * + * @param node The node to set the range's end before + */ + setEndBefore (node: Node): void { + // 1. Let parent be node’s parent. + const parent = node.parentNode; + + // 2. If parent is null, then throw an InvalidNodeTypeError. + if (parent === null) { + return throwInvalidNodeTypeError('Can not set range before node without a parent'); + } + + // 3. Set the end of the context object to boundary point (parent, node’s index). + this.setEnd(parent, getNodeIndex(node)); + } + + + /** + * Sets the end boundary point of the range to the position just after the given node. + * + * @param node The node to set the range's end before + */ + setEndAfter (node: Node): void { + // 1. Let parent be node’s parent. + const parent = node.parentNode; + + // 2. If parent is null, then throw an InvalidNodeTypeError. + if (parent === null) { + return throwInvalidNodeTypeError('Can not set range before node without a parent'); + } + + // 3. Set the end of the context object to boundary point (parent, node’s index plus one). + this.setEnd(parent, getNodeIndex(node) + 1); + } + + + /** + * Sets the range's boundary points to the same position. + * + * @param toStart If true, set both points to the start of the range, otherwise set them to the end + */ + collapse (toStart: boolean = false): void { + if (toStart) { + this.endContainer = this.startContainer; + this.endOffset = this.startOffset; + } + else { + this.startContainer = this.endContainer; + this.startOffset = this.endOffset; + } + } + + selectNode (node: Node): void { + // 1. Let parent be node’s parent. + let parent = node.parentNode; + + // 2. If parent is null, throw an InvalidNodeTypeError. + if (parent === null) { + return throwInvalidNodeTypeError('Can not select node with null parent'); + } + + // 3. Let index be node’s index. + const index = getNodeIndex(node); + + // 4. Set range’s start to boundary point (parent, index). + this.startContainer = parent; + this.startOffset = index; + + // 5. Set range’s end to boundary point (parent, index plus one). + this.endContainer = parent; + this.endOffset = index + 1; + } + + selectNodeContents (node: Node): void { + // 1. If node is a doctype, throw an InvalidNodeTypeError. + if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { + throwInvalidNodeTypeError('Can not place range inside a doctype node'); + } + + // 2. Let length be the length of node. + const length = determineLengthOfNode(node); + + // 3. Set start to the boundary point (node, 0). + this.startContainer = node; + this.startOffset = 0; + + // 4. Set end to the boundary point (node, length). + this.endContainer = node; + this.endOffset = length; + } + + static START_TO_START = 0; + static START_TO_END = 1; + static END_TO_END = 2; + static END_TO_START = 3; + + compareBoundaryPoints (how: number, sourceRange: Range): number { + // 1. If how is not one of START_TO_START, START_TO_END, END_TO_END, and END_TO_START, then throw a + // NotSupportedError. + if ( + how !== Range.START_TO_START && + how !== Range.START_TO_END && + how !== Range.END_TO_END && + how !== Range.END_TO_START + ) { + throwNotSupportedError('Unsupported comparison type'); + } + + // 2. If context object’s root is not the same as sourceRange’s root, then throw a WrongDocumentError. + if (getRootOfRange(this) !== getRootOfRange(sourceRange)) { + throwWrongDocumentError('Can not compare positions of ranges in different trees'); + } + + // 3. If how is: + switch (how) { + // START_TO_START: + case Range.START_TO_START: + // Let this point be the context object’s start. Let other point be sourceRange’s start. + return compareBoundaryPointPositions( + // this point + this.startContainer, + this.startOffset, + // other point + sourceRange.startContainer, + sourceRange.startOffset + ); + + // START_TO_END: + case Range.START_TO_END: + // Let this point be the context object’s end. Let other point be sourceRange’s start. + return compareBoundaryPointPositions( + // this point + this.endContainer, + this.endOffset, + // other point + sourceRange.startContainer, + sourceRange.startOffset + ); + + // END_TO_END: + case Range.END_TO_END: + // Let this point be the context object’s end. Let other point be sourceRange’s end. + return compareBoundaryPointPositions( + // this point + this.endContainer, + this.endOffset, + // other point + sourceRange.endContainer, + sourceRange.endOffset + ); + + // END_TO_START: + default: + // unreachable, fall through for type check + case Range.END_TO_START: + // Let this point be the context object’s start. Let other point be sourceRange’s end. + return compareBoundaryPointPositions( + // this point + this.startContainer, + this.startOffset, + // other point, + sourceRange.endContainer, + sourceRange.endOffset + ); + } + + // 4. If the position of this point relative to other point is + // before: Return −1. + // equal: Return 0. + // after: Return 1. + // (handled in switch above) + } + + /** + * Returns a range with the same start and end as the context object. + * + * @return A copy of the context object + */ + cloneRange (): Range { + const range = new Range(getNodeDocument(this.startContainer)); + range.startContainer = this.startContainer; + range.startOffset = this.startOffset; + range.endContainer = this.endContainer; + range.endOffset = this.endOffset; + return range; + } + + /** + * Stops tracking the range. + * + * (non-standard) According to the spec, this method must do nothing. However, as it is not possible to rely on + * garbage collection to determine when to stop updating a range for node mutations, this implementation requires + * calling detach to stop such updates from affecting the range. + */ + detach (): void { + const index = ranges.indexOf(this); + if (index >= 0) { + ranges.splice(index, 1); + } + } + + /** + * Returns true if the given point is after or equal to the start point and before or equal to the end point of the + * context object. + * + * @param node Node of point to check + * @param offset Offset of point to check + * + * @return Whether the point is in the range + */ + isPointInRange (node: Node, offset: number): boolean { + // 1. If node’s root is different from the context object’s root, return false. + if (getRootOfNode(node) !== getRootOfRange(this)) { + return false; + } + + // 2. If node is a doctype, then throw an InvalidNodeTypeError. + if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { + throwInvalidNodeTypeError('Point can not be under a doctype'); + } + + // 3. If offset is greater than node’s length, then throw an IndexSizeError. + if (offset > determineLengthOfNode(node)) { + throwIndexSizeError('Offset should not be past the end of node'); + } + + // 4. If (node, offset) is before start or after end, return false. + if ( + compareBoundaryPointPositions(node, offset, this.startContainer, this.startOffset) === POSITION_BEFORE || + compareBoundaryPointPositions(node, offset, this.endContainer, this.endOffset) === POSITION_AFTER + ) { + return false; + } + + // 5. Return true. + return true; + } + + /** + * Compares the given point to the range's boundary points. + * + * @param node Node of point to check + * @param offset Offset of point to check + * + * @return -1, 0 or 1 depending on whether the point is before, inside or after the range, respectively + */ + comparePoint (node: Node, offset: number): number { + // 1. If node’s root is different from the context object’s root, then throw a WrongDocumentError. + if (getRootOfNode(node) !== getRootOfRange(this)) { + throwWrongDocumentError('Can not compare point to range in different trees'); + } + + // 2. If node is a doctype, then throw an InvalidNodeTypeError. + if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { + throwInvalidNodeTypeError('Point can not be under a doctype'); + } + + // 3. If offset is greater than node’s length, then throw an IndexSizeError. + if (offset > determineLengthOfNode(node)) { + throwIndexSizeError('Offset should not be past the end of node'); + } + + // 4. If (node, offset) is before start, return −1. + if (compareBoundaryPointPositions(node, offset, this.startContainer, this.startOffset) === POSITION_BEFORE) { + return -1; + } + + // 5. If (node, offset) is after end, return 1. + if (compareBoundaryPointPositions(node, offset, this.endContainer, this.endOffset) === POSITION_AFTER) { + return 1; + } + + // 6. Return 0. + return 0; + } + + /** + * Returns true if range overlaps the range from before node to after node. + * + * @param node The node to check + * + * @return Whether the range intersects node + */ + intersectsNode (node: Node): boolean { + // 1. If node’s root is different from the context object’s root, return false. + if (getRootOfNode(node) !== getRootOfRange(this)) { + return false; + } + + // 2. Let parent be node’s parent. + const parent = node.parentNode; + + // 3. If parent is null, return true. + if (parent === null) { + return false; + } + + // 4. Let offset be node’s index. + const offset = getNodeIndex(node); + + // 5. If (parent, offset) is before end and (parent, offset + 1) is after start, return true. + // 6. Return false. + return compareBoundaryPointPositions(parent, offset, this.endContainer, this.endOffset) === POSITION_BEFORE && + compareBoundaryPointPositions(parent, offset + 1, this.startContainer, this.startOffset) === POSITION_AFTER; + } +} + +const POSITION_BEFORE = -1; +const POSITION_EQUAL = 0; +const POSITION_AFTER = 1; + +/** + * If the two nodes of boundary points (node A, offset A) and (node B, offset B) have the same root, the position of the + * first relative to the second is either before, equal, or after. + * + * Note: for efficiency reasons, this implementation deviates from the algorithm given in 4.2. + * + * @param nodeA First boundary point's node + * @param offsetA First boundary point's offset + * @param nodeB Second boundary point's node + * @param offsetB Second boundary point's offset + * + * @return -1, 0 or 1, depending on the boundary points' relative positions + */ +function compareBoundaryPointPositions (nodeA: Node, offsetA: number, nodeB: Node, offsetB: number): number { + if (nodeA !== nodeB) { + const ancestors1 = getInclusiveAncestors(nodeA); + const ancestors2 = getInclusiveAncestors(nodeB); + // This should not be called on nodes from different trees + if (ancestors1[0] !== ancestors2[0]) { + throw new Error('Can not compare positions of nodes from different trees.') + } + + // Skip common parents + while (ancestors1[0] && ancestors2[0] && ancestors1[0] === ancestors2[0]) { + ancestors1.shift(); + ancestors2.shift(); + } + + // Compute offsets at the level under the last common parent. Add 0.5 to bias positions inside the parent vs. + // those before or after. + if (ancestors1.length) { + offsetA = getNodeIndex(ancestors1[0]) + 0.5; + } + if (ancestors2.length) { + offsetB = getNodeIndex(ancestors2[0]) + 0.5; + } + } + + // Compare positions at this level + if (offsetA === offsetB) { + return POSITION_EQUAL; + } + return offsetA < offsetB ? POSITION_BEFORE : POSITION_AFTER; +} + +/** + * The root of a range is the root of its start node. + * + * @param range The range to get the root of + * + * @return The root of range + */ +function getRootOfRange (range: Range): Node { + return getRootOfNode(range.startContainer); +} diff --git a/src/Text.ts b/src/Text.ts index aa2b289..ad9f2db 100644 --- a/src/Text.ts +++ b/src/Text.ts @@ -1,98 +1,131 @@ -import CharacterData from './CharacterData'; +import { replaceData, substringData, default as CharacterData } from './CharacterData'; import Document from './Document'; -import Node from './Node'; - -import { getNodeIndex } from './util'; +import { ranges } from './Range'; +import { throwIndexSizeError } from './util/errorHelpers'; +import { insertNode } from './util/mutationAlgorithms'; +import { NodeType } from './util/NodeType'; +import { getNodeIndex } from './util/treeHelpers'; /** - * The Text interface represents the textual content of an Element node. If an element has no markup within its - * content, it has a single child implementing Text that contains the element's text. However, if the element - * contains markup, it is parsed into information items and Text nodes that form its children. - * - * New documents have a single Text node for each block of text. Over time, more Text nodes may be created as - * the document's content changes. The Node.normalize() method merges adjacent Text objects back into a single - * node for each block of text. + * 3.11. Interface Text */ export default class Text extends CharacterData { + // Node + + public get nodeType (): number { + return NodeType.TEXT_NODE; + } + + public get nodeName (): string { + return '#text'; + } + + // Text + /** - * @param content Content for the text node + * Returns a new Text node whose data is data. + * + * Non-standard: as this implementation does not have a document associated with the global object, it is required + * to pass a document to this constructor. + * + * @param document (non-standard) The node document for the new node + * @param data The data for the new text node */ - constructor (content: string) { - super(Node.TEXT_NODE, content); + constructor (document: Document, data: string = '') { + super(document, data); } /** - * Breaks the Text node into two nodes at the specified offset, keeping both nodes in the tree as siblings. + * Splits data at the given offset and returns the remainder as Text node. * - * After the split, the current node contains all the content up to the specified offset point, and a newly - * created node of the same type contains the remaining text. The newly created node is returned to the caller. - * If the original node had a parent, the new node is inserted as the next sibling of the original node. - * If the offset is equal to the length of the original node, the newly created node has no data. + * @param offset The offset at which to split * - * Separated text nodes can be concatenated using the Node.normalize() method. + * @return a text node containing the second half of the split node's data + */ + public splitText (offset: number): Text { + return splitText(this, offset); + } + + /** + * (non-standard) Creates a copy of the context object, not including its children. * - * @param offset Offset at which to split + * @param document The node document to associate with the copy * - * @return The new text node created to hold the second half of the split content + * @return A shallow copy of the context object */ - public splitText (offset: number): Text { - // Check offset - const length = this.length; - if (offset < 0) { - offset = 0; - } - if (offset > length) { - offset = length; - } - - const count = length - offset; - const newData = this.substringData(offset, count); - const document = this.ownerDocument as Document; - const newNode = document.createTextNode(newData); - - // If the current node is part of a tree, insert the new node - if (this.parentNode) { - this.parentNode.insertBefore(newNode, this.nextSibling); - - // Update ranges - var nodeIndex = getNodeIndex(this); - document._ranges.forEach(range => { - if (range.startContainer === this.parentNode && range.startOffset === nodeIndex + 1) { - range.setStart(range.startContainer as Node, range.startOffset + 1); - } - if (range.endContainer === this.parentNode && range.endOffset === nodeIndex + 1) { - range.setEnd(range.endContainer as Node, range.endOffset + 1); - } - if (range.startContainer === this && range.startOffset > offset) { - range.setStart(newNode, range.startOffset - offset); - } - if (range.endContainer === this && range.endOffset > offset) { - range.setEnd(newNode, range.endOffset - offset); - } - }); - } - - // Truncate our own data - this.deleteData(offset, count); - - if (!this.parentNode) { - // Update ranges - document._ranges.forEach(range => { - if (range.startContainer === this && range.startOffset > offset) { - range.setStart(range.startContainer, offset); - } - if (range.endContainer === this && range.endOffset > offset) { - range.setEnd(range.endContainer, offset); - } - }); - } - - // Return the new node - return newNode; + public _copy (document: Document): Text { + // Set copy’s data, to that of node. + return new Text(document, this.data); + } +} + +/** + * To split a Text node node with offset offset, run these steps: + * + * @param node The text node to split + * @param offset The offset to split at + * + * @return a text node containing the second half of the split node's data + */ +function splitText (node: Text, offset: number): Text { + // 1. Let length be node’s length. + const length = node.length; + + // 2. If offset is greater than length, then throw an IndexSizeError. + if (offset > length) { + throwIndexSizeError('can not split past the node\'s length'); } - public cloneNode (deep: boolean = true, copy?: Text): Text { - copy = copy || new Text(this.data); - return super.cloneNode(deep, copy) as Text; + // 3. Let count be length minus offset. + const count = length - offset; + + // 4. Let new data be the result of substringing data with node node, offset offset, and count count. + const newData = substringData(node, offset, count); + + // 5. Let new node be a new Text node, with the same node document as node. Set new node’s data to new data. + const newNode = new Text(node.ownerDocument!, newData); + + // 6. Let parent be node’s parent. + const parent = node.parentNode; + + // 7. If parent is not null, then: + if (parent !== null) { + // 7.1. Insert new node into parent before node’s next sibling. + insertNode(newNode, parent, node.nextSibling); + + const indexOfNodePlusOne = getNodeIndex(node) + 1; + ranges.forEach(range => { + // 7.2. For each range whose start node is node and start offset is greater than offset, set its start node + // to new node and decrease its start offset by offset. + if (range.startContainer === node && range.startOffset > offset) { + range.startContainer = newNode; + range.startOffset -= offset; + } + + // 7.3. For each range whose end node is node and end offset is greater than offset, set its end node to new + // node and decrease its end offset by offset. + if (range.endContainer === node && range.endOffset > offset) { + range.endContainer = newNode; + range.endOffset -= offset; + } + + // 7.4. For each range whose start node is parent and start offset is equal to the index of node + 1, + // increase its start offset by one. + if (range.startContainer === parent && range.startOffset === indexOfNodePlusOne) { + range.startOffset += 1; + } + + // 7.5. For each range whose end node is parent and end offset is equal to the index of node + 1, increase + // its end offset by one. + if (range.endContainer === parent && range.endOffset === indexOfNodePlusOne) { + range.endOffset += 1; + } + }) } + + // 8. Replace data with node node, offset offset, count count, and data the empty string. + replaceData(node, offset, count, ''); + + // 9. Return new node. + return newNode; } diff --git a/src/XMLDocument.ts b/src/XMLDocument.ts new file mode 100644 index 0000000..058af97 --- /dev/null +++ b/src/XMLDocument.ts @@ -0,0 +1,17 @@ +import Document from './Document'; + +export default class XMLDocument extends Document { + /** + * (non-standard) Creates a copy of the context object, not including its children. + * + * @param document The node document to associate with the copy + * + * @return A shallow copy of the context object + */ + public _copy (document: Document): XMLDocument { + // Set copy’s encoding, content type, URL, origin, type, and mode, to those of node. + // (properties not implemented) + + return new XMLDocument(); + } +} diff --git a/src/globals.ts b/src/globals.ts deleted file mode 100644 index aa5edf0..0000000 --- a/src/globals.ts +++ /dev/null @@ -1,3 +0,0 @@ -import DOMImplementation from './DOMImplementation'; - -export const implementation = new DOMImplementation() diff --git a/src/index.ts b/src/index.ts index 3550e8e..9843023 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,49 +1,11 @@ -import Document from './Document'; -import Node from './Node'; -import Element from './Element'; -import Range from './selections/Range'; -import MutationObserver from './mutations/MutationObserver'; +import { implementation } from './DOMImplementation'; +import XMLDocument from './XMLDocument'; -import DOMImplementation from './DOMImplementation'; -import { implementation } from './globals'; +export { implementation } from './DOMImplementation'; +export { default as Node } from './Node'; +export { default as Range } from './Range'; +export { default as MutationObserver } from './mutation-observer/MutationObserver'; -export default { - /** - * The DOMImplementation instance. - */ - implementation, - - /** - * Creates a new Document and returns it. - * - * @return The newly created Document. - */ - createDocument (): Document { - return implementation.createDocument(null, ''); - }, - - /** - * The Document constructor. - */ - Document, - - /** - * The Node constructor. - */ - Node, - - /** - * The Element constructor. - */ - Element, - - /** - * The Range constructor. - */ - Range, - - /** - * The MutationObserver constructor. - */ - MutationObserver -}; +export function createDocument (): XMLDocument { + return implementation.createDocument(null, ''); +} diff --git a/src/mixins.ts b/src/mixins.ts new file mode 100644 index 0000000..33c8f68 --- /dev/null +++ b/src/mixins.ts @@ -0,0 +1,109 @@ +import CharacterData from './CharacterData'; +import Document from './Document'; +import Element from './Element'; +import Node from './Node'; + +import { NodeType, isNodeOfType } from './util/NodeType'; + +/** + * 3.2.4. Mixin NonElementParentNode + */ +export interface NonElementParentNode { +} +// Document implements NonElementParentNode; +// DocumentFragment implements NonElementParentNode; + +/** + * 3.2.6. Mixin ParentNode + */ +export interface ParentNode { + readonly children: Element[]; + + firstElementChild: Element | null; + lastElementChild: Element | null; + childElementCount: number; +} +// Document implements ParentNode; +// DocumentFragment implements ParentNode; +// Element implements ParentNode; + +export function asParentNode (node: Node): ParentNode | null { + // (document fragments not implemented) + if (isNodeOfType(node, NodeType.ELEMENT_NODE, NodeType.DOCUMENT_NODE)) { + return node as Element | Document; + } + + return null; +} + +/** + * Returns the element children of node. + * + * (Non-standard) According to the spec, the children getter should return a live HTMLCollection. This implementation + * returns a static array instead. + * + * @param node The node to get element children of + * + * @return The + */ +export function getChildren (node: ParentNode): Element[] { + const elements: Element[] = []; + for (let child = node.firstElementChild; child; child = child.nextElementSibling) { + elements.push(child); + } + return elements; +} + +/** + * 3.2.7. Mixin NonDocumentTypeChildNode + */ +export interface NonDocumentTypeChildNode { + readonly previousElementSibling: Element | null; + readonly nextElementSibling: Element | null; +} +// Element implements NonDocumentTypeChildNode; +// CharacterData implements NonDocumentTypeChildNode; + +export function asNonDocumentTypeChildNode (node: Node): NonDocumentTypeChildNode | null { + if (isNodeOfType( + node, + NodeType.ELEMENT_NODE, + NodeType.COMMENT_NODE, + NodeType.PROCESSING_INSTRUCTION_NODE, + NodeType.TEXT_NODE, + NodeType.CDATA_SECTION_NODE + )) { + return node as Element | CharacterData; + } + + return null; +} + +export function getPreviousElementSibling (node: Node): Element | null { + for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) { + if (isNodeOfType(sibling, NodeType.ELEMENT_NODE)) { + return sibling as Element; + } + } + + return null; +} + +export function getNextElementSibling (node: Node): Element | null { + for (let sibling = node.nextSibling; sibling; sibling = sibling.nextSibling) { + if (isNodeOfType(sibling, NodeType.ELEMENT_NODE)) { + return sibling as Element; + } + } + + return null; +} + +/** + * 3.2.8. Mixin ChildNode + */ +export interface ChildNode { +} +// DocumentType implements ChildNode; +// Element implements ChildNode; +// CharacterData implements ChildNode; diff --git a/src/mutation-observer/MutationObserver.ts b/src/mutation-observer/MutationObserver.ts new file mode 100644 index 0000000..809cf3d --- /dev/null +++ b/src/mutation-observer/MutationObserver.ts @@ -0,0 +1,179 @@ +import MutationRecord from './MutationRecord'; +import NotifyList from './NotifyList'; +import RegisteredObserver from './RegisteredObserver'; +import Node from '../Node'; + +export interface MutationObserverInit { + /** + * Whether to observe childList mutations. + */ + childList?: boolean; + + /** + * Whether to observe attribute mutations. + */ + attributes?: boolean; + + /** + * Whether to observe character data mutations. + */ + characterData?: boolean; + + /** + * Whether to observe mutations on any descendant in addition to those on the target. + */ + subtree?: boolean; + + /** + * Whether to record the previous value of attributes. + */ + attributeOldValue?: boolean; + + /** + * Whether to record the previous value of character data nodes. + */ + characterDataOldValue?: boolean; +} + +export type MutationCallback = (records: MutationRecord[], observer: MutationObserver) => void; + +/** + * 3.3.1. Interface MutationObserver + * + * A MutationObserver object can be used to observe mutations to the tree of nodes. + */ +export default class MutationObserver { + /** + * The NotifyList instance is shared between all MutationObserver objects. It holds references to all + * MutationObserver instances that have collected records, and is responsible for invoking their callbacks when + * control returns to the event loop (using setImmediate or setTimeout). + */ + static _notifyList = new NotifyList(); + + /** + * The function that will be called when control returns to the event loop, if there are any queued records. The + * function is passed the MutationRecords and the observer instance that collected them. + */ + public _callback: MutationCallback; + + /** + * The list of nodes on which this observer is a RegisteredObserver's observer. + */ + public _nodes: Node[] = []; + + /** + * The list of MutationRecord objects collected so far. + */ + public _recordQueue: MutationRecord[] = []; + + /** + * Tracks transient registered observers created for this observer, to simplify their removal. + */ + public _transients: RegisteredObserver[] = []; + + /** + * Constructs a MutationObserver object and sets its callback to callback. The callback is invoked with a list of + * MutationRecord objects as first argument and the constructed MutationObserver object as second argument. It is + * invoked after nodes registered with the observe() method, are mutated. + * + * @param callback Function called after mutations have been observed. + */ + constructor (callback: MutationCallback) { + // create a new MutationObserver object with callback set to callback + this._callback = callback; + + // append it to the unit of related similar-origin browsing contexts' list of MutationObserver objects + // (for efficiency, this implementation only tracks MutationObserver objects that have records queued) + } + + /** + * Instructs the user agent to observe a given target (a node) and report any mutations based on the criteria given + * by options (an object). + * + * NOTE: Adding an observer to an element is just like addEventListener, if you observe the element multiple + * times it does not make a difference. Meaning if you observe element twice, the observe callback does not fire + * twice, nor will you have to run disconnect() twice. In other words, once an element is observed, observing it + * again with the same will do nothing. However if the callback object is different it will of course add + * another observer to it. + * + * @param target Node (or root of subtree) to observe + * @param options Determines which types of mutations to observe + */ + observe (target: Node, options: MutationObserverInit) { + // Defaults from IDL + options.childList = !!options.childList; + options.subtree = !!options.subtree; + + // 1. If either options’ attributeOldValue or attributeFilter is present and options’ attributes is omitted, set + // options’ attributes to true. + if (options.attributeOldValue !== undefined && options.attributes === undefined) { + options.attributes = true; + } + + // 2. If options’ characterDataOldValue is present and options’ characterData is omitted, set options’ + // characterData to true. + if (options.characterDataOldValue !== undefined && options.characterData === undefined) { + options.characterData = true; + } + // 3. If none of options’ childList, attributes, and characterData is true, throw a TypeError. + if (!(options.childList || options.attributes || options.characterData)) { + throw new TypeError( + 'The options object must set at least one of "attributes", "characterData", or "childList" to true.' + ); + } + + // 4. If options’ attributeOldValue is true and options’ attributes is false, throw a TypeError. + if (options.attributeOldValue && !options.attributes) { + throw new TypeError( + 'The options object may only set "attributeOldValue" to true when "attributes" is true or not present.' + ); + } + + // 5. If options’ attributeFilter is present and options’ attributes is false, throw a TypeError. + // (attributeFilter not yet implemented) + + // 6. If options’ characterDataOldValue is true and options’ characterData is false, throw a TypeError. + if (options.characterDataOldValue && !options.characterData) { + throw new TypeError( + 'The options object may only set "characterDataOldValue" to true when "characterData" is true or not ' + + 'present.' + ); + } + + // 7. For each registered observer registered in target’s list of registered observers whose observer is the + // context object: + // 7.1. Remove all transient registered observers whose source is registered. + // 7.2. Replace registered’s options with options. + // 8. Otherwise, add a new registered observer to target’s list of registered observers with the context object + // as the observer and options as the options, and add target to context object’s list of nodes on which it is + // registered. + target._registeredObservers.register(this, options); + } + + /** + * Stops the MutationObserver instance from receiving notifications of DOM mutations. Until the observe() method + * is used again, observer's callback will not be invoked. + */ + disconnect () { + // for each node node in context object’s list of nodes, remove any registered observer on node for which + // context object is the observer, + this._nodes.forEach(node => node._registeredObservers.removeForObserver(this)); + this._nodes.length = 0; + + // and also empty context object’s record queue. + this._recordQueue.length = 0; + } + + /** + * Empties the MutationObserver instance's record queue and returns what was in there. + * + * @return An Array of MutationRecord objects that were recorded. + */ + takeRecords (): MutationRecord[] { + // return a copy of the record queue + const recordQueue = this._recordQueue.concat(); + // and then empty the record queue + this._recordQueue.length = 0; + return recordQueue; + } +} diff --git a/src/mutation-observer/MutationRecord.ts b/src/mutation-observer/MutationRecord.ts new file mode 100644 index 0000000..e099757 --- /dev/null +++ b/src/mutation-observer/MutationRecord.ts @@ -0,0 +1,82 @@ +import Node from '../Node'; + +export interface MutationRecordInit { + name?: string, + namespace?: string | null, + oldValue?: string | null, + addedNodes?: Node[], + removedNodes?: Node[], + previousSibling?: Node | null, + nextSibling?: Node | null +} + +/** + * 3.3.3. Interface MutationRecord + * + * A helper class which describes a specific mutation as it is observed by a MutationObserver. + */ +export default class MutationRecord { + /** + * Returns "attributes" if it was an attribute mutation. "characterData" if it was a mutation to a CharacterData + * node. And "childList" if it was a mutation to the tree of nodes. + */ + public type: string; + + /** + * Returns the node the mutation affected, depending on the type. For "attributes", it is the element whose + * attribute changed. For "characterData", it is the CharacterData node. For "childList", it is the node whose + * children changed. + */ + public target: Node; + + /** + * Children of target added in this mutation. + * + * (non-standard) According to the spec this should be a NodeList. This implementation uses an array. + */ + public addedNodes: Node[] = []; + + /** + * Children of target removed in this mutation. + * + * (non-standard) According to the spec this should be a NodeList. This implementation uses an array. + */ + public removedNodes: Node[] = []; + + /** + * The previous sibling of the added or removed nodes, or null otherwise. + */ + public previousSibling: Node | null = null; + + /** + * The next sibling Node of the added or removed nodes, or null otherwise. + */ + public nextSibling: Node | null = null; + + /** + * The local name of the changed attribute, or null otherwise. + */ + public attributeName: string | null = null; + + /** + * The namespace of the changed attribute, or null otherwise. + */ + public attributeNamespace: string | null = null; + + /** + * The return value depends on type. For "attributes", it is the value of the changed attribute before the change. + * For "characterData", it is the data of the changed node before the change. For "childList", it is null. + */ + public oldValue: string | null = null; + + /** + * (non-standard) Constructs a MutationRecord + * + * @param type The value for the type property + * @param target The value for the target property + */ + constructor (type: string, target: Node) { + this.type = type; + this.target = target; + } +} diff --git a/src/mutation-observer/NotifyList.ts b/src/mutation-observer/NotifyList.ts new file mode 100644 index 0000000..fd5cbe4 --- /dev/null +++ b/src/mutation-observer/NotifyList.ts @@ -0,0 +1,94 @@ +import { MutationCallback, default as MutationObserver } from './MutationObserver'; +import MutationRecord from './MutationRecord'; +import { removeTransientRegisteredObserversForObserver } from './RegisteredObservers'; + +// Declare functions without having to bring in the entire DOM lib +declare function setImmediate (handler: (...args: any[]) => void): number; +declare function setTimeout (handler: (...args: any[]) => void, timeout: number): number; + +const hasSetImmediate = (typeof setImmediate === 'function'); + +function queueCompoundMicrotask (callback: (...args: any[]) => void, thisArg: NotifyList, ...args: any[]): number { + return (hasSetImmediate ? setImmediate : setTimeout)(() => { + callback.apply(thisArg, args); + }, 0); +} + +/** + * A helper class which is responsible for scheduling the queued MutationRecord objects for reporting by their + * observer. Reporting means the callback of the observer (a MutationObserver object) gets called with the + * relevant MutationRecord objects. + */ +export default class NotifyList { + private _notifyList: MutationObserver[] = []; + private _compoundMicrotaskQueued: number | null = null; + + /** + * Appends a given MutationRecord to the recordQueue of the given MutationObserver and schedules it for reporting. + * + * @param observer The observer for which to enqueue the record + * @param record The record to enqueue + */ + appendRecord (observer: MutationObserver, record: MutationRecord) { + observer._recordQueue.push(record); + this._notifyList.push(observer); + } + + /** + * To queue a mutation observer compound microtask, run these steps: + */ + public queueMutationObserverCompoundMicrotask () { + // 1. If mutation observer compound microtask queued flag is set, then return. + if (this._compoundMicrotaskQueued) { + return; + } + + // 2. Set mutation observer compound microtask queued flag. + // 3. Queue a compound microtask to notify mutation observers. + this._compoundMicrotaskQueued = queueCompoundMicrotask(() => { + this._notifyMutationObservers(); + }, this); + } + + /** + * To notify mutation observers, run these steps: + */ + private _notifyMutationObservers () { + // 1. Unset mutation observer compound microtask queued flag. + this._compoundMicrotaskQueued = null; + + // 2. Let notify list be a copy of unit of related similar-origin browsing contexts' list of MutationObserver + // objects. + const notifyList = this._notifyList.concat(); + // Clear the notify list - for efficiency this list only tracks observers that have a non-empty queue + this._notifyList.length = 0; + + // 3. Let signalList be a copy of unit of related similar-origin browsing contexts' signal slot list. + // 4. Empty unit of related similar-origin browsing contexts' signal slot list. + // (shadow dom not implemented) + + // 5. For each MutationObserver object mo in notify list, execute a compound microtask subtask to run these + // steps: [HTML] + notifyList.forEach(mo => { + queueCompoundMicrotask((mo: MutationObserver) => { + // 5.1. Let queue be a copy of mo’s record queue. + // 5.2. Empty mo’s record queue. + const queue = mo.takeRecords(); + + // 5.3. Remove all transient registered observers whose observer is mo. + removeTransientRegisteredObserversForObserver(mo); + + // 5.4. If queue is non-empty, invoke mo’s callback with a list of arguments consisting of queue and mo, + // and mo as the callback this value. If this throws an exception, report the exception. + if (queue.length) { + mo._callback(queue, mo); + } + + }, this, mo); + }); + + // 6. For each slot slot in signalList, in order, fire an event named slotchange, with its bubbles + // attribute set to true, at slot. + // (shadow dom not implemented) + } +} diff --git a/src/mutation-observer/RegisteredObserver.ts b/src/mutation-observer/RegisteredObserver.ts new file mode 100644 index 0000000..1cac4b0 --- /dev/null +++ b/src/mutation-observer/RegisteredObserver.ts @@ -0,0 +1,110 @@ +import { MutationObserverInit, default as MutationObserver } from './MutationObserver'; +import { MutationRecordInit, default as MutationRecord } from './MutationRecord'; +import Node from '../Node'; + +/** + * A registered observer consists of an observer (a MutationObserver object) and options (a MutationObserverInit + * dictionary). A transient registered observer is a specific type of registered observer that has a source which is a + * registered observer. + * + * Transient registered observers are used to track mutations within a given node’s descendants after node has been + * removed so they do not get lost when subtree is set to true on node’s parent. + */ +export default class RegisteredObserver { + /** + * The observer that is registered. + */ + public observer: MutationObserver; + + /** + * The Node that is being observed by the given observer. + */ + public node: Node; + + /** + * The options for the registered observer. + */ + public options: MutationObserverInit; + + /** + * A transient observer is an observer that has a source which is an observer. + */ + public source: RegisteredObserver | null = null; + + /** + * @param observer The observer being registered + * @param node The node being observed + * @param options Options for the registration + * @param source If not null, creates a transient registered observer for the given registered observer + */ + constructor (observer: MutationObserver, node: Node, options: MutationObserverInit, source?: RegisteredObserver) { + this.observer = observer; + this.node = node; + this.options = options; + this.source = source || null; + if (source) { + observer._transients.push(this); + } + } + + /** + * Adds the given mutationRecord to the NotifyList of the registered MutationObserver. It only adds the record + * when it's type isn't blocked by one of the flags of this registered MutationObserver options (formally the + * MutationObserverInit object). + * + * @param type The type of mutation record to queue + * @param target The target node + * @param data The data for the mutation record + * @param interestedObservers Array of mutation observer objects to append to + * @param pairedStrings Paired strings for the mutation observer objects + */ + public collectInterestedObservers (type: string, target: Node, data: MutationRecordInit, interestedObservers: MutationObserver[], pairedStrings: (string | null | undefined)[]) { + // (continued from RegisteredObservers#queueMutationRecord) + + // 3.1. If none of the following are true + // node is not target and options’ subtree is false + if (this.node !== target && !this.options.subtree) { + return; + } + + // type is "attributes" and options’ attributes is not true + if (type === 'attributes' && !this.options.attributes) { + return; + } + + // type is "attributes", options’ attributeFilter is present, and options’ attributeFilter does not contain name + // or namespace is non-null + // (attributeFilter not implemented) + + // type is "characterData" and options’ characterData is not true + if (type === 'characterData' && !this.options.characterData) { + return; + } + + // type is "childList" and options’ childList is false + if (type === 'childList' && !this.options.childList) { + return; + } + + // then: + + // 3.1.1. If registered observer’s observer is not in interested observers, append registered observer’s + // observer to interested observers. + let index = interestedObservers.indexOf(this.observer); + if (index < 0) { + index = interestedObservers.length; + interestedObservers.push(this.observer); + pairedStrings.push(undefined); + } + + // 3.1.2. If either type is "attributes" and options’ attributeOldValue is true, or type is "characterData" and + // options’ characterDataOldValue is true, set the paired string of registered observer’s observer in interested + // observers to oldValue. + if ( + (type === 'attributes' && this.options.attributeOldValue) || + (type === 'characterData' && this.options.characterDataOldValue) + ) { + pairedStrings[index] = data.oldValue; + } + } +} diff --git a/src/mutation-observer/RegisteredObservers.ts b/src/mutation-observer/RegisteredObservers.ts new file mode 100644 index 0000000..a67b41e --- /dev/null +++ b/src/mutation-observer/RegisteredObservers.ts @@ -0,0 +1,178 @@ +import { MutationObserverInit, default as MutationObserver } from './MutationObserver'; +import { MutationRecordInit } from './MutationRecord'; +import RegisteredObserver from './RegisteredObserver'; +import Node from '../Node'; + +/** + * Each node has an associated list of registered observers. + */ +export default class RegisteredObservers { + /** + * The node for which this RegisteredObservers lists registered MutationObserver objects. + */ + private _node: Node; + + private _registeredObservers: RegisteredObserver[] = []; + + /** + * @param node Node for which this instance holds RegisteredObserver instances. + */ + constructor (node: Node) { + this._node = node; + } + + /** + * Registers a given MutationObserver with the given options. + * + * @param observer Observer to create a registration for + * @param options Options for the registration + */ + public register (observer: MutationObserver, options: MutationObserverInit) { + // (continuing from MutationObserver#observe) + // 7. For each registered observer registered in target’s list of registered observers whose observer is the + // context object: + const registeredObservers = this._registeredObservers; + let hasRegisteredObserverForObserver = false; + registeredObservers.forEach(registered => { + if (registered.observer !== observer) { + return; + } + + hasRegisteredObserverForObserver = true; + + // 7.1. Remove all transient registered observers whose source is registered. + removeTransientRegisteredObserversForSource(registered); + + // 7.2. Replace registered’s options with options. + registered.options = options; + }); + + // 8. Otherwise, add a new registered observer to target’s list of registered observers with the context object + // as the observer and options as the options, and add target to context object’s list of nodes on which it is + // registered. + if (!hasRegisteredObserverForObserver) { + this._registeredObservers.push(new RegisteredObserver(observer, this._node, options)); + if (observer._nodes.indexOf(this._node) < 0) { + observer._nodes.push(this._node); + } + } + } + + /** + * Removes the given registered observer. + + * It is the caller's responsibility to remove the associated node from the observer's list of nodes, where + * appropriate. + * + * @param registeredObserver The registered observer to remove + */ + public remove (registeredObserver: RegisteredObserver): void { + const index = this._registeredObservers.indexOf(registeredObserver); + if (index >= 0) { + this._registeredObservers.splice(index, 1); + } + } + + /** + * Remove any registered observer on the associated node for which observer is the observer. + * + * As this only occurs for all nodes at once, it is the caller's responsibility to remove the associated node from + * the observer's list of nodes. + * + * @param observer Observer for which to remove the registration + */ + public removeForObserver (observer: MutationObserver): void { + // Filter the array in-place + let write = 0; + for (let read = 0, l = this._registeredObservers.length; read < l; ++read) { + const registered = this._registeredObservers[read]; + if (registered.observer === observer) { + continue; + } + + if (read !== write) { + this._registeredObservers[write] = registered; + } + ++write; + } + this._registeredObservers.length = write; + } + + /** + * Determines interested observers for the given record. + * + * @param type The type of mutation record to queue + * @param target The target node + * @param data The data for the mutation record + * @param interestedObservers Array of mutation observer objects to append to + * @param pairedStrings Paired strings for the mutation observer objects + */ + public collectInterestedObservers (type: string, target: Node, data: MutationRecordInit, interestedObservers: MutationObserver[], pairedStrings: (string | null | undefined)[]) { + // (continuing from queueMutationRecord) + // 3. ...and then for each registered observer (with registered observer’s options as options) in node’s list of + // registered observers: + this._registeredObservers.forEach(registeredObserver => { + registeredObserver.collectInterestedObservers( + type, + target, + data, + interestedObservers, + pairedStrings + ); + }); + } + + /** + * Append transient registered observers for any registered observers whose options' subtree is true. + * + * @param node Node to append the transient registered observers to + */ + public appendTransientRegisteredObservers (node: Node): void { + this._registeredObservers.forEach(registeredObserver => { + if (registeredObserver.options.subtree) { + node._registeredObservers.registerTransient(registeredObserver); + } + }); + } + + /** + * Appends a transient registered observer for the given registered observer. + * + * @param source The source registered observer + */ + public registerTransient (source: RegisteredObserver): void { + this._registeredObservers.push( + new RegisteredObserver(source.observer, this._node, source.options, source) + ); + // Note that node is not added to the transient observer's observer's list of nodes. + } +} + +/** + * Removes all transient registered observers whose observer is observer. + * + * @param observer The mutation observer object to remove transient registered observers for + */ +export function removeTransientRegisteredObserversForObserver (observer: MutationObserver): void { + observer._transients.forEach(transientRegisteredObserver => { + transientRegisteredObserver.node._registeredObservers.remove(transientRegisteredObserver); + }); + observer._transients.length = 0; +} + +/** + * Removes all transient registered observer whose source is source. + * + * @param source The registered observer to remove transient registered observers for + */ +export function removeTransientRegisteredObserversForSource (source: RegisteredObserver): void { + for (let i = source.observer._transients.length - 1; i >= 0; --i) { + const transientRegisteredObserver = source.observer._transients[i]; + if (transientRegisteredObserver.source !== source) { + return; + } + + transientRegisteredObserver.node._registeredObservers.remove(transientRegisteredObserver); + source.observer._transients.splice(i, 1); + } +} diff --git a/src/mutation-observer/queueMutationRecord.ts b/src/mutation-observer/queueMutationRecord.ts new file mode 100644 index 0000000..d67adbd --- /dev/null +++ b/src/mutation-observer/queueMutationRecord.ts @@ -0,0 +1,86 @@ +import MutationObserver from './MutationObserver'; +import { MutationRecordInit, default as MutationRecord } from './MutationRecord'; +import Node from '../Node'; + +/** + * 3.3.2. Queuing a mutation record + * + * To queue a mutation record of type for target with one or more of (depends on type) name name, namespace namespace, + * oldValue oldValue, addedNodes addedNodes, removedNodes removedNodes, previousSibling previousSibling, and nextSibling + * nextSibling, run these steps: + * + * @param type The type of mutation record to queue + * @param target The target node + * @param data The data for the mutation record + */ +export default function queueMutationRecord (type: string, target: Node, data: MutationRecordInit) { + // 1. Let interested observers be an initially empty set of MutationObserver objects optionally paired with a + // string. + const interestedObservers: MutationObserver[] = []; + const pairedStrings: (string | null | undefined)[] = []; + + // 2. Let nodes be the inclusive ancestors of target. + // 3. For each node in nodes, and then for each registered observer (with registered observer’s options as options) + // in node’s list of registered observers: + for (let node: Node | null = target; node; node = node.parentNode) { + // 3.1. If none of the following are true + // node is not target and options’ subtree is false + // type is "attributes" and options’ attributes is not true + // type is "attributes", options’ attributeFilter is present, and options’ attributeFilter does not contain name + // or namespace is non-null + // type is "characterData" and options’ characterData is not true + // type is "childList" and options’ childList is false + // then: + // 3.1.1. If registered observer’s observer is not in interested observers, append registered observer’s + // observer to interested observers. + // 3.1.2. If either type is "attributes" and options’ attributeOldValue is true, or type is "characterData" and + // options’ characterDataOldValue is true, set the paired string of registered observer’s observer in interested + // observers to oldValue. + node._registeredObservers.collectInterestedObservers(type, target, data, interestedObservers, pairedStrings); + } + + // 4. For each observer in interested observers: + interestedObservers.forEach((observer, index) => { + // 4.1. Let record be a new MutationRecord object with its type set to type and target set to target. + const record = new MutationRecord(type, target); + + // 4.2. If name and namespace are given, set record’s attributeName to name, and record’s attributeNamespace to + // namespace. + if (data.name !== undefined && data.namespace !== undefined) { + record.attributeName = data.name; + record.attributeNamespace = data.namespace; + } + + // 4.3. If addedNodes is given, set record’s addedNodes to addedNodes. + if (data.addedNodes !== undefined) { + record.addedNodes = data.addedNodes; + } + + // 4.4. If removedNodes is given, set record’s removedNodes to removedNodes, + if (data.removedNodes !== undefined) { + record.removedNodes = data.removedNodes; + } + + // 4.5. If previousSibling is given, set record’s previousSibling to previousSibling. + if (data.previousSibling !== undefined) { + record.previousSibling = data.previousSibling; + } + + // 4.6. If nextSibling is given, set record’s nextSibling to nextSibling. + if (data.nextSibling !== undefined) { + record.nextSibling = data.nextSibling; + } + + // 4.7. If observer has a paired string, set record’s oldValue to observer’s paired string. + const pairedString = pairedStrings[index]; + if (pairedString !== undefined) { + record.oldValue = pairedString; + } + + // 4.8. Append record to observer’s record queue. + MutationObserver._notifyList.appendRecord(observer, record); + }); + + // 5. Queue a mutation observer compound microtask. + MutationObserver._notifyList.queueMutationObserverCompoundMicrotask(); +} diff --git a/src/mutations/MutationObserver.ts b/src/mutations/MutationObserver.ts deleted file mode 100644 index d89ef10..0000000 --- a/src/mutations/MutationObserver.ts +++ /dev/null @@ -1,120 +0,0 @@ -import MutationRecord from './MutationRecord'; -import NotifyList from './NotifyList'; -import Node from '../Node'; - -export interface MutationObserverInit { - /** - * Whether to observe childList mutations. - */ - childList?: boolean; - - /** - * Whether to observe attribute mutations. - */ - attributes?: boolean; - - /** - * Whether to observe character data mutations. - */ - characterData?: boolean; - - /** - * (non-standard) whether to observe user data mutations. - */ - userData?: boolean; - - /** - * Whether to observe mutations on any descendant in addition to those on the target. - */ - subtree?: boolean; - - /** - * Whether to record the previous value of attributes. - */ - attributeOldValue?: boolean; - - /** - * Whether to record the previous value of character data nodes. - */ - characterDataOldValue?: boolean; -} - -export type MutationObserverCallback = (records: MutationRecord[], observer: MutationObserver) => void; - -/** - * A MutationObserver object can be used to observe mutations to the tree of nodes. - */ -export default class MutationObserver { - - /** - * (internal) The function that will be called on each DOM mutation. The observer will call this function with two - * arguments. The first is an array of objects, each of type MutationRecord. The second is this - * MutationObserver instance. - */ - public _callback: MutationObserverCallback; - - /** - * (internal) List of records collected so far. - */ - public _recordQueue: MutationRecord[] = []; - - /** - * (internal) A list of Node objects for which this MutationObserver is a registered observer. - */ - public _targets: Node[] = []; - - /** - * (internal) The NotifyList instance that is shared between all MutationObserver objects. Each observer queues - * its MutationRecord object on this list with a reference to itself. The NotifyList is then responsible for - * periodically reporting of these records to the observers. - */ - static _notifyList = new NotifyList(); - - /** - * @param callback Function called after mutations have been observed. - */ - constructor (callback: MutationObserverCallback) { - this._callback = callback; - } - - /** - * Registers the MutationObserver instance to receive notifications of DOM mutations on the specified node. - * - * NOTE: Adding an observer to an element is just like addEventListener, if you observe the element multiple - * times it does not make a difference. Meaning if you observe element twice, the observe callback does not fire - * twice, nor will you have to run disconnect() twice. In other words, once an element is observed, observing it - * again with the same will do nothing. However if the callback object is different it will of course add - * another observer to it. - * - * @param target Node (or root of subtree) to observe - * @param options Determines which types of mutations to observe - * @param isTransient (non-standard) Adds a transient registered observer if set to true - */ - observe (target: Node, options: MutationObserverInit, isTransient: boolean = false) { - target._registeredObservers.register(this, options, isTransient); - } - - /** - * Stops the MutationObserver instance from receiving notifications of DOM mutations. Until the observe() method - * is used again, observer's callback will not be invoked. - */ - disconnect () { - // Disconnect from each target - this._targets.forEach(target => target._registeredObservers.removeObserver(this)); - this._targets.length = 0; - - // Empty the record queue - this._recordQueue.length = 0; - } - - /** - * Empties the MutationObserver instance's record queue and returns what was in there. - * - * @return An Array of MutationRecord objects that were recorded. - */ - takeRecords (): MutationRecord[] { - const recordQueue = this._recordQueue; - this._recordQueue = []; - return recordQueue; - } -} diff --git a/src/mutations/MutationRecord.ts b/src/mutations/MutationRecord.ts deleted file mode 100644 index 93404e6..0000000 --- a/src/mutations/MutationRecord.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Node from '../Node'; - -export type MutationRecordType = 'attributes' | 'characterData' | 'childList' | 'userData'; - -/** - * A helper class which describes a specific mutation as it is observed by a MutationObserver. - */ - export default class MutationRecord { - /** - * The type of MutationRecord - */ - public type: MutationRecordType; - - /** - * The node on or under which the mutation took place. - */ - public target: Node; - - /** - * Children of target added in this mutation. - */ - public addedNodes: Node[] = []; - - /** - * Children of target removed in this mutation. - */ - public removedNodes: Node[] = []; - - /** - * The previous sibling Node of the added or removed nodes if there were any. - */ - public previousSibling: Node | null = null; - - /** - * The next sibling Node of the added or removed nodes if there were any. - */ - public nextSibling: Node | null = null; - - /** - * The name of the changed attribute if there was any. - */ - public attributeName: string | null = null; - - /** - * Depending on the type: for "attributes", it is the value of the changed attribute before the change; - * for "characterData", it is the data of the changed node before the change; for "childList", it is null. - */ - public oldValue: any | null = null; - - constructor (type: MutationRecordType, target: Node) { - this.type = type; - this.target = target; - } -} diff --git a/src/mutations/NotifyList.ts b/src/mutations/NotifyList.ts deleted file mode 100644 index 1fa27a4..0000000 --- a/src/mutations/NotifyList.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { MutationObserverCallback, default as MutationObserver } from './MutationObserver'; -import MutationRecord from './MutationRecord'; - -const hasSetImmediate = (typeof setImmediate === 'function'); - -function schedule (callback: MutationObserverCallback, thisArg: NotifyList, ...args: any[]): number { - return (hasSetImmediate ? setImmediate : setTimeout)(() => { - callback.apply(thisArg, args); - }, 0); -} - -function removeTransientRegisteredObserversForObserver (observer: MutationObserver) { - // Remove all transient registered observers for this observer - // Process in reverse order, as the targets array may change during traversal - for (var i = observer._targets.length - 1; i >= 0; --i) { - observer._targets[i]._registeredObservers.removeTransients(observer); - } -} - -/** - * A helper class which is responsible for scheduling the queued MutationRecord objects for reporting by their - * observer. Reporting means the callback of the observer (a MutationObserver object) gets called with the - * relevant MutationRecord objects. - */ -export default class NotifyList { - private _notifyList: MutationObserver[] = []; - private _scheduled: number | null = null; - private _callbacks: MutationObserverCallback[] = []; - - /** - * Adds a given MutationRecord to the recordQueue of the given MutationObserver and schedules it for reporting. - * - * @param observer The observer for which to enqueue the record - * @param record The record to enqueue - */ - queueRecord (observer: MutationObserver, record: MutationRecord) { - // Only queue the same record once per observer - if (observer._recordQueue[observer._recordQueue.length - 1] === record) { - return; - } - - observer._recordQueue.push(record); - this._notifyList.push(observer); - this._scheduleInvoke(); - } - - /** - * Takes all the records from all the observers currently on this list and clears the current list. - */ - clear () { - this._notifyList.forEach(observer => observer.takeRecords()); - this._notifyList.length = 0; - } - - /** - * An internal helper method which is used to start the scheduled invocation of the callback from each of the - * observers on the current list, i.e. to report the MutationRecords. - */ - private _scheduleInvoke () { - if (this._scheduled) { - return; - } - - this._scheduled = schedule(() => { - this._scheduled = null; - this._invokeMutationObservers(); - }, this); - } - - /** - * An internal helper method which is used to invoke the callback from each of the observers on the current - * list, i.e. to report the MutationRecords. - */ - private _invokeMutationObservers () { - // Process notify list - let numCallbacks = 0; - this._notifyList.forEach(observer => { - const queue = observer.takeRecords(); - if (!queue.length) { - removeTransientRegisteredObserversForObserver(observer); - return; - } - - // Observer has records, schedule its callback - ++numCallbacks; - schedule((queue, observer) => { - try { - // According to the spec, transient registered observers for observer - // should be removed just before its callback is called. - removeTransientRegisteredObserversForObserver(observer); - observer._callback.call(null, queue, observer); - } - finally { - --numCallbacks; - if (!numCallbacks) { - // Callbacks may have queued additional mutations, check again later - this._scheduleInvoke(); - } - } - }, this, queue, observer); - }); - - this._notifyList.length = 0; - } -} diff --git a/src/mutations/RegisteredObserver.ts b/src/mutations/RegisteredObserver.ts deleted file mode 100644 index da93c34..0000000 --- a/src/mutations/RegisteredObserver.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { MutationObserverInit, default as MutationObserver } from './MutationObserver'; -import MutationRecord from './MutationRecord'; -import Node from '../Node'; - -/** - * This is an internal helper class that is used to work with a MutationObserver. - * - * Each node has an associated list of registered observers. A registered observer consists of an observer - * (a MutationObserver object) and options (a MutationObserverInit dictionary). A transient registered observer - * is a specific type of registered observer that has a source which is a registered observer. - */ -export default class RegisteredObserver { - /** - * The observer that is registered. - */ - public observer: MutationObserver; - - /** - * The Node that is being observed by the given observer. - */ - public target: Node; - - /** - * An options object (formally a MutationObserverInit object, but just a plain js object in Slimdom) which - * specifies which DOM mutations should be reported. TODO: add options property docs. - */ - public options: MutationObserverInit; - - /** - * A transient observer is an observer that has a source which is an observer. TODO: clarify the "source" - * keyword in this context. - */ - public isTransient: boolean; - - /** - * @param observer The observer being registered - * @param target The node being observed - * @param options Options for the registration - * @param isTransient Whether the registration is automatically removed when control returns to the event loop - */ - constructor (observer: MutationObserver, target: Node, options: MutationObserverInit, isTransient: boolean) { - this.observer = observer; - this.target = target; - this.options = options; - this.isTransient = isTransient; - } - - /** - * Adds the given mutationRecord to the NotifyList of the registered MutationObserver. It only adds the record - * when it's type isn't blocked by one of the flags of this registered MutationObserver options (formally the - * MutationObserverInit object). - * - * @param mutationRecord The record to enqueue - */ - public queueRecord (mutationRecord: MutationRecord) { - const options = this.options; - - // Only trigger ancestors if they are listening for subtree mutations - if (mutationRecord.target !== this.target && !options.subtree) { - return; - } - - // Ignore attribute modifications if we're not listening for them - if (!options.attributes && mutationRecord.type === 'attributes') { - return; - } - - // TODO: implement attribute filter? - - // Ignore user data modifications if we're not listening for them - if (!options.userData && mutationRecord.type === 'userData') { - return; - } - - // Ignore character data modifications if we're not listening for them - if (!options.characterData && mutationRecord.type === 'characterData') { - return; - } - - // Ignore child list modifications if we're not listening for them - if (!options.childList && mutationRecord.type === 'childList') { - return; - } - - // Queue the record - // TODO: we should probably make a copy here according to the options, but who cares about extra info? - MutationObserver._notifyList.queueRecord(this.observer, mutationRecord); - } -} diff --git a/src/mutations/RegisteredObservers.ts b/src/mutations/RegisteredObservers.ts deleted file mode 100644 index 3bcd2cf..0000000 --- a/src/mutations/RegisteredObservers.ts +++ /dev/null @@ -1,133 +0,0 @@ -import MutationObserver from './MutationObserver'; -import MutationRecord from './MutationRecord'; -import RegisteredObserver from './RegisteredObserver'; -import Node from '../Node'; - -/** - * This is an internal helper class that is used to work with a MutationObserver. - * - * Each node has an associated list of registered observers. A registered observer consists of an observer - * (a MutationObserver object) and options (a MutationObserverInit dictionary). A transient registered observer - * is a specific type of registered observer that has a source which is a registered observer. - */ -export default class RegisteredObservers { - /** - * The node for which this RegisteredObservers lists registered MutationObserver objects. - */ - - private _target: Node; - - private _registeredObservers: RegisteredObserver[] = []; - - /** - * @param target Node for which this instance holds RegisteredObserver instances. - */ - constructor (target: Node) { - this._target = target; - } - - /** - * Registers a given MutationObserver with the given options. - * - * @param observer Observer to create a registration for - * @param options Options for the registration - * @param isTransient Whether the registration is automatically removed when control returns to the event loop - */ - public register (observer: MutationObserver, options: MutationObserverInit, isTransient: boolean) { - // Ensure our node is in the observer's list of targets - if (observer._targets.indexOf(this._target) < 0) { - observer._targets.push(this._target); - } - - if (!isTransient) { - // Replace options for existing registered observer, if any - for (var i = 0, l = this._registeredObservers.length; i < l; ++i) { - var registeredObserver = this._registeredObservers[i]; - if (registeredObserver.observer !== observer) { - continue; - } - - if (registeredObserver.isTransient) { - continue; - } - - registeredObserver.options = options; - return; - } - } - - this._registeredObservers.push(new RegisteredObserver(observer, this._target, options, isTransient)); - } - - /** - * Creates transient registrations for all subtree observers on an ancestor of our target when target nodes are - * removed from under that ancestor. - * - * @param registeredObserversForAncestor Registrations for an ancestor of our target - */ - public appendTransientsForAncestor (registeredObserversForAncestor: RegisteredObservers) { - registeredObserversForAncestor._registeredObservers.forEach(ancestorRegisteredObserver => { - // Only append transients for subtree observers - if (!ancestorRegisteredObserver.options.subtree) { - return; - } - - this.register(ancestorRegisteredObserver.observer, ancestorRegisteredObserver.options, true); - }); - }; - - /** - * @param observer Observer for which to remove the registration - * @param transientsOnly Whether to remove only transient registrations - * - * @return Whether any non-transient registrations were not removed because transientsOnly was set to true - */ - public removeObserver (observer: MutationObserver, transientsOnly: boolean = false): boolean { - // Remove all registered observers for this observer - let write = 0; - let hasMore = false; - for (let read = 0, l = this._registeredObservers.length; read < l; ++read) { - const registeredObserver = this._registeredObservers[read]; - if (registeredObserver.observer === observer) { - if (!transientsOnly || registeredObserver.isTransient) { - continue; - } - // Record the fact a non-transient registered observer was skipped - if (!registeredObserver.isTransient) { - hasMore = true; - } - } - - if (read !== write) { - this._registeredObservers[write] = registeredObserver; - } - ++write; - } - this._registeredObservers.length = write; - - return hasMore; - } - - /** - * @param observer Observer to remove any transient registrations for - */ - public removeTransients (observer: MutationObserver) { - const hasNonTransients = this.removeObserver(observer, true); - if (!hasNonTransients) { - // Remove target from observer - var targetIndex = observer._targets.indexOf(this._target); - if (targetIndex >= 0) { - observer._targets.splice(targetIndex, 1); - } - } - } - - /** - * Queues a given MutationRecord on each registered MutationObserver in this list of registered observers. - * - * @param mutationRecord Record to enqueue - */ - public queueRecord (mutationRecord: MutationRecord) { - this._registeredObservers.forEach(registeredObserver => registeredObserver.queueRecord(mutationRecord)); - } -} diff --git a/src/mutations/queueMutationRecord.ts b/src/mutations/queueMutationRecord.ts deleted file mode 100644 index edbfc94..0000000 --- a/src/mutations/queueMutationRecord.ts +++ /dev/null @@ -1,14 +0,0 @@ -import MutationRecord from './MutationRecord'; -import Node from '../Node'; - -/** - * Queues mutation on all target nodes, and on all target nodes of all its ancestors. - * - * @param mutationRecord The record to enqueue - */ -export default function queueMutationRecord (mutationRecord: MutationRecord) { - // Check all inclusive ancestors of the target for registered observers - for (let node: Node | null = mutationRecord.target; node; node = node.parentNode) { - node._registeredObservers.queueRecord(mutationRecord); - } -} diff --git a/src/selections/Range.ts b/src/selections/Range.ts deleted file mode 100644 index b7411d5..0000000 --- a/src/selections/Range.ts +++ /dev/null @@ -1,242 +0,0 @@ -import Document from '../Document'; -import Node from '../Node'; -import { commonAncestor, comparePoints, getNodeIndex } from '../util'; - -export default class Range { - /** - * The node at which this range starts. - */ - public startContainer: Node | null; - - /** - * The offset in the startContainer at which this range starts. - */ - public startOffset: number; - - /** - * The node at which this range ends. - */ - public endContainer: Node | null; - - /** - * The offset in the endContainer at which this range ends. - */ - public endOffset: number; - - /** - * A range is collapsed if its start and end positions are identical. - */ - public collapsed: boolean; - - /** - * The closest node that is a parent of both start and end positions. - */ - public commonAncestorContainer: Node | null; - - /** - * A detached range should no longer be used. - */ - private _isDetached: boolean; - - /** - * Do not use directly! Use Document#createRange to create an instance. - * - * @param document Document in which to create the Range - */ - constructor (document: Document) { - this.startContainer = document; - this.startOffset = 0; - this.endContainer = document; - this.endOffset = 0; - - this.collapsed = true; - this.commonAncestorContainer = document; - - this._isDetached = false; - - // Start tracking the range - document._ranges.push(this); - } - - static START_TO_START = 0; - static START_TO_END = 1; - static END_TO_END = 2; - static END_TO_START = 3; - - /** - * Disposes the range and removes it from it's document. - */ - public detach () { - // Stop tracking the range - const startContainer = this.startContainer as Node; - const document = startContainer instanceof Document ? startContainer : startContainer.ownerDocument as Document; - const rangeIndex = document._ranges.indexOf(this); - document._ranges.splice(rangeIndex, 1); - - // Clear properties - this.startContainer = null; - this.startOffset = 0; - this.endContainer = null; - this.endOffset = 0; - this.collapsed = true; - this.commonAncestorContainer = null; - this._isDetached = true; - } - - /** - * Helper used to update the range when start and/or end has changed - */ - private _pointsChanged () { - this.commonAncestorContainer = commonAncestor(this.startContainer as Node, this.endContainer as Node); - this.collapsed = (this.startContainer == this.endContainer && this.startOffset == this.endOffset); - } - - /** - * Sets the start position of a range to a given node and a given offset inside that node. - * - * @param node Container for the position - * @param offset Index of the child or character before which to place the position - */ - public setStart (node: Node, offset: number) { - this.startContainer = node; - this.startOffset = offset; - - // If start is after end, move end to start - if (comparePoints(this.startContainer as Node, this.startOffset, this.endContainer as Node, this.endOffset) as number > 0) { - this.setEnd(node, offset); - } - - this._pointsChanged(); - } - - /** - * Sets the end position of a range to a given node and a given offset inside that node. - * - * @param node Container for the position - * @param offset Index of the child or character before which to place the position - */ - public setEnd (node: Node, offset: number) { - this.endContainer = node; - this.endOffset = offset; - - // If end is before start, move start to end - if (comparePoints(this.startContainer as Node, this.startOffset, this.endContainer as Node, this.endOffset) as number > 0) { - this.setStart(node, offset); - } - - this._pointsChanged(); - } - - /** - * Sets the start position of this Range relative to another Node. - * - * @param referenceNode Node before which to place the position - */ - public setStartBefore (referenceNode: Node) { - this.setStart(referenceNode.parentNode as Node, getNodeIndex(referenceNode)); - } - - /** - * Sets the start position of this Range relative to another Node. - * - * @param referenceNode Node after which to place the position - */ - public setStartAfter (referenceNode: Node) { - this.setStart(referenceNode.parentNode as Node, getNodeIndex(referenceNode) + 1); - } - - /** - * Sets the end position of this Range relative to another Node. - * - * @param referenceNode Node before which to place the position - */ - public setEndBefore (referenceNode: Node) { - this.setEnd(referenceNode.parentNode as Node, getNodeIndex(referenceNode)); - } - - /** - * Sets the end position of this Range relative to another Node. - * - * @param referenceNode Node after which to place the position - */ - public setEndAfter (referenceNode: Node) { - this.setEnd(referenceNode.parentNode as Node, getNodeIndex(referenceNode) + 1); - } - - /** - * Sets the Range to contain the Node and its contents. - * - * @param referenceNode Node to place the range around - */ - public selectNode (referenceNode: Node) { - this.setStartBefore(referenceNode); - this.setEndAfter(referenceNode); - } - - /** - * Sets the Range to contain the contents of a Node. - * - * @param referenceNode Node to place the range within - */ - public selectNodeContents (referenceNode: Node) { - this.setStart(referenceNode, 0); - this.setEnd(referenceNode, referenceNode.childNodes.length); - } - - /** - * Collapses the Range to one of its boundary points. - * - * @param toStart Whether to collapse to the start rather than the end position - */ - public collapse (toStart: boolean = false) { - if (toStart) { - this.setEnd(this.startContainer as Node, this.startOffset); - } - else { - this.setStart(this.endContainer as Node, this.endOffset); - } - } - - /** - * Create a new range with the same boundary points. - * - * @return Copy of the current range - */ - public cloneRange (): Range { - const startContainer = this.startContainer as Node; - const document = startContainer instanceof Document ? startContainer : startContainer.ownerDocument as Document; - const newRange = document.createRange(); - newRange.setStart(this.startContainer as Node, this.startOffset); - newRange.setEnd(this.endContainer as Node, this.endOffset); - - return newRange; - } - - /** - * Compares a boundary of the current range with a boundary of the specified range. - * - * @param comparisonType One of the constants exposed on the Range constructor determining the comparison to make - * @param range Range against which to compare the current instance - * - * @return Either negative, zero or positive, depending on the relative positions of the points being compared - */ - public compareBoundaryPoints (comparisonType: number, range: Range): number | undefined { - switch (comparisonType) { - case Range.START_TO_START: - return comparePoints(this.startContainer as Node, this.startOffset, range.startContainer as Node, range.startOffset); - case Range.START_TO_END: - return comparePoints(this.startContainer as Node, this.startOffset, range.endContainer as Node, range.endOffset); - case Range.END_TO_END: - return comparePoints(this.endContainer as Node, this.endOffset, range.endContainer as Node, range.endOffset); - case Range.END_TO_START: - return comparePoints(this.endContainer as Node, this.endOffset, range.startContainer as Node, range.startOffset); - } - - return undefined; - } -} - -(Range.prototype as any).START_TO_START = 0; -(Range.prototype as any).START_TO_END = 1; -(Range.prototype as any).END_TO_END = 2; -(Range.prototype as any).END_TO_START = 3; diff --git a/src/util.ts b/src/util.ts deleted file mode 100644 index 34464eb..0000000 --- a/src/util.ts +++ /dev/null @@ -1,106 +0,0 @@ -import Node from './Node'; - -/** - * Get all inclusive ancestors of the given node. - * - * @param node Node to collect ancestors for - * - * @return All inclusive ancestors, ordered from root down to node - */ -export function parents (node: Node | null): Node[] { - const nodes = []; - while (node) { - nodes.unshift(node); - node = node.parentNode; - } - return nodes; -} - -/** - * Returns the index of the given node in its parent's childNodes. - * Used as an offset, this represents the position just before the given node. - * - * @param node Node to determine the index of - * - * @return The index among node's siblings - */ -export function getNodeIndex (node: Node): number { - return (node.parentNode as Node).childNodes.indexOf(node); -} - -/** - * Returns the first common ancestor of the two nodes. - * - * @param node1 First node - * @param node2 Second node - * - * @return Common ancestor of node1 and node2, or null if the nodes are in different trees - */ -export function commonAncestor (node1: Node, node2: Node): Node | null { - if (node1 === node2) { - return node1; - } - - const parents1 = parents(node1); - const parents2 = parents(node2); - let parent1 = parents1[0]; - let parent2 = parents2[0]; - - if (parent1 !== parent2) { - return null; - } - - for (let i = 1, l = Math.min(parents1.length, parents2.length); i < l; i++) { - // Let the commonAncestor be one step behind - const commonAncestor = parent1; - parent1 = parents1[i]; - parent2 = parents2[i]; - - if (!parent1 || !parent2 || parent1 !== parent2) { - return commonAncestor; - } - } - - // The common Ancestor is the node itself - return parent1; -} - -/** - * Compares two positions within the document. - * - * @param node1 Container of first position - * @param offset1 Offset of first position - * @param node2 Container of second position - * @param offset2 Offset of second position - * - * @return Negative, 0 or positive, depending on the relative ordering of the given positions, or undefined if the - * containers are in different trees - */ -export function comparePoints (node1: Node, offset1: number, node2: Node, offset2: number): number | undefined { - if (node1 !== node2) { - const parents1 = parents(node1); - const parents2 = parents(node2); - // This should not be called on nodes from different trees - if (parents1[0] !== parents2[0]) { - return undefined; - } - - // Skip common parents - while (parents1[0] && parents2[0] && parents1[0] === parents2[0]) { - parents1.shift(); - parents2.shift(); - } - - // Compute offsets at the level under the last common parent, - // we add 0.5 to indicate a position inside the parent rather than before or after - if (parents1.length) { - offset1 = getNodeIndex(parents1[0]) + 0.5; - } - if (parents2.length) { - offset2 = getNodeIndex(parents2[0]) + 0.5; - } - } - - // Compare positions at this level - return offset1 - offset2; -} diff --git a/src/util/NodeType.ts b/src/util/NodeType.ts new file mode 100644 index 0000000..b7a3c1f --- /dev/null +++ b/src/util/NodeType.ts @@ -0,0 +1,28 @@ +import Node from '../Node'; + +export const enum NodeType { + ELEMENT_NODE = 1, + ATTRIBUTE_NODE = 2, + TEXT_NODE = 3, + CDATA_SECTION_NODE = 4, + ENTITY_REFERENCE_NODE = 5, // historical + ENTITY_NODE = 6, // historical + PROCESSING_INSTRUCTION_NODE = 7, + COMMENT_NODE = 8, + DOCUMENT_NODE = 9, + DOCUMENT_TYPE_NODE = 10, + DOCUMENT_FRAGMENT_NODE = 11, + NOTATION_NODE = 12 // historical +} + +/** + * Checks whether the given node's nodeType is one of the specified values + * + * @param node The node to test + * @param types Possible nodeTypes for node + * + * @return Whether node.nodeType is one of the specified values + */ +export function isNodeOfType (node: Node, ...types: NodeType[]): boolean { + return types.some(t => node.nodeType === t); +} diff --git a/src/util/attrMutations.ts b/src/util/attrMutations.ts new file mode 100644 index 0000000..89ee502 --- /dev/null +++ b/src/util/attrMutations.ts @@ -0,0 +1,129 @@ +import Attr from '../Attr'; +import Element from '../Element'; +import queueMutationRecord from '../mutation-observer/queueMutationRecord'; + +/** + * To change an attribute attribute from an element element to value, run these steps: + * + * @param attribute The attribute to change + * @param element The element that has the attribute + * @param value The new value for the attribute + */ +export function changeAttribute (attribute: Attr, element: Element, value: string): void { + // 1. Queue a mutation record of "attributes" for element with name attribute’s local name, namespace attribute’s + // namespace, and oldValue attribute’s value. + queueMutationRecord('attributes', element, { + name: attribute.localName, + namespace: attribute.namespaceURI, + oldValue: attribute.value + }); + + // 2. If element is custom, then enqueue a custom element callback reaction with element, callback name + // "attributeChangedCallback", and an argument list containing attribute’s local name, attribute’s value, value, and + // attribute’s namespace. + // (custom elements not implemented) + + // 3. Run the attribute change steps with element, attribute’s local name, attribute’s value, value, and attribute’s + // namespace. + // (attribute change steps not implemented) + + // 4. Set attribute’s value to value. + (attribute as any)._value = value; +} + +/** + * To append an attribute attribute to an element element, run these steps: + * + * @param attribute The attribute to append + * @param element The element to append attribute to + */ +export function appendAttribute (attribute: Attr, element: Element): void { + // 1. Queue a mutation record of "attributes" for element with name attribute’s local name, namespace attribute’s + // namespace, and oldValue null. + queueMutationRecord('attributes', element, { + name: attribute.localName, + namespace: attribute.namespaceURI, + oldValue: null + }); + + // 2. If element is custom, then enqueue a custom element callback reaction with element, callback name + // "attributeChangedCallback", and an argument list containing attribute’s local name, null, attribute’s value, and + // attribute’s namespace. + // (custom elements not implemented) + + // 3. Run the attribute change steps with element, attribute’s local name, null, attribute’s value, and attribute’s + // namespace. + // (attribute change steps not implemented) + + // 4. Append attribute to element’s attribute list. + element.attributes.push(attribute); + + // 5. Set attribute’s element to element. + attribute.ownerElement = element; +} + +/** + * To remove an attribute attribute from an element element, run these steps: + * + * @param attribute The attribute to remove + * @param element The element to remove attribute from + */ +export function removeAttribute (attribute: Attr, element: Element): void { + // 1. Queue a mutation record of "attributes" for element with name attribute’s local name, namespace attribute’s + // namespace, and oldValue attribute’s value. + queueMutationRecord('attributes', element, { + name: attribute.localName, + namespace: attribute.namespaceURI, + oldValue: attribute.value + }); + + // 2. If element is custom, then enqueue a custom element callback reaction with element, callback name + // "attributeChangedCallback", and an argument list containing attribute’s local name, attribute’s value, null, and + // attribute’s namespace. + // (custom elements not implemented) + + // 3. Run the attribute change steps with element, attribute’s local name, attribute’s value, null, and attribute’s + // namespace. + // (attribute change steps not implemented) + + // 4. Remove attribute from element’s attribute list. + element.attributes.splice(element.attributes.indexOf(attribute), 1); + + // 5. Set attribute’s element to null. + attribute.ownerElement = null; +} + +/** + * To replace an attribute oldAttr by an attribute newAttr in an element element, run these steps: + * + * @param oldAttr The attribute to replace + * @param newAttr The attribute to replace oldAttr with + * @param element The element on which to replace the attribute + */ +export function replaceAttribute (oldAttr: Attr, newAttr: Attr, element: Element): void { + // 1. Queue a mutation record of "attributes" for element with name oldAttr’s local name, namespace oldAttr’s + // namespace, and oldValue oldAttr’s value. + queueMutationRecord('attributes', element, { + name: oldAttr.localName, + namespace: oldAttr.namespaceURI, + oldValue: oldAttr.value + }); + + // 2. If element is custom, then enqueue a custom element callback reaction with element, callback name + // "attributeChangedCallback", and an argument list containing oldAttr’s local name, oldAttr’s value, newAttr’s + // value, and oldAttr’s namespace. + // (custom elements not implemented) + + // 3. Run the attribute change steps with element, oldAttr’s local name, oldAttr’s value, newAttr’s value, and + // oldAttr’s namespace. + // (attribute change steps not implemented) + + // 4. Replace oldAttr by newAttr in element’s attribute list. + element.attributes.splice(element.attributes.indexOf(oldAttr), 1, newAttr); + + // 5. Set oldAttr’s element to null. + oldAttr.ownerElement = null; + + // 6. Set newAttr’s element to element. + newAttr.ownerElement = element; +} diff --git a/src/util/cloneNode.ts b/src/util/cloneNode.ts new file mode 100644 index 0000000..706e4ac --- /dev/null +++ b/src/util/cloneNode.ts @@ -0,0 +1,55 @@ +import Document from '../Document'; +import Node from '../Node'; + +import { getNodeDocument } from './treeHelpers'; + +// 3.4. Interface Node + +/** + * To clone a node, with an optional document and clone children flag, run these steps: + * + * @param node The node to clone + * @param cloneChildren Whether to also clone node's descendants + * @param document The document used to create the copy + */ +export default function cloneNode (node: Node, cloneChildren: boolean = false, document?: Document): Node { + // 1. If document is not given, let document be node’s node document. + if (!document) { + document = getNodeDocument(node); + } + + // 2. If node is an element, then: + // 2.1. Let copy be the result of creating an element, given document, node’s local name, node’s namespace, + // node’s namespace prefix, and the value of node’s is attribute if present (or null if not). The synchronous + // custom elements flag should be unset. + // 2.2. For each attribute in node’s attribute list: + // 2.2.1. Let copyAttribute be a clone of attribute. + // 2.2.2. Append copyAttribute to copy. + // 3. Otherwise, let copy be a node that implements the same interfaces as node, and fulfills these additional + // requirements, switching on node: + // Document: Set copy’s encoding, content type, URL, origin, type, and mode, to those of node. + // DocumentType: Set copy’s name, public ID, and system ID, to those of node. + // Attr: Set copy’s namespace, namespace prefix, local name, and value, to those of node. + // Text, Comment: Set copy’s data, to that of node. + // ProcessingInstruction: Set copy’s target and data to those of node. + // Any other node: — + // 4. Set copy’s node document and document to copy, if copy is a document, and set copy’s node document to document + // otherwise. + // (all handled by _copy method) + let copy = node._copy(document); + + // 5. Run any cloning steps defined for node in other applicable specifications and pass copy, node, document and the + // clone children flag if set, as parameters. + // (cloning steps not implemented) + + // 6. If the clone children flag is set, clone all the children of node and append them to copy, with document as + // specified and the clone children flag being set. + if (cloneChildren) { + for (let child = node.firstChild; child; child = child.nextSibling) { + copy.appendChild(cloneNode(child, true, document)) + } + } + + // 7. Return copy. + return copy; +} diff --git a/src/util/createElementNS.ts b/src/util/createElementNS.ts new file mode 100644 index 0000000..0020e18 --- /dev/null +++ b/src/util/createElementNS.ts @@ -0,0 +1,33 @@ +import Document from '../Document'; +import { createElement, default as Element } from '../Element'; +import { validateAndExtract } from './namespaceHelpers'; + +// 3.5. Interface Document + +/** + * The internal createElementNS steps, given document, namespace, qualifiedName, and options, are as follows: + * + * @param document The node document for the new element + * @param namespace The namespace for the new element + * @param qualifiedName The qualified name for the new element + * + * @return The new element + */ +export default function createElementNS (document: Document, namespace: string | null, qualifiedName: string): Element { + // 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName to validate and + // extract. + const { namespace: validatedNamespace, prefix, localName } = validateAndExtract(namespace, qualifiedName); + + // 2. Let is be the value of is member of options, or null if no such member exists. + // (custom elements not implemented) + + // 3. Let element be the result of creating an element given document, localName, namespace, prefix, is, and + // with the synchronous custom elements flag set. + const element = createElement(document, localName, validatedNamespace, prefix); + + // 4. If is is non-null, then set an attribute value for element using "is" and is. + // (custom elements not implemented) + + // 5. Return element. + return element; +} diff --git a/src/util/errorHelpers.ts b/src/util/errorHelpers.ts new file mode 100644 index 0000000..98a9e64 --- /dev/null +++ b/src/util/errorHelpers.ts @@ -0,0 +1,31 @@ +export function throwHierarchyRequestError (message: string): never { + throw new Error(`HierarchyRequestError: ${message}`); +} + +export function throwIndexSizeError (message: string): never { + throw new Error(`IndexSizeError: ${message}`); +} + +export function throwInUseAttributeError (message: string): never { + throw new Error(`InUseAttributeError: ${message}`); +} + +export function throwInvalidNodeTypeError (message: string): never { + throw new Error(`InvalidNodeTypeError: ${message}`); +} + +export function throwNamespaceError (message: string): never { + throw new Error(`NamespaceError: ${message}`); +} + +export function throwNotFoundError (message: string): never { + throw new Error(`NotFoundError: ${message}`); +} + +export function throwNotSupportedError (message: string): never { + throw new Error(`NotSupportedError: ${message}`); +} + +export function throwWrongDocumentError (message: string): never { + throw new Error(`WrongDocumentError: ${message}`); +} diff --git a/src/util/mutationAlgorithms.ts b/src/util/mutationAlgorithms.ts new file mode 100644 index 0000000..14942a1 --- /dev/null +++ b/src/util/mutationAlgorithms.ts @@ -0,0 +1,550 @@ +import { throwHierarchyRequestError, throwNotFoundError } from './errorHelpers'; +import { NodeType, isNodeOfType } from './NodeType'; +import { determineLengthOfNode, getNodeDocument, getNodeIndex, forEachInclusiveDescendant } from './treeHelpers'; +import { insertIntoChildren, removeFromChildren } from './treeMutations'; +import Document from '../Document'; +import Element from '../Element'; +import Node from '../Node'; +import { ranges } from '../Range'; +import queueMutationRecord from '../mutation-observer/queueMutationRecord'; + +// 3.2.3. Mutation algorithms + +/** + * To ensure pre-insertion validity of a node into a parent before a child, run these steps: + */ +function ensurePreInsertionValidity (node: Node, parent: Node, child: Node | null): void { + // 1. If parent is not a Document, DocumentFragment, or Element node, throw a HierarchyRequestError. + if (!isNodeOfType(parent, NodeType.DOCUMENT_NODE, NodeType.DOCUMENT_FRAGMENT_NODE, NodeType.ELEMENT_NODE)) { + throwHierarchyRequestError('parent must be a Document, DocumentFragment or Element node'); + } + + // 2. If node is a host-including inclusive ancestor of parent, throw a HierarchyRequestError. + if (node.contains(parent)) { + throwHierarchyRequestError('node must not be an inclusive ancestor of parent'); + } + + // 3. If child is not null and its parent is not parent, then throw a NotFoundError. + if (child && child.parentNode !== parent) { + throwNotFoundError('child is not a child of parent'); + } + + // 4. If node is not a DocumentFragment, DocumentType, Element, Text, ProcessingInstruction, or Comment node, throw + // a HierarchyRequestError. + if (!isNodeOfType( + node, + NodeType.DOCUMENT_FRAGMENT_NODE, + NodeType.DOCUMENT_TYPE_NODE, + NodeType.ELEMENT_NODE, + NodeType.TEXT_NODE, + NodeType.PROCESSING_INSTRUCTION_NODE, + NodeType.COMMENT_NODE + )) { + throwHierarchyRequestError( + 'node must be a DocumentFragment, DocumentType, Element, Text, ProcessingInstruction or Comment node' + ); + } + + // 5. If either node is a Text node and parent is a document, or node is a doctype and parent is not a document, + // throw a HierarchyRequestError. + if (isNodeOfType(node, NodeType.TEXT_NODE) && isNodeOfType(parent, NodeType.DOCUMENT_NODE)) { + throwHierarchyRequestError('can not insert a Text node under a Document'); + } + if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE) && !isNodeOfType(parent, NodeType.DOCUMENT_NODE)) { + throwHierarchyRequestError('can only insert a DocumentType node under a Document'); + } + + // 6. If parent is a document, and any of the statements below, switched on node, are true, throw a + // HierarchyRequestError. + if (isNodeOfType(parent, NodeType.DOCUMENT_NODE)) { + const parentDocument = parent as Document; + switch (node.nodeType) { + // DocumentFragment node + case NodeType.DOCUMENT_FRAGMENT_NODE: + // If node has more than one element child or has a Text node child. + // Otherwise, if node has one element child and either parent has an element child, child is a doctype, + // or child is not null and a doctype is following child. + // (document fragments not implemented) + break; + + // element + case NodeType.ELEMENT_NODE: + // parent has an element child, child is a doctype, or child is not null and a doctype is following + // child. + if ( + parentDocument.documentElement || + (child && isNodeOfType(child, NodeType.DOCUMENT_TYPE_NODE)) || + (child && parentDocument.doctype && getNodeIndex(child) < getNodeIndex(parentDocument.doctype)) + ) { + throwHierarchyRequestError( + 'Document should contain at most one doctype, followed by at most one element' + ); + } + break; + + // doctype + case NodeType.DOCUMENT_TYPE_NODE: + // parent has a doctype child, child is non-null and an element is preceding child, or child is null and + // parent has an element child. + if ( + parentDocument.doctype || + ( + child && + parentDocument.documentElement && + getNodeIndex(parentDocument.documentElement) < getNodeIndex(child) + ) || + (!child && parentDocument.documentElement) + ) { + throwHierarchyRequestError( + 'Document should contain at most one doctype, followed by at most one element' + ); + } + break; + } + } +} + +/** + * To pre-insert a node into a parent before a child, run these steps: + * + * @param node Node to pre-insert + * @param parent Parent to insert under + * @param child Child to insert before, or null to insert at the end of parent + * + * @return The inserted node + */ +export function preInsertNode (node: Node, parent: Node, child: Node | null): Node { + // 1. Ensure pre-insertion validity of node into parent before child. + ensurePreInsertionValidity(node, parent, child); + + // 2. Let reference child be child. + let referenceChild = child; + + // 3. If reference child is node, set it to node’s next sibling. + if (referenceChild === node) { + referenceChild = node.nextSibling; + } + + // 4. Adopt node into parent’s node document. + adoptNode(node, getNodeDocument(parent)); + + // 5. Insert node into parent before reference child. + insertNode(node, parent, referenceChild); + + // 6. Return node. + return node; +} + +/** + * To insert a node into a parent before a child, with an optional suppress observers flag, run these steps: + * + * @param node Node to insert + * @param parent Parent to insert under + * @param child Child to insert before, or null to insert at end of parent + * @param suppressObservers Whether to skip enqueueing a mutation record for this mutation + */ +export function insertNode (node: Node, parent: Node, child: Node | null, suppressObservers: boolean = false): void { + // 1. Let count be the number of children of node if it is a DocumentFragment node, and one otherwise. + const isDocumentFragment = isNodeOfType(node, NodeType.DOCUMENT_FRAGMENT_NODE); + const count = isDocumentFragment ? determineLengthOfNode(node) : 1; + + // 2. If child is non-null, then: + if (child !== null) { + const childIndex = getNodeIndex(child); + ranges.forEach(range => { + // 2.1. For each range whose start node is parent and start offset is greater than child’s index, increase + // its start offset by count. + if (range.startContainer === parent && range.startOffset > childIndex) { + range.startOffset += count; + } + + // 2.2. For each range whose end node is parent and end offset is greater than child’s index, increase its + // end offset by count. + if (range.endContainer === parent && range.endOffset > childIndex) { + range.endOffset += count; + } + }); + } + + // (see note at 7.) + const oldPreviousSibling = child === null ? parent.lastChild : child.previousSibling; + + // 3. Let nodes be node’s children if node is a DocumentFragment node, and a list containing solely node otherwise. + const nodes = isDocumentFragment ? Array.from(node.childNodes) : [node]; + + // 4. If node is a DocumentFragment node, remove its children with the suppress observers flag set. + if (isDocumentFragment) { + nodes.forEach(n => removeNode(n, node, true)); + } + + // 5. If node is a DocumentFragment node, queue a mutation record of "childList" for node with removedNodes nodes. + // This step intentionally does not pay attention to the suppress observers flag. + if (isDocumentFragment) { + queueMutationRecord('childList', node, { + removedNodes: nodes + }); + } + + // 6. For each node in nodes, in tree order: + nodes.forEach(node => { + // 6.1. If child is null, then append node to parent’s children. + // 6.2. Otherwise, insert node into parent’s children before child’s index. + insertIntoChildren(node, parent, child) + + // 6.3. If parent is a shadow host and node is a slotable, then assign a slot for node. + // 6.4. If parent is a slot whose assigned nodes is the empty list, then run signal a slot change for parent. + // 6.5. Run assign slotables for a tree with node’s tree and a set containing each inclusive descendant of node + // that is a slot. + // (shadow dom not implemented) + + // 6.6. For each shadow-including inclusive descendant inclusiveDescendant of node, in shadow-including tree + // order: + // 6.6.1. Run the insertion steps with inclusiveDescendant. + // (insertion steps not implemented) + + // 6.6.2. If inclusiveDescendant is connected, then: + // 6.6.2.1. If inclusiveDescendant is custom, then enqueue a custom element callback reaction with + // inclusiveDescendant, callback name "connectedCallback", and an empty argument list. + // 6.6.2.2. Otherwise, try to upgrade inclusiveDescendant. + // If this successfully upgrades inclusiveDescendant, its connectedCallback will be enqueued automatically + // during the upgrade an element algorithm. + // (custom elements not implemented) + }); + + // 7. If suppress observers flag is unset, queue a mutation record of "childList" for parent with addedNodes nodes, + // nextSibling child, and previousSibling child’s previous sibling or parent’s last child if child is null. + // Note: if implemented as stated in the spec, previous sibling would be determined after insertion, and would + // therefore always be the last of nodes. + if (!suppressObservers) { + queueMutationRecord('childList', parent, { + addedNodes: nodes, + nextSibling: child, + previousSibling: oldPreviousSibling + }); + } +} + +/** + * To append a node to a parent + * + * @param node Node to append + * @param parent Parent to append to + * + * @return The appended node + */ +export function appendNode (node: Node, parent: Node): Node { + // pre-insert node into parent before null. + return preInsertNode(node, parent, null); +} + +/** + * To replace a child with node within a parent, run these steps: + * + * @param child The child node to replace + * @param node The node to replace child with + * @param parent The parent to replace under + * + * @return The old child node + */ +export function replaceChildWithNode (child: Node, node: Node, parent: Node): Node { + // 1. If parent is not a Document, DocumentFragment, or Element node, throw a HierarchyRequestError. + if (!isNodeOfType(parent, NodeType.DOCUMENT_NODE, NodeType.DOCUMENT_FRAGMENT_NODE, NodeType.ELEMENT_NODE)) { + throwHierarchyRequestError('Can not replace under a non-parent node'); + } + + // 2. If node is a host-including inclusive ancestor of parent, throw a HierarchyRequestError. + if (node.contains(parent)) { + throwHierarchyRequestError('Can not insert a node under its own descendant'); + } + + // 3. If child’s parent is not parent, then throw a NotFoundError. + if (child.parentNode !== parent) { + throwNotFoundError('child is not a child of parent'); + } + + // 4. If node is not a DocumentFragment, DocumentType, Element, Text, ProcessingInstruction, or Comment node, throw + // a HierarchyRequestError. + if (!isNodeOfType( + node, + NodeType.DOCUMENT_FRAGMENT_NODE, + NodeType.DOCUMENT_TYPE_NODE, + NodeType.ELEMENT_NODE, + NodeType.TEXT_NODE, + NodeType.PROCESSING_INSTRUCTION_NODE, + NodeType.COMMENT_NODE + )) { + throwHierarchyRequestError( + 'Can not insert a node that isn\'t a DocumentFragment, DocumentType, Element, Text, ' + + 'ProcessingInstruction or Comment' + ); + } + + // 5. If either node is a Text node and parent is a document, or node is a doctype and parent is not a document, + // throw a HierarchyRequestError. + if (isNodeOfType(node, NodeType.TEXT_NODE) && isNodeOfType(parent, NodeType.DOCUMENT_NODE)) { + throwHierarchyRequestError('can not insert a Text node under a Document'); + } + if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE) && !isNodeOfType(node, NodeType.DOCUMENT_NODE)) { + throwHierarchyRequestError('can only insert a DocumentType node under a Document'); + } + + // 6. If parent is a document, and any of the statements below, switched on node, are true, throw a + // HierarchyRequestError. + if (isNodeOfType(parent, NodeType.DOCUMENT_NODE)) { + const parentDocument = parent as Document; + switch (node.nodeType) { + // DocumentFragment node + case NodeType.DOCUMENT_FRAGMENT_NODE: + // If node has more than one element child or has a Text node child. + // Otherwise, if node has one element child and either parent has an element child that is not child or + // a doctype is following child. + // (document fragments not implemented) + break; + + // element + case NodeType.ELEMENT_NODE: + // parent has an element child that is not child or a doctype is following child. + if ( + (parentDocument.documentElement && parentDocument.documentElement !== child) || + (parentDocument.doctype && getNodeIndex(child) < getNodeIndex(parentDocument.doctype)) + ) { + throwHierarchyRequestError( + 'Document should contain at most one doctype, followed by at most one element' + ); + } + break; + + // doctype + case NodeType.DOCUMENT_TYPE_NODE: + // parent has a doctype child that is not child, or an element is preceding child. + if ( + (parentDocument.doctype && parentDocument.doctype !== child) || + ( + parentDocument.documentElement && + getNodeIndex(parentDocument.documentElement) < getNodeIndex(child) + ) + ) { + throwHierarchyRequestError( + 'Document should contain at most one doctype, followed by at most one element' + ); + } + break; + } + // The above statements differ from the pre-insert algorithm. + } + + // 7. Let reference child be child’s next sibling. + let referenceChild = child.nextSibling; + + // 8. If reference child is node, set it to node’s next sibling. + if (referenceChild === node) { + referenceChild = node.nextSibling; + } + + // 9. Let previousSibling be child’s previous sibling. + const previousSibling = child.previousSibling; + + // 10. Adopt node into parent’s node document. + adoptNode(node, getNodeDocument(parent)); + + // 11. Let removedNodes be the empty list. + let removedNodes: Node[] = []; + + // 12. If child’s parent is not null, then: + if (child.parentNode !== null) { + // 12.1. Set removedNodes to a list solely containing child. + removedNodes.push(child); + + // 12.2. Remove child from its parent with the suppress observers flag set. + removeNode(child, child.parentNode, true); + } + // The above can only be false if child is node. + + // 13. Let nodes be node’s children if node is a DocumentFragment node, and a list containing solely node otherwise. + const nodes = isNodeOfType(node, NodeType.DOCUMENT_FRAGMENT_NODE) ? Array.from(node.childNodes) : [node]; + + // 14. Insert node into parent before reference child with the suppress observers flag set. + insertNode(node, parent, referenceChild, true); + + // 15. Queue a mutation record of "childList" for target parent with addedNodes nodes, removedNodes removedNodes, + // nextSibling reference child, and previousSibling previousSibling. + queueMutationRecord('childList', parent, { + addedNodes: nodes, + removedNodes: removedNodes, + nextSibling: referenceChild, + previousSibling: previousSibling + }); + + // 16. Return child. + return child; +} + +/** + * To pre-remove a child from a parent, run these steps: + * + * @param child Child node to remove + * @param parent Parent under which to remove child + * + * @return The removed child + */ +export function preRemoveChild (child: Node, parent: Node): Node { + // 1. If child’s parent is not parent, then throw a NotFoundError. + if (child.parentNode !== parent) { + throwNotFoundError('child is not a child of parent'); + } + + // 2. Remove child from parent. + removeNode(child, parent); + + // 3. Return child. + return child; +} + +/** + * To remove a node from a parent, with an optional suppress observers flag, run these steps: + * + * @param node Child to remove + * @param parent Parent to remove child from + * @param suppressObservers Whether to skip enqueueing a mutation record for this mutation + */ +export function removeNode (node: Node, parent: Node, suppressObservers: boolean = false): void { + // 1. Let index be node’s index. + const index = getNodeIndex(node); + + ranges.forEach(range => { + // 2. For each range whose start node is an inclusive descendant of node, set its start to (parent, index). + if (node.contains(range.startContainer)) { + range.startContainer = parent; + range.startOffset = index; + } + + // 3. For each range whose end node is an inclusive descendant of node, set its end to (parent, index). + if (node.contains(range.endContainer)) { + range.endContainer = parent; + range.endOffset = index; + } + + // 4. For each range whose start node is parent and start offset is greater than index, decrease its start + // offset by one. + if (range.startContainer === parent && range.startOffset > index) { + range.startOffset -= 1; + } + + // 5. For each range whose end node is parent and end offset is greater than index, decrease its end offset by + // one. + if (range.endContainer === parent && range.endOffset > index) { + range.endOffset -= 1; + } + }) + + // 6. For each NodeIterator object iterator whose root’s node document is node’s node document, run the NodeIterator + // pre-removing steps given node and iterator. + // (NodeIterator not implemented) + + // 7. Let oldPreviousSibling be node’s previous sibling. + const oldPreviousSibling = node.previousSibling; + + // 8. Let oldNextSibling be node’s next sibling. + const oldNextSibling = node.nextSibling; + + // 9. Remove node from its parent’s children. + removeFromChildren(node, parent); + + // 10. If node is assigned, then run assign slotables for node’s assigned slot. + // (shadow dom not implemented) + + // 11. If parent is a slot whose assigned nodes is the empty list, then run signal a slot change for parent. + // (shadow dom not implemented) + + // 12. If node has an inclusive descendant that is a slot, then: + // 12.1. Run assign slotables for a tree with parent’s tree. + // 12.2. Run assign slotables for a tree with node’s tree and a set containing each inclusive descendant of node + // that is a slot. + // (shadow dom not implemented) + + // 13. Run the removing steps with node and parent. + // (removing steps not implemented) + + // 14. If node is custom, then enqueue a custom element callback reaction with node, callback name + // "disconnectedCallback", and an empty argument list. + // It is intentional for now that custom elements do not get parent passed. This might change in the future if there + // is a need. + // (custom elements not implemented) + + // 15. For each shadow-including descendant descendant of node, in shadow-including tree order, then: + // 15.1. Run the removing steps with descendant. + // (shadow dom not implemented) + + // 15.2. If descendant is custom, then enqueue a custom element callback reaction with descendant, callback name + // "disconnectedCallback", and an empty argument list. + // (custom elements not implemented) + + // 16. For each inclusive ancestor inclusiveAncestor of parent, if inclusiveAncestor has any registered observers + // whose options' subtree is true, then for each such registered observer registered, append a transient registered + // observer whose observer and options are identical to those of registered and source which is registered to node’s + // list of registered observers. + for ( + let inclusiveAncestor: Node | null = parent; + inclusiveAncestor; + inclusiveAncestor = inclusiveAncestor.parentNode + ) { + inclusiveAncestor._registeredObservers.appendTransientRegisteredObservers(node); + } + + // 17. If suppress observers flag is unset, queue a mutation record of "childList" for parent with removedNodes a + // list solely containing node, nextSibling oldNextSibling, and previousSibling oldPreviousSibling. + if (!suppressObservers) { + queueMutationRecord('childList', parent, { + removedNodes: [node], + nextSibling: oldNextSibling, + previousSibling: oldPreviousSibling + }); + } +} + +/** + * 3.5. Interface Document + * + * To adopt a node into a document, run these steps: + * + * @param node Node to adopt + * @param document Document to adopt node into + */ +export function adoptNode (node: Node, document: Document): void { + // 1. Let oldDocument be node’s node document. + const oldDocument = getNodeDocument(node); + + // 2. If node’s parent is not null, remove node from its parent. + if (node.parentNode) { + removeNode(node, node.parentNode); + } + + // 3. If document is not oldDocument, then: + if (document === oldDocument) { + return; + } + + // 3.1. For each inclusiveDescendant in node’s shadow-including inclusive descendants: + forEachInclusiveDescendant(node, node => { + // 3.1.1. Set inclusiveDescendant’s node document to document. + if (!isNodeOfType(node, NodeType.DOCUMENT_NODE)) { + node.ownerDocument = document; + } + // 3.1.2. If inclusiveDescendant is an element, then set the node document of each attribute in + // inclusiveDescendant’s attribute list to document. + if (isNodeOfType(node, NodeType.ELEMENT_NODE)) { + for (const attr of (node as Element).attributes) { + attr.ownerDocument = document; + } + } + }) + + // 3.2. For each inclusiveDescendant in node’s shadow-including inclusive descendants that is custom, enqueue a + // custom element callback reaction with inclusiveDescendant, callback name "adoptedCallback", and an argument list + // containing oldDocument and document. + // (custom element support has not been implemented) + + // 3.3. For each inclusiveDescendant in node’s shadow-including inclusive descendants, in shadow-including tree + // order, run the adopting steps with inclusiveDescendant and oldDocument. + // (adopting steps not implemented) +} diff --git a/src/util/namespaceHelpers.ts b/src/util/namespaceHelpers.ts new file mode 100644 index 0000000..10a98c0 --- /dev/null +++ b/src/util/namespaceHelpers.ts @@ -0,0 +1,70 @@ +import { throwNamespaceError } from './errorHelpers'; + +// 1.5. Namespaces + +const XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace'; +const XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/'; + +/** + * To validate a qualifiedName, + * + * @param qualifiedName Qualified name to validate + */ +export function validateQualifiedName (qualifiedName: string): void { + // TODO: throw an InvalidCharacterError if qualifiedName does not match the Name or QName production. +} + +/** + * To validate and extract a namespace and qualifiedName, run these steps: + * + * @param namespace Namespace for the qualified name + * @param qualifiedName Qualified name to validate and extract the components of + * + * @return Namespace, prefix and localName + */ +export function validateAndExtract (namespace: string | null, qualifiedName: string): { namespace: string | null, prefix: string | null, localName: string } { + // 1. If namespace is the empty string, set it to null. + if (namespace === '') { + namespace = null; + } + + // 2. Validate qualifiedName. + validateQualifiedName(qualifiedName); + + // 3. Let prefix be null. + let prefix: string | null = null; + + // 4. Let localName be qualifiedName. + let localName = qualifiedName; + + // 5. If qualifiedName contains a ":" (U+003E), then split the string on it and set prefix to the part before and + // localName to the part after. + const index = qualifiedName.indexOf(':'); + if (index >= 0) { + prefix = qualifiedName.substring(0, index); + localName = qualifiedName.substring(index + 1); + } + + // 6. If prefix is non-null and namespace is null, then throw a NamespaceError. + if (prefix !== null && namespace === null) { + throwNamespaceError('Qualified name with prefix can not have a null namespace'); + } + + // 7. If prefix is "xml" and namespace is not the XML namespace, then throw a NamespaceError. + if (prefix === 'xml' && namespace !== XML_NAMESPACE) { + throwNamespaceError('xml prefix can only be used for the XML namespace'); + } + + // 8. If either qualifiedName or prefix is "xmlns" and namespace is not the XMLNS namespace, then throw a NamespaceError. + if ((qualifiedName === 'xmlns' || prefix === 'xmlns') && namespace !== XMLNS_NAMESPACE) { + throwNamespaceError('xmlns prefix or qualifiedName must use the XMLNS namespace'); + } + + // 9. If namespace is the XMLNS namespace and neither qualifiedName nor prefix is "xmlns", then throw a NamespaceError. + if (namespace === XMLNS_NAMESPACE && qualifiedName !== 'xmlns' && prefix !== 'xmlns') { + throwNamespaceError('xmlns prefix or qualifiedName must be used for the XMLNS namespace'); + } + + // 10. Return namespace, prefix, and localName. + return { namespace, prefix, localName }; +} diff --git a/src/util/treeHelpers.ts b/src/util/treeHelpers.ts new file mode 100644 index 0000000..9a67519 --- /dev/null +++ b/src/util/treeHelpers.ts @@ -0,0 +1,101 @@ +import CharacterData from '../CharacterData'; +import Document from '../Document'; +import Node from '../Node'; +import { NodeType, isNodeOfType } from './NodeType'; + +/** + * 3.2. Node Tree: to determine the length of a node, switch on node: + * + * @param node The node to determine the length of + * + * @return The length of the node + */ +export function determineLengthOfNode (node: Node): number { + switch (node.nodeType) { + // DocumentType: Zero. + case NodeType.DOCUMENT_TYPE_NODE: + return 0; + + // Text, ProcessingInstruction, Comment: The number of code units in its data. + case NodeType.TEXT_NODE: + case NodeType.PROCESSING_INSTRUCTION_NODE: + case NodeType.COMMENT_NODE: + return (node as CharacterData).data.length; + + // Any other node: Its number of children. + default: + return node.childNodes.length; + } +} + +/** + * Get inclusive ancestors of the given node. + * + * @param node Node to get inclusive ancestors of + * + * @return Node's inclusive ancestors, in tree order + */ +export function getInclusiveAncestors (node: Node): Node[] { + let ancestor: Node | null = node; + let ancestors: Node[] = []; + while (ancestor) { + ancestors.unshift(ancestor); + ancestor = ancestor.parentNode; + } + + return ancestors; +} + +/** + * Get the node document associated with the given node. + * + * @param node The node to get the node document for + * + * @return The node document for node + */ +export function getNodeDocument (node: Node): Document { + if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { + return node as Document; + } + + return node.ownerDocument!; +} + +/** + * Determine the index of the given node among its siblings. + * + * @param node Node to determine the index of + * + * @return The index of node in its parent's children + */ +export function getNodeIndex (node: Node): number { + return (node.parentNode as Node).childNodes.indexOf(node); +} + +/** + * The root of an object is itself, if its parent is null, or else it is the root of its parent. + * + * @param node Node to get the root of + * + * @return The root of node + */ +export function getRootOfNode (node: Node): Node { + while (node.parentNode) { + node = node.parentNode; + } + + return node; +} + +/** + * Invokes callback on each inclusive descendant of node, in tree order + * + * @param node Root of the subtree to process + * @param callback Callback to invoke for each descendant, should not modify node's position in the tree + */ +export function forEachInclusiveDescendant (node: Node, callback: (node: Node) => void): void { + callback(node); + for (let child = node.firstChild; child; child = child.nextSibling) { + forEachInclusiveDescendant(child, callback); + } +} diff --git a/src/util/treeMutations.ts b/src/util/treeMutations.ts new file mode 100644 index 0000000..ba4c556 --- /dev/null +++ b/src/util/treeMutations.ts @@ -0,0 +1,150 @@ +import { asParentNode, asNonDocumentTypeChildNode } from '../mixins'; +import Document from '../Document'; +import DocumentType from '../DocumentType'; +import Element from '../Element'; +import Node from '../Node'; + +import { NodeType, isNodeOfType } from './NodeType'; + +/** + * Insert node into parent's children before referenceNode. + * + * Updates the pointers that model the tree, as well as precomputing derived properties. + * + * @param node Node to insert + * @param parent Parent to insert under + * @param referenceChild Child to insert before + */ +export function insertIntoChildren (node: Node, parent: Node, referenceChild: Node | null): void { + // Node + node.parentNode = parent; + const previousSibling: Node | null = referenceChild === null ? parent.lastChild : referenceChild.previousSibling; + const nextSibling: Node | null = referenceChild === null ? null : referenceChild; + node.previousSibling = previousSibling; + node.nextSibling = nextSibling; + if (previousSibling) { + previousSibling.nextSibling = node; + } + else { + parent.firstChild = node; + } + if (nextSibling) { + nextSibling.previousSibling = node; + parent.childNodes.splice(parent.childNodes.indexOf(nextSibling), 0, node); + } + else { + parent.lastChild = node; + parent.childNodes.push(node); + } + + // ParentNode + if (isNodeOfType(node, NodeType.ELEMENT_NODE)) { + const element = node as Element; + const parentNode = asParentNode(parent); + if (parentNode) { + let previousElementSibling: Element | null = null; + for (let sibling = previousSibling; sibling; sibling = sibling.previousSibling) { + if (isNodeOfType(sibling, NodeType.ELEMENT_NODE)) { + previousElementSibling = sibling as Element; + break; + } + const siblingNonDocumentTypeChildNode = asNonDocumentTypeChildNode(sibling); + if (siblingNonDocumentTypeChildNode) { + previousElementSibling = siblingNonDocumentTypeChildNode.previousElementSibling; + break; + } + } + + let nextElementSibling: Element | null = null; + for (let sibling = nextSibling; sibling; sibling = sibling.nextSibling) { + if (isNodeOfType(sibling, NodeType.ELEMENT_NODE)) { + nextElementSibling = sibling as Element; + break; + } + const siblingNonDocumentTypeChildNode = asNonDocumentTypeChildNode(sibling); + if (siblingNonDocumentTypeChildNode) { + nextElementSibling = siblingNonDocumentTypeChildNode.nextElementSibling; + break; + } + } + + if (!previousElementSibling) { + parentNode.firstElementChild = element; + } + if (!nextElementSibling) { + parentNode.lastElementChild = element; + } + parentNode.childElementCount += 1; + } + } + + // Document + if (isNodeOfType(parent, NodeType.DOCUMENT_NODE)) { + const parentDocument = parent as Document; + if (isNodeOfType(node, NodeType.ELEMENT_NODE)) { + parentDocument.documentElement = node as Element; + } + else if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { + parentDocument.doctype = node as DocumentType; + } + } +} + +/** + * Remove node from parent's children. + * + * Updates the pointers that model the tree, as well as precomputing derived properties. + * + * @param node Node to remove + * @param parent Parent to remove from + */ +export function removeFromChildren (node: Node, parent: Node) { + const previousSibling = node.previousSibling; + const nextSibling = node.nextSibling; + const isElement = isNodeOfType(node, NodeType.ELEMENT_NODE); + const previousElementSibling = isElement ? (node as Element).previousElementSibling : null; + const nextElementSibling = isElement ? (node as Element).nextElementSibling : null; + + // Node + node.parentNode = null; + node.previousSibling = null; + node.nextSibling = null; + if (previousSibling) { + previousSibling.nextSibling = nextSibling; + } + else { + parent.firstChild = nextSibling; + } + if (nextSibling) { + nextSibling.previousSibling = previousSibling; + } + else { + parent.lastChild = previousSibling; + } + parent.childNodes.splice(parent.childNodes.indexOf(node), 1); + + // ParentNode + if (isElement) { + const parentNode = asParentNode(parent); + if (parentNode) { + if (parentNode.firstElementChild === node) { + parentNode.firstElementChild = nextElementSibling; + } + if (parentNode.lastElementChild === node) { + parentNode.lastElementChild = previousElementSibling; + } + parentNode.childElementCount -= 1; + } + } + + // Document + if (isNodeOfType(parent, NodeType.DOCUMENT_NODE)) { + const parentDocument = parent as Document; + if (isNodeOfType(node, NodeType.ELEMENT_NODE)) { + parentDocument.documentElement = null; + } + else if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { + parentDocument.doctype = null; + } + } +} diff --git a/test/Comment.tests.ts b/test/Comment.tests.ts index 7066db3..3fb3326 100644 --- a/test/Comment.tests.ts +++ b/test/Comment.tests.ts @@ -1,4 +1,4 @@ -import slimdom from '../src/index'; +import * as slimdom from '../src/index'; import Comment from '../src/Comment'; import Document from '../src/Document'; diff --git a/test/Document.tests.ts b/test/Document.tests.ts index 6ff7ab3..badd20e 100644 --- a/test/Document.tests.ts +++ b/test/Document.tests.ts @@ -1,4 +1,4 @@ -import slimdom from '../src/index'; +import * as slimdom from '../src/index'; import Document from '../src/Document'; import DOMImplementation from '../src/DOMImplementation'; @@ -24,12 +24,6 @@ describe('Document', () => { it('initially has no childNodes', () => chai.assert.deepEqual(document.childNodes, [])); - it('can have user data', () => { - chai.assert.equal(document.getUserData('test'), null); - document.setUserData('test', {abc: 123}); - chai.assert.deepEqual(document.getUserData('test'), {abc: 123}); - }); - describe('after appending a child element', () => { let element: Element; beforeEach(() => { diff --git a/test/DocumentType.tests.ts b/test/DocumentType.tests.ts index e51c4d6..f763ef1 100644 --- a/test/DocumentType.tests.ts +++ b/test/DocumentType.tests.ts @@ -1,4 +1,4 @@ -import slimdom from '../src/index'; +import * as slimdom from '../src/index'; import DocumentType from '../src/DocumentType'; diff --git a/test/Element.tests.ts b/test/Element.tests.ts index 4f23288..c7cfc59 100644 --- a/test/Element.tests.ts +++ b/test/Element.tests.ts @@ -1,5 +1,6 @@ -import slimdom from '../src/index' +import * as slimdom from '../src/index' +import Attr from '../src/Attr'; import Document from '../src/Document'; import Element from '../src/Element'; import Node from '../src/Node'; @@ -29,8 +30,7 @@ describe('Element', () => { it('initially has no child elements', () => { chai.assert.equal(element.firstElementChild, null); chai.assert.equal(element.lastElementChild, null); - // TODO: Element.children not yet supported - //chai.assert.deepEqual(element.children, []); + chai.assert.deepEqual(element.children, []); chai.assert.equal(element.childElementCount, 0); }); @@ -61,21 +61,27 @@ describe('Element', () => { chai.assert.equal(element.getAttribute('noSuchAttribute'), null); }); - it('has attributes', () => chai.assert.deepEqual(element.attributes, [ + function hasAttributes (attributes: Attr[], expected: { name: string, value: string }[]): boolean { + return attributes.length === expected.length && + attributes.every(attr => expected.some(pair => pair.name === attr.name && pair.value === attr.value)) && + expected.every(pair => attributes.some(attr => attr.name === pair.name && attr.value === pair.value)); + } + + it('has attributes', () => chai.assert(hasAttributes(element.attributes, [ {name: 'firstAttribute', value: 'first'}, {name: 'test', value: '123'}, {name: 'lastAttribute', value: 'last'} - ])); + ]))); it('can overwrite the attribute', () => { element.setAttribute('test', '456'); chai.assert(element.hasAttribute('test'), 'has the attribute'); chai.assert.equal(element.getAttribute('test'), '456'); - chai.assert.deepEqual(element.attributes, [ + chai.assert(hasAttributes(element.attributes, [ {name: 'firstAttribute', value: 'first'}, {name: 'test', value: '456'}, {name: 'lastAttribute', value: 'last'} - ]); + ])); }); it('can remove the attribute', () => { @@ -83,10 +89,10 @@ describe('Element', () => { chai.assert(element.hasAttribute('firstAttribute'), 'has attribute firstAttribute'); chai.assert(!element.hasAttribute('test'), 'does not have attribute test'); chai.assert(element.hasAttribute('lastAttribute'), 'has attribute lastAttribute'); - chai.assert.deepEqual(element.attributes, [ + chai.assert(hasAttributes(element.attributes, [ {name: 'firstAttribute', value: 'first'}, {name: 'lastAttribute', value: 'last'} - ]); + ])); }); it('ignores removing non-existent attributes', () => { @@ -94,11 +100,11 @@ describe('Element', () => { element.removeAttribute('other'); chai.assert(!element.hasAttribute('other'), 'does not have attribute other'); chai.assert(element.hasAttribute('test'), 'has attribute test'); - chai.assert.deepEqual(element.attributes, [ + chai.assert(hasAttributes(element.attributes, [ {name: 'firstAttribute', value: 'first'}, {name: 'test', value: '123'}, {name: 'lastAttribute', value: 'last'} - ]); + ])); }); }); @@ -118,8 +124,7 @@ describe('Element', () => { it('has child element references', () => { chai.assert.equal(element.firstElementChild, child); chai.assert.equal(element.lastElementChild, child); - // TODO: Element.children not yet supported - //chai.assert.deepEqual(element.children, [ child ]); + chai.assert.deepEqual(element.children, [ child ]); chai.assert.equal(element.childElementCount, 1); }); @@ -137,8 +142,7 @@ describe('Element', () => { it('has no child elements', () => { chai.assert.equal(element.firstElementChild, null); chai.assert.equal(element.lastElementChild, null); - // TODO: Element.children not yet supported - //chai.assert.deepEqual(element.children, []); + chai.assert.deepEqual(element.children, []); chai.assert.equal(element.childElementCount, 0); }); }); @@ -159,8 +163,7 @@ describe('Element', () => { it('has child element references', () => { chai.assert.equal(element.firstElementChild, otherChild); chai.assert.equal(element.lastElementChild, otherChild); - // TODO: Element.children not yet supported - //chai.assert.deepEqual(element.children, [ otherChild ]); + chai.assert.deepEqual(element.children, [ otherChild ]); chai.assert.equal(element.childElementCount, 1); }); }); @@ -181,8 +184,7 @@ describe('Element', () => { it('has child element references', () => { chai.assert.equal(element.firstElementChild, otherChild); chai.assert.equal(element.lastElementChild, child); - // TODO: Element.children not yet supported - //chai.assert.deepEqual(element.children, [ otherChild, child ]); + chai.assert.deepEqual(element.children, [ otherChild, child ]); chai.assert.equal(element.childElementCount, 2); }); @@ -215,8 +217,7 @@ describe('Element', () => { it('has child element references', () => { chai.assert.equal(element.firstElementChild, child); chai.assert.equal(element.lastElementChild, otherChild); - // TODO: Element.children not yet supported - //chai.assert.deepEqual(element.children, [ child, otherChild ]); + chai.assert.deepEqual(element.children, [ child, otherChild ]); chai.assert.equal(element.childElementCount, 2); }); @@ -247,8 +248,7 @@ describe('Element', () => { it('has child element references', () => { chai.assert.equal(element.firstElementChild, child); chai.assert.equal(element.lastElementChild, child); - // TODO: Element.children not yet supported - //chai.assert.deepEqual(element.children, [ child ]); + chai.assert.deepEqual(element.children, [ child ]); chai.assert.equal(element.childElementCount, 1); }); @@ -277,8 +277,7 @@ describe('Element', () => { it('has no child elements', () => { chai.assert.equal(element.firstElementChild, null); chai.assert.equal(element.lastElementChild, null); - // TODO: Element.children not yet supported - //chai.assert.deepEqual(element.children, []); + chai.assert.deepEqual(element.children, []); chai.assert.equal(element.childElementCount, 0); }); @@ -298,8 +297,7 @@ describe('Element', () => { it('has child element references', () => { chai.assert.equal(element.firstElementChild, otherChild); chai.assert.equal(element.lastElementChild, otherChild); - // TODO: Element.children not yet supported - //chai.assert.deepEqual(element.children, [ otherChild ]); + chai.assert.deepEqual(element.children, [ otherChild ]); chai.assert.equal(element.childElementCount, 1); }); }); diff --git a/test/mutations/MutationObserver.tests.ts b/test/MutationObserver.tests.ts similarity index 84% rename from test/mutations/MutationObserver.tests.ts rename to test/MutationObserver.tests.ts index 3bee6b7..4611a0c 100644 --- a/test/mutations/MutationObserver.tests.ts +++ b/test/MutationObserver.tests.ts @@ -1,9 +1,9 @@ -import slimdom from '../../src/index'; +import * as slimdom from '../src/index'; -import Document from '../../src/Document'; -import Element from '../../src/Element'; -import Text from '../../src/Text'; -import MutationObserver from '../../src/mutations/MutationObserver'; +import Document from '../src/Document'; +import Element from '../src/Element'; +import Text from '../src/Text'; +import MutationObserver from '../src/mutation-observer/MutationObserver'; import * as chai from 'chai'; import * as lolex from 'lolex'; @@ -41,8 +41,7 @@ describe('MutationObserver', () => { subtree: true, characterData: true, childList: true, - attributes: true, - userData: true + attributes: true }); }); @@ -54,6 +53,19 @@ describe('MutationObserver', () => { it('responds to text changes', () => { text.data = 'meep'; + const queue = observer.takeRecords(); + chai.assert.equal(queue[0].type, 'characterData'); + chai.assert.equal(queue[0].oldValue, null); + chai.assert.equal(queue[0].target, text); + + clock.tick(100); + chai.assert(!callbackCalled, 'callback was not called'); + }); + + it('records previous text values', () => { + observer.observe(element, { subtree: true, characterDataOldValue: true }); + + text.data = 'meep'; const queue = observer.takeRecords(); chai.assert.equal(queue[0].type, 'characterData'); chai.assert.equal(queue[0].oldValue, 'text'); @@ -76,14 +88,18 @@ describe('MutationObserver', () => { chai.assert(!callbackCalled, 'callback was not called'); }); - it('ignores same-value attribute changes', () => { + it('does not ignore same-value attribute changes', () => { element.setAttribute('test', 'meep'); let queue = observer.takeRecords(); + observer.observe(element, { attributeOldValue: true }); + element.setAttribute('test', 'meep'); queue = observer.takeRecords(); - chai.assert.deepEqual(queue, []); + chai.assert.equal(queue[0].type, 'attributes'); + chai.assert.equal(queue[0].oldValue, 'meep'); + chai.assert.equal(queue[0].target, element); clock.tick(100); chai.assert(!callbackCalled, 'callback was not called'); @@ -93,6 +109,8 @@ describe('MutationObserver', () => { element.setAttribute('test', 'meep'); let queue = observer.takeRecords(); + observer.observe(element, { attributeOldValue: true }); + element.setAttribute('test', 'maap'); queue = observer.takeRecords(); @@ -105,20 +123,6 @@ describe('MutationObserver', () => { chai.assert(!callbackCalled, 'callback was not called'); }); - it('responds to userData changes', () => { - const data = {}; - element.setUserData('test', data); - - const queue = observer.takeRecords(); - chai.assert.equal(queue[0].type, 'userData'); - chai.assert.equal(queue[0].attributeName, 'test'); - chai.assert.equal(queue[0].oldValue, null); - chai.assert.equal(queue[0].target, element); - - clock.tick(100); - chai.assert(!callbackCalled, 'callback was not called'); - }); - it('responds to insertions (appendChild)', () => { const newElement = document.createElement('meep'); element.appendChild(newElement); @@ -173,12 +177,14 @@ describe('MutationObserver', () => { const queue = observer.takeRecords(); chai.assert.equal(queue[0].type, 'childList'); + chai.assert.equal(queue[0].target, element); chai.assert.deepEqual(queue[0].addedNodes, []); chai.assert.deepEqual(queue[0].removedNodes, [ newElement ]); chai.assert.equal(queue[0].previousSibling, text); chai.assert.equal(queue[0].nextSibling, null); chai.assert.equal(queue[1].type, 'childList'); + chai.assert.equal(queue[1].target, element); chai.assert.deepEqual(queue[1].addedNodes, [ newElement ]); chai.assert.deepEqual(queue[1].removedNodes, [ text ]); chai.assert.equal(queue[1].previousSibling, null); @@ -186,6 +192,7 @@ describe('MutationObserver', () => { }); it('continues tracking under a removed node until javascript re-enters the event loop', () => { + observer.observe(element, { subtree: true, characterDataOldValue: true, childList: true }); const newElement = element.appendChild(document.createElement('meep')) as Element; const newText = newElement.appendChild(document.createTextNode('test')) as Text; element.appendChild(newElement); @@ -196,20 +203,30 @@ describe('MutationObserver', () => { newText.replaceData(0, text.length, 'meep'); let queue = observer.takeRecords(); + chai.assert.equal(queue.length, 1); chai.assert.equal(queue[0].type, 'characterData'); chai.assert.equal(queue[0].oldValue, 'test'); chai.assert.equal(queue[0].target, newText); newElement.removeChild(newText); queue = observer.takeRecords(); + chai.assert.equal(queue.length, 1); chai.assert.equal(queue[0].type, 'childList'); chai.assert.equal(queue[0].target, newElement); chai.assert.equal(queue[0].removedNodes[0], newText); + + clock.tick(100); + + newElement.appendChild(newText); + queue = observer.takeRecords(); + chai.assert.deepEqual(queue, []); }); }); describe('asynchronous usage', () => { it('responds to text changes', () => { + observer.observe(element, { subtree: true, characterDataOldValue: true }); + text.data = 'meep'; clock.tick(100); diff --git a/test/ProcessingInstruction.tests.ts b/test/ProcessingInstruction.tests.ts index 2e7b313..f7824ef 100644 --- a/test/ProcessingInstruction.tests.ts +++ b/test/ProcessingInstruction.tests.ts @@ -1,4 +1,4 @@ -import slimdom from '../src/index'; +import * as slimdom from '../src/index'; import Document from '../src/Document'; import ProcessingInstruction from '../src/ProcessingInstruction'; diff --git a/test/selections/Range.tests.ts b/test/Range.tests.ts similarity index 96% rename from test/selections/Range.tests.ts rename to test/Range.tests.ts index 9374d5f..782a784 100644 --- a/test/selections/Range.tests.ts +++ b/test/Range.tests.ts @@ -1,10 +1,10 @@ -import slimdom from '../../src/index'; +import * as slimdom from '../src/index'; -import Document from '../../src/Document'; -import Element from '../../src/Element'; -import Node from '../../src/Node'; -import Text from '../../src/Text'; -import Range from '../../src/selections/Range'; +import Document from '../src/Document'; +import Element from '../src/Element'; +import Node from '../src/Node'; +import Text from '../src/Text'; +import Range from '../src/Range'; import * as chai from 'chai'; @@ -214,7 +214,7 @@ describe('Range', () => { it('moves with text node deletes during normalization', () => { text.deleteData(0, 4); - element.normalize(true); + element.normalize(); chai.assert.equal(range.startContainer, element); chai.assert.equal(range.startOffset, 0); chai.assert.equal(range.endContainer, element); @@ -225,7 +225,7 @@ describe('Range', () => { const otherText = element.appendChild(document.createTextNode('more')) as Node; range.setStartBefore(otherText); range.setEnd(otherText, 2); - element.normalize(true); + element.normalize(); chai.assert.equal(range.startContainer, text); chai.assert.equal(range.startOffset, 4); chai.assert.equal(range.endContainer, text); diff --git a/test/Text.tests.ts b/test/Text.tests.ts index 19c81f3..cb42e99 100644 --- a/test/Text.tests.ts +++ b/test/Text.tests.ts @@ -1,4 +1,4 @@ -import slimdom from '../src/index'; +import * as slimdom from '../src/index'; import Document from '../src/Document'; import Element from '../src/Element'; @@ -30,12 +30,6 @@ describe('Text', () => { chai.assert.equal(text.length, newValue.length); }); - // TODO: wholeText not yet supported - it('has wholeText'); - //it('has wholeText', () => { - // chai.assert.equal(text.wholeText, 'text'); - //}) - it('can be cloned', () => { var clone = text.cloneNode(true) as Text; chai.assert.equal(clone.nodeType, 3); @@ -48,7 +42,10 @@ describe('Text', () => { chai.assert.equal(text.substringData(0, 2), 'te'); chai.assert.equal(text.substringData(2, 2), 'xt'); chai.assert.equal(text.substringData(1, 2), 'ex'); - chai.assert.equal(text.substringData(2), 'xt'); + chai.assert.equal(text.substringData(2, 9999), 'xt'); + + chai.assert.throws(() => text.substringData(-123, 1), 'IndexSizeError'); + chai.assert.throws(() => text.substringData(123, 1), 'IndexSizeError'); }); it('can appendData', () => { @@ -64,15 +61,18 @@ describe('Text', () => { chai.assert.equal(text.nodeValue, text.data); chai.assert.equal(text.length, 7); - text.insertData(-100, '123'); + text.insertData(0, '123'); chai.assert.equal(text.data, '123te123xt'); chai.assert.equal(text.nodeValue, text.data); chai.assert.equal(text.length, 10); - text.insertData(100, '123'); + text.insertData(text.length, '123'); chai.assert.equal(text.data, '123te123xt123'); chai.assert.equal(text.nodeValue, text.data); chai.assert.equal(text.length, 13); + + chai.assert.throws(() => text.insertData(-123, '123'), 'IndexSizeError'); + chai.assert.throws(() => text.insertData(123, '123'), 'IndexSizeError'); }); it('can deleteData', () => { @@ -81,25 +81,28 @@ describe('Text', () => { chai.assert.equal(text.nodeValue, text.data); chai.assert.equal(text.length, 4); - text.deleteData(-100, 1); - chai.assert.equal(text.data, 'text'); + text.deleteData(0, 1); + chai.assert.equal(text.data, 'ext'); chai.assert.equal(text.nodeValue, text.data); - chai.assert.equal(text.length, 4); + chai.assert.equal(text.length, 3); - text.deleteData(100, 2); - chai.assert.equal(text.data, 'text'); + text.deleteData(text.length, 2); + chai.assert.equal(text.data, 'ext'); chai.assert.equal(text.nodeValue, text.data); - chai.assert.equal(text.length, 4); + chai.assert.equal(text.length, 3); text.deleteData(1, 1); - chai.assert.equal(text.data, 'txt'); + chai.assert.equal(text.data, 'et'); chai.assert.equal(text.nodeValue, text.data); - chai.assert.equal(text.length, 3); + chai.assert.equal(text.length, 2); - text.deleteData(2); - chai.assert.equal(text.data, 'tx'); + text.deleteData(1, 9999); + chai.assert.equal(text.data, 'e'); chai.assert.equal(text.nodeValue, text.data); - chai.assert.equal(text.length, 2); + chai.assert.equal(text.length, 1); + + chai.assert.throws(() => text.deleteData(-123, 2), 'IndexSizeError'); + chai.assert.throws(() => text.deleteData(123, 2), 'IndexSizeError'); }); it('can replaceData', () => { @@ -108,20 +111,23 @@ describe('Text', () => { chai.assert.equal(text.nodeValue, text.data); chai.assert.equal(text.length, 4); - text.replaceData(-100, 10, 'asd'); - chai.assert.equal(text.data, 'asdtext'); + text.replaceData(0, 10, 'asd'); + chai.assert.equal(text.data, 'asd'); chai.assert.equal(text.nodeValue, text.data); - chai.assert.equal(text.length, 7); + chai.assert.equal(text.length, 3); - text.replaceData(100, 10, 'asd'); - chai.assert.equal(text.data, 'asdtextasd'); + text.replaceData(text.length, 10, 'fgh'); + chai.assert.equal(text.data, 'asdfgh'); chai.assert.equal(text.nodeValue, text.data); - chai.assert.equal(text.length, 10); + chai.assert.equal(text.length, 6); text.replaceData(3, 4, 'asd'); - chai.assert.equal(text.data, 'asdasdasd'); + chai.assert.equal(text.data, 'asdasd'); chai.assert.equal(text.nodeValue, text.data); - chai.assert.equal(text.length, 9); + chai.assert.equal(text.length, 6); + + chai.assert.throws(() => text.replaceData(-123, 2, 'text'), 'IndexSizeError'); + chai.assert.throws(() => text.replaceData(123, 2, 'text'), 'IndexSizeError'); }); describe('splitting', () => { @@ -131,8 +137,11 @@ describe('Text', () => { chai.assert.equal(text.nodeValue, text.data); chai.assert.equal(otherHalf.data, 'xt'); chai.assert.equal(otherHalf.nodeValue, otherHalf.data); + + chai.assert.throws(() => text.splitText(-123), 'IndexSizeError'); + chai.assert.throws(() => text.splitText(123), 'IndexSizeError'); }); - + describe('under a parent', () => { let element: Element; let otherHalf: Text; @@ -158,13 +167,6 @@ describe('Text', () => { chai.assert.equal(text.nextSibling, otherHalf); chai.assert.equal(otherHalf.previousSibling, text); }); - - // TODO: wholeText not yet supported - it('has wholeText'); - //it('has wholeText', () => { - // chai.assert.equal(text.wholeText, 'text'); - // chai.assert.equal(otherHalf.wholeText, 'text'); - //}); }); }); }); diff --git a/test/tsconfig.json b/test/tsconfig.json index b94df99..f1703e5 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -4,7 +4,14 @@ "sourceMap": true, "strict": true, "module": "commonjs", - "target": "es5" + "target": "es5", + "lib": [ + "es2015" + ], + "types": [ + "chai", + "mocha" + ] }, "include": [ "./**/*" diff --git a/tsconfig.json b/tsconfig.json index 6895eac..9962bfd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,11 @@ "strict": true, "module": "es6", "target": "es5", - "declaration": true + "declaration": true, + "lib": [ + "es2015" + ], + "types": [] }, "include": [ "src/**/*" From 7063b749d4f75cb69f35811fd2f0ad38ef210f15 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Fri, 26 May 2017 16:33:03 +0200 Subject: [PATCH 02/34] Add type checks and coercions to match WebIDL specs. --- src/CharacterData.ts | 26 ++++++++++++------- src/DOMImplementation.ts | 9 ++++--- src/Document.ts | 54 ++++++++++++++++++++++++++++++++++++++-- src/Element.ts | 11 ++++++++ src/Node.ts | 16 ++++++++++++ src/Range.ts | 29 +++++++++++++++++++++ src/util/errorHelpers.ts | 20 +++++++++++++++ src/util/typeHelpers.ts | 38 ++++++++++++++++++++++++++++ 8 files changed, 189 insertions(+), 14 deletions(-) create mode 100644 src/util/typeHelpers.ts diff --git a/src/CharacterData.ts b/src/CharacterData.ts index 8aea980..40c3b59 100644 --- a/src/CharacterData.ts +++ b/src/CharacterData.ts @@ -4,11 +4,8 @@ import Element from './Element'; import Node from './Node'; import { ranges } from './Range'; import queueMutationRecord from './mutation-observer/queueMutationRecord'; -import { throwIndexSizeError } from './util/errorHelpers'; - -function asUnsignedLong (number: number): number { - return number >>> 0; -} +import { expectArity, throwIndexSizeError } from './util/errorHelpers'; +import { asUnsignedLong, treatNullAsEmptyString } from './util/typeHelpers'; /** * 3.10. Interface CharacterData @@ -46,8 +43,12 @@ export default abstract class CharacterData extends Node implements NonDocumentT return this._data; } - public set data (data: string) { - replaceData(this, 0, this.length, data); + public set data (newValue: string) { + // [TreatNullAs=EmptyString] + newValue = treatNullAsEmptyString(newValue); + + // replace data with node context object, offset 0, count context object’s length, and data new value. + replaceData(this, 0, this.length, newValue); } public get length (): number { @@ -72,6 +73,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * @return The specified substring */ public substringData (offset: number, count: number): string { + expectArity(arguments, 2); return substringData(this, offset, count); } @@ -81,6 +83,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * @param data Data to append */ public appendData (data: string): void { + expectArity(arguments, 1); replaceData(this, this.length, 0, data); } @@ -91,6 +94,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * @param data Data to insert */ public insertData (offset: number, data: string): void { + expectArity(arguments, 1); replaceData(this, offset, 0, data); } @@ -101,6 +105,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * @param count Number of code units to delete */ public deleteData (offset: number, count: number): void { + expectArity(arguments, 2); replaceData(this, offset, count, ''); } @@ -112,6 +117,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * @param data Data to insert */ public replaceData (offset: number, count: number, data: string): void { + expectArity(arguments, 3); replaceData(this, offset, count, data); } } @@ -125,8 +131,9 @@ export default abstract class CharacterData extends Node implements NonDocumentT * @param data The data to insert in place of the removed data */ export function replaceData (node: CharacterData, offset: number, count: number, data: string): void { - // Match spec data type + // Match spec data types offset = asUnsignedLong(offset); + count = asUnsignedLong(count); // 1. Let length be node’s length. const length = node.length; @@ -190,8 +197,9 @@ export function replaceData (node: CharacterData, offset: number, count: number, * @return The requested substring */ export function substringData (node: CharacterData, offset: number, count: number): string { - // Match spec data type + // Match spec data types offset = asUnsignedLong(offset); + count = asUnsignedLong(count); // 1. Let length be node’s length. const length = node.length; diff --git a/src/DOMImplementation.ts b/src/DOMImplementation.ts index 6f166eb..33e34d0 100644 --- a/src/DOMImplementation.ts +++ b/src/DOMImplementation.ts @@ -2,7 +2,9 @@ import DocumentType from './DocumentType'; import XMLDocument from './XMLDocument'; import createElementNS from './util/createElementNS'; +import { expectArity } from './util/errorHelpers'; import { validateQualifiedName } from './util/namespaceHelpers'; +import { asNullableObject, asNullableString, treatNullAsEmptyString } from './util/typeHelpers'; export default class DOMImplementation { /** @@ -37,10 +39,11 @@ export default class DOMImplementation { * @return The new XMLDocument */ createDocument (namespace: string | null, qualifiedName: string | null, doctype: DocumentType | null = null): XMLDocument { + expectArity(arguments, 2); + namespace = asNullableString(namespace); // [TreatNullAs=EmptyString] for qualifiedName - if (qualifiedName === null) { - qualifiedName = ''; - } + qualifiedName = treatNullAsEmptyString(qualifiedName); + doctype = asNullableObject(doctype, DocumentType); // 1. Let document be a new XMLDocument. const document = new XMLDocument(); diff --git a/src/Document.ts b/src/Document.ts index 602019b..0744754 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -12,10 +12,11 @@ import Range from './Range'; import cloneNode from './util/cloneNode'; import createElementNS from './util/createElementNS'; -import { throwNotSupportedError } from './util/errorHelpers'; +import { throwInvalidCharacterError, throwNotSupportedError } from './util/errorHelpers'; import { adoptNode } from './util/mutationAlgorithms'; import { NodeType, isNodeOfType } from './util/NodeType'; -import { validateAndExtract } from './util/namespaceHelpers'; +import { matchesNameProduction, validateAndExtract } from './util/namespaceHelpers'; +import { asNullableString } from './util/typeHelpers'; /** * 3.5. Interface Document @@ -86,7 +87,12 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new element */ public createElement (localName: string): Element { + localName = String(localName); + // 1. If localName does not match the Name production, then throw an InvalidCharacterError. + if (!matchesNameProduction(localName)) { + throwInvalidCharacterError('The local name is not a valid Name'); + } // 2. If the context object is an HTML document, then set localName to localName in ASCII lowercase. // (html documents not implemented) @@ -119,6 +125,9 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new element */ public createElementNS (namespace: string | null, qualifiedName: string): Element { + namespace = asNullableString(namespace); + qualifiedName = String(qualifiedName); + // return the result of running the internal createElementNS steps, given context object, namespace, // qualifiedName, and options. return createElementNS(this, namespace, qualifiedName); @@ -132,6 +141,8 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new text node */ public createTextNode (data: string): Text { + data = String(data); + return new Text(this, data); } @@ -143,6 +154,17 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new CDATA section */ public createCDATASection (data: string): CDATASection { + data = String(data); + + // 1. If context object is an HTML document, then throw a NotSupportedError. + // (html documents not implemented) + + // 2. If data contains the string "]]>", then throw an InvalidCharacterError. + if (data.indexOf(']]>') >= 0) { + throwInvalidCharacterError('Data must not contain the string "]]>"'); + } + + // 3. Return a new CDATASection node with its data set to data and node document set to the context object. return new CDATASection(this, data); } @@ -154,6 +176,8 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new comment node */ public createComment (data: string): Comment { + data = String(data); + return new Comment(this, data); } @@ -166,7 +190,25 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new processing instruction */ public createProcessingInstruction (target: string, data: string): ProcessingInstruction { + target = String(target); + data = String(data); + + // 1. If target does not match the Name production, then throw an InvalidCharacterError. + if (!matchesNameProduction(target)) { + throwInvalidCharacterError('The target is not a valid Name'); + } + + // 2. If data contains the string "?>", then throw an InvalidCharacterError. + if (data.indexOf('?>') >= 0) { + throwInvalidCharacterError('Data must not contain the string "?>"'); + } + + // 3. Return a new ProcessingInstruction node, with target set to target, data set to data, and node document + // set to the context object. return new ProcessingInstruction(this, target, data); + + // Note: No check is performed that target contains "xml" or ":", or that data contains characters that match + // the Char production. } /** @@ -215,7 +257,12 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new attribute node */ public createAttribute (localName: string): Attr { + localName = String(localName); + // 1. If localName does not match the Name production in XML, then throw an InvalidCharacterError. + if (!matchesNameProduction(localName)) { + throwInvalidCharacterError('The local name is not a valid Name'); + } // 2. If the context object is an HTML document, then set localName to localName in ASCII lowercase. // (html documents not implemented) @@ -233,6 +280,9 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new attribute node */ public createAttributeNS (namespace: string | null, qualifiedName: string): Attr { + namespace = asNullableString(namespace); + qualifiedName = String(qualifiedName); + // 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName to validate and // extract. const { namespace: validatedNamespace, prefix, localName } = validateAndExtract(namespace, qualifiedName); diff --git a/src/Element.ts b/src/Element.ts index 2809944..60afa6d 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -8,6 +8,7 @@ import { appendAttribute, changeAttribute, removeAttribute, replaceAttribute } f import { throwInUseAttributeError, throwNotFoundError } from './util/errorHelpers'; import { validateAndExtract } from './util/namespaceHelpers'; import { NodeType } from './util/NodeType'; +import { asNullableString } from './util/typeHelpers'; /** * 3.9. Interface Element @@ -119,6 +120,8 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @return The value of the attribute, or null if no such attribute exists */ public getAttributeNS (namespace: string | null, localName: string): string | null { + namespace = asNullableString(namespace); + // 1. Let attr be the result of getting an attribute given namespace, localName, and the context object. const attr = getAttributeByNamespaceAndLocalName(namespace, localName, this); @@ -168,6 +171,8 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param value The value for the attribute */ public setAttributeNS (namespace: string | null, qualifiedName: string, value: string): void { + namespace = asNullableString(namespace); + // 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName to validate and // extract. const { namespace: validatedNamespace, prefix, localName } = validateAndExtract(namespace, qualifiedName); @@ -192,6 +197,8 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param localName The local name of the attribute */ public removeAttributeNS (namespace: string | null, localName: string): void { + namespace = asNullableString(namespace); + removeAttributeByNamespaceAndLocalName(namespace, localName, this); } @@ -217,6 +224,8 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param localName The local name of the attribute */ public hasAttributeNS (namespace: string | null, localName: string): boolean { + namespace = asNullableString(namespace); + // 1. If namespace is the empty string, set it to null. // (handled by getAttributeByNamespaceAndLocalName, called below) // 2. Return true if the context object has an attribute whose namespace is namespace and local name is @@ -244,6 +253,8 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @return The attribute, or null if no such attribute exists */ public getAttributeNodeNS (namespace: string | null, localName: string): Attr | null { + namespace = asNullableString(namespace); + return getAttributeByNamespaceAndLocalName(namespace, localName, this); } diff --git a/src/Node.ts b/src/Node.ts index 9475335..cffe5d7 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -4,9 +4,11 @@ import Text from './Text'; import { ranges } from './Range'; import RegisteredObservers from './mutation-observer/RegisteredObservers'; import cloneNode from './util/cloneNode'; +import { expectArity } from './util/errorHelpers'; import { preInsertNode, appendNode, replaceChildWithNode, preRemoveChild, removeNode } from './util/mutationAlgorithms'; import { NodeType, isNodeOfType } from './util/NodeType'; import { getNodeDocument } from './util/treeHelpers'; +import { asNullableObject, asObject } from './util/typeHelpers'; /** * 3.4. Interface Node @@ -244,6 +246,10 @@ export default abstract class Node { * @return The node that was inserted */ public insertBefore (node: Node, child: Node | null): Node { + expectArity(arguments, 2); + node = asObject(node, Node); + child = asNullableObject(child, Node); + return preInsertNode(node, this, child); } @@ -257,6 +263,9 @@ export default abstract class Node { * @return The node that was inserted */ public appendChild (node: Node): Node { + expectArity(arguments, 1); + node = asObject(node, Node); + return appendNode(node, this); } @@ -269,6 +278,10 @@ export default abstract class Node { * @return The node that was removed */ public replaceChild (node: Node, child: Node): Node { + expectArity(arguments, 2); + node = asObject(node, Node); + child = asObject(child, Node); + return replaceChildWithNode(child, node, this); } @@ -280,6 +293,9 @@ export default abstract class Node { * @return The node that was removed */ public removeChild (child: Node): Node { + expectArity(arguments, 1); + child = asObject(child, Node); + return preRemoveChild(child, this); } diff --git a/src/Range.ts b/src/Range.ts index b75bedc..3ee8590 100644 --- a/src/Range.ts +++ b/src/Range.ts @@ -3,6 +3,7 @@ import Node from './Node'; import { throwIndexSizeError, throwInvalidNodeTypeError, throwNotSupportedError, throwWrongDocumentError } from './util/errorHelpers'; import { NodeType, isNodeOfType } from './util/NodeType'; import { determineLengthOfNode, getInclusiveAncestors, getNodeDocument, getNodeIndex, getRootOfNode } from './util/treeHelpers'; +import { asObject, asUnsignedLong } from './util/typeHelpers'; export const ranges: Range[] = []; @@ -64,6 +65,9 @@ export default class Range { * @param offset The new start offset */ setStart (node: Node, offset: number): void { + node = asObject(node, Node); + offset = asUnsignedLong(offset); + // 1. If node is a doctype, then throw an InvalidNodeTypeError. if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { throwInvalidNodeTypeError('Can not set a range under a doctype node'); @@ -104,6 +108,9 @@ export default class Range { * @param offset The new end offset */ setEnd (node: Node, offset: number): void { + node = asObject(node, Node); + offset = asUnsignedLong(offset); + // 1. If node is a doctype, then throw an InvalidNodeTypeError. if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { throwInvalidNodeTypeError('Can not set a range under a doctype node'); @@ -143,6 +150,8 @@ export default class Range { * @param node The node to set the range's start before */ setStartBefore (node: Node): void { + node = asObject(node, Node); + // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -161,6 +170,8 @@ export default class Range { * @param node The node to set the range's start before */ setStartAfter (node: Node): void { + node = asObject(node, Node); + // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -179,6 +190,8 @@ export default class Range { * @param node The node to set the range's end before */ setEndBefore (node: Node): void { + node = asObject(node, Node); + // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -198,6 +211,8 @@ export default class Range { * @param node The node to set the range's end before */ setEndAfter (node: Node): void { + node = asObject(node, Node); + // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -228,6 +243,8 @@ export default class Range { } selectNode (node: Node): void { + node = asObject(node, Node); + // 1. Let parent be node’s parent. let parent = node.parentNode; @@ -249,6 +266,8 @@ export default class Range { } selectNodeContents (node: Node): void { + node = asObject(node, Node); + // 1. If node is a doctype, throw an InvalidNodeTypeError. if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { throwInvalidNodeTypeError('Can not place range inside a doctype node'); @@ -272,6 +291,8 @@ export default class Range { static END_TO_START = 3; compareBoundaryPoints (how: number, sourceRange: Range): number { + sourceRange = asObject(sourceRange, Range); + // 1. If how is not one of START_TO_START, START_TO_END, END_TO_END, and END_TO_START, then throw a // NotSupportedError. if ( @@ -386,6 +407,9 @@ export default class Range { * @return Whether the point is in the range */ isPointInRange (node: Node, offset: number): boolean { + node = asObject(node, Node); + offset = asUnsignedLong(offset); + // 1. If node’s root is different from the context object’s root, return false. if (getRootOfNode(node) !== getRootOfRange(this)) { return false; @@ -422,6 +446,9 @@ export default class Range { * @return -1, 0 or 1 depending on whether the point is before, inside or after the range, respectively */ comparePoint (node: Node, offset: number): number { + node = asObject(node, Node); + offset = asUnsignedLong(offset); + // 1. If node’s root is different from the context object’s root, then throw a WrongDocumentError. if (getRootOfNode(node) !== getRootOfRange(this)) { throwWrongDocumentError('Can not compare point to range in different trees'); @@ -459,6 +486,8 @@ export default class Range { * @return Whether the range intersects node */ intersectsNode (node: Node): boolean { + node = asObject(node, Node); + // 1. If node’s root is different from the context object’s root, return false. if (getRootOfNode(node) !== getRootOfRange(this)) { return false; diff --git a/src/util/errorHelpers.ts b/src/util/errorHelpers.ts index 98a9e64..0235ed7 100644 --- a/src/util/errorHelpers.ts +++ b/src/util/errorHelpers.ts @@ -1,3 +1,23 @@ +export function expectArity (args: IArguments, minArity: number): void { + // According to WebIDL overload resolution semantics, only a lower bound applies to the number of arguments provided + if (args.length < minArity) { + throw new TypeError(`Function should be called with at least ${minArity} arguments`); + } +} + +export function expectObject (value: T, Constructor: any): void { + if (!(value instanceof Constructor)) { + throw new TypeError(`Value should be an instance of ${Constructor.name}`); + } +} + +function createDOMException (name: string, code: number, message: string): Error { + const err = new Error(`${name}: ${message}`); + err.name = name; + (err as any).code = code; + return err; +} + export function throwHierarchyRequestError (message: string): never { throw new Error(`HierarchyRequestError: ${message}`); } diff --git a/src/util/typeHelpers.ts b/src/util/typeHelpers.ts new file mode 100644 index 0000000..5ef0b91 --- /dev/null +++ b/src/util/typeHelpers.ts @@ -0,0 +1,38 @@ +import { expectObject } from './errorHelpers'; + +export function asUnsignedLong (number: number): number { + return number >>> 0; +} + +export function treatNullAsEmptyString (value: string | null): string { + // Treat null as empty string + if (value === null) { + return ''; + } + + // Coerce other values to string + return String(value); +} + +export function asObject (value: T, Constructor: any): T { + expectObject(value, Constructor); + + return value; +} + +export function asNullableObject (value: T | null | undefined, Constructor: any): T | null { + if (value === undefined || value === null) { + return null; + } + + return asObject(value, Constructor); +} + +export function asNullableString (value: string | null | undefined): string | null { + // Treat undefined as null + if (value === undefined) { + return null; + } + + return value; +} From 3ec0c224f2810e1aef41cdda7cedbe00d6c988c3 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Wed, 31 May 2017 10:54:12 +0200 Subject: [PATCH 03/34] Export all public types. --- src/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/index.ts b/src/index.ts index 9843023..c40d2f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,19 @@ import { implementation } from './DOMImplementation'; import XMLDocument from './XMLDocument'; +export { default as Attr } from './Attr'; +export { default as CharacterData } from './CharacterData'; +export { default as Comment } from './Comment'; +export { default as Document } from './Document'; +export { default as DocumentType } from './DocumentType'; +export { default as DOMImplementation } from './DOMImplementation'; +export { default as Element } from './Element'; export { implementation } from './DOMImplementation'; export { default as Node } from './Node'; +export { default as ProcessingInstruction } from './ProcessingInstruction'; export { default as Range } from './Range'; +export { default as Text } from './Text'; +export { default as XMLDocument } from './XMLDocument'; export { default as MutationObserver } from './mutation-observer/MutationObserver'; export function createDocument (): XMLDocument { From 5623c2e30b060714564bd0b7024dcaa99b8fa149 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Wed, 31 May 2017 10:54:33 +0200 Subject: [PATCH 04/34] Create a separate DOMImplementation per Document. --- src/DOMImplementation.ts | 18 ++++++++++++------ src/Document.ts | 6 ++---- src/DocumentType.ts | 2 +- src/index.ts | 4 +--- test/DOMImplementation.tests.ts | 11 +++++------ test/DocumentType.tests.ts | 12 +++++------- 6 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/DOMImplementation.ts b/src/DOMImplementation.ts index 33e34d0..d3e92c6 100644 --- a/src/DOMImplementation.ts +++ b/src/DOMImplementation.ts @@ -7,11 +7,19 @@ import { validateQualifiedName } from './util/namespaceHelpers'; import { asNullableObject, asNullableString, treatNullAsEmptyString } from './util/typeHelpers'; export default class DOMImplementation { + private _document: Document; + /** - * Returns a doctype, with the given qualifiedName, publicId, and systemId. + * (non-standard) Use Document#implementation to access instances of this class * - * (Non-standard) As this implementation does not associate a document with the global object, the returned - * doctype does not have an associated node document until it is inserted in one. + * @param document The document to associate with this instance + */ + constructor (document: Document) { + this._document = document; + } + + /** + * Returns a doctype, with the given qualifiedName, publicId, and systemId. * * @param qualifiedName Qualified name for the doctype * @param publicId Public ID for the doctype @@ -25,7 +33,7 @@ export default class DOMImplementation { // 2. Return a new doctype, with qualifiedName as its name, publicId as its public ID, and systemId as its // system ID, and with its node document set to the associated document of the context object. - return new DocumentType(null, qualifiedName, publicId, systemId); + return new DocumentType(this._document, qualifiedName, publicId, systemId); } /** @@ -80,5 +88,3 @@ export default class DOMImplementation { return document; } } - -export const implementation = new DOMImplementation(); diff --git a/src/Document.ts b/src/Document.ts index 0744754..e1ce58b 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -3,7 +3,7 @@ import Attr from './Attr'; import CDATASection from './CDATASection'; import Comment from './Comment'; import DocumentType from './DocumentType'; -import { implementation, default as DOMImplementation } from './DOMImplementation'; +import DOMImplementation from './DOMImplementation'; import { createElement, default as Element } from './Element'; import Node from './Node'; import ProcessingInstruction from './ProcessingInstruction'; @@ -55,9 +55,7 @@ export default class Document extends Node implements NonElementParentNode, Pare /** * Returns a reference to the DOMImplementation object associated with the document. */ - public get implementation (): DOMImplementation { - return implementation; - } + public readonly implementation: DOMImplementation = new DOMImplementation(this); /** * The doctype, or null if there is none. diff --git a/src/DocumentType.ts b/src/DocumentType.ts index c729edb..c5b31e6 100644 --- a/src/DocumentType.ts +++ b/src/DocumentType.ts @@ -46,7 +46,7 @@ export default class DocumentType extends Node implements ChildNode { * @param publicId The public ID of the doctype * @param systemId The system ID of the doctype */ - constructor (document: Document | null, name: string, publicId: string = '', systemId: string = '') { + constructor (document: Document, name: string, publicId: string = '', systemId: string = '') { super(document); this.name = name; diff --git a/src/index.ts b/src/index.ts index c40d2f7..c49fb4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,3 @@ -import { implementation } from './DOMImplementation'; import XMLDocument from './XMLDocument'; export { default as Attr } from './Attr'; @@ -8,7 +7,6 @@ export { default as Document } from './Document'; export { default as DocumentType } from './DocumentType'; export { default as DOMImplementation } from './DOMImplementation'; export { default as Element } from './Element'; -export { implementation } from './DOMImplementation'; export { default as Node } from './Node'; export { default as ProcessingInstruction } from './ProcessingInstruction'; export { default as Range } from './Range'; @@ -17,5 +15,5 @@ export { default as XMLDocument } from './XMLDocument'; export { default as MutationObserver } from './mutation-observer/MutationObserver'; export function createDocument (): XMLDocument { - return implementation.createDocument(null, ''); + return new XMLDocument(); } diff --git a/test/DOMImplementation.tests.ts b/test/DOMImplementation.tests.ts index 2384e6e..376d287 100644 --- a/test/DOMImplementation.tests.ts +++ b/test/DOMImplementation.tests.ts @@ -1,12 +1,11 @@ -import DOMImplementation from '../src/DOMImplementation'; -import Element from '../src/Element'; - import * as chai from 'chai'; +import * as slimdom from '../src/index'; describe('DOMImplementation', () => { - let domImplementation: DOMImplementation; + let domImplementation: slimdom.DOMImplementation; beforeEach(() => { - domImplementation = new DOMImplementation(); + const document = new slimdom.Document(); + domImplementation = document.implementation; }); describe('.createDocumentType()', () => { @@ -39,7 +38,7 @@ describe('DOMImplementation', () => { const document = domImplementation.createDocument(null, 'someRootElementName'); chai.assert.equal(document.nodeType, 9); chai.assert.equal(document.firstChild, document.documentElement); - chai.assert.equal((document.documentElement as Element).nodeName, 'someRootElementName'); + chai.assert.equal((document.documentElement as slimdom.Element).nodeName, 'someRootElementName'); }); }); }); diff --git a/test/DocumentType.tests.ts b/test/DocumentType.tests.ts index f763ef1..dc69d5f 100644 --- a/test/DocumentType.tests.ts +++ b/test/DocumentType.tests.ts @@ -1,13 +1,11 @@ -import * as slimdom from '../src/index'; - -import DocumentType from '../src/DocumentType'; - import * as chai from 'chai'; +import * as slimdom from '../src/index'; describe('DocumentType', () => { - let doctype: DocumentType; + let doctype: slimdom.DocumentType; beforeEach(() => { - doctype = slimdom.implementation.createDocumentType('somename', 'somePublicId', 'someSystemId'); + const document = new slimdom.Document(); + doctype = document.implementation.createDocumentType('somename', 'somePublicId', 'someSystemId'); }); it('has nodeType 10', () => chai.assert.equal(doctype.nodeType, 10)); @@ -19,7 +17,7 @@ describe('DocumentType', () => { it('has a systemId', () => chai.assert.equal(doctype.systemId, 'someSystemId')); it('can be cloned', () => { - const clone = doctype.cloneNode(true) as DocumentType; + const clone = doctype.cloneNode(true) as slimdom.DocumentType; chai.assert.equal(clone.nodeType, 10); chai.assert.equal(clone.name, 'somename'); chai.assert.equal(clone.publicId, 'somePublicId'); From 852afc94f45da08c58882ab4f0f3fe5cf0bfaa20 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Wed, 31 May 2017 11:02:45 +0200 Subject: [PATCH 05/34] Use correct error codes for errors thrown. --- src/util/errorHelpers.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/util/errorHelpers.ts b/src/util/errorHelpers.ts index 0235ed7..4efab83 100644 --- a/src/util/errorHelpers.ts +++ b/src/util/errorHelpers.ts @@ -19,33 +19,33 @@ function createDOMException (name: string, code: number, message: string): Error } export function throwHierarchyRequestError (message: string): never { - throw new Error(`HierarchyRequestError: ${message}`); + throw createDOMException('HierarchyRequestError', 3, message); } export function throwIndexSizeError (message: string): never { - throw new Error(`IndexSizeError: ${message}`); + throw createDOMException('IndexSizeError', 1, message); } export function throwInUseAttributeError (message: string): never { - throw new Error(`InUseAttributeError: ${message}`); + throw createDOMException('InUseAttributeError', 10, message); } export function throwInvalidNodeTypeError (message: string): never { - throw new Error(`InvalidNodeTypeError: ${message}`); + throw createDOMException('InvalidNodeTypeError', 24, message); } export function throwNamespaceError (message: string): never { - throw new Error(`NamespaceError: ${message}`); + throw createDOMException('NamespaceError', 14, message); } export function throwNotFoundError (message: string): never { - throw new Error(`NotFoundError: ${message}`); + throw createDOMException('NotFoundError', 8, message); } export function throwNotSupportedError (message: string): never { - throw new Error(`NotSupportedError: ${message}`); + throw createDOMException('NotSupportedError', 9, message); } export function throwWrongDocumentError (message: string): never { - throw new Error(`WrongDocumentError: ${message}`); + throw createDOMException('WrongDocumentError', 4, message); } From 8e9be096365f26a79337d379758a57047a97afa3 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Wed, 31 May 2017 11:07:31 +0200 Subject: [PATCH 06/34] Implement name validation. Note: although the DOM standard links to the fifth edition of the XML spec, both browsers and the web platform seem to use the fourth edition rules. --- src/Element.ts | 7 ++- src/util/errorHelpers.ts | 4 ++ src/util/namespaceHelpers.ts | 110 ++++++++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/Element.ts b/src/Element.ts index 60afa6d..6fc4701 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -5,8 +5,8 @@ import Document from './Document'; import Node from './Node'; import { appendAttribute, changeAttribute, removeAttribute, replaceAttribute } from './util/attrMutations'; -import { throwInUseAttributeError, throwNotFoundError } from './util/errorHelpers'; -import { validateAndExtract } from './util/namespaceHelpers'; +import { throwInUseAttributeError, throwInvalidCharacterError, throwNotFoundError } from './util/errorHelpers'; +import { matchesNameProduction, validateAndExtract } from './util/namespaceHelpers'; import { NodeType } from './util/NodeType'; import { asNullableString } from './util/typeHelpers'; @@ -142,6 +142,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType */ public setAttribute (qualifiedName: string, value: string): void { // 1. If qualifiedName does not match the Name production in XML, then throw an InvalidCharacterError. + if (!matchesNameProduction(qualifiedName)) { + throwInvalidCharacterError('The qualified name does not match the Name production'); + } // 2. If the context object is in the HTML namespace and its node document is an HTML document, then set // qualifiedName to qualifiedName in ASCII lowercase. diff --git a/src/util/errorHelpers.ts b/src/util/errorHelpers.ts index 4efab83..1fb1174 100644 --- a/src/util/errorHelpers.ts +++ b/src/util/errorHelpers.ts @@ -30,6 +30,10 @@ export function throwInUseAttributeError (message: string): never { throw createDOMException('InUseAttributeError', 10, message); } +export function throwInvalidCharacterError (message: string): never { + throw createDOMException('InvalidCharacterError', 5, message); +} + export function throwInvalidNodeTypeError (message: string): never { throw createDOMException('InvalidNodeTypeError', 24, message); } diff --git a/src/util/namespaceHelpers.ts b/src/util/namespaceHelpers.ts index 10a98c0..be3aa13 100644 --- a/src/util/namespaceHelpers.ts +++ b/src/util/namespaceHelpers.ts @@ -1,17 +1,123 @@ -import { throwNamespaceError } from './errorHelpers'; +import { throwInvalidCharacterError, throwNamespaceError } from './errorHelpers'; // 1.5. Namespaces const XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace'; const XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/'; +/* +// NAME_REGEX_XML_1_0_FOURTH_EDITION generated using regenerate: +var regenerate = require("regenerate"); + +const productions = { + NameChar: "Letter | Digit | '.' | '-' | '_' | ':' | CombiningChar | Extender", + Letter: "BaseChar | Ideographic", + BaseChar: "[#x0041-#x005A] | [#x0061-#x007A] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | [#x00F8-#x00FF] | [#x0100-#x0131] | [#x0134-#x013E] | [#x0141-#x0148] | [#x014A-#x017E] | [#x0180-#x01C3] | [#x01CD-#x01F0] | [#x01F4-#x01F5] | [#x01FA-#x0217] | [#x0250-#x02A8] | [#x02BB-#x02C1] | #x0386 | [#x0388-#x038A] | #x038C | [#x038E-#x03A1] | [#x03A3-#x03CE] | [#x03D0-#x03D6] | #x03DA | #x03DC | #x03DE | #x03E0 | [#x03E2-#x03F3] | [#x0401-#x040C] | [#x040E-#x044F] | [#x0451-#x045C] | [#x045E-#x0481] | [#x0490-#x04C4] | [#x04C7-#x04C8] | [#x04CB-#x04CC] | [#x04D0-#x04EB] | [#x04EE-#x04F5] | [#x04F8-#x04F9] | [#x0531-#x0556] | #x0559 | [#x0561-#x0586] | [#x05D0-#x05EA] | [#x05F0-#x05F2] | [#x0621-#x063A] | [#x0641-#x064A] | [#x0671-#x06B7] | [#x06BA-#x06BE] | [#x06C0-#x06CE] | [#x06D0-#x06D3] | #x06D5 | [#x06E5-#x06E6] | [#x0905-#x0939] | #x093D | [#x0958-#x0961] | [#x0985-#x098C] | [#x098F-#x0990] | [#x0993-#x09A8] | [#x09AA-#x09B0] | #x09B2 | [#x09B6-#x09B9] | [#x09DC-#x09DD] | [#x09DF-#x09E1] | [#x09F0-#x09F1] | [#x0A05-#x0A0A] | [#x0A0F-#x0A10] | [#x0A13-#x0A28] | [#x0A2A-#x0A30] | [#x0A32-#x0A33] | [#x0A35-#x0A36] | [#x0A38-#x0A39] | [#x0A59-#x0A5C] | #x0A5E | [#x0A72-#x0A74] | [#x0A85-#x0A8B] | #x0A8D | [#x0A8F-#x0A91] | [#x0A93-#x0AA8] | [#x0AAA-#x0AB0] | [#x0AB2-#x0AB3] | [#x0AB5-#x0AB9] | #x0ABD | #x0AE0 | [#x0B05-#x0B0C] | [#x0B0F-#x0B10] | [#x0B13-#x0B28] | [#x0B2A-#x0B30] | [#x0B32-#x0B33] | [#x0B36-#x0B39] | #x0B3D | [#x0B5C-#x0B5D] | [#x0B5F-#x0B61] | [#x0B85-#x0B8A] | [#x0B8E-#x0B90] | [#x0B92-#x0B95] | [#x0B99-#x0B9A] | #x0B9C | [#x0B9E-#x0B9F] | [#x0BA3-#x0BA4] | [#x0BA8-#x0BAA] | [#x0BAE-#x0BB5] | [#x0BB7-#x0BB9] | [#x0C05-#x0C0C] | [#x0C0E-#x0C10] | [#x0C12-#x0C28] | [#x0C2A-#x0C33] | [#x0C35-#x0C39] | [#x0C60-#x0C61] | [#x0C85-#x0C8C] | [#x0C8E-#x0C90] | [#x0C92-#x0CA8] | [#x0CAA-#x0CB3] | [#x0CB5-#x0CB9] | #x0CDE | [#x0CE0-#x0CE1] | [#x0D05-#x0D0C] | [#x0D0E-#x0D10] | [#x0D12-#x0D28] | [#x0D2A-#x0D39] | [#x0D60-#x0D61] | [#x0E01-#x0E2E] | #x0E30 | [#x0E32-#x0E33] | [#x0E40-#x0E45] | [#x0E81-#x0E82] | #x0E84 | [#x0E87-#x0E88] | #x0E8A | #x0E8D | [#x0E94-#x0E97] | [#x0E99-#x0E9F] | [#x0EA1-#x0EA3] | #x0EA5 | #x0EA7 | [#x0EAA-#x0EAB] | [#x0EAD-#x0EAE] | #x0EB0 | [#x0EB2-#x0EB3] | #x0EBD | [#x0EC0-#x0EC4] | [#x0F40-#x0F47] | [#x0F49-#x0F69] | [#x10A0-#x10C5] | [#x10D0-#x10F6] | #x1100 | [#x1102-#x1103] | [#x1105-#x1107] | #x1109 | [#x110B-#x110C] | [#x110E-#x1112] | #x113C | #x113E | #x1140 | #x114C | #x114E | #x1150 | [#x1154-#x1155] | #x1159 | [#x115F-#x1161] | #x1163 | #x1165 | #x1167 | #x1169 | [#x116D-#x116E] | [#x1172-#x1173] | #x1175 | #x119E | #x11A8 | #x11AB | [#x11AE-#x11AF] | [#x11B7-#x11B8] | #x11BA | [#x11BC-#x11C2] | #x11EB | #x11F0 | #x11F9 | [#x1E00-#x1E9B] | [#x1EA0-#x1EF9] | [#x1F00-#x1F15] | [#x1F18-#x1F1D] | [#x1F20-#x1F45] | [#x1F48-#x1F4D] | [#x1F50-#x1F57] | #x1F59 | #x1F5B | #x1F5D | [#x1F5F-#x1F7D] | [#x1F80-#x1FB4] | [#x1FB6-#x1FBC] | #x1FBE | [#x1FC2-#x1FC4] | [#x1FC6-#x1FCC] | [#x1FD0-#x1FD3] | [#x1FD6-#x1FDB] | [#x1FE0-#x1FEC] | [#x1FF2-#x1FF4] | [#x1FF6-#x1FFC] | #x2126 | [#x212A-#x212B] | #x212E | [#x2180-#x2182] | [#x3041-#x3094] | [#x30A1-#x30FA] | [#x3105-#x312C] | [#xAC00-#xD7A3]", + Ideographic: "[#x4E00-#x9FA5] | #x3007 | [#x3021-#x3029]", + CombiningChar: "[#x0300-#x0345] | [#x0360-#x0361] | [#x0483-#x0486] | [#x0591-#x05A1] | [#x05A3-#x05B9] | [#x05BB-#x05BD] | #x05BF | [#x05C1-#x05C2] | #x05C4 | [#x064B-#x0652] | #x0670 | [#x06D6-#x06DC] | [#x06DD-#x06DF] | [#x06E0-#x06E4] | [#x06E7-#x06E8] | [#x06EA-#x06ED] | [#x0901-#x0903] | #x093C | [#x093E-#x094C] | #x094D | [#x0951-#x0954] | [#x0962-#x0963] | [#x0981-#x0983] | #x09BC | #x09BE | #x09BF | [#x09C0-#x09C4] | [#x09C7-#x09C8] | [#x09CB-#x09CD] | #x09D7 | [#x09E2-#x09E3] | #x0A02 | #x0A3C | #x0A3E | #x0A3F | [#x0A40-#x0A42] | [#x0A47-#x0A48] | [#x0A4B-#x0A4D] | [#x0A70-#x0A71] | [#x0A81-#x0A83] | #x0ABC | [#x0ABE-#x0AC5] | [#x0AC7-#x0AC9] | [#x0ACB-#x0ACD] | [#x0B01-#x0B03] | #x0B3C | [#x0B3E-#x0B43] | [#x0B47-#x0B48] | [#x0B4B-#x0B4D] | [#x0B56-#x0B57] | [#x0B82-#x0B83] | [#x0BBE-#x0BC2] | [#x0BC6-#x0BC8] | [#x0BCA-#x0BCD] | #x0BD7 | [#x0C01-#x0C03] | [#x0C3E-#x0C44] | [#x0C46-#x0C48] | [#x0C4A-#x0C4D] | [#x0C55-#x0C56] | [#x0C82-#x0C83] | [#x0CBE-#x0CC4] | [#x0CC6-#x0CC8] | [#x0CCA-#x0CCD] | [#x0CD5-#x0CD6] | [#x0D02-#x0D03] | [#x0D3E-#x0D43] | [#x0D46-#x0D48] | [#x0D4A-#x0D4D] | #x0D57 | #x0E31 | [#x0E34-#x0E3A] | [#x0E47-#x0E4E] | #x0EB1 | [#x0EB4-#x0EB9] | [#x0EBB-#x0EBC] | [#x0EC8-#x0ECD] | [#x0F18-#x0F19] | #x0F35 | #x0F37 | #x0F39 | #x0F3E | #x0F3F | [#x0F71-#x0F84] | [#x0F86-#x0F8B] | [#x0F90-#x0F95] | #x0F97 | [#x0F99-#x0FAD] | [#x0FB1-#x0FB7] | #x0FB9 | [#x20D0-#x20DC] | #x20E1 | [#x302A-#x302F] | #x3099 | #x309A", + Digit: "[#x0030-#x0039] | [#x0660-#x0669] | [#x06F0-#x06F9] | [#x0966-#x096F] | [#x09E6-#x09EF] | [#x0A66-#x0A6F] | [#x0AE6-#x0AEF] | [#x0B66-#x0B6F] | [#x0BE7-#x0BEF] | [#x0C66-#x0C6F] | [#x0CE6-#x0CEF] | [#x0D66-#x0D6F] | [#x0E50-#x0E59] | [#x0ED0-#x0ED9] | [#x0F20-#x0F29]", + Extender: "#x00B7 | #x02D0 | #x02D1 | #x0387 | #x0640 | #x0E46 | #x0EC6 | #x3005 | [#x3031-#x3035] | [#x309D-#x309E] | [#x30FC-#x30FE]" +}; + +function createSetRegex (prod, set = regenerate()) { + return prod.split(' | ').reduce((set, part) => { + let m = part.match(/^\[#x([0-9A-F]+)-#x([0-9A-F]+)\]$/); + if (m) { + return set.addRange(parseInt(m[1], 16), parseInt(m[2], 16)); + } + m = part.match(/^#x([0-9A-F]+)$/); + if (m) { + return set.add(parseInt(m[1], 16)); + } + m = part.match(/^'(.)'$/); + if (m) { + return set.add(m[1]); + } + return createSetRegex(productions[part], set); + }, set); +} + +// Name ::= (Letter | '_' | ':') (NameChar)* +`^(?:${createRegex("Letter | '_' | ':'")})(?:${createRegex('NameChar')})*$`; +*/ +const NAME_REGEX_XML_1_0_FOURTH_EDITION = /^(?:[:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u0131\u0134-\u013E\u0141-\u0148\u014A-\u017E\u0180-\u01C3\u01CD-\u01F0\u01F4\u01F5\u01FA-\u0217\u0250-\u02A8\u02BB-\u02C1\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03D6\u03DA\u03DC\u03DE\u03E0\u03E2-\u03F3\u0401-\u040C\u040E-\u044F\u0451-\u045C\u045E-\u0481\u0490-\u04C4\u04C7\u04C8\u04CB\u04CC\u04D0-\u04EB\u04EE-\u04F5\u04F8\u04F9\u0531-\u0556\u0559\u0561-\u0586\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0641-\u064A\u0671-\u06B7\u06BA-\u06BE\u06C0-\u06CE\u06D0-\u06D3\u06D5\u06E5\u06E6\u0905-\u0939\u093D\u0958-\u0961\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8B\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AE0\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B36-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB5\u0BB7-\u0BB9\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CDE\u0CE0\u0CE1\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D60\u0D61\u0E01-\u0E2E\u0E30\u0E32\u0E33\u0E40-\u0E45\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD\u0EAE\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0F40-\u0F47\u0F49-\u0F69\u10A0-\u10C5\u10D0-\u10F6\u1100\u1102\u1103\u1105-\u1107\u1109\u110B\u110C\u110E-\u1112\u113C\u113E\u1140\u114C\u114E\u1150\u1154\u1155\u1159\u115F-\u1161\u1163\u1165\u1167\u1169\u116D\u116E\u1172\u1173\u1175\u119E\u11A8\u11AB\u11AE\u11AF\u11B7\u11B8\u11BA\u11BC-\u11C2\u11EB\u11F0\u11F9\u1E00-\u1E9B\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2126\u212A\u212B\u212E\u2180-\u2182\u3007\u3021-\u3029\u3041-\u3094\u30A1-\u30FA\u3105-\u312C\u4E00-\u9FA5\uAC00-\uD7A3])(?:[\-\.0-:A-Z_a-z\xB7\xC0-\xD6\xD8-\xF6\xF8-\u0131\u0134-\u013E\u0141-\u0148\u014A-\u017E\u0180-\u01C3\u01CD-\u01F0\u01F4\u01F5\u01FA-\u0217\u0250-\u02A8\u02BB-\u02C1\u02D0\u02D1\u0300-\u0345\u0360\u0361\u0386-\u038A\u038C\u038E-\u03A1\u03A3-\u03CE\u03D0-\u03D6\u03DA\u03DC\u03DE\u03E0\u03E2-\u03F3\u0401-\u040C\u040E-\u044F\u0451-\u045C\u045E-\u0481\u0483-\u0486\u0490-\u04C4\u04C7\u04C8\u04CB\u04CC\u04D0-\u04EB\u04EE-\u04F5\u04F8\u04F9\u0531-\u0556\u0559\u0561-\u0586\u0591-\u05A1\u05A3-\u05B9\u05BB-\u05BD\u05BF\u05C1\u05C2\u05C4\u05D0-\u05EA\u05F0-\u05F2\u0621-\u063A\u0640-\u0652\u0660-\u0669\u0670-\u06B7\u06BA-\u06BE\u06C0-\u06CE\u06D0-\u06D3\u06D5-\u06E8\u06EA-\u06ED\u06F0-\u06F9\u0901-\u0903\u0905-\u0939\u093C-\u094D\u0951-\u0954\u0958-\u0963\u0966-\u096F\u0981-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC\u09BE-\u09C4\u09C7\u09C8\u09CB-\u09CD\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A02\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A59-\u0A5C\u0A5E\u0A66-\u0A74\u0A81-\u0A83\u0A85-\u0A8B\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AE0\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B36-\u0B39\u0B3C-\u0B43\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB5\u0BB7-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD7\u0BE7-\u0BEF\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3E-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C60\u0C61\u0C66-\u0C6F\u0C82\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBE-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0D02\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D28\u0D2A-\u0D39\u0D3E-\u0D43\u0D46-\u0D48\u0D4A-\u0D4D\u0D57\u0D60\u0D61\u0D66-\u0D6F\u0E01-\u0E2E\u0E30-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD\u0EAE\u0EB0-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F69\u0F71-\u0F84\u0F86-\u0F8B\u0F90-\u0F95\u0F97\u0F99-\u0FAD\u0FB1-\u0FB7\u0FB9\u10A0-\u10C5\u10D0-\u10F6\u1100\u1102\u1103\u1105-\u1107\u1109\u110B\u110C\u110E-\u1112\u113C\u113E\u1140\u114C\u114E\u1150\u1154\u1155\u1159\u115F-\u1161\u1163\u1165\u1167\u1169\u116D\u116E\u1172\u1173\u1175\u119E\u11A8\u11AB\u11AE\u11AF\u11B7\u11B8\u11BA\u11BC-\u11C2\u11EB\u11F0\u11F9\u1E00-\u1E9B\u1EA0-\u1EF9\u1F00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u20D0-\u20DC\u20E1\u2126\u212A\u212B\u212E\u2180-\u2182\u3005\u3007\u3021-\u302F\u3031-\u3035\u3041-\u3094\u3099\u309A\u309D\u309E\u30A1-\u30FA\u30FC-\u30FE\u3105-\u312C\u4E00-\u9FA5\uAC00-\uD7A3])*$/; + +/* +// NAME_REGEX_XML_1_0_FIFTH_EDITION generated using regenerate: +const regenerate = require('regenerate'); + +const NameStartChar = regenerate() + .add(':') + .addRange('A', 'Z') + .add('_') + .addRange('a', 'z') + .addRange(0xC0, 0xD6) + .addRange(0xD8, 0xF6) + .addRange(0xF8, 0x2FF) + .addRange(0x370, 0x37D) + .addRange(0x37F, 0x1FFF) + .addRange(0x200C, 0x200D) + .addRange(0x2070, 0x218F) + .addRange(0x2C00, 0x2FEF) + .addRange(0x3001, 0xD7FF) + .addRange(0xF900, 0xFDCF) + .addRange(0xFDF0, 0xFFFD) + .addRange(0x10000, 0xEFFFF); + +const NameChar = NameStartChar.clone() + .add('-') + .add('.') + .addRange('0', '9') + .add(0xB7) + .addRange(0x0300, 0x036F) + .addRange(0x203F, 0x2040); + +return `^(?:${NameStartChar.toString()})(?:${NameChar.toString()})*$`; +*/ +const NAME_REGEX_XML_1_0_FIFTH_EDITION = /^(?:[:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])(?:[\-\.0-:A-Z_a-z\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])*$/ + +/** + * Returns true if name matches the Name production. + * + * @param name The name to check + * + * @return true if name matches Name, otherwise false + */ +export function matchesNameProduction (name: string): boolean { + return NAME_REGEX_XML_1_0_FOURTH_EDITION.test(name); +} + +/** + * As we're already testing against Name, testing QName validity can be reduced to checking if the name contains at + * most a single colon which is not at the first or last position. + * + * @param name The name to check + * + * @return True if the name is a valid QName, provided it is also a valid Name, otherwise false + */ +function isValidQName (name: string): boolean { + const parts = name.split(':'); + if (parts.length > 2) { + return false; + } + if (parts.length === 1) { + return true; + } + // First part should not be empty, and the second part should be a valid name + return parts[0].length > 0 && matchesNameProduction(parts[1]); +} + /** * To validate a qualifiedName, * * @param qualifiedName Qualified name to validate */ export function validateQualifiedName (qualifiedName: string): void { - // TODO: throw an InvalidCharacterError if qualifiedName does not match the Name or QName production. + // throw an InvalidCharacterError if qualifiedName does not match the Name or QName production. + // (QName is basically (Name without ':') ':' (Name without ':'), so just check the position of the : + if (!isValidQName(qualifiedName) || !matchesNameProduction(qualifiedName)) { + throwInvalidCharacterError('The qualified name is not a valid Name or QName'); + } } /** From 260c063d70890b26eb11f214d8a2551d9481ee94 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 29 May 2017 15:33:53 +0200 Subject: [PATCH 07/34] Implement DOMImplementation#createHTMLDocument. --- src/DOMImplementation.ts | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/DOMImplementation.ts b/src/DOMImplementation.ts index d3e92c6..8295e53 100644 --- a/src/DOMImplementation.ts +++ b/src/DOMImplementation.ts @@ -1,4 +1,6 @@ +import Document from './Document'; import DocumentType from './DocumentType'; +import { createElement } from './Element'; import XMLDocument from './XMLDocument'; import createElementNS from './util/createElementNS'; @@ -6,6 +8,8 @@ import { expectArity } from './util/errorHelpers'; import { validateQualifiedName } from './util/namespaceHelpers'; import { asNullableObject, asNullableString, treatNullAsEmptyString } from './util/typeHelpers'; +const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; + export default class DOMImplementation { private _document: Document; @@ -87,4 +91,55 @@ export default class DOMImplementation { // 8. Return document. return document; } + + /** + * Returns a HTML document with a basic tree already constructed. + * + * @param title Optional title for the new HTML document + * + * @return The new document + */ + createHTMLDocument (title?: string | null): Document { + title = asNullableString(title); + + // 1. Let doc be a new document that is an HTML document. + const doc = new Document(); + + // 2. Set doc’s content type to "text/html". + // (content type not implemented) + + // 3. Append a new doctype, with "html" as its name and with its node document set to doc, to doc. + doc.appendChild(new DocumentType(doc, 'html')); + + // 4. Append the result of creating an element given doc, html, and the HTML namespace, to doc. + const htmlElement = createElement(doc, 'html', HTML_NAMESPACE); + doc.appendChild(htmlElement); + + // 5. Append the result of creating an element given doc, head, and the HTML namespace, to the html element + // created earlier. + const headElement = createElement(doc, 'head', HTML_NAMESPACE); + htmlElement.appendChild(headElement); + + // 6. If title is given: + if (title !== null) { + // 6.1. Append the result of creating an element given doc, title, and the HTML namespace, to the head + // element created earlier. + const titleElement = createElement(doc, 'title', HTML_NAMESPACE); + headElement.appendChild(titleElement); + + // 6.2. Append a new Text node, with its data set to title (which could be the empty string) and its node + // document set to doc, to the title element created earlier. + titleElement.appendChild(doc.createTextNode(title)); + } + + // 7. Append the result of creating an element given doc, body, and the HTML namespace, to the html element + // created earlier. + htmlElement.appendChild(createElement(doc, 'body', HTML_NAMESPACE)); + + // 8. doc’s origin is context object’s associated document’s origin. + // (origin not implemented) + + // 9. Return doc. + return doc; + } } From bc83c6c4f91028a2cf360487f1f978b26912e925 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Tue, 30 May 2017 16:12:36 +0200 Subject: [PATCH 08/34] Fix ownerElement for cloned Attr. --- src/Attr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Attr.ts b/src/Attr.ts index 9811af2..7c26f20 100644 --- a/src/Attr.ts +++ b/src/Attr.ts @@ -82,7 +82,7 @@ export default class Attr extends Node { */ public _copy (document: Document): Attr { // Set copy’s namespace, namespace prefix, local name, and value, to those of node. - return new Attr(document, this.namespaceURI, this.prefix, this.localName, this.value, this.ownerElement); + return new Attr(document, this.namespaceURI, this.prefix, this.localName, this.value, null); } } From 1b184294551b7acd75a88ca3bd3f45d2c2c57f56 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Tue, 30 May 2017 16:13:28 +0200 Subject: [PATCH 09/34] Fix hierarchy check when replacing a doctype. --- src/util/mutationAlgorithms.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/mutationAlgorithms.ts b/src/util/mutationAlgorithms.ts index 14942a1..1642436 100644 --- a/src/util/mutationAlgorithms.ts +++ b/src/util/mutationAlgorithms.ts @@ -284,7 +284,7 @@ export function replaceChildWithNode (child: Node, node: Node, parent: Node): No if (isNodeOfType(node, NodeType.TEXT_NODE) && isNodeOfType(parent, NodeType.DOCUMENT_NODE)) { throwHierarchyRequestError('can not insert a Text node under a Document'); } - if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE) && !isNodeOfType(node, NodeType.DOCUMENT_NODE)) { + if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE) && !isNodeOfType(parent, NodeType.DOCUMENT_NODE)) { throwHierarchyRequestError('can only insert a DocumentType node under a Document'); } From 4fcceceb33a92e84c381d979807c6bfc5504d9f3 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Tue, 30 May 2017 17:23:37 +0200 Subject: [PATCH 10/34] Implement DocumentFragment. --- src/Document.ts | 10 ++++++ src/DocumentFragment.ts | 59 ++++++++++++++++++++++++++++++++++ src/index.ts | 1 + src/mixins.ts | 6 ++-- src/util/mutationAlgorithms.ts | 40 +++++++++++++++++++++-- 5 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 src/DocumentFragment.ts diff --git a/src/Document.ts b/src/Document.ts index e1ce58b..b266e17 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -2,6 +2,7 @@ import { NonElementParentNode, ParentNode, getChildren } from './mixins'; import Attr from './Attr'; import CDATASection from './CDATASection'; import Comment from './Comment'; +import DocumentFragment from './DocumentFragment'; import DocumentType from './DocumentType'; import DOMImplementation from './DOMImplementation'; import { createElement, default as Element } from './Element'; @@ -131,6 +132,15 @@ export default class Document extends Node implements NonElementParentNode, Pare return createElementNS(this, namespace, qualifiedName); } + /** + * Creates a new DocumentFragment node. + * + * @return The new document fragment + */ + public createDocumentFragment (): DocumentFragment { + return new DocumentFragment(this); + } + /** * Creates a new text node with the given data. * diff --git a/src/DocumentFragment.ts b/src/DocumentFragment.ts new file mode 100644 index 0000000..dbe6f06 --- /dev/null +++ b/src/DocumentFragment.ts @@ -0,0 +1,59 @@ +import { NonElementParentNode, ParentNode, getChildren } from './mixins'; +import Document from './Document'; +import Element from './Element'; +import Node from './Node'; +import { NodeType } from './util/NodeType'; + +export default class DocumentFragment extends Node implements NonElementParentNode, ParentNode { + // Node + + public get nodeType (): number { + return NodeType.DOCUMENT_FRAGMENT_NODE; + } + + public get nodeName (): string { + return '#document-fragment'; + } + + public get nodeValue (): string | null { + return null; + } + + public set nodeValue (newValue: string | null) { + // Do nothing. + } + + + // ParentNode + + public get children (): Element[] { + return getChildren(this); + } + + public firstElementChild: Element | null = null; + public lastElementChild: Element | null = null; + public childElementCount: number = 0; + + /** + * Creates a new DocumentFragment. + * + * Non-standard: as this implementation does not have a document associated with the global object, it is required + * to pass a document to this constructor. + * + * @param document (non-standard) The node document to associate with the new document fragment + */ + constructor (document: Document) { + super(document); + } + + /** + * (non-standard) Creates a copy of the context object, not including its children. + * + * @param document The node document to associate with the copy + * + * @return A shallow copy of the context object + */ + public _copy (document: Document): DocumentFragment { + return new DocumentFragment(document); + } +} diff --git a/src/index.ts b/src/index.ts index c49fb4a..3950cef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export { default as Attr } from './Attr'; export { default as CharacterData } from './CharacterData'; export { default as Comment } from './Comment'; export { default as Document } from './Document'; +export { default as DocumentFragment } from './DocumentFragment'; export { default as DocumentType } from './DocumentType'; export { default as DOMImplementation } from './DOMImplementation'; export { default as Element } from './Element'; diff --git a/src/mixins.ts b/src/mixins.ts index 33c8f68..563bc45 100644 --- a/src/mixins.ts +++ b/src/mixins.ts @@ -1,5 +1,6 @@ import CharacterData from './CharacterData'; import Document from './Document'; +import DocumentFragment from './DocumentFragment'; import Element from './Element'; import Node from './Node'; @@ -28,9 +29,8 @@ export interface ParentNode { // Element implements ParentNode; export function asParentNode (node: Node): ParentNode | null { - // (document fragments not implemented) - if (isNodeOfType(node, NodeType.ELEMENT_NODE, NodeType.DOCUMENT_NODE)) { - return node as Element | Document; + if (isNodeOfType(node, NodeType.ELEMENT_NODE, NodeType.DOCUMENT_NODE, NodeType.DOCUMENT_FRAGMENT_NODE)) { + return node as Element | Document | DocumentFragment; } return null; diff --git a/src/util/mutationAlgorithms.ts b/src/util/mutationAlgorithms.ts index 1642436..9da20da 100644 --- a/src/util/mutationAlgorithms.ts +++ b/src/util/mutationAlgorithms.ts @@ -3,6 +3,7 @@ import { NodeType, isNodeOfType } from './NodeType'; import { determineLengthOfNode, getNodeDocument, getNodeIndex, forEachInclusiveDescendant } from './treeHelpers'; import { insertIntoChildren, removeFromChildren } from './treeMutations'; import Document from '../Document'; +import DocumentFragment from '../DocumentFragment'; import Element from '../Element'; import Node from '../Node'; import { ranges } from '../Range'; @@ -62,9 +63,27 @@ function ensurePreInsertionValidity (node: Node, parent: Node, child: Node | nul // DocumentFragment node case NodeType.DOCUMENT_FRAGMENT_NODE: // If node has more than one element child or has a Text node child. + const fragment = node as DocumentFragment; + if (fragment.firstElementChild !== fragment.lastElementChild) { + throwHierarchyRequestError('can not insert more than one element under a Document'); + } + if (Array.from(fragment.childNodes).some(child => isNodeOfType(child, NodeType.TEXT_NODE))) { + throwHierarchyRequestError('can not insert a Text node under a Document'); + } // Otherwise, if node has one element child and either parent has an element child, child is a doctype, // or child is not null and a doctype is following child. - // (document fragments not implemented) + if ( + fragment.firstElementChild && + ( + parentDocument.documentElement || + (child && isNodeOfType(child, NodeType.DOCUMENT_TYPE_NODE)) || + (child && parentDocument.doctype && getNodeIndex(child) < getNodeIndex(parentDocument.doctype)) + ) + ) { + throwHierarchyRequestError( + 'Document should contain at most one doctype, followed by at most one element' + ); + } break; // element @@ -296,9 +315,26 @@ export function replaceChildWithNode (child: Node, node: Node, parent: Node): No // DocumentFragment node case NodeType.DOCUMENT_FRAGMENT_NODE: // If node has more than one element child or has a Text node child. + const fragment = node as DocumentFragment; + if (fragment.firstElementChild !== fragment.lastElementChild) { + throwHierarchyRequestError('can not insert more than one element under a Document'); + } + if (Array.from(fragment.childNodes).some(child => isNodeOfType(child, NodeType.TEXT_NODE))) { + throwHierarchyRequestError('can not insert a Text node under a Document'); + } // Otherwise, if node has one element child and either parent has an element child that is not child or // a doctype is following child. - // (document fragments not implemented) + if ( + fragment.firstElementChild && + ( + (parentDocument.documentElement && parentDocument.documentElement !== child) || + (child && parentDocument.doctype && getNodeIndex(child) < getNodeIndex(parentDocument.doctype)) + ) + ) { + throwHierarchyRequestError( + 'Document should contain at most one doctype, followed by at most one element' + ); + } break; // element From 366380fce50111ca1ff914f3130ab02cd8bd6d64 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Thu, 1 Jun 2017 11:34:37 +0200 Subject: [PATCH 11/34] Remove bower.json. --- bower.json | 27 --------------------------- 1 file changed, 27 deletions(-) delete mode 100644 bower.json diff --git a/bower.json b/bower.json deleted file mode 100644 index 28baf3b..0000000 --- a/bower.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "slimdom", - "version": "0.5.3", - "homepage": "https://github.com/bwrrp/slimdom.js", - "authors": [ - "Stef Busking " - ], - "description": "Fast, tiny DOM implementation in pure JS", - "main": "src/main.js", - "moduleType": [ - "amd", - "node" - ], - "keywords": [ - "dom", - "xml" - ], - "license": "MIT", - "ignore": [ - "**/.*", - "*.sublime-project", - "*.sublime-workspace", - "node_modules", - "bower_components", - "test" - ] -} From a2a74993d62022688f651d5f36a4838bb3051dca Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Fri, 26 May 2017 15:46:43 +0200 Subject: [PATCH 12/34] Run web platform DOM tests. --- package.json | 3 +- test/tsconfig.json | 6 +- test/web-platform-tests/SlimdomTreeAdapter.ts | 183 ++++++ test/web-platform-tests/webPlatform.tests.ts | 598 ++++++++++++++++++ 4 files changed, 787 insertions(+), 3 deletions(-) create mode 100644 test/web-platform-tests/SlimdomTreeAdapter.ts create mode 100644 test/web-platform-tests/webPlatform.tests.ts diff --git a/package.json b/package.json index 88a6e05..3f8bb4d 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "build:bundle": "rimraf dist && rimraf lib && tsc && rollup -c", "docs": "typedoc --out docs --excludePrivate --excludeNotExported src/index.ts", "prepare": "npm run build:bundle", - "test": "rimraf test/bin && tsc -P test && mocha --recursive test/bin/test" + "test": "rimraf test/bin && tsc -P test && mocha --timeout 20000 --recursive test/bin/test" }, "files": [ "dist" @@ -33,6 +33,7 @@ "chai": "^3.5.0", "lolex": "^1.6.0", "mocha": "^3.3.0", + "parse5": "^3.0.2", "rimraf": "^2.6.1", "rollup": "^0.41.6", "rollup-plugin-babili": "^3.0.0", diff --git a/test/tsconfig.json b/test/tsconfig.json index f1703e5..3c8bb97 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -10,8 +10,10 @@ ], "types": [ "chai", - "mocha" - ] + "mocha", + "node" + ], + "moduleResolution": "node" }, "include": [ "./**/*" diff --git a/test/web-platform-tests/SlimdomTreeAdapter.ts b/test/web-platform-tests/SlimdomTreeAdapter.ts new file mode 100644 index 0000000..d4ea932 --- /dev/null +++ b/test/web-platform-tests/SlimdomTreeAdapter.ts @@ -0,0 +1,183 @@ +import * as parse5 from 'parse5'; + +import * as slimdom from '../../src/index'; +import Attr from '../../src/Attr'; +import { createElement } from '../../src/Element'; +import { appendAttribute } from '../../src/util/attrMutations'; + +function undefinedAsNull (value: T | undefined): T | null { + if (value === undefined) { + return null; + } + + return value; +} + +function qualifiedName (namespace: string | undefined, prefix: string | undefined, name: string) { + return prefix ? `${prefix}:${name}` : name; +} + +export default class SlimdomTreeAdapter implements parse5.AST.TreeAdapter { + private _globalDocument = new slimdom.Document(); + private _mode: parse5.AST.DocumentMode = 'no-quirks'; + + createDocument (): parse5.AST.Document { + return this._globalDocument.implementation.createDocument(null, ''); + } + + createDocumentFragment (): parse5.AST.DocumentFragment { + throw new Error("Method not implemented."); + } + + createElement (tagName: string, namespaceURI: string, attrs: parse5.AST.Default.Attribute[]): parse5.AST.Element { + const [ localName, prefix ] = tagName.indexOf(':') >= 0 ? tagName.split(':') : [ tagName, null ]; + // Create element without validation, as per HTML parser spec + const element = createElement(this._globalDocument, localName!, namespaceURI, prefix); + attrs.forEach(attr => { + // Create Attr node without validation, as per HTML parser spec + const attribute = new Attr(this._globalDocument, undefinedAsNull(attr.namespace), undefinedAsNull(attr.prefix), attr.name, attr.value, element); + appendAttribute(attribute, element); + }); + return element; + } + + createCommentNode (data: string): parse5.AST.CommentNode { + return this._globalDocument.createComment(data); + } + + appendChild (parentNode: parse5.AST.ParentNode, newNode: parse5.AST.Node): void { + (parentNode as slimdom.Node).appendChild(newNode as slimdom.Node); + } + + insertBefore (parentNode: parse5.AST.ParentNode, newNode: parse5.AST.Node, referenceNode: parse5.AST.Node): void { + (parentNode as slimdom.Node).insertBefore(newNode as slimdom.Node, referenceNode as slimdom.Node); + } + + setTemplateContent (templateElement: parse5.AST.Element, contentElement: parse5.AST.DocumentFragment): void { + throw new Error("Method not implemented."); + } + + getTemplateContent (templateElement: parse5.AST.Element): parse5.AST.DocumentFragment { + throw new Error("Method not implemented."); + } + + setDocumentType (document: parse5.AST.Document, name: string, publicId: string, systemId: string): void { + const doctype = this._globalDocument.implementation.createDocumentType(name, publicId, systemId); + const doc = document as slimdom.Document; + if (doc.doctype) { + doc.replaceChild(doctype, doc.doctype); + } + else { + doc.insertBefore(doctype, doc.documentElement); + } + } + + setDocumentMode (document: parse5.AST.Document, mode: parse5.AST.DocumentMode): void { + this._mode = mode; + } + + getDocumentMode (document: parse5.AST.Document): parse5.AST.DocumentMode { + return this._mode; + } + + detachNode (node: parse5.AST.Node): void { + const parent = (node as slimdom.Node).parentNode; + if (parent) { + parent.removeChild(node as slimdom.Node); + } + } + + insertText (parentNode: parse5.AST.ParentNode, text: string): void { + const lastChild = (parentNode as slimdom.Node).lastChild; + if (lastChild && lastChild.nodeType === slimdom.Node.TEXT_NODE) { + (lastChild as slimdom.Text).appendData(text); + return; + } + + (parentNode as slimdom.Node).appendChild(this._globalDocument.createTextNode(text)); + } + + insertTextBefore (parentNode: parse5.AST.ParentNode, text: string, referenceNode: parse5.AST.Node): void { + const sibling = referenceNode && (referenceNode as slimdom.Node).previousSibling; + if (sibling && sibling.nodeType === slimdom.Node.TEXT_NODE) { + (sibling as slimdom.Text).appendData(text); + return; + } + + (parentNode as slimdom.Node).insertBefore(this._globalDocument.createTextNode(text), referenceNode as slimdom.Node); + } + + adoptAttributes (recipient: parse5.AST.Element, attrs: parse5.AST.Default.Attribute[]): void { + const element = recipient as slimdom.Element; + attrs.forEach(attr => { + if (!element.hasAttributeNS(undefinedAsNull(attr.namespace), attr.name)) { + element.setAttributeNS(undefinedAsNull(attr.namespace), qualifiedName(attr.namespace, attr.prefix, attr.name), attr.value); + } + }); + } + + getFirstChild (node: parse5.AST.ParentNode): parse5.AST.Node { + return (node as slimdom.Node).firstChild!; + } + + getChildNodes (node: parse5.AST.ParentNode): parse5.AST.Node[] { + return (node as slimdom.Node).childNodes; + } + + getParentNode (node: parse5.AST.Node): parse5.AST.ParentNode { + return (node as slimdom.Node).parentNode!; + } + + getAttrList (element: parse5.AST.Element): parse5.AST.Default.Attribute[] { + return (element as slimdom.Element).attributes.map(attr => ({ + name: attr.localName, + namespace: attr.namespaceURI || undefined, + prefix: attr.prefix || undefined, + value: attr.value + })); + } + + getTagName (element: parse5.AST.Element): string { + return (element as slimdom.Element).tagName; + } + + getNamespaceURI (element: parse5.AST.Element): string { + return (element as slimdom.Element).namespaceURI!; + } + + getTextNodeContent (textNode: parse5.AST.TextNode): string { + return (textNode as slimdom.Text).data; + } + + getCommentNodeContent (commentNode: parse5.AST.CommentNode): string { + return (commentNode as slimdom.Comment).data; + } + + getDocumentTypeNodeName (doctypeNode: parse5.AST.DocumentType): string { + return (doctypeNode as slimdom.DocumentType).name; + } + + getDocumentTypeNodePublicId (doctypeNode: parse5.AST.DocumentType): string { + return (doctypeNode as slimdom.DocumentType).publicId; + } + + getDocumentTypeNodeSystemId (doctypeNode: parse5.AST.DocumentType): string { + return (doctypeNode as slimdom.DocumentType).systemId; + } + + isTextNode (node: parse5.AST.Node): boolean { + return node && (node as slimdom.Node).nodeType === slimdom.Node.TEXT_NODE; + } + + isCommentNode (node: parse5.AST.Node): boolean { + return node && (node as slimdom.Node).nodeType === slimdom.Node.COMMENT_NODE; + } + + isDocumentTypeNode (node: parse5.AST.Node): boolean { + return node && (node as slimdom.Node).nodeType === slimdom.Node.DOCUMENT_TYPE_NODE; + } + + isElementNode (node: parse5.AST.Node): boolean { + return node && (node as slimdom.Node).nodeType === slimdom.Node.ELEMENT_NODE; + } +} diff --git a/test/web-platform-tests/webPlatform.tests.ts b/test/web-platform-tests/webPlatform.tests.ts new file mode 100644 index 0000000..2a1181d --- /dev/null +++ b/test/web-platform-tests/webPlatform.tests.ts @@ -0,0 +1,598 @@ +import * as chai from 'chai'; +import * as fs from 'fs'; +import * as parse5 from 'parse5'; +import * as path from 'path'; + +import * as slimdom from '../../src/index'; + +import SlimdomTreeAdapter from './SlimdomTreeAdapter'; + +const TEST_BLACKLIST: { [key: string]: (string | { [key: string]: string }) } = { + 'dom/historical.html': 'WebIDL parsing not implemented', + 'dom/interface-objects.html': 'window not implemented', + 'dom/interfaces.html': 'WebIDL parsing not implemented', + 'dom/collections': 'This implementation uses arrays instead of collection types', + 'dom/events': 'Events not implemented', + 'dom/lists': 'DOMTokenList (Element#classList) not implemented', + 'dom/nodes/append-on-Document.html': 'ParentNode#append not implemented', + 'dom/nodes/attributes.html': { + 'setAttribute should lowercase its name argument (upper case attribute)': 'HTML attribute lowercasing not implemented', + 'setAttribute should lowercase its name argument (mixed case attribute)': 'HTML attribute lowercasing not implemented', + 'Attributes should work in document fragments.': 'Element#attributes not implemented as NamedNodeMap', + 'Only lowercase attributes are returned on HTML elements (upper case attribute)': 'HTML attribute lowercasing not implemented', + 'Only lowercase attributes are returned on HTML elements (mixed case attribute)': 'HTML attribute lowercasing not implemented', + 'setAttributeNode, if it fires mutation events, should fire one with the new node when resetting an existing attribute (outer shell)': 'Mutation events not implemented', + 'getAttributeNames tests': 'Element#getAttributeNames not implemented', + 'Own property correctness with basic attributes': 'Element#attributes not implemented as NamedNodeMap', + 'Own property correctness with non-namespaced attribute before same-name namespaced one': 'Element#attributes not implemented as NamedNodeMap', + 'Own property correctness with namespaced attribute before same-name non-namespaced one': 'Element#attributes not implemented as NamedNodeMap', + 'Own property correctness with two namespaced attributes with the same name-with-prefix': 'Element#attributes not implemented as NamedNodeMap', + 'Own property names should only include all-lowercase qualified names for an HTML element in an HTML document': 'Element#attributes not implemented as NamedNodeMap', + 'Own property names should include all qualified names for a non-HTML element in an HTML document': 'Element#attributes not implemented as NamedNodeMap', + 'Own property names should include all qualified names for an HTML element in a non-HTML document': 'Element#attributes not implemented as NamedNodeMap', + }, + 'dom/nodes/case.html': 'HTML case behavior not implemented', + 'dom/nodes/CharacterData-remove.html': 'ChildNode#remove not implemented', + 'dom/nodes/ChildNode-after.html': 'ChildNode#after not implemented', + 'dom/nodes/ChildNode-before.html': 'ChildNode#before not implemented', + 'dom/nodes/ChildNode-replaceWith.html': 'ChildNode#replaceWith not implemented', + 'dom/nodes/Comment-constructor.html': 'Comment constructor not implemented', + 'dom/nodes/Document-characterSet-normalization.html': 'Document#characterSet not implemented', + 'dom/nodes/Document-constructor.html': { + 'new Document(): URL parsing': 'HTMLAnchorElement not implemented' + }, + 'dom/nodes/Document-contentType': 'Document#contentType not implemented', + 'dom/nodes/Document-createAttribute.html': { + 'HTML document.createAttribute("TITLE")': 'HTML attribute lowercasing not implemented' + }, + 'dom/nodes/Document-createElement.html': 'Document load using iframe not implemented', + 'dom/nodes/Document-createElement-namespace.html': 'DOMParser / contentType not implemented', + 'dom/nodes/Document-createElement-namespace-tests': 'Document load using iframe not implemented', + 'dom/nodes/Document-createElementNS.html': 'Document load using iframe not implemented', + 'dom/nodes/Document-createEvent.html': 'Document#createEvent not implemented', + 'dom/nodes/Document-createTreeWalker.html': 'Document#createTreeWalker not implemented', + 'dom/nodes/Document-getElementById.html': 'Document#getElementById not implemented', + 'dom/nodes/Document-getElementsByTagName.html': 'Document#getElementsByTagName not implemented', + 'dom/nodes/Document-getElementsByTagNameNS.html': 'Document#getElementsByTagNameNS not implemented', + 'dom/nodes/Document-URL.sub.html': 'Document#URL not implemented', + 'dom/nodes/DocumentType-literal.html': 'Depends on HTML parsing', + 'dom/nodes/DocumentType-remove.html': 'ChildNode#remove not implemented', + 'dom/nodes/DOMImplementation-createDocument.html': { + 'createDocument test: metadata for "http://www.w3.org/1999/xhtml","",null': 'HTML contentType not implemented', + 'createDocument test: metadata for "http://www.w3.org/2000/svg","",null': 'SVG contentType not implemented' + }, + 'dom/nodes/DOMImplementation-createDocumentType.html': 'DocumentType#ownerDocument not implemented per spec', + 'dom/nodes/DOMImplementation-createHTMLDocument.html': 'HTML*Element interfaces not implemented', + 'dom/nodes/DOMImplementation-hasFeature.html': 'DOMImplementation#hasFeature not implemented', + 'dom/nodes/Element-children.html': 'Element#children not implemented as HTMLCollection', + 'dom/nodes/Element-classlist.html': 'Element#classList not implemented', + 'dom/nodes/Element-closest.html': 'Element#closest not implemented', + 'dom/nodes/Element-getElementsByClassName.html': 'Element#getElementsByClassName not implemented', + 'dom/nodes/Element-getElementsByTagName-change-document-HTMLNess.html': 'Element#getElementsByTagName not implemented', + 'dom/nodes/Element-getElementsByTagName-change-document-HTMLNess-iframe.html': 'Element#getElementsByTagName not implemented', + 'dom/nodes/Element-getElementsByTagName.html': 'Element#getElementsByTagName not implemented', + 'dom/nodes/Element-getElementsByTagNameNS.html': 'Element#getElementsByTagNameNS not implemented', + 'dom/nodes/Element-insertAdjacentElement.html': 'Element#insertAdjacentElement not implemented', + 'dom/nodes/Element-insertAdjacentText.html': 'Element#insertAdjacentText not implemented', + 'dom/nodes/Element-matches.html': 'Element#matches not implemented', + 'dom/nodes/Element-remove.html': 'ChildNode#remove not implemented', + 'dom/nodes/Element-tagName.html': 'HTML tagName uppercasing not implemented', + 'dom/nodes/Element-webkitMatchesSelector.html': 'Element#webkitMatchesSelector not implemented', + 'dom/nodes/insert-adjacent.html': 'Element#insertAdjacentElement / Element#insertAdjacentText not implemented', + 'dom/nodes/MutationObserver-attributes.html': { + 'attributes Element.id: update, no oldValue, mutation': 'Element#id not implemented', + 'attributes Element.id: update mutation': 'Element#id not implemented', + 'attributes Element.id: empty string update mutation': 'Element#id not implemented', + 'attributes Element.id: same value mutation': 'Element#id not implemented', + 'attributes Element.unknown: IDL attribute no mutation': 'Element#id not implemented', + 'attributes HTMLInputElement.type: type update mutation': 'HTMLInputElement not implemented', + 'attributes Element.className: new value mutation': 'Element#className not implemented', + 'attributes Element.className: empty string update mutation': 'Element#className not implemented', + 'attributes Element.className: same value mutation': 'Element#className not implemented', + 'attributes Element.className: same multiple values mutation': 'Element#className not implemented', + 'attributes Element.classList.add: single token addition mutation': 'Element#classList not implemented', + 'attributes Element.classList.add: multiple tokens addition mutation': 'Element#classList not implemented', + 'attributes Element.classList.add: syntax err/no mutation': 'Element#classList not implemented', + 'attributes Element.classList.add: invalid character/no mutation': 'Element#classList not implemented', + 'attributes Element.classList.add: same value mutation': 'Element#classList not implemented', + 'attributes Element.classList.remove: single token removal mutation': 'Element#classList not implemented', + 'attributes Element.classList.remove: multiple tokens removal mutation': 'Element#classList not implemented', + 'attributes Element.classList.remove: missing token removal mutation': 'Element#classList not implemented', + 'attributes Element.classList.toggle: token removal mutation': 'Element#classList not implemented', + 'attributes Element.classList.toggle: token addition mutation': 'Element#classList not implemented', + 'attributes Element.classList.toggle: forced token removal mutation': 'Element#classList not implemented', + 'attributes Element.classList.toggle: forced missing token removal no mutation': 'Element#classList not implemented', + 'attributes Element.classList.toggle: forced existing token addition no mutation': 'Element#classList not implemented', + 'attributes Element.classList.toggle: forced token addition mutation': 'Element#classList not implemented', + 'attributes Element.removeAttribute: removal no mutation': 'Element#id not implemented', + 'childList HTMLInputElement.removeAttribute: type removal mutation': 'Element#id not implemented', + 'attributes Element.removeAttributeNS: removal no mutation': 'Element#id not implemented', + 'attributes Element.removeAttributeNS: prefixed attribute removal no mutation': 'Element#id not implemented', + 'attributes/attributeFilter Element.id/Element.className: update mutation': 'attributeFilter not implemented', + 'attributes/attributeFilter Element.id/Element.className: multiple filter update mutation': 'attributeFilter not implemented', + 'attributeOldValue alone Element.id: update mutation': 'Element#id not implemented', + 'attributeFilter alone Element.id/Element.className: multiple filter update mutation': 'attributeFilter not implemented', + 'childList false: no childList mutation': 'Element#textContent setter not implemented' + }, + 'dom/nodes/MutationObserver-characterData.html': { + 'characterData Range.deleteContents: child and data removal mutation': 'Range#deleteContents not implemented', + 'characterData Range.deleteContents: child and data removal mutation (2)': 'Range#deleteContents not implemented', + 'characterData Range.extractContents: child and data removal mutation': 'Range#extractContents not implemented', + 'characterData Range.extractContents: child and data removal mutation (2)': 'Range#extractContents not implemented', + }, + 'dom/nodes/MutationObserver-childList.html': { + 'childList Node.textContent: replace content mutation': 'Element#textContent setter not implemented', + 'childList Node.textContent: no previous content mutation': 'Element#textContent setter not implemented', + 'childList Node.textContent: textContent no mutation': 'Element#textContent setter not implemented', + 'childList Node.textContent: empty string mutation': 'Element#textContent setter not implemented', + 'childList Range.deleteContents: child removal mutation': 'Range#deleteContents not implemented', + 'childList Range.deleteContents: child and data removal mutation': 'Range#deleteContents not implemented', + 'childList Range.extractContents: child removal mutation': 'Range#extractContents not implemented', + 'childList Range.extractContents: child and data removal mutation': 'Range#extractContents not implemented', + 'childList Range.insertNode: child insertion mutation': 'Range#insertNode not implemented', + 'childList Range.insertNode: children insertion mutation': 'Range#insertNode not implemented', + 'childList Range.surroundContents: children removal and addition mutation': 'Range#surroundContents not implemented', + }, + 'dom/nodes/MutationObserver-disconnect.html': 'Element#id not implemented', + 'dom/nodes/MutationObserver-document.html': 'Running script during parsing not implemented', + 'dom/nodes/MutationObserver-inner-outer.html': 'Element#innerHTML / Element#outerHTML not implemented', + 'dom/nodes/MutationObserver-subtree.html': 'Element#id not implemented', + 'dom/nodes/MutationObserver-takeRecords.html': 'Element#textContent setter not implemented', + 'dom/nodes/Node-baseURI.html': 'Node#baseURI not implemented', + 'dom/nodes/Node-childNodes.html': 'Node#childNodes not implemented as HTMLCollection', + 'dom/nodes/Node-cloneNode.html': { + 'createElement(a)': 'HTMLElement interfaces not implemented', + 'createElement(abbr)': 'HTMLElement interfaces not implemented', + 'createElement(acronym)': 'HTMLElement interfaces not implemented', + 'createElement(address)': 'HTMLElement interfaces not implemented', + 'createElement(applet)': 'HTMLElement interfaces not implemented', + 'createElement(area)': 'HTMLElement interfaces not implemented', + 'createElement(article)': 'HTMLElement interfaces not implemented', + 'createElement(aside)': 'HTMLElement interfaces not implemented', + 'createElement(audio)': 'HTMLElement interfaces not implemented', + 'createElement(b)': 'HTMLElement interfaces not implemented', + 'createElement(base)': 'HTMLElement interfaces not implemented', + 'createElement(bdi)': 'HTMLElement interfaces not implemented', + 'createElement(bdo)': 'HTMLElement interfaces not implemented', + 'createElement(bgsound)': 'HTMLElement interfaces not implemented', + 'createElement(big)': 'HTMLElement interfaces not implemented', + 'createElement(blockquote)': 'HTMLElement interfaces not implemented', + 'createElement(body)': 'HTMLElement interfaces not implemented', + 'createElement(br)': 'HTMLElement interfaces not implemented', + 'createElement(button)': 'HTMLElement interfaces not implemented', + 'createElement(canvas)': 'HTMLElement interfaces not implemented', + 'createElement(caption)': 'HTMLElement interfaces not implemented', + 'createElement(center)': 'HTMLElement interfaces not implemented', + 'createElement(cite)': 'HTMLElement interfaces not implemented', + 'createElement(code)': 'HTMLElement interfaces not implemented', + 'createElement(col)': 'HTMLElement interfaces not implemented', + 'createElement(colgroup)': 'HTMLElement interfaces not implemented', + 'createElement(data)': 'HTMLElement interfaces not implemented', + 'createElement(datalist)': 'HTMLElement interfaces not implemented', + 'createElement(dialog)': 'HTMLElement interfaces not implemented', + 'createElement(dd)': 'HTMLElement interfaces not implemented', + 'createElement(del)': 'HTMLElement interfaces not implemented', + 'createElement(details)': 'HTMLElement interfaces not implemented', + 'createElement(dfn)': 'HTMLElement interfaces not implemented', + 'createElement(dir)': 'HTMLElement interfaces not implemented', + 'createElement(div)': 'HTMLElement interfaces not implemented', + 'createElement(dl)': 'HTMLElement interfaces not implemented', + 'createElement(dt)': 'HTMLElement interfaces not implemented', + 'createElement(embed)': 'HTMLElement interfaces not implemented', + 'createElement(fieldset)': 'HTMLElement interfaces not implemented', + 'createElement(figcaption)': 'HTMLElement interfaces not implemented', + 'createElement(figure)': 'HTMLElement interfaces not implemented', + 'createElement(font)': 'HTMLElement interfaces not implemented', + 'createElement(footer)': 'HTMLElement interfaces not implemented', + 'createElement(form)': 'HTMLElement interfaces not implemented', + 'createElement(frame)': 'HTMLElement interfaces not implemented', + 'createElement(frameset)': 'HTMLElement interfaces not implemented', + 'createElement(h1)': 'HTMLElement interfaces not implemented', + 'createElement(h2)': 'HTMLElement interfaces not implemented', + 'createElement(h3)': 'HTMLElement interfaces not implemented', + 'createElement(h4)': 'HTMLElement interfaces not implemented', + 'createElement(h5)': 'HTMLElement interfaces not implemented', + 'createElement(h6)': 'HTMLElement interfaces not implemented', + 'createElement(head)': 'HTMLElement interfaces not implemented', + 'createElement(header)': 'HTMLElement interfaces not implemented', + 'createElement(hgroup)': 'HTMLElement interfaces not implemented', + 'createElement(hr)': 'HTMLElement interfaces not implemented', + 'createElement(html)': 'HTMLElement interfaces not implemented', + 'createElement(i)': 'HTMLElement interfaces not implemented', + 'createElement(iframe)': 'HTMLElement interfaces not implemented', + 'createElement(img)': 'HTMLElement interfaces not implemented', + 'createElement(input)': 'HTMLElement interfaces not implemented', + 'createElement(ins)': 'HTMLElement interfaces not implemented', + 'createElement(isindex)': 'HTMLElement interfaces not implemented', + 'createElement(kbd)': 'HTMLElement interfaces not implemented', + 'createElement(label)': 'HTMLElement interfaces not implemented', + 'createElement(legend)': 'HTMLElement interfaces not implemented', + 'createElement(li)': 'HTMLElement interfaces not implemented', + 'createElement(link)': 'HTMLElement interfaces not implemented', + 'createElement(main)': 'HTMLElement interfaces not implemented', + 'createElement(map)': 'HTMLElement interfaces not implemented', + 'createElement(mark)': 'HTMLElement interfaces not implemented', + 'createElement(marquee)': 'HTMLElement interfaces not implemented', + 'createElement(meta)': 'HTMLElement interfaces not implemented', + 'createElement(meter)': 'HTMLElement interfaces not implemented', + 'createElement(nav)': 'HTMLElement interfaces not implemented', + 'createElement(nobr)': 'HTMLElement interfaces not implemented', + 'createElement(noframes)': 'HTMLElement interfaces not implemented', + 'createElement(noscript)': 'HTMLElement interfaces not implemented', + 'createElement(object)': 'HTMLElement interfaces not implemented', + 'createElement(ol)': 'HTMLElement interfaces not implemented', + 'createElement(optgroup)': 'HTMLElement interfaces not implemented', + 'createElement(option)': 'HTMLElement interfaces not implemented', + 'createElement(output)': 'HTMLElement interfaces not implemented', + 'createElement(p)': 'HTMLElement interfaces not implemented', + 'createElement(param)': 'HTMLElement interfaces not implemented', + 'createElement(pre)': 'HTMLElement interfaces not implemented', + 'createElement(progress)': 'HTMLElement interfaces not implemented', + 'createElement(q)': 'HTMLElement interfaces not implemented', + 'createElement(rp)': 'HTMLElement interfaces not implemented', + 'createElement(rt)': 'HTMLElement interfaces not implemented', + 'createElement(ruby)': 'HTMLElement interfaces not implemented', + 'createElement(s)': 'HTMLElement interfaces not implemented', + 'createElement(samp)': 'HTMLElement interfaces not implemented', + 'createElement(script)': 'HTMLElement interfaces not implemented', + 'createElement(section)': 'HTMLElement interfaces not implemented', + 'createElement(select)': 'HTMLElement interfaces not implemented', + 'createElement(small)': 'HTMLElement interfaces not implemented', + 'createElement(source)': 'HTMLElement interfaces not implemented', + 'createElement(spacer)': 'HTMLElement interfaces not implemented', + 'createElement(span)': 'HTMLElement interfaces not implemented', + 'createElement(strike)': 'HTMLElement interfaces not implemented', + 'createElement(style)': 'HTMLElement interfaces not implemented', + 'createElement(sub)': 'HTMLElement interfaces not implemented', + 'createElement(summary)': 'HTMLElement interfaces not implemented', + 'createElement(sup)': 'HTMLElement interfaces not implemented', + 'createElement(table)': 'HTMLElement interfaces not implemented', + 'createElement(tbody)': 'HTMLElement interfaces not implemented', + 'createElement(td)': 'HTMLElement interfaces not implemented', + 'createElement(template)': 'HTMLElement interfaces not implemented', + 'createElement(textarea)': 'HTMLElement interfaces not implemented', + 'createElement(th)': 'HTMLElement interfaces not implemented', + 'createElement(time)': 'HTMLElement interfaces not implemented', + 'createElement(title)': 'HTMLElement interfaces not implemented', + 'createElement(tr)': 'HTMLElement interfaces not implemented', + 'createElement(tt)': 'HTMLElement interfaces not implemented', + 'createElement(track)': 'HTMLElement interfaces not implemented', + 'createElement(u)': 'HTMLElement interfaces not implemented', + 'createElement(ul)': 'HTMLElement interfaces not implemented', + 'createElement(var)': 'HTMLElement interfaces not implemented', + 'createElement(video)': 'HTMLElement interfaces not implemented', + 'createElement(unknown)': 'HTMLElement interfaces not implemented', + 'createElement(wbr)': 'HTMLElement interfaces not implemented', + 'createElementNS HTML': 'HTMLElement interfaces not implemented', + 'node with children': 'HTMLElement interfaces not implemented' + }, + 'dom/nodes/Node-compareDocumentPosition.html': 'Node#compareDocumentPosition not implemented', + 'dom/nodes/Node-constants.html': { + 'Constants for createDocumentPosition on Node interface object.': 'Node#compareDocumentPosition not implemented', + 'Constants for createDocumentPosition on Node prototype object.': 'Node#compareDocumentPosition not implemented', + 'Constants for createDocumentPosition on Element object.': 'Node#compareDocumentPosition not implemented', + 'Constants for createDocumentPosition on Text object.': 'Node#compareDocumentPosition not implemented' + }, + 'dom/nodes/Node-contains.html': 'Element#textContent setter not implemented', + 'dom/nodes/Node-isConnected.html': 'Node#isConnected not implemented', + 'dom/nodes/Node-isEqualNode.html': 'Node#isEqualNode not implemented', + 'dom/nodes/Node-isEqualNode-iframe1.html': 'Node#isEqualNode not implemented', + 'dom/nodes/Node-isEqualNode-iframe2.html': 'Node#isEqualNode not implemented', + 'dom/nodes/Node-isSameNode.html': 'Node#isSameNode not implemented', + 'dom/nodes/NodeList-Iterable.html': 'NodeList not implemented', + 'dom/nodes/Node-lookupNamespaceURI.html': 'Node#lookupNamespaceURI not implemented', + 'dom/nodes/Node-lookupPrefix.html': 'Node#lookupPrefix not implemented', + 'dom/nodes/Node-nodeName.html': { + 'For Element nodes, nodeName should return the same as tagName.': 'HTML tagName uppercasing not implemented' + }, + 'dom/nodes/Node-normalize.html': { + 'Node.normalize()': 'Element#textContent not implemented' + }, + 'dom/nodes/Node-parentNode.html': { + 'Removed iframe': 'Document load using iframe not implemented' + }, + 'dom/nodes/Node-properties.html': 'Element#textContent not implemented', + 'dom/nodes/Node-replaceChild.html': { + 'replaceChild should work in the presence of mutation events.': 'Mutation events not implemented' + }, + 'dom/nodes/Node-textContent.html': 'Node#textContent not implemented', + 'dom/nodes/ParentNode-append.html': 'ParentNode#append not implemented', + 'dom/nodes/ParentNode-prepend.html': 'ParentNode#prepend not implemented', + 'dom/nodes/ParentNode-querySelector-All-content.html': 'ParentNode#querySelectorAll not implemented', + 'dom/nodes/ParentNode-querySelector-All.html': 'ParentNode#querySelectorAll not implemented', + 'dom/nodes/prepend-on-Document.html': 'ParentNode#prepend not implemented', + 'dom/nodes/remove-unscopable.html': 'Methods not implemented', + 'dom/nodes/rootNode.html': 'Node#getRootNode not implemented', + 'dom/nodes/Text-constructor.html': 'Text constructor not implemented', + 'dom/ranges/Range-cloneContents.html': 'Range#cloneContents not implemented', + 'dom/ranges/Range-cloneRange.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-collapse.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-commonAncestorContainer.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-compareBoundaryPoints.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-comparePoint.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-constructor.html': 'Range constructor not implemented', + 'dom/ranges/Range-deleteContents.html': 'Range#deleteContents not implemented', + 'dom/ranges/Range-extractContents.html': 'Range#extractContents not implemented', + 'dom/ranges/Range-insertNode.html': 'Range#insertNode not implemented', + 'dom/ranges/Range-intersectsNode.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-isPointInRange.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-mutations-appendChild.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-mutations-appendData.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-mutations-dataChange.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-mutations-deleteData.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-mutations-insertBefore.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-mutations-insertData.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-mutations-removeChild.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-mutations-replaceChild.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-mutations-replaceData.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-mutations-splitText.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-selectNode.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-set.html': 'Element#textContent setter not implemented', + 'dom/ranges/Range-stringifier.html': 'Range#toString not implemented', + 'dom/ranges/Range-surroundContents.html': 'Range#surroundContents not implemented', + 'dom/traversal': 'NodeIterator and TreeWalker not implemented' +} + +function getNodes (root: slimdom.Node, ...path: string[]): slimdom.Node[] { + if (!path.length) { + return [root]; + } + + const [nodeName, ...remainder] = path; + const matchingChildren = Array.from((root as slimdom.Element).childNodes).filter(n => n.nodeName === nodeName); + return matchingChildren.reduce( + (nodes, child) => nodes.concat(getNodes(child, ...remainder)), + [] as slimdom.Node[] + ); +} + +function getAllText (root: slimdom.Node, ...path: string[]): string { + return getNodes(root, ...path) + .map(n => (n as slimdom.Text).data) + .join(''); +} + +function getAllScripts (doc: slimdom.Document, casePath: string) { + const scriptElements = (doc as any).getElementsByTagName('script'); + return scriptElements.reduce((scripts: string[], el: slimdom.Element) => { + const src = el.attributes.find(a => a.name === 'src'); + if (src) { + const resolvedPath = src.value.startsWith('/') + ? path.resolve(process.env.WEB_PLATFORM_TESTS_PATH, src.value.substring(1)) + : path.resolve(path.dirname(casePath), src.value); + return scripts.concat([fs.readFileSync(resolvedPath, 'utf-8')]); + } + + return scripts.concat([getAllText(el, '#text')]); + }, []).join('\n'); +} + +function createTest (casePath: string, blacklistReason: { [key: string]: string } = {}): void { + const document = parse5.parse(fs.readFileSync(casePath, 'utf-8'), { treeAdapter: new SlimdomTreeAdapter }) as slimdom.Document; + const title = getAllText(document, 'html', 'head', 'title', '#text') || path.basename(casePath); + const script = getAllScripts(document, casePath); + const scriptAsFunction = new Function('stubEnvironment', `with (stubEnvironment) { ${script} }`); + let stubs: { global: any, onLoadCallbacks: Function[], onErrorCallback?: Function }; + + function createStubEnvironment (document: slimdom.Document): { global: any, onLoadCallbacks: Function[], onErrorCallback?: Function } { + const onLoadCallbacks: Function[] = []; + let onErrorCallback: Function | undefined = undefined; + let global: any = { + document, + location: { href: casePath }, + window: null, + + get frames () { + return (document as any).getElementsByTagName('iframe').map((iframe: any) => { + if (!iframe.contentWindow) { + const stubs = createStubEnvironment(document.implementation.createHTMLDocument()); + iframe.contentWindow = stubs.global.window; + iframe.contentDocument = stubs.global.document; + iframe.document = stubs.global.document; + } + + return iframe; + }); + }, + + addEventListener (event: string, cb: Function) { + switch (event) { + case 'load': + onLoadCallbacks.push(cb); + break; + + case 'error': + onErrorCallback = cb; + break; + + default: + } + }, + + ...slimdom + } + global.window = global; + global.parent = global; + global.self = global; + + return { global, onLoadCallbacks, onErrorCallback }; + } + + beforeEach(() => { + stubs = createStubEnvironment(document); + }); + + it(title, (done: Function) => { + try { + scriptAsFunction(stubs.global); + + if (!stubs.global.add_completion_callback) { + // No test harness found, assume file is not really a test case + done(); + return; + } + + stubs.global.add_completion_callback(function (tests: any[], testStatus: any) { + // TODO: Seems to be triggered by duplicate names in the createDocument tests + //chai.assert.equal(testStatus.status, testStatus.OK, testStatus.message); + tests.forEach(test => { + // Ignore results of blacklisted tests + if (!blacklistReason[test.name]) { + chai.assert.equal(test.status, testStatus.OK, `${test.name}: ${test.message}`); + } + }); + done(); + }); + + stubs.onLoadCallbacks.forEach(cb => cb({})); + + // "Run" iframes + (stubs.global.frames as any[]).forEach(iframe => { + if (iframe.onload) { + iframe.onload(); + } + }); + } + catch (e) { + if (e instanceof chai.AssertionError) { + throw e; + } + + if (stubs.onErrorCallback) { + stubs.onErrorCallback(e); + } + else { + throw e; + } + } + }); +} + +function createTests (dirPath: string): void { + fs.readdirSync(dirPath).forEach(entry => { + const entryPath = path.join(dirPath, entry); + const relativePath = path.relative(process.env.WEB_PLATFORM_TESTS_PATH, entryPath); + const blacklistReason = TEST_BLACKLIST[relativePath]; + if (typeof blacklistReason === 'string') { + // Create a pending test + it(`${entry}: ${blacklistReason}`); + return; + } + + if (fs.statSync(entryPath).isDirectory()) { + describe(entry, () => { + createTests(entryPath); + }); + return; + } + + if (entry.endsWith('.html')) { + createTest(entryPath, blacklistReason); + } + }) +} + +describe('web platform DOM test suite', () => { + if (!process.env.WEB_PLATFORM_TESTS_PATH) { + it('requires the WEB_PLATFORM_TESTS_PATH environment variable to be set'); + return; + } + + (slimdom.Document.prototype as any).getElementsByTagName = function (this: slimdom.Document, tagName: string): slimdom.Node[] { + return (function getElementsByTagName (node: slimdom.Node): slimdom.Node[] { + return node.childNodes.reduce((elements, child) => { + if (child.nodeName === tagName) { + elements.push(child); + } + + if (child.nodeType === slimdom.Node.ELEMENT_NODE) { + elements = elements.concat(getElementsByTagName(child)); + } + + return elements; + }, [] as slimdom.Node[]); + })(this); + }; + + (slimdom.Document.prototype as any).getElementById = function getElementById (this: slimdom.Node, id: string): slimdom.Node | null { + return (function getElementById (node: slimdom.Node): slimdom.Node | null { + for (let child = node.firstChild; child; child = child.nextSibling) { + if (child.nodeType === slimdom.Node.ELEMENT_NODE && (child as slimdom.Element).getAttribute('id') === id) { + return child; + } + const descendant = getElementById(child); + if (descendant) { + return descendant; + } + } + + return null; + })(this); + }; + + // Stub not implemented properties to prevent createDocument tests from failing on these + Object.defineProperties(slimdom.Document.prototype, { + URL: { + value: 'about:blank' + }, + documentURI: { + value: 'about:blank' + }, + compatMode: { + value: 'CSS1Compat' + }, + characterSet: { + value: 'UTF-8' + }, + charset: { + value: 'UTF-8' + }, + inputEncoding: { + value: 'UTF-8' + }, + contentType: { + value: 'application/xml' + }, + origin: { + value: 'null' + }, + body: { + get () { + return this.getElementsByTagName('body')[0] || null; + } + }, + title: { + get () { + return getAllText(this, 'html', 'head', 'title', '#text'); + } + } + }); + + (slimdom.Document.prototype as any).querySelectorAll = () => []; + (slimdom.Document.prototype as any).querySelector = () => null; + + Object.defineProperties(slimdom.Attr.prototype, { + specified: { + value: true + }, + textContent: { + get () { + return this.nodeValue; + } + } + }); + Object.defineProperties(slimdom.CharacterData.prototype, { + textContent: { + get () { + return this.nodeValue; + } + } + }); + Object.defineProperties(slimdom.Element.prototype, { + style: { + value: {} + } + }); + + createTests(path.join(process.env.WEB_PLATFORM_TESTS_PATH, 'dom')); +}); From 86c5163cdec140bf92fa937e58bd016d66052b2c Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Tue, 13 Jun 2017 15:22:20 +0200 Subject: [PATCH 13/34] Format code using prettier. --- .vscode/settings.json | 6 + package.json | 1 + rollup.config.js | 9 +- src/Attr.ts | 30 ++-- src/CDATASection.ts | 8 +- src/CharacterData.ts | 30 ++-- src/Comment.ts | 8 +- src/DOMImplementation.ts | 12 +- src/Document.ts | 36 ++--- src/DocumentFragment.ts | 15 +- src/DocumentType.ts | 12 +- src/Element.ts | 88 ++++++----- src/Node.ts | 33 ++-- src/ProcessingInstruction.ts | 8 +- src/Range.ts | 70 +++++---- src/Text.ts | 16 +- src/XMLDocument.ts | 2 +- src/index.ts | 2 +- src/mixins.ts | 34 ++-- src/mutation-observer/MutationObserver.ts | 10 +- src/mutation-observer/MutationRecord.ts | 16 +- src/mutation-observer/NotifyList.ts | 43 ++--- src/mutation-observer/RegisteredObserver.ts | 10 +- src/mutation-observer/RegisteredObservers.ts | 36 ++--- src/mutation-observer/queueMutationRecord.ts | 2 +- src/util/NodeType.ts | 2 +- src/util/attrMutations.ts | 8 +- src/util/cloneNode.ts | 4 +- src/util/createElementNS.ts | 2 +- src/util/errorHelpers.ts | 24 +-- src/util/mutationAlgorithms.ts | 113 ++++++------- src/util/namespaceHelpers.ts | 13 +- src/util/treeHelpers.ts | 12 +- src/util/treeMutations.ts | 22 +-- src/util/typeHelpers.ts | 10 +- test/Document.tests.ts | 8 +- test/Element.tests.ts | 79 ++++++---- test/MutationObserver.tests.ts | 18 +-- test/Range.tests.ts | 1 - test/web-platform-tests/SlimdomTreeAdapter.ts | 90 ++++++----- test/web-platform-tests/webPlatform.tests.ts | 148 ++++++++++-------- 41 files changed, 581 insertions(+), 510 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7f2af3f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "prettier.printWidth": 120, + "prettier.singleQuote": true, + "prettier.useTabs": true +} diff --git a/package.json b/package.json index 3f8bb4d..ab07905 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "lolex": "^1.6.0", "mocha": "^3.3.0", "parse5": "^3.0.2", + "prettier": "^1.4.4", "rimraf": "^2.6.1", "rollup": "^0.41.6", "rollup-plugin-babili": "^3.0.0", diff --git a/rollup.config.js b/rollup.config.js index 704092a..60d6813 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,14 +4,11 @@ const { main: MAIN_DEST_FILE, module: MODULE_DEST_FILE } = require('./package.js export default { entry: 'lib/index.js', - targets: [ - { dest: MAIN_DEST_FILE, format: 'umd' }, - { dest: MODULE_DEST_FILE, format: 'es' }, - ], + targets: [{ dest: MAIN_DEST_FILE, format: 'umd' }, { dest: MODULE_DEST_FILE, format: 'es' }], moduleName: 'slimdom', exports: 'named', sourceMap: true, - onwarn (warning) { + onwarn(warning) { // Ignore "this is undefined" warning triggered by typescript's __extends helper if (warning.code === 'THIS_IS_UNDEFINED') { return; @@ -25,4 +22,4 @@ export default { sourceMap: true }) ] -} +}; diff --git a/src/Attr.ts b/src/Attr.ts index 7c26f20..cefcad2 100644 --- a/src/Attr.ts +++ b/src/Attr.ts @@ -10,20 +10,20 @@ import { NodeType } from './util/NodeType'; export default class Attr extends Node { // Node - public get nodeType (): number { + public get nodeType(): number { return NodeType.ATTRIBUTE_NODE; } - public get nodeName (): string { + public get nodeName(): string { // Return the qualified name return this.name; } - public get nodeValue (): string | null { + public get nodeValue(): string | null { return this._value; } - public set nodeValue (newValue: string | null) { + public set nodeValue(newValue: string | null) { // if the new value is null, act as if it was the empty string instead if (newValue === null) { newValue = ''; @@ -42,11 +42,11 @@ export default class Attr extends Node { private _value: string; - public get value (): string { + public get value(): string { return this._value; } - public set value (value: string) { + public set value(value: string) { setExistingAttributeValue(this, value); } @@ -62,7 +62,14 @@ export default class Attr extends Node { * @param value The value for the attribute * @param element The element for the attribute, or null if the attribute is not attached to an element */ - constructor (document: Document, namespace: string | null, prefix: string | null, localName: string, value: string, element: Element | null) { + constructor( + document: Document, + namespace: string | null, + prefix: string | null, + localName: string, + value: string, + element: Element | null + ) { super(document); this.namespaceURI = namespace; this.prefix = prefix; @@ -80,7 +87,7 @@ export default class Attr extends Node { * * @return A shallow copy of the context object */ - public _copy (document: Document): Attr { + public _copy(document: Document): Attr { // Set copy’s namespace, namespace prefix, local name, and value, to those of node. return new Attr(document, this.namespaceURI, this.prefix, this.localName, this.value, null); } @@ -92,14 +99,13 @@ export default class Attr extends Node { * @param attribute The attribute to set the value of * @param value The new value for attribute */ -function setExistingAttributeValue (attribute: Attr, value: string) { +function setExistingAttributeValue(attribute: Attr, value: string) { // 1. If attribute’s element is null, then set attribute’s value to value. const element = attribute.ownerElement; if (element === null) { (attribute as any)._value = value; - } - // 2. Otherwise, change attribute from attribute’s element to value. - else { + } else { + // 2. Otherwise, change attribute from attribute’s element to value. changeAttribute(attribute, element, value); } } diff --git a/src/CDATASection.ts b/src/CDATASection.ts index 7d24cc4..330c89e 100644 --- a/src/CDATASection.ts +++ b/src/CDATASection.ts @@ -5,11 +5,11 @@ import { NodeType } from './util/NodeType'; export default class CDATASection extends Text { // Node - public get nodeType (): number { + public get nodeType(): number { return NodeType.CDATA_SECTION_NODE; } - public get nodeName (): string { + public get nodeName(): string { return '#cdata-section'; } @@ -21,7 +21,7 @@ export default class CDATASection extends Text { * @param document (non-standard) The node document to associate with the node * @param data The data for the node */ - constructor (document: Document, data: string) { + constructor(document: Document, data: string) { super(document, data); } @@ -32,7 +32,7 @@ export default class CDATASection extends Text { * * @return A shallow copy of the context object */ - public _copy (document: Document): CDATASection { + public _copy(document: Document): CDATASection { // Set copy’s data, to that of node. return new CDATASection(document, this.data); } diff --git a/src/CharacterData.ts b/src/CharacterData.ts index 40c3b59..7b2ea7c 100644 --- a/src/CharacterData.ts +++ b/src/CharacterData.ts @@ -13,11 +13,11 @@ import { asUnsignedLong, treatNullAsEmptyString } from './util/typeHelpers'; export default abstract class CharacterData extends Node implements NonDocumentTypeChildNode, ChildNode { // Node - public get nodeValue (): string | null { + public get nodeValue(): string | null { return this._data; } - public set nodeValue (newValue: string | null) { + public set nodeValue(newValue: string | null) { // if the new value is null, act as if it was the empty string instead if (newValue === null) { newValue = ''; @@ -39,11 +39,11 @@ export default abstract class CharacterData extends Node implements NonDocumentT */ protected _data: string; - public get data (): string { + public get data(): string { return this._data; } - public set data (newValue: string) { + public set data(newValue: string) { // [TreatNullAs=EmptyString] newValue = treatNullAsEmptyString(newValue); @@ -51,7 +51,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT replaceData(this, 0, this.length, newValue); } - public get length (): number { + public get length(): number { return this.data.length; } @@ -59,7 +59,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * @param document The node document to associate with the node * @param data The data to associate with the node */ - protected constructor (document: Document, data: string) { + protected constructor(document: Document, data: string) { super(document); this._data = data; } @@ -72,7 +72,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * * @return The specified substring */ - public substringData (offset: number, count: number): string { + public substringData(offset: number, count: number): string { expectArity(arguments, 2); return substringData(this, offset, count); } @@ -82,7 +82,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * * @param data Data to append */ - public appendData (data: string): void { + public appendData(data: string): void { expectArity(arguments, 1); replaceData(this, this.length, 0, data); } @@ -93,7 +93,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * @param offset Offset at which to insert * @param data Data to insert */ - public insertData (offset: number, data: string): void { + public insertData(offset: number, data: string): void { expectArity(arguments, 1); replaceData(this, offset, 0, data); } @@ -104,7 +104,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * @param offset Offset at which to delete * @param count Number of code units to delete */ - public deleteData (offset: number, count: number): void { + public deleteData(offset: number, count: number): void { expectArity(arguments, 2); replaceData(this, offset, count, ''); } @@ -116,7 +116,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * @param count Number of code units to remove * @param data Data to insert */ - public replaceData (offset: number, count: number, data: string): void { + public replaceData(offset: number, count: number, data: string): void { expectArity(arguments, 3); replaceData(this, offset, count, data); } @@ -130,7 +130,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT * @param count The number of code units to replace * @param data The data to insert in place of the removed data */ -export function replaceData (node: CharacterData, offset: number, count: number, data: string): void { +export function replaceData(node: CharacterData, offset: number, count: number, data: string): void { // Match spec data types offset = asUnsignedLong(offset); count = asUnsignedLong(count); @@ -140,7 +140,7 @@ export function replaceData (node: CharacterData, offset: number, count: number, // 2. If offset is greater than length, then throw an IndexSizeError. if (offset > length) { - throwIndexSizeError('can not replace data past the node\'s length'); + throwIndexSizeError("can not replace data past the node's length"); } // 3. If offset plus count is greater than length, then set count to length minus offset. @@ -196,7 +196,7 @@ export function replaceData (node: CharacterData, offset: number, count: number, * * @return The requested substring */ -export function substringData (node: CharacterData, offset: number, count: number): string { +export function substringData(node: CharacterData, offset: number, count: number): string { // Match spec data types offset = asUnsignedLong(offset); count = asUnsignedLong(count); @@ -206,7 +206,7 @@ export function substringData (node: CharacterData, offset: number, count: numbe // 2. If offset is greater than length, then throw an IndexSizeError. if (offset > length) { - throwIndexSizeError('can not substring data past the node\'s length'); + throwIndexSizeError("can not substring data past the node's length"); } // 3. If offset plus count is greater than length, return a string whose value is the code units from the offsetth diff --git a/src/Comment.ts b/src/Comment.ts index fc2849b..f0522cc 100644 --- a/src/Comment.ts +++ b/src/Comment.ts @@ -5,11 +5,11 @@ import { NodeType } from './util/NodeType'; export default class Comment extends CharacterData { // Node - public get nodeType (): number { + public get nodeType(): number { return NodeType.COMMENT_NODE; } - public get nodeName (): string { + public get nodeName(): string { return '#comment'; } @@ -24,7 +24,7 @@ export default class Comment extends CharacterData { * @param document (non-standard) The node document to associate with the new comment * @param data The data for the new comment */ - constructor (document: Document, data: string = '') { + constructor(document: Document, data: string = '') { super(document, data); } @@ -35,7 +35,7 @@ export default class Comment extends CharacterData { * * @return A shallow copy of the context object */ - public _copy (document: Document): Comment { + public _copy(document: Document): Comment { // Set copy’s data, to that of node. return new Comment(document, this.data); } diff --git a/src/DOMImplementation.ts b/src/DOMImplementation.ts index 8295e53..d863bb3 100644 --- a/src/DOMImplementation.ts +++ b/src/DOMImplementation.ts @@ -18,7 +18,7 @@ export default class DOMImplementation { * * @param document The document to associate with this instance */ - constructor (document: Document) { + constructor(document: Document) { this._document = document; } @@ -31,7 +31,7 @@ export default class DOMImplementation { * * @return The new doctype node */ - createDocumentType (qualifiedName: string, publicId: string, systemId: string): DocumentType { + createDocumentType(qualifiedName: string, publicId: string, systemId: string): DocumentType { // 1. Validate qualifiedName. validateQualifiedName(qualifiedName); @@ -50,7 +50,11 @@ export default class DOMImplementation { * * @return The new XMLDocument */ - createDocument (namespace: string | null, qualifiedName: string | null, doctype: DocumentType | null = null): XMLDocument { + createDocument( + namespace: string | null, + qualifiedName: string | null, + doctype: DocumentType | null = null + ): XMLDocument { expectArity(arguments, 2); namespace = asNullableString(namespace); // [TreatNullAs=EmptyString] for qualifiedName @@ -99,7 +103,7 @@ export default class DOMImplementation { * * @return The new document */ - createHTMLDocument (title?: string | null): Document { + createHTMLDocument(title?: string | null): Document { title = asNullableString(title); // 1. Let doc be a new document that is an HTML document. diff --git a/src/Document.ts b/src/Document.ts index b266e17..15a4456 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -25,25 +25,25 @@ import { asNullableString } from './util/typeHelpers'; export default class Document extends Node implements NonElementParentNode, ParentNode { // Node - public get nodeType (): number { + public get nodeType(): number { return NodeType.DOCUMENT_NODE; } - public get nodeName (): string { + public get nodeName(): string { return '#document'; } - public get nodeValue (): string | null { + public get nodeValue(): string | null { return null; } - public set nodeValue (newValue: string | null) { + public set nodeValue(newValue: string | null) { // Do nothing. } // ParentNode - public get children (): Element[] { + public get children(): Element[] { return getChildren(this); } @@ -85,7 +85,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @return The new element */ - public createElement (localName: string): Element { + public createElement(localName: string): Element { localName = String(localName); // 1. If localName does not match the Name production, then throw an InvalidCharacterError. @@ -123,7 +123,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @return The new element */ - public createElementNS (namespace: string | null, qualifiedName: string): Element { + public createElementNS(namespace: string | null, qualifiedName: string): Element { namespace = asNullableString(namespace); qualifiedName = String(qualifiedName); @@ -137,7 +137,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @return The new document fragment */ - public createDocumentFragment (): DocumentFragment { + public createDocumentFragment(): DocumentFragment { return new DocumentFragment(this); } @@ -148,7 +148,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @return The new text node */ - public createTextNode (data: string): Text { + public createTextNode(data: string): Text { data = String(data); return new Text(this, data); @@ -161,7 +161,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @return The new CDATA section */ - public createCDATASection (data: string): CDATASection { + public createCDATASection(data: string): CDATASection { data = String(data); // 1. If context object is an HTML document, then throw a NotSupportedError. @@ -183,7 +183,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @return The new comment node */ - public createComment (data: string): Comment { + public createComment(data: string): Comment { data = String(data); return new Comment(this, data); @@ -197,7 +197,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @return The new processing instruction */ - public createProcessingInstruction (target: string, data: string): ProcessingInstruction { + public createProcessingInstruction(target: string, data: string): ProcessingInstruction { target = String(target); data = String(data); @@ -225,7 +225,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * @param node The node to import * @param deep Whether to also import node's children */ - public importNode (node: Node, deep: boolean = false): Node { + public importNode(node: Node, deep: boolean = false): Node { // 1. If node is a document or shadow root, then throw a NotSupportedError. if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { throwNotSupportedError('importing a Document node is not supported'); @@ -241,7 +241,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @param node The node to adopt */ - public adoptNode (node: Node): Node { + public adoptNode(node: Node): Node { // 1. If node is a document, then throw a NotSupportedError. if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { throwNotSupportedError('adopting a Document node is not supported'); @@ -264,7 +264,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @return The new attribute node */ - public createAttribute (localName: string): Attr { + public createAttribute(localName: string): Attr { localName = String(localName); // 1. If localName does not match the Name production in XML, then throw an InvalidCharacterError. @@ -287,7 +287,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @return The new attribute node */ - public createAttributeNS (namespace: string | null, qualifiedName: string): Attr { + public createAttributeNS(namespace: string | null, qualifiedName: string): Attr { namespace = asNullableString(namespace); qualifiedName = String(qualifiedName); @@ -308,7 +308,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @return The new Range */ - public createRange (): Range { + public createRange(): Range { return new Range(this); } @@ -319,7 +319,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * * @return A shallow copy of the context object */ - public _copy (document: Document): Document { + public _copy(document: Document): Document { // Set copy’s encoding, content type, URL, origin, type, and mode, to those of node. // (properties not implemented) diff --git a/src/DocumentFragment.ts b/src/DocumentFragment.ts index dbe6f06..a74d380 100644 --- a/src/DocumentFragment.ts +++ b/src/DocumentFragment.ts @@ -7,26 +7,25 @@ import { NodeType } from './util/NodeType'; export default class DocumentFragment extends Node implements NonElementParentNode, ParentNode { // Node - public get nodeType (): number { + public get nodeType(): number { return NodeType.DOCUMENT_FRAGMENT_NODE; } - public get nodeName (): string { + public get nodeName(): string { return '#document-fragment'; } - public get nodeValue (): string | null { + public get nodeValue(): string | null { return null; } - public set nodeValue (newValue: string | null) { + public set nodeValue(newValue: string | null) { // Do nothing. } - // ParentNode - public get children (): Element[] { + public get children(): Element[] { return getChildren(this); } @@ -42,7 +41,7 @@ export default class DocumentFragment extends Node implements NonElementParentNo * * @param document (non-standard) The node document to associate with the new document fragment */ - constructor (document: Document) { + constructor(document: Document) { super(document); } @@ -53,7 +52,7 @@ export default class DocumentFragment extends Node implements NonElementParentNo * * @return A shallow copy of the context object */ - public _copy (document: Document): DocumentFragment { + public _copy(document: Document): DocumentFragment { return new DocumentFragment(document); } } diff --git a/src/DocumentType.ts b/src/DocumentType.ts index c5b31e6..459877d 100644 --- a/src/DocumentType.ts +++ b/src/DocumentType.ts @@ -6,19 +6,19 @@ import { NodeType } from './util/NodeType'; export default class DocumentType extends Node implements ChildNode { // Node - public get nodeType (): number { + public get nodeType(): number { return NodeType.DOCUMENT_TYPE_NODE; } - public get nodeName (): string { + public get nodeName(): string { return this.name; } - public get nodeValue (): string | null { + public get nodeValue(): string | null { return null; } - public set nodeValue (newValue: string | null) { + public set nodeValue(newValue: string | null) { // Do nothing. } @@ -46,7 +46,7 @@ export default class DocumentType extends Node implements ChildNode { * @param publicId The public ID of the doctype * @param systemId The system ID of the doctype */ - constructor (document: Document, name: string, publicId: string = '', systemId: string = '') { + constructor(document: Document, name: string, publicId: string = '', systemId: string = '') { super(document); this.name = name; @@ -61,7 +61,7 @@ export default class DocumentType extends Node implements ChildNode { * * @return A shallow copy of the context object */ - public _copy (document: Document): DocumentType { + public _copy(document: Document): DocumentType { // Set copy’s name, public ID, and system ID, to those of node. return new DocumentType(document, this.name, this.publicId, this.systemId); } diff --git a/src/Element.ts b/src/Element.ts index 6fc4701..5031098 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -16,25 +16,25 @@ import { asNullableString } from './util/typeHelpers'; export default class Element extends Node implements ParentNode, NonDocumentTypeChildNode, ChildNode { // Node - public get nodeType (): number { + public get nodeType(): number { return NodeType.ELEMENT_NODE; } - public get nodeName (): string { + public get nodeName(): string { return this.tagName; } - public get nodeValue (): string | null { + public get nodeValue(): string | null { return null; } - public set nodeValue (newValue: string | null) { + public set nodeValue(newValue: string | null) { // Do nothing. } // ParentNode - public get children (): Element[] { + public get children(): Element[] { return getChildren(this); } @@ -44,11 +44,11 @@ export default class Element extends Node implements ParentNode, NonDocumentType // NonDocumentTypeChildNode - public get previousElementSibling (): Element | null { + public get previousElementSibling(): Element | null { return getPreviousElementSibling(this); } - public get nextElementSibling (): Element | null { + public get nextElementSibling(): Element | null { return getNextElementSibling(this); } @@ -67,7 +67,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param prefix Prefix for the element * @param localName Local name for the element */ - constructor (document: Document, namespace: string | null, prefix: string | null, localName: string) { + constructor(document: Document, namespace: string | null, prefix: string | null, localName: string) { super(document); this.namespaceURI = namespace; this.prefix = prefix; @@ -80,7 +80,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @return True if the element has attributes, otherwise false */ - public hasAttributes (): boolean { + public hasAttributes(): boolean { return this.attributes.length > 0; } @@ -98,7 +98,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @return The value of the attribute, or null if no such attribute exists */ - public getAttribute (qualifiedName: string): string | null { + public getAttribute(qualifiedName: string): string | null { // 1. Let attr be the result of getting an attribute given qualifiedName and the context object. const attr = getAttributeByName(qualifiedName, this); @@ -119,7 +119,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @return The value of the attribute, or null if no such attribute exists */ - public getAttributeNS (namespace: string | null, localName: string): string | null { + public getAttributeNS(namespace: string | null, localName: string): string | null { namespace = asNullableString(namespace); // 1. Let attr be the result of getting an attribute given namespace, localName, and the context object. @@ -140,7 +140,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param qualifiedName The qualified name of the attribute * @param value The new value for the attribute */ - public setAttribute (qualifiedName: string, value: string): void { + public setAttribute(qualifiedName: string, value: string): void { // 1. If qualifiedName does not match the Name production in XML, then throw an InvalidCharacterError. if (!matchesNameProduction(qualifiedName)) { throwInvalidCharacterError('The qualified name does not match the Name production'); @@ -173,7 +173,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param qualifiedName The qualified name of the attribute * @param value The value for the attribute */ - public setAttributeNS (namespace: string | null, qualifiedName: string, value: string): void { + public setAttributeNS(namespace: string | null, qualifiedName: string, value: string): void { namespace = asNullableString(namespace); // 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName to validate and @@ -189,7 +189,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @param qualifiedName The qualified name of the attribute */ - public removeAttribute (qualifiedName: string): void { + public removeAttribute(qualifiedName: string): void { removeAttributeByName(qualifiedName, this); } @@ -199,7 +199,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param namespace The namespace of the attribute * @param localName The local name of the attribute */ - public removeAttributeNS (namespace: string | null, localName: string): void { + public removeAttributeNS(namespace: string | null, localName: string): void { namespace = asNullableString(namespace); removeAttributeByNamespaceAndLocalName(namespace, localName, this); @@ -210,7 +210,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @param qualifiedName The qualified name of the attribute */ - public hasAttribute (qualifiedName: string): boolean { + public hasAttribute(qualifiedName: string): boolean { // 1. If the context object is in the HTML namespace and its node document is an HTML document, then set // qualifiedName to qualifiedName in ASCII lowercase. // (html documents not implemented) @@ -226,7 +226,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param namespace The namespace of the attribute * @param localName The local name of the attribute */ - public hasAttributeNS (namespace: string | null, localName: string): boolean { + public hasAttributeNS(namespace: string | null, localName: string): boolean { namespace = asNullableString(namespace); // 1. If namespace is the empty string, set it to null. @@ -243,7 +243,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @return The attribute, or null if no such attribute exists */ - public getAttributeNode (qualifiedName: string): Attr | null { + public getAttributeNode(qualifiedName: string): Attr | null { return getAttributeByName(qualifiedName, this); } @@ -255,7 +255,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @return The attribute, or null if no such attribute exists */ - public getAttributeNodeNS (namespace: string | null, localName: string): Attr | null { + public getAttributeNodeNS(namespace: string | null, localName: string): Attr | null { namespace = asNullableString(namespace); return getAttributeByNamespaceAndLocalName(namespace, localName, this); @@ -268,7 +268,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @return The previous attribute node for the attribute */ - public setAttributeNode (attr: Attr): Attr | null { + public setAttributeNode(attr: Attr): Attr | null { return setAttribute(attr, this); } @@ -279,7 +279,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @return The previous attribute node for the attribute */ - public setAttributeNodeNS (attr: Attr): Attr | null { + public setAttributeNodeNS(attr: Attr): Attr | null { return setAttribute(attr, this); } @@ -290,10 +290,10 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @return The removed attribute node */ - public removeAttributeNode (attr: Attr): Attr { + public removeAttributeNode(attr: Attr): Attr { // 1. If context object’s attribute list does not contain attr, then throw a NotFoundError. if (this.attributes.indexOf(attr) < 0) { - throwNotFoundError('the specified attribute does not exist') + throwNotFoundError('the specified attribute does not exist'); } // 2. Remove attr from context object. @@ -311,7 +311,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @return A shallow copy of the node */ - public _copy (document: Document): Element { + public _copy(document: Document): Element { // 2.1. Let copy be the result of creating an element, given document, node’s local name, node’s namespace, // node’s namespace prefix, and the value of node’s is attribute if present (or null if not). The synchronous // custom elements flag should be unset. @@ -341,7 +341,12 @@ export default class Element extends Node implements ParentNode, NonDocumentType * * @return The new element */ -export function createElement (document: Document, localName: string, namespace: string | null, prefix: string | null = null): Element { +export function createElement( + document: Document, + localName: string, + namespace: string | null, + prefix: string | null = null +): Element { // 1. If prefix was not given, let prefix be null. // (handled by default) @@ -423,7 +428,7 @@ export function createElement (document: Document, localName: string, namespace: * * @return The first matching attribute, or null otherwise */ -function getAttributeByName (qualifiedName: string, element: Element): Attr | null { +function getAttributeByName(qualifiedName: string, element: Element): Attr | null { // 1. If element is in the HTML namespace and its node document is an HTML document, then set qualifiedName to // qualifiedName in ASCII lowercase. // (html documents not implemented) @@ -442,7 +447,11 @@ function getAttributeByName (qualifiedName: string, element: Element): Attr | nu * * @return The first matching attribute, or null otherwise */ -function getAttributeByNamespaceAndLocalName (namespace: string | null, localName: string, element: Element): Attr | null { +function getAttributeByNamespaceAndLocalName( + namespace: string | null, + localName: string, + element: Element +): Attr | null { // 1. If namespace is the empty string, set it to null. if (namespace === '') { namespace = null; @@ -463,7 +472,7 @@ function getAttributeByNamespaceAndLocalName (namespace: string | null, localNam * * @return The value of the first matching attribute, or the empty string if no such attribute exists */ -function getAttributeValue (element: Element, localName: string, namespace: string | null = null): string { +function getAttributeValue(element: Element, localName: string, namespace: string | null = null): string { // 1. Let attr be the result of getting an attribute given namespace, localName, and element. const attr = getAttributeByNamespaceAndLocalName(namespace, localName, element); @@ -484,7 +493,7 @@ function getAttributeValue (element: Element, localName: string, namespace: stri * * @return The previous attribute with attr's namespace and local name, or null if there was no such attribute */ -function setAttribute (attr: Attr, element: Element): Attr | null { +function setAttribute(attr: Attr, element: Element): Attr | null { // 1. If attr’s element is neither null nor element, throw an InUseAttributeError. if (attr.ownerElement !== null && attr.ownerElement !== element) { throwInUseAttributeError('attribute is in use by another element'); @@ -501,9 +510,8 @@ function setAttribute (attr: Attr, element: Element): Attr | null { // 4. If oldAttr is non-null, replace it by attr in element. if (oldAttr !== null) { replaceAttribute(oldAttr, attr, element); - } - // 5. Otherwise, append attr to element. - else { + } else { + // 5. Otherwise, append attr to element. appendAttribute(attr, element); } @@ -521,7 +529,13 @@ function setAttribute (attr: Attr, element: Element): Attr | null { * @param prefix Prefix of the attribute * @param namespace Namespace of the attribute */ -function setAttributeValue (element: Element, localName: string, value: string, prefix: string | null = null, namespace: string | null = null): void { +function setAttributeValue( + element: Element, + localName: string, + value: string, + prefix: string | null = null, + namespace: string | null = null +): void { // 1. If prefix is not given, set it to null. // 2. If namespace is not given, set it to null. // (handled by default values) @@ -550,7 +564,7 @@ function setAttributeValue (element: Element, localName: string, value: string, * * @return The removed attribute, or null if no matching attribute exists */ -function removeAttributeByName (qualifiedName: string, element: Element): Attr | null { +function removeAttributeByName(qualifiedName: string, element: Element): Attr | null { // 1. Let attr be the result of getting an attribute given qualifiedName and element. const attr = getAttributeByName(qualifiedName, element); @@ -573,7 +587,11 @@ function removeAttributeByName (qualifiedName: string, element: Element): Attr | * * @return The removed attribute, or null if no matching attribute exists */ -function removeAttributeByNamespaceAndLocalName (namespace: string | null, localName: string, element: Element): Attr | null { +function removeAttributeByNamespaceAndLocalName( + namespace: string | null, + localName: string, + element: Element +): Attr | null { // 1. Let attr be the result of getting an attribute given namespace, localName, and element. const attr = getAttributeByNamespaceAndLocalName(namespace, localName, element); diff --git a/src/Node.ts b/src/Node.ts index cffe5d7..ffb0af4 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -30,12 +30,12 @@ export default abstract class Node { /** * Returns the type of node, represented by a number. */ - public abstract get nodeType (): number; + public abstract get nodeType(): number; /** * Returns a string appropriate for the type of node. */ - public abstract get nodeName (): string; + public abstract get nodeName(): string; /** * A reference to the Document node in which the current node resides. @@ -50,14 +50,14 @@ export default abstract class Node { /** * The parent if it is an element, or null otherwise. */ - public get parentElement (): Element | null { + public get parentElement(): Element | null { return this.parentNode && isNodeOfType(this.parentNode, NodeType.ELEMENT_NODE) ? this.parentNode as Element : null; } /** * Returns true if the context object has children, and false otherwise. */ - public hasChildNodes (): boolean { + public hasChildNodes(): boolean { return !!this.childNodes.length; } @@ -91,8 +91,8 @@ export default abstract class Node { /** * The value of the node. */ - public abstract get nodeValue (): string | null; - public abstract set nodeValue (value: string | null); + public abstract get nodeValue(): string | null; + public abstract set nodeValue(value: string | null); /** * (non-standard) Each node has an associated list of registered observers. @@ -104,7 +104,7 @@ export default abstract class Node { * * @param document The node document to associate with the node */ - constructor (document: Document | null) { + constructor(document: Document | null) { this.ownerDocument = document; } @@ -112,7 +112,7 @@ export default abstract class Node { * Puts the specified node and all of its subtree into a "normalized" form. In a normalized subtree, no text nodes * in the subtree are empty and there are no adjacent text nodes. */ - public normalize (): void { + public normalize(): void { // for each descendant exclusive Text node node of context object: let node = this.firstChild; let index = 0; @@ -142,7 +142,8 @@ export default abstract class Node { // itself), in tree order. let data = ''; const siblingsToRemove = []; - for (let sibling = textNode.nextSibling; + for ( + let sibling = textNode.nextSibling; sibling && isNodeOfType(sibling, NodeType.TEXT_NODE); sibling = sibling.nextSibling ) { @@ -216,7 +217,7 @@ export default abstract class Node { * * @return A copy of the current node */ - public cloneNode (deep: boolean = false): Node { + public cloneNode(deep: boolean = false): Node { return cloneNode(this, deep); } @@ -228,7 +229,7 @@ export default abstract class Node { * * @return Whether childNode is an inclusive descendant of the current node */ - public contains (other: Node | null): boolean { + public contains(other: Node | null): boolean { while (other && other != this) { other = other.parentNode; } @@ -245,7 +246,7 @@ export default abstract class Node { * * @return The node that was inserted */ - public insertBefore (node: Node, child: Node | null): Node { + public insertBefore(node: Node, child: Node | null): Node { expectArity(arguments, 2); node = asObject(node, Node); child = asNullableObject(child, Node); @@ -262,7 +263,7 @@ export default abstract class Node { * * @return The node that was inserted */ - public appendChild (node: Node): Node { + public appendChild(node: Node): Node { expectArity(arguments, 1); node = asObject(node, Node); @@ -277,7 +278,7 @@ export default abstract class Node { * * @return The node that was removed */ - public replaceChild (node: Node, child: Node): Node { + public replaceChild(node: Node, child: Node): Node { expectArity(arguments, 2); node = asObject(node, Node); child = asObject(child, Node); @@ -292,7 +293,7 @@ export default abstract class Node { * * @return The node that was removed */ - public removeChild (child: Node): Node { + public removeChild(child: Node): Node { expectArity(arguments, 1); child = asObject(child, Node); @@ -306,7 +307,7 @@ export default abstract class Node { * * @return A shallow copy of the context object */ - public abstract _copy (document: Document): Node; + public abstract _copy(document: Document): Node; } (Node.prototype as any).ELEMENT_NODE = NodeType.ELEMENT_NODE; diff --git a/src/ProcessingInstruction.ts b/src/ProcessingInstruction.ts index bcbd4b7..c7b80a1 100644 --- a/src/ProcessingInstruction.ts +++ b/src/ProcessingInstruction.ts @@ -8,11 +8,11 @@ import { NodeType } from './util/NodeType'; export default class ProcessingInstruction extends CharacterData { // Node - public get nodeType (): number { + public get nodeType(): number { return NodeType.PROCESSING_INSTRUCTION_NODE; } - public get nodeName (): string { + public get nodeName(): string { return this.target; } @@ -26,7 +26,7 @@ export default class ProcessingInstruction extends CharacterData { * @param document The node document to associate with the processing instruction * @param target The target of the processing instruction */ - constructor (document: Document, target: string, data: string) { + constructor(document: Document, target: string, data: string) { super(document, data); this.target = target; } @@ -38,7 +38,7 @@ export default class ProcessingInstruction extends CharacterData { * * @return A shallow copy of the context object */ - public _copy (document: Document): ProcessingInstruction { + public _copy(document: Document): ProcessingInstruction { // Set copy’s target and data to those of node. return new ProcessingInstruction(document, this.target, this.data); } diff --git a/src/Range.ts b/src/Range.ts index 3ee8590..72925ee 100644 --- a/src/Range.ts +++ b/src/Range.ts @@ -1,8 +1,19 @@ import Document from './Document'; import Node from './Node'; -import { throwIndexSizeError, throwInvalidNodeTypeError, throwNotSupportedError, throwWrongDocumentError } from './util/errorHelpers'; +import { + throwIndexSizeError, + throwInvalidNodeTypeError, + throwNotSupportedError, + throwWrongDocumentError +} from './util/errorHelpers'; import { NodeType, isNodeOfType } from './util/NodeType'; -import { determineLengthOfNode, getInclusiveAncestors, getNodeDocument, getNodeIndex, getRootOfNode } from './util/treeHelpers'; +import { + determineLengthOfNode, + getInclusiveAncestors, + getNodeDocument, + getNodeIndex, + getRootOfNode +} from './util/treeHelpers'; import { asObject, asUnsignedLong } from './util/typeHelpers'; export const ranges: Range[] = []; @@ -16,7 +27,7 @@ export default class Range { public endContainer: Node; public endOffset: number; - public get collapsed (): boolean { + public get collapsed(): boolean { return this.startContainer === this.endContainer && this.startOffset === this.endOffset; } @@ -25,7 +36,7 @@ export default class Range { * * Note: for efficiency reasons, this implementation deviates from the algorithm given in 4.2. */ - public get commonAncestorContainer (): Node { + public get commonAncestorContainer(): Node { const ancestors1 = getInclusiveAncestors(this.startContainer); const ancestors2 = getInclusiveAncestors(this.endContainer); let commonAncestorContainer = ancestors1[0]; @@ -50,7 +61,7 @@ export default class Range { * * @param document The document in which to initialize the Range */ - constructor (document: Document) { + constructor(document: Document) { this.startContainer = document; this.startOffset = 0; this.endContainer = document; @@ -64,7 +75,7 @@ export default class Range { * @param node The new start container * @param offset The new start offset */ - setStart (node: Node, offset: number): void { + setStart(node: Node, offset: number): void { node = asObject(node, Node); offset = asUnsignedLong(offset); @@ -107,7 +118,7 @@ export default class Range { * @param node The new end container * @param offset The new end offset */ - setEnd (node: Node, offset: number): void { + setEnd(node: Node, offset: number): void { node = asObject(node, Node); offset = asUnsignedLong(offset); @@ -149,7 +160,7 @@ export default class Range { * * @param node The node to set the range's start before */ - setStartBefore (node: Node): void { + setStartBefore(node: Node): void { node = asObject(node, Node); // 1. Let parent be node’s parent. @@ -169,7 +180,7 @@ export default class Range { * * @param node The node to set the range's start before */ - setStartAfter (node: Node): void { + setStartAfter(node: Node): void { node = asObject(node, Node); // 1. Let parent be node’s parent. @@ -189,7 +200,7 @@ export default class Range { * * @param node The node to set the range's end before */ - setEndBefore (node: Node): void { + setEndBefore(node: Node): void { node = asObject(node, Node); // 1. Let parent be node’s parent. @@ -204,13 +215,12 @@ export default class Range { this.setEnd(parent, getNodeIndex(node)); } - /** * Sets the end boundary point of the range to the position just after the given node. * * @param node The node to set the range's end before */ - setEndAfter (node: Node): void { + setEndAfter(node: Node): void { node = asObject(node, Node); // 1. Let parent be node’s parent. @@ -225,24 +235,22 @@ export default class Range { this.setEnd(parent, getNodeIndex(node) + 1); } - /** * Sets the range's boundary points to the same position. * * @param toStart If true, set both points to the start of the range, otherwise set them to the end */ - collapse (toStart: boolean = false): void { + collapse(toStart: boolean = false): void { if (toStart) { this.endContainer = this.startContainer; this.endOffset = this.startOffset; - } - else { + } else { this.startContainer = this.endContainer; this.startOffset = this.endOffset; } } - selectNode (node: Node): void { + selectNode(node: Node): void { node = asObject(node, Node); // 1. Let parent be node’s parent. @@ -265,7 +273,7 @@ export default class Range { this.endOffset = index + 1; } - selectNodeContents (node: Node): void { + selectNodeContents(node: Node): void { node = asObject(node, Node); // 1. If node is a doctype, throw an InvalidNodeTypeError. @@ -290,7 +298,7 @@ export default class Range { static END_TO_END = 2; static END_TO_START = 3; - compareBoundaryPoints (how: number, sourceRange: Range): number { + compareBoundaryPoints(how: number, sourceRange: Range): number { sourceRange = asObject(sourceRange, Range); // 1. If how is not one of START_TO_START, START_TO_END, END_TO_END, and END_TO_START, then throw a @@ -349,7 +357,7 @@ export default class Range { // END_TO_START: default: - // unreachable, fall through for type check + // unreachable, fall through for type check case Range.END_TO_START: // Let this point be the context object’s start. Let other point be sourceRange’s end. return compareBoundaryPointPositions( @@ -374,7 +382,7 @@ export default class Range { * * @return A copy of the context object */ - cloneRange (): Range { + cloneRange(): Range { const range = new Range(getNodeDocument(this.startContainer)); range.startContainer = this.startContainer; range.startOffset = this.startOffset; @@ -390,7 +398,7 @@ export default class Range { * garbage collection to determine when to stop updating a range for node mutations, this implementation requires * calling detach to stop such updates from affecting the range. */ - detach (): void { + detach(): void { const index = ranges.indexOf(this); if (index >= 0) { ranges.splice(index, 1); @@ -406,7 +414,7 @@ export default class Range { * * @return Whether the point is in the range */ - isPointInRange (node: Node, offset: number): boolean { + isPointInRange(node: Node, offset: number): boolean { node = asObject(node, Node); offset = asUnsignedLong(offset); @@ -445,7 +453,7 @@ export default class Range { * * @return -1, 0 or 1 depending on whether the point is before, inside or after the range, respectively */ - comparePoint (node: Node, offset: number): number { + comparePoint(node: Node, offset: number): number { node = asObject(node, Node); offset = asUnsignedLong(offset); @@ -485,7 +493,7 @@ export default class Range { * * @return Whether the range intersects node */ - intersectsNode (node: Node): boolean { + intersectsNode(node: Node): boolean { node = asObject(node, Node); // 1. If node’s root is different from the context object’s root, return false. @@ -506,8 +514,10 @@ export default class Range { // 5. If (parent, offset) is before end and (parent, offset + 1) is after start, return true. // 6. Return false. - return compareBoundaryPointPositions(parent, offset, this.endContainer, this.endOffset) === POSITION_BEFORE && - compareBoundaryPointPositions(parent, offset + 1, this.startContainer, this.startOffset) === POSITION_AFTER; + return ( + compareBoundaryPointPositions(parent, offset, this.endContainer, this.endOffset) === POSITION_BEFORE && + compareBoundaryPointPositions(parent, offset + 1, this.startContainer, this.startOffset) === POSITION_AFTER + ); } } @@ -528,13 +538,13 @@ const POSITION_AFTER = 1; * * @return -1, 0 or 1, depending on the boundary points' relative positions */ -function compareBoundaryPointPositions (nodeA: Node, offsetA: number, nodeB: Node, offsetB: number): number { +function compareBoundaryPointPositions(nodeA: Node, offsetA: number, nodeB: Node, offsetB: number): number { if (nodeA !== nodeB) { const ancestors1 = getInclusiveAncestors(nodeA); const ancestors2 = getInclusiveAncestors(nodeB); // This should not be called on nodes from different trees if (ancestors1[0] !== ancestors2[0]) { - throw new Error('Can not compare positions of nodes from different trees.') + throw new Error('Can not compare positions of nodes from different trees.'); } // Skip common parents @@ -567,6 +577,6 @@ function compareBoundaryPointPositions (nodeA: Node, offsetA: number, nodeB: Nod * * @return The root of range */ -function getRootOfRange (range: Range): Node { +function getRootOfRange(range: Range): Node { return getRootOfNode(range.startContainer); } diff --git a/src/Text.ts b/src/Text.ts index ad9f2db..1099413 100644 --- a/src/Text.ts +++ b/src/Text.ts @@ -12,11 +12,11 @@ import { getNodeIndex } from './util/treeHelpers'; export default class Text extends CharacterData { // Node - public get nodeType (): number { + public get nodeType(): number { return NodeType.TEXT_NODE; } - public get nodeName (): string { + public get nodeName(): string { return '#text'; } @@ -31,7 +31,7 @@ export default class Text extends CharacterData { * @param document (non-standard) The node document for the new node * @param data The data for the new text node */ - constructor (document: Document, data: string = '') { + constructor(document: Document, data: string = '') { super(document, data); } @@ -42,7 +42,7 @@ export default class Text extends CharacterData { * * @return a text node containing the second half of the split node's data */ - public splitText (offset: number): Text { + public splitText(offset: number): Text { return splitText(this, offset); } @@ -53,7 +53,7 @@ export default class Text extends CharacterData { * * @return A shallow copy of the context object */ - public _copy (document: Document): Text { + public _copy(document: Document): Text { // Set copy’s data, to that of node. return new Text(document, this.data); } @@ -67,13 +67,13 @@ export default class Text extends CharacterData { * * @return a text node containing the second half of the split node's data */ -function splitText (node: Text, offset: number): Text { +function splitText(node: Text, offset: number): Text { // 1. Let length be node’s length. const length = node.length; // 2. If offset is greater than length, then throw an IndexSizeError. if (offset > length) { - throwIndexSizeError('can not split past the node\'s length'); + throwIndexSizeError("can not split past the node's length"); } // 3. Let count be length minus offset. @@ -120,7 +120,7 @@ function splitText (node: Text, offset: number): Text { if (range.endContainer === parent && range.endOffset === indexOfNodePlusOne) { range.endOffset += 1; } - }) + }); } // 8. Replace data with node node, offset offset, count count, and data the empty string. diff --git a/src/XMLDocument.ts b/src/XMLDocument.ts index 058af97..d7e5962 100644 --- a/src/XMLDocument.ts +++ b/src/XMLDocument.ts @@ -8,7 +8,7 @@ export default class XMLDocument extends Document { * * @return A shallow copy of the context object */ - public _copy (document: Document): XMLDocument { + public _copy(document: Document): XMLDocument { // Set copy’s encoding, content type, URL, origin, type, and mode, to those of node. // (properties not implemented) diff --git a/src/index.ts b/src/index.ts index 3950cef..40df935 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,6 @@ export { default as Text } from './Text'; export { default as XMLDocument } from './XMLDocument'; export { default as MutationObserver } from './mutation-observer/MutationObserver'; -export function createDocument (): XMLDocument { +export function createDocument(): XMLDocument { return new XMLDocument(); } diff --git a/src/mixins.ts b/src/mixins.ts index 563bc45..95d3935 100644 --- a/src/mixins.ts +++ b/src/mixins.ts @@ -9,8 +9,7 @@ import { NodeType, isNodeOfType } from './util/NodeType'; /** * 3.2.4. Mixin NonElementParentNode */ -export interface NonElementParentNode { -} +export interface NonElementParentNode {} // Document implements NonElementParentNode; // DocumentFragment implements NonElementParentNode; @@ -28,7 +27,7 @@ export interface ParentNode { // DocumentFragment implements ParentNode; // Element implements ParentNode; -export function asParentNode (node: Node): ParentNode | null { +export function asParentNode(node: Node): ParentNode | null { if (isNodeOfType(node, NodeType.ELEMENT_NODE, NodeType.DOCUMENT_NODE, NodeType.DOCUMENT_FRAGMENT_NODE)) { return node as Element | Document | DocumentFragment; } @@ -46,7 +45,7 @@ export function asParentNode (node: Node): ParentNode | null { * * @return The */ -export function getChildren (node: ParentNode): Element[] { +export function getChildren(node: ParentNode): Element[] { const elements: Element[] = []; for (let child = node.firstElementChild; child; child = child.nextElementSibling) { elements.push(child); @@ -64,22 +63,24 @@ export interface NonDocumentTypeChildNode { // Element implements NonDocumentTypeChildNode; // CharacterData implements NonDocumentTypeChildNode; -export function asNonDocumentTypeChildNode (node: Node): NonDocumentTypeChildNode | null { - if (isNodeOfType( - node, - NodeType.ELEMENT_NODE, - NodeType.COMMENT_NODE, - NodeType.PROCESSING_INSTRUCTION_NODE, - NodeType.TEXT_NODE, - NodeType.CDATA_SECTION_NODE - )) { +export function asNonDocumentTypeChildNode(node: Node): NonDocumentTypeChildNode | null { + if ( + isNodeOfType( + node, + NodeType.ELEMENT_NODE, + NodeType.COMMENT_NODE, + NodeType.PROCESSING_INSTRUCTION_NODE, + NodeType.TEXT_NODE, + NodeType.CDATA_SECTION_NODE + ) + ) { return node as Element | CharacterData; } return null; } -export function getPreviousElementSibling (node: Node): Element | null { +export function getPreviousElementSibling(node: Node): Element | null { for (let sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) { if (isNodeOfType(sibling, NodeType.ELEMENT_NODE)) { return sibling as Element; @@ -89,7 +90,7 @@ export function getPreviousElementSibling (node: Node): Element | null { return null; } -export function getNextElementSibling (node: Node): Element | null { +export function getNextElementSibling(node: Node): Element | null { for (let sibling = node.nextSibling; sibling; sibling = sibling.nextSibling) { if (isNodeOfType(sibling, NodeType.ELEMENT_NODE)) { return sibling as Element; @@ -102,8 +103,7 @@ export function getNextElementSibling (node: Node): Element | null { /** * 3.2.8. Mixin ChildNode */ -export interface ChildNode { -} +export interface ChildNode {} // DocumentType implements ChildNode; // Element implements ChildNode; // CharacterData implements ChildNode; diff --git a/src/mutation-observer/MutationObserver.ts b/src/mutation-observer/MutationObserver.ts index 809cf3d..88f97f0 100644 --- a/src/mutation-observer/MutationObserver.ts +++ b/src/mutation-observer/MutationObserver.ts @@ -78,7 +78,7 @@ export default class MutationObserver { * * @param callback Function called after mutations have been observed. */ - constructor (callback: MutationCallback) { + constructor(callback: MutationCallback) { // create a new MutationObserver object with callback set to callback this._callback = callback; @@ -99,7 +99,7 @@ export default class MutationObserver { * @param target Node (or root of subtree) to observe * @param options Determines which types of mutations to observe */ - observe (target: Node, options: MutationObserverInit) { + observe(target: Node, options: MutationObserverInit) { // Defaults from IDL options.childList = !!options.childList; options.subtree = !!options.subtree; @@ -136,7 +136,7 @@ export default class MutationObserver { if (options.characterDataOldValue && !options.characterData) { throw new TypeError( 'The options object may only set "characterDataOldValue" to true when "characterData" is true or not ' + - 'present.' + 'present.' ); } @@ -154,7 +154,7 @@ export default class MutationObserver { * Stops the MutationObserver instance from receiving notifications of DOM mutations. Until the observe() method * is used again, observer's callback will not be invoked. */ - disconnect () { + disconnect() { // for each node node in context object’s list of nodes, remove any registered observer on node for which // context object is the observer, this._nodes.forEach(node => node._registeredObservers.removeForObserver(this)); @@ -169,7 +169,7 @@ export default class MutationObserver { * * @return An Array of MutationRecord objects that were recorded. */ - takeRecords (): MutationRecord[] { + takeRecords(): MutationRecord[] { // return a copy of the record queue const recordQueue = this._recordQueue.concat(); // and then empty the record queue diff --git a/src/mutation-observer/MutationRecord.ts b/src/mutation-observer/MutationRecord.ts index e099757..b421263 100644 --- a/src/mutation-observer/MutationRecord.ts +++ b/src/mutation-observer/MutationRecord.ts @@ -1,13 +1,13 @@ import Node from '../Node'; export interface MutationRecordInit { - name?: string, - namespace?: string | null, - oldValue?: string | null, - addedNodes?: Node[], - removedNodes?: Node[], - previousSibling?: Node | null, - nextSibling?: Node | null + name?: string; + namespace?: string | null; + oldValue?: string | null; + addedNodes?: Node[]; + removedNodes?: Node[]; + previousSibling?: Node | null; + nextSibling?: Node | null; } /** @@ -75,7 +75,7 @@ export default class MutationRecord { * @param type The value for the type property * @param target The value for the target property */ - constructor (type: string, target: Node) { + constructor(type: string, target: Node) { this.type = type; this.target = target; } diff --git a/src/mutation-observer/NotifyList.ts b/src/mutation-observer/NotifyList.ts index fd5cbe4..aeebaf9 100644 --- a/src/mutation-observer/NotifyList.ts +++ b/src/mutation-observer/NotifyList.ts @@ -3,12 +3,12 @@ import MutationRecord from './MutationRecord'; import { removeTransientRegisteredObserversForObserver } from './RegisteredObservers'; // Declare functions without having to bring in the entire DOM lib -declare function setImmediate (handler: (...args: any[]) => void): number; -declare function setTimeout (handler: (...args: any[]) => void, timeout: number): number; +declare function setImmediate(handler: (...args: any[]) => void): number +declare function setTimeout(handler: (...args: any[]) => void, timeout: number): number -const hasSetImmediate = (typeof setImmediate === 'function'); +const hasSetImmediate = typeof setImmediate === 'function'; -function queueCompoundMicrotask (callback: (...args: any[]) => void, thisArg: NotifyList, ...args: any[]): number { +function queueCompoundMicrotask(callback: (...args: any[]) => void, thisArg: NotifyList, ...args: any[]): number { return (hasSetImmediate ? setImmediate : setTimeout)(() => { callback.apply(thisArg, args); }, 0); @@ -29,7 +29,7 @@ export default class NotifyList { * @param observer The observer for which to enqueue the record * @param record The record to enqueue */ - appendRecord (observer: MutationObserver, record: MutationRecord) { + appendRecord(observer: MutationObserver, record: MutationRecord) { observer._recordQueue.push(record); this._notifyList.push(observer); } @@ -37,7 +37,7 @@ export default class NotifyList { /** * To queue a mutation observer compound microtask, run these steps: */ - public queueMutationObserverCompoundMicrotask () { + public queueMutationObserverCompoundMicrotask() { // 1. If mutation observer compound microtask queued flag is set, then return. if (this._compoundMicrotaskQueued) { return; @@ -53,7 +53,7 @@ export default class NotifyList { /** * To notify mutation observers, run these steps: */ - private _notifyMutationObservers () { + private _notifyMutationObservers() { // 1. Unset mutation observer compound microtask queued flag. this._compoundMicrotaskQueued = null; @@ -70,21 +70,24 @@ export default class NotifyList { // 5. For each MutationObserver object mo in notify list, execute a compound microtask subtask to run these // steps: [HTML] notifyList.forEach(mo => { - queueCompoundMicrotask((mo: MutationObserver) => { - // 5.1. Let queue be a copy of mo’s record queue. - // 5.2. Empty mo’s record queue. - const queue = mo.takeRecords(); + queueCompoundMicrotask( + (mo: MutationObserver) => { + // 5.1. Let queue be a copy of mo’s record queue. + // 5.2. Empty mo’s record queue. + const queue = mo.takeRecords(); - // 5.3. Remove all transient registered observers whose observer is mo. - removeTransientRegisteredObserversForObserver(mo); + // 5.3. Remove all transient registered observers whose observer is mo. + removeTransientRegisteredObserversForObserver(mo); - // 5.4. If queue is non-empty, invoke mo’s callback with a list of arguments consisting of queue and mo, - // and mo as the callback this value. If this throws an exception, report the exception. - if (queue.length) { - mo._callback(queue, mo); - } - - }, this, mo); + // 5.4. If queue is non-empty, invoke mo’s callback with a list of arguments consisting of queue and mo, + // and mo as the callback this value. If this throws an exception, report the exception. + if (queue.length) { + mo._callback(queue, mo); + } + }, + this, + mo + ); }); // 6. For each slot slot in signalList, in order, fire an event named slotchange, with its bubbles diff --git a/src/mutation-observer/RegisteredObserver.ts b/src/mutation-observer/RegisteredObserver.ts index 1cac4b0..1c87216 100644 --- a/src/mutation-observer/RegisteredObserver.ts +++ b/src/mutation-observer/RegisteredObserver.ts @@ -37,7 +37,7 @@ export default class RegisteredObserver { * @param options Options for the registration * @param source If not null, creates a transient registered observer for the given registered observer */ - constructor (observer: MutationObserver, node: Node, options: MutationObserverInit, source?: RegisteredObserver) { + constructor(observer: MutationObserver, node: Node, options: MutationObserverInit, source?: RegisteredObserver) { this.observer = observer; this.node = node; this.options = options; @@ -58,7 +58,13 @@ export default class RegisteredObserver { * @param interestedObservers Array of mutation observer objects to append to * @param pairedStrings Paired strings for the mutation observer objects */ - public collectInterestedObservers (type: string, target: Node, data: MutationRecordInit, interestedObservers: MutationObserver[], pairedStrings: (string | null | undefined)[]) { + public collectInterestedObservers( + type: string, + target: Node, + data: MutationRecordInit, + interestedObservers: MutationObserver[], + pairedStrings: (string | null | undefined)[] + ) { // (continued from RegisteredObservers#queueMutationRecord) // 3.1. If none of the following are true diff --git a/src/mutation-observer/RegisteredObservers.ts b/src/mutation-observer/RegisteredObservers.ts index a67b41e..1cbf1b4 100644 --- a/src/mutation-observer/RegisteredObservers.ts +++ b/src/mutation-observer/RegisteredObservers.ts @@ -17,7 +17,7 @@ export default class RegisteredObservers { /** * @param node Node for which this instance holds RegisteredObserver instances. */ - constructor (node: Node) { + constructor(node: Node) { this._node = node; } @@ -27,7 +27,7 @@ export default class RegisteredObservers { * @param observer Observer to create a registration for * @param options Options for the registration */ - public register (observer: MutationObserver, options: MutationObserverInit) { + public register(observer: MutationObserver, options: MutationObserverInit) { // (continuing from MutationObserver#observe) // 7. For each registered observer registered in target’s list of registered observers whose observer is the // context object: @@ -66,7 +66,7 @@ export default class RegisteredObservers { * * @param registeredObserver The registered observer to remove */ - public remove (registeredObserver: RegisteredObserver): void { + public remove(registeredObserver: RegisteredObserver): void { const index = this._registeredObservers.indexOf(registeredObserver); if (index >= 0) { this._registeredObservers.splice(index, 1); @@ -81,7 +81,7 @@ export default class RegisteredObservers { * * @param observer Observer for which to remove the registration */ - public removeForObserver (observer: MutationObserver): void { + public removeForObserver(observer: MutationObserver): void { // Filter the array in-place let write = 0; for (let read = 0, l = this._registeredObservers.length; read < l; ++read) { @@ -107,18 +107,18 @@ export default class RegisteredObservers { * @param interestedObservers Array of mutation observer objects to append to * @param pairedStrings Paired strings for the mutation observer objects */ - public collectInterestedObservers (type: string, target: Node, data: MutationRecordInit, interestedObservers: MutationObserver[], pairedStrings: (string | null | undefined)[]) { + public collectInterestedObservers( + type: string, + target: Node, + data: MutationRecordInit, + interestedObservers: MutationObserver[], + pairedStrings: (string | null | undefined)[] + ) { // (continuing from queueMutationRecord) // 3. ...and then for each registered observer (with registered observer’s options as options) in node’s list of // registered observers: this._registeredObservers.forEach(registeredObserver => { - registeredObserver.collectInterestedObservers( - type, - target, - data, - interestedObservers, - pairedStrings - ); + registeredObserver.collectInterestedObservers(type, target, data, interestedObservers, pairedStrings); }); } @@ -127,7 +127,7 @@ export default class RegisteredObservers { * * @param node Node to append the transient registered observers to */ - public appendTransientRegisteredObservers (node: Node): void { + public appendTransientRegisteredObservers(node: Node): void { this._registeredObservers.forEach(registeredObserver => { if (registeredObserver.options.subtree) { node._registeredObservers.registerTransient(registeredObserver); @@ -140,10 +140,8 @@ export default class RegisteredObservers { * * @param source The source registered observer */ - public registerTransient (source: RegisteredObserver): void { - this._registeredObservers.push( - new RegisteredObserver(source.observer, this._node, source.options, source) - ); + public registerTransient(source: RegisteredObserver): void { + this._registeredObservers.push(new RegisteredObserver(source.observer, this._node, source.options, source)); // Note that node is not added to the transient observer's observer's list of nodes. } } @@ -153,7 +151,7 @@ export default class RegisteredObservers { * * @param observer The mutation observer object to remove transient registered observers for */ -export function removeTransientRegisteredObserversForObserver (observer: MutationObserver): void { +export function removeTransientRegisteredObserversForObserver(observer: MutationObserver): void { observer._transients.forEach(transientRegisteredObserver => { transientRegisteredObserver.node._registeredObservers.remove(transientRegisteredObserver); }); @@ -165,7 +163,7 @@ export function removeTransientRegisteredObserversForObserver (observer: Mutatio * * @param source The registered observer to remove transient registered observers for */ -export function removeTransientRegisteredObserversForSource (source: RegisteredObserver): void { +export function removeTransientRegisteredObserversForSource(source: RegisteredObserver): void { for (let i = source.observer._transients.length - 1; i >= 0; --i) { const transientRegisteredObserver = source.observer._transients[i]; if (transientRegisteredObserver.source !== source) { diff --git a/src/mutation-observer/queueMutationRecord.ts b/src/mutation-observer/queueMutationRecord.ts index d67adbd..2cc5ebf 100644 --- a/src/mutation-observer/queueMutationRecord.ts +++ b/src/mutation-observer/queueMutationRecord.ts @@ -13,7 +13,7 @@ import Node from '../Node'; * @param target The target node * @param data The data for the mutation record */ -export default function queueMutationRecord (type: string, target: Node, data: MutationRecordInit) { +export default function queueMutationRecord(type: string, target: Node, data: MutationRecordInit) { // 1. Let interested observers be an initially empty set of MutationObserver objects optionally paired with a // string. const interestedObservers: MutationObserver[] = []; diff --git a/src/util/NodeType.ts b/src/util/NodeType.ts index b7a3c1f..0cc5c36 100644 --- a/src/util/NodeType.ts +++ b/src/util/NodeType.ts @@ -23,6 +23,6 @@ export const enum NodeType { * * @return Whether node.nodeType is one of the specified values */ -export function isNodeOfType (node: Node, ...types: NodeType[]): boolean { +export function isNodeOfType(node: Node, ...types: NodeType[]): boolean { return types.some(t => node.nodeType === t); } diff --git a/src/util/attrMutations.ts b/src/util/attrMutations.ts index 89ee502..aacb385 100644 --- a/src/util/attrMutations.ts +++ b/src/util/attrMutations.ts @@ -9,7 +9,7 @@ import queueMutationRecord from '../mutation-observer/queueMutationRecord'; * @param element The element that has the attribute * @param value The new value for the attribute */ -export function changeAttribute (attribute: Attr, element: Element, value: string): void { +export function changeAttribute(attribute: Attr, element: Element, value: string): void { // 1. Queue a mutation record of "attributes" for element with name attribute’s local name, namespace attribute’s // namespace, and oldValue attribute’s value. queueMutationRecord('attributes', element, { @@ -37,7 +37,7 @@ export function changeAttribute (attribute: Attr, element: Element, value: strin * @param attribute The attribute to append * @param element The element to append attribute to */ -export function appendAttribute (attribute: Attr, element: Element): void { +export function appendAttribute(attribute: Attr, element: Element): void { // 1. Queue a mutation record of "attributes" for element with name attribute’s local name, namespace attribute’s // namespace, and oldValue null. queueMutationRecord('attributes', element, { @@ -68,7 +68,7 @@ export function appendAttribute (attribute: Attr, element: Element): void { * @param attribute The attribute to remove * @param element The element to remove attribute from */ -export function removeAttribute (attribute: Attr, element: Element): void { +export function removeAttribute(attribute: Attr, element: Element): void { // 1. Queue a mutation record of "attributes" for element with name attribute’s local name, namespace attribute’s // namespace, and oldValue attribute’s value. queueMutationRecord('attributes', element, { @@ -100,7 +100,7 @@ export function removeAttribute (attribute: Attr, element: Element): void { * @param newAttr The attribute to replace oldAttr with * @param element The element on which to replace the attribute */ -export function replaceAttribute (oldAttr: Attr, newAttr: Attr, element: Element): void { +export function replaceAttribute(oldAttr: Attr, newAttr: Attr, element: Element): void { // 1. Queue a mutation record of "attributes" for element with name oldAttr’s local name, namespace oldAttr’s // namespace, and oldValue oldAttr’s value. queueMutationRecord('attributes', element, { diff --git a/src/util/cloneNode.ts b/src/util/cloneNode.ts index 706e4ac..5e45eb9 100644 --- a/src/util/cloneNode.ts +++ b/src/util/cloneNode.ts @@ -12,7 +12,7 @@ import { getNodeDocument } from './treeHelpers'; * @param cloneChildren Whether to also clone node's descendants * @param document The document used to create the copy */ -export default function cloneNode (node: Node, cloneChildren: boolean = false, document?: Document): Node { +export default function cloneNode(node: Node, cloneChildren: boolean = false, document?: Document): Node { // 1. If document is not given, let document be node’s node document. if (!document) { document = getNodeDocument(node); @@ -46,7 +46,7 @@ export default function cloneNode (node: Node, cloneChildren: boolean = false, d // specified and the clone children flag being set. if (cloneChildren) { for (let child = node.firstChild; child; child = child.nextSibling) { - copy.appendChild(cloneNode(child, true, document)) + copy.appendChild(cloneNode(child, true, document)); } } diff --git a/src/util/createElementNS.ts b/src/util/createElementNS.ts index 0020e18..c39ea96 100644 --- a/src/util/createElementNS.ts +++ b/src/util/createElementNS.ts @@ -13,7 +13,7 @@ import { validateAndExtract } from './namespaceHelpers'; * * @return The new element */ -export default function createElementNS (document: Document, namespace: string | null, qualifiedName: string): Element { +export default function createElementNS(document: Document, namespace: string | null, qualifiedName: string): Element { // 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName to validate and // extract. const { namespace: validatedNamespace, prefix, localName } = validateAndExtract(namespace, qualifiedName); diff --git a/src/util/errorHelpers.ts b/src/util/errorHelpers.ts index 1fb1174..290706a 100644 --- a/src/util/errorHelpers.ts +++ b/src/util/errorHelpers.ts @@ -1,55 +1,55 @@ -export function expectArity (args: IArguments, minArity: number): void { +export function expectArity(args: IArguments, minArity: number): void { // According to WebIDL overload resolution semantics, only a lower bound applies to the number of arguments provided if (args.length < minArity) { throw new TypeError(`Function should be called with at least ${minArity} arguments`); } } -export function expectObject (value: T, Constructor: any): void { +export function expectObject(value: T, Constructor: any): void { if (!(value instanceof Constructor)) { throw new TypeError(`Value should be an instance of ${Constructor.name}`); } } -function createDOMException (name: string, code: number, message: string): Error { +function createDOMException(name: string, code: number, message: string): Error { const err = new Error(`${name}: ${message}`); err.name = name; (err as any).code = code; return err; } -export function throwHierarchyRequestError (message: string): never { +export function throwHierarchyRequestError(message: string): never { throw createDOMException('HierarchyRequestError', 3, message); } -export function throwIndexSizeError (message: string): never { +export function throwIndexSizeError(message: string): never { throw createDOMException('IndexSizeError', 1, message); } -export function throwInUseAttributeError (message: string): never { +export function throwInUseAttributeError(message: string): never { throw createDOMException('InUseAttributeError', 10, message); } -export function throwInvalidCharacterError (message: string): never { +export function throwInvalidCharacterError(message: string): never { throw createDOMException('InvalidCharacterError', 5, message); } -export function throwInvalidNodeTypeError (message: string): never { +export function throwInvalidNodeTypeError(message: string): never { throw createDOMException('InvalidNodeTypeError', 24, message); } -export function throwNamespaceError (message: string): never { +export function throwNamespaceError(message: string): never { throw createDOMException('NamespaceError', 14, message); } -export function throwNotFoundError (message: string): never { +export function throwNotFoundError(message: string): never { throw createDOMException('NotFoundError', 8, message); } -export function throwNotSupportedError (message: string): never { +export function throwNotSupportedError(message: string): never { throw createDOMException('NotSupportedError', 9, message); } -export function throwWrongDocumentError (message: string): never { +export function throwWrongDocumentError(message: string): never { throw createDOMException('WrongDocumentError', 4, message); } diff --git a/src/util/mutationAlgorithms.ts b/src/util/mutationAlgorithms.ts index 9da20da..9cfea19 100644 --- a/src/util/mutationAlgorithms.ts +++ b/src/util/mutationAlgorithms.ts @@ -14,7 +14,7 @@ import queueMutationRecord from '../mutation-observer/queueMutationRecord'; /** * To ensure pre-insertion validity of a node into a parent before a child, run these steps: */ -function ensurePreInsertionValidity (node: Node, parent: Node, child: Node | null): void { +function ensurePreInsertionValidity(node: Node, parent: Node, child: Node | null): void { // 1. If parent is not a Document, DocumentFragment, or Element node, throw a HierarchyRequestError. if (!isNodeOfType(parent, NodeType.DOCUMENT_NODE, NodeType.DOCUMENT_FRAGMENT_NODE, NodeType.ELEMENT_NODE)) { throwHierarchyRequestError('parent must be a Document, DocumentFragment or Element node'); @@ -32,15 +32,17 @@ function ensurePreInsertionValidity (node: Node, parent: Node, child: Node | nul // 4. If node is not a DocumentFragment, DocumentType, Element, Text, ProcessingInstruction, or Comment node, throw // a HierarchyRequestError. - if (!isNodeOfType( - node, - NodeType.DOCUMENT_FRAGMENT_NODE, - NodeType.DOCUMENT_TYPE_NODE, - NodeType.ELEMENT_NODE, - NodeType.TEXT_NODE, - NodeType.PROCESSING_INSTRUCTION_NODE, - NodeType.COMMENT_NODE - )) { + if ( + !isNodeOfType( + node, + NodeType.DOCUMENT_FRAGMENT_NODE, + NodeType.DOCUMENT_TYPE_NODE, + NodeType.ELEMENT_NODE, + NodeType.TEXT_NODE, + NodeType.PROCESSING_INSTRUCTION_NODE, + NodeType.COMMENT_NODE + ) + ) { throwHierarchyRequestError( 'node must be a DocumentFragment, DocumentType, Element, Text, ProcessingInstruction or Comment node' ); @@ -74,15 +76,11 @@ function ensurePreInsertionValidity (node: Node, parent: Node, child: Node | nul // or child is not null and a doctype is following child. if ( fragment.firstElementChild && - ( - parentDocument.documentElement || + (parentDocument.documentElement || (child && isNodeOfType(child, NodeType.DOCUMENT_TYPE_NODE)) || - (child && parentDocument.doctype && getNodeIndex(child) < getNodeIndex(parentDocument.doctype)) - ) + (child && parentDocument.doctype && getNodeIndex(child) < getNodeIndex(parentDocument.doctype))) ) { - throwHierarchyRequestError( - 'Document should contain at most one doctype, followed by at most one element' - ); + throwHierarchyRequestError('Document should contain at most one doctype, followed by at most one element'); } break; @@ -95,9 +93,7 @@ function ensurePreInsertionValidity (node: Node, parent: Node, child: Node | nul (child && isNodeOfType(child, NodeType.DOCUMENT_TYPE_NODE)) || (child && parentDocument.doctype && getNodeIndex(child) < getNodeIndex(parentDocument.doctype)) ) { - throwHierarchyRequestError( - 'Document should contain at most one doctype, followed by at most one element' - ); + throwHierarchyRequestError('Document should contain at most one doctype, followed by at most one element'); } break; @@ -107,16 +103,12 @@ function ensurePreInsertionValidity (node: Node, parent: Node, child: Node | nul // parent has an element child. if ( parentDocument.doctype || - ( - child && + (child && parentDocument.documentElement && - getNodeIndex(parentDocument.documentElement) < getNodeIndex(child) - ) || + getNodeIndex(parentDocument.documentElement) < getNodeIndex(child)) || (!child && parentDocument.documentElement) ) { - throwHierarchyRequestError( - 'Document should contain at most one doctype, followed by at most one element' - ); + throwHierarchyRequestError('Document should contain at most one doctype, followed by at most one element'); } break; } @@ -132,7 +124,7 @@ function ensurePreInsertionValidity (node: Node, parent: Node, child: Node | nul * * @return The inserted node */ -export function preInsertNode (node: Node, parent: Node, child: Node | null): Node { +export function preInsertNode(node: Node, parent: Node, child: Node | null): Node { // 1. Ensure pre-insertion validity of node into parent before child. ensurePreInsertionValidity(node, parent, child); @@ -162,7 +154,7 @@ export function preInsertNode (node: Node, parent: Node, child: Node | null): No * @param child Child to insert before, or null to insert at end of parent * @param suppressObservers Whether to skip enqueueing a mutation record for this mutation */ -export function insertNode (node: Node, parent: Node, child: Node | null, suppressObservers: boolean = false): void { +export function insertNode(node: Node, parent: Node, child: Node | null, suppressObservers: boolean = false): void { // 1. Let count be the number of children of node if it is a DocumentFragment node, and one otherwise. const isDocumentFragment = isNodeOfType(node, NodeType.DOCUMENT_FRAGMENT_NODE); const count = isDocumentFragment ? determineLengthOfNode(node) : 1; @@ -208,7 +200,7 @@ export function insertNode (node: Node, parent: Node, child: Node | null, suppre nodes.forEach(node => { // 6.1. If child is null, then append node to parent’s children. // 6.2. Otherwise, insert node into parent’s children before child’s index. - insertIntoChildren(node, parent, child) + insertIntoChildren(node, parent, child); // 6.3. If parent is a shadow host and node is a slotable, then assign a slot for node. // 6.4. If parent is a slot whose assigned nodes is the empty list, then run signal a slot change for parent. @@ -251,7 +243,7 @@ export function insertNode (node: Node, parent: Node, child: Node | null, suppre * * @return The appended node */ -export function appendNode (node: Node, parent: Node): Node { +export function appendNode(node: Node, parent: Node): Node { // pre-insert node into parent before null. return preInsertNode(node, parent, null); } @@ -265,7 +257,7 @@ export function appendNode (node: Node, parent: Node): Node { * * @return The old child node */ -export function replaceChildWithNode (child: Node, node: Node, parent: Node): Node { +export function replaceChildWithNode(child: Node, node: Node, parent: Node): Node { // 1. If parent is not a Document, DocumentFragment, or Element node, throw a HierarchyRequestError. if (!isNodeOfType(parent, NodeType.DOCUMENT_NODE, NodeType.DOCUMENT_FRAGMENT_NODE, NodeType.ELEMENT_NODE)) { throwHierarchyRequestError('Can not replace under a non-parent node'); @@ -283,18 +275,20 @@ export function replaceChildWithNode (child: Node, node: Node, parent: Node): No // 4. If node is not a DocumentFragment, DocumentType, Element, Text, ProcessingInstruction, or Comment node, throw // a HierarchyRequestError. - if (!isNodeOfType( - node, - NodeType.DOCUMENT_FRAGMENT_NODE, - NodeType.DOCUMENT_TYPE_NODE, - NodeType.ELEMENT_NODE, - NodeType.TEXT_NODE, - NodeType.PROCESSING_INSTRUCTION_NODE, - NodeType.COMMENT_NODE - )) { + if ( + !isNodeOfType( + node, + NodeType.DOCUMENT_FRAGMENT_NODE, + NodeType.DOCUMENT_TYPE_NODE, + NodeType.ELEMENT_NODE, + NodeType.TEXT_NODE, + NodeType.PROCESSING_INSTRUCTION_NODE, + NodeType.COMMENT_NODE + ) + ) { throwHierarchyRequestError( - 'Can not insert a node that isn\'t a DocumentFragment, DocumentType, Element, Text, ' + - 'ProcessingInstruction or Comment' + "Can not insert a node that isn't a DocumentFragment, DocumentType, Element, Text, " + + 'ProcessingInstruction or Comment' ); } @@ -326,14 +320,10 @@ export function replaceChildWithNode (child: Node, node: Node, parent: Node): No // a doctype is following child. if ( fragment.firstElementChild && - ( - (parentDocument.documentElement && parentDocument.documentElement !== child) || - (child && parentDocument.doctype && getNodeIndex(child) < getNodeIndex(parentDocument.doctype)) - ) + ((parentDocument.documentElement && parentDocument.documentElement !== child) || + (child && parentDocument.doctype && getNodeIndex(child) < getNodeIndex(parentDocument.doctype))) ) { - throwHierarchyRequestError( - 'Document should contain at most one doctype, followed by at most one element' - ); + throwHierarchyRequestError('Document should contain at most one doctype, followed by at most one element'); } break; @@ -344,9 +334,7 @@ export function replaceChildWithNode (child: Node, node: Node, parent: Node): No (parentDocument.documentElement && parentDocument.documentElement !== child) || (parentDocument.doctype && getNodeIndex(child) < getNodeIndex(parentDocument.doctype)) ) { - throwHierarchyRequestError( - 'Document should contain at most one doctype, followed by at most one element' - ); + throwHierarchyRequestError('Document should contain at most one doctype, followed by at most one element'); } break; @@ -355,14 +343,9 @@ export function replaceChildWithNode (child: Node, node: Node, parent: Node): No // parent has a doctype child that is not child, or an element is preceding child. if ( (parentDocument.doctype && parentDocument.doctype !== child) || - ( - parentDocument.documentElement && - getNodeIndex(parentDocument.documentElement) < getNodeIndex(child) - ) + (parentDocument.documentElement && getNodeIndex(parentDocument.documentElement) < getNodeIndex(child)) ) { - throwHierarchyRequestError( - 'Document should contain at most one doctype, followed by at most one element' - ); + throwHierarchyRequestError('Document should contain at most one doctype, followed by at most one element'); } break; } @@ -423,7 +406,7 @@ export function replaceChildWithNode (child: Node, node: Node, parent: Node): No * * @return The removed child */ -export function preRemoveChild (child: Node, parent: Node): Node { +export function preRemoveChild(child: Node, parent: Node): Node { // 1. If child’s parent is not parent, then throw a NotFoundError. if (child.parentNode !== parent) { throwNotFoundError('child is not a child of parent'); @@ -443,7 +426,7 @@ export function preRemoveChild (child: Node, parent: Node): Node { * @param parent Parent to remove child from * @param suppressObservers Whether to skip enqueueing a mutation record for this mutation */ -export function removeNode (node: Node, parent: Node, suppressObservers: boolean = false): void { +export function removeNode(node: Node, parent: Node, suppressObservers: boolean = false): void { // 1. Let index be node’s index. const index = getNodeIndex(node); @@ -471,7 +454,7 @@ export function removeNode (node: Node, parent: Node, suppressObservers: boolean if (range.endContainer === parent && range.endOffset > index) { range.endOffset -= 1; } - }) + }); // 6. For each NodeIterator object iterator whose root’s node document is node’s node document, run the NodeIterator // pre-removing steps given node and iterator. @@ -546,7 +529,7 @@ export function removeNode (node: Node, parent: Node, suppressObservers: boolean * @param node Node to adopt * @param document Document to adopt node into */ -export function adoptNode (node: Node, document: Document): void { +export function adoptNode(node: Node, document: Document): void { // 1. Let oldDocument be node’s node document. const oldDocument = getNodeDocument(node); @@ -573,7 +556,7 @@ export function adoptNode (node: Node, document: Document): void { attr.ownerDocument = document; } } - }) + }); // 3.2. For each inclusiveDescendant in node’s shadow-including inclusive descendants that is custom, enqueue a // custom element callback reaction with inclusiveDescendant, callback name "adoptedCallback", and an argument list diff --git a/src/util/namespaceHelpers.ts b/src/util/namespaceHelpers.ts index be3aa13..62db877 100644 --- a/src/util/namespaceHelpers.ts +++ b/src/util/namespaceHelpers.ts @@ -74,7 +74,7 @@ const NameChar = NameStartChar.clone() return `^(?:${NameStartChar.toString()})(?:${NameChar.toString()})*$`; */ -const NAME_REGEX_XML_1_0_FIFTH_EDITION = /^(?:[:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])(?:[\-\.0-:A-Z_a-z\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])*$/ +const NAME_REGEX_XML_1_0_FIFTH_EDITION = /^(?:[:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])(?:[\-\.0-:A-Z_a-z\xB7\xC0-\xD6\xD8-\xF6\xF8-\u037D\u037F-\u1FFF\u200C\u200D\u203F\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD]|[\uD800-\uDB7F][\uDC00-\uDFFF])*$/; /** * Returns true if name matches the Name production. @@ -83,7 +83,7 @@ const NAME_REGEX_XML_1_0_FIFTH_EDITION = /^(?:[:A-Z_a-z\xC0-\xD6\xD8-\xF6\xF8-\u * * @return true if name matches Name, otherwise false */ -export function matchesNameProduction (name: string): boolean { +export function matchesNameProduction(name: string): boolean { return NAME_REGEX_XML_1_0_FOURTH_EDITION.test(name); } @@ -95,7 +95,7 @@ export function matchesNameProduction (name: string): boolean { * * @return True if the name is a valid QName, provided it is also a valid Name, otherwise false */ -function isValidQName (name: string): boolean { +function isValidQName(name: string): boolean { const parts = name.split(':'); if (parts.length > 2) { return false; @@ -112,7 +112,7 @@ function isValidQName (name: string): boolean { * * @param qualifiedName Qualified name to validate */ -export function validateQualifiedName (qualifiedName: string): void { +export function validateQualifiedName(qualifiedName: string): void { // throw an InvalidCharacterError if qualifiedName does not match the Name or QName production. // (QName is basically (Name without ':') ':' (Name without ':'), so just check the position of the : if (!isValidQName(qualifiedName) || !matchesNameProduction(qualifiedName)) { @@ -128,7 +128,10 @@ export function validateQualifiedName (qualifiedName: string): void { * * @return Namespace, prefix and localName */ -export function validateAndExtract (namespace: string | null, qualifiedName: string): { namespace: string | null, prefix: string | null, localName: string } { +export function validateAndExtract( + namespace: string | null, + qualifiedName: string +): { namespace: string | null; prefix: string | null; localName: string } { // 1. If namespace is the empty string, set it to null. if (namespace === '') { namespace = null; diff --git a/src/util/treeHelpers.ts b/src/util/treeHelpers.ts index 9a67519..6f7b508 100644 --- a/src/util/treeHelpers.ts +++ b/src/util/treeHelpers.ts @@ -10,7 +10,7 @@ import { NodeType, isNodeOfType } from './NodeType'; * * @return The length of the node */ -export function determineLengthOfNode (node: Node): number { +export function determineLengthOfNode(node: Node): number { switch (node.nodeType) { // DocumentType: Zero. case NodeType.DOCUMENT_TYPE_NODE: @@ -35,7 +35,7 @@ export function determineLengthOfNode (node: Node): number { * * @return Node's inclusive ancestors, in tree order */ -export function getInclusiveAncestors (node: Node): Node[] { +export function getInclusiveAncestors(node: Node): Node[] { let ancestor: Node | null = node; let ancestors: Node[] = []; while (ancestor) { @@ -53,7 +53,7 @@ export function getInclusiveAncestors (node: Node): Node[] { * * @return The node document for node */ -export function getNodeDocument (node: Node): Document { +export function getNodeDocument(node: Node): Document { if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { return node as Document; } @@ -68,7 +68,7 @@ export function getNodeDocument (node: Node): Document { * * @return The index of node in its parent's children */ -export function getNodeIndex (node: Node): number { +export function getNodeIndex(node: Node): number { return (node.parentNode as Node).childNodes.indexOf(node); } @@ -79,7 +79,7 @@ export function getNodeIndex (node: Node): number { * * @return The root of node */ -export function getRootOfNode (node: Node): Node { +export function getRootOfNode(node: Node): Node { while (node.parentNode) { node = node.parentNode; } @@ -93,7 +93,7 @@ export function getRootOfNode (node: Node): Node { * @param node Root of the subtree to process * @param callback Callback to invoke for each descendant, should not modify node's position in the tree */ -export function forEachInclusiveDescendant (node: Node, callback: (node: Node) => void): void { +export function forEachInclusiveDescendant(node: Node, callback: (node: Node) => void): void { callback(node); for (let child = node.firstChild; child; child = child.nextSibling) { forEachInclusiveDescendant(child, callback); diff --git a/src/util/treeMutations.ts b/src/util/treeMutations.ts index ba4c556..65951be 100644 --- a/src/util/treeMutations.ts +++ b/src/util/treeMutations.ts @@ -15,7 +15,7 @@ import { NodeType, isNodeOfType } from './NodeType'; * @param parent Parent to insert under * @param referenceChild Child to insert before */ -export function insertIntoChildren (node: Node, parent: Node, referenceChild: Node | null): void { +export function insertIntoChildren(node: Node, parent: Node, referenceChild: Node | null): void { // Node node.parentNode = parent; const previousSibling: Node | null = referenceChild === null ? parent.lastChild : referenceChild.previousSibling; @@ -24,15 +24,13 @@ export function insertIntoChildren (node: Node, parent: Node, referenceChild: No node.nextSibling = nextSibling; if (previousSibling) { previousSibling.nextSibling = node; - } - else { + } else { parent.firstChild = node; } if (nextSibling) { nextSibling.previousSibling = node; parent.childNodes.splice(parent.childNodes.indexOf(nextSibling), 0, node); - } - else { + } else { parent.lastChild = node; parent.childNodes.push(node); } @@ -83,8 +81,7 @@ export function insertIntoChildren (node: Node, parent: Node, referenceChild: No const parentDocument = parent as Document; if (isNodeOfType(node, NodeType.ELEMENT_NODE)) { parentDocument.documentElement = node as Element; - } - else if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { + } else if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { parentDocument.doctype = node as DocumentType; } } @@ -98,7 +95,7 @@ export function insertIntoChildren (node: Node, parent: Node, referenceChild: No * @param node Node to remove * @param parent Parent to remove from */ -export function removeFromChildren (node: Node, parent: Node) { +export function removeFromChildren(node: Node, parent: Node) { const previousSibling = node.previousSibling; const nextSibling = node.nextSibling; const isElement = isNodeOfType(node, NodeType.ELEMENT_NODE); @@ -111,14 +108,12 @@ export function removeFromChildren (node: Node, parent: Node) { node.nextSibling = null; if (previousSibling) { previousSibling.nextSibling = nextSibling; - } - else { + } else { parent.firstChild = nextSibling; } if (nextSibling) { nextSibling.previousSibling = previousSibling; - } - else { + } else { parent.lastChild = previousSibling; } parent.childNodes.splice(parent.childNodes.indexOf(node), 1); @@ -142,8 +137,7 @@ export function removeFromChildren (node: Node, parent: Node) { const parentDocument = parent as Document; if (isNodeOfType(node, NodeType.ELEMENT_NODE)) { parentDocument.documentElement = null; - } - else if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { + } else if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { parentDocument.doctype = null; } } diff --git a/src/util/typeHelpers.ts b/src/util/typeHelpers.ts index 5ef0b91..603f26f 100644 --- a/src/util/typeHelpers.ts +++ b/src/util/typeHelpers.ts @@ -1,10 +1,10 @@ import { expectObject } from './errorHelpers'; -export function asUnsignedLong (number: number): number { +export function asUnsignedLong(number: number): number { return number >>> 0; } -export function treatNullAsEmptyString (value: string | null): string { +export function treatNullAsEmptyString(value: string | null): string { // Treat null as empty string if (value === null) { return ''; @@ -14,13 +14,13 @@ export function treatNullAsEmptyString (value: string | null): string { return String(value); } -export function asObject (value: T, Constructor: any): T { +export function asObject(value: T, Constructor: any): T { expectObject(value, Constructor); return value; } -export function asNullableObject (value: T | null | undefined, Constructor: any): T | null { +export function asNullableObject(value: T | null | undefined, Constructor: any): T | null { if (value === undefined || value === null) { return null; } @@ -28,7 +28,7 @@ export function asNullableObject (value: T | null | undefined, Constructor: a return asObject(value, Constructor); } -export function asNullableString (value: string | null | undefined): string | null { +export function asNullableString(value: string | null | undefined): string | null { // Treat undefined as null if (value === undefined) { return null; diff --git a/test/Document.tests.ts b/test/Document.tests.ts index badd20e..28652a6 100644 --- a/test/Document.tests.ts +++ b/test/Document.tests.ts @@ -33,7 +33,7 @@ describe('Document', () => { it('has a documentElement', () => chai.assert.equal(document.documentElement, element)); - it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [ element ])); + it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [element])); it('the child element is adopted into the document', () => chai.assert.equal(element.ownerDocument, document)); @@ -56,7 +56,7 @@ describe('Document', () => { it('has the other element as documentElement', () => chai.assert.equal(document.documentElement, otherElement)); - it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [ otherElement ])); + it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [otherElement])); }); }); @@ -69,7 +69,7 @@ describe('Document', () => { it('has no documentElement', () => chai.assert.equal(document.documentElement, null)); - it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [ processingInstruction ])); + it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [processingInstruction])); describe('after replacing with an element', () => { let otherElement: Element; @@ -80,7 +80,7 @@ describe('Document', () => { it('has the other element as documentElement', () => chai.assert.equal(document.documentElement, otherElement)); - it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [ otherElement ])); + it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [otherElement])); }); }); diff --git a/test/Element.tests.ts b/test/Element.tests.ts index c7cfc59..5d01a7e 100644 --- a/test/Element.tests.ts +++ b/test/Element.tests.ts @@ -1,4 +1,4 @@ -import * as slimdom from '../src/index' +import * as slimdom from '../src/index'; import Attr from '../src/Attr'; import Document from '../src/Document'; @@ -61,27 +61,34 @@ describe('Element', () => { chai.assert.equal(element.getAttribute('noSuchAttribute'), null); }); - function hasAttributes (attributes: Attr[], expected: { name: string, value: string }[]): boolean { - return attributes.length === expected.length && + function hasAttributes(attributes: Attr[], expected: { name: string; value: string }[]): boolean { + return ( + attributes.length === expected.length && attributes.every(attr => expected.some(pair => pair.name === attr.name && pair.value === attr.value)) && - expected.every(pair => attributes.some(attr => attr.name === pair.name && attr.value === pair.value)); + expected.every(pair => attributes.some(attr => attr.name === pair.name && attr.value === pair.value)) + ); } - it('has attributes', () => chai.assert(hasAttributes(element.attributes, [ - {name: 'firstAttribute', value: 'first'}, - {name: 'test', value: '123'}, - {name: 'lastAttribute', value: 'last'} - ]))); + it('has attributes', () => + chai.assert( + hasAttributes(element.attributes, [ + { name: 'firstAttribute', value: 'first' }, + { name: 'test', value: '123' }, + { name: 'lastAttribute', value: 'last' } + ]) + )); it('can overwrite the attribute', () => { element.setAttribute('test', '456'); chai.assert(element.hasAttribute('test'), 'has the attribute'); chai.assert.equal(element.getAttribute('test'), '456'); - chai.assert(hasAttributes(element.attributes, [ - {name: 'firstAttribute', value: 'first'}, - {name: 'test', value: '456'}, - {name: 'lastAttribute', value: 'last'} - ])); + chai.assert( + hasAttributes(element.attributes, [ + { name: 'firstAttribute', value: 'first' }, + { name: 'test', value: '456' }, + { name: 'lastAttribute', value: 'last' } + ]) + ); }); it('can remove the attribute', () => { @@ -89,10 +96,12 @@ describe('Element', () => { chai.assert(element.hasAttribute('firstAttribute'), 'has attribute firstAttribute'); chai.assert(!element.hasAttribute('test'), 'does not have attribute test'); chai.assert(element.hasAttribute('lastAttribute'), 'has attribute lastAttribute'); - chai.assert(hasAttributes(element.attributes, [ - {name: 'firstAttribute', value: 'first'}, - {name: 'lastAttribute', value: 'last'} - ])); + chai.assert( + hasAttributes(element.attributes, [ + { name: 'firstAttribute', value: 'first' }, + { name: 'lastAttribute', value: 'last' } + ]) + ); }); it('ignores removing non-existent attributes', () => { @@ -100,11 +109,13 @@ describe('Element', () => { element.removeAttribute('other'); chai.assert(!element.hasAttribute('other'), 'does not have attribute other'); chai.assert(element.hasAttribute('test'), 'has attribute test'); - chai.assert(hasAttributes(element.attributes, [ - {name: 'firstAttribute', value: 'first'}, - {name: 'test', value: '123'}, - {name: 'lastAttribute', value: 'last'} - ])); + chai.assert( + hasAttributes(element.attributes, [ + { name: 'firstAttribute', value: 'first' }, + { name: 'test', value: '123' }, + { name: 'lastAttribute', value: 'last' } + ]) + ); }); }); @@ -124,7 +135,7 @@ describe('Element', () => { it('has child element references', () => { chai.assert.equal(element.firstElementChild, child); chai.assert.equal(element.lastElementChild, child); - chai.assert.deepEqual(element.children, [ child ]); + chai.assert.deepEqual(element.children, [child]); chai.assert.equal(element.childElementCount, 1); }); @@ -157,13 +168,13 @@ describe('Element', () => { it('has child node references', () => { chai.assert.equal(element.firstChild, otherChild); chai.assert.equal(element.lastChild, otherChild); - chai.assert.deepEqual(element.childNodes, [ otherChild ]); + chai.assert.deepEqual(element.childNodes, [otherChild]); }); it('has child element references', () => { chai.assert.equal(element.firstElementChild, otherChild); chai.assert.equal(element.lastElementChild, otherChild); - chai.assert.deepEqual(element.children, [ otherChild ]); + chai.assert.deepEqual(element.children, [otherChild]); chai.assert.equal(element.childElementCount, 1); }); }); @@ -178,13 +189,13 @@ describe('Element', () => { it('has child node references', () => { chai.assert.equal(element.firstChild, otherChild); chai.assert.equal(element.lastChild, child); - chai.assert.deepEqual(element.childNodes, [ otherChild, child ]); + chai.assert.deepEqual(element.childNodes, [otherChild, child]); }); it('has child element references', () => { chai.assert.equal(element.firstElementChild, otherChild); chai.assert.equal(element.lastElementChild, child); - chai.assert.deepEqual(element.children, [ otherChild, child ]); + chai.assert.deepEqual(element.children, [otherChild, child]); chai.assert.equal(element.childElementCount, 2); }); @@ -217,7 +228,7 @@ describe('Element', () => { it('has child element references', () => { chai.assert.equal(element.firstElementChild, child); chai.assert.equal(element.lastElementChild, otherChild); - chai.assert.deepEqual(element.children, [ child, otherChild ]); + chai.assert.deepEqual(element.children, [child, otherChild]); chai.assert.equal(element.childElementCount, 2); }); @@ -242,13 +253,13 @@ describe('Element', () => { it('has child node references', () => { chai.assert.equal(element.firstChild, child); chai.assert.equal(element.lastChild, child); - chai.assert.deepEqual(element.childNodes, [ child ]); + chai.assert.deepEqual(element.childNodes, [child]); }); it('has child element references', () => { chai.assert.equal(element.firstElementChild, child); chai.assert.equal(element.lastElementChild, child); - chai.assert.deepEqual(element.children, [ child ]); + chai.assert.deepEqual(element.children, [child]); chai.assert.equal(element.childElementCount, 1); }); @@ -271,7 +282,7 @@ describe('Element', () => { it('has child node references', () => { chai.assert.equal(element.firstChild, processingInstruction); chai.assert.equal(element.lastChild, processingInstruction); - chai.assert.deepEqual(element.childNodes, [ processingInstruction ]); + chai.assert.deepEqual(element.childNodes, [processingInstruction]); }); it('has no child elements', () => { @@ -291,13 +302,13 @@ describe('Element', () => { it('has child node references', () => { chai.assert.equal(element.firstChild, otherChild); chai.assert.equal(element.lastChild, otherChild); - chai.assert.deepEqual(element.childNodes, [ otherChild ]); + chai.assert.deepEqual(element.childNodes, [otherChild]); }); it('has child element references', () => { chai.assert.equal(element.firstElementChild, otherChild); chai.assert.equal(element.lastElementChild, otherChild); - chai.assert.deepEqual(element.children, [ otherChild ]); + chai.assert.deepEqual(element.children, [otherChild]); chai.assert.equal(element.childElementCount, 1); }); }); diff --git a/test/MutationObserver.tests.ts b/test/MutationObserver.tests.ts index 4611a0c..8503e2d 100644 --- a/test/MutationObserver.tests.ts +++ b/test/MutationObserver.tests.ts @@ -20,7 +20,7 @@ describe('MutationObserver', () => { let callbackCalled: boolean; let callbackArgs: any[] = []; - function callback (...args: any[]) { + function callback(...args: any[]) { callbackCalled = true; callbackArgs.push(args); } @@ -129,7 +129,7 @@ describe('MutationObserver', () => { const queue = observer.takeRecords(); chai.assert.equal(queue[0].type, 'childList'); - chai.assert.deepEqual(queue[0].addedNodes, [ newElement ]); + chai.assert.deepEqual(queue[0].addedNodes, [newElement]); chai.assert.deepEqual(queue[0].removedNodes, []); chai.assert.equal(queue[0].previousSibling, text); chai.assert.equal(queue[0].nextSibling, null); @@ -141,8 +141,8 @@ describe('MutationObserver', () => { const queue = observer.takeRecords(); chai.assert.equal(queue[0].type, 'childList'); - chai.assert.deepEqual(queue[0].addedNodes, [ newElement ]); - chai.assert.deepEqual(queue[0].removedNodes, [ text ]); + chai.assert.deepEqual(queue[0].addedNodes, [newElement]); + chai.assert.deepEqual(queue[0].removedNodes, [text]); chai.assert.equal(queue[0].previousSibling, null); chai.assert.equal(queue[0].nextSibling, null); }); @@ -157,12 +157,12 @@ describe('MutationObserver', () => { const queue = observer.takeRecords(); chai.assert.equal(queue[0].type, 'childList'); chai.assert.deepEqual(queue[0].addedNodes, []); - chai.assert.deepEqual(queue[0].removedNodes, [ newElement ]); + chai.assert.deepEqual(queue[0].removedNodes, [newElement]); chai.assert.equal(queue[0].previousSibling, text); chai.assert.equal(queue[0].nextSibling, null); chai.assert.equal(queue[1].type, 'childList'); - chai.assert.deepEqual(queue[1].addedNodes, [ newElement ]); + chai.assert.deepEqual(queue[1].addedNodes, [newElement]); chai.assert.deepEqual(queue[1].removedNodes, []); chai.assert.equal(queue[1].previousSibling, null); chai.assert.equal(queue[1].nextSibling, text); @@ -179,14 +179,14 @@ describe('MutationObserver', () => { chai.assert.equal(queue[0].type, 'childList'); chai.assert.equal(queue[0].target, element); chai.assert.deepEqual(queue[0].addedNodes, []); - chai.assert.deepEqual(queue[0].removedNodes, [ newElement ]); + chai.assert.deepEqual(queue[0].removedNodes, [newElement]); chai.assert.equal(queue[0].previousSibling, text); chai.assert.equal(queue[0].nextSibling, null); chai.assert.equal(queue[1].type, 'childList'); chai.assert.equal(queue[1].target, element); - chai.assert.deepEqual(queue[1].addedNodes, [ newElement ]); - chai.assert.deepEqual(queue[1].removedNodes, [ text ]); + chai.assert.deepEqual(queue[1].addedNodes, [newElement]); + chai.assert.deepEqual(queue[1].removedNodes, [text]); chai.assert.equal(queue[1].previousSibling, null); chai.assert.equal(queue[1].nextSibling, null); }); diff --git a/test/Range.tests.ts b/test/Range.tests.ts index 782a784..0dc5bcd 100644 --- a/test/Range.tests.ts +++ b/test/Range.tests.ts @@ -99,7 +99,6 @@ describe('Range', () => { }); describe('under mutations', () => { - describe('in element', () => { beforeEach(() => { range.setStart(element, 0); diff --git a/test/web-platform-tests/SlimdomTreeAdapter.ts b/test/web-platform-tests/SlimdomTreeAdapter.ts index d4ea932..236cb75 100644 --- a/test/web-platform-tests/SlimdomTreeAdapter.ts +++ b/test/web-platform-tests/SlimdomTreeAdapter.ts @@ -5,7 +5,7 @@ import Attr from '../../src/Attr'; import { createElement } from '../../src/Element'; import { appendAttribute } from '../../src/util/attrMutations'; -function undefinedAsNull (value: T | undefined): T | null { +function undefinedAsNull(value: T | undefined): T | null { if (value === undefined) { return null; } @@ -13,7 +13,7 @@ function undefinedAsNull (value: T | undefined): T | null { return value; } -function qualifiedName (namespace: string | undefined, prefix: string | undefined, name: string) { +function qualifiedName(namespace: string | undefined, prefix: string | undefined, name: string) { return prefix ? `${prefix}:${name}` : name; } @@ -21,73 +21,79 @@ export default class SlimdomTreeAdapter implements parse5.AST.TreeAdapter { private _globalDocument = new slimdom.Document(); private _mode: parse5.AST.DocumentMode = 'no-quirks'; - createDocument (): parse5.AST.Document { + createDocument(): parse5.AST.Document { return this._globalDocument.implementation.createDocument(null, ''); } - createDocumentFragment (): parse5.AST.DocumentFragment { - throw new Error("Method not implemented."); + createDocumentFragment(): parse5.AST.DocumentFragment { + throw new Error('Method not implemented.'); } - createElement (tagName: string, namespaceURI: string, attrs: parse5.AST.Default.Attribute[]): parse5.AST.Element { - const [ localName, prefix ] = tagName.indexOf(':') >= 0 ? tagName.split(':') : [ tagName, null ]; + createElement(tagName: string, namespaceURI: string, attrs: parse5.AST.Default.Attribute[]): parse5.AST.Element { + const [localName, prefix] = tagName.indexOf(':') >= 0 ? tagName.split(':') : [tagName, null]; // Create element without validation, as per HTML parser spec const element = createElement(this._globalDocument, localName!, namespaceURI, prefix); attrs.forEach(attr => { // Create Attr node without validation, as per HTML parser spec - const attribute = new Attr(this._globalDocument, undefinedAsNull(attr.namespace), undefinedAsNull(attr.prefix), attr.name, attr.value, element); + const attribute = new Attr( + this._globalDocument, + undefinedAsNull(attr.namespace), + undefinedAsNull(attr.prefix), + attr.name, + attr.value, + element + ); appendAttribute(attribute, element); }); return element; } - createCommentNode (data: string): parse5.AST.CommentNode { + createCommentNode(data: string): parse5.AST.CommentNode { return this._globalDocument.createComment(data); } - appendChild (parentNode: parse5.AST.ParentNode, newNode: parse5.AST.Node): void { + appendChild(parentNode: parse5.AST.ParentNode, newNode: parse5.AST.Node): void { (parentNode as slimdom.Node).appendChild(newNode as slimdom.Node); } - insertBefore (parentNode: parse5.AST.ParentNode, newNode: parse5.AST.Node, referenceNode: parse5.AST.Node): void { + insertBefore(parentNode: parse5.AST.ParentNode, newNode: parse5.AST.Node, referenceNode: parse5.AST.Node): void { (parentNode as slimdom.Node).insertBefore(newNode as slimdom.Node, referenceNode as slimdom.Node); } - setTemplateContent (templateElement: parse5.AST.Element, contentElement: parse5.AST.DocumentFragment): void { - throw new Error("Method not implemented."); + setTemplateContent(templateElement: parse5.AST.Element, contentElement: parse5.AST.DocumentFragment): void { + throw new Error('Method not implemented.'); } - getTemplateContent (templateElement: parse5.AST.Element): parse5.AST.DocumentFragment { - throw new Error("Method not implemented."); + getTemplateContent(templateElement: parse5.AST.Element): parse5.AST.DocumentFragment { + throw new Error('Method not implemented.'); } - setDocumentType (document: parse5.AST.Document, name: string, publicId: string, systemId: string): void { + setDocumentType(document: parse5.AST.Document, name: string, publicId: string, systemId: string): void { const doctype = this._globalDocument.implementation.createDocumentType(name, publicId, systemId); const doc = document as slimdom.Document; if (doc.doctype) { doc.replaceChild(doctype, doc.doctype); - } - else { + } else { doc.insertBefore(doctype, doc.documentElement); } } - setDocumentMode (document: parse5.AST.Document, mode: parse5.AST.DocumentMode): void { + setDocumentMode(document: parse5.AST.Document, mode: parse5.AST.DocumentMode): void { this._mode = mode; } - getDocumentMode (document: parse5.AST.Document): parse5.AST.DocumentMode { + getDocumentMode(document: parse5.AST.Document): parse5.AST.DocumentMode { return this._mode; } - detachNode (node: parse5.AST.Node): void { + detachNode(node: parse5.AST.Node): void { const parent = (node as slimdom.Node).parentNode; if (parent) { parent.removeChild(node as slimdom.Node); } } - insertText (parentNode: parse5.AST.ParentNode, text: string): void { + insertText(parentNode: parse5.AST.ParentNode, text: string): void { const lastChild = (parentNode as slimdom.Node).lastChild; if (lastChild && lastChild.nodeType === slimdom.Node.TEXT_NODE) { (lastChild as slimdom.Text).appendData(text); @@ -97,7 +103,7 @@ export default class SlimdomTreeAdapter implements parse5.AST.TreeAdapter { (parentNode as slimdom.Node).appendChild(this._globalDocument.createTextNode(text)); } - insertTextBefore (parentNode: parse5.AST.ParentNode, text: string, referenceNode: parse5.AST.Node): void { + insertTextBefore(parentNode: parse5.AST.ParentNode, text: string, referenceNode: parse5.AST.Node): void { const sibling = referenceNode && (referenceNode as slimdom.Node).previousSibling; if (sibling && sibling.nodeType === slimdom.Node.TEXT_NODE) { (sibling as slimdom.Text).appendData(text); @@ -107,28 +113,32 @@ export default class SlimdomTreeAdapter implements parse5.AST.TreeAdapter { (parentNode as slimdom.Node).insertBefore(this._globalDocument.createTextNode(text), referenceNode as slimdom.Node); } - adoptAttributes (recipient: parse5.AST.Element, attrs: parse5.AST.Default.Attribute[]): void { + adoptAttributes(recipient: parse5.AST.Element, attrs: parse5.AST.Default.Attribute[]): void { const element = recipient as slimdom.Element; attrs.forEach(attr => { if (!element.hasAttributeNS(undefinedAsNull(attr.namespace), attr.name)) { - element.setAttributeNS(undefinedAsNull(attr.namespace), qualifiedName(attr.namespace, attr.prefix, attr.name), attr.value); + element.setAttributeNS( + undefinedAsNull(attr.namespace), + qualifiedName(attr.namespace, attr.prefix, attr.name), + attr.value + ); } }); } - getFirstChild (node: parse5.AST.ParentNode): parse5.AST.Node { + getFirstChild(node: parse5.AST.ParentNode): parse5.AST.Node { return (node as slimdom.Node).firstChild!; } - getChildNodes (node: parse5.AST.ParentNode): parse5.AST.Node[] { + getChildNodes(node: parse5.AST.ParentNode): parse5.AST.Node[] { return (node as slimdom.Node).childNodes; } - getParentNode (node: parse5.AST.Node): parse5.AST.ParentNode { + getParentNode(node: parse5.AST.Node): parse5.AST.ParentNode { return (node as slimdom.Node).parentNode!; } - getAttrList (element: parse5.AST.Element): parse5.AST.Default.Attribute[] { + getAttrList(element: parse5.AST.Element): parse5.AST.Default.Attribute[] { return (element as slimdom.Element).attributes.map(attr => ({ name: attr.localName, namespace: attr.namespaceURI || undefined, @@ -137,47 +147,47 @@ export default class SlimdomTreeAdapter implements parse5.AST.TreeAdapter { })); } - getTagName (element: parse5.AST.Element): string { + getTagName(element: parse5.AST.Element): string { return (element as slimdom.Element).tagName; } - getNamespaceURI (element: parse5.AST.Element): string { + getNamespaceURI(element: parse5.AST.Element): string { return (element as slimdom.Element).namespaceURI!; } - getTextNodeContent (textNode: parse5.AST.TextNode): string { + getTextNodeContent(textNode: parse5.AST.TextNode): string { return (textNode as slimdom.Text).data; } - getCommentNodeContent (commentNode: parse5.AST.CommentNode): string { + getCommentNodeContent(commentNode: parse5.AST.CommentNode): string { return (commentNode as slimdom.Comment).data; } - getDocumentTypeNodeName (doctypeNode: parse5.AST.DocumentType): string { + getDocumentTypeNodeName(doctypeNode: parse5.AST.DocumentType): string { return (doctypeNode as slimdom.DocumentType).name; } - getDocumentTypeNodePublicId (doctypeNode: parse5.AST.DocumentType): string { + getDocumentTypeNodePublicId(doctypeNode: parse5.AST.DocumentType): string { return (doctypeNode as slimdom.DocumentType).publicId; } - getDocumentTypeNodeSystemId (doctypeNode: parse5.AST.DocumentType): string { + getDocumentTypeNodeSystemId(doctypeNode: parse5.AST.DocumentType): string { return (doctypeNode as slimdom.DocumentType).systemId; } - isTextNode (node: parse5.AST.Node): boolean { + isTextNode(node: parse5.AST.Node): boolean { return node && (node as slimdom.Node).nodeType === slimdom.Node.TEXT_NODE; } - isCommentNode (node: parse5.AST.Node): boolean { + isCommentNode(node: parse5.AST.Node): boolean { return node && (node as slimdom.Node).nodeType === slimdom.Node.COMMENT_NODE; } - isDocumentTypeNode (node: parse5.AST.Node): boolean { + isDocumentTypeNode(node: parse5.AST.Node): boolean { return node && (node as slimdom.Node).nodeType === slimdom.Node.DOCUMENT_TYPE_NODE; } - isElementNode (node: parse5.AST.Node): boolean { + isElementNode(node: parse5.AST.Node): boolean { return node && (node as slimdom.Node).nodeType === slimdom.Node.ELEMENT_NODE; } } diff --git a/test/web-platform-tests/webPlatform.tests.ts b/test/web-platform-tests/webPlatform.tests.ts index 2a1181d..fdaeb94 100644 --- a/test/web-platform-tests/webPlatform.tests.ts +++ b/test/web-platform-tests/webPlatform.tests.ts @@ -16,20 +16,31 @@ const TEST_BLACKLIST: { [key: string]: (string | { [key: string]: string }) } = 'dom/lists': 'DOMTokenList (Element#classList) not implemented', 'dom/nodes/append-on-Document.html': 'ParentNode#append not implemented', 'dom/nodes/attributes.html': { - 'setAttribute should lowercase its name argument (upper case attribute)': 'HTML attribute lowercasing not implemented', - 'setAttribute should lowercase its name argument (mixed case attribute)': 'HTML attribute lowercasing not implemented', + 'setAttribute should lowercase its name argument (upper case attribute)': + 'HTML attribute lowercasing not implemented', + 'setAttribute should lowercase its name argument (mixed case attribute)': + 'HTML attribute lowercasing not implemented', 'Attributes should work in document fragments.': 'Element#attributes not implemented as NamedNodeMap', - 'Only lowercase attributes are returned on HTML elements (upper case attribute)': 'HTML attribute lowercasing not implemented', - 'Only lowercase attributes are returned on HTML elements (mixed case attribute)': 'HTML attribute lowercasing not implemented', - 'setAttributeNode, if it fires mutation events, should fire one with the new node when resetting an existing attribute (outer shell)': 'Mutation events not implemented', + 'Only lowercase attributes are returned on HTML elements (upper case attribute)': + 'HTML attribute lowercasing not implemented', + 'Only lowercase attributes are returned on HTML elements (mixed case attribute)': + 'HTML attribute lowercasing not implemented', + 'setAttributeNode, if it fires mutation events, should fire one with the new node when resetting an existing attribute (outer shell)': + 'Mutation events not implemented', 'getAttributeNames tests': 'Element#getAttributeNames not implemented', 'Own property correctness with basic attributes': 'Element#attributes not implemented as NamedNodeMap', - 'Own property correctness with non-namespaced attribute before same-name namespaced one': 'Element#attributes not implemented as NamedNodeMap', - 'Own property correctness with namespaced attribute before same-name non-namespaced one': 'Element#attributes not implemented as NamedNodeMap', - 'Own property correctness with two namespaced attributes with the same name-with-prefix': 'Element#attributes not implemented as NamedNodeMap', - 'Own property names should only include all-lowercase qualified names for an HTML element in an HTML document': 'Element#attributes not implemented as NamedNodeMap', - 'Own property names should include all qualified names for a non-HTML element in an HTML document': 'Element#attributes not implemented as NamedNodeMap', - 'Own property names should include all qualified names for an HTML element in a non-HTML document': 'Element#attributes not implemented as NamedNodeMap', + 'Own property correctness with non-namespaced attribute before same-name namespaced one': + 'Element#attributes not implemented as NamedNodeMap', + 'Own property correctness with namespaced attribute before same-name non-namespaced one': + 'Element#attributes not implemented as NamedNodeMap', + 'Own property correctness with two namespaced attributes with the same name-with-prefix': + 'Element#attributes not implemented as NamedNodeMap', + 'Own property names should only include all-lowercase qualified names for an HTML element in an HTML document': + 'Element#attributes not implemented as NamedNodeMap', + 'Own property names should include all qualified names for a non-HTML element in an HTML document': + 'Element#attributes not implemented as NamedNodeMap', + 'Own property names should include all qualified names for an HTML element in a non-HTML document': + 'Element#attributes not implemented as NamedNodeMap' }, 'dom/nodes/case.html': 'HTML case behavior not implemented', 'dom/nodes/CharacterData-remove.html': 'ChildNode#remove not implemented', @@ -62,14 +73,16 @@ const TEST_BLACKLIST: { [key: string]: (string | { [key: string]: string }) } = 'createDocument test: metadata for "http://www.w3.org/2000/svg","",null': 'SVG contentType not implemented' }, 'dom/nodes/DOMImplementation-createDocumentType.html': 'DocumentType#ownerDocument not implemented per spec', - 'dom/nodes/DOMImplementation-createHTMLDocument.html': 'HTML*Element interfaces not implemented', + 'dom/nodes/DOMImplementation-createHTMLDocument.html': 'HTML*Element interfaces not implemented', 'dom/nodes/DOMImplementation-hasFeature.html': 'DOMImplementation#hasFeature not implemented', 'dom/nodes/Element-children.html': 'Element#children not implemented as HTMLCollection', 'dom/nodes/Element-classlist.html': 'Element#classList not implemented', 'dom/nodes/Element-closest.html': 'Element#closest not implemented', 'dom/nodes/Element-getElementsByClassName.html': 'Element#getElementsByClassName not implemented', - 'dom/nodes/Element-getElementsByTagName-change-document-HTMLNess.html': 'Element#getElementsByTagName not implemented', - 'dom/nodes/Element-getElementsByTagName-change-document-HTMLNess-iframe.html': 'Element#getElementsByTagName not implemented', + 'dom/nodes/Element-getElementsByTagName-change-document-HTMLNess.html': + 'Element#getElementsByTagName not implemented', + 'dom/nodes/Element-getElementsByTagName-change-document-HTMLNess-iframe.html': + 'Element#getElementsByTagName not implemented', 'dom/nodes/Element-getElementsByTagName.html': 'Element#getElementsByTagName not implemented', 'dom/nodes/Element-getElementsByTagNameNS.html': 'Element#getElementsByTagNameNS not implemented', 'dom/nodes/Element-insertAdjacentElement.html': 'Element#insertAdjacentElement not implemented', @@ -101,24 +114,28 @@ const TEST_BLACKLIST: { [key: string]: (string | { [key: string]: string }) } = 'attributes Element.classList.toggle: token removal mutation': 'Element#classList not implemented', 'attributes Element.classList.toggle: token addition mutation': 'Element#classList not implemented', 'attributes Element.classList.toggle: forced token removal mutation': 'Element#classList not implemented', - 'attributes Element.classList.toggle: forced missing token removal no mutation': 'Element#classList not implemented', - 'attributes Element.classList.toggle: forced existing token addition no mutation': 'Element#classList not implemented', + 'attributes Element.classList.toggle: forced missing token removal no mutation': + 'Element#classList not implemented', + 'attributes Element.classList.toggle: forced existing token addition no mutation': + 'Element#classList not implemented', 'attributes Element.classList.toggle: forced token addition mutation': 'Element#classList not implemented', 'attributes Element.removeAttribute: removal no mutation': 'Element#id not implemented', 'childList HTMLInputElement.removeAttribute: type removal mutation': 'Element#id not implemented', 'attributes Element.removeAttributeNS: removal no mutation': 'Element#id not implemented', 'attributes Element.removeAttributeNS: prefixed attribute removal no mutation': 'Element#id not implemented', 'attributes/attributeFilter Element.id/Element.className: update mutation': 'attributeFilter not implemented', - 'attributes/attributeFilter Element.id/Element.className: multiple filter update mutation': 'attributeFilter not implemented', + 'attributes/attributeFilter Element.id/Element.className: multiple filter update mutation': + 'attributeFilter not implemented', 'attributeOldValue alone Element.id: update mutation': 'Element#id not implemented', - 'attributeFilter alone Element.id/Element.className: multiple filter update mutation': 'attributeFilter not implemented', + 'attributeFilter alone Element.id/Element.className: multiple filter update mutation': + 'attributeFilter not implemented', 'childList false: no childList mutation': 'Element#textContent setter not implemented' }, 'dom/nodes/MutationObserver-characterData.html': { 'characterData Range.deleteContents: child and data removal mutation': 'Range#deleteContents not implemented', 'characterData Range.deleteContents: child and data removal mutation (2)': 'Range#deleteContents not implemented', 'characterData Range.extractContents: child and data removal mutation': 'Range#extractContents not implemented', - 'characterData Range.extractContents: child and data removal mutation (2)': 'Range#extractContents not implemented', + 'characterData Range.extractContents: child and data removal mutation (2)': 'Range#extractContents not implemented' }, 'dom/nodes/MutationObserver-childList.html': { 'childList Node.textContent: replace content mutation': 'Element#textContent setter not implemented', @@ -131,7 +148,7 @@ const TEST_BLACKLIST: { [key: string]: (string | { [key: string]: string }) } = 'childList Range.extractContents: child and data removal mutation': 'Range#extractContents not implemented', 'childList Range.insertNode: child insertion mutation': 'Range#insertNode not implemented', 'childList Range.insertNode: children insertion mutation': 'Range#insertNode not implemented', - 'childList Range.surroundContents: children removal and addition mutation': 'Range#surroundContents not implemented', + 'childList Range.surroundContents: children removal and addition mutation': 'Range#surroundContents not implemented' }, 'dom/nodes/MutationObserver-disconnect.html': 'Element#id not implemented', 'dom/nodes/MutationObserver-document.html': 'Running script during parsing not implemented', @@ -331,50 +348,51 @@ const TEST_BLACKLIST: { [key: string]: (string | { [key: string]: string }) } = 'dom/ranges/Range-stringifier.html': 'Range#toString not implemented', 'dom/ranges/Range-surroundContents.html': 'Range#surroundContents not implemented', 'dom/traversal': 'NodeIterator and TreeWalker not implemented' -} +}; -function getNodes (root: slimdom.Node, ...path: string[]): slimdom.Node[] { +function getNodes(root: slimdom.Node, ...path: string[]): slimdom.Node[] { if (!path.length) { return [root]; } const [nodeName, ...remainder] = path; const matchingChildren = Array.from((root as slimdom.Element).childNodes).filter(n => n.nodeName === nodeName); - return matchingChildren.reduce( - (nodes, child) => nodes.concat(getNodes(child, ...remainder)), - [] as slimdom.Node[] - ); + return matchingChildren.reduce((nodes, child) => nodes.concat(getNodes(child, ...remainder)), [] as slimdom.Node[]); } -function getAllText (root: slimdom.Node, ...path: string[]): string { - return getNodes(root, ...path) - .map(n => (n as slimdom.Text).data) - .join(''); +function getAllText(root: slimdom.Node, ...path: string[]): string { + return getNodes(root, ...path).map(n => (n as slimdom.Text).data).join(''); } -function getAllScripts (doc: slimdom.Document, casePath: string) { +function getAllScripts(doc: slimdom.Document, casePath: string) { const scriptElements = (doc as any).getElementsByTagName('script'); - return scriptElements.reduce((scripts: string[], el: slimdom.Element) => { - const src = el.attributes.find(a => a.name === 'src'); - if (src) { - const resolvedPath = src.value.startsWith('/') - ? path.resolve(process.env.WEB_PLATFORM_TESTS_PATH, src.value.substring(1)) - : path.resolve(path.dirname(casePath), src.value); - return scripts.concat([fs.readFileSync(resolvedPath, 'utf-8')]); - } + return scriptElements + .reduce((scripts: string[], el: slimdom.Element) => { + const src = el.attributes.find(a => a.name === 'src'); + if (src) { + const resolvedPath = src.value.startsWith('/') + ? path.resolve(process.env.WEB_PLATFORM_TESTS_PATH, src.value.substring(1)) + : path.resolve(path.dirname(casePath), src.value); + return scripts.concat([fs.readFileSync(resolvedPath, 'utf-8')]); + } - return scripts.concat([getAllText(el, '#text')]); - }, []).join('\n'); + return scripts.concat([getAllText(el, '#text')]); + }, []) + .join('\n'); } -function createTest (casePath: string, blacklistReason: { [key: string]: string } = {}): void { - const document = parse5.parse(fs.readFileSync(casePath, 'utf-8'), { treeAdapter: new SlimdomTreeAdapter }) as slimdom.Document; +function createTest(casePath: string, blacklistReason: { [key: string]: string } = {}): void { + const document = parse5.parse(fs.readFileSync(casePath, 'utf-8'), { + treeAdapter: new SlimdomTreeAdapter() + }) as slimdom.Document; const title = getAllText(document, 'html', 'head', 'title', '#text') || path.basename(casePath); const script = getAllScripts(document, casePath); const scriptAsFunction = new Function('stubEnvironment', `with (stubEnvironment) { ${script} }`); - let stubs: { global: any, onLoadCallbacks: Function[], onErrorCallback?: Function }; + let stubs: { global: any; onLoadCallbacks: Function[]; onErrorCallback?: Function }; - function createStubEnvironment (document: slimdom.Document): { global: any, onLoadCallbacks: Function[], onErrorCallback?: Function } { + function createStubEnvironment( + document: slimdom.Document + ): { global: any; onLoadCallbacks: Function[]; onErrorCallback?: Function } { const onLoadCallbacks: Function[] = []; let onErrorCallback: Function | undefined = undefined; let global: any = { @@ -382,7 +400,7 @@ function createTest (casePath: string, blacklistReason: { [key: string]: string location: { href: casePath }, window: null, - get frames () { + get frames() { return (document as any).getElementsByTagName('iframe').map((iframe: any) => { if (!iframe.contentWindow) { const stubs = createStubEnvironment(document.implementation.createHTMLDocument()); @@ -395,7 +413,7 @@ function createTest (casePath: string, blacklistReason: { [key: string]: string }); }, - addEventListener (event: string, cb: Function) { + addEventListener(event: string, cb: Function) { switch (event) { case 'load': onLoadCallbacks.push(cb); @@ -410,7 +428,7 @@ function createTest (casePath: string, blacklistReason: { [key: string]: string }, ...slimdom - } + }; global.window = global; global.parent = global; global.self = global; @@ -432,7 +450,7 @@ function createTest (casePath: string, blacklistReason: { [key: string]: string return; } - stubs.global.add_completion_callback(function (tests: any[], testStatus: any) { + stubs.global.add_completion_callback(function(tests: any[], testStatus: any) { // TODO: Seems to be triggered by duplicate names in the createDocument tests //chai.assert.equal(testStatus.status, testStatus.OK, testStatus.message); tests.forEach(test => { @@ -452,23 +470,21 @@ function createTest (casePath: string, blacklistReason: { [key: string]: string iframe.onload(); } }); - } - catch (e) { + } catch (e) { if (e instanceof chai.AssertionError) { throw e; } if (stubs.onErrorCallback) { stubs.onErrorCallback(e); - } - else { + } else { throw e; } } }); } -function createTests (dirPath: string): void { +function createTests(dirPath: string): void { fs.readdirSync(dirPath).forEach(entry => { const entryPath = path.join(dirPath, entry); const relativePath = path.relative(process.env.WEB_PLATFORM_TESTS_PATH, entryPath); @@ -489,7 +505,7 @@ function createTests (dirPath: string): void { if (entry.endsWith('.html')) { createTest(entryPath, blacklistReason); } - }) + }); } describe('web platform DOM test suite', () => { @@ -498,8 +514,11 @@ describe('web platform DOM test suite', () => { return; } - (slimdom.Document.prototype as any).getElementsByTagName = function (this: slimdom.Document, tagName: string): slimdom.Node[] { - return (function getElementsByTagName (node: slimdom.Node): slimdom.Node[] { + (slimdom.Document.prototype as any).getElementsByTagName = function( + this: slimdom.Document, + tagName: string + ): slimdom.Node[] { + return (function getElementsByTagName(node: slimdom.Node): slimdom.Node[] { return node.childNodes.reduce((elements, child) => { if (child.nodeName === tagName) { elements.push(child); @@ -514,8 +533,11 @@ describe('web platform DOM test suite', () => { })(this); }; - (slimdom.Document.prototype as any).getElementById = function getElementById (this: slimdom.Node, id: string): slimdom.Node | null { - return (function getElementById (node: slimdom.Node): slimdom.Node | null { + (slimdom.Document.prototype as any).getElementById = function getElementById( + this: slimdom.Node, + id: string + ): slimdom.Node | null { + return (function getElementById(node: slimdom.Node): slimdom.Node | null { for (let child = node.firstChild; child; child = child.nextSibling) { if (child.nodeType === slimdom.Node.ELEMENT_NODE && (child as slimdom.Element).getAttribute('id') === id) { return child; @@ -557,12 +579,12 @@ describe('web platform DOM test suite', () => { value: 'null' }, body: { - get () { + get() { return this.getElementsByTagName('body')[0] || null; } }, title: { - get () { + get() { return getAllText(this, 'html', 'head', 'title', '#text'); } } @@ -576,14 +598,14 @@ describe('web platform DOM test suite', () => { value: true }, textContent: { - get () { + get() { return this.nodeValue; } } }); Object.defineProperties(slimdom.CharacterData.prototype, { textContent: { - get () { + get() { return this.nodeValue; } } From 5c63b727b3bc3b2b08f21c962841bbbab85a9ecc Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Wed, 14 Jun 2017 16:46:28 +0200 Subject: [PATCH 14/34] Implement global object to make constructors follow the spec. This also serves to inject all constructors where possible, avoiding cyclic dependencies. --- src/Attr.ts | 12 +-- src/CDATASection.ts | 13 ++-- src/CharacterData.ts | 9 ++- src/Comment.ts | 21 +++--- src/DOMImplementation.ts | 17 +++-- src/Document.ts | 61 +++++++++++---- src/DocumentFragment.ts | 20 ++--- src/DocumentType.ts | 10 ++- src/Element.ts | 20 +++-- src/Node.ts | 9 --- src/ProcessingInstruction.ts | 15 ++-- src/Range.ts | 19 +++-- src/Text.ts | 23 +++--- src/XMLDocument.ts | 4 +- src/context/Context.ts | 75 +++++++++++++++++++ src/index.ts | 40 ++++++++-- src/util/cloneNode.ts | 1 + test/Comment.tests.ts | 14 ++-- test/Document.tests.ts | 31 +++----- test/Element.tests.ts | 38 ++++------ test/MutationObserver.tests.ts | 26 +++---- test/ProcessingInstruction.tests.ts | 14 ++-- test/Range.tests.ts | 27 +++---- test/Text.tests.ts | 19 ++--- test/web-platform-tests/SlimdomTreeAdapter.ts | 2 +- test/web-platform-tests/webPlatform.tests.ts | 4 +- 26 files changed, 335 insertions(+), 209 deletions(-) create mode 100644 src/context/Context.ts diff --git a/src/Attr.ts b/src/Attr.ts index cefcad2..f74725e 100644 --- a/src/Attr.ts +++ b/src/Attr.ts @@ -1,6 +1,7 @@ import Document from './Document'; import Element from './Element'; import Node from './Node'; +import { getContext } from './context/Context'; import { changeAttribute } from './util/attrMutations'; import { NodeType } from './util/NodeType'; @@ -55,7 +56,6 @@ export default class Attr extends Node { /** * (non-standard) use Document#createAttribute(NS) or Element#setAttribute(NS) to create attribute nodes * - * @param document The node document to associate with the attribute * @param namespace The namespace URI for the attribute * @param prefix The prefix for the attribute * @param localName The local name for the attribute @@ -63,21 +63,20 @@ export default class Attr extends Node { * @param element The element for the attribute, or null if the attribute is not attached to an element */ constructor( - document: Document, namespace: string | null, prefix: string | null, localName: string, value: string, element: Element | null ) { - super(document); + super(); + this.namespaceURI = namespace; this.prefix = prefix; this.localName = localName; this.name = prefix === null ? localName : `${prefix}:${localName}`; this._value = value; this.ownerElement = element; - this.ownerDocument = document; } /** @@ -89,7 +88,10 @@ export default class Attr extends Node { */ public _copy(document: Document): Attr { // Set copy’s namespace, namespace prefix, local name, and value, to those of node. - return new Attr(document, this.namespaceURI, this.prefix, this.localName, this.value, null); + const context = getContext(document); + const copy = new context.Attr(this.namespaceURI, this.prefix, this.localName, this.value, null); + copy.ownerDocument = document; + return copy; } } diff --git a/src/CDATASection.ts b/src/CDATASection.ts index 330c89e..1820d70 100644 --- a/src/CDATASection.ts +++ b/src/CDATASection.ts @@ -1,5 +1,6 @@ import Document from './Document'; import Text from './Text'; +import { getContext } from './context/Context'; import { NodeType } from './util/NodeType'; export default class CDATASection extends Text { @@ -18,11 +19,10 @@ export default class CDATASection extends Text { /** * (non-standard) use Document#createCDATASection to create a CDATA section. * - * @param document (non-standard) The node document to associate with the node - * @param data The data for the node + * @param data The data for the node */ - constructor(document: Document, data: string) { - super(document, data); + constructor(data: string) { + super(data); } /** @@ -34,6 +34,9 @@ export default class CDATASection extends Text { */ public _copy(document: Document): CDATASection { // Set copy’s data, to that of node. - return new CDATASection(document, this.data); + const context = getContext(document); + const copy = new context.CDATASection(this.data); + copy.ownerDocument = document; + return copy; } } diff --git a/src/CharacterData.ts b/src/CharacterData.ts index 7b2ea7c..3eda009 100644 --- a/src/CharacterData.ts +++ b/src/CharacterData.ts @@ -56,11 +56,12 @@ export default abstract class CharacterData extends Node implements NonDocumentT } /** - * @param document The node document to associate with the node - * @param data The data to associate with the node + * (non-standard) CharacterData should never be instantiated directly. + * + * @param data The data to associate with the node */ - protected constructor(document: Document, data: string) { - super(document); + protected constructor(data: string) { + super(); this._data = data; } diff --git a/src/Comment.ts b/src/Comment.ts index f0522cc..b23fac6 100644 --- a/src/Comment.ts +++ b/src/Comment.ts @@ -1,5 +1,6 @@ import CharacterData from './CharacterData'; import Document from './Document'; +import { getContext } from './context/Context'; import { NodeType } from './util/NodeType'; export default class Comment extends CharacterData { @@ -16,16 +17,15 @@ export default class Comment extends CharacterData { // Comment /** - * Returns a new Comment node whose data is data. + * Returns a new Comment node whose data is data and node document is current global object’s associated Document. * - * Non-standard: as this implementation does not have a document associated with the global object, it is required - * to pass a document to this constructor. - * - * @param document (non-standard) The node document to associate with the new comment - * @param data The data for the new comment + * @param data The data for the new comment */ - constructor(document: Document, data: string = '') { - super(document, data); + constructor(data: string = '') { + super(data); + + const context = getContext(this); + this.ownerDocument = context.document; } /** @@ -37,6 +37,9 @@ export default class Comment extends CharacterData { */ public _copy(document: Document): Comment { // Set copy’s data, to that of node. - return new Comment(document, this.data); + const context = getContext(document); + const copy = new context.Comment(this.data); + copy.ownerDocument = document; + return copy; } } diff --git a/src/DOMImplementation.ts b/src/DOMImplementation.ts index d863bb3..6836c64 100644 --- a/src/DOMImplementation.ts +++ b/src/DOMImplementation.ts @@ -2,7 +2,7 @@ import Document from './Document'; import DocumentType from './DocumentType'; import { createElement } from './Element'; import XMLDocument from './XMLDocument'; - +import { getContext } from './context/Context'; import createElementNS from './util/createElementNS'; import { expectArity } from './util/errorHelpers'; import { validateQualifiedName } from './util/namespaceHelpers'; @@ -37,7 +37,10 @@ export default class DOMImplementation { // 2. Return a new doctype, with qualifiedName as its name, publicId as its public ID, and systemId as its // system ID, and with its node document set to the associated document of the context object. - return new DocumentType(this._document, qualifiedName, publicId, systemId); + const context = getContext(this._document); + const doctype = new context.DocumentType(qualifiedName, publicId, systemId); + doctype.ownerDocument = this._document; + return doctype; } /** @@ -62,7 +65,8 @@ export default class DOMImplementation { doctype = asNullableObject(doctype, DocumentType); // 1. Let document be a new XMLDocument. - const document = new XMLDocument(); + const context = getContext(this._document); + const document = new context.XMLDocument(); // 2. Let element be null. let element = null; @@ -107,13 +111,16 @@ export default class DOMImplementation { title = asNullableString(title); // 1. Let doc be a new document that is an HTML document. - const doc = new Document(); + const context = getContext(this._document); + const doc = new context.Document(); // 2. Set doc’s content type to "text/html". // (content type not implemented) // 3. Append a new doctype, with "html" as its name and with its node document set to doc, to doc. - doc.appendChild(new DocumentType(doc, 'html')); + const doctype = new context.DocumentType('html'); + doctype.ownerDocument = doc; + doc.appendChild(doctype); // 4. Append the result of creating an element given doc, html, and the HTML namespace, to doc. const htmlElement = createElement(doc, 'html', HTML_NAMESPACE); diff --git a/src/Document.ts b/src/Document.ts index 15a4456..8dfc652 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -10,7 +10,7 @@ import Node from './Node'; import ProcessingInstruction from './ProcessingInstruction'; import Text from './Text'; import Range from './Range'; - +import { getContext } from './context/Context'; import cloneNode from './util/cloneNode'; import createElementNS from './util/createElementNS'; import { throwInvalidCharacterError, throwNotSupportedError } from './util/errorHelpers'; @@ -75,7 +75,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * document (Document object). */ constructor() { - super(null); + super(); } /** @@ -133,16 +133,19 @@ export default class Document extends Node implements NonElementParentNode, Pare } /** - * Creates a new DocumentFragment node. + * Returns a new DocumentFragment node with its node document set to the context object. * * @return The new document fragment */ public createDocumentFragment(): DocumentFragment { - return new DocumentFragment(this); + const context = getContext(this); + const documentFragment = new context.DocumentFragment(); + documentFragment.ownerDocument = this; + return documentFragment; } /** - * Creates a new text node with the given data. + * Returns a new Text node with its data set to data and node document set to the context object. * * @param data Data for the new text node * @@ -151,11 +154,14 @@ export default class Document extends Node implements NonElementParentNode, Pare public createTextNode(data: string): Text { data = String(data); - return new Text(this, data); + const context = getContext(this); + const text = new context.Text(data); + text.ownerDocument = this; + return text; } /** - * Creates a new CDATA section with the given data. + * Returns a new CDATA section with the given data and node document set to the context object. * * @param data Data for the new CDATA section * @@ -173,11 +179,14 @@ export default class Document extends Node implements NonElementParentNode, Pare } // 3. Return a new CDATASection node with its data set to data and node document set to the context object. - return new CDATASection(this, data); + const context = getContext(this); + const cdataSection = new context.CDATASection(data); + cdataSection.ownerDocument = this; + return cdataSection; } /** - * Creates a new comment node with the given data. + * Returns a new Comment node with its data set to data and node document set to the context object. * * @param data Data for the new comment * @@ -186,11 +195,15 @@ export default class Document extends Node implements NonElementParentNode, Pare public createComment(data: string): Comment { data = String(data); - return new Comment(this, data); + const context = getContext(this); + const comment = new context.Comment(data); + comment.ownerDocument = this; + return comment; } /** - * Creates a new processing instruction. + * Creates a new processing instruction node, with target set to target, data set to data, and node document set to + * the context object. * * @param target Target for the new processing instruction * @param data Data for the new processing instruction @@ -213,7 +226,10 @@ export default class Document extends Node implements NonElementParentNode, Pare // 3. Return a new ProcessingInstruction node, with target set to target, data set to data, and node document // set to the context object. - return new ProcessingInstruction(this, target, data); + const context = getContext(this); + const pi = new context.ProcessingInstruction(target, data); + pi.ownerDocument = this; + return pi; // Note: No check is performed that target contains "xml" or ":", or that data contains characters that match // the Char production. @@ -276,7 +292,10 @@ export default class Document extends Node implements NonElementParentNode, Pare // (html documents not implemented) // 3. Return a new attribute whose local name is localName and node document is context object. - return new Attr(this, null, null, localName, '', null); + const context = getContext(this); + const attr = new context.Attr(null, null, localName, '', null); + attr.ownerDocument = this; + return attr; } /** @@ -297,7 +316,10 @@ export default class Document extends Node implements NonElementParentNode, Pare // 2. Return a new attribute whose namespace is namespace, namespace prefix is prefix, local name is localName, // and node document is context object. - return new Attr(this, validatedNamespace, prefix, localName, '', null); + const context = getContext(this); + const attr = new context.Attr(validatedNamespace, prefix, localName, '', null); + attr.ownerDocument = this; + return attr; } /** @@ -309,7 +331,13 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new Range */ public createRange(): Range { - return new Range(this); + const context = getContext(this); + const range = new context.Range(); + range.startContainer = this; + range.startOffset = 0; + range.endContainer = this; + range.endOffset = 0; + return range; } /** @@ -323,6 +351,7 @@ export default class Document extends Node implements NonElementParentNode, Pare // Set copy’s encoding, content type, URL, origin, type, and mode, to those of node. // (properties not implemented) - return new Document(); + const context = getContext(document); + return new context.Document(); } } diff --git a/src/DocumentFragment.ts b/src/DocumentFragment.ts index a74d380..2d2eb39 100644 --- a/src/DocumentFragment.ts +++ b/src/DocumentFragment.ts @@ -2,6 +2,7 @@ import { NonElementParentNode, ParentNode, getChildren } from './mixins'; import Document from './Document'; import Element from './Element'; import Node from './Node'; +import { getContext } from './context/Context'; import { NodeType } from './util/NodeType'; export default class DocumentFragment extends Node implements NonElementParentNode, ParentNode { @@ -34,15 +35,13 @@ export default class DocumentFragment extends Node implements NonElementParentNo public childElementCount: number = 0; /** - * Creates a new DocumentFragment. - * - * Non-standard: as this implementation does not have a document associated with the global object, it is required - * to pass a document to this constructor. - * - * @param document (non-standard) The node document to associate with the new document fragment + * Return a new DocumentFragment node whose node document is current global object’s associated Document. */ - constructor(document: Document) { - super(document); + constructor() { + super(); + + const context = getContext(this); + this.ownerDocument = context.document; } /** @@ -53,6 +52,9 @@ export default class DocumentFragment extends Node implements NonElementParentNo * @return A shallow copy of the context object */ public _copy(document: Document): DocumentFragment { - return new DocumentFragment(document); + const context = getContext(document); + const copy = new context.DocumentFragment(); + copy.ownerDocument = document; + return copy; } } diff --git a/src/DocumentType.ts b/src/DocumentType.ts index 459877d..b5b2e9e 100644 --- a/src/DocumentType.ts +++ b/src/DocumentType.ts @@ -1,6 +1,7 @@ import { ChildNode } from './mixins'; import Document from './Document'; import Node from './Node'; +import { getContext } from './context/Context'; import { NodeType } from './util/NodeType'; export default class DocumentType extends Node implements ChildNode { @@ -46,8 +47,8 @@ export default class DocumentType extends Node implements ChildNode { * @param publicId The public ID of the doctype * @param systemId The system ID of the doctype */ - constructor(document: Document, name: string, publicId: string = '', systemId: string = '') { - super(document); + constructor(name: string, publicId: string = '', systemId: string = '') { + super(); this.name = name; this.publicId = publicId; @@ -63,6 +64,9 @@ export default class DocumentType extends Node implements ChildNode { */ public _copy(document: Document): DocumentType { // Set copy’s name, public ID, and system ID, to those of node. - return new DocumentType(document, this.name, this.publicId, this.systemId); + const context = getContext(document); + const copy = new context.DocumentType(this.name, this.publicId, this.systemId); + copy.ownerDocument = document; + return copy; } } diff --git a/src/Element.ts b/src/Element.ts index 5031098..3566673 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -3,7 +3,7 @@ import { getChildren, getPreviousElementSibling, getNextElementSibling } from '. import Attr from './Attr'; import Document from './Document'; import Node from './Node'; - +import { getContext } from './context/Context'; import { appendAttribute, changeAttribute, removeAttribute, replaceAttribute } from './util/attrMutations'; import { throwInUseAttributeError, throwInvalidCharacterError, throwNotFoundError } from './util/errorHelpers'; import { matchesNameProduction, validateAndExtract } from './util/namespaceHelpers'; @@ -62,13 +62,13 @@ export default class Element extends Node implements ParentNode, NonDocumentType /** * (non-standard) Use Document#createElement or Document#createElementNS to create an Element. * - * @param document Node document for the element * @param namespace Namespace for the element * @param prefix Prefix for the element * @param localName Local name for the element */ - constructor(document: Document, namespace: string | null, prefix: string | null, localName: string) { - super(document); + constructor(namespace: string | null, prefix: string | null, localName: string) { + super(); + this.namespaceURI = namespace; this.prefix = prefix; this.localName = localName; @@ -157,7 +157,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType // 4. If attribute is null, create an attribute whose local name is qualifiedName, value is value, and node // document is context object’s node document, then append this attribute to context object, and then return. if (attribute === null) { - const attribute = new Attr(this.ownerDocument!, null, null, qualifiedName, value, this); + const context = getContext(this); + const attribute = new context.Attr(null, null, qualifiedName, value, this); + attribute.ownerDocument = this.ownerDocument; appendAttribute(attribute, this); return; } @@ -410,7 +412,9 @@ export function createElement( // 7.2. Set result to a new element that implements interface, with no attributes, namespace set to namespace, // namespace prefix set to prefix, local name set to localName, custom element state set to "uncustomized", custom // element definition set to null, is value set to is, and node document set to document. - result = new Element(document, namespace, prefix, localName); + const context = getContext(document); + result = new context.Element(namespace, prefix, localName); + result.ownerDocument = document; // If namespace is the HTML namespace, and either localName is a valid custom element name or is is non-null, then // set result’s custom element state to "undefined". @@ -547,7 +551,9 @@ function setAttributeValue( // is localName, value is value, and node document is element’s node document, then append this attribute to // element, and then return. if (attribute === null) { - const attribute = new Attr(element.ownerDocument!, namespace, prefix, localName, value, element); + const context = getContext(element); + const attribute = new context.Attr(namespace, prefix, localName, value, element); + attribute.ownerDocument = element.ownerDocument; appendAttribute(attribute, element); return; } diff --git a/src/Node.ts b/src/Node.ts index ffb0af4..d75036a 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -99,15 +99,6 @@ export default abstract class Node { */ public _registeredObservers: RegisteredObservers = new RegisteredObservers(this); - /** - * (non-standard) Node should never be instantiated directly. - * - * @param document The node document to associate with the node - */ - constructor(document: Document | null) { - this.ownerDocument = document; - } - /** * Puts the specified node and all of its subtree into a "normalized" form. In a normalized subtree, no text nodes * in the subtree are empty and there are no adjacent text nodes. diff --git a/src/ProcessingInstruction.ts b/src/ProcessingInstruction.ts index c7b80a1..89ec73d 100644 --- a/src/ProcessingInstruction.ts +++ b/src/ProcessingInstruction.ts @@ -1,5 +1,6 @@ import CharacterData from './CharacterData'; import Document from './Document'; +import { getContext } from './context/Context'; import { NodeType } from './util/NodeType'; /** @@ -23,11 +24,12 @@ export default class ProcessingInstruction extends CharacterData { /** * (non-standard) Use Document#createProcessingInstruction to create a processing instruction. * - * @param document The node document to associate with the processing instruction - * @param target The target of the processing instruction + * @param target The target of the processing instruction + * @param data The data of the processing instruction */ - constructor(document: Document, target: string, data: string) { - super(document, data); + constructor(target: string, data: string) { + super(data); + this.target = target; } @@ -40,6 +42,9 @@ export default class ProcessingInstruction extends CharacterData { */ public _copy(document: Document): ProcessingInstruction { // Set copy’s target and data to those of node. - return new ProcessingInstruction(document, this.target, this.data); + const context = getContext(document); + const copy = new context.ProcessingInstruction(this.target, this.data); + copy.ownerDocument = document; + return copy; } } diff --git a/src/Range.ts b/src/Range.ts index 72925ee..0076a7a 100644 --- a/src/Range.ts +++ b/src/Range.ts @@ -1,5 +1,6 @@ import Document from './Document'; import Node from './Node'; +import { getContext } from './context/Context'; import { throwIndexSizeError, throwInvalidNodeTypeError, @@ -54,17 +55,14 @@ export default class Range { } /** - * (non-standard) Use Document#createRange instead. - * - * Note: the spec defines a constructor with no arguments. This implementation can not implement that version, as - * we don't associate any Document with the current global object. - * - * @param document The document in which to initialize the Range + * The Range() constructor, when invoked, must return a new range with (current global object’s associated Document, + * 0) as its start and end. */ - constructor(document: Document) { - this.startContainer = document; + constructor() { + const context = getContext(this); + this.startContainer = context.document; this.startOffset = 0; - this.endContainer = document; + this.endContainer = context.document; this.endOffset = 0; ranges.push(this); } @@ -383,7 +381,8 @@ export default class Range { * @return A copy of the context object */ cloneRange(): Range { - const range = new Range(getNodeDocument(this.startContainer)); + const context = getContext(this); + const range = new context.Range(); range.startContainer = this.startContainer; range.startOffset = this.startOffset; range.endContainer = this.endContainer; diff --git a/src/Text.ts b/src/Text.ts index 1099413..34910d6 100644 --- a/src/Text.ts +++ b/src/Text.ts @@ -1,6 +1,7 @@ import { replaceData, substringData, default as CharacterData } from './CharacterData'; import Document from './Document'; import { ranges } from './Range'; +import { getContext } from './context/Context'; import { throwIndexSizeError } from './util/errorHelpers'; import { insertNode } from './util/mutationAlgorithms'; import { NodeType } from './util/NodeType'; @@ -23,16 +24,15 @@ export default class Text extends CharacterData { // Text /** - * Returns a new Text node whose data is data. + * Returns a new Text node whose data is data and node document is current global object’s associated Document. * - * Non-standard: as this implementation does not have a document associated with the global object, it is required - * to pass a document to this constructor. - * - * @param document (non-standard) The node document for the new node * @param data The data for the new text node */ - constructor(document: Document, data: string = '') { - super(document, data); + constructor(data: string = '') { + super(data); + + const context = getContext(this); + this.ownerDocument = context.document; } /** @@ -55,7 +55,10 @@ export default class Text extends CharacterData { */ public _copy(document: Document): Text { // Set copy’s data, to that of node. - return new Text(document, this.data); + const context = getContext(document); + const copy = new context.Text(this.data); + copy.ownerDocument = document; + return copy; } } @@ -83,7 +86,9 @@ function splitText(node: Text, offset: number): Text { const newData = substringData(node, offset, count); // 5. Let new node be a new Text node, with the same node document as node. Set new node’s data to new data. - const newNode = new Text(node.ownerDocument!, newData); + const context = getContext(node); + const newNode = new context.Text(newData); + newNode.ownerDocument = node.ownerDocument; // 6. Let parent be node’s parent. const parent = node.parentNode; diff --git a/src/XMLDocument.ts b/src/XMLDocument.ts index d7e5962..cdd2149 100644 --- a/src/XMLDocument.ts +++ b/src/XMLDocument.ts @@ -1,4 +1,5 @@ import Document from './Document'; +import { getContext } from './context/Context'; export default class XMLDocument extends Document { /** @@ -12,6 +13,7 @@ export default class XMLDocument extends Document { // Set copy’s encoding, content type, URL, origin, type, and mode, to those of node. // (properties not implemented) - return new XMLDocument(); + const context = getContext(document); + return new context.XMLDocument(); } } diff --git a/src/context/Context.ts b/src/context/Context.ts new file mode 100644 index 0000000..9e4867e --- /dev/null +++ b/src/context/Context.ts @@ -0,0 +1,75 @@ +import Attr from '../Attr'; +import CDATASection from '../CDATASection'; +import Comment from '../Comment'; +import Document from '../Document'; +import DocumentFragment from '../DocumentFragment'; +import DocumentType from '../DocumentType'; +import DOMImplementation from '../DOMImplementation'; +import Element from '../Element'; +import Node from '../Node'; +import ProcessingInstruction from '../ProcessingInstruction'; +import Range from '../Range'; +import Text from '../Text'; +import XMLDocument from '../XMLDocument'; + +import { NodeType } from '../util/NodeType'; + +export type AttrConstructor = new (namespace: string | null, prefix: + | string + | null, localName: string, value: string, element: Element | null) => Attr; +export type CDATASectionConstructor = new (data: string) => CDATASection; +export type CommentConstructor = new (data: string) => Comment; +export type DocumentConstructor = new () => Document; +export type DocumentFragmentConstructor = new () => DocumentFragment; +export type DocumentTypeConstructor = new (name: string, publicId?: string, systemId?: string) => DocumentType; +export type DOMImplementationConstructor = new (document: Document) => DOMImplementation; +export type ElementConstructor = new (namespace: string | null, prefix: string | null, localName: string) => Element; +export type ProcessingInstructionConstructor = new (target: string, data: string) => ProcessingInstruction; +export type RangeConstructor = new () => Range; +export type TextConstructor = new (data: string) => Text; +export type XMLDocumentConstructor = new () => XMLDocument; + +export interface Context { + document: Document; + + Attr: AttrConstructor; + CDATASection: CDATASectionConstructor; + Comment: CommentConstructor; + Document: DocumentConstructor; + DocumentFragment: DocumentFragmentConstructor; + DocumentType: DocumentTypeConstructor; + DOMImplementation: DOMImplementationConstructor; + Element: ElementConstructor; + ProcessingInstruction: ProcessingInstructionConstructor; + Range: RangeConstructor; + Text: TextConstructor; + XMLDocument: XMLDocumentConstructor; +} + +/** + * The DefaultContext is comparable to the global object in that it tracks its associated document. It also serves as a + * way to inject the constructors for the constructable types, avoiding cyclic dependencies. + */ +export class DefaultContext implements Context { + public document: Document; + + public Attr: AttrConstructor; + public CDATASection: CDATASectionConstructor; + public Comment: CommentConstructor; + public Document: DocumentConstructor; + public DocumentFragment: DocumentFragmentConstructor; + public DocumentType: DocumentTypeConstructor; + public DOMImplementation: DOMImplementationConstructor; + public Element: ElementConstructor; + public ProcessingInstruction: ProcessingInstructionConstructor; + public Range: RangeConstructor; + public Text: TextConstructor; + public XMLDocument: XMLDocumentConstructor; +} + +// TODO: make it possible to create multiple contexts by binding constructors to each instance +export const defaultContext = new DefaultContext(); + +export function getContext(instance: Node | Range): Context { + return defaultContext; +} diff --git a/src/index.ts b/src/index.ts index 40df935..45e1ddc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ -import XMLDocument from './XMLDocument'; - export { default as Attr } from './Attr'; +export { default as CDATASection } from './CDATASection'; export { default as CharacterData } from './CharacterData'; export { default as Comment } from './Comment'; export { default as Document } from './Document'; @@ -15,6 +14,37 @@ export { default as Text } from './Text'; export { default as XMLDocument } from './XMLDocument'; export { default as MutationObserver } from './mutation-observer/MutationObserver'; -export function createDocument(): XMLDocument { - return new XMLDocument(); -} +// To avoid cyclic dependencies and enable multiple contexts with their own constructors later, inject all constructors +// as well as the global document into the default context (i.e., global object) here. +import { defaultContext } from './context/Context'; + +import Attr from './Attr'; +import CDATASection from './CDATASection'; +import Comment from './Comment'; +import Document from './Document'; +import DocumentFragment from './DocumentFragment'; +import DocumentType from './DocumentType'; +import DOMImplementation from './DOMImplementation'; +import Element from './Element'; +import ProcessingInstruction from './ProcessingInstruction'; +import Range from './Range'; +import Text from './Text'; +import XMLDocument from './XMLDocument'; +import MutationObserver from './mutation-observer/MutationObserver'; + +// Document to associate with the global object +export const document = new Document(); +defaultContext.document = document; + +defaultContext.Attr = Attr; +defaultContext.CDATASection = CDATASection; +defaultContext.Comment = Comment; +defaultContext.Document = Document; +defaultContext.DocumentFragment = DocumentFragment; +defaultContext.DocumentType = DocumentType; +defaultContext.DOMImplementation = DOMImplementation; +defaultContext.Element = Element; +defaultContext.ProcessingInstruction = ProcessingInstruction; +defaultContext.Range = Range; +defaultContext.Text = Text; +defaultContext.XMLDocument = XMLDocument; diff --git a/src/util/cloneNode.ts b/src/util/cloneNode.ts index 5e45eb9..0a4f9b1 100644 --- a/src/util/cloneNode.ts +++ b/src/util/cloneNode.ts @@ -1,6 +1,7 @@ import Document from '../Document'; import Node from '../Node'; +import { isNodeOfType, NodeType } from './NodeType'; import { getNodeDocument } from './treeHelpers'; // 3.4. Interface Node diff --git a/test/Comment.tests.ts b/test/Comment.tests.ts index 3fb3326..b21a843 100644 --- a/test/Comment.tests.ts +++ b/test/Comment.tests.ts @@ -1,15 +1,11 @@ -import * as slimdom from '../src/index'; - -import Comment from '../src/Comment'; -import Document from '../src/Document'; - import * as chai from 'chai'; +import * as slimdom from '../src/index'; describe('Comment', () => { - let document: Document; - let comment: Comment; + let document: slimdom.Document; + let comment: slimdom.Comment; beforeEach(() => { - document = slimdom.createDocument(); + document = new slimdom.Document(); comment = document.createComment('somedata'); }); @@ -21,7 +17,7 @@ describe('Comment', () => { }); it('can be cloned', () => { - var clone = comment.cloneNode(true) as Comment; + var clone = comment.cloneNode(true) as slimdom.Comment; chai.assert.equal(clone.nodeType, 8); chai.assert.equal(clone.nodeValue, 'somedata'); chai.assert.equal(clone.data, 'somedata'); diff --git a/test/Document.tests.ts b/test/Document.tests.ts index 28652a6..51a770b 100644 --- a/test/Document.tests.ts +++ b/test/Document.tests.ts @@ -1,22 +1,15 @@ -import * as slimdom from '../src/index'; - -import Document from '../src/Document'; -import DOMImplementation from '../src/DOMImplementation'; -import Element from '../src/Element'; -import Node from '../src/Node'; -import ProcessingInstruction from '../src/ProcessingInstruction'; - import * as chai from 'chai'; +import * as slimdom from '../src/index'; describe('Document', () => { - let document: Document; + let document: slimdom.Document; beforeEach(() => { - document = slimdom.createDocument(); + document = new slimdom.Document(); }); it('has nodeType 9', () => chai.assert.equal(document.nodeType, 9)); - it('exposes its DOMImplementation', () => chai.assert.instanceOf(document.implementation, DOMImplementation)); + it('exposes its DOMImplementation', () => chai.assert.instanceOf(document.implementation, slimdom.DOMImplementation)); it('initially has no doctype', () => chai.assert.equal(document.doctype, null)); @@ -25,7 +18,7 @@ describe('Document', () => { it('initially has no childNodes', () => chai.assert.deepEqual(document.childNodes, [])); describe('after appending a child element', () => { - let element: Element; + let element: slimdom.Element; beforeEach(() => { element = document.createElement('test'); document.appendChild(element); @@ -48,7 +41,7 @@ describe('Document', () => { }); describe('after replacing the element', () => { - let otherElement: Element; + let otherElement: slimdom.Element; beforeEach(() => { otherElement = document.createElement('other'); document.replaceChild(otherElement, element); @@ -61,7 +54,7 @@ describe('Document', () => { }); describe('after appending a processing instruction', () => { - var processingInstruction: ProcessingInstruction; + var processingInstruction: slimdom.ProcessingInstruction; beforeEach(() => { processingInstruction = document.createProcessingInstruction('sometarget', 'somedata'); document.appendChild(processingInstruction); @@ -72,7 +65,7 @@ describe('Document', () => { it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [processingInstruction])); describe('after replacing with an element', () => { - let otherElement: Element; + let otherElement: slimdom.Element; beforeEach(() => { otherElement = document.createElement('other'); document.replaceChild(otherElement, processingInstruction); @@ -85,10 +78,10 @@ describe('Document', () => { }); describe('cloning', () => { - var clone: Document; + var clone: slimdom.Document; beforeEach(() => { document.appendChild(document.createElement('root')); - clone = document.cloneNode(true) as Document; + clone = document.cloneNode(true) as slimdom.Document; }); it('is a new document', () => { @@ -97,8 +90,8 @@ describe('Document', () => { }); it('has a new document element', () => { - chai.assert.equal((clone.documentElement as Node).nodeType, 1); - chai.assert.equal((clone.documentElement as Element).nodeName, 'root'); + chai.assert.equal((clone.documentElement as slimdom.Node).nodeType, 1); + chai.assert.equal((clone.documentElement as slimdom.Element).nodeName, 'root'); chai.assert.notEqual(clone.documentElement, document.documentElement); }); }); diff --git a/test/Element.tests.ts b/test/Element.tests.ts index 5d01a7e..a088f4a 100644 --- a/test/Element.tests.ts +++ b/test/Element.tests.ts @@ -1,19 +1,11 @@ -import * as slimdom from '../src/index'; - -import Attr from '../src/Attr'; -import Document from '../src/Document'; -import Element from '../src/Element'; -import Node from '../src/Node'; -import ProcessingInstruction from '../src/ProcessingInstruction'; -import Text from '../src/Text'; - import * as chai from 'chai'; +import * as slimdom from '../src/index'; describe('Element', () => { - let document: Document; - let element: Element; + let document: slimdom.Document; + let element: slimdom.Element; beforeEach(() => { - document = slimdom.createDocument(); + document = new slimdom.Document(); element = document.createElement('root'); }); @@ -61,7 +53,7 @@ describe('Element', () => { chai.assert.equal(element.getAttribute('noSuchAttribute'), null); }); - function hasAttributes(attributes: Attr[], expected: { name: string; value: string }[]): boolean { + function hasAttributes(attributes: slimdom.Attr[], expected: { name: string; value: string }[]): boolean { return ( attributes.length === expected.length && attributes.every(attr => expected.some(pair => pair.name === attr.name && pair.value === attr.value)) && @@ -120,7 +112,7 @@ describe('Element', () => { }); describe('after appending a child element', () => { - let child: Element; + let child: slimdom.Element; beforeEach(() => { child = document.createElement('child'); element.appendChild(child); @@ -159,7 +151,7 @@ describe('Element', () => { }); describe('after replacing the child element', () => { - let otherChild: Element; + let otherChild: slimdom.Element; beforeEach(() => { otherChild = document.createElement('other'); element.replaceChild(otherChild, child); @@ -180,7 +172,7 @@ describe('Element', () => { }); describe('after inserting an element before the child', () => { - let otherChild: Element; + let otherChild: slimdom.Element; beforeEach(() => { otherChild = document.createElement('other'); element.insertBefore(otherChild, child); @@ -213,7 +205,7 @@ describe('Element', () => { }); describe('after inserting an element after the child', () => { - let otherChild: Element; + let otherChild: slimdom.Element; beforeEach(() => { otherChild = document.createElement('other'); element.appendChild(otherChild); @@ -273,7 +265,7 @@ describe('Element', () => { }); describe('after appending a processing instruction', () => { - let processingInstruction: ProcessingInstruction; + let processingInstruction: slimdom.ProcessingInstruction; beforeEach(() => { processingInstruction = document.createProcessingInstruction('test', 'test'); element.appendChild(processingInstruction); @@ -293,10 +285,10 @@ describe('Element', () => { }); describe('after replacing with an element', () => { - let otherChild: Element; + let otherChild: slimdom.Element; beforeEach(() => { otherChild = document.createElement('other'); - element.replaceChild(otherChild, element.firstChild as Node); + element.replaceChild(otherChild, element.firstChild!); }); it('has child node references', () => { @@ -316,7 +308,7 @@ describe('Element', () => { describe('normalization', () => { it('removes empty text nodes', () => { - let textNode = element.appendChild(document.createTextNode('')) as Node; + let textNode = element.appendChild(document.createTextNode('')); element.normalize(); chai.assert.equal(textNode.parentNode, null); }); @@ -328,8 +320,8 @@ describe('Element', () => { chai.assert.equal(element.childNodes.length, 3); element.normalize(); chai.assert.equal(element.childNodes.length, 1); - chai.assert.equal((element.firstChild as Text).nodeValue, 'test123abc'); - chai.assert.equal((element.firstChild as Text).data, 'test123abc'); + chai.assert.equal((element.firstChild as slimdom.Text).nodeValue, 'test123abc'); + chai.assert.equal((element.firstChild as slimdom.Text).data, 'test123abc'); }); }); }); diff --git a/test/MutationObserver.tests.ts b/test/MutationObserver.tests.ts index 8503e2d..78efc32 100644 --- a/test/MutationObserver.tests.ts +++ b/test/MutationObserver.tests.ts @@ -1,12 +1,6 @@ -import * as slimdom from '../src/index'; - -import Document from '../src/Document'; -import Element from '../src/Element'; -import Text from '../src/Text'; -import MutationObserver from '../src/mutation-observer/MutationObserver'; - import * as chai from 'chai'; import * as lolex from 'lolex'; +import * as slimdom from '../src/index'; describe('MutationObserver', () => { let clock: lolex.Clock; @@ -25,17 +19,17 @@ describe('MutationObserver', () => { callbackArgs.push(args); } - let document: Document; - let element: Element; - let text: Text; - let observer: MutationObserver; + let document: slimdom.Document; + let element: slimdom.Element; + let text: slimdom.Text; + let observer: slimdom.MutationObserver; beforeEach(() => { callbackCalled = false; callbackArgs.length = 0; - document = slimdom.createDocument(); - element = document.appendChild(document.createElement('root')) as Element; - text = element.appendChild(document.createTextNode('text')) as Text; + document = new slimdom.Document(); + element = document.appendChild(document.createElement('root')) as slimdom.Element; + text = element.appendChild(document.createTextNode('text')) as slimdom.Text; observer = new slimdom.MutationObserver(callback); observer.observe(element, { subtree: true, @@ -193,8 +187,8 @@ describe('MutationObserver', () => { it('continues tracking under a removed node until javascript re-enters the event loop', () => { observer.observe(element, { subtree: true, characterDataOldValue: true, childList: true }); - const newElement = element.appendChild(document.createElement('meep')) as Element; - const newText = newElement.appendChild(document.createTextNode('test')) as Text; + const newElement = element.appendChild(document.createElement('meep')) as slimdom.Element; + const newText = newElement.appendChild(document.createTextNode('test')) as slimdom.Text; element.appendChild(newElement); observer.takeRecords(); diff --git a/test/ProcessingInstruction.tests.ts b/test/ProcessingInstruction.tests.ts index f7824ef..b0a1998 100644 --- a/test/ProcessingInstruction.tests.ts +++ b/test/ProcessingInstruction.tests.ts @@ -1,15 +1,11 @@ -import * as slimdom from '../src/index'; - -import Document from '../src/Document'; -import ProcessingInstruction from '../src/ProcessingInstruction'; - import * as chai from 'chai'; +import * as slimdom from '../src/index'; describe('ProcessingInstruction', () => { - let document: Document; - let processingInstruction: ProcessingInstruction; + let document: slimdom.Document; + let processingInstruction: slimdom.ProcessingInstruction; beforeEach(() => { - document = slimdom.createDocument(); + document = new slimdom.Document(); processingInstruction = document.createProcessingInstruction('sometarget', 'somedata'); }); @@ -25,7 +21,7 @@ describe('ProcessingInstruction', () => { }); it('can be cloned', () => { - var clone = processingInstruction.cloneNode(true) as ProcessingInstruction; + var clone = processingInstruction.cloneNode(true) as slimdom.ProcessingInstruction; chai.assert.equal(clone.nodeType, 7); chai.assert.equal(clone.nodeValue, 'somedata'); chai.assert.equal(clone.data, 'somedata'); diff --git a/test/Range.tests.ts b/test/Range.tests.ts index 0dc5bcd..07536f5 100644 --- a/test/Range.tests.ts +++ b/test/Range.tests.ts @@ -1,22 +1,15 @@ -import * as slimdom from '../src/index'; - -import Document from '../src/Document'; -import Element from '../src/Element'; -import Node from '../src/Node'; -import Text from '../src/Text'; -import Range from '../src/Range'; - import * as chai from 'chai'; +import * as slimdom from '../src/index'; describe('Range', () => { - let document: Document; - let element: Element; - let text: Text; - let range: Range; + let document: slimdom.Document; + let element: slimdom.Element; + let text: slimdom.Text; + let range: slimdom.Range; beforeEach(() => { - document = slimdom.createDocument(); - element = document.appendChild(document.createElement('root')) as Element; - text = element.appendChild(document.createTextNode('text')) as Text; + document = new slimdom.Document(); + element = document.appendChild(document.createElement('root')) as slimdom.Element; + text = element.appendChild(document.createTextNode('text')) as slimdom.Text; range = document.createRange(); }); @@ -114,7 +107,7 @@ describe('Range', () => { }); it('moves positions beyond a remove', () => { - element.removeChild(element.firstChild as Node); + element.removeChild(element.firstChild!); chai.assert.equal(range.startContainer, element); chai.assert.equal(range.startOffset, 0); chai.assert.equal(range.endContainer, element); @@ -221,7 +214,7 @@ describe('Range', () => { }); it('moves with text node merges during normalization', () => { - const otherText = element.appendChild(document.createTextNode('more')) as Node; + const otherText = element.appendChild(document.createTextNode('more')); range.setStartBefore(otherText); range.setEnd(otherText, 2); element.normalize(); diff --git a/test/Text.tests.ts b/test/Text.tests.ts index cb42e99..9fe7a51 100644 --- a/test/Text.tests.ts +++ b/test/Text.tests.ts @@ -1,16 +1,11 @@ -import * as slimdom from '../src/index'; - -import Document from '../src/Document'; -import Element from '../src/Element'; -import Text from '../src/Text'; - import * as chai from 'chai'; +import * as slimdom from '../src/index'; describe('Text', () => { - let document: Document; - let text: Text; + let document: slimdom.Document; + let text: slimdom.Text; beforeEach(() => { - document = slimdom.createDocument(); + document = new slimdom.Document(); text = document.createTextNode('text'); }); @@ -31,7 +26,7 @@ describe('Text', () => { }); it('can be cloned', () => { - var clone = text.cloneNode(true) as Text; + var clone = text.cloneNode(true) as slimdom.Text; chai.assert.equal(clone.nodeType, 3); chai.assert.equal(clone.nodeValue, 'text'); chai.assert.equal(clone.data, 'text'); @@ -143,8 +138,8 @@ describe('Text', () => { }); describe('under a parent', () => { - let element: Element; - let otherHalf: Text; + let element: slimdom.Element; + let otherHalf: slimdom.Text; beforeEach(() => { element = document.createElement('parent'); element.appendChild(text); diff --git a/test/web-platform-tests/SlimdomTreeAdapter.ts b/test/web-platform-tests/SlimdomTreeAdapter.ts index 236cb75..e575738 100644 --- a/test/web-platform-tests/SlimdomTreeAdapter.ts +++ b/test/web-platform-tests/SlimdomTreeAdapter.ts @@ -36,13 +36,13 @@ export default class SlimdomTreeAdapter implements parse5.AST.TreeAdapter { attrs.forEach(attr => { // Create Attr node without validation, as per HTML parser spec const attribute = new Attr( - this._globalDocument, undefinedAsNull(attr.namespace), undefinedAsNull(attr.prefix), attr.name, attr.value, element ); + attribute.ownerDocument = this._globalDocument; appendAttribute(attribute, element); }); return element; diff --git a/test/web-platform-tests/webPlatform.tests.ts b/test/web-platform-tests/webPlatform.tests.ts index fdaeb94..165906e 100644 --- a/test/web-platform-tests/webPlatform.tests.ts +++ b/test/web-platform-tests/webPlatform.tests.ts @@ -390,6 +390,8 @@ function createTest(casePath: string, blacklistReason: { [key: string]: string } const scriptAsFunction = new Function('stubEnvironment', `with (stubEnvironment) { ${script} }`); let stubs: { global: any; onLoadCallbacks: Function[]; onErrorCallback?: Function }; + const { document: _, ...domInterfaces } = slimdom; + function createStubEnvironment( document: slimdom.Document ): { global: any; onLoadCallbacks: Function[]; onErrorCallback?: Function } { @@ -427,7 +429,7 @@ function createTest(casePath: string, blacklistReason: { [key: string]: string } } }, - ...slimdom + ...domInterfaces }; global.window = global; global.parent = global; From 6e61080ffa0d2a812cc1c40f13a2edd20535199f Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Thu, 15 Jun 2017 14:30:53 +0200 Subject: [PATCH 15/34] Implement Node's namespace / prefix lookup methods. --- src/Attr.ts | 35 +++++++++++ src/CharacterData.ts | 38 ++++++++++- src/Document.ts | 34 ++++++++++ src/DocumentFragment.ts | 20 ++++++ src/DocumentType.ts | 20 ++++++ src/Element.ts | 66 +++++++++++++++++++- src/Node.ts | 42 ++++++++++++- src/util/namespaceHelpers.ts | 33 +++++++++- test/web-platform-tests/webPlatform.tests.ts | 2 - 9 files changed, 284 insertions(+), 6 deletions(-) diff --git a/src/Attr.ts b/src/Attr.ts index f74725e..5481e7d 100644 --- a/src/Attr.ts +++ b/src/Attr.ts @@ -4,6 +4,7 @@ import Node from './Node'; import { getContext } from './context/Context'; import { changeAttribute } from './util/attrMutations'; import { NodeType } from './util/NodeType'; +import { asNullableString } from './util/typeHelpers'; /** * 3.9.2. Interface Attr @@ -34,6 +35,40 @@ export default class Attr extends Node { setExistingAttributeValue(this, newValue); } + public lookupPrefix(namespace: string | null): string | null { + namespace = asNullableString(namespace); + + // 1. If namespace is null or the empty string, then return null. + if (namespace === null || namespace === '') { + return null; + } + + // 2. Switch on the context object: + // Attr - Return the result of locating a namespace prefix for its element, if its element is non-null, and null + // otherwise. + if (this.ownerElement !== null) { + return this.ownerElement.lookupPrefix(namespace); + } + + return null; + } + + public lookupNamespaceURI(prefix: string | null): string | null { + // 1. If prefix is the empty string, then set it to null. + // (not necessary due to recursion) + + // 2. Return the result of running locate a namespace for the context object using prefix. + + // To locate a namespace for a node using prefix, switch on node: Attr + // 1. If its element is null, then return null. + if (this.ownerElement === null) { + return null; + } + + // 2. Return the result of running locate a namespace on its element using prefix. + return this.ownerElement.lookupNamespaceURI(prefix); + } + // Attr public readonly namespaceURI: string | null; diff --git a/src/CharacterData.ts b/src/CharacterData.ts index 3eda009..8f81c21 100644 --- a/src/CharacterData.ts +++ b/src/CharacterData.ts @@ -5,7 +5,7 @@ import Node from './Node'; import { ranges } from './Range'; import queueMutationRecord from './mutation-observer/queueMutationRecord'; import { expectArity, throwIndexSizeError } from './util/errorHelpers'; -import { asUnsignedLong, treatNullAsEmptyString } from './util/typeHelpers'; +import { asNullableString, asUnsignedLong, treatNullAsEmptyString } from './util/typeHelpers'; /** * 3.10. Interface CharacterData @@ -27,6 +27,42 @@ export default abstract class CharacterData extends Node implements NonDocumentT replaceData(this, 0, this.length, newValue); } + public lookupPrefix(namespace: string | null): string | null { + namespace = asNullableString(namespace); + + // 1. If namespace is null or the empty string, then return null. + if (namespace === null || namespace === '') { + return null; + } + + // 2. Switch on the context object: + // Any other node - Return the result of locating a namespace prefix for its parent element, if its parent + // element is non-null, and null otherwise. + const parentElement = this.parentElement; + if (parentElement !== null) { + return parentElement.lookupPrefix(namespace); + } + + return null; + } + + public lookupNamespaceURI(prefix: string | null): string | null { + // 1. If prefix is the empty string, then set it to null. + // (not necessary due to recursion) + + // 2. Return the result of running locate a namespace for the context object using prefix. + + // To locate a namespace for a node using prefix, switch on node: Any other node + // 1. If its parent element is null, then return null. + const parentElement = this.parentElement; + if (parentElement === null) { + return null; + } + + // 2. Return the result of running locate a namespace on its parent element using prefix. + return parentElement.lookupNamespaceURI(prefix); + } + // NonDocumentTypeChildNode previousElementSibling: Element | null = null; diff --git a/src/Document.ts b/src/Document.ts index 8dfc652..f30273a 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -41,6 +41,40 @@ export default class Document extends Node implements NonElementParentNode, Pare // Do nothing. } + public lookupPrefix(namespace: string | null): string | null { + namespace = asNullableString(namespace); + + // 1. If namespace is null or the empty string, then return null. + if (namespace === null || namespace === '') { + return null; + } + + // 2. Switch on the context object: + // Document - Return the result of locating a namespace prefix for its document element, if its document element + // is non-null, and null otherwise. + if (this.documentElement !== null) { + return this.documentElement.lookupPrefix(namespace); + } + + return null; + } + + public lookupNamespaceURI(prefix: string | null): string | null { + // 1. If prefix is the empty string, then set it to null. + // (not necessary due to recursion) + + // 2. Return the result of running locate a namespace for the context object using prefix. + + // To locate a namespace for a node using prefix, switch on node: Document + // 1. If its document element is null, then return null. + if (this.documentElement === null) { + return null; + } + + // 2. Return the result of running locate a namespace on its document element using prefix. + return this.documentElement.lookupNamespaceURI(prefix); + } + // ParentNode public get children(): Element[] { diff --git a/src/DocumentFragment.ts b/src/DocumentFragment.ts index 2d2eb39..6a1dfed 100644 --- a/src/DocumentFragment.ts +++ b/src/DocumentFragment.ts @@ -24,6 +24,26 @@ export default class DocumentFragment extends Node implements NonElementParentNo // Do nothing. } + public lookupPrefix(namespace: string | null): string | null { + // 1. If namespace is null or the empty string, then return null. + // (not necessary due to return value) + + // 2. Switch on the context object: + // DocumentFragment - Return null + return null; + } + + public lookupNamespaceURI(prefix: string | null): string | null { + // 1. If prefix is the empty string, then set it to null. + // (not necessary due to return value) + + // 2. Return the result of running locate a namespace for the context object using prefix. + + // To locate a namespace for a node using prefix, switch on node: DocumentFragment + // Return null. + return null; + } + // ParentNode public get children(): Element[] { diff --git a/src/DocumentType.ts b/src/DocumentType.ts index b5b2e9e..f0e0ff7 100644 --- a/src/DocumentType.ts +++ b/src/DocumentType.ts @@ -23,6 +23,26 @@ export default class DocumentType extends Node implements ChildNode { // Do nothing. } + public lookupPrefix(namespace: string | null): string | null { + // 1. If namespace is null or the empty string, then return null. + // (not necessary due to return value) + + // 2. Switch on the context object: + // DocumentType - Return null + return null; + } + + public lookupNamespaceURI(prefix: string | null): string | null { + // 1. If prefix is the empty string, then set it to null. + // (not necessary due to return value) + + // 2. Return the result of running locate a namespace for the context object using prefix. + + // To locate a namespace for a node using prefix, switch on node: DocumentType + // Return null. + return null; + } + // DocumentType /** diff --git a/src/Element.ts b/src/Element.ts index 3566673..ecdca1a 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -6,7 +6,12 @@ import Node from './Node'; import { getContext } from './context/Context'; import { appendAttribute, changeAttribute, removeAttribute, replaceAttribute } from './util/attrMutations'; import { throwInUseAttributeError, throwInvalidCharacterError, throwNotFoundError } from './util/errorHelpers'; -import { matchesNameProduction, validateAndExtract } from './util/namespaceHelpers'; +import { + matchesNameProduction, + validateAndExtract, + locateNamespacePrefix, + XMLNS_NAMESPACE +} from './util/namespaceHelpers'; import { NodeType } from './util/NodeType'; import { asNullableString } from './util/typeHelpers'; @@ -32,6 +37,65 @@ export default class Element extends Node implements ParentNode, NonDocumentType // Do nothing. } + public lookupPrefix(namespace: string | null): string | null { + namespace = asNullableString(namespace); + + // 1. If namespace is null or the empty string, then return null. + if (namespace === null || namespace === '') { + return null; + } + + // 2. Switch on the context object: + // Element - Return the result of locating a namespace prefix for it using namespace. + return locateNamespacePrefix(this, namespace); + } + + public lookupNamespaceURI(prefix: string | null): string | null { + prefix = asNullableString(prefix); + + // 1. If prefix is the empty string, then set it to null. + if (prefix === '') { + prefix = null; + } + + // 2. Return the result of running locate a namespace for the context object using prefix. + + // To locate a namespace for a node using prefix, switch on node: Element + // 1. If its namespace is not null and its namespace prefix is prefix, then return namespace. + if (this.namespaceURI !== null && this.prefix === prefix) { + return this.namespaceURI; + } + + // 2. If it has an attribute whose namespace is the XMLNS namespace, namespace prefix is "xmlns", and local name + // is prefix, or if prefix is null and it has an attribute whose namespace is the XMLNS namespace, namespace + // prefix is null, and local name is "xmlns", then return its value if it is not the empty string, and null + // otherwise. + let ns = null; + if (prefix !== null) { + const attr = this.getAttributeNodeNS(XMLNS_NAMESPACE, prefix); + if (attr && attr.prefix === 'xmlns') { + ns = attr.value; + } + } else { + const attr = this.getAttributeNodeNS(XMLNS_NAMESPACE, 'xmlns'); + if (attr && attr.prefix === null) { + ns = attr.value; + } + } + if (ns !== null) { + return ns !== '' ? ns : null; + } + + // 3. If its parent element is null, then return null. + const parentElement = this.parentElement; + if (parentElement === null) { + return null; + } + + // 4. Return the result of running locate a namespace on its parent element using prefix. + return parentElement.lookupNamespaceURI(prefix); + } + // ParentNode public get children(): Element[] { diff --git a/src/Node.ts b/src/Node.ts index d75036a..f1c60ff 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -8,7 +8,7 @@ import { expectArity } from './util/errorHelpers'; import { preInsertNode, appendNode, replaceChildWithNode, preRemoveChild, removeNode } from './util/mutationAlgorithms'; import { NodeType, isNodeOfType } from './util/NodeType'; import { getNodeDocument } from './util/treeHelpers'; -import { asNullableObject, asObject } from './util/typeHelpers'; +import { asNullableObject, asNullableString, asObject } from './util/typeHelpers'; /** * 3.4. Interface Node @@ -227,6 +227,46 @@ export default abstract class Node { return other === this; } + /** + * + * + * @param namespace The namespace to look up + * + * @return The prefix for the given namespace, or null if none was found + */ + public abstract lookupPrefix(namespace: string | null): string | null; + + /** + * Returns the namespace for the given prefix. + * + * @param prefix The prefix to look up + * + * @return The namespace for the given prefix, or null if the prefix is not defined + */ + public abstract lookupNamespaceURI(prefix: string | null): string | null; + + /** + * Return true if defaultNamespace is the same as namespace, and false otherwise. + * + * @param namespace The namespace to check + * + * @return Whether namespace is the default namespace + */ + public isDefaultNamespace(namespace: string | null): boolean { + namespace = asNullableString(namespace); + + // 1. If namespace is the empty string, then set it to null. + if (namespace === '') { + namespace = null; + } + + // 2. Let defaultNamespace be the result of running locate a namespace for context object using null. + const defaultNamespace = this.lookupNamespaceURI(null); + + // 3. Return true if defaultNamespace is the same as namespace, and false otherwise. + return defaultNamespace === namespace; + } + /** * Inserts the specified node before child within context object. * diff --git a/src/util/namespaceHelpers.ts b/src/util/namespaceHelpers.ts index 62db877..11af483 100644 --- a/src/util/namespaceHelpers.ts +++ b/src/util/namespaceHelpers.ts @@ -1,9 +1,11 @@ +import Element from '../Element'; +import Node from '../Node'; import { throwInvalidCharacterError, throwNamespaceError } from './errorHelpers'; // 1.5. Namespaces const XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace'; -const XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/'; +export const XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/'; /* // NAME_REGEX_XML_1_0_FOURTH_EDITION generated using regenerate: @@ -177,3 +179,32 @@ export function validateAndExtract( // 10. Return namespace, prefix, and localName. return { namespace, prefix, localName }; } + +/** + * To locate a namespace prefix for an element using namespace, run these steps: + * + * @param element The element at which to start the lookup + * @param namespace Namespace for which to look up the prefix + * + * @return The prefix, or null if there isn't one + */ +export function locateNamespacePrefix(element: Element, namespace: string | null): string | null { + // 1. If element’s namespace is namespace and its namespace prefix is not null, then return its namespace prefix. + if (element.namespaceURI === namespace && element.prefix !== null) { + return element.prefix; + } + + // 2. If element has an attribute whose namespace prefix is "xmlns" and value is namespace, then return element’s first such attribute’s local name. + const attr = Array.from(element.attributes).find(attr => attr.prefix === 'xmlns' && attr.value === namespace); + if (attr) { + return attr.localName; + } + + // 3. If element’s parent element is not null, then return the result of running locate a namespace prefix on that element using namespace. + if (element.parentElement !== null) { + return locateNamespacePrefix(element.parentElement, namespace); + } + + // 4. Return null. + return null; +} diff --git a/test/web-platform-tests/webPlatform.tests.ts b/test/web-platform-tests/webPlatform.tests.ts index 165906e..ff4cf68 100644 --- a/test/web-platform-tests/webPlatform.tests.ts +++ b/test/web-platform-tests/webPlatform.tests.ts @@ -297,8 +297,6 @@ const TEST_BLACKLIST: { [key: string]: (string | { [key: string]: string }) } = 'dom/nodes/Node-isEqualNode-iframe2.html': 'Node#isEqualNode not implemented', 'dom/nodes/Node-isSameNode.html': 'Node#isSameNode not implemented', 'dom/nodes/NodeList-Iterable.html': 'NodeList not implemented', - 'dom/nodes/Node-lookupNamespaceURI.html': 'Node#lookupNamespaceURI not implemented', - 'dom/nodes/Node-lookupPrefix.html': 'Node#lookupPrefix not implemented', 'dom/nodes/Node-nodeName.html': { 'For Element nodes, nodeName should return the same as tagName.': 'HTML tagName uppercasing not implemented' }, From 48e85e6dd8b3cec3dbafdcbc3dfd3c4d16f85e7f Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Fri, 16 Jun 2017 12:27:18 +0200 Subject: [PATCH 16/34] Determine coverage using nyc. This requires some restructuring in the way tests are transpiled, as nyc / istanbul seem to have trouble understanding the test/bin/test folder structure. --- .gitignore | 2 ++ package.json | 3 ++- test/tsconfig.json => tsconfig.test.json | 8 +++----- 3 files changed, 7 insertions(+), 6 deletions(-) rename test/tsconfig.json => tsconfig.test.json (80%) diff --git a/.gitignore b/.gitignore index c3204d8..8569a67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/.nyc_output/ +/coverage/ /dist/ /docs/ /lib/ diff --git a/package.json b/package.json index ab07905..6fe4242 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "build:bundle": "rimraf dist && rimraf lib && tsc && rollup -c", "docs": "typedoc --out docs --excludePrivate --excludeNotExported src/index.ts", "prepare": "npm run build:bundle", - "test": "rimraf test/bin && tsc -P test && mocha --timeout 20000 --recursive test/bin/test" + "test": "rimraf lib && tsc -P tsconfig.test.json && nyc --reporter html --reporter text --exclude lib/test mocha --timeout 20000 --recursive lib/test" }, "files": [ "dist" @@ -33,6 +33,7 @@ "chai": "^3.5.0", "lolex": "^1.6.0", "mocha": "^3.3.0", + "nyc": "^11.0.2", "parse5": "^3.0.2", "prettier": "^1.4.4", "rimraf": "^2.6.1", diff --git a/test/tsconfig.json b/tsconfig.test.json similarity index 80% rename from test/tsconfig.json rename to tsconfig.test.json index 3c8bb97..d6d625b 100644 --- a/test/tsconfig.json +++ b/tsconfig.test.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "outDir": "./bin/", + "outDir": "lib", "sourceMap": true, "strict": true, "module": "commonjs", @@ -16,9 +16,7 @@ "moduleResolution": "node" }, "include": [ - "./**/*" - ], - "exclude": [ - "bin" + "src/**/*", + "test/**/*" ] } From 07f73a8a5fb85251d9da3002549657eed1e898c9 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 17:08:07 +0200 Subject: [PATCH 17/34] Swap tsconfig files to restore type checking in the editor for tests. --- package.json | 11 ++++++----- tsconfig.test.json => tsconfig.build.json | 13 ++++--------- tsconfig.json | 15 ++++++++++----- 3 files changed, 20 insertions(+), 19 deletions(-) rename tsconfig.test.json => tsconfig.build.json (53%) diff --git a/package.json b/package.json index 6fe4242..cfea0f3 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,14 @@ "main": "dist/slimdom.js", "module": "dist/slimdom.mjs", "scripts": { - "build:amd": "rimraf lib && tsc --module amd", - "build:commonjs": "rimraf lib && tsc --module commonjs", - "build:es": "rimraf lib && tsc --module es6", - "build:bundle": "rimraf dist && rimraf lib && tsc && rollup -c", + "build:amd": "rimraf lib && tsc -P tsconfig.build.json --module amd", + "build:commonjs": "rimraf lib && tsc -P tsconfig.build.json --module commonjs", + "build:es": "rimraf lib && tsc -P tsconfig.build.json --module es6", + "build:bundle": "rimraf dist && rimraf lib && tsc -P tsconfig.build.json && rollup -c", "docs": "typedoc --out docs --excludePrivate --excludeNotExported src/index.ts", "prepare": "npm run build:bundle", - "test": "rimraf lib && tsc -P tsconfig.test.json && nyc --reporter html --reporter text --exclude lib/test mocha --timeout 20000 --recursive lib/test" + "test": "rimraf lib && tsc -P tsconfig.json && nyc --reporter html --reporter text --exclude lib/test mocha --timeout 20000 --recursive lib/test", + "test:debug": "rimraf lib && tsc -P tsconfig.json && mocha --timeout 20000 --recursive lib/test --inspect --debug-brk" }, "files": [ "dist" diff --git a/tsconfig.test.json b/tsconfig.build.json similarity index 53% rename from tsconfig.test.json rename to tsconfig.build.json index d6d625b..9962bfd 100644 --- a/tsconfig.test.json +++ b/tsconfig.build.json @@ -3,20 +3,15 @@ "outDir": "lib", "sourceMap": true, "strict": true, - "module": "commonjs", + "module": "es6", "target": "es5", + "declaration": true, "lib": [ "es2015" ], - "types": [ - "chai", - "mocha", - "node" - ], - "moduleResolution": "node" + "types": [] }, "include": [ - "src/**/*", - "test/**/*" + "src/**/*" ] } diff --git a/tsconfig.json b/tsconfig.json index 9962bfd..86b6a47 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,15 +3,20 @@ "outDir": "lib", "sourceMap": true, "strict": true, - "module": "es6", - "target": "es5", - "declaration": true, + "module": "commonjs", + "target": "es6", "lib": [ "es2015" ], - "types": [] + "types": [ + "chai", + "mocha", + "node" + ], + "moduleResolution": "node" }, "include": [ - "src/**/*" + "src/**/*", + "test/**/*" ] } From db142c2def56f0d47c2db3f41b4d4874eb04acfb Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 17:09:00 +0200 Subject: [PATCH 18/34] Implement CharacterData#previousElementSibling / nextElementSibling. --- src/CharacterData.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/CharacterData.ts b/src/CharacterData.ts index 8f81c21..8dc1a00 100644 --- a/src/CharacterData.ts +++ b/src/CharacterData.ts @@ -1,4 +1,4 @@ -import { NonDocumentTypeChildNode, ChildNode } from './mixins'; +import { NonDocumentTypeChildNode, ChildNode, getNextElementSibling, getPreviousElementSibling } from './mixins'; import Document from './Document'; import Element from './Element'; import Node from './Node'; @@ -65,8 +65,13 @@ export default abstract class CharacterData extends Node implements NonDocumentT // NonDocumentTypeChildNode - previousElementSibling: Element | null = null; - nextElementSibling: Element | null = null; + public get previousElementSibling(): Element | null { + return getPreviousElementSibling(this); + } + + public get nextElementSibling(): Element | null { + return getNextElementSibling(this); + } // CharacterData From 4937d697e492dee852e7ec94adb512ebddeb343f Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 17:12:15 +0200 Subject: [PATCH 19/34] Remove dead code. --- src/Attr.ts | 6 +--- src/CharacterData.ts | 6 +--- src/Document.ts | 6 +--- src/Element.ts | 27 ++--------------- src/Range.ts | 8 ++--- src/mutation-observer/RegisteredObservers.ts | 32 +++++++++++--------- src/util/cloneNode.ts | 2 +- src/util/mutationAlgorithms.ts | 5 ++- src/util/treeHelpers.ts | 3 +- 9 files changed, 28 insertions(+), 67 deletions(-) diff --git a/src/Attr.ts b/src/Attr.ts index 5481e7d..8068f5e 100644 --- a/src/Attr.ts +++ b/src/Attr.ts @@ -36,12 +36,8 @@ export default class Attr extends Node { } public lookupPrefix(namespace: string | null): string | null { - namespace = asNullableString(namespace); - // 1. If namespace is null or the empty string, then return null. - if (namespace === null || namespace === '') { - return null; - } + // (not necessary due to recursion) // 2. Switch on the context object: // Attr - Return the result of locating a namespace prefix for its element, if its element is non-null, and null diff --git a/src/CharacterData.ts b/src/CharacterData.ts index 8dc1a00..195cabd 100644 --- a/src/CharacterData.ts +++ b/src/CharacterData.ts @@ -28,12 +28,8 @@ export default abstract class CharacterData extends Node implements NonDocumentT } public lookupPrefix(namespace: string | null): string | null { - namespace = asNullableString(namespace); - // 1. If namespace is null or the empty string, then return null. - if (namespace === null || namespace === '') { - return null; - } + // (not necessary due to recursion) // 2. Switch on the context object: // Any other node - Return the result of locating a namespace prefix for its parent element, if its parent diff --git a/src/Document.ts b/src/Document.ts index f30273a..6ee5227 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -42,12 +42,8 @@ export default class Document extends Node implements NonElementParentNode, Pare } public lookupPrefix(namespace: string | null): string | null { - namespace = asNullableString(namespace); - // 1. If namespace is null or the empty string, then return null. - if (namespace === null || namespace === '') { - return null; - } + // (not necessary due to recursion) // 2. Switch on the context object: // Document - Return the result of locating a namespace prefix for its document element, if its document element diff --git a/src/Element.ts b/src/Element.ts index ecdca1a..ae1e668 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -530,29 +530,6 @@ function getAttributeByNamespaceAndLocalName( return element.attributes.find(attr => attr.namespaceURI === namespace && attr.localName === localName) || null; } -/** - * To get an attribute value given an element element, localName, and optionally a namespace (null unless stated - * otherwise), run these steps: - * - * @param element The element to get the attribute on - * @param localName The local name of the attribute - * @param namespace The namespace of the attribute - * - * @return The value of the first matching attribute, or the empty string if no such attribute exists - */ -function getAttributeValue(element: Element, localName: string, namespace: string | null = null): string { - // 1. Let attr be the result of getting an attribute given namespace, localName, and element. - const attr = getAttributeByNamespaceAndLocalName(namespace, localName, element); - - // 2. If attr is null, then return the empty string. - if (attr === null) { - return ''; - } - - // 3. Return attr’s value. - return attr.value; -} - /** * To set an attribute given an attr and element, run these steps: * @@ -601,8 +578,8 @@ function setAttributeValue( element: Element, localName: string, value: string, - prefix: string | null = null, - namespace: string | null = null + prefix: string | null, + namespace: string | null ): void { // 1. If prefix is not given, set it to null. // 2. If namespace is not given, set it to null. diff --git a/src/Range.ts b/src/Range.ts index 0076a7a..fd30f3b 100644 --- a/src/Range.ts +++ b/src/Range.ts @@ -355,8 +355,6 @@ export default class Range { // END_TO_START: default: - // unreachable, fall through for type check - case Range.END_TO_START: // Let this point be the context object’s start. Let other point be sourceRange’s end. return compareBoundaryPointPositions( // this point @@ -530,6 +528,8 @@ const POSITION_AFTER = 1; * * Note: for efficiency reasons, this implementation deviates from the algorithm given in 4.2. * + * This implementation assumes it is called on nodes under the same root. + * * @param nodeA First boundary point's node * @param offsetA First boundary point's offset * @param nodeB Second boundary point's node @@ -541,10 +541,6 @@ function compareBoundaryPointPositions(nodeA: Node, offsetA: number, nodeB: Node if (nodeA !== nodeB) { const ancestors1 = getInclusiveAncestors(nodeA); const ancestors2 = getInclusiveAncestors(nodeB); - // This should not be called on nodes from different trees - if (ancestors1[0] !== ancestors2[0]) { - throw new Error('Can not compare positions of nodes from different trees.'); - } // Skip common parents while (ancestors1[0] && ancestors2[0] && ancestors1[0] === ancestors2[0]) { diff --git a/src/mutation-observer/RegisteredObservers.ts b/src/mutation-observer/RegisteredObservers.ts index 1cbf1b4..99c1c4b 100644 --- a/src/mutation-observer/RegisteredObservers.ts +++ b/src/mutation-observer/RegisteredObservers.ts @@ -52,25 +52,23 @@ export default class RegisteredObservers { // registered. if (!hasRegisteredObserverForObserver) { this._registeredObservers.push(new RegisteredObserver(observer, this._node, options)); - if (observer._nodes.indexOf(this._node) < 0) { - observer._nodes.push(this._node); - } + // No registered observer for this observer at the current node means that node can't exist in the + // observer's list of nodes either. + observer._nodes.push(this._node); } } /** - * Removes the given registered observer. - - * It is the caller's responsibility to remove the associated node from the observer's list of nodes, where - * appropriate. + * Removes the given transient registered observer. * - * @param registeredObserver The registered observer to remove + * Transient registered observers never have a corresponding entry in the observer's list of nodes. They are + * guaranteed to be present in the array, as MutationObserver#_transients and + * RegisteredObservers#_registeredObservers are kept in sync. + * + * @param transientRegisteredObserver The registered observer to remove */ - public remove(registeredObserver: RegisteredObserver): void { - const index = this._registeredObservers.indexOf(registeredObserver); - if (index >= 0) { - this._registeredObservers.splice(index, 1); - } + public removeTransientRegisteredObserver(transientRegisteredObserver: RegisteredObserver): void { + this._registeredObservers.splice(this._registeredObservers.indexOf(transientRegisteredObserver), 1); } /** @@ -153,7 +151,9 @@ export default class RegisteredObservers { */ export function removeTransientRegisteredObserversForObserver(observer: MutationObserver): void { observer._transients.forEach(transientRegisteredObserver => { - transientRegisteredObserver.node._registeredObservers.remove(transientRegisteredObserver); + transientRegisteredObserver.node._registeredObservers.removeTransientRegisteredObserver( + transientRegisteredObserver + ); }); observer._transients.length = 0; } @@ -170,7 +170,9 @@ export function removeTransientRegisteredObserversForSource(source: RegisteredOb return; } - transientRegisteredObserver.node._registeredObservers.remove(transientRegisteredObserver); + transientRegisteredObserver.node._registeredObservers.removeTransientRegisteredObserver( + transientRegisteredObserver + ); source.observer._transients.splice(i, 1); } } diff --git a/src/util/cloneNode.ts b/src/util/cloneNode.ts index 0a4f9b1..e132c9d 100644 --- a/src/util/cloneNode.ts +++ b/src/util/cloneNode.ts @@ -13,7 +13,7 @@ import { getNodeDocument } from './treeHelpers'; * @param cloneChildren Whether to also clone node's descendants * @param document The document used to create the copy */ -export default function cloneNode(node: Node, cloneChildren: boolean = false, document?: Document): Node { +export default function cloneNode(node: Node, cloneChildren: boolean, document?: Document): Node { // 1. If document is not given, let document be node’s node document. if (!document) { document = getNodeDocument(node); diff --git a/src/util/mutationAlgorithms.ts b/src/util/mutationAlgorithms.ts index 9cfea19..2078f22 100644 --- a/src/util/mutationAlgorithms.ts +++ b/src/util/mutationAlgorithms.ts @@ -546,9 +546,8 @@ export function adoptNode(node: Node, document: Document): void { // 3.1. For each inclusiveDescendant in node’s shadow-including inclusive descendants: forEachInclusiveDescendant(node, node => { // 3.1.1. Set inclusiveDescendant’s node document to document. - if (!isNodeOfType(node, NodeType.DOCUMENT_NODE)) { - node.ownerDocument = document; - } + // (calling code ensures that node is never a Document) + node.ownerDocument = document; // 3.1.2. If inclusiveDescendant is an element, then set the node document of each attribute in // inclusiveDescendant’s attribute list to document. if (isNodeOfType(node, NodeType.ELEMENT_NODE)) { diff --git a/src/util/treeHelpers.ts b/src/util/treeHelpers.ts index 6f7b508..eecc860 100644 --- a/src/util/treeHelpers.ts +++ b/src/util/treeHelpers.ts @@ -13,8 +13,7 @@ import { NodeType, isNodeOfType } from './NodeType'; export function determineLengthOfNode(node: Node): number { switch (node.nodeType) { // DocumentType: Zero. - case NodeType.DOCUMENT_TYPE_NODE: - return 0; + // (not necessary, as doctypes never have children) // Text, ProcessingInstruction, Comment: The number of code units in its data. case NodeType.TEXT_NODE: From f67b875da43592fd3438ee7ec55bc91cc72af816 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 17:12:38 +0200 Subject: [PATCH 20/34] Add arity checks to Document and Element methods. --- src/Document.ts | 18 ++++++++++++++++-- src/Element.ts | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/src/Document.ts b/src/Document.ts index 6ee5227..bab5a3a 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -13,11 +13,11 @@ import Range from './Range'; import { getContext } from './context/Context'; import cloneNode from './util/cloneNode'; import createElementNS from './util/createElementNS'; -import { throwInvalidCharacterError, throwNotSupportedError } from './util/errorHelpers'; +import { expectArity, throwInvalidCharacterError, throwNotSupportedError } from './util/errorHelpers'; import { adoptNode } from './util/mutationAlgorithms'; import { NodeType, isNodeOfType } from './util/NodeType'; import { matchesNameProduction, validateAndExtract } from './util/namespaceHelpers'; -import { asNullableString } from './util/typeHelpers'; +import { asNullableString, asObject } from './util/typeHelpers'; /** * 3.5. Interface Document @@ -116,6 +116,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new element */ public createElement(localName: string): Element { + expectArity(arguments, 1); localName = String(localName); // 1. If localName does not match the Name production, then throw an InvalidCharacterError. @@ -154,6 +155,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new element */ public createElementNS(namespace: string | null, qualifiedName: string): Element { + expectArity(arguments, 2); namespace = asNullableString(namespace); qualifiedName = String(qualifiedName); @@ -182,6 +184,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new text node */ public createTextNode(data: string): Text { + expectArity(arguments, 1); data = String(data); const context = getContext(this); @@ -198,6 +201,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new CDATA section */ public createCDATASection(data: string): CDATASection { + expectArity(arguments, 1); data = String(data); // 1. If context object is an HTML document, then throw a NotSupportedError. @@ -223,6 +227,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new comment node */ public createComment(data: string): Comment { + expectArity(arguments, 1); data = String(data); const context = getContext(this); @@ -241,6 +246,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new processing instruction */ public createProcessingInstruction(target: string, data: string): ProcessingInstruction { + expectArity(arguments, 2); target = String(target); data = String(data); @@ -272,6 +278,9 @@ export default class Document extends Node implements NonElementParentNode, Pare * @param deep Whether to also import node's children */ public importNode(node: Node, deep: boolean = false): Node { + expectArity(arguments, 1); + node = asObject(node, Node); + // 1. If node is a document or shadow root, then throw a NotSupportedError. if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { throwNotSupportedError('importing a Document node is not supported'); @@ -288,6 +297,9 @@ export default class Document extends Node implements NonElementParentNode, Pare * @param node The node to adopt */ public adoptNode(node: Node): Node { + expectArity(arguments, 1); + node = asObject(node, Node); + // 1. If node is a document, then throw a NotSupportedError. if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { throwNotSupportedError('adopting a Document node is not supported'); @@ -311,6 +323,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new attribute node */ public createAttribute(localName: string): Attr { + expectArity(arguments, 1); localName = String(localName); // 1. If localName does not match the Name production in XML, then throw an InvalidCharacterError. @@ -337,6 +350,7 @@ export default class Document extends Node implements NonElementParentNode, Pare * @return The new attribute node */ public createAttributeNS(namespace: string | null, qualifiedName: string): Attr { + expectArity(arguments, 2); namespace = asNullableString(namespace); qualifiedName = String(qualifiedName); diff --git a/src/Element.ts b/src/Element.ts index ae1e668..02d2a17 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -5,7 +5,12 @@ import Document from './Document'; import Node from './Node'; import { getContext } from './context/Context'; import { appendAttribute, changeAttribute, removeAttribute, replaceAttribute } from './util/attrMutations'; -import { throwInUseAttributeError, throwInvalidCharacterError, throwNotFoundError } from './util/errorHelpers'; +import { + expectArity, + throwInUseAttributeError, + throwInvalidCharacterError, + throwNotFoundError +} from './util/errorHelpers'; import { matchesNameProduction, validateAndExtract, @@ -13,7 +18,7 @@ import { XMLNS_NAMESPACE } from './util/namespaceHelpers'; import { NodeType } from './util/NodeType'; -import { asNullableString } from './util/typeHelpers'; +import { asNullableString, asObject } from './util/typeHelpers'; /** * 3.9. Interface Element @@ -38,6 +43,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType } public lookupPrefix(namespace: string | null): string | null { + expectArity(arguments, 1); namespace = asNullableString(namespace); // 1. If namespace is null or the empty string, then return null. @@ -51,6 +57,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType } public lookupNamespaceURI(prefix: string | null): string | null { + expectArity(arguments, 1); prefix = asNullableString(prefix); // 1. If prefix is the empty string, then set it to null. @@ -163,6 +170,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @return The value of the attribute, or null if no such attribute exists */ public getAttribute(qualifiedName: string): string | null { + expectArity(arguments, 1); + qualifiedName = String(qualifiedName); + // 1. Let attr be the result of getting an attribute given qualifiedName and the context object. const attr = getAttributeByName(qualifiedName, this); @@ -184,6 +194,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @return The value of the attribute, or null if no such attribute exists */ public getAttributeNS(namespace: string | null, localName: string): string | null { + expectArity(arguments, 2); namespace = asNullableString(namespace); // 1. Let attr be the result of getting an attribute given namespace, localName, and the context object. @@ -205,6 +216,10 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param value The new value for the attribute */ public setAttribute(qualifiedName: string, value: string): void { + expectArity(arguments, 2); + qualifiedName = String(qualifiedName); + value = String(value); + // 1. If qualifiedName does not match the Name production in XML, then throw an InvalidCharacterError. if (!matchesNameProduction(qualifiedName)) { throwInvalidCharacterError('The qualified name does not match the Name production'); @@ -240,7 +255,10 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param value The value for the attribute */ public setAttributeNS(namespace: string | null, qualifiedName: string, value: string): void { + expectArity(arguments, 3); namespace = asNullableString(namespace); + qualifiedName = String(qualifiedName); + value = String(value); // 1. Let namespace, prefix, and localName be the result of passing namespace and qualifiedName to validate and // extract. @@ -256,6 +274,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param qualifiedName The qualified name of the attribute */ public removeAttribute(qualifiedName: string): void { + expectArity(arguments, 1); + qualifiedName = String(qualifiedName); + removeAttributeByName(qualifiedName, this); } @@ -266,7 +287,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param localName The local name of the attribute */ public removeAttributeNS(namespace: string | null, localName: string): void { + expectArity(arguments, 2); namespace = asNullableString(namespace); + localName = String(localName); removeAttributeByNamespaceAndLocalName(namespace, localName, this); } @@ -277,6 +300,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param qualifiedName The qualified name of the attribute */ public hasAttribute(qualifiedName: string): boolean { + expectArity(arguments, 1); + qualifiedName = String(qualifiedName); + // 1. If the context object is in the HTML namespace and its node document is an HTML document, then set // qualifiedName to qualifiedName in ASCII lowercase. // (html documents not implemented) @@ -293,7 +319,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @param localName The local name of the attribute */ public hasAttributeNS(namespace: string | null, localName: string): boolean { + expectArity(arguments, 2); namespace = asNullableString(namespace); + localName = String(localName); // 1. If namespace is the empty string, set it to null. // (handled by getAttributeByNamespaceAndLocalName, called below) @@ -310,6 +338,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @return The attribute, or null if no such attribute exists */ public getAttributeNode(qualifiedName: string): Attr | null { + expectArity(arguments, 1); + qualifiedName = String(qualifiedName); + return getAttributeByName(qualifiedName, this); } @@ -322,7 +353,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @return The attribute, or null if no such attribute exists */ public getAttributeNodeNS(namespace: string | null, localName: string): Attr | null { + expectArity(arguments, 2); namespace = asNullableString(namespace); + localName = String(localName); return getAttributeByNamespaceAndLocalName(namespace, localName, this); } @@ -335,6 +368,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @return The previous attribute node for the attribute */ public setAttributeNode(attr: Attr): Attr | null { + expectArity(arguments, 1); + attr = asObject(attr, Attr); + return setAttribute(attr, this); } @@ -346,6 +382,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @return The previous attribute node for the attribute */ public setAttributeNodeNS(attr: Attr): Attr | null { + expectArity(arguments, 1); + attr = asObject(attr, Attr); + return setAttribute(attr, this); } @@ -357,6 +396,9 @@ export default class Element extends Node implements ParentNode, NonDocumentType * @return The removed attribute node */ public removeAttributeNode(attr: Attr): Attr { + expectArity(arguments, 1); + attr = asObject(attr, Attr); + // 1. If context object’s attribute list does not contain attr, then throw a NotFoundError. if (this.attributes.indexOf(attr) < 0) { throwNotFoundError('the specified attribute does not exist'); From 79f56fa45e21f72bc666080296849ba8913260df Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 17:13:48 +0200 Subject: [PATCH 21/34] Fix error in Range#intersectsNode. --- src/Range.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Range.ts b/src/Range.ts index fd30f3b..53e6c65 100644 --- a/src/Range.ts +++ b/src/Range.ts @@ -503,7 +503,7 @@ export default class Range { // 3. If parent is null, return true. if (parent === null) { - return false; + return true; } // 4. Let offset be node’s index. From 7c29689e9e0516395b3a29c546a9147dab0c740b Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 17:13:58 +0200 Subject: [PATCH 22/34] Export MutationRecord. --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 45e1ddc..fd06332 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ export { default as Range } from './Range'; export { default as Text } from './Text'; export { default as XMLDocument } from './XMLDocument'; export { default as MutationObserver } from './mutation-observer/MutationObserver'; +export { default as MutationRecord } from './mutation-observer/MutationRecord'; // To avoid cyclic dependencies and enable multiple contexts with their own constructors later, inject all constructors // as well as the global document into the default context (i.e., global object) here. From 7012d641e9eda21fa52119c2545c3e7b6d813cf7 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 17:14:50 +0200 Subject: [PATCH 23/34] Cleanup comments. --- src/mutation-observer/MutationObserver.ts | 10 +++++----- src/mutation-observer/NotifyList.ts | 4 +--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/mutation-observer/MutationObserver.ts b/src/mutation-observer/MutationObserver.ts index 88f97f0..7dee753 100644 --- a/src/mutation-observer/MutationObserver.ts +++ b/src/mutation-observer/MutationObserver.ts @@ -90,11 +90,11 @@ export default class MutationObserver { * Instructs the user agent to observe a given target (a node) and report any mutations based on the criteria given * by options (an object). * - * NOTE: Adding an observer to an element is just like addEventListener, if you observe the element multiple - * times it does not make a difference. Meaning if you observe element twice, the observe callback does not fire - * twice, nor will you have to run disconnect() twice. In other words, once an element is observed, observing it - * again with the same will do nothing. However if the callback object is different it will of course add - * another observer to it. + * NOTE: Adding an observer to an element is just like addEventListener, if you observe the element multiple times + * it does not make a difference. Meaning if you observe element twice, the observe callback does not fire twice, + * nor will you have to run disconnect() twice. In other words, once an element is observed, observing it again with + * the same will do nothing. However if the callback object is different it will of course add another observer to + * it. * * @param target Node (or root of subtree) to observe * @param options Determines which types of mutations to observe diff --git a/src/mutation-observer/NotifyList.ts b/src/mutation-observer/NotifyList.ts index aeebaf9..a73a685 100644 --- a/src/mutation-observer/NotifyList.ts +++ b/src/mutation-observer/NotifyList.ts @@ -15,9 +15,7 @@ function queueCompoundMicrotask(callback: (...args: any[]) => void, thisArg: Not } /** - * A helper class which is responsible for scheduling the queued MutationRecord objects for reporting by their - * observer. Reporting means the callback of the observer (a MutationObserver object) gets called with the - * relevant MutationRecord objects. + * Tracks MutationObserver instances which have a non-empty record queue and schedules their callbacks to be called. */ export default class NotifyList { private _notifyList: MutationObserver[] = []; From 042c942b3623bca26f2fc1ead3eddb3f327ee0d5 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 17:15:34 +0200 Subject: [PATCH 24/34] Fix pre-insertion checks to support CDATASection. --- src/util/mutationAlgorithms.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/util/mutationAlgorithms.ts b/src/util/mutationAlgorithms.ts index 2078f22..cb956a0 100644 --- a/src/util/mutationAlgorithms.ts +++ b/src/util/mutationAlgorithms.ts @@ -39,6 +39,7 @@ function ensurePreInsertionValidity(node: Node, parent: Node, child: Node | null NodeType.DOCUMENT_TYPE_NODE, NodeType.ELEMENT_NODE, NodeType.TEXT_NODE, + NodeType.CDATA_SECTION_NODE, NodeType.PROCESSING_INSTRUCTION_NODE, NodeType.COMMENT_NODE ) @@ -282,6 +283,7 @@ export function replaceChildWithNode(child: Node, node: Node, parent: Node): Nod NodeType.DOCUMENT_TYPE_NODE, NodeType.ELEMENT_NODE, NodeType.TEXT_NODE, + NodeType.CDATA_SECTION_NODE, NodeType.PROCESSING_INSTRUCTION_NODE, NodeType.COMMENT_NODE ) @@ -548,6 +550,7 @@ export function adoptNode(node: Node, document: Document): void { // 3.1.1. Set inclusiveDescendant’s node document to document. // (calling code ensures that node is never a Document) node.ownerDocument = document; + // 3.1.2. If inclusiveDescendant is an element, then set the node document of each attribute in // inclusiveDescendant’s attribute list to document. if (isNodeOfType(node, NodeType.ELEMENT_NODE)) { From 80cce37c33bed94888d853b12f5378f21d70811f Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 17:16:17 +0200 Subject: [PATCH 25/34] Mark unreachable branches to be ignored for code coverage. --- src/mixins.ts | 3 +++ src/mutation-observer/NotifyList.ts | 2 ++ src/util/treeMutations.ts | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/src/mixins.ts b/src/mixins.ts index 95d3935..419887f 100644 --- a/src/mixins.ts +++ b/src/mixins.ts @@ -28,10 +28,13 @@ export interface ParentNode { // Element implements ParentNode; export function asParentNode(node: Node): ParentNode | null { + // This is only called from treeMutations.js, where node can never be anything other than these + /* istanbul ignore else */ if (isNodeOfType(node, NodeType.ELEMENT_NODE, NodeType.DOCUMENT_NODE, NodeType.DOCUMENT_FRAGMENT_NODE)) { return node as Element | Document | DocumentFragment; } + /* istanbul ignore next */ return null; } diff --git a/src/mutation-observer/NotifyList.ts b/src/mutation-observer/NotifyList.ts index a73a685..ea31005 100644 --- a/src/mutation-observer/NotifyList.ts +++ b/src/mutation-observer/NotifyList.ts @@ -9,6 +9,8 @@ declare function setTimeout(handler: (...args: any[]) => void, timeout: number): const hasSetImmediate = typeof setImmediate === 'function'; function queueCompoundMicrotask(callback: (...args: any[]) => void, thisArg: NotifyList, ...args: any[]): number { + // Branch taken is platform dependent and constant + /* istanbul ignore next */ return (hasSetImmediate ? setImmediate : setTimeout)(() => { callback.apply(thisArg, args); }, 0); diff --git a/src/util/treeMutations.ts b/src/util/treeMutations.ts index 65951be..eaca193 100644 --- a/src/util/treeMutations.ts +++ b/src/util/treeMutations.ts @@ -39,6 +39,8 @@ export function insertIntoChildren(node: Node, parent: Node, referenceChild: Nod if (isNodeOfType(node, NodeType.ELEMENT_NODE)) { const element = node as Element; const parentNode = asParentNode(parent); + // Functions calling this will ensure parent is always a ParentNode + /* istanbul ignore else */ if (parentNode) { let previousElementSibling: Element | null = null; for (let sibling = previousSibling; sibling; sibling = sibling.previousSibling) { @@ -60,6 +62,8 @@ export function insertIntoChildren(node: Node, parent: Node, referenceChild: Nod break; } const siblingNonDocumentTypeChildNode = asNonDocumentTypeChildNode(sibling); + // An element can never be inserted before a doctype + /* istanbul ignore else */ if (siblingNonDocumentTypeChildNode) { nextElementSibling = siblingNonDocumentTypeChildNode.nextElementSibling; break; @@ -121,6 +125,8 @@ export function removeFromChildren(node: Node, parent: Node) { // ParentNode if (isElement) { const parentNode = asParentNode(parent); + // Functions calling this will ensure parent is always a ParentNode + /* istanbul ignore else */ if (parentNode) { if (parentNode.firstElementChild === node) { parentNode.firstElementChild = nextElementSibling; From ad11d3f0d6fe81ac6efa245ea1f7a3972558c5f0 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 17:16:54 +0200 Subject: [PATCH 26/34] Extend test suite for better coverage. --- test/Attr.tests.ts | 92 +++++ test/CDATASection.tests.ts | 27 ++ test/Comment.tests.ts | 75 +++- test/DOMImplementation.tests.ts | 29 ++ test/Document.tests.ts | 213 +++++++++- test/DocumentFragment.tests.ts | 102 +++++ test/DocumentType.tests.ts | 47 ++- test/Element.tests.ts | 249 ++++++++++- test/MutationObserver.tests.ts | 612 ++++++++++++++++++++-------- test/ProcessingInstruction.tests.ts | 34 +- test/Range.tests.ts | 151 ++++++- test/Text.tests.ts | 108 ++++- test/XMLDocument.tests.ts | 17 + test/mutationAlgorithms.tests.ts | 303 ++++++++++++++ 14 files changed, 1782 insertions(+), 277 deletions(-) create mode 100644 test/Attr.tests.ts create mode 100644 test/CDATASection.tests.ts create mode 100644 test/DocumentFragment.tests.ts create mode 100644 test/XMLDocument.tests.ts create mode 100644 test/mutationAlgorithms.tests.ts diff --git a/test/Attr.tests.ts b/test/Attr.tests.ts new file mode 100644 index 0000000..11a0669 --- /dev/null +++ b/test/Attr.tests.ts @@ -0,0 +1,92 @@ +import * as chai from 'chai'; +import * as slimdom from '../src/index'; + +describe('Attr', () => { + let document: slimdom.Document; + beforeEach(() => { + document = new slimdom.Document(); + }); + + it('can be created using Document#createAttribute()', () => { + const attr = document.createAttribute('test'); + chai.assert.equal(attr.nodeType, 2); + chai.assert.equal(attr.nodeName, 'test'); + chai.assert.equal(attr.nodeValue, ''); + + chai.assert.equal(attr.namespaceURI, null); + chai.assert.equal(attr.prefix, null); + chai.assert.equal(attr.localName, 'test'); + chai.assert.equal(attr.name, 'test'); + chai.assert.equal(attr.value, ''); + }); + + it('can be created using Document#createAttributeNS()', () => { + const attr = document.createAttributeNS('http://www.example.com/ns', 'ns:test'); + chai.assert.equal(attr.nodeType, 2); + chai.assert.equal(attr.nodeName, 'ns:test'); + chai.assert.equal(attr.nodeValue, ''); + + chai.assert.equal(attr.namespaceURI, 'http://www.example.com/ns'); + chai.assert.equal(attr.prefix, 'ns'); + chai.assert.equal(attr.localName, 'test'); + chai.assert.equal(attr.name, 'ns:test'); + chai.assert.equal(attr.value, ''); + }); + + it('can set its value using nodeValue', () => { + const attr = document.createAttribute('test'); + attr.nodeValue = 'value'; + chai.assert.equal(attr.nodeValue, 'value'); + chai.assert.equal(attr.value, 'value'); + + attr.nodeValue = null; + chai.assert.equal(attr.nodeValue, ''); + chai.assert.equal(attr.value, ''); + }); + + it('can set its value using value', () => { + const attr = document.createAttribute('test'); + attr.value = 'value'; + chai.assert.equal(attr.nodeValue, 'value'); + chai.assert.equal(attr.value, 'value'); + }); + + it('can set its value when part of an element', () => { + const element = document.createElement('test'); + element.setAttribute('attr', 'value'); + const attr = element.getAttributeNode('attr')!; + chai.assert.equal(attr.value, 'value'); + + attr.value = 'new value'; + chai.assert.equal(element.getAttribute('attr'), 'new value'); + }); + + it('can be cloned', () => { + const attr = document.createAttributeNS('http://www.example.com/ns', 'ns:test'); + attr.value = 'some value'; + + const copy = attr.cloneNode() as slimdom.Attr; + chai.assert.equal(copy.nodeType, 2); + chai.assert.equal(copy.nodeName, 'ns:test'); + chai.assert.equal(copy.nodeValue, 'some value'); + + chai.assert.equal(copy.namespaceURI, 'http://www.example.com/ns'); + chai.assert.equal(copy.prefix, 'ns'); + chai.assert.equal(copy.localName, 'test'); + chai.assert.equal(copy.name, 'ns:test'); + chai.assert.equal(copy.value, 'some value'); + + chai.assert.notEqual(copy, attr); + }); + + it('can lookup a prefix or namespace on its owner element', () => { + const attr = document.createAttribute('attr'); + chai.assert.equal(attr.lookupNamespaceURI('prf'), null); + chai.assert.equal(attr.lookupPrefix('http://www.example.com/ns'), null); + + const element = document.createElementNS('http://www.example.com/ns', 'prf:test'); + element.setAttributeNode(attr); + chai.assert.equal(attr.lookupNamespaceURI('prf'), 'http://www.example.com/ns'); + chai.assert.equal(attr.lookupPrefix('http://www.example.com/ns'), 'prf'); + }); +}); diff --git a/test/CDATASection.tests.ts b/test/CDATASection.tests.ts new file mode 100644 index 0000000..cb6b3cc --- /dev/null +++ b/test/CDATASection.tests.ts @@ -0,0 +1,27 @@ +import * as chai from 'chai'; +import * as slimdom from '../src/index'; + +describe('CDATASection', () => { + let document: slimdom.Document; + beforeEach(() => { + document = new slimdom.Document(); + }); + + it('can be created', () => { + const cs = document.createCDATASection('some content'); + chai.assert.equal(cs.nodeType, 4); + chai.assert.equal(cs.nodeName, '#cdata-section'); + chai.assert.equal(cs.nodeValue, 'some content'); + chai.assert.equal(cs.data, 'some content'); + }); + + it('can be cloned', () => { + const cs = document.createCDATASection('some content'); + const copy = cs.cloneNode() as slimdom.CDATASection; + chai.assert.equal(copy.nodeType, 4); + chai.assert.equal(copy.nodeName, '#cdata-section'); + chai.assert.equal(copy.nodeValue, 'some content'); + chai.assert.equal(copy.data, 'some content'); + chai.assert.notEqual(copy, cs); + }); +}); diff --git a/test/Comment.tests.ts b/test/Comment.tests.ts index b21a843..5b610b6 100644 --- a/test/Comment.tests.ts +++ b/test/Comment.tests.ts @@ -3,24 +3,77 @@ import * as slimdom from '../src/index'; describe('Comment', () => { let document: slimdom.Document; - let comment: slimdom.Comment; beforeEach(() => { document = new slimdom.Document(); - comment = document.createComment('somedata'); }); - it('has nodeType 8', () => chai.assert.equal(comment.nodeType, 8)); + it('can be created using Document#createComment()', () => { + const comment = document.createComment('some data'); + chai.assert.equal(comment.nodeType, 8); + chai.assert.equal(comment.nodeName, '#comment'); + chai.assert.equal(comment.nodeValue, 'some data'); + chai.assert.equal(comment.data, 'some data'); + }); + + it('can be created using its constructor (with data)', () => { + const comment = new slimdom.Comment('some data'); + chai.assert.equal(comment.nodeType, 8); + chai.assert.equal(comment.nodeName, '#comment'); + chai.assert.equal(comment.nodeValue, 'some data'); + chai.assert.equal(comment.data, 'some data'); - it('has data', () => { - chai.assert.equal(comment.nodeValue, 'somedata'); - chai.assert.equal(comment.data, 'somedata'); + chai.assert.equal(comment.ownerDocument, slimdom.document); + }); + + it('can be created using its constructor (without arguments)', () => { + const comment = new slimdom.Comment(); + chai.assert.equal(comment.nodeType, 8); + chai.assert.equal(comment.nodeName, '#comment'); + chai.assert.equal(comment.nodeValue, ''); + chai.assert.equal(comment.data, ''); + + chai.assert.equal(comment.ownerDocument, slimdom.document); + }); + + it('can set its data using nodeValue', () => { + const comment = document.createComment('some data'); + comment.nodeValue = 'other data'; + chai.assert.equal(comment.nodeValue, 'other data'); + chai.assert.equal(comment.data, 'other data'); + + comment.nodeValue = null; + chai.assert.equal(comment.nodeValue, ''); + chai.assert.equal(comment.data, ''); + }); + + it('can set its data using data', () => { + const comment = document.createComment('some data'); + comment.data = 'other data'; + chai.assert.equal(comment.nodeValue, 'other data'); + chai.assert.equal(comment.data, 'other data'); + (comment as any).data = null; + chai.assert.equal(comment.nodeValue, ''); + chai.assert.equal(comment.data, ''); }); it('can be cloned', () => { - var clone = comment.cloneNode(true) as slimdom.Comment; - chai.assert.equal(clone.nodeType, 8); - chai.assert.equal(clone.nodeValue, 'somedata'); - chai.assert.equal(clone.data, 'somedata'); - chai.assert.notEqual(clone, comment); + const comment = document.createComment('some data'); + var copy = comment.cloneNode() as slimdom.Comment; + chai.assert.equal(copy.nodeType, 8); + chai.assert.equal(copy.nodeName, '#comment'); + chai.assert.equal(copy.nodeValue, 'some data'); + chai.assert.equal(copy.data, 'some data'); + chai.assert.notEqual(copy, comment); + }); + + it('can lookup a prefix or namespace on its parent element', () => { + const comment = document.createComment('some data'); + chai.assert.equal(comment.lookupNamespaceURI('prf'), null); + chai.assert.equal(comment.lookupPrefix('http://www.example.com/ns'), null); + + const element = document.createElementNS('http://www.example.com/ns', 'prf:test'); + element.appendChild(comment); + chai.assert.equal(comment.lookupNamespaceURI('prf'), 'http://www.example.com/ns'); + chai.assert.equal(comment.lookupPrefix('http://www.example.com/ns'), 'prf'); }); }); diff --git a/test/DOMImplementation.tests.ts b/test/DOMImplementation.tests.ts index 376d287..2b3bf10 100644 --- a/test/DOMImplementation.tests.ts +++ b/test/DOMImplementation.tests.ts @@ -41,4 +41,33 @@ describe('DOMImplementation', () => { chai.assert.equal((document.documentElement as slimdom.Element).nodeName, 'someRootElementName'); }); }); + + describe('.createHTMLDocument()', () => { + it('can create a document without a title', () => { + const document = domImplementation.createHTMLDocument(null); + const html = document.documentElement!; + chai.assert.equal(html.namespaceURI, 'http://www.w3.org/1999/xhtml'); + chai.assert.equal(html.localName, 'html'); + const head = html.firstElementChild!; + chai.assert.equal(head.localName, 'head'); + const body = html.lastElementChild!; + chai.assert.equal(body.localName, 'body'); + const title = head.firstElementChild; + chai.assert.equal(title, null); + }); + + it('can create a document with a title', () => { + const document = domImplementation.createHTMLDocument('some title'); + const html = document.documentElement!; + chai.assert.equal(html.namespaceURI, 'http://www.w3.org/1999/xhtml'); + chai.assert.equal(html.localName, 'html'); + const head = html.firstElementChild!; + chai.assert.equal(head.localName, 'head'); + const body = html.lastElementChild!; + chai.assert.equal(body.localName, 'body'); + const title = head.firstElementChild!; + chai.assert.equal(title.localName, 'title'); + chai.assert.equal((title.firstChild as slimdom.Text).data, 'some title'); + }); + }); }); diff --git a/test/Document.tests.ts b/test/Document.tests.ts index 51a770b..0d30e85 100644 --- a/test/Document.tests.ts +++ b/test/Document.tests.ts @@ -7,16 +7,35 @@ describe('Document', () => { document = new slimdom.Document(); }); - it('has nodeType 9', () => chai.assert.equal(document.nodeType, 9)); + it('can be created using its constructor', () => { + const document = new slimdom.Document(); + chai.assert.equal(document.nodeType, 9); + chai.assert.equal(document.nodeName, '#document'); + chai.assert.equal(document.nodeValue, null); + }); + + it('can not change its nodeValue', () => { + document.nodeValue = 'test'; + chai.assert.equal(document.nodeValue, null); + }); it('exposes its DOMImplementation', () => chai.assert.instanceOf(document.implementation, slimdom.DOMImplementation)); - it('initially has no doctype', () => chai.assert.equal(document.doctype, null)); + it('has a doctype property that reflects the presence of a doctype child', () => { + chai.assert.equal(document.doctype, null); + const doctype = document.implementation.createDocumentType('html', '', ''); + document.appendChild(doctype); + chai.assert.equal(document.doctype, doctype); + document.removeChild(doctype); + chai.assert.equal(document.doctype, null); + }); it('initially has no documentElement', () => chai.assert.equal(document.documentElement, null)); it('initially has no childNodes', () => chai.assert.deepEqual(document.childNodes, [])); + it('initially has no children', () => chai.assert.deepEqual(document.children, [])); + describe('after appending a child element', () => { let element: slimdom.Element; beforeEach(() => { @@ -28,6 +47,13 @@ describe('Document', () => { it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [element])); + it('has children', () => chai.assert.deepEqual(document.children, [element])); + + it('has a first and last element child', () => { + chai.assert.equal(document.firstElementChild, element); + chai.assert.equal(document.lastElementChild, element); + }); + it('the child element is adopted into the document', () => chai.assert.equal(element.ownerDocument, document)); describe('after removing the element', () => { @@ -38,6 +64,8 @@ describe('Document', () => { it('has no documentElement', () => chai.assert.equal(document.documentElement, null)); it('has no childNodes', () => chai.assert.deepEqual(document.childNodes, [])); + + it('has no children', () => chai.assert.deepEqual(document.children, [])); }); describe('after replacing the element', () => { @@ -50,6 +78,8 @@ describe('Document', () => { it('has the other element as documentElement', () => chai.assert.equal(document.documentElement, otherElement)); it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [otherElement])); + + it('has children', () => chai.assert.deepEqual(document.children, [otherElement])); }); }); @@ -64,6 +94,8 @@ describe('Document', () => { it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [processingInstruction])); + it('has no children', () => chai.assert.deepEqual(document.children, [])); + describe('after replacing with an element', () => { let otherElement: slimdom.Element; beforeEach(() => { @@ -74,25 +106,182 @@ describe('Document', () => { it('has the other element as documentElement', () => chai.assert.equal(document.documentElement, otherElement)); it('has childNodes', () => chai.assert.deepEqual(document.childNodes, [otherElement])); + + it('has children', () => chai.assert.deepEqual(document.children, [otherElement])); }); }); - describe('cloning', () => { - var clone: slimdom.Document; + describe('.cloneNode', () => { beforeEach(() => { document.appendChild(document.createElement('root')); - clone = document.cloneNode(true) as slimdom.Document; }); - it('is a new document', () => { - chai.assert.equal(clone.nodeType, 9); - chai.assert.notEqual(clone, document); + it('can be cloned (shallow)', () => { + const copy = document.cloneNode() as slimdom.Document; + + chai.assert.equal(copy.nodeType, 9); + chai.assert.equal(copy.nodeName, '#document'); + chai.assert.equal(copy.nodeValue, null); + + chai.assert.equal(copy.documentElement, null); + + chai.assert.notEqual(copy, document); + }); + + it('can be cloned (deep)', () => { + const copy = document.cloneNode(true) as slimdom.Document; + + chai.assert.equal(copy.nodeType, 9); + chai.assert.equal(copy.nodeName, '#document'); + chai.assert.equal(copy.nodeValue, null); + + chai.assert.equal(copy.documentElement!.nodeName, 'root'); + + chai.assert.notEqual(copy, document); + chai.assert.notEqual(copy.documentElement, document.documentElement); + }); + }); + + it('can lookup a prefix or namespace on its document element', () => { + chai.assert.equal(document.lookupNamespaceURI('prf'), null); + chai.assert.equal(document.lookupPrefix('http://www.example.com/ns'), null); + + const element = document.createElementNS('http://www.example.com/ns', 'prf:test'); + document.appendChild(element); + chai.assert.equal(document.lookupNamespaceURI('prf'), 'http://www.example.com/ns'); + chai.assert.equal(document.lookupPrefix('http://www.example.com/ns'), 'prf'); + }); + + describe('.createElement', () => { + it('throws if not given a name', () => { + chai.assert.throws(() => (document as any).createElement(), TypeError); + }); + + it('throws if given an invalid name', () => { + chai.assert.throws(() => document.createElement(String.fromCodePoint(0x200b)), 'InvalidCharacterError'); + }); + }); + + describe('.createElementNS', () => { + it('throws if given an invalid name', () => { + chai.assert.throws(() => document.createElementNS(null, String.fromCodePoint(0x200b)), 'InvalidCharacterError'); + chai.assert.throws(() => document.createElementNS(null, 'a:b:c'), 'InvalidCharacterError'); + }); + + it('throws if given a prefixed name without a namespace', () => { + chai.assert.throws(() => document.createElementNS('', 'prf:test'), 'NamespaceError'); + }); + + it('throws if given an invalid use of a reserved prefix', () => { + chai.assert.throws(() => document.createElementNS('not the xml namespace', 'xml:test')); + chai.assert.throws(() => document.createElementNS('not the xmlns namespace', 'xmlns:test')); + chai.assert.throws(() => document.createElementNS('http://www.w3.org/2000/xmlns/', 'pre:test')); }); + }); + + describe('.createCDATASection', () => { + it('throws if data contains "]]>"', () => { + chai.assert.throws(() => document.createCDATASection('meep]]>maap'), 'InvalidCharacterError'); + }); + }); + + describe('.createProcessingInstruction', () => { + it('throws if given an invalid target', () => { + chai.assert.throws( + () => document.createProcessingInstruction(String.fromCodePoint(0x200b), 'some data'), + 'InvalidCharacterError' + ); + }); + + it('throws if data contains "?>"', () => { + chai.assert.throws(() => document.createProcessingInstruction('target', 'some ?> data'), 'InvalidCharacterError'); + }); + }); + + describe('.importNode', () => { + let otherDocument: slimdom.Document; + beforeEach(() => { + otherDocument = new slimdom.Document(); + }); + + it('returns a clone with the document as node document', () => { + const element = otherDocument.createElement('test'); + chai.assert.equal(element.ownerDocument, otherDocument); + const copy = document.importNode(element); + chai.assert.equal(copy.ownerDocument, document); + chai.assert.equal(copy.nodeName, element.nodeName); + chai.assert.notEqual(copy, element); + chai.assert.deepEqual(copy.childNodes, []); + }); + + it('can clone descendants', () => { + const element = otherDocument.createElement('test'); + element.appendChild(otherDocument.createElement('child')).appendChild(otherDocument.createTextNode('content')); + chai.assert.equal(element.ownerDocument, otherDocument); + const copy = document.importNode(element, true) as slimdom.Element; + chai.assert.equal(copy.ownerDocument, document); + chai.assert.equal(copy.nodeName, element.nodeName); + chai.assert.notEqual(copy, element); + + const child = copy.firstElementChild!; + chai.assert.equal(child.nodeName, 'child'); + chai.assert.equal(child.ownerDocument, document); + chai.assert.notEqual(child, element.firstElementChild); + + chai.assert.equal(child.firstChild!.ownerDocument, document); + chai.assert.equal((child.firstChild as slimdom.Text).data, 'content'); + }); + + it('throws if given a document node', () => { + chai.assert.throws(() => document.importNode(otherDocument, true), 'NotSupportedError'); + }); + + it('throws if given something other than a node', () => { + chai.assert.throws(() => (document as any).importNode('not a node'), TypeError); + }); + }); + + describe('.adoptNode', () => { + let otherDocument: slimdom.Document; + beforeEach(() => { + otherDocument = new slimdom.Document(); + }); + + it('modifies the node to set the document as its node document', () => { + const element = otherDocument.createElement('test'); + chai.assert.equal(element.ownerDocument, otherDocument); + const adopted = document.adoptNode(element); + chai.assert.equal(adopted.ownerDocument, document); + chai.assert.equal(adopted.nodeName, element.nodeName); + chai.assert.equal(adopted, element); + }); + + it('also adopts descendants and attributes', () => { + const element = otherDocument.createElement('test'); + element.appendChild(otherDocument.createElement('child')).appendChild(otherDocument.createTextNode('content')); + element.setAttribute('test', 'value'); + chai.assert.equal(element.ownerDocument, otherDocument); + const adopted = document.adoptNode(element) as slimdom.Element; + chai.assert.equal(adopted.ownerDocument, document); + chai.assert.equal(adopted.nodeName, element.nodeName); + chai.assert.equal(adopted, element); + + const child = adopted.firstElementChild!; + chai.assert.equal(child.ownerDocument, document); + chai.assert.equal(child.firstChild!.ownerDocument, document); + + const attr = adopted.getAttributeNode('test'); + chai.assert.equal(attr!.ownerDocument, document); + }); + + it('throws if given a document node', () => { + chai.assert.throws(() => document.adoptNode(otherDocument), 'NotSupportedError'); + }); + }); - it('has a new document element', () => { - chai.assert.equal((clone.documentElement as slimdom.Node).nodeType, 1); - chai.assert.equal((clone.documentElement as slimdom.Element).nodeName, 'root'); - chai.assert.notEqual(clone.documentElement, document.documentElement); + describe('.createAttribute', () => { + it('throws if given an invalid name', () => { + chai.assert.throws(() => document.createAttribute(String.fromCodePoint(0x200b)), 'InvalidCharacterError'); }); }); }); diff --git a/test/DocumentFragment.tests.ts b/test/DocumentFragment.tests.ts new file mode 100644 index 0000000..9538f07 --- /dev/null +++ b/test/DocumentFragment.tests.ts @@ -0,0 +1,102 @@ +import * as chai from 'chai'; +import * as slimdom from '../src/index'; + +describe('DocumentFragment', () => { + let document: slimdom.Document; + let fragment: slimdom.DocumentFragment; + beforeEach(() => { + document = new slimdom.Document(); + fragment = document.createDocumentFragment(); + }); + + it('can be created using Document#createDocumentFragment()', () => { + const df = document.createDocumentFragment(); + chai.assert.equal(df.nodeType, 11); + chai.assert.equal(df.nodeName, '#document-fragment'); + chai.assert.equal(df.nodeValue, null); + }); + + it('can not change its nodeValue', () => { + fragment.nodeValue = 'test'; + chai.assert.equal(fragment.nodeValue, null); + }); + + it('can not lookup namespaces or prefixes', () => { + fragment.appendChild(document.createElementNS('http://www.example.com/ns', 'prf:test')); + chai.assert.equal(fragment.lookupNamespaceURI('prf'), null); + chai.assert.equal(fragment.lookupPrefix('http://www.example.com/ns'), null); + }); + + it('initially has no childNodes', () => chai.assert.deepEqual(fragment.childNodes, [])); + + it('initially has no children', () => chai.assert.deepEqual(fragment.children, [])); + + it('correctly updates its relation properties when children are added', () => { + const child1 = fragment.appendChild(document.createElement('child1')) as slimdom.Element; + const text = fragment.appendChild(document.createTextNode('text')); + const child2 = fragment.appendChild(document.createElement('child2')) as slimdom.Element; + const pi = fragment.appendChild(document.createProcessingInstruction('target', 'data')); + const child3 = fragment.appendChild(document.createElement('child3')) as slimdom.Element; + chai.assert.deepEqual(fragment.childNodes, [child1, text, child2, pi, child3]); + chai.assert.deepEqual(fragment.children, [child1, child2, child3]); + chai.assert.equal(fragment.firstElementChild, child1); + chai.assert.equal(fragment.firstElementChild!.nextElementSibling, child2); + chai.assert.equal(fragment.lastElementChild!.previousElementSibling, child2); + chai.assert.equal(fragment.lastElementChild, child3); + fragment.removeChild(child2); + chai.assert.deepEqual(fragment.childNodes, [child1, text, pi, child3]); + chai.assert.deepEqual(fragment.children, [child1, child3]); + chai.assert.equal(fragment.firstElementChild, child1); + chai.assert.equal(fragment.firstElementChild!.nextElementSibling, child3); + chai.assert.equal(fragment.lastElementChild!.previousElementSibling, child1); + chai.assert.equal(fragment.lastElementChild, child3); + }); + + it('inserts its children if inserted under another node', () => { + const child1 = fragment.appendChild(document.createElement('child1')) as slimdom.Element; + const text = fragment.appendChild(document.createTextNode('text')); + const child2 = fragment.appendChild(document.createElement('child2')) as slimdom.Element; + const pi = fragment.appendChild(document.createProcessingInstruction('target', 'data')); + const child3 = fragment.appendChild(document.createElement('child3')) as slimdom.Element; + const parent = document.createElement('parent'); + const existingChild = parent.appendChild(document.createComment('test')); + parent.insertBefore(fragment, existingChild); + chai.assert.deepEqual(parent.childNodes, [child1, text, child2, pi, child3, existingChild]); + chai.assert.deepEqual(parent.children, [child1, child2, child3]); + chai.assert.equal(parent.firstElementChild, child1); + chai.assert.equal(parent.firstElementChild!.nextElementSibling, child2); + chai.assert.equal(parent.lastElementChild!.previousElementSibling, child2); + chai.assert.equal(parent.lastElementChild, child3); + }); + + describe('.cloneNode', () => { + beforeEach(() => { + fragment.appendChild(document.createElement('root')); + }); + + it('can be cloned (shallow)', () => { + const copy = fragment.cloneNode() as slimdom.DocumentFragment; + + chai.assert.equal(copy.nodeType, 11); + chai.assert.equal(copy.nodeName, '#document-fragment'); + chai.assert.equal(copy.nodeValue, null); + + chai.assert.equal(copy.firstChild, null); + + chai.assert.notEqual(copy, fragment); + }); + + it('can be cloned (deep)', () => { + const copy = fragment.cloneNode(true) as slimdom.DocumentFragment; + + chai.assert.equal(copy.nodeType, 11); + chai.assert.equal(copy.nodeName, '#document-fragment'); + chai.assert.equal(copy.nodeValue, null); + + chai.assert.equal(copy.firstChild!.nodeName, 'root'); + + chai.assert.notEqual(copy, document); + chai.assert.notEqual(copy.firstChild, fragment.firstChild); + }); + }); +}); diff --git a/test/DocumentType.tests.ts b/test/DocumentType.tests.ts index dc69d5f..9596c4f 100644 --- a/test/DocumentType.tests.ts +++ b/test/DocumentType.tests.ts @@ -2,26 +2,47 @@ import * as chai from 'chai'; import * as slimdom from '../src/index'; describe('DocumentType', () => { + let document: slimdom.Document; let doctype: slimdom.DocumentType; beforeEach(() => { - const document = new slimdom.Document(); + document = new slimdom.Document(); doctype = document.implementation.createDocumentType('somename', 'somePublicId', 'someSystemId'); }); - it('has nodeType 10', () => chai.assert.equal(doctype.nodeType, 10)); - - it('has a name', () => chai.assert.equal(doctype.name, 'somename')); - - it('has a publicId', () => chai.assert.equal(doctype.publicId, 'somePublicId')); + it('can be created using DOMImplementation#createDocumentType', () => { + const doctype = document.implementation.createDocumentType( + 'HTML', + '-//W3C//DTD HTML 4.01//EN', + 'http://www.w3.org/TR/html4/strict.dtd' + ); + chai.assert.equal(doctype.nodeType, 10); + chai.assert.equal(doctype.nodeName, 'HTML'); + chai.assert.equal(doctype.nodeValue, null); + chai.assert.equal(doctype.name, 'HTML'); + chai.assert.equal(doctype.publicId, '-//W3C//DTD HTML 4.01//EN'); + chai.assert.equal(doctype.systemId, 'http://www.w3.org/TR/html4/strict.dtd'); + chai.assert.equal(doctype.ownerDocument, document); + }); - it('has a systemId', () => chai.assert.equal(doctype.systemId, 'someSystemId')); + it('can not change its nodeValue', () => { + doctype.nodeValue = 'test'; + chai.assert.equal(document.nodeValue, null); + }); it('can be cloned', () => { - const clone = doctype.cloneNode(true) as slimdom.DocumentType; - chai.assert.equal(clone.nodeType, 10); - chai.assert.equal(clone.name, 'somename'); - chai.assert.equal(clone.publicId, 'somePublicId'); - chai.assert.equal(clone.systemId, 'someSystemId'); - chai.assert.notEqual(clone, doctype); + const copy = doctype.cloneNode(true) as slimdom.DocumentType; + chai.assert.equal(copy.nodeType, 10); + chai.assert.equal(copy.name, 'somename'); + chai.assert.equal(copy.publicId, 'somePublicId'); + chai.assert.equal(copy.systemId, 'someSystemId'); + chai.assert.equal(copy.ownerDocument, document); + chai.assert.notEqual(copy, doctype); + }); + + it('can not lookup namespaces or prefixes', () => { + document.appendChild(doctype); + document.appendChild(document.createElementNS('http://www.example.com/ns', 'prf:test')); + chai.assert.equal(doctype.lookupNamespaceURI('prf'), null); + chai.assert.equal(doctype.lookupPrefix('http://www.example.com/ns'), null); }); }); diff --git a/test/Element.tests.ts b/test/Element.tests.ts index a088f4a..e01f073 100644 --- a/test/Element.tests.ts +++ b/test/Element.tests.ts @@ -6,20 +6,82 @@ describe('Element', () => { let element: slimdom.Element; beforeEach(() => { document = new slimdom.Document(); - element = document.createElement('root'); + element = document.createElementNS('http://www.w3.org/2000/svg', 'svg:g'); }); - it('has nodeType 1', () => chai.assert.equal(element.nodeType, 1)); + it('can be created using Document#createElement', () => { + const element = document.createElement('test'); + chai.assert.equal(element.nodeType, 1); + chai.assert.equal(element.nodeName, 'test'); + chai.assert.equal(element.nodeValue, null); + chai.assert.equal(element.ownerDocument, document); + chai.assert.equal(element.namespaceURI, null); + chai.assert.equal(element.localName, 'test'); + chai.assert.equal(element.prefix, null); + }); - it('is owned by the document', () => chai.assert.equal(element.ownerDocument, document)); + it('can be created using Document#createElementNS', () => { + const element = document.createElementNS('http://www.example.com/ns', 'prf:test'); + chai.assert.equal(element.nodeType, 1); + chai.assert.equal(element.nodeName, 'prf:test'); + chai.assert.equal(element.nodeValue, null); + chai.assert.equal(element.ownerDocument, document); + chai.assert.equal(element.namespaceURI, 'http://www.example.com/ns'); + chai.assert.equal(element.localName, 'test'); + chai.assert.equal(element.prefix, 'prf'); + }); - it('initially has no child nodes', () => { + it('can not change its nodeValue', () => { + element.nodeValue = 'test'; + chai.assert.equal(element.nodeValue, null); + }); + + it('can lookup its own prefix or namespace', () => { + chai.assert.equal(element.lookupPrefix(null), null); + chai.assert.equal(element.lookupNamespaceURI(''), null); + chai.assert.equal(element.lookupNamespaceURI('svg'), 'http://www.w3.org/2000/svg'); + chai.assert.equal(element.lookupPrefix('http://www.w3.org/2000/svg'), 'svg'); + }); + + it('can lookup a prefix or namespace declared on itself', () => { + element.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'http://www.w3.org/2000/svg'); + element.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:prf', 'http://www.example.com/ns'); + chai.assert.equal(element.lookupPrefix(null), null); + chai.assert.equal(element.lookupNamespaceURI('prf'), 'http://www.example.com/ns'); + chai.assert.equal(element.lookupPrefix('http://www.example.com/ns'), 'prf'); + chai.assert.equal(element.lookupNamespaceURI(null), 'http://www.w3.org/2000/svg'); + chai.assert.equal((element as any).lookupNamespaceURI(undefined), 'http://www.w3.org/2000/svg'); + chai.assert.equal(element.lookupPrefix('http://www.w3.org/2000/svg'), 'svg'); + }); + + it('can lookup a prefix or namespace declared on an ancestor', () => { + const parent = document.createElement('svg'); + parent.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', ''); + parent.appendChild(element); + parent.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:prf', 'http://www.example.com/ns'); + chai.assert.equal(element.lookupPrefix(null), null); + chai.assert.equal(element.lookupNamespaceURI(null), null); + chai.assert.equal(element.lookupNamespaceURI('prf'), 'http://www.example.com/ns'); + chai.assert.equal(element.lookupPrefix('http://www.example.com/ns'), 'prf'); + chai.assert.equal(element.lookupPrefix('unknown'), null); + chai.assert.equal(element.lookupNamespaceURI('unknown'), null); + }); + + it('can check the default namespace', () => { + const element = document.createElementNS('http://www.w3.org/1999/xhtml', 'html'); + chai.assert(!element.isDefaultNamespace('http://www.w3.org/2000/svg')); + chai.assert(element.isDefaultNamespace('http://www.w3.org/1999/xhtml')); + chai.assert(document.createElement('test').isDefaultNamespace('')); + }); + + it('initially has no childNodes', () => { chai.assert.equal(element.firstChild, null); chai.assert.equal(element.lastChild, null); + chai.assert(!element.hasChildNodes()); chai.assert.deepEqual(element.childNodes, []); }); - it('initially has no child elements', () => { + it('initially has no children', () => { chai.assert.equal(element.firstElementChild, null); chai.assert.equal(element.lastElementChild, null); chai.assert.deepEqual(element.children, []); @@ -27,30 +89,45 @@ describe('Element', () => { }); it('initially has no attributes', () => { - chai.assert(!element.hasAttribute('test')); - chai.assert.equal(element.getAttribute('test'), null); - chai.assert.deepEqual(element.attributes, []); + chai.assert.equal(element.hasAttributes(), false); + chai.assert.deepEqual(Array.from(element.attributes), []); }); describe('setting attributes', () => { beforeEach(() => { element.setAttribute('firstAttribute', 'first'); element.setAttribute('test', '123'); - element.setAttribute('lastAttribute', 'last'); + element.setAttributeNS('http://www.example.com/ns', 'prf:lastAttribute', 'last'); + }); + + it('throws if the attribute name is invalid', () => { + chai.assert.throws(() => element.setAttribute(String.fromCodePoint(0x200b), 'value')); }); it('has the attributes', () => { + chai.assert(element.hasAttributes()); chai.assert(element.hasAttribute('firstAttribute'), 'has attribute firstAttribute'); + chai.assert(element.hasAttributeNS(null, 'firstAttribute'), 'has attribute firstAttribute'); chai.assert(element.hasAttribute('test'), 'has attribute test'); - chai.assert(element.hasAttribute('lastAttribute'), 'has attribute lastAttribute'); + chai.assert(element.hasAttributeNS(null, 'test'), 'has attribute test'); + chai.assert(element.hasAttribute('prf:lastAttribute'), 'has attribute lastAttribute'); + chai.assert(element.hasAttributeNS('http://www.example.com/ns', 'lastAttribute'), 'has attribute lastAttribute'); chai.assert(!element.hasAttribute('noSuchAttribute'), 'does not have attribute noSuchAttribute'); + chai.assert( + !element.hasAttributeNS(null, 'prf:lastAttribute'), + 'does not have attribute prf:lastAttribute without namespace' + ); }); it('returns the attribute value', () => { chai.assert.equal(element.getAttribute('firstAttribute'), 'first'); + chai.assert.equal(element.getAttributeNS('', 'firstAttribute'), 'first'); chai.assert.equal(element.getAttribute('test'), '123'); - chai.assert.equal(element.getAttribute('lastAttribute'), 'last'); + chai.assert.equal(element.getAttributeNS(null, 'test'), '123'); + chai.assert.equal(element.getAttribute('prf:lastAttribute'), 'last'); + chai.assert.equal(element.getAttributeNS('http://www.example.com/ns', 'lastAttribute'), 'last'); chai.assert.equal(element.getAttribute('noSuchAttribute'), null); + chai.assert.equal(element.getAttributeNS(null, 'prf:noSuchAttribute'), null); }); function hasAttributes(attributes: slimdom.Attr[], expected: { name: string; value: string }[]): boolean { @@ -66,7 +143,7 @@ describe('Element', () => { hasAttributes(element.attributes, [ { name: 'firstAttribute', value: 'first' }, { name: 'test', value: '123' }, - { name: 'lastAttribute', value: 'last' } + { name: 'prf:lastAttribute', value: 'last' } ]) )); @@ -78,7 +155,18 @@ describe('Element', () => { hasAttributes(element.attributes, [ { name: 'firstAttribute', value: 'first' }, { name: 'test', value: '456' }, - { name: 'lastAttribute', value: 'last' } + { name: 'prf:lastAttribute', value: 'last' } + ]) + ); + + element.setAttributeNS('http://www.example.com/ns', 'prf:lastAttribute', 'new value'); + chai.assert(element.hasAttributeNS('http://www.example.com/ns', 'lastAttribute'), 'has the attribute'); + chai.assert.equal(element.getAttributeNS('http://www.example.com/ns', 'lastAttribute'), 'new value'); + chai.assert( + hasAttributes(element.attributes, [ + { name: 'firstAttribute', value: 'first' }, + { name: 'test', value: '456' }, + { name: 'prf:lastAttribute', value: 'new value' } ]) ); }); @@ -87,13 +175,18 @@ describe('Element', () => { element.removeAttribute('test'); chai.assert(element.hasAttribute('firstAttribute'), 'has attribute firstAttribute'); chai.assert(!element.hasAttribute('test'), 'does not have attribute test'); - chai.assert(element.hasAttribute('lastAttribute'), 'has attribute lastAttribute'); + chai.assert(element.hasAttribute('prf:lastAttribute'), 'has attribute lastAttribute'); chai.assert( hasAttributes(element.attributes, [ { name: 'firstAttribute', value: 'first' }, - { name: 'lastAttribute', value: 'last' } + { name: 'prf:lastAttribute', value: 'last' } ]) ); + element.removeAttributeNS('http://www.example.com/ns', 'lastAttribute'); + chai.assert(hasAttributes(element.attributes, [{ name: 'firstAttribute', value: 'first' }])); + // Removing something that doesn't exist does nothing + element.removeAttributeNS('http://www.example.com/ns', 'missingAttribute'); + chai.assert(hasAttributes(element.attributes, [{ name: 'firstAttribute', value: 'first' }])); }); it('ignores removing non-existent attributes', () => { @@ -105,9 +198,44 @@ describe('Element', () => { hasAttributes(element.attributes, [ { name: 'firstAttribute', value: 'first' }, { name: 'test', value: '123' }, - { name: 'lastAttribute', value: 'last' } + { name: 'prf:lastAttribute', value: 'last' } + ]) + ); + }); + + it('can set attributes using their nodes', () => { + const attr = document.createAttribute('attr'); + attr.value = 'some value'; + chai.assert.equal(element.setAttributeNodeNS(attr), null); + const namespacedAttr = document.createAttributeNS('http://www.example.com/ns', 'prf:aaa'); + element.setAttributeNode(namespacedAttr); + chai.assert( + hasAttributes(element.attributes, [ + { name: 'firstAttribute', value: 'first' }, + { name: 'test', value: '123' }, + { name: 'prf:lastAttribute', value: 'last' }, + { name: 'attr', value: 'some value' }, + { name: 'prf:aaa', value: '' } ]) ); + + // It returns the previous attribute node + chai.assert.equal(element.setAttributeNode(attr), attr); + chai.assert.equal(element.setAttributeNode(document.createAttribute('attr')), attr); + + const otherElement = document.createElement('test'); + chai.assert.throws(() => otherElement.setAttributeNode(namespacedAttr), 'InUseAttributeError'); + }); + + it('can remove attributes using their nodes', () => { + const attr = element.removeAttributeNode(element.attributes[1]); + chai.assert( + hasAttributes(element.attributes, [ + { name: 'firstAttribute', value: 'first' }, + { name: 'prf:lastAttribute', value: 'last' } + ]) + ); + chai.assert.throws(() => element.removeAttributeNode(attr), 'NotFoundError'); }); }); @@ -323,5 +451,94 @@ describe('Element', () => { chai.assert.equal((element.firstChild as slimdom.Text).nodeValue, 'test123abc'); chai.assert.equal((element.firstChild as slimdom.Text).data, 'test123abc'); }); + + it('recursively normalizes the entire subtree', () => { + element.appendChild(document.createTextNode('test')); + element.appendChild(document.createTextNode('123')); + element.appendChild(document.createTextNode('abc')); + const child = element.appendChild(document.createElement('child')); + child.appendChild(document.createTextNode('child')); + child.appendChild(document.createTextNode('')); + child.appendChild(document.createTextNode('content')); + const otherChild = element.appendChild(document.createElement('empty')); + otherChild.appendChild(document.createTextNode('text')); + otherChild.appendChild(document.createTextNode('')); + otherChild.appendChild(document.createTextNode('')); + element.normalize(); + chai.assert.equal(element.childNodes.length, 3); + chai.assert.equal((element.firstChild as slimdom.Text).nodeValue, 'test123abc'); + chai.assert.equal(child.childNodes.length, 1); + chai.assert.equal((child.firstChild as slimdom.Text).data, 'childcontent'); + chai.assert.equal(otherChild.childNodes.length, 1); + chai.assert.equal((otherChild.firstChild as slimdom.Text).data, 'text'); + }); + + it('adjusts ranges appropriately', () => { + let range1 = new slimdom.Range(); + let range2 = new slimdom.Range(); + element.appendChild(document.createTextNode('test')); + element.appendChild(document.createTextNode('123')); + element.appendChild(document.createTextNode('abc')); + range1.setStart(element.childNodes[1], 0); + range1.setEnd(element, 2); + range2.setStart(element, 1); + range2.setEnd(element.lastChild!, 0); + element.normalize(); + chai.assert.equal(range1.startContainer, element.firstChild); + chai.assert.equal(range1.startOffset, 4); + chai.assert.equal(range1.endContainer, element.firstChild); + chai.assert.equal(range1.endOffset, 7); + chai.assert.equal(range2.startContainer, element.firstChild); + chai.assert.equal(range2.startOffset, 4); + chai.assert.equal(range2.endContainer, element.firstChild); + chai.assert.equal(range2.endOffset, 7); + range1.detach(); + range2.detach(); + }); + }); + + describe('.cloneNode', () => { + beforeEach(() => { + document.appendChild(element); + element.setAttributeNS('http://www.example.com/ns', 'test', 'value'); + element.appendChild(document.createElement('child')); + }); + + it('can be cloned (shallow)', () => { + const copy = element.cloneNode() as slimdom.Element; + + chai.assert.equal(copy.nodeType, 1); + chai.assert.equal(copy.nodeName, 'svg:g'); + chai.assert.equal(copy.nodeValue, null); + chai.assert.equal(copy.ownerDocument, document); + chai.assert.equal(copy.namespaceURI, 'http://www.w3.org/2000/svg'); + chai.assert.equal(copy.localName, 'g'); + chai.assert.equal(copy.prefix, 'svg'); + chai.assert.equal(copy.ownerDocument, document); + chai.assert.equal(copy.firstChild, null); + chai.assert.notEqual(copy, element); + + chai.assert.equal(copy.getAttributeNS('http://www.example.com/ns', 'test'), 'value'); + }); + + it('can be cloned (deep)', () => { + const copy = element.cloneNode(true) as slimdom.Element; + + chai.assert.equal(copy.nodeType, 1); + chai.assert.equal(copy.nodeName, 'svg:g'); + chai.assert.equal(copy.nodeValue, null); + chai.assert.equal(copy.ownerDocument, document); + chai.assert.equal(copy.namespaceURI, 'http://www.w3.org/2000/svg'); + chai.assert.equal(copy.localName, 'g'); + chai.assert.equal(copy.prefix, 'svg'); + chai.assert.equal(copy.ownerDocument, document); + chai.assert.notEqual(copy, element); + + chai.assert.equal(copy.getAttributeNS('http://www.example.com/ns', 'test'), 'value'); + + const child = copy.firstChild!; + chai.assert.equal(child.nodeName, 'child'); + chai.assert.notEqual(child, element.firstChild); + }); }); }); diff --git a/test/MutationObserver.tests.ts b/test/MutationObserver.tests.ts index 78efc32..7872dea 100644 --- a/test/MutationObserver.tests.ts +++ b/test/MutationObserver.tests.ts @@ -12,222 +12,478 @@ describe('MutationObserver', () => { clock.uninstall(); }); + let document: slimdom.Document; + let observer: slimdom.MutationObserver; + let calls: { records: slimdom.MutationRecord[]; observer: slimdom.MutationObserver }[]; let callbackCalled: boolean; - let callbackArgs: any[] = []; - function callback(...args: any[]) { + function callback(records: slimdom.MutationRecord[], observer: slimdom.MutationObserver) { callbackCalled = true; - callbackArgs.push(args); + calls.push({ records, observer }); } - let document: slimdom.Document; - let element: slimdom.Element; - let text: slimdom.Text; - let observer: slimdom.MutationObserver; beforeEach(() => { - callbackCalled = false; - callbackArgs.length = 0; - document = new slimdom.Document(); - element = document.appendChild(document.createElement('root')) as slimdom.Element; - text = element.appendChild(document.createTextNode('text')) as slimdom.Text; observer = new slimdom.MutationObserver(callback); - observer.observe(element, { - subtree: true, - characterData: true, - childList: true, - attributes: true - }); + calls = []; + callbackCalled = false; }); afterEach(() => { observer.disconnect(); }); - describe('synchronous usage', () => { - it('responds to text changes', () => { - text.data = 'meep'; - - const queue = observer.takeRecords(); - chai.assert.equal(queue[0].type, 'characterData'); - chai.assert.equal(queue[0].oldValue, null); - chai.assert.equal(queue[0].target, text); + interface ExpectedRecord { + type?: string; + target?: slimdom.Node; + oldValue?: string | null; + attributeName?: string; + attributeNamespace?: string | null; + addedNodes?: slimdom.Node[]; + removedNodes?: slimdom.Node[]; + previousSibling?: slimdom.Node | null; + nextSibling?: slimdom.Node | null; + } - clock.tick(100); - chai.assert(!callbackCalled, 'callback was not called'); + function assertRecords(records: slimdom.MutationRecord[], expected: ExpectedRecord[]): void { + chai.assert.equal(records.length, expected.length); + expected.forEach((expectedRecord, i) => { + const actualRecord = records[i]; + Object.keys(expectedRecord).forEach(key => { + const expectedValue = (expectedRecord as any)[key]; + const actualValue = (actualRecord as any)[key]; + if (Array.isArray(expectedValue)) { + chai.assert.deepEqual(actualValue, expectedValue, `property ${key} of record ${i}`); + } else { + chai.assert.equal( + actualValue, + expectedValue, + `property ${key} of record ${i} is ${actualValue}, expected ${expectedValue}` + ); + } + }); }); + } - it('records previous text values', () => { - observer.observe(element, { subtree: true, characterDataOldValue: true }); - - text.data = 'meep'; - const queue = observer.takeRecords(); - chai.assert.equal(queue[0].type, 'characterData'); - chai.assert.equal(queue[0].oldValue, 'text'); - chai.assert.equal(queue[0].target, text); - - clock.tick(100); - chai.assert(!callbackCalled, 'callback was not called'); + describe('.observe', () => { + it("throws if options doesn't specify the types of mutation to observe", () => { + const observer = new slimdom.MutationObserver(() => {}); + chai.assert.throws(() => observer.observe(document, {}), TypeError); }); - it('responds to attribute changes', () => { - element.setAttribute('test', 'meep'); - - const queue = observer.takeRecords(); - chai.assert.equal(queue[0].type, 'attributes'); - chai.assert.equal(queue[0].attributeName, 'test'); - chai.assert.equal(queue[0].oldValue, null); - chai.assert.equal(queue[0].target, element); - - clock.tick(100); - chai.assert(!callbackCalled, 'callback was not called'); + it('throws if asking for the old value of attributes without observing them', () => { + const observer = new slimdom.MutationObserver(() => {}); + chai.assert.throws( + () => observer.observe(document, { attributes: false, attributeOldValue: true, childList: true }), + TypeError + ); }); - it('does not ignore same-value attribute changes', () => { - element.setAttribute('test', 'meep'); - let queue = observer.takeRecords(); - - observer.observe(element, { attributeOldValue: true }); - - element.setAttribute('test', 'meep'); - - queue = observer.takeRecords(); - chai.assert.equal(queue[0].type, 'attributes'); - chai.assert.equal(queue[0].oldValue, 'meep'); - chai.assert.equal(queue[0].target, element); - - clock.tick(100); - chai.assert(!callbackCalled, 'callback was not called'); + it('throws if asking for the old value of character data without observing them', () => { + const observer = new slimdom.MutationObserver(() => {}); + chai.assert.throws( + () => observer.observe(document, { characterData: false, characterDataOldValue: true, childList: true }), + TypeError + ); }); + }); - it('records previous attribute values', () => { - element.setAttribute('test', 'meep'); - let queue = observer.takeRecords(); - - observer.observe(element, { attributeOldValue: true }); + type TestCase = (observer: slimdom.MutationObserver) => ExpectedRecord[] | null; + const cases: { [description: string]: TestCase } = { + 'responds to text changes': observer => { + const element = document.createElement('test'); + const text = element.appendChild(document.createTextNode('text')) as slimdom.Text; + observer.observe(element, { subtree: true, characterData: true }); - element.setAttribute('test', 'maap'); + text.data = 'meep'; - queue = observer.takeRecords(); - chai.assert.equal(queue[0].type, 'attributes'); - chai.assert.equal(queue[0].attributeName, 'test'); - chai.assert.equal(queue[0].oldValue, 'meep'); - chai.assert.equal(queue[0].target, element); + return [{ type: 'characterData', oldValue: null, target: text }]; + }, - clock.tick(100); - chai.assert(!callbackCalled, 'callback was not called'); - }); + 'records previous text values': observer => { + const element = document.createElement('test'); + const text = element.appendChild(document.createTextNode('text')) as slimdom.Text; + observer.observe(element, { subtree: true, characterDataOldValue: true }); - it('responds to insertions (appendChild)', () => { - const newElement = document.createElement('meep'); - element.appendChild(newElement); + text.data = 'meep'; - const queue = observer.takeRecords(); - chai.assert.equal(queue[0].type, 'childList'); - chai.assert.deepEqual(queue[0].addedNodes, [newElement]); - chai.assert.deepEqual(queue[0].removedNodes, []); - chai.assert.equal(queue[0].previousSibling, text); - chai.assert.equal(queue[0].nextSibling, null); - }); + return [{ type: 'characterData', oldValue: 'text', target: text }]; + }, + + 'responds to attribute changes': observer => { + const element = document.createElement('test'); + element.setAttribute('attr', 'value'); + observer.observe(element, { attributes: true }); + + // Even same-value changes generate records + element.setAttribute('attr', 'value'); + element.setAttributeNS('http://www.example.com/ns', 'prf:attr', 'value'); + + return [ + { + type: 'attributes', + target: element, + attributeName: 'attr', + attributeNamespace: null, + oldValue: null + }, + { + type: 'attributes', + target: element, + attributeName: 'attr', + attributeNamespace: 'http://www.example.com/ns', + oldValue: null + } + ]; + }, + + 'records previous attribute values': observer => { + const element = document.createElement('test'); + element.setAttribute('attr', 'value'); + observer.observe(element, { attributeOldValue: true }); - it('responds to insertions (replaceChild)', () => { - const newElement = document.createElement('meep'); - element.replaceChild(newElement, text); + // Even same-value changes generate records + element.setAttribute('attr', 'value'); + element.setAttributeNS('http://www.example.com/ns', 'prf:attr', 'value'); + + return [ + { + type: 'attributes', + target: element, + attributeName: 'attr', + attributeNamespace: null, + oldValue: 'value' + }, + { + type: 'attributes', + target: element, + attributeName: 'attr', + attributeNamespace: 'http://www.example.com/ns', + oldValue: null + } + ]; + }, + + 'responds to insertions (appendChild)': observer => { + const comment = document.appendChild(document.createComment('test')); + const element = document.createElement('child'); + observer.observe(document, { childList: true }); + + document.appendChild(element); + + return [ + { + type: 'childList', + target: document, + addedNodes: [element], + removedNodes: [], + previousSibling: comment, + nextSibling: null + } + ]; + }, + + 'responds to insertions (replaceChild)': observer => { + const parent = document.appendChild(document.createElement('parent')); + const oldChild = parent.appendChild(document.createElement('old')); + const newChild = document.createElement('new'); + observer.observe(document, { childList: true, subtree: true }); + parent.replaceChild(newChild, oldChild); + + return [ + { + type: 'childList', + target: parent, + addedNodes: [newChild], + removedNodes: [oldChild], + nextSibling: null, + previousSibling: null + } + ]; + }, + + 'responds to moves (insertBefore)': observer => { + const comment = document.appendChild(document.createComment('comment')); + const element = document.appendChild(document.createElement('element')); + const text = element.appendChild(document.createTextNode('text')); + observer.observe(document, { childList: true, subtree: true }); + + element.insertBefore(comment, text); + + return [ + { + type: 'childList', + target: document, + addedNodes: [], + removedNodes: [comment], + nextSibling: element, + previousSibling: null + }, + { + type: 'childList', + target: element, + addedNodes: [comment], + removedNodes: [], + nextSibling: text, + previousSibling: null + } + ]; + }, + + 'does not respond to attribute changes if the attributes option is not set': observer => { + const element = document.createElement('test'); + observer.observe(element, { attributes: false, childList: true }); + element.setAttribute('test', 'value'); + + return null; + }, + + 'does not respond to character data changes if the characterData option is not set': observer => { + const text = document.createTextNode('test'); + observer.observe(text, { childList: true, characterData: false }); + text.nodeValue = 'prrrt'; + + return null; + }, + + 'does not respond to childList changes if the childList option is not set': observer => { + const element = document.createElement('test'); + observer.observe(element, { attributes: true, childList: false }); + element.appendChild(document.createElement('child')); + + return null; + }, + + 'does not respond to subtree mutations if the subtree option is not set': observer => { + const element = document.appendChild(document.createElement('test')) as slimdom.Element; + observer.observe(document, { attributes: true, childList: true }); + element.appendChild(document.createElement('child')); + element.setAttribute('test', 'value'); + + return null; + }, + + 'only responds once to subtree mutations, even when observing multiple ancestors': observer => { + const element = document.appendChild(document.createElement('element')); + observer.observe(document, { childList: true, subtree: true }); + observer.observe(element, { childList: true, subtree: true }); + const comment = element.appendChild(document.createComment('test')); + + return [ + { + type: 'childList', + target: element, + addedNodes: [comment], + removedNodes: [], + previousSibling: null, + nextSibling: null + } + ]; + }, + + 'continues tracking under a removed node until javascript re-enters the event loop': observer => { + const parent = document.appendChild(document.createElement('parent')); + const child = parent.appendChild(document.createElement('child')); + const text = child.appendChild(document.createTextNode('text')) as slimdom.Text; + observer.observe(document, { childList: true, characterDataOldValue: true, subtree: true }); + document.removeChild(parent); + parent.removeChild(child); + text.data = 'test'; + + return [ + { + type: 'childList', + target: document, + removedNodes: [parent] + }, + { + type: 'childList', + target: parent, + removedNodes: [child] + }, + { + type: 'characterData', + target: text, + oldValue: 'text' + } + ]; + }, + + 'does not add transient registered observers for non-subtree observers': observer => { + const parent = document.appendChild(document.createElement('parent')); + const child = parent.appendChild(document.createElement('child')); + const text = child.appendChild(document.createTextNode('text')) as slimdom.Text; + observer.observe(document, { childList: true, characterDataOldValue: true, subtree: false }); + document.removeChild(parent); + parent.removeChild(child); + text.data = 'test'; + + return [ + { + type: 'childList', + target: document, + removedNodes: [parent] + } + ]; + }, + + 'removes transient observers when observe is called for the same observer': observer => { + const parent = document.appendChild(document.createElement('parent')); + const child = parent.appendChild(document.createElement('child')); + const text = child.appendChild(document.createTextNode('text')) as slimdom.Text; + observer.observe(document, { childList: true, characterDataOldValue: true, subtree: true }); + document.removeChild(parent); + observer.observe(document, { childList: true, characterDataOldValue: true, subtree: true }); + parent.removeChild(child); + text.data = 'test'; + + return [ + { + type: 'childList', + target: document, + removedNodes: [parent] + } + ]; + }, + + 'does not remove transient observers when observe is called for a different observer': observer => { + const parent = document.appendChild(document.createElement('parent')); + const child = parent.appendChild(document.createElement('child')); + const text = child.appendChild(document.createTextNode('text')) as slimdom.Text; + observer.observe(document, { childList: true, characterDataOldValue: true, subtree: true }); + const otherObserver = new slimdom.MutationObserver(callback); + otherObserver.observe(document, { childList: true, characterDataOldValue: true, subtree: true }); + document.removeChild(parent); + otherObserver.observe(document, { childList: true, characterDataOldValue: true, subtree: true }); + parent.removeChild(child); + text.data = 'test'; + + assertRecords(otherObserver.takeRecords(), [ + { + type: 'childList', + target: document, + removedNodes: [parent] + } + ]); + + return [ + { + type: 'childList', + target: document, + removedNodes: [parent] + }, + { + type: 'childList', + target: parent, + removedNodes: [child] + }, + { + type: 'characterData', + target: text, + oldValue: 'text' + } + ]; + }, + + 'does not remove transient observers when observe is called for a different subtree': observer => { + const parent = document.appendChild(document.createElement('parent')); + const child1 = parent.appendChild(document.createElement('child1')); + const child2 = parent.appendChild(document.createElement('child2')); + observer.observe(parent, { childList: true, subtree: true }); + observer.observe(child1, { childList: true, subtree: true }); + observer.observe(child2, { childList: true, subtree: true }); + parent.removeChild(child1); + observer.observe(child2, { childList: true, subtree: true }); + const comment1 = child1.appendChild(document.createComment('test')); + const comment2 = child2.appendChild(document.createComment('test')); + + return [ + { + type: 'childList', + target: parent, + removedNodes: [child1] + }, + { + type: 'childList', + target: child1, + addedNodes: [comment1] + }, + { + type: 'childList', + target: child2, + addedNodes: [comment2] + } + ]; + }, + + 'does not observe after being disconnected': observer => { + observer.observe(document, { childList: true }); + observer.disconnect(); + document.appendChild(document.createComment('test')); + + return null; + }, + + 'does not affect other observers when disconnected': observer => { + const otherObserver = new slimdom.MutationObserver(callback); + otherObserver.observe(document, { childList: true, subtree: true }); + observer.observe(document, { childList: true }); + observer.disconnect(); + const comment = document.appendChild(document.createComment('test')); + + assertRecords(otherObserver.takeRecords(), [ + { + type: 'childList', + target: document, + addedNodes: [comment] + } + ]); + + return null; + } + }; - const queue = observer.takeRecords(); - chai.assert.equal(queue[0].type, 'childList'); - chai.assert.deepEqual(queue[0].addedNodes, [newElement]); - chai.assert.deepEqual(queue[0].removedNodes, [text]); - chai.assert.equal(queue[0].previousSibling, null); - chai.assert.equal(queue[0].nextSibling, null); - }); + describe('synchronous usage', () => { + Object.keys(cases).forEach(description => { + const testCase = cases[description]; + it(description, () => { + const expected = testCase(observer) || []; - it('responds to moves (insertBefore)', () => { - const newElement = document.createElement('meep'); - element.appendChild(newElement); - observer.takeRecords(); - - element.insertBefore(newElement, text); - - const queue = observer.takeRecords(); - chai.assert.equal(queue[0].type, 'childList'); - chai.assert.deepEqual(queue[0].addedNodes, []); - chai.assert.deepEqual(queue[0].removedNodes, [newElement]); - chai.assert.equal(queue[0].previousSibling, text); - chai.assert.equal(queue[0].nextSibling, null); - - chai.assert.equal(queue[1].type, 'childList'); - chai.assert.deepEqual(queue[1].addedNodes, [newElement]); - chai.assert.deepEqual(queue[1].removedNodes, []); - chai.assert.equal(queue[1].previousSibling, null); - chai.assert.equal(queue[1].nextSibling, text); - }); + const records = observer.takeRecords(); + assertRecords(records, expected); - it('responds to moves (replaceChild)', () => { - const newElement = document.createElement('meep'); - element.appendChild(newElement); - observer.takeRecords(); - - element.replaceChild(newElement, text); - - const queue = observer.takeRecords(); - chai.assert.equal(queue[0].type, 'childList'); - chai.assert.equal(queue[0].target, element); - chai.assert.deepEqual(queue[0].addedNodes, []); - chai.assert.deepEqual(queue[0].removedNodes, [newElement]); - chai.assert.equal(queue[0].previousSibling, text); - chai.assert.equal(queue[0].nextSibling, null); - - chai.assert.equal(queue[1].type, 'childList'); - chai.assert.equal(queue[1].target, element); - chai.assert.deepEqual(queue[1].addedNodes, [newElement]); - chai.assert.deepEqual(queue[1].removedNodes, [text]); - chai.assert.equal(queue[1].previousSibling, null); - chai.assert.equal(queue[1].nextSibling, null); - }); + clock.tick(100); - it('continues tracking under a removed node until javascript re-enters the event loop', () => { - observer.observe(element, { subtree: true, characterDataOldValue: true, childList: true }); - const newElement = element.appendChild(document.createElement('meep')) as slimdom.Element; - const newText = newElement.appendChild(document.createTextNode('test')) as slimdom.Text; - element.appendChild(newElement); - observer.takeRecords(); - - element.removeChild(newElement); - observer.takeRecords(); - - newText.replaceData(0, text.length, 'meep'); - let queue = observer.takeRecords(); - chai.assert.equal(queue.length, 1); - chai.assert.equal(queue[0].type, 'characterData'); - chai.assert.equal(queue[0].oldValue, 'test'); - chai.assert.equal(queue[0].target, newText); - - newElement.removeChild(newText); - queue = observer.takeRecords(); - chai.assert.equal(queue.length, 1); - chai.assert.equal(queue[0].type, 'childList'); - chai.assert.equal(queue[0].target, newElement); - chai.assert.equal(queue[0].removedNodes[0], newText); - - clock.tick(100); - - newElement.appendChild(newText); - queue = observer.takeRecords(); - chai.assert.deepEqual(queue, []); + chai.assert(!callbackCalled, 'callback was not called'); + }); }); }); describe('asynchronous usage', () => { - it('responds to text changes', () => { - observer.observe(element, { subtree: true, characterDataOldValue: true }); + let observer: slimdom.MutationObserver; + beforeEach(() => { + observer = new slimdom.MutationObserver(callback); + }); - text.data = 'meep'; + afterEach(() => { + observer.disconnect(); + }); - clock.tick(100); - chai.assert(callbackCalled, 'callback was called'); - chai.assert.equal(callbackArgs[0][0][0].type, 'characterData'); - chai.assert.equal(callbackArgs[0][0][0].oldValue, 'text'); - chai.assert.equal(callbackArgs[0][0][0].target, text); + Object.keys(cases).forEach(description => { + const testCase = cases[description]; + it(description, () => { + const expected = testCase(observer); + + clock.tick(100); + + if (expected !== null) { + chai.assert(callbackCalled, 'callback was called'); + chai.assert.equal(calls.length, 1); + chai.assert.equal(calls[0].observer, observer); + assertRecords(calls[0].records, expected); + } else { + chai.assert(!callbackCalled, 'callback was not called'); + } + }); }); }); }); diff --git a/test/ProcessingInstruction.tests.ts b/test/ProcessingInstruction.tests.ts index b0a1998..50fc100 100644 --- a/test/ProcessingInstruction.tests.ts +++ b/test/ProcessingInstruction.tests.ts @@ -3,29 +3,29 @@ import * as slimdom from '../src/index'; describe('ProcessingInstruction', () => { let document: slimdom.Document; - let processingInstruction: slimdom.ProcessingInstruction; beforeEach(() => { document = new slimdom.Document(); - processingInstruction = document.createProcessingInstruction('sometarget', 'somedata'); }); - it('has nodeType 7', () => chai.assert.equal(processingInstruction.nodeType, 7)); - - it('has data', () => { - chai.assert.equal(processingInstruction.nodeValue, 'somedata'); - chai.assert.equal(processingInstruction.data, 'somedata'); - }); - - it('has a target', () => { - chai.assert.equal(processingInstruction.target, 'sometarget'); + it('can be created using Document#createProcessingInstruction()', () => { + const pi = document.createProcessingInstruction('sometarget', 'some data'); + chai.assert.equal(pi.nodeType, 7); + chai.assert.equal(pi.nodeName, 'sometarget'); + chai.assert.equal(pi.nodeValue, 'some data'); + chai.assert.equal(pi.target, 'sometarget'); + chai.assert.equal(pi.data, 'some data'); + chai.assert.equal(pi.ownerDocument, document); }); it('can be cloned', () => { - var clone = processingInstruction.cloneNode(true) as slimdom.ProcessingInstruction; - chai.assert.equal(clone.nodeType, 7); - chai.assert.equal(clone.nodeValue, 'somedata'); - chai.assert.equal(clone.data, 'somedata'); - chai.assert.equal(clone.target, 'sometarget'); - chai.assert.notEqual(clone, processingInstruction); + const pi = document.createProcessingInstruction('sometarget', 'some data'); + var copy = pi.cloneNode() as slimdom.ProcessingInstruction; + chai.assert.equal(copy.nodeType, 7); + chai.assert.equal(copy.nodeName, 'sometarget'); + chai.assert.equal(copy.nodeValue, 'some data'); + chai.assert.equal(copy.target, 'sometarget'); + chai.assert.equal(copy.data, 'some data'); + chai.assert.equal(copy.ownerDocument, document); + chai.assert.notEqual(copy, pi); }); }); diff --git a/test/Range.tests.ts b/test/Range.tests.ts index 07536f5..6d75b26 100644 --- a/test/Range.tests.ts +++ b/test/Range.tests.ts @@ -19,6 +19,7 @@ describe('Range', () => { chai.assert.equal(range.endContainer, document); chai.assert.equal(range.startOffset, 0); chai.assert.equal(range.endOffset, 0); + chai.assert.equal(range.commonAncestorContainer, document); }); describe('setting positions', () => { @@ -29,6 +30,7 @@ describe('Range', () => { chai.assert.equal(range.endContainer, element); chai.assert.equal(range.endOffset, 0); chai.assert.equal(range.collapsed, true); + chai.assert.equal(range.commonAncestorContainer, element); }); it('end after start is ok', () => { @@ -38,6 +40,52 @@ describe('Range', () => { chai.assert.equal(range.startContainer, document); chai.assert.equal(range.startOffset, 0); chai.assert.equal(range.collapsed, false); + chai.assert.equal(range.commonAncestorContainer, document); + }); + + it('end before start moves start', () => { + range.setStart(element, 1); + range.setEnd(element, 0); + chai.assert.equal(range.endContainer, element); + chai.assert.equal(range.endOffset, 0); + chai.assert.equal(range.startContainer, element); + chai.assert.equal(range.startOffset, 0); + chai.assert.equal(range.collapsed, true); + chai.assert.equal(range.commonAncestorContainer, element); + }); + + it('throws if the container is a doctype', () => { + const doctype = document.implementation.createDocumentType('html', '', ''); + chai.assert.throws(() => range.setStart(doctype, 0), 'InvalidNodeTypeError'); + chai.assert.throws(() => range.setEnd(doctype, 0), 'InvalidNodeTypeError'); + }); + + it('throws if the index is beyond the length of the node', () => { + chai.assert.throws(() => range.setStart(text, 5), 'IndexSizeError'); + chai.assert.throws(() => range.setEnd(text, -1), 'IndexSizeError'); + }); + + it('can set its endpoints relative to a node', () => { + range.setStartBefore(element); + range.setEndBefore(text); + chai.assert.equal(range.startContainer, document); + chai.assert.equal(range.startOffset, 0); + chai.assert.equal(range.endContainer, element); + chai.assert.equal(range.endOffset, 0); + range.setStartAfter(text); + range.setEndAfter(element); + chai.assert.equal(range.startContainer, element); + chai.assert.equal(range.startOffset, 1); + chai.assert.equal(range.endContainer, document); + chai.assert.equal(range.endOffset, 1); + }); + + it('can not set an endpoint before or after a node without a parent', () => { + const detached = document.createElement('noparent'); + chai.assert.throws(() => range.setStartBefore(detached), 'InvalidNodeTypeError'); + chai.assert.throws(() => range.setStartAfter(detached), 'InvalidNodeTypeError'); + chai.assert.throws(() => range.setEndBefore(detached), 'InvalidNodeTypeError'); + chai.assert.throws(() => range.setEndAfter(detached), 'InvalidNodeTypeError'); }); it('can selectNode', () => { @@ -47,6 +95,12 @@ describe('Range', () => { chai.assert.equal(range.endContainer, document); chai.assert.equal(range.endOffset, 1); chai.assert.equal(range.collapsed, false); + chai.assert.equal(range.commonAncestorContainer, document); + }); + + it('can not selectNode a node without a parent', () => { + const detached = document.createElement('noparent'); + chai.assert.throws(() => range.selectNode(detached), 'InvalidNodeTypeError'); }); it('can selectNodeContents', () => { @@ -56,6 +110,12 @@ describe('Range', () => { chai.assert.equal(range.endContainer, element); chai.assert.equal(range.endOffset, 1); chai.assert.equal(range.collapsed, false); + chai.assert.equal(range.commonAncestorContainer, element); + }); + + it('can not selectNodeContents a doctype', () => { + const doctype = document.implementation.createDocumentType('html', '', ''); + chai.assert.throws(() => range.selectNodeContents(doctype), 'InvalidNodeTypeError'); }); it('can be collapsed to start', () => { @@ -70,7 +130,7 @@ describe('Range', () => { it('can be collapsed to end', () => { range.selectNodeContents(element); - range.collapse(false); + range.collapse(); chai.assert.equal(range.startContainer, element); chai.assert.equal(range.startOffset, 1); chai.assert.equal(range.endContainer, element); @@ -78,16 +138,85 @@ describe('Range', () => { chai.assert.equal(range.collapsed, true); }); - it('can be cloned', () => { - range.selectNodeContents(element); - const clone = range.cloneRange(); - range.setStart(document, 0); - range.collapse(true); - chai.assert.equal(clone.startContainer, element); - chai.assert.equal(clone.startOffset, 0); - chai.assert.equal(clone.endContainer, element); - chai.assert.equal(clone.endOffset, 1); - chai.assert.equal(clone.collapsed, false); + it('can compute the common ancestor', () => { + const child = element.appendChild(document.createElement('child')).appendChild(document.createTextNode('test')); + range.setStart(text, 0); + range.setEnd(child, 0); + chai.assert.equal(range.commonAncestorContainer, element); + }); + }); + + it('can be cloned', () => { + range.selectNodeContents(element); + const clone = range.cloneRange(); + range.setStart(document, 0); + range.collapse(true); + chai.assert.equal(clone.startContainer, element); + chai.assert.equal(clone.startOffset, 0); + chai.assert.equal(clone.endContainer, element); + chai.assert.equal(clone.endOffset, 1); + chai.assert.equal(clone.collapsed, false); + }); + + describe('comparing points', () => { + it('can compare boundary points agains another range', () => { + const range2 = document.createRange(); + range.setStart(element, 0); + range.setEnd(text, 2); + range2.setStart(text, 2); + range2.setEnd(document, 1); + chai.assert.throws(() => range.compareBoundaryPoints(98, range2), 'NotSupportedError'); + + chai.assert.equal(range.compareBoundaryPoints(slimdom.Range.START_TO_START, range2), -1); + chai.assert.equal(range.compareBoundaryPoints(slimdom.Range.START_TO_END, range2), 0); + chai.assert.equal(range2.compareBoundaryPoints(slimdom.Range.END_TO_END, range), 1); + chai.assert.equal(range.compareBoundaryPoints(slimdom.Range.END_TO_START, range2), -1); + range2.detach(); + }); + + it('can not compare boundary points if the ranges are in different documents', () => { + const range2 = new slimdom.Range(); + chai.assert.throws(() => range.compareBoundaryPoints(slimdom.Range.START_TO_START, range2)); + range2.detach(); + range2.detach(); + }); + + it('can compare a given point to the range', () => { + range.setStart(element, 0); + range.setEnd(element, 1); + + chai.assert(range.isPointInRange(text, 1)); + chai.assert(!range.isPointInRange(document, 1)); + + const range2 = new slimdom.Range(); + const doctype = document.implementation.createDocumentType('html', '', ''); + document.insertBefore(doctype, document.documentElement); + + chai.assert(!range2.isPointInRange(element, 0)); + chai.assert.throws(() => range.isPointInRange(doctype, 0), 'InvalidNodeTypeError'); + chai.assert.throws(() => range.isPointInRange(element, 3), 'IndexSizeError'); + + chai.assert.equal(range.comparePoint(element, 0), 0); + chai.assert.equal(range.comparePoint(document, 1), -1); + chai.assert.equal(range.comparePoint(document, 2), 1); + + chai.assert.throws(() => range2.comparePoint(element, 0), 'WrongDocumentError'); + chai.assert.throws(() => range.comparePoint(doctype, 0), 'InvalidNodeTypeError'); + chai.assert.throws(() => range.comparePoint(element, 3), 'IndexSizeError'); + }); + + it('can compare a given node to the range', () => { + range.setStart(text, 0); + range.setEnd(element, 1); + const child = element.appendChild(document.createElement('child')); + + chai.assert(range.intersectsNode(text), 'intersects text'); + chai.assert(range.intersectsNode(element), 'intersects element'); + chai.assert(range.intersectsNode(document), 'intersects the document'); + chai.assert(!range.intersectsNode(child), 'does not intersect child'); + + const range2 = new slimdom.Range(); + chai.assert(!range2.intersectsNode(document), "different roots don't intersect"); }); }); diff --git a/test/Text.tests.ts b/test/Text.tests.ts index 9fe7a51..dcba3d2 100644 --- a/test/Text.tests.ts +++ b/test/Text.tests.ts @@ -3,37 +3,81 @@ import * as slimdom from '../src/index'; describe('Text', () => { let document: slimdom.Document; - let text: slimdom.Text; beforeEach(() => { document = new slimdom.Document(); - text = document.createTextNode('text'); }); - it('has nodeType 3', () => chai.assert.equal(text.nodeType, 3)); + it('can be created using Document#createTextNode()', () => { + const text = document.createTextNode('some data'); + chai.assert.equal(text.nodeType, 3); + chai.assert.equal(text.nodeName, '#text'); + chai.assert.equal(text.nodeValue, 'some data'); + chai.assert.equal(text.data, 'some data'); - it('has data', () => chai.assert.equal(text.data, 'text')); + chai.assert.equal(text.ownerDocument, document); + }); + + it('can be created using its constructor (with data)', () => { + const text = new slimdom.Text('some data'); + chai.assert.equal(text.nodeType, 3); + chai.assert.equal(text.nodeName, '#text'); + chai.assert.equal(text.nodeValue, 'some data'); + chai.assert.equal(text.data, 'some data'); - it('has a nodeValue', () => chai.assert.equal(text.nodeValue, 'text')); + chai.assert.equal(text.ownerDocument, slimdom.document); + }); - it('has a length', () => chai.assert.equal(text.length, 4)); + it('can be created using its constructor (without arguments)', () => { + const text = new slimdom.Text(); + chai.assert.equal(text.nodeType, 3); + chai.assert.equal(text.nodeName, '#text'); + chai.assert.equal(text.nodeValue, ''); + chai.assert.equal(text.data, ''); - it('can set data property', () => { - var newValue = 'a new text value'; - text.data = newValue; - chai.assert.equal(text.data, newValue); - chai.assert.equal(text.nodeValue, newValue); - chai.assert.equal(text.length, newValue.length); + chai.assert.equal(text.ownerDocument, slimdom.document); + }); + + it('can set its data using nodeValue', () => { + const text = document.createTextNode('some data'); + text.nodeValue = 'other data'; + chai.assert.equal(text.nodeValue, 'other data'); + chai.assert.equal(text.data, 'other data'); + + text.nodeValue = null; + chai.assert.equal(text.nodeValue, ''); + chai.assert.equal(text.data, ''); + }); + + it('can set its data using data', () => { + const text = document.createTextNode('some data'); + text.data = 'other data'; + chai.assert.equal(text.nodeValue, 'other data'); + chai.assert.equal(text.data, 'other data'); }); it('can be cloned', () => { - var clone = text.cloneNode(true) as slimdom.Text; - chai.assert.equal(clone.nodeType, 3); - chai.assert.equal(clone.nodeValue, 'text'); - chai.assert.equal(clone.data, 'text'); - chai.assert.notEqual(clone, text); + const text = document.createTextNode('some data'); + var copy = text.cloneNode() as slimdom.Text; + chai.assert.equal(copy.nodeType, 3); + chai.assert.equal(copy.nodeName, '#text'); + chai.assert.equal(copy.nodeValue, 'some data'); + chai.assert.equal(copy.data, 'some data'); + chai.assert.notEqual(copy, text); + }); + + it('can lookup a prefix or namespace on its parent element', () => { + const text = document.createTextNode('some data'); + chai.assert.equal(text.lookupNamespaceURI('prf'), null); + chai.assert.equal(text.lookupPrefix('http://www.example.com/ns'), null); + + const element = document.createElementNS('http://www.example.com/ns', 'prf:test'); + element.appendChild(text); + chai.assert.equal(text.lookupNamespaceURI('prf'), 'http://www.example.com/ns'); + chai.assert.equal(text.lookupPrefix('http://www.example.com/ns'), 'prf'); }); it('can substring its data', () => { + const text = document.createTextNode('text'); chai.assert.equal(text.substringData(0, 2), 'te'); chai.assert.equal(text.substringData(2, 2), 'xt'); chai.assert.equal(text.substringData(1, 2), 'ex'); @@ -44,6 +88,7 @@ describe('Text', () => { }); it('can appendData', () => { + const text = document.createTextNode('text'); text.appendData('123'); chai.assert.equal(text.data, 'text123'); chai.assert.equal(text.nodeValue, text.data); @@ -51,6 +96,7 @@ describe('Text', () => { }); it('can insertData', () => { + const text = document.createTextNode('text'); text.insertData(2, '123'); chai.assert.equal(text.data, 'te123xt'); chai.assert.equal(text.nodeValue, text.data); @@ -71,6 +117,7 @@ describe('Text', () => { }); it('can deleteData', () => { + const text = document.createTextNode('text'); text.deleteData(0, 0); chai.assert.equal(text.data, 'text'); chai.assert.equal(text.nodeValue, text.data); @@ -101,6 +148,7 @@ describe('Text', () => { }); it('can replaceData', () => { + const text = document.createTextNode('text'); text.replaceData(0, 0, ''); chai.assert.equal(text.data, 'text'); chai.assert.equal(text.nodeValue, text.data); @@ -127,6 +175,7 @@ describe('Text', () => { describe('splitting', () => { it('can be split', () => { + const text = document.createTextNode('text'); const otherHalf = text.splitText(2); chai.assert.equal(text.data, 'te'); chai.assert.equal(text.nodeValue, text.data); @@ -138,15 +187,16 @@ describe('Text', () => { }); describe('under a parent', () => { + let text: slimdom.Text; let element: slimdom.Element; - let otherHalf: slimdom.Text; beforeEach(() => { element = document.createElement('parent'); + text = document.createTextNode('text'); element.appendChild(text); - otherHalf = text.splitText(2); }); it('is split correctly', () => { + const otherHalf = text.splitText(2); chai.assert.equal(text.data, 'te'); chai.assert.equal(text.nodeValue, text.data); chai.assert.equal(otherHalf.data, 'xt'); @@ -154,14 +204,34 @@ describe('Text', () => { }); it('both halves are children of the parent', () => { + const otherHalf = text.splitText(2); chai.assert.equal(text.parentNode, element); chai.assert.equal(otherHalf.parentNode, element); }); it('both halves are siblings', () => { + const otherHalf = text.splitText(2); chai.assert.equal(text.nextSibling, otherHalf); chai.assert.equal(otherHalf.previousSibling, text); }); + + it('updates ranges after the split point', () => { + const range1 = new slimdom.Range(); + const range2 = new slimdom.Range(); + range1.setStart(text, 3); + range1.setEnd(text, 4); + range2.setStart(element, 1); + range2.collapse(true); + const otherHalf = text.splitText(2); + chai.assert.equal(range1.startContainer, otherHalf); + chai.assert.equal(range1.startOffset, 1); + chai.assert.equal(range1.endContainer, otherHalf); + chai.assert.equal(range1.endOffset, 2); + chai.assert.equal(range2.startContainer, element); + chai.assert.equal(range2.startOffset, 2); + chai.assert.equal(range2.endContainer, element); + chai.assert.equal(range2.endOffset, 2); + }); }); }); }); diff --git a/test/XMLDocument.tests.ts b/test/XMLDocument.tests.ts new file mode 100644 index 0000000..ca8d041 --- /dev/null +++ b/test/XMLDocument.tests.ts @@ -0,0 +1,17 @@ +import * as chai from 'chai'; +import * as slimdom from '../src/index'; + +describe('XMLDocument', () => { + it('can be created using DOMImplementation#createDocument()', () => { + const doc = slimdom.document.implementation.createDocument(null, ''); + chai.assert.instanceOf(doc, slimdom.XMLDocument); + chai.assert.equal(doc.nodeType, 9); + }); + + it('can be cloned', () => { + const doc = slimdom.document.implementation.createDocument(null, ''); + const copy = doc.cloneNode(); + chai.assert.instanceOf(copy, slimdom.XMLDocument); + chai.assert.equal(copy.nodeType, 9); + }); +}); diff --git a/test/mutationAlgorithms.tests.ts b/test/mutationAlgorithms.tests.ts new file mode 100644 index 0000000..9a58765 --- /dev/null +++ b/test/mutationAlgorithms.tests.ts @@ -0,0 +1,303 @@ +import * as chai from 'chai'; +import * as slimdom from '../src/index'; + +describe('DOM mutations', () => { + let document: slimdom.Document; + beforeEach(() => { + document = new slimdom.Document(); + }); + + describe('Node#appendChild / Node#insertBefore', () => { + it('throws if inserting a node below one that can not have children', () => { + const text = document.createTextNode('test'); + const comment = document.createComment('test'); + const pi = document.createProcessingInstruction('test', 'test'); + chai.assert.throws(() => text.appendChild(comment), 'HierarchyRequestError'); + chai.assert.throws(() => comment.appendChild(pi), 'HierarchyRequestError'); + chai.assert.throws(() => pi.appendChild(text), 'HierarchyRequestError'); + }); + + it('throws if inserting a node below one of its descendants', () => { + const descendant = document + .appendChild(document.createElement('ancestor')) + .appendChild(document.createElement('middle')) + .appendChild(document.createElement('descendant')); + chai.assert.throws(() => descendant.appendChild(document.documentElement!), 'HierarchyRequestError'); + }); + + it('throws if the reference node is not a child of the parent', () => { + const parent = document.createElement('parent'); + const notChild = document.createElement('notChild'); + const text = document.createTextNode('test'); + chai.assert.throws(() => parent.insertBefore(text, notChild), 'NotFoundError'); + }); + + it('throws if inserting a node that can not be a child', () => { + const attr = document.createAttribute('test'); + const doc = new slimdom.Document(); + const element = document.createElement('test'); + chai.assert.throws(() => element.appendChild(attr), 'HierarchyRequestError'); + chai.assert.throws(() => element.appendChild(doc), 'HierarchyRequestError'); + }); + + it('throws if inserting a text node directly under the document', () => { + const text = document.createTextNode('test'); + chai.assert.throws(() => document.appendChild(text), 'HierarchyRequestError'); + }); + + it('throws if inserting a doctype under something other than a document', () => { + const doctype = document.implementation.createDocumentType('html', '', ''); + const fragment = document.createDocumentFragment(); + chai.assert.throws(() => fragment.appendChild(doctype), 'HierarchyRequestError'); + }); + + it('throws if inserting a fragment would add a text node under a document', () => { + const fragment = document.createDocumentFragment(); + fragment.appendChild(document.createTextNode('test')); + chai.assert.throws(() => document.appendChild(fragment), 'HierarchyRequestError'); + }); + + it('throws if inserting a fragment would add multiple document elements', () => { + const fragment = document.createDocumentFragment(); + fragment.appendChild(document.createElement('child1')); + fragment.appendChild(document.createElement('child2')); + chai.assert.throws(() => document.appendChild(fragment), 'HierarchyRequestError'); + }); + + it('throws if inserting a fragment would add another document element', () => { + const fragment = document.createDocumentFragment(); + document.appendChild(document.createElement('child1')); + fragment.appendChild(document.createElement('child2')); + chai.assert.throws(() => document.appendChild(fragment), 'HierarchyRequestError'); + }); + + it('throws if inserting a fragment would add a document element before the doctype', () => { + const fragment = document.createDocumentFragment(); + fragment.appendChild(document.createElement('child1')); + const doctype = document.appendChild(document.implementation.createDocumentType('html', '', '')); + chai.assert.throws(() => document.insertBefore(fragment, doctype), 'HierarchyRequestError'); + const comment = document.insertBefore(document.createComment('test'), doctype); + chai.assert.throws(() => document.insertBefore(fragment, comment), 'HierarchyRequestError'); + }); + + it('allows inserting a document element using a fragment', () => { + const fragment = document.createDocumentFragment(); + const child = fragment.appendChild(document.createElement('child1')) as slimdom.Element; + const doctype = document.appendChild(document.implementation.createDocumentType('html', '', '')); + document.appendChild(fragment); + chai.assert.equal(document.documentElement, child); + }); + + it('throws if inserting a document element before the doctype', () => { + const element = document.createElement('test'); + const doctype = document.appendChild(document.implementation.createDocumentType('html', '', '')); + chai.assert.throws(() => document.insertBefore(element, doctype), 'HierarchyRequestError'); + const comment = document.insertBefore(document.createComment('test'), doctype); + chai.assert.throws(() => document.insertBefore(element, comment), 'HierarchyRequestError'); + }); + + it('throws if inserting a second doctype', () => { + const htmlDocument = document.implementation.createHTMLDocument('test'); + const doctype = document.implementation.createDocumentType('test', '', ''); + chai.assert.throws(() => htmlDocument.appendChild(doctype), 'HierarchyRequestError'); + }); + + it('throws if inserting a doctype after the document element', () => { + const element = document.appendChild(document.createElement('test')); + const doctype = document.implementation.createDocumentType('html', '', ''); + chai.assert.throws(() => document.appendChild(doctype), 'HierarchyRequestError'); + }); + + it('correctly handles inserting a node before itself', () => { + const parent = document.appendChild(document.createElement('parent')) as slimdom.Element; + const element = parent.appendChild(document.createElement('child')) as slimdom.Element; + parent.insertBefore(element, element); + chai.assert.equal(parent.firstElementChild, element); + chai.assert.equal(element.nextElementSibling, null); + chai.assert.equal(element.previousElementSibling, null); + }); + + it('throws if inserting the document element before itself', () => { + const element = document.appendChild(document.createElement('test')) as slimdom.Element; + chai.assert.throws(() => document.insertBefore(element, element), 'HierarchyRequestError'); + }); + + describe('effect on ranges', () => { + let range: slimdom.Range; + beforeEach(() => { + range = document.createRange(); + }); + + it('updates ranges after the insertion point', () => { + const parent = document.createElement('parent'); + const child = parent.appendChild(document.createComment('test')); + range.setStartAfter(child); + range.collapse(true); + chai.assert.equal(range.startContainer, parent); + chai.assert.equal(range.startOffset, 1); + parent.insertBefore(document.createTextNode('test'), child); + chai.assert.equal(range.startContainer, parent); + chai.assert.equal(range.startOffset, 2); + }); + }); + }); + + describe('replaceChild', () => { + it('throws if replacing under a non-parent node', () => { + const doctype = document.implementation.createDocumentType('html', '', ''); + chai.assert.throws(() => doctype.replaceChild(doctype, doctype), 'HierarchyRequestError'); + }); + + it('throws if inserting a node below one of its descendants', () => { + const descendant = document + .appendChild(document.createElement('ancestor')) + .appendChild(document.createElement('middle')) + .appendChild(document.createElement('descendant')); + const pi = descendant.appendChild(document.createProcessingInstruction('target', 'test')); + chai.assert.throws(() => descendant.replaceChild(document.documentElement!, pi), 'HierarchyRequestError'); + }); + + it('throws if replacing a node that is not a child of the parent', () => { + const parent = document.createElement('parent'); + const notChild = document.createElement('notChild'); + const text = document.createTextNode('test'); + chai.assert.throws(() => parent.replaceChild(text, notChild), 'NotFoundError'); + }); + + it('throws if inserting a node that can not be a child', () => { + const parent = document.createElement('parent'); + const oldChild = parent.appendChild(document.createComment('')); + const attr = document.createAttribute('test'); + const doc = new slimdom.Document(); + chai.assert.throws(() => parent.replaceChild(attr, oldChild), 'HierarchyRequestError'); + chai.assert.throws(() => parent.replaceChild(doc, oldChild), 'HierarchyRequestError'); + }); + + it('throws if inserting a text node directly under the document', () => { + const text = document.createTextNode('test'); + const oldChild = document.appendChild(document.createComment('')); + chai.assert.throws(() => document.replaceChild(text, oldChild), 'HierarchyRequestError'); + }); + + it('throws if inserting a doctype under something other than a document', () => { + const doctype = document.implementation.createDocumentType('html', '', ''); + const fragment = document.createDocumentFragment(); + const oldChild = fragment.appendChild(document.createComment('')); + chai.assert.throws(() => fragment.replaceChild(doctype, oldChild), 'HierarchyRequestError'); + }); + + it('throws if inserting a fragment would add a text node under a document', () => { + const oldChild = document.appendChild(document.createComment('')); + const fragment = document.createDocumentFragment(); + fragment.appendChild(document.createTextNode('test')); + chai.assert.throws(() => document.replaceChild(fragment, oldChild), 'HierarchyRequestError'); + }); + + it('throws if inserting a fragment would add multiple document elements', () => { + const oldChild = document.appendChild(document.createComment('')); + const fragment = document.createDocumentFragment(); + fragment.appendChild(document.createElement('child1')); + fragment.appendChild(document.createElement('child2')); + chai.assert.throws(() => document.replaceChild(fragment, oldChild), 'HierarchyRequestError'); + }); + + it('throws if inserting a fragment would add another document element', () => { + const oldChild = document.appendChild(document.createComment('')); + const fragment = document.createDocumentFragment(); + document.appendChild(document.createElement('child1')); + fragment.appendChild(document.createElement('child2')); + chai.assert.throws(() => document.replaceChild(fragment, oldChild), 'HierarchyRequestError'); + }); + + it('throws if inserting a fragment would add a document element before the doctype', () => { + const oldChild = document.appendChild(document.createComment('')); + const fragment = document.createDocumentFragment(); + fragment.appendChild(document.createElement('child1')); + const doctype = document.appendChild(document.implementation.createDocumentType('html', '', '')); + chai.assert.throws(() => document.replaceChild(fragment, oldChild), 'HierarchyRequestError'); + }); + + it('allows inserting a document element using a fragment', () => { + const fragment = document.createDocumentFragment(); + const child = fragment.appendChild(document.createElement('child1')) as slimdom.Element; + const doctype = document.appendChild(document.implementation.createDocumentType('html', '', '')); + const oldChild = document.appendChild(document.createComment('')); + document.replaceChild(fragment, oldChild); + chai.assert.equal(document.documentElement, child); + }); + + it('throws if inserting a document element before the doctype', () => { + const element = document.createElement('test'); + const oldChild = document.appendChild(document.createComment('')); + const doctype = document.appendChild(document.implementation.createDocumentType('html', '', '')); + chai.assert.throws(() => document.replaceChild(element, oldChild), 'HierarchyRequestError'); + }); + + it('throws if inserting a second doctype', () => { + const htmlDocument = document.implementation.createHTMLDocument('test'); + const doctype = document.implementation.createDocumentType('test', '', ''); + const oldChild = htmlDocument.appendChild(document.createComment('')); + chai.assert.throws(() => htmlDocument.replaceChild(doctype, oldChild), 'HierarchyRequestError'); + }); + + it('throws if inserting a doctype after the document element', () => { + const element = document.appendChild(document.createElement('test')); + const oldChild = document.appendChild(document.createComment('')); + const doctype = document.implementation.createDocumentType('html', '', ''); + chai.assert.throws(() => document.replaceChild(doctype, oldChild), 'HierarchyRequestError'); + }); + + it('allows insert a doctype', () => { + const oldChild = document.appendChild(document.createComment('')); + const doctype = document.implementation.createDocumentType('html', '', ''); + document.replaceChild(doctype, oldChild); + chai.assert.equal(document.doctype, doctype); + }); + + it('correctly handles replacing a node with itself', () => { + const parent = document.appendChild(document.createElement('parent')) as slimdom.Element; + const element = parent.appendChild(document.createElement('child')) as slimdom.Element; + parent.replaceChild(element, element); + chai.assert.equal(parent.firstElementChild, element); + chai.assert.equal(element.nextElementSibling, null); + chai.assert.equal(element.previousElementSibling, null); + }); + + it('correctly handles replacing a node with its next sibling', () => { + const parent = document.appendChild(document.createElement('parent')) as slimdom.Element; + const element1 = parent.appendChild(document.createElement('child')) as slimdom.Element; + const element2 = parent.appendChild(document.createElement('child')) as slimdom.Element; + parent.replaceChild(element2, element1); + chai.assert.equal(parent.firstElementChild, element2); + chai.assert.equal(element2.nextElementSibling, null); + chai.assert.equal(element2.previousElementSibling, null); + }); + }); + + describe('removeChild', () => { + it('throws if the child is not a child of the parent', () => { + const element = document.createElement('element'); + chai.assert.throws(() => document.removeChild(element), 'NotFoundError'); + }); + + describe('effect on ranges', () => { + let range: slimdom.Range; + beforeEach(() => { + range = document.createRange(); + }); + + it('updates ranges after the deletion point', () => { + const parent = document.createElement('parent'); + const child = parent.appendChild(document.createComment('test')); + parent.appendChild(document.createTextNode('test')); + range.setStartAfter(parent.lastChild!); + range.collapse(true); + chai.assert.equal(range.startContainer, parent); + chai.assert.equal(range.startOffset, 2); + parent.removeChild(child); + chai.assert.equal(range.startContainer, parent); + chai.assert.equal(range.startOffset, 1); + }); + }); + }); +}); From 6f4ce2114041858e166d792bfaae189d66dde68b Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 22:54:47 +0200 Subject: [PATCH 27/34] Make type error messages work with minified code. --- src/DOMImplementation.ts | 2 +- src/Document.ts | 4 ++-- src/Element.ts | 6 +++--- src/Node.ts | 14 ++++++++------ src/Range.ts | 24 ++++++++++++------------ src/util/errorHelpers.ts | 4 ++-- src/util/typeHelpers.ts | 8 ++++---- 7 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/DOMImplementation.ts b/src/DOMImplementation.ts index 6836c64..40b9935 100644 --- a/src/DOMImplementation.ts +++ b/src/DOMImplementation.ts @@ -62,7 +62,7 @@ export default class DOMImplementation { namespace = asNullableString(namespace); // [TreatNullAs=EmptyString] for qualifiedName qualifiedName = treatNullAsEmptyString(qualifiedName); - doctype = asNullableObject(doctype, DocumentType); + doctype = asNullableObject(doctype, DocumentType, 'DocumentType'); // 1. Let document be a new XMLDocument. const context = getContext(this._document); diff --git a/src/Document.ts b/src/Document.ts index bab5a3a..08531c2 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -279,7 +279,7 @@ export default class Document extends Node implements NonElementParentNode, Pare */ public importNode(node: Node, deep: boolean = false): Node { expectArity(arguments, 1); - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); // 1. If node is a document or shadow root, then throw a NotSupportedError. if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { @@ -298,7 +298,7 @@ export default class Document extends Node implements NonElementParentNode, Pare */ public adoptNode(node: Node): Node { expectArity(arguments, 1); - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); // 1. If node is a document, then throw a NotSupportedError. if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { diff --git a/src/Element.ts b/src/Element.ts index 02d2a17..d0cbb9e 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -369,7 +369,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType */ public setAttributeNode(attr: Attr): Attr | null { expectArity(arguments, 1); - attr = asObject(attr, Attr); + attr = asObject(attr, Attr, 'Attr'); return setAttribute(attr, this); } @@ -383,7 +383,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType */ public setAttributeNodeNS(attr: Attr): Attr | null { expectArity(arguments, 1); - attr = asObject(attr, Attr); + attr = asObject(attr, Attr, 'Attr'); return setAttribute(attr, this); } @@ -397,7 +397,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType */ public removeAttributeNode(attr: Attr): Attr { expectArity(arguments, 1); - attr = asObject(attr, Attr); + attr = asObject(attr, Attr, 'Attr'); // 1. If context object’s attribute list does not contain attr, then throw a NotFoundError. if (this.attributes.indexOf(attr) < 0) { diff --git a/src/Node.ts b/src/Node.ts index f1c60ff..4c76df7 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -221,6 +221,8 @@ export default abstract class Node { * @return Whether childNode is an inclusive descendant of the current node */ public contains(other: Node | null): boolean { + other = asNullableObject(other, Node, 'Node'); + while (other && other != this) { other = other.parentNode; } @@ -279,8 +281,8 @@ export default abstract class Node { */ public insertBefore(node: Node, child: Node | null): Node { expectArity(arguments, 2); - node = asObject(node, Node); - child = asNullableObject(child, Node); + node = asObject(node, Node, 'Node'); + child = asNullableObject(child, Node, 'Node'); return preInsertNode(node, this, child); } @@ -296,7 +298,7 @@ export default abstract class Node { */ public appendChild(node: Node): Node { expectArity(arguments, 1); - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); return appendNode(node, this); } @@ -311,8 +313,8 @@ export default abstract class Node { */ public replaceChild(node: Node, child: Node): Node { expectArity(arguments, 2); - node = asObject(node, Node); - child = asObject(child, Node); + node = asObject(node, Node, 'Node'); + child = asObject(child, Node, 'Node'); return replaceChildWithNode(child, node, this); } @@ -326,7 +328,7 @@ export default abstract class Node { */ public removeChild(child: Node): Node { expectArity(arguments, 1); - child = asObject(child, Node); + child = asObject(child, Node, 'Node'); return preRemoveChild(child, this); } diff --git a/src/Range.ts b/src/Range.ts index 53e6c65..0e2687e 100644 --- a/src/Range.ts +++ b/src/Range.ts @@ -74,7 +74,7 @@ export default class Range { * @param offset The new start offset */ setStart(node: Node, offset: number): void { - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); offset = asUnsignedLong(offset); // 1. If node is a doctype, then throw an InvalidNodeTypeError. @@ -117,7 +117,7 @@ export default class Range { * @param offset The new end offset */ setEnd(node: Node, offset: number): void { - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); offset = asUnsignedLong(offset); // 1. If node is a doctype, then throw an InvalidNodeTypeError. @@ -159,7 +159,7 @@ export default class Range { * @param node The node to set the range's start before */ setStartBefore(node: Node): void { - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -179,7 +179,7 @@ export default class Range { * @param node The node to set the range's start before */ setStartAfter(node: Node): void { - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -199,7 +199,7 @@ export default class Range { * @param node The node to set the range's end before */ setEndBefore(node: Node): void { - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -219,7 +219,7 @@ export default class Range { * @param node The node to set the range's end before */ setEndAfter(node: Node): void { - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -249,7 +249,7 @@ export default class Range { } selectNode(node: Node): void { - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); // 1. Let parent be node’s parent. let parent = node.parentNode; @@ -272,7 +272,7 @@ export default class Range { } selectNodeContents(node: Node): void { - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); // 1. If node is a doctype, throw an InvalidNodeTypeError. if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { @@ -297,7 +297,7 @@ export default class Range { static END_TO_START = 3; compareBoundaryPoints(how: number, sourceRange: Range): number { - sourceRange = asObject(sourceRange, Range); + sourceRange = asObject(sourceRange, Range, 'Range'); // 1. If how is not one of START_TO_START, START_TO_END, END_TO_END, and END_TO_START, then throw a // NotSupportedError. @@ -412,7 +412,7 @@ export default class Range { * @return Whether the point is in the range */ isPointInRange(node: Node, offset: number): boolean { - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); offset = asUnsignedLong(offset); // 1. If node’s root is different from the context object’s root, return false. @@ -451,7 +451,7 @@ export default class Range { * @return -1, 0 or 1 depending on whether the point is before, inside or after the range, respectively */ comparePoint(node: Node, offset: number): number { - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); offset = asUnsignedLong(offset); // 1. If node’s root is different from the context object’s root, then throw a WrongDocumentError. @@ -491,7 +491,7 @@ export default class Range { * @return Whether the range intersects node */ intersectsNode(node: Node): boolean { - node = asObject(node, Node); + node = asObject(node, Node, 'Node'); // 1. If node’s root is different from the context object’s root, return false. if (getRootOfNode(node) !== getRootOfRange(this)) { diff --git a/src/util/errorHelpers.ts b/src/util/errorHelpers.ts index 290706a..dc16839 100644 --- a/src/util/errorHelpers.ts +++ b/src/util/errorHelpers.ts @@ -5,9 +5,9 @@ export function expectArity(args: IArguments, minArity: number): void { } } -export function expectObject(value: T, Constructor: any): void { +export function expectObject(value: T, Constructor: any, typeName: string): void { if (!(value instanceof Constructor)) { - throw new TypeError(`Value should be an instance of ${Constructor.name}`); + throw new TypeError(`Value should be an instance of ${typeName}`); } } diff --git a/src/util/typeHelpers.ts b/src/util/typeHelpers.ts index 603f26f..b0de952 100644 --- a/src/util/typeHelpers.ts +++ b/src/util/typeHelpers.ts @@ -14,18 +14,18 @@ export function treatNullAsEmptyString(value: string | null): string { return String(value); } -export function asObject(value: T, Constructor: any): T { - expectObject(value, Constructor); +export function asObject(value: T, Constructor: any, typeName: string): T { + expectObject(value, Constructor, typeName); return value; } -export function asNullableObject(value: T | null | undefined, Constructor: any): T | null { +export function asNullableObject(value: T | null | undefined, Constructor: any, typeName: string): T | null { if (value === undefined || value === null) { return null; } - return asObject(value, Constructor); + return asObject(value, Constructor, typeName); } export function asNullableString(value: string | null | undefined): string | null { From f70fc92f9e58a217ce2452513edafdc9b808340b Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 22:55:32 +0200 Subject: [PATCH 28/34] Add type checks for remaining public methods. --- src/Attr.ts | 12 +++++++----- src/CharacterData.ts | 11 ++++++----- src/DOMImplementation.ts | 5 +++++ src/Document.ts | 4 ++++ src/DocumentFragment.ts | 5 +++++ src/DocumentType.ts | 5 +++++ src/Element.ts | 1 + src/Node.ts | 2 ++ src/Range.ts | 13 +++++++++++++ src/Text.ts | 6 +++++- src/mutation-observer/MutationObserver.ts | 8 ++++++++ 11 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/Attr.ts b/src/Attr.ts index 8068f5e..28820a5 100644 --- a/src/Attr.ts +++ b/src/Attr.ts @@ -3,8 +3,9 @@ import Element from './Element'; import Node from './Node'; import { getContext } from './context/Context'; import { changeAttribute } from './util/attrMutations'; +import { expectArity } from './util/errorHelpers'; import { NodeType } from './util/NodeType'; -import { asNullableString } from './util/typeHelpers'; +import { treatNullAsEmptyString } from './util/typeHelpers'; /** * 3.9.2. Interface Attr @@ -26,16 +27,15 @@ export default class Attr extends Node { } public set nodeValue(newValue: string | null) { - // if the new value is null, act as if it was the empty string instead - if (newValue === null) { - newValue = ''; - } + newValue = treatNullAsEmptyString(newValue); // Set an existing attribute value with context object and new value. setExistingAttributeValue(this, newValue); } public lookupPrefix(namespace: string | null): string | null { + expectArity(arguments, 1); + // 1. If namespace is null or the empty string, then return null. // (not necessary due to recursion) @@ -50,6 +50,8 @@ export default class Attr extends Node { } public lookupNamespaceURI(prefix: string | null): string | null { + expectArity(arguments, 1); + // 1. If prefix is the empty string, then set it to null. // (not necessary due to recursion) diff --git a/src/CharacterData.ts b/src/CharacterData.ts index 195cabd..82e80c9 100644 --- a/src/CharacterData.ts +++ b/src/CharacterData.ts @@ -18,16 +18,15 @@ export default abstract class CharacterData extends Node implements NonDocumentT } public set nodeValue(newValue: string | null) { - // if the new value is null, act as if it was the empty string instead - if (newValue === null) { - newValue = ''; - } + newValue = treatNullAsEmptyString(newValue); // Set an existing attribute value with context object and new value. replaceData(this, 0, this.length, newValue); } public lookupPrefix(namespace: string | null): string | null { + expectArity(arguments, 1); + // 1. If namespace is null or the empty string, then return null. // (not necessary due to recursion) @@ -43,6 +42,8 @@ export default abstract class CharacterData extends Node implements NonDocumentT } public lookupNamespaceURI(prefix: string | null): string | null { + expectArity(arguments, 1); + // 1. If prefix is the empty string, then set it to null. // (not necessary due to recursion) @@ -99,7 +100,7 @@ export default abstract class CharacterData extends Node implements NonDocumentT */ protected constructor(data: string) { super(); - this._data = data; + this._data = String(data); } /** diff --git a/src/DOMImplementation.ts b/src/DOMImplementation.ts index 40b9935..2dee13e 100644 --- a/src/DOMImplementation.ts +++ b/src/DOMImplementation.ts @@ -32,6 +32,11 @@ export default class DOMImplementation { * @return The new doctype node */ createDocumentType(qualifiedName: string, publicId: string, systemId: string): DocumentType { + expectArity(arguments, 3); + qualifiedName = String(qualifiedName); + publicId = String(publicId); + systemId = String(systemId); + // 1. Validate qualifiedName. validateQualifiedName(qualifiedName); diff --git a/src/Document.ts b/src/Document.ts index 08531c2..9201b9d 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -42,6 +42,8 @@ export default class Document extends Node implements NonElementParentNode, Pare } public lookupPrefix(namespace: string | null): string | null { + expectArity(arguments, 1); + // 1. If namespace is null or the empty string, then return null. // (not necessary due to recursion) @@ -56,6 +58,8 @@ export default class Document extends Node implements NonElementParentNode, Pare } public lookupNamespaceURI(prefix: string | null): string | null { + expectArity(arguments, 1); + // 1. If prefix is the empty string, then set it to null. // (not necessary due to recursion) diff --git a/src/DocumentFragment.ts b/src/DocumentFragment.ts index 6a1dfed..f8b383f 100644 --- a/src/DocumentFragment.ts +++ b/src/DocumentFragment.ts @@ -3,6 +3,7 @@ import Document from './Document'; import Element from './Element'; import Node from './Node'; import { getContext } from './context/Context'; +import { expectArity } from './util/errorHelpers'; import { NodeType } from './util/NodeType'; export default class DocumentFragment extends Node implements NonElementParentNode, ParentNode { @@ -25,6 +26,8 @@ export default class DocumentFragment extends Node implements NonElementParentNo } public lookupPrefix(namespace: string | null): string | null { + expectArity(arguments, 1); + // 1. If namespace is null or the empty string, then return null. // (not necessary due to return value) @@ -34,6 +37,8 @@ export default class DocumentFragment extends Node implements NonElementParentNo } public lookupNamespaceURI(prefix: string | null): string | null { + expectArity(arguments, 1); + // 1. If prefix is the empty string, then set it to null. // (not necessary due to return value) diff --git a/src/DocumentType.ts b/src/DocumentType.ts index f0e0ff7..7ad624d 100644 --- a/src/DocumentType.ts +++ b/src/DocumentType.ts @@ -2,6 +2,7 @@ import { ChildNode } from './mixins'; import Document from './Document'; import Node from './Node'; import { getContext } from './context/Context'; +import { expectArity } from './util/errorHelpers'; import { NodeType } from './util/NodeType'; export default class DocumentType extends Node implements ChildNode { @@ -24,6 +25,8 @@ export default class DocumentType extends Node implements ChildNode { } public lookupPrefix(namespace: string | null): string | null { + expectArity(arguments, 1); + // 1. If namespace is null or the empty string, then return null. // (not necessary due to return value) @@ -33,6 +36,8 @@ export default class DocumentType extends Node implements ChildNode { } public lookupNamespaceURI(prefix: string | null): string | null { + expectArity(arguments, 1); + // 1. If prefix is the empty string, then set it to null. // (not necessary due to return value) diff --git a/src/Element.ts b/src/Element.ts index d0cbb9e..16eb467 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -196,6 +196,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType public getAttributeNS(namespace: string | null, localName: string): string | null { expectArity(arguments, 2); namespace = asNullableString(namespace); + localName = String(localName); // 1. Let attr be the result of getting an attribute given namespace, localName, and the context object. const attr = getAttributeByNamespaceAndLocalName(namespace, localName, this); diff --git a/src/Node.ts b/src/Node.ts index 4c76df7..b032cfe 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -221,6 +221,7 @@ export default abstract class Node { * @return Whether childNode is an inclusive descendant of the current node */ public contains(other: Node | null): boolean { + expectArity(arguments, 1); other = asNullableObject(other, Node, 'Node'); while (other && other != this) { @@ -255,6 +256,7 @@ export default abstract class Node { * @return Whether namespace is the default namespace */ public isDefaultNamespace(namespace: string | null): boolean { + expectArity(arguments, 1); namespace = asNullableString(namespace); // 1. If namespace is the empty string, then set it to null. diff --git a/src/Range.ts b/src/Range.ts index 0e2687e..44aac5c 100644 --- a/src/Range.ts +++ b/src/Range.ts @@ -2,6 +2,7 @@ import Document from './Document'; import Node from './Node'; import { getContext } from './context/Context'; import { + expectArity, throwIndexSizeError, throwInvalidNodeTypeError, throwNotSupportedError, @@ -74,6 +75,7 @@ export default class Range { * @param offset The new start offset */ setStart(node: Node, offset: number): void { + expectArity(arguments, 2); node = asObject(node, Node, 'Node'); offset = asUnsignedLong(offset); @@ -117,6 +119,7 @@ export default class Range { * @param offset The new end offset */ setEnd(node: Node, offset: number): void { + expectArity(arguments, 2); node = asObject(node, Node, 'Node'); offset = asUnsignedLong(offset); @@ -159,6 +162,7 @@ export default class Range { * @param node The node to set the range's start before */ setStartBefore(node: Node): void { + expectArity(arguments, 1); node = asObject(node, Node, 'Node'); // 1. Let parent be node’s parent. @@ -179,6 +183,7 @@ export default class Range { * @param node The node to set the range's start before */ setStartAfter(node: Node): void { + expectArity(arguments, 1); node = asObject(node, Node, 'Node'); // 1. Let parent be node’s parent. @@ -199,6 +204,7 @@ export default class Range { * @param node The node to set the range's end before */ setEndBefore(node: Node): void { + expectArity(arguments, 1); node = asObject(node, Node, 'Node'); // 1. Let parent be node’s parent. @@ -219,6 +225,7 @@ export default class Range { * @param node The node to set the range's end before */ setEndAfter(node: Node): void { + expectArity(arguments, 1); node = asObject(node, Node, 'Node'); // 1. Let parent be node’s parent. @@ -249,6 +256,7 @@ export default class Range { } selectNode(node: Node): void { + expectArity(arguments, 1); node = asObject(node, Node, 'Node'); // 1. Let parent be node’s parent. @@ -272,6 +280,7 @@ export default class Range { } selectNodeContents(node: Node): void { + expectArity(arguments, 1); node = asObject(node, Node, 'Node'); // 1. If node is a doctype, throw an InvalidNodeTypeError. @@ -297,6 +306,7 @@ export default class Range { static END_TO_START = 3; compareBoundaryPoints(how: number, sourceRange: Range): number { + expectArity(arguments, 2); sourceRange = asObject(sourceRange, Range, 'Range'); // 1. If how is not one of START_TO_START, START_TO_END, END_TO_END, and END_TO_START, then throw a @@ -412,6 +422,7 @@ export default class Range { * @return Whether the point is in the range */ isPointInRange(node: Node, offset: number): boolean { + expectArity(arguments, 2); node = asObject(node, Node, 'Node'); offset = asUnsignedLong(offset); @@ -451,6 +462,7 @@ export default class Range { * @return -1, 0 or 1 depending on whether the point is before, inside or after the range, respectively */ comparePoint(node: Node, offset: number): number { + expectArity(arguments, 2); node = asObject(node, Node, 'Node'); offset = asUnsignedLong(offset); @@ -491,6 +503,7 @@ export default class Range { * @return Whether the range intersects node */ intersectsNode(node: Node): boolean { + expectArity(arguments, 1); node = asObject(node, Node, 'Node'); // 1. If node’s root is different from the context object’s root, return false. diff --git a/src/Text.ts b/src/Text.ts index 34910d6..1f4556b 100644 --- a/src/Text.ts +++ b/src/Text.ts @@ -2,10 +2,11 @@ import { replaceData, substringData, default as CharacterData } from './Characte import Document from './Document'; import { ranges } from './Range'; import { getContext } from './context/Context'; -import { throwIndexSizeError } from './util/errorHelpers'; +import { expectArity, throwIndexSizeError } from './util/errorHelpers'; import { insertNode } from './util/mutationAlgorithms'; import { NodeType } from './util/NodeType'; import { getNodeIndex } from './util/treeHelpers'; +import { asUnsignedLong } from './util/typeHelpers'; /** * 3.11. Interface Text @@ -43,6 +44,9 @@ export default class Text extends CharacterData { * @return a text node containing the second half of the split node's data */ public splitText(offset: number): Text { + expectArity(arguments, 1); + offset = asUnsignedLong(offset); + return splitText(this, offset); } diff --git a/src/mutation-observer/MutationObserver.ts b/src/mutation-observer/MutationObserver.ts index 7dee753..8a8e652 100644 --- a/src/mutation-observer/MutationObserver.ts +++ b/src/mutation-observer/MutationObserver.ts @@ -2,6 +2,8 @@ import MutationRecord from './MutationRecord'; import NotifyList from './NotifyList'; import RegisteredObserver from './RegisteredObserver'; import Node from '../Node'; +import { expectArity } from '../util/errorHelpers'; +import { asObject } from '../util/typeHelpers'; export interface MutationObserverInit { /** @@ -79,6 +81,9 @@ export default class MutationObserver { * @param callback Function called after mutations have been observed. */ constructor(callback: MutationCallback) { + expectArity(arguments, 1); + callback = asObject(callback, Function, 'Function'); + // create a new MutationObserver object with callback set to callback this._callback = callback; @@ -100,6 +105,9 @@ export default class MutationObserver { * @param options Determines which types of mutations to observe */ observe(target: Node, options: MutationObserverInit) { + expectArity(arguments, 2); + target = asObject(target, Node, 'Node'); + // Defaults from IDL options.childList = !!options.childList; options.subtree = !!options.subtree; From 89d3f847c7924d95fa760f94dbc2c882960eee4d Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 22:56:12 +0200 Subject: [PATCH 29/34] Add work-around for WebIDLParser.js script alias. --- test/web-platform-tests/webPlatform.tests.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/web-platform-tests/webPlatform.tests.ts b/test/web-platform-tests/webPlatform.tests.ts index ff4cf68..6641591 100644 --- a/test/web-platform-tests/webPlatform.tests.ts +++ b/test/web-platform-tests/webPlatform.tests.ts @@ -368,9 +368,16 @@ function getAllScripts(doc: slimdom.Document, casePath: string) { .reduce((scripts: string[], el: slimdom.Element) => { const src = el.attributes.find(a => a.name === 'src'); if (src) { - const resolvedPath = src.value.startsWith('/') - ? path.resolve(process.env.WEB_PLATFORM_TESTS_PATH, src.value.substring(1)) - : path.resolve(path.dirname(casePath), src.value); + let resolvedPath: string; + if (src.value === '/resources/WebIDLParser.js') { + // Historical alias, unfortunately not an actual file + // https://github.com/w3c/web-platform-tests/issues/5608 + resolvedPath = path.resolve(process.env.WEB_PLATFORM_TESTS_PATH, 'resources/webidl2/lib/webidl2.js'); + } else { + resolvedPath = src.value.startsWith('/') + ? path.resolve(process.env.WEB_PLATFORM_TESTS_PATH, src.value.substring(1)) + : path.resolve(path.dirname(casePath), src.value); + } return scripts.concat([fs.readFileSync(resolvedPath, 'utf-8')]); } From e913e9278808d34070d8a29b506422ce992ed3ab Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Mon, 19 Jun 2017 23:01:46 +0200 Subject: [PATCH 30/34] Normalize Windows paths to slashes for blacklist check. --- test/web-platform-tests/webPlatform.tests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web-platform-tests/webPlatform.tests.ts b/test/web-platform-tests/webPlatform.tests.ts index 6641591..07c9890 100644 --- a/test/web-platform-tests/webPlatform.tests.ts +++ b/test/web-platform-tests/webPlatform.tests.ts @@ -495,7 +495,7 @@ function createTests(dirPath: string): void { fs.readdirSync(dirPath).forEach(entry => { const entryPath = path.join(dirPath, entry); const relativePath = path.relative(process.env.WEB_PLATFORM_TESTS_PATH, entryPath); - const blacklistReason = TEST_BLACKLIST[relativePath]; + const blacklistReason = TEST_BLACKLIST[relativePath.replace(/\\/g, '/')]; if (typeof blacklistReason === 'string') { // Create a pending test it(`${entry}: ${blacklistReason}`); From 7eb5a1c22f8d68a0c96f24ecafc3a3895f4ff8af Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Tue, 20 Jun 2017 17:10:25 +0200 Subject: [PATCH 31/34] Bump version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cfea0f3..51961a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "slimdom", - "version": "1.0.1", + "version": "2.0.0", "description": "Fast, tiny DOM implementation in pure JS", "author": "Stef Busking", "license": "MIT", From 710d26a3c24dea91f2b949813630f8c3c0838b4d Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Wed, 21 Jun 2017 09:36:15 +0200 Subject: [PATCH 32/34] Use mangle blacklist to preserve class names for public types. --- rollup.config.js | 19 ++++++++++++++++++ src/DOMImplementation.ts | 2 +- src/Document.ts | 4 ++-- src/Element.ts | 6 +++--- src/Node.ts | 14 ++++++------- src/Range.ts | 24 +++++++++++------------ src/mutation-observer/MutationObserver.ts | 4 ++-- src/util/errorHelpers.ts | 4 ++-- src/util/typeHelpers.ts | 8 ++++---- 9 files changed, 52 insertions(+), 33 deletions(-) diff --git a/rollup.config.js b/rollup.config.js index 60d6813..695983c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -19,6 +19,25 @@ export default { plugins: [ babili({ comments: false, + mangle: { + blacklist: [ + 'Attr', + 'CDATASection', + 'CharacterData', + 'Comment', + 'Document', + 'DocumentFragment', + 'DocumentType', + 'DOMImplementation', + 'Element', + 'Node', + 'MutationObserver', + 'ProcessingInstruction', + 'Range', + 'Text', + 'XMLDocument' + ] + }, sourceMap: true }) ] diff --git a/src/DOMImplementation.ts b/src/DOMImplementation.ts index 2dee13e..1319fbd 100644 --- a/src/DOMImplementation.ts +++ b/src/DOMImplementation.ts @@ -67,7 +67,7 @@ export default class DOMImplementation { namespace = asNullableString(namespace); // [TreatNullAs=EmptyString] for qualifiedName qualifiedName = treatNullAsEmptyString(qualifiedName); - doctype = asNullableObject(doctype, DocumentType, 'DocumentType'); + doctype = asNullableObject(doctype, DocumentType); // 1. Let document be a new XMLDocument. const context = getContext(this._document); diff --git a/src/Document.ts b/src/Document.ts index 9201b9d..9e9b73c 100644 --- a/src/Document.ts +++ b/src/Document.ts @@ -283,7 +283,7 @@ export default class Document extends Node implements NonElementParentNode, Pare */ public importNode(node: Node, deep: boolean = false): Node { expectArity(arguments, 1); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); // 1. If node is a document or shadow root, then throw a NotSupportedError. if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { @@ -302,7 +302,7 @@ export default class Document extends Node implements NonElementParentNode, Pare */ public adoptNode(node: Node): Node { expectArity(arguments, 1); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); // 1. If node is a document, then throw a NotSupportedError. if (isNodeOfType(node, NodeType.DOCUMENT_NODE)) { diff --git a/src/Element.ts b/src/Element.ts index 16eb467..0f3e4f2 100644 --- a/src/Element.ts +++ b/src/Element.ts @@ -370,7 +370,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType */ public setAttributeNode(attr: Attr): Attr | null { expectArity(arguments, 1); - attr = asObject(attr, Attr, 'Attr'); + attr = asObject(attr, Attr); return setAttribute(attr, this); } @@ -384,7 +384,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType */ public setAttributeNodeNS(attr: Attr): Attr | null { expectArity(arguments, 1); - attr = asObject(attr, Attr, 'Attr'); + attr = asObject(attr, Attr); return setAttribute(attr, this); } @@ -398,7 +398,7 @@ export default class Element extends Node implements ParentNode, NonDocumentType */ public removeAttributeNode(attr: Attr): Attr { expectArity(arguments, 1); - attr = asObject(attr, Attr, 'Attr'); + attr = asObject(attr, Attr); // 1. If context object’s attribute list does not contain attr, then throw a NotFoundError. if (this.attributes.indexOf(attr) < 0) { diff --git a/src/Node.ts b/src/Node.ts index b032cfe..87c0c5c 100644 --- a/src/Node.ts +++ b/src/Node.ts @@ -222,7 +222,7 @@ export default abstract class Node { */ public contains(other: Node | null): boolean { expectArity(arguments, 1); - other = asNullableObject(other, Node, 'Node'); + other = asNullableObject(other, Node); while (other && other != this) { other = other.parentNode; @@ -283,8 +283,8 @@ export default abstract class Node { */ public insertBefore(node: Node, child: Node | null): Node { expectArity(arguments, 2); - node = asObject(node, Node, 'Node'); - child = asNullableObject(child, Node, 'Node'); + node = asObject(node, Node); + child = asNullableObject(child, Node); return preInsertNode(node, this, child); } @@ -300,7 +300,7 @@ export default abstract class Node { */ public appendChild(node: Node): Node { expectArity(arguments, 1); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); return appendNode(node, this); } @@ -315,8 +315,8 @@ export default abstract class Node { */ public replaceChild(node: Node, child: Node): Node { expectArity(arguments, 2); - node = asObject(node, Node, 'Node'); - child = asObject(child, Node, 'Node'); + node = asObject(node, Node); + child = asObject(child, Node); return replaceChildWithNode(child, node, this); } @@ -330,7 +330,7 @@ export default abstract class Node { */ public removeChild(child: Node): Node { expectArity(arguments, 1); - child = asObject(child, Node, 'Node'); + child = asObject(child, Node); return preRemoveChild(child, this); } diff --git a/src/Range.ts b/src/Range.ts index 44aac5c..481f6e2 100644 --- a/src/Range.ts +++ b/src/Range.ts @@ -76,7 +76,7 @@ export default class Range { */ setStart(node: Node, offset: number): void { expectArity(arguments, 2); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); offset = asUnsignedLong(offset); // 1. If node is a doctype, then throw an InvalidNodeTypeError. @@ -120,7 +120,7 @@ export default class Range { */ setEnd(node: Node, offset: number): void { expectArity(arguments, 2); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); offset = asUnsignedLong(offset); // 1. If node is a doctype, then throw an InvalidNodeTypeError. @@ -163,7 +163,7 @@ export default class Range { */ setStartBefore(node: Node): void { expectArity(arguments, 1); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -184,7 +184,7 @@ export default class Range { */ setStartAfter(node: Node): void { expectArity(arguments, 1); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -205,7 +205,7 @@ export default class Range { */ setEndBefore(node: Node): void { expectArity(arguments, 1); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -226,7 +226,7 @@ export default class Range { */ setEndAfter(node: Node): void { expectArity(arguments, 1); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); // 1. Let parent be node’s parent. const parent = node.parentNode; @@ -257,7 +257,7 @@ export default class Range { selectNode(node: Node): void { expectArity(arguments, 1); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); // 1. Let parent be node’s parent. let parent = node.parentNode; @@ -281,7 +281,7 @@ export default class Range { selectNodeContents(node: Node): void { expectArity(arguments, 1); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); // 1. If node is a doctype, throw an InvalidNodeTypeError. if (isNodeOfType(node, NodeType.DOCUMENT_TYPE_NODE)) { @@ -307,7 +307,7 @@ export default class Range { compareBoundaryPoints(how: number, sourceRange: Range): number { expectArity(arguments, 2); - sourceRange = asObject(sourceRange, Range, 'Range'); + sourceRange = asObject(sourceRange, Range); // 1. If how is not one of START_TO_START, START_TO_END, END_TO_END, and END_TO_START, then throw a // NotSupportedError. @@ -423,7 +423,7 @@ export default class Range { */ isPointInRange(node: Node, offset: number): boolean { expectArity(arguments, 2); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); offset = asUnsignedLong(offset); // 1. If node’s root is different from the context object’s root, return false. @@ -463,7 +463,7 @@ export default class Range { */ comparePoint(node: Node, offset: number): number { expectArity(arguments, 2); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); offset = asUnsignedLong(offset); // 1. If node’s root is different from the context object’s root, then throw a WrongDocumentError. @@ -504,7 +504,7 @@ export default class Range { */ intersectsNode(node: Node): boolean { expectArity(arguments, 1); - node = asObject(node, Node, 'Node'); + node = asObject(node, Node); // 1. If node’s root is different from the context object’s root, return false. if (getRootOfNode(node) !== getRootOfRange(this)) { diff --git a/src/mutation-observer/MutationObserver.ts b/src/mutation-observer/MutationObserver.ts index 8a8e652..0b73646 100644 --- a/src/mutation-observer/MutationObserver.ts +++ b/src/mutation-observer/MutationObserver.ts @@ -82,7 +82,7 @@ export default class MutationObserver { */ constructor(callback: MutationCallback) { expectArity(arguments, 1); - callback = asObject(callback, Function, 'Function'); + callback = asObject(callback, Function); // create a new MutationObserver object with callback set to callback this._callback = callback; @@ -106,7 +106,7 @@ export default class MutationObserver { */ observe(target: Node, options: MutationObserverInit) { expectArity(arguments, 2); - target = asObject(target, Node, 'Node'); + target = asObject(target, Node); // Defaults from IDL options.childList = !!options.childList; diff --git a/src/util/errorHelpers.ts b/src/util/errorHelpers.ts index dc16839..113f6ba 100644 --- a/src/util/errorHelpers.ts +++ b/src/util/errorHelpers.ts @@ -5,9 +5,9 @@ export function expectArity(args: IArguments, minArity: number): void { } } -export function expectObject(value: T, Constructor: any, typeName: string): void { +export function expectObject(value: T, Constructor: Function): void { if (!(value instanceof Constructor)) { - throw new TypeError(`Value should be an instance of ${typeName}`); + throw new TypeError(`Value should be an instance of ${Constructor.name}`); } } diff --git a/src/util/typeHelpers.ts b/src/util/typeHelpers.ts index b0de952..603f26f 100644 --- a/src/util/typeHelpers.ts +++ b/src/util/typeHelpers.ts @@ -14,18 +14,18 @@ export function treatNullAsEmptyString(value: string | null): string { return String(value); } -export function asObject(value: T, Constructor: any, typeName: string): T { - expectObject(value, Constructor, typeName); +export function asObject(value: T, Constructor: any): T { + expectObject(value, Constructor); return value; } -export function asNullableObject(value: T | null | undefined, Constructor: any, typeName: string): T | null { +export function asNullableObject(value: T | null | undefined, Constructor: any): T | null { if (value === undefined || value === null) { return null; } - return asObject(value, Constructor, typeName); + return asObject(value, Constructor); } export function asNullableString(value: string | null | undefined): string | null { From 54a2d0da8598a6f9c4df9af02634f388c51f2bcf Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Wed, 21 Jun 2017 10:29:16 +0200 Subject: [PATCH 33/34] Add test for the DocumentFragment constructor. --- test/DocumentFragment.tests.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/DocumentFragment.tests.ts b/test/DocumentFragment.tests.ts index 9538f07..30baed4 100644 --- a/test/DocumentFragment.tests.ts +++ b/test/DocumentFragment.tests.ts @@ -14,6 +14,15 @@ describe('DocumentFragment', () => { chai.assert.equal(df.nodeType, 11); chai.assert.equal(df.nodeName, '#document-fragment'); chai.assert.equal(df.nodeValue, null); + chai.assert.equal(df.ownerDocument, document); + }); + + it('can be created using its constructor', () => { + const df = new slimdom.DocumentFragment(); + chai.assert.equal(df.nodeType, 11); + chai.assert.equal(df.nodeName, '#document-fragment'); + chai.assert.equal(df.nodeValue, null); + chai.assert.equal(df.ownerDocument, slimdom.document); }); it('can not change its nodeValue', () => { From ff206efa7b44dd548aa0bef015fc3dfed8002cd9 Mon Sep 17 00:00:00 2001 From: Stef Busking Date: Wed, 21 Jun 2017 11:50:18 +0200 Subject: [PATCH 34/34] Add some content to the README. --- README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b62877b..2cae2b7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,82 @@ -# SlimDOM.js [![Build Status](https://travis-ci.org/bwrrp/slimdom.js.png?branch=master)](https://travis-ci.org/bwrrp/slimdom.js) - -Fast, tiny DOM implementation. +# slimdom [![Build Status](https://travis-ci.org/bwrrp/slimdom.js.png?branch=master)](https://travis-ci.org/bwrrp/slimdom.js) + +Fast, tiny DOM implementation for node and the browser. + +This is a (partial) implementation of the [DOM living standard][DOMSTANDARD], as last updated 15 June 2017. See the 'Features' and 'Limitations' sections below for details on what's included and what's not. + +[DOMSTANDARD]: https://dom.spec.whatwg.org/ + +## Installation + +The slimdom library can be installed using npm or yarn: +``` +npm install --save slimdom +``` +or +``` +yarn add slimdom +``` + +The package includes both a commonJS bundle (`dist/slimdom.js`) and an ES6 module (`dist/slimdom.mjs`). + +## Usage + +Create documents using the slimdom.Document constructor, and manipulate them using the [standard DOM API][1]. + +``` +import * as slimdom from 'slimdom'; + +const document = new slimdom.Document(); +document.appendChild(document.createElement('root')); +// ... +``` + +Some DOM API's, such as the `DocumentFragment` constructor, require the presence of a global document. In these cases, slimdom will use the instance exposed through `slimdom.document`. Although you could mutate this document, it is recommended to create your own to avoid conflicts with other code using slimdom in your application. + +When using a `Range`, make sure to call `detach` when you don't need it anymore. As JavaScript currently does not have a way to detect when the instance can be garbage collected, we don't have any other way of detecting when we can stop updating the range for mutations to the surrounding nodes. + +## Features + +This library implements: + +* All node types: `Attr`, `CDATASection`, `Comment`, `Document`, `DocumentFragment`, `DocumentType`, `Element`, `ProcessingInstruction`, `Text` and `XMLDocument`. +* `Range`, which correctly updates under mutations +* `MutationObserver` + +## Limitations + +The following features are not (yet) implemented: + +* No events, no `createEvent` on `Document` +* Arrays are used instead of `HTMLCollection` / `NodeList` and `NamedNodeMap`. +* No `getElementById` / `getElementsByTagName` / `getElementsByTagNameNS` / `getElementsByClassName` +* No `prepend` / `append` +* No selectors, no `querySelector` / `querySelectorAll` on `ParentNode`, no `closest` / `matches` / `webkitMatchesSelector` on `Element` +* No `before` / `after` / `replaceWith` / `remove` +* No `attributeFilter` for mutation observers +* No `baseURI` / `isConnected` / `getRootNode` / `textContent` / `isEqualNode` / `isSameNode` / `compareDocumentPosition` on `Node` +* No `URL` / `documentURI` / `origin` / `compatMode` / `characterSet` / `charset` / `inputEncoding` / `contentType` on `Document` +* No `hasFeature` on `DOMImplementation` +* No `id` / `className` / `classList` / `insertAdjacentElement` / `insertAdjacentText` on `Element` +* No `specified` on `Attr` +* No `wholeText` on `Text` +* No `deleteContents` / `extractContents` / `cloneContents` / `insertNode` / `surroundContents` on `Range` +* No `NodeIterator` / `TreeWalker` / `NodeFilter`, no `createNodeIterator` / `createTreeWalker` on `Document` +* No HTML documents, including `HTMLElement` and its subclasses. This also includes HTML casing behavior for attributes and tagNames. +* No shadow DOM, `Slotable` / `ShadowRoot`, no `slot` / `attachShadow` / `shadowRoot` on `Element` +* No custom elements +* No XML or HTML parsing / serialization, but see `test/SlimdomTreeAdapter.ts` for an example on how to connect the parse5 HTML parser. + +Do not rely on the behavior or presence of any methods and properties not specified in the DOM standard. For example, do not use JavaScript array methods exposed on properties that should expose a NodeList and do not use Element as a constructor. This behavior is *not* considered public API and may change without warning in a future release. + +## Contributing + +Pull requests for missing features or tests, bug reports, questions and other feedback are always welcome! Just [open an issue](https://github.com/bwrrp/slimdom.js/issues/new) on the github repo, and provide as much detail as you can. + +To work on the slimdom library itself, clone [the repository](https://github.com/bwrrp/slimdom.js) and run `npm install` to install its dependencies. + +The slimdom library and tests are developed in [TypeScript](https://www.typescriptlang.org/), using [prettier](https://github.com/prettier/prettier) to automate formatting. Settings for the vscode [vscode-prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) are included. If you use prettier from another editor, please use options equivalent to the command line `--print-width 120 --use-tabs --single-quote`. + +This repository includes a full suite of tests based on [mocha](http://mochajs.org/) and [chai](http://chaijs.com/), with coverage computed using [istanbul and nyc](https://istanbul.js.org/). Run `npm test` to run the tests, or `npm run test:debug` to debug the tests and code by disabling coverage and enabling the node inspector (see [chrome://inspect](chrome://inspect) in Chrome). + +An experimental runner for the W3C [web platform tests](http://web-platform-tests.org/) is included in the `test/web-platform-tests` directory. To use it, clone the [web platform tests repository](https://github.com/w3c/web-platform-tests) somewhere and set the `WEB_PLATFORM_TESTS_PATH` environment variable to the corresponding path. Then run `npm test` as normal. The `webPlatform.tests.ts` file contains a blacklist of tests that don't currently run due to missing features.