@adeu/core 1.13.0 → 1.15.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.
package/dist/index.cjs CHANGED
@@ -1333,7 +1333,12 @@ ${header}`;
1333
1333
  this._add_virtual_text(prefix, current, item);
1334
1334
  current += prefix.length;
1335
1335
  }
1336
- current = this._map_paragraph_content(item, current, style_cache, default_pstyle);
1336
+ current = this._map_paragraph_content(
1337
+ item,
1338
+ current,
1339
+ style_cache,
1340
+ default_pstyle
1341
+ );
1337
1342
  is_first_para = false;
1338
1343
  previous_item = item;
1339
1344
  } else if (item instanceof Table) {
@@ -1406,7 +1411,13 @@ ${header}`;
1406
1411
  }
1407
1412
  _map_paragraph_content(paragraph, start_offset, style_cache, default_pstyle) {
1408
1413
  let current = start_offset;
1409
- const span = { start: current, end: current, text: "", run: null, paragraph };
1414
+ const span = {
1415
+ start: current,
1416
+ end: current,
1417
+ text: "",
1418
+ run: null,
1419
+ paragraph
1420
+ };
1410
1421
  this.spans.push(span);
1411
1422
  const active_ids = /* @__PURE__ */ new Set();
1412
1423
  const active_ins = {};
@@ -1424,7 +1435,7 @@ ${header}`;
1424
1435
  this._add_virtual_text(s_tok, current, paragraph);
1425
1436
  current += s_tok.length;
1426
1437
  }
1427
- for (const [kind, txt, r_obj, i_id, d_id] of pending_runs) {
1438
+ for (const [kind, txt, r_obj, i_id, d_id, c_ids] of pending_runs) {
1428
1439
  if (kind === "virtual") {
1429
1440
  this._add_virtual_text(txt, current, paragraph, active_hyperlink_id);
1430
1441
  } else {
@@ -1436,7 +1447,8 @@ ${header}`;
1436
1447
  paragraph,
1437
1448
  ins_id: i_id || void 0,
1438
1449
  del_id: d_id || void 0,
1439
- hyperlink_id: active_hyperlink_id || void 0
1450
+ hyperlink_id: active_hyperlink_id || void 0,
1451
+ comment_ids: c_ids.length > 0 ? c_ids : void 0
1440
1452
  };
1441
1453
  this.spans.push(s);
1442
1454
  this._text_chunks.push(txt);
@@ -1450,8 +1462,16 @@ ${header}`;
1450
1462
  pending_runs = [];
1451
1463
  };
1452
1464
  const items = Array.from(iter_paragraph_content(paragraph));
1453
- const is_heading = is_heading_paragraph(paragraph, style_cache, default_pstyle);
1454
- const native_heading = is_native_heading(paragraph, style_cache, default_pstyle);
1465
+ const is_heading = is_heading_paragraph(
1466
+ paragraph,
1467
+ style_cache,
1468
+ default_pstyle
1469
+ );
1470
+ const native_heading = is_native_heading(
1471
+ paragraph,
1472
+ style_cache,
1473
+ default_pstyle
1474
+ );
1455
1475
  let leading_strip_active = is_heading;
1456
1476
  for (let i = 0; i < items.length; i++) {
1457
1477
  const item = items[i];
@@ -1486,7 +1506,12 @@ ${header}`;
1486
1506
  const curr_ins_id = Object.keys(active_ins).pop() || null;
1487
1507
  const curr_del_id = Object.keys(active_del).pop() || null;
1488
1508
  if (full_seg_text && !(this.clean_view && curr_del_id) && !(this.original_view && curr_ins_id)) {
1489
- const new_wrappers = this.clean_view || this.original_view ? ["", ""] : this._get_wrappers(curr_ins_id, curr_del_id, active_ids, active_fmt);
1509
+ const new_wrappers = this.clean_view || this.original_view ? ["", ""] : this._get_wrappers(
1510
+ curr_ins_id,
1511
+ curr_del_id,
1512
+ active_ids,
1513
+ active_fmt
1514
+ );
1490
1515
  const new_style = [prefix, suffix];
1491
1516
  if (pending_runs.length > 0 && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
1492
1517
  let skip_leading_prefix = false;
@@ -1494,36 +1519,58 @@ ${header}`;
1494
1519
  pending_runs.pop();
1495
1520
  skip_leading_prefix = true;
1496
1521
  }
1522
+ const curr_comment_ids = Array.from(active_ids);
1497
1523
  for (const [kind, txt, r_obj] of run_parts) {
1498
1524
  if (skip_leading_prefix && kind === "virtual" && txt === new_style[0]) {
1499
1525
  skip_leading_prefix = false;
1500
1526
  continue;
1501
1527
  }
1502
- pending_runs.push([kind, txt, r_obj, curr_ins_id, curr_del_id]);
1528
+ pending_runs.push([
1529
+ kind,
1530
+ txt,
1531
+ r_obj,
1532
+ curr_ins_id,
1533
+ curr_del_id,
1534
+ curr_comment_ids
1535
+ ]);
1503
1536
  }
1504
1537
  current_style = new_style;
1505
1538
  } else {
1506
1539
  flush_pending_runs();
1507
1540
  current_wrappers = new_wrappers;
1508
1541
  current_style = new_style;
1542
+ const curr_comment_ids = Array.from(active_ids);
1509
1543
  for (const [kind, txt, r_obj] of run_parts) {
1510
- pending_runs.push([kind, txt, r_obj, curr_ins_id, curr_del_id]);
1544
+ pending_runs.push([
1545
+ kind,
1546
+ txt,
1547
+ r_obj,
1548
+ curr_ins_id,
1549
+ curr_del_id,
1550
+ curr_comment_ids
1551
+ ]);
1511
1552
  }
1512
1553
  }
1513
1554
  }
1514
1555
  if (!this.clean_view && !this.original_view) {
1515
1556
  const has_meta = Object.keys(active_ins).length > 0 || Object.keys(active_del).length > 0 || active_ids.size > 0 || Object.keys(active_fmt).length > 0;
1516
1557
  if (has_meta) {
1517
- deferred_meta_states.push([{ ...active_ins }, { ...active_del }, new Set(active_ids), { ...active_fmt }]);
1558
+ deferred_meta_states.push([
1559
+ { ...active_ins },
1560
+ { ...active_del },
1561
+ new Set(active_ids),
1562
+ { ...active_fmt }
1563
+ ]);
1518
1564
  }
1519
1565
  let should_defer = false;
1520
- const is_redline = curr_ins_id !== null || curr_del_id !== null || Object.keys(active_fmt).length > 0;
1521
- if (is_redline) {
1566
+ const has_any_meta = curr_ins_id !== null || curr_del_id !== null || Object.keys(active_fmt).length > 0 || active_ids.size > 0;
1567
+ if (has_any_meta) {
1522
1568
  let j = i + 1;
1523
- let next_is_redline = false;
1569
+ let next_has_meta = false;
1524
1570
  let temp_ins_count = Object.keys(active_ins).length;
1525
1571
  let temp_del_count = Object.keys(active_del).length;
1526
1572
  let temp_fmt_count = Object.keys(active_fmt).length;
1573
+ const temp_comment_ids = new Set(active_ids);
1527
1574
  while (j < items.length) {
1528
1575
  const next_item = items[j];
1529
1576
  if (next_item instanceof Run) {
@@ -1531,22 +1578,27 @@ ${header}`;
1531
1578
  j++;
1532
1579
  continue;
1533
1580
  }
1534
- if (temp_ins_count > 0 || temp_del_count > 0 || temp_fmt_count > 0) {
1535
- next_is_redline = true;
1581
+ if (temp_ins_count > 0 || temp_del_count > 0 || temp_fmt_count > 0 || temp_comment_ids.size > 0) {
1582
+ next_has_meta = true;
1536
1583
  }
1537
1584
  break;
1538
1585
  } else {
1539
1586
  const ev = next_item;
1540
1587
  if (ev.type === "ins_start") temp_ins_count++;
1541
- else if (ev.type === "ins_end") temp_ins_count = Math.max(0, temp_ins_count - 1);
1588
+ else if (ev.type === "ins_end")
1589
+ temp_ins_count = Math.max(0, temp_ins_count - 1);
1542
1590
  else if (ev.type === "del_start") temp_del_count++;
1543
- else if (ev.type === "del_end") temp_del_count = Math.max(0, temp_del_count - 1);
1591
+ else if (ev.type === "del_end")
1592
+ temp_del_count = Math.max(0, temp_del_count - 1);
1544
1593
  else if (ev.type === "fmt_start") temp_fmt_count++;
1545
- else if (ev.type === "fmt_end") temp_fmt_count = Math.max(0, temp_fmt_count - 1);
1594
+ else if (ev.type === "fmt_end")
1595
+ temp_fmt_count = Math.max(0, temp_fmt_count - 1);
1596
+ else if (ev.type === "start") temp_comment_ids.add(ev.id);
1597
+ else if (ev.type === "end") temp_comment_ids.delete(ev.id);
1546
1598
  }
1547
1599
  j++;
1548
1600
  }
1549
- if (next_is_redline) should_defer = true;
1601
+ if (next_has_meta) should_defer = true;
1550
1602
  }
1551
1603
  if (!should_defer && deferred_meta_states.length > 0) {
1552
1604
  const meta_block = this._build_merged_meta_block(deferred_meta_states);
@@ -1635,7 +1687,8 @@ ${header}`;
1635
1687
  _get_wrappers(ins_id, del_id, active_ids, active_fmt) {
1636
1688
  if (del_id) return ["{--", "--}"];
1637
1689
  if (ins_id) return ["{++", "++}"];
1638
- if (active_ids.size > 0 || Object.keys(active_fmt).length > 0) return ["{==", "==}"];
1690
+ if (active_ids.size > 0 || Object.keys(active_fmt).length > 0)
1691
+ return ["{==", "==}"];
1639
1692
  return ["", ""];
1640
1693
  }
1641
1694
  _build_merged_meta_block(states_list) {
@@ -1643,7 +1696,9 @@ ${header}`;
1643
1696
  const comment_lines = [];
1644
1697
  const seen_sigs = /* @__PURE__ */ new Set();
1645
1698
  for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
1646
- for (const [uid, meta] of Object.entries(ins_map)) {
1699
+ for (const [uid, meta] of Object.entries(
1700
+ ins_map
1701
+ )) {
1647
1702
  const sig = `Chg:${uid}`;
1648
1703
  if (!seen_sigs.has(sig)) {
1649
1704
  const auth = meta.author || "Unknown";
@@ -1651,7 +1706,9 @@ ${header}`;
1651
1706
  seen_sigs.add(sig);
1652
1707
  }
1653
1708
  }
1654
- for (const [uid, meta] of Object.entries(del_map)) {
1709
+ for (const [uid, meta] of Object.entries(
1710
+ del_map
1711
+ )) {
1655
1712
  const sig = `Chg:${uid}`;
1656
1713
  if (!seen_sigs.has(sig)) {
1657
1714
  const auth = meta.author || "Unknown";
@@ -1659,7 +1716,9 @@ ${header}`;
1659
1716
  seen_sigs.add(sig);
1660
1717
  }
1661
1718
  }
1662
- for (const [uid, meta] of Object.entries(fmt_map)) {
1719
+ for (const [uid, meta] of Object.entries(
1720
+ fmt_map
1721
+ )) {
1663
1722
  const sig = `Chg:${uid}`;
1664
1723
  if (!seen_sigs.has(sig)) {
1665
1724
  const auth = meta.author || "Unknown";
@@ -1733,7 +1792,16 @@ ${header}`;
1733
1792
  if (remaining) parts.push(escapeRegExp(remaining));
1734
1793
  return parts.join("");
1735
1794
  }
1736
- find_match_index(target_text) {
1795
+ find_match_index(target_text, is_regex = false) {
1796
+ if (is_regex) {
1797
+ try {
1798
+ const pattern = new RegExp(target_text);
1799
+ const match = pattern.exec(this.full_text);
1800
+ if (match) return [match.index, match[0].length];
1801
+ } catch (e) {
1802
+ }
1803
+ return [-1, 0];
1804
+ }
1737
1805
  let start_idx = this.full_text.indexOf(target_text);
1738
1806
  if (start_idx !== -1) return [start_idx, target_text.length];
1739
1807
  const norm_full = this._replace_smart_quotes(this.full_text);
@@ -1753,22 +1821,41 @@ ${header}`;
1753
1821
  }
1754
1822
  return [-1, 0];
1755
1823
  }
1756
- find_all_match_indices(target_text) {
1824
+ find_all_match_indices(target_text, is_regex = false) {
1757
1825
  if (!target_text) return [];
1826
+ if (is_regex) {
1827
+ try {
1828
+ const pattern = new RegExp(target_text, "g");
1829
+ const matches2 = [...this.full_text.matchAll(pattern)];
1830
+ if (matches2.length > 0)
1831
+ return matches2.map((m) => [m.index, m[0].length]);
1832
+ } catch (e) {
1833
+ }
1834
+ return [];
1835
+ }
1758
1836
  const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1759
- let matches = [...this.full_text.matchAll(new RegExp(escapeRegExp(target_text), "g"))];
1837
+ let matches = [
1838
+ ...this.full_text.matchAll(new RegExp(escapeRegExp(target_text), "g"))
1839
+ ];
1760
1840
  if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1761
1841
  const norm_full = this._replace_smart_quotes(this.full_text);
1762
1842
  const norm_target = this._replace_smart_quotes(target_text);
1763
- matches = [...norm_full.matchAll(new RegExp(escapeRegExp(norm_target), "g"))];
1843
+ matches = [
1844
+ ...norm_full.matchAll(new RegExp(escapeRegExp(norm_target), "g"))
1845
+ ];
1764
1846
  if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1765
1847
  const stripped_target = this._strip_markdown_formatting(target_text);
1766
- matches = [...this.full_text.matchAll(new RegExp(escapeRegExp(stripped_target), "g"))];
1848
+ matches = [
1849
+ ...this.full_text.matchAll(
1850
+ new RegExp(escapeRegExp(stripped_target), "g")
1851
+ )
1852
+ ];
1767
1853
  if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1768
1854
  try {
1769
1855
  const pattern = new RegExp(this._make_fuzzy_regex(target_text), "g");
1770
1856
  matches = [...this.full_text.matchAll(pattern)];
1771
- if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1857
+ if (matches.length > 0)
1858
+ return matches.map((m) => [m.index, m[0].length]);
1772
1859
  } catch (e) {
1773
1860
  }
1774
1861
  return [];
@@ -1779,14 +1866,22 @@ ${header}`;
1779
1866
  return this._resolve_runs_at_range(start_idx, start_idx + length);
1780
1867
  }
1781
1868
  find_target_runs_by_index(start_index, length, rebuild_map = true) {
1782
- return this._resolve_runs_at_range(start_index, start_index + length, rebuild_map);
1869
+ return this._resolve_runs_at_range(
1870
+ start_index,
1871
+ start_index + length,
1872
+ rebuild_map
1873
+ );
1783
1874
  }
1784
1875
  get_virtual_spans_in_range(start_index, length) {
1785
1876
  const end_index = start_index + length;
1786
- return this.spans.filter((s) => s.run === null && s.text === "\n\n" && s.start >= start_index && s.end <= end_index);
1877
+ return this.spans.filter(
1878
+ (s) => s.run === null && s.text === "\n\n" && s.start >= start_index && s.end <= end_index
1879
+ );
1787
1880
  }
1788
1881
  _resolve_runs_at_range(start_idx, end_idx, rebuild_map = true) {
1789
- const affected_spans = this.spans.filter((s) => s.end > start_idx && s.start < end_idx);
1882
+ const affected_spans = this.spans.filter(
1883
+ (s) => s.end > start_idx && s.start < end_idx
1884
+ );
1790
1885
  if (affected_spans.length === 0) return [];
1791
1886
  const working_runs = affected_spans.filter((s) => s.run !== null).map((s) => s.run);
1792
1887
  if (working_runs.length === 0) return [];
@@ -1797,7 +1892,10 @@ ${header}`;
1797
1892
  const local_start = start_idx - first_real_span.start;
1798
1893
  if (local_start > 0) {
1799
1894
  const idx_in_working = 0;
1800
- const [, right_run] = this._split_run_at_index(working_runs[idx_in_working], local_start);
1895
+ const [, right_run] = this._split_run_at_index(
1896
+ working_runs[idx_in_working],
1897
+ local_start
1898
+ );
1801
1899
  working_runs[idx_in_working] = right_run;
1802
1900
  dom_modified = true;
1803
1901
  start_split_adjustment = local_start;
@@ -1834,7 +1932,9 @@ ${header}`;
1834
1932
  if (preceding[i].paragraph) return [null, preceding[i].paragraph];
1835
1933
  }
1836
1934
  }
1837
- const containing = this.spans.filter((s) => s.start < index && index < s.end);
1935
+ const containing = this.spans.filter(
1936
+ (s) => s.start < index && index < s.end
1937
+ );
1838
1938
  if (containing.length > 0) {
1839
1939
  const span = containing[0];
1840
1940
  if (span.run === null) {
@@ -1857,10 +1957,12 @@ ${header}`;
1857
1957
  const preceding_gap = this.spans.filter((s) => s.end < index);
1858
1958
  if (preceding_gap.length > 0) {
1859
1959
  for (let i = preceding_gap.length - 1; i >= 0; i--) {
1860
- if (preceding_gap[i].run) return [preceding_gap[i].run, preceding_gap[i].paragraph];
1960
+ if (preceding_gap[i].run)
1961
+ return [preceding_gap[i].run, preceding_gap[i].paragraph];
1861
1962
  }
1862
1963
  for (let i = preceding_gap.length - 1; i >= 0; i--) {
1863
- if (preceding_gap[i].paragraph) return [null, preceding_gap[i].paragraph];
1964
+ if (preceding_gap[i].paragraph)
1965
+ return [null, preceding_gap[i].paragraph];
1864
1966
  }
1865
1967
  }
1866
1968
  return [null, null];
@@ -1873,7 +1975,10 @@ ${header}`;
1873
1975
  const new_r_element = run._element.cloneNode(true);
1874
1976
  this._set_run_text_elements(new_r_element, right_text);
1875
1977
  if (run._element.parentNode) {
1876
- run._element.parentNode.insertBefore(new_r_element, run._element.nextSibling);
1978
+ run._element.parentNode.insertBefore(
1979
+ new_r_element,
1980
+ run._element.nextSibling
1981
+ );
1877
1982
  }
1878
1983
  const new_run = new Run(new_r_element, run._parent);
1879
1984
  return [run, new_run];
@@ -1900,7 +2005,9 @@ ${header}`;
1900
2005
  }
1901
2006
  }
1902
2007
  get_context_at_range(start_idx, end_idx) {
1903
- const real_spans = this.spans.filter((s) => s.run && s.end > start_idx && s.start < end_idx);
2008
+ const real_spans = this.spans.filter(
2009
+ (s) => s.run && s.end > start_idx && s.start < end_idx
2010
+ );
1904
2011
  if (real_spans.length > 0) return real_spans[0];
1905
2012
  return null;
1906
2013
  }
@@ -2252,6 +2359,193 @@ function create_word_patch_diff(original_text, modified_text, original_path = "O
2252
2359
  return output.join("\n");
2253
2360
  }
2254
2361
 
2362
+ // src/pagination.ts
2363
+ var PAGE_TARGET_CHARS = 19e3;
2364
+ var APPENDIX_MARKER = "<!-- READONLY_BOUNDARY_START -->";
2365
+ var _CRITIC_TOKENS = {
2366
+ "{++": "++}",
2367
+ "{--": "--}",
2368
+ "{==": "==}",
2369
+ "{>>": "<<}"
2370
+ };
2371
+ var _CHG_ID_PATTERN = /\bChg:(\d+)\b/g;
2372
+ function split_structural_appendix(markdown) {
2373
+ if (!markdown) return ["", ""];
2374
+ const idx = markdown.indexOf(APPENDIX_MARKER);
2375
+ if (idx === -1) return [markdown, ""];
2376
+ const line_start = markdown.lastIndexOf("\n", idx) + 1;
2377
+ const body = markdown.substring(0, line_start).trimEnd();
2378
+ const appendix = markdown.substring(line_start);
2379
+ return [body, appendix];
2380
+ }
2381
+ function paginate(markdown_body, structural_appendix = "") {
2382
+ if (!markdown_body) {
2383
+ const appendix_clean = structural_appendix ? structural_appendix.trim() : "";
2384
+ const content = appendix_clean;
2385
+ return {
2386
+ pages: [{
2387
+ page: 1,
2388
+ total_pages: 1,
2389
+ has_next: false,
2390
+ has_prev: false,
2391
+ tracked_change_count: _count_tracked_changes(content),
2392
+ page_content: content
2393
+ }],
2394
+ total_pages: 1,
2395
+ body_pages: [""],
2396
+ body_page_offsets: [0]
2397
+ };
2398
+ }
2399
+ const block_records = _tokenize_into_atomic_blocks(markdown_body);
2400
+ const [body_pages, body_page_offsets] = _assemble_pages(block_records);
2401
+ let final_pages;
2402
+ if (structural_appendix && structural_appendix.trim()) {
2403
+ const appendix = structural_appendix.trim();
2404
+ final_pages = body_pages.map((bp) => bp ? `${bp}
2405
+
2406
+ ${appendix}` : appendix);
2407
+ } else {
2408
+ final_pages = [...body_pages];
2409
+ }
2410
+ const total = final_pages.length;
2411
+ const page_infos = final_pages.map((content, i) => ({
2412
+ page: i + 1,
2413
+ total_pages: total,
2414
+ has_next: i + 1 < total,
2415
+ has_prev: i + 1 > 1,
2416
+ tracked_change_count: _count_tracked_changes(content),
2417
+ page_content: content
2418
+ }));
2419
+ return {
2420
+ pages: page_infos,
2421
+ total_pages: total,
2422
+ body_pages,
2423
+ body_page_offsets
2424
+ };
2425
+ }
2426
+ function _tokenize_into_atomic_blocks(markdown_body) {
2427
+ const raw_blocks = _split_on_safe_paragraph_breaks(markdown_body);
2428
+ return _merge_footnote_sections(raw_blocks);
2429
+ }
2430
+ function _split_on_safe_paragraph_breaks(text) {
2431
+ const counters = { "++}": 0, "--}": 0, "==}": 0, "<<}": 0 };
2432
+ const blocks = [];
2433
+ let block_start = 0;
2434
+ let i = 0;
2435
+ const n = text.length;
2436
+ while (i < n) {
2437
+ let matched_open = false;
2438
+ for (const [open_tok, close_tok] of Object.entries(_CRITIC_TOKENS)) {
2439
+ if (text.startsWith(open_tok, i)) {
2440
+ counters[close_tok]++;
2441
+ i += open_tok.length;
2442
+ matched_open = true;
2443
+ break;
2444
+ }
2445
+ }
2446
+ if (matched_open) continue;
2447
+ let matched_close = false;
2448
+ for (const close_tok of Object.values(_CRITIC_TOKENS)) {
2449
+ if (text.startsWith(close_tok, i)) {
2450
+ if (counters[close_tok] > 0) counters[close_tok]--;
2451
+ i += close_tok.length;
2452
+ matched_close = true;
2453
+ break;
2454
+ }
2455
+ }
2456
+ if (matched_close) continue;
2457
+ if (text[i] === "\n" && i + 1 < n && text[i + 1] === "\n") {
2458
+ if (Object.values(counters).every((c) => c === 0)) {
2459
+ const block_text = text.substring(block_start, i);
2460
+ if (block_text) blocks.push([block_text, block_start]);
2461
+ let j = i;
2462
+ while (j < n && text[j] === "\n") j++;
2463
+ i = j;
2464
+ block_start = i;
2465
+ continue;
2466
+ }
2467
+ }
2468
+ i++;
2469
+ }
2470
+ if (block_start < n) {
2471
+ const block_text = text.substring(block_start, n);
2472
+ if (block_text) blocks.push([block_text, block_start]);
2473
+ }
2474
+ return blocks;
2475
+ }
2476
+ function _merge_footnote_sections(blocks) {
2477
+ if (!blocks.length) return blocks;
2478
+ const merged = [];
2479
+ let i = 0;
2480
+ while (i < blocks.length) {
2481
+ const [block_text, block_offset] = blocks[i];
2482
+ const stripped = block_text.trimStart();
2483
+ const is_section_header = stripped.startsWith("## Footnotes") || stripped.startsWith("## Endnotes");
2484
+ if (!is_section_header) {
2485
+ merged.push([block_text, block_offset]);
2486
+ i++;
2487
+ continue;
2488
+ }
2489
+ let accumulated_text = block_text;
2490
+ let j = i + 1;
2491
+ while (j < blocks.length) {
2492
+ const [next_text] = blocks[j];
2493
+ const next_stripped = next_text.trimStart();
2494
+ if (next_stripped.startsWith("[^fn-") || next_stripped.startsWith("[^en-")) {
2495
+ accumulated_text = `${accumulated_text}
2496
+
2497
+ ${next_text}`;
2498
+ j++;
2499
+ } else {
2500
+ break;
2501
+ }
2502
+ }
2503
+ merged.push([accumulated_text, block_offset]);
2504
+ i = j;
2505
+ }
2506
+ return merged;
2507
+ }
2508
+ function _assemble_pages(block_records) {
2509
+ if (!block_records.length) return [[""], [0]];
2510
+ const pages = [];
2511
+ const page_starts = [];
2512
+ let current_blocks = [];
2513
+ let current_size = 0;
2514
+ let current_start = -1;
2515
+ const flush_current = () => {
2516
+ if (current_blocks.length > 0) {
2517
+ pages.push(current_blocks.join("\n\n"));
2518
+ page_starts.push(current_start);
2519
+ }
2520
+ current_blocks = [];
2521
+ current_size = 0;
2522
+ current_start = -1;
2523
+ };
2524
+ for (const [block_text, block_offset] of block_records) {
2525
+ const block_size = block_text.length;
2526
+ const added_size = block_size + (current_blocks.length > 0 ? 2 : 0);
2527
+ if (current_blocks.length > 0 && current_size + added_size > PAGE_TARGET_CHARS) {
2528
+ flush_current();
2529
+ }
2530
+ if (current_blocks.length === 0 && block_size > PAGE_TARGET_CHARS) {
2531
+ pages.push(block_text);
2532
+ page_starts.push(block_offset);
2533
+ continue;
2534
+ }
2535
+ if (current_blocks.length === 0) current_start = block_offset;
2536
+ current_blocks.push(block_text);
2537
+ current_size += current_size > 0 ? added_size : block_size;
2538
+ }
2539
+ flush_current();
2540
+ if (!pages.length) return [[""], [0]];
2541
+ return [pages, page_starts];
2542
+ }
2543
+ function _count_tracked_changes(page_content) {
2544
+ const matches = [...page_content.matchAll(_CHG_ID_PATTERN)];
2545
+ const distinct = new Set(matches.map((m) => m[1]));
2546
+ return distinct.size;
2547
+ }
2548
+
2255
2549
  // src/markup.ts
2256
2550
  var AMBIGUITY_EXAMPLES_CAP = 5;
2257
2551
  var AMBIGUITY_CONTEXT_CHARS = 50;
@@ -2731,7 +3025,8 @@ var RedlineEngine = class {
2731
3025
  start_idx + length,
2732
3026
  start_idx + length + 30
2733
3027
  );
2734
- const critic_markup = `${context_before}{--${target_text}--}{++${new_text}++}${context_after}`;
3028
+ const insertion = new_text ? `{++${new_text}++}` : "";
3029
+ const critic_markup = `${context_before}{--${target_text}--}${insertion}${context_after}`;
2735
3030
  let clean_text = critic_markup;
2736
3031
  clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
2737
3032
  clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
@@ -2749,6 +3044,37 @@ var RedlineEngine = class {
2749
3044
  }
2750
3045
  return maxId;
2751
3046
  }
3047
+ _get_heading_path_and_page(start_idx, text, page_offsets) {
3048
+ let page = 1;
3049
+ for (let i = 0; i < page_offsets.length; i++) {
3050
+ if (start_idx >= page_offsets[i]) {
3051
+ page = i + 1;
3052
+ } else {
3053
+ break;
3054
+ }
3055
+ }
3056
+ const textBefore = text.substring(0, start_idx);
3057
+ const lines = textBefore.split("\n");
3058
+ const path = [];
3059
+ let current_level = 999;
3060
+ for (let i = lines.length - 1; i >= 0; i--) {
3061
+ const line = lines[i];
3062
+ const m = line.match(/^(#{1,6})\s+(.*)/);
3063
+ if (m) {
3064
+ const level = m[1].length;
3065
+ if (level < current_level) {
3066
+ let cleanHeading = m[2].replace(/\*\*|__|[*_]/g, "").replace(/\{#[^}]+\}/g, "").trim();
3067
+ if (cleanHeading.length > 80) {
3068
+ cleanHeading = cleanHeading.substring(0, 80) + "...";
3069
+ }
3070
+ path.unshift(cleanHeading);
3071
+ current_level = level;
3072
+ if (level === 1) break;
3073
+ }
3074
+ }
3075
+ }
3076
+ return [path.join(" > "), page];
3077
+ }
2752
3078
  accept_all_revisions() {
2753
3079
  const parts_to_process = [this.doc.element];
2754
3080
  for (const part of this.doc.pkg.parts) {
@@ -3447,13 +3773,15 @@ var RedlineEngine = class {
3447
3773
  for (let i = 0; i < edits.length; i++) {
3448
3774
  const edit = edits[i];
3449
3775
  if (!edit.target_text) continue;
3450
- let matches = this.mapper.find_all_match_indices(edit.target_text);
3776
+ const is_regex = edit.regex || false;
3777
+ const match_mode = edit.match_mode || "strict";
3778
+ let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
3451
3779
  let activeText = this.mapper.full_text;
3452
3780
  let target_mapper = this.mapper;
3453
3781
  if (matches.length === 0) {
3454
3782
  if (!this.clean_mapper)
3455
3783
  this.clean_mapper = new DocumentMapper(this.doc, true);
3456
- matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3784
+ matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
3457
3785
  if (matches.length > 0) {
3458
3786
  activeText = this.clean_mapper.full_text;
3459
3787
  target_mapper = this.clean_mapper;
@@ -3475,7 +3803,7 @@ var RedlineEngine = class {
3475
3803
  if (!this.original_mapper) {
3476
3804
  this.original_mapper = new DocumentMapper(this.doc, false, true);
3477
3805
  }
3478
- const orig_matches = this.original_mapper.find_all_match_indices(edit.target_text);
3806
+ const orig_matches = this.original_mapper.find_all_match_indices(edit.target_text, is_regex);
3479
3807
  if (orig_matches.length > 0) {
3480
3808
  is_deleted_text = true;
3481
3809
  for (const [start, length] of orig_matches) {
@@ -3512,7 +3840,7 @@ var RedlineEngine = class {
3512
3840
  "${edit.target_text}"`
3513
3841
  );
3514
3842
  }
3515
- } else if (matches.length > 1) {
3843
+ } else if (matches.length > 1 && match_mode === "strict") {
3516
3844
  const positions = matches.map(([start, length]) => [
3517
3845
  start,
3518
3846
  start + length
@@ -3570,6 +3898,14 @@ var RedlineEngine = class {
3570
3898
  if (auth && auth !== this.author) nestedAuthors.add(auth);
3571
3899
  }
3572
3900
  }
3901
+ if (s.comment_ids) {
3902
+ for (const cid of s.comment_ids) {
3903
+ const c_data = this.mapper.comments_map[cid];
3904
+ if (c_data && c_data.author && c_data.author !== this.author) {
3905
+ nestedAuthors.add(c_data.author);
3906
+ }
3907
+ }
3908
+ }
3573
3909
  }
3574
3910
  if (nestedAuthors.size > 0) {
3575
3911
  errors.push(
@@ -3618,6 +3954,22 @@ var RedlineEngine = class {
3618
3954
  return errors;
3619
3955
  }
3620
3956
  process_batch(changes, dry_run = false) {
3957
+ if (Array.isArray(changes)) {
3958
+ changes = changes.map((item) => {
3959
+ if (typeof item === "string") {
3960
+ try {
3961
+ const parsed = JSON.parse(item);
3962
+ if (parsed !== null && typeof parsed === "object") {
3963
+ return parsed;
3964
+ }
3965
+ return item;
3966
+ } catch {
3967
+ return item;
3968
+ }
3969
+ }
3970
+ return item;
3971
+ });
3972
+ }
3621
3973
  if (dry_run) {
3622
3974
  const baselines = /* @__PURE__ */ new Map();
3623
3975
  for (const part of this.doc.pkg.parts) {
@@ -3684,6 +4036,9 @@ var RedlineEngine = class {
3684
4036
  if (this.clean_mapper) this.clean_mapper["_build_map"]();
3685
4037
  }
3686
4038
  }
4039
+ const [body_text] = split_structural_appendix(this.mapper.full_text);
4040
+ const pag_res = paginate(body_text, "");
4041
+ const page_offsets = pag_res.body_page_offsets;
3687
4042
  const edits_reports = [];
3688
4043
  let applied_edits = 0;
3689
4044
  let skipped_edits = 0;
@@ -3707,9 +4062,8 @@ var RedlineEngine = class {
3707
4062
  });
3708
4063
  continue;
3709
4064
  }
3710
- const res = this.apply_edits([edit]);
3711
- const applied = res[0];
3712
- if (applied > 0) {
4065
+ const res = this.apply_edits([edit], page_offsets);
4066
+ if (edit._applied_status) {
3713
4067
  applied_edits++;
3714
4068
  const previews = this._build_edit_context_previews(edit);
3715
4069
  edits_reports.push({
@@ -3719,7 +4073,11 @@ var RedlineEngine = class {
3719
4073
  warning,
3720
4074
  error: null,
3721
4075
  critic_markup: previews[0],
3722
- clean_text: previews[1]
4076
+ clean_text: previews[1],
4077
+ pages: edit._pages || [],
4078
+ heading_path: edit._heading_path || "",
4079
+ occurrences_modified: edit._occurrences_modified || 0,
4080
+ match_mode: edit.match_mode || "strict"
3723
4081
  });
3724
4082
  } else {
3725
4083
  skipped_edits++;
@@ -3741,9 +4099,9 @@ var RedlineEngine = class {
3741
4099
  throw new BatchValidationError(errors);
3742
4100
  }
3743
4101
  const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
3744
- const res = this.apply_edits(cloned_edits);
3745
- applied_edits = res[0];
3746
- skipped_edits = res[1];
4102
+ const res = this.apply_edits(cloned_edits, page_offsets);
4103
+ applied_edits = cloned_edits.filter((e) => e._applied_status).length;
4104
+ skipped_edits = cloned_edits.length - applied_edits;
3747
4105
  for (const edit of cloned_edits) {
3748
4106
  const success = edit._applied_status || false;
3749
4107
  const error_msg = edit._error_msg || null;
@@ -3764,7 +4122,11 @@ var RedlineEngine = class {
3764
4122
  warning,
3765
4123
  error: error_msg,
3766
4124
  critic_markup,
3767
- clean_text
4125
+ clean_text,
4126
+ pages: edit._pages || [],
4127
+ heading_path: edit._heading_path || "",
4128
+ occurrences_modified: edit._occurrences_modified || 0,
4129
+ match_mode: edit.match_mode || "strict"
3768
4130
  });
3769
4131
  }
3770
4132
  }
@@ -3780,9 +4142,13 @@ var RedlineEngine = class {
3780
4142
  version: "1.10.0"
3781
4143
  };
3782
4144
  }
3783
- apply_edits(edits) {
4145
+ apply_edits(edits, page_offsets = []) {
3784
4146
  let applied = 0;
3785
4147
  let skipped = 0;
4148
+ if (!page_offsets || page_offsets.length === 0) {
4149
+ const [body_text] = split_structural_appendix(this.mapper.full_text);
4150
+ page_offsets = paginate(body_text, "").body_page_offsets;
4151
+ }
3786
4152
  const resolved_edits = [];
3787
4153
  for (const edit of edits) {
3788
4154
  edit._applied_status = false;
@@ -3884,6 +4250,19 @@ var RedlineEngine = class {
3884
4250
  const parent = edit._parent_edit_ref;
3885
4251
  if (parent) {
3886
4252
  parent._applied_status = true;
4253
+ parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
4254
+ const [path, page] = this._get_heading_path_and_page(start, this.mapper.full_text, page_offsets);
4255
+ const pages = parent._pages || [];
4256
+ if (!pages.includes(page)) pages.unshift(page);
4257
+ parent._pages = pages;
4258
+ parent._heading_path = path;
4259
+ } else {
4260
+ edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
4261
+ const [path, page] = this._get_heading_path_and_page(start, this.mapper.full_text, page_offsets);
4262
+ const pages = edit._pages || [];
4263
+ if (!pages.includes(page)) pages.unshift(page);
4264
+ edit._pages = pages;
4265
+ edit._heading_path = path;
3887
4266
  }
3888
4267
  } else {
3889
4268
  skipped++;
@@ -4036,96 +4415,192 @@ var RedlineEngine = class {
4036
4415
  }
4037
4416
  return false;
4038
4417
  }
4039
- /**
4040
- * Returns the first match of `target_text` in the raw mapper that is NOT
4041
- * entirely contained within a tracked deletion (<w:del>). Tracked-deleted
4042
- * copies are not live, editable text, so an edit must resolve to a live
4043
- * occurrence even when a dead copy appears earlier in the document
4044
- * (BUG-23-5). Falls back to the plain first match when no live copy is
4045
- * found (e.g. fuzzy/normalized matches the span filter cannot align).
4046
- */
4047
- _first_live_match(target_text) {
4048
- const all = this.mapper.find_all_match_indices(target_text);
4049
- if (all.length <= 1) {
4050
- return this.mapper.find_match_index(target_text);
4051
- }
4052
- for (const [start, length] of all) {
4053
- const realSpans = this.mapper.spans.filter(
4054
- (s) => s.run !== null && s.end > start && s.start < start + length
4055
- );
4056
- if (realSpans.length === 0) return [start, length];
4057
- if (realSpans.some((s) => !s.del_id)) return [start, length];
4058
- }
4059
- return this.mapper.find_match_index(target_text);
4060
- }
4061
4418
  _pre_resolve_heuristic_edit(edit) {
4062
4419
  if (!edit.target_text) return null;
4063
- let [start_idx, match_len] = this._first_live_match(edit.target_text);
4420
+ const is_regex = edit.regex || false;
4421
+ const match_mode = edit.match_mode || "strict";
4422
+ let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
4064
4423
  let use_clean_map = false;
4065
- if (start_idx === -1) {
4424
+ if (matches.length === 0) {
4066
4425
  if (!this.clean_mapper)
4067
4426
  this.clean_mapper = new DocumentMapper(this.doc, true);
4068
- [start_idx, match_len] = this.clean_mapper.find_match_index(
4069
- edit.target_text
4070
- );
4071
- if (start_idx !== -1) use_clean_map = true;
4427
+ matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
4428
+ if (matches.length > 0) use_clean_map = true;
4072
4429
  else return null;
4073
4430
  }
4074
4431
  const active_mapper = use_clean_map ? this.clean_mapper : this.mapper;
4075
- const effective_new_text = edit.new_text || "";
4076
- const actual_doc_text = this.mapper.full_text.substring(
4077
- start_idx,
4078
- start_idx + match_len
4079
- );
4080
- if (actual_doc_text === effective_new_text || edit.target_text === effective_new_text) {
4081
- return {
4082
- type: "modify",
4083
- target_text: actual_doc_text,
4084
- new_text: actual_doc_text,
4085
- comment: edit.comment,
4086
- _match_start_index: start_idx,
4087
- _internal_op: "COMMENT_ONLY",
4088
- _active_mapper_ref: active_mapper
4089
- };
4090
- }
4091
- let effective_op = "";
4092
- let final_target = "";
4093
- let final_new = "";
4094
- let effective_start_idx = start_idx;
4095
- if (effective_new_text.startsWith(actual_doc_text)) {
4096
- effective_op = "INSERTION";
4097
- final_new = effective_new_text.substring(actual_doc_text.length);
4098
- effective_start_idx = start_idx + match_len;
4099
- } else {
4100
- const [prefix_len, suffix_len] = trim_common_context(
4101
- actual_doc_text,
4102
- effective_new_text
4432
+ let live_matches = [];
4433
+ for (const [s, match_len] of matches) {
4434
+ const realSpans = active_mapper.spans.filter(
4435
+ (span) => span.run !== null && span.end > s && span.start < s + match_len
4103
4436
  );
4104
- const t_end = actual_doc_text.length - suffix_len;
4105
- const n_end = effective_new_text.length - suffix_len;
4106
- final_target = actual_doc_text.substring(prefix_len, t_end);
4107
- final_new = effective_new_text.substring(prefix_len, n_end);
4108
- effective_start_idx = start_idx + prefix_len;
4109
- if (!final_target && final_new) effective_op = "INSERTION";
4110
- else if (final_target && !final_new) effective_op = "DELETION";
4111
- else if (final_target && final_new) effective_op = "MODIFICATION";
4112
- else effective_op = "COMMENT_ONLY";
4437
+ if (realSpans.length === 0 || realSpans.some((span) => !span.del_id)) {
4438
+ live_matches.push([s, match_len]);
4439
+ }
4440
+ }
4441
+ if (live_matches.length === 0) return null;
4442
+ if (match_mode === "strict" || match_mode === "first") {
4443
+ live_matches = live_matches.slice(0, 1);
4444
+ }
4445
+ const all_sub_edits = [];
4446
+ for (const [start_idx, match_len] of live_matches) {
4447
+ const actual_doc_text = active_mapper.full_text.substring(
4448
+ start_idx,
4449
+ start_idx + match_len
4450
+ );
4451
+ let current_effective_new_text = edit.new_text || "";
4452
+ if (is_regex && current_effective_new_text) {
4453
+ try {
4454
+ current_effective_new_text = actual_doc_text.replace(new RegExp(edit.target_text), current_effective_new_text);
4455
+ } catch (e) {
4456
+ }
4457
+ }
4458
+ const [edit_target_clean, edit_target_style] = this._parse_markdown_style(edit.target_text);
4459
+ const [edit_new_clean, edit_new_style] = this._parse_markdown_style(current_effective_new_text);
4460
+ if (edit_target_style !== edit_new_style) {
4461
+ const [actual_clean] = this._parse_markdown_style(actual_doc_text);
4462
+ const final_target2 = actual_clean;
4463
+ const final_new2 = edit_new_clean;
4464
+ const style_op = final_target2 === final_new2 ? "STYLE_ONLY" : "STYLE_AND_TEXT";
4465
+ const prefix_offset = actual_doc_text.indexOf(actual_clean);
4466
+ const effective_start_idx2 = start_idx + (prefix_offset !== -1 ? prefix_offset : 0);
4467
+ const resolved_style = edit_new_style !== null ? edit_new_style : "Normal";
4468
+ all_sub_edits.push({
4469
+ type: "modify",
4470
+ target_text: final_target2,
4471
+ new_text: final_new2,
4472
+ comment: edit.comment,
4473
+ _match_start_index: effective_start_idx2,
4474
+ _internal_op: style_op,
4475
+ _new_style: resolved_style,
4476
+ _active_mapper_ref: active_mapper
4477
+ });
4478
+ continue;
4479
+ }
4480
+ if (actual_doc_text === current_effective_new_text || edit.target_text === current_effective_new_text) {
4481
+ all_sub_edits.push({
4482
+ type: "modify",
4483
+ target_text: actual_doc_text,
4484
+ new_text: actual_doc_text,
4485
+ comment: edit.comment,
4486
+ _match_start_index: start_idx,
4487
+ _internal_op: "COMMENT_ONLY",
4488
+ _active_mapper_ref: active_mapper
4489
+ });
4490
+ continue;
4491
+ }
4492
+ let effective_op = "";
4493
+ let final_target = "";
4494
+ let final_new = "";
4495
+ let effective_start_idx = start_idx;
4496
+ if (current_effective_new_text.startsWith(actual_doc_text)) {
4497
+ effective_op = "INSERTION";
4498
+ final_new = current_effective_new_text.substring(actual_doc_text.length);
4499
+ effective_start_idx = start_idx + match_len;
4500
+ } else {
4501
+ const [prefix_len, suffix_len] = trim_common_context(
4502
+ actual_doc_text,
4503
+ current_effective_new_text
4504
+ );
4505
+ const t_end = actual_doc_text.length - suffix_len;
4506
+ const n_end = current_effective_new_text.length - suffix_len;
4507
+ final_target = actual_doc_text.substring(prefix_len, t_end);
4508
+ final_new = current_effective_new_text.substring(prefix_len, n_end);
4509
+ effective_start_idx = start_idx + prefix_len;
4510
+ if (!final_target && final_new) effective_op = "INSERTION";
4511
+ else if (final_target && !final_new) effective_op = "DELETION";
4512
+ else if (final_target && final_new) effective_op = "MODIFICATION";
4513
+ else effective_op = "COMMENT_ONLY";
4514
+ }
4515
+ all_sub_edits.push({
4516
+ type: "modify",
4517
+ target_text: final_target,
4518
+ new_text: final_new,
4519
+ comment: edit.comment,
4520
+ _match_start_index: effective_start_idx,
4521
+ _internal_op: effective_op,
4522
+ _active_mapper_ref: active_mapper
4523
+ });
4524
+ }
4525
+ if (all_sub_edits.length === 0) return null;
4526
+ if (match_mode === "all" || all_sub_edits.length > 1) return all_sub_edits;
4527
+ return all_sub_edits[0];
4528
+ }
4529
+ _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
4530
+ let op = edit._internal_op;
4531
+ const active_mapper = edit._active_mapper_ref || this.mapper;
4532
+ const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
4533
+ const length = edit.target_text ? edit.target_text.length : 0;
4534
+ if (op === "STYLE_ONLY" || op === "STYLE_AND_TEXT") {
4535
+ const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
4536
+ start_idx,
4537
+ rebuild_map
4538
+ );
4539
+ let target_para_el = null;
4540
+ if (anchor_para) {
4541
+ target_para_el = anchor_para._element;
4542
+ } else if (anchor_run) {
4543
+ let walker = anchor_run._element;
4544
+ while (walker && walker.tagName !== "w:p") {
4545
+ walker = walker.parentNode;
4546
+ }
4547
+ target_para_el = walker;
4548
+ }
4549
+ if (target_para_el && edit._new_style) {
4550
+ this._set_paragraph_style(target_para_el, edit._new_style);
4551
+ }
4552
+ if (op === "STYLE_ONLY") {
4553
+ if (edit.comment) {
4554
+ const target_runs2 = active_mapper.find_target_runs_by_index(
4555
+ start_idx,
4556
+ length,
4557
+ rebuild_map
4558
+ );
4559
+ if (target_runs2.length > 0) {
4560
+ const first_el = target_runs2[0]._element;
4561
+ const last_el = target_runs2[target_runs2.length - 1]._element;
4562
+ let start_p = first_el;
4563
+ while (start_p && start_p.tagName !== "w:p")
4564
+ start_p = start_p.parentNode;
4565
+ let end_p = last_el;
4566
+ while (end_p && end_p.tagName !== "w:p")
4567
+ end_p = end_p.parentNode;
4568
+ if (start_p && end_p) {
4569
+ const ascend_to_paragraph_child = (el, p) => {
4570
+ let cur = el;
4571
+ while (cur.parentNode && cur.parentNode !== p) {
4572
+ cur = cur.parentNode;
4573
+ }
4574
+ return cur;
4575
+ };
4576
+ const first_anchor = ascend_to_paragraph_child(first_el, start_p);
4577
+ const last_anchor = ascend_to_paragraph_child(last_el, end_p);
4578
+ if (start_p === end_p) {
4579
+ this._attach_comment(start_p, first_anchor, last_anchor, edit.comment);
4580
+ } else {
4581
+ this._attach_comment_spanning(
4582
+ start_p,
4583
+ first_anchor,
4584
+ end_p,
4585
+ last_anchor,
4586
+ edit.comment
4587
+ );
4588
+ }
4589
+ }
4590
+ }
4591
+ }
4592
+ return true;
4593
+ }
4594
+ if (edit.target_text && edit.new_text) {
4595
+ op = "MODIFICATION";
4596
+ } else if (!edit.target_text && edit.new_text) {
4597
+ op = "INSERTION";
4598
+ } else if (edit.target_text && !edit.new_text) {
4599
+ op = "DELETION";
4600
+ } else {
4601
+ op = "COMMENT_ONLY";
4602
+ }
4113
4603
  }
4114
- return {
4115
- type: "modify",
4116
- target_text: final_target,
4117
- new_text: final_new,
4118
- comment: edit.comment,
4119
- _match_start_index: effective_start_idx,
4120
- _internal_op: effective_op,
4121
- _active_mapper_ref: active_mapper
4122
- };
4123
- }
4124
- _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
4125
- let op = edit._internal_op;
4126
- const active_mapper = edit._active_mapper_ref || this.mapper;
4127
- const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
4128
- const length = edit.target_text ? edit.target_text.length : 0;
4129
4604
  const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
4130
4605
  const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
4131
4606
  if (op === "COMMENT_ONLY") {
@@ -4508,1863 +4983,1814 @@ var RedlineEngine = class {
4508
4983
  }
4509
4984
  };
4510
4985
 
4511
- // src/pagination.ts
4512
- var PAGE_TARGET_CHARS = 19e3;
4513
- var APPENDIX_MARKER = "<!-- READONLY_BOUNDARY_START -->";
4514
- var _CRITIC_TOKENS = {
4515
- "{++": "++}",
4516
- "{--": "--}",
4517
- "{==": "==}",
4518
- "{>>": "<<}"
4519
- };
4520
- var _CHG_ID_PATTERN = /\bChg:(\d+)\b/g;
4521
- function split_structural_appendix(markdown) {
4522
- if (!markdown) return ["", ""];
4523
- const idx = markdown.indexOf(APPENDIX_MARKER);
4524
- if (idx === -1) return [markdown, ""];
4525
- const line_start = markdown.lastIndexOf("\n", idx) + 1;
4526
- const body = markdown.substring(0, line_start).trimEnd();
4527
- const appendix = markdown.substring(line_start);
4528
- return [body, appendix];
4529
- }
4530
- function paginate(markdown_body, structural_appendix = "") {
4531
- if (!markdown_body) {
4532
- const appendix_clean = structural_appendix ? structural_appendix.trim() : "";
4533
- const content = appendix_clean;
4534
- return {
4535
- pages: [{
4536
- page: 1,
4537
- total_pages: 1,
4538
- has_next: false,
4539
- has_prev: false,
4540
- tracked_change_count: _count_tracked_changes(content),
4541
- page_content: content
4542
- }],
4543
- total_pages: 1,
4544
- body_pages: [""],
4545
- body_page_offsets: [0]
4546
- };
4547
- }
4548
- const block_records = _tokenize_into_atomic_blocks(markdown_body);
4549
- const [body_pages, body_page_offsets] = _assemble_pages(block_records);
4550
- let final_pages;
4551
- if (structural_appendix && structural_appendix.trim()) {
4552
- const appendix = structural_appendix.trim();
4553
- final_pages = body_pages.map((bp) => bp ? `${bp}
4554
-
4555
- ${appendix}` : appendix);
4556
- } else {
4557
- final_pages = [...body_pages];
4986
+ // src/sanitize/transforms.ts
4987
+ function findDescendantsByLocalName(element, localName) {
4988
+ const result = [];
4989
+ const all = element.getElementsByTagName("*");
4990
+ for (let i = 0; i < all.length; i++) {
4991
+ const tag = all[i].tagName;
4992
+ if (tag === localName || tag.endsWith(":" + localName)) {
4993
+ result.push(all[i]);
4994
+ }
4558
4995
  }
4559
- const total = final_pages.length;
4560
- const page_infos = final_pages.map((content, i) => ({
4561
- page: i + 1,
4562
- total_pages: total,
4563
- has_next: i + 1 < total,
4564
- has_prev: i + 1 > 1,
4565
- tracked_change_count: _count_tracked_changes(content),
4566
- page_content: content
4567
- }));
4568
- return {
4569
- pages: page_infos,
4570
- total_pages: total,
4571
- body_pages,
4572
- body_page_offsets
4573
- };
4574
- }
4575
- function _tokenize_into_atomic_blocks(markdown_body) {
4576
- const raw_blocks = _split_on_safe_paragraph_breaks(markdown_body);
4577
- return _merge_footnote_sections(raw_blocks);
4996
+ return result;
4578
4997
  }
4579
- function _split_on_safe_paragraph_breaks(text) {
4580
- const counters = { "++}": 0, "--}": 0, "==}": 0, "<<}": 0 };
4581
- const blocks = [];
4582
- let block_start = 0;
4583
- let i = 0;
4584
- const n = text.length;
4585
- while (i < n) {
4586
- let matched_open = false;
4587
- for (const [open_tok, close_tok] of Object.entries(_CRITIC_TOKENS)) {
4588
- if (text.startsWith(open_tok, i)) {
4589
- counters[close_tok]++;
4590
- i += open_tok.length;
4591
- matched_open = true;
4592
- break;
4998
+ function coalesce_runs(doc) {
4999
+ let count = 0;
5000
+ function areRunsIdentical(rPr1, rPr2) {
5001
+ const xml1 = rPr1 ? rPr1.toString() : "";
5002
+ const xml2 = rPr2 ? rPr2.toString() : "";
5003
+ return xml1 === xml2;
5004
+ }
5005
+ function hasSpecialContent(run) {
5006
+ const safeTags = ["w:t", "w:tab", "w:br", "w:cr", "w:delText", "w:rPr"];
5007
+ for (let i = 0; i < run.childNodes.length; i++) {
5008
+ const child = run.childNodes[i];
5009
+ if (child.nodeType === 1) {
5010
+ const tag = child.tagName;
5011
+ if (!safeTags.includes(tag)) return true;
4593
5012
  }
4594
5013
  }
4595
- if (matched_open) continue;
4596
- let matched_close = false;
4597
- for (const close_tok of Object.values(_CRITIC_TOKENS)) {
4598
- if (text.startsWith(close_tok, i)) {
4599
- if (counters[close_tok] > 0) counters[close_tok]--;
4600
- i += close_tok.length;
4601
- matched_close = true;
4602
- break;
5014
+ return false;
5015
+ }
5016
+ function coalesceContainer(container) {
5017
+ const children = Array.from(container.childNodes).filter((n) => n.nodeType === 1);
5018
+ let i = 0;
5019
+ while (i < children.length - 1) {
5020
+ const curr = children[i];
5021
+ const nxt = children[i + 1];
5022
+ if (curr.tagName === "w:r" && nxt.tagName === "w:r") {
5023
+ if (!hasSpecialContent(curr) && !hasSpecialContent(nxt)) {
5024
+ const rPr1 = findChild(curr, "w:rPr");
5025
+ const rPr2 = findChild(nxt, "w:rPr");
5026
+ if (areRunsIdentical(rPr1, rPr2)) {
5027
+ let last_t = null;
5028
+ for (let c = 0; c < curr.childNodes.length; c++) {
5029
+ const child = curr.childNodes[c];
5030
+ if (child.nodeType === 1 && (child.tagName === "w:t" || child.tagName === "w:delText")) {
5031
+ last_t = child;
5032
+ }
5033
+ }
5034
+ const nxtChildren = Array.from(nxt.childNodes).filter((n) => n.nodeType === 1);
5035
+ for (const child of nxtChildren) {
5036
+ if (child.tagName === "w:rPr") continue;
5037
+ if ((child.tagName === "w:t" || child.tagName === "w:delText") && last_t && last_t.tagName === child.tagName) {
5038
+ const t1 = last_t.textContent || "";
5039
+ const t2 = child.textContent || "";
5040
+ const combined = t1 + t2;
5041
+ last_t.textContent = combined;
5042
+ if (combined.trim() !== combined) {
5043
+ last_t.setAttribute("xml:space", "preserve");
5044
+ }
5045
+ } else {
5046
+ curr.appendChild(child);
5047
+ if (child.tagName === "w:t" || child.tagName === "w:delText") {
5048
+ last_t = child;
5049
+ }
5050
+ }
5051
+ }
5052
+ container.removeChild(nxt);
5053
+ children.splice(i + 1, 1);
5054
+ count++;
5055
+ continue;
5056
+ }
5057
+ }
4603
5058
  }
4604
- }
4605
- if (matched_close) continue;
4606
- if (text[i] === "\n" && i + 1 < n && text[i + 1] === "\n") {
4607
- if (Object.values(counters).every((c) => c === 0)) {
4608
- const block_text = text.substring(block_start, i);
4609
- if (block_text) blocks.push([block_text, block_start]);
4610
- let j = i;
4611
- while (j < n && text[j] === "\n") j++;
4612
- i = j;
4613
- block_start = i;
4614
- continue;
5059
+ if (["w:ins", "w:del", "w:hyperlink", "w:sdt", "w:smartTag", "w:fldSimple", "w:sdtContent"].includes(curr.tagName)) {
5060
+ coalesceContainer(curr);
4615
5061
  }
4616
- }
4617
- i++;
4618
- }
4619
- if (block_start < n) {
4620
- const block_text = text.substring(block_start, n);
4621
- if (block_text) blocks.push([block_text, block_start]);
4622
- }
4623
- return blocks;
4624
- }
4625
- function _merge_footnote_sections(blocks) {
4626
- if (!blocks.length) return blocks;
4627
- const merged = [];
4628
- let i = 0;
4629
- while (i < blocks.length) {
4630
- const [block_text, block_offset] = blocks[i];
4631
- const stripped = block_text.trimStart();
4632
- const is_section_header = stripped.startsWith("## Footnotes") || stripped.startsWith("## Endnotes");
4633
- if (!is_section_header) {
4634
- merged.push([block_text, block_offset]);
4635
5062
  i++;
4636
- continue;
4637
5063
  }
4638
- let accumulated_text = block_text;
4639
- let j = i + 1;
4640
- while (j < blocks.length) {
4641
- const [next_text] = blocks[j];
4642
- const next_stripped = next_text.trimStart();
4643
- if (next_stripped.startsWith("[^fn-") || next_stripped.startsWith("[^en-")) {
4644
- accumulated_text = `${accumulated_text}
4645
-
4646
- ${next_text}`;
4647
- j++;
4648
- } else {
4649
- break;
5064
+ if (children.length > 0) {
5065
+ const last = children[children.length - 1];
5066
+ if (["w:ins", "w:del", "w:hyperlink", "w:sdt", "w:smartTag", "w:fldSimple", "w:sdtContent"].includes(last.tagName)) {
5067
+ coalesceContainer(last);
4650
5068
  }
4651
5069
  }
4652
- merged.push([accumulated_text, block_offset]);
4653
- i = j;
4654
5070
  }
4655
- return merged;
5071
+ const paragraphs = findAllDescendants(doc.element, "w:p");
5072
+ for (const p of paragraphs) coalesceContainer(p);
5073
+ return count ? [`Adjacent identical runs coalesced: ${count}`] : [];
4656
5074
  }
4657
- function _assemble_pages(block_records) {
4658
- if (!block_records.length) return [[""], [0]];
4659
- const pages = [];
4660
- const page_starts = [];
4661
- let current_blocks = [];
4662
- let current_size = 0;
4663
- let current_start = -1;
4664
- const flush_current = () => {
4665
- if (current_blocks.length > 0) {
4666
- pages.push(current_blocks.join("\n\n"));
4667
- page_starts.push(current_start);
5075
+ function strip_rsid(doc) {
5076
+ let count = 0;
5077
+ const rsidAttrs = ["w:rsidR", "w:rsidRPr", "w:rsidRDefault", "w:rsidP", "w:rsidDel", "w:rsidSect", "w:rsidTr"];
5078
+ const all = doc.element.getElementsByTagName("*");
5079
+ for (let i = 0; i < all.length; i++) {
5080
+ for (const attr of rsidAttrs) {
5081
+ if (all[i].hasAttribute(attr)) {
5082
+ all[i].removeAttribute(attr);
5083
+ count++;
5084
+ }
4668
5085
  }
4669
- current_blocks = [];
4670
- current_size = 0;
4671
- current_start = -1;
4672
- };
4673
- for (const [block_text, block_offset] of block_records) {
4674
- const block_size = block_text.length;
4675
- const added_size = block_size + (current_blocks.length > 0 ? 2 : 0);
4676
- if (current_blocks.length > 0 && current_size + added_size > PAGE_TARGET_CHARS) {
4677
- flush_current();
5086
+ }
5087
+ const rsidsElements = findAllDescendants(doc.element, "w:rsids");
5088
+ for (const el of rsidsElements) {
5089
+ if (el.parentNode) {
5090
+ el.parentNode.removeChild(el);
5091
+ count++;
4678
5092
  }
4679
- if (current_blocks.length === 0 && block_size > PAGE_TARGET_CHARS) {
4680
- pages.push(block_text);
4681
- page_starts.push(block_offset);
4682
- continue;
5093
+ }
5094
+ return count ? [`rsid attributes: ${count} removed`] : [];
5095
+ }
5096
+ function strip_para_ids(doc) {
5097
+ let count = 0;
5098
+ const attrs = ["w14:paraId", "w14:textId"];
5099
+ const all = doc.element.getElementsByTagName("*");
5100
+ for (let i = 0; i < all.length; i++) {
5101
+ for (const attr of attrs) {
5102
+ if (all[i].hasAttribute(attr)) {
5103
+ all[i].removeAttribute(attr);
5104
+ count++;
5105
+ }
4683
5106
  }
4684
- if (current_blocks.length === 0) current_start = block_offset;
4685
- current_blocks.push(block_text);
4686
- current_size += current_size > 0 ? added_size : block_size;
4687
5107
  }
4688
- flush_current();
4689
- if (!pages.length) return [[""], [0]];
4690
- return [pages, page_starts];
5108
+ return count ? [`Paragraph/text IDs: ${count} removed`] : [];
4691
5109
  }
4692
- function _count_tracked_changes(page_content) {
4693
- const matches = [...page_content.matchAll(_CHG_ID_PATTERN)];
4694
- const distinct = new Set(matches.map((m) => m[1]));
4695
- return distinct.size;
5110
+ function strip_proof_errors(doc) {
5111
+ const elements = findAllDescendants(doc.element, "w:proofErr");
5112
+ elements.forEach((el) => el.parentNode?.removeChild(el));
5113
+ return elements.length ? [`Spell check markers: ${elements.length} removed`] : [];
4696
5114
  }
4697
-
4698
- // src/domain.ts
4699
- function boundedLevenshtein(a, b, maxDist = 2) {
4700
- if (a === b) return 0;
4701
- if (Math.abs(a.length - b.length) > maxDist) return maxDist + 1;
4702
- if (a.length === 0) return b.length <= maxDist ? b.length : maxDist + 1;
4703
- if (b.length === 0) return a.length <= maxDist ? a.length : maxDist + 1;
4704
- if (a.length > b.length) {
4705
- const temp = a;
4706
- a = b;
4707
- b = temp;
5115
+ function strip_empty_properties(doc) {
5116
+ let count = 0;
5117
+ for (const tag of ["w:rPr", "w:pPr"]) {
5118
+ const elements = findAllDescendants(doc.element, tag);
5119
+ for (const el of elements) {
5120
+ if (el.childNodes.length === 0 || el.childNodes.length === 1 && el.childNodes[0].nodeType === 3 && !el.childNodes[0].textContent?.trim()) {
5121
+ el.parentNode?.removeChild(el);
5122
+ count++;
5123
+ }
5124
+ }
4708
5125
  }
4709
- let row = Array.from({ length: a.length + 1 }, (_, i) => i);
4710
- for (let i = 1; i <= b.length; i++) {
4711
- const newRow = [i];
4712
- let minInRow = i;
4713
- for (let j = 1; j <= a.length; j++) {
4714
- const cost = a[j - 1] === b[i - 1] ? 0 : 1;
4715
- const val = Math.min(
4716
- row[j] + 1,
4717
- newRow[j - 1] + 1,
4718
- row[j - 1] + cost
4719
- );
4720
- newRow.push(val);
4721
- if (val < minInRow) minInRow = val;
5126
+ return count ? [`Empty property elements: ${count} removed`] : [];
5127
+ }
5128
+ function strip_hidden_text(doc) {
5129
+ let count = 0;
5130
+ const elements = findAllDescendants(doc.element, "w:rPr");
5131
+ for (const rPr of elements) {
5132
+ if (findChild(rPr, "w:vanish") || findChild(rPr, "w:webHidden")) {
5133
+ const run = rPr.parentNode;
5134
+ if (run && run.tagName === "w:r" && run.parentNode) {
5135
+ run.parentNode.removeChild(run);
5136
+ count++;
5137
+ }
4722
5138
  }
4723
- if (minInRow > maxDist) return maxDist + 1;
4724
- row = newRow;
4725
5139
  }
4726
- return row[a.length] <= maxDist ? row[a.length] : maxDist + 1;
5140
+ return count ? [`Hidden text runs: ${count} removed`] : [];
4727
5141
  }
4728
- function _get_paragraph_text(p) {
4729
- let text = "";
4730
- const runs = findAllDescendants(p._element, "w:r");
4731
- for (const r of runs) {
4732
- text += get_run_text(new Run(r, p));
5142
+ function count_tracked_changes(doc) {
5143
+ const ins = findAllDescendants(doc.element, "w:ins").length;
5144
+ const del = findAllDescendants(doc.element, "w:del").length;
5145
+ const fmt = findAllDescendants(doc.element, "w:rPrChange").length + findAllDescendants(doc.element, "w:pPrChange").length + findAllDescendants(doc.element, "w:sectPrChange").length;
5146
+ return [ins, del, fmt];
5147
+ }
5148
+ function get_track_change_authors(doc) {
5149
+ const authors = /* @__PURE__ */ new Set();
5150
+ for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange", "w:sectPrChange"]) {
5151
+ for (const el of findAllDescendants(doc.element, tag)) {
5152
+ const author = el.getAttribute("w:author");
5153
+ if (author) authors.add(author);
5154
+ }
4733
5155
  }
4734
- return text;
5156
+ return authors;
4735
5157
  }
4736
- function extract_all_domain_metadata(doc, base_text) {
4737
- const definitions = {};
4738
- const duplicates = /* @__PURE__ */ new Set();
4739
- const raw_anchors = {};
4740
- const raw_references = [];
4741
- const leading_re = /^(?:[\d.\-()a-zA-Z]+\s*)?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”]/;
4742
- const inline_re = /\([^)]*?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”][^)]*?\)/g;
4743
- for (const item of iter_block_items(doc)) {
4744
- if (!(item instanceof Paragraph)) continue;
4745
- const text = _get_paragraph_text(item).trim();
4746
- if (!text) continue;
4747
- const extracted_terms = [];
4748
- const leading_match = text.match(leading_re);
4749
- if (leading_match) extracted_terms.push(leading_match[1].trim());
4750
- const inline_matches = text.matchAll(inline_re);
4751
- for (const m of inline_matches) {
4752
- extracted_terms.push(m[1].trim());
5158
+ function _getElementText(el) {
5159
+ const texts = [];
5160
+ const ts = findAllDescendants(el, "w:t");
5161
+ for (const t of ts) if (t.textContent) texts.push(t.textContent);
5162
+ const dts = findAllDescendants(el, "w:delText");
5163
+ for (const dt of dts) if (dt.textContent) texts.push(dt.textContent);
5164
+ return texts.join("");
5165
+ }
5166
+ function _truncate(text, maxLen = 60) {
5167
+ const clean = text.replace(/\n/g, " ").trim();
5168
+ if (clean.length <= maxLen) return clean;
5169
+ return clean.substring(0, maxLen - 3) + "...";
5170
+ }
5171
+ function accept_all_tracked_changes(doc) {
5172
+ const lines = [];
5173
+ const insEls = findAllDescendants(doc.element, "w:ins");
5174
+ const delEls = findAllDescendants(doc.element, "w:del");
5175
+ for (const ins of insEls) {
5176
+ const text = _getElementText(ins).trim();
5177
+ if (text) lines.push(` Accepted insertion: "${_truncate(text, 60)}"`);
5178
+ }
5179
+ for (const del of delEls) {
5180
+ const text = _getElementText(del).trim();
5181
+ if (text) lines.push(` Accepted deletion of: "${_truncate(text, 60)}"`);
5182
+ }
5183
+ const engine = new RedlineEngine(doc);
5184
+ engine.accept_all_revisions();
5185
+ for (const tag of ["w:rPrChange", "w:pPrChange", "w:sectPrChange"]) {
5186
+ for (const el of findAllDescendants(doc.element, tag)) {
5187
+ el.parentNode?.removeChild(el);
4753
5188
  }
4754
- for (const term of extracted_terms) {
4755
- if (definitions[term]) duplicates.add(term);
4756
- else definitions[term] = { count: 0 };
5189
+ }
5190
+ const total = insEls.length + delEls.length;
5191
+ if (total) {
5192
+ return [`Tracked changes auto-accepted: ${total}`].concat(lines);
5193
+ }
5194
+ return [];
5195
+ }
5196
+ function get_comments_summary(doc) {
5197
+ const data = extract_comments_data(doc.pkg);
5198
+ const comments = [];
5199
+ let openCount = 0;
5200
+ let resolvedCount = 0;
5201
+ for (const [cId, info] of Object.entries(data)) {
5202
+ if (info.resolved) resolvedCount++;
5203
+ else openCount++;
5204
+ comments.push({ id: cId, ...info });
5205
+ }
5206
+ return { total: comments.length, open: openCount, resolved: resolvedCount, comments };
5207
+ }
5208
+ function remove_all_comments(doc) {
5209
+ const data = extract_comments_data(doc.pkg);
5210
+ const keys = Object.keys(data);
5211
+ if (keys.length === 0) return [];
5212
+ const lines = [];
5213
+ const cm = new CommentsManager(doc);
5214
+ for (const [cId, info] of Object.entries(data)) {
5215
+ const status = info.resolved ? "[Resolved]" : "[Open]";
5216
+ lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
5217
+ cm.deleteComment(cId);
5218
+ }
5219
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
5220
+ for (const el of findAllDescendants(doc.element, tag)) {
5221
+ el.parentNode?.removeChild(el);
4757
5222
  }
4758
- const short_text = text.length > 60 ? text.substring(0, 60) + "..." : text;
4759
- const nodes = findAllDescendants(item._element, "*");
4760
- for (const node of nodes) {
4761
- if (node.tagName === "w:bookmarkStart") {
4762
- const b_name = node.getAttribute("w:name");
4763
- if (b_name && (!b_name.startsWith("_") || b_name.startsWith("_Ref"))) {
4764
- if (!raw_anchors[b_name]) {
4765
- raw_anchors[b_name] = { anchored_to: short_text, referenced_from: [] };
4766
- }
5223
+ }
5224
+ const refs = findAllDescendants(doc.element, "w:commentReference");
5225
+ for (const ref of refs) {
5226
+ const parent = ref.parentNode;
5227
+ if (parent) {
5228
+ if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
5229
+ const nonRprChildren = Array.from(parent.childNodes).filter(
5230
+ (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
5231
+ );
5232
+ if (nonRprChildren.length <= 1) {
5233
+ parent.parentNode?.removeChild(parent);
5234
+ } else {
5235
+ parent.removeChild(ref);
4767
5236
  }
5237
+ } else {
5238
+ parent.removeChild(ref);
4768
5239
  }
4769
- let target = null;
4770
- if (node.tagName === "w:fldSimple") {
4771
- const instr = node.getAttribute("w:instr") || "";
4772
- const parts = instr.trim().split(/\s+/);
4773
- if (parts.length > 1 && parts[0] === "REF") target = parts[1];
4774
- } else if (node.tagName === "w:instrText") {
4775
- const instr = node.textContent || "";
4776
- const parts = instr.trim().split(/\s+/);
4777
- if (parts.length > 1 && parts[0] === "REF") target = parts[1];
4778
- }
4779
- if (target) raw_references.push([target, short_text]);
4780
- }
4781
- }
4782
- for (const [target, ref_text] of raw_references) {
4783
- if (raw_anchors[target]) {
4784
- raw_anchors[target].referenced_from.push(ref_text);
4785
5240
  }
4786
5241
  }
4787
- const diagnostics = [];
4788
- const def_keys = Object.keys(definitions);
4789
- if (def_keys.length > 0) {
4790
- const sorted_terms = def_keys.sort((a, b) => b.length - a.length);
4791
- const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4792
- const alt = sorted_terms.map(escapeRegExp).join("|");
4793
- const usage_pattern = new RegExp(`(?<!["\u201C])\\b(${alt})\\b(?![\u201D"])`, "g");
4794
- for (const m of base_text.matchAll(usage_pattern)) {
4795
- const matched_term = m[1];
4796
- if (definitions[matched_term]) definitions[matched_term].count++;
5242
+ const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
5243
+ const openCount = Object.values(data).filter((c) => !c.resolved).length;
5244
+ return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
5245
+ }
5246
+ function eject_comment_parts(doc) {
5247
+ const pkg = doc.pkg;
5248
+ const comment_partnames = /* @__PURE__ */ new Set();
5249
+ for (const part of pkg.parts) {
5250
+ if (part.partname.toLowerCase().includes("comments")) {
5251
+ comment_partnames.add(part.partname);
5252
+ const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
5253
+ const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
5254
+ comment_partnames.add(withSlash);
5255
+ comment_partnames.add(withoutSlash);
4797
5256
  }
4798
- for (const term of def_keys) {
4799
- if (definitions[term].count === 0) {
4800
- delete definitions[term];
4801
- duplicates.delete(term);
5257
+ }
5258
+ if (comment_partnames.size === 0) return;
5259
+ for (const part of pkg.parts) {
5260
+ if (part.partname.endsWith(".rels")) {
5261
+ const rels = findAllDescendants(part._element, "Relationship");
5262
+ const toRemove = [];
5263
+ for (const rel of rels) {
5264
+ const target = rel.getAttribute("Target") || "";
5265
+ if (target.toLowerCase().includes("comments")) {
5266
+ toRemove.push(rel);
5267
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
5268
+ const sourcePart = pkg.getPartByPath(sourcePath);
5269
+ if (sourcePart) {
5270
+ const relId = rel.getAttribute("Id");
5271
+ if (relId) sourcePart.rels.delete(relId);
5272
+ }
5273
+ }
5274
+ }
5275
+ for (const relEl of toRemove) {
5276
+ relEl.parentNode?.removeChild(relEl);
4802
5277
  }
4803
5278
  }
4804
5279
  }
4805
- for (const term of duplicates) {
4806
- diagnostics.push(`[Error] Duplicate Definition: '${term}' is defined multiple times.`);
4807
- }
4808
- const stop_words = /* @__PURE__ */ new Set([
4809
- "The",
4810
- "This",
4811
- "That",
4812
- "Such",
4813
- "A",
4814
- "An",
4815
- "Any",
4816
- "All",
4817
- "Some",
4818
- "No",
4819
- "Every",
4820
- "Each",
4821
- "As",
4822
- "In",
4823
- "Of",
4824
- "For",
4825
- "To",
4826
- "On",
4827
- "By",
4828
- "With"
4829
- ]);
4830
- const all_cap_pattern = /\b[A-Z][a-zA-Z]*(?:\s+[A-Z][a-zA-Z]*)*\b/g;
4831
- const all_caps = new Set(base_text.match(all_cap_pattern) || []);
4832
- const valid_terms = new Set(Object.keys(definitions));
4833
- const terms_by_first_letter = {};
4834
- for (const term of valid_terms) {
4835
- const fl = term[0].toLowerCase();
4836
- if (!terms_by_first_letter[fl]) terms_by_first_letter[fl] = [];
4837
- terms_by_first_letter[fl].push(term);
4838
- }
4839
- const candidates_by_term = {};
4840
- for (const raw_candidate of all_caps) {
4841
- let candidate = raw_candidate.trim();
4842
- const words = candidate.split(/\s+/);
4843
- while (words.length > 0) {
4844
- const first = words[0];
4845
- const title = first.charAt(0).toUpperCase() + first.slice(1).toLowerCase();
4846
- if (stop_words.has(title)) words.shift();
4847
- else break;
4848
- }
4849
- candidate = words.join(" ");
4850
- if (candidate.length < 4) continue;
4851
- if (valid_terms.has(candidate)) continue;
4852
- const first_letter = candidate[0].toLowerCase();
4853
- let candidate_terms = terms_by_first_letter[first_letter] || [];
4854
- if (candidate.length > 5) {
4855
- for (const [k, v] of Object.entries(terms_by_first_letter)) {
4856
- if (k !== first_letter) candidate_terms = candidate_terms.concat(v);
5280
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
5281
+ if (ctPart) {
5282
+ const overrides = findAllDescendants(ctPart._element, "Override");
5283
+ const toRemove = [];
5284
+ for (const override of overrides) {
5285
+ const partName = override.getAttribute("PartName") || "";
5286
+ if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
5287
+ toRemove.push(override);
4857
5288
  }
4858
5289
  }
4859
- for (const term of candidate_terms) {
4860
- if (Math.abs(candidate.length - term.length) > 2) continue;
4861
- if (candidate === term + "s" || candidate === term + "es") continue;
4862
- if (term === candidate + "s" || term === candidate + "es") continue;
4863
- const dist = boundedLevenshtein(candidate, term, 2);
4864
- if (dist === 0 || dist > 2) continue;
4865
- if (term.length <= 5) {
4866
- if (dist > 1) continue;
4867
- if (candidate[0].toLowerCase() !== term[0].toLowerCase()) continue;
4868
- }
4869
- if (!candidates_by_term[term]) candidates_by_term[term] = [];
4870
- if (!candidates_by_term[term].includes(candidate)) candidates_by_term[term].push(candidate);
5290
+ for (const overrideEl of toRemove) {
5291
+ overrideEl.parentNode?.removeChild(overrideEl);
4871
5292
  }
4872
5293
  }
4873
- for (const [term, candidates] of Object.entries(candidates_by_term)) {
4874
- candidates.sort();
4875
- const c_str = candidates.map((c) => `'${c}'`).join(", ");
4876
- diagnostics.push(`[Info] Possible Typos for '${term}': Found ${c_str}`);
4877
- }
4878
- function diag_sort_key(msg) {
4879
- if (msg.startsWith("[Error]")) return 0;
4880
- if (msg.startsWith("[Warning]")) return 1;
4881
- return 2;
5294
+ pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
5295
+ for (const key of Object.keys(pkg.unzipped)) {
5296
+ if (key.toLowerCase().includes("comments")) {
5297
+ delete pkg.unzipped[key];
5298
+ }
4882
5299
  }
4883
- diagnostics.sort((a, b) => {
4884
- const keyA = diag_sort_key(a);
4885
- const keyB = diag_sort_key(b);
4886
- if (keyA !== keyB) return keyA - keyB;
4887
- return a.localeCompare(b);
4888
- });
4889
- return [definitions, diagnostics, raw_anchors];
4890
5300
  }
4891
- function build_structural_appendix(doc, base_text) {
4892
- const [defs, diagnostics, anchors] = extract_all_domain_metadata(doc, base_text);
4893
- const lines = [
4894
- "\n\n---",
4895
- "",
4896
- "<!-- READONLY_BOUNDARY_START -->",
4897
- "# Document Structure (Read-Only)",
4898
- "The content below is metadata describing the document's reference structure. Do not include this section in any tracked changes or edits \u2014 it is for your context only and will be discarded on write."
4899
- ];
4900
- let has_content = false;
4901
- if (Object.keys(defs).length > 0) {
4902
- has_content = true;
4903
- lines.push("\n## Defined Terms");
4904
- for (const [term, data] of Object.entries(defs)) {
4905
- lines.push(`- "${term}" \u2014 used ${data.count} times.`);
5301
+ function replace_comment_authors(doc, newAuthor) {
5302
+ const cm = new CommentsManager(doc);
5303
+ if (!cm.commentsPart) return [];
5304
+ const original = /* @__PURE__ */ new Set();
5305
+ const comments = findAllDescendants(cm.commentsPart._element, "w:comment");
5306
+ for (const c of comments) {
5307
+ const author = c.getAttribute("w:author");
5308
+ if (author) {
5309
+ original.add(author);
5310
+ c.setAttribute("w:author", newAuthor);
4906
5311
  }
4907
- }
4908
- if (diagnostics.length > 0) {
4909
- has_content = true;
4910
- lines.push("\n## Semantic Diagnostics");
4911
- for (const diag of diagnostics) {
4912
- lines.push(`- ${diag}`);
5312
+ if (c.hasAttribute("w:initials")) {
5313
+ const initials = newAuthor.split(" ").filter(Boolean).map((p) => p[0]).join("").toUpperCase();
5314
+ c.setAttribute("w:initials", initials);
4913
5315
  }
4914
5316
  }
4915
- if (Object.keys(anchors).length > 0) {
4916
- has_content = true;
4917
- lines.push("\n## Named Anchors");
4918
- for (const [b_name, data] of Object.entries(anchors)) {
4919
- lines.push(`- ${b_name} \u2192 Anchored to: "${data.anchored_to}"`);
4920
- for (const ref of data.referenced_from) {
4921
- lines.push(` - Referenced from: "${ref}"`);
5317
+ return original.size ? [`Comment authors replaced: ${Array.from(original).sort().join(", ")} \u2192 "${newAuthor}"`] : [];
5318
+ }
5319
+ function replace_change_authors(doc, newAuthor) {
5320
+ const original = /* @__PURE__ */ new Set();
5321
+ for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange"]) {
5322
+ for (const el of findAllDescendants(doc.element, tag)) {
5323
+ const author = el.getAttribute("w:author");
5324
+ if (author) {
5325
+ original.add(author);
5326
+ el.setAttribute("w:author", newAuthor);
4922
5327
  }
4923
5328
  }
4924
5329
  }
4925
- if (has_content) {
4926
- return lines.join("\n");
4927
- }
4928
- return "";
4929
- }
4930
-
4931
- // src/ingest.ts
4932
- async function extractTextFromBuffer(buffer, cleanView = false) {
4933
- const doc = await DocumentObject.load(buffer);
4934
- return _extractTextFromDoc(doc, cleanView);
5330
+ return original.size ? [`Track change authors replaced: ${Array.from(original).sort().join(", ")} \u2192 "${newAuthor}"`] : [];
4935
5331
  }
4936
- function _extractTextFromDoc(doc, cleanView = false, includeAppendix = true, return_paragraph_offsets = false) {
4937
- const comments_map = extract_comments_data(doc.pkg);
4938
- const full_text = [];
4939
- const paragraph_offsets = /* @__PURE__ */ new Map();
4940
- let cursor = 0;
4941
- for (const part of iter_document_parts(doc)) {
4942
- const part_cursor = full_text.length > 0 ? cursor + 2 : cursor;
4943
- const part_text = _extract_blocks(part, comments_map, cleanView, part_cursor, return_paragraph_offsets ? paragraph_offsets : void 0);
4944
- if (part_text) {
4945
- if (full_text.length > 0) cursor += 2;
4946
- full_text.push(part_text);
4947
- cursor += part_text.length;
5332
+ function normalize_change_dates(doc) {
5333
+ let count = 0;
5334
+ const fixed = "2025-01-01T00:00:00Z";
5335
+ for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange"]) {
5336
+ for (const el of findAllDescendants(doc.element, tag)) {
5337
+ if (el.hasAttribute("w:date")) {
5338
+ el.setAttribute("w:date", fixed);
5339
+ count++;
5340
+ }
4948
5341
  }
4949
5342
  }
4950
- let base_text = full_text.join("\n\n");
4951
- if (includeAppendix) {
4952
- const appendix = build_structural_appendix(doc, base_text);
4953
- if (appendix) base_text += appendix;
4954
- }
4955
- if (return_paragraph_offsets) {
4956
- return { text: base_text, paragraph_offsets };
4957
- }
4958
- return base_text;
5343
+ return count ? [`Track change timestamps: ${count} normalized`] : [];
4959
5344
  }
4960
- function _extract_blocks(container, comments_map, cleanView, cursor, paragraph_offsets) {
4961
- const part = container.part || container;
4962
- const [style_cache, default_pstyle] = _get_style_cache(part);
4963
- const blocks = [];
4964
- let local_cursor = cursor;
4965
- let is_first_block = true;
4966
- let is_first_para = true;
4967
- if (container.constructor && container.constructor.name === "NotesPart") {
4968
- const header = container.note_type === "fn" ? "## Footnotes" : "## Endnotes";
4969
- const sep = `---
4970
- ${header}`;
4971
- blocks.push(sep);
4972
- local_cursor += sep.length;
4973
- is_first_block = false;
4974
- }
4975
- for (const item of iter_block_items(container)) {
4976
- if (!is_first_block) local_cursor += 2;
4977
- const block_start = local_cursor;
4978
- if (item.constructor.name === "FootnoteItem") {
4979
- const fn_text = _extract_blocks(item, comments_map, cleanView, block_start, paragraph_offsets);
4980
- if (fn_text) {
4981
- blocks.push(fn_text);
4982
- local_cursor = block_start + fn_text.length;
4983
- is_first_block = false;
4984
- } else if (!is_first_block) {
4985
- local_cursor -= 2;
4986
- }
4987
- } else if (item instanceof Paragraph) {
4988
- let prefix = get_paragraph_prefix(item, style_cache, default_pstyle);
4989
- if (is_first_para && container.constructor.name === "FootnoteItem") {
4990
- prefix = `[^${container.note_type}-${container.id}]: ` + prefix;
5345
+ function scrub_doc_properties(doc) {
5346
+ const lines = [];
5347
+ const corePart = doc.pkg.getPartByPath("docProps/core.xml");
5348
+ if (corePart) {
5349
+ const creators = findDescendantsByLocalName(corePart._element, "creator");
5350
+ creators.forEach((c) => {
5351
+ if (c.textContent) {
5352
+ lines.push(`Author: ${c.textContent}`);
5353
+ c.textContent = "";
4991
5354
  }
4992
- const p_text = build_paragraph_text(item, comments_map, cleanView, style_cache, default_pstyle);
4993
- const full_block = prefix + p_text;
4994
- blocks.push(full_block);
4995
- if (paragraph_offsets) {
4996
- paragraph_offsets.set(item._element, [block_start, full_block.length]);
5355
+ });
5356
+ const modifiers = findDescendantsByLocalName(corePart._element, "lastModifiedBy");
5357
+ modifiers.forEach((c) => {
5358
+ if (c.textContent) {
5359
+ lines.push(`Last modified by: ${c.textContent}`);
5360
+ c.textContent = "";
4997
5361
  }
4998
- local_cursor = block_start + full_block.length;
4999
- is_first_para = false;
5000
- is_first_block = false;
5001
- } else if (item instanceof Table) {
5002
- const table_text = extract_table(item, comments_map, cleanView, block_start, paragraph_offsets);
5003
- if (table_text) {
5004
- blocks.push(table_text);
5005
- local_cursor = block_start + table_text.length;
5006
- is_first_block = false;
5007
- } else if (!is_first_block) {
5008
- local_cursor -= 2;
5362
+ });
5363
+ const revisions = findDescendantsByLocalName(corePart._element, "revision");
5364
+ revisions.forEach((c) => {
5365
+ if (c.textContent && parseInt(c.textContent) > 1) {
5366
+ lines.push(`Revision count: ${c.textContent} \u2192 1`);
5367
+ c.textContent = "1";
5009
5368
  }
5010
- is_first_para = false;
5369
+ });
5370
+ }
5371
+ const appPart = doc.pkg.getPartByPath("docProps/app.xml");
5372
+ if (appPart) {
5373
+ const docEl = appPart._element;
5374
+ const intFields = ["TotalTime", "Words", "Characters", "Paragraphs", "Lines", "CharactersWithSpaces"];
5375
+ for (const f of intFields) {
5376
+ findDescendantsByLocalName(docEl, f).forEach((el) => {
5377
+ if (el.textContent && el.textContent !== "0") {
5378
+ if (f === "TotalTime") lines.push(`Total editing time: ${el.textContent} minutes`);
5379
+ el.textContent = "0";
5380
+ }
5381
+ });
5382
+ }
5383
+ const strFields = ["Template", "Manager", "Company"];
5384
+ for (const f of strFields) {
5385
+ findDescendantsByLocalName(docEl, f).forEach((el) => {
5386
+ if (el.textContent) {
5387
+ lines.push(`${f}: ${el.textContent}`);
5388
+ el.textContent = "";
5389
+ }
5390
+ });
5011
5391
  }
5012
5392
  }
5013
- return blocks.join("\n\n");
5393
+ return lines.length ? ["Metadata scrubbed:", ...lines.map((l) => ` ${l}`)] : [];
5014
5394
  }
5015
- function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets) {
5016
- const rows_text = [];
5017
- let rows_processed = 0;
5018
- let local_cursor = cursor;
5019
- for (const row of table.rows) {
5020
- const cell_texts = [];
5021
- const seen_cells = /* @__PURE__ */ new Set();
5022
- const trPr = findChild(row._element, "w:trPr");
5023
- const ins = trPr ? findChild(trPr, "w:ins") : null;
5024
- const del_node = trPr ? findChild(trPr, "w:del") : null;
5025
- if (cleanView && del_node) continue;
5026
- const row_start = local_cursor + (rows_processed > 0 ? 1 : 0);
5027
- const wrapper_prefix_len = !cleanView && ins ? 4 : !cleanView && del_node ? 4 : 0;
5028
- let cell_cursor = row_start + wrapper_prefix_len;
5029
- let first_cell = true;
5030
- for (const cell of row.cells) {
5031
- if (seen_cells.has(cell)) continue;
5032
- seen_cells.add(cell);
5033
- if (!first_cell) cell_cursor += 3;
5034
- const cell_content = _extract_blocks(cell, comments_map, cleanView, cell_cursor, paragraph_offsets);
5035
- cell_texts.push(cell_content);
5036
- cell_cursor += cell_content.length;
5037
- first_cell = false;
5395
+ function scrub_timestamps(doc) {
5396
+ let modified = false;
5397
+ const epoch = "1970-01-01T00:00:00Z";
5398
+ const corePart = doc.pkg.getPartByPath("docProps/core.xml");
5399
+ if (corePart) {
5400
+ for (const tag of ["created", "modified", "lastPrinted"]) {
5401
+ findDescendantsByLocalName(corePart._element, tag).forEach((el) => {
5402
+ if (el.textContent && el.textContent !== epoch) {
5403
+ el.textContent = epoch;
5404
+ modified = true;
5405
+ }
5406
+ });
5038
5407
  }
5039
- let row_str = cell_texts.join(" | ");
5040
- if (!cleanView) {
5041
- if (ins) row_str = `{++ ${row_str} |Chg:${ins.getAttribute("w:id")}++}`;
5042
- else if (del_node) row_str = `{-- ${row_str} |Chg:${del_node.getAttribute("w:id")}--}`;
5408
+ }
5409
+ return modified ? ["Timestamps normalized to epoch"] : [];
5410
+ }
5411
+ function strip_custom_xml(doc) {
5412
+ const customParts = doc.pkg.parts.filter((p) => p.partname.includes("/customXml"));
5413
+ if (customParts.length === 0) return [];
5414
+ const partnames = new Set(customParts.map((p) => p.partname));
5415
+ doc.pkg.parts = doc.pkg.parts.filter((p) => !partnames.has(p.partname));
5416
+ const removeRelationsTo = (relsPart) => {
5417
+ const toRemove = [];
5418
+ for (const rel of findAllDescendants(relsPart._element, "Relationship")) {
5419
+ const target = rel.getAttribute("Target");
5420
+ if (target && target.includes("customXml")) toRemove.push(rel);
5043
5421
  }
5044
- rows_text.push(row_str);
5045
- local_cursor = row_start + row_str.length;
5046
- rows_processed++;
5422
+ toRemove.forEach((r) => r.parentNode?.removeChild(r));
5423
+ };
5424
+ const rootRels = doc.pkg.getPartByPath("_rels/.rels");
5425
+ if (rootRels) removeRelationsTo(rootRels);
5426
+ const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
5427
+ if (docRels) removeRelationsTo(docRels);
5428
+ for (const sdtPr of findAllDescendants(doc.element, "w:sdtPr")) {
5429
+ findChildren(sdtPr, "w:dataBinding").forEach((b) => sdtPr.removeChild(b));
5047
5430
  }
5048
- return rows_text.join("\n");
5431
+ return [`Custom XML parts: ${customParts.length} removed`];
5049
5432
  }
5050
- function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, default_pstyle) {
5051
- const parts = [];
5052
- const active_ins = {};
5053
- const active_del = {};
5054
- const active_comments = /* @__PURE__ */ new Set();
5055
- const active_fmt = {};
5056
- const deferred_meta_states = [];
5057
- let pending_text = "";
5058
- let current_wrappers = ["", ""];
5059
- let current_style = ["", ""];
5060
- const items = Array.from(iter_paragraph_content(paragraph));
5061
- const is_heading = is_heading_paragraph(paragraph, style_cache, default_pstyle);
5062
- const native_heading = is_native_heading(paragraph, style_cache, default_pstyle);
5063
- let leading_strip_active = is_heading;
5064
- for (let i = 0; i < items.length; i++) {
5065
- const item = items[i];
5066
- if (item instanceof Run) {
5067
- const [prefix, suffix] = get_run_style_markers(item, native_heading);
5068
- const text = get_run_text(item);
5069
- if (cleanView && Object.keys(active_del).length > 0) continue;
5070
- if (leading_strip_active) {
5071
- if (!text || !text.trim()) continue;
5072
- leading_strip_active = false;
5073
- }
5074
- const seg = apply_formatting_to_segments(text, prefix, suffix);
5075
- if (seg) {
5076
- const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
5077
- const new_style = [prefix, suffix];
5078
- if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
5079
- if (new_style[0] === current_style[0] && new_style[1] === current_style[1] && current_style[0] !== "" && pending_text.endsWith(current_style[1]) && seg.startsWith(new_style[0])) {
5080
- pending_text = pending_text.slice(0, -current_style[1].length) + seg.slice(new_style[0].length);
5081
- } else {
5082
- pending_text += seg;
5083
- }
5084
- current_style = new_style;
5085
- } else {
5086
- if (pending_text) parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5087
- pending_text = seg;
5088
- current_wrappers = new_wrappers;
5089
- current_style = new_style;
5090
- }
5091
- if (!cleanView) {
5092
- const has_meta = Object.keys(active_ins).length > 0 || Object.keys(active_del).length > 0 || active_comments.size > 0 || Object.keys(active_fmt).length > 0;
5093
- if (has_meta) {
5094
- deferred_meta_states.push([{ ...active_ins }, { ...active_del }, new Set(active_comments), { ...active_fmt }]);
5095
- }
5096
- let should_defer = false;
5097
- const is_redline = Object.keys(active_ins).length > 0 || Object.keys(active_del).length > 0 || Object.keys(active_fmt).length > 0;
5098
- if (is_redline) {
5099
- let j = i + 1;
5100
- let next_is_redline = false;
5101
- let temp_ins = Object.keys(active_ins).length;
5102
- let temp_del = Object.keys(active_del).length;
5103
- let temp_fmt = Object.keys(active_fmt).length;
5104
- while (j < items.length) {
5105
- const next_item = items[j];
5106
- if (next_item instanceof Run) {
5107
- if (!get_run_text(next_item)) {
5108
- j++;
5109
- continue;
5110
- }
5111
- if (temp_ins > 0 || temp_del > 0 || temp_fmt > 0) next_is_redline = true;
5112
- break;
5113
- } else {
5114
- const ev = next_item;
5115
- if (ev.type === "ins_start") temp_ins++;
5116
- else if (ev.type === "ins_end") temp_ins = Math.max(0, temp_ins - 1);
5117
- else if (ev.type === "del_start") temp_del++;
5118
- else if (ev.type === "del_end") temp_del = Math.max(0, temp_del - 1);
5119
- else if (ev.type === "fmt_start") temp_fmt++;
5120
- else if (ev.type === "fmt_end") temp_fmt = Math.max(0, temp_fmt - 1);
5121
- }
5122
- j++;
5123
- }
5124
- if (next_is_redline) should_defer = true;
5125
- }
5126
- if (!should_defer && deferred_meta_states.length > 0) {
5127
- const meta_block = _build_merged_meta_block(deferred_meta_states, comments_map);
5128
- if (meta_block) {
5129
- if (pending_text) {
5130
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5131
- pending_text = "";
5132
- current_wrappers = ["", ""];
5133
- current_style = ["", ""];
5134
- }
5135
- parts.push(`{>>${meta_block}<<}`);
5136
- }
5137
- deferred_meta_states.length = 0;
5138
- }
5139
- }
5433
+ function strip_image_alt_text(doc) {
5434
+ let count = 0;
5435
+ for (const docPr of findDescendantsByLocalName(doc.element, "docPr")) {
5436
+ const descr = docPr.getAttribute("descr");
5437
+ if (descr) {
5438
+ const isShort = descr.length < 10;
5439
+ const isFile = descr.includes(".") && descr.length < 60;
5440
+ if (isShort || isFile) {
5441
+ docPr.removeAttribute("descr");
5442
+ count++;
5140
5443
  }
5141
- } else {
5142
- const ev = item;
5143
- leading_strip_active = false;
5144
- if (!["ins_start", "ins_end", "del_start", "del_end", "fmt_start", "fmt_end"].includes(ev.type)) {
5145
- if (pending_text) {
5146
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5147
- pending_text = "";
5148
- current_wrappers = ["", ""];
5149
- current_style = ["", ""];
5444
+ }
5445
+ }
5446
+ return count ? [`Image alt text: ${count} auto-generated descriptions removed`] : [];
5447
+ }
5448
+ function audit_hyperlinks(doc) {
5449
+ const internal = ["sharepoint.com", "onedrive.com", ".internal", "intranet", "localhost", "10.", "192.168.", "172.16."];
5450
+ const warnings = [];
5451
+ const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
5452
+ for (const rel of findAllDescendants(docRels._element, "Relationship")) {
5453
+ if (rel.getAttribute("TargetMode") === "External") {
5454
+ const url = rel.getAttribute("Target") || "";
5455
+ for (const pattern of internal) {
5456
+ if (url.toLowerCase().includes(pattern.toLowerCase())) {
5457
+ warnings.push(`Hyperlink targets internal URL: ${_truncate(url, 80)}`);
5458
+ break;
5150
5459
  }
5151
5460
  }
5152
- if (ev.type === "start") active_comments.add(ev.id);
5153
- else if (ev.type === "end") active_comments.delete(ev.id);
5154
- else if (ev.type === "ins_start") active_ins[ev.id] = ev;
5155
- else if (ev.type === "ins_end") delete active_ins[ev.id];
5156
- else if (ev.type === "del_start") active_del[ev.id] = ev;
5157
- else if (ev.type === "del_end") delete active_del[ev.id];
5158
- else if (ev.type === "fmt_start") active_fmt[ev.id] = ev;
5159
- else if (ev.type === "fmt_end") delete active_fmt[ev.id];
5160
- else if (ev.type === "footnote" || ev.type === "endnote") {
5161
- if (pending_text) {
5162
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5163
- pending_text = "";
5164
- current_wrappers = ["", ""];
5165
- current_style = ["", ""];
5166
- }
5167
- parts.push(`[^${ev.type === "footnote" ? "fn" : "en"}-${ev.id}]`);
5168
- } else if (ev.type === "hyperlink_start") {
5169
- if (pending_text) {
5170
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5171
- pending_text = "";
5172
- current_wrappers = ["", ""];
5173
- current_style = ["", ""];
5174
- }
5175
- parts.push("[");
5176
- } else if (ev.type === "hyperlink_end") {
5177
- if (pending_text) {
5178
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5179
- pending_text = "";
5180
- current_wrappers = ["", ""];
5181
- current_style = ["", ""];
5182
- }
5183
- parts.push(`](${ev.date})`);
5184
- } else if (ev.type === "xref_start") {
5185
- if (pending_text) {
5186
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5187
- pending_text = "";
5188
- current_wrappers = ["", ""];
5189
- current_style = ["", ""];
5190
- }
5191
- parts.push("[~");
5192
- } else if (ev.type === "xref_end") {
5193
- if (pending_text) {
5194
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5195
- pending_text = "";
5196
- current_wrappers = ["", ""];
5197
- current_style = ["", ""];
5198
- }
5199
- parts.push(`~](#${ev.id})`);
5200
- } else if (ev.type === "bookmark") {
5201
- if (pending_text) {
5202
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5203
- pending_text = "";
5204
- current_wrappers = ["", ""];
5205
- current_style = ["", ""];
5206
- }
5207
- parts.push(`{#${ev.id}}`);
5208
- }
5209
5461
  }
5210
5462
  }
5211
- if (pending_text) parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5212
- if (deferred_meta_states.length > 0) {
5213
- const meta_block = _build_merged_meta_block(deferred_meta_states, comments_map);
5214
- if (meta_block) parts.push(`{>>${meta_block}<<}`);
5463
+ return warnings;
5464
+ }
5465
+
5466
+ // src/domain.ts
5467
+ function boundedLevenshtein(a, b, maxDist = 2) {
5468
+ if (a === b) return 0;
5469
+ if (Math.abs(a.length - b.length) > maxDist) return maxDist + 1;
5470
+ if (a.length === 0) return b.length <= maxDist ? b.length : maxDist + 1;
5471
+ if (b.length === 0) return a.length <= maxDist ? a.length : maxDist + 1;
5472
+ if (a.length > b.length) {
5473
+ const temp = a;
5474
+ a = b;
5475
+ b = temp;
5215
5476
  }
5216
- return parts.join("");
5477
+ let row = Array.from({ length: a.length + 1 }, (_, i) => i);
5478
+ for (let i = 1; i <= b.length; i++) {
5479
+ const newRow = [i];
5480
+ let minInRow = i;
5481
+ for (let j = 1; j <= a.length; j++) {
5482
+ const cost = a[j - 1] === b[i - 1] ? 0 : 1;
5483
+ const val = Math.min(row[j] + 1, newRow[j - 1] + 1, row[j - 1] + cost);
5484
+ newRow.push(val);
5485
+ if (val < minInRow) minInRow = val;
5486
+ }
5487
+ if (minInRow > maxDist) return maxDist + 1;
5488
+ row = newRow;
5489
+ }
5490
+ return row[a.length] <= maxDist ? row[a.length] : maxDist + 1;
5217
5491
  }
5218
- function _get_wrappers(ins, del, comments, fmt) {
5219
- if (Object.keys(del).length > 0) return ["{--", "--}"];
5220
- if (Object.keys(ins).length > 0) return ["{++", "++}"];
5221
- if (comments.size > 0 || Object.keys(fmt).length > 0) return ["{==", "==}"];
5222
- return ["", ""];
5492
+ function _get_paragraph_text(p) {
5493
+ let text = "";
5494
+ const runs = findAllDescendants(p._element, "w:r");
5495
+ for (const r of runs) {
5496
+ text += get_run_text(new Run(r, p));
5497
+ }
5498
+ return text;
5223
5499
  }
5224
- function _build_merged_meta_block(states_list, comments_map) {
5225
- const change_lines = [];
5226
- const comment_lines = [];
5227
- const seen_sigs = /* @__PURE__ */ new Set();
5228
- for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
5229
- let render_comment2 = function(cid) {
5230
- if (!comments_map[cid]) return;
5231
- const sig = `Com:${cid}`;
5232
- if (seen_sigs.has(sig)) return;
5233
- const data = comments_map[cid];
5234
- let header = `[${sig}] ${data.author}`;
5235
- if (data.date) header += ` @ ${data.date}`;
5236
- if (data.resolved) header += `(RESOLVED)`;
5237
- comment_lines.push(`${header}: ${data.text}`);
5238
- seen_sigs.add(sig);
5239
- if (children_map[cid]) {
5240
- const children = children_map[cid].sort((a, b) => (comments_map[a]?.date || "").localeCompare(comments_map[b]?.date || ""));
5241
- for (const child_id of children) {
5242
- render_comment2(child_id);
5500
+ function extract_all_domain_metadata(doc, base_text) {
5501
+ const definitions = {};
5502
+ const duplicates = /* @__PURE__ */ new Set();
5503
+ const raw_anchors = {};
5504
+ const raw_references = [];
5505
+ const leading_re = /^(?:[\d.\-()a-zA-Z]+\s*)?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”]/;
5506
+ const inline_re = /\([^)]*?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”][^)]*?\)/g;
5507
+ for (const item of iter_block_items(doc)) {
5508
+ if (!(item instanceof Paragraph)) continue;
5509
+ const text = _get_paragraph_text(item).trim();
5510
+ if (!text) continue;
5511
+ const extracted_terms = [];
5512
+ const leading_match = text.match(leading_re);
5513
+ if (leading_match) extracted_terms.push(leading_match[1].trim());
5514
+ const inline_matches = text.matchAll(inline_re);
5515
+ for (const m of inline_matches) {
5516
+ extracted_terms.push(m[1].trim());
5517
+ }
5518
+ for (const term of extracted_terms) {
5519
+ if (definitions[term]) duplicates.add(term);
5520
+ else definitions[term] = { count: 0 };
5521
+ }
5522
+ const short_text = text.length > 60 ? text.substring(0, 60) + "..." : text;
5523
+ const nodes = findAllDescendants(item._element, "*");
5524
+ for (const node of nodes) {
5525
+ if (node.tagName === "w:bookmarkStart") {
5526
+ const b_name = node.getAttribute("w:name");
5527
+ if (b_name && (!b_name.startsWith("_") || b_name.startsWith("_Ref"))) {
5528
+ if (!raw_anchors[b_name]) {
5529
+ raw_anchors[b_name] = {
5530
+ anchored_to: short_text,
5531
+ referenced_from: []
5532
+ };
5533
+ }
5243
5534
  }
5244
5535
  }
5245
- };
5246
- var render_comment = render_comment2;
5247
- for (const [uid, meta] of Object.entries(ins_map)) {
5248
- const sig = `Chg:${uid}`;
5249
- if (!seen_sigs.has(sig)) {
5250
- change_lines.push(`[${sig} insert] ${meta.author || "Unknown"}`);
5251
- seen_sigs.add(sig);
5536
+ let target = null;
5537
+ if (node.tagName === "w:fldSimple") {
5538
+ const instr = node.getAttribute("w:instr") || "";
5539
+ const parts = instr.trim().split(/\s+/);
5540
+ if (parts.length > 1 && parts[0] === "REF") target = parts[1];
5541
+ } else if (node.tagName === "w:instrText") {
5542
+ const instr = node.textContent || "";
5543
+ const parts = instr.trim().split(/\s+/);
5544
+ if (parts.length > 1 && parts[0] === "REF") target = parts[1];
5252
5545
  }
5546
+ if (target) raw_references.push([target, short_text]);
5253
5547
  }
5254
- for (const [uid, meta] of Object.entries(del_map)) {
5255
- const sig = `Chg:${uid}`;
5256
- if (!seen_sigs.has(sig)) {
5257
- change_lines.push(`[${sig} delete] ${meta.author || "Unknown"}`);
5258
- seen_sigs.add(sig);
5259
- }
5548
+ }
5549
+ for (const [target, ref_text] of raw_references) {
5550
+ if (raw_anchors[target]) {
5551
+ raw_anchors[target].referenced_from.push(ref_text);
5260
5552
  }
5261
- for (const [uid, meta] of Object.entries(fmt_map)) {
5262
- const sig = `Chg:${uid}`;
5263
- if (!seen_sigs.has(sig)) {
5264
- change_lines.push(`[${sig} format] ${meta.author || "Unknown"}`);
5265
- seen_sigs.add(sig);
5553
+ }
5554
+ const diagnostics = [];
5555
+ const def_keys = Object.keys(definitions);
5556
+ if (def_keys.length > 0) {
5557
+ const sorted_terms = def_keys.sort((a, b) => b.length - a.length);
5558
+ const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5559
+ const alt = sorted_terms.map(escapeRegExp).join("|");
5560
+ const usage_pattern = new RegExp(`(?<!["\u201C])\\b(${alt})\\b(?![\u201D"])`, "g");
5561
+ for (const m of base_text.matchAll(usage_pattern)) {
5562
+ const matched_term = m[1];
5563
+ if (definitions[matched_term]) definitions[matched_term].count++;
5564
+ }
5565
+ for (const term of def_keys) {
5566
+ if (definitions[term].count === 0) {
5567
+ delete definitions[term];
5568
+ duplicates.delete(term);
5266
5569
  }
5267
5570
  }
5268
- const children_map = {};
5269
- for (const [c_id, data] of Object.entries(comments_map)) {
5270
- const p_id = data.parent_id;
5271
- if (p_id) {
5272
- if (!children_map[p_id]) children_map[p_id] = [];
5273
- children_map[p_id].push(c_id);
5571
+ }
5572
+ for (const term of duplicates) {
5573
+ diagnostics.push(
5574
+ `[Error] Duplicate Definition: '${term}' is defined multiple times.`
5575
+ );
5576
+ }
5577
+ const stop_words = /* @__PURE__ */ new Set([
5578
+ "The",
5579
+ "This",
5580
+ "That",
5581
+ "Such",
5582
+ "A",
5583
+ "An",
5584
+ "Any",
5585
+ "All",
5586
+ "Some",
5587
+ "No",
5588
+ "Every",
5589
+ "Each",
5590
+ "As",
5591
+ "In",
5592
+ "Of",
5593
+ "For",
5594
+ "To",
5595
+ "On",
5596
+ "By",
5597
+ "With"
5598
+ ]);
5599
+ const all_cap_pattern = /\b[A-Z][a-zA-Z]*(?:\s+[A-Z][a-zA-Z]*)*\b/g;
5600
+ const all_caps = new Set(base_text.match(all_cap_pattern) || []);
5601
+ const valid_terms = new Set(Object.keys(definitions));
5602
+ const terms_by_first_letter = {};
5603
+ for (const term of valid_terms) {
5604
+ const fl = term[0].toLowerCase();
5605
+ if (!terms_by_first_letter[fl]) terms_by_first_letter[fl] = [];
5606
+ terms_by_first_letter[fl].push(term);
5607
+ }
5608
+ const candidates_by_term = {};
5609
+ for (const raw_candidate of all_caps) {
5610
+ let candidate = raw_candidate.trim();
5611
+ const words = candidate.split(/\s+/);
5612
+ while (words.length > 0) {
5613
+ const first = words[0];
5614
+ const title = first.charAt(0).toUpperCase() + first.slice(1).toLowerCase();
5615
+ if (stop_words.has(title)) words.shift();
5616
+ else break;
5617
+ }
5618
+ candidate = words.join(" ");
5619
+ if (candidate.length < 4) continue;
5620
+ if (valid_terms.has(candidate)) continue;
5621
+ const first_letter = candidate[0].toLowerCase();
5622
+ let candidate_terms = terms_by_first_letter[first_letter] || [];
5623
+ if (candidate.length > 5) {
5624
+ for (const [k, v] of Object.entries(terms_by_first_letter)) {
5625
+ if (k !== first_letter) candidate_terms = candidate_terms.concat(v);
5274
5626
  }
5275
5627
  }
5276
- const sorted_ids = Array.from(comments_set).sort();
5277
- for (const c_id of sorted_ids) {
5278
- render_comment2(c_id);
5628
+ for (const term of candidate_terms) {
5629
+ if (Math.abs(candidate.length - term.length) > 2) continue;
5630
+ if (candidate === term + "s" || candidate === term + "es") continue;
5631
+ if (term === candidate + "s" || term === candidate + "es") continue;
5632
+ const dist = boundedLevenshtein(candidate, term, 2);
5633
+ if (dist === 0 || dist > 2) continue;
5634
+ if (term.length <= 5) {
5635
+ if (dist > 1) continue;
5636
+ if (candidate[0].toLowerCase() !== term[0].toLowerCase()) continue;
5637
+ }
5638
+ if (!candidates_by_term[term]) candidates_by_term[term] = [];
5639
+ if (!candidates_by_term[term].includes(candidate))
5640
+ candidates_by_term[term].push(candidate);
5279
5641
  }
5280
5642
  }
5281
- return [...change_lines, ...comment_lines].join("\n");
5282
- }
5283
-
5284
- // src/outline.ts
5285
- var _HEADING_PREFIX_RE = /^(#{1,6}) /;
5286
- var _HEURISTIC_MIN_WORDS = 3;
5287
- function extract_outline(doc, projected_body, body_pages, body_page_offsets, paragraph_offsets = null) {
5288
- if (body_pages.length !== body_page_offsets.length) {
5289
- throw new Error("body_pages and body_page_offsets length mismatch");
5643
+ for (const [term, candidates] of Object.entries(candidates_by_term)) {
5644
+ candidates.sort();
5645
+ const c_str = candidates.map((c) => `'${c}'`).join(", ");
5646
+ diagnostics.push(`[Info] Possible Typos for '${term}': Found ${c_str}`);
5290
5647
  }
5291
- if (paragraph_offsets) {
5292
- return _extract_outline_fast(doc, projected_body, body_page_offsets, paragraph_offsets);
5648
+ function diag_sort_key(msg) {
5649
+ if (msg.startsWith("[Error]")) return 0;
5650
+ if (msg.startsWith("[Warning]")) return 1;
5651
+ return 2;
5293
5652
  }
5294
- const comments_map = extract_comments_data(doc.pkg);
5295
- const block_records = _walk_doc_body(doc, comments_map);
5296
- const heading_indices = [];
5297
- for (let idx = 0; idx < block_records.length; idx++) {
5298
- const rec = block_records[idx];
5299
- if (!(rec.is_paragraph && _is_heading(rec.item))) continue;
5300
- if (!_heading_passes_quality_filter(rec.item, comments_map)) continue;
5301
- heading_indices.push(idx);
5653
+ diagnostics.sort((a, b) => {
5654
+ const keyA = diag_sort_key(a);
5655
+ const keyB = diag_sort_key(b);
5656
+ if (keyA !== keyB) return keyA - keyB;
5657
+ return a.localeCompare(b);
5658
+ });
5659
+ return [definitions, diagnostics, raw_anchors];
5660
+ }
5661
+ function extract_document_settings_warnings(doc) {
5662
+ const warnings = [];
5663
+ const settingsPart = doc.pkg.getPartByPath("word/settings.xml");
5664
+ if (!settingsPart) return warnings;
5665
+ const isTruthy = (el) => {
5666
+ if (!el.hasAttribute("w:val")) return true;
5667
+ const val = (el.getAttribute("w:val") || "").toLowerCase();
5668
+ return val !== "0" && val !== "false" && val !== "off";
5669
+ };
5670
+ const removePersonal = findDescendantsByLocalName(
5671
+ settingsPart._element,
5672
+ "removePersonalInformation"
5673
+ );
5674
+ if (removePersonal.length > 0 && isTruthy(removePersonal[0])) {
5675
+ warnings.push(
5676
+ "[Warning] Privacy flag `removePersonalInformation` is enabled in word/settings.xml. Microsoft Word will strip the `w:author`, `w:initials`, and `w:date` attributes from every tracked change and comment the next time this document is opened and saved. Edits made by this agent will lose attribution, breaking audit trails and any multi-turn workflow that relies on identifying prior edits."
5677
+ );
5302
5678
  }
5303
- if (heading_indices.length === 0) return [];
5304
- const nodes = [];
5305
- for (let h_pos = 0; h_pos < heading_indices.length; h_pos++) {
5306
- const rec_idx = heading_indices[h_pos];
5307
- const rec = block_records[rec_idx];
5308
- const paragraph = rec.item;
5309
- const level = _heading_level(paragraph);
5310
- const text = _heading_text(paragraph, comments_map);
5311
- const style = _determine_heading_style(paragraph);
5312
- const owned_end = _find_owned_end(
5313
- block_records,
5314
- heading_indices,
5315
- h_pos,
5316
- level
5679
+ const removeDateTime = findDescendantsByLocalName(
5680
+ settingsPart._element,
5681
+ "removeDateAndTime"
5682
+ );
5683
+ if (removeDateTime.length > 0 && isTruthy(removeDateTime[0])) {
5684
+ warnings.push(
5685
+ "[Warning] Privacy flag `removeDateAndTime` is enabled in word/settings.xml. Microsoft Word will strip the `w:date` attribute from every tracked change and comment the next time this document is opened and saved. Timestamps on this agent's edits will be lost on the next Word save."
5317
5686
  );
5318
- const owned_blocks = block_records.slice(rec_idx + 1, owned_end);
5319
- const has_table = _direct_has_table(block_records, rec_idx + 1, owned_end);
5320
- const footnote_ids = _collect_footnote_ids(owned_blocks);
5321
- const page_num = _offset_to_page(rec.start_offset, body_page_offsets);
5322
- nodes.push({ level, text, page: page_num, style, has_table, footnote_ids });
5323
5687
  }
5324
- return nodes;
5688
+ return warnings;
5325
5689
  }
5326
- function _direct_has_table(block_records, range_start, range_end) {
5327
- for (let idx = range_start; idx < range_end; idx++) {
5328
- const rec = block_records[idx];
5329
- if (rec.is_paragraph && _is_heading(rec.item)) return false;
5330
- if (rec.is_table) return true;
5690
+ function build_structural_appendix(doc, base_text) {
5691
+ const [defs, diagnostics, anchors] = extract_all_domain_metadata(
5692
+ doc,
5693
+ base_text
5694
+ );
5695
+ const settings_warnings = extract_document_settings_warnings(doc);
5696
+ const lines = [
5697
+ "\n\n---",
5698
+ "",
5699
+ "<!-- READONLY_BOUNDARY_START -->",
5700
+ "# Document Structure (Read-Only)",
5701
+ "The content below is metadata describing the document's reference structure. Do not include this section in any tracked changes or edits \u2014 it is for your context only and will be discarded on write."
5702
+ ];
5703
+ let has_content = false;
5704
+ if (settings_warnings.length > 0) {
5705
+ has_content = true;
5706
+ lines.push("\n## Document Settings");
5707
+ for (const warning of settings_warnings) {
5708
+ lines.push(`- ${warning}`);
5709
+ }
5331
5710
  }
5332
- return false;
5333
- }
5334
- function _walk_doc_body(doc, comments_map) {
5335
- const parts = Array.from(iter_document_parts(doc));
5336
- let body_start_offset = 0;
5337
- let body_part = null;
5338
- for (const part of parts) {
5339
- if (part === doc) {
5340
- body_part = part;
5341
- break;
5711
+ if (Object.keys(defs).length > 0) {
5712
+ has_content = true;
5713
+ lines.push("\n## Defined Terms");
5714
+ for (const [term, data] of Object.entries(defs)) {
5715
+ lines.push(`- "${term}" \u2014 used ${data.count} times.`);
5342
5716
  }
5343
- const part_text = _project_part(part, comments_map);
5717
+ }
5718
+ if (diagnostics.length > 0) {
5719
+ has_content = true;
5720
+ lines.push("\n## Semantic Diagnostics");
5721
+ for (const diag of diagnostics) {
5722
+ lines.push(`- ${diag}`);
5723
+ }
5724
+ }
5725
+ if (Object.keys(anchors).length > 0) {
5726
+ has_content = true;
5727
+ lines.push("\n## Named Anchors");
5728
+ for (const [b_name, data] of Object.entries(anchors)) {
5729
+ lines.push(`- ${b_name} \u2192 Anchored to: "${data.anchored_to}"`);
5730
+ for (const ref of data.referenced_from) {
5731
+ lines.push(` - Referenced from: "${ref}"`);
5732
+ }
5733
+ }
5734
+ }
5735
+ if (has_content) {
5736
+ return lines.join("\n");
5737
+ }
5738
+ return "";
5739
+ }
5740
+
5741
+ // src/ingest.ts
5742
+ async function extractTextFromBuffer(buffer, cleanView = false) {
5743
+ const doc = await DocumentObject.load(buffer);
5744
+ return _extractTextFromDoc(doc, cleanView);
5745
+ }
5746
+ function _extractTextFromDoc(doc, cleanView = false, includeAppendix = true, return_paragraph_offsets = false) {
5747
+ const comments_map = extract_comments_data(doc.pkg);
5748
+ const full_text = [];
5749
+ const paragraph_offsets = /* @__PURE__ */ new Map();
5750
+ let cursor = 0;
5751
+ for (const part of iter_document_parts(doc)) {
5752
+ const part_cursor = full_text.length > 0 ? cursor + 2 : cursor;
5753
+ const part_text = _extract_blocks(
5754
+ part,
5755
+ comments_map,
5756
+ cleanView,
5757
+ part_cursor,
5758
+ return_paragraph_offsets ? paragraph_offsets : void 0
5759
+ );
5344
5760
  if (part_text) {
5345
- if (body_start_offset > 0) body_start_offset += 2;
5346
- body_start_offset += part_text.length;
5761
+ if (full_text.length > 0) cursor += 2;
5762
+ full_text.push(part_text);
5763
+ cursor += part_text.length;
5347
5764
  }
5348
5765
  }
5349
- if (!body_part) {
5350
- body_part = doc;
5351
- body_start_offset = 0;
5352
- } else {
5353
- if (body_start_offset > 0) body_start_offset += 2;
5766
+ let base_text = full_text.join("\n\n");
5767
+ if (includeAppendix) {
5768
+ const appendix = build_structural_appendix(doc, base_text);
5769
+ if (appendix) base_text += appendix;
5354
5770
  }
5355
- const records = [];
5356
- let cursor = body_start_offset;
5771
+ if (return_paragraph_offsets) {
5772
+ return { text: base_text, paragraph_offsets };
5773
+ }
5774
+ return base_text;
5775
+ }
5776
+ function _extract_blocks(container, comments_map, cleanView, cursor, paragraph_offsets) {
5777
+ const part = container.part || container;
5778
+ const [style_cache, default_pstyle] = _get_style_cache(part);
5779
+ const blocks = [];
5780
+ let local_cursor = cursor;
5357
5781
  let is_first_block = true;
5358
- for (const item of iter_block_items(body_part)) {
5359
- if (item instanceof Paragraph) {
5360
- const prefix = get_paragraph_prefix(item);
5361
- const p_text = build_paragraph_text(item, comments_map, false);
5362
- const block_len = (prefix + p_text).length;
5363
- if (!is_first_block) cursor += 2;
5364
- records.push({
5782
+ let is_first_para = true;
5783
+ if (container.constructor && container.constructor.name === "NotesPart") {
5784
+ const header = container.note_type === "fn" ? "## Footnotes" : "## Endnotes";
5785
+ const sep = `---
5786
+ ${header}`;
5787
+ blocks.push(sep);
5788
+ local_cursor += sep.length;
5789
+ is_first_block = false;
5790
+ }
5791
+ for (const item of iter_block_items(container)) {
5792
+ if (!is_first_block) local_cursor += 2;
5793
+ const block_start = local_cursor;
5794
+ if (item.constructor.name === "FootnoteItem") {
5795
+ const fn_text = _extract_blocks(
5365
5796
  item,
5366
- is_paragraph: true,
5367
- is_table: false,
5368
- start_offset: cursor,
5369
- projected_length: block_len
5370
- });
5371
- cursor += block_len;
5797
+ comments_map,
5798
+ cleanView,
5799
+ block_start,
5800
+ paragraph_offsets
5801
+ );
5802
+ if (fn_text) {
5803
+ blocks.push(fn_text);
5804
+ local_cursor = block_start + fn_text.length;
5805
+ is_first_block = false;
5806
+ } else if (!is_first_block) {
5807
+ local_cursor -= 2;
5808
+ }
5809
+ } else if (item instanceof Paragraph) {
5810
+ let prefix = get_paragraph_prefix(item, style_cache, default_pstyle);
5811
+ if (is_first_para && container.constructor.name === "FootnoteItem") {
5812
+ prefix = `[^${container.note_type}-${container.id}]: ` + prefix;
5813
+ }
5814
+ const p_text = build_paragraph_text(
5815
+ item,
5816
+ comments_map,
5817
+ cleanView,
5818
+ style_cache,
5819
+ default_pstyle
5820
+ );
5821
+ const full_block = prefix + p_text;
5822
+ blocks.push(full_block);
5823
+ if (paragraph_offsets) {
5824
+ paragraph_offsets.set(item._element, [block_start, full_block.length]);
5825
+ }
5826
+ local_cursor = block_start + full_block.length;
5827
+ is_first_para = false;
5372
5828
  is_first_block = false;
5373
5829
  } else if (item instanceof Table) {
5374
- const table_text = extract_table(item, comments_map, false, 0);
5375
- const block_len = table_text ? table_text.length : 0;
5376
- if (!is_first_block) cursor += 2;
5377
- const table_start = cursor;
5378
- records.push({
5830
+ const table_text = extract_table(
5379
5831
  item,
5380
- is_paragraph: false,
5381
- is_table: true,
5382
- start_offset: table_start,
5383
- projected_length: block_len
5384
- });
5385
- _record_table_inner_blocks_lite(item, table_start, records, comments_map);
5386
- cursor += block_len;
5387
- is_first_block = false;
5832
+ comments_map,
5833
+ cleanView,
5834
+ block_start,
5835
+ paragraph_offsets
5836
+ );
5837
+ if (table_text) {
5838
+ blocks.push(table_text);
5839
+ local_cursor = block_start + table_text.length;
5840
+ is_first_block = false;
5841
+ } else if (!is_first_block) {
5842
+ local_cursor -= 2;
5843
+ }
5844
+ is_first_para = false;
5388
5845
  }
5389
5846
  }
5390
- return records;
5847
+ return blocks.join("\n\n");
5391
5848
  }
5392
- function _compute_inner_block_offset(table, target_paragraph, table_start_offset, comments_map) {
5393
- const target_el = target_paragraph._element;
5394
- let cursor = table_start_offset;
5849
+ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets) {
5850
+ const rows_text = [];
5395
5851
  let rows_processed = 0;
5852
+ let local_cursor = cursor;
5396
5853
  for (const row of table.rows) {
5397
- if (rows_processed > 0) cursor += 1;
5854
+ const cell_texts = [];
5398
5855
  const seen_cells = /* @__PURE__ */ new Set();
5399
- let cells_in_row = 0;
5856
+ const trPr = findChild(row._element, "w:trPr");
5857
+ const ins = trPr ? findChild(trPr, "w:ins") : null;
5858
+ const del_node = trPr ? findChild(trPr, "w:del") : null;
5859
+ if (cleanView && del_node) continue;
5860
+ const row_start = local_cursor + (rows_processed > 0 ? 1 : 0);
5861
+ const wrapper_prefix_len = !cleanView && ins ? 4 : !cleanView && del_node ? 4 : 0;
5862
+ let cell_cursor = row_start + wrapper_prefix_len;
5863
+ let first_cell = true;
5400
5864
  for (const cell of row.cells) {
5401
5865
  if (seen_cells.has(cell)) continue;
5402
5866
  seen_cells.add(cell);
5403
- if (cells_in_row > 0) cursor += 3;
5404
- const [new_cursor, found] = _walk_cell_for_offset(
5867
+ if (!first_cell) cell_cursor += 3;
5868
+ const cell_content = _extract_blocks(
5405
5869
  cell,
5406
- target_el,
5407
- cursor,
5408
- comments_map
5870
+ comments_map,
5871
+ cleanView,
5872
+ cell_cursor,
5873
+ paragraph_offsets
5409
5874
  );
5410
- if (found) return new_cursor;
5411
- cursor = new_cursor;
5412
- cells_in_row++;
5875
+ cell_texts.push(cell_content);
5876
+ cell_cursor += cell_content.length;
5877
+ first_cell = false;
5878
+ }
5879
+ let row_str = cell_texts.join(" | ");
5880
+ if (!cleanView) {
5881
+ if (ins) row_str = `{++ ${row_str} |Chg:${ins.getAttribute("w:id")}++}`;
5882
+ else if (del_node)
5883
+ row_str = `{-- ${row_str} |Chg:${del_node.getAttribute("w:id")}--}`;
5413
5884
  }
5885
+ rows_text.push(row_str);
5886
+ local_cursor = row_start + row_str.length;
5414
5887
  rows_processed++;
5415
5888
  }
5416
- return table_start_offset;
5889
+ return rows_text.join("\n");
5417
5890
  }
5418
- function _walk_cell_for_offset(cell, target_el, cell_start_cursor, comments_map) {
5419
- let cursor = cell_start_cursor;
5420
- let is_first_block = true;
5421
- for (const inner_item of iter_block_items(cell)) {
5422
- if (!is_first_block) cursor += 2;
5423
- if (inner_item instanceof Paragraph) {
5424
- if (inner_item._element === target_el) return [cursor, true];
5425
- const prefix = get_paragraph_prefix(inner_item);
5426
- const p_text = build_paragraph_text(inner_item, comments_map, false);
5427
- cursor += (prefix + p_text).length;
5428
- } else if (inner_item instanceof Table) {
5429
- const nested_offset = _compute_inner_block_offset(
5430
- inner_item,
5431
- new Paragraph(target_el, null),
5432
- cursor,
5433
- comments_map
5434
- );
5435
- if (nested_offset !== cursor) {
5436
- if (_element_is_descendant(target_el, inner_item._element))
5437
- return [nested_offset, true];
5438
- }
5439
- const table_text = extract_table(inner_item, comments_map, false, 0);
5440
- cursor += table_text ? table_text.length : 0;
5441
- }
5442
- is_first_block = false;
5443
- }
5444
- return [cursor, false];
5445
- }
5446
- function _element_is_descendant(target_el, ancestor_el) {
5447
- let cur = target_el.parentNode;
5448
- while (cur) {
5449
- if (cur === ancestor_el) return true;
5450
- cur = cur.parentNode;
5451
- }
5452
- return false;
5453
- }
5454
- function _record_table_inner_blocks_lite(table, inherited_offset, records, comments_map) {
5455
- const seen_cells = /* @__PURE__ */ new Set();
5456
- for (const row of table.rows) {
5457
- for (const cell of row.cells) {
5458
- if (seen_cells.has(cell)) continue;
5459
- seen_cells.add(cell);
5460
- for (const inner_item of iter_block_items(cell)) {
5461
- if (inner_item instanceof Paragraph) {
5462
- const true_offset = _is_heading(inner_item) ? _compute_inner_block_offset(
5463
- table,
5464
- inner_item,
5465
- inherited_offset,
5466
- comments_map
5467
- ) : inherited_offset;
5468
- records.push({
5469
- item: inner_item,
5470
- is_paragraph: true,
5471
- is_table: false,
5472
- start_offset: true_offset,
5473
- projected_length: 0
5474
- });
5475
- } else if (inner_item instanceof Table) {
5476
- records.push({
5477
- item: inner_item,
5478
- is_paragraph: false,
5479
- is_table: true,
5480
- start_offset: inherited_offset,
5481
- projected_length: 0
5482
- });
5483
- _record_table_inner_blocks_lite(
5484
- inner_item,
5485
- inherited_offset,
5486
- records,
5487
- comments_map
5488
- );
5489
- }
5490
- }
5491
- }
5492
- }
5493
- }
5494
- function _project_part(part, comments_map) {
5495
- const blocks = [];
5496
- const c_type = part.constructor.name;
5497
- if (c_type === "NotesPart") {
5498
- const header = part.note_type === "fn" ? "## Footnotes" : "## Endnotes";
5499
- blocks.push(`---
5500
- ${header}`);
5501
- }
5502
- let is_first_para = true;
5503
- for (const item of iter_block_items(part)) {
5504
- if (item.constructor.name === "FootnoteItem") {
5505
- const fn_text = _project_part(item, comments_map);
5506
- if (fn_text) blocks.push(fn_text);
5507
- } else if (item instanceof Paragraph) {
5508
- let prefix = get_paragraph_prefix(item);
5509
- if (is_first_para && c_type === "FootnoteItem")
5510
- prefix = `[^${part.note_type}-${part.id}]: ${prefix}`;
5511
- const p_text = build_paragraph_text(item, comments_map, false);
5512
- blocks.push(prefix + p_text);
5513
- is_first_para = false;
5514
- } else if (item instanceof Table) {
5515
- const table_text = extract_table(item, comments_map, false, 0);
5516
- if (table_text) blocks.push(table_text);
5517
- is_first_para = false;
5518
- }
5519
- }
5520
- return blocks.join("\n\n");
5521
- }
5522
- function _is_heading(paragraph) {
5523
- return _HEADING_PREFIX_RE.test(get_paragraph_prefix(paragraph));
5524
- }
5525
- function _heading_passes_quality_filter(paragraph, comments_map) {
5526
- const style = _determine_heading_style(paragraph);
5527
- if (style !== "(heuristic)") return true;
5528
- const text = _heading_text(paragraph, comments_map);
5529
- if (!text) return false;
5530
- const word_count = (text.match(/\w+/g) || []).length;
5531
- return word_count >= _HEURISTIC_MIN_WORDS;
5532
- }
5533
- function _heading_level(paragraph) {
5534
- const match = _HEADING_PREFIX_RE.exec(get_paragraph_prefix(paragraph));
5535
- return match ? Math.min(match[1].length, 6) : 1;
5536
- }
5537
- function _heading_text(paragraph, comments_map) {
5538
- const p_text = build_paragraph_text(paragraph, comments_map, false);
5539
- let cleaned = _strip_critic_markup(p_text);
5540
- cleaned = _strip_inline_formatting(cleaned);
5541
- return cleaned.trim();
5542
- }
5543
- function _strip_critic_markup(text) {
5544
- if (!text) return "";
5545
- text = text.replace(/\{--[\s\S]*?--\}/g, "");
5546
- text = text.replace(/\{>>[\s\S]*?<<\}/g, "");
5547
- text = text.replace(/\{\+\+([\s\S]*?)\+\+\}/g, "$1");
5548
- text = text.replace(/\{==([\s\S]*?)==\}/g, "$1");
5549
- return text;
5550
- }
5551
- function _strip_inline_formatting(text) {
5552
- if (!text) return "";
5553
- text = text.replace(/\*\*(.+?)\*\*/g, "$1");
5554
- text = text.replace(/__(.+?)__/g, "$1");
5555
- text = text.replace(/(?<!\w)_(\S(?:.*?\S)?)_(?!\w)/g, "$1");
5556
- return text;
5557
- }
5558
- function _determine_heading_style(paragraph) {
5559
- const [style_cache, default_pstyle] = _get_style_cache(
5560
- paragraph._parent.part || paragraph._parent
5891
+ function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, default_pstyle) {
5892
+ const parts = [];
5893
+ const active_ins = {};
5894
+ const active_del = {};
5895
+ const active_comments = /* @__PURE__ */ new Set();
5896
+ const active_fmt = {};
5897
+ const deferred_meta_states = [];
5898
+ let pending_text = "";
5899
+ let current_wrappers = ["", ""];
5900
+ let current_style = ["", ""];
5901
+ const items = Array.from(iter_paragraph_content(paragraph));
5902
+ const is_heading = is_heading_paragraph(
5903
+ paragraph,
5904
+ style_cache,
5905
+ default_pstyle
5561
5906
  );
5562
- const pPr = findChild(paragraph._element, "w:pPr");
5563
- let style_id = default_pstyle;
5564
- if (pPr) {
5565
- const pStyle = findChild(pPr, "w:pStyle");
5566
- if (pStyle) style_id = pStyle.getAttribute("w:val") || default_pstyle;
5567
- }
5568
- let outline_level = null;
5569
- let outline_level_from_style = false;
5570
- if (pPr) {
5571
- const oLvl = findChild(pPr, "w:outlineLvl");
5572
- if (oLvl && /^\d+$/.test(oLvl.getAttribute("w:val") || "")) {
5573
- outline_level = parseInt(oLvl.getAttribute("w:val"), 10);
5574
- }
5575
- }
5576
- if (outline_level === null && style_id && style_cache && style_cache[style_id]) {
5577
- outline_level = style_cache[style_id].outline_level;
5578
- outline_level_from_style = true;
5579
- }
5580
- const style_name = style_id && style_cache && style_cache[style_id] ? style_cache[style_id].name : style_id;
5581
- let normalized_style_name = style_name;
5582
- if (normalized_style_name && typeof normalized_style_name === "string") {
5583
- if (normalized_style_name.toLowerCase().startsWith("heading")) {
5584
- normalized_style_name = normalized_style_name.replace(/^heading/i, "Heading");
5585
- } else if (normalized_style_name.toLowerCase() === "title") {
5586
- normalized_style_name = "Title";
5587
- }
5588
- }
5589
- if (outline_level_from_style && outline_level !== null) {
5590
- const is_heading_or_title = normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title");
5591
- if (!is_heading_or_title) {
5592
- outline_level = null;
5593
- }
5594
- }
5595
- if (outline_level !== null && outline_level >= 0 && outline_level <= 8) {
5596
- if (normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title")) {
5597
- return normalized_style_name;
5598
- }
5599
- return "(outline_level)";
5600
- }
5601
- if (normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title"))
5602
- return normalized_style_name;
5603
- if (normalized_style_name && /Heading[ ]?([1-6])(?![0-9])/.test(normalized_style_name))
5604
- return normalized_style_name;
5605
- return "(heuristic)";
5606
- }
5607
- function _find_owned_end(block_records, heading_indices, current_h_pos, current_level) {
5608
- for (let next_h_pos = current_h_pos + 1; next_h_pos < heading_indices.length; next_h_pos++) {
5609
- const next_idx = heading_indices[next_h_pos];
5610
- if (_heading_level(block_records[next_idx].item) <= current_level)
5611
- return next_idx;
5612
- }
5613
- return block_records.length;
5614
- }
5615
- function _collect_footnote_ids(owned_blocks) {
5616
- const seen = /* @__PURE__ */ new Set();
5617
- const ordered = [];
5618
- for (const rec of owned_blocks) {
5619
- if (!rec.is_paragraph) continue;
5620
- for (const event of iter_paragraph_content(rec.item)) {
5621
- if (!("type" in event)) continue;
5622
- let fn_id = "";
5623
- if (event.type === "footnote") fn_id = `fn-${event.id}`;
5624
- else if (event.type === "endnote") fn_id = `en-${event.id}`;
5625
- else continue;
5626
- if (!seen.has(fn_id)) {
5627
- seen.add(fn_id);
5628
- ordered.push(fn_id);
5907
+ const native_heading = is_native_heading(
5908
+ paragraph,
5909
+ style_cache,
5910
+ default_pstyle
5911
+ );
5912
+ let leading_strip_active = is_heading;
5913
+ for (let i = 0; i < items.length; i++) {
5914
+ const item = items[i];
5915
+ if (item instanceof Run) {
5916
+ const [prefix, suffix] = get_run_style_markers(item, native_heading);
5917
+ const text = get_run_text(item);
5918
+ if (cleanView && Object.keys(active_del).length > 0) continue;
5919
+ if (leading_strip_active) {
5920
+ if (!text || !text.trim()) continue;
5921
+ leading_strip_active = false;
5629
5922
  }
5630
- }
5631
- }
5632
- return ordered;
5633
- }
5634
- function _offset_to_page(offset, body_page_offsets) {
5635
- if (!body_page_offsets || body_page_offsets.length === 0) return 1;
5636
- let page = 1;
5637
- for (let i = 0; i < body_page_offsets.length; i++) {
5638
- if (offset >= body_page_offsets[i]) page = i + 1;
5639
- else break;
5640
- }
5641
- return page;
5642
- }
5643
- function _extract_outline_fast(doc, projected_body, body_page_offsets, paragraph_offsets) {
5644
- const paragraphs_and_tables = [];
5645
- const seen_cells = /* @__PURE__ */ new Set();
5646
- function walk(container) {
5647
- for (const item of iter_block_items(container)) {
5648
- const i_type = item.constructor.name;
5649
- if (i_type === "FootnoteItem") {
5650
- walk(item);
5651
- } else if (item instanceof Paragraph) {
5652
- paragraphs_and_tables.push(["p", item]);
5653
- } else if (item instanceof Table) {
5654
- paragraphs_and_tables.push(["t", item]);
5655
- for (const row of item.rows) {
5656
- for (const cell of row.cells) {
5657
- if (seen_cells.has(cell._element)) {
5658
- continue;
5923
+ const seg = apply_formatting_to_segments(text, prefix, suffix);
5924
+ if (seg) {
5925
+ const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
5926
+ const new_style = [prefix, suffix];
5927
+ if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
5928
+ if (new_style[0] === current_style[0] && new_style[1] === current_style[1] && current_style[0] !== "" && pending_text.endsWith(current_style[1]) && seg.startsWith(new_style[0])) {
5929
+ pending_text = pending_text.slice(0, -current_style[1].length) + seg.slice(new_style[0].length);
5930
+ } else {
5931
+ pending_text += seg;
5932
+ }
5933
+ current_style = new_style;
5934
+ } else {
5935
+ if (pending_text)
5936
+ parts.push(
5937
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
5938
+ );
5939
+ pending_text = seg;
5940
+ current_wrappers = new_wrappers;
5941
+ current_style = new_style;
5942
+ }
5943
+ if (!cleanView) {
5944
+ const has_meta = Object.keys(active_ins).length > 0 || Object.keys(active_del).length > 0 || active_comments.size > 0 || Object.keys(active_fmt).length > 0;
5945
+ if (has_meta) {
5946
+ deferred_meta_states.push([
5947
+ { ...active_ins },
5948
+ { ...active_del },
5949
+ new Set(active_comments),
5950
+ { ...active_fmt }
5951
+ ]);
5952
+ }
5953
+ let should_defer = false;
5954
+ const has_any_meta = Object.keys(active_ins).length > 0 || Object.keys(active_del).length > 0 || Object.keys(active_fmt).length > 0 || active_comments.size > 0;
5955
+ if (has_any_meta) {
5956
+ let j = i + 1;
5957
+ let next_has_meta = false;
5958
+ let temp_ins = Object.keys(active_ins).length;
5959
+ let temp_del = Object.keys(active_del).length;
5960
+ let temp_fmt = Object.keys(active_fmt).length;
5961
+ const temp_comments = new Set(active_comments);
5962
+ while (j < items.length) {
5963
+ const next_item = items[j];
5964
+ if (next_item instanceof Run) {
5965
+ if (!get_run_text(next_item)) {
5966
+ j++;
5967
+ continue;
5968
+ }
5969
+ if (temp_ins > 0 || temp_del > 0 || temp_fmt > 0 || temp_comments.size > 0)
5970
+ next_has_meta = true;
5971
+ break;
5972
+ } else {
5973
+ const ev = next_item;
5974
+ if (ev.type === "ins_start") temp_ins++;
5975
+ else if (ev.type === "ins_end")
5976
+ temp_ins = Math.max(0, temp_ins - 1);
5977
+ else if (ev.type === "del_start") temp_del++;
5978
+ else if (ev.type === "del_end")
5979
+ temp_del = Math.max(0, temp_del - 1);
5980
+ else if (ev.type === "fmt_start") temp_fmt++;
5981
+ else if (ev.type === "fmt_end")
5982
+ temp_fmt = Math.max(0, temp_fmt - 1);
5983
+ else if (ev.type === "start") temp_comments.add(ev.id);
5984
+ else if (ev.type === "end") temp_comments.delete(ev.id);
5985
+ }
5986
+ j++;
5659
5987
  }
5660
- seen_cells.add(cell._element);
5661
- walk(cell);
5988
+ if (next_has_meta) should_defer = true;
5989
+ }
5990
+ if (!should_defer && deferred_meta_states.length > 0) {
5991
+ const meta_block = _build_merged_meta_block(
5992
+ deferred_meta_states,
5993
+ comments_map
5994
+ );
5995
+ if (meta_block) {
5996
+ if (pending_text) {
5997
+ parts.push(
5998
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
5999
+ );
6000
+ pending_text = "";
6001
+ current_wrappers = ["", ""];
6002
+ current_style = ["", ""];
6003
+ }
6004
+ parts.push(`{>>${meta_block}<<}`);
6005
+ }
6006
+ deferred_meta_states.length = 0;
5662
6007
  }
5663
6008
  }
5664
6009
  }
5665
- }
5666
- }
5667
- walk(doc);
5668
- const heading_indices = [];
5669
- for (let idx = 0; idx < paragraphs_and_tables.length; idx++) {
5670
- const [kind, item] = paragraphs_and_tables[idx];
5671
- if (kind !== "p") continue;
5672
- let hasOffset = false;
5673
- if (paragraph_offsets instanceof Map) {
5674
- hasOffset = paragraph_offsets.has(item._element);
5675
6010
  } else {
5676
- hasOffset = item._element in paragraph_offsets;
5677
- }
5678
- if (!hasOffset) {
5679
- continue;
5680
- }
5681
- if (!_is_heading(item)) continue;
5682
- if (!_heading_passes_quality_filter_fast(item, projected_body, paragraph_offsets)) continue;
5683
- heading_indices.push(idx);
5684
- }
5685
- if (heading_indices.length === 0) return [];
5686
- const nodes = [];
5687
- for (let h_pos = 0; h_pos < heading_indices.length; h_pos++) {
5688
- const item_idx = heading_indices[h_pos];
5689
- const paragraph = paragraphs_and_tables[item_idx][1];
5690
- const level = _heading_level(paragraph);
5691
- const text = _heading_text_fast(paragraph, projected_body, paragraph_offsets);
5692
- const style = _determine_heading_style(paragraph);
5693
- let owned_end = item_idx;
5694
- for (let next_h_pos = h_pos + 1; next_h_pos < heading_indices.length; next_h_pos++) {
5695
- const next_idx = heading_indices[next_h_pos];
5696
- const next_paragraph = paragraphs_and_tables[next_idx][1];
5697
- if (_heading_level(next_paragraph) <= level) {
5698
- owned_end = next_idx;
5699
- break;
5700
- }
5701
- }
5702
- if (owned_end === item_idx) {
5703
- owned_end = paragraphs_and_tables.length;
5704
- }
5705
- const owned = paragraphs_and_tables.slice(item_idx + 1, owned_end);
5706
- let has_table = false;
5707
- for (const [kind2, item2] of owned) {
5708
- if (kind2 === "p" && _is_heading(item2)) {
5709
- break;
6011
+ const ev = item;
6012
+ leading_strip_active = false;
6013
+ if (![
6014
+ "ins_start",
6015
+ "ins_end",
6016
+ "del_start",
6017
+ "del_end",
6018
+ "fmt_start",
6019
+ "fmt_end"
6020
+ ].includes(ev.type)) {
6021
+ if (pending_text) {
6022
+ parts.push(
6023
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6024
+ );
6025
+ pending_text = "";
6026
+ current_wrappers = ["", ""];
6027
+ current_style = ["", ""];
6028
+ }
5710
6029
  }
5711
- if (kind2 === "t") {
5712
- has_table = true;
5713
- break;
6030
+ if (ev.type === "start") active_comments.add(ev.id);
6031
+ else if (ev.type === "end") active_comments.delete(ev.id);
6032
+ else if (ev.type === "ins_start") active_ins[ev.id] = ev;
6033
+ else if (ev.type === "ins_end") delete active_ins[ev.id];
6034
+ else if (ev.type === "del_start") active_del[ev.id] = ev;
6035
+ else if (ev.type === "del_end") delete active_del[ev.id];
6036
+ else if (ev.type === "fmt_start") active_fmt[ev.id] = ev;
6037
+ else if (ev.type === "fmt_end") delete active_fmt[ev.id];
6038
+ else if (ev.type === "footnote" || ev.type === "endnote") {
6039
+ if (pending_text) {
6040
+ parts.push(
6041
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6042
+ );
6043
+ pending_text = "";
6044
+ current_wrappers = ["", ""];
6045
+ current_style = ["", ""];
6046
+ }
6047
+ parts.push(`[^${ev.type === "footnote" ? "fn" : "en"}-${ev.id}]`);
6048
+ } else if (ev.type === "hyperlink_start") {
6049
+ if (pending_text) {
6050
+ parts.push(
6051
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6052
+ );
6053
+ pending_text = "";
6054
+ current_wrappers = ["", ""];
6055
+ current_style = ["", ""];
6056
+ }
6057
+ parts.push("[");
6058
+ } else if (ev.type === "hyperlink_end") {
6059
+ if (pending_text) {
6060
+ parts.push(
6061
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6062
+ );
6063
+ pending_text = "";
6064
+ current_wrappers = ["", ""];
6065
+ current_style = ["", ""];
6066
+ }
6067
+ parts.push(`](${ev.date})`);
6068
+ } else if (ev.type === "xref_start") {
6069
+ if (pending_text) {
6070
+ parts.push(
6071
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6072
+ );
6073
+ pending_text = "";
6074
+ current_wrappers = ["", ""];
6075
+ current_style = ["", ""];
6076
+ }
6077
+ parts.push("[~");
6078
+ } else if (ev.type === "xref_end") {
6079
+ if (pending_text) {
6080
+ parts.push(
6081
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6082
+ );
6083
+ pending_text = "";
6084
+ current_wrappers = ["", ""];
6085
+ current_style = ["", ""];
6086
+ }
6087
+ parts.push(`~](#${ev.id})`);
6088
+ } else if (ev.type === "bookmark") {
6089
+ if (pending_text) {
6090
+ parts.push(
6091
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6092
+ );
6093
+ pending_text = "";
6094
+ current_wrappers = ["", ""];
6095
+ current_style = ["", ""];
6096
+ }
6097
+ parts.push(`{#${ev.id}}`);
5714
6098
  }
5715
6099
  }
5716
- const footnote_ids = _collect_footnote_ids_fast(owned);
5717
- let para_offset;
5718
- if (paragraph_offsets instanceof Map) {
5719
- para_offset = paragraph_offsets.get(paragraph._element);
5720
- } else {
5721
- para_offset = paragraph_offsets[paragraph._element];
5722
- }
5723
- let page_num = 1;
5724
- if (para_offset !== void 0) {
5725
- const [start_offset] = para_offset;
5726
- page_num = _offset_to_page(start_offset, body_page_offsets);
5727
- }
5728
- nodes.push({
5729
- level,
5730
- text,
5731
- page: page_num,
5732
- style,
5733
- has_table,
5734
- footnote_ids
5735
- });
5736
- }
5737
- return nodes;
5738
- }
5739
- function _heading_passes_quality_filter_fast(paragraph, projected_body, paragraph_offsets) {
5740
- const style = _determine_heading_style(paragraph);
5741
- if (style !== "(heuristic)") return true;
5742
- const text = _heading_text_fast(paragraph, projected_body, paragraph_offsets);
5743
- if (!text) return false;
5744
- const words = text.match(/\w+/g) || [];
5745
- return words.length >= _HEURISTIC_MIN_WORDS;
5746
- }
5747
- function _heading_text_fast(paragraph, projected_body, paragraph_offsets) {
5748
- let offset;
5749
- if (paragraph_offsets instanceof Map) {
5750
- offset = paragraph_offsets.get(paragraph._element);
5751
- } else {
5752
- offset = paragraph_offsets[paragraph._element];
5753
6100
  }
5754
- if (offset === void 0) {
5755
- return "";
6101
+ if (pending_text)
6102
+ parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
6103
+ if (deferred_meta_states.length > 0) {
6104
+ const meta_block = _build_merged_meta_block(
6105
+ deferred_meta_states,
6106
+ comments_map
6107
+ );
6108
+ if (meta_block) parts.push(`{>>${meta_block}<<}`);
5756
6109
  }
5757
- const [start, length] = offset;
5758
- const raw = projected_body.substring(start, start + length);
5759
- let cleaned = _strip_critic_markup(raw);
5760
- cleaned = _strip_inline_formatting(cleaned);
5761
- cleaned = cleaned.replace(/^#+\s+/, "");
5762
- return cleaned.trim();
6110
+ return parts.join("");
5763
6111
  }
5764
- function _collect_footnote_ids_fast(owned_items) {
5765
- const seen = /* @__PURE__ */ new Set();
5766
- const ordered = [];
5767
- for (const [kind, item] of owned_items) {
5768
- if (kind !== "p") continue;
5769
- for (const event of iter_paragraph_content(item)) {
5770
- if (!("type" in event)) continue;
5771
- let fn_id = "";
5772
- if (event.type === "footnote") fn_id = `fn-${event.id}`;
5773
- else if (event.type === "endnote") fn_id = `en-${event.id}`;
5774
- else continue;
5775
- if (!seen.has(fn_id)) {
5776
- seen.add(fn_id);
5777
- ordered.push(fn_id);
5778
- }
5779
- }
5780
- }
5781
- return ordered;
6112
+ function _get_wrappers(ins, del, comments, fmt) {
6113
+ if (Object.keys(del).length > 0) return ["{--", "--}"];
6114
+ if (Object.keys(ins).length > 0) return ["{++", "++}"];
6115
+ if (comments.size > 0 || Object.keys(fmt).length > 0) return ["{==", "==}"];
6116
+ return ["", ""];
5782
6117
  }
5783
-
5784
- // src/sanitize/report.ts
5785
- var SanitizeReport = class {
5786
- filename;
5787
- mode;
5788
- author;
5789
- tracked_changes_found = 0;
5790
- tracked_changes_accepted = 0;
5791
- tracked_changes_kept = 0;
5792
- change_lines = [];
5793
- comments_removed = 0;
5794
- comments_kept = 0;
5795
- removed_comment_lines = [];
5796
- kept_comment_lines = [];
5797
- metadata_lines = [];
5798
- structural_lines = [];
5799
- warnings = [];
5800
- status = "clean";
5801
- blocked_reason = null;
5802
- constructor(filename, mode = "full", author = null) {
5803
- this.filename = filename;
5804
- this.mode = mode;
5805
- this.author = author;
5806
- }
5807
- add_transform_lines(lines) {
5808
- for (const line of lines) {
5809
- const lower = line.toLowerCase();
5810
- if (lower.includes("tracked change") || lower.includes("insertion") || lower.includes("deletion") || lower.includes("accepted")) {
5811
- this.change_lines.push(line);
5812
- } else if (lower.includes("comment") || lower.includes("[open]") || lower.includes("[resolved]")) {
5813
- if (lower.includes("kept") || lower.includes("visible")) {
5814
- this.kept_comment_lines.push(line);
5815
- } else {
5816
- this.removed_comment_lines.push(line);
5817
- }
5818
- } else if (lower.includes("author") || lower.includes("template") || lower.includes("company") || lower.includes("manager") || lower.includes("metadata") || lower.includes("timestamp") || lower.includes("custom xml") || lower.includes("last modified by") || lower.includes("revision count") || lower.includes("last printed")) {
5819
- this.metadata_lines.push(line);
5820
- } else if (lower.includes("hyperlink") || lower.includes("warning")) {
5821
- this.warnings.push(line);
5822
- } else {
5823
- this.structural_lines.push(line);
5824
- }
5825
- }
5826
- }
5827
- render() {
5828
- const sep = "\u2550".repeat(50);
5829
- const lines = [sep, `Finalization Report: ${this.filename}`];
5830
- const flags = [];
5831
- if (this.mode === "keep-markup") flags.push("--keep-markup");
5832
- if (this.author) flags.push(`--author "${this.author}"`);
5833
- if (this.tracked_changes_accepted > 0) flags.push("--accept-all");
5834
- if (flags.length > 0) lines.push(flags.join(" "));
5835
- lines.push(sep);
5836
- if (this.status === "blocked") {
5837
- lines.push("");
5838
- lines.push(`BLOCKED: ${this.blocked_reason}`);
5839
- lines.push(sep);
5840
- return lines.join("\n");
5841
- }
5842
- if (this.mode === "keep-markup" && (this.tracked_changes_kept > 0 || this.comments_kept > 0)) {
5843
- lines.push("");
5844
- lines.push("VISIBLE TO COUNTERPARTY");
5845
- if (this.tracked_changes_kept > 0) lines.push(` Tracked changes: ${this.tracked_changes_kept}`);
5846
- if (this.comments_kept > 0) {
5847
- lines.push(` Open comments: ${this.comments_kept}`);
5848
- for (const cl of this.kept_comment_lines) lines.push(` ${cl}`);
6118
+ function _build_merged_meta_block(states_list, comments_map) {
6119
+ const change_lines = [];
6120
+ const comment_lines = [];
6121
+ const seen_sigs = /* @__PURE__ */ new Set();
6122
+ for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
6123
+ let render_comment2 = function(cid) {
6124
+ if (!comments_map[cid]) return;
6125
+ const sig = `Com:${cid}`;
6126
+ if (seen_sigs.has(sig)) return;
6127
+ const data = comments_map[cid];
6128
+ let header = `[${sig}] ${data.author}`;
6129
+ if (data.date) header += ` @ ${data.date}`;
6130
+ if (data.resolved) header += `(RESOLVED)`;
6131
+ comment_lines.push(`${header}: ${data.text}`);
6132
+ seen_sigs.add(sig);
6133
+ if (children_map[cid]) {
6134
+ const children = children_map[cid].sort(
6135
+ (a, b) => (comments_map[a]?.date || "").localeCompare(
6136
+ comments_map[b]?.date || ""
6137
+ )
6138
+ );
6139
+ for (const child_id of children) {
6140
+ render_comment2(child_id);
6141
+ }
6142
+ }
6143
+ };
6144
+ var render_comment = render_comment2;
6145
+ for (const [uid, meta] of Object.entries(
6146
+ ins_map
6147
+ )) {
6148
+ const sig = `Chg:${uid}`;
6149
+ if (!seen_sigs.has(sig)) {
6150
+ change_lines.push(`[${sig} insert] ${meta.author || "Unknown"}`);
6151
+ seen_sigs.add(sig);
5849
6152
  }
5850
- if (this.author) lines.push(` Author on all markup: "${this.author}"`);
5851
- }
5852
- if (this.change_lines.length > 0) {
5853
- lines.push("");
5854
- lines.push("TRACKED CHANGES");
5855
- for (const cl of this.change_lines) lines.push(` ${cl}`);
5856
- }
5857
- if (this.removed_comment_lines.length > 0) {
5858
- lines.push("");
5859
- lines.push("COMMENTS (stripped)");
5860
- for (const cl of this.removed_comment_lines) lines.push(` ${cl}`);
5861
6153
  }
5862
- if (this.metadata_lines.length > 0) {
5863
- lines.push("");
5864
- lines.push("METADATA");
5865
- for (const ml of this.metadata_lines) lines.push(` ${ml}`);
6154
+ for (const [uid, meta] of Object.entries(
6155
+ del_map
6156
+ )) {
6157
+ const sig = `Chg:${uid}`;
6158
+ if (!seen_sigs.has(sig)) {
6159
+ change_lines.push(`[${sig} delete] ${meta.author || "Unknown"}`);
6160
+ seen_sigs.add(sig);
6161
+ }
5866
6162
  }
5867
- if (this.structural_lines.length > 0) {
5868
- lines.push("");
5869
- lines.push("STRUCTURAL & PROTECTION");
5870
- for (const sl of this.structural_lines) lines.push(` ${sl}`);
6163
+ for (const [uid, meta] of Object.entries(
6164
+ fmt_map
6165
+ )) {
6166
+ const sig = `Chg:${uid}`;
6167
+ if (!seen_sigs.has(sig)) {
6168
+ change_lines.push(`[${sig} format] ${meta.author || "Unknown"}`);
6169
+ seen_sigs.add(sig);
6170
+ }
5871
6171
  }
5872
- if (this.warnings.length > 0) {
5873
- lines.push("");
5874
- lines.push("WARNINGS");
5875
- for (const w of this.warnings) lines.push(` \u26A0 ${w}`);
6172
+ const children_map = {};
6173
+ for (const [c_id, data] of Object.entries(
6174
+ comments_map
6175
+ )) {
6176
+ const p_id = data.parent_id;
6177
+ if (p_id) {
6178
+ if (!children_map[p_id]) children_map[p_id] = [];
6179
+ children_map[p_id].push(c_id);
6180
+ }
5876
6181
  }
5877
- lines.push("");
5878
- lines.push(sep);
5879
- if (this.warnings.length > 0) {
5880
- lines.push(`Result: CLEAN WITH WARNINGS (${this.warnings.length} warning${this.warnings.length > 1 ? "s" : ""})`);
5881
- } else {
5882
- lines.push(`Result: CLEAN (${this.tracked_changes_found} changes resolved, ${this.comments_removed} comments removed)`);
6182
+ const sorted_ids = Array.from(comments_set).sort();
6183
+ for (const c_id of sorted_ids) {
6184
+ render_comment2(c_id);
5883
6185
  }
5884
- lines.push(sep);
5885
- return lines.join("\n");
5886
6186
  }
5887
- };
6187
+ return [...change_lines, ...comment_lines].join("\n");
6188
+ }
5888
6189
 
5889
- // src/sanitize/transforms.ts
5890
- function findDescendantsByLocalName(element, localName) {
5891
- const result = [];
5892
- const all = element.getElementsByTagName("*");
5893
- for (let i = 0; i < all.length; i++) {
5894
- const tag = all[i].tagName;
5895
- if (tag === localName || tag.endsWith(":" + localName)) {
5896
- result.push(all[i]);
5897
- }
6190
+ // src/outline.ts
6191
+ var _HEADING_PREFIX_RE = /^(#{1,6}) /;
6192
+ var _HEURISTIC_MIN_WORDS = 3;
6193
+ function extract_outline(doc, projected_body, body_pages, body_page_offsets, paragraph_offsets = null) {
6194
+ if (body_pages.length !== body_page_offsets.length) {
6195
+ throw new Error("body_pages and body_page_offsets length mismatch");
5898
6196
  }
5899
- return result;
5900
- }
5901
- function coalesce_runs(doc) {
5902
- let count = 0;
5903
- function areRunsIdentical(rPr1, rPr2) {
5904
- const xml1 = rPr1 ? rPr1.toString() : "";
5905
- const xml2 = rPr2 ? rPr2.toString() : "";
5906
- return xml1 === xml2;
6197
+ if (paragraph_offsets) {
6198
+ return _extract_outline_fast(doc, projected_body, body_page_offsets, paragraph_offsets);
5907
6199
  }
5908
- function hasSpecialContent(run) {
5909
- const safeTags = ["w:t", "w:tab", "w:br", "w:cr", "w:delText", "w:rPr"];
5910
- for (let i = 0; i < run.childNodes.length; i++) {
5911
- const child = run.childNodes[i];
5912
- if (child.nodeType === 1) {
5913
- const tag = child.tagName;
5914
- if (!safeTags.includes(tag)) return true;
5915
- }
5916
- }
5917
- return false;
6200
+ const comments_map = extract_comments_data(doc.pkg);
6201
+ const block_records = _walk_doc_body(doc, comments_map);
6202
+ const heading_indices = [];
6203
+ for (let idx = 0; idx < block_records.length; idx++) {
6204
+ const rec = block_records[idx];
6205
+ if (!(rec.is_paragraph && _is_heading(rec.item))) continue;
6206
+ if (!_heading_passes_quality_filter(rec.item, comments_map)) continue;
6207
+ heading_indices.push(idx);
5918
6208
  }
5919
- function coalesceContainer(container) {
5920
- const children = Array.from(container.childNodes).filter((n) => n.nodeType === 1);
5921
- let i = 0;
5922
- while (i < children.length - 1) {
5923
- const curr = children[i];
5924
- const nxt = children[i + 1];
5925
- if (curr.tagName === "w:r" && nxt.tagName === "w:r") {
5926
- if (!hasSpecialContent(curr) && !hasSpecialContent(nxt)) {
5927
- const rPr1 = findChild(curr, "w:rPr");
5928
- const rPr2 = findChild(nxt, "w:rPr");
5929
- if (areRunsIdentical(rPr1, rPr2)) {
5930
- let last_t = null;
5931
- for (let c = 0; c < curr.childNodes.length; c++) {
5932
- const child = curr.childNodes[c];
5933
- if (child.nodeType === 1 && (child.tagName === "w:t" || child.tagName === "w:delText")) {
5934
- last_t = child;
5935
- }
5936
- }
5937
- const nxtChildren = Array.from(nxt.childNodes).filter((n) => n.nodeType === 1);
5938
- for (const child of nxtChildren) {
5939
- if (child.tagName === "w:rPr") continue;
5940
- if ((child.tagName === "w:t" || child.tagName === "w:delText") && last_t && last_t.tagName === child.tagName) {
5941
- const t1 = last_t.textContent || "";
5942
- const t2 = child.textContent || "";
5943
- const combined = t1 + t2;
5944
- last_t.textContent = combined;
5945
- if (combined.trim() !== combined) {
5946
- last_t.setAttribute("xml:space", "preserve");
5947
- }
5948
- } else {
5949
- curr.appendChild(child);
5950
- if (child.tagName === "w:t" || child.tagName === "w:delText") {
5951
- last_t = child;
5952
- }
5953
- }
5954
- }
5955
- container.removeChild(nxt);
5956
- children.splice(i + 1, 1);
5957
- count++;
5958
- continue;
5959
- }
5960
- }
5961
- }
5962
- if (["w:ins", "w:del", "w:hyperlink", "w:sdt", "w:smartTag", "w:fldSimple", "w:sdtContent"].includes(curr.tagName)) {
5963
- coalesceContainer(curr);
5964
- }
5965
- i++;
6209
+ if (heading_indices.length === 0) return [];
6210
+ const nodes = [];
6211
+ for (let h_pos = 0; h_pos < heading_indices.length; h_pos++) {
6212
+ const rec_idx = heading_indices[h_pos];
6213
+ const rec = block_records[rec_idx];
6214
+ const paragraph = rec.item;
6215
+ const level = _heading_level(paragraph);
6216
+ const text = _heading_text(paragraph, comments_map);
6217
+ const style = _determine_heading_style(paragraph);
6218
+ const owned_end = _find_owned_end(
6219
+ block_records,
6220
+ heading_indices,
6221
+ h_pos,
6222
+ level
6223
+ );
6224
+ const owned_blocks = block_records.slice(rec_idx + 1, owned_end);
6225
+ const has_table = _direct_has_table(block_records, rec_idx + 1, owned_end);
6226
+ const footnote_ids = _collect_footnote_ids(owned_blocks);
6227
+ const page_num = _offset_to_page(rec.start_offset, body_page_offsets);
6228
+ nodes.push({ level, text, page: page_num, style, has_table, footnote_ids });
6229
+ }
6230
+ return nodes;
6231
+ }
6232
+ function _direct_has_table(block_records, range_start, range_end) {
6233
+ for (let idx = range_start; idx < range_end; idx++) {
6234
+ const rec = block_records[idx];
6235
+ if (rec.is_paragraph && _is_heading(rec.item)) return false;
6236
+ if (rec.is_table) return true;
6237
+ }
6238
+ return false;
6239
+ }
6240
+ function _walk_doc_body(doc, comments_map) {
6241
+ const parts = Array.from(iter_document_parts(doc));
6242
+ let body_start_offset = 0;
6243
+ let body_part = null;
6244
+ for (const part of parts) {
6245
+ if (part === doc) {
6246
+ body_part = part;
6247
+ break;
5966
6248
  }
5967
- if (children.length > 0) {
5968
- const last = children[children.length - 1];
5969
- if (["w:ins", "w:del", "w:hyperlink", "w:sdt", "w:smartTag", "w:fldSimple", "w:sdtContent"].includes(last.tagName)) {
5970
- coalesceContainer(last);
5971
- }
6249
+ const part_text = _project_part(part, comments_map);
6250
+ if (part_text) {
6251
+ if (body_start_offset > 0) body_start_offset += 2;
6252
+ body_start_offset += part_text.length;
5972
6253
  }
5973
6254
  }
5974
- const paragraphs = findAllDescendants(doc.element, "w:p");
5975
- for (const p of paragraphs) coalesceContainer(p);
5976
- return count ? [`Adjacent identical runs coalesced: ${count}`] : [];
5977
- }
5978
- function strip_rsid(doc) {
5979
- let count = 0;
5980
- const rsidAttrs = ["w:rsidR", "w:rsidRPr", "w:rsidRDefault", "w:rsidP", "w:rsidDel", "w:rsidSect", "w:rsidTr"];
5981
- const all = doc.element.getElementsByTagName("*");
5982
- for (let i = 0; i < all.length; i++) {
5983
- for (const attr of rsidAttrs) {
5984
- if (all[i].hasAttribute(attr)) {
5985
- all[i].removeAttribute(attr);
5986
- count++;
5987
- }
6255
+ if (!body_part) {
6256
+ body_part = doc;
6257
+ body_start_offset = 0;
6258
+ } else {
6259
+ if (body_start_offset > 0) body_start_offset += 2;
6260
+ }
6261
+ const records = [];
6262
+ let cursor = body_start_offset;
6263
+ let is_first_block = true;
6264
+ for (const item of iter_block_items(body_part)) {
6265
+ if (item instanceof Paragraph) {
6266
+ const prefix = get_paragraph_prefix(item);
6267
+ const p_text = build_paragraph_text(item, comments_map, false);
6268
+ const block_len = (prefix + p_text).length;
6269
+ if (!is_first_block) cursor += 2;
6270
+ records.push({
6271
+ item,
6272
+ is_paragraph: true,
6273
+ is_table: false,
6274
+ start_offset: cursor,
6275
+ projected_length: block_len
6276
+ });
6277
+ cursor += block_len;
6278
+ is_first_block = false;
6279
+ } else if (item instanceof Table) {
6280
+ const table_text = extract_table(item, comments_map, false, 0);
6281
+ const block_len = table_text ? table_text.length : 0;
6282
+ if (!is_first_block) cursor += 2;
6283
+ const table_start = cursor;
6284
+ records.push({
6285
+ item,
6286
+ is_paragraph: false,
6287
+ is_table: true,
6288
+ start_offset: table_start,
6289
+ projected_length: block_len
6290
+ });
6291
+ _record_table_inner_blocks_lite(item, table_start, records, comments_map);
6292
+ cursor += block_len;
6293
+ is_first_block = false;
5988
6294
  }
5989
6295
  }
5990
- const rsidsElements = findAllDescendants(doc.element, "w:rsids");
5991
- for (const el of rsidsElements) {
5992
- if (el.parentNode) {
5993
- el.parentNode.removeChild(el);
5994
- count++;
6296
+ return records;
6297
+ }
6298
+ function _compute_inner_block_offset(table, target_paragraph, table_start_offset, comments_map) {
6299
+ const target_el = target_paragraph._element;
6300
+ let cursor = table_start_offset;
6301
+ let rows_processed = 0;
6302
+ for (const row of table.rows) {
6303
+ if (rows_processed > 0) cursor += 1;
6304
+ const seen_cells = /* @__PURE__ */ new Set();
6305
+ let cells_in_row = 0;
6306
+ for (const cell of row.cells) {
6307
+ if (seen_cells.has(cell)) continue;
6308
+ seen_cells.add(cell);
6309
+ if (cells_in_row > 0) cursor += 3;
6310
+ const [new_cursor, found] = _walk_cell_for_offset(
6311
+ cell,
6312
+ target_el,
6313
+ cursor,
6314
+ comments_map
6315
+ );
6316
+ if (found) return new_cursor;
6317
+ cursor = new_cursor;
6318
+ cells_in_row++;
5995
6319
  }
6320
+ rows_processed++;
5996
6321
  }
5997
- return count ? [`rsid attributes: ${count} removed`] : [];
6322
+ return table_start_offset;
5998
6323
  }
5999
- function strip_para_ids(doc) {
6000
- let count = 0;
6001
- const attrs = ["w14:paraId", "w14:textId"];
6002
- const all = doc.element.getElementsByTagName("*");
6003
- for (let i = 0; i < all.length; i++) {
6004
- for (const attr of attrs) {
6005
- if (all[i].hasAttribute(attr)) {
6006
- all[i].removeAttribute(attr);
6007
- count++;
6324
+ function _walk_cell_for_offset(cell, target_el, cell_start_cursor, comments_map) {
6325
+ let cursor = cell_start_cursor;
6326
+ let is_first_block = true;
6327
+ for (const inner_item of iter_block_items(cell)) {
6328
+ if (!is_first_block) cursor += 2;
6329
+ if (inner_item instanceof Paragraph) {
6330
+ if (inner_item._element === target_el) return [cursor, true];
6331
+ const prefix = get_paragraph_prefix(inner_item);
6332
+ const p_text = build_paragraph_text(inner_item, comments_map, false);
6333
+ cursor += (prefix + p_text).length;
6334
+ } else if (inner_item instanceof Table) {
6335
+ const nested_offset = _compute_inner_block_offset(
6336
+ inner_item,
6337
+ new Paragraph(target_el, null),
6338
+ cursor,
6339
+ comments_map
6340
+ );
6341
+ if (nested_offset !== cursor) {
6342
+ if (_element_is_descendant(target_el, inner_item._element))
6343
+ return [nested_offset, true];
6008
6344
  }
6345
+ const table_text = extract_table(inner_item, comments_map, false, 0);
6346
+ cursor += table_text ? table_text.length : 0;
6009
6347
  }
6348
+ is_first_block = false;
6010
6349
  }
6011
- return count ? [`Paragraph/text IDs: ${count} removed`] : [];
6350
+ return [cursor, false];
6012
6351
  }
6013
- function strip_proof_errors(doc) {
6014
- const elements = findAllDescendants(doc.element, "w:proofErr");
6015
- elements.forEach((el) => el.parentNode?.removeChild(el));
6016
- return elements.length ? [`Spell check markers: ${elements.length} removed`] : [];
6352
+ function _element_is_descendant(target_el, ancestor_el) {
6353
+ let cur = target_el.parentNode;
6354
+ while (cur) {
6355
+ if (cur === ancestor_el) return true;
6356
+ cur = cur.parentNode;
6357
+ }
6358
+ return false;
6017
6359
  }
6018
- function strip_empty_properties(doc) {
6019
- let count = 0;
6020
- for (const tag of ["w:rPr", "w:pPr"]) {
6021
- const elements = findAllDescendants(doc.element, tag);
6022
- for (const el of elements) {
6023
- if (el.childNodes.length === 0 || el.childNodes.length === 1 && el.childNodes[0].nodeType === 3 && !el.childNodes[0].textContent?.trim()) {
6024
- el.parentNode?.removeChild(el);
6025
- count++;
6360
+ function _record_table_inner_blocks_lite(table, inherited_offset, records, comments_map) {
6361
+ const seen_cells = /* @__PURE__ */ new Set();
6362
+ for (const row of table.rows) {
6363
+ for (const cell of row.cells) {
6364
+ if (seen_cells.has(cell)) continue;
6365
+ seen_cells.add(cell);
6366
+ for (const inner_item of iter_block_items(cell)) {
6367
+ if (inner_item instanceof Paragraph) {
6368
+ const true_offset = _is_heading(inner_item) ? _compute_inner_block_offset(
6369
+ table,
6370
+ inner_item,
6371
+ inherited_offset,
6372
+ comments_map
6373
+ ) : inherited_offset;
6374
+ records.push({
6375
+ item: inner_item,
6376
+ is_paragraph: true,
6377
+ is_table: false,
6378
+ start_offset: true_offset,
6379
+ projected_length: 0
6380
+ });
6381
+ } else if (inner_item instanceof Table) {
6382
+ records.push({
6383
+ item: inner_item,
6384
+ is_paragraph: false,
6385
+ is_table: true,
6386
+ start_offset: inherited_offset,
6387
+ projected_length: 0
6388
+ });
6389
+ _record_table_inner_blocks_lite(
6390
+ inner_item,
6391
+ inherited_offset,
6392
+ records,
6393
+ comments_map
6394
+ );
6395
+ }
6026
6396
  }
6027
6397
  }
6028
6398
  }
6029
- return count ? [`Empty property elements: ${count} removed`] : [];
6030
6399
  }
6031
- function strip_hidden_text(doc) {
6032
- let count = 0;
6033
- const elements = findAllDescendants(doc.element, "w:rPr");
6034
- for (const rPr of elements) {
6035
- if (findChild(rPr, "w:vanish") || findChild(rPr, "w:webHidden")) {
6036
- const run = rPr.parentNode;
6037
- if (run && run.tagName === "w:r" && run.parentNode) {
6038
- run.parentNode.removeChild(run);
6039
- count++;
6040
- }
6400
+ function _project_part(part, comments_map) {
6401
+ const blocks = [];
6402
+ const c_type = part.constructor.name;
6403
+ if (c_type === "NotesPart") {
6404
+ const header = part.note_type === "fn" ? "## Footnotes" : "## Endnotes";
6405
+ blocks.push(`---
6406
+ ${header}`);
6407
+ }
6408
+ let is_first_para = true;
6409
+ for (const item of iter_block_items(part)) {
6410
+ if (item.constructor.name === "FootnoteItem") {
6411
+ const fn_text = _project_part(item, comments_map);
6412
+ if (fn_text) blocks.push(fn_text);
6413
+ } else if (item instanceof Paragraph) {
6414
+ let prefix = get_paragraph_prefix(item);
6415
+ if (is_first_para && c_type === "FootnoteItem")
6416
+ prefix = `[^${part.note_type}-${part.id}]: ${prefix}`;
6417
+ const p_text = build_paragraph_text(item, comments_map, false);
6418
+ blocks.push(prefix + p_text);
6419
+ is_first_para = false;
6420
+ } else if (item instanceof Table) {
6421
+ const table_text = extract_table(item, comments_map, false, 0);
6422
+ if (table_text) blocks.push(table_text);
6423
+ is_first_para = false;
6041
6424
  }
6042
6425
  }
6043
- return count ? [`Hidden text runs: ${count} removed`] : [];
6426
+ return blocks.join("\n\n");
6044
6427
  }
6045
- function count_tracked_changes(doc) {
6046
- const ins = findAllDescendants(doc.element, "w:ins").length;
6047
- const del = findAllDescendants(doc.element, "w:del").length;
6048
- const fmt = findAllDescendants(doc.element, "w:rPrChange").length + findAllDescendants(doc.element, "w:pPrChange").length + findAllDescendants(doc.element, "w:sectPrChange").length;
6049
- return [ins, del, fmt];
6428
+ function _is_heading(paragraph) {
6429
+ return _HEADING_PREFIX_RE.test(get_paragraph_prefix(paragraph));
6050
6430
  }
6051
- function get_track_change_authors(doc) {
6052
- const authors = /* @__PURE__ */ new Set();
6053
- for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange", "w:sectPrChange"]) {
6054
- for (const el of findAllDescendants(doc.element, tag)) {
6055
- const author = el.getAttribute("w:author");
6056
- if (author) authors.add(author);
6057
- }
6058
- }
6059
- return authors;
6431
+ function _heading_passes_quality_filter(paragraph, comments_map) {
6432
+ const style = _determine_heading_style(paragraph);
6433
+ if (style !== "(heuristic)") return true;
6434
+ const text = _heading_text(paragraph, comments_map);
6435
+ if (!text) return false;
6436
+ const word_count = (text.match(/\w+/g) || []).length;
6437
+ return word_count >= _HEURISTIC_MIN_WORDS;
6060
6438
  }
6061
- function _getElementText(el) {
6062
- const texts = [];
6063
- const ts = findAllDescendants(el, "w:t");
6064
- for (const t of ts) if (t.textContent) texts.push(t.textContent);
6065
- const dts = findAllDescendants(el, "w:delText");
6066
- for (const dt of dts) if (dt.textContent) texts.push(dt.textContent);
6067
- return texts.join("");
6439
+ function _heading_level(paragraph) {
6440
+ const match = _HEADING_PREFIX_RE.exec(get_paragraph_prefix(paragraph));
6441
+ return match ? Math.min(match[1].length, 6) : 1;
6068
6442
  }
6069
- function _truncate(text, maxLen = 60) {
6070
- const clean = text.replace(/\n/g, " ").trim();
6071
- if (clean.length <= maxLen) return clean;
6072
- return clean.substring(0, maxLen - 3) + "...";
6443
+ function _heading_text(paragraph, comments_map) {
6444
+ const p_text = build_paragraph_text(paragraph, comments_map, false);
6445
+ let cleaned = _strip_critic_markup(p_text);
6446
+ cleaned = _strip_inline_formatting(cleaned);
6447
+ return cleaned.trim();
6073
6448
  }
6074
- function accept_all_tracked_changes(doc) {
6075
- const lines = [];
6076
- const insEls = findAllDescendants(doc.element, "w:ins");
6077
- const delEls = findAllDescendants(doc.element, "w:del");
6078
- for (const ins of insEls) {
6079
- const text = _getElementText(ins).trim();
6080
- if (text) lines.push(` Accepted insertion: "${_truncate(text, 60)}"`);
6081
- }
6082
- for (const del of delEls) {
6083
- const text = _getElementText(del).trim();
6084
- if (text) lines.push(` Accepted deletion of: "${_truncate(text, 60)}"`);
6085
- }
6086
- const engine = new RedlineEngine(doc);
6087
- engine.accept_all_revisions();
6088
- for (const tag of ["w:rPrChange", "w:pPrChange", "w:sectPrChange"]) {
6089
- for (const el of findAllDescendants(doc.element, tag)) {
6090
- el.parentNode?.removeChild(el);
6091
- }
6092
- }
6093
- const total = insEls.length + delEls.length;
6094
- if (total) {
6095
- return [`Tracked changes auto-accepted: ${total}`].concat(lines);
6096
- }
6097
- return [];
6449
+ function _strip_critic_markup(text) {
6450
+ if (!text) return "";
6451
+ text = text.replace(/\{--[\s\S]*?--\}/g, "");
6452
+ text = text.replace(/\{>>[\s\S]*?<<\}/g, "");
6453
+ text = text.replace(/\{\+\+([\s\S]*?)\+\+\}/g, "$1");
6454
+ text = text.replace(/\{==([\s\S]*?)==\}/g, "$1");
6455
+ return text;
6098
6456
  }
6099
- function get_comments_summary(doc) {
6100
- const data = extract_comments_data(doc.pkg);
6101
- const comments = [];
6102
- let openCount = 0;
6103
- let resolvedCount = 0;
6104
- for (const [cId, info] of Object.entries(data)) {
6105
- if (info.resolved) resolvedCount++;
6106
- else openCount++;
6107
- comments.push({ id: cId, ...info });
6108
- }
6109
- return { total: comments.length, open: openCount, resolved: resolvedCount, comments };
6457
+ function _strip_inline_formatting(text) {
6458
+ if (!text) return "";
6459
+ text = text.replace(/\*\*(.+?)\*\*/g, "$1");
6460
+ text = text.replace(/__(.+?)__/g, "$1");
6461
+ text = text.replace(/(?<!\w)_(\S(?:.*?\S)?)_(?!\w)/g, "$1");
6462
+ return text;
6110
6463
  }
6111
- function remove_all_comments(doc) {
6112
- const data = extract_comments_data(doc.pkg);
6113
- const keys = Object.keys(data);
6114
- if (keys.length === 0) return [];
6115
- const lines = [];
6116
- const cm = new CommentsManager(doc);
6117
- for (const [cId, info] of Object.entries(data)) {
6118
- const status = info.resolved ? "[Resolved]" : "[Open]";
6119
- lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
6120
- cm.deleteComment(cId);
6121
- }
6122
- for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
6123
- for (const el of findAllDescendants(doc.element, tag)) {
6124
- el.parentNode?.removeChild(el);
6125
- }
6464
+ function _determine_heading_style(paragraph) {
6465
+ const [style_cache, default_pstyle] = _get_style_cache(
6466
+ paragraph._parent.part || paragraph._parent
6467
+ );
6468
+ const pPr = findChild(paragraph._element, "w:pPr");
6469
+ let style_id = default_pstyle;
6470
+ if (pPr) {
6471
+ const pStyle = findChild(pPr, "w:pStyle");
6472
+ if (pStyle) style_id = pStyle.getAttribute("w:val") || default_pstyle;
6126
6473
  }
6127
- const refs = findAllDescendants(doc.element, "w:commentReference");
6128
- for (const ref of refs) {
6129
- const parent = ref.parentNode;
6130
- if (parent) {
6131
- if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
6132
- const nonRprChildren = Array.from(parent.childNodes).filter(
6133
- (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
6134
- );
6135
- if (nonRprChildren.length <= 1) {
6136
- parent.parentNode?.removeChild(parent);
6137
- } else {
6138
- parent.removeChild(ref);
6139
- }
6140
- } else {
6141
- parent.removeChild(ref);
6142
- }
6474
+ let outline_level = null;
6475
+ let outline_level_from_style = false;
6476
+ if (pPr) {
6477
+ const oLvl = findChild(pPr, "w:outlineLvl");
6478
+ if (oLvl && /^\d+$/.test(oLvl.getAttribute("w:val") || "")) {
6479
+ outline_level = parseInt(oLvl.getAttribute("w:val"), 10);
6143
6480
  }
6144
6481
  }
6145
- const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
6146
- const openCount = Object.values(data).filter((c) => !c.resolved).length;
6147
- return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
6148
- }
6149
- function eject_comment_parts(doc) {
6150
- const pkg = doc.pkg;
6151
- const comment_partnames = /* @__PURE__ */ new Set();
6152
- for (const part of pkg.parts) {
6153
- if (part.partname.toLowerCase().includes("comments")) {
6154
- comment_partnames.add(part.partname);
6155
- const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
6156
- const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
6157
- comment_partnames.add(withSlash);
6158
- comment_partnames.add(withoutSlash);
6159
- }
6482
+ if (outline_level === null && style_id && style_cache && style_cache[style_id]) {
6483
+ outline_level = style_cache[style_id].outline_level;
6484
+ outline_level_from_style = true;
6160
6485
  }
6161
- if (comment_partnames.size === 0) return;
6162
- for (const part of pkg.parts) {
6163
- if (part.partname.endsWith(".rels")) {
6164
- const rels = findAllDescendants(part._element, "Relationship");
6165
- const toRemove = [];
6166
- for (const rel of rels) {
6167
- const target = rel.getAttribute("Target") || "";
6168
- if (target.toLowerCase().includes("comments")) {
6169
- toRemove.push(rel);
6170
- const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
6171
- const sourcePart = pkg.getPartByPath(sourcePath);
6172
- if (sourcePart) {
6173
- const relId = rel.getAttribute("Id");
6174
- if (relId) sourcePart.rels.delete(relId);
6175
- }
6176
- }
6177
- }
6178
- for (const relEl of toRemove) {
6179
- relEl.parentNode?.removeChild(relEl);
6180
- }
6486
+ const style_name = style_id && style_cache && style_cache[style_id] ? style_cache[style_id].name : style_id;
6487
+ let normalized_style_name = style_name;
6488
+ if (normalized_style_name && typeof normalized_style_name === "string") {
6489
+ if (normalized_style_name.toLowerCase().startsWith("heading")) {
6490
+ normalized_style_name = normalized_style_name.replace(/^heading/i, "Heading");
6491
+ } else if (normalized_style_name.toLowerCase() === "title") {
6492
+ normalized_style_name = "Title";
6181
6493
  }
6182
6494
  }
6183
- const ctPart = pkg.getPartByPath("[Content_Types].xml");
6184
- if (ctPart) {
6185
- const overrides = findAllDescendants(ctPart._element, "Override");
6186
- const toRemove = [];
6187
- for (const override of overrides) {
6188
- const partName = override.getAttribute("PartName") || "";
6189
- if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
6190
- toRemove.push(override);
6191
- }
6192
- }
6193
- for (const overrideEl of toRemove) {
6194
- overrideEl.parentNode?.removeChild(overrideEl);
6495
+ if (outline_level_from_style && outline_level !== null) {
6496
+ const is_heading_or_title = normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title");
6497
+ if (!is_heading_or_title) {
6498
+ outline_level = null;
6195
6499
  }
6196
6500
  }
6197
- pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
6198
- for (const key of Object.keys(pkg.unzipped)) {
6199
- if (key.toLowerCase().includes("comments")) {
6200
- delete pkg.unzipped[key];
6501
+ if (outline_level !== null && outline_level >= 0 && outline_level <= 8) {
6502
+ if (normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title")) {
6503
+ return normalized_style_name;
6201
6504
  }
6505
+ return "(outline_level)";
6202
6506
  }
6507
+ if (normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title"))
6508
+ return normalized_style_name;
6509
+ if (normalized_style_name && /Heading[ ]?([1-6])(?![0-9])/.test(normalized_style_name))
6510
+ return normalized_style_name;
6511
+ return "(heuristic)";
6203
6512
  }
6204
- function replace_comment_authors(doc, newAuthor) {
6205
- const cm = new CommentsManager(doc);
6206
- if (!cm.commentsPart) return [];
6207
- const original = /* @__PURE__ */ new Set();
6208
- const comments = findAllDescendants(cm.commentsPart._element, "w:comment");
6209
- for (const c of comments) {
6210
- const author = c.getAttribute("w:author");
6211
- if (author) {
6212
- original.add(author);
6213
- c.setAttribute("w:author", newAuthor);
6214
- }
6215
- if (c.hasAttribute("w:initials")) {
6216
- const initials = newAuthor.split(" ").filter(Boolean).map((p) => p[0]).join("").toUpperCase();
6217
- c.setAttribute("w:initials", initials);
6218
- }
6513
+ function _find_owned_end(block_records, heading_indices, current_h_pos, current_level) {
6514
+ for (let next_h_pos = current_h_pos + 1; next_h_pos < heading_indices.length; next_h_pos++) {
6515
+ const next_idx = heading_indices[next_h_pos];
6516
+ if (_heading_level(block_records[next_idx].item) <= current_level)
6517
+ return next_idx;
6219
6518
  }
6220
- return original.size ? [`Comment authors replaced: ${Array.from(original).sort().join(", ")} \u2192 "${newAuthor}"`] : [];
6519
+ return block_records.length;
6221
6520
  }
6222
- function replace_change_authors(doc, newAuthor) {
6223
- const original = /* @__PURE__ */ new Set();
6224
- for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange"]) {
6225
- for (const el of findAllDescendants(doc.element, tag)) {
6226
- const author = el.getAttribute("w:author");
6227
- if (author) {
6228
- original.add(author);
6229
- el.setAttribute("w:author", newAuthor);
6521
+ function _collect_footnote_ids(owned_blocks) {
6522
+ const seen = /* @__PURE__ */ new Set();
6523
+ const ordered = [];
6524
+ for (const rec of owned_blocks) {
6525
+ if (!rec.is_paragraph) continue;
6526
+ for (const event of iter_paragraph_content(rec.item)) {
6527
+ if (!("type" in event)) continue;
6528
+ let fn_id = "";
6529
+ if (event.type === "footnote") fn_id = `fn-${event.id}`;
6530
+ else if (event.type === "endnote") fn_id = `en-${event.id}`;
6531
+ else continue;
6532
+ if (!seen.has(fn_id)) {
6533
+ seen.add(fn_id);
6534
+ ordered.push(fn_id);
6230
6535
  }
6231
6536
  }
6232
6537
  }
6233
- return original.size ? [`Track change authors replaced: ${Array.from(original).sort().join(", ")} \u2192 "${newAuthor}"`] : [];
6538
+ return ordered;
6234
6539
  }
6235
- function normalize_change_dates(doc) {
6236
- let count = 0;
6237
- const fixed = "2025-01-01T00:00:00Z";
6238
- for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange"]) {
6239
- for (const el of findAllDescendants(doc.element, tag)) {
6240
- if (el.hasAttribute("w:date")) {
6241
- el.setAttribute("w:date", fixed);
6242
- count++;
6540
+ function _offset_to_page(offset, body_page_offsets) {
6541
+ if (!body_page_offsets || body_page_offsets.length === 0) return 1;
6542
+ let page = 1;
6543
+ for (let i = 0; i < body_page_offsets.length; i++) {
6544
+ if (offset >= body_page_offsets[i]) page = i + 1;
6545
+ else break;
6546
+ }
6547
+ return page;
6548
+ }
6549
+ function _extract_outline_fast(doc, projected_body, body_page_offsets, paragraph_offsets) {
6550
+ const paragraphs_and_tables = [];
6551
+ const seen_cells = /* @__PURE__ */ new Set();
6552
+ function walk(container) {
6553
+ for (const item of iter_block_items(container)) {
6554
+ const i_type = item.constructor.name;
6555
+ if (i_type === "FootnoteItem") {
6556
+ walk(item);
6557
+ } else if (item instanceof Paragraph) {
6558
+ paragraphs_and_tables.push(["p", item]);
6559
+ } else if (item instanceof Table) {
6560
+ paragraphs_and_tables.push(["t", item]);
6561
+ for (const row of item.rows) {
6562
+ for (const cell of row.cells) {
6563
+ if (seen_cells.has(cell._element)) {
6564
+ continue;
6565
+ }
6566
+ seen_cells.add(cell._element);
6567
+ walk(cell);
6568
+ }
6569
+ }
6243
6570
  }
6244
6571
  }
6245
6572
  }
6246
- return count ? [`Track change timestamps: ${count} normalized`] : [];
6247
- }
6248
- function scrub_doc_properties(doc) {
6249
- const lines = [];
6250
- const corePart = doc.pkg.getPartByPath("docProps/core.xml");
6251
- if (corePart) {
6252
- const creators = findDescendantsByLocalName(corePart._element, "creator");
6253
- creators.forEach((c) => {
6254
- if (c.textContent) {
6255
- lines.push(`Author: ${c.textContent}`);
6256
- c.textContent = "";
6573
+ walk(doc);
6574
+ const heading_indices = [];
6575
+ for (let idx = 0; idx < paragraphs_and_tables.length; idx++) {
6576
+ const [kind, item] = paragraphs_and_tables[idx];
6577
+ if (kind !== "p") continue;
6578
+ let hasOffset = false;
6579
+ if (paragraph_offsets instanceof Map) {
6580
+ hasOffset = paragraph_offsets.has(item._element);
6581
+ } else {
6582
+ hasOffset = item._element in paragraph_offsets;
6583
+ }
6584
+ if (!hasOffset) {
6585
+ continue;
6586
+ }
6587
+ if (!_is_heading(item)) continue;
6588
+ if (!_heading_passes_quality_filter_fast(item, projected_body, paragraph_offsets)) continue;
6589
+ heading_indices.push(idx);
6590
+ }
6591
+ if (heading_indices.length === 0) return [];
6592
+ const nodes = [];
6593
+ for (let h_pos = 0; h_pos < heading_indices.length; h_pos++) {
6594
+ const item_idx = heading_indices[h_pos];
6595
+ const paragraph = paragraphs_and_tables[item_idx][1];
6596
+ const level = _heading_level(paragraph);
6597
+ const text = _heading_text_fast(paragraph, projected_body, paragraph_offsets);
6598
+ const style = _determine_heading_style(paragraph);
6599
+ let owned_end = item_idx;
6600
+ for (let next_h_pos = h_pos + 1; next_h_pos < heading_indices.length; next_h_pos++) {
6601
+ const next_idx = heading_indices[next_h_pos];
6602
+ const next_paragraph = paragraphs_and_tables[next_idx][1];
6603
+ if (_heading_level(next_paragraph) <= level) {
6604
+ owned_end = next_idx;
6605
+ break;
6257
6606
  }
6258
- });
6259
- const modifiers = findDescendantsByLocalName(corePart._element, "lastModifiedBy");
6260
- modifiers.forEach((c) => {
6261
- if (c.textContent) {
6262
- lines.push(`Last modified by: ${c.textContent}`);
6263
- c.textContent = "";
6607
+ }
6608
+ if (owned_end === item_idx) {
6609
+ owned_end = paragraphs_and_tables.length;
6610
+ }
6611
+ const owned = paragraphs_and_tables.slice(item_idx + 1, owned_end);
6612
+ let has_table = false;
6613
+ for (const [kind2, item2] of owned) {
6614
+ if (kind2 === "p" && _is_heading(item2)) {
6615
+ break;
6264
6616
  }
6265
- });
6266
- const revisions = findDescendantsByLocalName(corePart._element, "revision");
6267
- revisions.forEach((c) => {
6268
- if (c.textContent && parseInt(c.textContent) > 1) {
6269
- lines.push(`Revision count: ${c.textContent} \u2192 1`);
6270
- c.textContent = "1";
6617
+ if (kind2 === "t") {
6618
+ has_table = true;
6619
+ break;
6271
6620
  }
6272
- });
6273
- }
6274
- const appPart = doc.pkg.getPartByPath("docProps/app.xml");
6275
- if (appPart) {
6276
- const docEl = appPart._element;
6277
- const intFields = ["TotalTime", "Words", "Characters", "Paragraphs", "Lines", "CharactersWithSpaces"];
6278
- for (const f of intFields) {
6279
- findDescendantsByLocalName(docEl, f).forEach((el) => {
6280
- if (el.textContent && el.textContent !== "0") {
6281
- if (f === "TotalTime") lines.push(`Total editing time: ${el.textContent} minutes`);
6282
- el.textContent = "0";
6283
- }
6284
- });
6285
6621
  }
6286
- const strFields = ["Template", "Manager", "Company"];
6287
- for (const f of strFields) {
6288
- findDescendantsByLocalName(docEl, f).forEach((el) => {
6289
- if (el.textContent) {
6290
- lines.push(`${f}: ${el.textContent}`);
6291
- el.textContent = "";
6292
- }
6293
- });
6622
+ const footnote_ids = _collect_footnote_ids_fast(owned);
6623
+ let para_offset;
6624
+ if (paragraph_offsets instanceof Map) {
6625
+ para_offset = paragraph_offsets.get(paragraph._element);
6626
+ } else {
6627
+ para_offset = paragraph_offsets[paragraph._element];
6294
6628
  }
6295
- }
6296
- return lines.length ? ["Metadata scrubbed:", ...lines.map((l) => ` ${l}`)] : [];
6297
- }
6298
- function scrub_timestamps(doc) {
6299
- let modified = false;
6300
- const epoch = "1970-01-01T00:00:00Z";
6301
- const corePart = doc.pkg.getPartByPath("docProps/core.xml");
6302
- if (corePart) {
6303
- for (const tag of ["created", "modified", "lastPrinted"]) {
6304
- findDescendantsByLocalName(corePart._element, tag).forEach((el) => {
6305
- if (el.textContent && el.textContent !== epoch) {
6306
- el.textContent = epoch;
6307
- modified = true;
6308
- }
6309
- });
6629
+ let page_num = 1;
6630
+ if (para_offset !== void 0) {
6631
+ const [start_offset] = para_offset;
6632
+ page_num = _offset_to_page(start_offset, body_page_offsets);
6310
6633
  }
6634
+ nodes.push({
6635
+ level,
6636
+ text,
6637
+ page: page_num,
6638
+ style,
6639
+ has_table,
6640
+ footnote_ids
6641
+ });
6311
6642
  }
6312
- return modified ? ["Timestamps normalized to epoch"] : [];
6643
+ return nodes;
6313
6644
  }
6314
- function strip_custom_xml(doc) {
6315
- const customParts = doc.pkg.parts.filter((p) => p.partname.includes("/customXml"));
6316
- if (customParts.length === 0) return [];
6317
- const partnames = new Set(customParts.map((p) => p.partname));
6318
- doc.pkg.parts = doc.pkg.parts.filter((p) => !partnames.has(p.partname));
6319
- const removeRelationsTo = (relsPart) => {
6320
- const toRemove = [];
6321
- for (const rel of findAllDescendants(relsPart._element, "Relationship")) {
6322
- const target = rel.getAttribute("Target");
6323
- if (target && target.includes("customXml")) toRemove.push(rel);
6324
- }
6325
- toRemove.forEach((r) => r.parentNode?.removeChild(r));
6326
- };
6327
- const rootRels = doc.pkg.getPartByPath("_rels/.rels");
6328
- if (rootRels) removeRelationsTo(rootRels);
6329
- const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
6330
- if (docRels) removeRelationsTo(docRels);
6331
- for (const sdtPr of findAllDescendants(doc.element, "w:sdtPr")) {
6332
- findChildren(sdtPr, "w:dataBinding").forEach((b) => sdtPr.removeChild(b));
6645
+ function _heading_passes_quality_filter_fast(paragraph, projected_body, paragraph_offsets) {
6646
+ const style = _determine_heading_style(paragraph);
6647
+ if (style !== "(heuristic)") return true;
6648
+ const text = _heading_text_fast(paragraph, projected_body, paragraph_offsets);
6649
+ if (!text) return false;
6650
+ const words = text.match(/\w+/g) || [];
6651
+ return words.length >= _HEURISTIC_MIN_WORDS;
6652
+ }
6653
+ function _heading_text_fast(paragraph, projected_body, paragraph_offsets) {
6654
+ let offset;
6655
+ if (paragraph_offsets instanceof Map) {
6656
+ offset = paragraph_offsets.get(paragraph._element);
6657
+ } else {
6658
+ offset = paragraph_offsets[paragraph._element];
6333
6659
  }
6334
- return [`Custom XML parts: ${customParts.length} removed`];
6660
+ if (offset === void 0) {
6661
+ return "";
6662
+ }
6663
+ const [start, length] = offset;
6664
+ const raw = projected_body.substring(start, start + length);
6665
+ let cleaned = _strip_critic_markup(raw);
6666
+ cleaned = _strip_inline_formatting(cleaned);
6667
+ cleaned = cleaned.replace(/^#+\s+/, "");
6668
+ return cleaned.trim();
6335
6669
  }
6336
- function strip_image_alt_text(doc) {
6337
- let count = 0;
6338
- for (const docPr of findDescendantsByLocalName(doc.element, "docPr")) {
6339
- const descr = docPr.getAttribute("descr");
6340
- if (descr) {
6341
- const isShort = descr.length < 10;
6342
- const isFile = descr.includes(".") && descr.length < 60;
6343
- if (isShort || isFile) {
6344
- docPr.removeAttribute("descr");
6345
- count++;
6670
+ function _collect_footnote_ids_fast(owned_items) {
6671
+ const seen = /* @__PURE__ */ new Set();
6672
+ const ordered = [];
6673
+ for (const [kind, item] of owned_items) {
6674
+ if (kind !== "p") continue;
6675
+ for (const event of iter_paragraph_content(item)) {
6676
+ if (!("type" in event)) continue;
6677
+ let fn_id = "";
6678
+ if (event.type === "footnote") fn_id = `fn-${event.id}`;
6679
+ else if (event.type === "endnote") fn_id = `en-${event.id}`;
6680
+ else continue;
6681
+ if (!seen.has(fn_id)) {
6682
+ seen.add(fn_id);
6683
+ ordered.push(fn_id);
6346
6684
  }
6347
6685
  }
6348
6686
  }
6349
- return count ? [`Image alt text: ${count} auto-generated descriptions removed`] : [];
6687
+ return ordered;
6350
6688
  }
6351
- function audit_hyperlinks(doc) {
6352
- const internal = ["sharepoint.com", "onedrive.com", ".internal", "intranet", "localhost", "10.", "192.168.", "172.16."];
6353
- const warnings = [];
6354
- const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
6355
- for (const rel of findAllDescendants(docRels._element, "Relationship")) {
6356
- if (rel.getAttribute("TargetMode") === "External") {
6357
- const url = rel.getAttribute("Target") || "";
6358
- for (const pattern of internal) {
6359
- if (url.toLowerCase().includes(pattern.toLowerCase())) {
6360
- warnings.push(`Hyperlink targets internal URL: ${_truncate(url, 80)}`);
6361
- break;
6689
+
6690
+ // src/sanitize/report.ts
6691
+ var SanitizeReport = class {
6692
+ filename;
6693
+ mode;
6694
+ author;
6695
+ tracked_changes_found = 0;
6696
+ tracked_changes_accepted = 0;
6697
+ tracked_changes_kept = 0;
6698
+ change_lines = [];
6699
+ comments_removed = 0;
6700
+ comments_kept = 0;
6701
+ removed_comment_lines = [];
6702
+ kept_comment_lines = [];
6703
+ metadata_lines = [];
6704
+ structural_lines = [];
6705
+ warnings = [];
6706
+ status = "clean";
6707
+ blocked_reason = null;
6708
+ constructor(filename, mode = "full", author = null) {
6709
+ this.filename = filename;
6710
+ this.mode = mode;
6711
+ this.author = author;
6712
+ }
6713
+ add_transform_lines(lines) {
6714
+ for (const line of lines) {
6715
+ const lower = line.toLowerCase();
6716
+ if (lower.includes("tracked change") || lower.includes("insertion") || lower.includes("deletion") || lower.includes("accepted")) {
6717
+ this.change_lines.push(line);
6718
+ } else if (lower.includes("comment") || lower.includes("[open]") || lower.includes("[resolved]")) {
6719
+ if (lower.includes("kept") || lower.includes("visible")) {
6720
+ this.kept_comment_lines.push(line);
6721
+ } else {
6722
+ this.removed_comment_lines.push(line);
6362
6723
  }
6724
+ } else if (lower.includes("author") || lower.includes("template") || lower.includes("company") || lower.includes("manager") || lower.includes("metadata") || lower.includes("timestamp") || lower.includes("custom xml") || lower.includes("last modified by") || lower.includes("revision count") || lower.includes("last printed")) {
6725
+ this.metadata_lines.push(line);
6726
+ } else if (lower.includes("hyperlink") || lower.includes("warning")) {
6727
+ this.warnings.push(line);
6728
+ } else {
6729
+ this.structural_lines.push(line);
6363
6730
  }
6364
6731
  }
6365
6732
  }
6366
- return warnings;
6367
- }
6733
+ render() {
6734
+ const sep = "\u2550".repeat(50);
6735
+ const lines = [sep, `Finalization Report: ${this.filename}`];
6736
+ const flags = [];
6737
+ if (this.mode === "keep-markup") flags.push("--keep-markup");
6738
+ if (this.author) flags.push(`--author "${this.author}"`);
6739
+ if (this.tracked_changes_accepted > 0) flags.push("--accept-all");
6740
+ if (flags.length > 0) lines.push(flags.join(" "));
6741
+ lines.push(sep);
6742
+ if (this.status === "blocked") {
6743
+ lines.push("");
6744
+ lines.push(`BLOCKED: ${this.blocked_reason}`);
6745
+ lines.push(sep);
6746
+ return lines.join("\n");
6747
+ }
6748
+ if (this.mode === "keep-markup" && (this.tracked_changes_kept > 0 || this.comments_kept > 0)) {
6749
+ lines.push("");
6750
+ lines.push("VISIBLE TO COUNTERPARTY");
6751
+ if (this.tracked_changes_kept > 0) lines.push(` Tracked changes: ${this.tracked_changes_kept}`);
6752
+ if (this.comments_kept > 0) {
6753
+ lines.push(` Open comments: ${this.comments_kept}`);
6754
+ for (const cl of this.kept_comment_lines) lines.push(` ${cl}`);
6755
+ }
6756
+ if (this.author) lines.push(` Author on all markup: "${this.author}"`);
6757
+ }
6758
+ if (this.change_lines.length > 0) {
6759
+ lines.push("");
6760
+ lines.push("TRACKED CHANGES");
6761
+ for (const cl of this.change_lines) lines.push(` ${cl}`);
6762
+ }
6763
+ if (this.removed_comment_lines.length > 0) {
6764
+ lines.push("");
6765
+ lines.push("COMMENTS (stripped)");
6766
+ for (const cl of this.removed_comment_lines) lines.push(` ${cl}`);
6767
+ }
6768
+ if (this.metadata_lines.length > 0) {
6769
+ lines.push("");
6770
+ lines.push("METADATA");
6771
+ for (const ml of this.metadata_lines) lines.push(` ${ml}`);
6772
+ }
6773
+ if (this.structural_lines.length > 0) {
6774
+ lines.push("");
6775
+ lines.push("STRUCTURAL & PROTECTION");
6776
+ for (const sl of this.structural_lines) lines.push(` ${sl}`);
6777
+ }
6778
+ if (this.warnings.length > 0) {
6779
+ lines.push("");
6780
+ lines.push("WARNINGS");
6781
+ for (const w of this.warnings) lines.push(` \u26A0 ${w}`);
6782
+ }
6783
+ lines.push("");
6784
+ lines.push(sep);
6785
+ if (this.warnings.length > 0) {
6786
+ lines.push(`Result: CLEAN WITH WARNINGS (${this.warnings.length} warning${this.warnings.length > 1 ? "s" : ""})`);
6787
+ } else {
6788
+ lines.push(`Result: CLEAN (${this.tracked_changes_found} changes resolved, ${this.comments_removed} comments removed)`);
6789
+ }
6790
+ lines.push(sep);
6791
+ return lines.join("\n");
6792
+ }
6793
+ };
6368
6794
 
6369
6795
  // src/sanitize/core.ts
6370
6796
  async function finalize_document(doc, options) {