@aicayzer/inkkit 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/callouts.d.ts +0 -1
- package/dist/callouts.js +12 -35
- package/dist/code-fences.js +6 -1
- package/dist/comments.d.ts +21 -0
- package/dist/comments.js +55 -4
- package/dist/core.d.ts +7 -0
- package/dist/core.js +2 -0
- package/dist/cursor.d.ts +2 -0
- package/dist/cursor.js +40 -0
- package/dist/dialect.js +100 -12
- package/dist/drop-placement.d.ts +19 -0
- package/dist/drop-placement.js +47 -0
- package/dist/editor.d.ts +32 -1
- package/dist/editor.js +293 -61
- package/dist/html-import.d.ts +13 -0
- package/dist/html-import.js +477 -0
- package/dist/image-blocks.d.ts +15 -0
- package/dist/image-blocks.js +44 -1
- package/dist/index.d.ts +1 -7
- package/dist/index.js +3 -2
- package/dist/link-safety.d.ts +17 -0
- package/dist/link-safety.js +29 -0
- package/dist/literals.d.ts +1 -0
- package/dist/literals.js +14 -9
- package/dist/mermaid.d.ts +21 -1
- package/dist/mermaid.js +92 -9
- package/dist/paste-html.d.ts +8 -0
- package/dist/paste-html.js +21 -0
- package/dist/paste.d.ts +7 -0
- package/dist/paste.js +152 -43
- package/dist/preserve.d.ts +20 -3
- package/dist/preserve.js +793 -154
- package/dist/print.js +2 -5
- package/dist/references.d.ts +2 -0
- package/dist/references.js +63 -13
- package/dist/scroll.d.ts +15 -0
- package/dist/scroll.js +27 -0
- package/dist/spreadsheet.d.ts +6 -1
- package/dist/spreadsheet.js +29 -15
- package/dist/style.css +11 -19
- package/dist/tables.js +24 -2
- package/package.json +12 -4
package/dist/editor.d.ts
CHANGED
|
@@ -100,6 +100,12 @@ export declare class InkKitEditor {
|
|
|
100
100
|
private plainSearch;
|
|
101
101
|
private literalMatch?;
|
|
102
102
|
private beforePlainInput?;
|
|
103
|
+
private pendingSource?;
|
|
104
|
+
private typedSteps;
|
|
105
|
+
private undoneSteps;
|
|
106
|
+
private formattedUndone;
|
|
107
|
+
private lastSourceInput;
|
|
108
|
+
private syncingSource;
|
|
103
109
|
private mode;
|
|
104
110
|
private sourcePreservations;
|
|
105
111
|
private sourceTexts;
|
|
@@ -154,11 +160,33 @@ export declare class InkKitEditor {
|
|
|
154
160
|
private updateSurface;
|
|
155
161
|
replaceSource(text: string, expectedGeneration?: number): boolean;
|
|
156
162
|
private applySource;
|
|
163
|
+
/**
|
|
164
|
+
* Takes typed source without parsing it. Parsing, preservation and replacing
|
|
165
|
+
* the formatted document cost as much as a document load, so they wait for
|
|
166
|
+
* the next thing that needs them.
|
|
167
|
+
*/
|
|
168
|
+
private deferSource;
|
|
169
|
+
private rememberTypedStep;
|
|
170
|
+
private showTypedSource;
|
|
171
|
+
/**
|
|
172
|
+
* Undoes or redoes typed source that the formatted document has not taken
|
|
173
|
+
* in, on the text alone. Returns whether there was a step to take.
|
|
174
|
+
*/
|
|
175
|
+
private stepTypedSource;
|
|
176
|
+
/** Brings the formatted document up to the source that was typed. */
|
|
177
|
+
private settleSource;
|
|
178
|
+
private commitSource;
|
|
157
179
|
undo(expectedGeneration?: number): boolean;
|
|
158
180
|
redo(expectedGeneration?: number): boolean;
|
|
159
181
|
private replaceLiteralSelection;
|
|
160
182
|
snapshot(expectedGeneration?: number): DocumentSnapshot;
|
|
161
183
|
printableSnapshot(expectedGeneration?: number): Promise<PrintableDocument>;
|
|
184
|
+
/**
|
|
185
|
+
* The document to export. Source typed in the source editor has not reached
|
|
186
|
+
* the formatted document, which would merge its undo steps, so a document
|
|
187
|
+
* read from the text stands in without touching history.
|
|
188
|
+
*/
|
|
189
|
+
private exportDocument;
|
|
162
190
|
clipboardSnapshot(all?: boolean): Promise<ClipboardOutput>;
|
|
163
191
|
paste(input: ClipboardInput): Promise<void>;
|
|
164
192
|
setCommentsVisible(visible: boolean): void;
|
|
@@ -180,7 +208,9 @@ export declare class InkKitEditor {
|
|
|
180
208
|
reveal?: boolean;
|
|
181
209
|
}): void;
|
|
182
210
|
/** Throws when a document reached by an edit, after every appended normalisation, cannot be
|
|
183
|
-
* saved as preserved Markdown. Dispatch and the replacement commands share this check.
|
|
211
|
+
* saved as preserved Markdown. Dispatch and the replacement commands share this check.
|
|
212
|
+
* `written` is false for history steps and source replacement, which restore or supply a
|
|
213
|
+
* document that may hold comment text the formatted editor would refuse to accept typed. */
|
|
184
214
|
private assertPreserved;
|
|
185
215
|
replaceTextRange(range: TextRange, text: string): boolean;
|
|
186
216
|
textRangeRects(range: TextRange): readonly TextRect[];
|
|
@@ -214,6 +244,7 @@ export declare class InkKitEditor {
|
|
|
214
244
|
alt: string;
|
|
215
245
|
title?: string;
|
|
216
246
|
}[], x?: number, y?: number): void;
|
|
247
|
+
private insertInline;
|
|
217
248
|
private insertBlocks;
|
|
218
249
|
private withMarkRunSelected;
|
|
219
250
|
private toggleInlineCode;
|
package/dist/editor.js
CHANGED
|
@@ -3,7 +3,8 @@ import { visibleClipboard, portableClipboard, clipboardText, clipboardContent, }
|
|
|
3
3
|
import { clipboard } from '@milkdown/kit/plugin/clipboard';
|
|
4
4
|
import { history, historyKeymap } from '@milkdown/kit/plugin/history';
|
|
5
5
|
import { closeHistory, undo as undoHistory, redo as redoHistory, } from '@milkdown/kit/prose/history';
|
|
6
|
-
import { cursor } from '
|
|
6
|
+
import { cursor } from './cursor.js';
|
|
7
|
+
import { blockDropRange } from './drop-placement.js';
|
|
7
8
|
import { blockquoteKeymap, blockquoteSchema, bulletListKeymap, codeBlockKeymap, createCodeBlockCommand, emphasisKeymap, headingKeymap, inlineCodeKeymap, inlineCodeSchema, linkSchema, listItemKeymap, orderedListKeymap, paragraphKeymap, strongKeymap, toggleEmphasisCommand, toggleInlineCodeCommand, toggleLinkCommand, toggleStrongCommand, turnIntoTextCommand, wrapInBulletListCommand, wrapInHeadingCommand, wrapInOrderedListCommand, } from '@milkdown/kit/preset/commonmark';
|
|
8
9
|
import { strikethroughKeymap, tableKeymap, toggleStrikethroughCommand, } from '@milkdown/kit/preset/gfm';
|
|
9
10
|
import { NodeRange, Fragment, Slice, } from '@milkdown/kit/prose/model';
|
|
@@ -18,10 +19,10 @@ import { labelsCtx, defaultLabels } from './labels.js';
|
|
|
18
19
|
import { createDialect, stringifyOptions } from './dialect.js';
|
|
19
20
|
import { Preservation } from './preserve.js';
|
|
20
21
|
import { sourceAttribute, cleanSourceDoc, editedPlainSource, sourceSelection, rawOffset, normalizedOffset, } from './source.js';
|
|
21
|
-
import { normaliseLabel, referenceDefinitions, footnoteDefinitions, } from './references.js';
|
|
22
|
+
import { normaliseLabel, referenceDefinitions, footnoteDefinitions, singleLine, } from './references.js';
|
|
22
23
|
import { selectionMarkdown, selectionContent, referenceMetadata, withReferenceMetadata, } from './reference-clipboard.js';
|
|
23
24
|
import { imageView } from './images.js';
|
|
24
|
-
import { isImage, isImageBlock } from './image-blocks.js';
|
|
25
|
+
import { imagePlacement, isImage, isImageBlock } from './image-blocks.js';
|
|
25
26
|
import { GapCursor } from '@milkdown/kit/prose/gapcursor';
|
|
26
27
|
import { wikiLinkView } from './wiki-view.js';
|
|
27
28
|
import { withFileSignal } from './files.js';
|
|
@@ -29,14 +30,15 @@ import { tablePlugins, tableCommand, tableCommands, tableAvailability, tableCont
|
|
|
29
30
|
import { InkKitError, } from './types.js';
|
|
30
31
|
import { readableProjection, projectionPosition, projectionOffset, validateOffsets, formattedRects, literalRangesRects, literalVisibleRanges, intersectsViewport, textRect, } from './text-ranges.js';
|
|
31
32
|
import { highlightKeymap, toggleHighlightCommand } from './inline-highlight.js';
|
|
32
|
-
import { commentSelectionContent, setCommentVisibility } from './comments.js';
|
|
33
|
+
import { assertCommentsEndWhereWritten, commentSelectionContent, isWrittenEdit, setCommentVisibility, sourceReplacement, } from './comments.js';
|
|
34
|
+
import { scrollOutward, scrollWholePixels } from './scroll.js';
|
|
33
35
|
import { PasteController } from './paste.js';
|
|
34
36
|
import { selectionPlugin } from './selection.js';
|
|
35
37
|
import { search, SearchQuery, setSearchState } from 'prosemirror-search';
|
|
36
38
|
import { findFormatted, findLiteral, replaceFormatted, replaceLiteral, replaceFormattedRange, } from './search.js';
|
|
37
39
|
import { collectFormattedHeadings, collectSourceHeadings, navigateFormattedHeading, sourceHeadingPosition, } from './outline.js';
|
|
38
40
|
import { taskListPlugin, toggleTaskList } from './tasks.js';
|
|
39
|
-
import { isMermaid, mermaidPreview } from './mermaid.js';
|
|
41
|
+
import { diagramsAvailable, isMermaid, mermaidPreview } from './mermaid.js';
|
|
40
42
|
import { hasAuthoredImagesInLiterals, printableMarkdown, printableText, } from './print.js';
|
|
41
43
|
const shortcutCommands = {
|
|
42
44
|
heading1: ['heading', 1],
|
|
@@ -244,6 +246,12 @@ function joinLists(tr, start, index) {
|
|
|
244
246
|
tr.join($start.posAtIndex(index));
|
|
245
247
|
}
|
|
246
248
|
let editorSequence = 0;
|
|
249
|
+
// A pause in source typing longer than this ends an undo step, as in the
|
|
250
|
+
// formatted editor's history.
|
|
251
|
+
const sourceGroupDelay = 500;
|
|
252
|
+
// Earlier undo steps of typed source that the formatted document has not taken
|
|
253
|
+
// in; the oldest merge into the next beyond this many.
|
|
254
|
+
const sourceStepLimit = 100;
|
|
247
255
|
export class InkKitEditor {
|
|
248
256
|
events;
|
|
249
257
|
options;
|
|
@@ -259,6 +267,21 @@ export class InkKitEditor {
|
|
|
259
267
|
plainSearch = '';
|
|
260
268
|
literalMatch;
|
|
261
269
|
beforePlainInput;
|
|
270
|
+
// Source typed in the source editor that the hidden formatted document has
|
|
271
|
+
// not taken in yet. The text is already authoritative: snapshots, change
|
|
272
|
+
// events and the textarea use it, and anything that reads the formatted
|
|
273
|
+
// document first settles it. Undo steps taken inside it need no document,
|
|
274
|
+
// so they run on the text alone: `typedSteps` hold the states at the end of
|
|
275
|
+
// earlier typing, `pendingSource` the latest, and `undoneSteps` what undo
|
|
276
|
+
// set aside.
|
|
277
|
+
pendingSource;
|
|
278
|
+
typedSteps = [];
|
|
279
|
+
undoneSteps = [];
|
|
280
|
+
// Formatted-history undos taken while source steps were undone, which a
|
|
281
|
+
// redo replays before it reaches those steps.
|
|
282
|
+
formattedUndone = 0;
|
|
283
|
+
lastSourceInput = 0;
|
|
284
|
+
syncingSource = false;
|
|
262
285
|
mode = 'formatted';
|
|
263
286
|
sourcePreservations = new WeakMap();
|
|
264
287
|
sourceTexts = new WeakMap();
|
|
@@ -298,6 +321,12 @@ export class InkKitEditor {
|
|
|
298
321
|
update: (view, previous) => {
|
|
299
322
|
if (this.loading || view.state.doc.eq(previous.doc))
|
|
300
323
|
return;
|
|
324
|
+
// Settling typed source brings the document up to a revision and a
|
|
325
|
+
// change event that were already reported.
|
|
326
|
+
if (this.syncingSource) {
|
|
327
|
+
this.mediaPolicy();
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
301
330
|
this.revision += 1;
|
|
302
331
|
this.cancelExports();
|
|
303
332
|
this.mediaPolicy();
|
|
@@ -533,8 +562,10 @@ export class InkKitEditor {
|
|
|
533
562
|
: caretState(state),
|
|
534
563
|
table: this.literalSurface ? undefined : tableContext(state),
|
|
535
564
|
commands: {
|
|
536
|
-
undo: mutable && undoHistory(state),
|
|
537
|
-
redo: mutable &&
|
|
565
|
+
undo: mutable && (!!this.pendingSource || undoHistory(state)),
|
|
566
|
+
redo: mutable &&
|
|
567
|
+
(this.undoneSteps.length > 0 ||
|
|
568
|
+
(!this.pendingSource && redoHistory(state))),
|
|
538
569
|
insertText: mutable,
|
|
539
570
|
paste: mutable,
|
|
540
571
|
replace: mutable,
|
|
@@ -568,6 +599,8 @@ export class InkKitEditor {
|
|
|
568
599
|
key: new PluginKey('inkkitInputPolicy'),
|
|
569
600
|
filterTransaction: (tr) => this.loading ||
|
|
570
601
|
this.writable ||
|
|
602
|
+
// Source that was typed while the editor was writable.
|
|
603
|
+
this.syncingSource ||
|
|
571
604
|
(!tr.docChanged && !tr.storedMarksSet),
|
|
572
605
|
props: {
|
|
573
606
|
editable: () => this.writable && !this.literalSurface,
|
|
@@ -603,7 +636,10 @@ export class InkKitEditor {
|
|
|
603
636
|
this.assertAlive();
|
|
604
637
|
return this.literalSurface ? 'source' : 'formatted';
|
|
605
638
|
}
|
|
606
|
-
currentText(doc
|
|
639
|
+
currentText(doc) {
|
|
640
|
+
if (!doc && this.pendingSource)
|
|
641
|
+
return this.pendingSource.text;
|
|
642
|
+
doc ??= this.editor.ctx.get(editorViewCtx).state.doc;
|
|
607
643
|
const cached = this.sourceTexts.get(doc);
|
|
608
644
|
if (cached !== undefined)
|
|
609
645
|
return cached;
|
|
@@ -628,6 +664,10 @@ export class InkKitEditor {
|
|
|
628
664
|
throw new RangeError('Editing mode must be source or formatted');
|
|
629
665
|
if (this.formatType === 'txt' || this.mode === mode)
|
|
630
666
|
return false;
|
|
667
|
+
this.settleSource();
|
|
668
|
+
// Source steps that were undone cannot be redone from the other mode.
|
|
669
|
+
this.undoneSteps = [];
|
|
670
|
+
this.formattedUndone = 0;
|
|
631
671
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
632
672
|
const focused = this.literalSurface
|
|
633
673
|
? this.plain.ownerDocument.activeElement === this.plain
|
|
@@ -681,11 +721,142 @@ export class InkKitEditor {
|
|
|
681
721
|
this.assertMutation(expectedGeneration);
|
|
682
722
|
return this.applySource(text, true);
|
|
683
723
|
}
|
|
684
|
-
applySource(text, isolated, from, to) {
|
|
724
|
+
applySource(text, isolated, from, to, native = false) {
|
|
685
725
|
if (!this.writable)
|
|
686
726
|
throw new InkKitError('read-only', 'The editor is read-only');
|
|
687
727
|
if (typeof text !== 'string')
|
|
688
728
|
throw new TypeError('Source must be a string');
|
|
729
|
+
// Source edits take the same steps whether typed or not; an edit that is
|
|
730
|
+
// not typing ends the typing step before it and is closed to the next.
|
|
731
|
+
if (this.literalSurface)
|
|
732
|
+
return this.deferSource(text, from, to, native, isolated);
|
|
733
|
+
return this.commitSource(text, isolated, from, to, this.beforePlainInput);
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Takes typed source without parsing it. Parsing, preservation and replacing
|
|
737
|
+
* the formatted document cost as much as a document load, so they wait for
|
|
738
|
+
* the next thing that needs them.
|
|
739
|
+
*/
|
|
740
|
+
deferSource(text, from, to, native, closed) {
|
|
741
|
+
if (text === this.currentText())
|
|
742
|
+
return false;
|
|
743
|
+
const length = from === undefined ? text.replace(/\r\n?/g, '\n').length : from;
|
|
744
|
+
const now = Date.now();
|
|
745
|
+
const previous = this.pendingSource;
|
|
746
|
+
const continuing = previous &&
|
|
747
|
+
!previous.closed &&
|
|
748
|
+
!closed &&
|
|
749
|
+
now - this.lastSourceInput <= sourceGroupDelay;
|
|
750
|
+
if (previous && !continuing)
|
|
751
|
+
this.rememberTypedStep(previous);
|
|
752
|
+
this.lastSourceInput = now;
|
|
753
|
+
this.undoneSteps = [];
|
|
754
|
+
this.formattedUndone = 0;
|
|
755
|
+
const pending = {
|
|
756
|
+
text,
|
|
757
|
+
from: length,
|
|
758
|
+
to: to ?? length,
|
|
759
|
+
before: (continuing ? previous.before : undefined) ??
|
|
760
|
+
this.beforePlainInput ?? {
|
|
761
|
+
from: this.plain.selectionStart,
|
|
762
|
+
to: this.plain.selectionEnd,
|
|
763
|
+
},
|
|
764
|
+
closed,
|
|
765
|
+
};
|
|
766
|
+
this.pendingSource = pending;
|
|
767
|
+
this.revision += 1;
|
|
768
|
+
this.cancelExports();
|
|
769
|
+
this.literalMatch = undefined;
|
|
770
|
+
// Text that did not come from the textarea is shown there.
|
|
771
|
+
if (!native)
|
|
772
|
+
this.showTypedSource(pending.text, pending.from, pending.to);
|
|
773
|
+
if (text !== this.lastMarkdown) {
|
|
774
|
+
this.lastMarkdown = text;
|
|
775
|
+
this.events.changed(text, this.generation);
|
|
776
|
+
}
|
|
777
|
+
this.publishCommandState();
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
rememberTypedStep(step) {
|
|
781
|
+
this.typedSteps.push(step);
|
|
782
|
+
if (this.typedSteps.length > sourceStepLimit)
|
|
783
|
+
this.typedSteps.shift();
|
|
784
|
+
}
|
|
785
|
+
showTypedSource(text, from, to) {
|
|
786
|
+
this.plain.value = text;
|
|
787
|
+
this.plain.setSelectionRange(Math.min(from, this.plain.value.length), Math.min(to, this.plain.value.length));
|
|
788
|
+
this.revealLiteralSelection();
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Undoes or redoes typed source that the formatted document has not taken
|
|
792
|
+
* in, on the text alone. Returns whether there was a step to take.
|
|
793
|
+
*/
|
|
794
|
+
stepTypedSource(direction) {
|
|
795
|
+
const current = this.pendingSource;
|
|
796
|
+
let text, from, to;
|
|
797
|
+
if (direction === 'undo') {
|
|
798
|
+
if (!current)
|
|
799
|
+
return false;
|
|
800
|
+
this.undoneSteps.push(current);
|
|
801
|
+
this.pendingSource = this.typedSteps.pop();
|
|
802
|
+
text = this.pendingSource?.text ?? this.currentText();
|
|
803
|
+
({ from, to } = current.before);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
const next = this.undoneSteps.pop();
|
|
807
|
+
if (!next)
|
|
808
|
+
return false;
|
|
809
|
+
if (current)
|
|
810
|
+
this.rememberTypedStep(current);
|
|
811
|
+
this.pendingSource = next;
|
|
812
|
+
text = next.text;
|
|
813
|
+
({ from, to } = next);
|
|
814
|
+
}
|
|
815
|
+
this.lastSourceInput = 0;
|
|
816
|
+
this.revision += 1;
|
|
817
|
+
this.cancelExports();
|
|
818
|
+
this.literalMatch = undefined;
|
|
819
|
+
this.showTypedSource(text, from, to);
|
|
820
|
+
if (text !== this.lastMarkdown) {
|
|
821
|
+
this.lastMarkdown = text;
|
|
822
|
+
this.events.changed(text, this.generation);
|
|
823
|
+
}
|
|
824
|
+
this.publishCommandState();
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
/** Brings the formatted document up to the source that was typed. */
|
|
828
|
+
settleSource() {
|
|
829
|
+
const pending = this.pendingSource;
|
|
830
|
+
if (!pending)
|
|
831
|
+
return;
|
|
832
|
+
const steps = this.typedSteps, undone = this.undoneSteps;
|
|
833
|
+
this.pendingSource = undefined;
|
|
834
|
+
this.typedSteps = [];
|
|
835
|
+
this.undoneSteps = [];
|
|
836
|
+
this.syncingSource = true;
|
|
837
|
+
try {
|
|
838
|
+
if (this.commitSource(pending.text,
|
|
839
|
+
// A step of its own, whatever edit follows within the group delay.
|
|
840
|
+
true, pending.from, pending.to, steps[0]?.before ?? pending.before)) {
|
|
841
|
+
const { doc } = this.editor.ctx.get(editorViewCtx).state;
|
|
842
|
+
const applied = doc.attrs[sourceAttribute];
|
|
843
|
+
// A plugin that refuses the change must not lose what was typed.
|
|
844
|
+
if (applied?.text !== pending.text)
|
|
845
|
+
throw new InkKitError('preservation', 'The typed source could not be applied');
|
|
846
|
+
this.sourceTexts.set(doc, pending.text);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
this.pendingSource = pending;
|
|
851
|
+
this.typedSteps = steps;
|
|
852
|
+
this.undoneSteps = undone;
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
finally {
|
|
856
|
+
this.syncingSource = false;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
commitSource(text, isolated, from, to, before) {
|
|
689
860
|
if (text === this.currentText())
|
|
690
861
|
return false;
|
|
691
862
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
@@ -712,8 +883,8 @@ export class InkKitEditor {
|
|
|
712
883
|
const previous = Object.freeze({
|
|
713
884
|
text: this.currentText(),
|
|
714
885
|
format: this.formatType,
|
|
715
|
-
from:
|
|
716
|
-
to:
|
|
886
|
+
from: before?.from ?? this.plain.selectionStart,
|
|
887
|
+
to: before?.to ?? this.plain.selectionEnd,
|
|
717
888
|
});
|
|
718
889
|
// History must retain the source caret even when the formatted document has no corresponding position.
|
|
719
890
|
this.loading = true;
|
|
@@ -728,7 +899,8 @@ export class InkKitEditor {
|
|
|
728
899
|
}
|
|
729
900
|
let tr = view.state.tr
|
|
730
901
|
.replaceWith(0, view.state.doc.content.size, parsed.content)
|
|
731
|
-
.setDocAttribute(sourceAttribute, provenance)
|
|
902
|
+
.setDocAttribute(sourceAttribute, provenance)
|
|
903
|
+
.setMeta(sourceReplacement, true);
|
|
732
904
|
tr.setSelection(Selection.atEnd(tr.doc));
|
|
733
905
|
if (isolated)
|
|
734
906
|
tr = closeHistory(tr);
|
|
@@ -739,12 +911,35 @@ export class InkKitEditor {
|
|
|
739
911
|
}
|
|
740
912
|
undo(expectedGeneration) {
|
|
741
913
|
this.assertMutation(expectedGeneration);
|
|
914
|
+
if (this.literalSurface && this.stepTypedSource('undo'))
|
|
915
|
+
return true;
|
|
742
916
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
743
|
-
|
|
917
|
+
const undone = undoHistory(view.state, view.dispatch);
|
|
918
|
+
if (undone) {
|
|
919
|
+
// Source steps that were undone come back after the formatted ones.
|
|
920
|
+
if (this.literalSurface && this.undoneSteps.length)
|
|
921
|
+
this.formattedUndone++;
|
|
922
|
+
else
|
|
923
|
+
this.undoneSteps = [];
|
|
924
|
+
}
|
|
925
|
+
return undone;
|
|
744
926
|
}
|
|
745
927
|
redo(expectedGeneration) {
|
|
746
928
|
this.assertMutation(expectedGeneration);
|
|
747
929
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
930
|
+
if (this.literalSurface) {
|
|
931
|
+
if (this.formattedUndone > 0) {
|
|
932
|
+
this.formattedUndone--;
|
|
933
|
+
if (redoHistory(view.state, view.dispatch))
|
|
934
|
+
return true;
|
|
935
|
+
this.formattedUndone = 0;
|
|
936
|
+
}
|
|
937
|
+
if (this.stepTypedSource('redo'))
|
|
938
|
+
return true;
|
|
939
|
+
// Typing after an undo ended the formatted history's redo steps.
|
|
940
|
+
if (this.pendingSource)
|
|
941
|
+
return false;
|
|
942
|
+
}
|
|
748
943
|
return redoHistory(view.state, view.dispatch);
|
|
749
944
|
}
|
|
750
945
|
replaceLiteralSelection(text, isolated = true, retainEndings = false) {
|
|
@@ -781,15 +976,16 @@ export class InkKitEditor {
|
|
|
781
976
|
async printableSnapshot(expectedGeneration) {
|
|
782
977
|
const snapshot = this.snapshot(expectedGeneration);
|
|
783
978
|
const state = this.editor.ctx.get(editorViewCtx).state;
|
|
979
|
+
const doc = this.exportDocument(snapshot.text);
|
|
784
980
|
const output = await this.exportContent(snapshot, async (signal) => {
|
|
785
981
|
if (snapshot.format === 'md' &&
|
|
786
982
|
!this.options.images &&
|
|
787
983
|
!this.options.files &&
|
|
788
|
-
hasAuthoredImagesInLiterals(
|
|
984
|
+
hasAuthoredImagesInLiterals(doc.content, (source) => this.editor.ctx.get(remarkCtx).parse(source)))
|
|
789
985
|
throw new InkKitError('image-unavailable', 'No image adapter is configured for the authored images.');
|
|
790
986
|
return snapshot.format === 'txt'
|
|
791
987
|
? printableText(snapshot.text)
|
|
792
|
-
: printableMarkdown(
|
|
988
|
+
: printableMarkdown(doc.content, state.schema, this.options.images, this.context(), this.options.files, signal);
|
|
793
989
|
});
|
|
794
990
|
return Object.freeze({
|
|
795
991
|
documentId: snapshot.documentId,
|
|
@@ -805,6 +1001,17 @@ export class InkKitEditor {
|
|
|
805
1001
|
warnings: Object.freeze(output.warnings.map((warning) => Object.freeze({ ...warning }))),
|
|
806
1002
|
});
|
|
807
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* The document to export. Source typed in the source editor has not reached
|
|
1006
|
+
* the formatted document, which would merge its undo steps, so a document
|
|
1007
|
+
* read from the text stands in without touching history.
|
|
1008
|
+
*/
|
|
1009
|
+
exportDocument(text) {
|
|
1010
|
+
const { doc } = this.editor.ctx.get(editorViewCtx).state;
|
|
1011
|
+
return this.pendingSource && this.formatType === 'md'
|
|
1012
|
+
? this.editor.ctx.get(parserCtx)(text)
|
|
1013
|
+
: doc;
|
|
1014
|
+
}
|
|
808
1015
|
async clipboardSnapshot(all = true) {
|
|
809
1016
|
const snapshot = this.snapshot();
|
|
810
1017
|
const epoch = this.documentEpoch;
|
|
@@ -822,7 +1029,11 @@ export class InkKitEditor {
|
|
|
822
1029
|
diagrams: [],
|
|
823
1030
|
};
|
|
824
1031
|
}
|
|
825
|
-
const {
|
|
1032
|
+
const { schema, selection } = this.editor.ctx.get(editorViewCtx).state;
|
|
1033
|
+
// A partial copy in the source editor reads only the selected source.
|
|
1034
|
+
const doc = all || this.mode !== 'source'
|
|
1035
|
+
? this.exportDocument(snapshot.text)
|
|
1036
|
+
: this.editor.ctx.get(editorViewCtx).state.doc;
|
|
826
1037
|
const selectedSource = !all && this.mode === 'source'
|
|
827
1038
|
? sourceSelection(snapshot.text, this.plain.selectionStart, this.plain.selectionEnd)
|
|
828
1039
|
: undefined;
|
|
@@ -874,7 +1085,9 @@ export class InkKitEditor {
|
|
|
874
1085
|
throw new InkKitError('stale-document', 'Document changed');
|
|
875
1086
|
if (result.images.some((image) => image.error))
|
|
876
1087
|
this.events.error?.(new InkKitError('image-unavailable', 'Some copied images were unavailable; their descriptions were retained'));
|
|
877
|
-
|
|
1088
|
+
// A host built without Mermaid chose not to render diagrams.
|
|
1089
|
+
if (diagramsAvailable() &&
|
|
1090
|
+
result.diagrams?.some((diagram) => diagram.error))
|
|
878
1091
|
this.events.error?.(new InkKitError('diagram-unavailable', 'Some diagrams could not be exported; their source was retained'));
|
|
879
1092
|
return result;
|
|
880
1093
|
}
|
|
@@ -955,6 +1168,10 @@ export class InkKitEditor {
|
|
|
955
1168
|
return;
|
|
956
1169
|
this.destroyed = true;
|
|
957
1170
|
this.ready = false;
|
|
1171
|
+
this.pendingSource = undefined;
|
|
1172
|
+
this.typedSteps = [];
|
|
1173
|
+
this.undoneSteps = [];
|
|
1174
|
+
this.formattedUndone = 0;
|
|
958
1175
|
this.cancelExports();
|
|
959
1176
|
this.mediaPolicy();
|
|
960
1177
|
this.pasteController.destroy();
|
|
@@ -1095,7 +1312,7 @@ export class InkKitEditor {
|
|
|
1095
1312
|
const next = this.state.apply(tr);
|
|
1096
1313
|
if (!instance.loading) {
|
|
1097
1314
|
try {
|
|
1098
|
-
instance.assertPreserved(this.state.doc, next.doc);
|
|
1315
|
+
instance.assertPreserved(this.state.doc, next.doc, isWrittenEdit(tr));
|
|
1099
1316
|
}
|
|
1100
1317
|
catch (error) {
|
|
1101
1318
|
this.updateState(this.state);
|
|
@@ -1132,8 +1349,8 @@ export class InkKitEditor {
|
|
|
1132
1349
|
.use(placeholderPlugin)
|
|
1133
1350
|
.use(options.rendering?.codeHighlighting === false
|
|
1134
1351
|
? []
|
|
1135
|
-
: (await import('./highlight')).highlightPlugin)
|
|
1136
|
-
.use(options.rendering?.diagramPreview === false
|
|
1352
|
+
: (await import('./highlight.js')).highlightPlugin)
|
|
1353
|
+
.use(options.rendering?.diagramPreview === false || !diagramsAvailable()
|
|
1137
1354
|
? []
|
|
1138
1355
|
: mermaidPreview((error) => events.error?.(error)))
|
|
1139
1356
|
.use(selectionPlugin)
|
|
@@ -1224,7 +1441,7 @@ export class InkKitEditor {
|
|
|
1224
1441
|
return;
|
|
1225
1442
|
}
|
|
1226
1443
|
try {
|
|
1227
|
-
instance.applySource(editedPlainSource(instance.currentText(), instance.plain.value), false, instance.plain.selectionStart, instance.plain.selectionEnd);
|
|
1444
|
+
instance.applySource(editedPlainSource(instance.currentText(), instance.plain.value), false, instance.plain.selectionStart, instance.plain.selectionEnd, true);
|
|
1228
1445
|
}
|
|
1229
1446
|
catch (error) {
|
|
1230
1447
|
events.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -1402,6 +1619,10 @@ export class InkKitEditor {
|
|
|
1402
1619
|
this.outlineEpoch += 1;
|
|
1403
1620
|
this.sourcePreservations = new WeakMap();
|
|
1404
1621
|
this.sourceTexts = new WeakMap();
|
|
1622
|
+
this.pendingSource = undefined;
|
|
1623
|
+
this.typedSteps = [];
|
|
1624
|
+
this.undoneSteps = [];
|
|
1625
|
+
this.formattedUndone = 0;
|
|
1405
1626
|
this.mode = 'formatted';
|
|
1406
1627
|
this.plainComposing = false;
|
|
1407
1628
|
this.beforePlainInput = undefined;
|
|
@@ -1605,9 +1826,15 @@ export class InkKitEditor {
|
|
|
1605
1826
|
this.publishCommandState();
|
|
1606
1827
|
}
|
|
1607
1828
|
/** 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
|
-
|
|
1610
|
-
|
|
1829
|
+
* saved as preserved Markdown. Dispatch and the replacement commands share this check.
|
|
1830
|
+
* `written` is false for history steps and source replacement, which restore or supply a
|
|
1831
|
+
* document that may hold comment text the formatted editor would refuse to accept typed. */
|
|
1832
|
+
assertPreserved(before, after, written = true) {
|
|
1833
|
+
if (this.literalSurface || after.eq(before))
|
|
1834
|
+
return;
|
|
1835
|
+
if (written)
|
|
1836
|
+
assertCommentsEndWhereWritten(before, after);
|
|
1837
|
+
if (this.preservation)
|
|
1611
1838
|
this.currentText(after);
|
|
1612
1839
|
}
|
|
1613
1840
|
replaceTextRange(range, text) {
|
|
@@ -1742,26 +1969,30 @@ export class InkKitEditor {
|
|
|
1742
1969
|
: 0;
|
|
1743
1970
|
let remainingTop = vertical, remainingLeft = horizontal;
|
|
1744
1971
|
if (this.literalSurface) {
|
|
1745
|
-
const
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1972
|
+
const style = this.root.ownerDocument.defaultView.getComputedStyle(this.plain);
|
|
1973
|
+
// The first or last line sits within the padding plus its own leading,
|
|
1974
|
+
// under half the height of its first rect, so a reveal that ends there
|
|
1975
|
+
// shows the edge. A range of many lines does not count as one line.
|
|
1976
|
+
const halfCaret = (leading.bottom - leading.top) / 2;
|
|
1977
|
+
const remaining = scrollWholePixels(this.plain, vertical, horizontal, {
|
|
1978
|
+
top: (Number.parseFloat(style.paddingTop) || 0) + halfCaret,
|
|
1979
|
+
bottom: (Number.parseFloat(style.paddingBottom) || 0) + halfCaret,
|
|
1980
|
+
});
|
|
1981
|
+
remainingTop = remaining.vertical;
|
|
1982
|
+
remainingLeft = remaining.horizontal;
|
|
1750
1983
|
}
|
|
1751
1984
|
let container = this.viewportContainer ?? this.root;
|
|
1752
1985
|
const document = this.root.ownerDocument;
|
|
1753
1986
|
while (container &&
|
|
1754
1987
|
container !== document.body &&
|
|
1755
1988
|
container !== document.documentElement) {
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
remainingTop -= container.scrollTop - top;
|
|
1760
|
-
remainingLeft -= container.scrollLeft - left;
|
|
1989
|
+
;
|
|
1990
|
+
({ vertical: remainingTop, horizontal: remainingLeft } =
|
|
1991
|
+
scrollWholePixels(container, remainingTop, remainingLeft));
|
|
1761
1992
|
container = container.parentElement;
|
|
1762
1993
|
}
|
|
1763
1994
|
if (remainingTop || remainingLeft)
|
|
1764
|
-
document.defaultView.scrollBy(remainingLeft, remainingTop);
|
|
1995
|
+
document.defaultView.scrollBy(scrollOutward(remainingLeft), scrollOutward(remainingTop));
|
|
1765
1996
|
}
|
|
1766
1997
|
revealCurrentSelection() {
|
|
1767
1998
|
if (this.loading || !this.ready)
|
|
@@ -2102,6 +2333,11 @@ export class InkKitEditor {
|
|
|
2102
2333
|
throw new InkKitError('image-unavailable', 'This editor has no image adapter');
|
|
2103
2334
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
2104
2335
|
const { schema } = view.state;
|
|
2336
|
+
const placement = x != null && y != null ? undefined : imagePlacement(view.state);
|
|
2337
|
+
if (placement?.kind === 'inline') {
|
|
2338
|
+
this.insertInline(images);
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2105
2341
|
// One paragraph per image: each is read back as a standalone image block where one fits.
|
|
2106
2342
|
const blocks = images.map((image) => schema.nodes.paragraph.create(null, schema.nodes.image.create({
|
|
2107
2343
|
src: image.path,
|
|
@@ -2112,7 +2348,9 @@ export class InkKitEditor {
|
|
|
2112
2348
|
this.insertBlocks(blocks, x, y);
|
|
2113
2349
|
return;
|
|
2114
2350
|
}
|
|
2115
|
-
const tr =
|
|
2351
|
+
const tr = placement?.kind === 'after'
|
|
2352
|
+
? view.state.tr.insert(placement.pos, blocks)
|
|
2353
|
+
: view.state.tr.replaceSelection(new Slice(Fragment.fromArray(blocks), 0, 0));
|
|
2116
2354
|
// After the last image, so typing continues below it rather than in the block that follows.
|
|
2117
2355
|
const last = blocks.at(-1).firstChild;
|
|
2118
2356
|
let after;
|
|
@@ -2125,32 +2363,26 @@ export class InkKitEditor {
|
|
|
2125
2363
|
tr.setSelection(TextSelection.create(tr.doc, after));
|
|
2126
2364
|
view.dispatch(tr.scrollIntoView());
|
|
2127
2365
|
}
|
|
2128
|
-
|
|
2366
|
+
// A table cell holds one line, so the images sit side by side where the selection was, with no
|
|
2367
|
+
// space between them: a space beside an image in a cell is saved as ` `.
|
|
2368
|
+
insertInline(images) {
|
|
2129
2369
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
2130
2370
|
const { state } = view;
|
|
2131
|
-
const
|
|
2132
|
-
const
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
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
|
-
}
|
|
2371
|
+
const oneLine = (text) => text.replace(/\s*[\r\n]+\s*/g, ' ');
|
|
2372
|
+
const nodes = images.map((image) => state.schema.nodes.image.create({
|
|
2373
|
+
src: image.path,
|
|
2374
|
+
alt: oneLine(image.alt),
|
|
2375
|
+
title: oneLine(image.title ?? ''),
|
|
2376
|
+
}));
|
|
2377
|
+
const { from, to } = state.selection;
|
|
2378
|
+
const tr = state.tr.replaceWith(from, to, nodes);
|
|
2379
|
+
tr.setSelection(TextSelection.create(tr.doc, from + nodes.length));
|
|
2380
|
+
view.dispatch(tr.scrollIntoView());
|
|
2381
|
+
}
|
|
2382
|
+
insertBlocks(blocks, x, y) {
|
|
2383
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
2384
|
+
const { from: at, to } = blockDropRange(view, x, y);
|
|
2385
|
+
const tr = view.state.tr.replaceWith(at, to, blocks);
|
|
2154
2386
|
const end = at + blocks.reduce((size, node) => size + node.nodeSize, 0) - 1;
|
|
2155
2387
|
view.dispatch(tr.setSelection(TextSelection.create(tr.doc, end)).scrollIntoView());
|
|
2156
2388
|
this.focus();
|
|
@@ -2360,7 +2592,7 @@ export class InkKitEditor {
|
|
|
2360
2592
|
view.dispatch(view.state.tr.setNodeMarkup(definition.pos, undefined, {
|
|
2361
2593
|
...definition.node.attrs,
|
|
2362
2594
|
url: destination,
|
|
2363
|
-
title: title
|
|
2595
|
+
title: title == null ? definition.node.attrs.title : singleLine(title),
|
|
2364
2596
|
}));
|
|
2365
2597
|
return true;
|
|
2366
2598
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Ctx } from '@milkdown/kit/ctx';
|
|
2
|
+
import { Fragment, Slice, type Node, type Schema } from '@milkdown/kit/prose/model';
|
|
3
|
+
/**
|
|
4
|
+
* The slice's nodes as blocks a document holds, wrapped as insertion would
|
|
5
|
+
* wrap them: inline nodes in a paragraph and list items in a list.
|
|
6
|
+
*/
|
|
7
|
+
export declare function topLevel(schema: Schema, content: Fragment): Node[];
|
|
8
|
+
/**
|
|
9
|
+
* Returns the slice with every part whose Markdown would read back as
|
|
10
|
+
* different structure replaced by a form that reads back the same, keeping as
|
|
11
|
+
* much of its text and formatting as possible.
|
|
12
|
+
*/
|
|
13
|
+
export declare function importableSlice(ctx: Ctx, slice: Slice): Slice;
|