You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
π₯ multi-assumptions: variant C of 3 parallel framings of the same goal.
Sibling variant(s): #347 (variant A) and #348 (variant B). Please compare and tell us which assumption is load-bearing. Original framing in #346.
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 C β tokens never enter cleanDocx; extract-stash-reapply at the importer)
Leave cleanDocx (and its 13 cleaners) entirely untouched. It stays token-blind and that is fine.
In packages/docx-io/src/lib/importDocx.ts (the tracking import path), insert an extraction step beforecleanDocx is called:
Walk the raw HTML string with the token regex.
For each match, record { tokenText, anchorBefore, anchorAfter, kind, payload } where anchorBefore/anchorAfter are the 32-character text fingerprints (HTML-decoded plain text) immediately preceding and following the token, ignoring whitespace.
Strip the token from the HTML.
Run cleanDocx(strippedHtml, rtf) exactly as the non-tracking path does today. The cleaner sees only Office-HTML cruft, no tokens.
Deserialize the cleaned HTML to Plate nodes via editor.api.html.deserialize.
Reapply tokens onto the deserialized tree:
For each stashed token, run a cross-node text search (the existing searchRange utility) to locate the position whose surrounding context matches anchorBefore + anchorAfter with edit distance β€ 2 (cleanup may have collapsed whitespace).
At that position, materialize the suggestion mark / comment mark directly on the affected text nodes, using payload. No token text is reinserted into editor.children.
Errors (anchor not found) are aggregated into result.errors and surfaced to the caller; the import does not throw. Acceptance: cleanDocx stays untouched; the @platejs/docx-io test suite covers (a) round-trip: import β export produces identical tracking metadata, and (b) anchor-resilience: a fixture where cleanDocx removes a wrapping <span> and merges adjacent text nodes still resolves the token's position.
Because the code is
packages/docx/src/lib/docx-cleaner/cleanDocx.ts β already a stable, well-tested pipeline. Touching it is the riskiest change in this family.
The searchRange cross-node text search (issue feat(docx-io): tracked-changes round-trip via embedded tracking tokensΒ #342) is already required to anchor tokens AFTER deserialization, since deserialization reorganizes Mammoth's run-level spans into Slate text nodes. A pre-clean extraction reuses the same anchoring infrastructure with one extra fingerprint.
jubarte-Mammoth output preserves enough surrounding text context that 32-character HTML-decoded fingerprints are unique within typical paragraphs (validated empirically against notes/4shapes.docx in potion_deploy).
Assumptions deliberately shuffled vs variants A and B
Axis
Variant C (extract-stash-reapply at importer)
Variant A (DOM placeholder swap)
Variant B (per-cleaner skip predicate)
Layer
importer (caller of cleanDocx)
cleaner pipeline wrapper (DOM)
each cleaner's internals
Trigger
pre-clean extraction + post-deserialize reapply
pre-pipeline + post-pipeline
during each cleaner's walk
Granularity
per-token, materialized as marks early
per-token
per-cleaner
Failure mode
partial β anchor miss drops one token's metadata, surfaced via errors[]
silent if placeholder dropped
loud (typecheck enforces predicate)
Reversibility
feature flag at the importer
feature flag at the wrapper boundary
hard cutover across many files
Round-trip fidelity
re-derived from sidecar; lossy if anchor matching fails
preserved byte-for-byte
preserved byte-for-byte
Affects cleanDocx public API
no (caller-side change)
no
no
Couples @platejs/docx and @platejs/docx-io
no β full decoupling
yes β shared constants module
yes β shared predicate module
Survives a future cleaner change
yes β cleaner can be replaced freely
maybe β new cleaner unaware of placeholders
yes if registry enforced
Validate this approach β especially but not limited to
Static analysis of packages/docx-io/src/lib/importDocx.ts, packages/docx-io/src/lib/searchRange.ts, packages/docx-io/src/lib/importTrackChanges.ts, packages/docx-io/src/lib/importComments.ts.
Review of our own code for the anchor-fingerprint approach β is 32 characters with edit-distance β€ 2 enough? What happens with two identical token-bearing ranges in one paragraph?
Adversarial input fixture: a paragraph with two identical insertions by the same author, one immediately after the other, separated by a single space β does the fingerprint disambiguate them? Should it use a per-token monotonic counter as a secondary anchor?
@coderabbitai plan
π₯ multi-assumptions: variant C of 3 parallel framings of the same goal.
Sibling variant(s): #347 (variant A) and #348 (variant B). Please compare and tell us which assumption is load-bearing. Original framing in #346.
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 C β tokens never enter
cleanDocx; extract-stash-reapply at the importer)cleanDocx(and its 13 cleaners) entirely untouched. It stays token-blind and that is fine.packages/docx-io/src/lib/importDocx.ts(the tracking import path), insert an extraction step beforecleanDocxis called:{ tokenText, anchorBefore, anchorAfter, kind, payload }whereanchorBefore/anchorAfterare the 32-character text fingerprints (HTML-decoded plain text) immediately preceding and following the token, ignoring whitespace.cleanDocx(strippedHtml, rtf)exactly as the non-tracking path does today. The cleaner sees only Office-HTML cruft, no tokens.editor.api.html.deserialize.searchRangeutility) to locate the position whose surrounding context matchesanchorBefore+anchorAfterwith edit distance β€ 2 (cleanup may have collapsed whitespace).payload. No token text is reinserted intoeditor.children.result.errorsand surfaced to the caller; the import does not throw.Acceptance:
cleanDocxstays untouched; the@platejs/docx-iotest suite covers (a) round-trip: import β export produces identical tracking metadata, and (b) anchor-resilience: a fixture wherecleanDocxremoves a wrapping<span>and merges adjacent text nodes still resolves the token's position.Because the code is
packages/docx/src/lib/docx-cleaner/cleanDocx.tsβ already a stable, well-tested pipeline. Touching it is the riskiest change in this family.packages/docx-io/src/lib/importDocx.ts(issue feat(docx-io): tracked-changes round-trip via embedded tracking tokensΒ #342 introduces the tracking branch) β the single place that knows about tokens. Keeping token-awareness here keeps the boundary clean.searchRangecross-node text search (issue feat(docx-io): tracked-changes round-trip via embedded tracking tokensΒ #342) is already required to anchor tokens AFTER deserialization, since deserialization reorganizes Mammoth's run-level spans into Slate text nodes. A pre-clean extraction reuses the same anchoring infrastructure with one extra fingerprint.notes/4shapes.docxin potion_deploy).Assumptions deliberately shuffled vs variants A and B
cleanDocx)errors[]cleanDocxpublic API@platejs/docxand@platejs/docx-ioValidate this approach β especially but not limited to
packages/docx-io/src/lib/importDocx.ts,packages/docx-io/src/lib/searchRange.ts,packages/docx-io/src/lib/importTrackChanges.ts,packages/docx-io/src/lib/importComments.ts.Full plan
packages/docx-io/src/lib/extractTokensForCleaning.tsexportingextractTokens(html: string)β{ stripped: string; tokens: StashedToken[] }.packages/docx-io/src/lib/reapplyTokens.tsexportingreapplyTokens(editor, nodes, tokens)using the existingsearchRangeutility from feat(docx-io): tracked-changes round-trip via embedded tracking tokensΒ #342.packages/docx-io/src/lib/importDocx.ts(tracking branch) to extract βcleanDocxβ deserialize β reapply.errors[]propagation throughImportDocxWithTrackingResult(the type already exists; just ensure anchor misses are routed there).mso-*styled spans thatcleanDocxwill unwrap@platejs/docx. The package version stays exactly where it is.References
notes/4shapes.docxin potion_deploy (referenced byconsolidateSuggestionsByTime)π₯ The cleanest carrot is the one nobody touched. Pluck the tokens before the kitchen, season after.