@aicayzer/inkkit 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,3 +46,5 @@ Start with the [feature guide](docs/supported-syntax.md) and [simple and rich in
46
46
  Scope and release status live in the [InkKit Project](https://github.com/users/aicayzer/projects/3) and [version milestones](https://github.com/aicayzer/inkkit/milestones). Optional media extensions are tracked there. Only documented, released features form the supported package contract.
47
47
 
48
48
  MIT licensed. InkKit builds on the public PadPad and Memos editors; see [NOTICE](NOTICE).
49
+
50
+ Optional wiki links and a host-owned asynchronous file adapter support named/path images, audio, video, PDFs and file cards. Authored source and sizing are preserved; portable copy/print uses validated images or documented descriptive fallbacks. See [linked syntax](docs/supported-syntax.md#linked-syntax) and [adapter contracts](docs/api.md#optional-wiki-links-and-files).
package/dist/callouts.js CHANGED
@@ -243,7 +243,7 @@ export const calloutView = $prose((ctx) => new Plugin({
243
243
  key: new PluginKey('inkkitCalloutView'),
244
244
  props: {
245
245
  nodeViews: {
246
- inkkit_callout(node) {
246
+ inkkit_callout(node, view) {
247
247
  const dom = document.createElement('blockquote');
248
248
  dom.className = 'inkkit-callout';
249
249
  dom.setAttribute('data-inkkit-callout', String(node.attrs.kind));
@@ -265,6 +265,7 @@ export const calloutView = $prose((ctx) => new Plugin({
265
265
  .toLowerCase()
266
266
  .replace(/^./, (letter) => letter.toUpperCase());
267
267
  dom.setAttribute('data-inkkit-folded', String(folded));
268
+ view.dom.dispatchEvent(new Event('inkkit-media-policy'));
268
269
  button.hidden = !node.attrs.fold;
269
270
  button.setAttribute('aria-expanded', String(!folded));
270
271
  button.setAttribute('aria-label', `${folded ? editorLabels(ctx).expandCallout : editorLabels(ctx).collapseCallout} ${title.textContent}`);
@@ -1,11 +1,11 @@
1
1
  import { type Fragment, type Schema } from '@milkdown/kit/prose/model';
2
- import type { ClipboardOutput, DocumentContext, ImageAdapter } from './types.js';
2
+ import type { ClipboardOutput, DocumentContext, ImageAdapter, FileAdapter } from './types.js';
3
3
  export interface ClipboardContent {
4
4
  text: string;
5
5
  html: string;
6
6
  }
7
- export declare function clipboardText(content: Fragment, missingImages?: ReadonlySet<string>): string;
7
+ export declare function clipboardText(content: Fragment, missingImages?: ReadonlySet<string>, fileDescriptions?: ReadonlyMap<object, string>): string;
8
8
  export declare function clipboardContent(content: Fragment, schema: Schema): ClipboardContent;
9
9
  export declare const visibleClipboard: import("@milkdown/kit/utils").$Prose;
10
10
  /** Export a frozen document fragment; presentation URLs never enter the clipboard. */
11
- export declare function portableClipboard(content: Fragment, schema: Schema, markdown: string, adapter: ImageAdapter | undefined, context: DocumentContext, completeDiagrams?: ReadonlySet<string>): Promise<ClipboardOutput>;
11
+ export declare function portableClipboard(content: Fragment, schema: Schema, markdown: string, adapter: ImageAdapter | undefined, context: DocumentContext, completeDiagrams?: ReadonlySet<string>, files?: FileAdapter, signal?: AbortSignal): Promise<ClipboardOutput>;
package/dist/clipboard.js CHANGED
@@ -3,30 +3,35 @@ import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
3
3
  import { $prose } from '@milkdown/kit/utils';
4
4
  import { shareableFragment } from './comments.js';
5
5
  import { diagramImage } from './mermaid.js';
6
+ import { fileReference } from './linked-syntax.js';
7
+ import { portableFile, portableFileImage } from './portable-files.js';
8
+ import { withFileSignal } from './files.js';
6
9
  // Clipboard text is the visible document, not Markdown serialization: escaping
7
10
  // punctuation or trailing spaces here leaks source syntax into other apps.
8
- export function clipboardText(content, missingImages = new Set()) {
11
+ export function clipboardText(content, missingImages = new Set(), fileDescriptions = new Map()) {
9
12
  content = shareableFragment(content);
10
13
  const blocks = [];
11
14
  content.forEach((node) => {
12
15
  if (node.type.name !== 'reference_definition')
13
- blocks.push(nodeText(node, missingImages));
16
+ blocks.push(nodeText(node, missingImages, fileDescriptions));
14
17
  });
15
18
  const inline = content.firstChild?.isInline ?? false;
16
19
  return blocks.join(inline ? '' : '\n\n');
17
20
  }
18
- function nodeText(node, missingImages) {
21
+ function nodeText(node, missingImages, fileDescriptions) {
22
+ if (node.type.name === 'inkkit_wiki_link')
23
+ return String(node.attrs.label || node.attrs.target || 'Link');
19
24
  if (node.type.name === 'inkkit_callout')
20
25
  return `${node.attrs.title ||
21
26
  String(node.attrs.kind)
22
27
  .toLowerCase()
23
- .replace(/^./, (letter) => letter.toUpperCase())}\n${clipboardText(node.content, missingImages)}`;
28
+ .replace(/^./, (letter) => letter.toUpperCase())}\n${clipboardText(node.content, missingImages, fileDescriptions)}`;
24
29
  if (node.type.name === 'reference_definition')
25
30
  return '';
26
31
  if (node.type.name === 'footnote_reference')
27
32
  return `[${node.attrs.label}]`;
28
33
  if (node.type.name === 'footnote_definition')
29
- return `[${node.attrs.label}] ${clipboardText(node.content, missingImages)}`;
34
+ return `[${node.attrs.label}] ${clipboardText(node.content, missingImages, fileDescriptions)}`;
30
35
  if (node.isText)
31
36
  return node.text ?? '';
32
37
  if (node.type.name === 'hardbreak' || node.type.name === 'hard_break')
@@ -42,7 +47,7 @@ function nodeText(node, missingImages) {
42
47
  const indent = ' '.repeat(marker.length);
43
48
  const blocks = [];
44
49
  item.forEach((child, _childOffset, childIndex) => {
45
- const text = nodeText(child, missingImages);
50
+ const text = nodeText(child, missingImages, fileDescriptions);
46
51
  const nestedList = child.type.name === 'bullet_list' ||
47
52
  child.type.name === 'ordered_list';
48
53
  const lines = text.split('\n');
@@ -58,7 +63,10 @@ function nodeText(node, missingImages) {
58
63
  return items.join('\n');
59
64
  }
60
65
  if (node.type.name === 'image') {
61
- const alt = String(node.attrs.alt ?? '').replace(/\|\d{1,5}$/, '') || 'Image';
66
+ const description = fileDescriptions.get(node.attrs);
67
+ if (description != null)
68
+ return description;
69
+ const alt = fileReference(node).label || 'Image';
62
70
  return missingImages.has(String(node.attrs.src ?? ''))
63
71
  ? `[${alt}: image unavailable]`
64
72
  : alt;
@@ -67,12 +75,12 @@ function nodeText(node, missingImages) {
67
75
  const rows = [];
68
76
  node.forEach((row) => {
69
77
  const cells = [];
70
- row.forEach((cell) => cells.push(clipboardText(cell.content, missingImages)));
78
+ row.forEach((cell) => cells.push(clipboardText(cell.content, missingImages, fileDescriptions)));
71
79
  rows.push(cells.join('\t'));
72
80
  });
73
81
  return rows.join('\n');
74
82
  }
75
- return clipboardText(node.content, missingImages);
83
+ return clipboardText(node.content, missingImages, fileDescriptions);
76
84
  }
77
85
  export function clipboardContent(content, schema) {
78
86
  content = shareableFragment(content);
@@ -82,6 +90,10 @@ export function clipboardContent(content, schema) {
82
90
  // fetches authored references or temporary presentation URLs.
83
91
  const safe = new DOMSerializer({
84
92
  ...serializer.nodes,
93
+ inkkit_wiki_link: (node) => [
94
+ 'span',
95
+ String(node.attrs.label || node.attrs.target || 'Link'),
96
+ ],
85
97
  image: (node) => [
86
98
  'img',
87
99
  {
@@ -111,31 +123,53 @@ export const visibleClipboard = $prose(() => new Plugin({
111
123
  },
112
124
  }));
113
125
  /** Export a frozen document fragment; presentation URLs never enter the clipboard. */
114
- export async function portableClipboard(content, schema, markdown, adapter, context, completeDiagrams) {
126
+ export async function portableClipboard(content, schema, markdown, adapter, context, completeDiagrams, files, signal = new AbortController().signal) {
127
+ signal.throwIfAborted();
115
128
  content = shareableFragment(content);
116
129
  const base = clipboardContent(content, schema);
117
130
  const container = document.createElement('div');
118
131
  container.innerHTML = base.html;
119
132
  const images = [];
120
133
  const references = [];
134
+ const fileDescriptions = new Map();
121
135
  content.descendants((node) => {
122
136
  if (node.type.name === 'image')
123
- references.push(String(node.attrs.src ?? ''));
137
+ references.push(node);
124
138
  });
125
139
  const captured = [...container.querySelectorAll('img')].map((element, index) => {
126
- const reference = references[index] ?? '';
127
- const alt = element.getAttribute('alt')?.replace(/\|\d{1,5}$/, '') || 'Image';
140
+ const node = references[index];
141
+ const reference = String(node?.attrs.src ?? '');
142
+ const { label: authoredAlt, width } = fileReference(node);
143
+ const alt = authoredAlt || 'Image';
144
+ if (width != null && width > 0)
145
+ element.setAttribute('width', String(Math.min(4096, width)));
128
146
  element.removeAttribute('src');
129
147
  element.removeAttribute('srcset');
130
- return { element, reference, alt };
148
+ return { element, node, reference, alt };
131
149
  });
132
- await Promise.all(captured.map(async ({ element, reference, alt }, index) => {
150
+ await Promise.all(captured.map(async ({ element, node, reference, alt }, index) => {
151
+ let label = alt;
133
152
  element.setAttribute('data-inkkit-image-slot', String(index));
134
153
  element.setAttribute('data-inkkit-authored-image-slot', String(index));
135
154
  try {
136
- if (!adapter)
155
+ const file = files
156
+ ? await portableFile(node, files, context, signal)
157
+ : undefined;
158
+ if (file && file.presentation.kind !== 'image') {
159
+ const fallback = document.createElement('span');
160
+ fallback.textContent = file.description;
161
+ element.replaceWith(fallback);
162
+ fileDescriptions.set(node.attrs, file.description);
163
+ return;
164
+ }
165
+ label = file?.label || alt;
166
+ if (!files && !adapter)
137
167
  throw new Error('The image is unavailable.');
138
- const image = await adapter.exportImage(reference, { ...context });
168
+ const input = file
169
+ ? await portableFileImage(file, files, adapter, context, signal)
170
+ : await withFileSignal(adapter.exportImage(reference, { ...context }), signal);
171
+ signal.throwIfAborted();
172
+ const image = { ...input, bytes: new Uint8Array(input.bytes) };
139
173
  if (!/^image\/(png|jpeg|gif|webp|avif|bmp|tiff)$/i.test(image.mimeType))
140
174
  throw new Error('The image format cannot be exported.');
141
175
  let binary = '';
@@ -143,15 +177,18 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
143
177
  binary += String.fromCharCode(byte);
144
178
  const portable = `data:${image.mimeType};base64,${btoa(binary)}`;
145
179
  element.setAttribute('src', portable);
146
- element.setAttribute('alt', alt);
147
- images[index] = { reference, alt, image };
180
+ element.setAttribute('alt', label);
181
+ fileDescriptions.set(node.attrs, label);
182
+ images[index] = { reference, alt: label, image };
148
183
  }
149
184
  catch (error) {
185
+ signal.throwIfAborted();
150
186
  const message = error instanceof Error ? error.message : 'The image is unavailable.';
151
187
  const warning = document.createElement('span');
152
- warning.textContent = `[${alt}: image unavailable]`;
188
+ warning.textContent = `[${label}: image unavailable]`;
153
189
  element.replaceWith(warning);
154
- images[index] = { reference, alt, error: message };
190
+ fileDescriptions.set(node.attrs, warning.textContent);
191
+ images[index] = { reference, alt: label, error: message };
155
192
  }
156
193
  }));
157
194
  const diagrams = [];
@@ -162,7 +199,7 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
162
199
  const result = { source };
163
200
  diagrams.push(result);
164
201
  try {
165
- const image = await diagramImage(source);
202
+ const image = await withFileSignal(diagramImage(source), signal);
166
203
  let binary = '';
167
204
  for (const byte of image.bytes)
168
205
  binary += String.fromCharCode(byte);
@@ -179,6 +216,7 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
179
216
  result.image = image;
180
217
  }
181
218
  catch (error) {
219
+ signal.throwIfAborted();
182
220
  result.error =
183
221
  error instanceof Error
184
222
  ? error.message
@@ -211,12 +249,13 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
211
249
  /^javascript:/i.test(attribute.value)))
212
250
  element.removeAttribute(attribute.name);
213
251
  }
214
- const missing = new Set(images.filter((image) => image.error).map((image) => image.reference));
252
+ const missing = new Set(images.filter((image) => image?.error).map((image) => image.reference));
253
+ signal.throwIfAborted();
215
254
  return {
216
- text: clipboardText(content, missing),
255
+ text: clipboardText(content, missing, fileDescriptions),
217
256
  html: container.innerHTML,
218
257
  markdown,
219
- images: [...orderedImages, ...images.filter((image) => image.error)],
258
+ images: [...orderedImages, ...images.filter((image) => image?.error)],
220
259
  diagrams,
221
260
  };
222
261
  }
package/dist/dialect.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { Node as ProseNode } from '@milkdown/kit/prose/model';
3
3
  import { type Options as StringifyOptions } from 'mdast-util-to-markdown';
4
4
  export declare const remarkDialectPlugin: import("@milkdown/kit/utils").$Remark<"remarkDialect", unknown>;
5
5
  export declare const tables: MilkdownPlugin[];
6
- export declare function createDialect(images: boolean): MilkdownPlugin[];
6
+ export declare function createDialect(images: boolean, wikiLinks?: boolean, files?: boolean): MilkdownPlugin[];
7
7
  export declare const dialect: MilkdownPlugin[];
8
8
  export declare const stringifyOptions: StringifyOptions;
9
9
  export declare function serialize(ctx: Ctx, doc?: ProseNode): string;
package/dist/dialect.js CHANGED
@@ -4,6 +4,7 @@ import { extendListItemSchemaForTask, strikethroughAttr, strikethroughInputRule,
4
4
  import { $node, $remark } from '@milkdown/kit/utils';
5
5
  import { autolinkInputRule } from './autolink.js';
6
6
  import { sourceAttribute } from './source.js';
7
+ import { linkedSyntax, wikiLinkSchema, namedFileMarkdown, fileReference, } from './linked-syntax.js';
7
8
  import { normalizeTableAlignment, normalizeTableColumnAlignment, } from './table-alignment.js';
8
9
  import { preservingCodeBlocks, remarkCodeFences, fencedCodeHandler, } from './code-fences.js';
9
10
  import { literalBlock, createLiteralPreservation } from './literals.js';
@@ -178,6 +179,14 @@ const normalizedImages = imageSchema.extendSchema((base) => (ctx) => {
178
179
  const schema = base(ctx);
179
180
  return {
180
181
  ...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
+ },
181
190
  parseDOM: [
182
191
  {
183
192
  tag: 'img[src]',
@@ -197,6 +206,29 @@ const normalizedImages = imageSchema.extendSchema((base) => (ctx) => {
197
206
  src: String(node.url ?? ''),
198
207
  alt: String(node.alt ?? ''),
199
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
+ });
215
+ },
216
+ },
217
+ toMarkdown: {
218
+ ...schema.toMarkdown,
219
+ runner(state, node) {
220
+ const raw = namedFileMarkdown(node, Boolean(node.attrs.inkkitFileInTable));
221
+ if (raw == null)
222
+ return schema.toMarkdown.runner(state, node);
223
+ state.addNode('image', undefined, undefined, {
224
+ url: node.attrs.src,
225
+ alt: node.attrs.alt,
226
+ title: node.attrs.title || null,
227
+ inkkitFileRaw: raw,
228
+ inkkitFileTarget: node.attrs.inkkitFileTarget,
229
+ inkkitFileFragment: node.attrs.inkkitFileFragment,
230
+ inkkitFileWidth: fileReference(node).width ?? null,
231
+ inkkitFileInTable: node.attrs.inkkitFileInTable,
200
232
  });
201
233
  },
202
234
  },
@@ -266,14 +298,15 @@ export const tables = [
266
298
  setAlignCommand,
267
299
  exitTable,
268
300
  ].flat();
269
- export function createDialect(images) {
301
+ export function createDialect(images, wikiLinks = false, files = false) {
270
302
  return [
271
303
  remarkCodeFences,
272
304
  remarkReferencesPlugin,
273
305
  remarkCalloutsPlugin,
274
306
  remarkCommentsPlugin,
307
+ wikiLinks || files ? linkedSyntax(wikiLinks, files) : [],
275
308
  // Protect unsupported inline HTML before the empty-line plugin consumes break nodes.
276
- createLiteralPreservation(images),
309
+ createLiteralPreservation(images || files),
277
310
  commonmarkWithLiteralReferences,
278
311
  $node('doc', () => ({
279
312
  ...docSchema.schema,
@@ -283,6 +316,7 @@ export function createDialect(images) {
283
316
  },
284
317
  })),
285
318
  references,
319
+ wikiLinks ? wikiLinkSchema : [],
286
320
  callouts,
287
321
  comments,
288
322
  inlineHighlight,
package/dist/editor.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type EditorLabels } from './labels.js';
2
2
  import { type TableCommand, type TableOptions } from './tables.js';
3
- import { type DocumentInput, type DocumentSnapshot, type ImageAdapter, type ClipboardInput, type ClipboardOutput, type PrintableDocument, type TextRange, type ReadableTextSnapshot, type TextRect, type ViewportOptions, type ViewportSnapshot } from './types.js';
3
+ import { type DocumentInput, type DocumentSnapshot, type ImageAdapter, type FileAdapter, type WikiLinkAdapter, type ClipboardInput, type ClipboardOutput, type PrintableDocument, type TextRange, type ReadableTextSnapshot, type TextRect, type ViewportOptions, type ViewportSnapshot } from './types.js';
4
4
  import { type Heading } from './outline.js';
5
5
  export type Mark = 'bold' | 'italic' | 'strikethrough' | 'highlight' | 'code' | 'link';
6
6
  export type Block = {
@@ -34,6 +34,8 @@ export interface TextInputPreferences {
34
34
  }
35
35
  export interface EditorOptions {
36
36
  images?: ImageAdapter;
37
+ files?: FileAdapter;
38
+ wikiLinks?: WikiLinkAdapter;
37
39
  editable?: boolean;
38
40
  textInput?: TextInputPreferences;
39
41
  labels?: Partial<EditorLabels>;
@@ -89,6 +91,7 @@ export declare class InkKitEditor {
89
91
  private revision;
90
92
  private documentEpoch;
91
93
  private operationSequence;
94
+ private exportControllers;
92
95
  private plainComposing;
93
96
  private plainSearch;
94
97
  private literalMatch?;
@@ -124,6 +127,9 @@ export declare class InkKitEditor {
124
127
  private keys;
125
128
  private constructor();
126
129
  private context;
130
+ private cancelExports;
131
+ private exportContent;
132
+ private mediaPolicy;
127
133
  private assertAlive;
128
134
  private assertCurrent;
129
135
  private assertMutation;
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 { wikiLinkView } from './wiki-view.js';
25
+ import { withFileSignal } from './files.js';
24
26
  import { tablePlugins, tableCommand, tableCommands, tableAvailability, tableContext, configureTableMovement, } from './tables.js';
25
27
  import { InkKitError, } from './types.js';
26
28
  import { readableProjection, projectionPosition, projectionOffset, validateOffsets, formattedRects, literalRects, literalVisibleRanges, intersectsViewport, textRect, } from './text-ranges.js';
@@ -251,6 +253,7 @@ export class InkKitEditor {
251
253
  revision = 0;
252
254
  documentEpoch = 0;
253
255
  operationSequence = 0;
256
+ exportControllers = new Set();
254
257
  plainComposing = false;
255
258
  plainSearch = '';
256
259
  literalMatch;
@@ -294,6 +297,8 @@ export class InkKitEditor {
294
297
  if (this.loading || view.state.doc.eq(previous.doc))
295
298
  return;
296
299
  this.revision += 1;
300
+ this.cancelExports();
301
+ this.mediaPolicy();
297
302
  this.literalMatch = undefined;
298
303
  let markdown;
299
304
  try {
@@ -335,6 +340,36 @@ export class InkKitEditor {
335
340
  operationId: `${this.documentEpoch}:${this.generation}:${++this.operationSequence}`,
336
341
  };
337
342
  }
343
+ cancelExports() {
344
+ for (const controller of this.exportControllers)
345
+ controller.abort();
346
+ this.exportControllers.clear();
347
+ }
348
+ async exportContent(snapshot, operation) {
349
+ const epoch = this.documentEpoch;
350
+ const mode = this.editingMode;
351
+ const controller = new AbortController();
352
+ this.exportControllers.add(controller);
353
+ try {
354
+ return await withFileSignal(operation(controller.signal), controller.signal);
355
+ }
356
+ finally {
357
+ this.exportControllers.delete(controller);
358
+ this.assertCurrent(snapshot.generation);
359
+ if (controller.signal.aborted ||
360
+ epoch !== this.documentEpoch ||
361
+ snapshot.documentId !== this.documentId ||
362
+ snapshot.revision !== this.revision ||
363
+ mode !== this.editingMode)
364
+ throw new InkKitError('stale-document', 'Document changed while preparing output');
365
+ }
366
+ }
367
+ mediaPolicy() {
368
+ if (this.editor && this.root)
369
+ this.editor.ctx
370
+ .get(editorViewCtx)
371
+ .dom.dispatchEvent(new Event('inkkit-media-policy'));
372
+ }
338
373
  assertAlive() {
339
374
  if (this.destroyed)
340
375
  throw new InkKitError('destroyed', 'The editor has been destroyed');
@@ -383,6 +418,7 @@ export class InkKitEditor {
383
418
  .get(editorViewCtx)
384
419
  .dom.dispatchEvent(new Event('inkkit-cancel-resize'));
385
420
  this.applyInputPolicy();
421
+ this.mediaPolicy();
386
422
  }
387
423
  finally {
388
424
  this.reportingSuppressed = suppressed;
@@ -593,6 +629,7 @@ export class InkKitEditor {
593
629
  const top = container.scrollTop, left = container.scrollLeft;
594
630
  const text = this.snapshot().text;
595
631
  this.mode = mode;
632
+ this.cancelExports();
596
633
  this.outlineEpoch += 1;
597
634
  this.plainSearch = '';
598
635
  this.literalMatch = undefined;
@@ -631,6 +668,7 @@ export class InkKitEditor {
631
668
  : (this.options.labels?.sourceEditor ?? defaultLabels.sourceEditor));
632
669
  this.editor.ctx.get(editorViewCtx).dom.parentElement.hidden =
633
670
  this.literalSurface;
671
+ this.mediaPolicy();
634
672
  }
635
673
  replaceSource(text, expectedGeneration) {
636
674
  this.assertMutation(expectedGeneration);
@@ -735,26 +773,17 @@ export class InkKitEditor {
735
773
  }
736
774
  async printableSnapshot(expectedGeneration) {
737
775
  const snapshot = this.snapshot(expectedGeneration);
738
- const epoch = this.documentEpoch;
739
776
  const state = this.editor.ctx.get(editorViewCtx).state;
740
- let output;
741
- try {
777
+ const output = await this.exportContent(snapshot, async (signal) => {
742
778
  if (snapshot.format === 'md' &&
743
779
  !this.options.images &&
780
+ !this.options.files &&
744
781
  hasAuthoredImagesInLiterals(state.doc.content, (source) => this.editor.ctx.get(remarkCtx).parse(source)))
745
782
  throw new InkKitError('image-unavailable', 'No image adapter is configured for the authored images.');
746
- output =
747
- snapshot.format === 'txt'
748
- ? printableText(snapshot.text)
749
- : await printableMarkdown(state.doc.content, state.schema, this.options.images, this.context());
750
- }
751
- finally {
752
- this.assertCurrent(snapshot.generation);
753
- if (snapshot.documentId !== this.documentId ||
754
- epoch !== this.documentEpoch ||
755
- snapshot.revision !== this.revision)
756
- throw new InkKitError('stale-document', 'Document changed while preparing printable content');
757
- }
783
+ return snapshot.format === 'txt'
784
+ ? printableText(snapshot.text)
785
+ : printableMarkdown(state.doc.content, state.schema, this.options.images, this.context(), this.options.files, signal);
786
+ });
758
787
  return Object.freeze({
759
788
  documentId: snapshot.documentId,
760
789
  generation: snapshot.generation,
@@ -827,7 +856,7 @@ export class InkKitEditor {
827
856
  ? error.message
828
857
  : 'Cannot copy the selected Markdown');
829
858
  }
830
- const result = await portableClipboard(all || selectedDoc ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context(), completeDiagrams);
859
+ const result = await this.exportContent(snapshot, (signal) => portableClipboard(all || selectedDoc ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context(), completeDiagrams, this.options.files, signal));
831
860
  const metadata = referenceMetadata(this.editor.ctx, selectedDoc ?? doc, content);
832
861
  if (metadata != null)
833
862
  result.html = withReferenceMetadata(result.html, metadata);
@@ -906,6 +935,8 @@ export class InkKitEditor {
906
935
  return;
907
936
  this.destroyed = true;
908
937
  this.ready = false;
938
+ this.cancelExports();
939
+ this.mediaPolicy();
909
940
  this.pasteController.destroy();
910
941
  if (this.clickHandler)
911
942
  this.root.removeEventListener('click', this.clickHandler);
@@ -1052,7 +1083,7 @@ export class InkKitEditor {
1052
1083
  .use(instance.pasteController.plugin)
1053
1084
  .use(visibleClipboard)
1054
1085
  .use(instance.keymapPlugin)
1055
- .use(createDialect(Boolean(options.images)))
1086
+ .use(createDialect(Boolean(options.images || options.files), Boolean(options.wikiLinks), Boolean(options.files)))
1056
1087
  .use(tablePlugins)
1057
1088
  .use(instance.changePlugin)
1058
1089
  .use(history)
@@ -1070,7 +1101,57 @@ export class InkKitEditor {
1070
1101
  .use(selectionPlugin)
1071
1102
  .use($prose(() => search()))
1072
1103
  .use(instance.policyPlugin)
1073
- .use(options.images ? imageView(options.images) : [])
1104
+ .use(options.wikiLinks
1105
+ ? wikiLinkView(options.wikiLinks, {
1106
+ capture: () => {
1107
+ const epoch = instance.documentEpoch;
1108
+ const context = instance.context();
1109
+ return {
1110
+ context,
1111
+ isCurrent: () => !instance.destroyed &&
1112
+ epoch === instance.documentEpoch &&
1113
+ context.documentId === instance.documentId &&
1114
+ context.generation === instance.generation,
1115
+ };
1116
+ },
1117
+ active: () => instance.ready &&
1118
+ !instance.destroyed &&
1119
+ !instance.literalSurface,
1120
+ labels: { ...defaultLabels, ...options.labels },
1121
+ error: (error) => events.error?.(error),
1122
+ })
1123
+ : [])
1124
+ .use(options.images || options.files
1125
+ ? imageView(options.images, options.files
1126
+ ? {
1127
+ adapter: options.files,
1128
+ capture: () => {
1129
+ const epoch = instance.documentEpoch;
1130
+ const context = instance.context();
1131
+ return {
1132
+ context,
1133
+ isCurrent: () => !instance.destroyed &&
1134
+ epoch === instance.documentEpoch &&
1135
+ context.documentId === instance.documentId &&
1136
+ context.generation === instance.generation,
1137
+ };
1138
+ },
1139
+ active: () => instance.ready &&
1140
+ !instance.destroyed &&
1141
+ !instance.literalSurface,
1142
+ labels: { ...defaultLabels, ...options.labels },
1143
+ error: (error) => events.error?.(error),
1144
+ }
1145
+ : undefined, () => {
1146
+ try {
1147
+ instance.assertMutation();
1148
+ return true;
1149
+ }
1150
+ catch {
1151
+ return false;
1152
+ }
1153
+ })
1154
+ : [])
1074
1155
  .create();
1075
1156
  configureTableMovement(instance.editor.ctx, () => {
1076
1157
  try {
@@ -1275,6 +1356,8 @@ export class InkKitEditor {
1275
1356
  ? this.plain.ownerDocument.activeElement === this.plain
1276
1357
  : this.editor.ctx.get(editorViewCtx).hasFocus();
1277
1358
  this.ready = false;
1359
+ this.cancelExports();
1360
+ this.mediaPolicy();
1278
1361
  this.pasteController.cancelPending();
1279
1362
  this.documentEpoch += 1;
1280
1363
  this.outlineEpoch += 1;
@@ -1294,6 +1377,7 @@ export class InkKitEditor {
1294
1377
  this.lastMarkdown = input.text;
1295
1378
  this.updateSurface();
1296
1379
  this.ready = true;
1380
+ this.mediaPolicy();
1297
1381
  if (focused) {
1298
1382
  if (this.literalSurface)
1299
1383
  this.plain.focus({ preventScroll: true });
@@ -0,0 +1,40 @@
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
+ }