@adeu/core 1.23.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2845,7 +2845,8 @@ function _row_ops_for_table(table_o, table_m, opcodes, warnings) {
2845
2845
  type: "modify",
2846
2846
  target_text: o_txt,
2847
2847
  new_text: m_txt,
2848
- comment: "Diff: Table row modified"
2848
+ comment: "Diff: Table row modified",
2849
+ _is_table_edit: true
2849
2850
  });
2850
2851
  }
2851
2852
  }
@@ -2947,14 +2948,18 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
2947
2948
  if (row_opcodes !== null) {
2948
2949
  edits.push(..._row_ops_for_table(t_o, t_m, row_opcodes, warnings));
2949
2950
  } else {
2950
- const tbl_edits = generate_edits_from_text(
2951
- text_orig.substring(t_o.start, t_o.end),
2952
- text_mod.substring(t_m.start, t_m.end)
2953
- );
2954
- for (const e of tbl_edits) {
2955
- e._match_start_index = (e._match_start_index || 0) + t_o.start;
2951
+ for (let k = 0; k < t_o.rows.length; k++) {
2952
+ const o_txt = t_o.rows[k].cells.join(" | ");
2953
+ const m_txt = t_m.rows[k].cells.join(" | ");
2954
+ if (o_txt === m_txt) continue;
2955
+ edits.push({
2956
+ type: "modify",
2957
+ target_text: o_txt,
2958
+ new_text: m_txt,
2959
+ comment: "Diff: Table row modified",
2960
+ _is_table_edit: true
2961
+ });
2956
2962
  }
2957
- edits.push(...tbl_edits);
2958
2963
  }
2959
2964
  }
2960
2965
  }
@@ -2972,7 +2977,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
2972
2977
  };
2973
2978
  let ambiguous_anchor_warned = false;
2974
2979
  for (const e of edits) {
2975
- if ((e.type === "insert_row" || e.type === "delete_row") && !ambiguous_anchor_warned) {
2980
+ if ((e.type === "insert_row" || e.type === "delete_row" || e._is_table_edit) && !ambiguous_anchor_warned) {
2976
2981
  if (e.target_text && countOccurrences(text_orig, e.target_text) > 1) {
2977
2982
  warnings.push(
2978
2983
  `The row anchor "${e.target_text.substring(0, 60)}" appears more than once in the document. Applying this diff from its JSON output may be rejected as ambiguous \u2014 make the anchor rows unique, or apply the row changes with explicit insert_row/delete_row edits.`
@@ -4724,7 +4729,7 @@ var RedlineEngine = class _RedlineEngine {
4724
4729
  *
4725
4730
  * Does NOT attach comments; callers handle that.
4726
4731
  */
4727
- _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
4732
+ _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null) {
4728
4733
  if (!text) {
4729
4734
  return {
4730
4735
  first_node: null,
@@ -4745,7 +4750,29 @@ var RedlineEngine = class _RedlineEngine {
4745
4750
  }
4746
4751
  current_p = walker;
4747
4752
  }
4748
- while (lines.length > 1 && lines[lines.length - 1] === "") {
4753
+ const suffix_nodes = [];
4754
+ const pos_source = positional_anchor_el && positional_anchor_el.parentNode ? positional_anchor_el : anchor_run !== null && anchor_run._element.parentNode ? anchor_run._element : null;
4755
+ if (current_p !== null && pos_source !== null) {
4756
+ let pos_anchor = pos_source;
4757
+ while (pos_anchor && pos_anchor.parentNode !== current_p) {
4758
+ pos_anchor = pos_anchor.parentNode;
4759
+ if (pos_anchor === current_p) {
4760
+ pos_anchor = null;
4761
+ break;
4762
+ }
4763
+ }
4764
+ if (pos_anchor) {
4765
+ const relocatable = /* @__PURE__ */ new Set(["w:r", "w:ins", "w:del"]);
4766
+ let nxt = pos_anchor.nextSibling;
4767
+ while (nxt) {
4768
+ if (nxt.nodeType === 1 && relocatable.has(nxt.tagName)) {
4769
+ suffix_nodes.push(nxt);
4770
+ }
4771
+ nxt = nxt.nextSibling;
4772
+ }
4773
+ }
4774
+ }
4775
+ while (lines.length > 1 && lines[lines.length - 1] === "" && suffix_nodes.length === 0) {
4749
4776
  lines.pop();
4750
4777
  }
4751
4778
  if (lines.length === 0) {
@@ -4843,6 +4870,12 @@ var RedlineEngine = class _RedlineEngine {
4843
4870
  first_node = new_p;
4844
4871
  }
4845
4872
  }
4873
+ if (!block_mode && last_p && suffix_nodes.length > 0) {
4874
+ for (const node of suffix_nodes) {
4875
+ node.parentNode?.removeChild(node);
4876
+ last_p.appendChild(node);
4877
+ }
4878
+ }
4846
4879
  return { first_node, last_p, last_ins, used_block_mode: block_mode };
4847
4880
  }
4848
4881
  /**
@@ -4900,7 +4933,7 @@ var RedlineEngine = class _RedlineEngine {
4900
4933
  if (stripped_text.startsWith("* ") || stripped_text.startsWith("- ")) {
4901
4934
  return [stripped_text.substring(2).trim(), "List Paragraph"];
4902
4935
  }
4903
- const match = stripped_text.match(/^\d+\.\s+/);
4936
+ const match = stripped_text.match(/^1\.\s+/);
4904
4937
  if (match) {
4905
4938
  return [stripped_text.substring(match[0].length).trim(), "List Number"];
4906
4939
  }
@@ -5782,14 +5815,17 @@ var RedlineEngine = class _RedlineEngine {
5782
5815
  resolved_edits.push([edit, edit.new_text || null]);
5783
5816
  } else if (edit.type === "insert_row" || edit.type === "delete_row") {
5784
5817
  let matches = this.mapper.find_all_match_indices(edit.target_text);
5818
+ let resolved_mapper = this.mapper;
5785
5819
  if (matches.length === 0) {
5786
5820
  if (!this.clean_mapper) {
5787
5821
  this.clean_mapper = new DocumentMapper(this.doc, true);
5788
5822
  }
5789
5823
  matches = this.clean_mapper.find_all_match_indices(edit.target_text);
5824
+ resolved_mapper = this.clean_mapper;
5790
5825
  }
5791
5826
  if (matches.length > 0) {
5792
5827
  edit._resolved_start_idx = matches[0][0];
5828
+ edit._active_mapper_ref = resolved_mapper;
5793
5829
  resolved_edits.push([edit, null]);
5794
5830
  } else {
5795
5831
  skipped++;
@@ -6038,7 +6074,8 @@ var RedlineEngine = class _RedlineEngine {
6038
6074
  }
6039
6075
  _apply_table_edit(edit, rebuild_map) {
6040
6076
  const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
6041
- const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
6077
+ const active_mapper = edit._active_mapper_ref || this.mapper;
6078
+ const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
6042
6079
  start_idx,
6043
6080
  rebuild_map
6044
6081
  );
@@ -6219,9 +6256,14 @@ var RedlineEngine = class _RedlineEngine {
6219
6256
  if (overlaps_virtual_pipe) {
6220
6257
  const actual_cells = actual_doc_text.split("|");
6221
6258
  const new_cells = current_effective_new_text.split("|");
6222
- if (actual_cells.length === new_cells.length && actual_cells.length > 1) {
6259
+ if (actual_cells.length !== new_cells.length) {
6260
+ throw new BatchValidationError([
6261
+ `Target text spans ${actual_cells.length} table cells, but replacement provides ${new_cells.length}. To modify text without altering table structure (rows or columns), ensure the replacement contains the exact same number of '|' separators (e.g., replace with 'CellC | ' to empty the second cell).`
6262
+ ]);
6263
+ }
6264
+ if (actual_cells.length > 1) {
6223
6265
  const sub_edits2 = [];
6224
- let search_offset = start_idx;
6266
+ let cell_start_in_target = 0;
6225
6267
  let target_comment_idx = 0;
6226
6268
  for (let idx = 0; idx < actual_cells.length; idx++) {
6227
6269
  if (actual_cells[idx].trim() !== new_cells[idx].trim()) {
@@ -6234,13 +6276,7 @@ var RedlineEngine = class _RedlineEngine {
6234
6276
  const n_cell = new_cells[cell_idx];
6235
6277
  const a_clean = a_cell.trim();
6236
6278
  const n_clean = n_cell.trim();
6237
- let actual_start = search_offset;
6238
- if (a_clean) {
6239
- actual_start = this.mapper.full_text.indexOf(a_clean, search_offset);
6240
- if (actual_start === -1 || actual_start > search_offset + 10) {
6241
- actual_start = search_offset;
6242
- }
6243
- }
6279
+ const actual_start = start_idx + cell_start_in_target + (a_clean ? a_cell.indexOf(a_clean) : 0);
6244
6280
  const should_attach_comment = edit.comment !== null && edit.comment !== void 0 && cell_idx === target_comment_idx;
6245
6281
  if (a_clean !== n_clean || should_attach_comment) {
6246
6282
  const cell_sub_edits = this._word_diff_sub_edits(
@@ -6257,24 +6293,12 @@ var RedlineEngine = class _RedlineEngine {
6257
6293
  sub_edits2.push(se);
6258
6294
  }
6259
6295
  }
6260
- if (a_clean) {
6261
- search_offset = actual_start + a_clean.length;
6262
- }
6263
- const next_pipe = this.mapper.full_text.indexOf(" | ", search_offset);
6264
- if (next_pipe !== -1 && next_pipe <= search_offset + 10) {
6265
- search_offset = next_pipe + 3;
6266
- } else {
6267
- search_offset += a_cell.length + 1;
6268
- }
6296
+ cell_start_in_target += a_cell.length + 1;
6269
6297
  }
6270
6298
  for (const sub of sub_edits2) {
6271
6299
  all_sub_edits.push(sub);
6272
6300
  }
6273
6301
  continue;
6274
- } else {
6275
- throw new BatchValidationError([
6276
- `Target text spans ${actual_cells.length} table cells, but replacement provides ${new_cells.length}. To modify text without altering table structure (rows or columns), ensure the replacement contains the exact same number of '|' separators (e.g., replace with 'CellC | ' to empty the second cell).`
6277
- ]);
6278
6302
  }
6279
6303
  }
6280
6304
  let has_markdown = false;
@@ -6368,6 +6392,19 @@ var RedlineEngine = class _RedlineEngine {
6368
6392
  continue;
6369
6393
  }
6370
6394
  }
6395
+ if (!final_target && final_new) {
6396
+ all_sub_edits.push({
6397
+ type: "modify",
6398
+ target_text: "",
6399
+ new_text: final_new,
6400
+ comment: edit.comment,
6401
+ _resolved_start_idx: effective_start_idx,
6402
+ _match_start_index: effective_start_idx,
6403
+ _internal_op: "INSERTION",
6404
+ _active_mapper_ref: active_mapper
6405
+ });
6406
+ continue;
6407
+ }
6371
6408
  const sub_edits = this._word_diff_sub_edits(
6372
6409
  actual_doc_text,
6373
6410
  current_effective_new_text,
@@ -6809,7 +6846,10 @@ var RedlineEngine = class _RedlineEngine {
6809
6846
  edit.new_text,
6810
6847
  style_source_run,
6811
6848
  mod_anchor_para,
6812
- ins_id
6849
+ ins_id,
6850
+ // The insertion physically follows the deletion block; the style
6851
+ // run was detached when the deletion cloned it into <w:del>.
6852
+ last_del
6813
6853
  );
6814
6854
  if (result.first_node) {
6815
6855
  const is_inline_first = result.first_node.tagName === "w:ins";
@@ -7356,7 +7396,10 @@ function scrub_doc_properties(doc) {
7356
7396
  ["keywords", "Keywords"],
7357
7397
  ["subject", "Subject"],
7358
7398
  ["contentStatus", "Content status"],
7359
- ["description", "Description/comments"]
7399
+ ["description", "Description/comments"],
7400
+ ["identifier", "Identifier"],
7401
+ ["language", "Language"],
7402
+ ["version", "Version"]
7360
7403
  ];
7361
7404
  for (const [local, label] of leakFields) {
7362
7405
  findDescendantsByLocalName(corePart._element, local).forEach((c) => {
@@ -7414,27 +7457,73 @@ function scrub_timestamps(doc) {
7414
7457
  }
7415
7458
  return modified ? ["Timestamps normalized to epoch"] : [];
7416
7459
  }
7417
- function strip_custom_xml(doc) {
7418
- const customParts = doc.pkg.parts.filter((p) => p.partname.includes("/customXml"));
7419
- if (customParts.length === 0) return [];
7420
- const partnames = new Set(customParts.map((p) => p.partname));
7421
- doc.pkg.parts = doc.pkg.parts.filter((p) => !partnames.has(p.partname));
7422
- const removeRelationsTo = (relsPart) => {
7460
+ function ejectPackageMembers(doc, matcher, relTargetMatcher) {
7461
+ const pkg = doc.pkg;
7462
+ const normalized = (p) => p.startsWith("/") ? p.substring(1) : p;
7463
+ for (const part of pkg.parts) {
7464
+ if (!part.partname.endsWith(".rels")) continue;
7423
7465
  const toRemove = [];
7424
- for (const rel of findAllDescendants(relsPart._element, "Relationship")) {
7425
- const target = rel.getAttribute("Target");
7426
- if (target && target.includes("customXml")) toRemove.push(rel);
7466
+ for (const rel of findAllDescendants(part._element, "Relationship")) {
7467
+ const target = rel.getAttribute("Target") || "";
7468
+ if (relTargetMatcher(target)) {
7469
+ toRemove.push(rel);
7470
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
7471
+ const sourcePart = pkg.getPartByPath(sourcePath);
7472
+ if (sourcePart) {
7473
+ const relId = rel.getAttribute("Id");
7474
+ if (relId) sourcePart.rels.delete(relId);
7475
+ }
7476
+ }
7427
7477
  }
7428
7478
  toRemove.forEach((r) => r.parentNode?.removeChild(r));
7429
- };
7430
- const rootRels = doc.pkg.getPartByPath("_rels/.rels");
7431
- if (rootRels) removeRelationsTo(rootRels);
7432
- const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
7433
- if (docRels) removeRelationsTo(docRels);
7479
+ }
7480
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
7481
+ if (ctPart) {
7482
+ const toRemove = [];
7483
+ for (const override of findAllDescendants(ctPart._element, "Override")) {
7484
+ const partName = override.getAttribute("PartName") || "";
7485
+ if (matcher(normalized(partName))) toRemove.push(override);
7486
+ }
7487
+ toRemove.forEach((o) => o.parentNode?.removeChild(o));
7488
+ }
7489
+ pkg.parts = pkg.parts.filter((p) => !matcher(normalized(p.partname)));
7490
+ for (const key of Object.keys(pkg.unzipped)) {
7491
+ if (matcher(normalized(key))) delete pkg.unzipped[key];
7492
+ }
7493
+ }
7494
+ function strip_custom_xml(doc) {
7495
+ const isCustomXml = (path) => path.startsWith("customXml/");
7496
+ const customParts = doc.pkg.parts.filter(
7497
+ (p) => isCustomXml(p.partname.startsWith("/") ? p.partname.substring(1) : p.partname)
7498
+ );
7499
+ const leakedMembers = Object.keys(doc.pkg.unzipped).filter(isCustomXml);
7500
+ if (customParts.length === 0 && leakedMembers.length === 0) return [];
7501
+ ejectPackageMembers(doc, isCustomXml, (target) => target.includes("customXml"));
7434
7502
  for (const sdtPr of findAllDescendants(doc.element, "w:sdtPr")) {
7435
7503
  findChildren(sdtPr, "w:dataBinding").forEach((b) => sdtPr.removeChild(b));
7436
7504
  }
7437
- return [`Custom XML parts: ${customParts.length} removed`];
7505
+ return [`Custom XML parts: ${Math.max(customParts.length, leakedMembers.length)} removed`];
7506
+ }
7507
+ var CUSTOM_PROPS_PATH = "docProps/custom.xml";
7508
+ function strip_custom_properties(doc) {
7509
+ const part = doc.pkg.getPartByPath(CUSTOM_PROPS_PATH);
7510
+ const leaked = Object.keys(doc.pkg.unzipped).includes(CUSTOM_PROPS_PATH);
7511
+ if (!part && !leaked) return [];
7512
+ const lines = [];
7513
+ if (part) {
7514
+ for (const prop of findDescendantsByLocalName(part._element, "property")) {
7515
+ const name = prop.getAttribute("name") || "(unnamed)";
7516
+ const value = (prop.textContent || "").trim();
7517
+ lines.push(` Custom property removed: ${name} = "${_truncate(value, 60)}"`);
7518
+ }
7519
+ }
7520
+ ejectPackageMembers(
7521
+ doc,
7522
+ (path) => path === CUSTOM_PROPS_PATH,
7523
+ (target) => target.includes("docProps/custom.xml")
7524
+ );
7525
+ const count = Math.max(lines.length, 1);
7526
+ return [`Custom document properties: ${count} removed (docProps/custom.xml)`].concat(lines);
7438
7527
  }
7439
7528
  function strip_image_alt_text(doc) {
7440
7529
  let count = 0;
@@ -8788,6 +8877,9 @@ function _collect_footnote_ids_fast(owned_items) {
8788
8877
  return ordered;
8789
8878
  }
8790
8879
 
8880
+ // src/sanitize/core.ts
8881
+ var import_fflate2 = require("fflate");
8882
+
8791
8883
  // src/sanitize/report.ts
8792
8884
  var SanitizeReport = class {
8793
8885
  filename;
@@ -8822,7 +8914,7 @@ var SanitizeReport = class {
8822
8914
  } else {
8823
8915
  this.removed_comment_lines.push(line);
8824
8916
  }
8825
- } 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")) {
8917
+ } 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("custom propert") || lower.includes("identifier") || lower.includes("language") || lower.includes("version") || lower.includes("last modified by") || lower.includes("revision count") || lower.includes("last printed")) {
8826
8918
  this.metadata_lines.push(line);
8827
8919
  } else if (lower.includes("hyperlink") || lower.includes("warning")) {
8828
8920
  this.warnings.push(line);
@@ -8935,6 +9027,7 @@ async function finalize_document(doc, options) {
8935
9027
  report.add_transform_lines(scrub_doc_properties(doc));
8936
9028
  report.add_transform_lines(scrub_timestamps(doc));
8937
9029
  report.add_transform_lines(strip_custom_xml(doc));
9030
+ report.add_transform_lines(strip_custom_properties(doc));
8938
9031
  report.add_transform_lines(strip_image_alt_text(doc));
8939
9032
  const warnings = audit_hyperlinks(doc);
8940
9033
  for (const w of warnings) report.warnings.push(w);
@@ -8982,8 +9075,47 @@ async function finalize_document(doc, options) {
8982
9075
  }
8983
9076
  if (report.warnings.length > 0) report.status = "clean_with_warnings";
8984
9077
  const outBuffer = await doc.save();
9078
+ verifySanitizedPackage(outBuffer);
8985
9079
  return { reportText: report.render(), outBuffer };
8986
9080
  }
9081
+ var VERIFIED_CORE_FIELDS = [
9082
+ ["creator", "author (dc:creator)"],
9083
+ ["lastModifiedBy", "last modified by (cp:lastModifiedBy)"],
9084
+ ["identifier", "identifier (dc:identifier)"],
9085
+ ["description", "description (dc:description)"],
9086
+ ["keywords", "keywords (cp:keywords)"],
9087
+ ["category", "category (cp:category)"],
9088
+ ["subject", "subject (dc:subject)"],
9089
+ ["contentStatus", "content status (cp:contentStatus)"],
9090
+ ["language", "language (dc:language)"],
9091
+ ["version", "version (cp:version)"]
9092
+ ];
9093
+ function verifySanitizedPackage(outBuffer) {
9094
+ const unzipped = (0, import_fflate2.unzipSync)(new Uint8Array(outBuffer));
9095
+ const names = Object.keys(unzipped);
9096
+ const problems = [];
9097
+ if (names.includes("docProps/custom.xml")) {
9098
+ problems.push("docProps/custom.xml (custom document properties) is still in the package");
9099
+ }
9100
+ if (names.some((n) => n.startsWith("customXml/"))) {
9101
+ problems.push("customXml/* parts are still in the package");
9102
+ }
9103
+ if (names.includes("docProps/core.xml")) {
9104
+ const core = parseXml((0, import_fflate2.strFromU8)(unzipped["docProps/core.xml"]));
9105
+ for (const [local, label] of VERIFIED_CORE_FIELDS) {
9106
+ for (const el of findDescendantsByLocalName(core.documentElement, local)) {
9107
+ if ((el.textContent || "").trim()) {
9108
+ problems.push(`core property ${label} still contains a value`);
9109
+ }
9110
+ }
9111
+ }
9112
+ }
9113
+ if (problems.length > 0) {
9114
+ throw new Error(
9115
+ "Sanitize integrity check failed \u2014 the saved package still contains metadata this run claims to remove:\n - " + problems.join("\n - ") + "\nRefusing to report a clean document."
9116
+ );
9117
+ }
9118
+ }
8987
9119
 
8988
9120
  // src/index.ts
8989
9121
  function identifyEngine() {