@coderabbitai plan
π₯ multi-assumptions: variant A of 3 parallel framings of the same goal.
Sibling variant(s): will be cross-linked once filed. Please compare and tell us which assumption is load-bearing. The carrot family also includes the original framing in #346, which this variant supersedes.
My goal
Make cleanDocx round-trip-safe for the DOCX tracking-token grammar ([[DOCX_INS_*]], [[DOCX_DEL_*]], [[DOCX_CMT_*]]) used by @platejs/docx-io, so the cleanup pipeline strips Office-HTML cruft without mangling JSON payloads, dropping end-only paragraphs, or splitting tokens across normalized text nodes.
To do so, do this (variant A β DOM placeholder swap around the existing pipeline)
- Add
protectTrackingTokens(body) that walks body once: for every text node containing [[DOCX_(INS|DEL|CMT)_(START|END|REF):β¦]], split the text node, replace each token slice with <span data-docx-tracking-token="<base64-of-original>" data-docx-tracking-kind="ins-start|...">β</span> (zero-width-space inside so no cleaner classifies it as empty).
- Run the existing
cleanDocx pipeline unchanged: cleanDocxFootnotes β cleanDocxImageElements β cleanHtmlEmptyElements β cleanDocxEmptyParagraphs β cleanDocxQuotes β cleanDocxSpans β cleanHtmlTextNodes β cleanDocxBrComments β cleanHtmlBrElements β cleanHtmlLinkElements β cleanHtmlFontElements β cleanDocxListElements β copyBlockMarksToSpanChild.
- Add
restoreTrackingTokens(body) that swaps every surviving <span data-docx-tracking-token> back to a Text node with the base64-decoded original token bytes.
- Add a string-level pre-pass for tokens that exist between tags (rare Mammoth output) before
DOMParser is called.
Acceptance: the existing cleanDocx test snapshot is unchanged for token-free HTML, and a fixture matrix (token in mso-styled paragraph, end-only paragraph, adjacent tokens, JSON with quotes/spaces) round-trips byte-for-byte.
Because the code is
packages/docx/src/lib/docx-cleaner/cleanDocx.ts:23-49 β the linear pipeline that runs cleanHtmlTextNodes, cleanDocxEmptyParagraphs, cleanDocxSpans, copyBlockMarksToSpanChild over body. None of these inspect attributes; they classify by tag/text content.
cleanDocxEmptyParagraphs drops paragraphs with no visible text. A paragraph whose only content is [[DOCX_INS_END:abc]] reads as visible text β survives. A paragraph whose only content is a <span class="track-end"> that gets unwrapped first by cleanDocxSpans and then leaves an empty <p> is dropped.
cleanHtmlTextNodes normalizes whitespace and merges adjacent text nodes β JSON payloads with author names containing spaces ({"author":"John Doe"}) get the spaces normalized.
- jubarte-Mammoth output keeps tokens as plain text inside
<span> wrappers; the DOM serializer round-trips attributes losslessly.
Assumptions deliberately shuffled vs variants B and C
| Axis |
Variant A (DOM placeholder swap) |
Variant B (per-cleaner skip predicate) |
Variant C (extract-stash-reapply at importer) |
| Layer |
cleaner pipeline wrapper (DOM) |
each cleaner's internals |
importer (caller of cleanDocx) |
| Trigger |
pre-pipeline + post-pipeline |
during each cleaner's walk |
pre-clean extraction + post-deserialize reapply |
| Granularity |
per-token |
per-cleaner |
per-token, materialized as marks early |
| Failure mode |
silent if placeholder dropped (mitigated by ID pairing) |
loud (typecheck enforces predicate) |
partial (reapply may fail to anchor β metadata drop) |
| Reversibility |
feature flag at the wrapper boundary |
hard cutover across many files |
feature flag at the importer |
| Round-trip fidelity |
preserved byte-for-byte |
preserved byte-for-byte |
re-derived from sidecar after deserialize |
Affects cleanDocx public API |
no |
no (internal predicate added per cleaner) |
no (caller-side change) |
Validate this approach β especially but not limited to
- Static analysis of
packages/docx/src/lib/docx-cleaner/cleanDocx.ts, packages/docx/src/lib/docx-cleaner/utils/*, and packages/docx-io/src/lib/importDocx.ts.
- Review of our own code for placeholder-element interactions: do
cleanDocxSpans or copyBlockMarksToSpanChild mutate elements with data-* attributes? If yes, the swap leaks.
- Comparative analysis vs sibling variants B and C β name the load-bearing assumption as a single sentence in the form "If matters more than , variant <A|B|C> wins; otherwise the other." Do not pick a winner yet β name the axis.
- Worst-case input fixture: a paragraph with
mso-* styles, an <o:p> empty wrapper, three adjacent tokens (END, START, REF), and a JSON payload with \"-escaped quotes inside body.
Full plan
- New module
packages/docx/src/lib/docx-cleaner/protectTrackingTokens.ts with protectTrackingTokens(body) and restoreTrackingTokens(body).
- New shared module
packages/utils/docx-tracking-tokens.ts exporting prefix/suffix constants and TRACKING_TOKEN_REGEX so @platejs/docx and @platejs/docx-io import from one place (no cycle).
- Modify
cleanDocx to wrap the pipeline: protectTrackingTokens(body); β¦existing pipelineβ¦; restoreTrackingTokens(body);.
- Add string-level pre-pass on raw HTML for tokens between tags.
- Add fixture matrix in
packages/docx/src/lib/docx-cleaner/__tests__/cleanDocx-tracking.spec.ts.
- Backport the constant import in
@platejs/docx-io to consume the new shared module.
References
π₯ The carrot patch grows three rows side by side β pick whichever sprouts the cleanest token through the dirt.
@coderabbitai plan
π₯ multi-assumptions: variant A of 3 parallel framings of the same goal.
Sibling variant(s): will be cross-linked once filed. Please compare and tell us which assumption is load-bearing. The carrot family also includes the original framing in #346, which this variant supersedes.
My goal
Make
cleanDocxround-trip-safe for the DOCX tracking-token grammar ([[DOCX_INS_*]],[[DOCX_DEL_*]],[[DOCX_CMT_*]]) used by@platejs/docx-io, so the cleanup pipeline strips Office-HTML cruft without mangling JSON payloads, dropping end-only paragraphs, or splitting tokens across normalized text nodes.To do so, do this (variant A β DOM placeholder swap around the existing pipeline)
protectTrackingTokens(body)that walksbodyonce: for every text node containing[[DOCX_(INS|DEL|CMT)_(START|END|REF):β¦]], split the text node, replace each token slice with<span data-docx-tracking-token="<base64-of-original>" data-docx-tracking-kind="ins-start|...">β</span>(zero-width-space inside so no cleaner classifies it as empty).cleanDocxpipeline unchanged:cleanDocxFootnotes β cleanDocxImageElements β cleanHtmlEmptyElements β cleanDocxEmptyParagraphs β cleanDocxQuotes β cleanDocxSpans β cleanHtmlTextNodes β cleanDocxBrComments β cleanHtmlBrElements β cleanHtmlLinkElements β cleanHtmlFontElements β cleanDocxListElements β copyBlockMarksToSpanChild.restoreTrackingTokens(body)that swaps every surviving<span data-docx-tracking-token>back to aTextnode with the base64-decoded original token bytes.DOMParseris called.Acceptance: the existing
cleanDocxtest snapshot is unchanged for token-free HTML, and a fixture matrix (token inmso-styled paragraph, end-only paragraph, adjacent tokens, JSON with quotes/spaces) round-trips byte-for-byte.Because the code is
packages/docx/src/lib/docx-cleaner/cleanDocx.ts:23-49β the linear pipeline that runscleanHtmlTextNodes,cleanDocxEmptyParagraphs,cleanDocxSpans,copyBlockMarksToSpanChildoverbody. None of these inspect attributes; they classify by tag/text content.cleanDocxEmptyParagraphsdrops paragraphs with no visible text. A paragraph whose only content is[[DOCX_INS_END:abc]]reads as visible text β survives. A paragraph whose only content is a<span class="track-end">that gets unwrapped first bycleanDocxSpansand then leaves an empty<p>is dropped.cleanHtmlTextNodesnormalizes whitespace and merges adjacent text nodes β JSON payloads with author names containing spaces ({"author":"John Doe"}) get the spaces normalized.<span>wrappers; the DOM serializer round-trips attributes losslessly.Assumptions deliberately shuffled vs variants B and C
cleanDocx)cleanDocxpublic APIValidate this approach β especially but not limited to
packages/docx/src/lib/docx-cleaner/cleanDocx.ts,packages/docx/src/lib/docx-cleaner/utils/*, andpackages/docx-io/src/lib/importDocx.ts.cleanDocxSpansorcopyBlockMarksToSpanChildmutate elements withdata-*attributes? If yes, the swap leaks.mso-*styles, an<o:p>empty wrapper, three adjacent tokens (END,START,REF), and a JSON payload with\"-escaped quotes insidebody.Full plan
packages/docx/src/lib/docx-cleaner/protectTrackingTokens.tswithprotectTrackingTokens(body)andrestoreTrackingTokens(body).packages/utils/docx-tracking-tokens.tsexporting prefix/suffix constants andTRACKING_TOKEN_REGEXso@platejs/docxand@platejs/docx-ioimport from one place (no cycle).cleanDocxto wrap the pipeline:protectTrackingTokens(body); β¦existing pipelineβ¦; restoreTrackingTokens(body);.packages/docx/src/lib/docx-cleaner/__tests__/cleanDocx-tracking.spec.ts.@platejs/docx-ioto consume the new shared module.References
π₯ The carrot patch grows three rows side by side β pick whichever sprouts the cleanest token through the dirt.