@aicayzer/inkkit 0.0.9 → 0.0.11

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/images.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { imageSchema } from '@milkdown/kit/preset/commonmark';
2
+ import { closeHistory } from '@milkdown/kit/prose/history';
2
3
  import { $view } from '@milkdown/kit/utils';
4
+ import { FileResolution } from './files.js';
5
+ import { fileReference } from './linked-syntax.js';
3
6
  /** A width rides in the alt text, as `![a picture|400](images/….png)`. Other Markdown readers
4
7
  * show the picture and keep the number without requiring a private extension. */
5
8
  export function splitAlt(alt) {
@@ -12,75 +15,274 @@ export function joinAlt(alt, width) {
12
15
  return width == null ? alt : `${alt}|${width}`;
13
16
  }
14
17
  const minimumWidth = 48;
15
- /** Draws the image and the handle that sizes it. The width is kept in the alt text, so the schema is the
16
- * one commonmark gives us; a picture is never wider than its editor, whatever number the alt text holds. */
17
18
  class ImageView {
18
19
  node;
19
20
  view;
20
21
  getPos;
21
22
  adapter;
23
+ runtime;
24
+ canMutate;
22
25
  dom;
23
26
  image;
27
+ media;
28
+ pdf;
24
29
  width = null;
25
30
  stopResize;
26
- constructor(node, view, getPos, adapter) {
31
+ resolution;
32
+ previewFailed = false;
33
+ constructor(node, view, getPos, adapter, runtime, canMutate = () => true) {
27
34
  this.node = node;
28
35
  this.view = view;
29
36
  this.getPos = getPos;
30
37
  this.adapter = adapter;
38
+ this.runtime = runtime;
39
+ this.canMutate = canMutate;
31
40
  this.dom = document.createElement('span');
32
41
  this.dom.className = 'image';
33
42
  this.view.dom.addEventListener('inkkit-cancel-resize', this.cancelResize);
43
+ this.view.dom.addEventListener('inkkit-media-policy', this.mediaPolicy);
44
+ if (runtime) {
45
+ this.resolution = new FileResolution({ ...runtime, active: () => this.active() }, () => {
46
+ this.previewFailed = false;
47
+ this.render();
48
+ });
49
+ this.resolution.update(this.reference());
50
+ }
34
51
  this.render();
35
52
  }
53
+ reference() {
54
+ return fileReference(this.node);
55
+ }
56
+ active() {
57
+ return (!!this.runtime?.active() &&
58
+ !this.dom.closest('[data-inkkit-folded="true"], [hidden]'));
59
+ }
36
60
  update(node) {
37
61
  if (node.type !== this.node.type)
38
62
  return false;
63
+ if (node.sameMarkup(this.node)) {
64
+ this.node = node;
65
+ return true;
66
+ }
39
67
  this.node = node;
68
+ this.previewFailed = false;
69
+ this.resolution?.update(this.reference());
40
70
  this.render();
41
71
  return true;
42
72
  }
43
- /// The handle's own events are the view's business, not the document's.
44
73
  stopEvent(event) {
45
- return (event.target instanceof HTMLElement &&
46
- event.target.classList.contains('image-handle'));
74
+ return (event.target instanceof Element &&
75
+ !!event.target.closest('.image-handle, .inkkit-file-control, audio, video, iframe'));
47
76
  }
48
77
  ignoreMutation() {
49
78
  return true;
50
79
  }
80
+ mediaPolicy = () => {
81
+ if (!this.runtime)
82
+ return;
83
+ if (!this.active()) {
84
+ this.stopResize?.();
85
+ this.stopResize = undefined;
86
+ this.resolution?.cancel();
87
+ this.releaseMedia();
88
+ this.render();
89
+ }
90
+ else
91
+ this.resolution?.update(this.reference());
92
+ };
51
93
  cancelResize = () => {
94
+ if (!this.stopResize)
95
+ return;
52
96
  this.stopResize?.();
53
97
  this.stopResize = undefined;
54
98
  this.render();
55
99
  };
100
+ releaseMedia() {
101
+ const media = this.media;
102
+ this.media = undefined;
103
+ if (media) {
104
+ media.pause();
105
+ media.removeAttribute('src');
106
+ media.load();
107
+ }
108
+ this.pdf?.removeAttribute('src');
109
+ this.pdf = undefined;
110
+ }
56
111
  destroy() {
57
112
  this.view.dom.removeEventListener('inkkit-cancel-resize', this.cancelResize);
113
+ this.view.dom.removeEventListener('inkkit-media-policy', this.mediaPolicy);
58
114
  this.stopResize?.();
59
115
  this.stopResize = undefined;
116
+ this.resolution?.destroy();
117
+ this.releaseMedia();
118
+ }
119
+ button(label, action) {
120
+ const button = document.createElement('button');
121
+ button.type = 'button';
122
+ button.className = 'inkkit-file-control';
123
+ button.textContent = label;
124
+ button.addEventListener('click', (event) => {
125
+ event.preventDefault();
126
+ action();
127
+ });
128
+ return button;
129
+ }
130
+ activate = () => {
131
+ if (!this.runtime || !this.active())
132
+ return;
133
+ const captured = this.runtime.capture();
134
+ try {
135
+ if (captured.isCurrent())
136
+ this.runtime.adapter.open?.(this.reference(), captured.context);
137
+ }
138
+ catch (error) {
139
+ this.report(error);
140
+ }
141
+ };
142
+ report(error) {
143
+ this.runtime?.error?.(error instanceof Error ? error : new Error(String(error)));
144
+ }
145
+ card(label, status, retry = false) {
146
+ const text = document.createElement('span');
147
+ text.className = 'inkkit-file-label';
148
+ text.textContent = status ? `${label}: ${status}` : label;
149
+ if (status)
150
+ text.setAttribute('role', 'status');
151
+ this.dom.append(text);
152
+ if (retry && this.runtime)
153
+ this.dom.append(this.button(this.runtime.labels.fileRetry, () => this.resolution?.retry()));
154
+ if (this.runtime?.adapter.open) {
155
+ const open = this.button(this.runtime.labels.fileOpen, this.activate);
156
+ open.setAttribute('aria-label', `${this.runtime.labels.fileOpen} ${label}`);
157
+ this.dom.append(open);
158
+ }
159
+ }
160
+ failPreview() {
161
+ this.previewFailed = true;
162
+ this.runtime?.error?.(new Error(this.runtime.labels.filePreviewUnavailable));
163
+ this.render();
60
164
  }
61
165
  render() {
62
166
  this.stopResize?.();
63
167
  this.stopResize = undefined;
64
- const src = String(this.node.attrs.src ?? '');
65
- const { alt, width } = splitAlt(String(this.node.attrs.alt ?? ''));
168
+ this.releaseMedia();
169
+ const reference = this.reference();
170
+ const { alt, width } = this.node.attrs.inkkitFileRaw
171
+ ? {
172
+ alt: reference.label ?? reference.reference,
173
+ width: reference.width ?? null,
174
+ }
175
+ : splitAlt(String(this.node.attrs.alt ?? ''));
66
176
  this.width = width;
67
177
  this.dom.textContent = '';
68
178
  this.image = undefined;
69
- // Only the host adapter decides which authored references can be presented.
70
- const presentation = this.adapter?.presentation(src);
179
+ if (this.runtime) {
180
+ this.dom.classList.add('inkkit-file');
181
+ this.dom.oncontextmenu = (event) => {
182
+ if (!this.runtime?.adapter.contextMenu || !this.active())
183
+ return;
184
+ const captured = this.runtime.capture();
185
+ if (!captured.isCurrent())
186
+ return;
187
+ event.preventDefault();
188
+ try {
189
+ this.runtime.adapter.contextMenu(this.reference(), { clientX: event.clientX, clientY: event.clientY }, captured.context);
190
+ }
191
+ catch (error) {
192
+ this.report(error);
193
+ }
194
+ };
195
+ const state = this.resolution.state;
196
+ const label = state.status === 'resolved'
197
+ ? state.presentation.label || alt || reference.reference
198
+ : alt || reference.reference;
199
+ this.dom.setAttribute('aria-label', label);
200
+ this.dom.dataset.inkkitFileState = this.previewFailed
201
+ ? 'error'
202
+ : state.status;
203
+ this.dom.dataset.inkkitFileKind =
204
+ state.status === 'resolved' ? state.presentation.kind : 'file';
205
+ if (this.previewFailed) {
206
+ this.card(label, this.runtime.labels.filePreviewUnavailable, true);
207
+ return;
208
+ }
209
+ if (state.status === 'loading' || state.status === 'idle') {
210
+ this.card(label, this.runtime.labels.fileLoading, true);
211
+ return;
212
+ }
213
+ if (state.status === 'failed') {
214
+ this.card(label, this.runtime.labels.fileError, true);
215
+ return;
216
+ }
217
+ const presentation = state.presentation;
218
+ if (presentation.kind === 'missing' || presentation.kind === 'error') {
219
+ this.card(label, presentation.kind === 'missing'
220
+ ? this.runtime.labels.fileMissing
221
+ : this.runtime.labels.fileError, true);
222
+ }
223
+ else if (presentation.kind === 'image') {
224
+ this.renderImage(presentation.url, label, width);
225
+ this.card(label);
226
+ }
227
+ else if (presentation.kind === 'audio' ||
228
+ presentation.kind === 'video') {
229
+ const media = document.createElement(presentation.kind);
230
+ media.controls = true;
231
+ media.preload = 'metadata';
232
+ if (width != null)
233
+ media.style.width = `${width}px`;
234
+ media.setAttribute('aria-label', label);
235
+ media.addEventListener('error', () => {
236
+ if (this.media === media)
237
+ this.failPreview();
238
+ });
239
+ media.src = presentation.url;
240
+ this.media = media;
241
+ this.dom.append(media);
242
+ this.card(label);
243
+ }
244
+ else if (presentation.kind === 'pdf') {
245
+ const pdf = document.createElement('iframe');
246
+ pdf.title = label;
247
+ pdf.setAttribute('sandbox', '');
248
+ pdf.referrerPolicy = 'no-referrer';
249
+ if (width != null)
250
+ pdf.style.width = `${width}px`;
251
+ pdf.addEventListener('error', () => {
252
+ if (this.pdf === pdf)
253
+ this.failPreview();
254
+ });
255
+ pdf.src = presentation.url;
256
+ this.pdf = pdf;
257
+ this.dom.append(pdf);
258
+ this.card(label, this.runtime.labels.filePDFPreview);
259
+ }
260
+ else
261
+ this.card(label);
262
+ return;
263
+ }
264
+ const presentation = this.adapter?.presentation(String(this.node.attrs.src ?? ''));
71
265
  if (!presentation) {
72
266
  this.dom.classList.add('image-absent');
73
- this.dom.textContent = alt || src;
267
+ this.dom.textContent = alt || String(this.node.attrs.src ?? '');
74
268
  return;
75
269
  }
76
270
  this.dom.classList.remove('image-absent');
271
+ this.renderImage(presentation.url, alt, width);
272
+ }
273
+ renderImage(url, alt, width) {
77
274
  const image = document.createElement('img');
78
- image.src = presentation.url;
79
275
  image.alt = alt;
80
276
  if (this.node.attrs.title)
81
277
  image.title = String(this.node.attrs.title);
82
278
  if (width != null)
83
279
  image.style.width = `${width}px`;
280
+ if (this.runtime)
281
+ image.addEventListener('error', () => {
282
+ if (this.image === image)
283
+ this.failPreview();
284
+ });
285
+ image.src = url;
84
286
  this.image = image;
85
287
  const handle = document.createElement('span');
86
288
  handle.className = 'image-handle';
@@ -88,53 +290,68 @@ class ImageView {
88
290
  this.dom.append(image, handle);
89
291
  }
90
292
  startResize = (event) => {
293
+ event.preventDefault();
91
294
  const image = this.image;
92
- if (!image || !this.view.editable)
295
+ if (!image || !this.canResize())
93
296
  return;
94
- event.preventDefault();
95
297
  const handle = event.currentTarget;
96
298
  const startX = event.clientX;
97
299
  const startWidth = image.getBoundingClientRect().width;
98
300
  const limit = this.view.dom.clientWidth;
99
301
  handle.setPointerCapture?.(event.pointerId);
100
302
  const move = (moved) => {
101
- if (!this.view.editable) {
303
+ if (!this.canResize()) {
102
304
  this.cancelResize();
103
305
  return;
104
306
  }
105
- const next = Math.round(Math.min(limit, Math.max(minimumWidth, startWidth + (moved.clientX - startX))));
106
- this.width = next;
107
- image.style.width = `${next}px`;
307
+ this.width = Math.round(Math.min(limit, Math.max(minimumWidth, startWidth + moved.clientX - startX)));
308
+ image.style.width = `${this.width}px`;
108
309
  };
109
310
  const cleanup = () => {
110
311
  handle.removeEventListener('pointermove', move);
111
312
  handle.removeEventListener('pointerup', done);
112
- handle.removeEventListener('pointercancel', done);
313
+ handle.removeEventListener('pointercancel', cancel);
113
314
  };
114
315
  const done = () => {
115
316
  cleanup();
116
317
  this.stopResize = undefined;
117
318
  this.commit();
118
319
  };
320
+ const cancel = () => {
321
+ cleanup();
322
+ this.stopResize = undefined;
323
+ this.render();
324
+ };
119
325
  this.stopResize = cleanup;
120
326
  handle.addEventListener('pointermove', move);
121
327
  handle.addEventListener('pointerup', done);
122
- handle.addEventListener('pointercancel', done);
328
+ handle.addEventListener('pointercancel', cancel);
123
329
  };
124
330
  commit() {
125
- if (!this.view.editable)
331
+ if (!this.canResize()) {
332
+ this.render();
126
333
  return;
334
+ }
127
335
  const pos = this.getPos();
128
336
  if (pos == null)
129
337
  return;
130
- const { alt } = splitAlt(String(this.node.attrs.alt ?? ''));
338
+ const alt = this.node.attrs.inkkitFileRaw
339
+ ? (this.reference().label ?? this.reference().reference)
340
+ : splitAlt(String(this.node.attrs.alt ?? '')).alt;
131
341
  const next = joinAlt(alt, this.width);
132
342
  if (next === this.node.attrs.alt)
133
343
  return;
134
344
  const { state } = this.view;
135
- this.view.dispatch(state.tr.setNodeMarkup(pos, undefined, { ...this.node.attrs, alt: next }));
345
+ this.view.dispatch(closeHistory(state.tr.setNodeMarkup(pos, undefined, {
346
+ ...this.node.attrs,
347
+ alt: next,
348
+ })));
349
+ this.view.dispatch(closeHistory(this.view.state.tr));
350
+ }
351
+ canResize() {
352
+ return this.view.editable && !this.view.composing && this.canMutate();
136
353
  }
137
354
  }
138
- export function imageView(adapter) {
139
- return $view(imageSchema.node, () => (node, view, getPos) => new ImageView(node, view, getPos, adapter));
355
+ export function imageView(adapter, runtime, canMutate = () => true) {
356
+ return $view(imageSchema.node, () => (node, view, getPos) => new ImageView(node, view, getPos, adapter, runtime, canMutate));
140
357
  }
package/dist/index.d.ts CHANGED
@@ -3,5 +3,5 @@ export type { EditorEvents, EditorOptions, TextInputPreferences, CommandState, E
3
3
  export type { TableCommand, TableOptions } from './tables.js';
4
4
  export type { Heading } from './outline.js';
5
5
  export { InkKitError } from './types.js';
6
- export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, ClipboardDiagram, PrintableDocument, PrintableWarning, TextRange, ReadableTextSnapshot, TextRect, ViewportInsets, ViewportOptions, ViewportSnapshot, } from './types.js';
6
+ export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, WikiLinkReference, WikiLinkAdapter, FileReference, FileContext, FilePresentation, FileAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, ClipboardDiagram, PrintableDocument, PrintableWarning, TextRange, ReadableTextSnapshot, TextRect, ViewportInsets, ViewportOptions, ViewportSnapshot, } from './types.js';
7
7
  export type { EditorLabels } from './labels.js';
package/dist/labels.d.ts CHANGED
@@ -22,6 +22,14 @@ export interface EditorLabels {
22
22
  tableAlignRight: string;
23
23
  tableMoveColumn: string;
24
24
  tableMoveRow: string;
25
+ fileLoading: string;
26
+ fileMissing: string;
27
+ fileError: string;
28
+ fileRetry: string;
29
+ fileOpen: string;
30
+ filePreviewUnavailable: string;
31
+ filePDFPreview: string;
32
+ wikiMissing: string;
25
33
  }
26
34
  export declare const defaultLabels: Readonly<EditorLabels>;
27
35
  export declare const labelsCtx: import("@milkdown/kit/utils").$Ctx<Readonly<EditorLabels>, "inkkitLabels">;
package/dist/labels.js CHANGED
@@ -19,6 +19,14 @@ export const defaultLabels = {
19
19
  tableAlignRight: 'Align right',
20
20
  tableMoveColumn: 'Move column',
21
21
  tableMoveRow: 'Move row',
22
+ fileLoading: 'Loading file…',
23
+ fileMissing: 'File unavailable',
24
+ fileError: 'File could not be loaded',
25
+ fileRetry: 'Retry',
26
+ fileOpen: 'Open',
27
+ filePreviewUnavailable: 'Preview unavailable',
28
+ filePDFPreview: 'Open to view this PDF',
29
+ wikiMissing: 'Link unavailable',
22
30
  };
23
31
  export const labelsCtx = $ctx(defaultLabels, 'inkkitLabels');
24
32
  export function editorLabels(ctx) {
@@ -0,0 +1,48 @@
1
+ import type { Node as ProseNode } from '@milkdown/kit/prose/model';
2
+ import type { Literal } from 'mdast';
3
+ import type { FileReference, WikiLinkReference } from './types.js';
4
+ interface WikiLink extends Literal {
5
+ type: 'inkkitWikiLink';
6
+ raw: string;
7
+ target: string;
8
+ fragment: string | null;
9
+ label: string;
10
+ inTable: boolean;
11
+ }
12
+ declare module 'mdast' {
13
+ interface PhrasingContentMap {
14
+ inkkitWikiLink: WikiLink;
15
+ }
16
+ interface RootContentMap {
17
+ inkkitWikiLink: WikiLink;
18
+ }
19
+ interface Image {
20
+ inkkitFileRaw?: string;
21
+ inkkitFileTarget?: string;
22
+ inkkitFileFragment?: string | null;
23
+ inkkitFileWidth?: number | null;
24
+ inkkitFileInTable?: boolean;
25
+ }
26
+ }
27
+ declare module 'micromark-util-types' {
28
+ interface TokenTypeMap {
29
+ inkkitLinkedSyntax: 'inkkitLinkedSyntax';
30
+ }
31
+ }
32
+ export interface LinkedSyntax {
33
+ raw: string;
34
+ target: string;
35
+ fragment: string | null;
36
+ label: string;
37
+ width: number | null;
38
+ embed: boolean;
39
+ inTable: boolean;
40
+ }
41
+ export declare function parseLinkedSyntax(raw: string, inTable?: boolean): LinkedSyntax | undefined;
42
+ export declare function wikiMarkdown(node: ProseNode, inTable?: boolean): string;
43
+ export declare function wikiReference(node: ProseNode): WikiLinkReference;
44
+ export declare function fileReference(node: ProseNode): FileReference;
45
+ export declare function namedFileMarkdown(node: ProseNode, inTable?: boolean): string | undefined;
46
+ export declare const wikiLinkSchema: import("@milkdown/kit/utils").$Node;
47
+ export declare function linkedSyntax(wikiLinks: boolean, files: boolean): import("@milkdown/kit/utils").$Remark<"inkkitLinkedSyntax", unknown>;
48
+ export {};