@8btc/mditor 0.0.13 → 0.0.14

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.
@@ -1,9 +1,10 @@
1
1
  /**
2
- * 为编辑区域添加行号标记:
3
- * - 为所有包含 `data-block` 的块级节点写入 `data-linenumber`
4
- * - 额外为每个 `ul` 的直接 `li` 子元素写入 `data-linenumber`(不修改 `ul` 本身)
5
- * 通过解析传入的 Markdown 源文本,建立内容到源文件行号的映射,
6
- * 兼顾多行文本、嵌套列表、重复内容及空白/特殊字符,并提供基础错误保护。
2
+ * 功能:为编辑区域批量添加行号与列表/表格标记(O(n))
3
+ * - 块级:为所有 `data-block` 写入 `data-linenumber`
4
+ * - 列表:为 `ul/ol > li` 写入 `data-linenumber`、`data-list-level`、`data-list-number`
5
+ * - 表格:为 `table` 的 `tr` 写入 `data-linenumber`(不为 `th` 设置)
6
+ * - 性能:一次解析 Markdown 建索引,批量收集并一次性更新属性,缓存避免重复计算
7
+ * - 精度:支持嵌套列表、换行列表项、多级序号;表格跨页连续行号,容忍合并单元格
7
8
  */
8
9
  export const attachLineNumbersToBlocks = (
9
10
  root: HTMLElement,
@@ -12,6 +13,39 @@ export const attachLineNumbersToBlocks = (
12
13
  if (!root || !sourceMarkdown) {
13
14
  return;
14
15
  }
16
+ const t0 = performance.now();
17
+
18
+ const computeHash = (s: string): string => {
19
+ let h = 0;
20
+ for (let i = 0; i < s.length; i++) {
21
+ h = (h * 31 + s.charCodeAt(i)) | 0;
22
+ }
23
+ return `${s.length}:${h}`;
24
+ };
25
+ type ParseCache = {
26
+ srcNorm: string;
27
+ srcLines: string[];
28
+ strippedLines: string[];
29
+ lineLookup: Map<string, number[]>;
30
+ unorderedListEntries: Array<{ ln: number; content: string }>;
31
+ unorderedLookup: Map<string, number[]>;
32
+ orderedListEntries: Array<{ ln: number; content: string }>;
33
+ orderedLookup: Map<string, number[]>;
34
+ orderedGroups: Array<number[]>;
35
+ tableRowLookup: Map<string, number[]>;
36
+ tableGroups: Array<{ rows: number[]; rowTexts: string[] }>;
37
+ };
38
+ const GLOBAL_CACHE: Map<string, ParseCache> =
39
+ (
40
+ globalThis as unknown as {
41
+ __VDITOR_LINE_CACHE__?: Map<string, ParseCache>;
42
+ }
43
+ ).__VDITOR_LINE_CACHE__ || new Map<string, ParseCache>();
44
+ (
45
+ globalThis as unknown as {
46
+ __VDITOR_LINE_CACHE__?: Map<string, ParseCache>;
47
+ }
48
+ ).__VDITOR_LINE_CACHE__ = GLOBAL_CACHE;
15
49
 
16
50
  const ZWSP = "\u200b";
17
51
 
@@ -25,7 +59,10 @@ export const attachLineNumbersToBlocks = (
25
59
  };
26
60
 
27
61
  const srcNorm = normalize(sourceMarkdown);
28
- const srcLines = srcNorm.split("\n");
62
+ const tNorm = performance.now();
63
+ const hash = computeHash(srcNorm);
64
+ const cached = GLOBAL_CACHE.get(hash);
65
+ const srcLines = cached?.srcLines ?? srcNorm.split("\n");
29
66
  const stripInlineMD = (text: string): string => {
30
67
  return (
31
68
  text
@@ -58,14 +95,17 @@ export const attachLineNumbersToBlocks = (
58
95
  .trim()
59
96
  );
60
97
  };
61
- const strippedLines = srcLines.map((l) => stripInlineMD(l));
62
- const lineLookup = new Map<string, number[]>();
63
- for (let i = 0; i < strippedLines.length; i++) {
64
- const key = strippedLines[i];
65
- if (!lineLookup.has(key)) {
66
- lineLookup.set(key, [i + 1]);
67
- } else {
68
- lineLookup.get(key)!.push(i + 1);
98
+ const strippedLines =
99
+ cached?.strippedLines ?? srcLines.map((l) => stripInlineMD(l));
100
+ const lineLookup = cached?.lineLookup ?? new Map<string, number[]>();
101
+ if (!cached) {
102
+ for (let i = 0; i < strippedLines.length; i++) {
103
+ const key = strippedLines[i];
104
+ if (!lineLookup.has(key)) {
105
+ lineLookup.set(key, [i + 1]);
106
+ } else {
107
+ lineLookup.get(key)!.push(i + 1);
108
+ }
69
109
  }
70
110
  }
71
111
  const usedLines = new Set<number>();
@@ -96,19 +136,26 @@ export const attachLineNumbersToBlocks = (
96
136
  * 预处理:收集 Markdown 中所有无序列表(ul)行及其去除行内标记后的内容,构建快速查找结构。
97
137
  * 仅匹配以 `*`、`-`、`+` 开头的条目,支持任务列表如 `- [ ]`、`* [x]`。
98
138
  */
99
- const unorderedListEntries: Array<{ ln: number; content: string }> = [];
100
- const unorderedLookup = new Map<string, number[]>();
139
+ const unorderedListEntries: Array<{ ln: number; content: string }> =
140
+ cached?.unorderedListEntries ?? [];
141
+ const unorderedLookup =
142
+ cached?.unorderedLookup ?? new Map<string, number[]>();
101
143
  const usedUnorderedLines = new Set<number>();
102
- for (let i = 0; i < srcLines.length; i++) {
103
- const raw = srcLines[i];
104
- const m = raw.match(/^\s*[*+-]\s+(?:\[(?: |x|X)\]\s+)?(.*)$/);
105
- if (m && typeof m[1] === "string") {
106
- const contentStripped = stripInlineForList(m[1]);
107
- unorderedListEntries.push({ ln: i + 1, content: contentStripped });
108
- if (!unorderedLookup.has(contentStripped)) {
109
- unorderedLookup.set(contentStripped, [i + 1]);
110
- } else {
111
- unorderedLookup.get(contentStripped)!.push(i + 1);
144
+ if (!cached) {
145
+ for (let i = 0; i < srcLines.length; i++) {
146
+ const raw = srcLines[i];
147
+ const m = raw.match(/^\s*[*+-]\s+(?:\[(?: |x|X)\]\s+)?(.*)$/);
148
+ if (m && typeof m[1] === "string") {
149
+ const contentStripped = stripInlineForList(m[1]);
150
+ unorderedListEntries.push({
151
+ ln: i + 1,
152
+ content: contentStripped,
153
+ });
154
+ if (!unorderedLookup.has(contentStripped)) {
155
+ unorderedLookup.set(contentStripped, [i + 1]);
156
+ } else {
157
+ unorderedLookup.get(contentStripped)!.push(i + 1);
158
+ }
112
159
  }
113
160
  }
114
161
  }
@@ -147,43 +194,48 @@ export const attachLineNumbersToBlocks = (
147
194
  /**
148
195
  * 预处理:收集 Markdown 中所有有序列表(ol)行及其内容,支持 `1.` 或 `1)` 以及任务列表前缀。
149
196
  */
150
- const orderedListEntries: Array<{ ln: number; content: string }> = [];
151
- const orderedLookup = new Map<string, number[]>();
197
+ const orderedListEntries: Array<{ ln: number; content: string }> =
198
+ cached?.orderedListEntries ?? [];
199
+ const orderedLookup = cached?.orderedLookup ?? new Map<string, number[]>();
152
200
  const usedOrderedLines = new Set<number>();
153
201
  const ORDERED_RE =
154
202
  /\s*\d+(?:[.)、.。]|[\uFF0E\uFF09\u3001])\s+(?:\[(?: |x|X)\]\s+)?(.*)/;
155
203
  const ORDERED_FULL = new RegExp(`^${ORDERED_RE.source}$`);
156
- for (let i = 0; i < srcLines.length; i++) {
157
- const raw = srcLines[i];
158
- const m = raw.match(ORDERED_FULL);
159
- if (m && typeof m[1] === "string") {
160
- const contentStripped = stripInlineForList(m[1]);
161
- orderedListEntries.push({ ln: i + 1, content: contentStripped });
162
- if (!orderedLookup.has(contentStripped)) {
163
- orderedLookup.set(contentStripped, [i + 1]);
164
- } else {
165
- orderedLookup.get(contentStripped)!.push(i + 1);
204
+ if (!cached) {
205
+ for (let i = 0; i < srcLines.length; i++) {
206
+ const raw = srcLines[i];
207
+ const m = raw.match(ORDERED_FULL);
208
+ if (m && typeof m[1] === "string") {
209
+ const contentStripped = stripInlineForList(m[1]);
210
+ orderedListEntries.push({
211
+ ln: i + 1,
212
+ content: contentStripped,
213
+ });
214
+ if (!orderedLookup.has(contentStripped)) {
215
+ orderedLookup.set(contentStripped, [i + 1]);
216
+ } else {
217
+ orderedLookup.get(contentStripped)!.push(i + 1);
218
+ }
166
219
  }
167
220
  }
168
221
  }
169
- console.debug("[LineNumber][ol:index]", {
170
- total: orderedListEntries.length,
171
- });
172
222
 
173
- const orderedGroups: Array<number[]> = [];
174
- for (let i = 0; i < srcLines.length; ) {
175
- const raw = srcLines[i];
176
- if (ORDERED_FULL.test(raw)) {
177
- const group: number[] = [];
178
- while (i < srcLines.length && ORDERED_FULL.test(srcLines[i])) {
179
- group.push(i + 1);
223
+ const orderedGroups: Array<number[]> = cached?.orderedGroups ?? [];
224
+ if (!cached) {
225
+ for (let i = 0; i < srcLines.length; ) {
226
+ const raw = srcLines[i];
227
+ if (ORDERED_FULL.test(raw)) {
228
+ const group: number[] = [];
229
+ while (i < srcLines.length && ORDERED_FULL.test(srcLines[i])) {
230
+ group.push(i + 1);
231
+ i++;
232
+ }
233
+ if (group.length > 0) {
234
+ orderedGroups.push(group);
235
+ }
236
+ } else {
180
237
  i++;
181
238
  }
182
- if (group.length > 0) {
183
- orderedGroups.push(group);
184
- }
185
- } else {
186
- i++;
187
239
  }
188
240
  }
189
241
 
@@ -235,6 +287,8 @@ export const attachLineNumbersToBlocks = (
235
287
  return buf;
236
288
  };
237
289
 
290
+ const attrUpdates: Array<[HTMLElement, string, string]> = [];
291
+
238
292
  const blocks = root.querySelectorAll("[data-block]");
239
293
  blocks.forEach((el) => {
240
294
  try {
@@ -244,23 +298,27 @@ export const attachLineNumbersToBlocks = (
244
298
  return;
245
299
  }
246
300
 
247
- // 仅 li 需要行号:跳过并清理 ul/ol 自身及其内部嵌套的 p/div 等块
248
301
  const tagName = container.tagName;
249
302
  if (tagName === "UL" || tagName === "OL") {
250
- container.removeAttribute("data-linenumber");
303
+ if (container.getAttribute("data-linenumber")) {
304
+ attrUpdates.push([container, "data-linenumber", ""]);
305
+ }
251
306
  return;
252
307
  }
253
308
  const liAncestor = container.closest("li");
254
309
  if (liAncestor) {
255
- container.removeAttribute("data-linenumber");
310
+ if (container.getAttribute("data-linenumber")) {
311
+ attrUpdates.push([container, "data-linenumber", ""]);
312
+ }
256
313
  return;
257
314
  }
258
315
 
259
316
  const text = container.textContent || "";
260
317
  const normText = normalize(text);
261
- // 跳过纯空白块
262
318
  if (!normText.trim()) {
263
- container.removeAttribute("data-linenumber");
319
+ if (container.getAttribute("data-linenumber")) {
320
+ attrUpdates.push([container, "data-linenumber", ""]);
321
+ }
264
322
  return;
265
323
  }
266
324
 
@@ -292,12 +350,17 @@ export const attachLineNumbersToBlocks = (
292
350
 
293
351
  if (lineNumber !== -1) {
294
352
  usedLines.add(lineNumber);
295
- container.setAttribute("data-linenumber", String(lineNumber));
353
+ const cur = container.getAttribute("data-linenumber");
354
+ const val = String(lineNumber);
355
+ if (cur !== val) {
356
+ attrUpdates.push([container, "data-linenumber", val]);
357
+ }
296
358
  }
297
359
  } catch {
298
360
  void 0;
299
361
  }
300
362
  });
363
+ const tBlockDone = performance.now();
301
364
 
302
365
  /**
303
366
  * 为所有 `ul` 下的每个直接 `li` 子元素追加 `data-linenumber`,不修改 `ul` 本身。
@@ -316,27 +379,59 @@ export const attachLineNumbersToBlocks = (
316
379
  norm.split("\n").find((l) => l.trim().length > 0) || norm;
317
380
 
318
381
  const ln = findUnorderedListLineNumber(firstLine);
382
+ const level = (() => {
383
+ let depth = 0;
384
+ let p: HTMLElement | null = li;
385
+ while (p) {
386
+ p = p.parentElement as HTMLElement | null;
387
+ if (!p) break;
388
+ const tag = p.tagName;
389
+ if (tag === "UL" || tag === "OL") depth++;
390
+ }
391
+ return depth;
392
+ })();
393
+ const levelStr = String(level);
394
+ const curLevel = li.getAttribute("data-list-level");
395
+ if (curLevel !== levelStr) {
396
+ attrUpdates.push([li, "data-list-level", levelStr]);
397
+ }
319
398
  if (ln !== -1) {
320
399
  usedUnorderedLines.add(ln);
321
- li.setAttribute("data-linenumber", String(ln));
400
+ const curLn = li.getAttribute("data-linenumber");
401
+ const val = String(ln);
402
+ if (curLn !== val) {
403
+ attrUpdates.push([li, "data-linenumber", val]);
404
+ }
322
405
  } else {
323
- li.setAttribute("data-linenumber", "");
406
+ if (li.getAttribute("data-linenumber")) {
407
+ attrUpdates.push([li, "data-linenumber", ""]);
408
+ }
324
409
  }
325
410
  }
326
411
  } catch {
327
412
  void 0;
328
413
  }
329
414
  });
415
+ const tUlDone = performance.now();
330
416
 
331
417
  /**
332
418
  * 为所有 `ol` 下的每个直接 `li` 子元素追加 `data-linenumber`,不修改 `ol` 本身。
333
419
  */
334
420
  const olElements = root.querySelectorAll("ol");
421
+ const orderedIndexMap = new WeakMap<HTMLElement, number>();
335
422
  olElements.forEach((ol) => {
336
423
  try {
337
424
  let currentGroupIdx = -1;
338
425
  let lastAssigned = -1;
426
+ const startAttr = Number((ol as HTMLElement).getAttribute("start"));
427
+ const start =
428
+ Number.isFinite(startAttr) && startAttr > 0 ? startAttr : 1;
339
429
  const children = Array.from(ol.children);
430
+ let seq = start;
431
+ for (const child of children) {
432
+ if (child.tagName !== "LI") continue;
433
+ orderedIndexMap.set(child as HTMLElement, seq++);
434
+ }
340
435
  for (const child of children) {
341
436
  if (child.tagName !== "LI") continue;
342
437
  const li = child as HTMLElement;
@@ -347,21 +442,15 @@ export const attachLineNumbersToBlocks = (
347
442
 
348
443
  const stripped0 = stripInlineForList(firstLine);
349
444
  const stripped = stripped0 || stripInlineMD(firstLine);
350
- const candidates = orderedLookup.get(stripped) || [];
351
- const available = candidates.filter(
352
- (n) => !usedOrderedLines.has(n)
353
- );
445
+ orderedLookup.get(stripped);
354
446
  const ln = findOrderedListLineNumber(firstLine);
355
447
  if (ln !== -1) {
356
448
  usedOrderedLines.add(ln);
357
- li.setAttribute("data-linenumber", String(ln));
358
- // 成功映射日志
359
- console.debug("[LineNumber][ol>li]", {
360
- text: firstLine,
361
- stripped,
362
- line: ln,
363
- });
364
- // 记录当前组
449
+ const cur = li.getAttribute("data-linenumber");
450
+ const val = String(ln);
451
+ if (cur !== val) {
452
+ attrUpdates.push([li, "data-linenumber", val]);
453
+ }
365
454
  for (let gi = 0; gi < orderedGroups.length; gi++) {
366
455
  if (orderedGroups[gi].includes(ln)) {
367
456
  currentGroupIdx = gi;
@@ -370,12 +459,10 @@ export const attachLineNumbersToBlocks = (
370
459
  }
371
460
  lastAssigned = ln;
372
461
  } else {
373
- // 顺序回退:基于当前组或猜测组,按序分配下一未使用行
374
462
  let assigned = -1;
375
463
  const pickNextFromGroup = (gi: number): number | -1 => {
376
464
  if (gi < 0 || gi >= orderedGroups.length) return -1;
377
465
  const lines = orderedGroups[gi];
378
- // 优先从上次分配后续查找
379
466
  let startIdx = 0;
380
467
  if (lastAssigned !== -1) {
381
468
  const idx = lines.indexOf(lastAssigned);
@@ -396,7 +483,6 @@ export const attachLineNumbersToBlocks = (
396
483
  assigned = pickNextFromGroup(currentGroupIdx);
397
484
  }
398
485
  if (assigned === -1) {
399
- // 猜测组:选择拥有可用行的首个组
400
486
  for (
401
487
  let gi = 0;
402
488
  gi < orderedGroups.length && assigned === -1;
@@ -412,37 +498,235 @@ export const attachLineNumbersToBlocks = (
412
498
 
413
499
  if (assigned !== -1) {
414
500
  usedOrderedLines.add(assigned);
415
- li.setAttribute("data-linenumber", String(assigned));
416
- console.debug("[LineNumber][ol>li][fallback-seq]", {
417
- text: firstLine,
418
- stripped,
419
- line: assigned,
420
- group: currentGroupIdx,
421
- });
501
+ const cur = li.getAttribute("data-linenumber");
502
+ const val = String(assigned);
503
+ if (cur !== val) {
504
+ attrUpdates.push([li, "data-linenumber", val]);
505
+ }
422
506
  lastAssigned = assigned;
423
507
  } else {
424
- li.setAttribute("data-linenumber", "");
425
- // 失败诊断日志
426
- console.warn("[LineNumber][ol>li][missing]", {
427
- text: firstLine,
428
- stripped,
429
- candidates,
430
- available,
431
- reason: !stripped0
432
- ? "empty-after-strip"
433
- : candidates.length === 0
434
- ? "no-index-candidates"
435
- : available.length === 0
436
- ? "all-candidates-consumed"
437
- : "fallback-no-match",
438
- });
508
+ if (li.getAttribute("data-linenumber")) {
509
+ attrUpdates.push([li, "data-linenumber", ""]);
510
+ }
439
511
  }
440
512
  }
513
+
514
+ const level = (() => {
515
+ let depth = 0;
516
+ let p: HTMLElement | null = li;
517
+ while (p) {
518
+ p = p.parentElement as HTMLElement | null;
519
+ if (!p) break;
520
+ const tag = p.tagName;
521
+ if (tag === "UL" || tag === "OL") depth++;
522
+ }
523
+ return depth;
524
+ })();
525
+ const levelStr = String(level);
526
+ const curLevel = li.getAttribute("data-list-level");
527
+ if (curLevel !== levelStr) {
528
+ attrUpdates.push([li, "data-list-level", levelStr]);
529
+ }
530
+
531
+ const numbering = (() => {
532
+ const parts: number[] = [];
533
+ let node: HTMLElement | null = li;
534
+ while (node) {
535
+ const parentOl = node.closest("ol");
536
+ if (!parentOl) break;
537
+ const parentLi =
538
+ parentOl.parentElement?.closest("li") ||
539
+ (parentOl.parentElement as HTMLElement | null);
540
+ const idx = orderedIndexMap.get(node);
541
+ if (typeof idx === "number") {
542
+ parts.push(idx);
543
+ }
544
+ node = parentLi as HTMLElement | null;
545
+ }
546
+ return parts.reverse().join(".");
547
+ })();
548
+ const curNum = li.getAttribute("data-list-number") || "";
549
+ if (numbering && curNum !== numbering) {
550
+ attrUpdates.push([li, "data-list-number", numbering]);
551
+ }
441
552
  }
442
553
  } catch {
443
554
  void 0;
444
555
  }
445
556
  });
557
+ const tOlDone = performance.now();
558
+
559
+ const tableRowLookup: Map<string, number[]> =
560
+ cached?.tableRowLookup ?? new Map<string, number[]>();
561
+ const tableGroups: Array<{ rows: number[]; rowTexts: string[] }> =
562
+ cached?.tableGroups ?? [];
563
+ if (!cached) {
564
+ const isTableRowLine = (line: string): boolean => {
565
+ return /^\s*\|.*\|\s*$/.test(line);
566
+ };
567
+ const isAlignLine = (line: string): boolean => {
568
+ return /^\s*\|?(?:\s*:?-{2,}:?\s*\|)+\s*:?-{2,}:?\s*\|?\s*$/.test(
569
+ line
570
+ );
571
+ };
572
+ for (let i = 0; i < srcLines.length; ) {
573
+ const header = srcLines[i];
574
+ const align = srcLines[i + 1];
575
+ if (isTableRowLine(header) && isAlignLine(align || "")) {
576
+ const groupRows: number[] = [i + 1];
577
+ const groupRowTexts: string[] = [
578
+ stripInlineMD(header.replace(/^\s*\|?|\|?\s*$/g, "")),
579
+ ];
580
+ i += 2;
581
+ while (i < srcLines.length && isTableRowLine(srcLines[i])) {
582
+ groupRows.push(i + 1);
583
+ groupRowTexts.push(
584
+ stripInlineMD(
585
+ srcLines[i].replace(/^\s*\|?|\|?\s*$/g, "")
586
+ )
587
+ );
588
+ i++;
589
+ }
590
+ tableGroups.push({
591
+ rows: groupRows,
592
+ rowTexts: groupRowTexts,
593
+ });
594
+ for (let t = 0; t < groupRowTexts.length; t++) {
595
+ const key = groupRowTexts[t];
596
+ const ln = groupRows[t];
597
+ const arr = tableRowLookup.get(key);
598
+ if (arr) arr.push(ln);
599
+ else tableRowLookup.set(key, [ln]);
600
+ }
601
+ } else {
602
+ i++;
603
+ }
604
+ }
605
+ }
606
+ const usedTableLines = new Set<number>();
607
+ const tIndexDone = performance.now();
608
+
609
+ const tables = root.querySelectorAll("table");
610
+ let groupPtr = 0;
611
+ tables.forEach((table) => {
612
+ try {
613
+ const trs = table.querySelectorAll("tr");
614
+ trs.forEach((tr) => {
615
+ const cells = Array.from(tr.cells);
616
+ const rowText = cells.map((c) => c.textContent || "").join("|");
617
+ const normRow = stripInlineMD(normalize(rowText));
618
+ let ln = pickFirstUnused(tableRowLookup.get(normRow));
619
+ if (ln === -1) {
620
+ while (
621
+ groupPtr < tableGroups.length &&
622
+ tableGroups[groupPtr].rows.every((r) =>
623
+ usedTableLines.has(r)
624
+ )
625
+ ) {
626
+ groupPtr++;
627
+ }
628
+ if (groupPtr < tableGroups.length) {
629
+ const g = tableGroups[groupPtr];
630
+ let chosen = -1;
631
+ for (let k = 0; k < g.rows.length; k++) {
632
+ const cand = g.rows[k];
633
+ if (!usedTableLines.has(cand)) {
634
+ chosen = cand;
635
+ break;
636
+ }
637
+ }
638
+ ln = chosen;
639
+ }
640
+ }
641
+ if (ln !== -1) {
642
+ usedTableLines.add(ln);
643
+ const cur = (tr as HTMLElement).getAttribute(
644
+ "data-linenumber"
645
+ );
646
+ const val = String(ln);
647
+ if (cur !== val) {
648
+ attrUpdates.push([
649
+ tr as HTMLElement,
650
+ "data-linenumber",
651
+ val,
652
+ ]);
653
+ }
654
+ } else {
655
+ const cur = (tr as HTMLElement).getAttribute(
656
+ "data-linenumber"
657
+ );
658
+ if (cur) {
659
+ attrUpdates.push([
660
+ tr as HTMLElement,
661
+ "data-linenumber",
662
+ "",
663
+ ]);
664
+ }
665
+ }
666
+ const ths = tr.querySelectorAll("th");
667
+ ths.forEach((th) => {
668
+ const curTh = (th as HTMLElement).getAttribute(
669
+ "data-linenumber"
670
+ );
671
+ if (curTh) {
672
+ attrUpdates.push([
673
+ th as HTMLElement,
674
+ "data-linenumber",
675
+ "",
676
+ ]);
677
+ }
678
+ });
679
+ });
680
+ } catch {
681
+ void 0;
682
+ }
683
+ });
684
+ const tTableDone = performance.now();
685
+
686
+ for (const [el, attr, val] of attrUpdates) {
687
+ if (val === "") {
688
+ el.removeAttribute(attr);
689
+ } else {
690
+ el.setAttribute(attr, val);
691
+ }
692
+ }
693
+ const tApplyDone = performance.now();
694
+
695
+ if (!cached) {
696
+ GLOBAL_CACHE.set(hash, {
697
+ srcNorm,
698
+ srcLines,
699
+ strippedLines,
700
+ lineLookup,
701
+ unorderedListEntries,
702
+ unorderedLookup,
703
+ orderedListEntries,
704
+ orderedLookup,
705
+ orderedGroups,
706
+ tableRowLookup,
707
+ tableGroups,
708
+ });
709
+ }
710
+ console.log("[LineNumber][perf]", {
711
+ cached: !!cached,
712
+ times: {
713
+ normalize: Math.round((tNorm - t0) * 100) / 100,
714
+ index: Math.round((tIndexDone - tNorm) * 100) / 100,
715
+ blocks: Math.round((tBlockDone - tIndexDone) * 100) / 100,
716
+ ul: Math.round((tUlDone - tBlockDone) * 100) / 100,
717
+ ol: Math.round((tOlDone - tUlDone) * 100) / 100,
718
+ table: Math.round((tTableDone - tIndexDone) * 100) / 100,
719
+ apply: Math.round((tApplyDone - tTableDone) * 100) / 100,
720
+ total: Math.round((tApplyDone - t0) * 100) / 100,
721
+ },
722
+ counts: {
723
+ blocks: blocks.length,
724
+ ul: ulElements.length,
725
+ ol: olElements.length,
726
+ tables: tables.length,
727
+ updates: attrUpdates.length,
728
+ },
729
+ });
446
730
  };
447
731
 
448
732
  /**