Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/nodejs-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:
- 20
- 22
- 24
- 26
- lts/*

steps:
Expand Down
10 changes: 9 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,13 @@ export default defineConfig([
"n/no-unsupported-features/es-builtins": 0,
},
},
eslintConfigBiome
eslintConfigBiome,
{
rules: {
// This module uses parser DOM nodes, which do not implement browser traversal APIs.
"unicorn/better-dom-traversing": "off",
// Use the Number namespace required by the existing Biome configuration.
"unicorn/prefer-global-number-constants": "off",
},
},
]);
421 changes: 221 additions & 200 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"devDependencies": {
"@biomejs/biome": "^2.5.15",
"@eslint/compat": "^2.1.1",
"@feedic/eslint-config": "^0.4.1",
"@feedic/eslint-config": "^0.5.0",
"@types/node": "^26.6.4",
"eslint": "^10.12.0",
"eslint-config-biome": "^2.1.3",
Expand Down
7 changes: 4 additions & 3 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import Handler, { type DomHandlerOptions, type Node } from "./index.js";
const basePath = resolve(fileURLToPath(import.meta.url), "..", "__fixtures__");

describe("DomHandler", () => {
for (const { name, html, options = {}, expected } of readdirSync(basePath)
const fixtures = readdirSync(basePath)
.filter((name) => name.endsWith(".json"))
.map((name) => resolve(basePath, name))
.map((path) => JSON.parse(readFileSync(path, "utf8")))) {
.map((path) => JSON.parse(readFileSync(path, "utf8")));
for (const { name, html, options = {}, expected } of fixtures) {
test(name, () => {
const result = parse(html, options);

Expand Down Expand Up @@ -59,7 +60,7 @@ function compare(actual: unknown, expected: unknown) {
expect(actual).toBe(expected);
} else {
for (const property in expected) {
expect(property in actual).toBeTruthy();
expect(actual).toHaveProperty([property]);
compare(actual[property as never], expected[property as never]);
}
}
Expand Down
18 changes: 9 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,6 @@ type ElementCallback = (element: Element) => void;
* Event-based handler that builds a DOM tree from parser callbacks.
*/
export class DomHandler {
/** The elements of the DOM */
dom: ChildNode[] = [];

/** The root element for the DOM */
root: Document = new Document(this.dom);

/** Called once parsing has completed. */
private readonly callback: Callback | null;

Expand All @@ -77,15 +71,21 @@ export class DomHandler {
/** Indicated whether parsing has been completed. */
private done = false;

/** Reference to the parser instance. Used for location information. */
private parser: ParserInterface | null = null;

/** The elements of the DOM */
dom: ChildNode[] = [];

/** The root element for the DOM */
root: Document = new Document(this.dom);

/** Stack of open tags. */
protected tagStack: ParentNode[] = [this.root];

/** A data node that is still being written to. */
protected lastNode: DataNode | null = null;

/** Reference to the parser instance. Used for location information. */
private parser: ParserInterface | null = null;

/**
* @param callback Called once parsing has completed.
* @param options Settings for the handler.
Expand Down
70 changes: 35 additions & 35 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,11 @@ export abstract class Node {

/**
* Clone this node, and optionally its children.
* @param recursive Clone child nodes as well.
* @param shouldRecurse Clone child nodes as well.
* @returns A clone of the node.
*/
cloneNode<T extends Node>(this: T, recursive = false): T {
return cloneNode(this, recursive);
cloneNode<T extends Node>(this: T, shouldRecurse = false): T {
return cloneNode(this, shouldRecurse);
}
}

Expand Down Expand Up @@ -184,6 +184,13 @@ export class ProcessingInstruction extends DataNode {
type: ElementType.Directive = ElementType.Directive;
name: string;

/** If this is a doctype, the document type name (parse5 only). */
"x-name"?: string;
/** If this is a doctype, the document type public identifier (parse5 only). */
"x-publicId"?: string;
/** If this is a doctype, the document type system identifier (parse5 only). */
"x-systemId"?: string;

constructor(name: string, data: string) {
super(data);
this.name = name;
Expand All @@ -192,13 +199,6 @@ export class ProcessingInstruction extends DataNode {
override get nodeType(): 1 {
return 1;
}

/** If this is a doctype, the document type name (parse5 only). */
"x-name"?: string;
/** If this is a doctype, the document type public identifier (parse5 only). */
"x-publicId"?: string;
/** If this is a doctype, the document type system identifier (parse5 only). */
"x-systemId"?: string;
}

/**
Expand All @@ -218,7 +218,7 @@ export abstract class NodeWithChildren extends Node {
// Aliases
/** First child of the node. */
get firstChild(): ChildNode | null {
return this.children[0] ?? null;
return this.children.at(0) ?? null;
}

/** Last child of the node. */
Expand Down Expand Up @@ -258,12 +258,12 @@ export class CDATA extends NodeWithChildren {
export class Document extends NodeWithChildren {
type: ElementType.Root = ElementType.Root;

/** [Document mode](https://dom.spec.whatwg.org/#concept-document-limited-quirks) (parse5 only). */
declare "x-mode"?: "no-quirks" | "quirks" | "limited-quirks";

get nodeType(): 9 {
return 9;
}

/** [Document mode](https://dom.spec.whatwg.org/#concept-document-limited-quirks) (parse5 only). */
declare "x-mode"?: "no-quirks" | "quirks" | "limited-quirks";
}

/**
Expand All @@ -284,6 +284,20 @@ export class Element extends NodeWithChildren {
attribs: { [name: string]: string };
type: ElementType.Tag | ElementType.Script | ElementType.Style;

/**
* `parse5` source code location info, with start & end tags.
*
* Available if parsing with parse5 and location info is enabled.
*/
declare sourceCodeLocation?: TagSourceCodeLocation | null;

/** Element namespace (parse5 only). */
namespace?: string;
/** Element attribute namespaces (parse5 only). */
"x-attribsNamespace"?: Record<string, string>;
/** Element attribute namespace-related prefixes (parse5 only). */
"x-attribsPrefix"?: Record<string, string>;

/**
* @param name Name of the tag, eg. `div`, `span`.
* @param attribs Object mapping attribute names to attribute values.
Expand Down Expand Up @@ -313,13 +327,6 @@ export class Element extends NodeWithChildren {
return 1;
}

/**
* `parse5` source code location info, with start & end tags.
*
* Available if parsing with parse5 and location info is enabled.
*/
declare sourceCodeLocation?: TagSourceCodeLocation | null;

// DOM Level 1 aliases

/**
Expand All @@ -335,20 +342,13 @@ export class Element extends NodeWithChildren {
}

get attributes(): Attribute[] {
return Object.keys(this.attribs).map((name) => ({
return Object.entries(this.attribs).map(([name, value]) => ({
name,
value: this.attribs[name],
value,
namespace: this["x-attribsNamespace"]?.[name],
prefix: this["x-attribsPrefix"]?.[name],
}));
}

/** Element namespace (parse5 only). */
namespace?: string;
/** Element attribute namespaces (parse5 only). */
"x-attribsNamespace"?: Record<string, string>;
/** Element attribute namespace-related prefixes (parse5 only). */
"x-attribsPrefix"?: Record<string, string>;
}

/**
Expand Down Expand Up @@ -417,18 +417,18 @@ export function hasChildren(node: Node): node is ParentNode {
/**
* Clone a node, and optionally its children.
* @param node Node to clone.
* @param recursive Clone child nodes as well.
* @param shouldRecurse Clone child nodes as well.
* @returns A clone of the node.
*/
export function cloneNode<T extends Node>(node: T, recursive = false): T {
export function cloneNode<T extends Node>(node: T, shouldRecurse = 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 children = shouldRecurse ? cloneChildren(node.children) : [];
const clone = new Element(node.name, { ...node.attribs }, children);
for (const child of children) {
child.parent = clone;
Expand All @@ -446,14 +446,14 @@ export function cloneNode<T extends Node>(node: T, recursive = false): T {

result = clone;
} else if (isCDATA(node)) {
const children = recursive ? cloneChildren(node.children) : [];
const children = shouldRecurse ? cloneChildren(node.children) : [];
const clone = new CDATA(children);
for (const child of children) {
child.parent = clone;
}
result = clone;
} else if (isDocument(node)) {
const children = recursive ? cloneChildren(node.children) : [];
const children = shouldRecurse ? cloneChildren(node.children) : [];
const clone = new Document(children);
for (const child of children) {
child.parent = clone;
Expand Down
Loading