@wsxjs/wsx-core 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-2ER76KOQ.mjs +1422 -0
- package/dist/chunk-FAPFH5ON.mjs +1372 -0
- package/dist/chunk-U74WFVRE.mjs +1308 -0
- package/dist/chunk-UTWWJJ4C.mjs +1360 -0
- package/dist/index.js +171 -42
- package/dist/index.mjs +47 -30
- package/dist/jsx-runtime.js +77 -13
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +77 -13
- package/dist/jsx.mjs +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/base-component.ts +14 -10
- package/src/index.ts +2 -0
- package/src/light-component.ts +61 -31
- package/src/utils/dom-utils.ts +13 -1
- package/src/utils/element-marking.ts +5 -3
- package/src/utils/element-update.ts +92 -14
- package/src/utils/update-children-helpers.ts +40 -13
package/src/light-component.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
import { h, type JSXChildren } from "./jsx-factory";
|
|
11
11
|
import { BaseComponent, type BaseComponentConfig } from "./base-component";
|
|
12
12
|
import { RenderContext } from "./render-context";
|
|
13
|
-
import { shouldPreserveElement } from "./utils/element-marking";
|
|
14
13
|
import { createLogger } from "@wsxjs/wsx-logger";
|
|
14
|
+
import { reconcileElement } from "./utils/element-update";
|
|
15
15
|
|
|
16
16
|
const logger = createLogger("LightComponent");
|
|
17
17
|
|
|
@@ -156,6 +156,10 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
156
156
|
return HTMLElement.prototype.querySelectorAll.call(this, selector) as NodeListOf<T>;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* 递归协调子元素
|
|
161
|
+
* 更新现有子元素的属性和内容,而不是替换整个子树
|
|
162
|
+
*/
|
|
159
163
|
/**
|
|
160
164
|
* 内部重渲染实现
|
|
161
165
|
* 包含从 rerender() 方法迁移的实际渲染逻辑
|
|
@@ -180,11 +184,11 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
180
184
|
|
|
181
185
|
try {
|
|
182
186
|
// 3. 重新渲染JSX内容
|
|
183
|
-
const
|
|
187
|
+
const newContent = RenderContext.runInContext(this, () => this.render());
|
|
184
188
|
|
|
185
189
|
// 4. 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
|
|
186
190
|
if (focusState && focusState.key && focusState.value !== undefined) {
|
|
187
|
-
const target =
|
|
191
|
+
const target = newContent.querySelector(
|
|
188
192
|
`[data-wsx-key="${focusState.key}"]`
|
|
189
193
|
) as HTMLElement;
|
|
190
194
|
|
|
@@ -220,17 +224,9 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
220
224
|
|
|
221
225
|
// 6. 使用 requestAnimationFrame 批量执行 DOM 操作
|
|
222
226
|
requestAnimationFrame(() => {
|
|
223
|
-
//
|
|
224
|
-
this.appendChild(content);
|
|
225
|
-
|
|
226
|
-
// 移除旧内容(保留 JSX children、样式元素和未标记元素)
|
|
227
|
-
// 关键修复:使用 shouldPreserveElement() 来保护手动创建的元素(如第三方库注入的元素)
|
|
227
|
+
// 获取当前的 children(排除样式元素和 JSX children)
|
|
228
228
|
const oldChildren = Array.from(this.children).filter((child) => {
|
|
229
|
-
//
|
|
230
|
-
if (child === content) {
|
|
231
|
-
return false;
|
|
232
|
-
}
|
|
233
|
-
// 保留样式元素
|
|
229
|
+
// 排除样式元素
|
|
234
230
|
if (
|
|
235
231
|
stylesToApply &&
|
|
236
232
|
child instanceof HTMLStyleElement &&
|
|
@@ -238,38 +234,72 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
238
234
|
) {
|
|
239
235
|
return false;
|
|
240
236
|
}
|
|
241
|
-
//
|
|
237
|
+
// 排除 JSX children
|
|
242
238
|
if (child instanceof HTMLElement && jsxChildren.includes(child)) {
|
|
243
239
|
return false;
|
|
244
240
|
}
|
|
245
|
-
// 保留未标记的元素(手动创建的元素、第三方库注入的元素)
|
|
246
|
-
// 这是 RFC 0037 Phase 5 的核心:保护未标记元素
|
|
247
|
-
if (shouldPreserveElement(child)) {
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
241
|
return true;
|
|
251
242
|
});
|
|
252
|
-
|
|
243
|
+
|
|
244
|
+
// 🔥 关键修复:实现真正的 DOM reconciliation
|
|
245
|
+
// 而不是简单的删除+添加,我们需要:
|
|
246
|
+
// 1. 如果新旧内容是相同类型的元素,更新其属性
|
|
247
|
+
// 2. 如果类型不同,才替换元素
|
|
248
|
+
|
|
249
|
+
if (oldChildren.length === 1 && newContent instanceof HTMLElement) {
|
|
250
|
+
const oldElement = oldChildren[0];
|
|
251
|
+
|
|
252
|
+
// 如果旧元素和新元素是相同类型的标签,更新属性而不是替换
|
|
253
|
+
if (
|
|
254
|
+
oldElement instanceof HTMLElement &&
|
|
255
|
+
oldElement.tagName === newContent.tagName
|
|
256
|
+
) {
|
|
257
|
+
// 更新属性
|
|
258
|
+
// 1. 移除旧属性
|
|
259
|
+
Array.from(oldElement.attributes).forEach((attr) => {
|
|
260
|
+
if (!newContent.hasAttribute(attr.name)) {
|
|
261
|
+
oldElement.removeAttribute(attr.name);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// 2. 设置/更新新属性
|
|
266
|
+
Array.from(newContent.attributes).forEach((attr) => {
|
|
267
|
+
if (oldElement.getAttribute(attr.name) !== attr.value) {
|
|
268
|
+
oldElement.setAttribute(attr.name, attr.value);
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// 3. 递归更新子元素
|
|
273
|
+
reconcileElement(oldElement, newContent);
|
|
274
|
+
} else {
|
|
275
|
+
// 类型不同,直接替换
|
|
276
|
+
oldElement.remove();
|
|
277
|
+
this.appendChild(newContent);
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
// 数量不匹配或者不是单个元素,使用简单替换
|
|
281
|
+
oldChildren.forEach((child) => child.remove());
|
|
282
|
+
this.appendChild(newContent);
|
|
283
|
+
}
|
|
253
284
|
|
|
254
285
|
// 确保样式元素存在并在第一个位置
|
|
255
|
-
// 关键修复:在元素复用场景中,如果 _autoStyles 存在但样式元素被意外移除,需要重新创建
|
|
256
286
|
if (stylesToApply) {
|
|
257
|
-
let
|
|
287
|
+
let styleEl = this.querySelector(
|
|
258
288
|
`style[data-wsx-light-component="${styleName}"]`
|
|
259
289
|
) as HTMLStyleElement | null;
|
|
260
290
|
|
|
261
|
-
if (!
|
|
291
|
+
if (!styleEl) {
|
|
262
292
|
// 样式元素被意外移除,重新创建
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.insertBefore(
|
|
267
|
-
} else if (
|
|
293
|
+
styleEl = document.createElement("style");
|
|
294
|
+
styleEl.setAttribute("data-wsx-light-component", styleName);
|
|
295
|
+
styleEl.textContent = stylesToApply;
|
|
296
|
+
this.insertBefore(styleEl, this.firstChild);
|
|
297
|
+
} else if (styleEl.textContent !== stylesToApply) {
|
|
268
298
|
// 样式内容已变化,更新
|
|
269
|
-
|
|
270
|
-
} else if (
|
|
299
|
+
styleEl.textContent = stylesToApply;
|
|
300
|
+
} else if (styleEl !== this.firstChild) {
|
|
271
301
|
// 样式元素存在但不在第一个位置,移动到第一个位置
|
|
272
|
-
this.insertBefore(
|
|
302
|
+
this.insertBefore(styleEl, this.firstChild);
|
|
273
303
|
}
|
|
274
304
|
}
|
|
275
305
|
|
package/src/utils/dom-utils.ts
CHANGED
|
@@ -51,9 +51,15 @@ export function parseHTMLToNodes(html: string): (HTMLElement | SVGElement | stri
|
|
|
51
51
|
// Text nodes are converted to strings, elements are kept as-is
|
|
52
52
|
return Array.from(temp.childNodes).map((node) => {
|
|
53
53
|
if (node instanceof HTMLElement || node instanceof SVGElement) {
|
|
54
|
+
// 关键修复:标记解析出的元素为框架管理
|
|
55
|
+
// 这确保了 shouldPreserveElement 不会错误地保留这些元素
|
|
56
|
+
// 从而防止了在频繁重渲染(如 Markdown 输入)时的元素堆积
|
|
57
|
+
(node as any).__wsxManaged = true;
|
|
54
58
|
return node;
|
|
55
59
|
} else {
|
|
56
60
|
// Convert text nodes and other node types to strings
|
|
61
|
+
// Note: When these strings are processed by appendChildrenToElement,
|
|
62
|
+
// they will be wraped in managed text nodes.
|
|
57
63
|
return node.textContent || "";
|
|
58
64
|
}
|
|
59
65
|
});
|
|
@@ -79,7 +85,13 @@ export function isHTMLString(str: string): boolean {
|
|
|
79
85
|
const looksLikeMath = /^[^<]*<[^>]*>[^>]*$/.test(trimmed) && !htmlTagPattern.test(trimmed);
|
|
80
86
|
if (looksLikeMath) return false;
|
|
81
87
|
|
|
82
|
-
|
|
88
|
+
const result = htmlTagPattern.test(trimmed);
|
|
89
|
+
if (result) {
|
|
90
|
+
console.log(`[WSX Debug] isHTMLString("${trimmed.substring(0, 50)}..."): ${result}`, {
|
|
91
|
+
looksLikeMath,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
83
95
|
}
|
|
84
96
|
|
|
85
97
|
/**
|
|
@@ -68,15 +68,17 @@ export function shouldPreserveElement(element: Node): boolean {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
// 规则 2: 没有标记的元素保留(自定义元素、第三方库注入)
|
|
71
|
-
|
|
71
|
+
// 关键修正:除了检查是否由 h() 创建,还要检查是否被框架显式标记为托管 (__wsxManaged)
|
|
72
|
+
// 这主要用于处理从 HTML 字符串解析出的元素 (parseHTMLToNodes)
|
|
73
|
+
if (!isCreatedByH(element) && (element as any).__wsxManaged !== true) {
|
|
72
74
|
return true;
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
// 规则 3: 显式标记保留
|
|
76
|
-
if (element.hasAttribute("data-wsx-preserve")) {
|
|
78
|
+
if (element instanceof HTMLElement && element.hasAttribute("data-wsx-preserve")) {
|
|
77
79
|
return true;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
// 规则 4: 由 h()
|
|
82
|
+
// 规则 4: 由 h() 创建或被标记为托管的元素 → 不保留(由框架管理)
|
|
81
83
|
return false;
|
|
82
84
|
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
removeNodes,
|
|
24
24
|
reinsertPreservedElements,
|
|
25
25
|
flattenChildrenSafe,
|
|
26
|
+
replaceOrInsertElementAtPosition,
|
|
26
27
|
} from "./update-children-helpers";
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -435,21 +436,16 @@ export function updateChildren(
|
|
|
435
436
|
? element.childNodes[insertionIndex.value]
|
|
436
437
|
: null;
|
|
437
438
|
|
|
438
|
-
//
|
|
439
|
-
// 使用
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
// 这对于旨在基于位置插入的场景会导致 Off-By-One 错误(插入到了后面)
|
|
448
|
-
element.insertBefore(newChild, insertBeforeNode);
|
|
449
|
-
}
|
|
439
|
+
// 甚至 newChild === oldNode,如果位置不对也需要移动
|
|
440
|
+
// 使用 helper 处理元素替换和插入,支持 HTML 解析内容的自动等价性匹配
|
|
441
|
+
replaceOrInsertElementAtPosition(
|
|
442
|
+
element,
|
|
443
|
+
newChild as HTMLElement | SVGElement,
|
|
444
|
+
oldNode,
|
|
445
|
+
insertBeforeNode,
|
|
446
|
+
processedNodes
|
|
447
|
+
);
|
|
450
448
|
|
|
451
|
-
// 标记新元素为已处理
|
|
452
|
-
processedNodes.add(newChild);
|
|
453
449
|
insertionIndex.value++;
|
|
454
450
|
} else {
|
|
455
451
|
// 类型变化:元素 -> 文本/Fragment
|
|
@@ -538,3 +534,85 @@ export function updateElement(
|
|
|
538
534
|
// 更新 children
|
|
539
535
|
updateChildren(element, oldChildren, newChildren, cacheManager);
|
|
540
536
|
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Recursively reconciles an old element to match a new element's structure.
|
|
540
|
+
* This is a pure function that updates the oldParent in-place to match newParent.
|
|
541
|
+
*
|
|
542
|
+
* Used by LightComponent to update DOM without full replacement, preserving:
|
|
543
|
+
* - Element references (important for event listeners, focus state)
|
|
544
|
+
* - User-added JSX children
|
|
545
|
+
* - Third-party library injected elements
|
|
546
|
+
*
|
|
547
|
+
* @param oldParent - The existing DOM element to update
|
|
548
|
+
* @param newParent - The new element structure to match
|
|
549
|
+
*/
|
|
550
|
+
export function reconcileElement(oldParent: HTMLElement, newParent: HTMLElement): void {
|
|
551
|
+
const oldChildren = Array.from(oldParent.childNodes);
|
|
552
|
+
const newChildren = Array.from(newParent.childNodes);
|
|
553
|
+
|
|
554
|
+
const maxLength = Math.max(oldChildren.length, newChildren.length);
|
|
555
|
+
|
|
556
|
+
for (let i = 0; i < maxLength; i++) {
|
|
557
|
+
const oldChild = oldChildren[i];
|
|
558
|
+
const newChild = newChildren[i];
|
|
559
|
+
|
|
560
|
+
if (!newChild) {
|
|
561
|
+
// 新的子节点不存在,删除旧的
|
|
562
|
+
oldChild?.remove();
|
|
563
|
+
} else if (!oldChild) {
|
|
564
|
+
// 旧的子节点不存在,添加新的
|
|
565
|
+
oldParent.appendChild(newChild.cloneNode(true));
|
|
566
|
+
} else if (oldChild.nodeType !== newChild.nodeType) {
|
|
567
|
+
// 节点类型不同,替换
|
|
568
|
+
oldParent.replaceChild(newChild.cloneNode(true), oldChild);
|
|
569
|
+
} else if (oldChild.nodeType === Node.TEXT_NODE) {
|
|
570
|
+
// 文本节点,更新内容
|
|
571
|
+
if (oldChild.textContent !== newChild.textContent) {
|
|
572
|
+
oldChild.textContent = newChild.textContent;
|
|
573
|
+
}
|
|
574
|
+
} else if (oldChild.nodeType === Node.ELEMENT_NODE) {
|
|
575
|
+
const oldEl = oldChild as HTMLElement;
|
|
576
|
+
const newEl = newChild as HTMLElement;
|
|
577
|
+
|
|
578
|
+
// 元素节点
|
|
579
|
+
if (oldEl.tagName !== newEl.tagName) {
|
|
580
|
+
// 标签不同,替换
|
|
581
|
+
oldParent.replaceChild(newEl.cloneNode(true), oldEl);
|
|
582
|
+
} else {
|
|
583
|
+
// 标签相同,更新属性
|
|
584
|
+
// 1. 移除旧属性
|
|
585
|
+
Array.from(oldEl.attributes).forEach((attr) => {
|
|
586
|
+
if (!newEl.hasAttribute(attr.name)) {
|
|
587
|
+
oldEl.removeAttribute(attr.name);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// 2. 设置/更新新属性
|
|
592
|
+
Array.from(newEl.attributes).forEach((attr) => {
|
|
593
|
+
if (oldEl.getAttribute(attr.name) !== attr.value) {
|
|
594
|
+
oldEl.setAttribute(attr.name, attr.value);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// 3. 特殊处理:className 是 property,不是 attribute
|
|
599
|
+
if (oldEl.className !== newEl.className) {
|
|
600
|
+
oldEl.className = newEl.className;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 4. 特殊处理:对于 input 元素,更新 checked 和 value 属性
|
|
604
|
+
if (oldEl instanceof HTMLInputElement && newEl instanceof HTMLInputElement) {
|
|
605
|
+
if (oldEl.checked !== newEl.checked) {
|
|
606
|
+
oldEl.checked = newEl.checked;
|
|
607
|
+
}
|
|
608
|
+
if (oldEl.value !== newEl.value) {
|
|
609
|
+
oldEl.value = newEl.value;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// 5. 递归更新子元素
|
|
614
|
+
reconcileElement(oldEl, newEl);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
@@ -207,7 +207,8 @@ export function replaceOrInsertElementAtPosition(
|
|
|
207
207
|
parent: HTMLElement | SVGElement,
|
|
208
208
|
newChild: HTMLElement | SVGElement,
|
|
209
209
|
oldNode: Node | null,
|
|
210
|
-
targetNextSibling: Node | null
|
|
210
|
+
targetNextSibling: Node | null,
|
|
211
|
+
processedNodes?: Set<Node>
|
|
211
212
|
): void {
|
|
212
213
|
// 如果新元素已经在其他父元素中,先移除
|
|
213
214
|
if (newChild.parentNode && newChild.parentNode !== parent) {
|
|
@@ -215,11 +216,15 @@ export function replaceOrInsertElementAtPosition(
|
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
// 检查元素是否已经在正确位置
|
|
219
|
+
// 1. 如果 newChild.nextSibling === targetNextSibling,说明在正确位置的前面
|
|
220
|
+
// 2. 关键修复:如果 targetNextSibling === newChild,说明 newChild 就在目标位置(它本身就是下一个节点)
|
|
218
221
|
const isInCorrectPosition =
|
|
219
|
-
newChild.parentNode === parent &&
|
|
222
|
+
newChild.parentNode === parent &&
|
|
223
|
+
(newChild.nextSibling === targetNextSibling || targetNextSibling === newChild);
|
|
220
224
|
|
|
221
225
|
if (isInCorrectPosition) {
|
|
222
226
|
// 已经在正确位置,不需要移动
|
|
227
|
+
if (processedNodes) processedNodes.add(newChild);
|
|
223
228
|
return;
|
|
224
229
|
}
|
|
225
230
|
|
|
@@ -227,25 +232,40 @@ export function replaceOrInsertElementAtPosition(
|
|
|
227
232
|
if (newChild.parentNode === parent) {
|
|
228
233
|
// insertBefore 会自动处理:如果元素已经在 DOM 中,会先移除再插入到新位置
|
|
229
234
|
parent.insertBefore(newChild, targetNextSibling);
|
|
235
|
+
if (processedNodes) processedNodes.add(newChild);
|
|
230
236
|
return;
|
|
231
237
|
}
|
|
232
238
|
|
|
233
|
-
// 元素不在 parent
|
|
239
|
+
// 元素不在 parent 中,或者需要替换
|
|
234
240
|
if (oldNode && oldNode.parentNode === parent && !shouldPreserveElement(oldNode)) {
|
|
235
241
|
if (oldNode !== newChild) {
|
|
242
|
+
// RFC 0048 & RFC 0053 关键修正:特殊处理从 HTML 字符串解析出的元素
|
|
243
|
+
// 如果旧节点和新节点都没有 cache key (说明是 HTML 解析出的) 且内容相同
|
|
244
|
+
// 则保留旧节点,不进行替换。这解决了 Markdown 渲染中的 DOM 抖动。
|
|
245
|
+
const oldCacheKey = getElementCacheKey(oldNode as HTMLElement | SVGElement);
|
|
246
|
+
const newCacheKey = getElementCacheKey(newChild);
|
|
247
|
+
|
|
248
|
+
if (!oldCacheKey && !newCacheKey && (oldNode as any).__wsxManaged === true) {
|
|
249
|
+
const oldTag = (oldNode as HTMLElement | SVGElement).tagName.toLowerCase();
|
|
250
|
+
const newTag = newChild.tagName.toLowerCase();
|
|
251
|
+
if (oldTag === newTag && oldNode.textContent === newChild.textContent) {
|
|
252
|
+
// 内容相同,逻辑上旧节点已经“处理”过了
|
|
253
|
+
if (processedNodes) processedNodes.add(oldNode);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
236
258
|
// 关键修复:在替换元素之前,标记旧元素内的所有文本节点为已处理
|
|
237
|
-
// 这样可以防止在移除阶段被误删
|
|
238
|
-
// 注意:这里只标记直接子文本节点,不递归处理嵌套元素内的文本节点
|
|
239
|
-
// 因为 replaceChild 会一起处理元素及其所有子节点
|
|
240
259
|
parent.replaceChild(newChild, oldNode);
|
|
260
|
+
if (processedNodes) processedNodes.add(newChild);
|
|
261
|
+
} else {
|
|
262
|
+
// 已经是同一个节点
|
|
263
|
+
if (processedNodes) processedNodes.add(newChild);
|
|
241
264
|
}
|
|
242
265
|
} else {
|
|
243
|
-
//
|
|
244
|
-
// 如果 newChild 已经在 parent 中,不应该再次插入
|
|
245
|
-
// 如果 parent 中已经存在相同标签名和内容的元素,也不应该插入
|
|
246
|
-
// 这样可以防止重复插入相同的元素(特别是从 HTML 字符串解析而来的元素)
|
|
266
|
+
// ... 继续原有的插入逻辑 ...
|
|
247
267
|
if (newChild.parentNode === parent) {
|
|
248
|
-
//
|
|
268
|
+
// 已经在正确位置,不需要再次插入
|
|
249
269
|
return;
|
|
250
270
|
}
|
|
251
271
|
// RFC 0048 关键修复:检查是否已经存在相同内容的元素
|
|
@@ -272,6 +292,13 @@ export function replaceOrInsertElementAtPosition(
|
|
|
272
292
|
) {
|
|
273
293
|
// 找到相同内容的元素(且都没有 cache key),不需要插入 newChild
|
|
274
294
|
// 这是从 HTML 字符串解析而来的重复元素
|
|
295
|
+
// 关键修复:必须将其标记为已处理,否则会被 shouldRemoveNode 移除
|
|
296
|
+
console.log(
|
|
297
|
+
"[WSX Debug] Found duplicate content, keeping existing:",
|
|
298
|
+
existingNode.tagName,
|
|
299
|
+
existingNode.textContent
|
|
300
|
+
);
|
|
301
|
+
if (processedNodes) processedNodes.add(existingNode);
|
|
275
302
|
return;
|
|
276
303
|
}
|
|
277
304
|
}
|
|
@@ -405,11 +432,11 @@ export function shouldRemoveNode(
|
|
|
405
432
|
|
|
406
433
|
const isProcessed = processedNodes && processedNodes.has(node);
|
|
407
434
|
|
|
408
|
-
if (
|
|
435
|
+
if (isProcessed) {
|
|
409
436
|
return false;
|
|
410
437
|
}
|
|
411
438
|
|
|
412
|
-
if (node
|
|
439
|
+
if (shouldPreserveElement(node)) {
|
|
413
440
|
return false;
|
|
414
441
|
}
|
|
415
442
|
|