Repository navigation
feat: tracked paragraph splits on newline (ISSUES.md #58+61) - #72
Conversation
A `\n` in any content text (replace's replace_with, insert_after/before's text, rewrite_paragraph's new_text, or a batch_edit op) now means a tracked paragraph split at that point: the first paragraph's mark is flagged as an inserted revision and the tail moves into a new following paragraph as unchanged content. Accepting keeps the split; rejecting rejoins. One `\n`-containing operation is one revision group / one changeset covering the delete, the per-paragraph insertions, and each inserted mark. - Split primitives + newline dispatch from replace/insert/rewrite; reopen reconstruction keys on the durable author+date+mark signal so a reopened split stays one inferred group across both paragraphs. - Document.split_paragraph(ref, *, before) convenience sugar. - EditResult.refs: every resulting paragraph ref in document order, resolved by element identity so a split that shifts later indexes still reports live positions (batch/rewrite share one revision-element index). - Reject all other C0/DEL control characters (tab, CR, NUL, ...) at every text input with a teaching error; only `\n` is special (a split). - Refuse to split a paragraph carrying a w:sectPr, or at a boundary inside an existing revision — the latter checked up front so a refused single-op split leaves no partial mutation (single edits have no rollback). Full suite: 1331 passed, 2 skipped.
📝 WalkthroughWalkthroughNewline-containing edits now create tracked paragraph splits, expose all resulting paragraph refs, support batch operations and rejection rejoining, and reject unsupported control characters across editing and comment APIs. Documentation and regression tests cover the new behavior. ChangesTracked paragraph splitting
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Document
participant RevisionManager
participant WordXML
Document->>RevisionManager: submit newline-containing edit
RevisionManager->>WordXML: create tracked paragraph split
RevisionManager-->>Document: return group and split metadata
Document->>RevisionManager: resolve resulting refs
RevisionManager-->>Document: return paragraph refs
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #72 +/- ##
=======================================
+ Coverage 95.9% 96.1% +0.2%
=======================================
Files 11 11
Lines 4081 4355 +274
Branches 809 859 +50
=======================================
+ Hits 3915 4187 +272
- Misses 77 78 +1
- Partials 89 90 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/api.md`:
- Around line 398-404: Correct the `split_paragraph` heading anchor referenced
by the Newlines paragraph so `#split_paragraphref-before-occurrencenone` matches
the heading’s generated slug. Update the heading text or explicit anchor as
needed, preserving the documented `split_paragraph` reference.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 13ab818c-5580-4cdf-94ce-21dde3ec0353
📒 Files selected for processing (9)
docs/api.mddocx_editor/comments.pydocx_editor/document.pydocx_editor/track_changes.pydocx_editor/xml_editor.pyskills/docx/SKILL.mdtests/test_error_quality.pytests/test_paragraph_split.pytests/test_revision_groups.py
| > **Newlines split paragraphs.** A `\n` in any content text (`replace`'s | ||
| > `replace_with`, `insert_after`/`insert_before`'s `text`, `rewrite_paragraph`'s | ||
| > `new_text`, or a `batch_edit` op) is a **tracked paragraph split**: the first | ||
| > paragraph's mark is flagged inserted and the tail moves into a new paragraph. | ||
| > Accepting keeps the split; rejecting rejoins. All other C0 control characters | ||
| > (`\t`, `\r`, …) are rejected with a teaching `ValueError`. See | ||
| > [`split_paragraph`](#split_paragraphref-before-occurrencenone) and [`EditResult`](#editresult). |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Broken in-page link fragment. The split_paragraph heading slugifies with a double hyphen between ref and before (the , *, sequence collapses to two spaces → --), so #split_paragraphref-before-occurrencenone does not resolve to any heading.
📝 Proposed fix
-> [`split_paragraph`](`#split_paragraphref-before-occurrencenone`) and [`EditResult`](`#editresult`).
+> [`split_paragraph`](`#split_paragraphref--before-occurrencenone`) and [`EditResult`](`#editresult`).📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| > **Newlines split paragraphs.** A `\n` in any content text (`replace`'s | |
| > `replace_with`, `insert_after`/`insert_before`'s `text`, `rewrite_paragraph`'s | |
| > `new_text`, or a `batch_edit` op) is a **tracked paragraph split**: the first | |
| > paragraph's mark is flagged inserted and the tail moves into a new paragraph. | |
| > Accepting keeps the split; rejecting rejoins. All other C0 control characters | |
| > (`\t`, `\r`, …) are rejected with a teaching `ValueError`. See | |
| > [`split_paragraph`](#split_paragraphref-before-occurrencenone) and [`EditResult`](#editresult). | |
| > **Newlines split paragraphs.** A `\n` in any content text (`replace`'s | |
| > `replace_with`, `insert_after`/`insert_before`'s `text`, `rewrite_paragraph`'s | |
| > `new_text`, or a `batch_edit` op) is a **tracked paragraph split**: the first | |
| > paragraph's mark is flagged inserted and the tail moves into a new paragraph. | |
| > Accepting keeps the split; rejecting rejoins. All other C0 control characters | |
| > (`\t`, `\r`, …) are rejected with a teaching `ValueError`. See | |
| > [`split_paragraph`](`#split_paragraphref--before-occurrencenone`) and [`EditResult`](`#editresult`). |
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)
[warning] 404-404: Link fragments should be valid
(MD051, link-fragments)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/api.md` around lines 398 - 404, Correct the `split_paragraph` heading
anchor referenced by the Newlines paragraph so
`#split_paragraphref-before-occurrencenone` matches the heading’s generated
slug. Update the heading text or explicit anchor as needed, preserving the
documented `split_paragraph` reference.
Source: Linters/SAST tools
Address issues found in multi-reviewer review of the split feature: - reject_revision of a non-terminal split mark no longer orphans the downstream break: _rejoin_paragraph migrates the following paragraph's pending mark onto the surviving paragraph, so the later split stays tracked and rejectable (group/changeset reject was already safe via reverse-id order). - Refuse re-splitting a paragraph that already carries a pending inserted mark, up front and atomically — a second mark would be invalid OOXML (CT_ParaRPr allows one w:ins) and mis-attribute the break. - rewrite_paragraph refuses (atomically, before mutating) a rewrite whose newlines fall into separate diff hunks — that would split one paragraph in several places. A single hunk with several newlines still threads fine. - Split-inserted segments inherit the surrounding run's rPr instead of dropping to document default. - Extract _paragraph_mark_rPr (schema-order pPr/rPr) shared by mark insertion and the rejoin migration; simplify reconstruction run_key to (author, date); add missing return annotations; quote 'reply' in a control-char error. Regression tests for each. Full suite: 1336 passed, 2 skipped.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docx_editor/track_changes.py (1)
3009-3028: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winSplit segments after the first lose formatting when the split lands at the paragraph's end.
_insert_ins_at_paragraph_startderives the inserted segment'srPronly fromparagraph.getElementsByTagName("w:r"), falling back to""when the (new) paragraph has no runs yet. In_apply_paragraph_splits, every intermediate tail paragraph created by_split_paragraph_at_positionis genuinely empty until its own segment is inserted whenever the split point is at or beyond the end of the current paragraph's text — which is exactly what happens for a multi-\ninsert_after/insert_before/replaceat the end of a paragraph (e.g. appending"A\nB\nC"after the last word).Segment 1 (
"A") correctly inherits formatting because_rewrite_insert_at's "insert at end" branch pullsrPrfrom the original last run. But segments 2+ ("B","C", ...) are inserted into a paragraph with zero runs at that point, so they silently drop to""(document-default) formatting instead of the surrounding bold/italic/etc. — contradicting this PR's stated goal that split segments inherit surrounding run properties.test_split_inserted_segment_inherits_run_formatonly exercises a single split with a non-empty tail, so this gap is untested.Suggest threading a fallback
rPr(captured once from the pre-split boundary run) through_apply_paragraph_splitsinto_insert_ins_at_paragraph_start, used only when the target paragraph has no runs of its own yet.🎨 Sketch of the fix
- def _apply_paragraph_splits(self, p1, split_pos: int, segments: list[str]) -> int: + def _apply_paragraph_splits(self, p1, split_pos: int, segments: list[str], fallback_rpr: str = "") -> int: member_id = -1 current_p = p1 pos = split_pos for segment in segments: new_p, mark_id = self._split_paragraph_at_position(current_p, pos) member_id = mark_id if segment: - self._insert_ins_at_paragraph_start(new_p, segment) + self._insert_ins_at_paragraph_start(new_p, segment, fallback_rpr) current_p = new_p pos = len(segment) return member_id - def _insert_ins_at_paragraph_start(self, paragraph, text: str) -> None: + def _insert_ins_at_paragraph_start(self, paragraph, text: str, fallback_rpr: str = "") -> None: runs = paragraph.getElementsByTagName("w:r") - rPr_xml = get_rPr_xml(runs[0]) if runs else "" + rPr_xml = get_rPr_xml(runs[0]) if runs else fallback_rprCallers (
_split_replace,_split_insert,_rewrite_insert_at) would capture the boundary run'srPronce (e.g. viaself._get_run_infoon the last visible run before the split point) and pass it through.Also applies to: 3141-3161
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docx_editor/track_changes.py` around lines 3009 - 3028, Thread a captured boundary-run rPr fallback through the paragraph-splitting flow so empty intermediate paragraphs retain surrounding formatting. Update callers such as _split_replace, _split_insert, and _rewrite_insert_at to capture the pre-split boundary rPr once, pass it into _apply_paragraph_splits, and have _apply_paragraph_splits forward it to _insert_ins_at_paragraph_start; use the fallback only when the target paragraph has no runs, preserving each paragraph’s own formatting when present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@docx_editor/track_changes.py`:
- Around line 3009-3028: Thread a captured boundary-run rPr fallback through the
paragraph-splitting flow so empty intermediate paragraphs retain surrounding
formatting. Update callers such as _split_replace, _split_insert, and
_rewrite_insert_at to capture the pre-split boundary rPr once, pass it into
_apply_paragraph_splits, and have _apply_paragraph_splits forward it to
_insert_ins_at_paragraph_start; use the fallback only when the target paragraph
has no runs, preserving each paragraph’s own formatting when present.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3ed269d8-acfa-474c-86eb-650f8d47f00b
📒 Files selected for processing (4)
docx_editor/comments.pydocx_editor/track_changes.pytests/test_paragraph_split.pytests/test_revision_groups.py
Bring patch coverage back over target after the hardening round: - Tests for the previously-untested split paths: splitting a paragraph with rich properties (pStyle/rPr/pPrChange/mark) so the tail's pPr copy is exercised and cleaned; a pStyle paragraph with no rPr; rewrite append that diffs into an insert hunk (empty and non-empty first segment); replace with a leading newline (empty first segment); reply_to_comment control-char rejection; and the foreign-authored-mark reconstruction branch. - Mark genuinely-unreachable defensive branches with `# pragma: no cover` (rejoin with no following paragraph, split-continuation prev_para guard, _resulting_refs fallbacks); replace the parentNode ty-guard with an assert. - ruff format normalization. Full suite: 1343 passed, 2 skipped; every changed source line covered.
A multi-newline split at the end of a paragraph (e.g. appending "A\nB\nC" past the last word) opens empty tail paragraphs, so a segment inserted there had no adjacent run to copy formatting from and dropped to the document default — only the first segment kept the surrounding format. Thread the boundary run's rPr through _apply_paragraph_splits so every segment inherits it, propagating across a run of empty tails. Regression tests for end-split formatting and the consecutive-newline (empty middle paragraph) edge.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
docx_editor/document.py (1)
397-419: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftPreserve target identity for no-op items in splitting batches.
When a batch splits
P1and a no-op targets originalP2,group_id is Nonetakes this stale-index fallback and returns the newP2(the split tail), not the original paragraph now atP3. This affects bothbatch_edit()andbatch_rewrite(). Preserve each operation’s pre-mutation paragraph element and resolve its live index when no group exists.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docx_editor/document.py` around lines 397 - 419, Update the no-group path in the operation handling used by batch_edit() and batch_rewrite() to preserve each operation’s original paragraph element before mutations and resolve that element’s current index afterward, rather than relying on the stale old_ref index. Keep the existing grouped-split handling unchanged, and use the preserved element identity to return the original no-op target even when earlier splits shift paragraph positions.docx_editor/track_changes.py (2)
675-678: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winKeep paragraph-mark IDs consistent with rollback and resolution.
_paragraph_mark_idsis mutable registry state, but_registry_snapshot()excludes it and_restore_registry()does not restore it, so failed edit/rewrite batches restore the old paragraph-mark snapshot instead of rolling it back. Resolved paragraph-mark insertions are also never discarded, so accepted or rejected IDs can still be counted bysplit_count(). Add this set to the snapshot and remove IDs when paragraph-mark insertions are accepted or rejected.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docx_editor/track_changes.py` around lines 675 - 678, Update the registry snapshot and restore logic to include _paragraph_mark_ids so failed edit or rewrite batches roll back this mutable state. In the paragraph-mark insertion resolution paths, remove each ID from _paragraph_mark_ids when the insertion is accepted or rejected, ensuring split_count() only counts unresolved paragraph-mark insertions.
3678-3689: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick winResolve the owner paragraph before removing the mark.
removeChild(mark_ins)detaches the mark from itsparentNode, so_ancestor_paragraph(mark_ins)returnsNoneand the rejoin returns without merging the following paragraph. Capturep1before removing the node.Proposed fix
+ p1 = _ancestor_paragraph(mark_ins) + if p1 is None: + return rPr = mark_ins.parentNode rPr.removeChild(mark_ins) ... - p1 = _ancestor_paragraph(mark_ins) - if p1 is None: - return🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docx_editor/track_changes.py` around lines 3678 - 3689, Move the _ancestor_paragraph(mark_ins) call in the mark-ins removal flow so p1 is resolved before rPr.removeChild(mark_ins) detaches the node. Preserve the existing wrapper cleanup and subsequent paragraph-rejoin behavior, using the captured p1 for the later guard and merge logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docx_editor/track_changes.py`:
- Around line 3022-3032: Update the split-segment loop around
_split_paragraph_at_position so fallback_rPr retains the last non-empty run
formatting when the current paragraph has no runs. Only replace fallback_rPr
when boundary_runs contains a run, allowing later non-empty segments such as “C”
to reuse formatting across empty paragraphs while preserving the existing
insertion flow.
---
Outside diff comments:
In `@docx_editor/document.py`:
- Around line 397-419: Update the no-group path in the operation handling used
by batch_edit() and batch_rewrite() to preserve each operation’s original
paragraph element before mutations and resolve that element’s current index
afterward, rather than relying on the stale old_ref index. Keep the existing
grouped-split handling unchanged, and use the preserved element identity to
return the original no-op target even when earlier splits shift paragraph
positions.
In `@docx_editor/track_changes.py`:
- Around line 675-678: Update the registry snapshot and restore logic to include
_paragraph_mark_ids so failed edit or rewrite batches roll back this mutable
state. In the paragraph-mark insertion resolution paths, remove each ID from
_paragraph_mark_ids when the insertion is accepted or rejected, ensuring
split_count() only counts unresolved paragraph-mark insertions.
- Around line 3678-3689: Move the _ancestor_paragraph(mark_ins) call in the
mark-ins removal flow so p1 is resolved before rPr.removeChild(mark_ins)
detaches the node. Preserve the existing wrapper cleanup and subsequent
paragraph-rejoin behavior, using the captured p1 for the later guard and merge
logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: ab1c0577-7f6a-4acb-9fc2-1ee2cb52a69b
📒 Files selected for processing (5)
docx_editor/document.pydocx_editor/track_changes.pytests/test_error_quality.pytests/test_paragraph_split.pytests/test_revision_groups.py
| # Formatting to fall back on when the new paragraph's tail is empty | ||
| # (a split at the end of the current paragraph): its trailing run | ||
| # carries the boundary formatting. This propagates through a run of | ||
| # empty tails (e.g. appending "A\nB\nC" past the last word), so every | ||
| # segment keeps the surrounding format instead of only the first. | ||
| boundary_runs = current_p.getElementsByTagName("w:r") | ||
| fallback_rPr = get_rPr_xml(boundary_runs[-1]) if boundary_runs else "" | ||
| new_p, mark_id = self._split_paragraph_at_position(current_p, pos) | ||
| member_id = mark_id | ||
| if segment: | ||
| self._insert_ins_at_paragraph_start(new_p, segment, fallback_rPr) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Preserve fallback formatting across empty split segments.
For formatted A\n\nC inserts at paragraph end, the empty middle paragraph has no runs, so the next iteration resets fallback_rPr to "" and C loses formatting. Retain the last non-empty fallback.
Proposed fix
+ fallback_rPr = ""
for segment in segments:
boundary_runs = current_p.getElementsByTagName("w:r")
- fallback_rPr = get_rPr_xml(boundary_runs[-1]) if boundary_runs else ""
+ if boundary_runs:
+ fallback_rPr = get_rPr_xml(boundary_runs[-1])📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # Formatting to fall back on when the new paragraph's tail is empty | |
| # (a split at the end of the current paragraph): its trailing run | |
| # carries the boundary formatting. This propagates through a run of | |
| # empty tails (e.g. appending "A\nB\nC" past the last word), so every | |
| # segment keeps the surrounding format instead of only the first. | |
| boundary_runs = current_p.getElementsByTagName("w:r") | |
| fallback_rPr = get_rPr_xml(boundary_runs[-1]) if boundary_runs else "" | |
| new_p, mark_id = self._split_paragraph_at_position(current_p, pos) | |
| member_id = mark_id | |
| if segment: | |
| self._insert_ins_at_paragraph_start(new_p, segment, fallback_rPr) | |
| fallback_rPr = "" | |
| for segment in segments: | |
| # Formatting to fall back on when the new paragraph's tail is empty | |
| # (a split at the end of the current paragraph): its trailing run | |
| # carries the boundary formatting. This propagates through a run of | |
| # empty tails (e.g. appending "A\nB\nC" past the last word), so every | |
| # segment keeps the surrounding format instead of only the first. | |
| boundary_runs = current_p.getElementsByTagName("w:r") | |
| if boundary_runs: | |
| fallback_rPr = get_rPr_xml(boundary_runs[-1]) | |
| new_p, mark_id = self._split_paragraph_at_position(current_p, pos) | |
| member_id = mark_id | |
| if segment: | |
| self._insert_ins_at_paragraph_start(new_p, segment, fallback_rPr) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docx_editor/track_changes.py` around lines 3022 - 3032, Update the
split-segment loop around _split_paragraph_at_position so fallback_rPr retains
the last non-empty run formatting when the current paragraph has no runs. Only
replace fallback_rPr when boundary_runs contains a run, allowing later non-empty
segments such as “C” to reuse formatting across empty paragraphs while
preserving the existing insertion flow.
…OADMAP.md #72) DOCX_SESSION_START_TIMEOUT becomes DOCX_EDITOR_SESSION_START_TIMEOUT, matching the repo's one existing env var before the name ships. The budget now rejects inf (an unbounded wait defeats the diagnostic), CI sets 60 instead of 120 and bounds the job at 45 minutes so a runner where every kernel start times out fails in bounded time. The three fake-kernel tests that only checked the error text now also prove the stand-in died, the stop-session kill spy filters on the kernel's pid, and the second-phase budget is computed at its use so the report cannot total more than the budget.
* fix: make the session tests robust to CI load (ROADMAP.md #72) start_session reads DOCX_SESSION_START_TIMEOUT for its budget (explicit argument wins, 30 s default unchanged) and a start that runs out of budget now reports which phase timed out, whether the kernel was still alive, the load average, the connection-file summary and the kernel's stderr. CI sets the variable to 120. The session tests replace their six timing sleeps with a handshake on the kernel's execute_input broadcast, replace the wall-clock stop assertion with a behavioural one (no signal sent, kernel gone), and add fake-kernel regression tests for the diagnosed failure shape: connection file present, control channel never answering. * fix: prefix the start-budget env var, reject inf, bound the CI job (ROADMAP.md #72) DOCX_SESSION_START_TIMEOUT becomes DOCX_EDITOR_SESSION_START_TIMEOUT, matching the repo's one existing env var before the name ships. The budget now rejects inf (an unbounded wait defeats the diagnostic), CI sets 60 instead of 120 and bounds the job at 45 minutes so a runner where every kernel start times out fails in bounded time. The three fake-kernel tests that only checked the error text now also prove the stand-in died, the stop-session kill spy filters on the kernel's pid, and the second-phase budget is computed at its use so the report cannot total more than the budget.
Summary
A
\nin any content text is now a tracked paragraph split at that point: the first paragraph's mark is flagged as an inserted revision and the tail moves into a new following paragraph as unchanged content. Accepting keeps the split; rejecting rejoins. Works inreplace(replace_with),insert_after/insert_before(text),rewrite_paragraph(new_text), and insidebatch_editops. Multiple\ns make multiple splits.One
\n-containing operation is one revision group / one changeset covering the delete, the per-paragraph insertions, and each inserted paragraph mark —reject_group/reject_changesetrevert the whole split atomically.Details
replace/insert/rewrite. Reopen reconstruction keys on the durable author+date+mark signal so a reopened split stays one inferred group across both paragraphs (survives Word stripping our extension attributes).Document.split_paragraph(ref, *, before)— explicit sugar for a pure split.EditResult.refs— every resulting paragraph ref in document order, resolved by element identity so a split that shifts later indexes still reports live positions (batch/rewrite share one revision-element index; no-split edits stay DOM-walk-free).\nis special.w:sectPr, or at a boundary inside an existing revision. The latter is checked up front so a refused single-op split leaves no partial mutation (single edits have no DOM rollback).Verification
ruff checkclean;ty checkexit 0.tests/test_paragraph_split.pyplus split-reconstruction and control-char coverage.Summary by CodeRabbit
\n(including insert/replace/rewrite and batch operations).split_paragraphand extendedEditResultwithrefsto describe all resulting paragraphs.split_paragraph,EditResult.refs, and newline/control-character rules.