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