@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.js CHANGED
@@ -2787,7 +2787,8 @@ function _row_ops_for_table(table_o, table_m, opcodes, warnings) {
2787
2787
  type: "modify",
2788
2788
  target_text: o_txt,
2789
2789
  new_text: m_txt,
2790
- comment: "Diff: Table row modified"
2790
+ comment: "Diff: Table row modified",
2791
+ _is_table_edit: true
2791
2792
  });
2792
2793
  }
2793
2794
  }
@@ -2889,14 +2890,18 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
2889
2890
  if (row_opcodes !== null) {
2890
2891
  edits.push(..._row_ops_for_table(t_o, t_m, row_opcodes, warnings));
2891
2892
  } else {
2892
- const tbl_edits = generate_edits_from_text(
2893
- text_orig.substring(t_o.start, t_o.end),
2894
- text_mod.substring(t_m.start, t_m.end)
2895
- );
2896
- for (const e of tbl_edits) {
2897
- e._match_start_index = (e._match_start_index || 0) + t_o.start;
2893
+ for (let k = 0; k < t_o.rows.length; k++) {
2894
+ const o_txt = t_o.rows[k].cells.join(" | ");
2895
+ const m_txt = t_m.rows[k].cells.join(" | ");
2896
+ if (o_txt === m_txt) continue;
2897
+ edits.push({
2898
+ type: "modify",
2899
+ target_text: o_txt,
2900
+ new_text: m_txt,
2901
+ comment: "Diff: Table row modified",
2902
+ _is_table_edit: true
2903
+ });
2898
2904
  }
2899
- edits.push(...tbl_edits);
2900
2905
  }
2901
2906
  }
2902
2907
  }
@@ -2914,7 +2919,7 @@ function generate_structured_edits(text_orig, struct_orig, text_mod, struct_mod)
2914
2919
  };
2915
2920
  let ambiguous_anchor_warned = false;
2916
2921
  for (const e of edits) {
2917
- if ((e.type === "insert_row" || e.type === "delete_row") && !ambiguous_anchor_warned) {
2922
+ if ((e.type === "insert_row" || e.type === "delete_row" || e._is_table_edit) && !ambiguous_anchor_warned) {
2918
2923
  if (e.target_text && countOccurrences(text_orig, e.target_text) > 1) {
2919
2924
  warnings.push(
2920
2925
  `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.`
@@ -4666,7 +4671,7 @@ var RedlineEngine = class _RedlineEngine {
4666
4671
  *
4667
4672
  * Does NOT attach comments; callers handle that.
4668
4673
  */
4669
- _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id) {
4674
+ _track_insert_multiline(text, anchor_run, anchor_paragraph, reuse_id, positional_anchor_el = null) {
4670
4675
  if (!text) {
4671
4676
  return {
4672
4677
  first_node: null,
@@ -4687,7 +4692,29 @@ var RedlineEngine = class _RedlineEngine {
4687
4692
  }
4688
4693
  current_p = walker;
4689
4694
  }
4690
- while (lines.length > 1 && lines[lines.length - 1] === "") {
4695
+ const suffix_nodes = [];
4696
+ const pos_source = positional_anchor_el && positional_anchor_el.parentNode ? positional_anchor_el : anchor_run !== null && anchor_run._element.parentNode ? anchor_run._element : null;
4697
+ if (current_p !== null && pos_source !== null) {
4698
+ let pos_anchor = pos_source;
4699
+ while (pos_anchor && pos_anchor.parentNode !== current_p) {
4700
+ pos_anchor = pos_anchor.parentNode;
4701
+ if (pos_anchor === current_p) {
4702
+ pos_anchor = null;
4703
+ break;
4704
+ }
4705
+ }
4706
+ if (pos_anchor) {
4707
+ const relocatable = /* @__PURE__ */ new Set(["w:r", "w:ins", "w:del"]);
4708
+ let nxt = pos_anchor.nextSibling;
4709
+ while (nxt) {
4710
+ if (nxt.nodeType === 1 && relocatable.has(nxt.tagName)) {
4711
+ suffix_nodes.push(nxt);
4712
+ }
4713
+ nxt = nxt.nextSibling;
4714
+ }
4715
+ }
4716
+ }
4717
+ while (lines.length > 1 && lines[lines.length - 1] === "" && suffix_nodes.length === 0) {
4691
4718
  lines.pop();
4692
4719
  }
4693
4720
  if (lines.length === 0) {
@@ -4785,6 +4812,12 @@ var RedlineEngine = class _RedlineEngine {
4785
4812
  first_node = new_p;
4786
4813
  }
4787
4814
  }
4815
+ if (!block_mode && last_p && suffix_nodes.length > 0) {
4816
+ for (const node of suffix_nodes) {
4817
+ node.parentNode?.removeChild(node);
4818
+ last_p.appendChild(node);
4819
+ }
4820
+ }
4788
4821
  return { first_node, last_p, last_ins, used_block_mode: block_mode };
4789
4822
  }
4790
4823
  /**
@@ -4842,7 +4875,7 @@ var RedlineEngine = class _RedlineEngine {
4842
4875
  if (stripped_text.startsWith("* ") || stripped_text.startsWith("- ")) {
4843
4876
  return [stripped_text.substring(2).trim(), "List Paragraph"];
4844
4877
  }
4845
- const match = stripped_text.match(/^\d+\.\s+/);
4878
+ const match = stripped_text.match(/^1\.\s+/);
4846
4879
  if (match) {
4847
4880
  return [stripped_text.substring(match[0].length).trim(), "List Number"];
4848
4881
  }
@@ -5724,14 +5757,17 @@ var RedlineEngine = class _RedlineEngine {
5724
5757
  resolved_edits.push([edit, edit.new_text || null]);
5725
5758
  } else if (edit.type === "insert_row" || edit.type === "delete_row") {
5726
5759
  let matches = this.mapper.find_all_match_indices(edit.target_text);
5760
+ let resolved_mapper = this.mapper;
5727
5761
  if (matches.length === 0) {
5728
5762
  if (!this.clean_mapper) {
5729
5763
  this.clean_mapper = new DocumentMapper(this.doc, true);
5730
5764
  }
5731
5765
  matches = this.clean_mapper.find_all_match_indices(edit.target_text);
5766
+ resolved_mapper = this.clean_mapper;
5732
5767
  }
5733
5768
  if (matches.length > 0) {
5734
5769
  edit._resolved_start_idx = matches[0][0];
5770
+ edit._active_mapper_ref = resolved_mapper;
5735
5771
  resolved_edits.push([edit, null]);
5736
5772
  } else {
5737
5773
  skipped++;
@@ -5980,7 +6016,8 @@ var RedlineEngine = class _RedlineEngine {
5980
6016
  }
5981
6017
  _apply_table_edit(edit, rebuild_map) {
5982
6018
  const start_idx = edit._resolved_start_idx !== void 0 && edit._resolved_start_idx !== null ? edit._resolved_start_idx : edit._match_start_index || 0;
5983
- const [anchor_run, anchor_para] = this.mapper.get_insertion_anchor(
6019
+ const active_mapper = edit._active_mapper_ref || this.mapper;
6020
+ const [anchor_run, anchor_para] = active_mapper.get_insertion_anchor(
5984
6021
  start_idx,
5985
6022
  rebuild_map
5986
6023
  );
@@ -6161,9 +6198,14 @@ var RedlineEngine = class _RedlineEngine {
6161
6198
  if (overlaps_virtual_pipe) {
6162
6199
  const actual_cells = actual_doc_text.split("|");
6163
6200
  const new_cells = current_effective_new_text.split("|");
6164
- if (actual_cells.length === new_cells.length && actual_cells.length > 1) {
6201
+ if (actual_cells.length !== new_cells.length) {
6202
+ throw new BatchValidationError([
6203
+ `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).`
6204
+ ]);
6205
+ }
6206
+ if (actual_cells.length > 1) {
6165
6207
  const sub_edits2 = [];
6166
- let search_offset = start_idx;
6208
+ let cell_start_in_target = 0;
6167
6209
  let target_comment_idx = 0;
6168
6210
  for (let idx = 0; idx < actual_cells.length; idx++) {
6169
6211
  if (actual_cells[idx].trim() !== new_cells[idx].trim()) {
@@ -6176,13 +6218,7 @@ var RedlineEngine = class _RedlineEngine {
6176
6218
  const n_cell = new_cells[cell_idx];
6177
6219
  const a_clean = a_cell.trim();
6178
6220
  const n_clean = n_cell.trim();
6179
- let actual_start = search_offset;
6180
- if (a_clean) {
6181
- actual_start = this.mapper.full_text.indexOf(a_clean, search_offset);
6182
- if (actual_start === -1 || actual_start > search_offset + 10) {
6183
- actual_start = search_offset;
6184
- }
6185
- }
6221
+ const actual_start = start_idx + cell_start_in_target + (a_clean ? a_cell.indexOf(a_clean) : 0);
6186
6222
  const should_attach_comment = edit.comment !== null && edit.comment !== void 0 && cell_idx === target_comment_idx;
6187
6223
  if (a_clean !== n_clean || should_attach_comment) {
6188
6224
  const cell_sub_edits = this._word_diff_sub_edits(
@@ -6199,24 +6235,12 @@ var RedlineEngine = class _RedlineEngine {
6199
6235
  sub_edits2.push(se);
6200
6236
  }
6201
6237
  }
6202
- if (a_clean) {
6203
- search_offset = actual_start + a_clean.length;
6204
- }
6205
- const next_pipe = this.mapper.full_text.indexOf(" | ", search_offset);
6206
- if (next_pipe !== -1 && next_pipe <= search_offset + 10) {
6207
- search_offset = next_pipe + 3;
6208
- } else {
6209
- search_offset += a_cell.length + 1;
6210
- }
6238
+ cell_start_in_target += a_cell.length + 1;
6211
6239
  }
6212
6240
  for (const sub of sub_edits2) {
6213
6241
  all_sub_edits.push(sub);
6214
6242
  }
6215
6243
  continue;
6216
- } else {
6217
- throw new BatchValidationError([
6218
- `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).`
6219
- ]);
6220
6244
  }
6221
6245
  }
6222
6246
  let has_markdown = false;
@@ -6310,6 +6334,19 @@ var RedlineEngine = class _RedlineEngine {
6310
6334
  continue;
6311
6335
  }
6312
6336
  }
6337
+ if (!final_target && final_new) {
6338
+ all_sub_edits.push({
6339
+ type: "modify",
6340
+ target_text: "",
6341
+ new_text: final_new,
6342
+ comment: edit.comment,
6343
+ _resolved_start_idx: effective_start_idx,
6344
+ _match_start_index: effective_start_idx,
6345
+ _internal_op: "INSERTION",
6346
+ _active_mapper_ref: active_mapper
6347
+ });
6348
+ continue;
6349
+ }
6313
6350
  const sub_edits = this._word_diff_sub_edits(
6314
6351
  actual_doc_text,
6315
6352
  current_effective_new_text,
@@ -6751,7 +6788,10 @@ var RedlineEngine = class _RedlineEngine {
6751
6788
  edit.new_text,
6752
6789
  style_source_run,
6753
6790
  mod_anchor_para,
6754
- ins_id
6791
+ ins_id,
6792
+ // The insertion physically follows the deletion block; the style
6793
+ // run was detached when the deletion cloned it into <w:del>.
6794
+ last_del
6755
6795
  );
6756
6796
  if (result.first_node) {
6757
6797
  const is_inline_first = result.first_node.tagName === "w:ins";
@@ -7298,7 +7338,10 @@ function scrub_doc_properties(doc) {
7298
7338
  ["keywords", "Keywords"],
7299
7339
  ["subject", "Subject"],
7300
7340
  ["contentStatus", "Content status"],
7301
- ["description", "Description/comments"]
7341
+ ["description", "Description/comments"],
7342
+ ["identifier", "Identifier"],
7343
+ ["language", "Language"],
7344
+ ["version", "Version"]
7302
7345
  ];
7303
7346
  for (const [local, label] of leakFields) {
7304
7347
  findDescendantsByLocalName(corePart._element, local).forEach((c) => {
@@ -7356,27 +7399,73 @@ function scrub_timestamps(doc) {
7356
7399
  }
7357
7400
  return modified ? ["Timestamps normalized to epoch"] : [];
7358
7401
  }
7359
- function strip_custom_xml(doc) {
7360
- const customParts = doc.pkg.parts.filter((p) => p.partname.includes("/customXml"));
7361
- if (customParts.length === 0) return [];
7362
- const partnames = new Set(customParts.map((p) => p.partname));
7363
- doc.pkg.parts = doc.pkg.parts.filter((p) => !partnames.has(p.partname));
7364
- const removeRelationsTo = (relsPart) => {
7402
+ function ejectPackageMembers(doc, matcher, relTargetMatcher) {
7403
+ const pkg = doc.pkg;
7404
+ const normalized = (p) => p.startsWith("/") ? p.substring(1) : p;
7405
+ for (const part of pkg.parts) {
7406
+ if (!part.partname.endsWith(".rels")) continue;
7365
7407
  const toRemove = [];
7366
- for (const rel of findAllDescendants(relsPart._element, "Relationship")) {
7367
- const target = rel.getAttribute("Target");
7368
- if (target && target.includes("customXml")) toRemove.push(rel);
7408
+ for (const rel of findAllDescendants(part._element, "Relationship")) {
7409
+ const target = rel.getAttribute("Target") || "";
7410
+ if (relTargetMatcher(target)) {
7411
+ toRemove.push(rel);
7412
+ const sourcePath = part.partname.replace("/_rels/", "/").replace(".rels", "");
7413
+ const sourcePart = pkg.getPartByPath(sourcePath);
7414
+ if (sourcePart) {
7415
+ const relId = rel.getAttribute("Id");
7416
+ if (relId) sourcePart.rels.delete(relId);
7417
+ }
7418
+ }
7369
7419
  }
7370
7420
  toRemove.forEach((r) => r.parentNode?.removeChild(r));
7371
- };
7372
- const rootRels = doc.pkg.getPartByPath("_rels/.rels");
7373
- if (rootRels) removeRelationsTo(rootRels);
7374
- const docRels = doc.pkg.getOrCreateRelsPart(doc.part.partname);
7375
- if (docRels) removeRelationsTo(docRels);
7421
+ }
7422
+ const ctPart = pkg.getPartByPath("[Content_Types].xml");
7423
+ if (ctPart) {
7424
+ const toRemove = [];
7425
+ for (const override of findAllDescendants(ctPart._element, "Override")) {
7426
+ const partName = override.getAttribute("PartName") || "";
7427
+ if (matcher(normalized(partName))) toRemove.push(override);
7428
+ }
7429
+ toRemove.forEach((o) => o.parentNode?.removeChild(o));
7430
+ }
7431
+ pkg.parts = pkg.parts.filter((p) => !matcher(normalized(p.partname)));
7432
+ for (const key of Object.keys(pkg.unzipped)) {
7433
+ if (matcher(normalized(key))) delete pkg.unzipped[key];
7434
+ }
7435
+ }
7436
+ function strip_custom_xml(doc) {
7437
+ const isCustomXml = (path) => path.startsWith("customXml/");
7438
+ const customParts = doc.pkg.parts.filter(
7439
+ (p) => isCustomXml(p.partname.startsWith("/") ? p.partname.substring(1) : p.partname)
7440
+ );
7441
+ const leakedMembers = Object.keys(doc.pkg.unzipped).filter(isCustomXml);
7442
+ if (customParts.length === 0 && leakedMembers.length === 0) return [];
7443
+ ejectPackageMembers(doc, isCustomXml, (target) => target.includes("customXml"));
7376
7444
  for (const sdtPr of findAllDescendants(doc.element, "w:sdtPr")) {
7377
7445
  findChildren(sdtPr, "w:dataBinding").forEach((b) => sdtPr.removeChild(b));
7378
7446
  }
7379
- return [`Custom XML parts: ${customParts.length} removed`];
7447
+ return [`Custom XML parts: ${Math.max(customParts.length, leakedMembers.length)} removed`];
7448
+ }
7449
+ var CUSTOM_PROPS_PATH = "docProps/custom.xml";
7450
+ function strip_custom_properties(doc) {
7451
+ const part = doc.pkg.getPartByPath(CUSTOM_PROPS_PATH);
7452
+ const leaked = Object.keys(doc.pkg.unzipped).includes(CUSTOM_PROPS_PATH);
7453
+ if (!part && !leaked) return [];
7454
+ const lines = [];
7455
+ if (part) {
7456
+ for (const prop of findDescendantsByLocalName(part._element, "property")) {
7457
+ const name = prop.getAttribute("name") || "(unnamed)";
7458
+ const value = (prop.textContent || "").trim();
7459
+ lines.push(` Custom property removed: ${name} = "${_truncate(value, 60)}"`);
7460
+ }
7461
+ }
7462
+ ejectPackageMembers(
7463
+ doc,
7464
+ (path) => path === CUSTOM_PROPS_PATH,
7465
+ (target) => target.includes("docProps/custom.xml")
7466
+ );
7467
+ const count = Math.max(lines.length, 1);
7468
+ return [`Custom document properties: ${count} removed (docProps/custom.xml)`].concat(lines);
7380
7469
  }
7381
7470
  function strip_image_alt_text(doc) {
7382
7471
  let count = 0;
@@ -8730,6 +8819,9 @@ function _collect_footnote_ids_fast(owned_items) {
8730
8819
  return ordered;
8731
8820
  }
8732
8821
 
8822
+ // src/sanitize/core.ts
8823
+ import { strFromU8 as strFromU82, unzipSync as unzipSync2 } from "fflate";
8824
+
8733
8825
  // src/sanitize/report.ts
8734
8826
  var SanitizeReport = class {
8735
8827
  filename;
@@ -8764,7 +8856,7 @@ var SanitizeReport = class {
8764
8856
  } else {
8765
8857
  this.removed_comment_lines.push(line);
8766
8858
  }
8767
- } 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")) {
8859
+ } 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")) {
8768
8860
  this.metadata_lines.push(line);
8769
8861
  } else if (lower.includes("hyperlink") || lower.includes("warning")) {
8770
8862
  this.warnings.push(line);
@@ -8877,6 +8969,7 @@ async function finalize_document(doc, options) {
8877
8969
  report.add_transform_lines(scrub_doc_properties(doc));
8878
8970
  report.add_transform_lines(scrub_timestamps(doc));
8879
8971
  report.add_transform_lines(strip_custom_xml(doc));
8972
+ report.add_transform_lines(strip_custom_properties(doc));
8880
8973
  report.add_transform_lines(strip_image_alt_text(doc));
8881
8974
  const warnings = audit_hyperlinks(doc);
8882
8975
  for (const w of warnings) report.warnings.push(w);
@@ -8924,8 +9017,47 @@ async function finalize_document(doc, options) {
8924
9017
  }
8925
9018
  if (report.warnings.length > 0) report.status = "clean_with_warnings";
8926
9019
  const outBuffer = await doc.save();
9020
+ verifySanitizedPackage(outBuffer);
8927
9021
  return { reportText: report.render(), outBuffer };
8928
9022
  }
9023
+ var VERIFIED_CORE_FIELDS = [
9024
+ ["creator", "author (dc:creator)"],
9025
+ ["lastModifiedBy", "last modified by (cp:lastModifiedBy)"],
9026
+ ["identifier", "identifier (dc:identifier)"],
9027
+ ["description", "description (dc:description)"],
9028
+ ["keywords", "keywords (cp:keywords)"],
9029
+ ["category", "category (cp:category)"],
9030
+ ["subject", "subject (dc:subject)"],
9031
+ ["contentStatus", "content status (cp:contentStatus)"],
9032
+ ["language", "language (dc:language)"],
9033
+ ["version", "version (cp:version)"]
9034
+ ];
9035
+ function verifySanitizedPackage(outBuffer) {
9036
+ const unzipped = unzipSync2(new Uint8Array(outBuffer));
9037
+ const names = Object.keys(unzipped);
9038
+ const problems = [];
9039
+ if (names.includes("docProps/custom.xml")) {
9040
+ problems.push("docProps/custom.xml (custom document properties) is still in the package");
9041
+ }
9042
+ if (names.some((n) => n.startsWith("customXml/"))) {
9043
+ problems.push("customXml/* parts are still in the package");
9044
+ }
9045
+ if (names.includes("docProps/core.xml")) {
9046
+ const core = parseXml(strFromU82(unzipped["docProps/core.xml"]));
9047
+ for (const [local, label] of VERIFIED_CORE_FIELDS) {
9048
+ for (const el of findDescendantsByLocalName(core.documentElement, local)) {
9049
+ if ((el.textContent || "").trim()) {
9050
+ problems.push(`core property ${label} still contains a value`);
9051
+ }
9052
+ }
9053
+ }
9054
+ }
9055
+ if (problems.length > 0) {
9056
+ throw new Error(
9057
+ "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."
9058
+ );
9059
+ }
9060
+ }
8929
9061
 
8930
9062
  // src/index.ts
8931
9063
  function identifyEngine() {