@aicayzer/inkkit 0.0.17 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/NOTICE +4 -2
  2. package/README.md +12 -6
  3. package/dist/THIRD-PARTY-NOTICES.txt +2742 -0
  4. package/dist/chunks/core-vtCr4MBS.js +56503 -0
  5. package/dist/chunks/highlight-Daj2fv_u.js +14122 -0
  6. package/dist/core.d.ts +4 -0
  7. package/dist/core.js +2 -0
  8. package/dist/editor.d.ts +261 -48
  9. package/dist/index.d.ts +1 -7
  10. package/dist/index.js +5 -2
  11. package/dist/style.css +242 -221
  12. package/dist/types.d.ts +143 -18
  13. package/package.json +26 -15
  14. package/dist/autolink.d.ts +0 -1
  15. package/dist/autolink.js +0 -18
  16. package/dist/callouts.d.ts +0 -26
  17. package/dist/callouts.js +0 -311
  18. package/dist/clipboard.d.ts +0 -11
  19. package/dist/clipboard.js +0 -261
  20. package/dist/code-fences.d.ts +0 -8
  21. package/dist/code-fences.js +0 -166
  22. package/dist/comments.d.ts +0 -55
  23. package/dist/comments.js +0 -494
  24. package/dist/decorations.d.ts +0 -2
  25. package/dist/decorations.js +0 -67
  26. package/dist/dialect.d.ts +0 -9
  27. package/dist/dialect.js +0 -358
  28. package/dist/editor.js +0 -2441
  29. package/dist/files.d.ts +0 -40
  30. package/dist/files.js +0 -133
  31. package/dist/highlight.d.ts +0 -1
  32. package/dist/highlight.js +0 -136
  33. package/dist/image-blocks.d.ts +0 -71
  34. package/dist/image-blocks.js +0 -286
  35. package/dist/images.d.ts +0 -51
  36. package/dist/images.js +0 -398
  37. package/dist/inline-highlight.d.ts +0 -6
  38. package/dist/inline-highlight.js +0 -112
  39. package/dist/labels.d.ts +0 -36
  40. package/dist/labels.js +0 -34
  41. package/dist/linked-syntax.d.ts +0 -48
  42. package/dist/linked-syntax.js +0 -320
  43. package/dist/literals.d.ts +0 -19
  44. package/dist/literals.js +0 -211
  45. package/dist/mermaid.d.ts +0 -8
  46. package/dist/mermaid.js +0 -321
  47. package/dist/outline.d.ts +0 -22
  48. package/dist/outline.js +0 -79
  49. package/dist/paste-html.d.ts +0 -23
  50. package/dist/paste-html.js +0 -441
  51. package/dist/paste.d.ts +0 -26
  52. package/dist/paste.js +0 -482
  53. package/dist/portable-files.d.ts +0 -10
  54. package/dist/portable-files.js +0 -36
  55. package/dist/preserve.d.ts +0 -21
  56. package/dist/preserve.js +0 -757
  57. package/dist/print.d.ts +0 -7
  58. package/dist/print.js +0 -409
  59. package/dist/reference-clipboard.d.ts +0 -8
  60. package/dist/reference-clipboard.js +0 -201
  61. package/dist/references.d.ts +0 -14
  62. package/dist/references.js +0 -368
  63. package/dist/search.d.ts +0 -15
  64. package/dist/search.js +0 -202
  65. package/dist/selection.d.ts +0 -1
  66. package/dist/selection.js +0 -59
  67. package/dist/source.d.ts +0 -16
  68. package/dist/source.js +0 -57
  69. package/dist/spreadsheet.d.ts +0 -3
  70. package/dist/spreadsheet.js +0 -275
  71. package/dist/table-alignment.d.ts +0 -2
  72. package/dist/table-alignment.js +0 -46
  73. package/dist/tables.d.ts +0 -27
  74. package/dist/tables.js +0 -322
  75. package/dist/tasks.d.ts +0 -3
  76. package/dist/tasks.js +0 -51
  77. package/dist/text-ranges.d.ts +0 -29
  78. package/dist/text-ranges.js +0 -256
  79. package/dist/types.js +0 -8
  80. package/dist/wiki-view.d.ts +0 -21
  81. package/dist/wiki-view.js +0 -78
package/dist/files.d.ts DELETED
@@ -1,40 +0,0 @@
1
- import type { DocumentContext, FileAdapter, FileContext, FilePresentation, FileReference } from './types.js';
2
- import type { EditorLabels } from './labels.js';
3
- export interface FileViewRuntime {
4
- adapter: FileAdapter;
5
- capture(): {
6
- context: DocumentContext;
7
- isCurrent(): boolean;
8
- };
9
- active(): boolean;
10
- labels: Readonly<EditorLabels>;
11
- error?(error: Error): void;
12
- }
13
- export declare function withFileSignal<T>(operation: Promise<T>, signal: AbortSignal): Promise<T>;
14
- export declare function resolveFile(adapter: FileAdapter, reference: FileReference, context: FileContext): Promise<FilePresentation>;
15
- export type FileResolutionState = {
16
- status: 'idle';
17
- } | {
18
- status: 'loading';
19
- } | {
20
- status: 'resolved';
21
- presentation: FilePresentation;
22
- } | {
23
- status: 'failed';
24
- error: Error;
25
- };
26
- export declare class FileResolution {
27
- private readonly runtime;
28
- private readonly changed;
29
- state: FileResolutionState;
30
- private reference?;
31
- private key;
32
- private request?;
33
- private dead;
34
- constructor(runtime: FileViewRuntime, changed: () => void);
35
- update(reference: FileReference): void;
36
- retry(): void;
37
- private current;
38
- cancel(): void;
39
- destroy(): void;
40
- }
package/dist/files.js DELETED
@@ -1,133 +0,0 @@
1
- function abort(signal) {
2
- if (signal.aborted)
3
- throw new DOMException('File resolution cancelled', 'AbortError');
4
- }
5
- export function withFileSignal(operation, signal) {
6
- return new Promise((resolve, reject) => {
7
- const cleanup = () => signal.removeEventListener('abort', cancelled);
8
- const cancelled = () => {
9
- cleanup();
10
- reject(new DOMException('File operation cancelled', 'AbortError'));
11
- };
12
- if (signal.aborted)
13
- cancelled();
14
- else
15
- signal.addEventListener('abort', cancelled, { once: true });
16
- operation.then((result) => {
17
- cleanup();
18
- if (signal.aborted)
19
- cancelled();
20
- else
21
- resolve(result);
22
- }, (error) => {
23
- cleanup();
24
- reject(error);
25
- });
26
- });
27
- }
28
- export async function resolveFile(adapter, reference, context) {
29
- abort(context.signal);
30
- const presentation = await withFileSignal(adapter.resolve(Object.freeze({ ...reference }), context), context.signal);
31
- abort(context.signal);
32
- if (!presentation ||
33
- typeof presentation !== 'object' ||
34
- !['image', 'audio', 'video', 'pdf', 'file', 'missing', 'error'].includes(presentation.kind))
35
- throw new TypeError('File adapter returned an unsupported presentation');
36
- if (presentation.label !== undefined &&
37
- typeof presentation.label !== 'string')
38
- throw new TypeError('File presentation label must be text');
39
- if (presentation.kind === 'error' && typeof presentation.message !== 'string')
40
- throw new TypeError('File presentation error must include a message');
41
- if ('url' in presentation) {
42
- if (typeof presentation.url !== 'string' || !presentation.url.trim())
43
- throw new TypeError('File presentation needs a resource URL');
44
- const url = new URL(presentation.url, document.baseURI);
45
- if (['javascript:', 'vbscript:'].includes(url.protocol))
46
- throw new TypeError('Executable file presentation URLs are not supported');
47
- const prefix = presentation.kind === 'pdf' ? 'application/pdf' : `${presentation.kind}/`;
48
- const mime = presentation.mimeType?.toLowerCase().split(';')[0]?.trim();
49
- if (mime &&
50
- (presentation.kind === 'pdf' ? mime !== prefix : !mime.startsWith(prefix)))
51
- throw new TypeError('File presentation MIME type does not match its kind');
52
- const dataMime = url.pathname.toLowerCase().split(/[;,]/)[0] ?? '';
53
- if (url.protocol === 'data:' &&
54
- (presentation.kind === 'pdf'
55
- ? dataMime !== prefix
56
- : !dataMime.startsWith(prefix)))
57
- throw new TypeError('File presentation data URL does not match its kind');
58
- }
59
- else if (['image', 'audio', 'video', 'pdf'].includes(presentation.kind)) {
60
- throw new TypeError('File presentation needs a resource URL');
61
- }
62
- return Object.freeze({ ...presentation });
63
- }
64
- export class FileResolution {
65
- runtime;
66
- changed;
67
- state = { status: 'idle' };
68
- reference;
69
- key = '';
70
- request;
71
- dead = false;
72
- constructor(runtime, changed) {
73
- this.runtime = runtime;
74
- this.changed = changed;
75
- }
76
- update(reference) {
77
- const key = JSON.stringify([
78
- reference.reference,
79
- reference.kind,
80
- reference.fragment,
81
- ]);
82
- this.reference = Object.freeze({ ...reference });
83
- if (this.key === key && this.request?.isCurrent())
84
- return;
85
- this.key = key;
86
- this.retry();
87
- }
88
- retry() {
89
- this.cancel();
90
- if (this.dead || !this.reference || !this.runtime.active())
91
- return;
92
- const captured = this.runtime.capture();
93
- const request = {
94
- controller: new AbortController(),
95
- isCurrent: captured.isCurrent,
96
- };
97
- this.request = request;
98
- this.state = { status: 'loading' };
99
- this.changed();
100
- void resolveFile(this.runtime.adapter, this.reference, {
101
- ...captured.context,
102
- signal: request.controller.signal,
103
- }).then((presentation) => {
104
- if (!this.current(request))
105
- return;
106
- this.state = { status: 'resolved', presentation };
107
- this.changed();
108
- }, (failure) => {
109
- if (!this.current(request))
110
- return;
111
- const error = failure instanceof Error ? failure : new Error(String(failure));
112
- this.state = { status: 'failed', error };
113
- this.changed();
114
- this.runtime.error?.(error);
115
- });
116
- }
117
- current(request) {
118
- return (!this.dead &&
119
- this.request === request &&
120
- !request.controller.signal.aborted &&
121
- this.runtime.active() &&
122
- request.isCurrent());
123
- }
124
- cancel() {
125
- this.request?.controller.abort();
126
- this.request = undefined;
127
- this.state = { status: 'idle' };
128
- }
129
- destroy() {
130
- this.dead = true;
131
- this.cancel();
132
- }
133
- }
@@ -1 +0,0 @@
1
- export declare const highlightPlugin: import("@milkdown/kit/utils").$Prose;
package/dist/highlight.js DELETED
@@ -1,136 +0,0 @@
1
- import { common, createLowlight } from 'lowlight';
2
- import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
3
- import { Decoration, DecorationSet } from '@milkdown/kit/prose/view';
4
- import { $prose } from '@milkdown/kit/utils';
5
- const lowlight = createLowlight(common);
6
- // Colors are decorations over the plain text, so the markdown is untouched;
7
- // a block without a known language stays plain.
8
- function blockDecorations(node, pos) {
9
- const language = String(node.attrs.language ?? '').toLowerCase();
10
- const found = [];
11
- if (!language || !lowlight.registered(language))
12
- return found;
13
- let offset = pos + 1;
14
- const walk = (children, classes) => {
15
- for (const child of children) {
16
- if (child.type === 'text') {
17
- const end = offset + child.value.length;
18
- if (classes.length > 0)
19
- found.push(Decoration.inline(offset, end, { class: classes.join(' ') }));
20
- offset = end;
21
- }
22
- else if (child.type === 'element') {
23
- const own = child.properties?.className ?? [];
24
- walk(child.children, [...classes, ...own]);
25
- }
26
- }
27
- };
28
- walk(lowlight.highlight(language, node.textContent).children, []);
29
- return found;
30
- }
31
- function decorations(doc) {
32
- const found = [];
33
- doc.descendants((node, pos) => {
34
- if (!node.isTextblock)
35
- return true;
36
- if (node.type.name === 'code_block')
37
- for (const decoration of blockDecorations(node, pos))
38
- found.push(decoration);
39
- return false;
40
- });
41
- return DecorationSet.create(doc, found);
42
- }
43
- // Every range of the new document an update may have touched: each step's own
44
- // range, plus the whole span where the two documents differ, which also covers
45
- // steps that change a node without changing its size.
46
- function touched(tr) {
47
- const ranges = [];
48
- tr.mapping.maps.forEach((map, index) => {
49
- const rest = tr.mapping.slice(index + 1);
50
- map.forEach((_, __, from, to) => ranges.push([rest.map(from, -1), rest.map(to, 1)]));
51
- });
52
- const start = tr.before.content.findDiffStart(tr.doc.content);
53
- if (start != null) {
54
- const end = tr.before.content.findDiffEnd(tr.doc.content);
55
- // The scans overlap when inserted or deleted text repeats its neighbours.
56
- if (end)
57
- ranges.push([start, end.b + Math.max(0, start - Math.min(end.a, end.b))]);
58
- }
59
- return ranges;
60
- }
61
- // Mapping carries colors of unchanged blocks to their new positions without
62
- // reading them, but leaves stale colors inside any block that was edited. Those
63
- // blocks are highlighted again; no other block is. Stale colors are dropped by
64
- // rebuilding the set: DecorationSet.remove compares every removed decoration
65
- // with every other in its block, which is quadratic in a large block.
66
- function update(mapped, tr) {
67
- const stale = [];
68
- const fresh = [];
69
- const seen = new Set();
70
- for (const [from, to] of touched(tr)) {
71
- let start = from;
72
- let end = to;
73
- const blocks = [];
74
- tr.doc.nodesBetween(from, to, (node, pos) => {
75
- if (!node.isTextblock)
76
- return true;
77
- start = Math.min(start, pos);
78
- end = Math.max(end, pos + node.nodeSize);
79
- if (node.type.name === 'code_block' && !seen.has(pos)) {
80
- seen.add(pos);
81
- blocks.push([node, pos]);
82
- }
83
- return false;
84
- });
85
- stale.push([start, end]);
86
- for (const [node, pos] of blocks)
87
- for (const decoration of blockDecorations(node, pos))
88
- fresh.push(decoration);
89
- }
90
- const found = [];
91
- for (const [start, end] of stale)
92
- for (const decoration of mapped.find(start, end))
93
- found.push(decoration);
94
- if (found.length === 0)
95
- return mapped.add(tr.doc, fresh);
96
- // remove is quadratic in the decorations it removes, so only a large block takes the rebuild below.
97
- if (found.length <= 256)
98
- return mapped.remove(found).add(tr.doc, fresh);
99
- stale.sort((a, b) => a[0] - b[0]);
100
- const merged = [];
101
- for (const range of stale) {
102
- const last = merged.at(-1);
103
- if (last && range[0] <= last[1])
104
- last[1] = Math.max(last[1], range[1]);
105
- else
106
- merged.push([range[0], range[1]]);
107
- }
108
- const kept = mapped.find().filter((decoration) => {
109
- let low = 0;
110
- let high = merged.length;
111
- while (low < high) {
112
- const middle = (low + high) >> 1;
113
- if (merged[middle][0] < decoration.to)
114
- low = middle + 1;
115
- else
116
- high = middle;
117
- }
118
- const range = merged[low - 1];
119
- return !range || range[1] <= decoration.from;
120
- });
121
- return DecorationSet.create(tr.doc, kept.concat(fresh));
122
- }
123
- export const highlightPlugin = $prose(() => new Plugin({
124
- key: new PluginKey('highlight'),
125
- state: {
126
- init: (_, state) => decorations(state.doc),
127
- apply: (tr, previous) => tr.docChanged
128
- ? update(previous.map(tr.mapping, tr.doc), tr)
129
- : previous,
130
- },
131
- props: {
132
- decorations(state) {
133
- return this.getState(state);
134
- },
135
- },
136
- }));
@@ -1,71 +0,0 @@
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 {};
@@ -1,286 +0,0 @@
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();