@aicayzer/inkkit 0.0.18 → 0.1.1

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