diff --git a/src/index.ts b/src/index.ts index 3dd38bda..a6e5c781 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,14 @@ import { ElementType } from "domelementtype"; import { - Node, + ChildNode, Element, DataNode, Text, Comment, - NodeWithChildren, + CDATA, Document, ProcessingInstruction, + ParentNode, } from "./node"; export * from "./node"; @@ -51,12 +52,12 @@ interface ParserInterface { endIndex: number | null; } -type Callback = (error: Error | null, dom: Node[]) => void; +type Callback = (error: Error | null, dom: ChildNode[]) => void; type ElementCallback = (element: Element) => void; export class DomHandler { /** The elements of the DOM */ - public dom: Node[] = []; + public dom: ChildNode[] = []; /** The root element for the DOM */ public root = new Document(this.dom); @@ -74,7 +75,7 @@ export class DomHandler { private done = false; /** Stack of open tags. */ - protected tagStack: NodeWithChildren[] = [this.root]; + protected tagStack: ParentNode[] = [this.root]; /** A data node that is still being written to. */ protected lastNode: DataNode | null = null; @@ -184,7 +185,7 @@ export class DomHandler { public oncdatastart(): void { const text = new Text(""); - const node = new NodeWithChildren(ElementType.CDATA, [text]); + const node = new CDATA([text]); this.addNode(node); @@ -209,10 +210,10 @@ export class DomHandler { } } - protected addNode(node: Node): void { + protected addNode(node: ChildNode): void { const parent = this.tagStack[this.tagStack.length - 1]; const previousSibling = parent.children[parent.children.length - 1] as - | Node + | ChildNode | undefined; if (this.options.withStartIndices) { diff --git a/src/node.spec.ts b/src/node.spec.ts index f7fe6b50..50ebe78c 100644 --- a/src/node.spec.ts +++ b/src/node.spec.ts @@ -65,7 +65,11 @@ describe("Nodes", () => { }); it("should throw an error when cloning unsupported types", () => { - const el = new node.Node(ElementType.Doctype); + class Doctype extends node.Node { + type = ElementType.Doctype; + nodeType = NaN; + } + const el = new Doctype(); expect(() => el.cloneNode()).toThrow("Not implemented yet: doctype"); }); @@ -81,6 +85,21 @@ describe("Nodes", () => { expect(node.isDirective(result)).toBe(false); expect(node.isDocument(result)).toBe(false); }); + + it("should support using tagged types", () => { + // We want to make sure TS is happy about the tagged types. + const parent: node.ParentNode = new node.Document([]); + + function setQuirks(el: node.ParentNode): void { + if (el.type === ElementType.Root) { + el["x-mode"] = "no-quirks"; + } + } + + setQuirks(parent); + + expect(parent).toHaveProperty("x-mode", "no-quirks"); + }); }); type Options = DomHandlerOptions & ParserOptions; diff --git a/src/node.ts b/src/node.ts index 137863f3..38040994 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,16 +1,5 @@ import { ElementType, isTag as isTagRaw } from "domelementtype"; -const nodeTypes = new Map([ - [ElementType.Tag, 1], - [ElementType.Script, 1], - [ElementType.Style, 1], - [ElementType.Directive, 1], - [ElementType.Text, 3], - [ElementType.CDATA, 4], - [ElementType.Comment, 8], - [ElementType.Root, 9], -]); - interface SourceCodeLocation { /** One-based line index of the first character. */ startLine: number; @@ -31,19 +20,26 @@ interface TagSourceCodeLocation extends SourceCodeLocation { endTag?: SourceCodeLocation; } +export type ParentNode = Document | Element | CDATA; +export type ChildNode = DataNode | Element | CDATA; +export type AnyNode = ParentNode | ChildNode; + /** * This object will be used as the prototype for Nodes when creating a * DOM-Level-1-compliant structure. */ -export class Node { +export abstract class Node { + /** The type of the node. */ + abstract readonly type: ElementType; + /** Parent of the node */ - parent: NodeWithChildren | null = null; + parent: ParentNode | null = null; /** Previous sibling */ - prev: Node | null = null; + prev: ChildNode | null = null; /** Next sibling */ - next: Node | null = null; + next: ChildNode | null = null; /** The start index of the node. Requires `withStartIndices` on the handler to be `true. */ startIndex: number | null = null; @@ -58,21 +54,13 @@ export class Node { */ sourceCodeLocation?: SourceCodeLocation | null; - /** - * - * @param type The type of the node. - */ - constructor(public type: ElementType) {} - // Read-only aliases /** * [DOM spec](https://dom.spec.whatwg.org/#dom-node-nodetype)-compatible * node {@link type}. */ - get nodeType(): number { - return nodeTypes.get(this.type) ?? 1; - } + abstract readonly nodeType: number; // Read-write aliases for properties @@ -80,11 +68,11 @@ export class Node { * Same as {@link parent}. * [DOM spec](https://dom.spec.whatwg.org)-compatible alias. */ - get parentNode(): NodeWithChildren | null { + get parentNode(): ParentNode | null { return this.parent; } - set parentNode(parent: NodeWithChildren | null) { + set parentNode(parent: ParentNode | null) { this.parent = parent; } @@ -92,11 +80,11 @@ export class Node { * Same as {@link prev}. * [DOM spec](https://dom.spec.whatwg.org)-compatible alias. */ - get previousSibling(): Node | null { + get previousSibling(): ChildNode | null { return this.prev; } - set previousSibling(prev: Node | null) { + set previousSibling(prev: ChildNode | null) { this.prev = prev; } @@ -104,11 +92,11 @@ export class Node { * Same as {@link next}. * [DOM spec](https://dom.spec.whatwg.org)-compatible alias. */ - get nextSibling(): Node | null { + get nextSibling(): ChildNode | null { return this.next; } - set nextSibling(next: Node | null) { + set nextSibling(next: ChildNode | null) { this.next = next; } @@ -126,16 +114,12 @@ export class Node { /** * A node that contains some data. */ -export class DataNode extends Node { +export abstract class DataNode extends Node { /** - * @param type The type of the node * @param data The content of the data node */ - constructor( - type: ElementType.Comment | ElementType.Text | ElementType.Directive, - public data: string - ) { - super(type); + constructor(public data: string) { + super(); } /** @@ -155,8 +139,10 @@ export class DataNode extends Node { * Text within the document. */ export class Text extends DataNode { - constructor(data: string) { - super(ElementType.Text, data); + type: ElementType.Text = ElementType.Text; + + get nodeType(): 3 { + return 3; } } @@ -164,8 +150,10 @@ export class Text extends DataNode { * Comments within the document. */ export class Comment extends DataNode { - constructor(data: string) { - super(ElementType.Comment, data); + type: ElementType.Comment = ElementType.Comment; + + get nodeType(): 8 { + return 8; } } @@ -173,8 +161,14 @@ export class Comment extends DataNode { * Processing instructions, including doc types. */ export class ProcessingInstruction extends DataNode { + type: ElementType.Directive = ElementType.Directive; + constructor(public name: string, data: string) { - super(ElementType.Directive, data); + super(data); + } + + override get nodeType(): 1 { + return 1; } /** If this is a doctype, the document type name (parse5 only). */ @@ -188,31 +182,22 @@ export class ProcessingInstruction extends DataNode { /** * A `Node` that can have children. */ -export class NodeWithChildren extends Node { +export abstract class NodeWithChildren extends Node { /** - * @param type Type of the node. * @param children Children of the node. Only certain node types can have children. */ - constructor( - type: - | ElementType.Root - | ElementType.CDATA - | ElementType.Script - | ElementType.Style - | ElementType.Tag, - public children: Node[] - ) { - super(type); + constructor(public children: ChildNode[]) { + super(); } // Aliases /** First child of the node. */ - get firstChild(): Node | null { + get firstChild(): ChildNode | null { return this.children[0] ?? null; } /** Last child of the node. */ - get lastChild(): Node | null { + get lastChild(): ChildNode | null { return this.children.length > 0 ? this.children[this.children.length - 1] : null; @@ -222,21 +207,31 @@ export class NodeWithChildren extends Node { * Same as {@link children}. * [DOM spec](https://dom.spec.whatwg.org)-compatible alias. */ - get childNodes(): Node[] { + get childNodes(): ChildNode[] { return this.children; } - set childNodes(children: Node[]) { + set childNodes(children: ChildNode[]) { this.children = children; } } +export class CDATA extends NodeWithChildren { + type: ElementType.CDATA = ElementType.CDATA; + + get nodeType(): 4 { + return 4; + } +} + /** * The root node of the document. */ export class Document extends NodeWithChildren { - constructor(children: Node[]) { - super(ElementType.Root, children); + type: ElementType.Root = ElementType.Root; + + get nodeType(): 9 { + return 9; } /** [Document mode](https://dom.spec.whatwg.org/#concept-document-limited-quirks) (parse5 only). */ @@ -265,8 +260,8 @@ export class Element extends NodeWithChildren { constructor( public name: string, public attribs: { [name: string]: string }, - children: Node[] = [], - type: + children: ChildNode[] = [], + public type: | ElementType.Tag | ElementType.Script | ElementType.Style = name === "script" @@ -275,7 +270,11 @@ export class Element extends NodeWithChildren { ? ElementType.Style : ElementType.Tag ) { - super(type, children); + super(children); + } + + get nodeType(): 1 { + return 1; } /** @@ -328,7 +327,7 @@ export function isTag(node: Node): node is Element { * @param node Node to check. * @returns `true` if the node has the type `CDATA`, `false` otherwise. */ -export function isCDATA(node: Node): node is NodeWithChildren { +export function isCDATA(node: Node): node is CDATA { return node.type === ElementType.CDATA; } @@ -366,9 +365,9 @@ export function isDocument(node: Node): node is Document { /** * @param node Node to check. - * @returns `true` if the node is a `NodeWithChildren` (has children), `false` otherwise. + * @returns `true` if the node has children, `false` otherwise. */ -export function hasChildren(node: Node): node is NodeWithChildren { +export function hasChildren(node: Node): node is ParentNode { return Object.prototype.hasOwnProperty.call(node, "children"); } @@ -403,7 +402,7 @@ export function cloneNode(node: T, recursive = false): T { result = clone; } else if (isCDATA(node)) { const children = recursive ? cloneChildren(node.children) : []; - const clone = new NodeWithChildren(ElementType.CDATA, children); + const clone = new CDATA(children); children.forEach((child) => (child.parent = clone)); result = clone; } else if (isDocument(node)) { @@ -440,7 +439,7 @@ export function cloneNode(node: T, recursive = false): T { return result as T; } -function cloneChildren(childs: Node[]): Node[] { +function cloneChildren(childs: ChildNode[]): ChildNode[] { const children = childs.map((child) => cloneNode(child, true)); for (let i = 1; i < children.length; i++) {