@charcuterie/ui 3.0.0 → 3.2.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/README.md +10 -0
- package/dist/Badge/Badge.d.ts +33 -3
- package/dist/Badge/Badge.d.ts.map +1 -1
- package/dist/Badge/Badge.js +20 -2
- package/dist/Badge/Badge.js.map +1 -1
- package/dist/Board/Board.d.ts +119 -0
- package/dist/Board/Board.d.ts.map +1 -0
- package/dist/Board/Board.js +165 -0
- package/dist/Board/Board.js.map +1 -0
- package/dist/Board/BoardCard.d.ts +120 -0
- package/dist/Board/BoardCard.d.ts.map +1 -0
- package/dist/Board/BoardCard.js +77 -0
- package/dist/Board/BoardCard.js.map +1 -0
- package/dist/Board/BoardLaneList.d.ts +103 -0
- package/dist/Board/BoardLaneList.d.ts.map +1 -0
- package/dist/Board/BoardLaneList.js +69 -0
- package/dist/Board/BoardLaneList.js.map +1 -0
- package/dist/Board/boardMove.d.ts +116 -0
- package/dist/Board/boardMove.d.ts.map +1 -0
- package/dist/Board/boardMove.js +116 -0
- package/dist/Board/boardMove.js.map +1 -0
- package/dist/Board/useBoardDrag.d.ts +44 -0
- package/dist/Board/useBoardDrag.d.ts.map +1 -0
- package/dist/Board/useBoardDrag.js +237 -0
- package/dist/Board/useBoardDrag.js.map +1 -0
- package/dist/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/Checkbox/Checkbox.js +15 -4
- package/dist/Checkbox/Checkbox.js.map +1 -1
- package/dist/DataTable/DataTable.d.ts +159 -0
- package/dist/DataTable/DataTable.d.ts.map +1 -0
- package/dist/DataTable/DataTable.js +197 -0
- package/dist/DataTable/DataTable.js.map +1 -0
- package/dist/DatePicker/DateGrid.d.ts +79 -0
- package/dist/DatePicker/DateGrid.d.ts.map +1 -0
- package/dist/DatePicker/DateGrid.js +220 -0
- package/dist/DatePicker/DateGrid.js.map +1 -0
- package/dist/DatePicker/DatePicker.d.ts +138 -0
- package/dist/DatePicker/DatePicker.d.ts.map +1 -0
- package/dist/DatePicker/DatePicker.js +384 -0
- package/dist/DatePicker/DatePicker.js.map +1 -0
- package/dist/DatePicker/datePresets.d.ts +29 -0
- package/dist/DatePicker/datePresets.d.ts.map +1 -0
- package/dist/DatePicker/datePresets.js +28 -0
- package/dist/DatePicker/datePresets.js.map +1 -0
- package/dist/DatePicker/parseDateInput.d.ts +106 -0
- package/dist/DatePicker/parseDateInput.d.ts.map +1 -0
- package/dist/DatePicker/parseDateInput.js +419 -0
- package/dist/DatePicker/parseDateInput.js.map +1 -0
- package/dist/DatePicker/plainDate.d.ts +167 -0
- package/dist/DatePicker/plainDate.d.ts.map +1 -0
- package/dist/DatePicker/plainDate.js +272 -0
- package/dist/DatePicker/plainDate.js.map +1 -0
- package/dist/Dialog/Dialog.d.ts +12 -2
- package/dist/Dialog/Dialog.d.ts.map +1 -1
- package/dist/Dialog/Dialog.js +2 -2
- package/dist/Dialog/Dialog.js.map +1 -1
- package/dist/MarkdownEditor/MarkdownEditor.d.ts +147 -0
- package/dist/MarkdownEditor/MarkdownEditor.d.ts.map +1 -0
- package/dist/MarkdownEditor/MarkdownEditor.js +492 -0
- package/dist/MarkdownEditor/MarkdownEditor.js.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts +202 -0
- package/dist/MarkdownEditor/markdownCommands.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownCommands.js +379 -0
- package/dist/MarkdownEditor/markdownCommands.js.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts +92 -0
- package/dist/MarkdownEditor/markdownSpans.d.ts.map +1 -0
- package/dist/MarkdownEditor/markdownSpans.js +303 -0
- package/dist/MarkdownEditor/markdownSpans.js.map +1 -0
- package/dist/Modal/Modal.d.ts +6 -2
- package/dist/Modal/Modal.d.ts.map +1 -1
- package/dist/Modal/Modal.js +1 -1
- package/dist/Modal/Modal.js.map +1 -1
- package/dist/Overlay/OverlayPanel.d.ts +13 -2
- package/dist/Overlay/OverlayPanel.d.ts.map +1 -1
- package/dist/Overlay/OverlayPanel.js +5 -1
- package/dist/Overlay/OverlayPanel.js.map +1 -1
- package/dist/categoricalStyles.d.ts +53 -0
- package/dist/categoricalStyles.d.ts.map +1 -0
- package/dist/categoricalStyles.js +195 -0
- package/dist/categoricalStyles.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Badge/Badge.mdx +46 -0
- package/src/Badge/Badge.stories.tsx +98 -4
- package/src/Badge/Badge.test.tsx +157 -2
- package/src/Badge/Badge.tsx +49 -3
- package/src/Board/Board.mdx +199 -0
- package/src/Board/Board.stories.tsx +707 -0
- package/src/Board/Board.test.tsx +310 -0
- package/src/Board/Board.tsx +333 -0
- package/src/Board/BoardCard.tsx +290 -0
- package/src/Board/BoardLaneList.tsx +275 -0
- package/src/Board/boardMove.test.ts +213 -0
- package/src/Board/boardMove.ts +225 -0
- package/src/Board/useBoardDrag.ts +413 -0
- package/src/Checkbox/Checkbox.tsx +34 -3
- package/src/DataTable/DataTable.mdx +123 -0
- package/src/DataTable/DataTable.stories.tsx +411 -0
- package/src/DataTable/DataTable.test.tsx +386 -0
- package/src/DataTable/DataTable.tsx +497 -0
- package/src/DatePicker/DateGrid.tsx +467 -0
- package/src/DatePicker/DatePicker.mdx +170 -0
- package/src/DatePicker/DatePicker.stories.tsx +268 -0
- package/src/DatePicker/DatePicker.test.tsx +455 -0
- package/src/DatePicker/DatePicker.tsx +852 -0
- package/src/DatePicker/datePresets.ts +34 -0
- package/src/DatePicker/parseDateInput.test.ts +187 -0
- package/src/DatePicker/parseDateInput.ts +629 -0
- package/src/DatePicker/plainDate.test.ts +375 -0
- package/src/DatePicker/plainDate.ts +465 -0
- package/src/Dialog/Dialog.stories.tsx +48 -0
- package/src/Dialog/Dialog.test.tsx +30 -0
- package/src/Dialog/Dialog.tsx +13 -1
- package/src/MarkdownEditor/MarkdownEditor.mdx +189 -0
- package/src/MarkdownEditor/MarkdownEditor.stories.tsx +267 -0
- package/src/MarkdownEditor/MarkdownEditor.test.tsx +627 -0
- package/src/MarkdownEditor/MarkdownEditor.tsx +862 -0
- package/src/MarkdownEditor/markdownCommands.test.ts +384 -0
- package/src/MarkdownEditor/markdownCommands.ts +566 -0
- package/src/MarkdownEditor/markdownSpans.test.ts +217 -0
- package/src/MarkdownEditor/markdownSpans.ts +458 -0
- package/src/Modal/Modal.tsx +7 -1
- package/src/Overlay/OverlayPanel.tsx +17 -1
- package/src/argTypes.storyHelpers.ts +39 -0
- package/src/categoricalStyles.ts +244 -0
- package/src/index.ts +89 -0
- package/src/sourceRules.test.ts +52 -1
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { expect, test } from "vitest"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
continueList,
|
|
5
|
+
getMinimalEdit,
|
|
6
|
+
indentLines,
|
|
7
|
+
insertImage,
|
|
8
|
+
insertLink,
|
|
9
|
+
insertText,
|
|
10
|
+
MARKDOWN_LINE_PREFIXES,
|
|
11
|
+
outdentLines,
|
|
12
|
+
toggleHeading,
|
|
13
|
+
toggleInlineMarker,
|
|
14
|
+
toggleLinePrefix,
|
|
15
|
+
toMarkdownImage,
|
|
16
|
+
} from "./markdownCommands.ts"
|
|
17
|
+
|
|
18
|
+
const at = (text: string, marker = "|") => {
|
|
19
|
+
const selectionStart = text.indexOf(marker)
|
|
20
|
+
|
|
21
|
+
const withoutFirst = text.replace(marker, "")
|
|
22
|
+
|
|
23
|
+
const second = withoutFirst.indexOf(marker)
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
selectionEnd: second === -1 ? selectionStart : second,
|
|
27
|
+
selectionStart,
|
|
28
|
+
text: withoutFirst.replace(marker, ""),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
test("bold wraps a selection", () => {
|
|
33
|
+
expect(
|
|
34
|
+
toggleInlineMarker(at("a |word| b"), "**"),
|
|
35
|
+
).toEqual({
|
|
36
|
+
selectionEnd: 8,
|
|
37
|
+
selectionStart: 4,
|
|
38
|
+
text: "a **word** b",
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The case a hand-rolled toggle misses. Double-click a word inside
|
|
44
|
+
* `**bold**` and the browser selects `bold`, not `**bold**` — so
|
|
45
|
+
* looking only *inside* the selection produces `****bold****`.
|
|
46
|
+
*/
|
|
47
|
+
test("bold unwraps when the markers are outside the selection", () => {
|
|
48
|
+
expect(
|
|
49
|
+
toggleInlineMarker(at("a **|word|** b"), "**"),
|
|
50
|
+
).toEqual({
|
|
51
|
+
selectionEnd: 6,
|
|
52
|
+
selectionStart: 2,
|
|
53
|
+
text: "a word b",
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("bold unwraps when the markers are inside the selection", () => {
|
|
58
|
+
expect(
|
|
59
|
+
toggleInlineMarker(at("a |**word**| b"), "**"),
|
|
60
|
+
).toEqual({
|
|
61
|
+
selectionEnd: 6,
|
|
62
|
+
selectionStart: 2,
|
|
63
|
+
text: "a word b",
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test("bold with no selection parks the caret between the markers", () => {
|
|
68
|
+
const result = toggleInlineMarker(at("a |b"), "**")
|
|
69
|
+
|
|
70
|
+
expect(result.text).toBe("a ****b")
|
|
71
|
+
|
|
72
|
+
expect(result.selectionStart).toBe(4)
|
|
73
|
+
|
|
74
|
+
expect(result.selectionEnd).toBe(4)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("a bulleted list toggles across every selected line", () => {
|
|
78
|
+
const bulleted = toggleLinePrefix(
|
|
79
|
+
at("|one\ntwo\nthree|"),
|
|
80
|
+
MARKDOWN_LINE_PREFIXES.bulletList,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
expect(bulleted.text).toBe("- one\n- two\n- three")
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
toggleLinePrefix(
|
|
87
|
+
{ ...bulleted, selectionStart: 0 },
|
|
88
|
+
MARKDOWN_LINE_PREFIXES.bulletList,
|
|
89
|
+
).text,
|
|
90
|
+
).toBe("one\ntwo\nthree")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test("a numbered list renumbers rather than repeating `1.`", () => {
|
|
94
|
+
expect(
|
|
95
|
+
toggleLinePrefix(
|
|
96
|
+
at("|one\ntwo\nthree|"),
|
|
97
|
+
MARKDOWN_LINE_PREFIXES.orderedList,
|
|
98
|
+
).text,
|
|
99
|
+
).toBe("1. one\n2. two\n3. three")
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A mixed block gets the prefix added to all of it. Removal wins
|
|
104
|
+
* only when *every* line already has it — otherwise the button's
|
|
105
|
+
* effect depends on invisible state.
|
|
106
|
+
*/
|
|
107
|
+
test("a mixed block gains the prefix rather than losing it", () => {
|
|
108
|
+
expect(
|
|
109
|
+
toggleLinePrefix(
|
|
110
|
+
at("|- one\ntwo|"),
|
|
111
|
+
MARKDOWN_LINE_PREFIXES.bulletList,
|
|
112
|
+
).text,
|
|
113
|
+
).toBe("- one\n- two")
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test("a task replaces a bullet instead of nesting inside it", () => {
|
|
117
|
+
expect(
|
|
118
|
+
toggleLinePrefix(
|
|
119
|
+
at("|- one|"),
|
|
120
|
+
MARKDOWN_LINE_PREFIXES.taskList,
|
|
121
|
+
).text,
|
|
122
|
+
).toBe("- [ ] one")
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test("indentation survives a prefix toggle", () => {
|
|
126
|
+
expect(
|
|
127
|
+
toggleLinePrefix(
|
|
128
|
+
at(" |one|"),
|
|
129
|
+
MARKDOWN_LINE_PREFIXES.bulletList,
|
|
130
|
+
).text,
|
|
131
|
+
).toBe(" - one")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test("a heading replaces a heading rather than stacking", () => {
|
|
135
|
+
expect(toggleHeading(at("### |a|"), 2).text).toBe("## a")
|
|
136
|
+
|
|
137
|
+
expect(toggleHeading(at("## |a|"), 2).text).toBe("a")
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test("Enter continues a bullet, a task, an ordered item and a quote", () => {
|
|
141
|
+
expect(continueList(at("- one|"))?.text).toBe("- one\n- ")
|
|
142
|
+
|
|
143
|
+
expect(continueList(at("- [x] one|"))?.text).toBe(
|
|
144
|
+
"- [x] one\n- [ ] ",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
expect(continueList(at("3. one|"))?.text).toBe(
|
|
148
|
+
"3. one\n4. ",
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
expect(continueList(at("> one|"))?.text).toBe("> one\n> ")
|
|
152
|
+
|
|
153
|
+
expect(continueList(at(" - one|"))?.text).toBe(
|
|
154
|
+
" - one\n - ",
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Without this an empty bullet is a trap: every Enter makes another
|
|
160
|
+
* empty bullet and the only way out is Backspace.
|
|
161
|
+
*/
|
|
162
|
+
test("Enter on an empty item clears the marker", () => {
|
|
163
|
+
const cleared = continueList(at("- one\n- |"))
|
|
164
|
+
|
|
165
|
+
expect(cleared?.text).toBe("- one\n")
|
|
166
|
+
|
|
167
|
+
expect(cleared?.selectionStart).toBe(6)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* `null` rather than an unchanged state, so the component does not
|
|
172
|
+
* call `preventDefault` and the Enter lands on the native undo
|
|
173
|
+
* stack as an ordinary typing step.
|
|
174
|
+
*/
|
|
175
|
+
test("Enter outside a list reports null", () => {
|
|
176
|
+
expect(continueList(at("plain|"))).toBeNull()
|
|
177
|
+
|
|
178
|
+
// A selection is a replacement, not a continuation.
|
|
179
|
+
expect(continueList(at("- |one|"))).toBeNull()
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test("indent and outdent move whole lines", () => {
|
|
183
|
+
const indented = indentLines(at("|one\ntwo|"))
|
|
184
|
+
|
|
185
|
+
expect(indented.text).toBe(" one\n two")
|
|
186
|
+
|
|
187
|
+
expect(
|
|
188
|
+
outdentLines({ ...indented, selectionStart: 0 }).text,
|
|
189
|
+
).toBe("one\ntwo")
|
|
190
|
+
|
|
191
|
+
// Outdenting a line with nothing to give leaves it alone rather
|
|
192
|
+
// than eating its first character.
|
|
193
|
+
expect(outdentLines(at("|one|")).text).toBe("one")
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test("a link parks the caret in the destination", () => {
|
|
197
|
+
const linked = insertLink(at("see |this| page"))
|
|
198
|
+
|
|
199
|
+
expect(linked.text).toBe("see [this]() page")
|
|
200
|
+
|
|
201
|
+
expect(linked.selectionStart).toBe(11)
|
|
202
|
+
|
|
203
|
+
expect(linked.selectionEnd).toBe(11)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test("an image escapes its alt text and brackets a dirty URL", () => {
|
|
207
|
+
expect(
|
|
208
|
+
toMarkdownImage({
|
|
209
|
+
alt: "a [bracket] and a \\slash",
|
|
210
|
+
url: "https://example.test/a.png",
|
|
211
|
+
}),
|
|
212
|
+
).toBe(
|
|
213
|
+
"![a \\[bracket\\] and a \\\\slash](https://example.test/a.png)",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
expect(
|
|
217
|
+
toMarkdownImage({
|
|
218
|
+
alt: "spaced",
|
|
219
|
+
url: "https://example.test/one two.png",
|
|
220
|
+
}),
|
|
221
|
+
).toBe("")
|
|
222
|
+
|
|
223
|
+
// A newline in either half would end the construct early and
|
|
224
|
+
// leave stray text in the document.
|
|
225
|
+
expect(
|
|
226
|
+
toMarkdownImage({
|
|
227
|
+
alt: "two\nlines",
|
|
228
|
+
url: "https://example.test/a\n.png",
|
|
229
|
+
}),
|
|
230
|
+
).toBe("")
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test("an image lands at the caret", () => {
|
|
234
|
+
expect(
|
|
235
|
+
insertImage(at("before | after"), {
|
|
236
|
+
alt: "shot",
|
|
237
|
+
url: "https://example.test/s.png",
|
|
238
|
+
}).text,
|
|
239
|
+
).toBe("before  after")
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* **The no-HTML round trip.**
|
|
244
|
+
*
|
|
245
|
+
* Docket exists partly because HTML in a description field
|
|
246
|
+
* destroyed a previous tracker's data, so this is the assertion
|
|
247
|
+
* that has to hold no matter what else changes: put in a value full
|
|
248
|
+
* of the characters that tempt escaping, run every command over it,
|
|
249
|
+
* and there must still be no tag and no entity in the string that
|
|
250
|
+
* gets stored.
|
|
251
|
+
*
|
|
252
|
+
* It is a strong test because it is checking a *structural*
|
|
253
|
+
* property rather than a behaviour: none of these functions
|
|
254
|
+
* constructs HTML, so none of them can leak it. If somebody later
|
|
255
|
+
* reaches for a "render to HTML and parse it back" shortcut, this
|
|
256
|
+
* is what fails.
|
|
257
|
+
*/
|
|
258
|
+
test("no command can put an HTML tag or an entity in the value", () => {
|
|
259
|
+
const dangerous = [
|
|
260
|
+
"<script>alert('xss')</script>",
|
|
261
|
+
"<b>already bold</b>",
|
|
262
|
+
"a < b && c > d",
|
|
263
|
+
"& < > " '",
|
|
264
|
+
"\"double\" and 'single' quotes",
|
|
265
|
+
"5 < 6 & 7 > 6",
|
|
266
|
+
"- <li>a list item that is not one</li>",
|
|
267
|
+
"| <td> | </td> |",
|
|
268
|
+
].join("\n")
|
|
269
|
+
|
|
270
|
+
const wholeDocument = {
|
|
271
|
+
selectionEnd: dangerous.length,
|
|
272
|
+
selectionStart: 0,
|
|
273
|
+
text: dangerous,
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Everything that transforms the document while leaving its
|
|
277
|
+
// characters in place. An escaping pass anywhere in the pipeline
|
|
278
|
+
// would shred all three of the fragments below.
|
|
279
|
+
const transformed = [
|
|
280
|
+
toggleInlineMarker(wholeDocument, "**"),
|
|
281
|
+
toggleInlineMarker(wholeDocument, "`"),
|
|
282
|
+
toggleHeading(wholeDocument, 3),
|
|
283
|
+
toggleLinePrefix(
|
|
284
|
+
wholeDocument,
|
|
285
|
+
MARKDOWN_LINE_PREFIXES.bulletList,
|
|
286
|
+
),
|
|
287
|
+
toggleLinePrefix(
|
|
288
|
+
wholeDocument,
|
|
289
|
+
MARKDOWN_LINE_PREFIXES.orderedList,
|
|
290
|
+
),
|
|
291
|
+
toggleLinePrefix(
|
|
292
|
+
wholeDocument,
|
|
293
|
+
MARKDOWN_LINE_PREFIXES.taskList,
|
|
294
|
+
),
|
|
295
|
+
toggleLinePrefix(
|
|
296
|
+
wholeDocument,
|
|
297
|
+
MARKDOWN_LINE_PREFIXES.blockquote,
|
|
298
|
+
),
|
|
299
|
+
indentLines(wholeDocument),
|
|
300
|
+
outdentLines(wholeDocument),
|
|
301
|
+
insertLink(wholeDocument, "https://example.test/x"),
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
for (const result of transformed) {
|
|
305
|
+
// Verbatim, not merely "contains no `<`" — the source
|
|
306
|
+
// deliberately *has* `<` in it as literal text, so a naive
|
|
307
|
+
// absence check would pass on an escaped value and fail on a
|
|
308
|
+
// correct one.
|
|
309
|
+
expect(result.text).toContain(
|
|
310
|
+
"<script>alert('xss')</script>",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
expect(result.text).toContain("<b>already bold</b>")
|
|
314
|
+
|
|
315
|
+
expect(result.text).toContain(
|
|
316
|
+
"& < > " '",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
expect(result.text).not.toContain("<script>")
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// The two commands that *replace* the selection rather than
|
|
323
|
+
// transforming it, checked on what they emit.
|
|
324
|
+
expect(
|
|
325
|
+
insertImage(wholeDocument, {
|
|
326
|
+
alt: "<img onerror=alert(1)>",
|
|
327
|
+
url: "https://example.test/x.png",
|
|
328
|
+
}).text,
|
|
329
|
+
).toBe(
|
|
330
|
+
"",
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
expect(insertText(wholeDocument, dangerous).text).toBe(
|
|
334
|
+
dangerous,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
// And the round trip: typing the value straight back in returns
|
|
338
|
+
// it character for character.
|
|
339
|
+
expect(
|
|
340
|
+
insertText(
|
|
341
|
+
{ selectionEnd: 0, selectionStart: 0, text: "" },
|
|
342
|
+
dangerous,
|
|
343
|
+
).text,
|
|
344
|
+
).toBe(dangerous)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Whole-string assignment wipes the browser's undo stack, so every
|
|
349
|
+
* command's result is turned back into the smallest range
|
|
350
|
+
* replacement and applied through the platform's own editing
|
|
351
|
+
* pipeline. One bold toggle should be one undo step over four
|
|
352
|
+
* characters, not a document-sized one.
|
|
353
|
+
*/
|
|
354
|
+
test("the minimal edit is the smallest range that explains the change", () => {
|
|
355
|
+
expect(
|
|
356
|
+
getMinimalEdit("a word b", "a **word** b"),
|
|
357
|
+
).toEqual({ end: 6, start: 2, text: "**word**" })
|
|
358
|
+
|
|
359
|
+
expect(getMinimalEdit("same", "same")).toEqual({
|
|
360
|
+
end: 4,
|
|
361
|
+
start: 4,
|
|
362
|
+
text: "",
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
expect(getMinimalEdit("abc", "ac")).toEqual({
|
|
366
|
+
end: 2,
|
|
367
|
+
start: 1,
|
|
368
|
+
text: "",
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
// Repetitive strings are where a naive prefix/suffix scan
|
|
372
|
+
// produces an overlapping — i.e. negative-length — range.
|
|
373
|
+
expect(getMinimalEdit("aaaa", "aaaaa")).toEqual({
|
|
374
|
+
end: 4,
|
|
375
|
+
start: 4,
|
|
376
|
+
text: "a",
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
expect(getMinimalEdit("aaaaa", "aaaa")).toEqual({
|
|
380
|
+
end: 5,
|
|
381
|
+
start: 4,
|
|
382
|
+
text: "",
|
|
383
|
+
})
|
|
384
|
+
})
|