@linxiraos/pi-utils 1.0.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 (244) hide show
  1. package/CHANGELOG.md +449 -0
  2. package/README.md +36 -0
  3. package/dist/types/abortable.d.ts +32 -0
  4. package/dist/types/acp/connection.d.ts +118 -0
  5. package/dist/types/acp/protocol.d.ts +526 -0
  6. package/dist/types/acp/schema.d.ts +41 -0
  7. package/dist/types/acp/stream.d.ts +8 -0
  8. package/dist/types/acp/transport.d.ts +81 -0
  9. package/dist/types/acp.d.ts +6 -0
  10. package/dist/types/async.d.ts +20 -0
  11. package/dist/types/binary.d.ts +17 -0
  12. package/dist/types/browsers.d.ts +68 -0
  13. package/dist/types/chalk.d.ts +125 -0
  14. package/dist/types/cli.d.ts +134 -0
  15. package/dist/types/color.d.ts +102 -0
  16. package/dist/types/dates.d.ts +7 -0
  17. package/dist/types/dirs.d.ts +282 -0
  18. package/dist/types/docx/converter.d.ts +46 -0
  19. package/dist/types/docx/xml.d.ts +26 -0
  20. package/dist/types/docx/zip.d.ts +6 -0
  21. package/dist/types/docx.d.ts +11 -0
  22. package/dist/types/dom/core.d.ts +431 -0
  23. package/dist/types/dom/parser.d.ts +7 -0
  24. package/dist/types/dom/selector.d.ts +5 -0
  25. package/dist/types/dom.d.ts +5 -0
  26. package/dist/types/env.d.ts +124 -0
  27. package/dist/types/fetch-retry.d.ts +96 -0
  28. package/dist/types/file-lock.d.ts +23 -0
  29. package/dist/types/format.d.ts +37 -0
  30. package/dist/types/frontmatter.d.ts +46 -0
  31. package/dist/types/fs-error.d.ts +31 -0
  32. package/dist/types/glob.d.ts +28 -0
  33. package/dist/types/headers.d.ts +34 -0
  34. package/dist/types/index.d.ts +39 -0
  35. package/dist/types/json-parse.d.ts +74 -0
  36. package/dist/types/json.d.ts +14 -0
  37. package/dist/types/logger/rotating-file.d.ts +18 -0
  38. package/dist/types/logger.d.ts +96 -0
  39. package/dist/types/loop-phase.d.ts +10 -0
  40. package/dist/types/lru.d.ts +46 -0
  41. package/dist/types/marked/core.d.ts +445 -0
  42. package/dist/types/marked.d.ts +2 -0
  43. package/dist/types/mermaid-ascii.d.ts +11 -0
  44. package/dist/types/mime.d.ts +29 -0
  45. package/dist/types/module-timer.d.ts +1 -0
  46. package/dist/types/path-tree.d.ts +76 -0
  47. package/dist/types/path.d.ts +2 -0
  48. package/dist/types/peek-file.d.ts +29 -0
  49. package/dist/types/postmortem.d.ts +104 -0
  50. package/dist/types/process-name.d.ts +7 -0
  51. package/dist/types/procmgr.d.ts +74 -0
  52. package/dist/types/prompt.d.ts +18 -0
  53. package/dist/types/ptree.d.ts +110 -0
  54. package/dist/types/readability/readability.d.ts +9 -0
  55. package/dist/types/readability/readerable.d.ts +10 -0
  56. package/dist/types/readability/types.d.ts +70 -0
  57. package/dist/types/readability.d.ts +4 -0
  58. package/dist/types/ring.d.ts +93 -0
  59. package/dist/types/runtime-install.d.ts +78 -0
  60. package/dist/types/sanitize-text.d.ts +29 -0
  61. package/dist/types/snowflake.d.ts +25 -0
  62. package/dist/types/stderr-guard.d.ts +22 -0
  63. package/dist/types/stream.d.ts +67 -0
  64. package/dist/types/tab-spacing.d.ts +24 -0
  65. package/dist/types/temp.d.ts +17 -0
  66. package/dist/types/template.d.ts +62 -0
  67. package/dist/types/timing-buffer.d.ts +22 -0
  68. package/dist/types/tls-fetch.d.ts +37 -0
  69. package/dist/types/turndown/gfm.d.ts +11 -0
  70. package/dist/types/turndown/html.d.ts +5 -0
  71. package/dist/types/turndown/service.d.ts +21 -0
  72. package/dist/types/turndown/types.d.ts +70 -0
  73. package/dist/types/turndown.d.ts +4 -0
  74. package/dist/types/type-guards.d.ts +5 -0
  75. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +41 -0
  76. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +89 -0
  77. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +7 -0
  78. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +12 -0
  79. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +66 -0
  80. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +48 -0
  81. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +43 -0
  82. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +7 -0
  83. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +56 -0
  84. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +65 -0
  85. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +27 -0
  86. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +17 -0
  87. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +7 -0
  88. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +11 -0
  89. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +34 -0
  90. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +11 -0
  91. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +11 -0
  92. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +26 -0
  93. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +31 -0
  94. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +11 -0
  95. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +59 -0
  96. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +17 -0
  97. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +30 -0
  98. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +55 -0
  99. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +206 -0
  100. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +51 -0
  101. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +2 -0
  102. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +6 -0
  103. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +102 -0
  104. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +6 -0
  105. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +76 -0
  106. package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -0
  107. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +9 -0
  108. package/dist/types/vendor/mermaid-ascii/parser.d.ts +7 -0
  109. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +6 -0
  110. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +130 -0
  111. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +21 -0
  112. package/dist/types/vendor/mermaid-ascii/types.d.ts +114 -0
  113. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +25 -0
  114. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +6 -0
  115. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +145 -0
  116. package/dist/types/version.d.ts +18 -0
  117. package/dist/types/vterm/buffer.d.ts +99 -0
  118. package/dist/types/vterm/terminal.d.ts +44 -0
  119. package/dist/types/vterm.d.ts +8 -0
  120. package/dist/types/which.d.ts +37 -0
  121. package/dist/types/worker-host.d.ts +51 -0
  122. package/dist/types/xml.d.ts +31 -0
  123. package/package.json +59 -0
  124. package/src/abortable.ts +109 -0
  125. package/src/acp/connection.ts +344 -0
  126. package/src/acp/protocol.ts +466 -0
  127. package/src/acp/schema.ts +160 -0
  128. package/src/acp/stream.ts +82 -0
  129. package/src/acp/transport.ts +213 -0
  130. package/src/acp.ts +6 -0
  131. package/src/async.ts +91 -0
  132. package/src/binary.ts +50 -0
  133. package/src/browsers.ts +501 -0
  134. package/src/chalk.ts +312 -0
  135. package/src/cli.ts +529 -0
  136. package/src/color.ts +302 -0
  137. package/src/dates.ts +194 -0
  138. package/src/dirs.ts +1026 -0
  139. package/src/docx/converter.ts +681 -0
  140. package/src/docx/xml.ts +166 -0
  141. package/src/docx/zip.ts +87 -0
  142. package/src/docx.ts +20 -0
  143. package/src/dom/core.ts +1254 -0
  144. package/src/dom/parser.ts +370 -0
  145. package/src/dom/selector.ts +290 -0
  146. package/src/dom.ts +33 -0
  147. package/src/env.ts +400 -0
  148. package/src/fetch-retry.ts +379 -0
  149. package/src/file-lock.ts +69 -0
  150. package/src/format.ts +113 -0
  151. package/src/frontmatter.ts +205 -0
  152. package/src/fs-error.ts +56 -0
  153. package/src/glob.ts +189 -0
  154. package/src/headers.ts +167 -0
  155. package/src/index.ts +59 -0
  156. package/src/json-parse.ts +821 -0
  157. package/src/json.ts +23 -0
  158. package/src/logger/rotating-file.ts +149 -0
  159. package/src/logger.ts +798 -0
  160. package/src/loop-phase.ts +49 -0
  161. package/src/lru.ts +185 -0
  162. package/src/marked/core.ts +1576 -0
  163. package/src/marked.ts +2 -0
  164. package/src/mermaid-ascii.ts +31 -0
  165. package/src/mime.ts +159 -0
  166. package/src/module-timer.ts +148 -0
  167. package/src/path-tree.ts +147 -0
  168. package/src/path.ts +28 -0
  169. package/src/peek-file.ts +188 -0
  170. package/src/postmortem.ts +433 -0
  171. package/src/process-name.ts +57 -0
  172. package/src/procmgr.ts +247 -0
  173. package/src/prompt.ts +545 -0
  174. package/src/ptree.ts +412 -0
  175. package/src/readability/readability.ts +533 -0
  176. package/src/readability/readerable.ts +51 -0
  177. package/src/readability/types.ts +72 -0
  178. package/src/readability.ts +11 -0
  179. package/src/ring.ts +169 -0
  180. package/src/runtime-install.ts +393 -0
  181. package/src/sanitize-text.ts +96 -0
  182. package/src/snowflake.ts +121 -0
  183. package/src/stderr-guard.ts +155 -0
  184. package/src/stream.ts +454 -0
  185. package/src/tab-spacing.ts +336 -0
  186. package/src/temp.ts +134 -0
  187. package/src/template.ts +586 -0
  188. package/src/timing-buffer.ts +47 -0
  189. package/src/tls-fetch.ts +178 -0
  190. package/src/turndown/gfm.ts +106 -0
  191. package/src/turndown/html.ts +257 -0
  192. package/src/turndown/service.ts +334 -0
  193. package/src/turndown/types.ts +81 -0
  194. package/src/turndown.ts +5 -0
  195. package/src/type-guards.ts +17 -0
  196. package/src/vendor/mermaid-ascii/NOTICE +33 -0
  197. package/src/vendor/mermaid-ascii/ascii/ansi.ts +409 -0
  198. package/src/vendor/mermaid-ascii/ascii/canvas.ts +476 -0
  199. package/src/vendor/mermaid-ascii/ascii/class-diagram.ts +699 -0
  200. package/src/vendor/mermaid-ascii/ascii/converter.ts +271 -0
  201. package/src/vendor/mermaid-ascii/ascii/draw.ts +1382 -0
  202. package/src/vendor/mermaid-ascii/ascii/edge-bundling.ts +328 -0
  203. package/src/vendor/mermaid-ascii/ascii/edge-routing.ts +297 -0
  204. package/src/vendor/mermaid-ascii/ascii/er-diagram.ts +441 -0
  205. package/src/vendor/mermaid-ascii/ascii/grid.ts +578 -0
  206. package/src/vendor/mermaid-ascii/ascii/index.ts +187 -0
  207. package/src/vendor/mermaid-ascii/ascii/multiline-utils.ts +78 -0
  208. package/src/vendor/mermaid-ascii/ascii/pathfinder.ts +277 -0
  209. package/src/vendor/mermaid-ascii/ascii/sequence.ts +460 -0
  210. package/src/vendor/mermaid-ascii/ascii/shapes/circle.ts +27 -0
  211. package/src/vendor/mermaid-ascii/ascii/shapes/corners.ts +127 -0
  212. package/src/vendor/mermaid-ascii/ascii/shapes/diamond.ts +27 -0
  213. package/src/vendor/mermaid-ascii/ascii/shapes/hexagon.ts +27 -0
  214. package/src/vendor/mermaid-ascii/ascii/shapes/index.ts +101 -0
  215. package/src/vendor/mermaid-ascii/ascii/shapes/rectangle.ts +175 -0
  216. package/src/vendor/mermaid-ascii/ascii/shapes/rounded.ts +27 -0
  217. package/src/vendor/mermaid-ascii/ascii/shapes/special.ts +296 -0
  218. package/src/vendor/mermaid-ascii/ascii/shapes/stadium.ts +114 -0
  219. package/src/vendor/mermaid-ascii/ascii/shapes/state.ts +192 -0
  220. package/src/vendor/mermaid-ascii/ascii/shapes/types.ts +73 -0
  221. package/src/vendor/mermaid-ascii/ascii/types.ts +273 -0
  222. package/src/vendor/mermaid-ascii/ascii/validate.ts +120 -0
  223. package/src/vendor/mermaid-ascii/ascii/xychart.ts +875 -0
  224. package/src/vendor/mermaid-ascii/class/parser.ts +290 -0
  225. package/src/vendor/mermaid-ascii/class/types.ts +121 -0
  226. package/src/vendor/mermaid-ascii/er/parser.ts +181 -0
  227. package/src/vendor/mermaid-ascii/er/types.ts +91 -0
  228. package/src/vendor/mermaid-ascii/index.ts +14 -0
  229. package/src/vendor/mermaid-ascii/multiline-utils.ts +30 -0
  230. package/src/vendor/mermaid-ascii/parser.ts +645 -0
  231. package/src/vendor/mermaid-ascii/sequence/parser.ts +207 -0
  232. package/src/vendor/mermaid-ascii/sequence/types.ts +146 -0
  233. package/src/vendor/mermaid-ascii/text-metrics.ts +71 -0
  234. package/src/vendor/mermaid-ascii/types.ts +164 -0
  235. package/src/vendor/mermaid-ascii/xychart/colors.ts +140 -0
  236. package/src/vendor/mermaid-ascii/xychart/parser.ts +115 -0
  237. package/src/vendor/mermaid-ascii/xychart/types.ts +150 -0
  238. package/src/version.ts +99 -0
  239. package/src/vterm/buffer.ts +218 -0
  240. package/src/vterm/terminal.ts +773 -0
  241. package/src/vterm.ts +8 -0
  242. package/src/which.ts +234 -0
  243. package/src/worker-host.ts +100 -0
  244. package/src/xml.ts +313 -0
@@ -0,0 +1,1254 @@
1
+ /** Behavior-compatible reimplementation of linkedom's used surface. */
2
+
3
+ import { parseFragment } from "./parser";
4
+ import { matchesSelector, querySelectorAllFrom } from "./selector";
5
+
6
+ /** DOM node type constants. */
7
+ export const enum NodeType {
8
+ ELEMENT = 1,
9
+ ATTRIBUTE = 2,
10
+ TEXT = 3,
11
+ COMMENT = 8,
12
+ DOCUMENT = 9,
13
+ DOCUMENT_FRAGMENT = 11,
14
+ }
15
+
16
+ interface EventInit {
17
+ bubbles?: boolean;
18
+ cancelable?: boolean;
19
+ composed?: boolean;
20
+ }
21
+
22
+ interface CustomEventInit<T> extends EventInit {
23
+ detail?: T;
24
+ }
25
+
26
+ interface ElementCreationOptions {
27
+ is?: string;
28
+ }
29
+
30
+ type FrameRequestCallback = (time: number) => void;
31
+
32
+ /** Minimal browser event implementation. */
33
+ export class Event {
34
+ type: string;
35
+ bubbles: boolean;
36
+ cancelable: boolean;
37
+ target: EventTarget | null = null;
38
+ currentTarget: EventTarget | null = null;
39
+ defaultPrevented = false;
40
+ #stopped = false;
41
+
42
+ constructor(type: string, init: EventInit = {}) {
43
+ this.type = type;
44
+ this.bubbles = init.bubbles ?? false;
45
+ this.cancelable = init.cancelable ?? false;
46
+ }
47
+
48
+ /** Initialize an event created through Document.createEvent. */
49
+ initEvent(type: string, bubbles = false, cancelable = false): void {
50
+ this.type = type;
51
+ this.bubbles = bubbles;
52
+ this.cancelable = cancelable;
53
+ this.defaultPrevented = false;
54
+ }
55
+
56
+ /** Cancel this event when it is cancelable. */
57
+ preventDefault(): void {
58
+ if (this.cancelable) this.defaultPrevented = true;
59
+ }
60
+
61
+ /** Stop bubbling this event. */
62
+ stopPropagation(): void {
63
+ this.#stopped = true;
64
+ }
65
+
66
+ /** Whether propagation has been stopped. */
67
+ get propagationStopped(): boolean {
68
+ return this.#stopped;
69
+ }
70
+ }
71
+
72
+ /** Browser custom event carrying a detail value. */
73
+ export class CustomEvent<T = unknown> extends Event {
74
+ readonly detail: T | undefined;
75
+
76
+ constructor(type: string, init: CustomEventInit<T> = {}) {
77
+ super(type, init);
78
+ this.detail = init.detail;
79
+ }
80
+ }
81
+
82
+ type EventListener = ((event: Event) => void) | { handleEvent(event: Event): void };
83
+
84
+ /** Minimal event-target implementation used by DOM nodes and window. */
85
+ export class EventTarget {
86
+ #listeners = new Map<string, Set<EventListener>>();
87
+
88
+ /** Register an event listener. */
89
+ addEventListener(type: string, listener: EventListener | null): void {
90
+ if (!listener) return;
91
+ let listeners = this.#listeners.get(type);
92
+ if (!listeners) {
93
+ listeners = new Set();
94
+ this.#listeners.set(type, listeners);
95
+ }
96
+ listeners.add(listener);
97
+ }
98
+
99
+ /** Remove an event listener. */
100
+ removeEventListener(type: string, listener: EventListener | null): void {
101
+ if (listener) this.#listeners.get(type)?.delete(listener);
102
+ }
103
+
104
+ /** Dispatch an event to listeners and optionally through node ancestors. */
105
+ dispatchEvent(event: Event): boolean {
106
+ if (!event.target) event.target = this;
107
+ event.currentTarget = this;
108
+ for (const listener of [...(this.#listeners.get(event.type) ?? [])]) {
109
+ if (typeof listener === "function") listener.call(this, event);
110
+ else listener.handleEvent(event);
111
+ if (event.propagationStopped) break;
112
+ }
113
+ if (event.bubbles && !event.propagationStopped && this instanceof Node && this.parentNode) {
114
+ this.parentNode.dispatchEvent(event);
115
+ }
116
+ return !event.defaultPrevented;
117
+ }
118
+ }
119
+
120
+ /** Base class for the implemented DOM tree. */
121
+ export class Node extends EventTarget {
122
+ static readonly ELEMENT_NODE = NodeType.ELEMENT;
123
+ static readonly ATTRIBUTE_NODE = NodeType.ATTRIBUTE;
124
+ static readonly TEXT_NODE = NodeType.TEXT;
125
+ static readonly COMMENT_NODE = NodeType.COMMENT;
126
+ static readonly DOCUMENT_NODE = NodeType.DOCUMENT;
127
+ static readonly DOCUMENT_FRAGMENT_NODE = NodeType.DOCUMENT_FRAGMENT;
128
+ readonly nodeType: number;
129
+ readonly nodeName: string;
130
+ parentNode: Node | null = null;
131
+ ownerDocument: Document | null;
132
+ childNodes: Node[] = [];
133
+
134
+ constructor(nodeType: number, nodeName: string, ownerDocument: Document | null = null) {
135
+ super();
136
+ this.nodeType = nodeType;
137
+ this.nodeName = nodeName;
138
+ this.ownerDocument = ownerDocument;
139
+ }
140
+
141
+ /** First child node, if present. */
142
+ get firstChild(): Node | null {
143
+ return this.childNodes[0] ?? null;
144
+ }
145
+
146
+ /** Last child node, if present. */
147
+ get lastChild(): Node | null {
148
+ return this.childNodes[this.childNodes.length - 1] ?? null;
149
+ }
150
+
151
+ /** Previous node with the same parent. */
152
+ get previousSibling(): Node | null {
153
+ if (!this.parentNode) return null;
154
+ const index = this.parentNode.childNodes.indexOf(this);
155
+ return index > 0 ? this.parentNode.childNodes[index - 1] : null;
156
+ }
157
+
158
+ /** Next node with the same parent. */
159
+ get nextSibling(): Node | null {
160
+ if (!this.parentNode) return null;
161
+ const index = this.parentNode.childNodes.indexOf(this);
162
+ return index >= 0 ? (this.parentNode.childNodes[index + 1] ?? null) : null;
163
+ }
164
+
165
+ /** Connectedness to a document. */
166
+ get isConnected(): boolean {
167
+ let node: Node | null = this;
168
+ while (node?.parentNode) node = node.parentNode;
169
+ return node?.nodeType === NodeType.DOCUMENT;
170
+ }
171
+
172
+ /** Node value for character-data nodes. */
173
+ get nodeValue(): string | null {
174
+ return null;
175
+ }
176
+
177
+ set nodeValue(_value: string | null) {}
178
+
179
+ /** Text contained by this node. */
180
+ get textContent(): string | null {
181
+ return this.childNodes.map(child => child.textContent ?? "").join("");
182
+ }
183
+
184
+ set textContent(value: string | null) {
185
+ this.replaceChildren();
186
+ if (value) this.appendChild(this.documentForCreation().createTextNode(value));
187
+ }
188
+
189
+ /** Parent element, excluding document and fragments. */
190
+ get parentElement(): Element | null {
191
+ return this.parentNode instanceof Element ? this.parentNode : null;
192
+ }
193
+
194
+ /** Append a node, moving it from its old parent. */
195
+ appendChild<T extends Node>(child: T): T {
196
+ const node: Node = child;
197
+ if (node === this || node.contains(this)) throw new Error("The new child is an ancestor of this node");
198
+ if (child instanceof DocumentFragment) {
199
+ for (const nested of [...child.childNodes]) this.appendChild(nested);
200
+ return child;
201
+ }
202
+ child.parentNode?.removeChild(child);
203
+ child.parentNode = this;
204
+ child.setOwnerDocument(this.documentForCreation());
205
+ this.childNodes.push(child);
206
+ return child;
207
+ }
208
+
209
+ /** Insert a node before a current child, or append for null. */
210
+ insertBefore<T extends Node>(child: T, reference: Node | null): T {
211
+ if (reference === null) return this.appendChild(child);
212
+ const index = this.childNodes.indexOf(reference);
213
+ if (index < 0) throw new Error("The reference node is not a child of this node");
214
+ if (child instanceof DocumentFragment) {
215
+ for (const nested of [...child.childNodes]) this.insertBefore(nested, reference);
216
+ return child;
217
+ }
218
+ child.parentNode?.removeChild(child);
219
+ child.parentNode = this;
220
+ child.setOwnerDocument(this.documentForCreation());
221
+ this.childNodes.splice(index, 0, child);
222
+ return child;
223
+ }
224
+
225
+ /** Replace a current child with another node. */
226
+ replaceChild<T extends Node>(child: Node, previous: T): T {
227
+ const index = this.childNodes.indexOf(previous);
228
+ if (index < 0) throw new Error("The node to replace is not a child of this node");
229
+ this.removeChild(previous);
230
+ this.insertBefore(child, this.childNodes[index] ?? null);
231
+ return previous;
232
+ }
233
+
234
+ /** Remove a current child. */
235
+ removeChild<T extends Node>(child: T): T {
236
+ const index = this.childNodes.indexOf(child);
237
+ if (index < 0) throw new Error("The node to remove is not a child of this node");
238
+ this.childNodes.splice(index, 1);
239
+ child.parentNode = null;
240
+ return child;
241
+ }
242
+
243
+ /** Replace all children with nodes or strings. */
244
+ replaceChildren(...children: Array<Node | string>): void {
245
+ for (const child of this.childNodes) child.parentNode = null;
246
+ this.childNodes = [];
247
+ this.append(...children);
248
+ }
249
+
250
+ /** Append nodes or strings. */
251
+ append(...children: Array<Node | string>): void {
252
+ for (const child of children) {
253
+ this.appendChild(typeof child === "string" ? this.documentForCreation().createTextNode(child) : child);
254
+ }
255
+ }
256
+
257
+ /** Prepend nodes or strings. */
258
+ prepend(...children: Array<Node | string>): void {
259
+ const reference = this.firstChild;
260
+ for (const child of children) {
261
+ this.insertBefore(
262
+ typeof child === "string" ? this.documentForCreation().createTextNode(child) : child,
263
+ reference,
264
+ );
265
+ }
266
+ }
267
+
268
+ /** Remove this node from its parent. */
269
+ remove(): void {
270
+ this.parentNode?.removeChild(this);
271
+ }
272
+
273
+ /** Replace this node in its parent. */
274
+ replaceWith(...nodes: Array<Node | string>): void {
275
+ const parent = this.parentNode;
276
+ if (!parent) return;
277
+ for (const node of nodes) {
278
+ parent.insertBefore(typeof node === "string" ? this.documentForCreation().createTextNode(node) : node, this);
279
+ }
280
+ parent.removeChild(this);
281
+ }
282
+
283
+ /** Whether this node contains another node. */
284
+ contains(other: Node | null): boolean {
285
+ for (let node = other; node; node = node.parentNode) if (node === this) return true;
286
+ return false;
287
+ }
288
+
289
+ /** Clone this node, optionally including descendants. */
290
+ cloneNode(deep = false): Node {
291
+ const clone = new Node(this.nodeType, this.nodeName, this.ownerDocument);
292
+ if (deep) for (const child of this.childNodes) clone.appendChild(child.cloneNode(true));
293
+ return clone;
294
+ }
295
+
296
+ /** Compare tree position sufficiently for document-order consumers. */
297
+ compareDocumentPosition(other: Node): number {
298
+ if (this === other) return 0;
299
+ if (this.contains(other)) return 20;
300
+ if (other.contains(this)) return 10;
301
+ const root = this.ownerDocument ?? this;
302
+ const nodes: Node[] = [];
303
+ const visit = (node: Node): void => {
304
+ nodes.push(node);
305
+ for (const child of node.childNodes) visit(child);
306
+ };
307
+ visit(root);
308
+ return nodes.indexOf(this) < nodes.indexOf(other) ? 4 : 2;
309
+ }
310
+
311
+ setOwnerDocument(document: Document): void {
312
+ if (this.nodeType !== NodeType.DOCUMENT) this.ownerDocument = document;
313
+ for (const child of this.childNodes) child.setOwnerDocument(document);
314
+ }
315
+
316
+ documentForCreation(): Document {
317
+ if (this instanceof Document) return this;
318
+ if (!this.ownerDocument) throw new Error("This node has no owner document");
319
+ return this.ownerDocument;
320
+ }
321
+ }
322
+
323
+ /** Text node. */
324
+ export class Text extends Node {
325
+ data: string;
326
+ serializeRaw: boolean;
327
+
328
+ constructor(data: string, ownerDocument: Document | null = null, serializeRaw = false) {
329
+ super(NodeType.TEXT, "#text", ownerDocument);
330
+ this.data = data;
331
+ this.serializeRaw = serializeRaw;
332
+ }
333
+
334
+ /** Number of UTF-16 code units. */
335
+ get length(): number {
336
+ return this.data.length;
337
+ }
338
+
339
+ override get nodeValue(): string {
340
+ return this.data;
341
+ }
342
+
343
+ override set nodeValue(value: string | null) {
344
+ this.data = value ?? "";
345
+ }
346
+
347
+ override get textContent(): string {
348
+ return this.data;
349
+ }
350
+
351
+ override set textContent(value: string | null) {
352
+ this.data = value ?? "";
353
+ }
354
+
355
+ /** Split this text node at an offset. */
356
+ splitText(offset: number): Text {
357
+ const tail = new Text(this.data.slice(offset), this.ownerDocument, this.serializeRaw);
358
+ this.data = this.data.slice(0, offset);
359
+ this.parentNode?.insertBefore(tail, this.nextSibling);
360
+ return tail;
361
+ }
362
+
363
+ override cloneNode(): Text {
364
+ return new Text(this.data, this.ownerDocument, this.serializeRaw);
365
+ }
366
+ }
367
+
368
+ /** Comment node. */
369
+ export class Comment extends Text {
370
+ constructor(data: string, ownerDocument: Document | null = null) {
371
+ super(data, ownerDocument);
372
+ Object.defineProperty(this, "nodeType", { value: NodeType.COMMENT });
373
+ Object.defineProperty(this, "nodeName", { value: "#comment" });
374
+ }
375
+
376
+ override cloneNode(): Comment {
377
+ return new Comment(this.data, this.ownerDocument);
378
+ }
379
+ }
380
+
381
+ /** DOM attribute value object. */
382
+ export class Attr extends Node {
383
+ readonly name: string;
384
+ value: string;
385
+ readonly namespaceURI: string | null;
386
+
387
+ constructor(name: string, value: string, ownerDocument: Document | null = null, namespaceURI: string | null = null) {
388
+ super(NodeType.ATTRIBUTE, name, ownerDocument);
389
+ this.name = name;
390
+ this.value = value;
391
+ this.namespaceURI = namespaceURI;
392
+ }
393
+
394
+ override get nodeValue(): string {
395
+ return this.value;
396
+ }
397
+
398
+ override set nodeValue(value: string | null) {
399
+ this.value = value ?? "";
400
+ }
401
+
402
+ override get textContent(): string {
403
+ return this.value;
404
+ }
405
+
406
+ override set textContent(value: string | null) {
407
+ this.value = value ?? "";
408
+ }
409
+
410
+ override cloneNode(): Attr {
411
+ return new Attr(this.name, this.value, this.ownerDocument, this.namespaceURI);
412
+ }
413
+ }
414
+
415
+ /** Array-like attribute collection. */
416
+ export class NamedNodeMap extends Array<Attr> {
417
+ /** Find an attribute case-insensitively. */
418
+ getNamedItem(name: string): Attr | null {
419
+ const normalized = name.toLowerCase();
420
+ return this.find(attr => attr.name.toLowerCase() === normalized) ?? null;
421
+ }
422
+
423
+ /** Set an attribute object and return the prior one. */
424
+ setNamedItem(attr: Attr): Attr | null {
425
+ const previous = this.getNamedItem(attr.name);
426
+ if (previous) this.splice(this.indexOf(previous), 1, attr);
427
+ else this.push(attr);
428
+ return previous;
429
+ }
430
+
431
+ /** Remove and return a named attribute. */
432
+ removeNamedItem(name: string): Attr {
433
+ const attr = this.getNamedItem(name);
434
+ if (!attr) throw new Error(`Attribute not found: ${name}`);
435
+ this.splice(this.indexOf(attr), 1);
436
+ return attr;
437
+ }
438
+ }
439
+
440
+ /** Token-list view over an element class attribute. */
441
+ export class DOMTokenList implements Iterable<string> {
442
+ #element: Element;
443
+
444
+ constructor(element: Element) {
445
+ this.#element = element;
446
+ }
447
+
448
+ #tokens(): string[] {
449
+ return this.#element.className.trim() ? this.#element.className.trim().split(/\s+/) : [];
450
+ }
451
+
452
+ #write(tokens: string[]): void {
453
+ this.#element.className = [...new Set(tokens)].join(" ");
454
+ }
455
+
456
+ /** Token count. */
457
+ get length(): number {
458
+ return this.#tokens().length;
459
+ }
460
+
461
+ /** String representation. */
462
+ get value(): string {
463
+ return this.#element.className;
464
+ }
465
+
466
+ set value(value: string) {
467
+ this.#element.className = value;
468
+ }
469
+
470
+ /** Add class tokens. */
471
+ add(...tokens: string[]): void {
472
+ this.#write([...this.#tokens(), ...tokens]);
473
+ }
474
+
475
+ /** Remove class tokens. */
476
+ remove(...tokens: string[]): void {
477
+ const removed = new Set(tokens);
478
+ this.#write(this.#tokens().filter(token => !removed.has(token)));
479
+ }
480
+
481
+ /** Whether a class token exists. */
482
+ contains(token: string): boolean {
483
+ return this.#tokens().includes(token);
484
+ }
485
+
486
+ /** Toggle a class token. */
487
+ toggle(token: string, force?: boolean): boolean {
488
+ const present = this.contains(token);
489
+ const enabled = force ?? !present;
490
+ if (enabled && !present) this.add(token);
491
+ else if (!enabled && present) this.remove(token);
492
+ return enabled;
493
+ }
494
+
495
+ /** Replace one class token with another. */
496
+ replace(previous: string, next: string): boolean {
497
+ const tokens = this.#tokens();
498
+ const index = tokens.indexOf(previous);
499
+ if (index < 0) return false;
500
+ tokens[index] = next;
501
+ this.#write(tokens);
502
+ return true;
503
+ }
504
+
505
+ /** Token at an index. */
506
+ item(index: number): string | null {
507
+ return this.#tokens()[index] ?? null;
508
+ }
509
+
510
+ [Symbol.iterator](): Iterator<string> {
511
+ return this.#tokens()[Symbol.iterator]();
512
+ }
513
+
514
+ toString(): string {
515
+ return this.value;
516
+ }
517
+ }
518
+
519
+ /** Mutable inline style declaration. */
520
+ export class CSSStyleDeclaration {
521
+ #values = new Map<string, string>();
522
+
523
+ /** Serialized declarations. */
524
+ get cssText(): string {
525
+ return [...this.#values].map(([name, value]) => `${name}: ${value};`).join(" ");
526
+ }
527
+
528
+ set cssText(value: string) {
529
+ this.#values.clear();
530
+ for (const declaration of value.split(";")) {
531
+ const colon = declaration.indexOf(":");
532
+ if (colon > 0) this.setProperty(declaration.slice(0, colon).trim(), declaration.slice(colon + 1).trim());
533
+ }
534
+ }
535
+
536
+ /** Read a CSS property. */
537
+ getPropertyValue(name: string): string {
538
+ return this.#values.get(name) ?? "";
539
+ }
540
+
541
+ /** Set a CSS property. */
542
+ setProperty(name: string, value: string | null, _priority?: string): void {
543
+ if (value === null || value === "") this.#values.delete(name);
544
+ else this.#values.set(name, String(value));
545
+ }
546
+
547
+ /** Remove and return a CSS property. */
548
+ removeProperty(name: string): string {
549
+ const value = this.getPropertyValue(name);
550
+ this.#values.delete(name);
551
+ return value;
552
+ }
553
+ }
554
+
555
+ const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
556
+ const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
557
+
558
+ /** DOM element with attributes, selectors, and HTML serialization. */
559
+ export class Element extends Node {
560
+ readonly localName: string;
561
+ readonly tagName: string;
562
+ readonly namespaceURI: string | null;
563
+ readonly qualifiedName: string;
564
+ readonly attributes = new NamedNodeMap();
565
+ readonly classList: DOMTokenList;
566
+ readonly style = new CSSStyleDeclaration();
567
+
568
+ constructor(tagName: string, ownerDocument: Document | null = null, namespaceURI: string | null = HTML_NAMESPACE) {
569
+ const localName = tagName.toLowerCase();
570
+ const qualified = namespaceURI === HTML_NAMESPACE ? localName.toUpperCase() : tagName;
571
+ super(NodeType.ELEMENT, qualified, ownerDocument);
572
+ this.localName = localName;
573
+ this.tagName = qualified;
574
+ this.qualifiedName = tagName;
575
+ this.namespaceURI = namespaceURI;
576
+ this.classList = new DOMTokenList(this);
577
+ }
578
+
579
+ /** Element children. */
580
+ get children(): Element[] {
581
+ return this.childNodes.filter((child): child is Element => child instanceof Element);
582
+ }
583
+
584
+ /** First element child. */
585
+ get firstElementChild(): Element | null {
586
+ return this.children[0] ?? null;
587
+ }
588
+
589
+ /** Last element child. */
590
+ get lastElementChild(): Element | null {
591
+ const children = this.children;
592
+ return children[children.length - 1] ?? null;
593
+ }
594
+
595
+ /** Previous sibling that is an element. */
596
+ get previousElementSibling(): Element | null {
597
+ for (let node = this.previousSibling; node; node = node.previousSibling) if (node instanceof Element) return node;
598
+ return null;
599
+ }
600
+
601
+ /** Next sibling that is an element. */
602
+ get nextElementSibling(): Element | null {
603
+ for (let node = this.nextSibling; node; node = node.nextSibling) if (node instanceof Element) return node;
604
+ return null;
605
+ }
606
+
607
+ /** Attribute-backed element id. */
608
+ get id(): string {
609
+ return this.getAttribute("id") ?? "";
610
+ }
611
+
612
+ set id(value: string) {
613
+ this.setAttribute("id", value);
614
+ }
615
+
616
+ /** Attribute-backed class string. */
617
+ get className(): string {
618
+ return this.getAttribute("class") ?? "";
619
+ }
620
+
621
+ set className(value: string) {
622
+ this.setAttribute("class", value);
623
+ }
624
+
625
+ /** Data attributes exposed as camel-cased properties. */
626
+ get dataset(): Record<string, string> {
627
+ const element = this;
628
+ return new Proxy<Record<string, string>>(
629
+ {},
630
+ {
631
+ get(_target, property) {
632
+ if (typeof property !== "string") return undefined;
633
+ return (
634
+ element.getAttribute(`data-${property.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`) ??
635
+ undefined
636
+ );
637
+ },
638
+ set(_target, property, value) {
639
+ if (typeof property === "string") {
640
+ element.setAttribute(
641
+ `data-${property.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`,
642
+ String(value),
643
+ );
644
+ }
645
+ return true;
646
+ },
647
+ ownKeys() {
648
+ return element.attributes
649
+ .filter(attr => attr.name.startsWith("data-"))
650
+ .map(attr => attr.name.slice(5).replace(/-([a-z])/g, (_m, letter: string) => letter.toUpperCase()));
651
+ },
652
+ getOwnPropertyDescriptor() {
653
+ return { configurable: true, enumerable: true };
654
+ },
655
+ },
656
+ );
657
+ }
658
+
659
+ /** Linkedom-compatible legacy text property (only specialized elements define a value). */
660
+ get text(): string | undefined {
661
+ return undefined;
662
+ }
663
+
664
+ /** HTML contained inside this element. */
665
+ get innerHTML(): string {
666
+ return this.childNodes.map(serializeNode).join("");
667
+ }
668
+
669
+ set innerHTML(value: string) {
670
+ this.replaceChildren();
671
+ for (const child of parseFragment(value, this.documentForCreation(), this.localName).childNodes.slice())
672
+ this.appendChild(child);
673
+ }
674
+
675
+ /** Serialized element and descendants. */
676
+ get outerHTML(): string {
677
+ return serializeNode(this);
678
+ }
679
+
680
+ set outerHTML(value: string) {
681
+ const parent = this.parentNode;
682
+ if (!parent) return;
683
+ const fragment = parseFragment(value, this.documentForCreation(), this.parentElement?.localName);
684
+ for (const child of [...fragment.childNodes]) parent.insertBefore(child, this);
685
+ parent.removeChild(this);
686
+ }
687
+
688
+ /** Read an attribute. */
689
+ getAttribute(name: string): string | null {
690
+ if (name.toLowerCase() === "style" && this.style.cssText) return this.style.cssText;
691
+ return this.attributes.getNamedItem(name)?.value ?? null;
692
+ }
693
+
694
+ /** Read an attribute object. */
695
+ getAttributeNode(name: string): Attr | null {
696
+ return this.attributes.getNamedItem(name);
697
+ }
698
+
699
+ /** Whether an attribute exists. */
700
+ hasAttribute(name: string): boolean {
701
+ return (
702
+ this.attributes.getNamedItem(name) !== null || (name.toLowerCase() === "style" && Boolean(this.style.cssText))
703
+ );
704
+ }
705
+
706
+ /** Set an attribute. */
707
+ setAttribute(name: string, value: string): void {
708
+ const normalized = name.toLowerCase();
709
+ if (normalized === "style") this.style.cssText = String(value);
710
+ const existing = this.attributes.getNamedItem(normalized);
711
+ if (existing) existing.value = String(value);
712
+ else this.attributes.unshift(new Attr(name, String(value), this.ownerDocument));
713
+ }
714
+
715
+ /** Set a namespaced attribute. */
716
+ setAttributeNS(_namespace: string | null, name: string, value: string): void {
717
+ this.setAttribute(name, value);
718
+ }
719
+
720
+ /** Remove an attribute. */
721
+ removeAttribute(name: string): void {
722
+ const existing = this.attributes.getNamedItem(name);
723
+ if (existing) this.attributes.splice(this.attributes.indexOf(existing), 1);
724
+ if (name.toLowerCase() === "style") this.style.cssText = "";
725
+ }
726
+
727
+ /** Find the first descendant matching a selector. */
728
+ querySelector(selector: string): Element | null {
729
+ return querySelectorAllFrom(this, selector, false)[0] ?? null;
730
+ }
731
+
732
+ /** Find all descendants matching a selector. */
733
+ querySelectorAll(selector: string): Element[] {
734
+ return querySelectorAllFrom(this, selector, false);
735
+ }
736
+
737
+ /** Whether this element matches a selector. */
738
+ matches(selector: string): boolean {
739
+ return matchesSelector(this, selector);
740
+ }
741
+
742
+ /** Find the nearest matching ancestor including this element. */
743
+ closest(selector: string): Element | null {
744
+ for (let element: Element | null = this; element; element = element.parentElement) {
745
+ if (element.matches(selector)) return element;
746
+ }
747
+ return null;
748
+ }
749
+
750
+ /** Descendant elements with a tag name. */
751
+ getElementsByTagName(tagName: string): Element[] {
752
+ return this.querySelectorAll(tagName === "*" ? "*" : tagName);
753
+ }
754
+
755
+ /** Descendant elements containing all requested class tokens. */
756
+ getElementsByClassName(classNames: string): Element[] {
757
+ const tokens = classNames.trim().split(/\s+/).filter(Boolean);
758
+ return querySelectorAllFrom(this, "*", false).filter(element =>
759
+ tokens.every(token => element.classList.contains(token)),
760
+ );
761
+ }
762
+
763
+ /**
764
+ * Return a zero-sized rectangle because this parser DOM has no layout.
765
+ * Browser-side scripts only use this method's standard shape.
766
+ */
767
+ getBoundingClientRect(): {
768
+ x: number;
769
+ y: number;
770
+ bottom: number;
771
+ height: number;
772
+ left: number;
773
+ right: number;
774
+ top: number;
775
+ width: number;
776
+ } {
777
+ return { x: 0, y: 0, bottom: 0, height: 0, left: 0, right: 0, top: 0, width: 0 };
778
+ }
779
+
780
+ /** Focus this element in its owner document. */
781
+ focus(): void {
782
+ if (this.ownerDocument) this.ownerDocument.activeElement = this;
783
+ this.dispatchEvent(new Event("focus"));
784
+ }
785
+
786
+ /** Clear focus from this element. */
787
+ blur(): void {
788
+ if (this.ownerDocument?.activeElement === this) this.ownerDocument.activeElement = this.ownerDocument.body;
789
+ this.dispatchEvent(new Event("blur"));
790
+ }
791
+
792
+ /** Trigger a synthetic click event. */
793
+ click(): void {
794
+ this.dispatchEvent(new Event("click", { bubbles: true, cancelable: true }));
795
+ }
796
+
797
+ override cloneNode(deep = false): Element {
798
+ const clone =
799
+ this.ownerDocument?.createElementNS(this.namespaceURI, this.qualifiedName) ??
800
+ new Element(this.qualifiedName, null, this.namespaceURI);
801
+ for (let index = this.attributes.length - 1; index >= 0; index--) {
802
+ const attr = this.attributes[index];
803
+ clone.setAttribute(attr.name, attr.value);
804
+ }
805
+ if (deep) for (const child of this.childNodes) clone.appendChild(child.cloneNode(true));
806
+ return clone;
807
+ }
808
+ }
809
+
810
+ /** HTML element implementation. */
811
+ export class HTMLElement extends Element {
812
+ value = "";
813
+ checked = false;
814
+ selected = false;
815
+ disabled = false;
816
+ multiple = false;
817
+ defaultValue = "";
818
+ defaultChecked = false;
819
+
820
+ /** Reflected title attribute. */
821
+ get title(): string {
822
+ return this.getAttribute("title") ?? "";
823
+ }
824
+
825
+ set title(value: string) {
826
+ this.setAttribute("title", value);
827
+ }
828
+
829
+ /** Reflected href attribute. */
830
+ get href(): string {
831
+ const value = this.getAttribute("href") ?? "";
832
+ try {
833
+ return new URL(value, this.ownerDocument?.URL || "about:blank").href;
834
+ } catch {
835
+ return value;
836
+ }
837
+ }
838
+
839
+ set href(value: string) {
840
+ this.setAttribute("href", value);
841
+ }
842
+
843
+ /** Reflected input type. */
844
+ get type(): string {
845
+ return this.getAttribute("type") ?? "";
846
+ }
847
+
848
+ set type(value: string) {
849
+ this.setAttribute("type", value);
850
+ }
851
+
852
+ /** Reflected name. */
853
+ get name(): string {
854
+ return this.getAttribute("name") ?? "";
855
+ }
856
+
857
+ set name(value: string) {
858
+ this.setAttribute("name", value);
859
+ }
860
+ }
861
+
862
+ /** HTML meta element with a reflected content attribute. */
863
+ export class HTMLMetaElement extends HTMLElement {
864
+ /** Reflected metadata content. */
865
+ get content(): string {
866
+ return this.getAttribute("content") ?? "";
867
+ }
868
+
869
+ set content(value: string) {
870
+ this.setAttribute("content", value);
871
+ }
872
+ }
873
+
874
+ /** SVG element marker used by React feature checks. */
875
+ export class SVGElement extends Element {}
876
+
877
+ /** Iframe element marker used by React selection restoration. */
878
+ export class HTMLIFrameElement extends HTMLElement {}
879
+
880
+ /** Document fragment. */
881
+ export class DocumentFragment extends Node {
882
+ constructor(ownerDocument: Document | null = null) {
883
+ super(NodeType.DOCUMENT_FRAGMENT, "#document-fragment", ownerDocument);
884
+ }
885
+
886
+ /** Element children. */
887
+ get children(): Element[] {
888
+ return this.childNodes.filter((child): child is Element => child instanceof Element);
889
+ }
890
+
891
+ /** First element child. */
892
+ get firstElementChild(): Element | null {
893
+ return this.children[0] ?? null;
894
+ }
895
+
896
+ /** Last element child. */
897
+ get lastElementChild(): Element | null {
898
+ const children = this.children;
899
+ return children[children.length - 1] ?? null;
900
+ }
901
+
902
+ /** First matching descendant. */
903
+ querySelector(selector: string): Element | null {
904
+ return querySelectorAllFrom(this, selector, false)[0] ?? null;
905
+ }
906
+
907
+ /** All matching descendants. */
908
+ querySelectorAll(selector: string): Element[] {
909
+ return querySelectorAllFrom(this, selector, false);
910
+ }
911
+
912
+ override cloneNode(deep = false): DocumentFragment {
913
+ const clone = new DocumentFragment(this.ownerDocument);
914
+ if (deep) for (const child of this.childNodes) clone.appendChild(child.cloneNode(true));
915
+ return clone;
916
+ }
917
+ }
918
+
919
+ /** HTML template whose parsed descendants live in a document fragment. */
920
+ export class HTMLTemplateElement extends HTMLElement {
921
+ readonly content: DocumentFragment;
922
+
923
+ constructor(tagName: string, ownerDocument: Document | null = null, namespaceURI: string | null = HTML_NAMESPACE) {
924
+ super(tagName, ownerDocument, namespaceURI);
925
+ this.content = new DocumentFragment(ownerDocument);
926
+ }
927
+
928
+ override get innerHTML(): string {
929
+ return this.content.childNodes.map(serializeNode).join("");
930
+ }
931
+
932
+ override set innerHTML(value: string) {
933
+ this.content.replaceChildren();
934
+ for (const child of parseFragment(value, this.documentForCreation(), "template").childNodes.slice()) {
935
+ this.content.appendChild(child);
936
+ }
937
+ }
938
+
939
+ override get textContent(): string {
940
+ return this.content.textContent ?? "";
941
+ }
942
+
943
+ override set textContent(value: string | null) {
944
+ this.content.textContent = value;
945
+ }
946
+
947
+ override appendChild<T extends Node>(child: T): T {
948
+ return this.content.appendChild(child);
949
+ }
950
+
951
+ override insertBefore<T extends Node>(child: T, reference: Node | null): T {
952
+ return this.content.insertBefore(child, reference);
953
+ }
954
+
955
+ override replaceChild<T extends Node>(child: Node, previous: T): T {
956
+ return this.content.replaceChild(child, previous);
957
+ }
958
+
959
+ override removeChild<T extends Node>(child: T): T {
960
+ return this.content.removeChild(child);
961
+ }
962
+
963
+ override cloneNode(deep = false): HTMLTemplateElement {
964
+ const clone = new HTMLTemplateElement(this.qualifiedName, this.ownerDocument, this.namespaceURI);
965
+ for (let index = this.attributes.length - 1; index >= 0; index--) {
966
+ const attr = this.attributes[index];
967
+ clone.setAttribute(attr.name, attr.value);
968
+ }
969
+ if (deep) for (const child of this.content.childNodes) clone.content.appendChild(child.cloneNode(true));
970
+ return clone;
971
+ }
972
+ }
973
+
974
+ /** Minimal HTML document. */
975
+ export class Document extends Node {
976
+ defaultView: DOMWindow | null = null;
977
+ URL = "about:blank";
978
+ activeElement: Element | null = null;
979
+
980
+ constructor() {
981
+ super(NodeType.DOCUMENT, "#document", null);
982
+ this.ownerDocument = null;
983
+ }
984
+
985
+ /** Root element of the document. */
986
+ get documentElement(): Element | null {
987
+ return this.children[0] ?? null;
988
+ }
989
+
990
+ /** Element children. */
991
+ get children(): Element[] {
992
+ return this.childNodes.filter((child): child is Element => child instanceof Element);
993
+ }
994
+
995
+ /** HTML body, or a detached empty body when absent like linkedom. */
996
+ get body(): HTMLElement {
997
+ const body = this.querySelector("body");
998
+ return body instanceof HTMLElement ? body : this.createElement("body");
999
+ }
1000
+
1001
+ /** HTML head, or a detached empty head when absent. */
1002
+ get head(): HTMLElement {
1003
+ const head = this.querySelector("head");
1004
+ return head instanceof HTMLElement ? head : this.createElement("head");
1005
+ }
1006
+
1007
+ /** Document title text. */
1008
+ get title(): string {
1009
+ return this.querySelector("title")?.textContent ?? "";
1010
+ }
1011
+
1012
+ set title(value: string) {
1013
+ let title = this.querySelector("title");
1014
+ if (!title) {
1015
+ title = this.createElement("title");
1016
+ this.head.appendChild(title);
1017
+ }
1018
+ title.textContent = value;
1019
+ }
1020
+
1021
+ override get textContent(): null {
1022
+ return null;
1023
+ }
1024
+
1025
+ override set textContent(_value: string | null) {}
1026
+
1027
+ /** Create an HTML element. */
1028
+ createElement(tagName: string, _options?: ElementCreationOptions): HTMLElement {
1029
+ switch (tagName.toLowerCase()) {
1030
+ case "iframe":
1031
+ return new HTMLIFrameElement(tagName, this, HTML_NAMESPACE);
1032
+ case "meta":
1033
+ return new HTMLMetaElement(tagName, this, HTML_NAMESPACE);
1034
+ case "template":
1035
+ return new HTMLTemplateElement(tagName, this, HTML_NAMESPACE);
1036
+ default:
1037
+ return new HTMLElement(tagName, this, HTML_NAMESPACE);
1038
+ }
1039
+ }
1040
+
1041
+ /** Create a namespaced element. */
1042
+ createElementNS(namespace: string | null, tagName: string): Element {
1043
+ if (namespace === SVG_NAMESPACE) return new SVGElement(tagName, this, namespace);
1044
+ if (namespace === null || namespace === HTML_NAMESPACE) return this.createElement(tagName);
1045
+ return new HTMLElement(tagName, this, namespace);
1046
+ }
1047
+
1048
+ /** Create a text node. */
1049
+ createTextNode(data: string): Text {
1050
+ return new Text(data, this);
1051
+ }
1052
+
1053
+ /** Create a comment. */
1054
+ createComment(data: string): Comment {
1055
+ return new Comment(data, this);
1056
+ }
1057
+
1058
+ /** Create an attribute. */
1059
+ createAttribute(name: string): Attr {
1060
+ return new Attr(name.toLowerCase(), "", this);
1061
+ }
1062
+
1063
+ /** Create a document fragment. */
1064
+ createDocumentFragment(): DocumentFragment {
1065
+ return new DocumentFragment(this);
1066
+ }
1067
+
1068
+ /** Find the first element by id. */
1069
+ getElementById(id: string): HTMLElement | null {
1070
+ const element = this.querySelector(`#${cssEscapeIdentifier(id)}`);
1071
+ return element instanceof HTMLElement ? element : null;
1072
+ }
1073
+
1074
+ /** Find the first matching descendant. */
1075
+ querySelector(selector: string): Element | null {
1076
+ return querySelectorAllFrom(this, selector, true)[0] ?? null;
1077
+ }
1078
+
1079
+ /** Find all matching descendants. */
1080
+ querySelectorAll(selector: string): Element[] {
1081
+ return querySelectorAllFrom(this, selector, true);
1082
+ }
1083
+
1084
+ /** Descendant elements with a tag name. */
1085
+ getElementsByTagName(tagName: string): Element[] {
1086
+ return this.querySelectorAll(tagName === "*" ? "*" : tagName);
1087
+ }
1088
+
1089
+ /** Descendant elements containing all class tokens. */
1090
+ getElementsByClassName(classNames: string): Element[] {
1091
+ const tokens = classNames.trim().split(/\s+/).filter(Boolean);
1092
+ return this.querySelectorAll("*").filter(element => tokens.every(token => element.classList.contains(token)));
1093
+ }
1094
+
1095
+ /** Adopt a node into this document. */
1096
+ adoptNode<T extends Node>(node: T): T {
1097
+ node.parentNode?.removeChild(node);
1098
+ node.setOwnerDocument(this);
1099
+ return node;
1100
+ }
1101
+
1102
+ /** Import a cloned node into this document. */
1103
+ importNode<T extends Node>(node: T, deep = false): T {
1104
+ const clone = node.cloneNode(deep) as T;
1105
+ clone.setOwnerDocument(this);
1106
+ return clone;
1107
+ }
1108
+
1109
+ /** Legacy event factory used by libraries. */
1110
+ createEvent(_kind: string): Event {
1111
+ return new Event("");
1112
+ }
1113
+
1114
+ override cloneNode(deep = false): Document {
1115
+ const clone = new Document();
1116
+ if (deep) for (const child of this.childNodes) clone.appendChild(child.cloneNode(true));
1117
+ return clone;
1118
+ }
1119
+ }
1120
+
1121
+ /** Window shape returned by parseHTML. */
1122
+ export class DOMWindow extends EventTarget {
1123
+ readonly document: Document;
1124
+ readonly Node = Node;
1125
+ readonly Element = Element;
1126
+ readonly HTMLElement = HTMLElement;
1127
+ readonly HTMLIFrameElement = HTMLIFrameElement;
1128
+ readonly SVGElement = SVGElement;
1129
+ readonly Text = Text;
1130
+ readonly Comment = Comment;
1131
+ readonly Document = Document;
1132
+ readonly DocumentFragment = DocumentFragment;
1133
+ readonly Event = Event;
1134
+ readonly CustomEvent = CustomEvent;
1135
+ readonly navigator = { userAgent: "pi-utils-dom", platform: "" };
1136
+ readonly location = { href: "about:blank" };
1137
+ readonly window: DOMWindow;
1138
+ readonly self: DOMWindow;
1139
+ readonly top: DOMWindow;
1140
+ readonly parent: DOMWindow;
1141
+
1142
+ constructor(document: Document) {
1143
+ super();
1144
+ this.document = document;
1145
+ this.window = this;
1146
+ this.self = this;
1147
+ this.top = this;
1148
+ this.parent = this;
1149
+ document.defaultView = this;
1150
+ }
1151
+
1152
+ /** Browser-compatible computed style placeholder. */
1153
+ getComputedStyle(element: Element): CSSStyleDeclaration {
1154
+ return element.style;
1155
+ }
1156
+
1157
+ /** Schedule an animation callback. */
1158
+ requestAnimationFrame(callback: FrameRequestCallback): number {
1159
+ return globalThis.setTimeout(() => callback(Date.now()), 16) as unknown as number;
1160
+ }
1161
+
1162
+ /** Cancel an animation callback. */
1163
+ cancelAnimationFrame(handle: number): void {
1164
+ globalThis.clearTimeout(handle);
1165
+ }
1166
+
1167
+ /** Empty selection object. */
1168
+ getSelection(): { rangeCount: number; removeAllRanges(): void } {
1169
+ return { rangeCount: 0, removeAllRanges() {} };
1170
+ }
1171
+ }
1172
+
1173
+ const VOID_ELEMENTS: Record<string, true> = {
1174
+ area: true,
1175
+ base: true,
1176
+ br: true,
1177
+ col: true,
1178
+ embed: true,
1179
+ hr: true,
1180
+ img: true,
1181
+ input: true,
1182
+ link: true,
1183
+ meta: true,
1184
+ param: true,
1185
+ source: true,
1186
+ track: true,
1187
+ wbr: true,
1188
+ };
1189
+ const BOOLEAN_ATTRIBUTES: Record<string, true> = {
1190
+ allowfullscreen: true,
1191
+ async: true,
1192
+ autofocus: true,
1193
+ autoplay: true,
1194
+ checked: true,
1195
+ controls: true,
1196
+ default: true,
1197
+ defer: true,
1198
+ disabled: true,
1199
+ formnovalidate: true,
1200
+ hidden: true,
1201
+ inert: true,
1202
+ ismap: true,
1203
+ itemscope: true,
1204
+ loop: true,
1205
+ multiple: true,
1206
+ muted: true,
1207
+ nomodule: true,
1208
+ novalidate: true,
1209
+ open: true,
1210
+ playsinline: true,
1211
+ readonly: true,
1212
+ required: true,
1213
+ reversed: true,
1214
+ selected: true,
1215
+ };
1216
+
1217
+ function escapeText(value: string): string {
1218
+ return value.replace(/&/g, "&amp;").replace(/ /g, "&#160;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1219
+ }
1220
+
1221
+ function escapeAttribute(value: string): string {
1222
+ return value.replace(/"/g, "&quot;");
1223
+ }
1224
+
1225
+ /** Serialize a DOM node as HTML. */
1226
+ export function serializeNode(node: Node): string {
1227
+ if (node instanceof Comment) return `<!--${node.data}-->`;
1228
+ if (node instanceof Text) return node.serializeRaw ? node.data : escapeText(node.data);
1229
+ if (node instanceof Document || node instanceof DocumentFragment) return node.childNodes.map(serializeNode).join("");
1230
+ if (!(node instanceof Element)) return "";
1231
+ const attributes = node.attributes
1232
+ .map(attr =>
1233
+ attr.value === "" && BOOLEAN_ATTRIBUTES[attr.name.toLowerCase()]
1234
+ ? ` ${attr.name}`
1235
+ : ` ${attr.name}="${escapeAttribute(attr.value)}"`,
1236
+ )
1237
+ .join("");
1238
+ const style =
1239
+ node.style.cssText && !node.attributes.getNamedItem("style")
1240
+ ? ` style="${escapeAttribute(node.style.cssText)}"`
1241
+ : "";
1242
+ const opening = `<${node.qualifiedName}${attributes}${style}>`;
1243
+ if (VOID_ELEMENTS[node.localName]) return opening;
1244
+ const raw = node.localName === "script" || node.localName === "style";
1245
+ const childNodes = node instanceof HTMLTemplateElement ? node.content.childNodes : node.childNodes;
1246
+ const content = raw
1247
+ ? childNodes.map(child => (child instanceof Text ? child.data : serializeNode(child))).join("")
1248
+ : childNodes.map(serializeNode).join("");
1249
+ return `${opening}${content}</${node.qualifiedName}>`;
1250
+ }
1251
+
1252
+ function cssEscapeIdentifier(value: string): string {
1253
+ return value.replace(/([^a-zA-Z0-9_-])/g, "\\$1");
1254
+ }