UseJunior Book a Demo

safe-docx · Detect Tracked Changes

Insertion and deletion revision wrapper detection

When reviewing OOXML for tracked edits, insertion and deletion revision wrappers (i.e., <w:ins> and <w:del> elements that wrap added or removed content) must be counted as document-body content markers. Counting these wrappers prevents a document from being reported as untracked while the body still contains visible tracked-change markup.[3]

The hasTrackedChanges_tool reads the opened document body, counts tracked-change markers, and returns a report that separates content markers from property markers. Because revision wrappers are valid OOXML tracked-change structures, the report treats them as body-level evidence of tracked changes.[1]

Below is a test scenario of the baseline successful case of hasTrackedChanges_tool: detects insertion and deletion revision wrappers in body content.

The scenario

Given a document body contains one paragraph with normal content, an insertion revision wrapper, and a deletion revision wrapper,
When has_tracked_changes is called for the opened session file,
Then tracked changes are reported with content marker counts:

  • result!.has_tracked_changes is true.
  • result!.scope is "document_body".
  • markerStats.insertions is 1.
  • markerStats.deletions is 1.
  • markerStats.content_markers is 2.
  • markerStats.total_markers is 2.

The test fixture

The fixture builds a document body with one normal run element (i.e., a <w:r> element that holds run-level content), one insertion revision wrapper, and one deletion revision wrapper. The opened session then calls hasTrackedChanges_tool for that file path.

Below is the test fixture code in relevant part.

const docXml =
  `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` +
  `<w:document xmlns:w="${W_NS}">` +
  `<w:body>` +
  `<w:p><w:r><w:t>Base</w:t></w:r>` +
  `<w:ins w:author="A" w:date="2026-01-01T00:00:00Z"><w:r><w:t> plus</w:t></w:r></w:ins>` +
  `<w:del w:author="B" w:date="2026-01-01T00:00:00Z"><w:r><w:delText> minus</w:delText></w:r></w:del>` +
  `</w:p>` +
  `</w:body></w:document>`;

const { mgr, inputPath } = await openSession([], { xml: docXml });

let result: Awaited<ReturnType<typeof hasTrackedChanges_tool>>;
await when('has_tracked_changes is called', async () => {
  result = await hasTrackedChanges_tool(mgr, { file_path: inputPath });
});
assertSuccess(result!, 'has_tracked_changes');
await attachPrettyJson('result', result!);

await then('tracked changes are reported with content marker counts', () => {
  const markerStats = result!.marker_stats as MarkerStats;
  expect(result!.has_tracked_changes).toBe(true);
  expect(result!.scope).toBe('document_body');
  expect(markerStats.insertions).toBe(1);
  expect(markerStats.deletions).toBe(1);
  expect(markerStats.content_markers).toBe(2);
  expect(markerStats.total_markers).toBe(2);
});

The expected result shape

The scenario asserts on the returned report from hasTrackedChanges_tool, so the expected shape is the set of fields and nested marker counts checked by the assertions.[2]

Below is the result that hasTrackedChanges_tool is expected to return for this scenario.

{
  has_tracked_changes: true,
  scope: 'document_body',
  marker_stats: {
    insertions: 1,
    deletions: 1,
    content_markers: 2,
    total_markers: 2,
  },
}

Below is a description of the expected fields:

A non-obvious detail

The marker count is taken from the document body rather than from a plain-text rendering. That matters because insertion and deletion revision wrappers preserve document-editing semantics that would be lost if only visible text were inspected.