@aicayzer/inkkit 0.0.14 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/comments.js +7 -0
- package/dist/dialect.js +10 -19
- package/dist/editor.d.ts +3 -0
- package/dist/editor.js +52 -27
- package/dist/image-blocks.d.ts +71 -0
- package/dist/image-blocks.js +286 -0
- package/dist/images.d.ts +3 -1
- package/dist/images.js +52 -23
- package/dist/paste.js +8 -3
- package/dist/search.d.ts +3 -3
- package/dist/search.js +2 -2
- package/dist/style.css +98 -4
- package/dist/text-ranges.js +10 -1
- package/package.json +1 -1
package/dist/comments.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { $node, $prose, $remark } from '@milkdown/kit/utils';
|
|
2
2
|
import { Fragment } from '@milkdown/kit/prose/model';
|
|
3
3
|
import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
|
|
4
|
+
import { isImageBlock } from './image-blocks.js';
|
|
4
5
|
import { fromMarkdown } from 'mdast-util-from-markdown';
|
|
5
6
|
const lineEnding = (code) => code === -3 || code === -4 || code === -5;
|
|
6
7
|
const whitespace = (code) => code === 32 || code === -1 || code === -2;
|
|
@@ -418,6 +419,12 @@ export function shareableFragment(content) {
|
|
|
418
419
|
content.forEach((node) => {
|
|
419
420
|
if (isComment(node))
|
|
420
421
|
return;
|
|
422
|
+
// Shared output treats a standalone image as the paragraph its Markdown holds.
|
|
423
|
+
if (isImageBlock(node)) {
|
|
424
|
+
const { paragraph, image } = node.type.schema.nodes;
|
|
425
|
+
filtered.push(paragraph.create(null, image.create(node.attrs)));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
421
428
|
if (node.type.name === 'reference_definition' &&
|
|
422
429
|
labelHasComments(node.attrs.label))
|
|
423
430
|
return;
|
package/dist/dialect.js
CHANGED
|
@@ -8,6 +8,7 @@ import { linkedSyntax, wikiLinkSchema, namedFileMarkdown, fileReference, } from
|
|
|
8
8
|
import { normalizeTableAlignment, normalizeTableColumnAlignment, } from './table-alignment.js';
|
|
9
9
|
import { preservingCodeBlocks, remarkCodeFences, fencedCodeHandler, } from './code-fences.js';
|
|
10
10
|
import { literalBlock, createLiteralPreservation } from './literals.js';
|
|
11
|
+
import { imageAttrs, imageBlocks, markdownImageAttrs, readParagraph, } from './image-blocks.js';
|
|
11
12
|
import { references, remarkReferencesPlugin } from './references.js';
|
|
12
13
|
import { callouts, remarkCalloutsPlugin } from './callouts.js';
|
|
13
14
|
import { inlineHighlight } from './inline-highlight.js';
|
|
@@ -110,6 +111,12 @@ const preserveSpacerParagraphs = paragraphSchema.extendSchema((base) => (ctx) =>
|
|
|
110
111
|
const schema = base(ctx);
|
|
111
112
|
return {
|
|
112
113
|
...schema,
|
|
114
|
+
parseMarkdown: {
|
|
115
|
+
...schema.parseMarkdown,
|
|
116
|
+
runner(state, node, type) {
|
|
117
|
+
readParagraph(state, node, type);
|
|
118
|
+
},
|
|
119
|
+
},
|
|
113
120
|
toMarkdown: {
|
|
114
121
|
...schema.toMarkdown,
|
|
115
122
|
runner(state, node) {
|
|
@@ -174,19 +181,11 @@ const visibleSoftbreaks = hardbreakSchema.extendSchema((base) => (ctx) => {
|
|
|
174
181
|
],
|
|
175
182
|
};
|
|
176
183
|
});
|
|
177
|
-
// Remark represents a missing title as null; the image schema accepts strings.
|
|
178
184
|
const normalizedImages = imageSchema.extendSchema((base) => (ctx) => {
|
|
179
185
|
const schema = base(ctx);
|
|
180
186
|
return {
|
|
181
187
|
...schema,
|
|
182
|
-
attrs:
|
|
183
|
-
...schema.attrs,
|
|
184
|
-
inkkitFileRaw: { default: null },
|
|
185
|
-
inkkitFileTarget: { default: null },
|
|
186
|
-
inkkitFileFragment: { default: null },
|
|
187
|
-
inkkitFileWidth: { default: null },
|
|
188
|
-
inkkitFileInTable: { default: false },
|
|
189
|
-
},
|
|
188
|
+
attrs: imageAttrs,
|
|
190
189
|
parseDOM: [
|
|
191
190
|
{
|
|
192
191
|
tag: 'img[src]',
|
|
@@ -202,16 +201,7 @@ const normalizedImages = imageSchema.extendSchema((base) => (ctx) => {
|
|
|
202
201
|
parseMarkdown: {
|
|
203
202
|
...schema.parseMarkdown,
|
|
204
203
|
runner(state, node, type) {
|
|
205
|
-
state.addNode(type,
|
|
206
|
-
src: String(node.url ?? ''),
|
|
207
|
-
alt: String(node.alt ?? ''),
|
|
208
|
-
title: String(node.title ?? ''),
|
|
209
|
-
inkkitFileRaw: node.inkkitFileRaw ?? null,
|
|
210
|
-
inkkitFileTarget: node.inkkitFileTarget ?? null,
|
|
211
|
-
inkkitFileFragment: node.inkkitFileFragment ?? null,
|
|
212
|
-
inkkitFileWidth: node.inkkitFileWidth ?? null,
|
|
213
|
-
inkkitFileInTable: node.inkkitFileInTable ?? false,
|
|
214
|
-
});
|
|
204
|
+
state.addNode(type, markdownImageAttrs(node));
|
|
215
205
|
},
|
|
216
206
|
},
|
|
217
207
|
toMarkdown: {
|
|
@@ -324,6 +314,7 @@ export function createDialect(images, wikiLinks = false, files = false) {
|
|
|
324
314
|
preserveSpacerParagraphs,
|
|
325
315
|
visibleSoftbreaks,
|
|
326
316
|
normalizedImages,
|
|
317
|
+
imageBlocks,
|
|
327
318
|
autolinkInputRule,
|
|
328
319
|
extendListItemSchemaForTask,
|
|
329
320
|
strikethroughAttr,
|
package/dist/editor.d.ts
CHANGED
|
@@ -179,6 +179,9 @@ export declare class InkKitEditor {
|
|
|
179
179
|
focus?: boolean;
|
|
180
180
|
reveal?: boolean;
|
|
181
181
|
}): void;
|
|
182
|
+
/** 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. */
|
|
184
|
+
private assertPreserved;
|
|
182
185
|
replaceTextRange(range: TextRange, text: string): boolean;
|
|
183
186
|
textRangeRects(range: TextRange): readonly TextRect[];
|
|
184
187
|
textRangesRects(ranges: readonly TextRange[]): readonly (readonly TextRect[])[];
|
package/dist/editor.js
CHANGED
|
@@ -21,6 +21,8 @@ import { sourceAttribute, cleanSourceDoc, editedPlainSource, sourceSelection, ra
|
|
|
21
21
|
import { normaliseLabel, referenceDefinitions, footnoteDefinitions, } from './references.js';
|
|
22
22
|
import { selectionMarkdown, selectionContent, referenceMetadata, withReferenceMetadata, } from './reference-clipboard.js';
|
|
23
23
|
import { imageView } from './images.js';
|
|
24
|
+
import { isImage, isImageBlock } from './image-blocks.js';
|
|
25
|
+
import { GapCursor } from '@milkdown/kit/prose/gapcursor';
|
|
24
26
|
import { wikiLinkView } from './wiki-view.js';
|
|
25
27
|
import { withFileSignal } from './files.js';
|
|
26
28
|
import { tablePlugins, tableCommand, tableCommands, tableAvailability, tableContext, configureTableMovement, } from './tables.js';
|
|
@@ -991,7 +993,7 @@ export class InkKitEditor {
|
|
|
991
993
|
const parsed = this.editor.ctx.get(parserCtx)(selected.shareable);
|
|
992
994
|
let portable = false;
|
|
993
995
|
parsed.descendants((node) => {
|
|
994
|
-
if (node
|
|
996
|
+
if (isImage(node) || isMermaid(node))
|
|
995
997
|
portable = true;
|
|
996
998
|
});
|
|
997
999
|
const cut = event.type === 'cut' && this.writable;
|
|
@@ -1091,13 +1093,9 @@ export class InkKitEditor {
|
|
|
1091
1093
|
...options,
|
|
1092
1094
|
dispatchTransaction: function (tr) {
|
|
1093
1095
|
const next = this.state.apply(tr);
|
|
1094
|
-
if (!instance.loading
|
|
1095
|
-
instance.preservation &&
|
|
1096
|
-
!instance.literalSurface &&
|
|
1097
|
-
!next.doc.eq(this.state.doc)) {
|
|
1096
|
+
if (!instance.loading) {
|
|
1098
1097
|
try {
|
|
1099
|
-
|
|
1100
|
-
instance.currentText(next.doc);
|
|
1098
|
+
instance.assertPreserved(this.state.doc, next.doc);
|
|
1101
1099
|
}
|
|
1102
1100
|
catch (error) {
|
|
1103
1101
|
this.updateState(this.state);
|
|
@@ -1319,7 +1317,7 @@ export class InkKitEditor {
|
|
|
1319
1317
|
const expanded = selectionContent(view.state.doc, fragment);
|
|
1320
1318
|
let hasImages = false;
|
|
1321
1319
|
expanded.descendants((node) => {
|
|
1322
|
-
if (node
|
|
1320
|
+
if (isImage(node) || isMermaid(node))
|
|
1323
1321
|
hasImages = true;
|
|
1324
1322
|
});
|
|
1325
1323
|
if (!hasImages) {
|
|
@@ -1606,6 +1604,12 @@ export class InkKitEditor {
|
|
|
1606
1604
|
this.revealTextRange(range);
|
|
1607
1605
|
this.publishCommandState();
|
|
1608
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
|
+
}
|
|
1609
1613
|
replaceTextRange(range, text) {
|
|
1610
1614
|
const projection = this.validateTextRange(range);
|
|
1611
1615
|
this.assertMutation();
|
|
@@ -1631,9 +1635,8 @@ export class InkKitEditor {
|
|
|
1631
1635
|
throw new InkKitError('invalid-range', 'Replacement cannot remove embedded content or structural separators');
|
|
1632
1636
|
const from = projectionPosition(projection, range.from, 1);
|
|
1633
1637
|
const to = projectionPosition(projection, range.to, range.from === range.to ? 1 : -1);
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
});
|
|
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));
|
|
1637
1640
|
}
|
|
1638
1641
|
textRangeRects(range) {
|
|
1639
1642
|
return this.textRangesRects([range])[0];
|
|
@@ -1804,10 +1807,12 @@ export class InkKitEditor {
|
|
|
1804
1807
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
1805
1808
|
// A caret at the end, where writing carries on; at the start it would sit in a first-line heading and
|
|
1806
1809
|
// show its marks. The last place text can go, so a document ending in a rule takes a caret above it
|
|
1807
|
-
// rather than selecting the rule itself.
|
|
1810
|
+
// rather than selecting the rule itself. After a final image, typing opens a paragraph below it.
|
|
1808
1811
|
const { doc } = view.state;
|
|
1809
|
-
const end =
|
|
1810
|
-
|
|
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));
|
|
1811
1816
|
view.dispatch(setSearchState(view.state.tr.setSelection(end), new SearchQuery({ search: '' })));
|
|
1812
1817
|
}
|
|
1813
1818
|
find(text, expectedGeneration) {
|
|
@@ -1875,9 +1880,8 @@ export class InkKitEditor {
|
|
|
1875
1880
|
this.plainSearch = search;
|
|
1876
1881
|
return result.count;
|
|
1877
1882
|
}
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
}, all);
|
|
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);
|
|
1881
1885
|
}
|
|
1882
1886
|
outlineContext() {
|
|
1883
1887
|
return {
|
|
@@ -2098,22 +2102,34 @@ export class InkKitEditor {
|
|
|
2098
2102
|
throw new InkKitError('image-unavailable', 'This editor has no image adapter');
|
|
2099
2103
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
2100
2104
|
const { schema } = view.state;
|
|
2101
|
-
|
|
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({
|
|
2102
2107
|
src: image.path,
|
|
2103
2108
|
alt: image.alt,
|
|
2104
2109
|
title: image.title ?? '',
|
|
2105
|
-
}));
|
|
2106
|
-
if (x != null && y != null)
|
|
2107
|
-
this.insertBlocks(
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
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());
|
|
2112
2127
|
}
|
|
2113
2128
|
insertBlocks(blocks, x, y) {
|
|
2114
2129
|
const view = this.editor.ctx.get(editorViewCtx);
|
|
2115
2130
|
const { state } = view;
|
|
2116
|
-
const
|
|
2131
|
+
const hit = view.posAtCoords({ left: x, top: y });
|
|
2132
|
+
const $pos = state.doc.resolve(hit?.pos ?? state.selection.from);
|
|
2117
2133
|
const tr = state.tr;
|
|
2118
2134
|
let at;
|
|
2119
2135
|
if ($pos.depth === 1 &&
|
|
@@ -2123,7 +2139,16 @@ export class InkKitEditor {
|
|
|
2123
2139
|
tr.replaceWith(at, $pos.after(1), blocks);
|
|
2124
2140
|
}
|
|
2125
2141
|
else {
|
|
2126
|
-
|
|
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;
|
|
2127
2152
|
tr.insert(at, blocks);
|
|
2128
2153
|
}
|
|
2129
2154
|
const end = at + blocks.reduce((size, node) => size + node.nodeSize, 0) - 1;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { MilkdownPlugin } from '@milkdown/kit/ctx';
|
|
2
|
+
import { type Node as ProseNode, type NodeType } from '@milkdown/kit/prose/model';
|
|
3
|
+
import type { ParserState } from '@milkdown/kit/transformer';
|
|
4
|
+
export declare const imageBlockName = "image_block";
|
|
5
|
+
/** Attributes shared by an inline image and a standalone image block. */
|
|
6
|
+
export declare const imageAttrs: {
|
|
7
|
+
src: {
|
|
8
|
+
default: string;
|
|
9
|
+
validate: string;
|
|
10
|
+
};
|
|
11
|
+
alt: {
|
|
12
|
+
default: string;
|
|
13
|
+
validate: string;
|
|
14
|
+
};
|
|
15
|
+
title: {
|
|
16
|
+
default: string;
|
|
17
|
+
validate: string;
|
|
18
|
+
};
|
|
19
|
+
inkkitFileRaw: {
|
|
20
|
+
default: null;
|
|
21
|
+
};
|
|
22
|
+
inkkitFileTarget: {
|
|
23
|
+
default: null;
|
|
24
|
+
};
|
|
25
|
+
inkkitFileFragment: {
|
|
26
|
+
default: null;
|
|
27
|
+
};
|
|
28
|
+
inkkitFileWidth: {
|
|
29
|
+
default: null;
|
|
30
|
+
};
|
|
31
|
+
inkkitFileInTable: {
|
|
32
|
+
default: boolean;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
interface MarkdownParagraph {
|
|
36
|
+
children?: unknown[];
|
|
37
|
+
value?: unknown;
|
|
38
|
+
}
|
|
39
|
+
interface MarkdownImage {
|
|
40
|
+
type: string;
|
|
41
|
+
url?: unknown;
|
|
42
|
+
alt?: unknown;
|
|
43
|
+
title?: unknown;
|
|
44
|
+
inkkitFileRaw?: unknown;
|
|
45
|
+
inkkitFileTarget?: unknown;
|
|
46
|
+
inkkitFileFragment?: unknown;
|
|
47
|
+
inkkitFileWidth?: unknown;
|
|
48
|
+
inkkitFileInTable?: unknown;
|
|
49
|
+
}
|
|
50
|
+
export declare function markdownImageAttrs(node: MarkdownImage): {
|
|
51
|
+
src: string;
|
|
52
|
+
alt: string;
|
|
53
|
+
title: string;
|
|
54
|
+
inkkitFileRaw: {} | null;
|
|
55
|
+
inkkitFileTarget: {} | null;
|
|
56
|
+
inkkitFileFragment: {} | null;
|
|
57
|
+
inkkitFileWidth: {} | null;
|
|
58
|
+
inkkitFileInTable: {};
|
|
59
|
+
};
|
|
60
|
+
/** Whether a node displays an authored image, inline or as its own block. */
|
|
61
|
+
export declare function isImage(node: ProseNode): boolean;
|
|
62
|
+
export declare function isImageBlock(node: ProseNode | null | undefined): boolean;
|
|
63
|
+
/** The image a paragraph holds as its only content, without marks, which makes the paragraph a
|
|
64
|
+
* standalone image block. A linked or emphasised image stays inline so its marks are kept. */
|
|
65
|
+
export declare function standaloneImage(children: readonly ProseNode[]): ProseNode | undefined;
|
|
66
|
+
/** Reads a Markdown paragraph, as an image block when it holds a standalone image and the enclosing
|
|
67
|
+
* block accepts one. */
|
|
68
|
+
export declare function readParagraph(state: ParserState, node: MarkdownParagraph, paragraph: NodeType): void;
|
|
69
|
+
export declare const imageBlockSchema: import("@milkdown/kit/utils").$NodeSchema<"image_block">;
|
|
70
|
+
export declare const imageBlocks: MilkdownPlugin[];
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { keydownHandler } from '@milkdown/kit/prose/keymap';
|
|
2
|
+
import { GapCursor } from '@milkdown/kit/prose/gapcursor';
|
|
3
|
+
import { Fragment, } from '@milkdown/kit/prose/model';
|
|
4
|
+
import { NodeSelection, Plugin, PluginKey, Selection, TextSelection, } from '@milkdown/kit/prose/state';
|
|
5
|
+
import { $nodeSchema, $prose } from '@milkdown/kit/utils';
|
|
6
|
+
export const imageBlockName = 'image_block';
|
|
7
|
+
/** Attributes shared by an inline image and a standalone image block. */
|
|
8
|
+
export const imageAttrs = {
|
|
9
|
+
src: { default: '', validate: 'string' },
|
|
10
|
+
alt: { default: '', validate: 'string' },
|
|
11
|
+
title: { default: '', validate: 'string' },
|
|
12
|
+
inkkitFileRaw: { default: null },
|
|
13
|
+
inkkitFileTarget: { default: null },
|
|
14
|
+
inkkitFileFragment: { default: null },
|
|
15
|
+
inkkitFileWidth: { default: null },
|
|
16
|
+
inkkitFileInTable: { default: false },
|
|
17
|
+
};
|
|
18
|
+
// Remark represents a missing title as null; the image schema accepts strings.
|
|
19
|
+
export function markdownImageAttrs(node) {
|
|
20
|
+
return {
|
|
21
|
+
src: String(node.url ?? ''),
|
|
22
|
+
alt: String(node.alt ?? ''),
|
|
23
|
+
title: String(node.title ?? ''),
|
|
24
|
+
inkkitFileRaw: node.inkkitFileRaw ?? null,
|
|
25
|
+
inkkitFileTarget: node.inkkitFileTarget ?? null,
|
|
26
|
+
inkkitFileFragment: node.inkkitFileFragment ?? null,
|
|
27
|
+
inkkitFileWidth: node.inkkitFileWidth ?? null,
|
|
28
|
+
inkkitFileInTable: node.inkkitFileInTable ?? false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Whether a node displays an authored image, inline or as its own block. */
|
|
32
|
+
export function isImage(node) {
|
|
33
|
+
return node.type.name === 'image' || node.type.name === imageBlockName;
|
|
34
|
+
}
|
|
35
|
+
export function isImageBlock(node) {
|
|
36
|
+
return node?.type.name === imageBlockName;
|
|
37
|
+
}
|
|
38
|
+
function accepts(parent, index, type) {
|
|
39
|
+
return parent.canReplaceWith(index, index + 1, type);
|
|
40
|
+
}
|
|
41
|
+
/** The image a paragraph holds as its only content, without marks, which makes the paragraph a
|
|
42
|
+
* standalone image block. A linked or emphasised image stays inline so its marks are kept. */
|
|
43
|
+
export function standaloneImage(children) {
|
|
44
|
+
const [image] = children;
|
|
45
|
+
return children.length === 1 &&
|
|
46
|
+
image.type.name === 'image' &&
|
|
47
|
+
!image.marks.length
|
|
48
|
+
? image
|
|
49
|
+
: undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Reads a Markdown paragraph, as an image block when it holds a standalone image and the enclosing
|
|
52
|
+
* block accepts one. */
|
|
53
|
+
export function readParagraph(state, node, paragraph) {
|
|
54
|
+
state.openNode(paragraph);
|
|
55
|
+
if (node.children)
|
|
56
|
+
state.next(node.children);
|
|
57
|
+
else
|
|
58
|
+
state.addText(String(node.value ?? ''));
|
|
59
|
+
const type = state.schema.nodes[imageBlockName];
|
|
60
|
+
const element = state.top();
|
|
61
|
+
const image = standaloneImage(element.content);
|
|
62
|
+
state.close();
|
|
63
|
+
const parent = state.top();
|
|
64
|
+
if (image &&
|
|
65
|
+
type &&
|
|
66
|
+
parent?.type.contentMatch
|
|
67
|
+
.matchFragment(Fragment.fromArray(parent.content))
|
|
68
|
+
?.matchType(type))
|
|
69
|
+
state.addNode(type, image.attrs);
|
|
70
|
+
else {
|
|
71
|
+
state.openNode(paragraph);
|
|
72
|
+
for (const child of element.content)
|
|
73
|
+
state.top().push(child);
|
|
74
|
+
state.closeNode();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// A standalone image is a block with no inside, so no edit can put text beside it. In Markdown it
|
|
78
|
+
// remains a paragraph holding the image, which is how it is read and written.
|
|
79
|
+
export const imageBlockSchema = $nodeSchema(imageBlockName, () => ({
|
|
80
|
+
group: 'block',
|
|
81
|
+
atom: true,
|
|
82
|
+
selectable: true,
|
|
83
|
+
draggable: true,
|
|
84
|
+
marks: '',
|
|
85
|
+
attrs: imageAttrs,
|
|
86
|
+
toDOM: (node) => [
|
|
87
|
+
'p',
|
|
88
|
+
{ class: 'inkkit-image-block' },
|
|
89
|
+
[
|
|
90
|
+
'img',
|
|
91
|
+
{
|
|
92
|
+
src: String(node.attrs.src),
|
|
93
|
+
alt: String(node.attrs.alt),
|
|
94
|
+
title: String(node.attrs.title),
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
],
|
|
98
|
+
parseMarkdown: { match: () => false, runner: () => undefined },
|
|
99
|
+
toMarkdown: {
|
|
100
|
+
match: (node) => node.type.name === imageBlockName,
|
|
101
|
+
runner: (state, node) => {
|
|
102
|
+
state.openNode('paragraph');
|
|
103
|
+
state.next(node.type.schema.nodes.image.create(node.attrs));
|
|
104
|
+
state.closeNode();
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
// An edit can leave a paragraph holding only an image, which reading its Markdown again would give
|
|
109
|
+
// as an image block. The caret keeps its side of the image.
|
|
110
|
+
const normalise = $prose(() => new Plugin({
|
|
111
|
+
key: new PluginKey('inkkitImageBlockNormalise'),
|
|
112
|
+
appendTransaction(transactions, old, state) {
|
|
113
|
+
const type = state.schema.nodes[imageBlockName];
|
|
114
|
+
if (!type || !transactions.some((tr) => tr.docChanged))
|
|
115
|
+
return null;
|
|
116
|
+
// Compare the documents, as mark and attribute steps change no positions.
|
|
117
|
+
const start = old.doc.content.findDiffStart(state.doc.content);
|
|
118
|
+
const end = old.doc.content.findDiffEnd(state.doc.content);
|
|
119
|
+
if (start == null || !end)
|
|
120
|
+
return null;
|
|
121
|
+
const found = new Map();
|
|
122
|
+
state.doc.nodesBetween(Math.min(start, end.b), Math.max(start, end.b), (node, pos, parent, index) => {
|
|
123
|
+
if (!node.isTextblock)
|
|
124
|
+
return true;
|
|
125
|
+
const children = [];
|
|
126
|
+
node.forEach((child) => children.push(child));
|
|
127
|
+
if (node.type.name === 'paragraph' &&
|
|
128
|
+
standaloneImage(children) &&
|
|
129
|
+
parent &&
|
|
130
|
+
accepts(parent, index, type))
|
|
131
|
+
found.set(pos, node);
|
|
132
|
+
return false;
|
|
133
|
+
});
|
|
134
|
+
if (!found.size)
|
|
135
|
+
return null;
|
|
136
|
+
const tr = state.tr;
|
|
137
|
+
const { head } = state.selection;
|
|
138
|
+
let caret;
|
|
139
|
+
for (const [pos, node] of [...found].sort(([a], [b]) => b - a)) {
|
|
140
|
+
const end = pos + node.nodeSize;
|
|
141
|
+
if (head > pos && head < end)
|
|
142
|
+
caret = head > pos + 1 ? { pos: end, side: 1 } : { pos, side: -1 };
|
|
143
|
+
tr.replaceWith(pos, end, type.create(node.firstChild.attrs));
|
|
144
|
+
}
|
|
145
|
+
if (caret)
|
|
146
|
+
tr.setSelection(new GapCursor(tr.doc.resolve(tr.mapping.map(caret.pos, caret.side))));
|
|
147
|
+
return tr;
|
|
148
|
+
},
|
|
149
|
+
}));
|
|
150
|
+
/** The image block a selection rests beside or selects, as the position for a new paragraph. */
|
|
151
|
+
function newParagraphAt(state) {
|
|
152
|
+
const { selection } = state;
|
|
153
|
+
if (selection instanceof NodeSelection && isImageBlock(selection.node))
|
|
154
|
+
return selection.to;
|
|
155
|
+
if (selection instanceof GapCursor &&
|
|
156
|
+
(isImageBlock(selection.$head.nodeBefore) ||
|
|
157
|
+
isImageBlock(selection.$head.nodeAfter)))
|
|
158
|
+
return selection.head;
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
/** Opens an empty paragraph beside a selected image block, or at a gap beside one. */
|
|
162
|
+
function openParagraph(state) {
|
|
163
|
+
const pos = newParagraphAt(state);
|
|
164
|
+
const paragraph = state.schema.nodes.paragraph;
|
|
165
|
+
if (pos == null || !paragraph)
|
|
166
|
+
return undefined;
|
|
167
|
+
const $pos = state.doc.resolve(pos);
|
|
168
|
+
if (!accepts($pos.parent, $pos.index(), paragraph))
|
|
169
|
+
return undefined;
|
|
170
|
+
const tr = state.tr.insert(pos, paragraph.create());
|
|
171
|
+
return tr.setSelection(TextSelection.create(tr.doc, pos + 1));
|
|
172
|
+
}
|
|
173
|
+
const enter = (state, dispatch) => {
|
|
174
|
+
const tr = openParagraph(state);
|
|
175
|
+
if (!tr)
|
|
176
|
+
return false;
|
|
177
|
+
dispatch?.(tr.scrollIntoView());
|
|
178
|
+
return true;
|
|
179
|
+
};
|
|
180
|
+
/** Backspace or Delete toward an image block selects it before anything is deleted; from a gap beside
|
|
181
|
+
* one toward text, the caret moves into the text. An empty paragraph next to one is removed. */
|
|
182
|
+
function towardImage(dir) {
|
|
183
|
+
return (state, dispatch) => {
|
|
184
|
+
const { selection, doc } = state;
|
|
185
|
+
if (!selection.empty)
|
|
186
|
+
return false;
|
|
187
|
+
const $head = selection.$head;
|
|
188
|
+
if (selection instanceof GapCursor) {
|
|
189
|
+
const node = dir < 0 ? $head.nodeBefore : $head.nodeAfter;
|
|
190
|
+
const other = dir < 0 ? $head.nodeAfter : $head.nodeBefore;
|
|
191
|
+
if (!node || !(isImageBlock(other) || isImageBlock(node)))
|
|
192
|
+
return false;
|
|
193
|
+
const next = isImageBlock(node)
|
|
194
|
+
? NodeSelection.create(doc, dir < 0 ? $head.pos - 1 : $head.pos)
|
|
195
|
+
: Selection.findFrom($head, dir, true);
|
|
196
|
+
if (!next)
|
|
197
|
+
return false;
|
|
198
|
+
dispatch?.(state.tr.setSelection(next).scrollIntoView());
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
if (!$head.parent.isTextblock || $head.depth === 0)
|
|
202
|
+
return false;
|
|
203
|
+
const atEdge = dir < 0
|
|
204
|
+
? $head.parentOffset === 0
|
|
205
|
+
: $head.parentOffset === $head.parent.content.size;
|
|
206
|
+
if (!atEdge)
|
|
207
|
+
return false;
|
|
208
|
+
const index = $head.index(-1) + dir;
|
|
209
|
+
const parent = $head.node(-1);
|
|
210
|
+
const neighbour = index >= 0 && index < parent.childCount ? parent.child(index) : undefined;
|
|
211
|
+
if (!isImageBlock(neighbour))
|
|
212
|
+
return false;
|
|
213
|
+
const block = dir < 0 ? $head.before() - 1 : $head.after();
|
|
214
|
+
// An empty paragraph goes, unless its parent needs it, such as a list item's first line.
|
|
215
|
+
if ($head.parent.content.size === 0 &&
|
|
216
|
+
$head.parent.type.name === 'paragraph' &&
|
|
217
|
+
parent.canReplace($head.index(-1), $head.index(-1) + 1)) {
|
|
218
|
+
const tr = state.tr.delete($head.before(), $head.after());
|
|
219
|
+
dispatch?.(tr
|
|
220
|
+
.setSelection(new GapCursor(tr.doc.resolve($head.before())))
|
|
221
|
+
.scrollIntoView());
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (!dispatch)
|
|
225
|
+
return true;
|
|
226
|
+
dispatch(state.tr.setSelection(NodeSelection.create(doc, block)).scrollIntoView());
|
|
227
|
+
return true;
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const keys = keydownHandler({
|
|
231
|
+
Enter: enter,
|
|
232
|
+
'Shift-Enter': enter,
|
|
233
|
+
Backspace: towardImage(-1),
|
|
234
|
+
Delete: towardImage(1),
|
|
235
|
+
});
|
|
236
|
+
const interaction = $prose(() => new Plugin({
|
|
237
|
+
key: new PluginKey('inkkitImageBlocks'),
|
|
238
|
+
props: {
|
|
239
|
+
handleKeyDown: (view, event) => !view.composing && view.editable && keys(view, event),
|
|
240
|
+
handleTextInput(view, _from, _to, text) {
|
|
241
|
+
const tr = openParagraph(view.state);
|
|
242
|
+
if (!tr)
|
|
243
|
+
return false;
|
|
244
|
+
view.dispatch(tr.insertText(text).scrollIntoView());
|
|
245
|
+
return true;
|
|
246
|
+
},
|
|
247
|
+
// A click beside an image card, not on it, puts the caret after the image.
|
|
248
|
+
handleClickOn(view, _pos, node, nodePos, event, direct) {
|
|
249
|
+
if (!direct ||
|
|
250
|
+
!view.editable ||
|
|
251
|
+
!isImageBlock(node) ||
|
|
252
|
+
!(event.target instanceof Element) ||
|
|
253
|
+
event.target.closest('.image'))
|
|
254
|
+
return false;
|
|
255
|
+
view.dispatch(view.state.tr.setSelection(new GapCursor(view.state.doc.resolve(nodePos + node.nodeSize))));
|
|
256
|
+
return true;
|
|
257
|
+
},
|
|
258
|
+
handleDOMEvents: {
|
|
259
|
+
// ProseMirror replaces a selection when composition starts; a selected image is kept and
|
|
260
|
+
// the composed text goes after it.
|
|
261
|
+
compositionstart(view) {
|
|
262
|
+
const { selection } = view.state;
|
|
263
|
+
if (view.editable &&
|
|
264
|
+
selection instanceof NodeSelection &&
|
|
265
|
+
isImageBlock(selection.node))
|
|
266
|
+
view.dispatch(view.state.tr.setSelection(new GapCursor(view.state.doc.resolve(selection.to))));
|
|
267
|
+
return false;
|
|
268
|
+
},
|
|
269
|
+
// Composed text needs a paragraph to land in before the browser writes it, as the gap
|
|
270
|
+
// cursor plugin does for its own gaps.
|
|
271
|
+
beforeinput(view, event) {
|
|
272
|
+
if (event.inputType !== 'insertCompositionText' || !view.editable)
|
|
273
|
+
return false;
|
|
274
|
+
const tr = openParagraph(view.state);
|
|
275
|
+
if (tr)
|
|
276
|
+
view.dispatch(tr);
|
|
277
|
+
return false;
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
}));
|
|
282
|
+
export const imageBlocks = [
|
|
283
|
+
imageBlockSchema,
|
|
284
|
+
normalise,
|
|
285
|
+
interaction,
|
|
286
|
+
].flat();
|
package/dist/images.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ declare class ImageView implements NodeView {
|
|
|
17
17
|
private readonly runtime?;
|
|
18
18
|
private readonly canMutate;
|
|
19
19
|
dom: HTMLElement;
|
|
20
|
+
/** The card or picture, which a standalone image's block row holds. */
|
|
21
|
+
private element;
|
|
20
22
|
private image?;
|
|
21
23
|
private media?;
|
|
22
24
|
private pdf?;
|
|
@@ -45,5 +47,5 @@ declare class ImageView implements NodeView {
|
|
|
45
47
|
private commit;
|
|
46
48
|
private canResize;
|
|
47
49
|
}
|
|
48
|
-
export declare function imageView(adapter?: ImageAdapter, runtime?: FileViewRuntime, canMutate?: () => boolean): import("@milkdown/kit/utils").$View<import("@milkdown/kit/utils").$Node, (node: ProseNode, view: EditorView, getPos: () => number | undefined) => ImageView
|
|
50
|
+
export declare function imageView(adapter?: ImageAdapter, runtime?: FileViewRuntime, canMutate?: () => boolean): import("@milkdown/kit/utils").$View<import("@milkdown/kit/utils").$Node, (node: ProseNode, view: EditorView, getPos: () => number | undefined) => ImageView>[];
|
|
49
51
|
export {};
|
package/dist/images.js
CHANGED
|
@@ -3,6 +3,7 @@ import { closeHistory } from '@milkdown/kit/prose/history';
|
|
|
3
3
|
import { $view } from '@milkdown/kit/utils';
|
|
4
4
|
import { FileResolution } from './files.js';
|
|
5
5
|
import { fileReference } from './linked-syntax.js';
|
|
6
|
+
import { imageBlockSchema } from './image-blocks.js';
|
|
6
7
|
/** A width rides in the alt text, as ``. Other Markdown readers
|
|
7
8
|
* show the picture and keep the number without requiring a private extension. */
|
|
8
9
|
export function splitAlt(alt) {
|
|
@@ -23,6 +24,8 @@ class ImageView {
|
|
|
23
24
|
runtime;
|
|
24
25
|
canMutate;
|
|
25
26
|
dom;
|
|
27
|
+
/** The card or picture, which a standalone image's block row holds. */
|
|
28
|
+
element;
|
|
26
29
|
image;
|
|
27
30
|
media;
|
|
28
31
|
pdf;
|
|
@@ -37,8 +40,15 @@ class ImageView {
|
|
|
37
40
|
this.adapter = adapter;
|
|
38
41
|
this.runtime = runtime;
|
|
39
42
|
this.canMutate = canMutate;
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
43
|
+
this.element = document.createElement('span');
|
|
44
|
+
this.element.className = 'image';
|
|
45
|
+
if (node.isBlock) {
|
|
46
|
+
this.dom = document.createElement('div');
|
|
47
|
+
this.dom.className = 'inkkit-image-block';
|
|
48
|
+
this.dom.append(this.element);
|
|
49
|
+
}
|
|
50
|
+
else
|
|
51
|
+
this.dom = this.element;
|
|
42
52
|
this.view.dom.addEventListener('inkkit-cancel-resize', this.cancelResize);
|
|
43
53
|
this.view.dom.addEventListener('inkkit-media-policy', this.mediaPolicy);
|
|
44
54
|
if (runtime) {
|
|
@@ -145,11 +155,22 @@ class ImageView {
|
|
|
145
155
|
card(label, status, retry = false) {
|
|
146
156
|
const footer = document.createElement('span');
|
|
147
157
|
footer.className = 'inkkit-file-footer';
|
|
158
|
+
const name = document.createElement('span');
|
|
159
|
+
name.className = 'inkkit-file-name';
|
|
148
160
|
const text = document.createElement('span');
|
|
149
161
|
text.className = 'inkkit-file-label';
|
|
150
162
|
text.textContent = label;
|
|
151
163
|
text.title = label;
|
|
152
|
-
|
|
164
|
+
name.append(text);
|
|
165
|
+
if (this.runtime?.adapter.open) {
|
|
166
|
+
const open = this.button('', this.activate);
|
|
167
|
+
open.classList.add('inkkit-file-open');
|
|
168
|
+
open.setAttribute('aria-label', `${this.runtime.labels.fileOpen} ${label}`);
|
|
169
|
+
open.title = this.runtime.labels.fileOpen;
|
|
170
|
+
open.append(openArrow());
|
|
171
|
+
name.append(open);
|
|
172
|
+
}
|
|
173
|
+
footer.append(name);
|
|
153
174
|
if (status) {
|
|
154
175
|
const description = document.createElement('span');
|
|
155
176
|
description.className = 'inkkit-file-status';
|
|
@@ -159,12 +180,7 @@ class ImageView {
|
|
|
159
180
|
}
|
|
160
181
|
if (retry && this.runtime)
|
|
161
182
|
footer.append(this.button(this.runtime.labels.fileRetry, () => this.resolution?.retry()));
|
|
162
|
-
|
|
163
|
-
const open = this.button(this.runtime.labels.fileOpen, this.activate);
|
|
164
|
-
open.setAttribute('aria-label', `${this.runtime.labels.fileOpen} ${label}`);
|
|
165
|
-
footer.append(open);
|
|
166
|
-
}
|
|
167
|
-
this.dom.append(footer);
|
|
183
|
+
this.element.append(footer);
|
|
168
184
|
}
|
|
169
185
|
failPreview() {
|
|
170
186
|
this.previewFailed = true;
|
|
@@ -183,11 +199,11 @@ class ImageView {
|
|
|
183
199
|
}
|
|
184
200
|
: splitAlt(String(this.node.attrs.alt ?? ''));
|
|
185
201
|
this.width = width;
|
|
186
|
-
this.
|
|
202
|
+
this.element.textContent = '';
|
|
187
203
|
this.image = undefined;
|
|
188
204
|
if (this.runtime) {
|
|
189
|
-
this.
|
|
190
|
-
this.
|
|
205
|
+
this.element.classList.add('inkkit-file');
|
|
206
|
+
this.element.oncontextmenu = (event) => {
|
|
191
207
|
if (!this.runtime?.adapter.contextMenu || !this.active())
|
|
192
208
|
return;
|
|
193
209
|
const captured = this.runtime.capture();
|
|
@@ -205,11 +221,11 @@ class ImageView {
|
|
|
205
221
|
const label = state.status === 'resolved'
|
|
206
222
|
? state.presentation.label || alt || reference.reference
|
|
207
223
|
: alt || reference.reference;
|
|
208
|
-
this.
|
|
209
|
-
this.
|
|
224
|
+
this.element.setAttribute('aria-label', label);
|
|
225
|
+
this.element.dataset.inkkitFileState = this.previewFailed
|
|
210
226
|
? 'error'
|
|
211
227
|
: state.status;
|
|
212
|
-
this.
|
|
228
|
+
this.element.dataset.inkkitFileKind =
|
|
213
229
|
state.status === 'resolved' ? state.presentation.kind : 'file';
|
|
214
230
|
if (this.previewFailed) {
|
|
215
231
|
this.card(label, this.runtime.labels.filePreviewUnavailable, true);
|
|
@@ -247,7 +263,7 @@ class ImageView {
|
|
|
247
263
|
});
|
|
248
264
|
media.src = presentation.url;
|
|
249
265
|
this.media = media;
|
|
250
|
-
this.
|
|
266
|
+
this.element.append(media);
|
|
251
267
|
this.card(label);
|
|
252
268
|
}
|
|
253
269
|
else if (presentation.kind === 'pdf') {
|
|
@@ -263,7 +279,7 @@ class ImageView {
|
|
|
263
279
|
});
|
|
264
280
|
pdf.src = presentation.url;
|
|
265
281
|
this.pdf = pdf;
|
|
266
|
-
this.
|
|
282
|
+
this.element.append(pdf);
|
|
267
283
|
this.card(label, this.runtime.labels.filePDFPreview);
|
|
268
284
|
}
|
|
269
285
|
else
|
|
@@ -272,11 +288,11 @@ class ImageView {
|
|
|
272
288
|
}
|
|
273
289
|
const presentation = this.adapter?.presentation(String(this.node.attrs.src ?? ''));
|
|
274
290
|
if (!presentation) {
|
|
275
|
-
this.
|
|
276
|
-
this.
|
|
291
|
+
this.element.classList.add('image-absent');
|
|
292
|
+
this.element.textContent = alt || String(this.node.attrs.src ?? '');
|
|
277
293
|
return;
|
|
278
294
|
}
|
|
279
|
-
this.
|
|
295
|
+
this.element.classList.remove('image-absent');
|
|
280
296
|
this.renderImage(presentation.url, alt, width);
|
|
281
297
|
}
|
|
282
298
|
renderImage(url, alt, width) {
|
|
@@ -295,11 +311,12 @@ class ImageView {
|
|
|
295
311
|
this.image = image;
|
|
296
312
|
const preview = document.createElement('span');
|
|
297
313
|
preview.className = 'inkkit-image-preview';
|
|
314
|
+
preview.append(image);
|
|
315
|
+
// The grip sits on the whole card, so it centres on the picture and its name together.
|
|
298
316
|
const handle = document.createElement('span');
|
|
299
317
|
handle.className = 'image-handle';
|
|
300
318
|
handle.addEventListener('pointerdown', this.startResize);
|
|
301
|
-
|
|
302
|
-
this.dom.append(preview);
|
|
319
|
+
this.element.append(preview, handle);
|
|
303
320
|
}
|
|
304
321
|
startResize = (event) => {
|
|
305
322
|
event.preventDefault();
|
|
@@ -364,6 +381,18 @@ class ImageView {
|
|
|
364
381
|
return this.view.editable && !this.view.composing && this.canMutate();
|
|
365
382
|
}
|
|
366
383
|
}
|
|
384
|
+
const svg = 'http://www.w3.org/2000/svg';
|
|
385
|
+
function openArrow() {
|
|
386
|
+
const icon = document.createElementNS(svg, 'svg');
|
|
387
|
+
icon.setAttribute('viewBox', '0 0 12 12');
|
|
388
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
389
|
+
icon.setAttribute('focusable', 'false');
|
|
390
|
+
const path = document.createElementNS(svg, 'path');
|
|
391
|
+
path.setAttribute('d', 'M4 2.5h5.5V8M9.5 2.5 2.5 9.5');
|
|
392
|
+
icon.append(path);
|
|
393
|
+
return icon;
|
|
394
|
+
}
|
|
367
395
|
export function imageView(adapter, runtime, canMutate = () => true) {
|
|
368
|
-
|
|
396
|
+
const view = (schema) => $view(schema.node, () => (node, view, getPos) => new ImageView(node, view, getPos, adapter, runtime, canMutate));
|
|
397
|
+
return [view(imageSchema), view(imageBlockSchema)];
|
|
369
398
|
}
|
package/dist/paste.js
CHANGED
|
@@ -3,6 +3,7 @@ import { DOMParser as ProseParser, DOMSerializer, Fragment, Slice, } from '@milk
|
|
|
3
3
|
import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
|
|
4
4
|
import { $prose } from '@milkdown/kit/utils';
|
|
5
5
|
import { pasteSpreadsheet } from './spreadsheet.js';
|
|
6
|
+
import { isImage } from './image-blocks.js';
|
|
6
7
|
import { avoidReferenceCollisions, markdownFromHTML, } from './reference-clipboard.js';
|
|
7
8
|
import { InkKitError, } from './types.js';
|
|
8
9
|
export class PasteController {
|
|
@@ -334,14 +335,18 @@ export class PasteController {
|
|
|
334
335
|
const parsed = ctx.get(parserCtx)(markdown);
|
|
335
336
|
let imageIndex = 0;
|
|
336
337
|
const hydrate = (node) => {
|
|
337
|
-
if (node
|
|
338
|
+
if (isImage(node)) {
|
|
338
339
|
const slot = /^inkkit-clipboard-image:(\d+)$/.exec(String(node.attrs.src));
|
|
339
340
|
const position = slot ? Number(slot[1]) : imageIndex++;
|
|
340
341
|
const element = elements.some((element) => element.hasAttribute('data-inkkit-image-slot'))
|
|
341
342
|
? elements.find((element) => Number(element.getAttribute('data-inkkit-authored-image-slot') ?? element.getAttribute('data-inkkit-image-slot')) === position)
|
|
342
343
|
: elements[position];
|
|
343
|
-
if (!element || !container.contains(element))
|
|
344
|
-
|
|
344
|
+
if (!element || !container.contains(element)) {
|
|
345
|
+
const text = schema.text(`[${node.attrs.alt || 'Image'}: image unavailable]`);
|
|
346
|
+
return node.isBlock
|
|
347
|
+
? schema.nodes.paragraph.create(null, text)
|
|
348
|
+
: text;
|
|
349
|
+
}
|
|
345
350
|
return node.type.create({ ...node.attrs, src: element.getAttribute('src') }, null, node.marks);
|
|
346
351
|
}
|
|
347
352
|
if (node.isText)
|
package/dist/search.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type Transaction } from '@milkdown/kit/prose/state';
|
|
2
2
|
import type { EditorView } from '@milkdown/kit/prose/view';
|
|
3
3
|
export interface TextMatch {
|
|
4
4
|
from: number;
|
|
@@ -11,5 +11,5 @@ export interface LiteralReplacement extends TextMatch {
|
|
|
11
11
|
export declare function findLiteral(text: string, search: string, from?: number): TextMatch | undefined;
|
|
12
12
|
export declare function replaceLiteral(text: string, search: string, replacement: string, selection: TextMatch, all?: boolean): LiteralReplacement;
|
|
13
13
|
export declare function findFormatted(view: EditorView, text: string): boolean;
|
|
14
|
-
export declare function replaceFormatted(view: EditorView, search: string, replacement: string, preflight: (
|
|
15
|
-
export declare function replaceFormattedRange(view: EditorView, from: number, to: number, replacement: string, preflight: (
|
|
14
|
+
export declare function replaceFormatted(view: EditorView, search: string, replacement: string, preflight: (tr: Transaction) => void, all?: boolean): number;
|
|
15
|
+
export declare function replaceFormattedRange(view: EditorView, from: number, to: number, replacement: string, preflight: (tr: Transaction) => void): boolean;
|
package/dist/search.js
CHANGED
|
@@ -162,7 +162,7 @@ export function replaceFormatted(view, search, replacement, preflight, all = fal
|
|
|
162
162
|
tr = tr.setSelection(TextSelection.create(tr.doc, match.from, tr.mapping.map(match.to, 1)));
|
|
163
163
|
}
|
|
164
164
|
try {
|
|
165
|
-
preflight(tr
|
|
165
|
+
preflight(tr);
|
|
166
166
|
}
|
|
167
167
|
catch (error) {
|
|
168
168
|
if (error instanceof InkKitError)
|
|
@@ -189,7 +189,7 @@ export function replaceFormattedRange(view, from, to, replacement, preflight) {
|
|
|
189
189
|
const tr = closeHistory(view.state.tr.replace(from, to, replacementSlice(view.state, from, to, value)));
|
|
190
190
|
tr.setSelection(TextSelection.create(tr.doc, from, tr.mapping.map(to, 1)));
|
|
191
191
|
try {
|
|
192
|
-
preflight(tr
|
|
192
|
+
preflight(tr);
|
|
193
193
|
}
|
|
194
194
|
catch (error) {
|
|
195
195
|
if (error instanceof InkKitError)
|
package/dist/style.css
CHANGED
|
@@ -201,6 +201,54 @@
|
|
|
201
201
|
max-width: 100%;
|
|
202
202
|
vertical-align: bottom;
|
|
203
203
|
}
|
|
204
|
+
/* A standalone image is its own block; the row is full width so a click beside the card lands on it. */
|
|
205
|
+
.inkkit-root .ProseMirror .inkkit-image-block {
|
|
206
|
+
display: block;
|
|
207
|
+
margin: 0 0
|
|
208
|
+
var(--inkkit-paragraph-spacing, calc(var(--inkkit-font-size, 15px) * 0.667));
|
|
209
|
+
line-height: 0;
|
|
210
|
+
}
|
|
211
|
+
.inkkit-root .ProseMirror .inkkit-image-block > .image {
|
|
212
|
+
line-height: normal;
|
|
213
|
+
}
|
|
214
|
+
/* A card that shares its paragraph with text sits on its own line within it. */
|
|
215
|
+
.inkkit-root .ProseMirror :not(.inkkit-image-block) > .image.inkkit-file {
|
|
216
|
+
display: block;
|
|
217
|
+
width: fit-content;
|
|
218
|
+
}
|
|
219
|
+
.inkkit-root .ProseMirror .inkkit-image-block.ProseMirror-selectednode {
|
|
220
|
+
outline: none;
|
|
221
|
+
}
|
|
222
|
+
.inkkit-root
|
|
223
|
+
.ProseMirror
|
|
224
|
+
.inkkit-image-block.ProseMirror-selectednode
|
|
225
|
+
> .image {
|
|
226
|
+
outline: 2px solid color-mix(in srgb, var(--_inkkit-accent) 60%, transparent);
|
|
227
|
+
outline-offset: 2px;
|
|
228
|
+
border-radius: 6px;
|
|
229
|
+
}
|
|
230
|
+
.inkkit-root .ProseMirror-gapcursor {
|
|
231
|
+
display: none;
|
|
232
|
+
pointer-events: none;
|
|
233
|
+
position: absolute;
|
|
234
|
+
}
|
|
235
|
+
.inkkit-root .ProseMirror-gapcursor::after {
|
|
236
|
+
content: '';
|
|
237
|
+
display: block;
|
|
238
|
+
position: absolute;
|
|
239
|
+
top: -2px;
|
|
240
|
+
width: 20px;
|
|
241
|
+
border-top: 2px solid var(--_inkkit-accent);
|
|
242
|
+
animation: inkkit-gapcursor-blink 1.1s steps(2, start) infinite;
|
|
243
|
+
}
|
|
244
|
+
@keyframes inkkit-gapcursor-blink {
|
|
245
|
+
to {
|
|
246
|
+
visibility: hidden;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
.inkkit-root .ProseMirror-focused .ProseMirror-gapcursor {
|
|
250
|
+
display: block;
|
|
251
|
+
}
|
|
204
252
|
.inkkit-root .ProseMirror .image img {
|
|
205
253
|
display: block;
|
|
206
254
|
max-width: 100%;
|
|
@@ -222,6 +270,7 @@
|
|
|
222
270
|
position: absolute;
|
|
223
271
|
right: 0;
|
|
224
272
|
top: 50%;
|
|
273
|
+
z-index: 1;
|
|
225
274
|
transform: translateY(-50%);
|
|
226
275
|
width: 20px;
|
|
227
276
|
height: 32px;
|
|
@@ -255,7 +304,7 @@
|
|
|
255
304
|
vertical-align: middle;
|
|
256
305
|
border: 1px solid var(--_inkkit-rule);
|
|
257
306
|
border-radius: 6px;
|
|
258
|
-
padding:
|
|
307
|
+
padding: 6px;
|
|
259
308
|
white-space: normal;
|
|
260
309
|
}
|
|
261
310
|
|
|
@@ -263,13 +312,30 @@
|
|
|
263
312
|
display: flex;
|
|
264
313
|
flex-wrap: wrap;
|
|
265
314
|
align-items: center;
|
|
266
|
-
gap: 0.5em;
|
|
315
|
+
gap: 0.25em 0.5em;
|
|
267
316
|
font-size: 0.9em;
|
|
268
317
|
line-height: 1.4;
|
|
269
318
|
color: var(--_inkkit-muted);
|
|
270
319
|
}
|
|
271
320
|
.inkkit-root .ProseMirror .inkkit-file-footer:not(:first-child) {
|
|
272
|
-
margin-block-start: 0.
|
|
321
|
+
margin-block-start: 0.25em;
|
|
322
|
+
}
|
|
323
|
+
/* Below a picture, the name takes the picture's width rather than widening the card, so the grip
|
|
324
|
+
stays at the picture's edge; a short picture still leaves room for a few characters. */
|
|
325
|
+
.inkkit-root .ProseMirror .inkkit-image-preview ~ .inkkit-file-footer {
|
|
326
|
+
width: 0;
|
|
327
|
+
min-width: 100%;
|
|
328
|
+
}
|
|
329
|
+
.inkkit-root .ProseMirror .image.inkkit-file:has(> .inkkit-image-preview) {
|
|
330
|
+
min-width: min(100%, 8em);
|
|
331
|
+
}
|
|
332
|
+
/* The name truncates; the open arrow stays visible right after it. */
|
|
333
|
+
.inkkit-root .ProseMirror .inkkit-file-name {
|
|
334
|
+
display: flex;
|
|
335
|
+
flex: 1 1 auto;
|
|
336
|
+
align-items: center;
|
|
337
|
+
gap: 0.25em;
|
|
338
|
+
min-width: 0;
|
|
273
339
|
}
|
|
274
340
|
|
|
275
341
|
.inkkit-root .ProseMirror .inkkit-file audio,
|
|
@@ -290,7 +356,7 @@
|
|
|
290
356
|
}
|
|
291
357
|
|
|
292
358
|
.inkkit-root .ProseMirror .inkkit-file-label {
|
|
293
|
-
flex:
|
|
359
|
+
flex: 0 1 auto;
|
|
294
360
|
min-width: 0;
|
|
295
361
|
overflow: hidden;
|
|
296
362
|
text-overflow: ellipsis;
|
|
@@ -313,6 +379,34 @@
|
|
|
313
379
|
border-radius: 3px;
|
|
314
380
|
cursor: pointer;
|
|
315
381
|
}
|
|
382
|
+
.inkkit-root .ProseMirror .inkkit-file-open {
|
|
383
|
+
display: inline-flex;
|
|
384
|
+
align-items: center;
|
|
385
|
+
justify-content: center;
|
|
386
|
+
min-width: 1.5em;
|
|
387
|
+
min-height: 1.5em;
|
|
388
|
+
padding: 0;
|
|
389
|
+
color: var(--_inkkit-muted);
|
|
390
|
+
background: transparent;
|
|
391
|
+
border-color: transparent;
|
|
392
|
+
}
|
|
393
|
+
.inkkit-root .ProseMirror .inkkit-file-open:hover {
|
|
394
|
+
color: var(--_inkkit-text);
|
|
395
|
+
background: var(--_inkkit-code-bg);
|
|
396
|
+
}
|
|
397
|
+
.inkkit-root .ProseMirror .inkkit-file-open:focus-visible {
|
|
398
|
+
outline: 2px solid var(--_inkkit-accent);
|
|
399
|
+
outline-offset: 1px;
|
|
400
|
+
}
|
|
401
|
+
.inkkit-root .ProseMirror .inkkit-file-open svg {
|
|
402
|
+
width: 0.85em;
|
|
403
|
+
height: 0.85em;
|
|
404
|
+
fill: none;
|
|
405
|
+
stroke: currentColor;
|
|
406
|
+
stroke-width: 1.5;
|
|
407
|
+
stroke-linecap: round;
|
|
408
|
+
stroke-linejoin: round;
|
|
409
|
+
}
|
|
316
410
|
|
|
317
411
|
.inkkit-root .ProseMirror {
|
|
318
412
|
/* The viewport, not the wrapper Milkdown adds, so a click below the text still places the caret. */
|
package/dist/text-ranges.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { InkKitError } from './types.js';
|
|
2
2
|
import { fileReference } from './linked-syntax.js';
|
|
3
|
+
import { imageBlockName } from './image-blocks.js';
|
|
3
4
|
export function readableProjection(doc) {
|
|
4
5
|
const result = { text: '', spans: [] };
|
|
5
6
|
let blocks = 0;
|
|
@@ -24,6 +25,11 @@ export function readableProjection(doc) {
|
|
|
24
25
|
String(node.attrs.kind).replace(/^./, (value) => value.toUpperCase()), position, position + 1, 'embed');
|
|
25
26
|
return true;
|
|
26
27
|
}
|
|
28
|
+
if (name === imageBlockName) {
|
|
29
|
+
block(position);
|
|
30
|
+
append(fileReference(node).label || 'Image', position, position + node.nodeSize, 'embed');
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
27
33
|
if (node.isTextblock) {
|
|
28
34
|
block(position + 1);
|
|
29
35
|
if (!node.content.size)
|
|
@@ -128,9 +134,12 @@ export function formattedRects(view, projection, from, to) {
|
|
|
128
134
|
if (span.kind === 'embed' || span.kind === 'break') {
|
|
129
135
|
const node = view.nodeDOM(span.start);
|
|
130
136
|
if (node instanceof Element) {
|
|
137
|
+
// A standalone image's node is its full-width row; the card is what shows.
|
|
131
138
|
const label = node.matches('.inkkit-callout')
|
|
132
139
|
? (node.querySelector(':scope > .inkkit-callout-header > .inkkit-callout-title') ?? node)
|
|
133
|
-
: node
|
|
140
|
+
: node.matches('.inkkit-image-block')
|
|
141
|
+
? (node.firstElementChild ?? node)
|
|
142
|
+
: node;
|
|
134
143
|
rects.push(...Array.from(label.getClientRects(), textRect));
|
|
135
144
|
}
|
|
136
145
|
continue;
|