@tkeron/html-parser 1.1.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm_deploy.yml +14 -4
- package/README.md +6 -6
- package/bun.lock +6 -8
- package/check-versions.ts +147 -0
- package/index.ts +4 -8
- package/package.json +5 -6
- package/src/dom-simulator/append-child.ts +130 -0
- package/src/dom-simulator/append.ts +18 -0
- package/src/dom-simulator/attributes.ts +23 -0
- package/src/dom-simulator/clone-node.ts +51 -0
- package/src/dom-simulator/convert-ast-node-to-dom.ts +37 -0
- package/src/dom-simulator/create-cdata.ts +18 -0
- package/src/dom-simulator/create-comment.ts +23 -0
- package/src/dom-simulator/create-doctype.ts +24 -0
- package/src/dom-simulator/create-document.ts +81 -0
- package/src/dom-simulator/create-element.ts +195 -0
- package/src/dom-simulator/create-processing-instruction.ts +19 -0
- package/src/dom-simulator/create-temp-parent.ts +9 -0
- package/src/dom-simulator/create-text-node.ts +23 -0
- package/src/dom-simulator/escape-text-content.ts +6 -0
- package/src/dom-simulator/find-special-elements.ts +14 -0
- package/src/dom-simulator/get-text-content.ts +18 -0
- package/src/dom-simulator/index.ts +36 -0
- package/src/dom-simulator/inner-outer-html.ts +182 -0
- package/src/dom-simulator/insert-after.ts +20 -0
- package/src/dom-simulator/insert-before.ts +108 -0
- package/src/dom-simulator/matches.ts +26 -0
- package/src/dom-simulator/node-types.ts +26 -0
- package/src/dom-simulator/prepend.ts +24 -0
- package/src/dom-simulator/remove-child.ts +68 -0
- package/src/dom-simulator/remove.ts +7 -0
- package/src/dom-simulator/replace-child.ts +152 -0
- package/src/dom-simulator/set-text-content.ts +33 -0
- package/src/dom-simulator/update-element-content.ts +56 -0
- package/src/dom-simulator.ts +12 -1126
- package/src/encoding/constants.ts +8 -0
- package/src/encoding/detect-encoding.ts +21 -0
- package/src/encoding/index.ts +1 -0
- package/src/encoding/normalize-encoding.ts +6 -0
- package/src/html-entities.ts +2127 -0
- package/src/index.ts +5 -5
- package/src/parser/adoption-agency-helpers.ts +145 -0
- package/src/parser/constants.ts +137 -0
- package/src/parser/dom-to-ast.ts +79 -0
- package/src/parser/foster-parenting-helpers.ts +48 -0
- package/src/parser/implicit-table-structure.ts +65 -0
- package/src/parser/index.ts +9 -0
- package/src/parser/parse.ts +924 -0
- package/src/parser/types.ts +56 -0
- package/src/selectors/find-elements-descendant.ts +47 -0
- package/src/selectors/index.ts +2 -0
- package/src/selectors/matches-selector.ts +12 -0
- package/src/selectors/matches-token.ts +27 -0
- package/src/selectors/parse-selector.ts +48 -0
- package/src/selectors/query-selector-all.ts +43 -0
- package/src/selectors/query-selector.ts +6 -0
- package/src/selectors/types.ts +10 -0
- package/src/serializer/attributes.ts +74 -0
- package/src/serializer/escape.ts +13 -0
- package/src/serializer/index.ts +1 -0
- package/src/serializer/serialize-tokens.ts +511 -0
- package/src/tokenizer/calculate-position.ts +10 -0
- package/src/tokenizer/constants.ts +11 -0
- package/src/tokenizer/decode-entities.ts +64 -0
- package/src/tokenizer/index.ts +2 -0
- package/src/tokenizer/parse-attributes.ts +74 -0
- package/src/tokenizer/tokenize.ts +165 -0
- package/src/tokenizer/types.ts +25 -0
- package/tests/adoption-agency-helpers.test.ts +304 -0
- package/tests/advanced.test.ts +242 -221
- package/tests/cloneNode.test.ts +19 -66
- package/tests/custom-elements-head.test.ts +54 -55
- package/tests/dom-extended.test.ts +77 -64
- package/tests/dom-manipulation.test.ts +51 -24
- package/tests/dom.test.ts +15 -13
- package/tests/encoding/detect-encoding.test.ts +33 -0
- package/tests/foster-parenting.test.ts +127 -0
- package/tests/google-dom.test.ts +2 -2
- package/tests/helpers/tokenizer-adapter.test.ts +29 -43
- package/tests/helpers/tokenizer-adapter.ts +36 -33
- package/tests/helpers/tree-adapter.test.ts +20 -20
- package/tests/helpers/tree-adapter.ts +34 -24
- package/tests/html-entities-text.test.ts +6 -2
- package/tests/innerhtml-void-elements.test.ts +52 -36
- package/tests/outerHTML-replacement.test.ts +37 -65
- package/tests/parser/dom-to-ast.test.ts +109 -0
- package/tests/parser/parse.test.ts +139 -0
- package/tests/parser.test.ts +281 -217
- package/tests/selectors/query-selector-all.test.ts +39 -0
- package/tests/selectors/query-selector.test.ts +42 -0
- package/tests/serializer/attributes.test.ts +132 -0
- package/tests/serializer/escape.test.ts +51 -0
- package/tests/serializer/serialize-tokens.test.ts +80 -0
- package/tests/serializer-core.test.ts +6 -6
- package/tests/serializer-injectmeta.test.ts +6 -6
- package/tests/serializer-optionaltags.test.ts +9 -6
- package/tests/serializer-options.test.ts +6 -6
- package/tests/serializer-whitespace.test.ts +6 -6
- package/tests/tokenizer/calculate-position.test.ts +34 -0
- package/tests/tokenizer/decode-entities.test.ts +31 -0
- package/tests/tokenizer/parse-attributes.test.ts +44 -0
- package/tests/tokenizer/tokenize.test.ts +757 -0
- package/tests/tokenizer-namedEntities.test.ts +10 -7
- package/tests/tokenizer-pendingSpecChanges.test.ts +10 -7
- package/tests/tokenizer.test.ts +268 -256
- package/tests/tree-construction-adoption01.test.ts +25 -16
- package/tests/tree-construction-adoption02.test.ts +30 -19
- package/tests/tree-construction-domjs-unsafe.test.ts +6 -4
- package/tests/tree-construction-entities02.test.ts +18 -16
- package/tests/tree-construction-html5test-com.test.ts +16 -10
- package/tests/tree-construction-math.test.ts +11 -9
- package/tests/tree-construction-namespace-sensitivity.test.ts +11 -9
- package/tests/tree-construction-noscript01.test.ts +11 -9
- package/tests/tree-construction-ruby.test.ts +6 -4
- package/tests/tree-construction-scriptdata01.test.ts +6 -4
- package/tests/tree-construction-svg.test.ts +6 -4
- package/tests/tree-construction-template.test.ts +6 -4
- package/tests/tree-construction-tests10.test.ts +6 -4
- package/tests/tree-construction-tests11.test.ts +6 -4
- package/tests/tree-construction-tests20.test.ts +7 -4
- package/tests/tree-construction-tests21.test.ts +7 -4
- package/tests/tree-construction-tests23.test.ts +7 -4
- package/tests/tree-construction-tests24.test.ts +7 -4
- package/tests/tree-construction-tests5.test.ts +6 -5
- package/tests/tree-construction-tests6.test.ts +6 -5
- package/tests/tree-construction-tests_innerHTML_1.test.ts +6 -5
- package/tests/void-elements.test.ts +85 -40
- package/tsconfig.json +1 -1
- package/src/css-selector.ts +0 -185
- package/src/encoding.ts +0 -39
- package/src/parser.ts +0 -682
- package/src/serializer.ts +0 -450
- package/src/tokenizer.ts +0 -325
- package/tests/selectors.test.ts +0 -128
|
@@ -16,9 +16,19 @@ jobs:
|
|
|
16
16
|
|
|
17
17
|
- uses: oven-sh/setup-bun@v2
|
|
18
18
|
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
- name: Install dependencies
|
|
20
|
+
run: bun i
|
|
21
|
+
|
|
22
|
+
- name: Run tests
|
|
23
|
+
run: bun test --concurrent
|
|
24
|
+
|
|
25
|
+
- name: Check version
|
|
26
|
+
id: check
|
|
27
|
+
run: bun check-versions.ts
|
|
28
|
+
continue-on-error: true
|
|
29
|
+
|
|
30
|
+
- name: Publish to npm
|
|
31
|
+
if: steps.check.outcome == 'success'
|
|
32
|
+
run: npm publish --access public
|
|
23
33
|
env:
|
|
24
34
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
# HTML Parser
|
|
1
|
+
# HTML Parser
|
|
2
2
|
|
|
3
|
-
A fast and lightweight HTML parser for Bun that converts HTML strings into DOM Document objects.
|
|
3
|
+
A fast and lightweight HTML parser for Bun that converts HTML strings into DOM Document objects. Built with a custom tokenizer optimized for Bun runtime.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- ⚡ **
|
|
7
|
+
- ⚡ **Custom Tokenizer**: Tokenizer specifically optimized for Bun runtime
|
|
8
8
|
- 🚀 **Ultra Fast**: Leverages Bun's native optimizations
|
|
9
|
-
- 🪶 **Lightweight**:
|
|
9
|
+
- 🪶 **Lightweight**: Zero external dependencies
|
|
10
10
|
- 🌐 **Standards Compliant**: Returns standard DOM Document objects
|
|
11
11
|
- 🔧 **TypeScript Support**: Full TypeScript definitions included
|
|
12
|
-
- ✅ **Well Tested**: Comprehensive test suite (
|
|
13
|
-
-
|
|
12
|
+
- ✅ **Well Tested**: Comprehensive test suite (5600+ tests passing)
|
|
13
|
+
- 🎯 **HTML5 Spec**: Implements Adoption Agency Algorithm for proper formatting element handling
|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
package/bun.lock
CHANGED
|
@@ -4,11 +4,9 @@
|
|
|
4
4
|
"workspaces": {
|
|
5
5
|
"": {
|
|
6
6
|
"name": "@tkeron/html-parser",
|
|
7
|
-
"dependencies": {
|
|
8
|
-
"all-named-html-entities": "^3.1.3",
|
|
9
|
-
},
|
|
10
7
|
"devDependencies": {
|
|
11
|
-
"@types/bun": "^1.3.
|
|
8
|
+
"@types/bun": "^1.3.8",
|
|
9
|
+
"prettier": "^3.8.1",
|
|
12
10
|
},
|
|
13
11
|
"peerDependencies": {
|
|
14
12
|
"typescript": "^5.9.3",
|
|
@@ -16,13 +14,13 @@
|
|
|
16
14
|
},
|
|
17
15
|
},
|
|
18
16
|
"packages": {
|
|
19
|
-
"@types/bun": ["@types/bun@1.3.
|
|
17
|
+
"@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="],
|
|
20
18
|
|
|
21
|
-
"@types/node": ["@types/node@25.0
|
|
19
|
+
"@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
|
|
22
20
|
|
|
23
|
-
"
|
|
21
|
+
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
|
24
22
|
|
|
25
|
-
"
|
|
23
|
+
"prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
|
|
26
24
|
|
|
27
25
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
28
26
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Compares two semver versions including pre-release identifiers
|
|
8
|
+
* Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
|
|
9
|
+
*/
|
|
10
|
+
function compareVersions(v1: string, v2: string): number {
|
|
11
|
+
const parseVersion = (version: string) => {
|
|
12
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
|
|
13
|
+
if (!match) throw new Error(`Invalid version format: ${version}`);
|
|
14
|
+
|
|
15
|
+
const [, major, minor, patch, preRelease] = match;
|
|
16
|
+
if (!major || !minor || !patch)
|
|
17
|
+
throw new Error(`Invalid version format: ${version}`);
|
|
18
|
+
return {
|
|
19
|
+
major: parseInt(major),
|
|
20
|
+
minor: parseInt(minor),
|
|
21
|
+
patch: parseInt(patch),
|
|
22
|
+
preRelease: preRelease || null,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const v1Parts = parseVersion(v1);
|
|
27
|
+
const v2Parts = parseVersion(v2);
|
|
28
|
+
|
|
29
|
+
if (v1Parts.major !== v2Parts.major)
|
|
30
|
+
return v1Parts.major > v2Parts.major ? 1 : -1;
|
|
31
|
+
if (v1Parts.minor !== v2Parts.minor)
|
|
32
|
+
return v1Parts.minor > v2Parts.minor ? 1 : -1;
|
|
33
|
+
if (v1Parts.patch !== v2Parts.patch)
|
|
34
|
+
return v1Parts.patch > v2Parts.patch ? 1 : -1;
|
|
35
|
+
|
|
36
|
+
if (!v1Parts.preRelease && !v2Parts.preRelease) return 0;
|
|
37
|
+
|
|
38
|
+
if (!v1Parts.preRelease) return 1;
|
|
39
|
+
if (!v2Parts.preRelease) return -1;
|
|
40
|
+
|
|
41
|
+
const preReleasePriority: Record<string, number> = {
|
|
42
|
+
alpha: 1,
|
|
43
|
+
beta: 2,
|
|
44
|
+
rc: 3,
|
|
45
|
+
pre: 4,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const parsePreRelease = (pr: string) => {
|
|
49
|
+
const match = pr.match(/^([a-z]+)\.?(\d+)?$/);
|
|
50
|
+
if (!match) return { type: pr, num: 0 };
|
|
51
|
+
const type = match[1];
|
|
52
|
+
if (!type) return { type: pr, num: 0 };
|
|
53
|
+
return {
|
|
54
|
+
type,
|
|
55
|
+
num: match[2] ? parseInt(match[2]) : 0,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const pr1 = parsePreRelease(v1Parts.preRelease);
|
|
60
|
+
const pr2 = parsePreRelease(v2Parts.preRelease);
|
|
61
|
+
|
|
62
|
+
const priority1 = preReleasePriority[pr1.type] || 999;
|
|
63
|
+
const priority2 = preReleasePriority[pr2.type] || 999;
|
|
64
|
+
|
|
65
|
+
if (priority1 !== priority2) return priority1 > priority2 ? 1 : -1;
|
|
66
|
+
if (pr1.num !== pr2.num) return pr1.num > pr2.num ? 1 : -1;
|
|
67
|
+
|
|
68
|
+
return v1Parts.preRelease.localeCompare(v2Parts.preRelease);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function checkVersions() {
|
|
72
|
+
try {
|
|
73
|
+
const packageJsonPath = join(process.cwd(), "package.json");
|
|
74
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
75
|
+
const localVersion = packageJson.version;
|
|
76
|
+
|
|
77
|
+
console.log("📦 Package: @tkeron/html-parser\n");
|
|
78
|
+
console.log("🏠 Local version:", localVersion);
|
|
79
|
+
|
|
80
|
+
const getDistTag = (version: string): string => {
|
|
81
|
+
if (!version.includes("-")) return "latest";
|
|
82
|
+
|
|
83
|
+
const preRelease = version.split("-")[1];
|
|
84
|
+
if (!preRelease) return "latest";
|
|
85
|
+
if (preRelease.startsWith("alpha")) return "alpha";
|
|
86
|
+
if (preRelease.startsWith("beta")) return "beta";
|
|
87
|
+
if (preRelease.startsWith("rc")) return "rc";
|
|
88
|
+
if (preRelease.startsWith("pre")) return "pre";
|
|
89
|
+
|
|
90
|
+
return "latest";
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const distTag = getDistTag(localVersion);
|
|
94
|
+
console.log("🏷️ Comparing against tag:", distTag);
|
|
95
|
+
|
|
96
|
+
const response = await fetch(
|
|
97
|
+
"https://registry.npmjs.org/@tkeron/html-parser",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const data = await response.json();
|
|
105
|
+
const publishedVersion = data["dist-tags"][distTag];
|
|
106
|
+
|
|
107
|
+
if (!publishedVersion) {
|
|
108
|
+
console.log(`\n${"=".repeat(50)}`);
|
|
109
|
+
console.log(`ℹ️ No published version found for tag '${distTag}'`);
|
|
110
|
+
console.log(" → This will be the first version with this tag");
|
|
111
|
+
console.log("=".repeat(50));
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log("📡 Published version:", publishedVersion);
|
|
116
|
+
|
|
117
|
+
const comparison = compareVersions(localVersion, publishedVersion);
|
|
118
|
+
|
|
119
|
+
console.log("\n" + "=".repeat(50));
|
|
120
|
+
|
|
121
|
+
if (comparison > 0) {
|
|
122
|
+
console.log("✅ NEW VERSION DETECTED");
|
|
123
|
+
console.log(` ${localVersion} > ${publishedVersion}`);
|
|
124
|
+
console.log(" → Should publish to npm");
|
|
125
|
+
console.log("=".repeat(50));
|
|
126
|
+
process.exit(0);
|
|
127
|
+
} else if (comparison === 0) {
|
|
128
|
+
console.log("ℹ️ SAME VERSION");
|
|
129
|
+
console.log(` ${localVersion} = ${publishedVersion}`);
|
|
130
|
+
console.log(" → No need to publish");
|
|
131
|
+
console.log("=".repeat(50));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
} else {
|
|
134
|
+
console.log("⚠️ LOCAL VERSION IS OLDER");
|
|
135
|
+
console.log(` ${localVersion} < ${publishedVersion}`);
|
|
136
|
+
console.log(" → Should not publish");
|
|
137
|
+
console.log("=".repeat(50));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
} catch (error: unknown) {
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
console.error("❌ Error:", message);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
checkVersions();
|
package/index.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
import { tokenize } from
|
|
2
|
-
import { parse } from
|
|
3
|
-
import {
|
|
4
|
-
astToDOM,
|
|
5
|
-
} from './src/dom-simulator.js';
|
|
1
|
+
import { tokenize } from "./src/tokenizer/index.js";
|
|
2
|
+
import { parse } from "./src/parser/index.js";
|
|
3
|
+
import { astToDOM } from "./src/dom-simulator.js";
|
|
6
4
|
|
|
7
5
|
export function parseHTML(html: string = ""): Document {
|
|
8
6
|
const tokens = tokenize(html);
|
|
9
7
|
const ast = parse(tokens);
|
|
10
8
|
// If parse already returns a DOM document, return it directly
|
|
11
|
-
if (ast && typeof ast.nodeType ===
|
|
9
|
+
if (ast && typeof ast.nodeType === "number" && ast.nodeType === 9) {
|
|
12
10
|
return ast;
|
|
13
11
|
}
|
|
14
12
|
return astToDOM(ast);
|
|
15
13
|
}
|
|
16
|
-
|
|
17
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tkeron/html-parser",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A fast and lightweight HTML parser for Bun",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
"author": "tkeron",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "bun test --concurrent"
|
|
11
|
+
"test": "bun test --concurrent",
|
|
12
|
+
"lint": "prettier --write ."
|
|
12
13
|
},
|
|
13
14
|
"devDependencies": {
|
|
14
|
-
"@types/bun": "^1.3.
|
|
15
|
+
"@types/bun": "^1.3.8",
|
|
16
|
+
"prettier": "^3.8.1"
|
|
15
17
|
},
|
|
16
18
|
"peerDependencies": {
|
|
17
19
|
"typescript": "^5.9.3"
|
|
@@ -25,8 +27,5 @@
|
|
|
25
27
|
],
|
|
26
28
|
"repository": {
|
|
27
29
|
"url": "git@github.com:tkeron/html-parser.git"
|
|
28
|
-
},
|
|
29
|
-
"dependencies": {
|
|
30
|
-
"all-named-html-entities": "^3.1.3"
|
|
31
30
|
}
|
|
32
31
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { NodeType } from "./node-types.js";
|
|
2
|
+
import { updateElementContent } from "./update-element-content.js";
|
|
3
|
+
|
|
4
|
+
const removeChild = (parent: any, child: any): void => {
|
|
5
|
+
const index = parent.childNodes.indexOf(child);
|
|
6
|
+
if (index === -1) return;
|
|
7
|
+
|
|
8
|
+
parent.childNodes.splice(index, 1);
|
|
9
|
+
|
|
10
|
+
if (child.previousSibling) {
|
|
11
|
+
child.previousSibling.nextSibling = child.nextSibling;
|
|
12
|
+
}
|
|
13
|
+
if (child.nextSibling) {
|
|
14
|
+
child.nextSibling.previousSibling = child.previousSibling;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (parent.firstChild === child) {
|
|
18
|
+
parent.firstChild = child.nextSibling;
|
|
19
|
+
}
|
|
20
|
+
if (parent.lastChild === child) {
|
|
21
|
+
parent.lastChild = child.previousSibling;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
child.parentNode = null;
|
|
25
|
+
child.nextSibling = null;
|
|
26
|
+
child.previousSibling = null;
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
parent.nodeType === NodeType.ELEMENT_NODE &&
|
|
30
|
+
child.nodeType === NodeType.ELEMENT_NODE
|
|
31
|
+
) {
|
|
32
|
+
const parentElement = parent;
|
|
33
|
+
const childElement = child;
|
|
34
|
+
|
|
35
|
+
const elementIndex = parentElement.children.indexOf(childElement);
|
|
36
|
+
if (elementIndex !== -1) {
|
|
37
|
+
parentElement.children.splice(elementIndex, 1);
|
|
38
|
+
|
|
39
|
+
if (childElement.previousElementSibling) {
|
|
40
|
+
childElement.previousElementSibling.nextElementSibling =
|
|
41
|
+
childElement.nextElementSibling;
|
|
42
|
+
}
|
|
43
|
+
if (childElement.nextElementSibling) {
|
|
44
|
+
childElement.nextElementSibling.previousElementSibling =
|
|
45
|
+
childElement.previousElementSibling;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (parentElement.firstElementChild === childElement) {
|
|
49
|
+
parentElement.firstElementChild = childElement.nextElementSibling;
|
|
50
|
+
}
|
|
51
|
+
if (parentElement.lastElementChild === childElement) {
|
|
52
|
+
parentElement.lastElementChild = childElement.previousElementSibling;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
childElement.parentElement = null;
|
|
56
|
+
childElement.nextElementSibling = null;
|
|
57
|
+
childElement.previousElementSibling = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (parent.nodeType === NodeType.ELEMENT_NODE) {
|
|
62
|
+
updateElementContent(parent);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const appendChild = (parent: any, child: any): void => {
|
|
67
|
+
if (
|
|
68
|
+
child.nodeType === NodeType.ELEMENT_NODE ||
|
|
69
|
+
child.nodeType === NodeType.DOCUMENT_NODE
|
|
70
|
+
) {
|
|
71
|
+
let ancestor = parent;
|
|
72
|
+
while (ancestor) {
|
|
73
|
+
if (ancestor === child) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"HierarchyRequestError: Cannot insert a node as a descendant of itself",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
ancestor = ancestor.parentNode;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (child.parentNode) {
|
|
83
|
+
removeChild(child.parentNode, child);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
child.parentNode = parent;
|
|
87
|
+
parent.childNodes.push(child);
|
|
88
|
+
|
|
89
|
+
if (parent.childNodes.length > 1) {
|
|
90
|
+
const previousSibling = parent.childNodes[parent.childNodes.length - 2];
|
|
91
|
+
if (previousSibling) {
|
|
92
|
+
previousSibling.nextSibling = child;
|
|
93
|
+
child.previousSibling = previousSibling;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (parent.childNodes.length === 1) {
|
|
98
|
+
parent.firstChild = child;
|
|
99
|
+
}
|
|
100
|
+
parent.lastChild = child;
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
parent.nodeType === NodeType.ELEMENT_NODE &&
|
|
104
|
+
child.nodeType === NodeType.ELEMENT_NODE
|
|
105
|
+
) {
|
|
106
|
+
const parentElement = parent;
|
|
107
|
+
const childElement = child;
|
|
108
|
+
|
|
109
|
+
childElement.parentElement = parentElement;
|
|
110
|
+
parentElement.children.push(childElement);
|
|
111
|
+
|
|
112
|
+
if (parentElement.children.length === 1) {
|
|
113
|
+
parentElement.firstElementChild = childElement;
|
|
114
|
+
}
|
|
115
|
+
parentElement.lastElementChild = childElement;
|
|
116
|
+
|
|
117
|
+
if (parentElement.children.length > 1) {
|
|
118
|
+
const previousElementSibling =
|
|
119
|
+
parentElement.children[parentElement.children.length - 2];
|
|
120
|
+
if (previousElementSibling) {
|
|
121
|
+
previousElementSibling.nextElementSibling = childElement;
|
|
122
|
+
childElement.previousElementSibling = previousElementSibling;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (parent.nodeType === NodeType.ELEMENT_NODE) {
|
|
128
|
+
updateElementContent(parent);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createTextNode } from "./create-text-node.js";
|
|
2
|
+
import { appendChild } from "./append-child.js";
|
|
3
|
+
|
|
4
|
+
export const append = (parent: any, ...nodes: any[]): void => {
|
|
5
|
+
if (nodes.length === 0) return;
|
|
6
|
+
|
|
7
|
+
for (const node of nodes) {
|
|
8
|
+
let childNode: any;
|
|
9
|
+
|
|
10
|
+
if (typeof node === "string") {
|
|
11
|
+
childNode = createTextNode(node);
|
|
12
|
+
} else {
|
|
13
|
+
childNode = node;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
appendChild(parent, childNode);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { updateElementContent } from "./update-element-content.js";
|
|
2
|
+
|
|
3
|
+
export const getAttribute = (element: any, name: string): string | null => {
|
|
4
|
+
return element.attributes[name] || null;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const hasAttribute = (element: any, name: string): boolean => {
|
|
8
|
+
return name in element.attributes;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const setAttribute = (
|
|
12
|
+
element: any,
|
|
13
|
+
name: string,
|
|
14
|
+
value: string,
|
|
15
|
+
): void => {
|
|
16
|
+
element.attributes[name] = value;
|
|
17
|
+
updateElementContent(element);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const removeAttribute = (element: any, name: string): void => {
|
|
21
|
+
delete element.attributes[name];
|
|
22
|
+
updateElementContent(element);
|
|
23
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { NodeType } from "./node-types.js";
|
|
2
|
+
import { createElement } from "./index.js";
|
|
3
|
+
|
|
4
|
+
const cloneNode = (node: any, deep: boolean = false): any => {
|
|
5
|
+
if (node.nodeType === NodeType.ELEMENT_NODE) {
|
|
6
|
+
const cloned = createElement(node.tagName, node.attributes);
|
|
7
|
+
|
|
8
|
+
if (deep) {
|
|
9
|
+
for (const child of node.childNodes) {
|
|
10
|
+
const clonedChild = cloneNode(child, true);
|
|
11
|
+
cloned.appendChild(clonedChild);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return cloned;
|
|
16
|
+
} else if (node.nodeType === NodeType.TEXT_NODE) {
|
|
17
|
+
return {
|
|
18
|
+
nodeType: NodeType.TEXT_NODE,
|
|
19
|
+
textContent: node.textContent,
|
|
20
|
+
parentNode: null,
|
|
21
|
+
parentElement: null,
|
|
22
|
+
previousSibling: null,
|
|
23
|
+
nextSibling: null,
|
|
24
|
+
};
|
|
25
|
+
} else if (node.nodeType === NodeType.COMMENT_NODE) {
|
|
26
|
+
return {
|
|
27
|
+
nodeType: NodeType.COMMENT_NODE,
|
|
28
|
+
data: node.data,
|
|
29
|
+
textContent: node.textContent,
|
|
30
|
+
parentNode: null,
|
|
31
|
+
parentElement: null,
|
|
32
|
+
previousSibling: null,
|
|
33
|
+
nextSibling: null,
|
|
34
|
+
};
|
|
35
|
+
} else if (node.nodeType === NodeType.DOCUMENT_TYPE_NODE) {
|
|
36
|
+
return {
|
|
37
|
+
nodeType: NodeType.DOCUMENT_TYPE_NODE,
|
|
38
|
+
name: node.name,
|
|
39
|
+
publicId: node.publicId,
|
|
40
|
+
systemId: node.systemId,
|
|
41
|
+
parentNode: null,
|
|
42
|
+
parentElement: null,
|
|
43
|
+
previousSibling: null,
|
|
44
|
+
nextSibling: null,
|
|
45
|
+
};
|
|
46
|
+
} else {
|
|
47
|
+
throw new Error(`Unsupported node type: ${node.nodeType}`);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export { cloneNode };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ASTNode } from "../parser/index";
|
|
2
|
+
import { createElement } from "./create-element.js";
|
|
3
|
+
import { createTextNode } from "./create-text-node.js";
|
|
4
|
+
import { createComment } from "./create-comment.js";
|
|
5
|
+
import { appendChild } from "./append-child.js";
|
|
6
|
+
import { updateElementContent } from "./update-element-content.js";
|
|
7
|
+
|
|
8
|
+
export const convertASTNodeToDOM = (astNode: ASTNode): any => {
|
|
9
|
+
switch (astNode.type) {
|
|
10
|
+
case "ELEMENT":
|
|
11
|
+
const tagName = astNode.tagName || "div";
|
|
12
|
+
const element = createElement(tagName, astNode.attributes || {});
|
|
13
|
+
|
|
14
|
+
if (astNode.children) {
|
|
15
|
+
for (const child of astNode.children) {
|
|
16
|
+
const domChild = convertASTNodeToDOM(child);
|
|
17
|
+
if (domChild) {
|
|
18
|
+
appendChild(element, domChild);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
updateElementContent(element);
|
|
24
|
+
return element;
|
|
25
|
+
case "TEXT":
|
|
26
|
+
return createTextNode(astNode.content || "");
|
|
27
|
+
case "COMMENT":
|
|
28
|
+
return createComment(astNode.content || "");
|
|
29
|
+
case "CDATA":
|
|
30
|
+
return createTextNode(astNode.content || "");
|
|
31
|
+
case "DOCTYPE":
|
|
32
|
+
case "PROCESSING_INSTRUCTION":
|
|
33
|
+
return null;
|
|
34
|
+
default:
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NodeType } from "./node-types.js";
|
|
2
|
+
|
|
3
|
+
export const createCDATA = (content: string): any => {
|
|
4
|
+
const cdataNode: any = {
|
|
5
|
+
nodeType: NodeType.CDATA_SECTION_NODE,
|
|
6
|
+
nodeName: "#cdata-section",
|
|
7
|
+
nodeValue: content,
|
|
8
|
+
textContent: content,
|
|
9
|
+
data: content,
|
|
10
|
+
childNodes: [],
|
|
11
|
+
parentNode: null,
|
|
12
|
+
firstChild: null,
|
|
13
|
+
lastChild: null,
|
|
14
|
+
nextSibling: null,
|
|
15
|
+
previousSibling: null,
|
|
16
|
+
};
|
|
17
|
+
return cdataNode;
|
|
18
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NodeType } from "./node-types.js";
|
|
2
|
+
import { remove } from "./index.js";
|
|
3
|
+
|
|
4
|
+
export const createComment = (content: string): any => {
|
|
5
|
+
const commentNode: any = {
|
|
6
|
+
nodeType: NodeType.COMMENT_NODE,
|
|
7
|
+
nodeName: "#comment",
|
|
8
|
+
nodeValue: content,
|
|
9
|
+
textContent: content,
|
|
10
|
+
data: content,
|
|
11
|
+
childNodes: [],
|
|
12
|
+
parentNode: null,
|
|
13
|
+
firstChild: null,
|
|
14
|
+
lastChild: null,
|
|
15
|
+
nextSibling: null,
|
|
16
|
+
previousSibling: null,
|
|
17
|
+
|
|
18
|
+
remove(): void {
|
|
19
|
+
remove(commentNode);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
return commentNode;
|
|
23
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NodeType } from "./node-types.js";
|
|
2
|
+
|
|
3
|
+
export const createDoctype = (
|
|
4
|
+
name: string = "html",
|
|
5
|
+
publicId?: string,
|
|
6
|
+
systemId?: string,
|
|
7
|
+
): any => {
|
|
8
|
+
const doctypeNode: any = {
|
|
9
|
+
nodeType: NodeType.DOCUMENT_TYPE_NODE,
|
|
10
|
+
nodeName: name.toUpperCase(),
|
|
11
|
+
name: name.toLowerCase(),
|
|
12
|
+
nodeValue: null,
|
|
13
|
+
textContent: "",
|
|
14
|
+
publicId: publicId || null,
|
|
15
|
+
systemId: systemId || null,
|
|
16
|
+
childNodes: [],
|
|
17
|
+
parentNode: null,
|
|
18
|
+
firstChild: null,
|
|
19
|
+
lastChild: null,
|
|
20
|
+
nextSibling: null,
|
|
21
|
+
previousSibling: null,
|
|
22
|
+
};
|
|
23
|
+
return doctypeNode;
|
|
24
|
+
};
|