@magic-marker/prosemirror-suggest-changes 0.3.1 → 0.3.3-wrap-unwrap.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands.d.ts +3 -1
- package/dist/commands.js +14 -2
- package/dist/contentBetween.d.ts +2 -0
- package/dist/contentBetween.js +33 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapAllNodes.data.d.ts +38 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapAllNodes.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapAllNodes.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapOneNode.data.d.ts +45 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapOneNode.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapOneNode.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapSingleNode.data.d.ts +38 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapSingleNode.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteUnwrapSingleNode.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapAllNodes.data.d.ts +38 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapAllNodes.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapAllNodes.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapSingleNode.data.d.ts +38 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapSingleNode.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/blockquoteWrapSingleNode.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftLast.data.d.ts +54 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftLast.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftLast.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMiddle.data.d.ts +48 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMiddle.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMiddle.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMultipleToTheTop.data.d.ts +74 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMultipleToTheTop.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftMultipleToTheTop.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftNestedOnce.data.d.ts +71 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftNestedOnce.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftNestedOnce.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftTop.data.d.ts +54 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftTop.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemLiftTop.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.data.d.ts +181 -0
- package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.page.d.ts +30 -0
- package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemSinkMultiple.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemSinkOneOnce.data.d.ts +51 -0
- package/dist/features/wrapUnwrap/__tests__/listItemSinkOneOnce.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemSinkOneOnce.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemsLiftMixedLevels.data.d.ts +74 -0
- package/dist/features/wrapUnwrap/__tests__/listItemsLiftMixedLevels.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/listItemsLiftMixedLevels.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/nestedListItemLiftToOuter.data.d.ts +71 -0
- package/dist/features/wrapUnwrap/__tests__/nestedListItemLiftToOuter.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/nestedListItemLiftToOuter.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/nestedListItemsLiftMultipleLevels.data.d.ts +114 -0
- package/dist/features/wrapUnwrap/__tests__/nestedListItemsLiftMultipleLevels.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/nestedListItemsLiftMultipleLevels.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/testUtils.d.ts +5 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesToTheTop.data.d.ts +44 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesToTheTop.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesToTheTop.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesUp.data.d.ts +38 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesUp.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftMultipleNodesUp.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeFromMiddle.data.d.ts +46 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeFromMiddle.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeFromMiddle.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeToTheTop.data.d.ts +57 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeToTheTop.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeToTheTop.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeUp.data.d.ts +45 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeUp.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteLiftOneNodeUp.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrap.data.d.ts +44 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrap.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrap.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrapMultiple.data.d.ts +44 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrapMultiple.playwright.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/__tests__/tripleBlockquoteWrapMultiple.test.d.ts +1 -0
- package/dist/features/wrapUnwrap/addStructureMark.d.ts +41 -0
- package/dist/features/wrapUnwrap/addStructureMark.js +15 -0
- package/dist/features/wrapUnwrap/findMatchingNodeSides.d.ts +15 -0
- package/dist/features/wrapUnwrap/findMatchingNodeSides.js +133 -0
- package/dist/features/wrapUnwrap/handleStructureStep.d.ts +4 -0
- package/dist/features/wrapUnwrap/handleStructureStep.js +174 -0
- package/dist/features/wrapUnwrap/revertStructureSuggestion.d.ts +44 -0
- package/dist/features/wrapUnwrap/revertStructureSuggestion.js +374 -0
- package/dist/generateId.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/rebaseStep.d.ts +9 -0
- package/dist/rebaseStep.js +11 -0
- package/dist/replaceAroundStep.js +6 -1
- package/dist/replaceStep.js +5 -0
- package/dist/schema.d.ts +2 -1
- package/dist/schema.js +37 -1
- package/dist/testing/testBuilders.d.ts +1 -2
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +6 -2
- package/package.json +1 -1
- package/src/features/wrapUnwrap/README.md +198 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Structure Suggestion Tracking & Reversal
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Track structural changes (wrap/unwrap in blockquotes, list item lift/sink) in
|
|
6
|
+
ProseMirror as "suggestions" that can be reverted later, without losing the
|
|
7
|
+
ability to reconstruct the original document state.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why Marks on Nodes?
|
|
12
|
+
|
|
13
|
+
**The Problem**: Document positions are ephemeral. A position like `from: 10`
|
|
14
|
+
becomes invalid after any edit before it.
|
|
15
|
+
|
|
16
|
+
**The Solution**: Anchor positions to **actual nodes** in the document using
|
|
17
|
+
marks. To recover a position:
|
|
18
|
+
|
|
19
|
+
1. Find the marked node (which moves with document edits)
|
|
20
|
+
2. Compute position from node's current `pos` + the `position` attribute:
|
|
21
|
+
- `position: "start"` → use `pos`
|
|
22
|
+
- `position: "end"` → use `pos + node.nodeSize`
|
|
23
|
+
|
|
24
|
+
This way, positions are **self-healing** — they remain valid as long as the
|
|
25
|
+
marked nodes exist.
|
|
26
|
+
|
|
27
|
+
**Why "start" vs "end"?** Because the 4 positions of a `ReplaceAroundStep` don't
|
|
28
|
+
always align with node boundaries in the same way. The mark must be placed on a
|
|
29
|
+
node that **exists in the final document**, and the position must be derivable
|
|
30
|
+
from that node's boundaries.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Mark Structure
|
|
35
|
+
|
|
36
|
+
### For `ReplaceAroundStep` (4 marks)
|
|
37
|
+
|
|
38
|
+
Each mark contains:
|
|
39
|
+
|
|
40
|
+
| Field | Purpose |
|
|
41
|
+
| ----------- | ------------------------------------------------------- |
|
|
42
|
+
| `id` | Suggestion ID (groups marks together) |
|
|
43
|
+
| `value` | `"from"` \| `"to"` \| `"gapFrom"` \| `"gapTo"` |
|
|
44
|
+
| `position` | `"start"` \| `"end"` (how to derive position from node) |
|
|
45
|
+
| `type` | `"replaceAround"` |
|
|
46
|
+
| `slice` | JSON of the slice for the **inverse** step |
|
|
47
|
+
| `insert` | Where gap content goes in the slice |
|
|
48
|
+
| `structure` | Boolean flag for structural step |
|
|
49
|
+
|
|
50
|
+
### For `ReplaceStep` (2 marks)
|
|
51
|
+
|
|
52
|
+
Only `from` and `to` marks are needed (no gap positions).
|
|
53
|
+
|
|
54
|
+
| Field | Purpose |
|
|
55
|
+
| ----------- | ------------------------------------------ |
|
|
56
|
+
| `id` | Suggestion ID |
|
|
57
|
+
| `value` | `"from"` \| `"to"` |
|
|
58
|
+
| `position` | `"start"` \| `"end"` |
|
|
59
|
+
| `type` | `"replace"` |
|
|
60
|
+
| `slice` | JSON of the slice for the **inverse** step |
|
|
61
|
+
| `structure` | Boolean flag |
|
|
62
|
+
|
|
63
|
+
### Mark Nesting
|
|
64
|
+
|
|
65
|
+
Marks are nested around content in the document:
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
doc(
|
|
69
|
+
structure[from, position:start](
|
|
70
|
+
structure[to, position:end](
|
|
71
|
+
blockquote(
|
|
72
|
+
structure[gapFrom, position:start](
|
|
73
|
+
structure[gapTo, position:end](
|
|
74
|
+
paragraph("Hello World")
|
|
75
|
+
))))))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
When traversing with `nodesBetween`, we find each marked node and compute
|
|
79
|
+
positions from it.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## How Reversal Works
|
|
84
|
+
|
|
85
|
+
The `revertStructureSuggestion(suggestionId)` command:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
1. FIND the main suggestion's marks
|
|
89
|
+
└── findStructureMarkGroupBySuggestionId(suggestionId)
|
|
90
|
+
└── Scan document for marks with matching ID
|
|
91
|
+
└── Collect: markFrom, markTo, [markGapFrom, markGapTo]
|
|
92
|
+
|
|
93
|
+
2. FIND nested suggestions within the range
|
|
94
|
+
└── nodesBetween(from, to) to find all structure marks
|
|
95
|
+
└── Collect all unique suggestion IDs
|
|
96
|
+
|
|
97
|
+
3. SORT by decreasing ID (revert newest first)
|
|
98
|
+
└── [3, 2, 1] — important for nested changes!
|
|
99
|
+
|
|
100
|
+
4. For EACH suggestion, REVERT:
|
|
101
|
+
└── Compute positions: getPosFromMark(mark, pos, node)
|
|
102
|
+
└── Deserialize slice from JSON
|
|
103
|
+
└── Reconstruct inverse step:
|
|
104
|
+
- ReplaceAroundStep(from, to, gapFrom, gapTo, slice, insert, structure)
|
|
105
|
+
- or ReplaceStep(from, to, slice, structure)
|
|
106
|
+
└── Remove the marks
|
|
107
|
+
└── Apply the step
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Why reverse order?** If step 1 created structure A, and step 2 modified within
|
|
111
|
+
A, you must undo step 2 before step 1, otherwise positions break.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## How Tests Work
|
|
116
|
+
|
|
117
|
+
Each test file has 3 data structures:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// 1. Initial document state (before any changes)
|
|
121
|
+
const initialState = doc(paragraph("Hello World"));
|
|
122
|
+
|
|
123
|
+
// 2. Final state (after applying steps, NO marks)
|
|
124
|
+
const finalState = doc(blockquote(paragraph("Hello World")));
|
|
125
|
+
|
|
126
|
+
// 3. Final state WITH structure marks embedded
|
|
127
|
+
// (manually constructed to simulate what forward tracking would produce)
|
|
128
|
+
const finalStateWithMarks = doc(
|
|
129
|
+
structure({ id: 1, data: { value: "from", position: "start", ... } },
|
|
130
|
+
structure({ id: 1, data: { value: "to", position: "end", ... } },
|
|
131
|
+
blockquote(
|
|
132
|
+
structure({ id: 1, data: { value: "gapFrom", ... } },
|
|
133
|
+
structure({ id: 1, data: { value: "gapTo", ... } },
|
|
134
|
+
paragraph("Hello World")
|
|
135
|
+
)))))
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// The steps that transform initial → final
|
|
139
|
+
const steps = [{ stepType: "replaceAround", from: 0, to: 13, ... }];
|
|
140
|
+
|
|
141
|
+
// The inverse steps that transform final → initial
|
|
142
|
+
const inverseSteps = [{ stepType: "replaceAround", from: 0, to: 15, ... }];
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Three test cases per scenario:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// Test 1: Forward transformation works
|
|
149
|
+
it("applies steps correctly", () => {
|
|
150
|
+
assertDocumentChanged(initialState, finalState, applySteps(steps));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Test 2: Inverse steps work (sanity check)
|
|
154
|
+
it("applies inverse steps correctly", () => {
|
|
155
|
+
assertDocumentChanged(
|
|
156
|
+
finalState,
|
|
157
|
+
initialState,
|
|
158
|
+
applySteps(inverseSteps.reverse()),
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Test 3: THE MAIN TEST - revert from marks works
|
|
163
|
+
it("reverts via structure suggestion", () => {
|
|
164
|
+
assertDocumentChanged(
|
|
165
|
+
finalStateWithMarks,
|
|
166
|
+
initialState,
|
|
167
|
+
revertStructureSuggestion(1),
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## What's Missing: Forward Tracking
|
|
175
|
+
|
|
176
|
+
The code that would:
|
|
177
|
+
|
|
178
|
+
1. Intercept `ReplaceAroundStep` / `ReplaceStep` in suggest-changes mode
|
|
179
|
+
2. Apply the step to get the new document
|
|
180
|
+
3. Compute the inverse step
|
|
181
|
+
4. Place structure marks on appropriate nodes with:
|
|
182
|
+
- Position encoding (which node, start vs end)
|
|
183
|
+
- Inverse step data (slice, insert, structure flag)
|
|
184
|
+
|
|
185
|
+
Currently `suggestReplaceAroundStep` in `replaceAroundStep.ts` converts
|
|
186
|
+
structural operations to `ReplaceStep` and uses insertion/deletion marks
|
|
187
|
+
instead, losing the structural information needed for proper reversal.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Additional Notes
|
|
192
|
+
|
|
193
|
+
### Offset Fields
|
|
194
|
+
|
|
195
|
+
The marks contain offset fields (`gapFromOffset`, `fromOffset`, `toOffset`,
|
|
196
|
+
`gapToOffset`) and a `debug` object with computed inverse positions. These are
|
|
197
|
+
present in test data but **not currently used** by the revert code — positions
|
|
198
|
+
are computed purely from node position + `position` field.
|