@adeu/core 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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(
@@ -3567,6 +3903,22 @@ var RedlineEngine = class {
3567
3903
  return errors;
3568
3904
  }
3569
3905
  process_batch(changes, dry_run = false) {
3906
+ if (Array.isArray(changes)) {
3907
+ changes = changes.map((item) => {
3908
+ if (typeof item === "string") {
3909
+ try {
3910
+ const parsed = JSON.parse(item);
3911
+ if (parsed !== null && typeof parsed === "object") {
3912
+ return parsed;
3913
+ }
3914
+ return item;
3915
+ } catch {
3916
+ return item;
3917
+ }
3918
+ }
3919
+ return item;
3920
+ });
3921
+ }
3570
3922
  if (dry_run) {
3571
3923
  const baselines = /* @__PURE__ */ new Map();
3572
3924
  for (const part of this.doc.pkg.parts) {
@@ -3633,6 +3985,9 @@ var RedlineEngine = class {
3633
3985
  if (this.clean_mapper) this.clean_mapper["_build_map"]();
3634
3986
  }
3635
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;
3636
3991
  const edits_reports = [];
3637
3992
  let applied_edits = 0;
3638
3993
  let skipped_edits = 0;
@@ -3656,9 +4011,8 @@ var RedlineEngine = class {
3656
4011
  });
3657
4012
  continue;
3658
4013
  }
3659
- const res = this.apply_edits([edit]);
3660
- const applied = res[0];
3661
- if (applied > 0) {
4014
+ const res = this.apply_edits([edit], page_offsets);
4015
+ if (edit._applied_status) {
3662
4016
  applied_edits++;
3663
4017
  const previews = this._build_edit_context_previews(edit);
3664
4018
  edits_reports.push({
@@ -3668,7 +4022,11 @@ var RedlineEngine = class {
3668
4022
  warning,
3669
4023
  error: null,
3670
4024
  critic_markup: previews[0],
3671
- 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"
3672
4030
  });
3673
4031
  } else {
3674
4032
  skipped_edits++;
@@ -3690,9 +4048,9 @@ var RedlineEngine = class {
3690
4048
  throw new BatchValidationError(errors);
3691
4049
  }
3692
4050
  const cloned_edits = edits.map((e) => JSON.parse(JSON.stringify(e)));
3693
- const res = this.apply_edits(cloned_edits);
3694
- applied_edits = res[0];
3695
- 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;
3696
4054
  for (const edit of cloned_edits) {
3697
4055
  const success = edit._applied_status || false;
3698
4056
  const error_msg = edit._error_msg || null;
@@ -3713,7 +4071,11 @@ var RedlineEngine = class {
3713
4071
  warning,
3714
4072
  error: error_msg,
3715
4073
  critic_markup,
3716
- 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"
3717
4079
  });
3718
4080
  }
3719
4081
  }
@@ -3729,9 +4091,13 @@ var RedlineEngine = class {
3729
4091
  version: "1.10.0"
3730
4092
  };
3731
4093
  }
3732
- apply_edits(edits) {
4094
+ apply_edits(edits, page_offsets = []) {
3733
4095
  let applied = 0;
3734
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
+ }
3735
4101
  const resolved_edits = [];
3736
4102
  for (const edit of edits) {
3737
4103
  edit._applied_status = false;
@@ -3833,6 +4199,19 @@ var RedlineEngine = class {
3833
4199
  const parent = edit._parent_edit_ref;
3834
4200
  if (parent) {
3835
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;
3836
4215
  }
3837
4216
  } else {
3838
4217
  skipped++;
@@ -3985,96 +4364,192 @@ var RedlineEngine = class {
3985
4364
  }
3986
4365
  return false;
3987
4366
  }
3988
- /**
3989
- * Returns the first match of `target_text` in the raw mapper that is NOT
3990
- * entirely contained within a tracked deletion (<w:del>). Tracked-deleted
3991
- * copies are not live, editable text, so an edit must resolve to a live
3992
- * occurrence even when a dead copy appears earlier in the document
3993
- * (BUG-23-5). Falls back to the plain first match when no live copy is
3994
- * found (e.g. fuzzy/normalized matches the span filter cannot align).
3995
- */
3996
- _first_live_match(target_text) {
3997
- const all = this.mapper.find_all_match_indices(target_text);
3998
- if (all.length <= 1) {
3999
- return this.mapper.find_match_index(target_text);
4000
- }
4001
- for (const [start, length] of all) {
4002
- const realSpans = this.mapper.spans.filter(
4003
- (s) => s.run !== null && s.end > start && s.start < start + length
4004
- );
4005
- if (realSpans.length === 0) return [start, length];
4006
- if (realSpans.some((s) => !s.del_id)) return [start, length];
4007
- }
4008
- return this.mapper.find_match_index(target_text);
4009
- }
4010
4367
  _pre_resolve_heuristic_edit(edit) {
4011
4368
  if (!edit.target_text) return null;
4012
- 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);
4013
4372
  let use_clean_map = false;
4014
- if (start_idx === -1) {
4373
+ if (matches.length === 0) {
4015
4374
  if (!this.clean_mapper)
4016
4375
  this.clean_mapper = new DocumentMapper(this.doc, true);
4017
- [start_idx, match_len] = this.clean_mapper.find_match_index(
4018
- edit.target_text
4019
- );
4020
- 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;
4021
4378
  else return null;
4022
4379
  }
4023
4380
  const active_mapper = use_clean_map ? this.clean_mapper : this.mapper;
4024
- const effective_new_text = edit.new_text || "";
4025
- const actual_doc_text = this.mapper.full_text.substring(
4026
- start_idx,
4027
- start_idx + match_len
4028
- );
4029
- if (actual_doc_text === effective_new_text || edit.target_text === effective_new_text) {
4030
- return {
4031
- type: "modify",
4032
- target_text: actual_doc_text,
4033
- new_text: actual_doc_text,
4034
- comment: edit.comment,
4035
- _match_start_index: start_idx,
4036
- _internal_op: "COMMENT_ONLY",
4037
- _active_mapper_ref: active_mapper
4038
- };
4039
- }
4040
- let effective_op = "";
4041
- let final_target = "";
4042
- let final_new = "";
4043
- let effective_start_idx = start_idx;
4044
- if (effective_new_text.startsWith(actual_doc_text)) {
4045
- effective_op = "INSERTION";
4046
- final_new = effective_new_text.substring(actual_doc_text.length);
4047
- effective_start_idx = start_idx + match_len;
4048
- } else {
4049
- const [prefix_len, suffix_len] = trim_common_context(
4050
- actual_doc_text,
4051
- effective_new_text
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
4052
4385
  );
4053
- const t_end = actual_doc_text.length - suffix_len;
4054
- const n_end = effective_new_text.length - suffix_len;
4055
- final_target = actual_doc_text.substring(prefix_len, t_end);
4056
- final_new = effective_new_text.substring(prefix_len, n_end);
4057
- effective_start_idx = start_idx + prefix_len;
4058
- if (!final_target && final_new) effective_op = "INSERTION";
4059
- else if (final_target && !final_new) effective_op = "DELETION";
4060
- else if (final_target && final_new) effective_op = "MODIFICATION";
4061
- else effective_op = "COMMENT_ONLY";
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({
4465
+ type: "modify",
4466
+ target_text: final_target,
4467
+ new_text: final_new,
4468
+ comment: edit.comment,
4469
+ _match_start_index: effective_start_idx,
4470
+ _internal_op: effective_op,
4471
+ _active_mapper_ref: active_mapper
4472
+ });
4473
+ }
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];
4477
+ }
4478
+ _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
4479
+ let op = edit._internal_op;
4480
+ const active_mapper = edit._active_mapper_ref || this.mapper;
4481
+ const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
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
+ }
4062
4552
  }
4063
- return {
4064
- type: "modify",
4065
- target_text: final_target,
4066
- new_text: final_new,
4067
- comment: edit.comment,
4068
- _match_start_index: effective_start_idx,
4069
- _internal_op: effective_op,
4070
- _active_mapper_ref: active_mapper
4071
- };
4072
- }
4073
- _apply_single_edit_indexed(edit, orig_new, rebuild_map) {
4074
- let op = edit._internal_op;
4075
- const active_mapper = edit._active_mapper_ref || this.mapper;
4076
- const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
4077
- const length = edit.target_text ? edit.target_text.length : 0;
4078
4553
  const del_id = ["DELETION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
4079
4554
  const ins_id = ["INSERTION", "MODIFICATION"].includes(op) ? this._getNextId() : null;
4080
4555
  if (op === "COMMENT_ONLY") {
@@ -4457,1863 +4932,1814 @@ var RedlineEngine = class {
4457
4932
  }
4458
4933
  };
4459
4934
 
4460
- // src/pagination.ts
4461
- var PAGE_TARGET_CHARS = 19e3;
4462
- var APPENDIX_MARKER = "<!-- READONLY_BOUNDARY_START -->";
4463
- var _CRITIC_TOKENS = {
4464
- "{++": "++}",
4465
- "{--": "--}",
4466
- "{==": "==}",
4467
- "{>>": "<<}"
4468
- };
4469
- var _CHG_ID_PATTERN = /\bChg:(\d+)\b/g;
4470
- function split_structural_appendix(markdown) {
4471
- if (!markdown) return ["", ""];
4472
- const idx = markdown.indexOf(APPENDIX_MARKER);
4473
- if (idx === -1) return [markdown, ""];
4474
- const line_start = markdown.lastIndexOf("\n", idx) + 1;
4475
- const body = markdown.substring(0, line_start).trimEnd();
4476
- const appendix = markdown.substring(line_start);
4477
- return [body, appendix];
4478
- }
4479
- function paginate(markdown_body, structural_appendix = "") {
4480
- if (!markdown_body) {
4481
- const appendix_clean = structural_appendix ? structural_appendix.trim() : "";
4482
- const content = appendix_clean;
4483
- return {
4484
- pages: [{
4485
- page: 1,
4486
- total_pages: 1,
4487
- has_next: false,
4488
- has_prev: false,
4489
- tracked_change_count: _count_tracked_changes(content),
4490
- page_content: content
4491
- }],
4492
- total_pages: 1,
4493
- body_pages: [""],
4494
- body_page_offsets: [0]
4495
- };
4496
- }
4497
- const block_records = _tokenize_into_atomic_blocks(markdown_body);
4498
- const [body_pages, body_page_offsets] = _assemble_pages(block_records);
4499
- let final_pages;
4500
- if (structural_appendix && structural_appendix.trim()) {
4501
- const appendix = structural_appendix.trim();
4502
- final_pages = body_pages.map((bp) => bp ? `${bp}
4503
-
4504
- ${appendix}` : appendix);
4505
- } else {
4506
- 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
+ }
4507
4944
  }
4508
- const total = final_pages.length;
4509
- const page_infos = final_pages.map((content, i) => ({
4510
- page: i + 1,
4511
- total_pages: total,
4512
- has_next: i + 1 < total,
4513
- has_prev: i + 1 > 1,
4514
- tracked_change_count: _count_tracked_changes(content),
4515
- page_content: content
4516
- }));
4517
- return {
4518
- pages: page_infos,
4519
- total_pages: total,
4520
- body_pages,
4521
- body_page_offsets
4522
- };
4523
- }
4524
- function _tokenize_into_atomic_blocks(markdown_body) {
4525
- const raw_blocks = _split_on_safe_paragraph_breaks(markdown_body);
4526
- return _merge_footnote_sections(raw_blocks);
4945
+ return result;
4527
4946
  }
4528
- function _split_on_safe_paragraph_breaks(text) {
4529
- const counters = { "++}": 0, "--}": 0, "==}": 0, "<<}": 0 };
4530
- const blocks = [];
4531
- let block_start = 0;
4532
- let i = 0;
4533
- const n = text.length;
4534
- while (i < n) {
4535
- let matched_open = false;
4536
- for (const [open_tok, close_tok] of Object.entries(_CRITIC_TOKENS)) {
4537
- if (text.startsWith(open_tok, i)) {
4538
- counters[close_tok]++;
4539
- i += open_tok.length;
4540
- matched_open = true;
4541
- 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;
4542
4961
  }
4543
4962
  }
4544
- if (matched_open) continue;
4545
- let matched_close = false;
4546
- for (const close_tok of Object.values(_CRITIC_TOKENS)) {
4547
- if (text.startsWith(close_tok, i)) {
4548
- if (counters[close_tok] > 0) counters[close_tok]--;
4549
- i += close_tok.length;
4550
- matched_close = true;
4551
- 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
+ }
4552
5007
  }
4553
- }
4554
- if (matched_close) continue;
4555
- if (text[i] === "\n" && i + 1 < n && text[i + 1] === "\n") {
4556
- if (Object.values(counters).every((c) => c === 0)) {
4557
- const block_text = text.substring(block_start, i);
4558
- if (block_text) blocks.push([block_text, block_start]);
4559
- let j = i;
4560
- while (j < n && text[j] === "\n") j++;
4561
- i = j;
4562
- block_start = i;
4563
- continue;
5008
+ if (["w:ins", "w:del", "w:hyperlink", "w:sdt", "w:smartTag", "w:fldSimple", "w:sdtContent"].includes(curr.tagName)) {
5009
+ coalesceContainer(curr);
4564
5010
  }
4565
- }
4566
- i++;
4567
- }
4568
- if (block_start < n) {
4569
- const block_text = text.substring(block_start, n);
4570
- if (block_text) blocks.push([block_text, block_start]);
4571
- }
4572
- return blocks;
4573
- }
4574
- function _merge_footnote_sections(blocks) {
4575
- if (!blocks.length) return blocks;
4576
- const merged = [];
4577
- let i = 0;
4578
- while (i < blocks.length) {
4579
- const [block_text, block_offset] = blocks[i];
4580
- const stripped = block_text.trimStart();
4581
- const is_section_header = stripped.startsWith("## Footnotes") || stripped.startsWith("## Endnotes");
4582
- if (!is_section_header) {
4583
- merged.push([block_text, block_offset]);
4584
5011
  i++;
4585
- continue;
4586
5012
  }
4587
- let accumulated_text = block_text;
4588
- let j = i + 1;
4589
- while (j < blocks.length) {
4590
- const [next_text] = blocks[j];
4591
- const next_stripped = next_text.trimStart();
4592
- if (next_stripped.startsWith("[^fn-") || next_stripped.startsWith("[^en-")) {
4593
- accumulated_text = `${accumulated_text}
4594
-
4595
- ${next_text}`;
4596
- j++;
4597
- } else {
4598
- break;
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);
4599
5017
  }
4600
5018
  }
4601
- merged.push([accumulated_text, block_offset]);
4602
- i = j;
4603
5019
  }
4604
- return merged;
5020
+ const paragraphs = findAllDescendants(doc.element, "w:p");
5021
+ for (const p of paragraphs) coalesceContainer(p);
5022
+ return count ? [`Adjacent identical runs coalesced: ${count}`] : [];
4605
5023
  }
4606
- function _assemble_pages(block_records) {
4607
- if (!block_records.length) return [[""], [0]];
4608
- const pages = [];
4609
- const page_starts = [];
4610
- let current_blocks = [];
4611
- let current_size = 0;
4612
- let current_start = -1;
4613
- const flush_current = () => {
4614
- if (current_blocks.length > 0) {
4615
- pages.push(current_blocks.join("\n\n"));
4616
- page_starts.push(current_start);
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++;
5033
+ }
4617
5034
  }
4618
- current_blocks = [];
4619
- current_size = 0;
4620
- current_start = -1;
4621
- };
4622
- for (const [block_text, block_offset] of block_records) {
4623
- const block_size = block_text.length;
4624
- const added_size = block_size + (current_blocks.length > 0 ? 2 : 0);
4625
- if (current_blocks.length > 0 && current_size + added_size > PAGE_TARGET_CHARS) {
4626
- flush_current();
5035
+ }
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++;
4627
5041
  }
4628
- if (current_blocks.length === 0 && block_size > PAGE_TARGET_CHARS) {
4629
- pages.push(block_text);
4630
- page_starts.push(block_offset);
4631
- continue;
5042
+ }
5043
+ return count ? [`rsid attributes: ${count} removed`] : [];
5044
+ }
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
+ }
4632
5055
  }
4633
- if (current_blocks.length === 0) current_start = block_offset;
4634
- current_blocks.push(block_text);
4635
- current_size += current_size > 0 ? added_size : block_size;
4636
5056
  }
4637
- flush_current();
4638
- if (!pages.length) return [[""], [0]];
4639
- return [pages, page_starts];
5057
+ return count ? [`Paragraph/text IDs: ${count} removed`] : [];
4640
5058
  }
4641
- function _count_tracked_changes(page_content) {
4642
- const matches = [...page_content.matchAll(_CHG_ID_PATTERN)];
4643
- const distinct = new Set(matches.map((m) => m[1]));
4644
- return distinct.size;
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`] : [];
4645
5063
  }
4646
-
4647
- // src/domain.ts
4648
- function boundedLevenshtein(a, b, maxDist = 2) {
4649
- if (a === b) return 0;
4650
- if (Math.abs(a.length - b.length) > maxDist) return maxDist + 1;
4651
- if (a.length === 0) return b.length <= maxDist ? b.length : maxDist + 1;
4652
- if (b.length === 0) return a.length <= maxDist ? a.length : maxDist + 1;
4653
- if (a.length > b.length) {
4654
- const temp = a;
4655
- a = b;
4656
- b = temp;
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
+ }
5073
+ }
4657
5074
  }
4658
- let row = Array.from({ length: a.length + 1 }, (_, i) => i);
4659
- for (let i = 1; i <= b.length; i++) {
4660
- const newRow = [i];
4661
- let minInRow = i;
4662
- for (let j = 1; j <= a.length; j++) {
4663
- const cost = a[j - 1] === b[i - 1] ? 0 : 1;
4664
- const val = Math.min(
4665
- row[j] + 1,
4666
- newRow[j - 1] + 1,
4667
- row[j - 1] + cost
4668
- );
4669
- newRow.push(val);
4670
- if (val < minInRow) minInRow = val;
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
+ }
4671
5087
  }
4672
- if (minInRow > maxDist) return maxDist + 1;
4673
- row = newRow;
4674
5088
  }
4675
- return row[a.length] <= maxDist ? row[a.length] : maxDist + 1;
5089
+ return count ? [`Hidden text runs: ${count} removed`] : [];
4676
5090
  }
4677
- function _get_paragraph_text(p) {
4678
- let text = "";
4679
- const runs = findAllDescendants(p._element, "w:r");
4680
- for (const r of runs) {
4681
- text += get_run_text(new Run(r, p));
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];
5096
+ }
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
+ }
4682
5104
  }
4683
- return text;
5105
+ return authors;
4684
5106
  }
4685
- function extract_all_domain_metadata(doc, base_text) {
4686
- const definitions = {};
4687
- const duplicates = /* @__PURE__ */ new Set();
4688
- const raw_anchors = {};
4689
- const raw_references = [];
4690
- const leading_re = /^(?:[\d.\-()a-zA-Z]+\s*)?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”]/;
4691
- const inline_re = /\([^)]*?["“]([A-Z][A-Za-z0-9\s\-&'’]{1,60})["”][^)]*?\)/g;
4692
- for (const item of iter_block_items(doc)) {
4693
- if (!(item instanceof Paragraph)) continue;
4694
- const text = _get_paragraph_text(item).trim();
4695
- if (!text) continue;
4696
- const extracted_terms = [];
4697
- const leading_match = text.match(leading_re);
4698
- if (leading_match) extracted_terms.push(leading_match[1].trim());
4699
- const inline_matches = text.matchAll(inline_re);
4700
- for (const m of inline_matches) {
4701
- extracted_terms.push(m[1].trim());
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);
4702
5137
  }
4703
- for (const term of extracted_terms) {
4704
- if (definitions[term]) duplicates.add(term);
4705
- else definitions[term] = { count: 0 };
5138
+ }
5139
+ const total = insEls.length + delEls.length;
5140
+ if (total) {
5141
+ return [`Tracked changes auto-accepted: ${total}`].concat(lines);
5142
+ }
5143
+ return [];
5144
+ }
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 });
5154
+ }
5155
+ return { total: comments.length, open: openCount, resolved: resolvedCount, comments };
5156
+ }
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);
4706
5171
  }
4707
- const short_text = text.length > 60 ? text.substring(0, 60) + "..." : text;
4708
- const nodes = findAllDescendants(item._element, "*");
4709
- for (const node of nodes) {
4710
- if (node.tagName === "w:bookmarkStart") {
4711
- const b_name = node.getAttribute("w:name");
4712
- if (b_name && (!b_name.startsWith("_") || b_name.startsWith("_Ref"))) {
4713
- if (!raw_anchors[b_name]) {
4714
- raw_anchors[b_name] = { anchored_to: short_text, referenced_from: [] };
4715
- }
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);
4716
5185
  }
5186
+ } else {
5187
+ parent.removeChild(ref);
4717
5188
  }
4718
- let target = null;
4719
- if (node.tagName === "w:fldSimple") {
4720
- const instr = node.getAttribute("w:instr") || "";
4721
- const parts = instr.trim().split(/\s+/);
4722
- if (parts.length > 1 && parts[0] === "REF") target = parts[1];
4723
- } else if (node.tagName === "w:instrText") {
4724
- const instr = node.textContent || "";
4725
- const parts = instr.trim().split(/\s+/);
4726
- if (parts.length > 1 && parts[0] === "REF") target = parts[1];
4727
- }
4728
- if (target) raw_references.push([target, short_text]);
4729
- }
4730
- }
4731
- for (const [target, ref_text] of raw_references) {
4732
- if (raw_anchors[target]) {
4733
- raw_anchors[target].referenced_from.push(ref_text);
4734
5189
  }
4735
5190
  }
4736
- const diagnostics = [];
4737
- const def_keys = Object.keys(definitions);
4738
- if (def_keys.length > 0) {
4739
- const sorted_terms = def_keys.sort((a, b) => b.length - a.length);
4740
- const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4741
- const alt = sorted_terms.map(escapeRegExp).join("|");
4742
- const usage_pattern = new RegExp(`(?<!["\u201C])\\b(${alt})\\b(?![\u201D"])`, "g");
4743
- for (const m of base_text.matchAll(usage_pattern)) {
4744
- const matched_term = m[1];
4745
- if (definitions[matched_term]) definitions[matched_term].count++;
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);
4746
5205
  }
4747
- for (const term of def_keys) {
4748
- if (definitions[term].count === 0) {
4749
- delete definitions[term];
4750
- duplicates.delete(term);
5206
+ }
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);
4751
5226
  }
4752
5227
  }
4753
5228
  }
4754
- for (const term of duplicates) {
4755
- diagnostics.push(`[Error] Duplicate Definition: '${term}' is defined multiple times.`);
4756
- }
4757
- const stop_words = /* @__PURE__ */ new Set([
4758
- "The",
4759
- "This",
4760
- "That",
4761
- "Such",
4762
- "A",
4763
- "An",
4764
- "Any",
4765
- "All",
4766
- "Some",
4767
- "No",
4768
- "Every",
4769
- "Each",
4770
- "As",
4771
- "In",
4772
- "Of",
4773
- "For",
4774
- "To",
4775
- "On",
4776
- "By",
4777
- "With"
4778
- ]);
4779
- const all_cap_pattern = /\b[A-Z][a-zA-Z]*(?:\s+[A-Z][a-zA-Z]*)*\b/g;
4780
- const all_caps = new Set(base_text.match(all_cap_pattern) || []);
4781
- const valid_terms = new Set(Object.keys(definitions));
4782
- const terms_by_first_letter = {};
4783
- for (const term of valid_terms) {
4784
- const fl = term[0].toLowerCase();
4785
- if (!terms_by_first_letter[fl]) terms_by_first_letter[fl] = [];
4786
- terms_by_first_letter[fl].push(term);
4787
- }
4788
- const candidates_by_term = {};
4789
- for (const raw_candidate of all_caps) {
4790
- let candidate = raw_candidate.trim();
4791
- const words = candidate.split(/\s+/);
4792
- while (words.length > 0) {
4793
- const first = words[0];
4794
- const title = first.charAt(0).toUpperCase() + first.slice(1).toLowerCase();
4795
- if (stop_words.has(title)) words.shift();
4796
- else break;
4797
- }
4798
- candidate = words.join(" ");
4799
- if (candidate.length < 4) continue;
4800
- if (valid_terms.has(candidate)) continue;
4801
- const first_letter = candidate[0].toLowerCase();
4802
- let candidate_terms = terms_by_first_letter[first_letter] || [];
4803
- if (candidate.length > 5) {
4804
- for (const [k, v] of Object.entries(terms_by_first_letter)) {
4805
- 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);
4806
5237
  }
4807
5238
  }
4808
- for (const term of candidate_terms) {
4809
- if (Math.abs(candidate.length - term.length) > 2) continue;
4810
- if (candidate === term + "s" || candidate === term + "es") continue;
4811
- if (term === candidate + "s" || term === candidate + "es") continue;
4812
- const dist = boundedLevenshtein(candidate, term, 2);
4813
- if (dist === 0 || dist > 2) continue;
4814
- if (term.length <= 5) {
4815
- if (dist > 1) continue;
4816
- if (candidate[0].toLowerCase() !== term[0].toLowerCase()) continue;
4817
- }
4818
- if (!candidates_by_term[term]) candidates_by_term[term] = [];
4819
- if (!candidates_by_term[term].includes(candidate)) candidates_by_term[term].push(candidate);
5239
+ for (const overrideEl of toRemove) {
5240
+ overrideEl.parentNode?.removeChild(overrideEl);
4820
5241
  }
4821
5242
  }
4822
- for (const [term, candidates] of Object.entries(candidates_by_term)) {
4823
- candidates.sort();
4824
- const c_str = candidates.map((c) => `'${c}'`).join(", ");
4825
- diagnostics.push(`[Info] Possible Typos for '${term}': Found ${c_str}`);
4826
- }
4827
- function diag_sort_key(msg) {
4828
- if (msg.startsWith("[Error]")) return 0;
4829
- if (msg.startsWith("[Warning]")) return 1;
4830
- 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
+ }
4831
5248
  }
4832
- diagnostics.sort((a, b) => {
4833
- const keyA = diag_sort_key(a);
4834
- const keyB = diag_sort_key(b);
4835
- if (keyA !== keyB) return keyA - keyB;
4836
- return a.localeCompare(b);
4837
- });
4838
- return [definitions, diagnostics, raw_anchors];
4839
5249
  }
4840
- function build_structural_appendix(doc, base_text) {
4841
- const [defs, diagnostics, anchors] = extract_all_domain_metadata(doc, base_text);
4842
- const lines = [
4843
- "\n\n---",
4844
- "",
4845
- "<!-- READONLY_BOUNDARY_START -->",
4846
- "# Document Structure (Read-Only)",
4847
- "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."
4848
- ];
4849
- let has_content = false;
4850
- if (Object.keys(defs).length > 0) {
4851
- has_content = true;
4852
- lines.push("\n## Defined Terms");
4853
- for (const [term, data] of Object.entries(defs)) {
4854
- 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);
4855
5260
  }
4856
- }
4857
- if (diagnostics.length > 0) {
4858
- has_content = true;
4859
- lines.push("\n## Semantic Diagnostics");
4860
- for (const diag of diagnostics) {
4861
- 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);
4862
5264
  }
4863
5265
  }
4864
- if (Object.keys(anchors).length > 0) {
4865
- has_content = true;
4866
- lines.push("\n## Named Anchors");
4867
- for (const [b_name, data] of Object.entries(anchors)) {
4868
- lines.push(`- ${b_name} \u2192 Anchored to: "${data.anchored_to}"`);
4869
- for (const ref of data.referenced_from) {
4870
- 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);
4871
5276
  }
4872
5277
  }
4873
5278
  }
4874
- if (has_content) {
4875
- return lines.join("\n");
4876
- }
4877
- return "";
4878
- }
4879
-
4880
- // src/ingest.ts
4881
- async function extractTextFromBuffer(buffer, cleanView = false) {
4882
- const doc = await DocumentObject.load(buffer);
4883
- return _extractTextFromDoc(doc, cleanView);
5279
+ return original.size ? [`Track change authors replaced: ${Array.from(original).sort().join(", ")} \u2192 "${newAuthor}"`] : [];
4884
5280
  }
4885
- function _extractTextFromDoc(doc, cleanView = false, includeAppendix = true, return_paragraph_offsets = false) {
4886
- const comments_map = extract_comments_data(doc.pkg);
4887
- const full_text = [];
4888
- const paragraph_offsets = /* @__PURE__ */ new Map();
4889
- let cursor = 0;
4890
- for (const part of iter_document_parts(doc)) {
4891
- const part_cursor = full_text.length > 0 ? cursor + 2 : cursor;
4892
- const part_text = _extract_blocks(part, comments_map, cleanView, part_cursor, return_paragraph_offsets ? paragraph_offsets : void 0);
4893
- if (part_text) {
4894
- if (full_text.length > 0) cursor += 2;
4895
- full_text.push(part_text);
4896
- 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
+ }
4897
5290
  }
4898
5291
  }
4899
- let base_text = full_text.join("\n\n");
4900
- if (includeAppendix) {
4901
- const appendix = build_structural_appendix(doc, base_text);
4902
- if (appendix) base_text += appendix;
4903
- }
4904
- if (return_paragraph_offsets) {
4905
- return { text: base_text, paragraph_offsets };
4906
- }
4907
- return base_text;
5292
+ return count ? [`Track change timestamps: ${count} normalized`] : [];
4908
5293
  }
4909
- function _extract_blocks(container, comments_map, cleanView, cursor, paragraph_offsets) {
4910
- const part = container.part || container;
4911
- const [style_cache, default_pstyle] = _get_style_cache(part);
4912
- const blocks = [];
4913
- let local_cursor = cursor;
4914
- let is_first_block = true;
4915
- let is_first_para = true;
4916
- if (container.constructor && container.constructor.name === "NotesPart") {
4917
- const header = container.note_type === "fn" ? "## Footnotes" : "## Endnotes";
4918
- const sep = `---
4919
- ${header}`;
4920
- blocks.push(sep);
4921
- local_cursor += sep.length;
4922
- is_first_block = false;
4923
- }
4924
- for (const item of iter_block_items(container)) {
4925
- if (!is_first_block) local_cursor += 2;
4926
- const block_start = local_cursor;
4927
- if (item.constructor.name === "FootnoteItem") {
4928
- const fn_text = _extract_blocks(item, comments_map, cleanView, block_start, paragraph_offsets);
4929
- if (fn_text) {
4930
- blocks.push(fn_text);
4931
- local_cursor = block_start + fn_text.length;
4932
- is_first_block = false;
4933
- } else if (!is_first_block) {
4934
- local_cursor -= 2;
4935
- }
4936
- } else if (item instanceof Paragraph) {
4937
- let prefix = get_paragraph_prefix(item, style_cache, default_pstyle);
4938
- if (is_first_para && container.constructor.name === "FootnoteItem") {
4939
- 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 = "";
4940
5303
  }
4941
- const p_text = build_paragraph_text(item, comments_map, cleanView, style_cache, default_pstyle);
4942
- const full_block = prefix + p_text;
4943
- blocks.push(full_block);
4944
- if (paragraph_offsets) {
4945
- 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 = "";
4946
5310
  }
4947
- local_cursor = block_start + full_block.length;
4948
- is_first_para = false;
4949
- is_first_block = false;
4950
- } else if (item instanceof Table) {
4951
- const table_text = extract_table(item, comments_map, cleanView, block_start, paragraph_offsets);
4952
- if (table_text) {
4953
- blocks.push(table_text);
4954
- local_cursor = block_start + table_text.length;
4955
- is_first_block = false;
4956
- } else if (!is_first_block) {
4957
- 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";
4958
5317
  }
4959
- 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
+ });
4960
5340
  }
4961
5341
  }
4962
- return blocks.join("\n\n");
5342
+ return lines.length ? ["Metadata scrubbed:", ...lines.map((l) => ` ${l}`)] : [];
4963
5343
  }
4964
- function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets) {
4965
- const rows_text = [];
4966
- let rows_processed = 0;
4967
- let local_cursor = cursor;
4968
- for (const row of table.rows) {
4969
- const cell_texts = [];
4970
- const seen_cells = /* @__PURE__ */ new Set();
4971
- const trPr = findChild(row._element, "w:trPr");
4972
- const ins = trPr ? findChild(trPr, "w:ins") : null;
4973
- const del_node = trPr ? findChild(trPr, "w:del") : null;
4974
- if (cleanView && del_node) continue;
4975
- const row_start = local_cursor + (rows_processed > 0 ? 1 : 0);
4976
- const wrapper_prefix_len = !cleanView && ins ? 4 : !cleanView && del_node ? 4 : 0;
4977
- let cell_cursor = row_start + wrapper_prefix_len;
4978
- let first_cell = true;
4979
- for (const cell of row.cells) {
4980
- if (seen_cells.has(cell)) continue;
4981
- seen_cells.add(cell);
4982
- if (!first_cell) cell_cursor += 3;
4983
- const cell_content = _extract_blocks(cell, comments_map, cleanView, cell_cursor, paragraph_offsets);
4984
- cell_texts.push(cell_content);
4985
- cell_cursor += cell_content.length;
4986
- 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
+ });
4987
5356
  }
4988
- let row_str = cell_texts.join(" | ");
4989
- if (!cleanView) {
4990
- if (ins) row_str = `{++ ${row_str} |Chg:${ins.getAttribute("w:id")}++}`;
4991
- 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);
4992
5370
  }
4993
- rows_text.push(row_str);
4994
- local_cursor = row_start + row_str.length;
4995
- 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));
4996
5379
  }
4997
- return rows_text.join("\n");
5380
+ return [`Custom XML parts: ${customParts.length} removed`];
4998
5381
  }
4999
- function build_paragraph_text(paragraph, comments_map, cleanView, style_cache, default_pstyle) {
5000
- const parts = [];
5001
- const active_ins = {};
5002
- const active_del = {};
5003
- const active_comments = /* @__PURE__ */ new Set();
5004
- const active_fmt = {};
5005
- const deferred_meta_states = [];
5006
- let pending_text = "";
5007
- let current_wrappers = ["", ""];
5008
- let current_style = ["", ""];
5009
- const items = Array.from(iter_paragraph_content(paragraph));
5010
- const is_heading = is_heading_paragraph(paragraph, style_cache, default_pstyle);
5011
- const native_heading = is_native_heading(paragraph, style_cache, default_pstyle);
5012
- let leading_strip_active = is_heading;
5013
- for (let i = 0; i < items.length; i++) {
5014
- const item = items[i];
5015
- if (item instanceof Run) {
5016
- const [prefix, suffix] = get_run_style_markers(item, native_heading);
5017
- const text = get_run_text(item);
5018
- if (cleanView && Object.keys(active_del).length > 0) continue;
5019
- if (leading_strip_active) {
5020
- if (!text || !text.trim()) continue;
5021
- leading_strip_active = false;
5022
- }
5023
- const seg = apply_formatting_to_segments(text, prefix, suffix);
5024
- if (seg) {
5025
- const new_wrappers = cleanView ? ["", ""] : _get_wrappers(active_ins, active_del, active_comments, active_fmt);
5026
- const new_style = [prefix, suffix];
5027
- if (pending_text && new_wrappers[0] === current_wrappers[0] && new_wrappers[1] === current_wrappers[1]) {
5028
- 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])) {
5029
- pending_text = pending_text.slice(0, -current_style[1].length) + seg.slice(new_style[0].length);
5030
- } else {
5031
- pending_text += seg;
5032
- }
5033
- current_style = new_style;
5034
- } else {
5035
- if (pending_text) parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5036
- pending_text = seg;
5037
- current_wrappers = new_wrappers;
5038
- current_style = new_style;
5039
- }
5040
- if (!cleanView) {
5041
- 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;
5042
- if (has_meta) {
5043
- deferred_meta_states.push([{ ...active_ins }, { ...active_del }, new Set(active_comments), { ...active_fmt }]);
5044
- }
5045
- let should_defer = false;
5046
- const is_redline = Object.keys(active_ins).length > 0 || Object.keys(active_del).length > 0 || Object.keys(active_fmt).length > 0;
5047
- if (is_redline) {
5048
- let j = i + 1;
5049
- let next_is_redline = false;
5050
- let temp_ins = Object.keys(active_ins).length;
5051
- let temp_del = Object.keys(active_del).length;
5052
- let temp_fmt = Object.keys(active_fmt).length;
5053
- while (j < items.length) {
5054
- const next_item = items[j];
5055
- if (next_item instanceof Run) {
5056
- if (!get_run_text(next_item)) {
5057
- j++;
5058
- continue;
5059
- }
5060
- if (temp_ins > 0 || temp_del > 0 || temp_fmt > 0) next_is_redline = true;
5061
- break;
5062
- } else {
5063
- const ev = next_item;
5064
- if (ev.type === "ins_start") temp_ins++;
5065
- else if (ev.type === "ins_end") temp_ins = Math.max(0, temp_ins - 1);
5066
- else if (ev.type === "del_start") temp_del++;
5067
- else if (ev.type === "del_end") temp_del = Math.max(0, temp_del - 1);
5068
- else if (ev.type === "fmt_start") temp_fmt++;
5069
- else if (ev.type === "fmt_end") temp_fmt = Math.max(0, temp_fmt - 1);
5070
- }
5071
- j++;
5072
- }
5073
- if (next_is_redline) should_defer = true;
5074
- }
5075
- if (!should_defer && deferred_meta_states.length > 0) {
5076
- const meta_block = _build_merged_meta_block(deferred_meta_states, comments_map);
5077
- if (meta_block) {
5078
- if (pending_text) {
5079
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5080
- pending_text = "";
5081
- current_wrappers = ["", ""];
5082
- current_style = ["", ""];
5083
- }
5084
- parts.push(`{>>${meta_block}<<}`);
5085
- }
5086
- deferred_meta_states.length = 0;
5087
- }
5088
- }
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++;
5089
5392
  }
5090
- } else {
5091
- const ev = item;
5092
- leading_strip_active = false;
5093
- if (!["ins_start", "ins_end", "del_start", "del_end", "fmt_start", "fmt_end"].includes(ev.type)) {
5094
- if (pending_text) {
5095
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5096
- pending_text = "";
5097
- current_wrappers = ["", ""];
5098
- current_style = ["", ""];
5393
+ }
5394
+ }
5395
+ return count ? [`Image alt text: ${count} auto-generated descriptions removed`] : [];
5396
+ }
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;
5099
5408
  }
5100
5409
  }
5101
- if (ev.type === "start") active_comments.add(ev.id);
5102
- else if (ev.type === "end") active_comments.delete(ev.id);
5103
- else if (ev.type === "ins_start") active_ins[ev.id] = ev;
5104
- else if (ev.type === "ins_end") delete active_ins[ev.id];
5105
- else if (ev.type === "del_start") active_del[ev.id] = ev;
5106
- else if (ev.type === "del_end") delete active_del[ev.id];
5107
- else if (ev.type === "fmt_start") active_fmt[ev.id] = ev;
5108
- else if (ev.type === "fmt_end") delete active_fmt[ev.id];
5109
- else if (ev.type === "footnote" || ev.type === "endnote") {
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
- parts.push(`[^${ev.type === "footnote" ? "fn" : "en"}-${ev.id}]`);
5117
- } else if (ev.type === "hyperlink_start") {
5118
- if (pending_text) {
5119
- parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5120
- pending_text = "";
5121
- current_wrappers = ["", ""];
5122
- current_style = ["", ""];
5123
- }
5124
- parts.push("[");
5125
- } else if (ev.type === "hyperlink_end") {
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.date})`);
5133
- } else if (ev.type === "xref_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 === "xref_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.id})`);
5149
- } else if (ev.type === "bookmark") {
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(`{#${ev.id}}`);
5157
- }
5158
5410
  }
5159
5411
  }
5160
- if (pending_text) parts.push(`${current_wrappers[0]}${pending_text}${current_wrappers[1]}`);
5161
- if (deferred_meta_states.length > 0) {
5162
- const meta_block = _build_merged_meta_block(deferred_meta_states, comments_map);
5163
- if (meta_block) parts.push(`{>>${meta_block}<<}`);
5412
+ return warnings;
5413
+ }
5414
+
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;
5164
5425
  }
5165
- return parts.join("");
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;
5438
+ }
5439
+ return row[a.length] <= maxDist ? row[a.length] : maxDist + 1;
5166
5440
  }
5167
- function _get_wrappers(ins, del, comments, fmt) {
5168
- if (Object.keys(del).length > 0) return ["{--", "--}"];
5169
- if (Object.keys(ins).length > 0) return ["{++", "++}"];
5170
- if (comments.size > 0 || Object.keys(fmt).length > 0) return ["{==", "==}"];
5171
- return ["", ""];
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));
5446
+ }
5447
+ return text;
5172
5448
  }
5173
- function _build_merged_meta_block(states_list, comments_map) {
5174
- const change_lines = [];
5175
- const comment_lines = [];
5176
- const seen_sigs = /* @__PURE__ */ new Set();
5177
- for (const [ins_map, del_map, comments_set, fmt_map] of states_list) {
5178
- let render_comment2 = function(cid) {
5179
- if (!comments_map[cid]) return;
5180
- const sig = `Com:${cid}`;
5181
- if (seen_sigs.has(sig)) return;
5182
- const data = comments_map[cid];
5183
- let header = `[${sig}] ${data.author}`;
5184
- if (data.date) header += ` @ ${data.date}`;
5185
- if (data.resolved) header += `(RESOLVED)`;
5186
- comment_lines.push(`${header}: ${data.text}`);
5187
- seen_sigs.add(sig);
5188
- if (children_map[cid]) {
5189
- const children = children_map[cid].sort((a, b) => (comments_map[a]?.date || "").localeCompare(comments_map[b]?.date || ""));
5190
- for (const child_id of children) {
5191
- render_comment2(child_id);
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());
5466
+ }
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
+ }
5192
5483
  }
5193
5484
  }
5194
- };
5195
- var render_comment = render_comment2;
5196
- for (const [uid, meta] of Object.entries(ins_map)) {
5197
- const sig = `Chg:${uid}`;
5198
- if (!seen_sigs.has(sig)) {
5199
- change_lines.push(`[${sig} insert] ${meta.author || "Unknown"}`);
5200
- seen_sigs.add(sig);
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];
5201
5494
  }
5495
+ if (target) raw_references.push([target, short_text]);
5202
5496
  }
5203
- for (const [uid, meta] of Object.entries(del_map)) {
5204
- const sig = `Chg:${uid}`;
5205
- if (!seen_sigs.has(sig)) {
5206
- change_lines.push(`[${sig} delete] ${meta.author || "Unknown"}`);
5207
- seen_sigs.add(sig);
5208
- }
5497
+ }
5498
+ for (const [target, ref_text] of raw_references) {
5499
+ if (raw_anchors[target]) {
5500
+ raw_anchors[target].referenced_from.push(ref_text);
5209
5501
  }
5210
- for (const [uid, meta] of Object.entries(fmt_map)) {
5211
- const sig = `Chg:${uid}`;
5212
- if (!seen_sigs.has(sig)) {
5213
- change_lines.push(`[${sig} format] ${meta.author || "Unknown"}`);
5214
- seen_sigs.add(sig);
5502
+ }
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);
5215
5518
  }
5216
5519
  }
5217
- const children_map = {};
5218
- for (const [c_id, data] of Object.entries(comments_map)) {
5219
- const p_id = data.parent_id;
5220
- if (p_id) {
5221
- if (!children_map[p_id]) children_map[p_id] = [];
5222
- children_map[p_id].push(c_id);
5520
+ }
5521
+ for (const term of duplicates) {
5522
+ diagnostics.push(
5523
+ `[Error] Duplicate Definition: '${term}' is defined multiple times.`
5524
+ );
5525
+ }
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);
5223
5575
  }
5224
5576
  }
5225
- const sorted_ids = Array.from(comments_set).sort();
5226
- for (const c_id of sorted_ids) {
5227
- render_comment2(c_id);
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);
5228
5590
  }
5229
5591
  }
5230
- return [...change_lines, ...comment_lines].join("\n");
5231
- }
5232
-
5233
- // src/outline.ts
5234
- var _HEADING_PREFIX_RE = /^(#{1,6}) /;
5235
- var _HEURISTIC_MIN_WORDS = 3;
5236
- function extract_outline(doc, projected_body, body_pages, body_page_offsets, paragraph_offsets = null) {
5237
- if (body_pages.length !== body_page_offsets.length) {
5238
- throw new Error("body_pages and body_page_offsets length mismatch");
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}`);
5239
5596
  }
5240
- if (paragraph_offsets) {
5241
- return _extract_outline_fast(doc, projected_body, body_page_offsets, paragraph_offsets);
5597
+ function diag_sort_key(msg) {
5598
+ if (msg.startsWith("[Error]")) return 0;
5599
+ if (msg.startsWith("[Warning]")) return 1;
5600
+ return 2;
5242
5601
  }
5243
- const comments_map = extract_comments_data(doc.pkg);
5244
- const block_records = _walk_doc_body(doc, comments_map);
5245
- const heading_indices = [];
5246
- for (let idx = 0; idx < block_records.length; idx++) {
5247
- const rec = block_records[idx];
5248
- if (!(rec.is_paragraph && _is_heading(rec.item))) continue;
5249
- if (!_heading_passes_quality_filter(rec.item, comments_map)) continue;
5250
- heading_indices.push(idx);
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];
5609
+ }
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
+ );
5251
5627
  }
5252
- if (heading_indices.length === 0) return [];
5253
- const nodes = [];
5254
- for (let h_pos = 0; h_pos < heading_indices.length; h_pos++) {
5255
- const rec_idx = heading_indices[h_pos];
5256
- const rec = block_records[rec_idx];
5257
- const paragraph = rec.item;
5258
- const level = _heading_level(paragraph);
5259
- const text = _heading_text(paragraph, comments_map);
5260
- const style = _determine_heading_style(paragraph);
5261
- const owned_end = _find_owned_end(
5262
- block_records,
5263
- heading_indices,
5264
- h_pos,
5265
- level
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."
5266
5635
  );
5267
- const owned_blocks = block_records.slice(rec_idx + 1, owned_end);
5268
- const has_table = _direct_has_table(block_records, rec_idx + 1, owned_end);
5269
- const footnote_ids = _collect_footnote_ids(owned_blocks);
5270
- const page_num = _offset_to_page(rec.start_offset, body_page_offsets);
5271
- nodes.push({ level, text, page: page_num, style, has_table, footnote_ids });
5272
5636
  }
5273
- return nodes;
5637
+ return warnings;
5274
5638
  }
5275
- function _direct_has_table(block_records, range_start, range_end) {
5276
- for (let idx = range_start; idx < range_end; idx++) {
5277
- const rec = block_records[idx];
5278
- if (rec.is_paragraph && _is_heading(rec.item)) return false;
5279
- if (rec.is_table) return true;
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}`);
5658
+ }
5280
5659
  }
5281
- return false;
5282
- }
5283
- function _walk_doc_body(doc, comments_map) {
5284
- const parts = Array.from(iter_document_parts(doc));
5285
- let body_start_offset = 0;
5286
- let body_part = null;
5287
- for (const part of parts) {
5288
- if (part === doc) {
5289
- body_part = part;
5290
- break;
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.`);
5291
5665
  }
5292
- const part_text = _project_part(part, comments_map);
5666
+ }
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}`);
5672
+ }
5673
+ }
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 "";
5688
+ }
5689
+
5690
+ // src/ingest.ts
5691
+ async function extractTextFromBuffer(buffer, cleanView = false) {
5692
+ const doc = await DocumentObject.load(buffer);
5693
+ return _extractTextFromDoc(doc, cleanView);
5694
+ }
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
+ );
5293
5709
  if (part_text) {
5294
- if (body_start_offset > 0) body_start_offset += 2;
5295
- body_start_offset += part_text.length;
5710
+ if (full_text.length > 0) cursor += 2;
5711
+ full_text.push(part_text);
5712
+ cursor += part_text.length;
5296
5713
  }
5297
5714
  }
5298
- if (!body_part) {
5299
- body_part = doc;
5300
- body_start_offset = 0;
5301
- } else {
5302
- if (body_start_offset > 0) body_start_offset += 2;
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;
5303
5719
  }
5304
- const records = [];
5305
- let cursor = body_start_offset;
5720
+ if (return_paragraph_offsets) {
5721
+ return { text: base_text, paragraph_offsets };
5722
+ }
5723
+ return base_text;
5724
+ }
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;
5306
5730
  let is_first_block = true;
5307
- for (const item of iter_block_items(body_part)) {
5308
- if (item instanceof Paragraph) {
5309
- const prefix = get_paragraph_prefix(item);
5310
- const p_text = build_paragraph_text(item, comments_map, false);
5311
- const block_len = (prefix + p_text).length;
5312
- if (!is_first_block) cursor += 2;
5313
- records.push({
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;
5739
+ }
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(
5314
5745
  item,
5315
- is_paragraph: true,
5316
- is_table: false,
5317
- start_offset: cursor,
5318
- projected_length: block_len
5319
- });
5320
- cursor += block_len;
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;
5757
+ }
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]);
5774
+ }
5775
+ local_cursor = block_start + full_block.length;
5776
+ is_first_para = false;
5321
5777
  is_first_block = false;
5322
5778
  } else if (item instanceof Table) {
5323
- const table_text = extract_table(item, comments_map, false, 0);
5324
- const block_len = table_text ? table_text.length : 0;
5325
- if (!is_first_block) cursor += 2;
5326
- const table_start = cursor;
5327
- records.push({
5779
+ const table_text = extract_table(
5328
5780
  item,
5329
- is_paragraph: false,
5330
- is_table: true,
5331
- start_offset: table_start,
5332
- projected_length: block_len
5333
- });
5334
- _record_table_inner_blocks_lite(item, table_start, records, comments_map);
5335
- cursor += block_len;
5336
- is_first_block = false;
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;
5337
5794
  }
5338
5795
  }
5339
- return records;
5796
+ return blocks.join("\n\n");
5340
5797
  }
5341
- function _compute_inner_block_offset(table, target_paragraph, table_start_offset, comments_map) {
5342
- const target_el = target_paragraph._element;
5343
- let cursor = table_start_offset;
5798
+ function extract_table(table, comments_map, cleanView, cursor, paragraph_offsets) {
5799
+ const rows_text = [];
5344
5800
  let rows_processed = 0;
5801
+ let local_cursor = cursor;
5345
5802
  for (const row of table.rows) {
5346
- if (rows_processed > 0) cursor += 1;
5803
+ const cell_texts = [];
5347
5804
  const seen_cells = /* @__PURE__ */ new Set();
5348
- let cells_in_row = 0;
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;
5349
5813
  for (const cell of row.cells) {
5350
5814
  if (seen_cells.has(cell)) continue;
5351
5815
  seen_cells.add(cell);
5352
- if (cells_in_row > 0) cursor += 3;
5353
- const [new_cursor, found] = _walk_cell_for_offset(
5816
+ if (!first_cell) cell_cursor += 3;
5817
+ const cell_content = _extract_blocks(
5354
5818
  cell,
5355
- target_el,
5356
- cursor,
5357
- comments_map
5819
+ comments_map,
5820
+ cleanView,
5821
+ cell_cursor,
5822
+ paragraph_offsets
5358
5823
  );
5359
- if (found) return new_cursor;
5360
- cursor = new_cursor;
5361
- cells_in_row++;
5824
+ cell_texts.push(cell_content);
5825
+ cell_cursor += cell_content.length;
5826
+ first_cell = false;
5827
+ }
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")}--}`;
5362
5833
  }
5834
+ rows_text.push(row_str);
5835
+ local_cursor = row_start + row_str.length;
5363
5836
  rows_processed++;
5364
5837
  }
5365
- return table_start_offset;
5838
+ return rows_text.join("\n");
5366
5839
  }
5367
- function _walk_cell_for_offset(cell, target_el, cell_start_cursor, comments_map) {
5368
- let cursor = cell_start_cursor;
5369
- let is_first_block = true;
5370
- for (const inner_item of iter_block_items(cell)) {
5371
- if (!is_first_block) cursor += 2;
5372
- if (inner_item instanceof Paragraph) {
5373
- if (inner_item._element === target_el) return [cursor, true];
5374
- const prefix = get_paragraph_prefix(inner_item);
5375
- const p_text = build_paragraph_text(inner_item, comments_map, false);
5376
- cursor += (prefix + p_text).length;
5377
- } else if (inner_item instanceof Table) {
5378
- const nested_offset = _compute_inner_block_offset(
5379
- inner_item,
5380
- new Paragraph(target_el, null),
5381
- cursor,
5382
- comments_map
5383
- );
5384
- if (nested_offset !== cursor) {
5385
- if (_element_is_descendant(target_el, inner_item._element))
5386
- return [nested_offset, true];
5387
- }
5388
- const table_text = extract_table(inner_item, comments_map, false, 0);
5389
- cursor += table_text ? table_text.length : 0;
5390
- }
5391
- is_first_block = false;
5392
- }
5393
- return [cursor, false];
5394
- }
5395
- function _element_is_descendant(target_el, ancestor_el) {
5396
- let cur = target_el.parentNode;
5397
- while (cur) {
5398
- if (cur === ancestor_el) return true;
5399
- cur = cur.parentNode;
5400
- }
5401
- return false;
5402
- }
5403
- function _record_table_inner_blocks_lite(table, inherited_offset, records, comments_map) {
5404
- const seen_cells = /* @__PURE__ */ new Set();
5405
- for (const row of table.rows) {
5406
- for (const cell of row.cells) {
5407
- if (seen_cells.has(cell)) continue;
5408
- seen_cells.add(cell);
5409
- for (const inner_item of iter_block_items(cell)) {
5410
- if (inner_item instanceof Paragraph) {
5411
- const true_offset = _is_heading(inner_item) ? _compute_inner_block_offset(
5412
- table,
5413
- inner_item,
5414
- inherited_offset,
5415
- comments_map
5416
- ) : inherited_offset;
5417
- records.push({
5418
- item: inner_item,
5419
- is_paragraph: true,
5420
- is_table: false,
5421
- start_offset: true_offset,
5422
- projected_length: 0
5423
- });
5424
- } else if (inner_item instanceof Table) {
5425
- records.push({
5426
- item: inner_item,
5427
- is_paragraph: false,
5428
- is_table: true,
5429
- start_offset: inherited_offset,
5430
- projected_length: 0
5431
- });
5432
- _record_table_inner_blocks_lite(
5433
- inner_item,
5434
- inherited_offset,
5435
- records,
5436
- comments_map
5437
- );
5438
- }
5439
- }
5440
- }
5441
- }
5442
- }
5443
- function _project_part(part, comments_map) {
5444
- const blocks = [];
5445
- const c_type = part.constructor.name;
5446
- if (c_type === "NotesPart") {
5447
- const header = part.note_type === "fn" ? "## Footnotes" : "## Endnotes";
5448
- blocks.push(`---
5449
- ${header}`);
5450
- }
5451
- let is_first_para = true;
5452
- for (const item of iter_block_items(part)) {
5453
- if (item.constructor.name === "FootnoteItem") {
5454
- const fn_text = _project_part(item, comments_map);
5455
- if (fn_text) blocks.push(fn_text);
5456
- } else if (item instanceof Paragraph) {
5457
- let prefix = get_paragraph_prefix(item);
5458
- if (is_first_para && c_type === "FootnoteItem")
5459
- prefix = `[^${part.note_type}-${part.id}]: ${prefix}`;
5460
- const p_text = build_paragraph_text(item, comments_map, false);
5461
- blocks.push(prefix + p_text);
5462
- is_first_para = false;
5463
- } else if (item instanceof Table) {
5464
- const table_text = extract_table(item, comments_map, false, 0);
5465
- if (table_text) blocks.push(table_text);
5466
- is_first_para = false;
5467
- }
5468
- }
5469
- return blocks.join("\n\n");
5470
- }
5471
- function _is_heading(paragraph) {
5472
- return _HEADING_PREFIX_RE.test(get_paragraph_prefix(paragraph));
5473
- }
5474
- function _heading_passes_quality_filter(paragraph, comments_map) {
5475
- const style = _determine_heading_style(paragraph);
5476
- if (style !== "(heuristic)") return true;
5477
- const text = _heading_text(paragraph, comments_map);
5478
- if (!text) return false;
5479
- const word_count = (text.match(/\w+/g) || []).length;
5480
- return word_count >= _HEURISTIC_MIN_WORDS;
5481
- }
5482
- function _heading_level(paragraph) {
5483
- const match = _HEADING_PREFIX_RE.exec(get_paragraph_prefix(paragraph));
5484
- return match ? Math.min(match[1].length, 6) : 1;
5485
- }
5486
- function _heading_text(paragraph, comments_map) {
5487
- const p_text = build_paragraph_text(paragraph, comments_map, false);
5488
- let cleaned = _strip_critic_markup(p_text);
5489
- cleaned = _strip_inline_formatting(cleaned);
5490
- return cleaned.trim();
5491
- }
5492
- function _strip_critic_markup(text) {
5493
- if (!text) return "";
5494
- text = text.replace(/\{--[\s\S]*?--\}/g, "");
5495
- text = text.replace(/\{>>[\s\S]*?<<\}/g, "");
5496
- text = text.replace(/\{\+\+([\s\S]*?)\+\+\}/g, "$1");
5497
- text = text.replace(/\{==([\s\S]*?)==\}/g, "$1");
5498
- return text;
5499
- }
5500
- function _strip_inline_formatting(text) {
5501
- if (!text) return "";
5502
- text = text.replace(/\*\*(.+?)\*\*/g, "$1");
5503
- text = text.replace(/__(.+?)__/g, "$1");
5504
- text = text.replace(/(?<!\w)_(\S(?:.*?\S)?)_(?!\w)/g, "$1");
5505
- return text;
5506
- }
5507
- function _determine_heading_style(paragraph) {
5508
- const [style_cache, default_pstyle] = _get_style_cache(
5509
- paragraph._parent.part || paragraph._parent
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
5510
5855
  );
5511
- const pPr = findChild(paragraph._element, "w:pPr");
5512
- let style_id = default_pstyle;
5513
- if (pPr) {
5514
- const pStyle = findChild(pPr, "w:pStyle");
5515
- if (pStyle) style_id = pStyle.getAttribute("w:val") || default_pstyle;
5516
- }
5517
- let outline_level = null;
5518
- let outline_level_from_style = false;
5519
- if (pPr) {
5520
- const oLvl = findChild(pPr, "w:outlineLvl");
5521
- if (oLvl && /^\d+$/.test(oLvl.getAttribute("w:val") || "")) {
5522
- outline_level = parseInt(oLvl.getAttribute("w:val"), 10);
5523
- }
5524
- }
5525
- if (outline_level === null && style_id && style_cache && style_cache[style_id]) {
5526
- outline_level = style_cache[style_id].outline_level;
5527
- outline_level_from_style = true;
5528
- }
5529
- const style_name = style_id && style_cache && style_cache[style_id] ? style_cache[style_id].name : style_id;
5530
- let normalized_style_name = style_name;
5531
- if (normalized_style_name && typeof normalized_style_name === "string") {
5532
- if (normalized_style_name.toLowerCase().startsWith("heading")) {
5533
- normalized_style_name = normalized_style_name.replace(/^heading/i, "Heading");
5534
- } else if (normalized_style_name.toLowerCase() === "title") {
5535
- normalized_style_name = "Title";
5536
- }
5537
- }
5538
- if (outline_level_from_style && outline_level !== null) {
5539
- const is_heading_or_title = normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title");
5540
- if (!is_heading_or_title) {
5541
- outline_level = null;
5542
- }
5543
- }
5544
- if (outline_level !== null && outline_level >= 0 && outline_level <= 8) {
5545
- if (normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title")) {
5546
- return normalized_style_name;
5547
- }
5548
- return "(outline_level)";
5549
- }
5550
- if (normalized_style_name && (normalized_style_name.startsWith("Heading") || normalized_style_name === "Title"))
5551
- return normalized_style_name;
5552
- if (normalized_style_name && /Heading[ ]?([1-6])(?![0-9])/.test(normalized_style_name))
5553
- return normalized_style_name;
5554
- return "(heuristic)";
5555
- }
5556
- function _find_owned_end(block_records, heading_indices, current_h_pos, current_level) {
5557
- for (let next_h_pos = current_h_pos + 1; next_h_pos < heading_indices.length; next_h_pos++) {
5558
- const next_idx = heading_indices[next_h_pos];
5559
- if (_heading_level(block_records[next_idx].item) <= current_level)
5560
- return next_idx;
5561
- }
5562
- return block_records.length;
5563
- }
5564
- function _collect_footnote_ids(owned_blocks) {
5565
- const seen = /* @__PURE__ */ new Set();
5566
- const ordered = [];
5567
- for (const rec of owned_blocks) {
5568
- if (!rec.is_paragraph) continue;
5569
- for (const event of iter_paragraph_content(rec.item)) {
5570
- if (!("type" in event)) continue;
5571
- let fn_id = "";
5572
- if (event.type === "footnote") fn_id = `fn-${event.id}`;
5573
- else if (event.type === "endnote") fn_id = `en-${event.id}`;
5574
- else continue;
5575
- if (!seen.has(fn_id)) {
5576
- seen.add(fn_id);
5577
- ordered.push(fn_id);
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;
5578
5871
  }
5579
- }
5580
- }
5581
- return ordered;
5582
- }
5583
- function _offset_to_page(offset, body_page_offsets) {
5584
- if (!body_page_offsets || body_page_offsets.length === 0) return 1;
5585
- let page = 1;
5586
- for (let i = 0; i < body_page_offsets.length; i++) {
5587
- if (offset >= body_page_offsets[i]) page = i + 1;
5588
- else break;
5589
- }
5590
- return page;
5591
- }
5592
- function _extract_outline_fast(doc, projected_body, body_page_offsets, paragraph_offsets) {
5593
- const paragraphs_and_tables = [];
5594
- const seen_cells = /* @__PURE__ */ new Set();
5595
- function walk(container) {
5596
- for (const item of iter_block_items(container)) {
5597
- const i_type = item.constructor.name;
5598
- if (i_type === "FootnoteItem") {
5599
- walk(item);
5600
- } else if (item instanceof Paragraph) {
5601
- paragraphs_and_tables.push(["p", item]);
5602
- } else if (item instanceof Table) {
5603
- paragraphs_and_tables.push(["t", item]);
5604
- for (const row of item.rows) {
5605
- for (const cell of row.cells) {
5606
- if (seen_cells.has(cell._element)) {
5607
- continue;
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++;
5608
5936
  }
5609
- seen_cells.add(cell._element);
5610
- walk(cell);
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;
5611
5956
  }
5612
5957
  }
5613
5958
  }
5614
- }
5615
- }
5616
- walk(doc);
5617
- const heading_indices = [];
5618
- for (let idx = 0; idx < paragraphs_and_tables.length; idx++) {
5619
- const [kind, item] = paragraphs_and_tables[idx];
5620
- if (kind !== "p") continue;
5621
- let hasOffset = false;
5622
- if (paragraph_offsets instanceof Map) {
5623
- hasOffset = paragraph_offsets.has(item._element);
5624
5959
  } else {
5625
- hasOffset = item._element in paragraph_offsets;
5626
- }
5627
- if (!hasOffset) {
5628
- continue;
5629
- }
5630
- if (!_is_heading(item)) continue;
5631
- if (!_heading_passes_quality_filter_fast(item, projected_body, paragraph_offsets)) continue;
5632
- heading_indices.push(idx);
5633
- }
5634
- if (heading_indices.length === 0) return [];
5635
- const nodes = [];
5636
- for (let h_pos = 0; h_pos < heading_indices.length; h_pos++) {
5637
- const item_idx = heading_indices[h_pos];
5638
- const paragraph = paragraphs_and_tables[item_idx][1];
5639
- const level = _heading_level(paragraph);
5640
- const text = _heading_text_fast(paragraph, projected_body, paragraph_offsets);
5641
- const style = _determine_heading_style(paragraph);
5642
- let owned_end = item_idx;
5643
- for (let next_h_pos = h_pos + 1; next_h_pos < heading_indices.length; next_h_pos++) {
5644
- const next_idx = heading_indices[next_h_pos];
5645
- const next_paragraph = paragraphs_and_tables[next_idx][1];
5646
- if (_heading_level(next_paragraph) <= level) {
5647
- owned_end = next_idx;
5648
- break;
5649
- }
5650
- }
5651
- if (owned_end === item_idx) {
5652
- owned_end = paragraphs_and_tables.length;
5653
- }
5654
- const owned = paragraphs_and_tables.slice(item_idx + 1, owned_end);
5655
- let has_table = false;
5656
- for (const [kind2, item2] of owned) {
5657
- if (kind2 === "p" && _is_heading(item2)) {
5658
- break;
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
+ }
5659
5978
  }
5660
- if (kind2 === "t") {
5661
- has_table = true;
5662
- break;
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 = ["", ""];
5995
+ }
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}}`);
5663
6047
  }
5664
6048
  }
5665
- const footnote_ids = _collect_footnote_ids_fast(owned);
5666
- let para_offset;
5667
- if (paragraph_offsets instanceof Map) {
5668
- para_offset = paragraph_offsets.get(paragraph._element);
5669
- } else {
5670
- para_offset = paragraph_offsets[paragraph._element];
5671
- }
5672
- let page_num = 1;
5673
- if (para_offset !== void 0) {
5674
- const [start_offset] = para_offset;
5675
- page_num = _offset_to_page(start_offset, body_page_offsets);
5676
- }
5677
- nodes.push({
5678
- level,
5679
- text,
5680
- page: page_num,
5681
- style,
5682
- has_table,
5683
- footnote_ids
5684
- });
5685
- }
5686
- return nodes;
5687
- }
5688
- function _heading_passes_quality_filter_fast(paragraph, projected_body, paragraph_offsets) {
5689
- const style = _determine_heading_style(paragraph);
5690
- if (style !== "(heuristic)") return true;
5691
- const text = _heading_text_fast(paragraph, projected_body, paragraph_offsets);
5692
- if (!text) return false;
5693
- const words = text.match(/\w+/g) || [];
5694
- return words.length >= _HEURISTIC_MIN_WORDS;
5695
- }
5696
- function _heading_text_fast(paragraph, projected_body, paragraph_offsets) {
5697
- let offset;
5698
- if (paragraph_offsets instanceof Map) {
5699
- offset = paragraph_offsets.get(paragraph._element);
5700
- } else {
5701
- offset = paragraph_offsets[paragraph._element];
5702
6049
  }
5703
- if (offset === void 0) {
5704
- return "";
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}<<}`);
5705
6058
  }
5706
- const [start, length] = offset;
5707
- const raw = projected_body.substring(start, start + length);
5708
- let cleaned = _strip_critic_markup(raw);
5709
- cleaned = _strip_inline_formatting(cleaned);
5710
- cleaned = cleaned.replace(/^#+\s+/, "");
5711
- return cleaned.trim();
6059
+ return parts.join("");
5712
6060
  }
5713
- function _collect_footnote_ids_fast(owned_items) {
5714
- const seen = /* @__PURE__ */ new Set();
5715
- const ordered = [];
5716
- for (const [kind, item] of owned_items) {
5717
- if (kind !== "p") continue;
5718
- for (const event of iter_paragraph_content(item)) {
5719
- if (!("type" in event)) continue;
5720
- let fn_id = "";
5721
- if (event.type === "footnote") fn_id = `fn-${event.id}`;
5722
- else if (event.type === "endnote") fn_id = `en-${event.id}`;
5723
- else continue;
5724
- if (!seen.has(fn_id)) {
5725
- seen.add(fn_id);
5726
- ordered.push(fn_id);
5727
- }
5728
- }
5729
- }
5730
- return ordered;
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 ["", ""];
5731
6066
  }
5732
-
5733
- // src/sanitize/report.ts
5734
- var SanitizeReport = class {
5735
- filename;
5736
- mode;
5737
- author;
5738
- tracked_changes_found = 0;
5739
- tracked_changes_accepted = 0;
5740
- tracked_changes_kept = 0;
5741
- change_lines = [];
5742
- comments_removed = 0;
5743
- comments_kept = 0;
5744
- removed_comment_lines = [];
5745
- kept_comment_lines = [];
5746
- metadata_lines = [];
5747
- structural_lines = [];
5748
- warnings = [];
5749
- status = "clean";
5750
- blocked_reason = null;
5751
- constructor(filename, mode = "full", author = null) {
5752
- this.filename = filename;
5753
- this.mode = mode;
5754
- this.author = author;
5755
- }
5756
- add_transform_lines(lines) {
5757
- for (const line of lines) {
5758
- const lower = line.toLowerCase();
5759
- if (lower.includes("tracked change") || lower.includes("insertion") || lower.includes("deletion") || lower.includes("accepted")) {
5760
- this.change_lines.push(line);
5761
- } else if (lower.includes("comment") || lower.includes("[open]") || lower.includes("[resolved]")) {
5762
- if (lower.includes("kept") || lower.includes("visible")) {
5763
- this.kept_comment_lines.push(line);
5764
- } else {
5765
- this.removed_comment_lines.push(line);
5766
- }
5767
- } 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")) {
5768
- this.metadata_lines.push(line);
5769
- } else if (lower.includes("hyperlink") || lower.includes("warning")) {
5770
- this.warnings.push(line);
5771
- } else {
5772
- this.structural_lines.push(line);
5773
- }
5774
- }
5775
- }
5776
- render() {
5777
- const sep = "\u2550".repeat(50);
5778
- const lines = [sep, `Finalization Report: ${this.filename}`];
5779
- const flags = [];
5780
- if (this.mode === "keep-markup") flags.push("--keep-markup");
5781
- if (this.author) flags.push(`--author "${this.author}"`);
5782
- if (this.tracked_changes_accepted > 0) flags.push("--accept-all");
5783
- if (flags.length > 0) lines.push(flags.join(" "));
5784
- lines.push(sep);
5785
- if (this.status === "blocked") {
5786
- lines.push("");
5787
- lines.push(`BLOCKED: ${this.blocked_reason}`);
5788
- lines.push(sep);
5789
- return lines.join("\n");
5790
- }
5791
- if (this.mode === "keep-markup" && (this.tracked_changes_kept > 0 || this.comments_kept > 0)) {
5792
- lines.push("");
5793
- lines.push("VISIBLE TO COUNTERPARTY");
5794
- if (this.tracked_changes_kept > 0) lines.push(` Tracked changes: ${this.tracked_changes_kept}`);
5795
- if (this.comments_kept > 0) {
5796
- lines.push(` Open comments: ${this.comments_kept}`);
5797
- for (const cl of this.kept_comment_lines) lines.push(` ${cl}`);
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);
5798
6101
  }
5799
- if (this.author) lines.push(` Author on all markup: "${this.author}"`);
5800
- }
5801
- if (this.change_lines.length > 0) {
5802
- lines.push("");
5803
- lines.push("TRACKED CHANGES");
5804
- for (const cl of this.change_lines) lines.push(` ${cl}`);
5805
- }
5806
- if (this.removed_comment_lines.length > 0) {
5807
- lines.push("");
5808
- lines.push("COMMENTS (stripped)");
5809
- for (const cl of this.removed_comment_lines) lines.push(` ${cl}`);
5810
6102
  }
5811
- if (this.metadata_lines.length > 0) {
5812
- lines.push("");
5813
- lines.push("METADATA");
5814
- 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
+ }
5815
6111
  }
5816
- if (this.structural_lines.length > 0) {
5817
- lines.push("");
5818
- lines.push("STRUCTURAL & PROTECTION");
5819
- 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
+ }
5820
6120
  }
5821
- if (this.warnings.length > 0) {
5822
- lines.push("");
5823
- lines.push("WARNINGS");
5824
- 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
+ }
5825
6130
  }
5826
- lines.push("");
5827
- lines.push(sep);
5828
- if (this.warnings.length > 0) {
5829
- lines.push(`Result: CLEAN WITH WARNINGS (${this.warnings.length} warning${this.warnings.length > 1 ? "s" : ""})`);
5830
- } else {
5831
- 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);
5832
6134
  }
5833
- lines.push(sep);
5834
- return lines.join("\n");
5835
6135
  }
5836
- };
6136
+ return [...change_lines, ...comment_lines].join("\n");
6137
+ }
5837
6138
 
5838
- // src/sanitize/transforms.ts
5839
- function findDescendantsByLocalName(element, localName) {
5840
- const result = [];
5841
- const all = element.getElementsByTagName("*");
5842
- for (let i = 0; i < all.length; i++) {
5843
- const tag = all[i].tagName;
5844
- if (tag === localName || tag.endsWith(":" + localName)) {
5845
- result.push(all[i]);
5846
- }
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");
5847
6145
  }
5848
- return result;
5849
- }
5850
- function coalesce_runs(doc) {
5851
- let count = 0;
5852
- function areRunsIdentical(rPr1, rPr2) {
5853
- const xml1 = rPr1 ? rPr1.toString() : "";
5854
- const xml2 = rPr2 ? rPr2.toString() : "";
5855
- return xml1 === xml2;
6146
+ if (paragraph_offsets) {
6147
+ return _extract_outline_fast(doc, projected_body, body_page_offsets, paragraph_offsets);
5856
6148
  }
5857
- function hasSpecialContent(run) {
5858
- const safeTags = ["w:t", "w:tab", "w:br", "w:cr", "w:delText", "w:rPr"];
5859
- for (let i = 0; i < run.childNodes.length; i++) {
5860
- const child = run.childNodes[i];
5861
- if (child.nodeType === 1) {
5862
- const tag = child.tagName;
5863
- if (!safeTags.includes(tag)) return true;
5864
- }
5865
- }
5866
- 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);
5867
6157
  }
5868
- function coalesceContainer(container) {
5869
- const children = Array.from(container.childNodes).filter((n) => n.nodeType === 1);
5870
- let i = 0;
5871
- while (i < children.length - 1) {
5872
- const curr = children[i];
5873
- const nxt = children[i + 1];
5874
- if (curr.tagName === "w:r" && nxt.tagName === "w:r") {
5875
- if (!hasSpecialContent(curr) && !hasSpecialContent(nxt)) {
5876
- const rPr1 = findChild(curr, "w:rPr");
5877
- const rPr2 = findChild(nxt, "w:rPr");
5878
- if (areRunsIdentical(rPr1, rPr2)) {
5879
- let last_t = null;
5880
- for (let c = 0; c < curr.childNodes.length; c++) {
5881
- const child = curr.childNodes[c];
5882
- if (child.nodeType === 1 && (child.tagName === "w:t" || child.tagName === "w:delText")) {
5883
- last_t = child;
5884
- }
5885
- }
5886
- const nxtChildren = Array.from(nxt.childNodes).filter((n) => n.nodeType === 1);
5887
- for (const child of nxtChildren) {
5888
- if (child.tagName === "w:rPr") continue;
5889
- if ((child.tagName === "w:t" || child.tagName === "w:delText") && last_t && last_t.tagName === child.tagName) {
5890
- const t1 = last_t.textContent || "";
5891
- const t2 = child.textContent || "";
5892
- const combined = t1 + t2;
5893
- last_t.textContent = combined;
5894
- if (combined.trim() !== combined) {
5895
- last_t.setAttribute("xml:space", "preserve");
5896
- }
5897
- } else {
5898
- curr.appendChild(child);
5899
- if (child.tagName === "w:t" || child.tagName === "w:delText") {
5900
- last_t = child;
5901
- }
5902
- }
5903
- }
5904
- container.removeChild(nxt);
5905
- children.splice(i + 1, 1);
5906
- count++;
5907
- continue;
5908
- }
5909
- }
5910
- }
5911
- if (["w:ins", "w:del", "w:hyperlink", "w:sdt", "w:smartTag", "w:fldSimple", "w:sdtContent"].includes(curr.tagName)) {
5912
- coalesceContainer(curr);
5913
- }
5914
- 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;
5915
6197
  }
5916
- if (children.length > 0) {
5917
- const last = children[children.length - 1];
5918
- if (["w:ins", "w:del", "w:hyperlink", "w:sdt", "w:smartTag", "w:fldSimple", "w:sdtContent"].includes(last.tagName)) {
5919
- coalesceContainer(last);
5920
- }
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;
5921
6202
  }
5922
6203
  }
5923
- const paragraphs = findAllDescendants(doc.element, "w:p");
5924
- for (const p of paragraphs) coalesceContainer(p);
5925
- return count ? [`Adjacent identical runs coalesced: ${count}`] : [];
5926
- }
5927
- function strip_rsid(doc) {
5928
- let count = 0;
5929
- const rsidAttrs = ["w:rsidR", "w:rsidRPr", "w:rsidRDefault", "w:rsidP", "w:rsidDel", "w:rsidSect", "w:rsidTr"];
5930
- const all = doc.element.getElementsByTagName("*");
5931
- for (let i = 0; i < all.length; i++) {
5932
- for (const attr of rsidAttrs) {
5933
- if (all[i].hasAttribute(attr)) {
5934
- all[i].removeAttribute(attr);
5935
- count++;
5936
- }
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;
5937
6243
  }
5938
6244
  }
5939
- const rsidsElements = findAllDescendants(doc.element, "w:rsids");
5940
- for (const el of rsidsElements) {
5941
- if (el.parentNode) {
5942
- el.parentNode.removeChild(el);
5943
- 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++;
5944
6268
  }
6269
+ rows_processed++;
5945
6270
  }
5946
- return count ? [`rsid attributes: ${count} removed`] : [];
6271
+ return table_start_offset;
5947
6272
  }
5948
- function strip_para_ids(doc) {
5949
- let count = 0;
5950
- const attrs = ["w14:paraId", "w14:textId"];
5951
- const all = doc.element.getElementsByTagName("*");
5952
- for (let i = 0; i < all.length; i++) {
5953
- for (const attr of attrs) {
5954
- if (all[i].hasAttribute(attr)) {
5955
- all[i].removeAttribute(attr);
5956
- 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];
5957
6293
  }
6294
+ const table_text = extract_table(inner_item, comments_map, false, 0);
6295
+ cursor += table_text ? table_text.length : 0;
5958
6296
  }
6297
+ is_first_block = false;
5959
6298
  }
5960
- return count ? [`Paragraph/text IDs: ${count} removed`] : [];
6299
+ return [cursor, false];
5961
6300
  }
5962
- function strip_proof_errors(doc) {
5963
- const elements = findAllDescendants(doc.element, "w:proofErr");
5964
- elements.forEach((el) => el.parentNode?.removeChild(el));
5965
- 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;
5966
6308
  }
5967
- function strip_empty_properties(doc) {
5968
- let count = 0;
5969
- for (const tag of ["w:rPr", "w:pPr"]) {
5970
- const elements = findAllDescendants(doc.element, tag);
5971
- for (const el of elements) {
5972
- if (el.childNodes.length === 0 || el.childNodes.length === 1 && el.childNodes[0].nodeType === 3 && !el.childNodes[0].textContent?.trim()) {
5973
- el.parentNode?.removeChild(el);
5974
- 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
+ }
5975
6345
  }
5976
6346
  }
5977
6347
  }
5978
- return count ? [`Empty property elements: ${count} removed`] : [];
5979
6348
  }
5980
- function strip_hidden_text(doc) {
5981
- let count = 0;
5982
- const elements = findAllDescendants(doc.element, "w:rPr");
5983
- for (const rPr of elements) {
5984
- if (findChild(rPr, "w:vanish") || findChild(rPr, "w:webHidden")) {
5985
- const run = rPr.parentNode;
5986
- if (run && run.tagName === "w:r" && run.parentNode) {
5987
- run.parentNode.removeChild(run);
5988
- count++;
5989
- }
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;
5990
6373
  }
5991
6374
  }
5992
- return count ? [`Hidden text runs: ${count} removed`] : [];
6375
+ return blocks.join("\n\n");
5993
6376
  }
5994
- function count_tracked_changes(doc) {
5995
- const ins = findAllDescendants(doc.element, "w:ins").length;
5996
- const del = findAllDescendants(doc.element, "w:del").length;
5997
- const fmt = findAllDescendants(doc.element, "w:rPrChange").length + findAllDescendants(doc.element, "w:pPrChange").length + findAllDescendants(doc.element, "w:sectPrChange").length;
5998
- return [ins, del, fmt];
6377
+ function _is_heading(paragraph) {
6378
+ return _HEADING_PREFIX_RE.test(get_paragraph_prefix(paragraph));
5999
6379
  }
6000
- function get_track_change_authors(doc) {
6001
- const authors = /* @__PURE__ */ new Set();
6002
- for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange", "w:sectPrChange"]) {
6003
- for (const el of findAllDescendants(doc.element, tag)) {
6004
- const author = el.getAttribute("w:author");
6005
- if (author) authors.add(author);
6006
- }
6007
- }
6008
- 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;
6009
6387
  }
6010
- function _getElementText(el) {
6011
- const texts = [];
6012
- const ts = findAllDescendants(el, "w:t");
6013
- for (const t of ts) if (t.textContent) texts.push(t.textContent);
6014
- const dts = findAllDescendants(el, "w:delText");
6015
- for (const dt of dts) if (dt.textContent) texts.push(dt.textContent);
6016
- 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;
6017
6391
  }
6018
- function _truncate(text, maxLen = 60) {
6019
- const clean = text.replace(/\n/g, " ").trim();
6020
- if (clean.length <= maxLen) return clean;
6021
- 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();
6022
6397
  }
6023
- function accept_all_tracked_changes(doc) {
6024
- const lines = [];
6025
- const insEls = findAllDescendants(doc.element, "w:ins");
6026
- const delEls = findAllDescendants(doc.element, "w:del");
6027
- for (const ins of insEls) {
6028
- const text = _getElementText(ins).trim();
6029
- if (text) lines.push(` Accepted insertion: "${_truncate(text, 60)}"`);
6030
- }
6031
- for (const del of delEls) {
6032
- const text = _getElementText(del).trim();
6033
- if (text) lines.push(` Accepted deletion of: "${_truncate(text, 60)}"`);
6034
- }
6035
- const engine = new RedlineEngine(doc);
6036
- engine.accept_all_revisions();
6037
- for (const tag of ["w:rPrChange", "w:pPrChange", "w:sectPrChange"]) {
6038
- for (const el of findAllDescendants(doc.element, tag)) {
6039
- el.parentNode?.removeChild(el);
6040
- }
6041
- }
6042
- const total = insEls.length + delEls.length;
6043
- if (total) {
6044
- return [`Tracked changes auto-accepted: ${total}`].concat(lines);
6045
- }
6046
- 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;
6047
6405
  }
6048
- function get_comments_summary(doc) {
6049
- const data = extract_comments_data(doc.pkg);
6050
- const comments = [];
6051
- let openCount = 0;
6052
- let resolvedCount = 0;
6053
- for (const [cId, info] of Object.entries(data)) {
6054
- if (info.resolved) resolvedCount++;
6055
- else openCount++;
6056
- comments.push({ id: cId, ...info });
6057
- }
6058
- 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;
6059
6412
  }
6060
- function remove_all_comments(doc) {
6061
- const data = extract_comments_data(doc.pkg);
6062
- const keys = Object.keys(data);
6063
- if (keys.length === 0) return [];
6064
- const lines = [];
6065
- const cm = new CommentsManager(doc);
6066
- for (const [cId, info] of Object.entries(data)) {
6067
- const status = info.resolved ? "[Resolved]" : "[Open]";
6068
- lines.push(` ${status} "${_truncate(info.text || "", 60)}" (${info.author || "Unknown"})`);
6069
- cm.deleteComment(cId);
6070
- }
6071
- for (const tag of ["w:commentRangeStart", "w:commentRangeEnd"]) {
6072
- for (const el of findAllDescendants(doc.element, tag)) {
6073
- el.parentNode?.removeChild(el);
6074
- }
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;
6075
6422
  }
6076
- const refs = findAllDescendants(doc.element, "w:commentReference");
6077
- for (const ref of refs) {
6078
- const parent = ref.parentNode;
6079
- if (parent) {
6080
- if (parent.tagName === "w:r" || parent.tagName.endsWith(":r")) {
6081
- const nonRprChildren = Array.from(parent.childNodes).filter(
6082
- (c) => c.nodeType === 1 && c.tagName !== "w:rPr" && c.tagName !== "rPr"
6083
- );
6084
- if (nonRprChildren.length <= 1) {
6085
- parent.parentNode?.removeChild(parent);
6086
- } else {
6087
- parent.removeChild(ref);
6088
- }
6089
- } else {
6090
- parent.removeChild(ref);
6091
- }
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);
6092
6429
  }
6093
6430
  }
6094
- const resolvedCount = Object.values(data).filter((c) => c.resolved).length;
6095
- const openCount = Object.values(data).filter((c) => !c.resolved).length;
6096
- return [`Comments removed: ${keys.length} (${resolvedCount} resolved, ${openCount} open)`].concat(lines);
6097
- }
6098
- function eject_comment_parts(doc) {
6099
- const pkg = doc.pkg;
6100
- const comment_partnames = /* @__PURE__ */ new Set();
6101
- for (const part of pkg.parts) {
6102
- if (part.partname.toLowerCase().includes("comments")) {
6103
- comment_partnames.add(part.partname);
6104
- const withSlash = part.partname.startsWith("/") ? part.partname : "/" + part.partname;
6105
- const withoutSlash = part.partname.startsWith("/") ? part.partname.substring(1) : part.partname;
6106
- comment_partnames.add(withSlash);
6107
- comment_partnames.add(withoutSlash);
6108
- }
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;
6109
6434
  }
6110
- if (comment_partnames.size === 0) return;
6111
- for (const part of pkg.parts) {
6112
- if (part.partname.endsWith(".rels")) {
6113
- const rels = findAllDescendants(part._element, "Relationship");
6114
- const toRemove = [];
6115
- for (const rel of rels) {
6116
- const target = rel.getAttribute("Target") || "";
6117
- if (target.toLowerCase().includes("comments")) {
6118
- toRemove.push(rel);
6119
- const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
6120
- const sourcePart = pkg.getPartByPath(sourcePath);
6121
- if (sourcePart) {
6122
- const relId = rel.getAttribute("Id");
6123
- if (relId) sourcePart.rels.delete(relId);
6124
- }
6125
- }
6126
- }
6127
- for (const relEl of toRemove) {
6128
- relEl.parentNode?.removeChild(relEl);
6129
- }
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";
6130
6442
  }
6131
6443
  }
6132
- const ctPart = pkg.getPartByPath("[Content_Types].xml");
6133
- if (ctPart) {
6134
- const overrides = findAllDescendants(ctPart._element, "Override");
6135
- const toRemove = [];
6136
- for (const override of overrides) {
6137
- const partName = override.getAttribute("PartName") || "";
6138
- if (comment_partnames.has(partName) || partName.toLowerCase().includes("comments")) {
6139
- toRemove.push(override);
6140
- }
6141
- }
6142
- for (const overrideEl of toRemove) {
6143
- 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;
6144
6448
  }
6145
6449
  }
6146
- pkg.parts = pkg.parts.filter((p) => !p.partname.toLowerCase().includes("comments"));
6147
- for (const key of Object.keys(pkg.unzipped)) {
6148
- if (key.toLowerCase().includes("comments")) {
6149
- 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;
6150
6453
  }
6454
+ return "(outline_level)";
6151
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)";
6152
6461
  }
6153
- function replace_comment_authors(doc, newAuthor) {
6154
- const cm = new CommentsManager(doc);
6155
- if (!cm.commentsPart) return [];
6156
- const original = /* @__PURE__ */ new Set();
6157
- const comments = findAllDescendants(cm.commentsPart._element, "w:comment");
6158
- for (const c of comments) {
6159
- const author = c.getAttribute("w:author");
6160
- if (author) {
6161
- original.add(author);
6162
- c.setAttribute("w:author", newAuthor);
6163
- }
6164
- if (c.hasAttribute("w:initials")) {
6165
- const initials = newAuthor.split(" ").filter(Boolean).map((p) => p[0]).join("").toUpperCase();
6166
- c.setAttribute("w:initials", initials);
6167
- }
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;
6168
6467
  }
6169
- return original.size ? [`Comment authors replaced: ${Array.from(original).sort().join(", ")} \u2192 "${newAuthor}"`] : [];
6468
+ return block_records.length;
6170
6469
  }
6171
- function replace_change_authors(doc, newAuthor) {
6172
- const original = /* @__PURE__ */ new Set();
6173
- for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange"]) {
6174
- for (const el of findAllDescendants(doc.element, tag)) {
6175
- const author = el.getAttribute("w:author");
6176
- if (author) {
6177
- original.add(author);
6178
- 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);
6179
6484
  }
6180
6485
  }
6181
6486
  }
6182
- return original.size ? [`Track change authors replaced: ${Array.from(original).sort().join(", ")} \u2192 "${newAuthor}"`] : [];
6487
+ return ordered;
6183
6488
  }
6184
- function normalize_change_dates(doc) {
6185
- let count = 0;
6186
- const fixed = "2025-01-01T00:00:00Z";
6187
- for (const tag of ["w:ins", "w:del", "w:rPrChange", "w:pPrChange"]) {
6188
- for (const el of findAllDescendants(doc.element, tag)) {
6189
- if (el.hasAttribute("w:date")) {
6190
- el.setAttribute("w:date", fixed);
6191
- 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
+ }
6192
6519
  }
6193
6520
  }
6194
6521
  }
6195
- return count ? [`Track change timestamps: ${count} normalized`] : [];
6196
- }
6197
- function scrub_doc_properties(doc) {
6198
- const lines = [];
6199
- const corePart = doc.pkg.getPartByPath("docProps/core.xml");
6200
- if (corePart) {
6201
- const creators = findDescendantsByLocalName(corePart._element, "creator");
6202
- creators.forEach((c) => {
6203
- if (c.textContent) {
6204
- lines.push(`Author: ${c.textContent}`);
6205
- 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;
6206
6555
  }
6207
- });
6208
- const modifiers = findDescendantsByLocalName(corePart._element, "lastModifiedBy");
6209
- modifiers.forEach((c) => {
6210
- if (c.textContent) {
6211
- lines.push(`Last modified by: ${c.textContent}`);
6212
- 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;
6213
6565
  }
6214
- });
6215
- const revisions = findDescendantsByLocalName(corePart._element, "revision");
6216
- revisions.forEach((c) => {
6217
- if (c.textContent && parseInt(c.textContent) > 1) {
6218
- lines.push(`Revision count: ${c.textContent} \u2192 1`);
6219
- c.textContent = "1";
6566
+ if (kind2 === "t") {
6567
+ has_table = true;
6568
+ break;
6220
6569
  }
6221
- });
6222
- }
6223
- const appPart = doc.pkg.getPartByPath("docProps/app.xml");
6224
- if (appPart) {
6225
- const docEl = appPart._element;
6226
- const intFields = ["TotalTime", "Words", "Characters", "Paragraphs", "Lines", "CharactersWithSpaces"];
6227
- for (const f of intFields) {
6228
- findDescendantsByLocalName(docEl, f).forEach((el) => {
6229
- if (el.textContent && el.textContent !== "0") {
6230
- if (f === "TotalTime") lines.push(`Total editing time: ${el.textContent} minutes`);
6231
- el.textContent = "0";
6232
- }
6233
- });
6234
6570
  }
6235
- const strFields = ["Template", "Manager", "Company"];
6236
- for (const f of strFields) {
6237
- findDescendantsByLocalName(docEl, f).forEach((el) => {
6238
- if (el.textContent) {
6239
- lines.push(`${f}: ${el.textContent}`);
6240
- el.textContent = "";
6241
- }
6242
- });
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];
6243
6577
  }
6244
- }
6245
- return lines.length ? ["Metadata scrubbed:", ...lines.map((l) => ` ${l}`)] : [];
6246
- }
6247
- function scrub_timestamps(doc) {
6248
- let modified = false;
6249
- const epoch = "1970-01-01T00:00:00Z";
6250
- const corePart = doc.pkg.getPartByPath("docProps/core.xml");
6251
- if (corePart) {
6252
- for (const tag of ["created", "modified", "lastPrinted"]) {
6253
- findDescendantsByLocalName(corePart._element, tag).forEach((el) => {
6254
- if (el.textContent && el.textContent !== epoch) {
6255
- el.textContent = epoch;
6256
- modified = true;
6257
- }
6258
- });
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);
6259
6582
  }
6583
+ nodes.push({
6584
+ level,
6585
+ text,
6586
+ page: page_num,
6587
+ style,
6588
+ has_table,
6589
+ footnote_ids
6590
+ });
6260
6591
  }
6261
- return modified ? ["Timestamps normalized to epoch"] : [];
6592
+ return nodes;
6262
6593
  }
6263
- function strip_custom_xml(doc) {
6264
- const customParts = doc.pkg.parts.filter((p) => p.partname.includes("/customXml"));
6265
- if (customParts.length === 0) return [];
6266
- const partnames = new Set(customParts.map((p) => p.partname));
6267
- doc.pkg.parts = doc.pkg.parts.filter((p) => !partnames.has(p.partname));
6268
- const removeRelationsTo = (relsPart) => {
6269
- const toRemove = [];
6270
- for (const rel of findAllDescendants(relsPart._element, "Relationship")) {
6271
- const target = rel.getAttribute("Target");
6272
- if (target && target.includes("customXml")) toRemove.push(rel);
6273
- }
6274
- toRemove.forEach((r) => r.parentNode?.removeChild(r));
6275
- };
6276
- const rootRels = doc.pkg.getPartByPath("_rels/.rels");
6277
- if (rootRels) removeRelationsTo(rootRels);
6278
- const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
6279
- if (docRels) removeRelationsTo(docRels);
6280
- for (const sdtPr of findAllDescendants(doc.element, "w:sdtPr")) {
6281
- 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];
6282
6608
  }
6283
- 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();
6284
6618
  }
6285
- function strip_image_alt_text(doc) {
6286
- let count = 0;
6287
- for (const docPr of findDescendantsByLocalName(doc.element, "docPr")) {
6288
- const descr = docPr.getAttribute("descr");
6289
- if (descr) {
6290
- const isShort = descr.length < 10;
6291
- const isFile = descr.includes(".") && descr.length < 60;
6292
- if (isShort || isFile) {
6293
- docPr.removeAttribute("descr");
6294
- 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);
6295
6633
  }
6296
6634
  }
6297
6635
  }
6298
- return count ? [`Image alt text: ${count} auto-generated descriptions removed`] : [];
6636
+ return ordered;
6299
6637
  }
6300
- function audit_hyperlinks(doc) {
6301
- const internal = ["sharepoint.com", "onedrive.com", ".internal", "intranet", "localhost", "10.", "192.168.", "172.16."];
6302
- const warnings = [];
6303
- const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
6304
- for (const rel of findAllDescendants(docRels._element, "Relationship")) {
6305
- if (rel.getAttribute("TargetMode") === "External") {
6306
- const url = rel.getAttribute("Target") || "";
6307
- for (const pattern of internal) {
6308
- if (url.toLowerCase().includes(pattern.toLowerCase())) {
6309
- warnings.push(`Hyperlink targets internal URL: ${_truncate(url, 80)}`);
6310
- 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);
6311
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);
6312
6679
  }
6313
6680
  }
6314
6681
  }
6315
- return warnings;
6316
- }
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
+ };
6317
6743
 
6318
6744
  // src/sanitize/core.ts
6319
6745
  async function finalize_document(doc, options) {