Summary
On 1.46.1, accepting a tracked whole-block deletion created by doc.blocks.delete(..., { changeMode: "tracked" }) joins the deleted paragraph into its successor and resolves the joined paragraph's properties from the deleted block. In Word, the surviving paragraph mark's properties win — so the result is inverted relative to the DOCX semantics being modelled.
The effect is silent and reaches the exported file: a body clause following a deleted list item is absorbed into the list's numbering; a paragraph following a deleted heading inherits the heading style.
Repro
Requires @harbour-enterprises/superdoc@1.46.1, jsdom, jszip. No patches applied.
import { JSDOM } from "jsdom";
import JSZip from "jszip";
const map = new Map();
globalThis.localStorage = {
getItem: (k) => map.get(k) ?? null, setItem: (k, v) => void map.set(k, String(v)),
removeItem: (k) => void map.delete(k), clear: () => map.clear(), key: () => null,
get length() { return map.size; },
};
const { Editor, BLANK_DOCX_BASE64 } =
await import("@harbour-enterprises/superdoc/super-editor");
async function run(label, html, deleteText) {
const dom = new JSDOM("<!doctype html><html><body></body></html>");
const editor = await Editor.open(Buffer.from(BLANK_DOCX_BASE64, "base64"), {
html, document: dom.window.document, documentMode: "suggesting",
user: { name: "Reviewer", email: "reviewer@example.com" },
});
const listing = await editor.doc.blocks.list({ includeText: true });
const target = (listing.blocks ?? listing)
.find((b) => (b.textPreview ?? "").includes(deleteText));
await editor.doc.blocks.delete(
{ target: { kind: "block", nodeType: target.nodeType, nodeId: target.nodeId } },
{ changeMode: "tracked" },
);
for (const c of editor.doc.trackChanges.list().items ?? []) {
editor.doc.trackChanges.decide({ decision: "accept", target: { id: c.id } });
}
const xml = await (await JSZip.loadAsync(Buffer.from(await editor.exportDocx())))
.file("word/document.xml").async("string");
const paras = (xml.match(/<w:p[ >][\s\S]*?<\/w:p>|<w:p\/>/g) ?? []).map((p) => {
const text = (p.match(/<w:t[^>]*>[^<]*</g) ?? [])
.map((m) => m.replace(/<w:t[^>]*>/, "").replace(/<$/, "")).join("");
const style = p.match(/<w:pStyle w:val="([^"]+)"/);
return `"${text}"${p.includes("<w:numPr>") ? " +numPr" : ""}${style ? ` +${style[1]}` : ""}`;
});
console.log(`${label}\n export: ${paras.join(" | ")}`);
editor.destroy?.();
}
await run("A. delete a list item, successor is a plain paragraph",
"<p>Intro.</p><ol><li>Item one.</li></ol><p>Following clause.</p>", "Item one.");
await run("B. delete a heading, successor is a plain paragraph",
"<h1>Section heading</h1><p>Body paragraph.</p>", "Section heading");
await run("C. control - paragraph to paragraph",
"<p>Alpha.</p><p>Bravo.</p><p>Charlie.</p>", "Bravo.");
Actual
A. export: "Intro." | "Following clause." +numPr <- absorbed into the numbered list
B. export: "Body paragraph." +Heading1 <- promoted to the deleted heading's style
C. export: "Alpha." | "Charlie." <- control, correct
Expected
A. export: "Intro." | "Following clause." <- unnumbered, as authored
B. export: "Body paragraph." <- body style retained
The survivor should keep its own paragraph properties (numbering, pStyle, and the rest of pPr), matching how Word resolves a deleted pilcrow.
Notes
- The control case is clean, so this only shows when the two blocks differ in shape — which makes it easy to miss and silent when it happens.
w:numPr and w:pStyle are the visible cases; we have not audited the rest of pPr (indents, spacing, alignment), which we would expect to travel the same path.
- Reproduced on a pristine 1.46.1 tarball with no patches; also observed through the higher-level flows in our app (bulk accept, engine-driven accept), so it does not appear specific to the raw API path.
- Our workaround is to route block deletions to the older text-level path whenever the next block's shape differs, which trades this for a visible empty paragraph. A fix upstream would let us drop that.
Happy to test a patch against our corpus if that is useful.
Summary
On 1.46.1, accepting a tracked whole-block deletion created by
doc.blocks.delete(..., { changeMode: "tracked" })joins the deleted paragraph into its successor and resolves the joined paragraph's properties from the deleted block. In Word, the surviving paragraph mark's properties win — so the result is inverted relative to the DOCX semantics being modelled.The effect is silent and reaches the exported file: a body clause following a deleted list item is absorbed into the list's numbering; a paragraph following a deleted heading inherits the heading style.
Repro
Requires
@harbour-enterprises/superdoc@1.46.1,jsdom,jszip. No patches applied.Actual
Expected
The survivor should keep its own paragraph properties (numbering,
pStyle, and the rest ofpPr), matching how Word resolves a deleted pilcrow.Notes
w:numPrandw:pStyleare the visible cases; we have not audited the rest ofpPr(indents, spacing, alignment), which we would expect to travel the same path.Happy to test a patch against our corpus if that is useful.