@tkeron/html-parser 1.1.2 → 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 (131) 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/encoding/detect-encoding.test.ts +33 -0
  75. package/tests/google-dom.test.ts +2 -2
  76. package/tests/helpers/tokenizer-adapter.test.ts +29 -43
  77. package/tests/helpers/tokenizer-adapter.ts +36 -33
  78. package/tests/helpers/tree-adapter.test.ts +20 -20
  79. package/tests/helpers/tree-adapter.ts +34 -24
  80. package/tests/html-entities-text.test.ts +6 -2
  81. package/tests/innerhtml-void-elements.test.ts +52 -36
  82. package/tests/outerHTML-replacement.test.ts +37 -65
  83. package/tests/parser/dom-to-ast.test.ts +109 -0
  84. package/tests/parser/parse.test.ts +139 -0
  85. package/tests/parser.test.ts +281 -217
  86. package/tests/selectors/query-selector-all.test.ts +39 -0
  87. package/tests/selectors/query-selector.test.ts +42 -0
  88. package/tests/serializer/attributes.test.ts +132 -0
  89. package/tests/serializer/escape.test.ts +51 -0
  90. package/tests/serializer/serialize-tokens.test.ts +80 -0
  91. package/tests/serializer-core.test.ts +6 -6
  92. package/tests/serializer-injectmeta.test.ts +6 -6
  93. package/tests/serializer-optionaltags.test.ts +9 -6
  94. package/tests/serializer-options.test.ts +6 -6
  95. package/tests/serializer-whitespace.test.ts +6 -6
  96. package/tests/tokenizer/calculate-position.test.ts +34 -0
  97. package/tests/tokenizer/decode-entities.test.ts +31 -0
  98. package/tests/tokenizer/parse-attributes.test.ts +44 -0
  99. package/tests/tokenizer/tokenize.test.ts +757 -0
  100. package/tests/tokenizer-namedEntities.test.ts +10 -7
  101. package/tests/tokenizer-pendingSpecChanges.test.ts +10 -7
  102. package/tests/tokenizer.test.ts +268 -256
  103. package/tests/tree-construction-adoption01.test.ts +25 -16
  104. package/tests/tree-construction-adoption02.test.ts +30 -19
  105. package/tests/tree-construction-domjs-unsafe.test.ts +6 -4
  106. package/tests/tree-construction-entities02.test.ts +18 -16
  107. package/tests/tree-construction-html5test-com.test.ts +16 -10
  108. package/tests/tree-construction-math.test.ts +11 -9
  109. package/tests/tree-construction-namespace-sensitivity.test.ts +11 -9
  110. package/tests/tree-construction-noscript01.test.ts +11 -9
  111. package/tests/tree-construction-ruby.test.ts +6 -4
  112. package/tests/tree-construction-scriptdata01.test.ts +6 -4
  113. package/tests/tree-construction-svg.test.ts +6 -4
  114. package/tests/tree-construction-template.test.ts +6 -4
  115. package/tests/tree-construction-tests10.test.ts +6 -4
  116. package/tests/tree-construction-tests11.test.ts +6 -4
  117. package/tests/tree-construction-tests20.test.ts +7 -4
  118. package/tests/tree-construction-tests21.test.ts +7 -4
  119. package/tests/tree-construction-tests23.test.ts +7 -4
  120. package/tests/tree-construction-tests24.test.ts +7 -4
  121. package/tests/tree-construction-tests5.test.ts +6 -5
  122. package/tests/tree-construction-tests6.test.ts +6 -5
  123. package/tests/tree-construction-tests_innerHTML_1.test.ts +6 -5
  124. package/tests/void-elements.test.ts +85 -40
  125. package/tsconfig.json +1 -1
  126. package/src/css-selector.ts +0 -185
  127. package/src/encoding.ts +0 -39
  128. package/src/parser.ts +0 -682
  129. package/src/serializer.ts +0 -450
  130. package/src/tokenizer.ts +0 -325
  131. package/tests/selectors.test.ts +0 -128
@@ -0,0 +1,165 @@
1
+ import { TokenType, Token } from "./types.js";
2
+ import { RAW_TEXT_ELEMENTS, RCDATA_ELEMENTS } from "./constants.js";
3
+ import { decodeEntities } from "./decode-entities.js";
4
+ import { parseAttributes } from "./parse-attributes.js";
5
+ import { calculatePosition } from "./calculate-position.js";
6
+
7
+ export const tokenize = (html: string): Token[] => {
8
+ const tokens: Token[] = [];
9
+ let currentPos = 0;
10
+
11
+ while (currentPos < html.length) {
12
+ const char = html[currentPos];
13
+
14
+ if (char === "<") {
15
+ const remaining = html.slice(currentPos);
16
+
17
+ const doctypeMatch = remaining.match(/^<!DOCTYPE\s+[^>]*>/i);
18
+ if (doctypeMatch) {
19
+ const match = doctypeMatch[0];
20
+ const nameMatch = match.match(/<!DOCTYPE\s+([^\s>]+)/i);
21
+ tokens.push({
22
+ type: TokenType.DOCTYPE,
23
+ value: nameMatch && nameMatch[1] ? nameMatch[1].toLowerCase() : match,
24
+ position: calculatePosition(html, currentPos),
25
+ });
26
+ currentPos += match.length;
27
+ continue;
28
+ }
29
+
30
+ const commentMatch = remaining.match(/^<!--([\s\S]*?)(?:-->|$)/);
31
+ if (commentMatch) {
32
+ const match = commentMatch[0];
33
+ tokens.push({
34
+ type: TokenType.COMMENT,
35
+ value: match.slice(4, match.endsWith("-->") ? -3 : match.length),
36
+ position: calculatePosition(html, currentPos),
37
+ });
38
+ currentPos += match.length;
39
+ continue;
40
+ }
41
+
42
+ const cdataMatch = remaining.match(/^<!\[CDATA\[([\s\S]*?)\]\]>/);
43
+ if (cdataMatch) {
44
+ const content = cdataMatch[1];
45
+ tokens.push({
46
+ type: TokenType.COMMENT,
47
+ value: "[CDATA[" + content + "]]",
48
+ position: calculatePosition(html, currentPos),
49
+ });
50
+ currentPos += cdataMatch[0].length;
51
+ continue;
52
+ }
53
+
54
+ const piMatch = remaining.match(/^<(\?[\s\S]*?)>/);
55
+ if (piMatch) {
56
+ tokens.push({
57
+ type: TokenType.COMMENT,
58
+ value: piMatch[1],
59
+ position: calculatePosition(html, currentPos),
60
+ });
61
+ currentPos += piMatch[0].length;
62
+ continue;
63
+ }
64
+
65
+ const tagMatch = remaining.match(/^<\/?([a-zA-Z][^\s/>]*)([^>]*)>/);
66
+
67
+ if (tagMatch) {
68
+ const fullTag = tagMatch[0];
69
+ const tagName = tagMatch[1]?.toLowerCase();
70
+ if (!tagName) {
71
+ currentPos++;
72
+ continue;
73
+ }
74
+
75
+ const isClosing = fullTag.startsWith("</");
76
+ const isSelfClosing = fullTag.endsWith("/>");
77
+
78
+ let attributes: Record<string, string> = {};
79
+ if (!isClosing) {
80
+ const attrMatch = fullTag.match(/^<[a-zA-Z][^\s/>]*\s+([^>]*?)\/?>$/);
81
+ if (attrMatch && attrMatch[1]) {
82
+ attributes = parseAttributes(attrMatch[1]);
83
+ }
84
+ }
85
+
86
+ tokens.push({
87
+ type: isClosing ? TokenType.TAG_CLOSE : TokenType.TAG_OPEN,
88
+ value: tagName,
89
+ position: calculatePosition(html, currentPos),
90
+ ...(isClosing
91
+ ? { isClosing: true }
92
+ : {
93
+ attributes,
94
+ isSelfClosing,
95
+ }),
96
+ });
97
+
98
+ currentPos += fullTag.length;
99
+
100
+ if (
101
+ !isClosing &&
102
+ !isSelfClosing &&
103
+ (RAW_TEXT_ELEMENTS.has(tagName) || RCDATA_ELEMENTS.has(tagName))
104
+ ) {
105
+ const closeTagPattern = new RegExp(`</${tagName}\\s*>`, "i");
106
+ const restOfHtml = html.slice(currentPos);
107
+ const closeMatch = restOfHtml.match(closeTagPattern);
108
+
109
+ if (closeMatch && closeMatch.index !== undefined) {
110
+ const rawContent = restOfHtml.slice(0, closeMatch.index);
111
+ if (rawContent) {
112
+ tokens.push({
113
+ type: TokenType.TEXT,
114
+ value: RCDATA_ELEMENTS.has(tagName)
115
+ ? decodeEntities(rawContent)
116
+ : rawContent,
117
+ position: calculatePosition(html, currentPos),
118
+ });
119
+ }
120
+ currentPos += rawContent.length;
121
+ }
122
+ }
123
+ } else {
124
+ const textStart = currentPos;
125
+ currentPos++;
126
+
127
+ while (currentPos < html.length && html[currentPos] !== "<") {
128
+ currentPos++;
129
+ }
130
+
131
+ const textContent = html.slice(textStart, currentPos);
132
+ if (textContent) {
133
+ tokens.push({
134
+ type: TokenType.TEXT,
135
+ value: decodeEntities(textContent),
136
+ position: calculatePosition(html, textStart),
137
+ });
138
+ }
139
+ }
140
+ } else {
141
+ const textStart = currentPos;
142
+
143
+ while (currentPos < html.length && html[currentPos] !== "<") {
144
+ currentPos++;
145
+ }
146
+
147
+ const textContent = html.slice(textStart, currentPos);
148
+ if (textContent) {
149
+ tokens.push({
150
+ type: TokenType.TEXT,
151
+ value: decodeEntities(textContent),
152
+ position: calculatePosition(html, textStart),
153
+ });
154
+ }
155
+ }
156
+ }
157
+
158
+ tokens.push({
159
+ type: TokenType.EOF,
160
+ value: "",
161
+ position: calculatePosition(html, html.length),
162
+ });
163
+
164
+ return tokens;
165
+ };
@@ -0,0 +1,25 @@
1
+ export enum TokenType {
2
+ TAG_OPEN = "TAG_OPEN",
3
+ TAG_CLOSE = "TAG_CLOSE",
4
+ TEXT = "TEXT",
5
+ COMMENT = "COMMENT",
6
+ CDATA = "CDATA",
7
+ DOCTYPE = "DOCTYPE",
8
+ PROCESSING_INSTRUCTION = "PROCESSING_INSTRUCTION",
9
+ EOF = "EOF",
10
+ }
11
+
12
+ export interface Position {
13
+ line: number;
14
+ column: number;
15
+ offset: number;
16
+ }
17
+
18
+ export interface Token {
19
+ type: TokenType;
20
+ value: string;
21
+ position: Position;
22
+ attributes?: Record<string, string>;
23
+ isSelfClosing?: boolean;
24
+ isClosing?: boolean;
25
+ }
@@ -0,0 +1,304 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import {
3
+ findFormattingElementInStack,
4
+ findFurthestBlock,
5
+ getCommonAncestor,
6
+ isSpecialElement,
7
+ cloneFormattingElement,
8
+ reparentChildren,
9
+ } from "../src/parser/adoption-agency-helpers.js";
10
+ import { createElement, createDocument } from "../src/dom-simulator/index.js";
11
+
12
+ describe("findFormattingElementInStack", () => {
13
+ it("returns null for empty stack", () => {
14
+ const result = findFormattingElementInStack([], "b");
15
+ expect(result).toBeNull();
16
+ });
17
+
18
+ it("returns null when formatting element not found", () => {
19
+ const div = createElement("div", {});
20
+ const span = createElement("span", {});
21
+ const stack = [div, span];
22
+ const result = findFormattingElementInStack(stack, "b");
23
+ expect(result).toBeNull();
24
+ });
25
+
26
+ it("finds formatting element at top of stack", () => {
27
+ const div = createElement("div", {});
28
+ const b = createElement("b", {});
29
+ const stack = [div, b];
30
+ const result = findFormattingElementInStack(stack, "b");
31
+ expect(result).toEqual({ element: b, index: 1 });
32
+ });
33
+
34
+ it("finds formatting element in middle of stack", () => {
35
+ const div = createElement("div", {});
36
+ const b = createElement("b", {});
37
+ const span = createElement("span", {});
38
+ const stack = [div, b, span];
39
+ const result = findFormattingElementInStack(stack, "b");
40
+ expect(result).toEqual({ element: b, index: 1 });
41
+ });
42
+
43
+ it("finds last matching element (most recent)", () => {
44
+ const div = createElement("div", {});
45
+ const b1 = createElement("b", {});
46
+ const b2 = createElement("b", {});
47
+ const stack = [div, b1, b2];
48
+ const result = findFormattingElementInStack(stack, "b");
49
+ expect(result).toEqual({ element: b2, index: 2 });
50
+ });
51
+
52
+ it("is case insensitive", () => {
53
+ const div = createElement("div", {});
54
+ const B = createElement("B", {});
55
+ const stack = [div, B];
56
+ const result = findFormattingElementInStack(stack, "b");
57
+ expect(result).toEqual({ element: B, index: 1 });
58
+ });
59
+ });
60
+
61
+ describe("findFurthestBlock", () => {
62
+ it("returns null when no special elements after formatting element", () => {
63
+ const doc = createDocument();
64
+ const div = createElement("div", {});
65
+ const b = createElement("b", {});
66
+ const span = createElement("span", {});
67
+ const stack = [doc, div, b, span];
68
+ const result = findFurthestBlock(stack, 2);
69
+ expect(result).toBeNull();
70
+ });
71
+
72
+ it("finds first special element after formatting element", () => {
73
+ const doc = createDocument();
74
+ const body = createElement("body", {});
75
+ const b = createElement("b", {});
76
+ const p = createElement("p", {});
77
+ const stack = [doc, body, b, p];
78
+ const result = findFurthestBlock(stack, 2);
79
+ expect(result).toEqual({ element: p, index: 3 });
80
+ });
81
+
82
+ it("finds first special element, not the last", () => {
83
+ const doc = createDocument();
84
+ const body = createElement("body", {});
85
+ const b = createElement("b", {});
86
+ const p = createElement("p", {});
87
+ const div = createElement("div", {});
88
+ const stack = [doc, body, b, p, div];
89
+ const result = findFurthestBlock(stack, 2);
90
+ expect(result).toEqual({ element: p, index: 3 });
91
+ });
92
+
93
+ it("returns null when formatting element is at end of stack", () => {
94
+ const doc = createDocument();
95
+ const body = createElement("body", {});
96
+ const b = createElement("b", {});
97
+ const stack = [doc, body, b];
98
+ const result = findFurthestBlock(stack, 2);
99
+ expect(result).toBeNull();
100
+ });
101
+
102
+ it("skips non-special elements before finding special element", () => {
103
+ const doc = createDocument();
104
+ const body = createElement("body", {});
105
+ const b = createElement("b", {});
106
+ const em = createElement("em", {});
107
+ const span = createElement("span", {});
108
+ const div = createElement("div", {});
109
+ const stack = [doc, body, b, em, span, div];
110
+ const result = findFurthestBlock(stack, 2);
111
+ expect(result).toEqual({ element: div, index: 5 });
112
+ });
113
+ });
114
+
115
+ describe("getCommonAncestor", () => {
116
+ it("returns element before formatting element index", () => {
117
+ const doc = createDocument();
118
+ const body = createElement("body", {});
119
+ const b = createElement("b", {});
120
+ const stack = [doc, body, b];
121
+ const result = getCommonAncestor(stack, 2);
122
+ expect(result).toBe(body);
123
+ });
124
+
125
+ it("returns doc when formatting element is first after doc", () => {
126
+ const doc = createDocument();
127
+ const b = createElement("b", {});
128
+ const stack = [doc, b];
129
+ const result = getCommonAncestor(stack, 1);
130
+ expect(result).toBe(doc);
131
+ });
132
+
133
+ it("returns null for invalid index 0", () => {
134
+ const doc = createDocument();
135
+ const stack = [doc];
136
+ const result = getCommonAncestor(stack, 0);
137
+ expect(result).toBeNull();
138
+ });
139
+
140
+ it("returns null for negative index", () => {
141
+ const doc = createDocument();
142
+ const stack = [doc];
143
+ const result = getCommonAncestor(stack, -1);
144
+ expect(result).toBeNull();
145
+ });
146
+ });
147
+
148
+ describe("isSpecialElement", () => {
149
+ it("returns true for div", () => {
150
+ expect(isSpecialElement("div")).toBe(true);
151
+ });
152
+
153
+ it("returns true for p", () => {
154
+ expect(isSpecialElement("p")).toBe(true);
155
+ });
156
+
157
+ it("returns true for table", () => {
158
+ expect(isSpecialElement("table")).toBe(true);
159
+ });
160
+
161
+ it("returns true for body", () => {
162
+ expect(isSpecialElement("body")).toBe(true);
163
+ });
164
+
165
+ it("returns true for html", () => {
166
+ expect(isSpecialElement("html")).toBe(true);
167
+ });
168
+
169
+ it("returns false for b", () => {
170
+ expect(isSpecialElement("b")).toBe(false);
171
+ });
172
+
173
+ it("returns false for i", () => {
174
+ expect(isSpecialElement("i")).toBe(false);
175
+ });
176
+
177
+ it("returns false for span", () => {
178
+ expect(isSpecialElement("span")).toBe(false);
179
+ });
180
+
181
+ it("returns false for a", () => {
182
+ expect(isSpecialElement("a")).toBe(false);
183
+ });
184
+
185
+ it("returns false for em", () => {
186
+ expect(isSpecialElement("em")).toBe(false);
187
+ });
188
+
189
+ it("is case insensitive for DIV", () => {
190
+ expect(isSpecialElement("DIV")).toBe(true);
191
+ });
192
+
193
+ it("is case insensitive for SPAN", () => {
194
+ expect(isSpecialElement("SPAN")).toBe(false);
195
+ });
196
+ });
197
+
198
+ describe("cloneFormattingElement", () => {
199
+ it("clones element with same tagName", () => {
200
+ const original = createElement("b", {});
201
+ const clone = cloneFormattingElement(original);
202
+ expect(clone.tagName.toLowerCase()).toBe("b");
203
+ });
204
+
205
+ it("clones element with attributes", () => {
206
+ const original = createElement("b", { class: "bold", id: "test" });
207
+ const clone = cloneFormattingElement(original);
208
+ expect(clone.getAttribute("class")).toBe("bold");
209
+ expect(clone.getAttribute("id")).toBe("test");
210
+ });
211
+
212
+ it("creates new element instance", () => {
213
+ const original = createElement("b", {});
214
+ const clone = cloneFormattingElement(original);
215
+ expect(clone).not.toBe(original);
216
+ });
217
+
218
+ it("does not clone children", () => {
219
+ const original = createElement("b", {});
220
+ const child = createElement("span", {});
221
+ original.childNodes.push(child);
222
+ child.parentNode = original;
223
+ const clone = cloneFormattingElement(original);
224
+ expect(clone.childNodes.length).toBe(0);
225
+ });
226
+
227
+ it("has no parent", () => {
228
+ const parent = createElement("div", {});
229
+ const original = createElement("b", {});
230
+ original.parentNode = parent;
231
+ const clone = cloneFormattingElement(original);
232
+ expect(clone.parentNode).toBeNull();
233
+ });
234
+ });
235
+
236
+ describe("reparentChildren", () => {
237
+ it("moves all children from source to target", () => {
238
+ const source = createElement("b", {});
239
+ const target = createElement("b", {});
240
+ const child1 = createElement("span", {});
241
+ const child2 = createElement("em", {});
242
+
243
+ source.childNodes.push(child1, child2);
244
+ child1.parentNode = source;
245
+ child2.parentNode = source;
246
+
247
+ reparentChildren(source, target);
248
+
249
+ expect(source.childNodes.length).toBe(0);
250
+ expect(target.childNodes.length).toBe(2);
251
+ expect(target.childNodes[0]).toBe(child1);
252
+ expect(target.childNodes[1]).toBe(child2);
253
+ expect(child1.parentNode).toBe(target);
254
+ expect(child2.parentNode).toBe(target);
255
+ });
256
+
257
+ it("handles empty source", () => {
258
+ const source = createElement("b", {});
259
+ const target = createElement("b", {});
260
+
261
+ reparentChildren(source, target);
262
+
263
+ expect(source.childNodes.length).toBe(0);
264
+ expect(target.childNodes.length).toBe(0);
265
+ });
266
+
267
+ it("preserves order of children", () => {
268
+ const source = createElement("b", {});
269
+ const target = createElement("b", {});
270
+ const child1 = createElement("span", {});
271
+ const child2 = createElement("em", {});
272
+ const child3 = createElement("strong", {});
273
+
274
+ source.childNodes.push(child1, child2, child3);
275
+ child1.parentNode = source;
276
+ child2.parentNode = source;
277
+ child3.parentNode = source;
278
+
279
+ reparentChildren(source, target);
280
+
281
+ expect(target.childNodes[0]).toBe(child1);
282
+ expect(target.childNodes[1]).toBe(child2);
283
+ expect(target.childNodes[2]).toBe(child3);
284
+ });
285
+
286
+ it("appends to existing children in target", () => {
287
+ const source = createElement("b", {});
288
+ const target = createElement("b", {});
289
+ const existingChild = createElement("pre", {});
290
+ const child1 = createElement("span", {});
291
+
292
+ target.childNodes.push(existingChild);
293
+ existingChild.parentNode = target;
294
+
295
+ source.childNodes.push(child1);
296
+ child1.parentNode = source;
297
+
298
+ reparentChildren(source, target);
299
+
300
+ expect(target.childNodes.length).toBe(2);
301
+ expect(target.childNodes[0]).toBe(existingChild);
302
+ expect(target.childNodes[1]).toBe(child1);
303
+ });
304
+ });