@adeu/core 1.14.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.js CHANGED
@@ -1282,7 +1282,12 @@ ${header}`;
1282
1282
  this._add_virtual_text(prefix, current, item);
1283
1283
  current += prefix.length;
1284
1284
  }
1285
- current = this._map_paragraph_content(item, current, style_cache, default_pstyle);
1285
+ current = this._map_paragraph_content(
1286
+ item,
1287
+ current,
1288
+ style_cache,
1289
+ default_pstyle
1290
+ );
1286
1291
  is_first_para = false;
1287
1292
  previous_item = item;
1288
1293
  } else if (item instanceof Table) {
@@ -1355,7 +1360,13 @@ ${header}`;
1355
1360
  }
1356
1361
  _map_paragraph_content(paragraph, start_offset, style_cache, default_pstyle) {
1357
1362
  let current = start_offset;
1358
- const span = { start: current, end: current, text: "", run: null, paragraph };
1363
+ const span = {
1364
+ start: current,
1365
+ end: current,
1366
+ text: "",
1367
+ run: null,
1368
+ paragraph
1369
+ };
1359
1370
  this.spans.push(span);
1360
1371
  const active_ids = /* @__PURE__ */ new Set();
1361
1372
  const active_ins = {};
@@ -1373,7 +1384,7 @@ ${header}`;
1373
1384
  this._add_virtual_text(s_tok, current, paragraph);
1374
1385
  current += s_tok.length;
1375
1386
  }
1376
- for (const [kind, txt, r_obj, i_id, d_id] of pending_runs) {
1387
+ for (const [kind, txt, r_obj, i_id, d_id, c_ids] of pending_runs) {
1377
1388
  if (kind === "virtual") {
1378
1389
  this._add_virtual_text(txt, current, paragraph, active_hyperlink_id);
1379
1390
  } else {
@@ -1385,7 +1396,8 @@ ${header}`;
1385
1396
  paragraph,
1386
1397
  ins_id: i_id || void 0,
1387
1398
  del_id: d_id || void 0,
1388
- hyperlink_id: active_hyperlink_id || void 0
1399
+ hyperlink_id: active_hyperlink_id || void 0,
1400
+ comment_ids: c_ids.length > 0 ? c_ids : void 0
1389
1401
  };
1390
1402
  this.spans.push(s);
1391
1403
  this._text_chunks.push(txt);
@@ -1399,8 +1411,16 @@ ${header}`;
1399
1411
  pending_runs = [];
1400
1412
  };
1401
1413
  const items = Array.from(iter_paragraph_content(paragraph));
1402
- const is_heading = is_heading_paragraph(paragraph, style_cache, default_pstyle);
1403
- const native_heading = is_native_heading(paragraph, style_cache, default_pstyle);
1414
+ const is_heading = is_heading_paragraph(
1415
+ paragraph,
1416
+ style_cache,
1417
+ default_pstyle
1418
+ );
1419
+ const native_heading = is_native_heading(
1420
+ paragraph,
1421
+ style_cache,
1422
+ default_pstyle
1423
+ );
1404
1424
  let leading_strip_active = is_heading;
1405
1425
  for (let i = 0; i < items.length; i++) {
1406
1426
  const item = items[i];
@@ -1435,7 +1455,12 @@ ${header}`;
1435
1455
  const curr_ins_id = Object.keys(active_ins).pop() || null;
1436
1456
  const curr_del_id = Object.keys(active_del).pop() || null;
1437
1457
  if (full_seg_text && !(this.clean_view && curr_del_id) && !(this.original_view && curr_ins_id)) {
1438
- const new_wrappers = this.clean_view || this.original_view ? ["", ""] : this._get_wrappers(curr_ins_id, curr_del_id, active_ids, active_fmt);
1458
+ const new_wrappers = this.clean_view || this.original_view ? ["", ""] : this._get_wrappers(
1459
+ curr_ins_id,
1460
+ curr_del_id,
1461
+ active_ids,
1462
+ active_fmt
1463
+ );
1439
1464
  const new_style = [prefix, suffix];
1440
1465
  if (pending_runs.length > 0 && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
1441
1466
  let skip_leading_prefix = false;
@@ -1443,36 +1468,58 @@ ${header}`;
1443
1468
  pending_runs.pop();
1444
1469
  skip_leading_prefix = true;
1445
1470
  }
1471
+ const curr_comment_ids = Array.from(active_ids);
1446
1472
  for (const [kind, txt, r_obj] of run_parts) {
1447
1473
  if (skip_leading_prefix && kind === "virtual" && txt === new_style[0]) {
1448
1474
  skip_leading_prefix = false;
1449
1475
  continue;
1450
1476
  }
1451
- pending_runs.push([kind, txt, r_obj, curr_ins_id, curr_del_id]);
1477
+ pending_runs.push([
1478
+ kind,
1479
+ txt,
1480
+ r_obj,
1481
+ curr_ins_id,
1482
+ curr_del_id,
1483
+ curr_comment_ids
1484
+ ]);
1452
1485
  }
1453
1486
  current_style = new_style;
1454
1487
  } else {
1455
1488
  flush_pending_runs();
1456
1489
  current_wrappers = new_wrappers;
1457
1490
  current_style = new_style;
1491
+ const curr_comment_ids = Array.from(active_ids);
1458
1492
  for (const [kind, txt, r_obj] of run_parts) {
1459
- pending_runs.push([kind, txt, r_obj, curr_ins_id, curr_del_id]);
1493
+ pending_runs.push([
1494
+ kind,
1495
+ txt,
1496
+ r_obj,
1497
+ curr_ins_id,
1498
+ curr_del_id,
1499
+ curr_comment_ids
1500
+ ]);
1460
1501
  }
1461
1502
  }
1462
1503
  }
1463
1504
  if (!this.clean_view && !this.original_view) {
1464
1505
  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;
1465
1506
  if (has_meta) {
1466
- deferred_meta_states.push([{ ...active_ins }, { ...active_del }, new Set(active_ids), { ...active_fmt }]);
1507
+ deferred_meta_states.push([
1508
+ { ...active_ins },
1509
+ { ...active_del },
1510
+ new Set(active_ids),
1511
+ { ...active_fmt }
1512
+ ]);
1467
1513
  }
1468
1514
  let should_defer = false;
1469
- const is_redline = curr_ins_id !== null || curr_del_id !== null || Object.keys(active_fmt).length > 0;
1470
- if (is_redline) {
1515
+ const has_any_meta = curr_ins_id !== null || curr_del_id !== null || Object.keys(active_fmt).length > 0 || active_ids.size > 0;
1516
+ if (has_any_meta) {
1471
1517
  let j = i + 1;
1472
- let next_is_redline = false;
1518
+ let next_has_meta = false;
1473
1519
  let temp_ins_count = Object.keys(active_ins).length;
1474
1520
  let temp_del_count = Object.keys(active_del).length;
1475
1521
  let temp_fmt_count = Object.keys(active_fmt).length;
1522
+ const temp_comment_ids = new Set(active_ids);
1476
1523
  while (j < items.length) {
1477
1524
  const next_item = items[j];
1478
1525
  if (next_item instanceof Run) {
@@ -1480,22 +1527,27 @@ ${header}`;
1480
1527
  j++;
1481
1528
  continue;
1482
1529
  }
1483
- if (temp_ins_count > 0 || temp_del_count > 0 || temp_fmt_count > 0) {
1484
- next_is_redline = true;
1530
+ if (temp_ins_count > 0 || temp_del_count > 0 || temp_fmt_count > 0 || temp_comment_ids.size > 0) {
1531
+ next_has_meta = true;
1485
1532
  }
1486
1533
  break;
1487
1534
  } else {
1488
1535
  const ev = next_item;
1489
1536
  if (ev.type === "ins_start") temp_ins_count++;
1490
- else if (ev.type === "ins_end") temp_ins_count = Math.max(0, temp_ins_count - 1);
1537
+ else if (ev.type === "ins_end")
1538
+ temp_ins_count = Math.max(0, temp_ins_count - 1);
1491
1539
  else if (ev.type === "del_start") temp_del_count++;
1492
- else if (ev.type === "del_end") temp_del_count = Math.max(0, temp_del_count - 1);
1540
+ else if (ev.type === "del_end")
1541
+ temp_del_count = Math.max(0, temp_del_count - 1);
1493
1542
  else if (ev.type === "fmt_start") temp_fmt_count++;
1494
- else if (ev.type === "fmt_end") temp_fmt_count = Math.max(0, temp_fmt_count - 1);
1543
+ else if (ev.type === "fmt_end")
1544
+ temp_fmt_count = Math.max(0, temp_fmt_count - 1);
1545
+ else if (ev.type === "start") temp_comment_ids.add(ev.id);
1546
+ else if (ev.type === "end") temp_comment_ids.delete(ev.id);
1495
1547
  }
1496
1548
  j++;
1497
1549
  }
1498
- if (next_is_redline) should_defer = true;
1550
+ if (next_has_meta) should_defer = true;
1499
1551
  }
1500
1552
  if (!should_defer && deferred_meta_states.length > 0) {
1501
1553
  const meta_block = this._build_merged_meta_block(deferred_meta_states);
@@ -1584,7 +1636,8 @@ ${header}`;
1584
1636
  _get_wrappers(ins_id, del_id, active_ids, active_fmt) {
1585
1637
  if (del_id) return ["{--", "--}"];
1586
1638
  if (ins_id) return ["{++", "++}"];
1587
- if (active_ids.size > 0 || Object.keys(active_fmt).length > 0) return ["{==", "==}"];
1639
+ if (active_ids.size > 0 || Object.keys(active_fmt).length > 0)
1640
+ return ["{==", "==}"];
1588
1641
  return ["", ""];
1589
1642
  }
1590
1643
  _build_merged_meta_block(states_list) {
@@ -1592,7 +1645,9 @@ ${header}`;
1592
1645
  const comment_lines = [];
1593
1646
  const seen_sigs = /* @__PURE__ */ new Set();
1594
1647
  for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
1595
- for (const [uid, meta] of Object.entries(ins_map)) {
1648
+ for (const [uid, meta] of Object.entries(
1649
+ ins_map
1650
+ )) {
1596
1651
  const sig = `Chg:${uid}`;
1597
1652
  if (!seen_sigs.has(sig)) {
1598
1653
  const auth = meta.author || "Unknown";
@@ -1600,7 +1655,9 @@ ${header}`;
1600
1655
  seen_sigs.add(sig);
1601
1656
  }
1602
1657
  }
1603
- for (const [uid, meta] of Object.entries(del_map)) {
1658
+ for (const [uid, meta] of Object.entries(
1659
+ del_map
1660
+ )) {
1604
1661
  const sig = `Chg:${uid}`;
1605
1662
  if (!seen_sigs.has(sig)) {
1606
1663
  const auth = meta.author || "Unknown";
@@ -1608,7 +1665,9 @@ ${header}`;
1608
1665
  seen_sigs.add(sig);
1609
1666
  }
1610
1667
  }
1611
- for (const [uid, meta] of Object.entries(fmt_map)) {
1668
+ for (const [uid, meta] of Object.entries(
1669
+ fmt_map
1670
+ )) {
1612
1671
  const sig = `Chg:${uid}`;
1613
1672
  if (!seen_sigs.has(sig)) {
1614
1673
  const auth = meta.author || "Unknown";
@@ -1682,7 +1741,16 @@ ${header}`;
1682
1741
  if (remaining) parts.push(escapeRegExp(remaining));
1683
1742
  return parts.join("");
1684
1743
  }
1685
- find_match_index(target_text) {
1744
+ find_match_index(target_text, is_regex = false) {
1745
+ if (is_regex) {
1746
+ try {
1747
+ const pattern = new RegExp(target_text);
1748
+ const match = pattern.exec(this.full_text);
1749
+ if (match) return [match.index, match[0].length];
1750
+ } catch (e) {
1751
+ }
1752
+ return [-1, 0];
1753
+ }
1686
1754
  let start_idx = this.full_text.indexOf(target_text);
1687
1755
  if (start_idx !== -1) return [start_idx, target_text.length];
1688
1756
  const norm_full = this._replace_smart_quotes(this.full_text);
@@ -1702,22 +1770,41 @@ ${header}`;
1702
1770
  }
1703
1771
  return [-1, 0];
1704
1772
  }
1705
- find_all_match_indices(target_text) {
1773
+ find_all_match_indices(target_text, is_regex = false) {
1706
1774
  if (!target_text) return [];
1775
+ if (is_regex) {
1776
+ try {
1777
+ const pattern = new RegExp(target_text, "g");
1778
+ const matches2 = [...this.full_text.matchAll(pattern)];
1779
+ if (matches2.length > 0)
1780
+ return matches2.map((m) => [m.index, m[0].length]);
1781
+ } catch (e) {
1782
+ }
1783
+ return [];
1784
+ }
1707
1785
  const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1708
- let matches = [...this.full_text.matchAll(new RegExp(escapeRegExp(target_text), "g"))];
1786
+ let matches = [
1787
+ ...this.full_text.matchAll(new RegExp(escapeRegExp(target_text), "g"))
1788
+ ];
1709
1789
  if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1710
1790
  const norm_full = this._replace_smart_quotes(this.full_text);
1711
1791
  const norm_target = this._replace_smart_quotes(target_text);
1712
- matches = [...norm_full.matchAll(new RegExp(escapeRegExp(norm_target), "g"))];
1792
+ matches = [
1793
+ ...norm_full.matchAll(new RegExp(escapeRegExp(norm_target), "g"))
1794
+ ];
1713
1795
  if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1714
1796
  const stripped_target = this._strip_markdown_formatting(target_text);
1715
- matches = [...this.full_text.matchAll(new RegExp(escapeRegExp(stripped_target), "g"))];
1797
+ matches = [
1798
+ ...this.full_text.matchAll(
1799
+ new RegExp(escapeRegExp(stripped_target), "g")
1800
+ )
1801
+ ];
1716
1802
  if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1717
1803
  try {
1718
1804
  const pattern = new RegExp(this._make_fuzzy_regex(target_text), "g");
1719
1805
  matches = [...this.full_text.matchAll(pattern)];
1720
- if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1806
+ if (matches.length > 0)
1807
+ return matches.map((m) => [m.index, m[0].length]);
1721
1808
  } catch (e) {
1722
1809
  }
1723
1810
  return [];
@@ -1728,14 +1815,22 @@ ${header}`;
1728
1815
  return this._resolve_runs_at_range(start_idx, start_idx + length);
1729
1816
  }
1730
1817
  find_target_runs_by_index(start_index, length, rebuild_map = true) {
1731
- return this._resolve_runs_at_range(start_index, start_index + length, rebuild_map);
1818
+ return this._resolve_runs_at_range(
1819
+ start_index,
1820
+ start_index + length,
1821
+ rebuild_map
1822
+ );
1732
1823
  }
1733
1824
  get_virtual_spans_in_range(start_index, length) {
1734
1825
  const end_index = start_index + length;
1735
- return this.spans.filter((s) => s.run === null && s.text === "\n\n" && s.start >= start_index && s.end <= end_index);
1826
+ return this.spans.filter(
1827
+ (s) => s.run === null && s.text === "\n\n" && s.start >= start_index && s.end <= end_index
1828
+ );
1736
1829
  }
1737
1830
  _resolve_runs_at_range(start_idx, end_idx, rebuild_map = true) {
1738
- const affected_spans = this.spans.filter((s) => s.end > start_idx && s.start < end_idx);
1831
+ const affected_spans = this.spans.filter(
1832
+ (s) => s.end > start_idx && s.start < end_idx
1833
+ );
1739
1834
  if (affected_spans.length === 0) return [];
1740
1835
  const working_runs = affected_spans.filter((s) => s.run !== null).map((s) => s.run);
1741
1836
  if (working_runs.length === 0) return [];
@@ -1746,7 +1841,10 @@ ${header}`;
1746
1841
  const local_start = start_idx - first_real_span.start;
1747
1842
  if (local_start > 0) {
1748
1843
  const idx_in_working = 0;
1749
- const [, right_run] = this._split_run_at_index(working_runs[idx_in_working], local_start);
1844
+ const [, right_run] = this._split_run_at_index(
1845
+ working_runs[idx_in_working],
1846
+ local_start
1847
+ );
1750
1848
  working_runs[idx_in_working] = right_run;
1751
1849
  dom_modified = true;
1752
1850
  start_split_adjustment = local_start;
@@ -1783,7 +1881,9 @@ ${header}`;
1783
1881
  if (preceding[i].paragraph) return [null, preceding[i].paragraph];
1784
1882
  }
1785
1883
  }
1786
- const containing = this.spans.filter((s) => s.start < index && index < s.end);
1884
+ const containing = this.spans.filter(
1885
+ (s) => s.start < index && index < s.end
1886
+ );
1787
1887
  if (containing.length > 0) {
1788
1888
  const span = containing[0];
1789
1889
  if (span.run === null) {
@@ -1806,10 +1906,12 @@ ${header}`;
1806
1906
  const preceding_gap = this.spans.filter((s) => s.end < index);
1807
1907
  if (preceding_gap.length > 0) {
1808
1908
  for (let i = preceding_gap.length - 1; i >= 0; i--) {
1809
- if (preceding_gap[i].run) return [preceding_gap[i].run, preceding_gap[i].paragraph];
1909
+ if (preceding_gap[i].run)
1910
+ return [preceding_gap[i].run, preceding_gap[i].paragraph];
1810
1911
  }
1811
1912
  for (let i = preceding_gap.length - 1; i >= 0; i--) {
1812
- if (preceding_gap[i].paragraph) return [null, preceding_gap[i].paragraph];
1913
+ if (preceding_gap[i].paragraph)
1914
+ return [null, preceding_gap[i].paragraph];
1813
1915
  }
1814
1916
  }
1815
1917
  return [null, null];
@@ -1822,7 +1924,10 @@ ${header}`;
1822
1924
  const new_r_element = run._element.cloneNode(true);
1823
1925
  this._set_run_text_elements(new_r_element, right_text);
1824
1926
  if (run._element.parentNode) {
1825
- run._element.parentNode.insertBefore(new_r_element, run._element.nextSibling);
1927
+ run._element.parentNode.insertBefore(
1928
+ new_r_element,
1929
+ run._element.nextSibling
1930
+ );
1826
1931
  }
1827
1932
  const new_run = new Run(new_r_element, run._parent);
1828
1933
  return [run, new_run];
@@ -1849,7 +1954,9 @@ ${header}`;
1849
1954
  }
1850
1955
  }
1851
1956
  get_context_at_range(start_idx, end_idx) {
1852
- const real_spans = this.spans.filter((s) => s.run && s.end > start_idx && s.start < end_idx);
1957
+ const real_spans = this.spans.filter(
1958
+ (s) => s.run && s.end > start_idx && s.start < end_idx
1959
+ );
1853
1960
  if (real_spans.length > 0) return real_spans[0];
1854
1961
  return null;
1855
1962
  }
@@ -2201,6 +2308,193 @@ function create_word_patch_diff(original_text, modified_text, original_path = "O
2201
2308
  return output.join("\n");
2202
2309
  }
2203
2310
 
2311
+ // src/pagination.ts
2312
+ var PAGE_TARGET_CHARS = 19e3;
2313
+ var APPENDIX_MARKER = "<!-- READONLY_BOUNDARY_START -->";
2314
+ var _CRITIC_TOKENS = {
2315
+ "{++": "++}",
2316
+ "{--": "--}",
2317
+ "{==": "==}",
2318
+ "{>>": "<<}"
2319
+ };
2320
+ var _CHG_ID_PATTERN = /\bChg:(\d+)\b/g;
2321
+ function split_structural_appendix(markdown) {
2322
+ if (!markdown) return ["", ""];
2323
+ const idx = markdown.indexOf(APPENDIX_MARKER);
2324
+ if (idx === -1) return [markdown, ""];
2325
+ const line_start = markdown.lastIndexOf("\n", idx) + 1;
2326
+ const body = markdown.substring(0, line_start).trimEnd();
2327
+ const appendix = markdown.substring(line_start);
2328
+ return [body, appendix];
2329
+ }
2330
+ function paginate(markdown_body, structural_appendix = "") {
2331
+ if (!markdown_body) {
2332
+ const appendix_clean = structural_appendix ? structural_appendix.trim() : "";
2333
+ const content = appendix_clean;
2334
+ return {
2335
+ pages: [{
2336
+ page: 1,
2337
+ total_pages: 1,
2338
+ has_next: false,
2339
+ has_prev: false,
2340
+ tracked_change_count: _count_tracked_changes(content),
2341
+ page_content: content
2342
+ }],
2343
+ total_pages: 1,
2344
+ body_pages: [""],
2345
+ body_page_offsets: [0]
2346
+ };
2347
+ }
2348
+ const block_records = _tokenize_into_atomic_blocks(markdown_body);
2349
+ const [body_pages, body_page_offsets] = _assemble_pages(block_records);
2350
+ let final_pages;
2351
+ if (structural_appendix && structural_appendix.trim()) {
2352
+ const appendix = structural_appendix.trim();
2353
+ final_pages = body_pages.map((bp) => bp ? `${bp}
2354
+
2355
+ ${appendix}` : appendix);
2356
+ } else {
2357
+ final_pages = [...body_pages];
2358
+ }
2359
+ const total = final_pages.length;
2360
+ const page_infos = final_pages.map((content, i) => ({
2361
+ page: i + 1,
2362
+ total_pages: total,
2363
+ has_next: i + 1 < total,
2364
+ has_prev: i + 1 > 1,
2365
+ tracked_change_count: _count_tracked_changes(content),
2366
+ page_content: content
2367
+ }));
2368
+ return {
2369
+ pages: page_infos,
2370
+ total_pages: total,
2371
+ body_pages,
2372
+ body_page_offsets
2373
+ };
2374
+ }
2375
+ function _tokenize_into_atomic_blocks(markdown_body) {
2376
+ const raw_blocks = _split_on_safe_paragraph_breaks(markdown_body);
2377
+ return _merge_footnote_sections(raw_blocks);
2378
+ }
2379
+ function _split_on_safe_paragraph_breaks(text) {
2380
+ const counters = { "++}": 0, "--}": 0, "==}": 0, "<<}": 0 };
2381
+ const blocks = [];
2382
+ let block_start = 0;
2383
+ let i = 0;
2384
+ const n = text.length;
2385
+ while (i < n) {
2386
+ let matched_open = false;
2387
+ for (const [open_tok, close_tok] of Object.entries(_CRITIC_TOKENS)) {
2388
+ if (text.startsWith(open_tok, i)) {
2389
+ counters[close_tok]++;
2390
+ i += open_tok.length;
2391
+ matched_open = true;
2392
+ break;
2393
+ }
2394
+ }
2395
+ if (matched_open) continue;
2396
+ let matched_close = false;
2397
+ for (const close_tok of Object.values(_CRITIC_TOKENS)) {
2398
+ if (text.startsWith(close_tok, i)) {
2399
+ if (counters[close_tok] > 0) counters[close_tok]--;
2400
+ i += close_tok.length;
2401
+ matched_close = true;
2402
+ break;
2403
+ }
2404
+ }
2405
+ if (matched_close) continue;
2406
+ if (text[i] === "\n" && i + 1 < n && text[i + 1] === "\n") {
2407
+ if (Object.values(counters).every((c) => c === 0)) {
2408
+ const block_text = text.substring(block_start, i);
2409
+ if (block_text) blocks.push([block_text, block_start]);
2410
+ let j = i;
2411
+ while (j < n && text[j] === "\n") j++;
2412
+ i = j;
2413
+ block_start = i;
2414
+ continue;
2415
+ }
2416
+ }
2417
+ i++;
2418
+ }
2419
+ if (block_start < n) {
2420
+ const block_text = text.substring(block_start, n);
2421
+ if (block_text) blocks.push([block_text, block_start]);
2422
+ }
2423
+ return blocks;
2424
+ }
2425
+ function _merge_footnote_sections(blocks) {
2426
+ if (!blocks.length) return blocks;
2427
+ const merged = [];
2428
+ let i = 0;
2429
+ while (i < blocks.length) {
2430
+ const [block_text, block_offset] = blocks[i];
2431
+ const stripped = block_text.trimStart();
2432
+ const is_section_header = stripped.startsWith("## Footnotes") || stripped.startsWith("## Endnotes");
2433
+ if (!is_section_header) {
2434
+ merged.push([block_text, block_offset]);
2435
+ i++;
2436
+ continue;
2437
+ }
2438
+ let accumulated_text = block_text;
2439
+ let j = i + 1;
2440
+ while (j < blocks.length) {
2441
+ const [next_text] = blocks[j];
2442
+ const next_stripped = next_text.trimStart();
2443
+ if (next_stripped.startsWith("[^fn-") || next_stripped.startsWith("[^en-")) {
2444
+ accumulated_text = `${accumulated_text}
2445
+
2446
+ ${next_text}`;
2447
+ j++;
2448
+ } else {
2449
+ break;
2450
+ }
2451
+ }
2452
+ merged.push([accumulated_text, block_offset]);
2453
+ i = j;
2454
+ }
2455
+ return merged;
2456
+ }
2457
+ function _assemble_pages(block_records) {
2458
+ if (!block_records.length) return [[""], [0]];
2459
+ const pages = [];
2460
+ const page_starts = [];
2461
+ let current_blocks = [];
2462
+ let current_size = 0;
2463
+ let current_start = -1;
2464
+ const flush_current = () => {
2465
+ if (current_blocks.length > 0) {
2466
+ pages.push(current_blocks.join("\n\n"));
2467
+ page_starts.push(current_start);
2468
+ }
2469
+ current_blocks = [];
2470
+ current_size = 0;
2471
+ current_start = -1;
2472
+ };
2473
+ for (const [block_text, block_offset] of block_records) {
2474
+ const block_size = block_text.length;
2475
+ const added_size = block_size + (current_blocks.length > 0 ? 2 : 0);
2476
+ if (current_blocks.length > 0 && current_size + added_size > PAGE_TARGET_CHARS) {
2477
+ flush_current();
2478
+ }
2479
+ if (current_blocks.length === 0 && block_size > PAGE_TARGET_CHARS) {
2480
+ pages.push(block_text);
2481
+ page_starts.push(block_offset);
2482
+ continue;
2483
+ }
2484
+ if (current_blocks.length === 0) current_start = block_offset;
2485
+ current_blocks.push(block_text);
2486
+ current_size += current_size > 0 ? added_size : block_size;
2487
+ }
2488
+ flush_current();
2489
+ if (!pages.length) return [[""], [0]];
2490
+ return [pages, page_starts];
2491
+ }
2492
+ function _count_tracked_changes(page_content) {
2493
+ const matches = [...page_content.matchAll(_CHG_ID_PATTERN)];
2494
+ const distinct = new Set(matches.map((m) => m[1]));
2495
+ return distinct.size;
2496
+ }
2497
+
2204
2498
  // src/markup.ts
2205
2499
  var AMBIGUITY_EXAMPLES_CAP = 5;
2206
2500
  var AMBIGUITY_CONTEXT_CHARS = 50;
@@ -2680,7 +2974,8 @@ var RedlineEngine = class {
2680
2974
  start_idx + length,
2681
2975
  start_idx + length + 30
2682
2976
  );
2683
- const critic_markup = `${context_before}{--${target_text}--}{++${new_text}++}${context_after}`;
2977
+ const insertion = new_text ? `{++${new_text}++}` : "";
2978
+ const critic_markup = `${context_before}{--${target_text}--}${insertion}${context_after}`;
2684
2979
  let clean_text = critic_markup;
2685
2980
  clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
2686
2981
  clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
@@ -2698,6 +2993,37 @@ var RedlineEngine = class {
2698
2993
  }
2699
2994
  return maxId;
2700
2995
  }
2996
+ _get_heading_path_and_page(start_idx, text, page_offsets) {
2997
+ let page = 1;
2998
+ for (let i = 0; i < page_offsets.length; i++) {
2999
+ if (start_idx >= page_offsets[i]) {
3000
+ page = i + 1;
3001
+ } else {
3002
+ break;
3003
+ }
3004
+ }
3005
+ const textBefore = text.substring(0, start_idx);
3006
+ const lines = textBefore.split("\n");
3007
+ const path = [];
3008
+ let current_level = 999;
3009
+ for (let i = lines.length - 1; i >= 0; i--) {
3010
+ const line = lines[i];
3011
+ const m = line.match(/^(#{1,6})\s+(.*)/);
3012
+ if (m) {
3013
+ const level = m[1].length;
3014
+ if (level < current_level) {
3015
+ let cleanHeading = m[2].replace(/\*\*|__|[*_]/g, "").replace(/\{#[^}]+\}/g, "").trim();
3016
+ if (cleanHeading.length > 80) {
3017
+ cleanHeading = cleanHeading.substring(0, 80) + "...";
3018
+ }
3019
+ path.unshift(cleanHeading);
3020
+ current_level = level;
3021
+ if (level === 1) break;
3022
+ }
3023
+ }
3024
+ }
3025
+ return [path.join(" > "), page];
3026
+ }
2701
3027
  accept_all_revisions() {
2702
3028
  const parts_to_process = [this.doc.element];
2703
3029
  for (const part of this.doc.pkg.parts) {
@@ -3396,13 +3722,15 @@ var RedlineEngine = class {
3396
3722
  for (let i = 0; i < edits.length; i++) {
3397
3723
  const edit = edits[i];
3398
3724
  if (!edit.target_text) continue;
3399
- let matches = this.mapper.find_all_match_indices(edit.target_text);
3725
+ const is_regex = edit.regex || false;
3726
+ const match_mode = edit.match_mode || "strict";
3727
+ let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
3400
3728
  let activeText = this.mapper.full_text;
3401
3729
  let target_mapper = this.mapper;
3402
3730
  if (matches.length === 0) {
3403
3731
  if (!this.clean_mapper)
3404
3732
  this.clean_mapper = new DocumentMapper(this.doc, true);
3405
- matches = this.clean_mapper.find_all_match_indices(edit.target_text);
3733
+ matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
3406
3734
  if (matches.length > 0) {
3407
3735
  activeText = this.clean_mapper.full_text;
3408
3736
  target_mapper = this.clean_mapper;
@@ -3424,7 +3752,7 @@ var RedlineEngine = class {
3424
3752
  if (!this.original_mapper) {
3425
3753
  this.original_mapper = new DocumentMapper(this.doc, false, true);
3426
3754
  }
3427
- const orig_matches = this.original_mapper.find_all_match_indices(edit.target_text);
3755
+ const orig_matches = this.original_mapper.find_all_match_indices(edit.target_text, is_regex);
3428
3756
  if (orig_matches.length > 0) {
3429
3757
  is_deleted_text = true;
3430
3758
  for (const [start, length] of orig_matches) {
@@ -3461,7 +3789,7 @@ var RedlineEngine = class {
3461
3789
  "${edit.target_text}"`
3462
3790
  );
3463
3791
  }
3464
- } else if (matches.length > 1) {
3792
+ } else if (matches.length > 1 && match_mode === "strict") {
3465
3793
  const positions = matches.map(([start, length]) => [
3466
3794
  start,
3467
3795
  start + length
@@ -3519,6 +3847,14 @@ var RedlineEngine = class {
3519
3847
  if (auth && auth !== this.author) nestedAuthors.add(auth);
3520
3848
  }
3521
3849
  }
3850
+ if (s.comment_ids) {
3851
+ for (const cid of s.comment_ids) {
3852
+ const c_data = this.mapper.comments_map[cid];
3853
+ if (c_data && c_data.author && c_data.author !== this.author) {
3854
+ nestedAuthors.add(c_data.author);
3855
+ }
3856
+ }
3857
+ }
3522
3858
  }
3523
3859
  if (nestedAuthors.size > 0) {
3524
3860
  errors.push(
@@ -3649,6 +3985,9 @@ var RedlineEngine = class {
3649
3985
  if (this.clean_mapper) this.clean_mapper["_build_map"]();
3650
3986
  }
3651
3987
  }
3988
+ const [body_text] = split_structural_appendix(this.mapper.full_text);
3989
+ const pag_res = paginate(body_text, "");
3990
+ const page_offsets = pag_res.body_page_offsets;
3652
3991
  const edits_reports = [];
3653
3992
  let applied_edits = 0;
3654
3993
  let skipped_edits = 0;
@@ -3672,9 +4011,8 @@ var RedlineEngine = class {
3672
4011
  });
3673
4012
  continue;
3674
4013
  }
3675
- const res = this.apply_edits([edit]);
3676
- const applied = res[0];
3677
- if (applied > 0) {
4014
+ const res = this.apply_edits([edit], page_offsets);
4015
+ if (edit._applied_status) {
3678
4016
  applied_edits++;
3679
4017
  const previews = this._build_edit_context_previews(edit);
3680
4018
  edits_reports.push({
@@ -3684,7 +4022,11 @@ var RedlineEngine = class {
3684
4022
  warning,
3685
4023
  error: null,
3686
4024
  critic_markup: previews[0],
3687
- clean_text: previews[1]
4025
+ clean_text: previews[1],
4026
+ pages: edit._pages || [],
4027
+ heading_path: edit._heading_path || "",
4028
+ occurrences_modified: edit._occurrences_modified || 0,
4029
+ match_mode: edit.match_mode || "strict"
3688
4030
  });
3689
4031
  } else {
3690
4032
  skipped_edits++;
@@ -3706,9 +4048,9 @@ var RedlineEngine = class {
3706
4048
  throw new BatchValidationError(errors);
3707
4049
  }
3708
4050
  const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
3709
- const res = this.apply_edits(cloned_edits);
3710
- applied_edits = res[0];
3711
- skipped_edits = res[1];
4051
+ const res = this.apply_edits(cloned_edits, page_offsets);
4052
+ applied_edits = cloned_edits.filter((e) => e._applied_status).length;
4053
+ skipped_edits = cloned_edits.length - applied_edits;
3712
4054
  for (const edit of cloned_edits) {
3713
4055
  const success = edit._applied_status || false;
3714
4056
  const error_msg = edit._error_msg || null;
@@ -3729,7 +4071,11 @@ var RedlineEngine = class {
3729
4071
  warning,
3730
4072
  error: error_msg,
3731
4073
  critic_markup,
3732
- clean_text
4074
+ clean_text,
4075
+ pages: edit._pages || [],
4076
+ heading_path: edit._heading_path || "",
4077
+ occurrences_modified: edit._occurrences_modified || 0,
4078
+ match_mode: edit.match_mode || "strict"
3733
4079
  });
3734
4080
  }
3735
4081
  }
@@ -3745,9 +4091,13 @@ var RedlineEngine = class {
3745
4091
  version: "1.10.0"
3746
4092
  };
3747
4093
  }
3748
- apply_edits(edits) {
4094
+ apply_edits(edits, page_offsets = []) {
3749
4095
  let applied = 0;
3750
4096
  let skipped = 0;
4097
+ if (!page_offsets || page_offsets.length === 0) {
4098
+ const [body_text] = split_structural_appendix(this.mapper.full_text);
4099
+ page_offsets = paginate(body_text, "").body_page_offsets;
4100
+ }
3751
4101
  const resolved_edits = [];
3752
4102
  for (const edit of edits) {
3753
4103
  edit._applied_status = false;
@@ -3849,6 +4199,19 @@ var RedlineEngine = class {
3849
4199
  const parent = edit._parent_edit_ref;
3850
4200
  if (parent) {
3851
4201
  parent._applied_status = true;
4202
+ parent._occurrences_modified = (parent._occurrences_modified || 0) + 1;
4203
+ const [path, page] = this._get_heading_path_and_page(start, this.mapper.full_text, page_offsets);
4204
+ const pages = parent._pages || [];
4205
+ if (!pages.includes(page)) pages.unshift(page);
4206
+ parent._pages = pages;
4207
+ parent._heading_path = path;
4208
+ } else {
4209
+ edit._occurrences_modified = (edit._occurrences_modified || 0) + 1;
4210
+ const [path, page] = this._get_heading_path_and_page(start, this.mapper.full_text, page_offsets);
4211
+ const pages = edit._pages || [];
4212
+ if (!pages.includes(page)) pages.unshift(page);
4213
+ edit._pages = pages;
4214
+ edit._heading_path = path;
3852
4215
  }
3853
4216
  } else {
3854
4217
  skipped++;
@@ -4001,96 +4364,192 @@ var RedlineEngine = class {
4001
4364
  }
4002
4365
  return false;
4003
4366
  }
4004
- /**
4005
- * Returns the first match of `target_text` in the raw mapper that is NOT
4006
- * entirely contained within a tracked deletion (<w:del>). Tracked-deleted
4007
- * copies are not live, editable text, so an edit must resolve to a live
4008
- * occurrence even when a dead copy appears earlier in the document
4009
- * (BUG-23-5). Falls back to the plain first match when no live copy is
4010
- * found (e.g. fuzzy/normalized matches the span filter cannot align).
4011
- */
4012
- _first_live_match(target_text) {
4013
- const all = this.mapper.find_all_match_indices(target_text);
4014
- if (all.length <= 1) {
4015
- return this.mapper.find_match_index(target_text);
4016
- }
4017
- for (const [start, length] of all) {
4018
- const realSpans = this.mapper.spans.filter(
4019
- (s) => s.run !== null && s.end > start && s.start < start + length
4020
- );
4021
- if (realSpans.length === 0) return [start, length];
4022
- if (realSpans.some((s) => !s.del_id)) return [start, length];
4023
- }
4024
- return this.mapper.find_match_index(target_text);
4025
- }
4026
4367
  _pre_resolve_heuristic_edit(edit) {
4027
4368
  if (!edit.target_text) return null;
4028
- let [start_idx, match_len] = this._first_live_match(edit.target_text);
4369
+ const is_regex = edit.regex || false;
4370
+ const match_mode = edit.match_mode || "strict";
4371
+ let matches = this.mapper.find_all_match_indices(edit.target_text, is_regex);
4029
4372
  let use_clean_map = false;
4030
- if (start_idx === -1) {
4373
+ if (matches.length === 0) {
4031
4374
  if (!this.clean_mapper)
4032
4375
  this.clean_mapper = new DocumentMapper(this.doc, true);
4033
- [start_idx, match_len] = this.clean_mapper.find_match_index(
4034
- edit.target_text
4035
- );
4036
- if (start_idx !== -1) use_clean_map = true;
4376
+ matches = this.clean_mapper.find_all_match_indices(edit.target_text, is_regex);
4377
+ if (matches.length > 0) use_clean_map = true;
4037
4378
  else return null;
4038
4379
  }
4039
4380
  const active_mapper = use_clean_map ? this.clean_mapper : this.mapper;
4040
- const effective_new_text = edit.new_text || "";
4041
- const actual_doc_text = this.mapper.full_text.substring(
4042
- start_idx,
4043
- start_idx + match_len
4044
- );
4045
- if (actual_doc_text === effective_new_text || edit.target_text === effective_new_text) {
4046
- return {
4381
+ let live_matches = [];
4382
+ for (const [s, match_len] of matches) {
4383
+ const realSpans = active_mapper.spans.filter(
4384
+ (span) => span.run !== null && span.end > s && span.start < s + match_len
4385
+ );
4386
+ if (realSpans.length === 0 || realSpans.some((span) => !span.del_id)) {
4387
+ live_matches.push([s, match_len]);
4388
+ }
4389
+ }
4390
+ if (live_matches.length === 0) return null;
4391
+ if (match_mode === "strict" || match_mode === "first") {
4392
+ live_matches = live_matches.slice(0, 1);
4393
+ }
4394
+ const all_sub_edits = [];
4395
+ for (const [start_idx, match_len] of live_matches) {
4396
+ const actual_doc_text = active_mapper.full_text.substring(
4397
+ start_idx,
4398
+ start_idx + match_len
4399
+ );
4400
+ let current_effective_new_text = edit.new_text || "";
4401
+ if (is_regex && current_effective_new_text) {
4402
+ try {
4403
+ current_effective_new_text = actual_doc_text.replace(new RegExp(edit.target_text), current_effective_new_text);
4404
+ } catch (e) {
4405
+ }
4406
+ }
4407
+ const [edit_target_clean, edit_target_style] = this._parse_markdown_style(edit.target_text);
4408
+ const [edit_new_clean, edit_new_style] = this._parse_markdown_style(current_effective_new_text);
4409
+ if (edit_target_style !== edit_new_style) {
4410
+ const [actual_clean] = this._parse_markdown_style(actual_doc_text);
4411
+ const final_target2 = actual_clean;
4412
+ const final_new2 = edit_new_clean;
4413
+ const style_op = final_target2 === final_new2 ? "STYLE_ONLY" : "STYLE_AND_TEXT";
4414
+ const prefix_offset = actual_doc_text.indexOf(actual_clean);
4415
+ const effective_start_idx2 = start_idx + (prefix_offset !== -1 ? prefix_offset : 0);
4416
+ const resolved_style = edit_new_style !== null ? edit_new_style : "Normal";
4417
+ all_sub_edits.push({
4418
+ type: "modify",
4419
+ target_text: final_target2,
4420
+ new_text: final_new2,
4421
+ comment: edit.comment,
4422
+ _match_start_index: effective_start_idx2,
4423
+ _internal_op: style_op,
4424
+ _new_style: resolved_style,
4425
+ _active_mapper_ref: active_mapper
4426
+ });
4427
+ continue;
4428
+ }
4429
+ if (actual_doc_text === current_effective_new_text || edit.target_text === current_effective_new_text) {
4430
+ all_sub_edits.push({
4431
+ type: "modify",
4432
+ target_text: actual_doc_text,
4433
+ new_text: actual_doc_text,
4434
+ comment: edit.comment,
4435
+ _match_start_index: start_idx,
4436
+ _internal_op: "COMMENT_ONLY",
4437
+ _active_mapper_ref: active_mapper
4438
+ });
4439
+ continue;
4440
+ }
4441
+ let effective_op = "";
4442
+ let final_target = "";
4443
+ let final_new = "";
4444
+ let effective_start_idx = start_idx;
4445
+ if (current_effective_new_text.startsWith(actual_doc_text)) {
4446
+ effective_op = "INSERTION";
4447
+ final_new = current_effective_new_text.substring(actual_doc_text.length);
4448
+ effective_start_idx = start_idx + match_len;
4449
+ } else {
4450
+ const [prefix_len, suffix_len] = trim_common_context(
4451
+ actual_doc_text,
4452
+ current_effective_new_text
4453
+ );
4454
+ const t_end = actual_doc_text.length - suffix_len;
4455
+ const n_end = current_effective_new_text.length - suffix_len;
4456
+ final_target = actual_doc_text.substring(prefix_len, t_end);
4457
+ final_new = current_effective_new_text.substring(prefix_len, n_end);
4458
+ effective_start_idx = start_idx + prefix_len;
4459
+ if (!final_target && final_new) effective_op = "INSERTION";
4460
+ else if (final_target && !final_new) effective_op = "DELETION";
4461
+ else if (final_target && final_new) effective_op = "MODIFICATION";
4462
+ else effective_op = "COMMENT_ONLY";
4463
+ }
4464
+ all_sub_edits.push({
4047
4465
  type: "modify",
4048
- target_text: actual_doc_text,
4049
- new_text: actual_doc_text,
4466
+ target_text: final_target,
4467
+ new_text: final_new,
4050
4468
  comment: edit.comment,
4051
- _match_start_index: start_idx,
4052
- _internal_op: "COMMENT_ONLY",
4469
+ _match_start_index: effective_start_idx,
4470
+ _internal_op: effective_op,
4053
4471
  _active_mapper_ref: active_mapper
4054
- };
4472
+ });
4055
4473
  }
4056
- let effective_op = "";
4057
- let final_target = "";
4058
- let final_new = "";
4059
- let effective_start_idx = start_idx;
4060
- if (effective_new_text.startsWith(actual_doc_text)) {
4061
- effective_op = "INSERTION";
4062
- final_new = effective_new_text.substring(actual_doc_text.length);
4063
- effective_start_idx = start_idx + match_len;
4064
- } else {
4065
- const [prefix_len, suffix_len] = trim_common_context(
4066
- actual_doc_text,
4067
- effective_new_text
4068
- );
4069
- const t_end = actual_doc_text.length - suffix_len;
4070
- const n_end = effective_new_text.length - suffix_len;
4071
- final_target = actual_doc_text.substring(prefix_len, t_end);
4072
- final_new = effective_new_text.substring(prefix_len, n_end);
4073
- effective_start_idx = start_idx + prefix_len;
4074
- if (!final_target && final_new) effective_op = "INSERTION";
4075
- else if (final_target && !final_new) effective_op = "DELETION";
4076
- else if (final_target && final_new) effective_op = "MODIFICATION";
4077
- else effective_op = "COMMENT_ONLY";
4078
- }
4079
- return {
4080
- type: "modify",
4081
- target_text: final_target,
4082
- new_text: final_new,
4083
- comment: edit.comment,
4084
- _match_start_index: effective_start_idx,
4085
- _internal_op: effective_op,
4086
- _active_mapper_ref: active_mapper
4087
- };
4474
+ if (all_sub_edits.length === 0) return null;
4475
+ if (match_mode === "all" || all_sub_edits.length > 1) return all_sub_edits;
4476
+ return all_sub_edits[0];
4088
4477
  }
4089
4478
  _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
4090
4479
  let op = edit._internal_op;
4091
4480
  const active_mapper = edit._active_mapper_ref || this.mapper;
4092
4481
  const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
4093
4482
  const length = edit.target_text ? edit.target_text.length : 0;
4483
+ if (op === "STYLE_ONLY" || op === "STYLE_AND_TEXT") {
4484
+ const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
4485
+ start_idx,
4486
+ rebuild_map
4487
+ );
4488
+ let target_para_el = null;
4489
+ if (anchor_para) {
4490
+ target_para_el = anchor_para._element;
4491
+ } else if (anchor_run) {
4492
+ let walker = anchor_run._element;
4493
+ while (walker && walker.tagName !== "w:p") {
4494
+ walker = walker.parentNode;
4495
+ }
4496
+ target_para_el = walker;
4497
+ }
4498
+ if (target_para_el && edit._new_style) {
4499
+ this._set_paragraph_style(target_para_el, edit._new_style);
4500
+ }
4501
+ if (op === "STYLE_ONLY") {
4502
+ if (edit.comment) {
4503
+ const target_runs2 = active_mapper.find_target_runs_by_index(
4504
+ start_idx,
4505
+ length,
4506
+ rebuild_map
4507
+ );
4508
+ if (target_runs2.length > 0) {
4509
+ const first_el = target_runs2[0]._element;
4510
+ const last_el = target_runs2[target_runs2.length - 1]._element;
4511
+ let start_p = first_el;
4512
+ while (start_p && start_p.tagName !== "w:p")
4513
+ start_p = start_p.parentNode;
4514
+ let end_p = last_el;
4515
+ while (end_p && end_p.tagName !== "w:p")
4516
+ end_p = end_p.parentNode;
4517
+ if (start_p && end_p) {
4518
+ const ascend_to_paragraph_child = (el, p) => {
4519
+ let cur = el;
4520
+ while (cur.parentNode && cur.parentNode !== p) {
4521
+ cur = cur.parentNode;
4522
+ }
4523
+ return cur;
4524
+ };
4525
+ const first_anchor = ascend_to_paragraph_child(first_el, start_p);
4526
+ const last_anchor = ascend_to_paragraph_child(last_el, end_p);
4527
+ if (start_p === end_p) {
4528
+ this._attach_comment(start_p, first_anchor, last_anchor, edit.comment);
4529
+ } else {
4530
+ this._attach_comment_spanning(
4531
+ start_p,
4532
+ first_anchor,
4533
+ end_p,
4534
+ last_anchor,
4535
+ edit.comment
4536
+ );
4537
+ }
4538
+ }
4539
+ }
4540
+ }
4541
+ return true;
4542
+ }
4543
+ if (edit.target_text && edit.new_text) {
4544
+ op = "MODIFICATION";
4545
+ } else if (!edit.target_text && edit.new_text) {
4546
+ op = "INSERTION";
4547
+ } else if (edit.target_text && !edit.new_text) {
4548
+ op = "DELETION";
4549
+ } else {
4550
+ op = "COMMENT_ONLY";
4551
+ }
4552
+ }
4094
4553
  const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
4095
4554
  const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
4096
4555
  if (op === "COMMENT_ONLY") {
@@ -4473,1863 +4932,1814 @@ var RedlineEngine = class {
4473
4932
  }
4474
4933
  };
4475
4934
 
4476
- // src/pagination.ts
4477
- var PAGE_TARGET_CHARS = 19e3;
4478
- var APPENDIX_MARKER = "<!-- READONLY_BOUNDARY_START -->";
4479
- var _CRITIC_TOKENS = {
4480
- "{++": "++}",
4481
- "{--": "--}",
4482
- "{==": "==}",
4483
- "{>>": "<<}"
4484
- };
4485
- var _CHG_ID_PATTERN = /\bChg:(\d+)\b/g;
4486
- function split_structural_appendix(markdown) {
4487
- if (!markdown) return ["", ""];
4488
- const idx = markdown.indexOf(APPENDIX_MARKER);
4489
- if (idx === -1) return [markdown, ""];
4490
- const line_start = markdown.lastIndexOf("\n", idx) + 1;
4491
- const body = markdown.substring(0, line_start).trimEnd();
4492
- const appendix = markdown.substring(line_start);
4493
- return [body, appendix];
4494
- }
4495
- function paginate(markdown_body, structural_appendix = "") {
4496
- if (!markdown_body) {
4497
- const appendix_clean = structural_appendix ? structural_appendix.trim() : "";
4498
- const content = appendix_clean;
4499
- return {
4500
- pages: [{
4501
- page: 1,
4502
- total_pages: 1,
4503
- has_next: false,
4504
- has_prev: false,
4505
- tracked_change_count: _count_tracked_changes(content),
4506
- page_content: content
4507
- }],
4508
- total_pages: 1,
4509
- body_pages: [""],
4510
- body_page_offsets: [0]
4511
- };
4512
- }
4513
- const block_records = _tokenize_into_atomic_blocks(markdown_body);
4514
- const [body_pages, body_page_offsets] = _assemble_pages(block_records);
4515
- let final_pages;
4516
- if (structural_appendix && structural_appendix.trim()) {
4517
- const appendix = structural_appendix.trim();
4518
- final_pages = body_pages.map((bp) => bp ? `${bp}
4519
-
4520
- ${appendix}` : appendix);
4521
- } else {
4522
- final_pages = [...body_pages];
4935
+ // src/sanitize/transforms.ts
4936
+ function findDescendantsByLocalName(element, localName) {
4937
+ const result = [];
4938
+ const all = element.getElementsByTagName("*");
4939
+ for (let i = 0; i < all.length; i++) {
4940
+ const tag = all[i].tagName;
4941
+ if (tag === localName || tag.endsWith(":" + localName)) {
4942
+ result.push(all[i]);
4943
+ }
4523
4944
  }
4524
- const total = final_pages.length;
4525
- const page_infos = final_pages.map((content, i) => ({
4526
- page: i + 1,
4527
- total_pages: total,
4528
- has_next: i + 1 < total,
4529
- has_prev: i + 1 > 1,
4530
- tracked_change_count: _count_tracked_changes(content),
4531
- page_content: content
4532
- }));
4533
- return {
4534
- pages: page_infos,
4535
- total_pages: total,
4536
- body_pages,
4537
- body_page_offsets
4538
- };
4539
- }
4540
- function _tokenize_into_atomic_blocks(markdown_body) {
4541
- const raw_blocks = _split_on_safe_paragraph_breaks(markdown_body);
4542
- return _merge_footnote_sections(raw_blocks);
4945
+ return result;
4543
4946
  }
4544
- function _split_on_safe_paragraph_breaks(text) {
4545
- const counters = { "++}": 0, "--}": 0, "==}": 0, "<<}": 0 };
4546
- const blocks = [];
4547
- let block_start = 0;
4548
- let i = 0;
4549
- const n = text.length;
4550
- while (i < n) {
4551
- let matched_open = false;
4552
- for (const [open_tok, close_tok] of Object.entries(_CRITIC_TOKENS)) {
4553
- if (text.startsWith(open_tok, i)) {
4554
- counters[close_tok]++;
4555
- i += open_tok.length;
4556
- matched_open = true;
4557
- break;
4947
+ function coalesce_runs(doc) {
4948
+ let count = 0;
4949
+ function areRunsIdentical(rPr1, rPr2) {
4950
+ const xml1 = rPr1 ? rPr1.toString() : "";
4951
+ const xml2 = rPr2 ? rPr2.toString() : "";
4952
+ return xml1 === xml2;
4953
+ }
4954
+ function hasSpecialContent(run) {
4955
+ const safeTags = ["w:t", "w:tab", "w:br", "w:cr", "w:delText", "w:rPr"];
4956
+ for (let i = 0; i < run.childNodes.length; i++) {
4957
+ const child = run.childNodes[i];
4958
+ if (child.nodeType === 1) {
4959
+ const tag = child.tagName;
4960
+ if (!safeTags.includes(tag)) return true;
4558
4961
  }
4559
4962
  }
4560
- if (matched_open) continue;
4561
- let matched_close = false;
4562
- for (const close_tok of Object.values(_CRITIC_TOKENS)) {
4563
- if (text.startsWith(close_tok, i)) {
4564
- if (counters[close_tok] > 0) counters[close_tok]--;
4565
- i += close_tok.length;
4566
- matched_close = true;
4567
- break;
4963
+ return false;
4964
+ }
4965
+ function coalesceContainer(container) {
4966
+ const children = Array.from(container.childNodes).filter((n) => n.nodeType === 1);
4967
+ let i = 0;
4968
+ while (i < children.length - 1) {
4969
+ const curr = children[i];
4970
+ const nxt = children[i + 1];
4971
+ if (curr.tagName === "w:r" && nxt.tagName === "w:r") {
4972
+ if (!hasSpecialContent(curr) && !hasSpecialContent(nxt)) {
4973
+ const rPr1 = findChild(curr, "w:rPr");
4974
+ const rPr2 = findChild(nxt, "w:rPr");
4975
+ if (areRunsIdentical(rPr1, rPr2)) {
4976
+ let last_t = null;
4977
+ for (let c = 0; c < curr.childNodes.length; c++) {
4978
+ const child = curr.childNodes[c];
4979
+ if (child.nodeType === 1 && (child.tagName === "w:t" || child.tagName === "w:delText")) {
4980
+ last_t = child;
4981
+ }
4982
+ }
4983
+ const nxtChildren = Array.from(nxt.childNodes).filter((n) => n.nodeType === 1);
4984
+ for (const child of nxtChildren) {
4985
+ if (child.tagName === "w:rPr") continue;
4986
+ if ((child.tagName === "w:t" || child.tagName === "w:delText") && last_t && last_t.tagName === child.tagName) {
4987
+ const t1 = last_t.textContent || "";
4988
+ const t2 = child.textContent || "";
4989
+ const combined = t1 + t2;
4990
+ last_t.textContent = combined;
4991
+ if (combined.trim() !== combined) {
4992
+ last_t.setAttribute("xml:space", "preserve");
4993
+ }
4994
+ } else {
4995
+ curr.appendChild(child);
4996
+ if (child.tagName === "w:t" || child.tagName === "w:delText") {
4997
+ last_t = child;
4998
+ }
4999
+ }
5000
+ }
5001
+ container.removeChild(nxt);
5002
+ children.splice(i + 1, 1);
5003
+ count++;
5004
+ continue;
5005
+ }
5006
+ }
5007
+ }
5008
+ if (["w:ins", "w:del", "w:hyperlink", "w:sdt", "w:smartTag", "w:fldSimple", "w:sdtContent"].includes(curr.tagName)) {
5009
+ coalesceContainer(curr);
4568
5010
  }
5011
+ i++;
4569
5012
  }
4570
- if (matched_close) continue;
4571
- if (text[i] === "\n" && i + 1 < n && text[i + 1] === "\n") {
4572
- if (Object.values(counters).every((c) => c === 0)) {
4573
- const block_text = text.substring(block_start, i);
4574
- if (block_text) blocks.push([block_text, block_start]);
4575
- let j = i;
4576
- while (j < n && text[j] === "\n") j++;
4577
- i = j;
4578
- block_start = i;
4579
- continue;
5013
+ if (children.length > 0) {
5014
+ const last = children[children.length - 1];
5015
+ if (["w:ins", "w:del", "w:hyperlink", "w:sdt", "w:smartTag", "w:fldSimple", "w:sdtContent"].includes(last.tagName)) {
5016
+ coalesceContainer(last);
4580
5017
  }
4581
5018
  }
4582
- i++;
4583
- }
4584
- if (block_start < n) {
4585
- const block_text = text.substring(block_start, n);
4586
- if (block_text) blocks.push([block_text, block_start]);
4587
5019
  }
4588
- return blocks;
5020
+ const paragraphs = findAllDescendants(doc.element, "w:p");
5021
+ for (const p of paragraphs) coalesceContainer(p);
5022
+ return count ? [`Adjacent identical runs coalesced: ${count}`] : [];
4589
5023
  }
4590
- function _merge_footnote_sections(blocks) {
4591
- if (!blocks.length) return blocks;
4592
- const merged = [];
4593
- let i = 0;
4594
- while (i < blocks.length) {
4595
- const [block_text, block_offset] = blocks[i];
4596
- const stripped = block_text.trimStart();
4597
- const is_section_header = stripped.startsWith("## Footnotes") || stripped.startsWith("## Endnotes");
4598
- if (!is_section_header) {
4599
- merged.push([block_text, block_offset]);
4600
- i++;
4601
- continue;
4602
- }
4603
- let accumulated_text = block_text;
4604
- let j = i + 1;
4605
- while (j < blocks.length) {
4606
- const [next_text] = blocks[j];
4607
- const next_stripped = next_text.trimStart();
4608
- if (next_stripped.startsWith("[^fn-") || next_stripped.startsWith("[^en-")) {
4609
- accumulated_text = `${accumulated_text}
4610
-
4611
- ${next_text}`;
4612
- j++;
4613
- } else {
4614
- break;
5024
+ function strip_rsid(doc) {
5025
+ let count = 0;
5026
+ const rsidAttrs = ["w:rsidR", "w:rsidRPr", "w:rsidRDefault", "w:rsidP", "w:rsidDel", "w:rsidSect", "w:rsidTr"];
5027
+ const all = doc.element.getElementsByTagName("*");
5028
+ for (let i = 0; i < all.length; i++) {
5029
+ for (const attr of rsidAttrs) {
5030
+ if (all[i].hasAttribute(attr)) {
5031
+ all[i].removeAttribute(attr);
5032
+ count++;
4615
5033
  }
4616
5034
  }
4617
- merged.push([accumulated_text, block_offset]);
4618
- i = j;
4619
5035
  }
4620
- return merged;
5036
+ const rsidsElements = findAllDescendants(doc.element, "w:rsids");
5037
+ for (const el of rsidsElements) {
5038
+ if (el.parentNode) {
5039
+ el.parentNode.removeChild(el);
5040
+ count++;
5041
+ }
5042
+ }
5043
+ return count ? [`rsid attributes: ${count} removed`] : [];
4621
5044
  }
4622
- function _assemble_pages(block_records) {
4623
- if (!block_records.length) return [[""], [0]];
4624
- const pages = [];
4625
- const page_starts = [];
4626
- let current_blocks = [];
4627
- let current_size = 0;
4628
- let current_start = -1;
4629
- const flush_current = () => {
4630
- if (current_blocks.length > 0) {
4631
- pages.push(current_blocks.join("\n\n"));
4632
- page_starts.push(current_start);
5045
+ function strip_para_ids(doc) {
5046
+ let count = 0;
5047
+ const attrs = ["w14:paraId", "w14:textId"];
5048
+ const all = doc.element.getElementsByTagName("*");
5049
+ for (let i = 0; i < all.length; i++) {
5050
+ for (const attr of attrs) {
5051
+ if (all[i].hasAttribute(attr)) {
5052
+ all[i].removeAttribute(attr);
5053
+ count++;
5054
+ }
4633
5055
  }
4634
- current_blocks = [];
4635
- current_size = 0;
4636
- current_start = -1;
4637
- };
4638
- for (const [block_text, block_offset] of block_records) {
4639
- const block_size = block_text.length;
4640
- const added_size = block_size + (current_blocks.length > 0 ? 2 : 0);
4641
- if (current_blocks.length > 0 && current_size + added_size > PAGE_TARGET_CHARS) {
4642
- flush_current();
5056
+ }
5057
+ return count ? [`Paragraph/text IDs: ${count} removed`] : [];
5058
+ }
5059
+ function strip_proof_errors(doc) {
5060
+ const elements = findAllDescendants(doc.element, "w:proofErr");
5061
+ elements.forEach((el) => el.parentNode?.removeChild(el));
5062
+ return elements.length ? [`Spell check markers: ${elements.length} removed`] : [];
5063
+ }
5064
+ function strip_empty_properties(doc) {
5065
+ let count = 0;
5066
+ for (const tag of ["w:rPr", "w:pPr"]) {
5067
+ const elements = findAllDescendants(doc.element, tag);
5068
+ for (const el of elements) {
5069
+ if (el.childNodes.length === 0 || el.childNodes.length === 1 && el.childNodes[0].nodeType === 3 && !el.childNodes[0].textContent?.trim()) {
5070
+ el.parentNode?.removeChild(el);
5071
+ count++;
5072
+ }
4643
5073
  }
4644
- if (current_blocks.length === 0 && block_size > PAGE_TARGET_CHARS) {
4645
- pages.push(block_text);
4646
- page_starts.push(block_offset);
4647
- continue;
5074
+ }
5075
+ return count ? [`Empty property elements: ${count} removed`] : [];
5076
+ }
5077
+ function strip_hidden_text(doc) {
5078
+ let count = 0;
5079
+ const elements = findAllDescendants(doc.element, "w:rPr");
5080
+ for (const rPr of elements) {
5081
+ if (findChild(rPr, "w:vanish") || findChild(rPr, "w:webHidden")) {
5082
+ const run = rPr.parentNode;
5083
+ if (run && run.tagName === "w:r" && run.parentNode) {
5084
+ run.parentNode.removeChild(run);
5085
+ count++;
5086
+ }
4648
5087
  }
4649
- if (current_blocks.length === 0) current_start = block_offset;
4650
- current_blocks.push(block_text);
4651
- current_size += current_size > 0 ? added_size : block_size;
4652
5088
  }
4653
- flush_current();
4654
- if (!pages.length) return [[""], [0]];
4655
- return [pages, page_starts];
5089
+ return count ? [`Hidden text runs: ${count} removed`] : [];
4656
5090
  }
4657
- function _count_tracked_changes(page_content) {
4658
- const matches = [...page_content.matchAll(_CHG_ID_PATTERN)];
4659
- const distinct = new Set(matches.map((m) => m[1]));
4660
- return distinct.size;
5091
+ function count_tracked_changes(doc) {
5092
+ const ins = findAllDescendants(doc.element, "w:ins").length;
5093
+ const del = findAllDescendants(doc.element, "w:del").length;
5094
+ const fmt = findAllDescendants(doc.element, "w:rPrChange").length + findAllDescendants(doc.element, "w:pPrChange").length + findAllDescendants(doc.element, "w:sectPrChange").length;
5095
+ return [ins, del, fmt];
4661
5096
  }
4662
-
4663
- // src/domain.ts
4664
- function boundedLevenshtein(a, b, maxDist = 2) {
4665
- if (a === b) return 0;
4666
- if (Math.abs(a.length - b.length) > maxDist) return maxDist + 1;
4667
- if (a.length === 0) return b.length <= maxDist ? b.length : maxDist + 1;
4668
- if (b.length === 0) return a.length <= maxDist ? a.length : maxDist + 1;
4669
- if (a.length > b.length) {
4670
- const temp = a;
4671
- a = b;
4672
- b = temp;
5097
+ function get_track_change_authors(doc) {
5098
+ const authors = /* @__PURE__ */ new Set();
5099
+ for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange", "w:sectPrChange"]) {
5100
+ for (const el of findAllDescendants(doc.element, tag)) {
5101
+ const author = el.getAttribute("w:author");
5102
+ if (author) authors.add(author);
5103
+ }
4673
5104
  }
4674
- let row = Array.from({ length: a.length + 1 }, (_, i) => i);
4675
- for (let i = 1; i <= b.length; i++) {
4676
- const newRow = [i];
4677
- let minInRow = i;
4678
- for (let j = 1; j <= a.length; j++) {
4679
- const cost = a[j - 1] === b[i - 1] ? 0 : 1;
4680
- const val = Math.min(
4681
- row[j] + 1,
4682
- newRow[j - 1] + 1,
4683
- row[j - 1] + cost
4684
- );
4685
- newRow.push(val);
4686
- if (val < minInRow) minInRow = val;
5105
+ return authors;
5106
+ }
5107
+ function _getElementText(el) {
5108
+ const texts = [];
5109
+ const ts = findAllDescendants(el, "w:t");
5110
+ for (const t of ts) if (t.textContent) texts.push(t.textContent);
5111
+ const dts = findAllDescendants(el, "w:delText");
5112
+ for (const dt of dts) if (dt.textContent) texts.push(dt.textContent);
5113
+ return texts.join("");
5114
+ }
5115
+ function _truncate(text, maxLen = 60) {
5116
+ const clean = text.replace(/\n/g, " ").trim();
5117
+ if (clean.length <= maxLen) return clean;
5118
+ return clean.substring(0, maxLen - 3) + "...";
5119
+ }
5120
+ function accept_all_tracked_changes(doc) {
5121
+ const lines = [];
5122
+ const insEls = findAllDescendants(doc.element, "w:ins");
5123
+ const delEls = findAllDescendants(doc.element, "w:del");
5124
+ for (const ins of insEls) {
5125
+ const text = _getElementText(ins).trim();
5126
+ if (text) lines.push(` Accepted insertion: "${_truncate(text, 60)}"`);
5127
+ }
5128
+ for (const del of delEls) {
5129
+ const text = _getElementText(del).trim();
5130
+ if (text) lines.push(` Accepted deletion of: "${_truncate(text, 60)}"`);
5131
+ }
5132
+ const engine = new RedlineEngine(doc);
5133
+ engine.accept_all_revisions();
5134
+ for (const tag of ["w:rPrChange", "w:pPrChange", "w:sectPrChange"]) {
5135
+ for (const el of findAllDescendants(doc.element, tag)) {
5136
+ el.parentNode?.removeChild(el);
4687
5137
  }
4688
- if (minInRow > maxDist) return maxDist + 1;
4689
- row = newRow;
4690
5138
  }
4691
- return row[a.length] <= maxDist ? row[a.length] : maxDist + 1;
5139
+ const total = insEls.length + delEls.length;
5140
+ if (total) {
5141
+ return [`Tracked changes auto-accepted: ${total}`].concat(lines);
5142
+ }
5143
+ return [];
4692
5144
  }
4693
- function _get_paragraph_text(p) {
4694
- let text = "";
4695
- const runs = findAllDescendants(p._element, "w:r");
4696
- for (const r of runs) {
4697
- text += get_run_text(new Run(r, p));
5145
+ function get_comments_summary(doc) {
5146
+ const data = extract_comments_data(doc.pkg);
5147
+ const comments = [];
5148
+ let openCount = 0;
5149
+ let resolvedCount = 0;
5150
+ for (const [cId, info] of Object.entries(data)) {
5151
+ if (info.resolved) resolvedCount++;
5152
+ else openCount++;
5153
+ comments.push({ id: cId, ...info });
4698
5154
  }
4699
- return text;
5155
+ return { total: comments.length, open: openCount, resolved: resolvedCount, comments };
4700
5156
  }
4701
- function extract_all_domain_metadata(doc, base_text) {
4702
- const definitions = {};
4703
- const duplicates = /* @__PURE__ */ new Set();
4704
- const raw_anchors = {};
4705
- const raw_references = [];
4706
- const leading_re = /^(?:[\d.\-()a-zA-Z]+\s*)?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”]/;
4707
- const inline_re = /\([^)]*?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”][^)]*?\)/g;
4708
- for (const item of iter_block_items(doc)) {
4709
- if (!(item instanceof Paragraph)) continue;
4710
- const text = _get_paragraph_text(item).trim();
4711
- if (!text) continue;
4712
- const extracted_terms = [];
4713
- const leading_match = text.match(leading_re);
4714
- if (leading_match) extracted_terms.push(leading_match[1].trim());
4715
- const inline_matches = text.matchAll(inline_re);
4716
- for (const m of inline_matches) {
4717
- extracted_terms.push(m[1].trim());
4718
- }
4719
- for (const term of extracted_terms) {
4720
- if (definitions[term]) duplicates.add(term);
4721
- else definitions[term] = { count: 0 };
5157
+ function remove_all_comments(doc) {
5158
+ const data = extract_comments_data(doc.pkg);
5159
+ const keys = Object.keys(data);
5160
+ if (keys.length === 0) return [];
5161
+ const lines = [];
5162
+ const cm = new CommentsManager(doc);
5163
+ for (const [cId, info] of Object.entries(data)) {
5164
+ const status = info.resolved ? "[Resolved]" : "[Open]";
5165
+ lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
5166
+ cm.deleteComment(cId);
5167
+ }
5168
+ for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
5169
+ for (const el of findAllDescendants(doc.element, tag)) {
5170
+ el.parentNode?.removeChild(el);
4722
5171
  }
4723
- const short_text = text.length > 60 ? text.substring(0, 60) + "..." : text;
4724
- const nodes = findAllDescendants(item._element, "*");
4725
- for (const node of nodes) {
4726
- if (node.tagName === "w:bookmarkStart") {
4727
- const b_name = node.getAttribute("w:name");
4728
- if (b_name && (!b_name.startsWith("_") || b_name.startsWith("_Ref"))) {
4729
- if (!raw_anchors[b_name]) {
4730
- raw_anchors[b_name] = { anchored_to: short_text, referenced_from: [] };
4731
- }
5172
+ }
5173
+ const refs = findAllDescendants(doc.element, "w:commentReference");
5174
+ for (const ref of refs) {
5175
+ const parent = ref.parentNode;
5176
+ if (parent) {
5177
+ if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
5178
+ const nonRprChildren = Array.from(parent.childNodes).filter(
5179
+ (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
5180
+ );
5181
+ if (nonRprChildren.length <= 1) {
5182
+ parent.parentNode?.removeChild(parent);
5183
+ } else {
5184
+ parent.removeChild(ref);
4732
5185
  }
5186
+ } else {
5187
+ parent.removeChild(ref);
4733
5188
  }
4734
- let target = null;
4735
- if (node.tagName === "w:fldSimple") {
4736
- const instr = node.getAttribute("w:instr") || "";
4737
- const parts = instr.trim().split(/\s+/);
4738
- if (parts.length > 1 && parts[0] === "REF") target = parts[1];
4739
- } else if (node.tagName === "w:instrText") {
4740
- const instr = node.textContent || "";
4741
- const parts = instr.trim().split(/\s+/);
4742
- if (parts.length > 1 && parts[0] === "REF") target = parts[1];
4743
- }
4744
- if (target) raw_references.push([target, short_text]);
4745
5189
  }
4746
5190
  }
4747
- for (const [target, ref_text] of raw_references) {
4748
- if (raw_anchors[target]) {
4749
- raw_anchors[target].referenced_from.push(ref_text);
5191
+ const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
5192
+ const openCount = Object.values(data).filter((c) => !c.resolved).length;
5193
+ return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
5194
+ }
5195
+ function eject_comment_parts(doc) {
5196
+ const pkg = doc.pkg;
5197
+ const comment_partnames = /* @__PURE__ */ new Set();
5198
+ for (const part of pkg.parts) {
5199
+ if (part.partname.toLowerCase().includes("comments")) {
5200
+ comment_partnames.add(part.partname);
5201
+ const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
5202
+ const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
5203
+ comment_partnames.add(withSlash);
5204
+ comment_partnames.add(withoutSlash);
4750
5205
  }
4751
5206
  }
4752
- const diagnostics = [];
4753
- const def_keys = Object.keys(definitions);
4754
- if (def_keys.length > 0) {
4755
- const sorted_terms = def_keys.sort((a, b) => b.length - a.length);
4756
- const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4757
- const alt = sorted_terms.map(escapeRegExp).join("|");
4758
- const usage_pattern = new RegExp(`(?<!["\u201C])\\b(${alt})\\b(?![\u201D"])`, "g");
4759
- for (const m of base_text.matchAll(usage_pattern)) {
4760
- const matched_term = m[1];
4761
- if (definitions[matched_term]) definitions[matched_term].count++;
4762
- }
4763
- for (const term of def_keys) {
4764
- if (definitions[term].count === 0) {
4765
- delete definitions[term];
4766
- duplicates.delete(term);
5207
+ if (comment_partnames.size === 0) return;
5208
+ for (const part of pkg.parts) {
5209
+ if (part.partname.endsWith(".rels")) {
5210
+ const rels = findAllDescendants(part._element, "Relationship");
5211
+ const toRemove = [];
5212
+ for (const rel of rels) {
5213
+ const target = rel.getAttribute("Target") || "";
5214
+ if (target.toLowerCase().includes("comments")) {
5215
+ toRemove.push(rel);
5216
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
5217
+ const sourcePart = pkg.getPartByPath(sourcePath);
5218
+ if (sourcePart) {
5219
+ const relId = rel.getAttribute("Id");
5220
+ if (relId) sourcePart.rels.delete(relId);
5221
+ }
5222
+ }
5223
+ }
5224
+ for (const relEl of toRemove) {
5225
+ relEl.parentNode?.removeChild(relEl);
4767
5226
  }
4768
5227
  }
4769
5228
  }
4770
- for (const term of duplicates) {
4771
- diagnostics.push(`[Error] Duplicate Definition: '${term}' is defined multiple times.`);
4772
- }
4773
- const stop_words = /* @__PURE__ */ new Set([
4774
- "The",
4775
- "This",
4776
- "That",
4777
- "Such",
4778
- "A",
4779
- "An",
4780
- "Any",
4781
- "All",
4782
- "Some",
4783
- "No",
4784
- "Every",
4785
- "Each",
4786
- "As",
4787
- "In",
4788
- "Of",
4789
- "For",
4790
- "To",
4791
- "On",
4792
- "By",
4793
- "With"
4794
- ]);
4795
- const all_cap_pattern = /\b[A-Z][a-zA-Z]*(?:\s+[A-Z][a-zA-Z]*)*\b/g;
4796
- const all_caps = new Set(base_text.match(all_cap_pattern) || []);
4797
- const valid_terms = new Set(Object.keys(definitions));
4798
- const terms_by_first_letter = {};
4799
- for (const term of valid_terms) {
4800
- const fl = term[0].toLowerCase();
4801
- if (!terms_by_first_letter[fl]) terms_by_first_letter[fl] = [];
4802
- terms_by_first_letter[fl].push(term);
4803
- }
4804
- const candidates_by_term = {};
4805
- for (const raw_candidate of all_caps) {
4806
- let candidate = raw_candidate.trim();
4807
- const words = candidate.split(/\s+/);
4808
- while (words.length > 0) {
4809
- const first = words[0];
4810
- const title = first.charAt(0).toUpperCase() + first.slice(1).toLowerCase();
4811
- if (stop_words.has(title)) words.shift();
4812
- else break;
4813
- }
4814
- candidate = words.join(" ");
4815
- if (candidate.length < 4) continue;
4816
- if (valid_terms.has(candidate)) continue;
4817
- const first_letter = candidate[0].toLowerCase();
4818
- let candidate_terms = terms_by_first_letter[first_letter] || [];
4819
- if (candidate.length > 5) {
4820
- for (const [k, v] of Object.entries(terms_by_first_letter)) {
4821
- if (k !== first_letter) candidate_terms = candidate_terms.concat(v);
5229
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
5230
+ if (ctPart) {
5231
+ const overrides = findAllDescendants(ctPart._element, "Override");
5232
+ const toRemove = [];
5233
+ for (const override of overrides) {
5234
+ const partName = override.getAttribute("PartName") || "";
5235
+ if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
5236
+ toRemove.push(override);
4822
5237
  }
4823
5238
  }
4824
- for (const term of candidate_terms) {
4825
- if (Math.abs(candidate.length - term.length) > 2) continue;
4826
- if (candidate === term + "s" || candidate === term + "es") continue;
4827
- if (term === candidate + "s" || term === candidate + "es") continue;
4828
- const dist = boundedLevenshtein(candidate, term, 2);
4829
- if (dist === 0 || dist > 2) continue;
4830
- if (term.length <= 5) {
4831
- if (dist > 1) continue;
4832
- if (candidate[0].toLowerCase() !== term[0].toLowerCase()) continue;
4833
- }
4834
- if (!candidates_by_term[term]) candidates_by_term[term] = [];
4835
- if (!candidates_by_term[term].includes(candidate)) candidates_by_term[term].push(candidate);
5239
+ for (const overrideEl of toRemove) {
5240
+ overrideEl.parentNode?.removeChild(overrideEl);
4836
5241
  }
4837
5242
  }
4838
- for (const [term, candidates] of Object.entries(candidates_by_term)) {
4839
- candidates.sort();
4840
- const c_str = candidates.map((c) => `'${c}'`).join(", ");
4841
- diagnostics.push(`[Info] Possible Typos for '${term}': Found ${c_str}`);
4842
- }
4843
- function diag_sort_key(msg) {
4844
- if (msg.startsWith("[Error]")) return 0;
4845
- if (msg.startsWith("[Warning]")) return 1;
4846
- return 2;
5243
+ pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
5244
+ for (const key of Object.keys(pkg.unzipped)) {
5245
+ if (key.toLowerCase().includes("comments")) {
5246
+ delete pkg.unzipped[key];
5247
+ }
4847
5248
  }
4848
- diagnostics.sort((a, b) => {
4849
- const keyA = diag_sort_key(a);
4850
- const keyB = diag_sort_key(b);
4851
- if (keyA !== keyB) return keyA - keyB;
4852
- return a.localeCompare(b);
4853
- });
4854
- return [definitions, diagnostics, raw_anchors];
4855
5249
  }
4856
- function build_structural_appendix(doc, base_text) {
4857
- const [defs, diagnostics, anchors] = extract_all_domain_metadata(doc, base_text);
4858
- const lines = [
4859
- "\n\n---",
4860
- "",
4861
- "<!-- READONLY_BOUNDARY_START -->",
4862
- "# Document Structure (Read-Only)",
4863
- "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."
4864
- ];
4865
- let has_content = false;
4866
- if (Object.keys(defs).length > 0) {
4867
- has_content = true;
4868
- lines.push("\n## Defined Terms");
4869
- for (const [term, data] of Object.entries(defs)) {
4870
- lines.push(`- "${term}" \u2014 used ${data.count} times.`);
5250
+ function replace_comment_authors(doc, newAuthor) {
5251
+ const cm = new CommentsManager(doc);
5252
+ if (!cm.commentsPart) return [];
5253
+ const original = /* @__PURE__ */ new Set();
5254
+ const comments = findAllDescendants(cm.commentsPart._element, "w:comment");
5255
+ for (const c of comments) {
5256
+ const author = c.getAttribute("w:author");
5257
+ if (author) {
5258
+ original.add(author);
5259
+ c.setAttribute("w:author", newAuthor);
4871
5260
  }
4872
- }
4873
- if (diagnostics.length > 0) {
4874
- has_content = true;
4875
- lines.push("\n## Semantic Diagnostics");
4876
- for (const diag of diagnostics) {
4877
- lines.push(`- ${diag}`);
5261
+ if (c.hasAttribute("w:initials")) {
5262
+ const initials = newAuthor.split(" ").filter(Boolean).map((p) => p[0]).join("").toUpperCase();
5263
+ c.setAttribute("w:initials", initials);
4878
5264
  }
4879
5265
  }
4880
- if (Object.keys(anchors).length > 0) {
4881
- has_content = true;
4882
- lines.push("\n## Named Anchors");
4883
- for (const [b_name, data] of Object.entries(anchors)) {
4884
- lines.push(`- ${b_name} \u2192 Anchored to: "${data.anchored_to}"`);
4885
- for (const ref of data.referenced_from) {
4886
- lines.push(` - Referenced from: "${ref}"`);
5266
+ return original.size ? [`Comment authors replaced: ${Array.from(original).sort().join(", ")} \u2192 "${newAuthor}"`] : [];
5267
+ }
5268
+ function replace_change_authors(doc, newAuthor) {
5269
+ const original = /* @__PURE__ */ new Set();
5270
+ for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange"]) {
5271
+ for (const el of findAllDescendants(doc.element, tag)) {
5272
+ const author = el.getAttribute("w:author");
5273
+ if (author) {
5274
+ original.add(author);
5275
+ el.setAttribute("w:author", newAuthor);
4887
5276
  }
4888
5277
  }
4889
5278
  }
4890
- if (has_content) {
4891
- return lines.join("\n");
4892
- }
4893
- return "";
4894
- }
4895
-
4896
- // src/ingest.ts
4897
- async function extractTextFromBuffer(buffer, cleanView = false) {
4898
- const doc = await DocumentObject.load(buffer);
4899
- return _extractTextFromDoc(doc, cleanView);
5279
+ return original.size ? [`Track change authors replaced: ${Array.from(original).sort().join(", ")} \u2192 "${newAuthor}"`] : [];
4900
5280
  }
4901
- function _extractTextFromDoc(doc, cleanView = false, includeAppendix = true, return_paragraph_offsets = false) {
4902
- const comments_map = extract_comments_data(doc.pkg);
4903
- const full_text = [];
4904
- const paragraph_offsets = /* @__PURE__ */ new Map();
4905
- let cursor = 0;
4906
- for (const part of iter_document_parts(doc)) {
4907
- const part_cursor = full_text.length > 0 ? cursor + 2 : cursor;
4908
- const part_text = _extract_blocks(part, comments_map, cleanView, part_cursor, return_paragraph_offsets ? paragraph_offsets : void 0);
4909
- if (part_text) {
4910
- if (full_text.length > 0) cursor += 2;
4911
- full_text.push(part_text);
4912
- cursor += part_text.length;
5281
+ function normalize_change_dates(doc) {
5282
+ let count = 0;
5283
+ const fixed = "2025-01-01T00:00:00Z";
5284
+ for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange"]) {
5285
+ for (const el of findAllDescendants(doc.element, tag)) {
5286
+ if (el.hasAttribute("w:date")) {
5287
+ el.setAttribute("w:date", fixed);
5288
+ count++;
5289
+ }
4913
5290
  }
4914
5291
  }
4915
- let base_text = full_text.join("\n\n");
4916
- if (includeAppendix) {
4917
- const appendix = build_structural_appendix(doc, base_text);
4918
- if (appendix) base_text += appendix;
4919
- }
4920
- if (return_paragraph_offsets) {
4921
- return { text: base_text, paragraph_offsets };
4922
- }
4923
- return base_text;
5292
+ return count ? [`Track change timestamps: ${count} normalized`] : [];
4924
5293
  }
4925
- function _extract_blocks(container, comments_map, cleanView, cursor, paragraph_offsets) {
4926
- const part = container.part || container;
4927
- const [style_cache, default_pstyle] = _get_style_cache(part);
4928
- const blocks = [];
4929
- let local_cursor = cursor;
4930
- let is_first_block = true;
4931
- let is_first_para = true;
4932
- if (container.constructor && container.constructor.name === "NotesPart") {
4933
- const header = container.note_type === "fn" ? "## Footnotes" : "## Endnotes";
4934
- const sep = `---
4935
- ${header}`;
4936
- blocks.push(sep);
4937
- local_cursor += sep.length;
4938
- is_first_block = false;
4939
- }
4940
- for (const item of iter_block_items(container)) {
4941
- if (!is_first_block) local_cursor += 2;
4942
- const block_start = local_cursor;
4943
- if (item.constructor.name === "FootnoteItem") {
4944
- const fn_text = _extract_blocks(item, comments_map, cleanView, block_start, paragraph_offsets);
4945
- if (fn_text) {
4946
- blocks.push(fn_text);
4947
- local_cursor = block_start + fn_text.length;
4948
- is_first_block = false;
4949
- } else if (!is_first_block) {
4950
- local_cursor -= 2;
4951
- }
4952
- } else if (item instanceof Paragraph) {
4953
- let prefix = get_paragraph_prefix(item, style_cache, default_pstyle);
4954
- if (is_first_para && container.constructor.name === "FootnoteItem") {
4955
- prefix = `[^${container.note_type}-${container.id}]: ` + prefix;
5294
+ function scrub_doc_properties(doc) {
5295
+ const lines = [];
5296
+ const corePart = doc.pkg.getPartByPath("docProps/core.xml");
5297
+ if (corePart) {
5298
+ const creators = findDescendantsByLocalName(corePart._element, "creator");
5299
+ creators.forEach((c) => {
5300
+ if (c.textContent) {
5301
+ lines.push(`Author: ${c.textContent}`);
5302
+ c.textContent = "";
4956
5303
  }
4957
- const p_text = build_paragraph_text(item, comments_map, cleanView, style_cache, default_pstyle);
4958
- const full_block = prefix + p_text;
4959
- blocks.push(full_block);
4960
- if (paragraph_offsets) {
4961
- paragraph_offsets.set(item._element, [block_start, full_block.length]);
5304
+ });
5305
+ const modifiers = findDescendantsByLocalName(corePart._element, "lastModifiedBy");
5306
+ modifiers.forEach((c) => {
5307
+ if (c.textContent) {
5308
+ lines.push(`Last modified by: ${c.textContent}`);
5309
+ c.textContent = "";
4962
5310
  }
4963
- local_cursor = block_start + full_block.length;
4964
- is_first_para = false;
4965
- is_first_block = false;
4966
- } else if (item instanceof Table) {
4967
- const table_text = extract_table(item, comments_map, cleanView, block_start, paragraph_offsets);
4968
- if (table_text) {
4969
- blocks.push(table_text);
4970
- local_cursor = block_start + table_text.length;
4971
- is_first_block = false;
4972
- } else if (!is_first_block) {
4973
- local_cursor -= 2;
5311
+ });
5312
+ const revisions = findDescendantsByLocalName(corePart._element, "revision");
5313
+ revisions.forEach((c) => {
5314
+ if (c.textContent && parseInt(c.textContent) > 1) {
5315
+ lines.push(`Revision count: ${c.textContent} \u2192 1`);
5316
+ c.textContent = "1";
4974
5317
  }
4975
- is_first_para = false;
5318
+ });
5319
+ }
5320
+ const appPart = doc.pkg.getPartByPath("docProps/app.xml");
5321
+ if (appPart) {
5322
+ const docEl = appPart._element;
5323
+ const intFields = ["TotalTime", "Words", "Characters", "Paragraphs", "Lines", "CharactersWithSpaces"];
5324
+ for (const f of intFields) {
5325
+ findDescendantsByLocalName(docEl, f).forEach((el) => {
5326
+ if (el.textContent && el.textContent !== "0") {
5327
+ if (f === "TotalTime") lines.push(`Total editing time: ${el.textContent} minutes`);
5328
+ el.textContent = "0";
5329
+ }
5330
+ });
5331
+ }
5332
+ const strFields = ["Template", "Manager", "Company"];
5333
+ for (const f of strFields) {
5334
+ findDescendantsByLocalName(docEl, f).forEach((el) => {
5335
+ if (el.textContent) {
5336
+ lines.push(`${f}: ${el.textContent}`);
5337
+ el.textContent = "";
5338
+ }
5339
+ });
4976
5340
  }
4977
5341
  }
4978
- return blocks.join("\n\n");
5342
+ return lines.length ? ["Metadata scrubbed:", ...lines.map((l) => ` ${l}`)] : [];
4979
5343
  }
4980
- function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets) {
4981
- const rows_text = [];
4982
- let rows_processed = 0;
4983
- let local_cursor = cursor;
4984
- for (const row of table.rows) {
4985
- const cell_texts = [];
4986
- const seen_cells = /* @__PURE__ */ new Set();
4987
- const trPr = findChild(row._element, "w:trPr");
4988
- const ins = trPr ? findChild(trPr, "w:ins") : null;
4989
- const del_node = trPr ? findChild(trPr, "w:del") : null;
4990
- if (cleanView && del_node) continue;
4991
- const row_start = local_cursor + (rows_processed > 0 ? 1 : 0);
4992
- const wrapper_prefix_len = !cleanView && ins ? 4 : !cleanView && del_node ? 4 : 0;
4993
- let cell_cursor = row_start + wrapper_prefix_len;
4994
- let first_cell = true;
4995
- for (const cell of row.cells) {
4996
- if (seen_cells.has(cell)) continue;
4997
- seen_cells.add(cell);
4998
- if (!first_cell) cell_cursor += 3;
4999
- const cell_content = _extract_blocks(cell, comments_map, cleanView, cell_cursor, paragraph_offsets);
5000
- cell_texts.push(cell_content);
5001
- cell_cursor += cell_content.length;
5002
- first_cell = false;
5344
+ function scrub_timestamps(doc) {
5345
+ let modified = false;
5346
+ const epoch = "1970-01-01T00:00:00Z";
5347
+ const corePart = doc.pkg.getPartByPath("docProps/core.xml");
5348
+ if (corePart) {
5349
+ for (const tag of ["created", "modified", "lastPrinted"]) {
5350
+ findDescendantsByLocalName(corePart._element, tag).forEach((el) => {
5351
+ if (el.textContent && el.textContent !== epoch) {
5352
+ el.textContent = epoch;
5353
+ modified = true;
5354
+ }
5355
+ });
5003
5356
  }
5004
- let row_str = cell_texts.join(" | ");
5005
- if (!cleanView) {
5006
- if (ins) row_str = `{++ ${row_str} |Chg:${ins.getAttribute("w:id")}++}`;
5007
- else if (del_node) row_str = `{-- ${row_str} |Chg:${del_node.getAttribute("w:id")}--}`;
5357
+ }
5358
+ return modified ? ["Timestamps normalized to epoch"] : [];
5359
+ }
5360
+ function strip_custom_xml(doc) {
5361
+ const customParts = doc.pkg.parts.filter((p) => p.partname.includes("/customXml"));
5362
+ if (customParts.length === 0) return [];
5363
+ const partnames = new Set(customParts.map((p) => p.partname));
5364
+ doc.pkg.parts = doc.pkg.parts.filter((p) => !partnames.has(p.partname));
5365
+ const removeRelationsTo = (relsPart) => {
5366
+ const toRemove = [];
5367
+ for (const rel of findAllDescendants(relsPart._element, "Relationship")) {
5368
+ const target = rel.getAttribute("Target");
5369
+ if (target && target.includes("customXml")) toRemove.push(rel);
5008
5370
  }
5009
- rows_text.push(row_str);
5010
- local_cursor = row_start + row_str.length;
5011
- rows_processed++;
5371
+ toRemove.forEach((r) => r.parentNode?.removeChild(r));
5372
+ };
5373
+ const rootRels = doc.pkg.getPartByPath("_rels/.rels");
5374
+ if (rootRels) removeRelationsTo(rootRels);
5375
+ const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
5376
+ if (docRels) removeRelationsTo(docRels);
5377
+ for (const sdtPr of findAllDescendants(doc.element, "w:sdtPr")) {
5378
+ findChildren(sdtPr, "w:dataBinding").forEach((b) => sdtPr.removeChild(b));
5012
5379
  }
5013
- return rows_text.join("\n");
5380
+ return [`Custom XML parts: ${customParts.length} removed`];
5014
5381
  }
5015
- function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, default_pstyle) {
5016
- const parts = [];
5017
- const active_ins = {};
5018
- const active_del = {};
5019
- const active_comments = /* @__PURE__ */ new Set();
5020
- const active_fmt = {};
5021
- const deferred_meta_states = [];
5022
- let pending_text = "";
5023
- let current_wrappers = ["", ""];
5024
- let current_style = ["", ""];
5025
- const items = Array.from(iter_paragraph_content(paragraph));
5026
- const is_heading = is_heading_paragraph(paragraph, style_cache, default_pstyle);
5027
- const native_heading = is_native_heading(paragraph, style_cache, default_pstyle);
5028
- let leading_strip_active = is_heading;
5029
- for (let i = 0; i < items.length; i++) {
5030
- const item = items[i];
5031
- if (item instanceof Run) {
5032
- const [prefix, suffix] = get_run_style_markers(item, native_heading);
5033
- const text = get_run_text(item);
5034
- if (cleanView && Object.keys(active_del).length > 0) continue;
5035
- if (leading_strip_active) {
5036
- if (!text || !text.trim()) continue;
5037
- leading_strip_active = false;
5038
- }
5039
- const seg = apply_formatting_to_segments(text, prefix, suffix);
5040
- if (seg) {
5041
- const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
5042
- const new_style = [prefix, suffix];
5043
- if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
5044
- 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])) {
5045
- pending_text = pending_text.slice(0, -current_style[1].length) + seg.slice(new_style[0].length);
5046
- } else {
5047
- pending_text += seg;
5048
- }
5049
- current_style = new_style;
5050
- } else {
5051
- if (pending_text) parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5052
- pending_text = seg;
5053
- current_wrappers = new_wrappers;
5054
- current_style = new_style;
5055
- }
5056
- if (!cleanView) {
5057
- 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;
5058
- if (has_meta) {
5059
- deferred_meta_states.push([{ ...active_ins }, { ...active_del }, new Set(active_comments), { ...active_fmt }]);
5060
- }
5061
- let should_defer = false;
5062
- const is_redline = Object.keys(active_ins).length > 0 || Object.keys(active_del).length > 0 || Object.keys(active_fmt).length > 0;
5063
- if (is_redline) {
5064
- let j = i + 1;
5065
- let next_is_redline = false;
5066
- let temp_ins = Object.keys(active_ins).length;
5067
- let temp_del = Object.keys(active_del).length;
5068
- let temp_fmt = Object.keys(active_fmt).length;
5069
- while (j < items.length) {
5070
- const next_item = items[j];
5071
- if (next_item instanceof Run) {
5072
- if (!get_run_text(next_item)) {
5073
- j++;
5074
- continue;
5075
- }
5076
- if (temp_ins > 0 || temp_del > 0 || temp_fmt > 0) next_is_redline = true;
5077
- break;
5078
- } else {
5079
- const ev = next_item;
5080
- if (ev.type === "ins_start") temp_ins++;
5081
- else if (ev.type === "ins_end") temp_ins = Math.max(0, temp_ins - 1);
5082
- else if (ev.type === "del_start") temp_del++;
5083
- else if (ev.type === "del_end") temp_del = Math.max(0, temp_del - 1);
5084
- else if (ev.type === "fmt_start") temp_fmt++;
5085
- else if (ev.type === "fmt_end") temp_fmt = Math.max(0, temp_fmt - 1);
5086
- }
5087
- j++;
5088
- }
5089
- if (next_is_redline) should_defer = true;
5090
- }
5091
- if (!should_defer && deferred_meta_states.length > 0) {
5092
- const meta_block = _build_merged_meta_block(deferred_meta_states, comments_map);
5093
- if (meta_block) {
5094
- if (pending_text) {
5095
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5096
- pending_text = "";
5097
- current_wrappers = ["", ""];
5098
- current_style = ["", ""];
5099
- }
5100
- parts.push(`{>>${meta_block}<<}`);
5101
- }
5102
- deferred_meta_states.length = 0;
5103
- }
5104
- }
5105
- }
5106
- } else {
5107
- const ev = item;
5108
- leading_strip_active = false;
5109
- if (!["ins_start", "ins_end", "del_start", "del_end", "fmt_start", "fmt_end"].includes(ev.type)) {
5110
- if (pending_text) {
5111
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5112
- pending_text = "";
5113
- current_wrappers = ["", ""];
5114
- current_style = ["", ""];
5115
- }
5116
- }
5117
- if (ev.type === "start") active_comments.add(ev.id);
5118
- else if (ev.type === "end") active_comments.delete(ev.id);
5119
- else if (ev.type === "ins_start") active_ins[ev.id] = ev;
5120
- else if (ev.type === "ins_end") delete active_ins[ev.id];
5121
- else if (ev.type === "del_start") active_del[ev.id] = ev;
5122
- else if (ev.type === "del_end") delete active_del[ev.id];
5123
- else if (ev.type === "fmt_start") active_fmt[ev.id] = ev;
5124
- else if (ev.type === "fmt_end") delete active_fmt[ev.id];
5125
- else if (ev.type === "footnote" || ev.type === "endnote") {
5126
- if (pending_text) {
5127
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5128
- pending_text = "";
5129
- current_wrappers = ["", ""];
5130
- current_style = ["", ""];
5131
- }
5132
- parts.push(`[^${ev.type === "footnote" ? "fn" : "en"}-${ev.id}]`);
5133
- } else if (ev.type === "hyperlink_start") {
5134
- if (pending_text) {
5135
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5136
- pending_text = "";
5137
- current_wrappers = ["", ""];
5138
- current_style = ["", ""];
5139
- }
5140
- parts.push("[");
5141
- } else if (ev.type === "hyperlink_end") {
5142
- if (pending_text) {
5143
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5144
- pending_text = "";
5145
- current_wrappers = ["", ""];
5146
- current_style = ["", ""];
5147
- }
5148
- parts.push(`](${ev.date})`);
5149
- } else if (ev.type === "xref_start") {
5150
- if (pending_text) {
5151
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5152
- pending_text = "";
5153
- current_wrappers = ["", ""];
5154
- current_style = ["", ""];
5155
- }
5156
- parts.push("[~");
5157
- } else if (ev.type === "xref_end") {
5158
- if (pending_text) {
5159
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5160
- pending_text = "";
5161
- current_wrappers = ["", ""];
5162
- current_style = ["", ""];
5163
- }
5164
- parts.push(`~](#${ev.id})`);
5165
- } else if (ev.type === "bookmark") {
5166
- if (pending_text) {
5167
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5168
- pending_text = "";
5169
- current_wrappers = ["", ""];
5170
- current_style = ["", ""];
5171
- }
5172
- parts.push(`{#${ev.id}}`);
5382
+ function strip_image_alt_text(doc) {
5383
+ let count = 0;
5384
+ for (const docPr of findDescendantsByLocalName(doc.element, "docPr")) {
5385
+ const descr = docPr.getAttribute("descr");
5386
+ if (descr) {
5387
+ const isShort = descr.length < 10;
5388
+ const isFile = descr.includes(".") && descr.length < 60;
5389
+ if (isShort || isFile) {
5390
+ docPr.removeAttribute("descr");
5391
+ count++;
5173
5392
  }
5174
5393
  }
5175
5394
  }
5176
- if (pending_text) parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5177
- if (deferred_meta_states.length > 0) {
5178
- const meta_block = _build_merged_meta_block(deferred_meta_states, comments_map);
5179
- if (meta_block) parts.push(`{>>${meta_block}<<}`);
5180
- }
5181
- return parts.join("");
5395
+ return count ? [`Image alt text: ${count} auto-generated descriptions removed`] : [];
5182
5396
  }
5183
- function _get_wrappers(ins, del, comments, fmt) {
5184
- if (Object.keys(del).length > 0) return ["{--", "--}"];
5185
- if (Object.keys(ins).length > 0) return ["{++", "++}"];
5186
- if (comments.size > 0 || Object.keys(fmt).length > 0) return ["{==", "==}"];
5187
- return ["", ""];
5188
- }
5189
- function _build_merged_meta_block(states_list, comments_map) {
5190
- const change_lines = [];
5191
- const comment_lines = [];
5192
- const seen_sigs = /* @__PURE__ */ new Set();
5193
- for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
5194
- let render_comment2 = function(cid) {
5195
- if (!comments_map[cid]) return;
5196
- const sig = `Com:${cid}`;
5197
- if (seen_sigs.has(sig)) return;
5198
- const data = comments_map[cid];
5199
- let header = `[${sig}] ${data.author}`;
5200
- if (data.date) header += ` @ ${data.date}`;
5201
- if (data.resolved) header += `(RESOLVED)`;
5202
- comment_lines.push(`${header}: ${data.text}`);
5203
- seen_sigs.add(sig);
5204
- if (children_map[cid]) {
5205
- const children = children_map[cid].sort((a, b) => (comments_map[a]?.date || "").localeCompare(comments_map[b]?.date || ""));
5206
- for (const child_id of children) {
5207
- render_comment2(child_id);
5397
+ function audit_hyperlinks(doc) {
5398
+ const internal = ["sharepoint.com", "onedrive.com", ".internal", "intranet", "localhost", "10.", "192.168.", "172.16."];
5399
+ const warnings = [];
5400
+ const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
5401
+ for (const rel of findAllDescendants(docRels._element, "Relationship")) {
5402
+ if (rel.getAttribute("TargetMode") === "External") {
5403
+ const url = rel.getAttribute("Target") || "";
5404
+ for (const pattern of internal) {
5405
+ if (url.toLowerCase().includes(pattern.toLowerCase())) {
5406
+ warnings.push(`Hyperlink targets internal URL: ${_truncate(url, 80)}`);
5407
+ break;
5208
5408
  }
5209
5409
  }
5210
- };
5211
- var render_comment = render_comment2;
5212
- for (const [uid, meta] of Object.entries(ins_map)) {
5213
- const sig = `Chg:${uid}`;
5214
- if (!seen_sigs.has(sig)) {
5215
- change_lines.push(`[${sig} insert] ${meta.author || "Unknown"}`);
5216
- seen_sigs.add(sig);
5217
- }
5218
- }
5219
- for (const [uid, meta] of Object.entries(del_map)) {
5220
- const sig = `Chg:${uid}`;
5221
- if (!seen_sigs.has(sig)) {
5222
- change_lines.push(`[${sig} delete] ${meta.author || "Unknown"}`);
5223
- seen_sigs.add(sig);
5224
- }
5225
- }
5226
- for (const [uid, meta] of Object.entries(fmt_map)) {
5227
- const sig = `Chg:${uid}`;
5228
- if (!seen_sigs.has(sig)) {
5229
- change_lines.push(`[${sig} format] ${meta.author || "Unknown"}`);
5230
- seen_sigs.add(sig);
5231
- }
5232
- }
5233
- const children_map = {};
5234
- for (const [c_id, data] of Object.entries(comments_map)) {
5235
- const p_id = data.parent_id;
5236
- if (p_id) {
5237
- if (!children_map[p_id]) children_map[p_id] = [];
5238
- children_map[p_id].push(c_id);
5239
- }
5240
- }
5241
- const sorted_ids = Array.from(comments_set).sort();
5242
- for (const c_id of sorted_ids) {
5243
- render_comment2(c_id);
5244
5410
  }
5245
5411
  }
5246
- return [...change_lines, ...comment_lines].join("\n");
5412
+ return warnings;
5247
5413
  }
5248
5414
 
5249
- // src/outline.ts
5250
- var _HEADING_PREFIX_RE = /^(#{1,6}) /;
5251
- var _HEURISTIC_MIN_WORDS = 3;
5252
- function extract_outline(doc, projected_body, body_pages, body_page_offsets, paragraph_offsets = null) {
5253
- if (body_pages.length !== body_page_offsets.length) {
5254
- throw new Error("body_pages and body_page_offsets length mismatch");
5255
- }
5256
- if (paragraph_offsets) {
5257
- return _extract_outline_fast(doc, projected_body, body_page_offsets, paragraph_offsets);
5258
- }
5259
- const comments_map = extract_comments_data(doc.pkg);
5260
- const block_records = _walk_doc_body(doc, comments_map);
5261
- const heading_indices = [];
5262
- for (let idx = 0; idx < block_records.length; idx++) {
5263
- const rec = block_records[idx];
5264
- if (!(rec.is_paragraph && _is_heading(rec.item))) continue;
5265
- if (!_heading_passes_quality_filter(rec.item, comments_map)) continue;
5266
- heading_indices.push(idx);
5415
+ // src/domain.ts
5416
+ function boundedLevenshtein(a, b, maxDist = 2) {
5417
+ if (a === b) return 0;
5418
+ if (Math.abs(a.length - b.length) > maxDist) return maxDist + 1;
5419
+ if (a.length === 0) return b.length <= maxDist ? b.length : maxDist + 1;
5420
+ if (b.length === 0) return a.length <= maxDist ? a.length : maxDist + 1;
5421
+ if (a.length > b.length) {
5422
+ const temp = a;
5423
+ a = b;
5424
+ b = temp;
5267
5425
  }
5268
- if (heading_indices.length === 0) return [];
5269
- const nodes = [];
5270
- for (let h_pos = 0; h_pos < heading_indices.length; h_pos++) {
5271
- const rec_idx = heading_indices[h_pos];
5272
- const rec = block_records[rec_idx];
5273
- const paragraph = rec.item;
5274
- const level = _heading_level(paragraph);
5275
- const text = _heading_text(paragraph, comments_map);
5276
- const style = _determine_heading_style(paragraph);
5277
- const owned_end = _find_owned_end(
5278
- block_records,
5279
- heading_indices,
5280
- h_pos,
5281
- level
5282
- );
5283
- const owned_blocks = block_records.slice(rec_idx + 1, owned_end);
5284
- const has_table = _direct_has_table(block_records, rec_idx + 1, owned_end);
5285
- const footnote_ids = _collect_footnote_ids(owned_blocks);
5286
- const page_num = _offset_to_page(rec.start_offset, body_page_offsets);
5287
- nodes.push({ level, text, page: page_num, style, has_table, footnote_ids });
5426
+ let row = Array.from({ length: a.length + 1 }, (_, i) => i);
5427
+ for (let i = 1; i <= b.length; i++) {
5428
+ const newRow = [i];
5429
+ let minInRow = i;
5430
+ for (let j = 1; j <= a.length; j++) {
5431
+ const cost = a[j - 1] === b[i - 1] ? 0 : 1;
5432
+ const val = Math.min(row[j] + 1, newRow[j - 1] + 1, row[j - 1] + cost);
5433
+ newRow.push(val);
5434
+ if (val < minInRow) minInRow = val;
5435
+ }
5436
+ if (minInRow > maxDist) return maxDist + 1;
5437
+ row = newRow;
5288
5438
  }
5289
- return nodes;
5439
+ return row[a.length] <= maxDist ? row[a.length] : maxDist + 1;
5290
5440
  }
5291
- function _direct_has_table(block_records, range_start, range_end) {
5292
- for (let idx = range_start; idx < range_end; idx++) {
5293
- const rec = block_records[idx];
5294
- if (rec.is_paragraph && _is_heading(rec.item)) return false;
5295
- if (rec.is_table) return true;
5441
+ function _get_paragraph_text(p) {
5442
+ let text = "";
5443
+ const runs = findAllDescendants(p._element, "w:r");
5444
+ for (const r of runs) {
5445
+ text += get_run_text(new Run(r, p));
5296
5446
  }
5297
- return false;
5447
+ return text;
5298
5448
  }
5299
- function _walk_doc_body(doc, comments_map) {
5300
- const parts = Array.from(iter_document_parts(doc));
5301
- let body_start_offset = 0;
5302
- let body_part = null;
5303
- for (const part of parts) {
5304
- if (part === doc) {
5305
- body_part = part;
5306
- break;
5449
+ function extract_all_domain_metadata(doc, base_text) {
5450
+ const definitions = {};
5451
+ const duplicates = /* @__PURE__ */ new Set();
5452
+ const raw_anchors = {};
5453
+ const raw_references = [];
5454
+ const leading_re = /^(?:[\d.\-()a-zA-Z]+\s*)?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”]/;
5455
+ const inline_re = /\([^)]*?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”][^)]*?\)/g;
5456
+ for (const item of iter_block_items(doc)) {
5457
+ if (!(item instanceof Paragraph)) continue;
5458
+ const text = _get_paragraph_text(item).trim();
5459
+ if (!text) continue;
5460
+ const extracted_terms = [];
5461
+ const leading_match = text.match(leading_re);
5462
+ if (leading_match) extracted_terms.push(leading_match[1].trim());
5463
+ const inline_matches = text.matchAll(inline_re);
5464
+ for (const m of inline_matches) {
5465
+ extracted_terms.push(m[1].trim());
5307
5466
  }
5308
- const part_text = _project_part(part, comments_map);
5309
- if (part_text) {
5310
- if (body_start_offset > 0) body_start_offset += 2;
5311
- body_start_offset += part_text.length;
5467
+ for (const term of extracted_terms) {
5468
+ if (definitions[term]) duplicates.add(term);
5469
+ else definitions[term] = { count: 0 };
5470
+ }
5471
+ const short_text = text.length > 60 ? text.substring(0, 60) + "..." : text;
5472
+ const nodes = findAllDescendants(item._element, "*");
5473
+ for (const node of nodes) {
5474
+ if (node.tagName === "w:bookmarkStart") {
5475
+ const b_name = node.getAttribute("w:name");
5476
+ if (b_name && (!b_name.startsWith("_") || b_name.startsWith("_Ref"))) {
5477
+ if (!raw_anchors[b_name]) {
5478
+ raw_anchors[b_name] = {
5479
+ anchored_to: short_text,
5480
+ referenced_from: []
5481
+ };
5482
+ }
5483
+ }
5484
+ }
5485
+ let target = null;
5486
+ if (node.tagName === "w:fldSimple") {
5487
+ const instr = node.getAttribute("w:instr") || "";
5488
+ const parts = instr.trim().split(/\s+/);
5489
+ if (parts.length > 1 && parts[0] === "REF") target = parts[1];
5490
+ } else if (node.tagName === "w:instrText") {
5491
+ const instr = node.textContent || "";
5492
+ const parts = instr.trim().split(/\s+/);
5493
+ if (parts.length > 1 && parts[0] === "REF") target = parts[1];
5494
+ }
5495
+ if (target) raw_references.push([target, short_text]);
5312
5496
  }
5313
5497
  }
5314
- if (!body_part) {
5315
- body_part = doc;
5316
- body_start_offset = 0;
5317
- } else {
5318
- if (body_start_offset > 0) body_start_offset += 2;
5498
+ for (const [target, ref_text] of raw_references) {
5499
+ if (raw_anchors[target]) {
5500
+ raw_anchors[target].referenced_from.push(ref_text);
5501
+ }
5319
5502
  }
5320
- const records = [];
5321
- let cursor = body_start_offset;
5322
- let is_first_block = true;
5323
- for (const item of iter_block_items(body_part)) {
5324
- if (item instanceof Paragraph) {
5325
- const prefix = get_paragraph_prefix(item);
5326
- const p_text = build_paragraph_text(item, comments_map, false);
5327
- const block_len = (prefix + p_text).length;
5328
- if (!is_first_block) cursor += 2;
5329
- records.push({
5330
- item,
5331
- is_paragraph: true,
5332
- is_table: false,
5333
- start_offset: cursor,
5334
- projected_length: block_len
5335
- });
5336
- cursor += block_len;
5337
- is_first_block = false;
5338
- } else if (item instanceof Table) {
5339
- const table_text = extract_table(item, comments_map, false, 0);
5340
- const block_len = table_text ? table_text.length : 0;
5341
- if (!is_first_block) cursor += 2;
5342
- const table_start = cursor;
5343
- records.push({
5344
- item,
5345
- is_paragraph: false,
5346
- is_table: true,
5347
- start_offset: table_start,
5348
- projected_length: block_len
5349
- });
5350
- _record_table_inner_blocks_lite(item, table_start, records, comments_map);
5351
- cursor += block_len;
5352
- is_first_block = false;
5503
+ const diagnostics = [];
5504
+ const def_keys = Object.keys(definitions);
5505
+ if (def_keys.length > 0) {
5506
+ const sorted_terms = def_keys.sort((a, b) => b.length - a.length);
5507
+ const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5508
+ const alt = sorted_terms.map(escapeRegExp).join("|");
5509
+ const usage_pattern = new RegExp(`(?<!["\u201C])\\b(${alt})\\b(?![\u201D"])`, "g");
5510
+ for (const m of base_text.matchAll(usage_pattern)) {
5511
+ const matched_term = m[1];
5512
+ if (definitions[matched_term]) definitions[matched_term].count++;
5513
+ }
5514
+ for (const term of def_keys) {
5515
+ if (definitions[term].count === 0) {
5516
+ delete definitions[term];
5517
+ duplicates.delete(term);
5518
+ }
5353
5519
  }
5354
5520
  }
5355
- return records;
5356
- }
5357
- function _compute_inner_block_offset(table, target_paragraph, table_start_offset, comments_map) {
5358
- const target_el = target_paragraph._element;
5359
- let cursor = table_start_offset;
5360
- let rows_processed = 0;
5361
- for (const row of table.rows) {
5362
- if (rows_processed > 0) cursor += 1;
5363
- const seen_cells = /* @__PURE__ */ new Set();
5364
- let cells_in_row = 0;
5365
- for (const cell of row.cells) {
5366
- if (seen_cells.has(cell)) continue;
5367
- seen_cells.add(cell);
5368
- if (cells_in_row > 0) cursor += 3;
5369
- const [new_cursor, found] = _walk_cell_for_offset(
5370
- cell,
5371
- target_el,
5372
- cursor,
5373
- comments_map
5374
- );
5375
- if (found) return new_cursor;
5376
- cursor = new_cursor;
5377
- cells_in_row++;
5378
- }
5379
- rows_processed++;
5521
+ for (const term of duplicates) {
5522
+ diagnostics.push(
5523
+ `[Error] Duplicate Definition: '${term}' is defined multiple times.`
5524
+ );
5380
5525
  }
5381
- return table_start_offset;
5382
- }
5383
- function _walk_cell_for_offset(cell, target_el, cell_start_cursor, comments_map) {
5384
- let cursor = cell_start_cursor;
5385
- let is_first_block = true;
5386
- for (const inner_item of iter_block_items(cell)) {
5387
- if (!is_first_block) cursor += 2;
5388
- if (inner_item instanceof Paragraph) {
5389
- if (inner_item._element === target_el) return [cursor, true];
5390
- const prefix = get_paragraph_prefix(inner_item);
5391
- const p_text = build_paragraph_text(inner_item, comments_map, false);
5392
- cursor += (prefix + p_text).length;
5393
- } else if (inner_item instanceof Table) {
5394
- const nested_offset = _compute_inner_block_offset(
5395
- inner_item,
5396
- new Paragraph(target_el, null),
5397
- cursor,
5398
- comments_map
5399
- );
5400
- if (nested_offset !== cursor) {
5401
- if (_element_is_descendant(target_el, inner_item._element))
5402
- return [nested_offset, true];
5526
+ const stop_words = /* @__PURE__ */ new Set([
5527
+ "The",
5528
+ "This",
5529
+ "That",
5530
+ "Such",
5531
+ "A",
5532
+ "An",
5533
+ "Any",
5534
+ "All",
5535
+ "Some",
5536
+ "No",
5537
+ "Every",
5538
+ "Each",
5539
+ "As",
5540
+ "In",
5541
+ "Of",
5542
+ "For",
5543
+ "To",
5544
+ "On",
5545
+ "By",
5546
+ "With"
5547
+ ]);
5548
+ const all_cap_pattern = /\b[A-Z][a-zA-Z]*(?:\s+[A-Z][a-zA-Z]*)*\b/g;
5549
+ const all_caps = new Set(base_text.match(all_cap_pattern) || []);
5550
+ const valid_terms = new Set(Object.keys(definitions));
5551
+ const terms_by_first_letter = {};
5552
+ for (const term of valid_terms) {
5553
+ const fl = term[0].toLowerCase();
5554
+ if (!terms_by_first_letter[fl]) terms_by_first_letter[fl] = [];
5555
+ terms_by_first_letter[fl].push(term);
5556
+ }
5557
+ const candidates_by_term = {};
5558
+ for (const raw_candidate of all_caps) {
5559
+ let candidate = raw_candidate.trim();
5560
+ const words = candidate.split(/\s+/);
5561
+ while (words.length > 0) {
5562
+ const first = words[0];
5563
+ const title = first.charAt(0).toUpperCase() + first.slice(1).toLowerCase();
5564
+ if (stop_words.has(title)) words.shift();
5565
+ else break;
5566
+ }
5567
+ candidate = words.join(" ");
5568
+ if (candidate.length < 4) continue;
5569
+ if (valid_terms.has(candidate)) continue;
5570
+ const first_letter = candidate[0].toLowerCase();
5571
+ let candidate_terms = terms_by_first_letter[first_letter] || [];
5572
+ if (candidate.length > 5) {
5573
+ for (const [k, v] of Object.entries(terms_by_first_letter)) {
5574
+ if (k !== first_letter) candidate_terms = candidate_terms.concat(v);
5403
5575
  }
5404
- const table_text = extract_table(inner_item, comments_map, false, 0);
5405
- cursor += table_text ? table_text.length : 0;
5406
5576
  }
5407
- is_first_block = false;
5577
+ for (const term of candidate_terms) {
5578
+ if (Math.abs(candidate.length - term.length) > 2) continue;
5579
+ if (candidate === term + "s" || candidate === term + "es") continue;
5580
+ if (term === candidate + "s" || term === candidate + "es") continue;
5581
+ const dist = boundedLevenshtein(candidate, term, 2);
5582
+ if (dist === 0 || dist > 2) continue;
5583
+ if (term.length <= 5) {
5584
+ if (dist > 1) continue;
5585
+ if (candidate[0].toLowerCase() !== term[0].toLowerCase()) continue;
5586
+ }
5587
+ if (!candidates_by_term[term]) candidates_by_term[term] = [];
5588
+ if (!candidates_by_term[term].includes(candidate))
5589
+ candidates_by_term[term].push(candidate);
5590
+ }
5408
5591
  }
5409
- return [cursor, false];
5592
+ for (const [term, candidates] of Object.entries(candidates_by_term)) {
5593
+ candidates.sort();
5594
+ const c_str = candidates.map((c) => `'${c}'`).join(", ");
5595
+ diagnostics.push(`[Info] Possible Typos for '${term}': Found ${c_str}`);
5596
+ }
5597
+ function diag_sort_key(msg) {
5598
+ if (msg.startsWith("[Error]")) return 0;
5599
+ if (msg.startsWith("[Warning]")) return 1;
5600
+ return 2;
5601
+ }
5602
+ diagnostics.sort((a, b) => {
5603
+ const keyA = diag_sort_key(a);
5604
+ const keyB = diag_sort_key(b);
5605
+ if (keyA !== keyB) return keyA - keyB;
5606
+ return a.localeCompare(b);
5607
+ });
5608
+ return [definitions, diagnostics, raw_anchors];
5410
5609
  }
5411
- function _element_is_descendant(target_el, ancestor_el) {
5412
- let cur = target_el.parentNode;
5413
- while (cur) {
5414
- if (cur === ancestor_el) return true;
5415
- cur = cur.parentNode;
5610
+ function extract_document_settings_warnings(doc) {
5611
+ const warnings = [];
5612
+ const settingsPart = doc.pkg.getPartByPath("word/settings.xml");
5613
+ if (!settingsPart) return warnings;
5614
+ const isTruthy = (el) => {
5615
+ if (!el.hasAttribute("w:val")) return true;
5616
+ const val = (el.getAttribute("w:val") || "").toLowerCase();
5617
+ return val !== "0" && val !== "false" && val !== "off";
5618
+ };
5619
+ const removePersonal = findDescendantsByLocalName(
5620
+ settingsPart._element,
5621
+ "removePersonalInformation"
5622
+ );
5623
+ if (removePersonal.length > 0 && isTruthy(removePersonal[0])) {
5624
+ warnings.push(
5625
+ "[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."
5626
+ );
5416
5627
  }
5417
- return false;
5628
+ const removeDateTime = findDescendantsByLocalName(
5629
+ settingsPart._element,
5630
+ "removeDateAndTime"
5631
+ );
5632
+ if (removeDateTime.length > 0 && isTruthy(removeDateTime[0])) {
5633
+ warnings.push(
5634
+ "[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."
5635
+ );
5636
+ }
5637
+ return warnings;
5418
5638
  }
5419
- function _record_table_inner_blocks_lite(table, inherited_offset, records, comments_map) {
5420
- const seen_cells = /* @__PURE__ */ new Set();
5421
- for (const row of table.rows) {
5422
- for (const cell of row.cells) {
5423
- if (seen_cells.has(cell)) continue;
5424
- seen_cells.add(cell);
5425
- for (const inner_item of iter_block_items(cell)) {
5426
- if (inner_item instanceof Paragraph) {
5427
- const true_offset = _is_heading(inner_item) ? _compute_inner_block_offset(
5428
- table,
5429
- inner_item,
5430
- inherited_offset,
5431
- comments_map
5432
- ) : inherited_offset;
5433
- records.push({
5434
- item: inner_item,
5435
- is_paragraph: true,
5436
- is_table: false,
5437
- start_offset: true_offset,
5438
- projected_length: 0
5439
- });
5440
- } else if (inner_item instanceof Table) {
5441
- records.push({
5442
- item: inner_item,
5443
- is_paragraph: false,
5444
- is_table: true,
5445
- start_offset: inherited_offset,
5446
- projected_length: 0
5447
- });
5448
- _record_table_inner_blocks_lite(
5449
- inner_item,
5450
- inherited_offset,
5451
- records,
5452
- comments_map
5453
- );
5454
- }
5455
- }
5639
+ function build_structural_appendix(doc, base_text) {
5640
+ const [defs, diagnostics, anchors] = extract_all_domain_metadata(
5641
+ doc,
5642
+ base_text
5643
+ );
5644
+ const settings_warnings = extract_document_settings_warnings(doc);
5645
+ const lines = [
5646
+ "\n\n---",
5647
+ "",
5648
+ "<!-- READONLY_BOUNDARY_START -->",
5649
+ "# Document Structure (Read-Only)",
5650
+ "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."
5651
+ ];
5652
+ let has_content = false;
5653
+ if (settings_warnings.length > 0) {
5654
+ has_content = true;
5655
+ lines.push("\n## Document Settings");
5656
+ for (const warning of settings_warnings) {
5657
+ lines.push(`- ${warning}`);
5456
5658
  }
5457
5659
  }
5458
- }
5459
- function _project_part(part, comments_map) {
5460
- const blocks = [];
5461
- const c_type = part.constructor.name;
5462
- if (c_type === "NotesPart") {
5463
- const header = part.note_type === "fn" ? "## Footnotes" : "## Endnotes";
5464
- blocks.push(`---
5465
- ${header}`);
5660
+ if (Object.keys(defs).length > 0) {
5661
+ has_content = true;
5662
+ lines.push("\n## Defined Terms");
5663
+ for (const [term, data] of Object.entries(defs)) {
5664
+ lines.push(`- "${term}" \u2014 used ${data.count} times.`);
5665
+ }
5466
5666
  }
5467
- let is_first_para = true;
5468
- for (const item of iter_block_items(part)) {
5469
- if (item.constructor.name === "FootnoteItem") {
5470
- const fn_text = _project_part(item, comments_map);
5471
- if (fn_text) blocks.push(fn_text);
5472
- } else if (item instanceof Paragraph) {
5473
- let prefix = get_paragraph_prefix(item);
5474
- if (is_first_para && c_type === "FootnoteItem")
5475
- prefix = `[^${part.note_type}-${part.id}]: ${prefix}`;
5476
- const p_text = build_paragraph_text(item, comments_map, false);
5477
- blocks.push(prefix + p_text);
5478
- is_first_para = false;
5479
- } else if (item instanceof Table) {
5480
- const table_text = extract_table(item, comments_map, false, 0);
5481
- if (table_text) blocks.push(table_text);
5482
- is_first_para = false;
5667
+ if (diagnostics.length > 0) {
5668
+ has_content = true;
5669
+ lines.push("\n## Semantic Diagnostics");
5670
+ for (const diag of diagnostics) {
5671
+ lines.push(`- ${diag}`);
5483
5672
  }
5484
5673
  }
5485
- return blocks.join("\n\n");
5486
- }
5487
- function _is_heading(paragraph) {
5488
- return _HEADING_PREFIX_RE.test(get_paragraph_prefix(paragraph));
5489
- }
5490
- function _heading_passes_quality_filter(paragraph, comments_map) {
5491
- const style = _determine_heading_style(paragraph);
5492
- if (style !== "(heuristic)") return true;
5493
- const text = _heading_text(paragraph, comments_map);
5494
- if (!text) return false;
5495
- const word_count = (text.match(/\w+/g) || []).length;
5496
- return word_count >= _HEURISTIC_MIN_WORDS;
5674
+ if (Object.keys(anchors).length > 0) {
5675
+ has_content = true;
5676
+ lines.push("\n## Named Anchors");
5677
+ for (const [b_name, data] of Object.entries(anchors)) {
5678
+ lines.push(`- ${b_name} \u2192 Anchored to: "${data.anchored_to}"`);
5679
+ for (const ref of data.referenced_from) {
5680
+ lines.push(` - Referenced from: "${ref}"`);
5681
+ }
5682
+ }
5683
+ }
5684
+ if (has_content) {
5685
+ return lines.join("\n");
5686
+ }
5687
+ return "";
5497
5688
  }
5498
- function _heading_level(paragraph) {
5499
- const match = _HEADING_PREFIX_RE.exec(get_paragraph_prefix(paragraph));
5500
- return match ? Math.min(match[1].length, 6) : 1;
5689
+
5690
+ // src/ingest.ts
5691
+ async function extractTextFromBuffer(buffer, cleanView = false) {
5692
+ const doc = await DocumentObject.load(buffer);
5693
+ return _extractTextFromDoc(doc, cleanView);
5501
5694
  }
5502
- function _heading_text(paragraph, comments_map) {
5503
- const p_text = build_paragraph_text(paragraph, comments_map, false);
5504
- let cleaned = _strip_critic_markup(p_text);
5505
- cleaned = _strip_inline_formatting(cleaned);
5506
- return cleaned.trim();
5507
- }
5508
- function _strip_critic_markup(text) {
5509
- if (!text) return "";
5510
- text = text.replace(/\{--[\s\S]*?--\}/g, "");
5511
- text = text.replace(/\{>>[\s\S]*?<<\}/g, "");
5512
- text = text.replace(/\{\+\+([\s\S]*?)\+\+\}/g, "$1");
5513
- text = text.replace(/\{==([\s\S]*?)==\}/g, "$1");
5514
- return text;
5515
- }
5516
- function _strip_inline_formatting(text) {
5517
- if (!text) return "";
5518
- text = text.replace(/\*\*(.+?)\*\*/g, "$1");
5519
- text = text.replace(/__(.+?)__/g, "$1");
5520
- text = text.replace(/(?<!\w)_(\S(?:.*?\S)?)_(?!\w)/g, "$1");
5521
- return text;
5522
- }
5523
- function _determine_heading_style(paragraph) {
5524
- const [style_cache, default_pstyle] = _get_style_cache(
5525
- paragraph._parent.part || paragraph._parent
5526
- );
5527
- const pPr = findChild(paragraph._element, "w:pPr");
5528
- let style_id = default_pstyle;
5529
- if (pPr) {
5530
- const pStyle = findChild(pPr, "w:pStyle");
5531
- if (pStyle) style_id = pStyle.getAttribute("w:val") || default_pstyle;
5532
- }
5533
- let outline_level = null;
5534
- let outline_level_from_style = false;
5535
- if (pPr) {
5536
- const oLvl = findChild(pPr, "w:outlineLvl");
5537
- if (oLvl && /^\d+$/.test(oLvl.getAttribute("w:val") || "")) {
5538
- outline_level = parseInt(oLvl.getAttribute("w:val"), 10);
5539
- }
5540
- }
5541
- if (outline_level === null && style_id && style_cache && style_cache[style_id]) {
5542
- outline_level = style_cache[style_id].outline_level;
5543
- outline_level_from_style = true;
5544
- }
5545
- const style_name = style_id && style_cache && style_cache[style_id] ? style_cache[style_id].name : style_id;
5546
- let normalized_style_name = style_name;
5547
- if (normalized_style_name && typeof normalized_style_name === "string") {
5548
- if (normalized_style_name.toLowerCase().startsWith("heading")) {
5549
- normalized_style_name = normalized_style_name.replace(/^heading/i, "Heading");
5550
- } else if (normalized_style_name.toLowerCase() === "title") {
5551
- normalized_style_name = "Title";
5695
+ function _extractTextFromDoc(doc, cleanView = false, includeAppendix = true, return_paragraph_offsets = false) {
5696
+ const comments_map = extract_comments_data(doc.pkg);
5697
+ const full_text = [];
5698
+ const paragraph_offsets = /* @__PURE__ */ new Map();
5699
+ let cursor = 0;
5700
+ for (const part of iter_document_parts(doc)) {
5701
+ const part_cursor = full_text.length > 0 ? cursor + 2 : cursor;
5702
+ const part_text = _extract_blocks(
5703
+ part,
5704
+ comments_map,
5705
+ cleanView,
5706
+ part_cursor,
5707
+ return_paragraph_offsets ? paragraph_offsets : void 0
5708
+ );
5709
+ if (part_text) {
5710
+ if (full_text.length > 0) cursor += 2;
5711
+ full_text.push(part_text);
5712
+ cursor += part_text.length;
5552
5713
  }
5553
5714
  }
5554
- if (outline_level_from_style && outline_level !== null) {
5555
- const is_heading_or_title = normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title");
5556
- if (!is_heading_or_title) {
5557
- outline_level = null;
5558
- }
5715
+ let base_text = full_text.join("\n\n");
5716
+ if (includeAppendix) {
5717
+ const appendix = build_structural_appendix(doc, base_text);
5718
+ if (appendix) base_text += appendix;
5559
5719
  }
5560
- if (outline_level !== null && outline_level >= 0 && outline_level <= 8) {
5561
- if (normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title")) {
5562
- return normalized_style_name;
5563
- }
5564
- return "(outline_level)";
5720
+ if (return_paragraph_offsets) {
5721
+ return { text: base_text, paragraph_offsets };
5565
5722
  }
5566
- if (normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title"))
5567
- return normalized_style_name;
5568
- if (normalized_style_name && /Heading[ ]?([1-6])(?![0-9])/.test(normalized_style_name))
5569
- return normalized_style_name;
5570
- return "(heuristic)";
5723
+ return base_text;
5571
5724
  }
5572
- function _find_owned_end(block_records, heading_indices, current_h_pos, current_level) {
5573
- for (let next_h_pos = current_h_pos + 1; next_h_pos < heading_indices.length; next_h_pos++) {
5574
- const next_idx = heading_indices[next_h_pos];
5575
- if (_heading_level(block_records[next_idx].item) <= current_level)
5576
- return next_idx;
5725
+ function _extract_blocks(container, comments_map, cleanView, cursor, paragraph_offsets) {
5726
+ const part = container.part || container;
5727
+ const [style_cache, default_pstyle] = _get_style_cache(part);
5728
+ const blocks = [];
5729
+ let local_cursor = cursor;
5730
+ let is_first_block = true;
5731
+ let is_first_para = true;
5732
+ if (container.constructor && container.constructor.name === "NotesPart") {
5733
+ const header = container.note_type === "fn" ? "## Footnotes" : "## Endnotes";
5734
+ const sep = `---
5735
+ ${header}`;
5736
+ blocks.push(sep);
5737
+ local_cursor += sep.length;
5738
+ is_first_block = false;
5577
5739
  }
5578
- return block_records.length;
5579
- }
5580
- function _collect_footnote_ids(owned_blocks) {
5581
- const seen = /* @__PURE__ */ new Set();
5582
- const ordered = [];
5583
- for (const rec of owned_blocks) {
5584
- if (!rec.is_paragraph) continue;
5585
- for (const event of iter_paragraph_content(rec.item)) {
5586
- if (!("type" in event)) continue;
5587
- let fn_id = "";
5588
- if (event.type === "footnote") fn_id = `fn-${event.id}`;
5589
- else if (event.type === "endnote") fn_id = `en-${event.id}`;
5590
- else continue;
5591
- if (!seen.has(fn_id)) {
5592
- seen.add(fn_id);
5593
- ordered.push(fn_id);
5740
+ for (const item of iter_block_items(container)) {
5741
+ if (!is_first_block) local_cursor += 2;
5742
+ const block_start = local_cursor;
5743
+ if (item.constructor.name === "FootnoteItem") {
5744
+ const fn_text = _extract_blocks(
5745
+ item,
5746
+ comments_map,
5747
+ cleanView,
5748
+ block_start,
5749
+ paragraph_offsets
5750
+ );
5751
+ if (fn_text) {
5752
+ blocks.push(fn_text);
5753
+ local_cursor = block_start + fn_text.length;
5754
+ is_first_block = false;
5755
+ } else if (!is_first_block) {
5756
+ local_cursor -= 2;
5594
5757
  }
5595
- }
5596
- }
5597
- return ordered;
5598
- }
5599
- function _offset_to_page(offset, body_page_offsets) {
5600
- if (!body_page_offsets || body_page_offsets.length === 0) return 1;
5601
- let page = 1;
5602
- for (let i = 0; i < body_page_offsets.length; i++) {
5603
- if (offset >= body_page_offsets[i]) page = i + 1;
5604
- else break;
5605
- }
5606
- return page;
5607
- }
5608
- function _extract_outline_fast(doc, projected_body, body_page_offsets, paragraph_offsets) {
5609
- const paragraphs_and_tables = [];
5610
- const seen_cells = /* @__PURE__ */ new Set();
5611
- function walk(container) {
5612
- for (const item of iter_block_items(container)) {
5613
- const i_type = item.constructor.name;
5614
- if (i_type === "FootnoteItem") {
5615
- walk(item);
5616
- } else if (item instanceof Paragraph) {
5617
- paragraphs_and_tables.push(["p", item]);
5618
- } else if (item instanceof Table) {
5619
- paragraphs_and_tables.push(["t", item]);
5620
- for (const row of item.rows) {
5621
- for (const cell of row.cells) {
5622
- if (seen_cells.has(cell._element)) {
5623
- continue;
5624
- }
5625
- seen_cells.add(cell._element);
5626
- walk(cell);
5627
- }
5628
- }
5758
+ } else if (item instanceof Paragraph) {
5759
+ let prefix = get_paragraph_prefix(item, style_cache, default_pstyle);
5760
+ if (is_first_para && container.constructor.name === "FootnoteItem") {
5761
+ prefix = `[^${container.note_type}-${container.id}]: ` + prefix;
5762
+ }
5763
+ const p_text = build_paragraph_text(
5764
+ item,
5765
+ comments_map,
5766
+ cleanView,
5767
+ style_cache,
5768
+ default_pstyle
5769
+ );
5770
+ const full_block = prefix + p_text;
5771
+ blocks.push(full_block);
5772
+ if (paragraph_offsets) {
5773
+ paragraph_offsets.set(item._element, [block_start, full_block.length]);
5629
5774
  }
5775
+ local_cursor = block_start + full_block.length;
5776
+ is_first_para = false;
5777
+ is_first_block = false;
5778
+ } else if (item instanceof Table) {
5779
+ const table_text = extract_table(
5780
+ item,
5781
+ comments_map,
5782
+ cleanView,
5783
+ block_start,
5784
+ paragraph_offsets
5785
+ );
5786
+ if (table_text) {
5787
+ blocks.push(table_text);
5788
+ local_cursor = block_start + table_text.length;
5789
+ is_first_block = false;
5790
+ } else if (!is_first_block) {
5791
+ local_cursor -= 2;
5792
+ }
5793
+ is_first_para = false;
5630
5794
  }
5631
5795
  }
5632
- walk(doc);
5633
- const heading_indices = [];
5634
- for (let idx = 0; idx < paragraphs_and_tables.length; idx++) {
5635
- const [kind, item] = paragraphs_and_tables[idx];
5636
- if (kind !== "p") continue;
5637
- let hasOffset = false;
5638
- if (paragraph_offsets instanceof Map) {
5639
- hasOffset = paragraph_offsets.has(item._element);
5640
- } else {
5641
- hasOffset = item._element in paragraph_offsets;
5796
+ return blocks.join("\n\n");
5797
+ }
5798
+ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets) {
5799
+ const rows_text = [];
5800
+ let rows_processed = 0;
5801
+ let local_cursor = cursor;
5802
+ for (const row of table.rows) {
5803
+ const cell_texts = [];
5804
+ const seen_cells = /* @__PURE__ */ new Set();
5805
+ const trPr = findChild(row._element, "w:trPr");
5806
+ const ins = trPr ? findChild(trPr, "w:ins") : null;
5807
+ const del_node = trPr ? findChild(trPr, "w:del") : null;
5808
+ if (cleanView && del_node) continue;
5809
+ const row_start = local_cursor + (rows_processed > 0 ? 1 : 0);
5810
+ const wrapper_prefix_len = !cleanView && ins ? 4 : !cleanView && del_node ? 4 : 0;
5811
+ let cell_cursor = row_start + wrapper_prefix_len;
5812
+ let first_cell = true;
5813
+ for (const cell of row.cells) {
5814
+ if (seen_cells.has(cell)) continue;
5815
+ seen_cells.add(cell);
5816
+ if (!first_cell) cell_cursor += 3;
5817
+ const cell_content = _extract_blocks(
5818
+ cell,
5819
+ comments_map,
5820
+ cleanView,
5821
+ cell_cursor,
5822
+ paragraph_offsets
5823
+ );
5824
+ cell_texts.push(cell_content);
5825
+ cell_cursor += cell_content.length;
5826
+ first_cell = false;
5642
5827
  }
5643
- if (!hasOffset) {
5644
- continue;
5828
+ let row_str = cell_texts.join(" | ");
5829
+ if (!cleanView) {
5830
+ if (ins) row_str = `{++ ${row_str} |Chg:${ins.getAttribute("w:id")}++}`;
5831
+ else if (del_node)
5832
+ row_str = `{-- ${row_str} |Chg:${del_node.getAttribute("w:id")}--}`;
5645
5833
  }
5646
- if (!_is_heading(item)) continue;
5647
- if (!_heading_passes_quality_filter_fast(item, projected_body, paragraph_offsets)) continue;
5648
- heading_indices.push(idx);
5834
+ rows_text.push(row_str);
5835
+ local_cursor = row_start + row_str.length;
5836
+ rows_processed++;
5649
5837
  }
5650
- if (heading_indices.length === 0) return [];
5651
- const nodes = [];
5652
- for (let h_pos = 0; h_pos < heading_indices.length; h_pos++) {
5653
- const item_idx = heading_indices[h_pos];
5654
- const paragraph = paragraphs_and_tables[item_idx][1];
5655
- const level = _heading_level(paragraph);
5656
- const text = _heading_text_fast(paragraph, projected_body, paragraph_offsets);
5657
- const style = _determine_heading_style(paragraph);
5658
- let owned_end = item_idx;
5659
- for (let next_h_pos = h_pos + 1; next_h_pos < heading_indices.length; next_h_pos++) {
5660
- const next_idx = heading_indices[next_h_pos];
5661
- const next_paragraph = paragraphs_and_tables[next_idx][1];
5662
- if (_heading_level(next_paragraph) <= level) {
5663
- owned_end = next_idx;
5664
- break;
5665
- }
5666
- }
5667
- if (owned_end === item_idx) {
5668
- owned_end = paragraphs_and_tables.length;
5669
- }
5670
- const owned = paragraphs_and_tables.slice(item_idx + 1, owned_end);
5671
- let has_table = false;
5672
- for (const [kind2, item2] of owned) {
5673
- if (kind2 === "p" && _is_heading(item2)) {
5674
- break;
5838
+ return rows_text.join("\n");
5839
+ }
5840
+ function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, default_pstyle) {
5841
+ const parts = [];
5842
+ const active_ins = {};
5843
+ const active_del = {};
5844
+ const active_comments = /* @__PURE__ */ new Set();
5845
+ const active_fmt = {};
5846
+ const deferred_meta_states = [];
5847
+ let pending_text = "";
5848
+ let current_wrappers = ["", ""];
5849
+ let current_style = ["", ""];
5850
+ const items = Array.from(iter_paragraph_content(paragraph));
5851
+ const is_heading = is_heading_paragraph(
5852
+ paragraph,
5853
+ style_cache,
5854
+ default_pstyle
5855
+ );
5856
+ const native_heading = is_native_heading(
5857
+ paragraph,
5858
+ style_cache,
5859
+ default_pstyle
5860
+ );
5861
+ let leading_strip_active = is_heading;
5862
+ for (let i = 0; i < items.length; i++) {
5863
+ const item = items[i];
5864
+ if (item instanceof Run) {
5865
+ const [prefix, suffix] = get_run_style_markers(item, native_heading);
5866
+ const text = get_run_text(item);
5867
+ if (cleanView && Object.keys(active_del).length > 0) continue;
5868
+ if (leading_strip_active) {
5869
+ if (!text || !text.trim()) continue;
5870
+ leading_strip_active = false;
5675
5871
  }
5676
- if (kind2 === "t") {
5677
- has_table = true;
5678
- break;
5872
+ const seg = apply_formatting_to_segments(text, prefix, suffix);
5873
+ if (seg) {
5874
+ const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
5875
+ const new_style = [prefix, suffix];
5876
+ if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
5877
+ 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])) {
5878
+ pending_text = pending_text.slice(0, -current_style[1].length) + seg.slice(new_style[0].length);
5879
+ } else {
5880
+ pending_text += seg;
5881
+ }
5882
+ current_style = new_style;
5883
+ } else {
5884
+ if (pending_text)
5885
+ parts.push(
5886
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
5887
+ );
5888
+ pending_text = seg;
5889
+ current_wrappers = new_wrappers;
5890
+ current_style = new_style;
5891
+ }
5892
+ if (!cleanView) {
5893
+ 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;
5894
+ if (has_meta) {
5895
+ deferred_meta_states.push([
5896
+ { ...active_ins },
5897
+ { ...active_del },
5898
+ new Set(active_comments),
5899
+ { ...active_fmt }
5900
+ ]);
5901
+ }
5902
+ let should_defer = false;
5903
+ 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;
5904
+ if (has_any_meta) {
5905
+ let j = i + 1;
5906
+ let next_has_meta = false;
5907
+ let temp_ins = Object.keys(active_ins).length;
5908
+ let temp_del = Object.keys(active_del).length;
5909
+ let temp_fmt = Object.keys(active_fmt).length;
5910
+ const temp_comments = new Set(active_comments);
5911
+ while (j < items.length) {
5912
+ const next_item = items[j];
5913
+ if (next_item instanceof Run) {
5914
+ if (!get_run_text(next_item)) {
5915
+ j++;
5916
+ continue;
5917
+ }
5918
+ if (temp_ins > 0 || temp_del > 0 || temp_fmt > 0 || temp_comments.size > 0)
5919
+ next_has_meta = true;
5920
+ break;
5921
+ } else {
5922
+ const ev = next_item;
5923
+ if (ev.type === "ins_start") temp_ins++;
5924
+ else if (ev.type === "ins_end")
5925
+ temp_ins = Math.max(0, temp_ins - 1);
5926
+ else if (ev.type === "del_start") temp_del++;
5927
+ else if (ev.type === "del_end")
5928
+ temp_del = Math.max(0, temp_del - 1);
5929
+ else if (ev.type === "fmt_start") temp_fmt++;
5930
+ else if (ev.type === "fmt_end")
5931
+ temp_fmt = Math.max(0, temp_fmt - 1);
5932
+ else if (ev.type === "start") temp_comments.add(ev.id);
5933
+ else if (ev.type === "end") temp_comments.delete(ev.id);
5934
+ }
5935
+ j++;
5936
+ }
5937
+ if (next_has_meta) should_defer = true;
5938
+ }
5939
+ if (!should_defer && deferred_meta_states.length > 0) {
5940
+ const meta_block = _build_merged_meta_block(
5941
+ deferred_meta_states,
5942
+ comments_map
5943
+ );
5944
+ if (meta_block) {
5945
+ if (pending_text) {
5946
+ parts.push(
5947
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
5948
+ );
5949
+ pending_text = "";
5950
+ current_wrappers = ["", ""];
5951
+ current_style = ["", ""];
5952
+ }
5953
+ parts.push(`{>>${meta_block}<<}`);
5954
+ }
5955
+ deferred_meta_states.length = 0;
5956
+ }
5957
+ }
5679
5958
  }
5680
- }
5681
- const footnote_ids = _collect_footnote_ids_fast(owned);
5682
- let para_offset;
5683
- if (paragraph_offsets instanceof Map) {
5684
- para_offset = paragraph_offsets.get(paragraph._element);
5685
5959
  } else {
5686
- para_offset = paragraph_offsets[paragraph._element];
5687
- }
5688
- let page_num = 1;
5689
- if (para_offset !== void 0) {
5690
- const [start_offset] = para_offset;
5691
- page_num = _offset_to_page(start_offset, body_page_offsets);
5692
- }
5693
- nodes.push({
5694
- level,
5695
- text,
5696
- page: page_num,
5697
- style,
5698
- has_table,
5699
- footnote_ids
5700
- });
5701
- }
5702
- return nodes;
5703
- }
5704
- function _heading_passes_quality_filter_fast(paragraph, projected_body, paragraph_offsets) {
5705
- const style = _determine_heading_style(paragraph);
5706
- if (style !== "(heuristic)") return true;
5707
- const text = _heading_text_fast(paragraph, projected_body, paragraph_offsets);
5708
- if (!text) return false;
5709
- const words = text.match(/\w+/g) || [];
5710
- return words.length >= _HEURISTIC_MIN_WORDS;
5711
- }
5712
- function _heading_text_fast(paragraph, projected_body, paragraph_offsets) {
5713
- let offset;
5714
- if (paragraph_offsets instanceof Map) {
5715
- offset = paragraph_offsets.get(paragraph._element);
5716
- } else {
5717
- offset = paragraph_offsets[paragraph._element];
5718
- }
5719
- if (offset === void 0) {
5720
- return "";
5721
- }
5722
- const [start, length] = offset;
5723
- const raw = projected_body.substring(start, start + length);
5724
- let cleaned = _strip_critic_markup(raw);
5725
- cleaned = _strip_inline_formatting(cleaned);
5726
- cleaned = cleaned.replace(/^#+\s+/, "");
5727
- return cleaned.trim();
5728
- }
5729
- function _collect_footnote_ids_fast(owned_items) {
5730
- const seen = /* @__PURE__ */ new Set();
5731
- const ordered = [];
5732
- for (const [kind, item] of owned_items) {
5733
- if (kind !== "p") continue;
5734
- for (const event of iter_paragraph_content(item)) {
5735
- if (!("type" in event)) continue;
5736
- let fn_id = "";
5737
- if (event.type === "footnote") fn_id = `fn-${event.id}`;
5738
- else if (event.type === "endnote") fn_id = `en-${event.id}`;
5739
- else continue;
5740
- if (!seen.has(fn_id)) {
5741
- seen.add(fn_id);
5742
- ordered.push(fn_id);
5960
+ const ev = item;
5961
+ leading_strip_active = false;
5962
+ if (![
5963
+ "ins_start",
5964
+ "ins_end",
5965
+ "del_start",
5966
+ "del_end",
5967
+ "fmt_start",
5968
+ "fmt_end"
5969
+ ].includes(ev.type)) {
5970
+ if (pending_text) {
5971
+ parts.push(
5972
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
5973
+ );
5974
+ pending_text = "";
5975
+ current_wrappers = ["", ""];
5976
+ current_style = ["", ""];
5977
+ }
5743
5978
  }
5744
- }
5745
- }
5746
- return ordered;
5747
- }
5748
-
5749
- // src/sanitize/report.ts
5750
- var SanitizeReport = class {
5751
- filename;
5752
- mode;
5753
- author;
5754
- tracked_changes_found = 0;
5755
- tracked_changes_accepted = 0;
5756
- tracked_changes_kept = 0;
5757
- change_lines = [];
5758
- comments_removed = 0;
5759
- comments_kept = 0;
5760
- removed_comment_lines = [];
5761
- kept_comment_lines = [];
5762
- metadata_lines = [];
5763
- structural_lines = [];
5764
- warnings = [];
5765
- status = "clean";
5766
- blocked_reason = null;
5767
- constructor(filename, mode = "full", author = null) {
5768
- this.filename = filename;
5769
- this.mode = mode;
5770
- this.author = author;
5771
- }
5772
- add_transform_lines(lines) {
5773
- for (const line of lines) {
5774
- const lower = line.toLowerCase();
5775
- if (lower.includes("tracked change") || lower.includes("insertion") || lower.includes("deletion") || lower.includes("accepted")) {
5776
- this.change_lines.push(line);
5777
- } else if (lower.includes("comment") || lower.includes("[open]") || lower.includes("[resolved]")) {
5778
- if (lower.includes("kept") || lower.includes("visible")) {
5779
- this.kept_comment_lines.push(line);
5780
- } else {
5781
- this.removed_comment_lines.push(line);
5979
+ if (ev.type === "start") active_comments.add(ev.id);
5980
+ else if (ev.type === "end") active_comments.delete(ev.id);
5981
+ else if (ev.type === "ins_start") active_ins[ev.id] = ev;
5982
+ else if (ev.type === "ins_end") delete active_ins[ev.id];
5983
+ else if (ev.type === "del_start") active_del[ev.id] = ev;
5984
+ else if (ev.type === "del_end") delete active_del[ev.id];
5985
+ else if (ev.type === "fmt_start") active_fmt[ev.id] = ev;
5986
+ else if (ev.type === "fmt_end") delete active_fmt[ev.id];
5987
+ else if (ev.type === "footnote" || ev.type === "endnote") {
5988
+ if (pending_text) {
5989
+ parts.push(
5990
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
5991
+ );
5992
+ pending_text = "";
5993
+ current_wrappers = ["", ""];
5994
+ current_style = ["", ""];
5782
5995
  }
5783
- } 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")) {
5784
- this.metadata_lines.push(line);
5785
- } else if (lower.includes("hyperlink") || lower.includes("warning")) {
5786
- this.warnings.push(line);
5787
- } else {
5788
- this.structural_lines.push(line);
5996
+ parts.push(`[^${ev.type === "footnote" ? "fn" : "en"}-${ev.id}]`);
5997
+ } else if (ev.type === "hyperlink_start") {
5998
+ if (pending_text) {
5999
+ parts.push(
6000
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6001
+ );
6002
+ pending_text = "";
6003
+ current_wrappers = ["", ""];
6004
+ current_style = ["", ""];
6005
+ }
6006
+ parts.push("[");
6007
+ } else if (ev.type === "hyperlink_end") {
6008
+ if (pending_text) {
6009
+ parts.push(
6010
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6011
+ );
6012
+ pending_text = "";
6013
+ current_wrappers = ["", ""];
6014
+ current_style = ["", ""];
6015
+ }
6016
+ parts.push(`](${ev.date})`);
6017
+ } else if (ev.type === "xref_start") {
6018
+ if (pending_text) {
6019
+ parts.push(
6020
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6021
+ );
6022
+ pending_text = "";
6023
+ current_wrappers = ["", ""];
6024
+ current_style = ["", ""];
6025
+ }
6026
+ parts.push("[~");
6027
+ } else if (ev.type === "xref_end") {
6028
+ if (pending_text) {
6029
+ parts.push(
6030
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6031
+ );
6032
+ pending_text = "";
6033
+ current_wrappers = ["", ""];
6034
+ current_style = ["", ""];
6035
+ }
6036
+ parts.push(`~](#${ev.id})`);
6037
+ } else if (ev.type === "bookmark") {
6038
+ if (pending_text) {
6039
+ parts.push(
6040
+ `${current_wrappers[0]}${pending_text}${current_wrappers[1]}`
6041
+ );
6042
+ pending_text = "";
6043
+ current_wrappers = ["", ""];
6044
+ current_style = ["", ""];
6045
+ }
6046
+ parts.push(`{#${ev.id}}`);
5789
6047
  }
5790
6048
  }
5791
6049
  }
5792
- render() {
5793
- const sep = "\u2550".repeat(50);
5794
- const lines = [sep, `Finalization Report: ${this.filename}`];
5795
- const flags = [];
5796
- if (this.mode === "keep-markup") flags.push("--keep-markup");
5797
- if (this.author) flags.push(`--author "${this.author}"`);
5798
- if (this.tracked_changes_accepted > 0) flags.push("--accept-all");
5799
- if (flags.length > 0) lines.push(flags.join(" "));
5800
- lines.push(sep);
5801
- if (this.status === "blocked") {
5802
- lines.push("");
5803
- lines.push(`BLOCKED: ${this.blocked_reason}`);
5804
- lines.push(sep);
5805
- return lines.join("\n");
5806
- }
5807
- if (this.mode === "keep-markup" && (this.tracked_changes_kept > 0 || this.comments_kept > 0)) {
5808
- lines.push("");
5809
- lines.push("VISIBLE TO COUNTERPARTY");
5810
- if (this.tracked_changes_kept > 0) lines.push(` Tracked changes: ${this.tracked_changes_kept}`);
5811
- if (this.comments_kept > 0) {
5812
- lines.push(` Open comments: ${this.comments_kept}`);
5813
- for (const cl of this.kept_comment_lines) lines.push(` ${cl}`);
6050
+ if (pending_text)
6051
+ parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
6052
+ if (deferred_meta_states.length > 0) {
6053
+ const meta_block = _build_merged_meta_block(
6054
+ deferred_meta_states,
6055
+ comments_map
6056
+ );
6057
+ if (meta_block) parts.push(`{>>${meta_block}<<}`);
6058
+ }
6059
+ return parts.join("");
6060
+ }
6061
+ function _get_wrappers(ins, del, comments, fmt) {
6062
+ if (Object.keys(del).length > 0) return ["{--", "--}"];
6063
+ if (Object.keys(ins).length > 0) return ["{++", "++}"];
6064
+ if (comments.size > 0 || Object.keys(fmt).length > 0) return ["{==", "==}"];
6065
+ return ["", ""];
6066
+ }
6067
+ function _build_merged_meta_block(states_list, comments_map) {
6068
+ const change_lines = [];
6069
+ const comment_lines = [];
6070
+ const seen_sigs = /* @__PURE__ */ new Set();
6071
+ for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
6072
+ let render_comment2 = function(cid) {
6073
+ if (!comments_map[cid]) return;
6074
+ const sig = `Com:${cid}`;
6075
+ if (seen_sigs.has(sig)) return;
6076
+ const data = comments_map[cid];
6077
+ let header = `[${sig}] ${data.author}`;
6078
+ if (data.date) header += ` @ ${data.date}`;
6079
+ if (data.resolved) header += `(RESOLVED)`;
6080
+ comment_lines.push(`${header}: ${data.text}`);
6081
+ seen_sigs.add(sig);
6082
+ if (children_map[cid]) {
6083
+ const children = children_map[cid].sort(
6084
+ (a, b) => (comments_map[a]?.date || "").localeCompare(
6085
+ comments_map[b]?.date || ""
6086
+ )
6087
+ );
6088
+ for (const child_id of children) {
6089
+ render_comment2(child_id);
6090
+ }
6091
+ }
6092
+ };
6093
+ var render_comment = render_comment2;
6094
+ for (const [uid, meta] of Object.entries(
6095
+ ins_map
6096
+ )) {
6097
+ const sig = `Chg:${uid}`;
6098
+ if (!seen_sigs.has(sig)) {
6099
+ change_lines.push(`[${sig} insert] ${meta.author || "Unknown"}`);
6100
+ seen_sigs.add(sig);
5814
6101
  }
5815
- if (this.author) lines.push(` Author on all markup: "${this.author}"`);
5816
- }
5817
- if (this.change_lines.length > 0) {
5818
- lines.push("");
5819
- lines.push("TRACKED CHANGES");
5820
- for (const cl of this.change_lines) lines.push(` ${cl}`);
5821
- }
5822
- if (this.removed_comment_lines.length > 0) {
5823
- lines.push("");
5824
- lines.push("COMMENTS (stripped)");
5825
- for (const cl of this.removed_comment_lines) lines.push(` ${cl}`);
5826
6102
  }
5827
- if (this.metadata_lines.length > 0) {
5828
- lines.push("");
5829
- lines.push("METADATA");
5830
- for (const ml of this.metadata_lines) lines.push(` ${ml}`);
6103
+ for (const [uid, meta] of Object.entries(
6104
+ del_map
6105
+ )) {
6106
+ const sig = `Chg:${uid}`;
6107
+ if (!seen_sigs.has(sig)) {
6108
+ change_lines.push(`[${sig} delete] ${meta.author || "Unknown"}`);
6109
+ seen_sigs.add(sig);
6110
+ }
5831
6111
  }
5832
- if (this.structural_lines.length > 0) {
5833
- lines.push("");
5834
- lines.push("STRUCTURAL & PROTECTION");
5835
- for (const sl of this.structural_lines) lines.push(` ${sl}`);
6112
+ for (const [uid, meta] of Object.entries(
6113
+ fmt_map
6114
+ )) {
6115
+ const sig = `Chg:${uid}`;
6116
+ if (!seen_sigs.has(sig)) {
6117
+ change_lines.push(`[${sig} format] ${meta.author || "Unknown"}`);
6118
+ seen_sigs.add(sig);
6119
+ }
5836
6120
  }
5837
- if (this.warnings.length > 0) {
5838
- lines.push("");
5839
- lines.push("WARNINGS");
5840
- for (const w of this.warnings) lines.push(` \u26A0 ${w}`);
6121
+ const children_map = {};
6122
+ for (const [c_id, data] of Object.entries(
6123
+ comments_map
6124
+ )) {
6125
+ const p_id = data.parent_id;
6126
+ if (p_id) {
6127
+ if (!children_map[p_id]) children_map[p_id] = [];
6128
+ children_map[p_id].push(c_id);
6129
+ }
5841
6130
  }
5842
- lines.push("");
5843
- lines.push(sep);
5844
- if (this.warnings.length > 0) {
5845
- lines.push(`Result: CLEAN WITH WARNINGS (${this.warnings.length} warning${this.warnings.length > 1 ? "s" : ""})`);
5846
- } else {
5847
- lines.push(`Result: CLEAN (${this.tracked_changes_found} changes resolved, ${this.comments_removed} comments removed)`);
6131
+ const sorted_ids = Array.from(comments_set).sort();
6132
+ for (const c_id of sorted_ids) {
6133
+ render_comment2(c_id);
5848
6134
  }
5849
- lines.push(sep);
5850
- return lines.join("\n");
5851
6135
  }
5852
- };
6136
+ return [...change_lines, ...comment_lines].join("\n");
6137
+ }
5853
6138
 
5854
- // src/sanitize/transforms.ts
5855
- function findDescendantsByLocalName(element, localName) {
5856
- const result = [];
5857
- const all = element.getElementsByTagName("*");
5858
- for (let i = 0; i < all.length; i++) {
5859
- const tag = all[i].tagName;
5860
- if (tag === localName || tag.endsWith(":" + localName)) {
5861
- result.push(all[i]);
5862
- }
6139
+ // src/outline.ts
6140
+ var _HEADING_PREFIX_RE = /^(#{1,6}) /;
6141
+ var _HEURISTIC_MIN_WORDS = 3;
6142
+ function extract_outline(doc, projected_body, body_pages, body_page_offsets, paragraph_offsets = null) {
6143
+ if (body_pages.length !== body_page_offsets.length) {
6144
+ throw new Error("body_pages and body_page_offsets length mismatch");
5863
6145
  }
5864
- return result;
5865
- }
5866
- function coalesce_runs(doc) {
5867
- let count = 0;
5868
- function areRunsIdentical(rPr1, rPr2) {
5869
- const xml1 = rPr1 ? rPr1.toString() : "";
5870
- const xml2 = rPr2 ? rPr2.toString() : "";
5871
- return xml1 === xml2;
6146
+ if (paragraph_offsets) {
6147
+ return _extract_outline_fast(doc, projected_body, body_page_offsets, paragraph_offsets);
5872
6148
  }
5873
- function hasSpecialContent(run) {
5874
- const safeTags = ["w:t", "w:tab", "w:br", "w:cr", "w:delText", "w:rPr"];
5875
- for (let i = 0; i < run.childNodes.length; i++) {
5876
- const child = run.childNodes[i];
5877
- if (child.nodeType === 1) {
5878
- const tag = child.tagName;
5879
- if (!safeTags.includes(tag)) return true;
5880
- }
5881
- }
5882
- return false;
6149
+ const comments_map = extract_comments_data(doc.pkg);
6150
+ const block_records = _walk_doc_body(doc, comments_map);
6151
+ const heading_indices = [];
6152
+ for (let idx = 0; idx < block_records.length; idx++) {
6153
+ const rec = block_records[idx];
6154
+ if (!(rec.is_paragraph && _is_heading(rec.item))) continue;
6155
+ if (!_heading_passes_quality_filter(rec.item, comments_map)) continue;
6156
+ heading_indices.push(idx);
5883
6157
  }
5884
- function coalesceContainer(container) {
5885
- const children = Array.from(container.childNodes).filter((n) => n.nodeType === 1);
5886
- let i = 0;
5887
- while (i < children.length - 1) {
5888
- const curr = children[i];
5889
- const nxt = children[i + 1];
5890
- if (curr.tagName === "w:r" && nxt.tagName === "w:r") {
5891
- if (!hasSpecialContent(curr) && !hasSpecialContent(nxt)) {
5892
- const rPr1 = findChild(curr, "w:rPr");
5893
- const rPr2 = findChild(nxt, "w:rPr");
5894
- if (areRunsIdentical(rPr1, rPr2)) {
5895
- let last_t = null;
5896
- for (let c = 0; c < curr.childNodes.length; c++) {
5897
- const child = curr.childNodes[c];
5898
- if (child.nodeType === 1 && (child.tagName === "w:t" || child.tagName === "w:delText")) {
5899
- last_t = child;
5900
- }
5901
- }
5902
- const nxtChildren = Array.from(nxt.childNodes).filter((n) => n.nodeType === 1);
5903
- for (const child of nxtChildren) {
5904
- if (child.tagName === "w:rPr") continue;
5905
- if ((child.tagName === "w:t" || child.tagName === "w:delText") && last_t && last_t.tagName === child.tagName) {
5906
- const t1 = last_t.textContent || "";
5907
- const t2 = child.textContent || "";
5908
- const combined = t1 + t2;
5909
- last_t.textContent = combined;
5910
- if (combined.trim() !== combined) {
5911
- last_t.setAttribute("xml:space", "preserve");
5912
- }
5913
- } else {
5914
- curr.appendChild(child);
5915
- if (child.tagName === "w:t" || child.tagName === "w:delText") {
5916
- last_t = child;
5917
- }
5918
- }
5919
- }
5920
- container.removeChild(nxt);
5921
- children.splice(i + 1, 1);
5922
- count++;
5923
- continue;
5924
- }
5925
- }
5926
- }
5927
- if (["w:ins", "w:del", "w:hyperlink", "w:sdt", "w:smartTag", "w:fldSimple", "w:sdtContent"].includes(curr.tagName)) {
5928
- coalesceContainer(curr);
5929
- }
5930
- i++;
6158
+ if (heading_indices.length === 0) return [];
6159
+ const nodes = [];
6160
+ for (let h_pos = 0; h_pos < heading_indices.length; h_pos++) {
6161
+ const rec_idx = heading_indices[h_pos];
6162
+ const rec = block_records[rec_idx];
6163
+ const paragraph = rec.item;
6164
+ const level = _heading_level(paragraph);
6165
+ const text = _heading_text(paragraph, comments_map);
6166
+ const style = _determine_heading_style(paragraph);
6167
+ const owned_end = _find_owned_end(
6168
+ block_records,
6169
+ heading_indices,
6170
+ h_pos,
6171
+ level
6172
+ );
6173
+ const owned_blocks = block_records.slice(rec_idx + 1, owned_end);
6174
+ const has_table = _direct_has_table(block_records, rec_idx + 1, owned_end);
6175
+ const footnote_ids = _collect_footnote_ids(owned_blocks);
6176
+ const page_num = _offset_to_page(rec.start_offset, body_page_offsets);
6177
+ nodes.push({ level, text, page: page_num, style, has_table, footnote_ids });
6178
+ }
6179
+ return nodes;
6180
+ }
6181
+ function _direct_has_table(block_records, range_start, range_end) {
6182
+ for (let idx = range_start; idx < range_end; idx++) {
6183
+ const rec = block_records[idx];
6184
+ if (rec.is_paragraph && _is_heading(rec.item)) return false;
6185
+ if (rec.is_table) return true;
6186
+ }
6187
+ return false;
6188
+ }
6189
+ function _walk_doc_body(doc, comments_map) {
6190
+ const parts = Array.from(iter_document_parts(doc));
6191
+ let body_start_offset = 0;
6192
+ let body_part = null;
6193
+ for (const part of parts) {
6194
+ if (part === doc) {
6195
+ body_part = part;
6196
+ break;
5931
6197
  }
5932
- if (children.length > 0) {
5933
- const last = children[children.length - 1];
5934
- if (["w:ins", "w:del", "w:hyperlink", "w:sdt", "w:smartTag", "w:fldSimple", "w:sdtContent"].includes(last.tagName)) {
5935
- coalesceContainer(last);
5936
- }
6198
+ const part_text = _project_part(part, comments_map);
6199
+ if (part_text) {
6200
+ if (body_start_offset > 0) body_start_offset += 2;
6201
+ body_start_offset += part_text.length;
5937
6202
  }
5938
6203
  }
5939
- const paragraphs = findAllDescendants(doc.element, "w:p");
5940
- for (const p of paragraphs) coalesceContainer(p);
5941
- return count ? [`Adjacent identical runs coalesced: ${count}`] : [];
5942
- }
5943
- function strip_rsid(doc) {
5944
- let count = 0;
5945
- const rsidAttrs = ["w:rsidR", "w:rsidRPr", "w:rsidRDefault", "w:rsidP", "w:rsidDel", "w:rsidSect", "w:rsidTr"];
5946
- const all = doc.element.getElementsByTagName("*");
5947
- for (let i = 0; i < all.length; i++) {
5948
- for (const attr of rsidAttrs) {
5949
- if (all[i].hasAttribute(attr)) {
5950
- all[i].removeAttribute(attr);
5951
- count++;
5952
- }
6204
+ if (!body_part) {
6205
+ body_part = doc;
6206
+ body_start_offset = 0;
6207
+ } else {
6208
+ if (body_start_offset > 0) body_start_offset += 2;
6209
+ }
6210
+ const records = [];
6211
+ let cursor = body_start_offset;
6212
+ let is_first_block = true;
6213
+ for (const item of iter_block_items(body_part)) {
6214
+ if (item instanceof Paragraph) {
6215
+ const prefix = get_paragraph_prefix(item);
6216
+ const p_text = build_paragraph_text(item, comments_map, false);
6217
+ const block_len = (prefix + p_text).length;
6218
+ if (!is_first_block) cursor += 2;
6219
+ records.push({
6220
+ item,
6221
+ is_paragraph: true,
6222
+ is_table: false,
6223
+ start_offset: cursor,
6224
+ projected_length: block_len
6225
+ });
6226
+ cursor += block_len;
6227
+ is_first_block = false;
6228
+ } else if (item instanceof Table) {
6229
+ const table_text = extract_table(item, comments_map, false, 0);
6230
+ const block_len = table_text ? table_text.length : 0;
6231
+ if (!is_first_block) cursor += 2;
6232
+ const table_start = cursor;
6233
+ records.push({
6234
+ item,
6235
+ is_paragraph: false,
6236
+ is_table: true,
6237
+ start_offset: table_start,
6238
+ projected_length: block_len
6239
+ });
6240
+ _record_table_inner_blocks_lite(item, table_start, records, comments_map);
6241
+ cursor += block_len;
6242
+ is_first_block = false;
5953
6243
  }
5954
6244
  }
5955
- const rsidsElements = findAllDescendants(doc.element, "w:rsids");
5956
- for (const el of rsidsElements) {
5957
- if (el.parentNode) {
5958
- el.parentNode.removeChild(el);
5959
- count++;
6245
+ return records;
6246
+ }
6247
+ function _compute_inner_block_offset(table, target_paragraph, table_start_offset, comments_map) {
6248
+ const target_el = target_paragraph._element;
6249
+ let cursor = table_start_offset;
6250
+ let rows_processed = 0;
6251
+ for (const row of table.rows) {
6252
+ if (rows_processed > 0) cursor += 1;
6253
+ const seen_cells = /* @__PURE__ */ new Set();
6254
+ let cells_in_row = 0;
6255
+ for (const cell of row.cells) {
6256
+ if (seen_cells.has(cell)) continue;
6257
+ seen_cells.add(cell);
6258
+ if (cells_in_row > 0) cursor += 3;
6259
+ const [new_cursor, found] = _walk_cell_for_offset(
6260
+ cell,
6261
+ target_el,
6262
+ cursor,
6263
+ comments_map
6264
+ );
6265
+ if (found) return new_cursor;
6266
+ cursor = new_cursor;
6267
+ cells_in_row++;
5960
6268
  }
6269
+ rows_processed++;
5961
6270
  }
5962
- return count ? [`rsid attributes: ${count} removed`] : [];
6271
+ return table_start_offset;
5963
6272
  }
5964
- function strip_para_ids(doc) {
5965
- let count = 0;
5966
- const attrs = ["w14:paraId", "w14:textId"];
5967
- const all = doc.element.getElementsByTagName("*");
5968
- for (let i = 0; i < all.length; i++) {
5969
- for (const attr of attrs) {
5970
- if (all[i].hasAttribute(attr)) {
5971
- all[i].removeAttribute(attr);
5972
- count++;
6273
+ function _walk_cell_for_offset(cell, target_el, cell_start_cursor, comments_map) {
6274
+ let cursor = cell_start_cursor;
6275
+ let is_first_block = true;
6276
+ for (const inner_item of iter_block_items(cell)) {
6277
+ if (!is_first_block) cursor += 2;
6278
+ if (inner_item instanceof Paragraph) {
6279
+ if (inner_item._element === target_el) return [cursor, true];
6280
+ const prefix = get_paragraph_prefix(inner_item);
6281
+ const p_text = build_paragraph_text(inner_item, comments_map, false);
6282
+ cursor += (prefix + p_text).length;
6283
+ } else if (inner_item instanceof Table) {
6284
+ const nested_offset = _compute_inner_block_offset(
6285
+ inner_item,
6286
+ new Paragraph(target_el, null),
6287
+ cursor,
6288
+ comments_map
6289
+ );
6290
+ if (nested_offset !== cursor) {
6291
+ if (_element_is_descendant(target_el, inner_item._element))
6292
+ return [nested_offset, true];
5973
6293
  }
6294
+ const table_text = extract_table(inner_item, comments_map, false, 0);
6295
+ cursor += table_text ? table_text.length : 0;
5974
6296
  }
6297
+ is_first_block = false;
5975
6298
  }
5976
- return count ? [`Paragraph/text IDs: ${count} removed`] : [];
6299
+ return [cursor, false];
5977
6300
  }
5978
- function strip_proof_errors(doc) {
5979
- const elements = findAllDescendants(doc.element, "w:proofErr");
5980
- elements.forEach((el) => el.parentNode?.removeChild(el));
5981
- return elements.length ? [`Spell check markers: ${elements.length} removed`] : [];
6301
+ function _element_is_descendant(target_el, ancestor_el) {
6302
+ let cur = target_el.parentNode;
6303
+ while (cur) {
6304
+ if (cur === ancestor_el) return true;
6305
+ cur = cur.parentNode;
6306
+ }
6307
+ return false;
5982
6308
  }
5983
- function strip_empty_properties(doc) {
5984
- let count = 0;
5985
- for (const tag of ["w:rPr", "w:pPr"]) {
5986
- const elements = findAllDescendants(doc.element, tag);
5987
- for (const el of elements) {
5988
- if (el.childNodes.length === 0 || el.childNodes.length === 1 && el.childNodes[0].nodeType === 3 && !el.childNodes[0].textContent?.trim()) {
5989
- el.parentNode?.removeChild(el);
5990
- count++;
6309
+ function _record_table_inner_blocks_lite(table, inherited_offset, records, comments_map) {
6310
+ const seen_cells = /* @__PURE__ */ new Set();
6311
+ for (const row of table.rows) {
6312
+ for (const cell of row.cells) {
6313
+ if (seen_cells.has(cell)) continue;
6314
+ seen_cells.add(cell);
6315
+ for (const inner_item of iter_block_items(cell)) {
6316
+ if (inner_item instanceof Paragraph) {
6317
+ const true_offset = _is_heading(inner_item) ? _compute_inner_block_offset(
6318
+ table,
6319
+ inner_item,
6320
+ inherited_offset,
6321
+ comments_map
6322
+ ) : inherited_offset;
6323
+ records.push({
6324
+ item: inner_item,
6325
+ is_paragraph: true,
6326
+ is_table: false,
6327
+ start_offset: true_offset,
6328
+ projected_length: 0
6329
+ });
6330
+ } else if (inner_item instanceof Table) {
6331
+ records.push({
6332
+ item: inner_item,
6333
+ is_paragraph: false,
6334
+ is_table: true,
6335
+ start_offset: inherited_offset,
6336
+ projected_length: 0
6337
+ });
6338
+ _record_table_inner_blocks_lite(
6339
+ inner_item,
6340
+ inherited_offset,
6341
+ records,
6342
+ comments_map
6343
+ );
6344
+ }
5991
6345
  }
5992
6346
  }
5993
6347
  }
5994
- return count ? [`Empty property elements: ${count} removed`] : [];
5995
6348
  }
5996
- function strip_hidden_text(doc) {
5997
- let count = 0;
5998
- const elements = findAllDescendants(doc.element, "w:rPr");
5999
- for (const rPr of elements) {
6000
- if (findChild(rPr, "w:vanish") || findChild(rPr, "w:webHidden")) {
6001
- const run = rPr.parentNode;
6002
- if (run && run.tagName === "w:r" && run.parentNode) {
6003
- run.parentNode.removeChild(run);
6004
- count++;
6005
- }
6349
+ function _project_part(part, comments_map) {
6350
+ const blocks = [];
6351
+ const c_type = part.constructor.name;
6352
+ if (c_type === "NotesPart") {
6353
+ const header = part.note_type === "fn" ? "## Footnotes" : "## Endnotes";
6354
+ blocks.push(`---
6355
+ ${header}`);
6356
+ }
6357
+ let is_first_para = true;
6358
+ for (const item of iter_block_items(part)) {
6359
+ if (item.constructor.name === "FootnoteItem") {
6360
+ const fn_text = _project_part(item, comments_map);
6361
+ if (fn_text) blocks.push(fn_text);
6362
+ } else if (item instanceof Paragraph) {
6363
+ let prefix = get_paragraph_prefix(item);
6364
+ if (is_first_para && c_type === "FootnoteItem")
6365
+ prefix = `[^${part.note_type}-${part.id}]: ${prefix}`;
6366
+ const p_text = build_paragraph_text(item, comments_map, false);
6367
+ blocks.push(prefix + p_text);
6368
+ is_first_para = false;
6369
+ } else if (item instanceof Table) {
6370
+ const table_text = extract_table(item, comments_map, false, 0);
6371
+ if (table_text) blocks.push(table_text);
6372
+ is_first_para = false;
6006
6373
  }
6007
6374
  }
6008
- return count ? [`Hidden text runs: ${count} removed`] : [];
6375
+ return blocks.join("\n\n");
6009
6376
  }
6010
- function count_tracked_changes(doc) {
6011
- const ins = findAllDescendants(doc.element, "w:ins").length;
6012
- const del = findAllDescendants(doc.element, "w:del").length;
6013
- const fmt = findAllDescendants(doc.element, "w:rPrChange").length + findAllDescendants(doc.element, "w:pPrChange").length + findAllDescendants(doc.element, "w:sectPrChange").length;
6014
- return [ins, del, fmt];
6377
+ function _is_heading(paragraph) {
6378
+ return _HEADING_PREFIX_RE.test(get_paragraph_prefix(paragraph));
6015
6379
  }
6016
- function get_track_change_authors(doc) {
6017
- const authors = /* @__PURE__ */ new Set();
6018
- for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange", "w:sectPrChange"]) {
6019
- for (const el of findAllDescendants(doc.element, tag)) {
6020
- const author = el.getAttribute("w:author");
6021
- if (author) authors.add(author);
6022
- }
6023
- }
6024
- return authors;
6380
+ function _heading_passes_quality_filter(paragraph, comments_map) {
6381
+ const style = _determine_heading_style(paragraph);
6382
+ if (style !== "(heuristic)") return true;
6383
+ const text = _heading_text(paragraph, comments_map);
6384
+ if (!text) return false;
6385
+ const word_count = (text.match(/\w+/g) || []).length;
6386
+ return word_count >= _HEURISTIC_MIN_WORDS;
6025
6387
  }
6026
- function _getElementText(el) {
6027
- const texts = [];
6028
- const ts = findAllDescendants(el, "w:t");
6029
- for (const t of ts) if (t.textContent) texts.push(t.textContent);
6030
- const dts = findAllDescendants(el, "w:delText");
6031
- for (const dt of dts) if (dt.textContent) texts.push(dt.textContent);
6032
- return texts.join("");
6388
+ function _heading_level(paragraph) {
6389
+ const match = _HEADING_PREFIX_RE.exec(get_paragraph_prefix(paragraph));
6390
+ return match ? Math.min(match[1].length, 6) : 1;
6033
6391
  }
6034
- function _truncate(text, maxLen = 60) {
6035
- const clean = text.replace(/\n/g, " ").trim();
6036
- if (clean.length <= maxLen) return clean;
6037
- return clean.substring(0, maxLen - 3) + "...";
6392
+ function _heading_text(paragraph, comments_map) {
6393
+ const p_text = build_paragraph_text(paragraph, comments_map, false);
6394
+ let cleaned = _strip_critic_markup(p_text);
6395
+ cleaned = _strip_inline_formatting(cleaned);
6396
+ return cleaned.trim();
6038
6397
  }
6039
- function accept_all_tracked_changes(doc) {
6040
- const lines = [];
6041
- const insEls = findAllDescendants(doc.element, "w:ins");
6042
- const delEls = findAllDescendants(doc.element, "w:del");
6043
- for (const ins of insEls) {
6044
- const text = _getElementText(ins).trim();
6045
- if (text) lines.push(` Accepted insertion: "${_truncate(text, 60)}"`);
6046
- }
6047
- for (const del of delEls) {
6048
- const text = _getElementText(del).trim();
6049
- if (text) lines.push(` Accepted deletion of: "${_truncate(text, 60)}"`);
6050
- }
6051
- const engine = new RedlineEngine(doc);
6052
- engine.accept_all_revisions();
6053
- for (const tag of ["w:rPrChange", "w:pPrChange", "w:sectPrChange"]) {
6054
- for (const el of findAllDescendants(doc.element, tag)) {
6055
- el.parentNode?.removeChild(el);
6056
- }
6057
- }
6058
- const total = insEls.length + delEls.length;
6059
- if (total) {
6060
- return [`Tracked changes auto-accepted: ${total}`].concat(lines);
6061
- }
6062
- return [];
6398
+ function _strip_critic_markup(text) {
6399
+ if (!text) return "";
6400
+ text = text.replace(/\{--[\s\S]*?--\}/g, "");
6401
+ text = text.replace(/\{>>[\s\S]*?<<\}/g, "");
6402
+ text = text.replace(/\{\+\+([\s\S]*?)\+\+\}/g, "$1");
6403
+ text = text.replace(/\{==([\s\S]*?)==\}/g, "$1");
6404
+ return text;
6063
6405
  }
6064
- function get_comments_summary(doc) {
6065
- const data = extract_comments_data(doc.pkg);
6066
- const comments = [];
6067
- let openCount = 0;
6068
- let resolvedCount = 0;
6069
- for (const [cId, info] of Object.entries(data)) {
6070
- if (info.resolved) resolvedCount++;
6071
- else openCount++;
6072
- comments.push({ id: cId, ...info });
6073
- }
6074
- return { total: comments.length, open: openCount, resolved: resolvedCount, comments };
6406
+ function _strip_inline_formatting(text) {
6407
+ if (!text) return "";
6408
+ text = text.replace(/\*\*(.+?)\*\*/g, "$1");
6409
+ text = text.replace(/__(.+?)__/g, "$1");
6410
+ text = text.replace(/(?<!\w)_(\S(?:.*?\S)?)_(?!\w)/g, "$1");
6411
+ return text;
6075
6412
  }
6076
- function remove_all_comments(doc) {
6077
- const data = extract_comments_data(doc.pkg);
6078
- const keys = Object.keys(data);
6079
- if (keys.length === 0) return [];
6080
- const lines = [];
6081
- const cm = new CommentsManager(doc);
6082
- for (const [cId, info] of Object.entries(data)) {
6083
- const status = info.resolved ? "[Resolved]" : "[Open]";
6084
- lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
6085
- cm.deleteComment(cId);
6086
- }
6087
- for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
6088
- for (const el of findAllDescendants(doc.element, tag)) {
6089
- el.parentNode?.removeChild(el);
6090
- }
6413
+ function _determine_heading_style(paragraph) {
6414
+ const [style_cache, default_pstyle] = _get_style_cache(
6415
+ paragraph._parent.part || paragraph._parent
6416
+ );
6417
+ const pPr = findChild(paragraph._element, "w:pPr");
6418
+ let style_id = default_pstyle;
6419
+ if (pPr) {
6420
+ const pStyle = findChild(pPr, "w:pStyle");
6421
+ if (pStyle) style_id = pStyle.getAttribute("w:val") || default_pstyle;
6091
6422
  }
6092
- const refs = findAllDescendants(doc.element, "w:commentReference");
6093
- for (const ref of refs) {
6094
- const parent = ref.parentNode;
6095
- if (parent) {
6096
- if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
6097
- const nonRprChildren = Array.from(parent.childNodes).filter(
6098
- (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
6099
- );
6100
- if (nonRprChildren.length <= 1) {
6101
- parent.parentNode?.removeChild(parent);
6102
- } else {
6103
- parent.removeChild(ref);
6104
- }
6105
- } else {
6106
- parent.removeChild(ref);
6107
- }
6423
+ let outline_level = null;
6424
+ let outline_level_from_style = false;
6425
+ if (pPr) {
6426
+ const oLvl = findChild(pPr, "w:outlineLvl");
6427
+ if (oLvl && /^\d+$/.test(oLvl.getAttribute("w:val") || "")) {
6428
+ outline_level = parseInt(oLvl.getAttribute("w:val"), 10);
6108
6429
  }
6109
6430
  }
6110
- const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
6111
- const openCount = Object.values(data).filter((c) => !c.resolved).length;
6112
- return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
6113
- }
6114
- function eject_comment_parts(doc) {
6115
- const pkg = doc.pkg;
6116
- const comment_partnames = /* @__PURE__ */ new Set();
6117
- for (const part of pkg.parts) {
6118
- if (part.partname.toLowerCase().includes("comments")) {
6119
- comment_partnames.add(part.partname);
6120
- const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
6121
- const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
6122
- comment_partnames.add(withSlash);
6123
- comment_partnames.add(withoutSlash);
6124
- }
6431
+ if (outline_level === null && style_id && style_cache && style_cache[style_id]) {
6432
+ outline_level = style_cache[style_id].outline_level;
6433
+ outline_level_from_style = true;
6125
6434
  }
6126
- if (comment_partnames.size === 0) return;
6127
- for (const part of pkg.parts) {
6128
- if (part.partname.endsWith(".rels")) {
6129
- const rels = findAllDescendants(part._element, "Relationship");
6130
- const toRemove = [];
6131
- for (const rel of rels) {
6132
- const target = rel.getAttribute("Target") || "";
6133
- if (target.toLowerCase().includes("comments")) {
6134
- toRemove.push(rel);
6135
- const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
6136
- const sourcePart = pkg.getPartByPath(sourcePath);
6137
- if (sourcePart) {
6138
- const relId = rel.getAttribute("Id");
6139
- if (relId) sourcePart.rels.delete(relId);
6140
- }
6141
- }
6142
- }
6143
- for (const relEl of toRemove) {
6144
- relEl.parentNode?.removeChild(relEl);
6145
- }
6435
+ const style_name = style_id && style_cache && style_cache[style_id] ? style_cache[style_id].name : style_id;
6436
+ let normalized_style_name = style_name;
6437
+ if (normalized_style_name && typeof normalized_style_name === "string") {
6438
+ if (normalized_style_name.toLowerCase().startsWith("heading")) {
6439
+ normalized_style_name = normalized_style_name.replace(/^heading/i, "Heading");
6440
+ } else if (normalized_style_name.toLowerCase() === "title") {
6441
+ normalized_style_name = "Title";
6146
6442
  }
6147
6443
  }
6148
- const ctPart = pkg.getPartByPath("[Content_Types].xml");
6149
- if (ctPart) {
6150
- const overrides = findAllDescendants(ctPart._element, "Override");
6151
- const toRemove = [];
6152
- for (const override of overrides) {
6153
- const partName = override.getAttribute("PartName") || "";
6154
- if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
6155
- toRemove.push(override);
6156
- }
6157
- }
6158
- for (const overrideEl of toRemove) {
6159
- overrideEl.parentNode?.removeChild(overrideEl);
6444
+ if (outline_level_from_style && outline_level !== null) {
6445
+ const is_heading_or_title = normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title");
6446
+ if (!is_heading_or_title) {
6447
+ outline_level = null;
6160
6448
  }
6161
6449
  }
6162
- pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
6163
- for (const key of Object.keys(pkg.unzipped)) {
6164
- if (key.toLowerCase().includes("comments")) {
6165
- delete pkg.unzipped[key];
6450
+ if (outline_level !== null && outline_level >= 0 && outline_level <= 8) {
6451
+ if (normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title")) {
6452
+ return normalized_style_name;
6166
6453
  }
6454
+ return "(outline_level)";
6167
6455
  }
6456
+ if (normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title"))
6457
+ return normalized_style_name;
6458
+ if (normalized_style_name && /Heading[ ]?([1-6])(?![0-9])/.test(normalized_style_name))
6459
+ return normalized_style_name;
6460
+ return "(heuristic)";
6168
6461
  }
6169
- function replace_comment_authors(doc, newAuthor) {
6170
- const cm = new CommentsManager(doc);
6171
- if (!cm.commentsPart) return [];
6172
- const original = /* @__PURE__ */ new Set();
6173
- const comments = findAllDescendants(cm.commentsPart._element, "w:comment");
6174
- for (const c of comments) {
6175
- const author = c.getAttribute("w:author");
6176
- if (author) {
6177
- original.add(author);
6178
- c.setAttribute("w:author", newAuthor);
6179
- }
6180
- if (c.hasAttribute("w:initials")) {
6181
- const initials = newAuthor.split(" ").filter(Boolean).map((p) => p[0]).join("").toUpperCase();
6182
- c.setAttribute("w:initials", initials);
6183
- }
6462
+ function _find_owned_end(block_records, heading_indices, current_h_pos, current_level) {
6463
+ for (let next_h_pos = current_h_pos + 1; next_h_pos < heading_indices.length; next_h_pos++) {
6464
+ const next_idx = heading_indices[next_h_pos];
6465
+ if (_heading_level(block_records[next_idx].item) <= current_level)
6466
+ return next_idx;
6184
6467
  }
6185
- return original.size ? [`Comment authors replaced: ${Array.from(original).sort().join(", ")} \u2192 "${newAuthor}"`] : [];
6468
+ return block_records.length;
6186
6469
  }
6187
- function replace_change_authors(doc, newAuthor) {
6188
- const original = /* @__PURE__ */ new Set();
6189
- for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange"]) {
6190
- for (const el of findAllDescendants(doc.element, tag)) {
6191
- const author = el.getAttribute("w:author");
6192
- if (author) {
6193
- original.add(author);
6194
- el.setAttribute("w:author", newAuthor);
6470
+ function _collect_footnote_ids(owned_blocks) {
6471
+ const seen = /* @__PURE__ */ new Set();
6472
+ const ordered = [];
6473
+ for (const rec of owned_blocks) {
6474
+ if (!rec.is_paragraph) continue;
6475
+ for (const event of iter_paragraph_content(rec.item)) {
6476
+ if (!("type" in event)) continue;
6477
+ let fn_id = "";
6478
+ if (event.type === "footnote") fn_id = `fn-${event.id}`;
6479
+ else if (event.type === "endnote") fn_id = `en-${event.id}`;
6480
+ else continue;
6481
+ if (!seen.has(fn_id)) {
6482
+ seen.add(fn_id);
6483
+ ordered.push(fn_id);
6195
6484
  }
6196
6485
  }
6197
6486
  }
6198
- return original.size ? [`Track change authors replaced: ${Array.from(original).sort().join(", ")} \u2192 "${newAuthor}"`] : [];
6487
+ return ordered;
6199
6488
  }
6200
- function normalize_change_dates(doc) {
6201
- let count = 0;
6202
- const fixed = "2025-01-01T00:00:00Z";
6203
- for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange"]) {
6204
- for (const el of findAllDescendants(doc.element, tag)) {
6205
- if (el.hasAttribute("w:date")) {
6206
- el.setAttribute("w:date", fixed);
6207
- count++;
6489
+ function _offset_to_page(offset, body_page_offsets) {
6490
+ if (!body_page_offsets || body_page_offsets.length === 0) return 1;
6491
+ let page = 1;
6492
+ for (let i = 0; i < body_page_offsets.length; i++) {
6493
+ if (offset >= body_page_offsets[i]) page = i + 1;
6494
+ else break;
6495
+ }
6496
+ return page;
6497
+ }
6498
+ function _extract_outline_fast(doc, projected_body, body_page_offsets, paragraph_offsets) {
6499
+ const paragraphs_and_tables = [];
6500
+ const seen_cells = /* @__PURE__ */ new Set();
6501
+ function walk(container) {
6502
+ for (const item of iter_block_items(container)) {
6503
+ const i_type = item.constructor.name;
6504
+ if (i_type === "FootnoteItem") {
6505
+ walk(item);
6506
+ } else if (item instanceof Paragraph) {
6507
+ paragraphs_and_tables.push(["p", item]);
6508
+ } else if (item instanceof Table) {
6509
+ paragraphs_and_tables.push(["t", item]);
6510
+ for (const row of item.rows) {
6511
+ for (const cell of row.cells) {
6512
+ if (seen_cells.has(cell._element)) {
6513
+ continue;
6514
+ }
6515
+ seen_cells.add(cell._element);
6516
+ walk(cell);
6517
+ }
6518
+ }
6208
6519
  }
6209
6520
  }
6210
6521
  }
6211
- return count ? [`Track change timestamps: ${count} normalized`] : [];
6212
- }
6213
- function scrub_doc_properties(doc) {
6214
- const lines = [];
6215
- const corePart = doc.pkg.getPartByPath("docProps/core.xml");
6216
- if (corePart) {
6217
- const creators = findDescendantsByLocalName(corePart._element, "creator");
6218
- creators.forEach((c) => {
6219
- if (c.textContent) {
6220
- lines.push(`Author: ${c.textContent}`);
6221
- c.textContent = "";
6522
+ walk(doc);
6523
+ const heading_indices = [];
6524
+ for (let idx = 0; idx < paragraphs_and_tables.length; idx++) {
6525
+ const [kind, item] = paragraphs_and_tables[idx];
6526
+ if (kind !== "p") continue;
6527
+ let hasOffset = false;
6528
+ if (paragraph_offsets instanceof Map) {
6529
+ hasOffset = paragraph_offsets.has(item._element);
6530
+ } else {
6531
+ hasOffset = item._element in paragraph_offsets;
6532
+ }
6533
+ if (!hasOffset) {
6534
+ continue;
6535
+ }
6536
+ if (!_is_heading(item)) continue;
6537
+ if (!_heading_passes_quality_filter_fast(item, projected_body, paragraph_offsets)) continue;
6538
+ heading_indices.push(idx);
6539
+ }
6540
+ if (heading_indices.length === 0) return [];
6541
+ const nodes = [];
6542
+ for (let h_pos = 0; h_pos < heading_indices.length; h_pos++) {
6543
+ const item_idx = heading_indices[h_pos];
6544
+ const paragraph = paragraphs_and_tables[item_idx][1];
6545
+ const level = _heading_level(paragraph);
6546
+ const text = _heading_text_fast(paragraph, projected_body, paragraph_offsets);
6547
+ const style = _determine_heading_style(paragraph);
6548
+ let owned_end = item_idx;
6549
+ for (let next_h_pos = h_pos + 1; next_h_pos < heading_indices.length; next_h_pos++) {
6550
+ const next_idx = heading_indices[next_h_pos];
6551
+ const next_paragraph = paragraphs_and_tables[next_idx][1];
6552
+ if (_heading_level(next_paragraph) <= level) {
6553
+ owned_end = next_idx;
6554
+ break;
6222
6555
  }
6223
- });
6224
- const modifiers = findDescendantsByLocalName(corePart._element, "lastModifiedBy");
6225
- modifiers.forEach((c) => {
6226
- if (c.textContent) {
6227
- lines.push(`Last modified by: ${c.textContent}`);
6228
- c.textContent = "";
6556
+ }
6557
+ if (owned_end === item_idx) {
6558
+ owned_end = paragraphs_and_tables.length;
6559
+ }
6560
+ const owned = paragraphs_and_tables.slice(item_idx + 1, owned_end);
6561
+ let has_table = false;
6562
+ for (const [kind2, item2] of owned) {
6563
+ if (kind2 === "p" && _is_heading(item2)) {
6564
+ break;
6229
6565
  }
6230
- });
6231
- const revisions = findDescendantsByLocalName(corePart._element, "revision");
6232
- revisions.forEach((c) => {
6233
- if (c.textContent && parseInt(c.textContent) > 1) {
6234
- lines.push(`Revision count: ${c.textContent} \u2192 1`);
6235
- c.textContent = "1";
6566
+ if (kind2 === "t") {
6567
+ has_table = true;
6568
+ break;
6236
6569
  }
6237
- });
6238
- }
6239
- const appPart = doc.pkg.getPartByPath("docProps/app.xml");
6240
- if (appPart) {
6241
- const docEl = appPart._element;
6242
- const intFields = ["TotalTime", "Words", "Characters", "Paragraphs", "Lines", "CharactersWithSpaces"];
6243
- for (const f of intFields) {
6244
- findDescendantsByLocalName(docEl, f).forEach((el) => {
6245
- if (el.textContent && el.textContent !== "0") {
6246
- if (f === "TotalTime") lines.push(`Total editing time: ${el.textContent} minutes`);
6247
- el.textContent = "0";
6248
- }
6249
- });
6250
6570
  }
6251
- const strFields = ["Template", "Manager", "Company"];
6252
- for (const f of strFields) {
6253
- findDescendantsByLocalName(docEl, f).forEach((el) => {
6254
- if (el.textContent) {
6255
- lines.push(`${f}: ${el.textContent}`);
6256
- el.textContent = "";
6257
- }
6258
- });
6571
+ const footnote_ids = _collect_footnote_ids_fast(owned);
6572
+ let para_offset;
6573
+ if (paragraph_offsets instanceof Map) {
6574
+ para_offset = paragraph_offsets.get(paragraph._element);
6575
+ } else {
6576
+ para_offset = paragraph_offsets[paragraph._element];
6259
6577
  }
6260
- }
6261
- return lines.length ? ["Metadata scrubbed:", ...lines.map((l) => ` ${l}`)] : [];
6262
- }
6263
- function scrub_timestamps(doc) {
6264
- let modified = false;
6265
- const epoch = "1970-01-01T00:00:00Z";
6266
- const corePart = doc.pkg.getPartByPath("docProps/core.xml");
6267
- if (corePart) {
6268
- for (const tag of ["created", "modified", "lastPrinted"]) {
6269
- findDescendantsByLocalName(corePart._element, tag).forEach((el) => {
6270
- if (el.textContent && el.textContent !== epoch) {
6271
- el.textContent = epoch;
6272
- modified = true;
6273
- }
6274
- });
6578
+ let page_num = 1;
6579
+ if (para_offset !== void 0) {
6580
+ const [start_offset] = para_offset;
6581
+ page_num = _offset_to_page(start_offset, body_page_offsets);
6275
6582
  }
6583
+ nodes.push({
6584
+ level,
6585
+ text,
6586
+ page: page_num,
6587
+ style,
6588
+ has_table,
6589
+ footnote_ids
6590
+ });
6276
6591
  }
6277
- return modified ? ["Timestamps normalized to epoch"] : [];
6592
+ return nodes;
6278
6593
  }
6279
- function strip_custom_xml(doc) {
6280
- const customParts = doc.pkg.parts.filter((p) => p.partname.includes("/customXml"));
6281
- if (customParts.length === 0) return [];
6282
- const partnames = new Set(customParts.map((p) => p.partname));
6283
- doc.pkg.parts = doc.pkg.parts.filter((p) => !partnames.has(p.partname));
6284
- const removeRelationsTo = (relsPart) => {
6285
- const toRemove = [];
6286
- for (const rel of findAllDescendants(relsPart._element, "Relationship")) {
6287
- const target = rel.getAttribute("Target");
6288
- if (target && target.includes("customXml")) toRemove.push(rel);
6289
- }
6290
- toRemove.forEach((r) => r.parentNode?.removeChild(r));
6291
- };
6292
- const rootRels = doc.pkg.getPartByPath("_rels/.rels");
6293
- if (rootRels) removeRelationsTo(rootRels);
6294
- const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
6295
- if (docRels) removeRelationsTo(docRels);
6296
- for (const sdtPr of findAllDescendants(doc.element, "w:sdtPr")) {
6297
- findChildren(sdtPr, "w:dataBinding").forEach((b) => sdtPr.removeChild(b));
6594
+ function _heading_passes_quality_filter_fast(paragraph, projected_body, paragraph_offsets) {
6595
+ const style = _determine_heading_style(paragraph);
6596
+ if (style !== "(heuristic)") return true;
6597
+ const text = _heading_text_fast(paragraph, projected_body, paragraph_offsets);
6598
+ if (!text) return false;
6599
+ const words = text.match(/\w+/g) || [];
6600
+ return words.length >= _HEURISTIC_MIN_WORDS;
6601
+ }
6602
+ function _heading_text_fast(paragraph, projected_body, paragraph_offsets) {
6603
+ let offset;
6604
+ if (paragraph_offsets instanceof Map) {
6605
+ offset = paragraph_offsets.get(paragraph._element);
6606
+ } else {
6607
+ offset = paragraph_offsets[paragraph._element];
6298
6608
  }
6299
- return [`Custom XML parts: ${customParts.length} removed`];
6609
+ if (offset === void 0) {
6610
+ return "";
6611
+ }
6612
+ const [start, length] = offset;
6613
+ const raw = projected_body.substring(start, start + length);
6614
+ let cleaned = _strip_critic_markup(raw);
6615
+ cleaned = _strip_inline_formatting(cleaned);
6616
+ cleaned = cleaned.replace(/^#+\s+/, "");
6617
+ return cleaned.trim();
6300
6618
  }
6301
- function strip_image_alt_text(doc) {
6302
- let count = 0;
6303
- for (const docPr of findDescendantsByLocalName(doc.element, "docPr")) {
6304
- const descr = docPr.getAttribute("descr");
6305
- if (descr) {
6306
- const isShort = descr.length < 10;
6307
- const isFile = descr.includes(".") && descr.length < 60;
6308
- if (isShort || isFile) {
6309
- docPr.removeAttribute("descr");
6310
- count++;
6619
+ function _collect_footnote_ids_fast(owned_items) {
6620
+ const seen = /* @__PURE__ */ new Set();
6621
+ const ordered = [];
6622
+ for (const [kind, item] of owned_items) {
6623
+ if (kind !== "p") continue;
6624
+ for (const event of iter_paragraph_content(item)) {
6625
+ if (!("type" in event)) continue;
6626
+ let fn_id = "";
6627
+ if (event.type === "footnote") fn_id = `fn-${event.id}`;
6628
+ else if (event.type === "endnote") fn_id = `en-${event.id}`;
6629
+ else continue;
6630
+ if (!seen.has(fn_id)) {
6631
+ seen.add(fn_id);
6632
+ ordered.push(fn_id);
6311
6633
  }
6312
6634
  }
6313
6635
  }
6314
- return count ? [`Image alt text: ${count} auto-generated descriptions removed`] : [];
6636
+ return ordered;
6315
6637
  }
6316
- function audit_hyperlinks(doc) {
6317
- const internal = ["sharepoint.com", "onedrive.com", ".internal", "intranet", "localhost", "10.", "192.168.", "172.16."];
6318
- const warnings = [];
6319
- const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
6320
- for (const rel of findAllDescendants(docRels._element, "Relationship")) {
6321
- if (rel.getAttribute("TargetMode") === "External") {
6322
- const url = rel.getAttribute("Target") || "";
6323
- for (const pattern of internal) {
6324
- if (url.toLowerCase().includes(pattern.toLowerCase())) {
6325
- warnings.push(`Hyperlink targets internal URL: ${_truncate(url, 80)}`);
6326
- break;
6638
+
6639
+ // src/sanitize/report.ts
6640
+ var SanitizeReport = class {
6641
+ filename;
6642
+ mode;
6643
+ author;
6644
+ tracked_changes_found = 0;
6645
+ tracked_changes_accepted = 0;
6646
+ tracked_changes_kept = 0;
6647
+ change_lines = [];
6648
+ comments_removed = 0;
6649
+ comments_kept = 0;
6650
+ removed_comment_lines = [];
6651
+ kept_comment_lines = [];
6652
+ metadata_lines = [];
6653
+ structural_lines = [];
6654
+ warnings = [];
6655
+ status = "clean";
6656
+ blocked_reason = null;
6657
+ constructor(filename, mode = "full", author = null) {
6658
+ this.filename = filename;
6659
+ this.mode = mode;
6660
+ this.author = author;
6661
+ }
6662
+ add_transform_lines(lines) {
6663
+ for (const line of lines) {
6664
+ const lower = line.toLowerCase();
6665
+ if (lower.includes("tracked change") || lower.includes("insertion") || lower.includes("deletion") || lower.includes("accepted")) {
6666
+ this.change_lines.push(line);
6667
+ } else if (lower.includes("comment") || lower.includes("[open]") || lower.includes("[resolved]")) {
6668
+ if (lower.includes("kept") || lower.includes("visible")) {
6669
+ this.kept_comment_lines.push(line);
6670
+ } else {
6671
+ this.removed_comment_lines.push(line);
6327
6672
  }
6673
+ } 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")) {
6674
+ this.metadata_lines.push(line);
6675
+ } else if (lower.includes("hyperlink") || lower.includes("warning")) {
6676
+ this.warnings.push(line);
6677
+ } else {
6678
+ this.structural_lines.push(line);
6328
6679
  }
6329
6680
  }
6330
6681
  }
6331
- return warnings;
6332
- }
6682
+ render() {
6683
+ const sep = "\u2550".repeat(50);
6684
+ const lines = [sep, `Finalization Report: ${this.filename}`];
6685
+ const flags = [];
6686
+ if (this.mode === "keep-markup") flags.push("--keep-markup");
6687
+ if (this.author) flags.push(`--author "${this.author}"`);
6688
+ if (this.tracked_changes_accepted > 0) flags.push("--accept-all");
6689
+ if (flags.length > 0) lines.push(flags.join(" "));
6690
+ lines.push(sep);
6691
+ if (this.status === "blocked") {
6692
+ lines.push("");
6693
+ lines.push(`BLOCKED: ${this.blocked_reason}`);
6694
+ lines.push(sep);
6695
+ return lines.join("\n");
6696
+ }
6697
+ if (this.mode === "keep-markup" && (this.tracked_changes_kept > 0 || this.comments_kept > 0)) {
6698
+ lines.push("");
6699
+ lines.push("VISIBLE TO COUNTERPARTY");
6700
+ if (this.tracked_changes_kept > 0) lines.push(` Tracked changes: ${this.tracked_changes_kept}`);
6701
+ if (this.comments_kept > 0) {
6702
+ lines.push(` Open comments: ${this.comments_kept}`);
6703
+ for (const cl of this.kept_comment_lines) lines.push(` ${cl}`);
6704
+ }
6705
+ if (this.author) lines.push(` Author on all markup: "${this.author}"`);
6706
+ }
6707
+ if (this.change_lines.length > 0) {
6708
+ lines.push("");
6709
+ lines.push("TRACKED CHANGES");
6710
+ for (const cl of this.change_lines) lines.push(` ${cl}`);
6711
+ }
6712
+ if (this.removed_comment_lines.length > 0) {
6713
+ lines.push("");
6714
+ lines.push("COMMENTS (stripped)");
6715
+ for (const cl of this.removed_comment_lines) lines.push(` ${cl}`);
6716
+ }
6717
+ if (this.metadata_lines.length > 0) {
6718
+ lines.push("");
6719
+ lines.push("METADATA");
6720
+ for (const ml of this.metadata_lines) lines.push(` ${ml}`);
6721
+ }
6722
+ if (this.structural_lines.length > 0) {
6723
+ lines.push("");
6724
+ lines.push("STRUCTURAL & PROTECTION");
6725
+ for (const sl of this.structural_lines) lines.push(` ${sl}`);
6726
+ }
6727
+ if (this.warnings.length > 0) {
6728
+ lines.push("");
6729
+ lines.push("WARNINGS");
6730
+ for (const w of this.warnings) lines.push(` \u26A0 ${w}`);
6731
+ }
6732
+ lines.push("");
6733
+ lines.push(sep);
6734
+ if (this.warnings.length > 0) {
6735
+ lines.push(`Result: CLEAN WITH WARNINGS (${this.warnings.length} warning${this.warnings.length > 1 ? "s" : ""})`);
6736
+ } else {
6737
+ lines.push(`Result: CLEAN (${this.tracked_changes_found} changes resolved, ${this.comments_removed} comments removed)`);
6738
+ }
6739
+ lines.push(sep);
6740
+ return lines.join("\n");
6741
+ }
6742
+ };
6333
6743
 
6334
6744
  // src/sanitize/core.ts
6335
6745
  async function finalize_document(doc, options) {