Skip to content

Commit

Permalink
feat(node): Add type checks, make cloneNode return passed type
Browse files Browse the repository at this point in the history
  • Loading branch information
fb55 committed Apr 11, 2021
1 parent 8108a70 commit d5422c2
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 64 deletions.
20 changes: 20 additions & 0 deletions src/node.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ElementType } from "domelementtype";
import { Parser, ParserOptions } from "htmlparser2";
import Handler, { NodeWithChildren, DomHandlerOptions } from ".";
import * as node from "./node";

describe("Nodes", () => {
it("should serialize to a Jest snapshot", () => {
Expand Down Expand Up @@ -61,6 +63,24 @@ describe("Nodes", () => {
expect(clone.startIndex).toBe(0);
expect(clone.endIndex).toBe(22);
});

it("should throw an error when cloning unsupported types", () => {
const el = new node.Node(ElementType.Doctype);
expect(() => el.cloneNode()).toThrow("Not implemented yet: doctype");
});

it("should detect tag types", () => {
const result = parse("<div foo=bar><div><div>").children[0];

expect(node.isTag(result)).toBe(true);
expect(node.hasChildren(result)).toBe(true);

expect(node.isCDATA(result)).toBe(false);
expect(node.isText(result)).toBe(false);
expect(node.isComment(result)).toBe(false);
expect(node.isDirective(result)).toBe(false);
expect(node.isDocument(result)).toBe(false);
});
});

type Options = DomHandlerOptions & ParserOptions;
Expand Down
166 changes: 102 additions & 64 deletions src/node.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ElementType } from "domelementtype";
import { ElementType, isTag as isTagRaw } from "domelementtype";

const nodeTypes = new Map<ElementType, number>([
[ElementType.Tag, 1],
Expand Down Expand Up @@ -73,7 +73,7 @@ export class Node {
* @param recursive Clone child nodes as well.
* @returns A clone of the node.
*/
cloneNode(recursive = false): Node {
cloneNode<T extends Node>(this: T, recursive = false): T {
return cloneNode(this, recursive);
}
}
Expand Down Expand Up @@ -219,82 +219,120 @@ export class Element extends NodeWithChildren {
"x-attribsPrefix"?: Record<string, string>;
}

/**
* @param node Node to check.
* @returns `true` if the node is a `Element`, `false` otherwise.
*/
export function isTag(node: Node): node is Element {
return isTagRaw(node);
}

/**
* @param node Node to check.
* @returns `true` if the node has the type `CDATA`, `false` otherwise.
*/
export function isCDATA(node: Node): node is NodeWithChildren {
return node.type === ElementType.CDATA;
}

/**
* @param node Node to check.
* @returns `true` if the node has the type `Text`, `false` otherwise.
*/
export function isText(node: Node): node is DataNode {
return node.type === ElementType.Text;
}

/**
* @param node Node to check.
* @returns `true` if the node has the type `Comment`, `false` otherwise.
*/
export function isComment(node: Node): node is DataNode {
return node.type === ElementType.Comment;
}

/**
* @param node Node to check.
* @returns `true` if the node has the type `ProcessingInstruction`, `false` otherwise.
*/
export function isDirective(node: Node): node is ProcessingInstruction {
return node.type === ElementType.Directive;
}

/**
* @param node Node to check.
* @returns `true` if the node has the type `ProcessingInstruction`, `false` otherwise.
*/
export function isDocument(node: Node): node is Document {
return node.type === ElementType.Root;
}

/**
* @param node Node to check.
* @returns `true` if the node is a `NodeWithChildren` (has children), `false` otherwise.
*/
export function hasChildren(node: Node): node is NodeWithChildren {
return Object.prototype.hasOwnProperty.call(node, "children");
}

/**
* Clone a node, and optionally its children.
*
* @param recursive Clone child nodes as well.
* @returns A clone of the node.
*/
export function cloneNode(node: Node, recursive = false): Node {
let result;

switch (node.type) {
case ElementType.Text:
result = new Text((node as Text).data);
break;
case ElementType.Directive: {
const instr = node as ProcessingInstruction;
result = new ProcessingInstruction(instr.name, instr.data);

if (instr["x-name"] != null) {
result["x-name"] = instr["x-name"];
result["x-publicId"] = instr["x-publicId"];
result["x-systemId"] = instr["x-systemId"];
}

break;
}
case ElementType.Comment:
result = new Comment((node as Comment).data);
break;
case ElementType.Tag:
case ElementType.Script:
case ElementType.Style: {
const elem = node as Element;
const children = recursive ? cloneChildren(elem.children) : [];
const clone = new Element(elem.name, { ...elem.attribs }, children);
children.forEach((child) => (child.parent = clone));

if (elem["x-attribsNamespace"]) {
clone["x-attribsNamespace"] = { ...elem["x-attribsNamespace"] };
}
if (elem["x-attribsPrefix"]) {
clone["x-attribsPrefix"] = { ...elem["x-attribsPrefix"] };
}

result = clone;
break;
export function cloneNode<T extends Node>(node: T, recursive = false): T {
let result: Node;

if (isText(node)) {
result = new Text(node.data);
} else if (isComment(node)) {
result = new Comment(node.data);
} else if (isTag(node)) {
const children = recursive ? cloneChildren(node.children) : [];
const clone = new Element(node.name, { ...node.attribs }, children);
children.forEach((child) => (child.parent = clone));

if (node["x-attribsNamespace"]) {
clone["x-attribsNamespace"] = { ...node["x-attribsNamespace"] };
}
case ElementType.CDATA: {
const cdata = node as NodeWithChildren;
const children = recursive ? cloneChildren(cdata.children) : [];
const clone = new NodeWithChildren(node.type, children);
children.forEach((child) => (child.parent = clone));
result = clone;
break;
if (node["x-attribsPrefix"]) {
clone["x-attribsPrefix"] = { ...node["x-attribsPrefix"] };
}
case ElementType.Root: {
const doc = node as Document;
const children = recursive ? cloneChildren(doc.children) : [];
const clone = new Document(children);
children.forEach((child) => (child.parent = clone));

if (doc["x-mode"]) {
clone["x-mode"] = doc["x-mode"];
}

result = clone;
break;

result = clone;
} else if (isCDATA(node)) {
const children = recursive ? cloneChildren(node.children) : [];
const clone = new NodeWithChildren(ElementType.CDATA, children);
children.forEach((child) => (child.parent = clone));
result = clone;
} else if (isDocument(node)) {
const children = recursive ? cloneChildren(node.children) : [];
const clone = new Document(children);
children.forEach((child) => (child.parent = clone));

if (node["x-mode"]) {
clone["x-mode"] = node["x-mode"];
}
case ElementType.Doctype: {
// This type isn't used yet.
throw new Error("Not implemented yet: ElementType.Doctype case");

result = clone;
} else if (isDirective(node)) {
const instruction = new ProcessingInstruction(node.name, node.data);

if (node["x-name"] != null) {
instruction["x-name"] = node["x-name"];
instruction["x-publicId"] = node["x-publicId"];
instruction["x-systemId"] = node["x-systemId"];
}

result = instruction;
} else {
throw new Error(`Not implemented yet: ${node.type}`);
}

result.startIndex = node.startIndex;
result.endIndex = node.endIndex;
return result;
return result as T;
}

function cloneChildren(childs: Node[]): Node[] {
Expand Down

0 comments on commit d5422c2

Please sign in to comment.