@prosekit/extensions 0.12.2 → 0.13.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/list/style.css +5 -5
- package/dist/list/style.css.map +1 -1
- package/dist/prosekit-extensions-autocomplete.d.ts +11 -3
- package/dist/prosekit-extensions-autocomplete.d.ts.map +1 -1
- package/dist/prosekit-extensions-autocomplete.js +171 -60
- package/dist/prosekit-extensions-autocomplete.js.map +1 -1
- package/dist/prosekit-extensions-blockquote.js +1 -1
- package/dist/prosekit-extensions-blockquote.js.map +1 -1
- package/dist/prosekit-extensions-code.d.ts.map +1 -1
- package/dist/prosekit-extensions-commit.js +1 -1
- package/dist/prosekit-extensions-commit.js.map +1 -1
- package/dist/prosekit-extensions-heading.d.ts.map +1 -1
- package/dist/prosekit-extensions-heading.js +6 -6
- package/dist/prosekit-extensions-heading.js.map +1 -1
- package/dist/prosekit-extensions-loro.d.ts +16 -17
- package/dist/prosekit-extensions-loro.d.ts.map +1 -1
- package/dist/prosekit-extensions-loro.js +13 -6
- package/dist/prosekit-extensions-loro.js.map +1 -1
- package/dist/prosekit-extensions-paragraph.js +1 -1
- package/dist/prosekit-extensions-paragraph.js.map +1 -1
- package/dist/prosekit-extensions-placeholder.d.ts.map +1 -1
- package/dist/prosekit-extensions-placeholder.js +2 -3
- package/dist/prosekit-extensions-placeholder.js.map +1 -1
- package/dist/prosekit-extensions-strike.js +2 -2
- package/dist/prosekit-extensions-strike.js.map +1 -1
- package/dist/prosekit-extensions-table.js +0 -1
- package/dist/prosekit-extensions-text-align.js +4 -4
- package/dist/prosekit-extensions-text-align.js.map +1 -1
- package/dist/prosekit-extensions-yjs.js +1 -1
- package/dist/prosekit-extensions-yjs.js.map +1 -1
- package/package.json +15 -14
- package/src/autocomplete/autocomplete-helpers.ts +18 -9
- package/src/autocomplete/autocomplete-plugin.ts +261 -117
- package/src/autocomplete/autocomplete-rule.ts +3 -3
- package/src/autocomplete/autocomplete.spec.ts +239 -20
- package/src/autocomplete/autocomplete.ts +8 -0
- package/src/blockquote/blockquote-keymap.spec.ts +4 -4
- package/src/blockquote/blockquote-keymap.ts +1 -1
- package/src/commit/index.ts +1 -1
- package/src/hard-break/hard-break-keymap.spec.ts +5 -7
- package/src/heading/heading-keymap.spec.ts +7 -7
- package/src/heading/heading-keymap.ts +6 -6
- package/src/link/index.spec.ts +9 -8
- package/src/list/list-keymap.spec.ts +5 -5
- package/src/list/style.css +5 -5
- package/src/loro/loro-cursor-plugin.ts +23 -13
- package/src/loro/loro-keymap.ts +1 -1
- package/src/loro/loro.ts +14 -10
- package/src/paragraph/paragraph-keymap.ts +1 -1
- package/src/placeholder/index.ts +2 -1
- package/src/strike/index.ts +2 -2
- package/src/testing/index.ts +2 -2
- package/src/testing/keyboard.ts +0 -30
- package/src/text-align/index.ts +4 -4
- package/src/yjs/yjs-keymap.ts +1 -1
|
@@ -8,15 +8,13 @@ import {
|
|
|
8
8
|
it,
|
|
9
9
|
vi,
|
|
10
10
|
} from 'vitest'
|
|
11
|
+
import { keyboard } from 'vitest-browser-commands/playwright'
|
|
11
12
|
|
|
12
13
|
import {
|
|
13
14
|
defineTestExtension,
|
|
14
15
|
setupTestFromExtension,
|
|
15
16
|
} from '../testing'
|
|
16
|
-
import {
|
|
17
|
-
inputText,
|
|
18
|
-
pressKey,
|
|
19
|
-
} from '../testing/keyboard'
|
|
17
|
+
import { inputText } from '../testing/keyboard'
|
|
20
18
|
|
|
21
19
|
import { defineAutocomplete } from './autocomplete'
|
|
22
20
|
import {
|
|
@@ -26,10 +24,20 @@ import {
|
|
|
26
24
|
} from './autocomplete-rule'
|
|
27
25
|
|
|
28
26
|
function setupSlashMenu() {
|
|
29
|
-
const regex = canUseRegexLookbehind() ? /(?<!\S)\/(
|
|
27
|
+
const regex = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u
|
|
28
|
+
|
|
29
|
+
let matching: MatchHandlerOptions | null = null
|
|
30
30
|
|
|
31
|
-
const onEnter = vi.fn<MatchHandler>()
|
|
32
|
-
|
|
31
|
+
const onEnter = vi.fn<MatchHandler>((options) => {
|
|
32
|
+
matching = options
|
|
33
|
+
})
|
|
34
|
+
const onLeave = vi.fn<VoidFunction>(() => {
|
|
35
|
+
if (matching) {
|
|
36
|
+
matching = null
|
|
37
|
+
} else {
|
|
38
|
+
throw new Error('onLeave should not be called when there is no matching')
|
|
39
|
+
}
|
|
40
|
+
})
|
|
33
41
|
|
|
34
42
|
const rule = new AutocompleteRule({ regex, onEnter, onLeave })
|
|
35
43
|
const extension = union(defineTestExtension(), defineAutocomplete(rule))
|
|
@@ -38,16 +46,34 @@ function setupSlashMenu() {
|
|
|
38
46
|
const doc = n.doc(n.paragraph('<a>'))
|
|
39
47
|
editor.set(doc)
|
|
40
48
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
const isMatching = (): boolean => {
|
|
50
|
+
return !!matching
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const getMatching = (): MatchHandlerOptions => {
|
|
54
|
+
if (!matching) {
|
|
55
|
+
throw new Error('No matching found')
|
|
46
56
|
}
|
|
47
|
-
return
|
|
57
|
+
return matching
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
|
|
60
|
+
const getMatchingText = (): string => {
|
|
61
|
+
return getMatching().match[0]
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const showSelection = (): string => {
|
|
65
|
+
const { selection, doc } = editor.state
|
|
66
|
+
const textBackward = doc.textBetween(0, selection.from, '\n')
|
|
67
|
+
const textSelected = doc.textBetween(selection.from, selection.to, '\n')
|
|
68
|
+
const textForward = doc.textBetween(selection.to, doc.content.size, '\n')
|
|
69
|
+
if (selection.empty) {
|
|
70
|
+
return textBackward + '<cursor>' + textForward
|
|
71
|
+
} else {
|
|
72
|
+
return textBackward + '<selection>' + textSelected + '<selection>' + textForward
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { editor, n, m, onEnter, onLeave, getMatching, isMatching, getMatchingText, showSelection }
|
|
51
77
|
}
|
|
52
78
|
|
|
53
79
|
describe('defineAutocomplete', () => {
|
|
@@ -82,27 +108,27 @@ describe('defineAutocomplete', () => {
|
|
|
82
108
|
expect(onLeave).toHaveBeenCalledTimes(0)
|
|
83
109
|
|
|
84
110
|
// Slash menu should not be triggered when typing "/ "
|
|
85
|
-
await
|
|
111
|
+
await keyboard.press('Space')
|
|
86
112
|
expect(onEnter).toHaveBeenCalledTimes(1)
|
|
87
113
|
expect(onLeave).toHaveBeenCalledTimes(1)
|
|
88
114
|
})
|
|
89
115
|
|
|
90
116
|
it('can delete the matched text', async () => {
|
|
91
|
-
const { editor, onEnter,
|
|
117
|
+
const { editor, onEnter, getMatching } = setupSlashMenu()
|
|
92
118
|
|
|
93
119
|
expect(onEnter).not.toHaveBeenCalled()
|
|
94
120
|
|
|
95
121
|
await inputText('/')
|
|
96
122
|
expect(onEnter).toHaveBeenCalledTimes(1)
|
|
97
123
|
|
|
98
|
-
const options =
|
|
124
|
+
const options = getMatching()
|
|
99
125
|
expect(editor.state.doc.textContent).toBe('/')
|
|
100
126
|
options.deleteMatch()
|
|
101
127
|
expect(editor.state.doc.textContent).toBe('')
|
|
102
128
|
})
|
|
103
129
|
|
|
104
|
-
it('can ignore the match', async () => {
|
|
105
|
-
const { editor, onEnter, onLeave,
|
|
130
|
+
it('can ignore the match by calling `ignoreMatch`', async () => {
|
|
131
|
+
const { editor, onEnter, onLeave, getMatching } = setupSlashMenu()
|
|
106
132
|
|
|
107
133
|
expect(onEnter).not.toHaveBeenCalled()
|
|
108
134
|
|
|
@@ -118,7 +144,7 @@ describe('defineAutocomplete', () => {
|
|
|
118
144
|
expect(editor.state.doc.textContent).toBe('/a')
|
|
119
145
|
|
|
120
146
|
// Call `ignoreMatch` to dismiss the match
|
|
121
|
-
const options =
|
|
147
|
+
const options = getMatching()
|
|
122
148
|
options.ignoreMatch()
|
|
123
149
|
expect(onEnter).toHaveBeenCalledTimes(2)
|
|
124
150
|
expect(onLeave).toHaveBeenCalledTimes(1)
|
|
@@ -129,4 +155,197 @@ describe('defineAutocomplete', () => {
|
|
|
129
155
|
expect(onLeave).toHaveBeenCalledTimes(1)
|
|
130
156
|
expect(editor.state.doc.textContent).toBe('/aa')
|
|
131
157
|
})
|
|
158
|
+
|
|
159
|
+
it('can dismiss the match by deleting the matched text', async () => {
|
|
160
|
+
const { isMatching, showSelection } = setupSlashMenu()
|
|
161
|
+
|
|
162
|
+
expect(showSelection()).toMatchInlineSnapshot(`"<cursor>"`)
|
|
163
|
+
expect(isMatching()).toBe(false)
|
|
164
|
+
|
|
165
|
+
await inputText('/')
|
|
166
|
+
expect(showSelection()).toMatchInlineSnapshot(`"/<cursor>"`)
|
|
167
|
+
expect(isMatching()).toBe(true)
|
|
168
|
+
|
|
169
|
+
await keyboard.press('Backspace')
|
|
170
|
+
expect(showSelection()).toMatchInlineSnapshot(`"<cursor>"`)
|
|
171
|
+
expect(isMatching()).toBe(false)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('can recover the match after dismissing from Backspace', async () => {
|
|
175
|
+
const { isMatching, showSelection } = setupSlashMenu()
|
|
176
|
+
|
|
177
|
+
expect(showSelection()).toMatchInlineSnapshot(`"<cursor>"`)
|
|
178
|
+
expect(isMatching()).toBe(false)
|
|
179
|
+
|
|
180
|
+
await inputText('/')
|
|
181
|
+
expect(showSelection()).toMatchInlineSnapshot(`"/<cursor>"`)
|
|
182
|
+
expect(isMatching()).toBe(true)
|
|
183
|
+
|
|
184
|
+
await keyboard.press('Backspace')
|
|
185
|
+
expect(showSelection()).toMatchInlineSnapshot(`"<cursor>"`)
|
|
186
|
+
expect(isMatching()).toBe(false)
|
|
187
|
+
|
|
188
|
+
await inputText('/')
|
|
189
|
+
expect(showSelection()).toMatchInlineSnapshot(`"/<cursor>"`)
|
|
190
|
+
expect(isMatching()).toBe(true)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('can recover the match after dismissing from onLeave', async () => {
|
|
194
|
+
const { isMatching, showSelection, getMatching } = setupSlashMenu()
|
|
195
|
+
|
|
196
|
+
expect(showSelection()).toMatchInlineSnapshot(`"<cursor>"`)
|
|
197
|
+
expect(isMatching()).toBe(false)
|
|
198
|
+
|
|
199
|
+
await inputText('/')
|
|
200
|
+
expect(showSelection()).toMatchInlineSnapshot(`"/<cursor>"`)
|
|
201
|
+
expect(isMatching()).toBe(true)
|
|
202
|
+
|
|
203
|
+
const matching = getMatching()
|
|
204
|
+
expect(matching).toBeTruthy()
|
|
205
|
+
matching?.ignoreMatch()
|
|
206
|
+
expect(showSelection()).toMatchInlineSnapshot(`"/<cursor>"`)
|
|
207
|
+
expect(isMatching()).toBe(false)
|
|
208
|
+
|
|
209
|
+
await keyboard.press('Backspace')
|
|
210
|
+
expect(showSelection()).toMatchInlineSnapshot(`"<cursor>"`)
|
|
211
|
+
expect(isMatching()).toBe(false)
|
|
212
|
+
|
|
213
|
+
await inputText('/')
|
|
214
|
+
expect(showSelection()).toMatchInlineSnapshot(`"/<cursor>"`)
|
|
215
|
+
expect(isMatching()).toBe(true)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('can start a new match after dismissing the previous match', async () => {
|
|
219
|
+
const { isMatching, showSelection, getMatching, getMatchingText } = setupSlashMenu()
|
|
220
|
+
|
|
221
|
+
expect(showSelection()).toMatchInlineSnapshot(`"<cursor>"`)
|
|
222
|
+
expect(isMatching()).toBe(false)
|
|
223
|
+
|
|
224
|
+
await inputText('a /b')
|
|
225
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /b<cursor>"`)
|
|
226
|
+
expect(isMatching()).toBe(true)
|
|
227
|
+
|
|
228
|
+
getMatching().ignoreMatch()
|
|
229
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /b<cursor>"`)
|
|
230
|
+
expect(isMatching()).toBe(false)
|
|
231
|
+
|
|
232
|
+
await keyboard.press('Space')
|
|
233
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /b <cursor>"`)
|
|
234
|
+
expect(isMatching()).toBe(false)
|
|
235
|
+
|
|
236
|
+
await inputText('/')
|
|
237
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /b /<cursor>"`)
|
|
238
|
+
expect(isMatching()).toBe(true)
|
|
239
|
+
expect(getMatchingText()).toBe('/')
|
|
240
|
+
|
|
241
|
+
await inputText('c')
|
|
242
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /b /c<cursor>"`)
|
|
243
|
+
expect(isMatching()).toBe(true)
|
|
244
|
+
expect(getMatchingText()).toBe('/c')
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('can dismiss the match by creating a new paragraph', async () => {
|
|
248
|
+
const { isMatching, showSelection } = setupSlashMenu()
|
|
249
|
+
|
|
250
|
+
expect(showSelection()).toMatchInlineSnapshot(`"<cursor>"`)
|
|
251
|
+
expect(isMatching()).toBe(false)
|
|
252
|
+
|
|
253
|
+
await inputText('/')
|
|
254
|
+
expect(showSelection()).toMatchInlineSnapshot(`"/<cursor>"`)
|
|
255
|
+
expect(isMatching()).toBe(true)
|
|
256
|
+
|
|
257
|
+
await keyboard.press('Enter')
|
|
258
|
+
expect(showSelection()).toMatchInlineSnapshot(`
|
|
259
|
+
"/
|
|
260
|
+
<cursor>"
|
|
261
|
+
`)
|
|
262
|
+
expect(isMatching()).toBe(false)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('can keep the match when selecting the text', async () => {
|
|
266
|
+
const { isMatching, showSelection } = setupSlashMenu()
|
|
267
|
+
|
|
268
|
+
expect(showSelection()).toMatchInlineSnapshot(`"<cursor>"`)
|
|
269
|
+
expect(isMatching()).toBe(false)
|
|
270
|
+
|
|
271
|
+
await inputText('/page')
|
|
272
|
+
expect(showSelection()).toMatchInlineSnapshot(`"/page<cursor>"`)
|
|
273
|
+
expect(isMatching()).toBe(true)
|
|
274
|
+
|
|
275
|
+
await keyboard.down('Shift')
|
|
276
|
+
await keyboard.press('ArrowLeft')
|
|
277
|
+
await keyboard.press('ArrowLeft')
|
|
278
|
+
await keyboard.up('Shift')
|
|
279
|
+
expect(showSelection()).toMatchInlineSnapshot(`"/pa<selection>ge<selection>"`)
|
|
280
|
+
expect(isMatching()).toBe(true)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('can ignore the match by moving the text cursor outside of the match', async () => {
|
|
284
|
+
const { onEnter, isMatching, getMatchingText, showSelection } = setupSlashMenu()
|
|
285
|
+
|
|
286
|
+
expect(onEnter).not.toHaveBeenCalled()
|
|
287
|
+
|
|
288
|
+
expect(showSelection()).toMatchInlineSnapshot(`"<cursor>"`)
|
|
289
|
+
expect(isMatching()).toBe(false)
|
|
290
|
+
|
|
291
|
+
await inputText('a ')
|
|
292
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a <cursor>"`)
|
|
293
|
+
expect(isMatching()).toBe(false)
|
|
294
|
+
|
|
295
|
+
await inputText('/')
|
|
296
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /<cursor>"`)
|
|
297
|
+
expect(isMatching()).toBe(true)
|
|
298
|
+
expect(getMatchingText()).toBe('/')
|
|
299
|
+
|
|
300
|
+
await inputText('b')
|
|
301
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /b<cursor>"`)
|
|
302
|
+
expect(isMatching()).toBe(true)
|
|
303
|
+
expect(getMatchingText()).toBe('/b')
|
|
304
|
+
|
|
305
|
+
await keyboard.press('ArrowLeft')
|
|
306
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /<cursor>b"`)
|
|
307
|
+
expect(isMatching()).toBe(true)
|
|
308
|
+
expect(getMatchingText()).toBe('/b')
|
|
309
|
+
|
|
310
|
+
await keyboard.press('ArrowLeft')
|
|
311
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a <cursor>/b"`)
|
|
312
|
+
expect(isMatching()).toBe(true)
|
|
313
|
+
expect(getMatchingText()).toBe('/b')
|
|
314
|
+
|
|
315
|
+
await keyboard.press('ArrowLeft')
|
|
316
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a<cursor> /b"`)
|
|
317
|
+
expect(isMatching()).toBe(false)
|
|
318
|
+
|
|
319
|
+
await keyboard.press('ArrowRight')
|
|
320
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a <cursor>/b"`)
|
|
321
|
+
expect(isMatching()).toBe(false)
|
|
322
|
+
|
|
323
|
+
await keyboard.press('ArrowRight')
|
|
324
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /<cursor>b"`)
|
|
325
|
+
expect(isMatching()).toBe(false)
|
|
326
|
+
|
|
327
|
+
await keyboard.press('Backspace')
|
|
328
|
+
await keyboard.press('Backspace')
|
|
329
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a<cursor>b"`)
|
|
330
|
+
expect(isMatching()).toBe(false)
|
|
331
|
+
|
|
332
|
+
await inputText(' /')
|
|
333
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /<cursor>b"`)
|
|
334
|
+
expect(isMatching()).toBe(true)
|
|
335
|
+
expect(getMatchingText()).toBe('/')
|
|
336
|
+
|
|
337
|
+
await inputText('c')
|
|
338
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /c<cursor>b"`)
|
|
339
|
+
expect(isMatching()).toBe(true)
|
|
340
|
+
expect(getMatchingText()).toBe('/c')
|
|
341
|
+
|
|
342
|
+
await inputText('d')
|
|
343
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /cd<cursor>b"`)
|
|
344
|
+
expect(isMatching()).toBe(true)
|
|
345
|
+
expect(getMatchingText()).toBe('/cd')
|
|
346
|
+
|
|
347
|
+
await keyboard.press('ArrowRight')
|
|
348
|
+
expect(showSelection()).toMatchInlineSnapshot(`"a /cdb<cursor>"`)
|
|
349
|
+
expect(isMatching()).toBe(false)
|
|
350
|
+
})
|
|
132
351
|
})
|
|
@@ -9,6 +9,14 @@ import {
|
|
|
9
9
|
import { createAutocompletePlugin } from './autocomplete-plugin'
|
|
10
10
|
import type { AutocompleteRule } from './autocomplete-rule'
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Defines an autocomplete extension that executes logic when the text before
|
|
14
|
+
* the cursor matches the given regular expression.
|
|
15
|
+
*
|
|
16
|
+
* When a match is found, an inline decoration is applied to the matched text
|
|
17
|
+
* with the class `prosekit-autocomplete-match` and a `data-autocomplete-match-text`
|
|
18
|
+
* attribute containing the full matched string.
|
|
19
|
+
*/
|
|
12
20
|
export function defineAutocomplete(rule: AutocompleteRule): Extension {
|
|
13
21
|
return defineFacetPayload(autocompleteFacet, [rule])
|
|
14
22
|
}
|
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
expect,
|
|
4
4
|
it,
|
|
5
5
|
} from 'vitest'
|
|
6
|
+
import { keyboard } from 'vitest-browser-commands/playwright'
|
|
6
7
|
|
|
7
8
|
import { setupTest } from '../testing'
|
|
8
|
-
import { pressKey } from '../testing/keyboard'
|
|
9
9
|
|
|
10
10
|
describe('blockquote keymap', () => {
|
|
11
11
|
it('should wrap paragraph into blockquote with shortcut', async () => {
|
|
@@ -14,7 +14,7 @@ describe('blockquote keymap', () => {
|
|
|
14
14
|
const doc1 = n.doc(n.p('hel<a>lo'))
|
|
15
15
|
editor.set(doc1)
|
|
16
16
|
|
|
17
|
-
await
|
|
17
|
+
await keyboard.press('ControlOrMeta+Shift+B')
|
|
18
18
|
|
|
19
19
|
const doc2 = n.doc(n.blockquote(n.p('hello')))
|
|
20
20
|
expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
|
|
@@ -25,7 +25,7 @@ describe('blockquote keymap', () => {
|
|
|
25
25
|
const doc1 = n.doc(n.blockquote(n.p('hello')))
|
|
26
26
|
editor.set(doc1)
|
|
27
27
|
|
|
28
|
-
await
|
|
28
|
+
await keyboard.press('ControlOrMeta+Shift+B')
|
|
29
29
|
|
|
30
30
|
const doc2 = n.doc(n.p('hello'))
|
|
31
31
|
expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
|
|
@@ -37,7 +37,7 @@ describe('blockquote keymap', () => {
|
|
|
37
37
|
|
|
38
38
|
editor.set(doc1)
|
|
39
39
|
|
|
40
|
-
await
|
|
40
|
+
await keyboard.press('Backspace')
|
|
41
41
|
|
|
42
42
|
const doc2 = n.doc(n.p('hello'))
|
|
43
43
|
expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
|
|
@@ -25,7 +25,7 @@ function backspaceUnsetBlockquote(): Command {
|
|
|
25
25
|
*/
|
|
26
26
|
export function defineBlockquoteKeymap(): PlainExtension {
|
|
27
27
|
return defineKeymap({
|
|
28
|
-
'
|
|
28
|
+
'Mod-B': toggleBlockquoteKeybinding(),
|
|
29
29
|
'Backspace': backspaceUnsetBlockquote(),
|
|
30
30
|
})
|
|
31
31
|
}
|
package/src/commit/index.ts
CHANGED
|
@@ -250,7 +250,7 @@ function defineCommitDecoration(commit: Commit): PlainExtension {
|
|
|
250
250
|
*/
|
|
251
251
|
function defineCommitViewer(commit: Commit): PlainExtension {
|
|
252
252
|
return union(
|
|
253
|
-
defineDefaultState({
|
|
253
|
+
defineDefaultState({ defaultContent: commit.doc }),
|
|
254
254
|
defineCommitDecoration(commit),
|
|
255
255
|
)
|
|
256
256
|
}
|
|
@@ -3,12 +3,10 @@ import {
|
|
|
3
3
|
expect,
|
|
4
4
|
it,
|
|
5
5
|
} from 'vitest'
|
|
6
|
+
import { keyboard } from 'vitest-browser-commands/playwright'
|
|
6
7
|
|
|
7
8
|
import { setupTest } from '../testing'
|
|
8
|
-
import {
|
|
9
|
-
inputText,
|
|
10
|
-
pressKey,
|
|
11
|
-
} from '../testing/keyboard'
|
|
9
|
+
import { inputText } from '../testing/keyboard'
|
|
12
10
|
|
|
13
11
|
describe('defineHardBreakKeymap', () => {
|
|
14
12
|
it('should insert hard break', async () => {
|
|
@@ -19,12 +17,12 @@ describe('defineHardBreakKeymap', () => {
|
|
|
19
17
|
|
|
20
18
|
editor.set(doc1)
|
|
21
19
|
expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
|
|
22
|
-
await
|
|
20
|
+
await keyboard.press('Shift+Enter')
|
|
23
21
|
expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
|
|
24
22
|
|
|
25
23
|
editor.set(doc1)
|
|
26
24
|
expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
|
|
27
|
-
await
|
|
25
|
+
await keyboard.press('ControlOrMeta+Enter')
|
|
28
26
|
expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
|
|
29
27
|
})
|
|
30
28
|
|
|
@@ -37,7 +35,7 @@ describe('defineHardBreakKeymap', () => {
|
|
|
37
35
|
|
|
38
36
|
editor.set(doc1)
|
|
39
37
|
expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
|
|
40
|
-
await
|
|
38
|
+
await keyboard.press('Shift+Enter')
|
|
41
39
|
expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
|
|
42
40
|
await inputText('baz')
|
|
43
41
|
expect(editor.state.doc.toJSON()).toEqual(doc3.toJSON())
|
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
expect,
|
|
4
4
|
it,
|
|
5
5
|
} from 'vitest'
|
|
6
|
+
import { keyboard } from 'vitest-browser-commands/playwright'
|
|
6
7
|
|
|
7
8
|
import { setupTest } from '../testing'
|
|
8
|
-
import { pressKey } from '../testing/keyboard'
|
|
9
9
|
|
|
10
10
|
describe('defineHeadingKeymap', () => {
|
|
11
11
|
it('should toggle heading', async () => {
|
|
@@ -17,13 +17,13 @@ describe('defineHeadingKeymap', () => {
|
|
|
17
17
|
|
|
18
18
|
editor.set(doc)
|
|
19
19
|
expect(editor.state.doc.toJSON()).toEqual(doc.toJSON())
|
|
20
|
-
await
|
|
20
|
+
await keyboard.press('ControlOrMeta+Alt+1')
|
|
21
21
|
expect(editor.state.doc.toJSON()).toEqual(docH1.toJSON())
|
|
22
|
-
await
|
|
22
|
+
await keyboard.press('ControlOrMeta+Alt+1')
|
|
23
23
|
expect(editor.state.doc.toJSON()).toEqual(doc.toJSON())
|
|
24
|
-
await
|
|
24
|
+
await keyboard.press('ControlOrMeta+Alt+3')
|
|
25
25
|
expect(editor.state.doc.toJSON()).toEqual(docH3.toJSON())
|
|
26
|
-
await
|
|
26
|
+
await keyboard.press('ControlOrMeta+Alt+1')
|
|
27
27
|
expect(editor.state.doc.toJSON()).toEqual(docH1.toJSON())
|
|
28
28
|
})
|
|
29
29
|
|
|
@@ -35,7 +35,7 @@ describe('defineHeadingKeymap', () => {
|
|
|
35
35
|
|
|
36
36
|
editor.set(doc1)
|
|
37
37
|
expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
|
|
38
|
-
await
|
|
38
|
+
await keyboard.press('Backspace')
|
|
39
39
|
expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
|
|
40
40
|
})
|
|
41
41
|
|
|
@@ -47,7 +47,7 @@ describe('defineHeadingKeymap', () => {
|
|
|
47
47
|
|
|
48
48
|
editor.set(doc1)
|
|
49
49
|
expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
|
|
50
|
-
await
|
|
50
|
+
await keyboard.press('Backspace')
|
|
51
51
|
expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
|
|
52
52
|
})
|
|
53
53
|
})
|
|
@@ -29,12 +29,12 @@ const backspaceUnsetHeading: Command = (state, dispatch, view) => {
|
|
|
29
29
|
*/
|
|
30
30
|
export function defineHeadingKeymap(): PlainExtension {
|
|
31
31
|
return defineKeymap({
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
32
|
+
'Mod-Alt-1': toggleHeadingKeybinding(1),
|
|
33
|
+
'Mod-Alt-2': toggleHeadingKeybinding(2),
|
|
34
|
+
'Mod-Alt-3': toggleHeadingKeybinding(3),
|
|
35
|
+
'Mod-Alt-4': toggleHeadingKeybinding(4),
|
|
36
|
+
'Mod-Alt-5': toggleHeadingKeybinding(5),
|
|
37
|
+
'Mod-Alt-6': toggleHeadingKeybinding(6),
|
|
38
38
|
'Backspace': backspaceUnsetHeading,
|
|
39
39
|
})
|
|
40
40
|
}
|
package/src/link/index.spec.ts
CHANGED
|
@@ -3,9 +3,10 @@ import {
|
|
|
3
3
|
expect,
|
|
4
4
|
it,
|
|
5
5
|
} from 'vitest'
|
|
6
|
-
import {
|
|
6
|
+
import { keyboard } from 'vitest-browser-commands/playwright'
|
|
7
7
|
|
|
8
8
|
import { setupTest } from '../testing'
|
|
9
|
+
import { inputText } from '../testing/keyboard'
|
|
9
10
|
|
|
10
11
|
describe('defineLinkCommands', () => {
|
|
11
12
|
const { editor, n, m } = setupTest()
|
|
@@ -46,11 +47,11 @@ describe('defineLinkCommands', () => {
|
|
|
46
47
|
describe('defineLinkInputRule', () => {
|
|
47
48
|
it('should insert a link after pressing Space', async () => {
|
|
48
49
|
const { editor } = setupTest()
|
|
49
|
-
await
|
|
50
|
+
await inputText('https://example.com')
|
|
50
51
|
expect(editor.view.state.doc.toString()).toMatchInlineSnapshot(
|
|
51
52
|
`"doc(paragraph("https://example.com"))"`,
|
|
52
53
|
)
|
|
53
|
-
await
|
|
54
|
+
await inputText(' ')
|
|
54
55
|
expect(editor.view.state.doc.toString()).toMatchInlineSnapshot(
|
|
55
56
|
`"doc(paragraph(link("https://example.com"), " "))"`,
|
|
56
57
|
)
|
|
@@ -58,15 +59,15 @@ describe('defineLinkInputRule', () => {
|
|
|
58
59
|
|
|
59
60
|
it('should handle a link before a period', async () => {
|
|
60
61
|
const { editor } = setupTest()
|
|
61
|
-
await
|
|
62
|
+
await inputText('https://example.com')
|
|
62
63
|
expect(editor.view.state.doc.toString()).toMatchInlineSnapshot(
|
|
63
64
|
`"doc(paragraph("https://example.com"))"`,
|
|
64
65
|
)
|
|
65
|
-
await
|
|
66
|
+
await inputText('.')
|
|
66
67
|
expect(editor.view.state.doc.toString()).toMatchInlineSnapshot(
|
|
67
68
|
`"doc(paragraph("https://example.com."))"`,
|
|
68
69
|
)
|
|
69
|
-
await
|
|
70
|
+
await inputText(' ')
|
|
70
71
|
expect(editor.view.state.doc.toString()).toMatchInlineSnapshot(
|
|
71
72
|
`"doc(paragraph(link("https://example.com"), ". "))"`,
|
|
72
73
|
)
|
|
@@ -76,11 +77,11 @@ describe('defineLinkInputRule', () => {
|
|
|
76
77
|
describe('defineLinkEnterRule', () => {
|
|
77
78
|
it('should insert a link after pressing Enter', async () => {
|
|
78
79
|
const { editor } = setupTest()
|
|
79
|
-
await
|
|
80
|
+
await inputText('https://example.com')
|
|
80
81
|
expect(editor.view.state.doc.toString()).toMatchInlineSnapshot(
|
|
81
82
|
`"doc(paragraph("https://example.com"))"`,
|
|
82
83
|
)
|
|
83
|
-
await
|
|
84
|
+
await keyboard.press('Enter')
|
|
84
85
|
expect(editor.view.state.doc.toString()).toMatchInlineSnapshot(
|
|
85
86
|
`"doc(paragraph(link("https://example.com")), paragraph)"`,
|
|
86
87
|
)
|
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
expect,
|
|
4
4
|
it,
|
|
5
5
|
} from 'vitest'
|
|
6
|
+
import { keyboard } from 'vitest-browser-commands/playwright'
|
|
6
7
|
|
|
7
8
|
import { setupTest } from '../testing'
|
|
8
|
-
import { pressKey } from '../testing/keyboard'
|
|
9
9
|
|
|
10
10
|
describe('keymap', () => {
|
|
11
11
|
const { editor, n } = setupTest()
|
|
@@ -26,14 +26,14 @@ describe('keymap', () => {
|
|
|
26
26
|
)
|
|
27
27
|
editor.set(doc1)
|
|
28
28
|
|
|
29
|
-
await
|
|
29
|
+
await keyboard.press('ControlOrMeta+]')
|
|
30
30
|
expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
|
|
31
|
-
await
|
|
31
|
+
await keyboard.press('ControlOrMeta+[')
|
|
32
32
|
expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
|
|
33
33
|
|
|
34
|
-
await
|
|
34
|
+
await keyboard.press('Tab')
|
|
35
35
|
expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
|
|
36
|
-
await
|
|
36
|
+
await keyboard.press('Shift+Tab')
|
|
37
37
|
expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
|
|
38
38
|
})
|
|
39
39
|
})
|
package/src/list/style.css
CHANGED
|
@@ -14,17 +14,17 @@
|
|
|
14
14
|
|
|
15
15
|
& > .list-marker {
|
|
16
16
|
position: absolute;
|
|
17
|
-
left: 0;
|
|
18
17
|
width: 1.5em;
|
|
19
18
|
width: 1lh;
|
|
20
19
|
height: 1.5em;
|
|
21
20
|
height: 1lh;
|
|
21
|
+
inset-inline-start: 0;
|
|
22
22
|
text-align: center;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
& > .list-content {
|
|
26
|
-
margin-
|
|
27
|
-
margin-
|
|
26
|
+
margin-inline-start: 1.5em;
|
|
27
|
+
margin-inline-start: 1lh;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
&[data-list-kind="bullet"] > .list-marker,
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
|
|
65
65
|
&::before {
|
|
66
66
|
position: absolute;
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
inset-inline-end: calc(100% - 1.5em);
|
|
68
|
+
inset-inline-end: calc(100% - 1lh);
|
|
69
69
|
content: counter(prosemirror-flat-list-counter, decimal) ". ";
|
|
70
70
|
font-variant-numeric: tabular-nums;
|
|
71
71
|
}
|
|
@@ -2,27 +2,37 @@ import {
|
|
|
2
2
|
definePlugin,
|
|
3
3
|
type PlainExtension,
|
|
4
4
|
} from '@prosekit/core'
|
|
5
|
+
import type { Plugin } from '@prosekit/pm/state'
|
|
5
6
|
import type {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import type { PeerID } from 'loro-crdt'
|
|
7
|
+
CursorAwareness,
|
|
8
|
+
CursorEphemeralStore,
|
|
9
|
+
CursorPluginOptions,
|
|
10
|
+
} from 'loro-prosemirror'
|
|
11
11
|
import {
|
|
12
12
|
LoroCursorPlugin,
|
|
13
|
-
|
|
13
|
+
LoroEphemeralCursorPlugin,
|
|
14
14
|
} from 'loro-prosemirror'
|
|
15
15
|
|
|
16
|
-
export interface LoroCursorOptions {
|
|
17
|
-
awareness
|
|
18
|
-
|
|
19
|
-
createCursor?: (user: PeerID) => Element
|
|
20
|
-
createSelection?: (user: PeerID) => DecorationAttrs
|
|
16
|
+
export interface LoroCursorOptions extends CursorPluginOptions {
|
|
17
|
+
awareness?: CursorAwareness
|
|
18
|
+
presence?: CursorEphemeralStore
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
export function defineLoroCursorPlugin(
|
|
24
22
|
options: LoroCursorOptions,
|
|
25
23
|
): PlainExtension {
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
return definePlugin(createLoroCursorPlugin(options))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createLoroCursorPlugin(options: LoroCursorOptions): Plugin {
|
|
28
|
+
const { awareness, presence, ...rest } = options
|
|
29
|
+
if (awareness && presence) {
|
|
30
|
+
throw new Error('Only one of awareness and presence can be provided')
|
|
31
|
+
} else if (awareness) {
|
|
32
|
+
return LoroCursorPlugin(awareness, rest)
|
|
33
|
+
} else if (presence) {
|
|
34
|
+
return LoroEphemeralCursorPlugin(presence, rest)
|
|
35
|
+
} else {
|
|
36
|
+
throw new Error('Either awareness or presence must be provided')
|
|
37
|
+
}
|
|
28
38
|
}
|