@aicayzer/inkkit 0.0.17 → 0.1.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.
Files changed (81) hide show
  1. package/NOTICE +4 -2
  2. package/README.md +12 -6
  3. package/dist/THIRD-PARTY-NOTICES.txt +2742 -0
  4. package/dist/chunks/core-vtCr4MBS.js +56503 -0
  5. package/dist/chunks/highlight-Daj2fv_u.js +14122 -0
  6. package/dist/core.d.ts +4 -0
  7. package/dist/core.js +2 -0
  8. package/dist/editor.d.ts +261 -48
  9. package/dist/index.d.ts +1 -7
  10. package/dist/index.js +5 -2
  11. package/dist/style.css +242 -221
  12. package/dist/types.d.ts +143 -18
  13. package/package.json +26 -15
  14. package/dist/autolink.d.ts +0 -1
  15. package/dist/autolink.js +0 -18
  16. package/dist/callouts.d.ts +0 -26
  17. package/dist/callouts.js +0 -311
  18. package/dist/clipboard.d.ts +0 -11
  19. package/dist/clipboard.js +0 -261
  20. package/dist/code-fences.d.ts +0 -8
  21. package/dist/code-fences.js +0 -166
  22. package/dist/comments.d.ts +0 -55
  23. package/dist/comments.js +0 -494
  24. package/dist/decorations.d.ts +0 -2
  25. package/dist/decorations.js +0 -67
  26. package/dist/dialect.d.ts +0 -9
  27. package/dist/dialect.js +0 -358
  28. package/dist/editor.js +0 -2441
  29. package/dist/files.d.ts +0 -40
  30. package/dist/files.js +0 -133
  31. package/dist/highlight.d.ts +0 -1
  32. package/dist/highlight.js +0 -136
  33. package/dist/image-blocks.d.ts +0 -71
  34. package/dist/image-blocks.js +0 -286
  35. package/dist/images.d.ts +0 -51
  36. package/dist/images.js +0 -398
  37. package/dist/inline-highlight.d.ts +0 -6
  38. package/dist/inline-highlight.js +0 -112
  39. package/dist/labels.d.ts +0 -36
  40. package/dist/labels.js +0 -34
  41. package/dist/linked-syntax.d.ts +0 -48
  42. package/dist/linked-syntax.js +0 -320
  43. package/dist/literals.d.ts +0 -19
  44. package/dist/literals.js +0 -211
  45. package/dist/mermaid.d.ts +0 -8
  46. package/dist/mermaid.js +0 -321
  47. package/dist/outline.d.ts +0 -22
  48. package/dist/outline.js +0 -79
  49. package/dist/paste-html.d.ts +0 -23
  50. package/dist/paste-html.js +0 -441
  51. package/dist/paste.d.ts +0 -26
  52. package/dist/paste.js +0 -482
  53. package/dist/portable-files.d.ts +0 -10
  54. package/dist/portable-files.js +0 -36
  55. package/dist/preserve.d.ts +0 -21
  56. package/dist/preserve.js +0 -757
  57. package/dist/print.d.ts +0 -7
  58. package/dist/print.js +0 -409
  59. package/dist/reference-clipboard.d.ts +0 -8
  60. package/dist/reference-clipboard.js +0 -201
  61. package/dist/references.d.ts +0 -14
  62. package/dist/references.js +0 -368
  63. package/dist/search.d.ts +0 -15
  64. package/dist/search.js +0 -202
  65. package/dist/selection.d.ts +0 -1
  66. package/dist/selection.js +0 -59
  67. package/dist/source.d.ts +0 -16
  68. package/dist/source.js +0 -57
  69. package/dist/spreadsheet.d.ts +0 -3
  70. package/dist/spreadsheet.js +0 -275
  71. package/dist/table-alignment.d.ts +0 -2
  72. package/dist/table-alignment.js +0 -46
  73. package/dist/tables.d.ts +0 -27
  74. package/dist/tables.js +0 -322
  75. package/dist/tasks.d.ts +0 -3
  76. package/dist/tasks.js +0 -51
  77. package/dist/text-ranges.d.ts +0 -29
  78. package/dist/text-ranges.js +0 -256
  79. package/dist/types.js +0 -8
  80. package/dist/wiki-view.d.ts +0 -21
  81. package/dist/wiki-view.js +0 -78
package/dist/editor.js DELETED
@@ -1,2441 +0,0 @@
1
- import { Editor, commandsCtx, defaultValueCtx, editorViewCtx, parserCtx, editorViewOptionsCtx, remarkCtx, remarkStringifyOptionsCtx, rootCtx, } from '@milkdown/kit/core';
2
- import { visibleClipboard, portableClipboard, clipboardText, clipboardContent, } from './clipboard.js';
3
- import { clipboard } from '@milkdown/kit/plugin/clipboard';
4
- import { history, historyKeymap } from '@milkdown/kit/plugin/history';
5
- import { closeHistory, undo as undoHistory, redo as redoHistory, } from '@milkdown/kit/prose/history';
6
- import { cursor } from '@milkdown/kit/plugin/cursor';
7
- import { blockquoteKeymap, blockquoteSchema, bulletListKeymap, codeBlockKeymap, createCodeBlockCommand, emphasisKeymap, headingKeymap, inlineCodeKeymap, inlineCodeSchema, linkSchema, listItemKeymap, orderedListKeymap, paragraphKeymap, strongKeymap, toggleEmphasisCommand, toggleInlineCodeCommand, toggleLinkCommand, toggleStrongCommand, turnIntoTextCommand, wrapInBulletListCommand, wrapInHeadingCommand, wrapInOrderedListCommand, } from '@milkdown/kit/preset/commonmark';
8
- import { strikethroughKeymap, tableKeymap, toggleStrikethroughCommand, } from '@milkdown/kit/preset/gfm';
9
- import { NodeRange, Fragment, Slice, } from '@milkdown/kit/prose/model';
10
- import { keydownHandler, keymap } from '@milkdown/kit/prose/keymap';
11
- import { goToNextCell } from '@milkdown/kit/prose/tables';
12
- import { liftListItem } from '@milkdown/kit/prose/schema-list';
13
- import { findWrapping, liftTarget } from '@milkdown/kit/prose/transform';
14
- import { AllSelection, NodeSelection, Plugin, PluginKey, Selection, TextSelection, } from '@milkdown/kit/prose/state';
15
- import { $prose, callCommand, replaceAll, } from '@milkdown/kit/utils';
16
- import { codeCopyPlugin, placeholderPlugin } from './decorations.js';
17
- import { labelsCtx, defaultLabels } from './labels.js';
18
- import { createDialect, stringifyOptions } from './dialect.js';
19
- import { Preservation } from './preserve.js';
20
- import { sourceAttribute, cleanSourceDoc, editedPlainSource, sourceSelection, rawOffset, normalizedOffset, } from './source.js';
21
- import { normaliseLabel, referenceDefinitions, footnoteDefinitions, } from './references.js';
22
- import { selectionMarkdown, selectionContent, referenceMetadata, withReferenceMetadata, } from './reference-clipboard.js';
23
- import { imageView } from './images.js';
24
- import { isImage, isImageBlock } from './image-blocks.js';
25
- import { GapCursor } from '@milkdown/kit/prose/gapcursor';
26
- import { wikiLinkView } from './wiki-view.js';
27
- import { withFileSignal } from './files.js';
28
- import { tablePlugins, tableCommand, tableCommands, tableAvailability, tableContext, configureTableMovement, } from './tables.js';
29
- import { InkKitError, } from './types.js';
30
- import { readableProjection, projectionPosition, projectionOffset, validateOffsets, formattedRects, literalRangesRects, literalVisibleRanges, intersectsViewport, textRect, } from './text-ranges.js';
31
- import { highlightKeymap, toggleHighlightCommand } from './inline-highlight.js';
32
- import { commentSelectionContent, setCommentVisibility } from './comments.js';
33
- import { PasteController } from './paste.js';
34
- import { selectionPlugin } from './selection.js';
35
- import { search, SearchQuery, setSearchState } from 'prosemirror-search';
36
- import { findFormatted, findLiteral, replaceFormatted, replaceLiteral, replaceFormattedRange, } from './search.js';
37
- import { collectFormattedHeadings, collectSourceHeadings, navigateFormattedHeading, sourceHeadingPosition, } from './outline.js';
38
- import { taskListPlugin, toggleTaskList } from './tasks.js';
39
- import { isMermaid, mermaidPreview } from './mermaid.js';
40
- import { hasAuthoredImagesInLiterals, printableMarkdown, printableText, } from './print.js';
41
- const shortcutCommands = {
42
- heading1: ['heading', 1],
43
- heading2: ['heading', 2],
44
- heading3: ['heading', 3],
45
- heading4: ['heading', 4],
46
- heading5: ['heading', 5],
47
- heading6: ['heading', 6],
48
- paragraph: ['paragraph'],
49
- bold: ['bold'],
50
- italic: ['italic'],
51
- strikethrough: ['strikethrough'],
52
- highlight: ['highlight'],
53
- code: ['code'],
54
- codeBlock: ['codeBlock'],
55
- quote: ['quote'],
56
- bulletList: ['bulletList'],
57
- orderedList: ['orderedList'],
58
- taskList: ['taskList'],
59
- };
60
- const markNames = {
61
- strong: 'bold',
62
- emphasis: 'italic',
63
- strike_through: 'strikethrough',
64
- highlight: 'highlight',
65
- inlineCode: 'code',
66
- link: 'link',
67
- };
68
- // Select All resolves at the document level, where no block can be read.
69
- function textBounds(state) {
70
- const { selection, doc } = state;
71
- if (selection instanceof AllSelection)
72
- return TextSelection.between(doc.resolve(0), doc.resolve(doc.content.size));
73
- return selection;
74
- }
75
- function caretState(state) {
76
- const { empty } = state.selection;
77
- const { $from, $to } = textBounds(state);
78
- const active = new Set();
79
- const marks = empty ? (state.storedMarks ?? $from.marks()) : [];
80
- for (const mark of marks) {
81
- const name = markNames[mark.type.name];
82
- if (name)
83
- active.add(name);
84
- }
85
- if (!empty) {
86
- for (const [name, mark] of Object.entries(markNames)) {
87
- const type = state.schema.marks[name];
88
- if (type && state.doc.rangeHasMark($from.pos, $to.pos, type))
89
- active.add(mark);
90
- }
91
- }
92
- return {
93
- marks: [...active],
94
- block: blockAt($from.parent, $from),
95
- quoted: quoted($from, $to),
96
- };
97
- }
98
- // True only when one quote holds the whole selection, so the state matches what the command can lift.
99
- function quoted($from, $to) {
100
- return ($from.blockRange($to, (node) => node.type.name === 'blockquote') != null);
101
- }
102
- function blockAt(parent, $from) {
103
- for (let depth = $from.depth; depth > 0; depth--) {
104
- const node = $from.node(depth);
105
- switch (node.type.name) {
106
- case 'heading':
107
- return { type: 'heading', level: node.attrs.level };
108
- case 'code_block':
109
- return { type: 'codeBlock' };
110
- case 'list_item':
111
- return node.attrs.checked == null
112
- ? {
113
- type: $from.node(depth - 1).type.name === 'ordered_list'
114
- ? 'orderedList'
115
- : 'bulletList',
116
- }
117
- : { type: 'taskList' };
118
- }
119
- }
120
- if (parent.type.name === 'heading')
121
- return { type: 'heading', level: parent.attrs.level };
122
- return { type: 'paragraph' };
123
- }
124
- // Backspace at the start of a quote's first block is left alone by the preset;
125
- // leaving the quote is what a Backspace there means.
126
- const quoteBackspace = $prose(() => keymap({
127
- Backspace: (state, dispatch) => {
128
- const { $from, empty } = state.selection;
129
- if (!empty || $from.parentOffset > 0 || $from.depth < 2)
130
- return false;
131
- if ($from.node($from.depth - 1).type.name !== 'blockquote' ||
132
- $from.index($from.depth - 1) > 0)
133
- return false;
134
- const range = $from.blockRange($from, (node) => node.type.name === 'blockquote');
135
- const target = range && liftTarget(range);
136
- if (!range || target == null)
137
- return false;
138
- dispatch?.(state.tr.lift(range, target).scrollIntoView());
139
- return true;
140
- },
141
- }));
142
- // The preset lifts only an item's first line, which cannot leave on its own while the item holds
143
- // more, such as a nested list: at the top of a document Backspace did nothing, and below another block
144
- // it merged the line upward and left an empty bullet. The whole item is lifted instead, and a nested
145
- // list it held joins the items that followed, since both now sit at one level.
146
- const listItemBackspace = $prose(() => keymap({
147
- Backspace: (state, dispatch) => {
148
- const { $from, empty } = state.selection;
149
- if (!empty || $from.parentOffset > 0 || $from.depth < 3)
150
- return false;
151
- const item = $from.node(-1);
152
- if (item.type.name !== 'list_item' ||
153
- item.childCount < 2 ||
154
- $from.index(-1) > 0 ||
155
- $from.index(-2) > 0)
156
- return false;
157
- const { doc } = state;
158
- const range = new NodeRange(doc.resolve($from.start(-1)), doc.resolve($from.end(-1)), $from.depth - 1);
159
- const target = liftTarget(range);
160
- if (target == null)
161
- return false;
162
- const tr = state.tr.lift(range, target);
163
- const $line = tr.doc.resolve(tr.mapping.map($from.pos));
164
- joinLists(tr, $line.start(-1), $line.index(-1) + item.childCount);
165
- dispatch?.(tr.scrollIntoView());
166
- return true;
167
- },
168
- }));
169
- // Lifting an item out of a list that no item holds, the preset leaves a nested list the item held
170
- // beside the items that followed. Out of a nested list it joins the two itself.
171
- const liftItem = (state, dispatch) => {
172
- const itemType = state.schema.nodes.list_item;
173
- const { $from, $to } = state.selection;
174
- const range = $from.blockRange($to, (node) => node.firstChild?.type === itemType);
175
- const lift = liftListItem(itemType);
176
- if (!range || !dispatch || $from.node(range.depth - 1).type === itemType)
177
- return lift(state, dispatch);
178
- let lifted;
179
- if (!lift(state, (tr) => (lifted = tr)) || !lifted)
180
- return false;
181
- let count = 0;
182
- for (let index = range.startIndex; index < range.endIndex; index++)
183
- count += range.parent.child(index).childCount;
184
- const start = range.$from.start(range.depth - 1);
185
- const first = range.$from.index(range.depth - 1) + (range.startIndex > 0 ? 1 : 0);
186
- joinLists(lifted, start, first + count);
187
- joinLists(lifted, start, first);
188
- dispatch(lifted);
189
- return true;
190
- };
191
- const listItemShiftTab = $prose(() => keymap({ 'Shift-Tab': liftItem }));
192
- // A control chord that nothing handles reaches the page as its ASCII control character
193
- // (Control-N as U+000E), which the web view would insert as text.
194
- const controlCharacters = /^[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]+$/;
195
- const dropControlCharacters = $prose(() => new Plugin({
196
- key: new PluginKey('dropControlCharacters'),
197
- props: {
198
- handleTextInput: (_view, _from, _to, text) => controlCharacters.test(text),
199
- },
200
- }));
201
- // The listener plugin reports selection changes from inside state.apply, before
202
- // the view holds the new state, so the caret state is read from the view instead.
203
- function caretStatePlugin(events, active = () => true) {
204
- return $prose(() => new Plugin({
205
- key: new PluginKey('caretState'),
206
- view: () => ({
207
- update(view, previous) {
208
- if (!active())
209
- return;
210
- const { state } = view;
211
- if (state.selection.eq(previous.selection) &&
212
- state.doc.eq(previous.doc) &&
213
- state.storedMarks === previous.storedMarks)
214
- return;
215
- events.stateChanged(caretState(state));
216
- },
217
- }),
218
- }));
219
- }
220
- function outermostListDepth($pos) {
221
- for (let depth = 1; depth <= $pos.depth; depth++) {
222
- if (isList($pos.node(depth)))
223
- return depth;
224
- }
225
- return null;
226
- }
227
- function innermostListDepth($pos) {
228
- for (let depth = $pos.depth; depth >= 1; depth--) {
229
- if (isList($pos.node(depth)))
230
- return depth;
231
- }
232
- return null;
233
- }
234
- function isList(node) {
235
- return node.type.name === 'bullet_list' || node.type.name === 'ordered_list';
236
- }
237
- // Two lists of one kind side by side are written with a changed marker to keep them apart, and the
238
- // document shows a gap between them, so an edit that leaves them so joins them.
239
- function joinLists(tr, start, index) {
240
- const $start = tr.doc.resolve(start);
241
- const before = $start.parent.maybeChild(index - 1);
242
- const after = $start.parent.maybeChild(index);
243
- if (before && after && isList(before) && after.type === before.type)
244
- tr.join($start.posAtIndex(index));
245
- }
246
- let editorSequence = 0;
247
- export class InkKitEditor {
248
- events;
249
- options;
250
- editor;
251
- lastMarkdown = '';
252
- documentId = '';
253
- formatType = 'md';
254
- revision = 0;
255
- documentEpoch = 0;
256
- operationSequence = 0;
257
- exportControllers = new Set();
258
- plainComposing = false;
259
- plainSearch = '';
260
- literalMatch;
261
- beforePlainInput;
262
- mode = 'formatted';
263
- sourcePreservations = new WeakMap();
264
- sourceTexts = new WeakMap();
265
- outlineEpoch = 0;
266
- ready = false;
267
- destroyed = false;
268
- preservation;
269
- pasteController;
270
- plain;
271
- root;
272
- ownsRootClass = false;
273
- clickHandler;
274
- footnoteKeyHandler;
275
- copyHandler;
276
- originalSource = '';
277
- generation = 0;
278
- reportingSuppressed = false;
279
- loading = false;
280
- textIdentity = ++editorSequence;
281
- textScope;
282
- viewportContainer;
283
- viewportInsets = {
284
- top: 0,
285
- right: 0,
286
- bottom: 0,
287
- left: 0,
288
- };
289
- writable = true;
290
- inputPreferences = {};
291
- policyEpoch = 0;
292
- literalKeys = () => false;
293
- configuredKeys = {};
294
- readOnlyKeys = () => false;
295
- changePlugin = $prose(() => new Plugin({
296
- key: new PluginKey('documentChanges'),
297
- view: () => ({
298
- update: (view, previous) => {
299
- if (this.loading || view.state.doc.eq(previous.doc))
300
- return;
301
- this.revision += 1;
302
- this.cancelExports();
303
- this.mediaPolicy();
304
- this.literalMatch = undefined;
305
- let markdown;
306
- try {
307
- markdown = this.currentText(view.state.doc);
308
- }
309
- catch (error) {
310
- this.events.error?.(error instanceof Error ? error : new Error(String(error)));
311
- return;
312
- }
313
- if (this.literalSurface) {
314
- const anchor = view.state.doc.attrs[sourceAttribute];
315
- const from = anchor?.from ?? this.plain.selectionStart;
316
- const to = anchor?.to ?? this.plain.selectionEnd;
317
- if (!this.plainComposing)
318
- this.plain.value = markdown;
319
- if (!this.plainComposing) {
320
- this.plain.setSelectionRange(Math.min(from, this.plain.value.length), Math.min(to, this.plain.value.length));
321
- this.revealLiteralSelection();
322
- }
323
- }
324
- if (markdown === this.lastMarkdown)
325
- return;
326
- this.lastMarkdown = markdown;
327
- this.events.changed(markdown, this.generation);
328
- },
329
- }),
330
- }));
331
- keys = () => false;
332
- constructor(events, options) {
333
- this.events = events;
334
- this.options = options;
335
- this.writable = options.editable ?? true;
336
- this.inputPreferences = { ...options.textInput };
337
- }
338
- context() {
339
- return {
340
- documentId: this.documentId,
341
- generation: this.generation,
342
- operationId: `${this.documentEpoch}:${this.generation}:${++this.operationSequence}`,
343
- };
344
- }
345
- cancelExports() {
346
- for (const controller of this.exportControllers)
347
- controller.abort();
348
- this.exportControllers.clear();
349
- }
350
- async exportContent(snapshot, operation) {
351
- const epoch = this.documentEpoch;
352
- const mode = this.editingMode;
353
- const controller = new AbortController();
354
- this.exportControllers.add(controller);
355
- try {
356
- return await withFileSignal(operation(controller.signal), controller.signal);
357
- }
358
- finally {
359
- this.exportControllers.delete(controller);
360
- this.assertCurrent(snapshot.generation);
361
- if (controller.signal.aborted ||
362
- epoch !== this.documentEpoch ||
363
- snapshot.documentId !== this.documentId ||
364
- snapshot.revision !== this.revision ||
365
- mode !== this.editingMode)
366
- throw new InkKitError('stale-document', 'Document changed while preparing output');
367
- }
368
- }
369
- mediaPolicy() {
370
- if (this.editor && this.root)
371
- this.editor.ctx
372
- .get(editorViewCtx)
373
- .dom.dispatchEvent(new Event('inkkit-media-policy'));
374
- }
375
- assertAlive() {
376
- if (this.destroyed)
377
- throw new InkKitError('destroyed', 'The editor has been destroyed');
378
- }
379
- assertCurrent(generation = this.generation) {
380
- this.assertAlive();
381
- if (!this.ready)
382
- throw new InkKitError('not-ready', 'No document has been loaded');
383
- if (generation !== this.generation)
384
- throw new InkKitError('stale-document', 'Document changed');
385
- if (this.literalSurface
386
- ? this.plainComposing
387
- : this.editor.ctx.get(editorViewCtx).composing)
388
- throw new InkKitError('composition', 'Text composition is in progress');
389
- if (this.pasteController.pending)
390
- throw new InkKitError('operation-pending', 'An image import is in progress');
391
- }
392
- assertMutation(generation = this.generation) {
393
- this.assertCurrent(generation);
394
- if (!this.writable)
395
- throw new InkKitError('read-only', 'The editor is read-only');
396
- }
397
- get composing() {
398
- return this.literalSurface
399
- ? this.plainComposing
400
- : this.editor.ctx.get(editorViewCtx).composing;
401
- }
402
- get editable() {
403
- this.assertAlive();
404
- return this.writable;
405
- }
406
- setEditable(editable) {
407
- this.assertAlive();
408
- if (this.composing)
409
- throw new InkKitError('composition', 'Text composition is in progress');
410
- if (this.writable === editable)
411
- return;
412
- const suppressed = this.reportingSuppressed;
413
- this.reportingSuppressed = true;
414
- try {
415
- this.writable = editable;
416
- this.policyEpoch += 1;
417
- if (!editable)
418
- this.pasteController.cancelPending();
419
- this.editor.ctx
420
- .get(editorViewCtx)
421
- .dom.dispatchEvent(new Event('inkkit-cancel-resize'));
422
- this.applyInputPolicy();
423
- this.mediaPolicy();
424
- }
425
- finally {
426
- this.reportingSuppressed = suppressed;
427
- }
428
- this.publishCommandState();
429
- }
430
- setTextInputPreferences(preferences) {
431
- this.assertAlive();
432
- if (this.composing)
433
- throw new InkKitError('composition', 'Text composition is in progress');
434
- const suppressed = this.reportingSuppressed;
435
- this.reportingSuppressed = true;
436
- try {
437
- this.inputPreferences = { ...preferences };
438
- this.applyInputPolicy();
439
- }
440
- finally {
441
- this.reportingSuppressed = suppressed;
442
- }
443
- this.publishCommandState();
444
- }
445
- applyInputPolicy() {
446
- const view = this.editor.ctx.get(editorViewCtx);
447
- const attributes = typeof view.props.attributes === 'function'
448
- ? view.props.attributes(view.state)
449
- : view.props.attributes;
450
- view.setProps({
451
- editable: () => this.writable && !this.literalSurface,
452
- attributes: {
453
- ...attributes,
454
- role: 'textbox',
455
- 'aria-multiline': 'true',
456
- 'aria-label': this.options.labels?.formattedEditor ?? defaultLabels.formattedEditor,
457
- },
458
- });
459
- this.plain.readOnly = !this.writable;
460
- for (const surface of [view.dom, this.plain]) {
461
- for (const name of [
462
- 'spellcheck',
463
- 'autocorrect',
464
- 'autocapitalize',
465
- ]) {
466
- const value = this.inputPreferences[name];
467
- if (value == null)
468
- surface.removeAttribute(name);
469
- else
470
- surface.setAttribute(name, name === 'autocorrect' ? (value ? 'on' : 'off') : String(value));
471
- }
472
- }
473
- }
474
- publishCommandState() {
475
- if (!this.ready ||
476
- this.loading ||
477
- this.reportingSuppressed ||
478
- this.destroyed)
479
- return;
480
- this.events.commandStateChanged?.(this.commandState());
481
- }
482
- commandState(expectedGeneration = this.generation) {
483
- this.assertAlive();
484
- if (!this.ready)
485
- throw new InkKitError('not-ready', 'No document has been loaded');
486
- if (expectedGeneration !== this.generation)
487
- throw new InkKitError('stale-document', 'Document changed');
488
- const view = this.editor.ctx.get(editorViewCtx);
489
- const { state } = view;
490
- const mutable = this.writable && !this.composing && !this.pasteController.pending;
491
- const formatted = mutable && !this.literalSurface;
492
- const manager = this.editor.ctx.get(commandsCtx);
493
- const check = (key, payload) => formatted && manager.get(key)(payload)(state);
494
- const format = {
495
- heading: check(wrapInHeadingCommand.key, 1) || check(turnIntoTextCommand.key),
496
- paragraph: check(turnIntoTextCommand.key),
497
- bold: check(toggleStrongCommand.key),
498
- italic: check(toggleEmphasisCommand.key),
499
- strikethrough: check(toggleStrikethroughCommand.key),
500
- highlight: check(toggleHighlightCommand.key),
501
- code: formatted &&
502
- (state.selection.empty
503
- ? state.selection.$from.parent.inlineContent &&
504
- state.selection.$from.parent.type.allowsMarkType(inlineCodeSchema.type(this.editor.ctx))
505
- : check(toggleInlineCodeCommand.key)),
506
- codeBlock: check(createCodeBlockCommand.key, '') || check(turnIntoTextCommand.key),
507
- quote: formatted && this.canQuote(state),
508
- bulletList: formatted &&
509
- (innermostListDepth(textBounds(state).$from) != null ||
510
- check(wrapInBulletListCommand.key)),
511
- orderedList: formatted &&
512
- (innermostListDepth(textBounds(state).$from) != null ||
513
- check(wrapInOrderedListCommand.key)),
514
- taskList: formatted &&
515
- (innermostListDepth(textBounds(state).$from) != null ||
516
- check(wrapInBulletListCommand.key)),
517
- link: check(toggleLinkCommand.key, {}),
518
- };
519
- const tables = formatted
520
- ? tableAvailability(this.editor.ctx)
521
- : Object.fromEntries(tableCommands.map((name) => [name, false]));
522
- return {
523
- documentId: this.documentId,
524
- generation: this.generation,
525
- revision: this.revision,
526
- format: this.formatType,
527
- mode: this.editingMode,
528
- editable: this.writable,
529
- composing: this.composing,
530
- pending: this.pasteController.pending,
531
- caret: this.literalSurface
532
- ? { marks: [], block: { type: 'paragraph' }, quoted: false }
533
- : caretState(state),
534
- table: this.literalSurface ? undefined : tableContext(state),
535
- commands: {
536
- undo: mutable && undoHistory(state),
537
- redo: mutable && redoHistory(state),
538
- insertText: mutable,
539
- paste: mutable,
540
- replace: mutable,
541
- replaceSource: mutable,
542
- insertImages: formatted && !!this.options.images,
543
- insertPaths: mutable,
544
- insertFootnote: formatted,
545
- editReferenceDefinition: formatted && referenceDefinitions(state.doc).size > 0,
546
- format,
547
- table: tables,
548
- },
549
- };
550
- }
551
- canQuote(state) {
552
- const { $from, $to } = textBounds(state);
553
- if (quoted($from, $to)) {
554
- const range = $from.blockRange($to, (node) => node.type.name === 'blockquote');
555
- return !!range && liftTarget(range) != null;
556
- }
557
- const type = blockquoteSchema.type(this.editor.ctx);
558
- let range = $from.blockRange($to);
559
- if (range && findWrapping(range, type))
560
- return true;
561
- const depth = outermostListDepth($from);
562
- if (depth == null)
563
- return false;
564
- range = new NodeRange(state.doc.resolve($from.before(depth)), state.doc.resolve($from.after(depth)), depth - 1);
565
- return !!findWrapping(range, type);
566
- }
567
- policyPlugin = $prose(() => new Plugin({
568
- key: new PluginKey('inkkitInputPolicy'),
569
- filterTransaction: (tr) => this.loading ||
570
- this.writable ||
571
- (!tr.docChanged && !tr.storedMarksSet),
572
- props: {
573
- editable: () => this.writable && !this.literalSurface,
574
- handleDOMEvents: {
575
- compositionstart: () => {
576
- queueMicrotask(() => this.publishCommandState());
577
- return false;
578
- },
579
- compositionend: () => {
580
- queueMicrotask(() => this.publishCommandState());
581
- return false;
582
- },
583
- beforeinput: (_view, event) => {
584
- if (this.writable)
585
- return false;
586
- event.preventDefault();
587
- return true;
588
- },
589
- drop: (_view, event) => {
590
- if (this.writable)
591
- return false;
592
- event.preventDefault();
593
- return true;
594
- },
595
- },
596
- },
597
- view: () => ({ update: () => this.publishCommandState() }),
598
- }));
599
- get literalSurface() {
600
- return this.formatType === 'txt' || this.mode === 'source';
601
- }
602
- get editingMode() {
603
- this.assertAlive();
604
- return this.literalSurface ? 'source' : 'formatted';
605
- }
606
- currentText(doc = this.editor.ctx.get(editorViewCtx).state.doc) {
607
- const cached = this.sourceTexts.get(doc);
608
- if (cached !== undefined)
609
- return cached;
610
- const provenance = doc.attrs[sourceAttribute];
611
- if (this.formatType === 'txt')
612
- return provenance?.text ?? this.originalSource;
613
- let preservation = this.preservation;
614
- if (provenance) {
615
- preservation = this.sourcePreservations.get(provenance);
616
- if (!preservation) {
617
- preservation = new Preservation(this.editor.ctx, provenance.text);
618
- this.sourcePreservations.set(provenance, preservation);
619
- }
620
- }
621
- const text = preservation.serialize(cleanSourceDoc(doc));
622
- this.sourceTexts.set(doc, text);
623
- return text;
624
- }
625
- setEditingMode(mode, expectedGeneration) {
626
- this.assertCurrent(expectedGeneration);
627
- if (mode !== 'source' && mode !== 'formatted')
628
- throw new RangeError('Editing mode must be source or formatted');
629
- if (this.formatType === 'txt' || this.mode === mode)
630
- return false;
631
- const view = this.editor.ctx.get(editorViewCtx);
632
- const focused = this.literalSurface
633
- ? this.plain.ownerDocument.activeElement === this.plain
634
- : view.hasFocus();
635
- const container = this.viewportContainer ?? this.root;
636
- const top = container.scrollTop, left = container.scrollLeft;
637
- const text = this.snapshot().text;
638
- this.mode = mode;
639
- this.cancelExports();
640
- this.outlineEpoch += 1;
641
- this.plainSearch = '';
642
- this.literalMatch = undefined;
643
- if (mode === 'source') {
644
- this.plain.value = text;
645
- this.plain.setSelectionRange(this.plain.value.length, this.plain.value.length);
646
- }
647
- const suppressed = this.reportingSuppressed;
648
- this.reportingSuppressed = true;
649
- try {
650
- this.updateSurface();
651
- }
652
- finally {
653
- this.reportingSuppressed = suppressed;
654
- }
655
- if (focused) {
656
- if (this.literalSurface)
657
- this.plain.focus({ preventScroll: true });
658
- else
659
- view.focus();
660
- }
661
- container.scrollTop = top;
662
- container.scrollLeft = left;
663
- this.events.stateChanged(this.literalSurface
664
- ? { marks: [], block: { type: 'paragraph' }, quoted: false }
665
- : caretState(this.editor.ctx.get(editorViewCtx).state));
666
- this.publishCommandState();
667
- return true;
668
- }
669
- updateSurface() {
670
- this.plain.hidden = !this.literalSurface;
671
- this.applyInputPolicy();
672
- this.plain.setAttribute('aria-label', this.formatType === 'txt'
673
- ? (this.options.labels?.plainTextEditor ??
674
- defaultLabels.plainTextEditor)
675
- : (this.options.labels?.sourceEditor ?? defaultLabels.sourceEditor));
676
- this.editor.ctx.get(editorViewCtx).dom.parentElement.hidden =
677
- this.literalSurface;
678
- this.mediaPolicy();
679
- }
680
- replaceSource(text, expectedGeneration) {
681
- this.assertMutation(expectedGeneration);
682
- return this.applySource(text, true);
683
- }
684
- applySource(text, isolated, from, to) {
685
- if (!this.writable)
686
- throw new InkKitError('read-only', 'The editor is read-only');
687
- if (typeof text !== 'string')
688
- throw new TypeError('Source must be a string');
689
- if (text === this.currentText())
690
- return false;
691
- const view = this.editor.ctx.get(editorViewCtx);
692
- const normalizedLength = text.replace(/\r\n?/g, '\n').length;
693
- const provenance = Object.freeze({
694
- text,
695
- format: this.formatType,
696
- from: from ?? normalizedLength,
697
- to: to ?? from ?? normalizedLength,
698
- });
699
- let parsed;
700
- try {
701
- parsed =
702
- this.formatType === 'txt'
703
- ? view.state.schema.topNodeType.create(null, view.state.schema.nodes.paragraph.create(null, text ? view.state.schema.text(text) : undefined))
704
- : this.editor.ctx.get(parserCtx)(text);
705
- if (this.formatType === 'md')
706
- this.sourcePreservations.set(provenance, new Preservation(this.editor.ctx, text));
707
- }
708
- catch (error) {
709
- throw new InkKitError('preservation', error instanceof Error ? error.message : 'Cannot read Markdown source');
710
- }
711
- if (this.literalSurface) {
712
- const previous = Object.freeze({
713
- text: this.currentText(),
714
- format: this.formatType,
715
- from: this.beforePlainInput?.from ?? this.plain.selectionStart,
716
- to: this.beforePlainInput?.to ?? this.plain.selectionEnd,
717
- });
718
- // History must retain the source caret even when the formatted document has no corresponding position.
719
- this.loading = true;
720
- try {
721
- view.dispatch(view.state.tr
722
- .setDocAttribute(sourceAttribute, previous)
723
- .setMeta('addToHistory', false));
724
- }
725
- finally {
726
- this.loading = false;
727
- }
728
- }
729
- let tr = view.state.tr
730
- .replaceWith(0, view.state.doc.content.size, parsed.content)
731
- .setDocAttribute(sourceAttribute, provenance);
732
- tr.setSelection(Selection.atEnd(tr.doc));
733
- if (isolated)
734
- tr = closeHistory(tr);
735
- view.dispatch(tr);
736
- if (isolated)
737
- view.dispatch(closeHistory(view.state.tr).setMeta('addToHistory', false));
738
- return true;
739
- }
740
- undo(expectedGeneration) {
741
- this.assertMutation(expectedGeneration);
742
- const view = this.editor.ctx.get(editorViewCtx);
743
- return undoHistory(view.state, view.dispatch);
744
- }
745
- redo(expectedGeneration) {
746
- this.assertMutation(expectedGeneration);
747
- const view = this.editor.ctx.get(editorViewCtx);
748
- return redoHistory(view.state, view.dispatch);
749
- }
750
- replaceLiteralSelection(text, isolated = true, retainEndings = false) {
751
- const value = this.plain.value;
752
- const start = this.plain.selectionStart, end = this.plain.selectionEnd;
753
- const next = value.slice(0, start) + text.replace(/\r\n?/g, '\n') + value.slice(end);
754
- const caret = start + text.replace(/\r\n?/g, '\n').length;
755
- const previous = this.currentText();
756
- const source = retainEndings
757
- ? previous.slice(0, rawOffset(previous, start)) +
758
- text +
759
- previous.slice(rawOffset(previous, end))
760
- : editedPlainSource(previous, next);
761
- this.applySource(source, isolated, caret, caret);
762
- }
763
- snapshot(expectedGeneration) {
764
- this.assertCurrent(expectedGeneration);
765
- let text;
766
- try {
767
- text = this.currentText();
768
- }
769
- catch (error) {
770
- throw new InkKitError('preservation', error instanceof Error ? error.message : 'Cannot preserve Markdown');
771
- }
772
- return {
773
- documentId: this.documentId,
774
- generation: this.generation,
775
- revision: this.revision,
776
- format: this.formatType,
777
- text,
778
- dirty: text !== this.originalSource,
779
- };
780
- }
781
- async printableSnapshot(expectedGeneration) {
782
- const snapshot = this.snapshot(expectedGeneration);
783
- const state = this.editor.ctx.get(editorViewCtx).state;
784
- const output = await this.exportContent(snapshot, async (signal) => {
785
- if (snapshot.format === 'md' &&
786
- !this.options.images &&
787
- !this.options.files &&
788
- hasAuthoredImagesInLiterals(state.doc.content, (source) => this.editor.ctx.get(remarkCtx).parse(source)))
789
- throw new InkKitError('image-unavailable', 'No image adapter is configured for the authored images.');
790
- return snapshot.format === 'txt'
791
- ? printableText(snapshot.text)
792
- : printableMarkdown(state.doc.content, state.schema, this.options.images, this.context(), this.options.files, signal);
793
- });
794
- return Object.freeze({
795
- documentId: snapshot.documentId,
796
- generation: snapshot.generation,
797
- revision: snapshot.revision,
798
- format: snapshot.format,
799
- html: output.html,
800
- styles: output.styles,
801
- assets: Object.freeze(output.assets.map((asset) => Object.freeze({
802
- ...asset,
803
- bytes: new Uint8Array(asset.bytes),
804
- }))),
805
- warnings: Object.freeze(output.warnings.map((warning) => Object.freeze({ ...warning }))),
806
- });
807
- }
808
- async clipboardSnapshot(all = true) {
809
- const snapshot = this.snapshot();
810
- const epoch = this.documentEpoch;
811
- if (this.formatType === 'txt') {
812
- const text = all
813
- ? snapshot.text
814
- : snapshot.text.slice(rawOffset(snapshot.text, this.plain.selectionStart), rawOffset(snapshot.text, this.plain.selectionEnd));
815
- const pre = document.createElement('pre');
816
- pre.textContent = text;
817
- return {
818
- text,
819
- html: pre.outerHTML,
820
- markdown: text,
821
- images: [],
822
- diagrams: [],
823
- };
824
- }
825
- const { doc, schema, selection } = this.editor.ctx.get(editorViewCtx).state;
826
- const selectedSource = !all && this.mode === 'source'
827
- ? sourceSelection(snapshot.text, this.plain.selectionStart, this.plain.selectionEnd)
828
- : undefined;
829
- const selectedDoc = selectedSource
830
- ? this.editor.ctx.get(parserCtx)(selectedSource.shareable)
831
- : undefined;
832
- const completeDiagrams = new Set();
833
- (selectedDoc ?? doc).descendants((node, pos) => {
834
- if (isMermaid(node) &&
835
- (selectedDoc
836
- ? Boolean(node.attrs.authoredFence?.close)
837
- : all ||
838
- (selection.from <= pos + 1 &&
839
- selection.to >= pos + node.nodeSize - 1))) {
840
- completeDiagrams.add(node.textContent.replace(/\r\n?/g, '\n'));
841
- }
842
- });
843
- const content = selectedDoc
844
- ? selectedDoc.content
845
- : all
846
- ? doc.content
847
- : commentSelectionContent(doc, selection.from, selection.to, selection.content().content);
848
- const valid = content.firstChild?.isInline
849
- ? Fragment.from(schema.nodes.paragraph.create(null, content))
850
- : content;
851
- let markdown;
852
- try {
853
- markdown = selectedSource
854
- ? selectedSource.markdown
855
- : all
856
- ? snapshot.text
857
- : completeDiagrams.size
858
- ? new Preservation(this.editor.ctx, snapshot.text).serialize(doc.type.create(null, selectionContent(doc, valid)))
859
- : selectionMarkdown(this.editor.ctx, doc, valid);
860
- }
861
- catch (error) {
862
- throw new InkKitError('preservation', error instanceof Error
863
- ? error.message
864
- : 'Cannot copy the selected Markdown');
865
- }
866
- const result = await this.exportContent(snapshot, (signal) => portableClipboard(all || selectedDoc ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context(), completeDiagrams, this.options.files, signal));
867
- const metadata = referenceMetadata(this.editor.ctx, selectedDoc ?? doc, content);
868
- if (metadata != null)
869
- result.html = withReferenceMetadata(result.html, metadata);
870
- this.assertCurrent(snapshot.generation);
871
- if (snapshot.documentId !== this.documentId ||
872
- epoch !== this.documentEpoch ||
873
- snapshot.revision !== this.revision)
874
- throw new InkKitError('stale-document', 'Document changed');
875
- if (result.images.some((image) => image.error))
876
- this.events.error?.(new InkKitError('image-unavailable', 'Some copied images were unavailable; their descriptions were retained'));
877
- if (result.diagrams?.some((diagram) => diagram.error))
878
- this.events.error?.(new InkKitError('diagram-unavailable', 'Some diagrams could not be exported; their source was retained'));
879
- return result;
880
- }
881
- async paste(input) {
882
- this.assertMutation();
883
- if (this.literalSurface || input.plainText) {
884
- this.pasteAsPlainText(this.mode === 'source' && this.formatType === 'md'
885
- ? (input.markdown ?? input.text)
886
- : input.text);
887
- return;
888
- }
889
- await this.pasteController.paste(input);
890
- }
891
- setCommentsVisible(visible) {
892
- this.assertAlive();
893
- setCommentVisibility(this.editor.ctx.get(editorViewCtx), visible);
894
- }
895
- pasteAsPlainText(text) {
896
- this.assertMutation();
897
- if (this.literalSurface) {
898
- this.replaceLiteralSelection(text, true, this.formatType === 'md');
899
- }
900
- else {
901
- const view = this.editor.ctx.get(editorViewCtx);
902
- if (view.state.selection.$from.parent.type.spec.code) {
903
- view.dispatch(view.state.tr
904
- .insertText(text.replace(/\r\n?/g, '\n'))
905
- .scrollIntoView());
906
- }
907
- else {
908
- const { schema } = view.state;
909
- const marks = view.state.storedMarks ??
910
- (view.state.selection.empty
911
- ? view.state.selection.$from.marks()
912
- : view.state.selection.$from.marksAcross(view.state.selection.$to)) ??
913
- [];
914
- const children = [];
915
- const lines = text.replace(/\r\n?/g, '\n').split('\n');
916
- lines.forEach((line, index) => {
917
- const finalBreak = index === lines.length - 1 && line === '';
918
- if (index)
919
- children.push(schema.nodes.hardbreak.create({
920
- isHTML: finalBreak,
921
- }, null,
922
- // A terminal HTML spacer must remain outside emphasis to reopen as a break.
923
- finalBreak ? [] : marks));
924
- if (line)
925
- children.push(schema.text(line, marks));
926
- });
927
- if (!children.length) {
928
- view.dispatch(view.state.tr.deleteSelection());
929
- return;
930
- }
931
- const paragraph = schema.nodes.paragraph.create(null, children);
932
- const transaction = view.state.tr
933
- .replaceSelection(new Slice(Fragment.from(paragraph), 1, 1))
934
- .scrollIntoView();
935
- try {
936
- this.currentText(transaction.doc);
937
- }
938
- catch (error) {
939
- throw new InkKitError('preservation', error instanceof Error
940
- ? error.message
941
- : 'Cannot preserve inserted text');
942
- }
943
- view.dispatch(transaction);
944
- }
945
- }
946
- }
947
- table(command, options) {
948
- this.assertMutation();
949
- if (this.literalSurface)
950
- return false;
951
- return tableCommand(this.editor.ctx, command, options);
952
- }
953
- async destroy() {
954
- if (this.destroyed)
955
- return;
956
- this.destroyed = true;
957
- this.ready = false;
958
- this.cancelExports();
959
- this.mediaPolicy();
960
- this.pasteController.destroy();
961
- if (this.clickHandler)
962
- this.root.removeEventListener('click', this.clickHandler);
963
- if (this.footnoteKeyHandler)
964
- this.root.removeEventListener('keydown', this.footnoteKeyHandler, true);
965
- if (this.copyHandler) {
966
- this.root.removeEventListener('copy', this.copyHandler, true);
967
- this.root.removeEventListener('cut', this.copyHandler, true);
968
- }
969
- this.plain.remove();
970
- await this.editor.destroy();
971
- if (this.ownsRootClass)
972
- this.root.classList.remove('inkkit-root');
973
- }
974
- // The app's bindings, replaced whole whenever they change; the plugin stays.
975
- keymapPlugin = $prose(() => new Plugin({
976
- key: new PluginKey('appKeymap'),
977
- props: {
978
- handleKeyDown: (view, event) => {
979
- if (event.altKey && event.key === 'Enter') {
980
- return this.navigateFootnote(event.shiftKey ? 'reference' : 'definition');
981
- }
982
- return this.keys(view, event);
983
- },
984
- },
985
- }));
986
- copySourceSelection(event) {
987
- event.preventDefault();
988
- event.stopPropagation();
989
- try {
990
- const snapshot = this.snapshot();
991
- const from = this.plain.selectionStart, to = this.plain.selectionEnd;
992
- const selected = sourceSelection(snapshot.text, from, to);
993
- const parsed = this.editor.ctx.get(parserCtx)(selected.shareable);
994
- let portable = false;
995
- parsed.descendants((node) => {
996
- if (isImage(node) || isMermaid(node))
997
- portable = true;
998
- });
999
- const cut = event.type === 'cut' && this.writable;
1000
- const policyEpoch = this.policyEpoch;
1001
- const epoch = this.documentEpoch;
1002
- if (portable && this.events.clipboard) {
1003
- void this.clipboardSnapshot(false)
1004
- .then(async (content) => {
1005
- if (cut &&
1006
- (content.images.some((image) => image.error) ||
1007
- content.diagrams?.some((diagram) => diagram.error)))
1008
- throw new InkKitError('image-unavailable', 'The selected content could not be cut safely');
1009
- await this.events.clipboard(content);
1010
- if (cut) {
1011
- this.assertMutation(snapshot.generation);
1012
- if (epoch !== this.documentEpoch ||
1013
- policyEpoch !== this.policyEpoch ||
1014
- snapshot.revision !== this.revision)
1015
- throw new InkKitError('stale-document', 'Document changed before cutting');
1016
- this.plain.setSelectionRange(from, to);
1017
- this.replaceLiteralSelection('');
1018
- }
1019
- })
1020
- .catch((error) => this.events.error?.(error));
1021
- }
1022
- else {
1023
- const output = clipboardContent(parsed.content, parsed.type.schema);
1024
- if (!event.clipboardData)
1025
- return;
1026
- event.clipboardData.setData('text/plain', output.text);
1027
- event.clipboardData.setData('text/html', output.html);
1028
- if (portable) {
1029
- this.events.error?.(new InkKitError('image-unavailable', 'The host must provide a clipboard handler for portable images and diagrams'));
1030
- return;
1031
- }
1032
- if (cut)
1033
- this.replaceLiteralSelection('');
1034
- }
1035
- }
1036
- catch (error) {
1037
- this.events.error?.(error instanceof Error ? error : new Error(String(error)));
1038
- }
1039
- }
1040
- static async mount(root, events, options = {}) {
1041
- const instance = new InkKitEditor(events, options);
1042
- instance.root = root;
1043
- instance.ownsRootClass = !root.classList.contains('inkkit-root');
1044
- root.classList.add('inkkit-root');
1045
- instance.pasteController = new PasteController({
1046
- ctx: () => instance.editor.ctx,
1047
- context: () => instance.context(),
1048
- adapter: options.images,
1049
- onError: (error) => events.error?.(error instanceof Error ? error : new Error(String(error))),
1050
- literalText: () => instance.literalSurface,
1051
- editable: () => instance.writable && !instance.literalSurface,
1052
- onPendingChanged: () => instance.publishCommandState(),
1053
- });
1054
- instance.editor = await Editor.make()
1055
- .config((ctx) => {
1056
- ctx.set(rootCtx, root);
1057
- ctx.update(labelsCtx.key, () => ({
1058
- ...defaultLabels,
1059
- ...options.labels,
1060
- }));
1061
- ctx.set(defaultValueCtx, '');
1062
- ctx.set(remarkStringifyOptionsCtx, stringifyOptions);
1063
- // The preset also binds Mod-[ and Mod-] here; the app uses those for back and forward.
1064
- // Shift-Tab is bound to liftItem instead.
1065
- ctx.update(listItemKeymap.key, (keys) => ({
1066
- ...keys,
1067
- SinkListItem: { shortcuts: 'Tab' },
1068
- LiftListItem: { shortcuts: [] },
1069
- }));
1070
- // Formatting keys are the app's to set, through setKeymap; the presets' own go.
1071
- const unbind = (keymap, keep = []) => ctx.update(keymap.key, (keys) => {
1072
- const cleared = { ...keys };
1073
- for (const name of Object.keys(cleared)) {
1074
- if (!keep.includes(name))
1075
- cleared[name] = { shortcuts: [] };
1076
- }
1077
- return cleared;
1078
- });
1079
- unbind(tableKeymap, ['NextCell', 'PrevCell']);
1080
- unbind(historyKeymap);
1081
- unbind(strongKeymap);
1082
- unbind(emphasisKeymap);
1083
- unbind(inlineCodeKeymap);
1084
- unbind(strikethroughKeymap);
1085
- unbind(highlightKeymap);
1086
- unbind(headingKeymap, ['DowngradeHeading']);
1087
- unbind(paragraphKeymap);
1088
- unbind(blockquoteKeymap);
1089
- unbind(codeBlockKeymap);
1090
- unbind(bulletListKeymap);
1091
- unbind(orderedListKeymap);
1092
- ctx.update(editorViewOptionsCtx, (options) => ({
1093
- ...options,
1094
- dispatchTransaction: function (tr) {
1095
- const next = this.state.apply(tr);
1096
- if (!instance.loading) {
1097
- try {
1098
- instance.assertPreserved(this.state.doc, next.doc);
1099
- }
1100
- catch (error) {
1101
- this.updateState(this.state);
1102
- instance.events.error?.(new InkKitError('preservation', `The edit was not applied because its Markdown could not be preserved: ${error instanceof Error ? error.message : String(error)}`));
1103
- return;
1104
- }
1105
- }
1106
- this.updateState(next);
1107
- },
1108
- handleScrollToSelection: () => {
1109
- if (!instance.loading && instance.ready)
1110
- instance.revealCurrentSelection();
1111
- return true;
1112
- },
1113
- }));
1114
- })
1115
- .use(labelsCtx)
1116
- .use(caretStatePlugin(events, () => !instance.loading && !instance.literalSurface))
1117
- .use(instance.pasteController.plugin)
1118
- .use(visibleClipboard)
1119
- .use(instance.keymapPlugin)
1120
- .use(createDialect(Boolean(options.images || options.files), Boolean(options.wikiLinks), Boolean(options.files)))
1121
- .use(tablePlugins)
1122
- .use(instance.changePlugin)
1123
- .use(history)
1124
- .use(clipboard)
1125
- .use(cursor)
1126
- .use(taskListPlugin)
1127
- .use(quoteBackspace)
1128
- .use(listItemBackspace)
1129
- .use(listItemShiftTab)
1130
- .use(dropControlCharacters)
1131
- .use(codeCopyPlugin((text) => events.copy(text)))
1132
- .use(placeholderPlugin)
1133
- .use(options.rendering?.codeHighlighting === false
1134
- ? []
1135
- : (await import('./highlight')).highlightPlugin)
1136
- .use(options.rendering?.diagramPreview === false
1137
- ? []
1138
- : mermaidPreview((error) => events.error?.(error)))
1139
- .use(selectionPlugin)
1140
- .use($prose(() => search()))
1141
- .use(instance.policyPlugin)
1142
- .use(options.wikiLinks
1143
- ? wikiLinkView(options.wikiLinks, {
1144
- capture: () => {
1145
- const epoch = instance.documentEpoch;
1146
- const context = instance.context();
1147
- return {
1148
- context,
1149
- isCurrent: () => !instance.destroyed &&
1150
- epoch === instance.documentEpoch &&
1151
- context.documentId === instance.documentId &&
1152
- context.generation === instance.generation,
1153
- };
1154
- },
1155
- active: () => instance.ready &&
1156
- !instance.destroyed &&
1157
- !instance.literalSurface,
1158
- labels: { ...defaultLabels, ...options.labels },
1159
- error: (error) => events.error?.(error),
1160
- })
1161
- : [])
1162
- .use(options.images || options.files
1163
- ? imageView(options.images, options.files
1164
- ? {
1165
- adapter: options.files,
1166
- capture: () => {
1167
- const epoch = instance.documentEpoch;
1168
- const context = instance.context();
1169
- return {
1170
- context,
1171
- isCurrent: () => !instance.destroyed &&
1172
- epoch === instance.documentEpoch &&
1173
- context.documentId === instance.documentId &&
1174
- context.generation === instance.generation,
1175
- };
1176
- },
1177
- active: () => instance.ready &&
1178
- !instance.destroyed &&
1179
- !instance.literalSurface,
1180
- labels: { ...defaultLabels, ...options.labels },
1181
- error: (error) => events.error?.(error),
1182
- }
1183
- : undefined, () => {
1184
- try {
1185
- instance.assertMutation();
1186
- return true;
1187
- }
1188
- catch {
1189
- return false;
1190
- }
1191
- })
1192
- : [])
1193
- .create();
1194
- configureTableMovement(instance.editor.ctx, () => {
1195
- try {
1196
- instance.assertCurrent();
1197
- return instance.writable && !instance.literalSurface;
1198
- }
1199
- catch {
1200
- return false;
1201
- }
1202
- });
1203
- instance.plain = document.createElement('textarea');
1204
- instance.plain.className = 'inkkit-plain';
1205
- instance.plain.setAttribute('aria-label', 'Plain text editor');
1206
- instance.plain.hidden = true;
1207
- root.append(instance.plain);
1208
- instance.plain.addEventListener('beforeinput', (event) => {
1209
- if (!instance.writable) {
1210
- event.preventDefault();
1211
- return;
1212
- }
1213
- if (instance.ready && instance.literalSurface)
1214
- instance.beforePlainInput = {
1215
- from: instance.plain.selectionStart,
1216
- to: instance.plain.selectionEnd,
1217
- };
1218
- });
1219
- instance.plain.addEventListener('input', () => {
1220
- if (!instance.ready || !instance.literalSurface)
1221
- return;
1222
- if (!instance.writable) {
1223
- instance.plain.value = instance.currentText();
1224
- return;
1225
- }
1226
- try {
1227
- instance.applySource(editedPlainSource(instance.currentText(), instance.plain.value), false, instance.plain.selectionStart, instance.plain.selectionEnd);
1228
- }
1229
- catch (error) {
1230
- events.error?.(error instanceof Error ? error : new Error(String(error)));
1231
- }
1232
- finally {
1233
- instance.beforePlainInput = undefined;
1234
- }
1235
- });
1236
- instance.plain.addEventListener('keydown', (event) => {
1237
- if (instance.plainComposing)
1238
- return;
1239
- if (instance.literalKeys(instance.editor.ctx.get(editorViewCtx), event))
1240
- event.preventDefault();
1241
- });
1242
- for (const event of ['select', 'keyup', 'click'])
1243
- instance.plain.addEventListener(event, () => instance.publishCommandState());
1244
- instance.plain.addEventListener('paste', (event) => {
1245
- if (!instance.writable) {
1246
- event.preventDefault();
1247
- return;
1248
- }
1249
- if (!event.clipboardData)
1250
- return;
1251
- event.preventDefault();
1252
- void instance
1253
- .paste({
1254
- text: event.clipboardData.getData('text/plain'),
1255
- markdown: event.clipboardData.getData('text/markdown') || undefined,
1256
- })
1257
- .catch((error) => events.error?.(error));
1258
- });
1259
- instance.plain.addEventListener('compositionstart', () => {
1260
- instance.plainComposing = true;
1261
- instance.publishCommandState();
1262
- });
1263
- instance.plain.addEventListener('compositionend', () => {
1264
- instance.plainComposing = false;
1265
- instance.publishCommandState();
1266
- });
1267
- const activateFootnote = (event) => {
1268
- const element = event.target.closest('[data-inkkit-footnote-reference]');
1269
- if (!element)
1270
- return false;
1271
- const identifier = element.getAttribute('data-inkkit-footnote-reference');
1272
- const view = instance.editor.ctx.get(editorViewCtx);
1273
- let position;
1274
- view.state.doc.descendants((node, pos) => {
1275
- if (position == null &&
1276
- node.type.name === 'footnote_reference' &&
1277
- node.attrs.identifier === identifier)
1278
- position = pos;
1279
- });
1280
- if (position == null)
1281
- return false;
1282
- event.preventDefault();
1283
- try {
1284
- view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, position)));
1285
- instance.navigateFootnote('definition');
1286
- }
1287
- catch (error) {
1288
- events.error?.(error instanceof Error ? error : new Error(String(error)));
1289
- }
1290
- return true;
1291
- };
1292
- instance.footnoteKeyHandler = (event) => {
1293
- if ((event.key === 'Enter' || event.key === ' ') &&
1294
- activateFootnote(event))
1295
- event.stopPropagation();
1296
- };
1297
- root.addEventListener('keydown', instance.footnoteKeyHandler, true);
1298
- instance.clickHandler = (event) => {
1299
- if (activateFootnote(event))
1300
- return;
1301
- const anchor = event.target.closest('a[href]');
1302
- if (anchor && event.metaKey) {
1303
- event.preventDefault();
1304
- events.openLink(anchor.getAttribute('href') ?? '');
1305
- }
1306
- };
1307
- root.addEventListener('click', instance.clickHandler);
1308
- instance.copyHandler = (event) => {
1309
- if (instance.formatType === 'txt')
1310
- return;
1311
- if (instance.mode === 'source') {
1312
- instance.copySourceSelection(event);
1313
- return;
1314
- }
1315
- const view = instance.editor.ctx.get(editorViewCtx);
1316
- const fragment = commentSelectionContent(view.state.doc, view.state.selection.from, view.state.selection.to, view.state.selection.content().content);
1317
- const expanded = selectionContent(view.state.doc, fragment);
1318
- let hasImages = false;
1319
- expanded.descendants((node) => {
1320
- if (isImage(node) || isMermaid(node))
1321
- hasImages = true;
1322
- });
1323
- if (!hasImages) {
1324
- if (!event.clipboardData || fragment.size === 0)
1325
- return;
1326
- event.preventDefault();
1327
- event.stopPropagation();
1328
- try {
1329
- instance.snapshot();
1330
- const content = expanded;
1331
- const valid = fragment.firstChild?.isInline
1332
- ? Fragment.from(view.state.schema.nodes.paragraph.create(null, fragment))
1333
- : fragment;
1334
- const markdown = selectionMarkdown(instance.editor.ctx, view.state.doc, valid);
1335
- const output = clipboardContent(content, view.state.schema);
1336
- event.clipboardData.setData('text/plain', output.text);
1337
- const metadata = referenceMetadata(instance.editor.ctx, view.state.doc, valid);
1338
- event.clipboardData.setData('text/html', metadata != null
1339
- ? withReferenceMetadata(output.html, metadata)
1340
- : output.html);
1341
- if (event.type === 'cut' && instance.writable)
1342
- view.dispatch(view.state.tr.deleteSelection().scrollIntoView());
1343
- }
1344
- catch (error) {
1345
- events.error?.(error instanceof Error ? error : new Error(String(error)));
1346
- }
1347
- return;
1348
- }
1349
- event.preventDefault();
1350
- event.stopPropagation();
1351
- if (events.clipboard) {
1352
- const epoch = instance.documentEpoch, revision = instance.revision;
1353
- const { from, to } = view.state.selection;
1354
- const cut = event.type === 'cut' && instance.writable;
1355
- const policyEpoch = instance.policyEpoch;
1356
- void instance
1357
- .clipboardSnapshot(false)
1358
- .then(async (content) => {
1359
- if (cut &&
1360
- (content.images.some((image) => image.error) ||
1361
- content.diagrams?.some((diagram) => diagram.error)))
1362
- throw new InkKitError('image-unavailable', 'The selected content could not be cut safely');
1363
- await events.clipboard(content);
1364
- if (cut) {
1365
- instance.assertMutation();
1366
- if (epoch !== instance.documentEpoch ||
1367
- policyEpoch !== instance.policyEpoch ||
1368
- revision !== instance.revision)
1369
- throw new InkKitError('stale-document', 'Document changed before cutting');
1370
- view.dispatch(view.state.tr.deleteRange(from, to).scrollIntoView());
1371
- }
1372
- })
1373
- .catch((error) => events.error?.(error instanceof Error ? error : new Error(String(error))));
1374
- }
1375
- else {
1376
- const container = document.createElement('div');
1377
- container.textContent = clipboardText(expanded);
1378
- event.clipboardData?.setData('text/plain', container.textContent ?? '');
1379
- event.clipboardData?.setData('text/html', container.outerHTML);
1380
- events.error?.(new InkKitError('image-unavailable', 'The host must provide a clipboard handler for portable images and diagrams'));
1381
- }
1382
- };
1383
- root.addEventListener('copy', instance.copyHandler, true);
1384
- root.addEventListener('cut', instance.copyHandler, true);
1385
- instance.applyInputPolicy();
1386
- instance.setKeymap(options.keymap ?? {});
1387
- return instance;
1388
- }
1389
- loadDocument(input) {
1390
- this.assertAlive();
1391
- const container = this.viewportContainer ?? this.root;
1392
- const top = container.scrollTop, left = container.scrollLeft;
1393
- const active = this.root.ownerDocument.activeElement;
1394
- if (active === this.plain ||
1395
- active === this.editor.ctx.get(editorViewCtx).dom)
1396
- active.blur();
1397
- this.ready = false;
1398
- this.cancelExports();
1399
- this.mediaPolicy();
1400
- this.pasteController.cancelPending();
1401
- this.documentEpoch += 1;
1402
- this.outlineEpoch += 1;
1403
- this.sourcePreservations = new WeakMap();
1404
- this.sourceTexts = new WeakMap();
1405
- this.mode = 'formatted';
1406
- this.plainComposing = false;
1407
- this.beforePlainInput = undefined;
1408
- this.plainSearch = '';
1409
- this.literalMatch = undefined;
1410
- this.documentId = input.documentId;
1411
- this.formatType = input.format;
1412
- this.revision = 0;
1413
- this.plain.value = input.text;
1414
- this.plain.setSelectionRange(this.plain.value.length, this.plain.value.length);
1415
- this.load(input.format === 'txt' ? '' : input.text, input.generation);
1416
- this.originalSource = input.text;
1417
- this.lastMarkdown = input.text;
1418
- this.updateSurface();
1419
- this.ready = true;
1420
- this.mediaPolicy();
1421
- container.scrollTop = top;
1422
- container.scrollLeft = left;
1423
- this.events.stateChanged(this.literalSurface
1424
- ? { marks: [], block: { type: 'paragraph' }, quoted: false }
1425
- : caretState(this.editor.ctx.get(editorViewCtx).state));
1426
- this.publishCommandState();
1427
- }
1428
- reloadDocument(input) {
1429
- this.assertAlive();
1430
- const view = this.editor.ctx.get(editorViewCtx);
1431
- const wasLiteral = this.literalSurface;
1432
- const previousMode = this.editingMode;
1433
- const previousFormat = this.formatType;
1434
- const selectedNode = !wasLiteral && view.state.selection instanceof NodeSelection
1435
- ? view.state.selection.node
1436
- : undefined;
1437
- const at = wasLiteral
1438
- ? this.plain.selectionStart
1439
- : view.state.selection.anchor;
1440
- const end = wasLiteral ? this.plain.selectionEnd : view.state.selection.head;
1441
- const direction = this.plain.selectionDirection;
1442
- const container = this.viewportContainer ?? this.root;
1443
- const top = container.scrollTop, left = container.scrollLeft;
1444
- const focused = wasLiteral
1445
- ? document.activeElement === this.plain
1446
- : view.hasFocus();
1447
- const plainTop = this.plain.scrollTop, plainLeft = this.plain.scrollLeft;
1448
- this.reportingSuppressed = true;
1449
- try {
1450
- this.loadDocument(input);
1451
- if (previousFormat === 'md' &&
1452
- input.format === 'md' &&
1453
- previousMode === 'source')
1454
- this.setEditingMode('source', input.generation);
1455
- if (this.literalSurface)
1456
- this.plain.setSelectionRange(Math.min(at, this.plain.value.length), Math.min(end, this.plain.value.length), direction);
1457
- else {
1458
- const anchor = Math.min(at, view.state.doc.content.size);
1459
- const node = view.state.doc.nodeAt(anchor);
1460
- view.dispatch(view.state.tr.setSelection(selectedNode &&
1461
- node?.type === selectedNode.type &&
1462
- NodeSelection.isSelectable(node)
1463
- ? NodeSelection.create(view.state.doc, anchor)
1464
- : TextSelection.between(view.state.doc.resolve(anchor), view.state.doc.resolve(Math.min(end, view.state.doc.content.size)))));
1465
- }
1466
- if (focused) {
1467
- if (this.literalSurface)
1468
- this.plain.focus({ preventScroll: true });
1469
- else
1470
- view.focus();
1471
- }
1472
- container.scrollTop = top;
1473
- container.scrollLeft = left;
1474
- if (wasLiteral && this.literalSurface) {
1475
- this.plain.scrollTop = plainTop;
1476
- this.plain.scrollLeft = plainLeft;
1477
- }
1478
- }
1479
- finally {
1480
- this.reportingSuppressed = false;
1481
- this.publishCommandState();
1482
- }
1483
- }
1484
- setViewport(options) {
1485
- this.assertAlive();
1486
- const container = options.scrollContainer ?? this.root;
1487
- if (container !== this.root && !container.contains(this.root))
1488
- throw new RangeError('The scroll container must be the editor root or an ancestor');
1489
- const insets = { top: 0, right: 0, bottom: 0, left: 0, ...options.insets };
1490
- if (Object.values(insets).some((value) => !Number.isFinite(value) || value < 0))
1491
- throw new RangeError('Viewport insets must be finite non-negative CSS pixel values');
1492
- this.viewportContainer = container;
1493
- this.viewportInsets = insets;
1494
- }
1495
- viewport() {
1496
- this.assertAlive();
1497
- const container = this.viewportContainer ?? this.root;
1498
- const bounds = container.getBoundingClientRect();
1499
- const root = this.root.getBoundingClientRect();
1500
- const insets = this.viewportInsets;
1501
- const window = this.root.ownerDocument.defaultView;
1502
- const left = Math.max(bounds.left + container.clientLeft, root.left, 0);
1503
- const top = Math.max(bounds.top + container.clientTop, root.top, 0);
1504
- const right = Math.max(left, Math.min(bounds.left + container.clientLeft + container.clientWidth, root.right, window.innerWidth));
1505
- const bottom = Math.max(top, Math.min(bounds.top + container.clientTop + container.clientHeight, root.bottom, window.innerHeight));
1506
- let rect = textRect({ left, top, right, bottom });
1507
- if (this.literalSurface) {
1508
- const plain = this.plain.getBoundingClientRect();
1509
- const literalLeft = Math.max(rect.left, plain.left + this.plain.clientLeft);
1510
- const literalTop = Math.max(rect.top, plain.top + this.plain.clientTop);
1511
- rect = textRect({
1512
- left: literalLeft,
1513
- top: literalTop,
1514
- right: Math.max(literalLeft, Math.min(rect.right, plain.left + this.plain.clientLeft + this.plain.clientWidth)),
1515
- bottom: Math.max(literalTop, Math.min(rect.bottom, plain.top + this.plain.clientTop + this.plain.clientHeight)),
1516
- });
1517
- }
1518
- const insetLeft = Math.min(rect.right, rect.left + insets.left);
1519
- const insetTop = Math.min(rect.bottom, rect.top + insets.top);
1520
- rect = textRect({
1521
- left: insetLeft,
1522
- top: insetTop,
1523
- right: Math.max(insetLeft, rect.right - insets.right),
1524
- bottom: Math.max(insetTop, rect.bottom - insets.bottom),
1525
- });
1526
- return Object.freeze({
1527
- rect: Object.freeze(rect),
1528
- insets: Object.freeze({ ...insets }),
1529
- scrollTop: container.scrollTop + (this.literalSurface ? this.plain.scrollTop : 0),
1530
- scrollLeft: container.scrollLeft +
1531
- (this.literalSurface ? this.plain.scrollLeft : 0),
1532
- });
1533
- }
1534
- readableScope() {
1535
- const key = `${this.documentEpoch}:${this.revision}:${this.outlineEpoch}:${this.editingMode}`;
1536
- if (this.textScope?.key !== key)
1537
- this.textScope = {
1538
- key,
1539
- id: `${this.textIdentity}:${key}`,
1540
- projection: this.literalSurface
1541
- ? { text: this.plain.value, spans: [] }
1542
- : readableProjection(this.editor.ctx.get(editorViewCtx).state.doc),
1543
- };
1544
- return this.textScope;
1545
- }
1546
- textSnapshot(expectedGeneration) {
1547
- this.assertCurrent(expectedGeneration);
1548
- const scope = this.readableScope();
1549
- const selection = this.editor.ctx.get(editorViewCtx).state.selection;
1550
- return Object.freeze({
1551
- snapshotId: scope.id,
1552
- documentId: this.documentId,
1553
- generation: this.generation,
1554
- revision: this.revision,
1555
- format: this.formatType,
1556
- mode: this.editingMode,
1557
- text: scope.projection.text,
1558
- selection: Object.freeze({
1559
- snapshotId: scope.id,
1560
- from: this.literalSurface
1561
- ? this.plain.selectionStart
1562
- : projectionOffset(scope.projection, selection.from),
1563
- to: this.literalSurface
1564
- ? this.plain.selectionEnd
1565
- : projectionOffset(scope.projection, selection.to),
1566
- }),
1567
- });
1568
- }
1569
- validateTextRange(range) {
1570
- this.assertCurrent();
1571
- const scope = this.readableScope();
1572
- if (range.snapshotId !== scope.id)
1573
- throw new InkKitError('stale-document', 'The readable text snapshot changed');
1574
- validateOffsets(scope.projection.text, range.from, range.to);
1575
- return scope.projection;
1576
- }
1577
- selectTextRange(range, options = {}) {
1578
- const projection = this.validateTextRange(range);
1579
- if (this.literalSurface)
1580
- this.plain.setSelectionRange(range.from, range.to);
1581
- else {
1582
- for (const span of projection.spans)
1583
- if (span.kind === 'embed' &&
1584
- ((range.from > span.from && range.from < span.to) ||
1585
- (range.to > span.from && range.to < span.to)))
1586
- throw new InkKitError('invalid-range', 'Embedded labels must be selected as complete ranges');
1587
- const view = this.editor.ctx.get(editorViewCtx);
1588
- const from = projectionPosition(projection, range.from, 1);
1589
- const to = projectionPosition(projection, range.to, range.from === range.to ? 1 : -1);
1590
- const embed = projection.spans.find((span) => span.kind === 'embed' &&
1591
- span.from === range.from &&
1592
- span.to === range.to);
1593
- view.dispatch(view.state.tr.setSelection(embed && view.state.doc.nodeAt(embed.start)
1594
- ? NodeSelection.create(view.state.doc, embed.start)
1595
- : TextSelection.between(view.state.doc.resolve(from), view.state.doc.resolve(to))));
1596
- }
1597
- if (options.focus) {
1598
- if (this.literalSurface)
1599
- this.plain.focus({ preventScroll: true });
1600
- else
1601
- this.editor.ctx.get(editorViewCtx).focus();
1602
- }
1603
- if (options.reveal)
1604
- this.revealTextRange(range);
1605
- this.publishCommandState();
1606
- }
1607
- /** Throws when a document reached by an edit, after every appended normalisation, cannot be
1608
- * saved as preserved Markdown. Dispatch and the replacement commands share this check. */
1609
- assertPreserved(before, after) {
1610
- if (this.preservation && !this.literalSurface && !after.eq(before))
1611
- this.currentText(after);
1612
- }
1613
- replaceTextRange(range, text) {
1614
- const projection = this.validateTextRange(range);
1615
- this.assertMutation();
1616
- if (typeof text !== 'string')
1617
- throw new TypeError('Replacement must be a string');
1618
- if (this.literalSurface) {
1619
- const raw = this.currentText();
1620
- const from = rawOffset(raw, range.from), to = rawOffset(raw, range.to);
1621
- const ending = /\r\n|\r|\n/.exec(raw)?.[0] ?? '\n';
1622
- const value = text.replace(/\r\n?/g, '\n');
1623
- const replaced = raw.slice(0, from) + value.replaceAll('\n', ending) + raw.slice(to);
1624
- return this.applySource(replaced, true, range.from, range.from + value.length);
1625
- }
1626
- if (projection.spans.some((span) => (span.kind === 'embed' &&
1627
- range.from > span.from &&
1628
- range.from < span.to) ||
1629
- (span.kind === 'embed' &&
1630
- range.to > span.from &&
1631
- range.to < span.to) ||
1632
- (span.to > range.from &&
1633
- span.from < range.to &&
1634
- span.kind !== 'text')))
1635
- throw new InkKitError('invalid-range', 'Replacement cannot remove embedded content or structural separators');
1636
- const from = projectionPosition(projection, range.from, 1);
1637
- const to = projectionPosition(projection, range.to, range.from === range.to ? 1 : -1);
1638
- const view = this.editor.ctx.get(editorViewCtx);
1639
- return replaceFormattedRange(view, from, to, text, (tr) => this.assertPreserved(view.state.doc, view.state.apply(tr).doc));
1640
- }
1641
- textRangeRects(range) {
1642
- return this.textRangesRects([range])[0];
1643
- }
1644
- textRangesRects(ranges) {
1645
- this.assertCurrent();
1646
- const projections = ranges.map((range) => this.validateTextRange(range));
1647
- if (!ranges.length)
1648
- return Object.freeze([]);
1649
- const results = this.literalSurface
1650
- ? literalRangesRects(this.plain, ranges)
1651
- : ranges.map((range, index) => formattedRects(this.editor.ctx.get(editorViewCtx), projections[index], range.from, range.to));
1652
- return Object.freeze(results.map((rects) => Object.freeze(rects.map((rect) => Object.freeze(rect)))));
1653
- }
1654
- visibleTextRanges(snapshotId) {
1655
- const snapshot = this.textSnapshot();
1656
- this.validateTextRange({ snapshotId, from: 0, to: 0 });
1657
- const viewport = this.viewport().rect;
1658
- if (this.literalSurface)
1659
- return Object.freeze(literalVisibleRanges(this.plain, viewport).map((range) => Object.freeze({ snapshotId, ...range })));
1660
- const visible = (rect) => intersectsViewport(rect, viewport);
1661
- const ranges = [];
1662
- const projection = this.readableScope().projection;
1663
- const view = this.editor.ctx.get(editorViewCtx);
1664
- for (const span of projection.spans) {
1665
- const current = { text: projection.text, spans: [span] };
1666
- if (span.kind === 'separator' ||
1667
- !formattedRects(view, current, span.from, span.to).some(visible))
1668
- continue;
1669
- for (let from = span.from; from < span.to;) {
1670
- const to = from + (snapshot.text.codePointAt(from) > 0xffff ? 2 : 1);
1671
- if (formattedRects(view, current, from, to).some(visible)) {
1672
- const previous = ranges.at(-1);
1673
- if (previous?.to === from)
1674
- previous.to = to;
1675
- else
1676
- ranges.push({ snapshotId, from, to });
1677
- }
1678
- from = to;
1679
- }
1680
- }
1681
- return Object.freeze(ranges.map((range) => Object.freeze(range)));
1682
- }
1683
- revealTextRange(range) {
1684
- const projection = this.validateTextRange(range);
1685
- if (!this.literalSurface) {
1686
- const view = this.editor.ctx.get(editorViewCtx);
1687
- const position = projectionPosition(projection, range.from, 1);
1688
- const at = view.domAtPos(position).node;
1689
- let element = at instanceof Element ? at : at.parentElement;
1690
- while (element && this.root.contains(element)) {
1691
- if (element.matches('.inkkit-callout[data-inkkit-folded="true"]'))
1692
- element
1693
- .querySelector(':scope > .inkkit-callout-header [data-inkkit-callout-toggle]')
1694
- ?.click();
1695
- element = element.parentElement;
1696
- }
1697
- }
1698
- this.revealRects(this.textRangeRects(range));
1699
- }
1700
- revealRects(rects) {
1701
- if (!rects.length)
1702
- return;
1703
- const viewport = this.viewport().rect;
1704
- const leading = rects[0];
1705
- const envelope = {
1706
- top: leading.top,
1707
- bottom: leading.bottom,
1708
- left: leading.left,
1709
- right: leading.right,
1710
- };
1711
- for (const rect of rects) {
1712
- envelope.top = Math.min(envelope.top, rect.top);
1713
- envelope.bottom = Math.max(envelope.bottom, rect.bottom);
1714
- envelope.left = Math.min(envelope.left, rect.left);
1715
- envelope.right = Math.max(envelope.right, rect.right);
1716
- }
1717
- const fitsHeight = envelope.bottom - envelope.top <= viewport.height;
1718
- const fitsWidth = envelope.right - envelope.left <= viewport.width;
1719
- const rect = {
1720
- top: fitsHeight ? envelope.top : leading.top,
1721
- bottom: fitsHeight
1722
- ? envelope.bottom
1723
- : Math.min(leading.bottom, leading.top + viewport.height),
1724
- left: fitsWidth ? envelope.left : leading.left,
1725
- right: fitsWidth
1726
- ? envelope.right
1727
- : Math.min(leading.right, leading.left + viewport.width),
1728
- };
1729
- const vertical = viewport.height <= 0
1730
- ? 0
1731
- : rect.top < viewport.top
1732
- ? rect.top - viewport.top
1733
- : rect.bottom > viewport.bottom
1734
- ? rect.bottom - viewport.bottom
1735
- : 0;
1736
- const horizontal = viewport.width <= 0
1737
- ? 0
1738
- : rect.left < viewport.left
1739
- ? rect.left - viewport.left
1740
- : rect.right > viewport.right
1741
- ? rect.right - viewport.right
1742
- : 0;
1743
- let remainingTop = vertical, remainingLeft = horizontal;
1744
- if (this.literalSurface) {
1745
- const top = this.plain.scrollTop, left = this.plain.scrollLeft;
1746
- this.plain.scrollTop += vertical;
1747
- this.plain.scrollLeft += horizontal;
1748
- remainingTop -= this.plain.scrollTop - top;
1749
- remainingLeft -= this.plain.scrollLeft - left;
1750
- }
1751
- let container = this.viewportContainer ?? this.root;
1752
- const document = this.root.ownerDocument;
1753
- while (container &&
1754
- container !== document.body &&
1755
- container !== document.documentElement) {
1756
- const top = container.scrollTop, left = container.scrollLeft;
1757
- container.scrollTop += remainingTop;
1758
- container.scrollLeft += remainingLeft;
1759
- remainingTop -= container.scrollTop - top;
1760
- remainingLeft -= container.scrollLeft - left;
1761
- container = container.parentElement;
1762
- }
1763
- if (remainingTop || remainingLeft)
1764
- document.defaultView.scrollBy(remainingLeft, remainingTop);
1765
- }
1766
- revealCurrentSelection() {
1767
- if (this.loading || !this.ready)
1768
- return;
1769
- if (this.literalSurface) {
1770
- this.revealLiteralSelection();
1771
- return;
1772
- }
1773
- const view = this.editor.ctx.get(editorViewCtx);
1774
- const projection = this.readableScope().projection;
1775
- const from = projectionOffset(projection, view.state.selection.from);
1776
- const to = projectionOffset(projection, view.state.selection.to);
1777
- if (this.composing || this.pasteController.pending) {
1778
- this.revealRects(formattedRects(view, projection, from, to));
1779
- return;
1780
- }
1781
- this.revealTextRange({
1782
- snapshotId: this.readableScope().id,
1783
- from,
1784
- to,
1785
- });
1786
- }
1787
- revealLiteralSelection() {
1788
- this.revealRects(literalRangesRects(this.plain, [
1789
- {
1790
- from: this.plain.selectionStart,
1791
- to: this.plain.selectionEnd,
1792
- },
1793
- ])[0]);
1794
- }
1795
- load(markdown, generation) {
1796
- this.generation = generation;
1797
- this.originalSource = markdown;
1798
- this.loading = true;
1799
- try {
1800
- this.editor.action(replaceAll(markdown, true));
1801
- }
1802
- finally {
1803
- this.loading = false;
1804
- }
1805
- this.preservation = new Preservation(this.editor.ctx, markdown);
1806
- this.lastMarkdown = markdown;
1807
- const view = this.editor.ctx.get(editorViewCtx);
1808
- // A caret at the end, where writing carries on; at the start it would sit in a first-line heading and
1809
- // show its marks. The last place text can go, so a document ending in a rule takes a caret above it
1810
- // rather than selecting the rule itself. After a final image, typing opens a paragraph below it.
1811
- const { doc } = view.state;
1812
- const $end = doc.resolve(doc.content.size);
1813
- const end = isImageBlock(doc.lastChild)
1814
- ? new GapCursor($end)
1815
- : (Selection.findFrom($end, -1, true) ?? Selection.atEnd(doc));
1816
- view.dispatch(setSearchState(view.state.tr.setSelection(end), new SearchQuery({ search: '' })));
1817
- }
1818
- find(text, expectedGeneration) {
1819
- this.assertCurrent(expectedGeneration);
1820
- if (this.literalSurface) {
1821
- const from = this.plainSearch === text
1822
- ? this.plain.selectionEnd
1823
- : this.plain.selectionStart;
1824
- const raw = this.currentText();
1825
- const match = findLiteral(raw, text, rawOffset(raw, from));
1826
- this.literalMatch = undefined;
1827
- if (match) {
1828
- // A textarea displays CRLF as one character; retain a match of either raw half for replacement.
1829
- const start = raw[match.from] === '\n' && raw[match.from - 1] === '\r'
1830
- ? match.from - 1
1831
- : match.from;
1832
- const displayedFrom = normalizedOffset(raw, start), displayedTo = normalizedOffset(raw, match.to);
1833
- this.plain.setSelectionRange(displayedFrom, displayedTo);
1834
- this.literalMatch = {
1835
- ...match,
1836
- query: text,
1837
- displayedFrom,
1838
- displayedTo,
1839
- revision: this.revision,
1840
- epoch: this.documentEpoch,
1841
- };
1842
- }
1843
- else if (this.plainSearch)
1844
- this.plain.setSelectionRange(this.plain.selectionEnd, this.plain.selectionEnd);
1845
- this.plainSearch = text;
1846
- this.revealLiteralSelection();
1847
- return;
1848
- }
1849
- if (findFormatted(this.editor.ctx.get(editorViewCtx), text))
1850
- this.revealCurrentSelection();
1851
- }
1852
- replace(search, replacement, expectedGeneration) {
1853
- this.assertMutation(expectedGeneration);
1854
- return this.replaceMatches(search, replacement, false) > 0;
1855
- }
1856
- replaceAll(search, replacement, expectedGeneration) {
1857
- this.assertMutation(expectedGeneration);
1858
- return this.replaceMatches(search, replacement, true);
1859
- }
1860
- replaceMatches(search, replacement, all) {
1861
- if (this.literalSurface) {
1862
- const raw = this.currentText();
1863
- const remembered = this.literalMatch;
1864
- const selection = remembered &&
1865
- remembered.query === search &&
1866
- remembered.revision === this.revision &&
1867
- remembered.epoch === this.documentEpoch &&
1868
- remembered.displayedFrom === this.plain.selectionStart &&
1869
- remembered.displayedTo === this.plain.selectionEnd
1870
- ? { from: remembered.from, to: remembered.to }
1871
- : {
1872
- from: rawOffset(raw, this.plain.selectionStart),
1873
- to: rawOffset(raw, this.plain.selectionEnd),
1874
- };
1875
- if (remembered?.query !== search)
1876
- this.literalMatch = undefined;
1877
- const result = replaceLiteral(raw, search, replacement, selection, all);
1878
- if (result.count)
1879
- this.applySource(result.text, true, normalizedOffset(result.text, result.from), normalizedOffset(result.text, result.to));
1880
- this.plainSearch = search;
1881
- return result.count;
1882
- }
1883
- const view = this.editor.ctx.get(editorViewCtx);
1884
- return replaceFormatted(view, search, replacement, (tr) => this.assertPreserved(view.state.doc, view.state.apply(tr).doc), all);
1885
- }
1886
- outlineContext() {
1887
- return {
1888
- documentId: this.documentId,
1889
- generation: this.generation,
1890
- revision: this.revision,
1891
- epoch: this.outlineEpoch,
1892
- mode: this.editingMode,
1893
- };
1894
- }
1895
- headings(expectedGeneration) {
1896
- this.assertCurrent(expectedGeneration);
1897
- if (this.formatType === 'txt')
1898
- return Object.freeze([]);
1899
- return this.mode === 'source'
1900
- ? collectSourceHeadings(this.editor.ctx, this.plain.value, this.outlineContext())
1901
- : collectFormattedHeadings(this.editor.ctx.get(editorViewCtx).state.doc, this.outlineContext());
1902
- }
1903
- navigateHeading(heading) {
1904
- this.assertCurrent();
1905
- if (!heading || typeof heading !== 'object')
1906
- throw new InkKitError('stale-document', 'The heading is no longer current');
1907
- this.assertCurrent(heading.generation);
1908
- if (this.formatType === 'txt')
1909
- throw new InkKitError('stale-document', 'The heading is no longer current');
1910
- if (this.mode === 'source') {
1911
- const position = sourceHeadingPosition(this.editor.ctx, this.plain.value, heading, this.outlineContext());
1912
- this.plain.setSelectionRange(position, position);
1913
- this.plain.focus({ preventScroll: true });
1914
- this.revealLiteralSelection();
1915
- return true;
1916
- }
1917
- return navigateFormattedHeading(this.editor.ctx.get(editorViewCtx), heading, this.outlineContext());
1918
- }
1919
- /** Buffered native typing follows the same input rules as direct typing. */
1920
- insertText(text, generation) {
1921
- this.assertAlive();
1922
- if ((this.literalSurface
1923
- ? this.plainComposing
1924
- : this.editor.ctx.get(editorViewCtx).composing) &&
1925
- generation === this.generation)
1926
- return false;
1927
- this.assertMutation(generation);
1928
- if (this.literalSurface) {
1929
- this.replaceLiteralSelection(text, false);
1930
- return true;
1931
- }
1932
- if (/[\r\n]/.test(text)) {
1933
- this.pasteAsPlainText(text);
1934
- return true;
1935
- }
1936
- const view = this.editor.ctx.get(editorViewCtx);
1937
- if (view.composing)
1938
- return false;
1939
- const { from, to } = view.state.selection;
1940
- const transaction = () => view.state.tr.insertText(text, from, to);
1941
- if (!view.someProp('handleTextInput', (handler) => handler(view, from, to, text, transaction))) {
1942
- view.dispatch(transaction().scrollIntoView());
1943
- }
1944
- return true;
1945
- }
1946
- /** Let existing keymaps handle buffered editing commands before native fallback. */
1947
- keyDown(key, code, metaKey, ctrlKey, altKey, shiftKey, generation) {
1948
- this.assertAlive();
1949
- if ((this.literalSurface
1950
- ? this.plainComposing
1951
- : this.editor.ctx.get(editorViewCtx).composing) &&
1952
- generation === this.generation)
1953
- return false;
1954
- this.assertCurrent(generation);
1955
- const configuredEvent = new KeyboardEvent('keydown', {
1956
- key,
1957
- code,
1958
- metaKey,
1959
- ctrlKey,
1960
- altKey,
1961
- shiftKey,
1962
- bubbles: true,
1963
- cancelable: true,
1964
- });
1965
- if (!this.literalSurface && altKey && key === 'Enter')
1966
- return this.navigateFootnote(shiftKey ? 'reference' : 'definition');
1967
- if (!this.writable &&
1968
- !this.literalSurface &&
1969
- key === 'Tab' &&
1970
- !metaKey &&
1971
- !ctrlKey &&
1972
- !altKey) {
1973
- const view = this.editor.ctx.get(editorViewCtx);
1974
- return goToNextCell(shiftKey ? -1 : 1)(view.state, view.dispatch);
1975
- }
1976
- if (!this.writable) {
1977
- if (['Enter', 'Tab', 'Backspace', 'Delete'].includes(key))
1978
- this.assertMutation(generation);
1979
- this.readOnlyKeys(this.editor.ctx.get(editorViewCtx), configuredEvent);
1980
- }
1981
- if (this.literalSurface) {
1982
- if (this.literalKeys(this.editor.ctx.get(editorViewCtx), configuredEvent))
1983
- return true;
1984
- if (!this.writable &&
1985
- ['Enter', 'Tab', 'Backspace', 'Delete'].includes(key))
1986
- throw new InkKitError('read-only', 'The editor is read-only');
1987
- if (metaKey || ctrlKey || altKey)
1988
- return false;
1989
- if (key === 'Enter' || key === 'Tab') {
1990
- this.replaceLiteralSelection(key === 'Enter' ? '\n' : '\t', false);
1991
- return true;
1992
- }
1993
- if (key === 'Backspace' || key === 'Delete') {
1994
- const start = this.plain.selectionStart, end = this.plain.selectionEnd;
1995
- if (start === end) {
1996
- if (key === 'Backspace' && start > 0)
1997
- this.plain.setSelectionRange(start - [...this.plain.value.slice(0, start)].at(-1).length, end);
1998
- else if (key === 'Delete' && end < this.plain.value.length)
1999
- this.plain.setSelectionRange(start, end + [...this.plain.value.slice(end)][0].length);
2000
- else
2001
- return false;
2002
- }
2003
- this.replaceLiteralSelection('', false);
2004
- return true;
2005
- }
2006
- return false;
2007
- }
2008
- const view = this.editor.ctx.get(editorViewCtx);
2009
- if (view.composing)
2010
- return false;
2011
- const event = new KeyboardEvent('keydown', {
2012
- key,
2013
- code,
2014
- metaKey,
2015
- ctrlKey,
2016
- altKey,
2017
- shiftKey,
2018
- bubbles: true,
2019
- cancelable: true,
2020
- });
2021
- return Boolean(view.someProp('handleKeyDown', (handler) => handler(view, event)));
2022
- }
2023
- focus() {
2024
- this.assertAlive();
2025
- if (!this.ready)
2026
- throw new InkKitError('not-ready', 'No document has been loaded');
2027
- if (this.literalSurface)
2028
- this.plain.focus();
2029
- else
2030
- this.editor.ctx.get(editorViewCtx).focus();
2031
- }
2032
- /** Binds keys, in ProseMirror's names, to the formatting each shortcut runs. Walked in the table's
2033
- * order, so a key given to two shortcuts lands the same way every time. */
2034
- setKeymap(keymap) {
2035
- this.assertAlive();
2036
- this.configuredKeys = {
2037
- undo: ['Mod-z'],
2038
- redo: ['Mod-y', 'Shift-Mod-z'],
2039
- tableExit: ['Mod-Enter', 'Enter'],
2040
- ...keymap,
2041
- };
2042
- const bindings = {};
2043
- // Consuming unbound history keys prevents the browser's separate undo stack
2044
- // from changing source/TXT outside the shared document history.
2045
- const historyBindings = Object.fromEntries(['Mod-z', 'Mod-y', 'Shift-Mod-z'].map((key) => [key, () => true]));
2046
- for (const name of ['undo', 'redo']) {
2047
- for (const key of this.configuredKeys[name] ?? []) {
2048
- historyBindings[key] = () => {
2049
- if (!this.ready || !this.commandState().commands[name])
2050
- return true;
2051
- return this[name]();
2052
- };
2053
- }
2054
- }
2055
- Object.assign(bindings, historyBindings);
2056
- for (const [name, command] of Object.entries(shortcutCommands)) {
2057
- for (const key of keymap[name] ?? []) {
2058
- bindings[key] = () => {
2059
- if (!this.ready || !this.commandState().commands.format[command[0]])
2060
- return true;
2061
- this.format(...command);
2062
- return true;
2063
- };
2064
- }
2065
- }
2066
- for (const command of tableCommands) {
2067
- const name = 'table' + command[0].toUpperCase() + command.slice(1);
2068
- for (const key of this.configuredKeys[name] ?? [])
2069
- bindings[key] = () => {
2070
- if (!this.ready || !this.commandState().commands.table[command])
2071
- return false;
2072
- return this.table(command);
2073
- };
2074
- }
2075
- this.readOnlyKeys = keydownHandler(Object.fromEntries(Object.keys(bindings).map((key) => [
2076
- key,
2077
- () => {
2078
- throw new InkKitError('read-only', 'The editor is read-only');
2079
- },
2080
- ])));
2081
- this.keys = keydownHandler(bindings);
2082
- this.literalKeys = keydownHandler(historyBindings);
2083
- }
2084
- /** Dropped files land as one paragraph per path at the drop point: in place of an empty block, after
2085
- * the top-level block otherwise, so a list or quote is not opened up by them. */
2086
- insertPaths(paths, x, y) {
2087
- this.assertMutation();
2088
- if (paths.length === 0)
2089
- return;
2090
- if (this.literalSurface) {
2091
- this.pasteAsPlainText(paths.join('\n\n'));
2092
- return;
2093
- }
2094
- const { schema } = this.editor.ctx.get(editorViewCtx).state;
2095
- this.insertBlocks(paths.map((path) => schema.nodes.paragraph.create(null, schema.text(path))), x, y);
2096
- }
2097
- insertImages(images, x, y) {
2098
- this.assertMutation();
2099
- if (!images.length)
2100
- return;
2101
- if (!this.options.images || this.literalSurface)
2102
- throw new InkKitError('image-unavailable', 'This editor has no image adapter');
2103
- const view = this.editor.ctx.get(editorViewCtx);
2104
- const { schema } = view.state;
2105
- // One paragraph per image: each is read back as a standalone image block where one fits.
2106
- const blocks = images.map((image) => schema.nodes.paragraph.create(null, schema.nodes.image.create({
2107
- src: image.path,
2108
- alt: image.alt,
2109
- title: image.title ?? '',
2110
- })));
2111
- if (x != null && y != null) {
2112
- this.insertBlocks(blocks, x, y);
2113
- return;
2114
- }
2115
- const tr = view.state.tr.replaceSelection(new Slice(Fragment.fromArray(blocks), 0, 0));
2116
- // After the last image, so typing continues below it rather than in the block that follows.
2117
- const last = blocks.at(-1).firstChild;
2118
- let after;
2119
- tr.doc.descendants((node, pos) => {
2120
- if (node === last)
2121
- after = pos + node.nodeSize;
2122
- return after == null;
2123
- });
2124
- if (after != null)
2125
- tr.setSelection(TextSelection.create(tr.doc, after));
2126
- view.dispatch(tr.scrollIntoView());
2127
- }
2128
- insertBlocks(blocks, x, y) {
2129
- const view = this.editor.ctx.get(editorViewCtx);
2130
- const { state } = view;
2131
- const hit = view.posAtCoords({ left: x, top: y });
2132
- const $pos = state.doc.resolve(hit?.pos ?? state.selection.from);
2133
- const tr = state.tr;
2134
- let at;
2135
- if ($pos.depth === 1 &&
2136
- $pos.parent.isTextblock &&
2137
- $pos.parent.content.size === 0) {
2138
- at = $pos.before(1);
2139
- tr.replaceWith(at, $pos.after(1), blocks);
2140
- }
2141
- else {
2142
- // A point over a top-level leaf, such as an image, resolves beside it rather than inside.
2143
- const leaf = $pos.depth === 0 && hit && hit.inside >= 0
2144
- ? state.doc.resolve(hit.inside)
2145
- : undefined;
2146
- at =
2147
- $pos.depth > 0
2148
- ? $pos.after(1)
2149
- : leaf?.depth === 0
2150
- ? hit.inside + state.doc.nodeAt(hit.inside).nodeSize
2151
- : $pos.pos;
2152
- tr.insert(at, blocks);
2153
- }
2154
- const end = at + blocks.reduce((size, node) => size + node.nodeSize, 0) - 1;
2155
- view.dispatch(tr.setSelection(TextSelection.create(tr.doc, end)).scrollIntoView());
2156
- this.focus();
2157
- }
2158
- // Removing a mark at a caret only clears the stored mark, so the whole marked
2159
- // run is selected for the command and the caret put back after it.
2160
- withMarkRunSelected(mark, command) {
2161
- const view = this.editor.ctx.get(editorViewCtx);
2162
- const { selection, doc } = view.state;
2163
- if (!selection.empty)
2164
- return false;
2165
- const $pos = selection.$from;
2166
- const parent = $pos.parent;
2167
- const start = $pos.start();
2168
- let from = $pos.pos;
2169
- let to = $pos.pos;
2170
- parent.forEach((child, offset) => {
2171
- const childFrom = start + offset;
2172
- const childTo = childFrom + child.nodeSize;
2173
- if (!mark.isInSet(child.marks))
2174
- return;
2175
- if (childTo >= $pos.pos && childFrom <= to) {
2176
- from = Math.min(from, childFrom);
2177
- to = Math.max(to, childTo);
2178
- }
2179
- });
2180
- if (from === to)
2181
- return false;
2182
- view.dispatch(view.state.tr.setSelection(TextSelection.create(doc, from, to)));
2183
- command();
2184
- const caret = Math.min($pos.pos, view.state.doc.content.size);
2185
- view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, caret)));
2186
- return true;
2187
- }
2188
- // Milkdown's inline code command ignores a caret; a stored mark makes the
2189
- // next typed text code, the way bold and italic behave.
2190
- toggleInlineCode() {
2191
- const view = this.editor.ctx.get(editorViewCtx);
2192
- const type = inlineCodeSchema.type(this.editor.ctx);
2193
- const run = () => {
2194
- this.editor.action(callCommand(toggleInlineCodeCommand.key));
2195
- };
2196
- const { selection, storedMarks } = view.state;
2197
- if (!selection.empty)
2198
- return run();
2199
- // A pending stored mark is only that; the span next to the caret is left alone.
2200
- if (storedMarks) {
2201
- const tr = type.isInSet(storedMarks)
2202
- ? view.state.tr.removeStoredMark(type)
2203
- : view.state.tr.addStoredMark(type.create());
2204
- return view.dispatch(tr);
2205
- }
2206
- if (!type.isInSet(selection.$from.marks()))
2207
- view.dispatch(view.state.tr.addStoredMark(type.create()));
2208
- else
2209
- this.withMarkRunSelected(type, run);
2210
- }
2211
- list(kind) {
2212
- const view = this.editor.ctx.get(editorViewCtx);
2213
- const { state } = view;
2214
- const { $from } = textBounds(state);
2215
- const wanted = kind === 'bulletList' ? 'bullet_list' : 'ordered_list';
2216
- const depth = innermostListDepth($from);
2217
- if (depth == null) {
2218
- const command = kind === 'bulletList'
2219
- ? wrapInBulletListCommand
2220
- : wrapInOrderedListCommand;
2221
- this.editor.action(callCommand(command.key));
2222
- return;
2223
- }
2224
- const list = $from.node(depth);
2225
- if (list.type.name === wanted) {
2226
- liftItem(state, view.dispatch);
2227
- return;
2228
- }
2229
- // The items carry the list kind too, and a bullet list whose items say
2230
- // ordered is turned back into one by the preset.
2231
- const pos = $from.before(depth);
2232
- const ordered = wanted === 'ordered_list';
2233
- const attrs = ordered
2234
- ? { order: 1, spread: list.attrs.spread }
2235
- : { spread: list.attrs.spread };
2236
- let tr = state.tr.setNodeMarkup(pos, state.schema.nodes[wanted], attrs);
2237
- list.forEach((item, offset) => {
2238
- tr = tr.setNodeMarkup(pos + 1 + offset, undefined, {
2239
- ...item.attrs,
2240
- listType: ordered ? 'ordered' : 'bullet',
2241
- label: ordered ? '1.' : '•',
2242
- });
2243
- });
2244
- view.dispatch(tr);
2245
- }
2246
- quote() {
2247
- const view = this.editor.ctx.get(editorViewCtx);
2248
- const type = blockquoteSchema.type(this.editor.ctx);
2249
- const { $from, $to } = textBounds(view.state);
2250
- let range = $from.blockRange($to);
2251
- let wrapping = range && findWrapping(range, type);
2252
- if (!wrapping) {
2253
- // A list item cannot hold a quote, so the whole list is quoted instead.
2254
- const depth = outermostListDepth($from);
2255
- if (depth == null)
2256
- return;
2257
- const doc = view.state.doc;
2258
- range = new NodeRange(doc.resolve($from.before(depth)), doc.resolve($from.after(depth)), depth - 1);
2259
- wrapping = findWrapping(range, type);
2260
- }
2261
- if (!range || !wrapping)
2262
- return;
2263
- view.dispatch(view.state.tr.wrap(range, wrapping));
2264
- }
2265
- unquote() {
2266
- const view = this.editor.ctx.get(editorViewCtx);
2267
- const { $from, $to } = textBounds(view.state);
2268
- const range = $from.blockRange($to, (node) => node.type.name === 'blockquote');
2269
- if (!range)
2270
- return;
2271
- const target = liftTarget(range);
2272
- if (target == null)
2273
- return;
2274
- view.dispatch(view.state.tr.lift(range, target));
2275
- }
2276
- format(command, arg) {
2277
- this.assertMutation();
2278
- if (this.literalSurface)
2279
- return;
2280
- const run = (cmd, payload) => this.editor.action(callCommand(cmd, payload));
2281
- const state = caretState(this.editor.ctx.get(editorViewCtx).state);
2282
- switch (command) {
2283
- case 'heading': {
2284
- const level = Number(arg ?? 1);
2285
- if (state.block.type === 'heading' && state.block.level === level)
2286
- run(turnIntoTextCommand.key);
2287
- else
2288
- run(wrapInHeadingCommand.key, level);
2289
- break;
2290
- }
2291
- case 'paragraph':
2292
- run(turnIntoTextCommand.key);
2293
- break;
2294
- case 'bold':
2295
- run(toggleStrongCommand.key);
2296
- break;
2297
- case 'italic':
2298
- run(toggleEmphasisCommand.key);
2299
- break;
2300
- case 'strikethrough':
2301
- run(toggleStrikethroughCommand.key);
2302
- break;
2303
- case 'highlight':
2304
- run(toggleHighlightCommand.key);
2305
- break;
2306
- case 'code':
2307
- this.toggleInlineCode();
2308
- break;
2309
- case 'codeBlock':
2310
- if (state.block.type === 'codeBlock')
2311
- run(turnIntoTextCommand.key);
2312
- else
2313
- run(createCodeBlockCommand.key, typeof arg === 'string' ? arg : '');
2314
- break;
2315
- case 'quote':
2316
- if (state.quoted)
2317
- this.unquote();
2318
- else
2319
- this.quote();
2320
- break;
2321
- case 'bulletList':
2322
- case 'orderedList':
2323
- this.list(command);
2324
- break;
2325
- case 'taskList':
2326
- toggleTaskList(this.editor.ctx);
2327
- break;
2328
- case 'link': {
2329
- const view = this.editor.ctx.get(editorViewCtx);
2330
- const reference = view.state.selection.$from
2331
- .marks()
2332
- .find((mark) => mark.type.name === 'link' && mark.attrs.identifier) ??
2333
- view.state.doc
2334
- .nodeAt(view.state.selection.from)
2335
- ?.marks.find((mark) => mark.type.name === 'link' && mark.attrs.identifier);
2336
- if (reference && typeof arg === 'string') {
2337
- this.editReferenceDefinition(String(reference.attrs.identifier), arg);
2338
- break;
2339
- }
2340
- const payload = typeof arg === 'string' ? { href: arg } : {};
2341
- const toggle = () => run(toggleLinkCommand.key, payload);
2342
- if (!state.marks.includes('link') ||
2343
- !this.withMarkRunSelected(linkSchema.type(this.editor.ctx), toggle))
2344
- toggle();
2345
- break;
2346
- }
2347
- }
2348
- this.focus();
2349
- }
2350
- editReferenceDefinition(label, destination, title) {
2351
- this.assertMutation();
2352
- if (this.literalSurface)
2353
- return false;
2354
- const view = this.editor.ctx.get(editorViewCtx);
2355
- const definitions = referenceDefinitions(view.state.doc);
2356
- const definition = definitions.get(normaliseLabel(label)) ??
2357
- [...definitions.values()].find(({ node }) => normaliseLabel(String(node.attrs.label)) === normaliseLabel(label));
2358
- if (!definition)
2359
- return false;
2360
- view.dispatch(view.state.tr.setNodeMarkup(definition.pos, undefined, {
2361
- ...definition.node.attrs,
2362
- url: destination,
2363
- title: title ?? definition.node.attrs.title,
2364
- }));
2365
- return true;
2366
- }
2367
- insertFootnote(label) {
2368
- this.assertMutation();
2369
- if (this.literalSurface)
2370
- return;
2371
- const view = this.editor.ctx.get(editorViewCtx);
2372
- const definitions = footnoteDefinitions(view.state.doc);
2373
- if (label == null) {
2374
- let number = 1;
2375
- const labels = new Set(definitions.keys());
2376
- for (const match of this.snapshot().text.matchAll(/\[\^([^\]\r\n]+)\]/g))
2377
- labels.add(normaliseLabel(match[1]));
2378
- view.state.doc.descendants((node) => {
2379
- if (node.type.name === 'footnote_reference')
2380
- labels.add(String(node.attrs.identifier));
2381
- });
2382
- while (labels.has(normaliseLabel(String(number))))
2383
- number++;
2384
- label = String(number);
2385
- }
2386
- if (!label || /[\s\[\]\\]/.test(label) || label.length > 999)
2387
- throw new Error('A footnote label must be 1–999 characters without whitespace, brackets or backslashes');
2388
- const identifier = normaliseLabel(label);
2389
- const reference = view.state.schema.nodes.footnote_reference.create({
2390
- label,
2391
- identifier,
2392
- });
2393
- const tr = view.state.tr.insert(view.state.selection.to, reference);
2394
- if (!definitions.has(identifier)) {
2395
- const definition = view.state.schema.nodes.footnote_definition.create({ label, identifier }, view.state.schema.nodes.paragraph.create());
2396
- const position = tr.doc.content.size;
2397
- tr.insert(position, definition).setSelection(TextSelection.create(tr.doc, position + 2));
2398
- }
2399
- view.dispatch(tr.scrollIntoView());
2400
- this.focus();
2401
- }
2402
- navigateFootnote(target) {
2403
- this.assertCurrent();
2404
- if (this.literalSurface)
2405
- return false;
2406
- const view = this.editor.ctx.get(editorViewCtx);
2407
- const { $from } = view.state.selection;
2408
- let identifier;
2409
- for (let depth = $from.depth; depth > 0; depth--) {
2410
- const node = $from.node(depth);
2411
- if (node.type.name === 'footnote_definition')
2412
- identifier = String(node.attrs.identifier);
2413
- }
2414
- const at = view.state.doc.nodeAt(view.state.selection.from);
2415
- const adjacent = at?.type.name === 'footnote_reference' ? at : $from.nodeBefore;
2416
- if (adjacent?.type.name === 'footnote_reference')
2417
- identifier = String(adjacent.attrs.identifier);
2418
- if (!identifier)
2419
- return false;
2420
- let position;
2421
- if (target === 'definition') {
2422
- const found = footnoteDefinitions(view.state.doc).get(identifier);
2423
- if (found)
2424
- position = found.pos + 2;
2425
- }
2426
- else
2427
- view.state.doc.descendants((node, pos) => {
2428
- if (position == null &&
2429
- node.type.name === 'footnote_reference' &&
2430
- node.attrs.identifier === identifier)
2431
- position = pos;
2432
- });
2433
- if (position == null)
2434
- return false;
2435
- view.dispatch(view.state.tr
2436
- .setSelection(TextSelection.near(view.state.doc.resolve(position)))
2437
- .scrollIntoView());
2438
- this.focus();
2439
- return true;
2440
- }
2441
- }