fix: export the loader's canonical form for nested lists - #84
Merged
jjrodcast merged 1 commit intoJul 30, 2026
Merged
Conversation
Two converter changes make toJson a fixed point in every nested state (issue jjrodcast#81): - Lists nested below a task item are exported as document-ordered sibling lists one level under the item, because on load anything inside a taskItem re-enters at exactly that level (listItem chains, which do preserve relative depth, keep their nesting as before). Live edits could build deeper chains under a task item, and their exported shape restructured on reload. - A lone line-break piece is filtered from paragraph content next to the existing decorator/empty filters: a loaded empty nested item carries its break as a standalone piece, and toInlineNode strips the break and emitted it as an empty text node — invalid ProseMirror the next reload drops. With this and the removal-window fix, the seeded stress sweep (300 seeds x 250 ops) runs with zero crashes and zero export divergences.
Contributor
There was a problem hiding this comment.
Pull request overview
Updates TextKit’s piece-table → JSON export to emit the loader’s canonical nested-list shape (so toJson() → load → toJson() becomes a fixed point for deeply nested list states), and adds regression coverage for the previously divergent cases.
Changes:
- Add
NestedListCanonicalFormTestto pin fixed-point behavior for nested list exports (including the “empty text node” regression). - In
PieceTableConverter, flatten task-item nested list subtrees to a loadable canonical form and filter lone line-break pieces to avoid emitting{"text":""}. - Introduce a helper to flatten a positional-paragraph subtree in document order.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| shared/src/commonTest/kotlin/com/jjrodcast/textkit/NestedListCanonicalFormTest.kt | Adds tests asserting fixed-point export/load behavior for canonical nested-list forms. |
| shared/src/commonMain/kotlin/com/jjrodcast/textkit/editor/core/converters/PieceTableConverter.kt | Adjusts task-item nested-list export canonicalization and prevents emitting empty text nodes from lone line-break pieces. |
Comments suppressed due to low confidence (1)
shared/src/commonMain/kotlin/com/jjrodcast/textkit/editor/core/converters/PieceTableConverter.kt:504
nestedElementsis built by callingcreateParagraphModel(listOf(positionalParagraph), ...)for every (flattened) nested list item. If a task item contains a nested list with multiple items, this will emit multiple adjacent list nodes (each with a single item) instead of one list node containing all items. This is inconsistent with how top-level/conventional list groups are exported (grouped into one list node per run) and can bloat/fragment the exported JSON for downstream consumers.
val nestedElements = positionalParagraphs.flatMap { it.flattenSubtree() }.map { positionalParagraph ->
// endIndex is exclusive: (0, 0) sublists to nothing, so a nested list rebuilt
// here came out empty — `{"content":[],"type":"taskList"}` in the export, which
// a reload drops (issue #79). The single element needs (0, 1).
createParagraphModel(listOf(positionalParagraph), startIndex = 0, endIndex = 1)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
jjrodcast
approved these changes
Jul 30, 2026
Contributor
Author
|
@jjrodcast I think it can be merged |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #81
Defines the canonical nested-list form as what a load can represent, and makes the export emit it, so
toJson()→ load →toJson()is a fixed point in every nested state:listItemchains keep their nesting — the loader preserves relative depth throughlistItemcontent, so nothing changes for bullet/ordered chains (a guard test pins this, including indentation across reload).taskItemre-enters at exactly that level, so deeper chains built by live edits restructured on reload. The export now flattens the subtree to the loadable shape.toInlineNodestrips the break and emitted{"text":""}, which the next reload dropped. Filtered next to the existing decorator/empty-piece filters.Tests:
NestedListCanonicalFormTest— 2 cases fail onmain, plus thelistItem-chain guard. Full suite passes, JS/Wasm compile clean.With this and #83, the #46 seeded sweep runs 300 seeds × 250 ops with zero crashes and zero export divergences.