@tkeron/html-parser 1.1.1 → 1.3.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.
Files changed (132) hide show
  1. package/.github/workflows/npm_deploy.yml +14 -4
  2. package/README.md +6 -6
  3. package/bun.lock +6 -8
  4. package/check-versions.ts +147 -0
  5. package/index.ts +4 -8
  6. package/package.json +5 -6
  7. package/src/dom-simulator/append-child.ts +130 -0
  8. package/src/dom-simulator/append.ts +18 -0
  9. package/src/dom-simulator/attributes.ts +23 -0
  10. package/src/dom-simulator/clone-node.ts +51 -0
  11. package/src/dom-simulator/convert-ast-node-to-dom.ts +37 -0
  12. package/src/dom-simulator/create-cdata.ts +18 -0
  13. package/src/dom-simulator/create-comment.ts +23 -0
  14. package/src/dom-simulator/create-doctype.ts +24 -0
  15. package/src/dom-simulator/create-document.ts +81 -0
  16. package/src/dom-simulator/create-element.ts +195 -0
  17. package/src/dom-simulator/create-processing-instruction.ts +19 -0
  18. package/src/dom-simulator/create-temp-parent.ts +9 -0
  19. package/src/dom-simulator/create-text-node.ts +23 -0
  20. package/src/dom-simulator/escape-text-content.ts +6 -0
  21. package/src/dom-simulator/find-special-elements.ts +14 -0
  22. package/src/dom-simulator/get-text-content.ts +18 -0
  23. package/src/dom-simulator/index.ts +36 -0
  24. package/src/dom-simulator/inner-outer-html.ts +182 -0
  25. package/src/dom-simulator/insert-after.ts +20 -0
  26. package/src/dom-simulator/insert-before.ts +108 -0
  27. package/src/dom-simulator/matches.ts +26 -0
  28. package/src/dom-simulator/node-types.ts +26 -0
  29. package/src/dom-simulator/prepend.ts +24 -0
  30. package/src/dom-simulator/remove-child.ts +68 -0
  31. package/src/dom-simulator/remove.ts +7 -0
  32. package/src/dom-simulator/replace-child.ts +152 -0
  33. package/src/dom-simulator/set-text-content.ts +33 -0
  34. package/src/dom-simulator/update-element-content.ts +56 -0
  35. package/src/dom-simulator.ts +12 -1126
  36. package/src/encoding/constants.ts +8 -0
  37. package/src/encoding/detect-encoding.ts +21 -0
  38. package/src/encoding/index.ts +1 -0
  39. package/src/encoding/normalize-encoding.ts +6 -0
  40. package/src/html-entities.ts +2127 -0
  41. package/src/index.ts +5 -5
  42. package/src/parser/adoption-agency-helpers.ts +145 -0
  43. package/src/parser/constants.ts +137 -0
  44. package/src/parser/dom-to-ast.ts +79 -0
  45. package/src/parser/index.ts +9 -0
  46. package/src/parser/parse.ts +772 -0
  47. package/src/parser/types.ts +56 -0
  48. package/src/selectors/find-elements-descendant.ts +47 -0
  49. package/src/selectors/index.ts +2 -0
  50. package/src/selectors/matches-selector.ts +12 -0
  51. package/src/selectors/matches-token.ts +27 -0
  52. package/src/selectors/parse-selector.ts +48 -0
  53. package/src/selectors/query-selector-all.ts +43 -0
  54. package/src/selectors/query-selector.ts +6 -0
  55. package/src/selectors/types.ts +10 -0
  56. package/src/serializer/attributes.ts +74 -0
  57. package/src/serializer/escape.ts +13 -0
  58. package/src/serializer/index.ts +1 -0
  59. package/src/serializer/serialize-tokens.ts +511 -0
  60. package/src/tokenizer/calculate-position.ts +10 -0
  61. package/src/tokenizer/constants.ts +11 -0
  62. package/src/tokenizer/decode-entities.ts +64 -0
  63. package/src/tokenizer/index.ts +2 -0
  64. package/src/tokenizer/parse-attributes.ts +74 -0
  65. package/src/tokenizer/tokenize.ts +165 -0
  66. package/src/tokenizer/types.ts +25 -0
  67. package/tests/adoption-agency-helpers.test.ts +304 -0
  68. package/tests/advanced.test.ts +242 -221
  69. package/tests/cloneNode.test.ts +19 -66
  70. package/tests/custom-elements-head.test.ts +54 -55
  71. package/tests/dom-extended.test.ts +77 -64
  72. package/tests/dom-manipulation.test.ts +51 -24
  73. package/tests/dom.test.ts +15 -13
  74. package/tests/edge-cases.test.ts +300 -174
  75. package/tests/encoding/detect-encoding.test.ts +33 -0
  76. package/tests/google-dom.test.ts +2 -2
  77. package/tests/helpers/tokenizer-adapter.test.ts +29 -43
  78. package/tests/helpers/tokenizer-adapter.ts +36 -33
  79. package/tests/helpers/tree-adapter.test.ts +20 -20
  80. package/tests/helpers/tree-adapter.ts +34 -24
  81. package/tests/html-entities-text.test.ts +71 -0
  82. package/tests/innerhtml-void-elements.test.ts +52 -36
  83. package/tests/outerHTML-replacement.test.ts +37 -65
  84. package/tests/parser/dom-to-ast.test.ts +109 -0
  85. package/tests/parser/parse.test.ts +139 -0
  86. package/tests/parser.test.ts +281 -217
  87. package/tests/selectors/query-selector-all.test.ts +39 -0
  88. package/tests/selectors/query-selector.test.ts +42 -0
  89. package/tests/serializer/attributes.test.ts +132 -0
  90. package/tests/serializer/escape.test.ts +51 -0
  91. package/tests/serializer/serialize-tokens.test.ts +80 -0
  92. package/tests/serializer-core.test.ts +6 -6
  93. package/tests/serializer-injectmeta.test.ts +6 -6
  94. package/tests/serializer-optionaltags.test.ts +9 -6
  95. package/tests/serializer-options.test.ts +6 -6
  96. package/tests/serializer-whitespace.test.ts +6 -6
  97. package/tests/tokenizer/calculate-position.test.ts +34 -0
  98. package/tests/tokenizer/decode-entities.test.ts +31 -0
  99. package/tests/tokenizer/parse-attributes.test.ts +44 -0
  100. package/tests/tokenizer/tokenize.test.ts +757 -0
  101. package/tests/tokenizer-namedEntities.test.ts +10 -7
  102. package/tests/tokenizer-pendingSpecChanges.test.ts +10 -7
  103. package/tests/tokenizer.test.ts +268 -256
  104. package/tests/tree-construction-adoption01.test.ts +25 -16
  105. package/tests/tree-construction-adoption02.test.ts +30 -19
  106. package/tests/tree-construction-domjs-unsafe.test.ts +7 -5
  107. package/tests/tree-construction-entities02.test.ts +18 -16
  108. package/tests/tree-construction-html5test-com.test.ts +16 -10
  109. package/tests/tree-construction-math.test.ts +11 -9
  110. package/tests/tree-construction-namespace-sensitivity.test.ts +11 -9
  111. package/tests/tree-construction-noscript01.test.ts +11 -9
  112. package/tests/tree-construction-ruby.test.ts +6 -4
  113. package/tests/tree-construction-scriptdata01.test.ts +6 -4
  114. package/tests/tree-construction-svg.test.ts +6 -4
  115. package/tests/tree-construction-template.test.ts +6 -4
  116. package/tests/tree-construction-tests10.test.ts +6 -4
  117. package/tests/tree-construction-tests11.test.ts +6 -4
  118. package/tests/tree-construction-tests20.test.ts +7 -4
  119. package/tests/tree-construction-tests21.test.ts +7 -4
  120. package/tests/tree-construction-tests23.test.ts +7 -4
  121. package/tests/tree-construction-tests24.test.ts +7 -4
  122. package/tests/tree-construction-tests5.test.ts +6 -5
  123. package/tests/tree-construction-tests6.test.ts +6 -5
  124. package/tests/tree-construction-tests_innerHTML_1.test.ts +6 -5
  125. package/tests/void-elements.test.ts +85 -40
  126. package/tsconfig.json +1 -1
  127. package/src/css-selector.ts +0 -185
  128. package/src/encoding.ts +0 -39
  129. package/src/parser.ts +0 -682
  130. package/src/serializer.ts +0 -450
  131. package/src/tokenizer.ts +0 -325
  132. 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
- - run: |
20
- bun i
21
- bun test
22
- npm publish --access public
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 - Powered by Bun Native Tokenizer
1
+ # HTML Parser
2
2
 
3
- A fast and lightweight HTML parser for Bun that converts HTML strings into DOM Document objects. **Now powered by a native Bun tokenizer** for optimal performance.
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
- - ⚡ **Bun Native Tokenizer**: Optimized specifically for Bun runtime
7
+ - ⚡ **Custom Tokenizer**: Tokenizer specifically optimized for Bun runtime
8
8
  - 🚀 **Ultra Fast**: Leverages Bun's native optimizations
9
- - 🪶 **Lightweight**: Minimal dependencies, native implementation
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 (5200+ tests passing)
13
- - 🔄 **100% Compatible**: Drop-in replacement, same API
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.6",
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.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
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.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
19
+ "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
22
20
 
23
- "all-named-html-entities": ["all-named-html-entities@3.1.3", "", {}, "sha512-eG7/XkhxyIUWApWvhVPcusxZ3PTebJo1AvkFkQj7MDSkBYmzXZsNadKZWuo1UxEX6QrE7y7JQx7G3Fx0YjVtnA=="],
21
+ "bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
24
22
 
25
- "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
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 './src/tokenizer.js';
2
- import { parse } from './src/parser.js';
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 === 'number' && ast.nodeType === 9) {
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.1.1",
3
+ "version": "1.3.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.6"
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
+ };