@wsxjs/wsx-core 0.0.27 → 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.
@@ -15,6 +15,14 @@ export function collectPreservedElements(element: HTMLElement | SVGElement): Nod
15
15
  const preserved: Node[] = [];
16
16
  for (let i = 0; i < element.childNodes.length; i++) {
17
17
  const child = element.childNodes[i];
18
+
19
+ // RFC 0048 & RFC 0053 关键修复:文本节点通常不应该被 collectPreservedElements 捕获
20
+ // 因为它们会通过 updateChildren 的主循环进行 reconcile
21
+ // 只有当父元素本身是保留元素时,文本节点才应该被保留(但在 updateChildren 中,这种情况会跳过处理)
22
+ if (child.nodeType === Node.TEXT_NODE) {
23
+ continue;
24
+ }
25
+
18
26
  if (shouldPreserveElement(child)) {
19
27
  preserved.push(child);
20
28
  }
@@ -77,20 +85,20 @@ export function findElementNode(
77
85
  */
78
86
  export function findTextNode(
79
87
  parent: HTMLElement | SVGElement,
80
- domIndex: { value: number }
88
+ domIndex: { value: number },
89
+ processedNodes: Set<Node>
81
90
  ): Node | null {
82
- while (domIndex.value < parent.childNodes.length) {
83
- const node = parent.childNodes[domIndex.value];
84
- // 关键修复:只检查直接子文本节点,确保 node.parentNode === parent
85
- // 这样可以防止将元素内部的文本节点(如 span 内部的文本节点)误判为独立的文本节点
86
- if (node.nodeType === Node.TEXT_NODE && node.parentNode === parent) {
87
- const textNode = node;
88
- domIndex.value++;
89
- return textNode;
91
+ // RFC 0053: 从当前索引开始查找第一个未被处理的、框架管理的文本节点
92
+ for (let i = domIndex.value; i < parent.childNodes.length; i++) {
93
+ const node = parent.childNodes[i];
94
+ if (
95
+ node.nodeType === Node.TEXT_NODE &&
96
+ (node as any).__wsxManaged === true &&
97
+ !processedNodes.has(node)
98
+ ) {
99
+ domIndex.value = i + 1;
100
+ return node;
90
101
  }
91
- // 跳过元素节点和其他类型的节点(它们会在自己的迭代中处理)
92
- // 关键:必须递增 domIndex,否则会无限循环
93
- domIndex.value++;
94
102
  }
95
103
  return null;
96
104
  }
@@ -117,45 +125,52 @@ export function shouldUpdateTextNode(
117
125
  export function updateOrCreateTextNode(
118
126
  parent: HTMLElement | SVGElement,
119
127
  oldNode: Node | null,
120
- newText: string
128
+ newText: string,
129
+ insertBeforeNode?: Node | null
121
130
  ): Node {
131
+ // RFC 0048 & RFC 0053 关键修复:如果 parent 是保留元素(第三方组件),跳过处理
132
+ // 防止框架错误地管理第三方组件内部的文本节点
133
+ if (shouldPreserveElement(parent)) {
134
+ // 如果 parent 是保留元素,直接返回 oldNode(如果存在)或创建新节点但不插入
135
+ // 注意:这种情况下,文本节点应该由第三方组件自己管理
136
+ if (oldNode && oldNode.nodeType === Node.TEXT_NODE) {
137
+ return oldNode;
138
+ }
139
+ // 对于保留元素,我们不应该插入文本节点,但为了兼容性,返回一个虚拟节点
140
+ // 实际上,这种情况不应该发生,因为保留元素的子节点不应该被框架处理
141
+ return document.createTextNode(newText);
142
+ }
143
+
122
144
  if (oldNode && oldNode.nodeType === Node.TEXT_NODE) {
123
145
  // 只有当文本内容不同时才更新
124
146
  if (oldNode.textContent !== newText) {
125
147
  oldNode.textContent = newText;
126
148
  }
127
- return oldNode;
128
- } else {
129
- // RFC 0048 关键修复:如果 oldNode 为 null,先检查是否已经存在相同内容的直接子文本节点
130
- // 这样可以防止重复创建文本节点
131
- // 只检查直接子文本节点,不检查元素内部的文本节点
132
- if (!oldNode) {
133
- for (let i = 0; i < parent.childNodes.length; i++) {
134
- const node = parent.childNodes[i];
135
- // 关键修复:只检查直接子文本节点,确保 node.parentNode === parent
136
- // 这样可以防止将元素内部的文本节点(如 span 内部的文本节点)误判为独立的文本节点
137
- if (
138
- node.nodeType === Node.TEXT_NODE &&
139
- node.parentNode === parent &&
140
- node.textContent === newText
141
- ) {
142
- // 找到相同内容的直接子文本节点,返回它而不是创建新节点
143
- return node;
144
- }
149
+ // RFC 0053 扩展:确保节点在正确的位置
150
+ // 如果指定了插入位置且当前节点不在该位置,则移动它
151
+ if (insertBeforeNode !== undefined) {
152
+ if (oldNode !== insertBeforeNode && oldNode.nextSibling !== insertBeforeNode) {
153
+ parent.insertBefore(oldNode, insertBeforeNode);
145
154
  }
146
155
  }
156
+ return oldNode;
157
+ } else {
158
+ // RFC 0048 & RFC 0053 关键修复:文本节点应该基于位置匹配,而不是内容匹配
159
+ // 当 oldNode 为 null 时,直接创建新节点,不进行内容匹配搜索
160
+ // 这防止了日历等场景中相同内容在不同位置被错误匹配的问题
161
+ // 例如:3 月的 "30" 在底部行,4 月的第一个日期是 "1",不应该将 "30" 错误匹配到 "1" 的位置
162
+ // 如果 oldNode 为 null,说明在当前位置没有找到对应的文本节点,应该创建新节点
147
163
 
148
- // 如果没有找到相同内容的文本节点,创建新节点
149
164
  const newTextNode = document.createTextNode(newText);
165
+ (newTextNode as any).__wsxManaged = true; // 标记为框架管理
150
166
  if (oldNode && !shouldPreserveElement(oldNode)) {
151
167
  parent.replaceChild(newTextNode, oldNode);
152
168
  } else {
153
- parent.insertBefore(newTextNode, oldNode || null);
169
+ parent.insertBefore(newTextNode, insertBeforeNode ?? null);
154
170
  }
155
171
  return newTextNode;
156
172
  }
157
173
  }
158
-
159
174
  /**
160
175
  * 移除节点(如果不应该保留)
161
176
  */
@@ -192,7 +207,8 @@ export function replaceOrInsertElementAtPosition(
192
207
  parent: HTMLElement | SVGElement,
193
208
  newChild: HTMLElement | SVGElement,
194
209
  oldNode: Node | null,
195
- targetNextSibling: Node | null
210
+ targetNextSibling: Node | null,
211
+ processedNodes?: Set<Node>
196
212
  ): void {
197
213
  // 如果新元素已经在其他父元素中,先移除
198
214
  if (newChild.parentNode && newChild.parentNode !== parent) {
@@ -200,11 +216,15 @@ export function replaceOrInsertElementAtPosition(
200
216
  }
201
217
 
202
218
  // 检查元素是否已经在正确位置
219
+ // 1. 如果 newChild.nextSibling === targetNextSibling,说明在正确位置的前面
220
+ // 2. 关键修复:如果 targetNextSibling === newChild,说明 newChild 就在目标位置(它本身就是下一个节点)
203
221
  const isInCorrectPosition =
204
- newChild.parentNode === parent && newChild.nextSibling === targetNextSibling;
222
+ newChild.parentNode === parent &&
223
+ (newChild.nextSibling === targetNextSibling || targetNextSibling === newChild);
205
224
 
206
225
  if (isInCorrectPosition) {
207
226
  // 已经在正确位置,不需要移动
227
+ if (processedNodes) processedNodes.add(newChild);
208
228
  return;
209
229
  }
210
230
 
@@ -212,25 +232,40 @@ export function replaceOrInsertElementAtPosition(
212
232
  if (newChild.parentNode === parent) {
213
233
  // insertBefore 会自动处理:如果元素已经在 DOM 中,会先移除再插入到新位置
214
234
  parent.insertBefore(newChild, targetNextSibling);
235
+ if (processedNodes) processedNodes.add(newChild);
215
236
  return;
216
237
  }
217
238
 
218
- // 元素不在 parent 中,需要插入或替换
239
+ // 元素不在 parent 中,或者需要替换
219
240
  if (oldNode && oldNode.parentNode === parent && !shouldPreserveElement(oldNode)) {
220
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
+
221
258
  // 关键修复:在替换元素之前,标记旧元素内的所有文本节点为已处理
222
- // 这样可以防止在移除阶段被误删
223
- // 注意:这里只标记直接子文本节点,不递归处理嵌套元素内的文本节点
224
- // 因为 replaceChild 会一起处理元素及其所有子节点
225
259
  parent.replaceChild(newChild, oldNode);
260
+ if (processedNodes) processedNodes.add(newChild);
261
+ } else {
262
+ // 已经是同一个节点
263
+ if (processedNodes) processedNodes.add(newChild);
226
264
  }
227
265
  } else {
228
- // RFC 0048 关键修复:在插入元素之前,检查是否已经存在相同内容的元素
229
- // 如果 newChild 已经在 parent 中,不应该再次插入
230
- // 如果 parent 中已经存在相同标签名和内容的元素,也不应该插入
231
- // 这样可以防止重复插入相同的元素(特别是从 HTML 字符串解析而来的元素)
266
+ // ... 继续原有的插入逻辑 ...
232
267
  if (newChild.parentNode === parent) {
233
- // 元素已经在 parent 中,不需要再次插入
268
+ // 已经在正确位置,不需要再次插入
234
269
  return;
235
270
  }
236
271
  // RFC 0048 关键修复:检查是否已经存在相同内容的元素
@@ -257,6 +292,13 @@ export function replaceOrInsertElementAtPosition(
257
292
  ) {
258
293
  // 找到相同内容的元素(且都没有 cache key),不需要插入 newChild
259
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);
260
302
  return;
261
303
  }
262
304
  }
@@ -281,6 +323,7 @@ export function appendNewChild(
281
323
 
282
324
  if (typeof child === "string" || typeof child === "number") {
283
325
  const newTextNode = document.createTextNode(String(child));
326
+ (newTextNode as any).__wsxManaged = true; // 标记为框架管理
284
327
  parent.appendChild(newTextNode);
285
328
  // 关键修复:标记新创建的文本节点为已处理,防止在移除阶段被误删
286
329
  if (processedNodes) {
@@ -305,6 +348,13 @@ export function appendNewChild(
305
348
  processedNodes.add(child);
306
349
  }
307
350
  } else if (child instanceof DocumentFragment) {
351
+ // 关键修复:记录 Fragment 中的所有子节点为已处理
352
+ // 否则它们会被后续的 collectNodesToRemove 误删
353
+ if (processedNodes) {
354
+ for (let i = 0; i < child.childNodes.length; i++) {
355
+ processedNodes.add(child.childNodes[i]);
356
+ }
357
+ }
308
358
  parent.appendChild(child);
309
359
  }
310
360
  }
@@ -347,32 +397,49 @@ export function shouldRemoveNode(
347
397
  cacheKeyMap: Map<string, HTMLElement | SVGElement>,
348
398
  processedNodes?: Set<Node>
349
399
  ): boolean {
350
- // 保留的元素不移除
351
- if (shouldPreserveElement(node)) {
352
- return false;
400
+ // RFC 0048 & RFC 0053 关键修复:文本节点应该由框架管理,不应该被 shouldPreserveElement 保留
401
+ // 文本节点本身不是元素,shouldPreserveElement 对文本节点总是返回 true
402
+ // 但是,如果文本节点的父元素是由框架管理的,文本节点也应该由框架管理
403
+ // 只有当文本节点的父元素是保留元素时,文本节点才应该被保留
404
+ if (node.nodeType === Node.TEXT_NODE) {
405
+ // 如果是框架管理的文本节点
406
+ if ((node as any).__wsxManaged === true) {
407
+ // 如果已被处理过,保留
408
+ if (processedNodes && processedNodes.has(node)) {
409
+ return false;
410
+ }
411
+ // 否则移除
412
+ return true;
413
+ }
414
+
415
+ // 如果是非框架管理的文本节点(第三方注入)
416
+ // 检查其父元素是否被保留
417
+ const parent = node.parentNode;
418
+ if (parent && (parent instanceof HTMLElement || parent instanceof SVGElement)) {
419
+ if (shouldPreserveElement(parent)) {
420
+ // 如果父元素是保留元素,文本节点也应该被保留
421
+ return false;
422
+ }
423
+ }
424
+ // 对于未标记且父元素未保留的文本,默认移除以防止泄漏
425
+ return true;
426
+ } else {
427
+ // 对于元素节点,使用原有的逻辑
428
+ if (shouldPreserveElement(node)) {
429
+ return false;
430
+ }
353
431
  }
354
432
 
355
- // 关键修复:如果文本节点已被处理,不应该移除
356
- if (node.nodeType === Node.TEXT_NODE && processedNodes && processedNodes.has(node)) {
433
+ const isProcessed = processedNodes && processedNodes.has(node);
434
+
435
+ if (isProcessed) {
357
436
  return false;
358
437
  }
359
438
 
360
- // 关键修复:如果文本节点在已处理的元素内部,不应该移除
361
- // 这样可以防止在元素被替换时,元素内部的文本节点被误删
362
- // 但是,只有当父元素还在当前 parent 的子树中时才检查
363
- if (node.nodeType === Node.TEXT_NODE && processedNodes) {
364
- let parent = node.parentNode;
365
- while (parent) {
366
- // 关键修复:只有当父元素还在 DOM 中并且在当前 parent 的子树中时才检查
367
- // 因为如果父元素被 replaceChild 移除了,它就不在 DOM 中了
368
- if (processedNodes.has(parent) && parent.parentNode) {
369
- return false; // 文本节点在已处理的元素内部,不应该移除
370
- }
371
- parent = parent.parentNode;
372
- }
439
+ if (shouldPreserveElement(node)) {
440
+ return false;
373
441
  }
374
442
 
375
- // 检查是否在新子元素集合中(通过引用)
376
443
  if (
377
444
  node instanceof HTMLElement ||
378
445
  node instanceof SVGElement ||
@@ -382,7 +449,6 @@ export function shouldRemoveNode(
382
449
  return false;
383
450
  }
384
451
 
385
- // 检查是否通过 cache key 匹配
386
452
  if (node instanceof HTMLElement || node instanceof SVGElement) {
387
453
  const cacheKey = getElementCacheKey(node);
388
454
  if (cacheKey && cacheKeyMap.has(cacheKey)) {
@@ -446,7 +512,8 @@ export function collectNodesToRemove(
446
512
 
447
513
  for (let i = 0; i < parent.childNodes.length; i++) {
448
514
  const node = parent.childNodes[i];
449
- if (shouldRemoveNode(node, elementSet, cacheKeyMap, processedNodes)) {
515
+ const removed = shouldRemoveNode(node, elementSet, cacheKeyMap, processedNodes);
516
+ if (removed) {
450
517
  nodesToRemove.push(node);
451
518
  }
452
519
  }
@@ -225,9 +225,10 @@ export abstract class WebComponent extends BaseComponent {
225
225
  this.shadowRoot.appendChild(content);
226
226
  }
227
227
 
228
- // 移除旧内容(保留样式元素和未标记元素)
229
- // 关键修复:使用 shouldPreserveElement() 来保护第三方库注入的元素
230
- const oldChildren = Array.from(this.shadowRoot.children).filter((child) => {
228
+ // 移除旧内容(保留样式元素、未标记元素和新渲染的内容)
229
+ // 关键修复 (RFC-0042): 使用 childNodes 而不是 children,确保文本节点也被清理
230
+ // 否则,在 Shadow DOM 根部的文本节点会发生泄漏
231
+ const oldNodes = Array.from(this.shadowRoot.childNodes).filter((child) => {
231
232
  // 保留新添加的内容(或已经在 shadowRoot 中的 content)
232
233
  if (child === content) {
233
234
  return false;
@@ -237,13 +238,13 @@ export abstract class WebComponent extends BaseComponent {
237
238
  return false;
238
239
  }
239
240
  // 保留未标记的元素(第三方库注入的元素、自定义元素)
240
- // 这是 RFC 0037 Phase 5 的核心:保护未标记元素
241
+ // 对于文本节点,shouldPreserveElement 逻辑已在 element-marking.ts 中优化
241
242
  if (shouldPreserveElement(child)) {
242
243
  return false;
243
244
  }
244
245
  return true;
245
246
  });
246
- oldChildren.forEach((child) => child.remove());
247
+ oldNodes.forEach((node) => node.remove());
247
248
 
248
249
  // 7. 恢复 adopted stylesheets(在 DOM 操作之后,确保样式不被意外移除)
249
250
  // 关键修复:在 DOM 操作之后恢复样式,防止样式在 DOM 操作过程中被意外清空