Skip to content

feat(document): a text edit that spans several runs - #873

Merged
andiwand merged 1 commit into
mainfrom
feat/text-edit-runs
Sep 10, 2026
Merged

feat(document): a text edit that spans several runs#873
andiwand merged 1 commit into
mainfrom
feat/text-edit-runs

Conversation

@andiwand

@andiwand andiwand commented Sep 10, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Second of five; #872 is merged, so this now sits on main. See
docs/design/document-editing.md for the decisions.

What this one does

insertText puts a run beside one that is there, in the same parent so it
carries the same style, and removeElement takes an element and its subtree
away. With setText on each end, a selection that starts in one run and ends
in another replays as the two ends rewritten and what lay between removed.

Document::remove, Document::insert_text_before and insert_text_after are
the same three in the C++ API. They sit on the document rather than on a
handle: a handle says what an element holds, the document says what the tree
holds. An Element is an immutable handle, so restructuring the tree through
one would leave a handle naming something unreachable. Each refuses an element
of another document.

Created elements

An operation that creates one states a negative id for it, and a later
operation names it by the same number. Replay keeps a per-log map. That keeps
replay a pure function of the log — returning minted ids to the browser is the
round trip architecture A exists to avoid. A sign rather than a reserved range,
because odf already spends the top bit of ElementIdentifier on a positional
cell id.

The registry grows the links a structural edit needs

insert_sibling_after, insert_sibling_before and unlink_child. Until now
nothing built a tree except a parser reading forward, so append_child was the
whole surface. An unlinked element keeps its id and stops being reachable, so
an id an edit already handed out never names something else.

xml::TreeEditor is where the dom half lives, over any registry whose elements
carry a pugi::xml_node. odf, ooxml text and ooxml presentation all resolve an
id to a node, splice the subtree and fix up the links; only the tag names
differ, and those come from the nodes.

Read-only engines take a default that throws, rather than eight identical
bodies.

Fix

A .docx edit dropped the space at either end of a run: the w:t now states
xml:space="preserve" where the text needs it ([ECMA-376] Part 1 17.3.3.31). A
lone space is part of a string token, so the token type never said one was
there. Verified with headless LibreOffice on the saved package.

@andiwand
andiwand force-pushed the feat/text-edit-by-id branch from b7195cf to 12310b4 Compare September 10, 2026 12:56
Base automatically changed from feat/text-edit-by-id to main September 10, 2026 12:57
@andiwand
andiwand force-pushed the feat/text-edit-runs branch 3 times, most recently from 5c7adcb to d0a4632 Compare September 10, 2026 13:19
`insertText` puts a run beside one that is there, in the same parent so it
carries the same style, and `removeElement` takes an element and its
subtree away. With `setText` on each end, a selection that starts in one
run and ends in another replays as the two ends rewritten and what lay
between removed.

`Document::remove`, `Document::insert_text_before` and `insert_text_after`
are the same three in the C++ API. They sit on the document rather than on
a handle because a handle says what an element holds and the document says
what the tree holds - an `Element` is immutable, and restructuring the tree
through one would leave a handle naming something unreachable. Each refuses
an element of another document.

An operation that creates an element states a negative id for it, and a
later operation names it by the same number, so replay stays a pure
function of the log.

The shared registry grows the links a structural edit needs -
`insert_sibling_after`, `insert_sibling_before` and `unlink_child`. Until
now nothing built a tree except a parser reading forward, so
`append_child` was the whole surface. An unlinked element keeps its id and
stops being reachable, so an id an edit already handed out never names
something else.

`xml::TreeEditor` is where the dom half lives, over any registry whose
elements carry a `pugi::xml_node`. odf, ooxml text and ooxml presentation
all resolve an id to a node, splice the subtree and fix up the links; only
the tag names differ, and those come from the nodes. Each engine writes
its own text nodes and nothing else.

Fixes a `.docx` edit dropping the space at either end of a run: the `w:t`
now states `xml:space="preserve"` where the text needs it. A lone space is
part of a `string` token, so the token type never said one was there.
Verified with headless LibreOffice on the saved package.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKFKbUVCYF2VhujdmjhhPW
@andiwand
andiwand merged commit 7eb9400 into main Sep 10, 2026
11 checks passed
@andiwand
andiwand deleted the feat/text-edit-runs branch September 10, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant