@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.
@@ -0,0 +1,308 @@
1
+ import { $node, $remark } from '@milkdown/kit/utils';
2
+ import { defaultHandlers } from 'mdast-util-to-markdown';
3
+ const fromTable = (raw) => raw.replace(/(\\*)\|/g, (_match, escapes) => `${'\\'.repeat(Math.floor(escapes.length / 2))}|`);
4
+ const toTable = (raw) => raw.replace(/(\\*)\|/g, (_match, escapes) => `${'\\'.repeat(escapes.length * 2 + 1)}|`);
5
+ export function parseLinkedSyntax(raw, inTable = false) {
6
+ const source = inTable ? fromTable(raw) : raw;
7
+ const embed = source.startsWith('!');
8
+ const prefix = embed ? '![[' : '[[';
9
+ if (!source.startsWith(prefix) || !source.endsWith(']]'))
10
+ return undefined;
11
+ const content = source.slice(prefix.length, -2);
12
+ const parts = { target: '', fragment: '', alias: '' };
13
+ let section = 'target', hasFragment = false, hasAlias = false;
14
+ for (let index = 0; index < content.length; index++) {
15
+ const character = content[index];
16
+ if (character === '\\') {
17
+ const escaped = content[++index];
18
+ if (!escaped || !'[]|#\\'.includes(escaped))
19
+ return undefined;
20
+ parts[section] += escaped;
21
+ }
22
+ else if (character === '|') {
23
+ if (hasAlias)
24
+ return undefined;
25
+ hasAlias = true;
26
+ section = 'alias';
27
+ }
28
+ else if (character === '#' && section !== 'alias') {
29
+ if (hasFragment)
30
+ return undefined;
31
+ hasFragment = true;
32
+ section = 'fragment';
33
+ }
34
+ else {
35
+ if ('[]'.includes(character) ||
36
+ character.charCodeAt(0) < 32 ||
37
+ character === '\u007f')
38
+ return undefined;
39
+ parts[section] += character;
40
+ }
41
+ }
42
+ if ((!parts.target.trim() && !(hasFragment && parts.fragment.trim())) ||
43
+ (hasFragment && !parts.fragment.trim()) ||
44
+ (hasAlias && !parts.alias.trim()) ||
45
+ parts.target.trimStart().startsWith('^') ||
46
+ parts.fragment.trimStart().startsWith('^'))
47
+ return undefined;
48
+ if (embed &&
49
+ (!parts.target.trim() ||
50
+ (hasAlias && (!/^\d{1,5}$/.test(parts.alias) || Number(parts.alias) < 1))))
51
+ return undefined;
52
+ return {
53
+ raw,
54
+ target: parts.target,
55
+ fragment: hasFragment ? parts.fragment : null,
56
+ label: !embed && hasAlias
57
+ ? parts.alias
58
+ : parts.target + (hasFragment ? `#${parts.fragment}` : ''),
59
+ width: embed && hasAlias ? Number(parts.alias) : null,
60
+ embed,
61
+ inTable,
62
+ };
63
+ }
64
+ function linkedTokenizer(embed) {
65
+ return function (effects, ok, nok) {
66
+ const prefix = embed ? '![[' : '[[';
67
+ let index = 0, raw = '';
68
+ return opening;
69
+ function consume(code) {
70
+ raw += String.fromCodePoint(code);
71
+ effects.consume(code);
72
+ }
73
+ function opening(code) {
74
+ if (code !== prefix.charCodeAt(index))
75
+ return nok(code);
76
+ if (!index)
77
+ effects.enter('inkkitLinkedSyntax');
78
+ consume(code);
79
+ index++;
80
+ return index === prefix.length ? body : opening;
81
+ }
82
+ function body(code) {
83
+ if (code === null || code < 32 || code === 91)
84
+ return nok(code);
85
+ consume(code);
86
+ if (code === 92)
87
+ return escaped;
88
+ return code === 93 ? closing : body;
89
+ }
90
+ function escaped(code) {
91
+ if (code === null || code < 32)
92
+ return nok(code);
93
+ consume(code);
94
+ return body;
95
+ }
96
+ function closing(code) {
97
+ if (code !== 93)
98
+ return nok(code);
99
+ consume(code);
100
+ if (!parseLinkedSyntax(raw) && !parseLinkedSyntax(raw, true))
101
+ return nok(code);
102
+ effects.exit('inkkitLinkedSyntax');
103
+ return ok;
104
+ }
105
+ };
106
+ }
107
+ function tableForm(raw, authoredInTable, outputInTable) {
108
+ if (authoredInTable === outputInTable)
109
+ return raw;
110
+ return outputInTable ? toTable(raw) : fromTable(raw);
111
+ }
112
+ export function wikiMarkdown(node, inTable = false) {
113
+ return tableForm(String(node.attrs.raw), Boolean(node.attrs.inTable), inTable);
114
+ }
115
+ export function wikiReference(node) {
116
+ return {
117
+ target: String(node.attrs.target),
118
+ ...(node.attrs.fragment != null
119
+ ? { fragment: String(node.attrs.fragment) }
120
+ : {}),
121
+ label: String(node.attrs.label),
122
+ };
123
+ }
124
+ export function fileReference(node) {
125
+ const reference = String(node.attrs.src ?? '');
126
+ const named = node.attrs.inkkitFileRaw != null;
127
+ const alt = String(node.attrs.alt ?? '');
128
+ const match = /\|(\d{1,5})$/.exec(alt);
129
+ const fragment = named
130
+ ? node.attrs.inkkitFileFragment
131
+ : reference.includes('#')
132
+ ? reference.slice(reference.indexOf('#') + 1)
133
+ : null;
134
+ const label = named
135
+ ? String(node.attrs.inkkitFileTarget ?? '') +
136
+ (fragment != null ? `#${fragment}` : '')
137
+ : match
138
+ ? alt.slice(0, match.index)
139
+ : alt;
140
+ const width = named && alt === label && node.attrs.inkkitFileWidth == null
141
+ ? null
142
+ : match
143
+ ? Number(match[1])
144
+ : null;
145
+ return {
146
+ reference,
147
+ kind: named ? 'wiki' : 'path',
148
+ ...(fragment != null ? { fragment: String(fragment) } : {}),
149
+ ...(label ? { label } : {}),
150
+ ...(width != null ? { width } : {}),
151
+ };
152
+ }
153
+ export function namedFileMarkdown(node, inTable = false) {
154
+ if (node.attrs.inkkitFileRaw == null)
155
+ return undefined;
156
+ let raw = String(node.attrs.inkkitFileRaw);
157
+ const authoredInTable = Boolean(node.attrs.inkkitFileInTable);
158
+ const width = fileReference(node).width ?? null;
159
+ const original = node.attrs.inkkitFileWidth ?? null;
160
+ if (width !== original) {
161
+ if (authoredInTable)
162
+ raw = fromTable(raw);
163
+ if (original != null)
164
+ raw = raw.replace(/\|\d{1,5}\]\]$/, ']]');
165
+ if (width != null)
166
+ raw = `${raw.slice(0, -2)}|${width}]]`;
167
+ return inTable ? toTable(raw) : raw;
168
+ }
169
+ return tableForm(raw, authoredInTable, inTable);
170
+ }
171
+ export const wikiLinkSchema = $node('inkkit_wiki_link', () => ({
172
+ inline: true,
173
+ group: 'inline',
174
+ atom: true,
175
+ marks: '',
176
+ attrs: {
177
+ raw: { default: '' },
178
+ target: { default: '' },
179
+ fragment: { default: null },
180
+ label: { default: '' },
181
+ inTable: { default: false },
182
+ },
183
+ parseDOM: [
184
+ {
185
+ tag: 'span[data-inkkit-wiki]',
186
+ getAttrs(dom) {
187
+ const parsed = parseLinkedSyntax(dom.getAttribute('data-inkkit-wiki-raw') ?? '', dom.getAttribute('data-inkkit-wiki-table') === 'true');
188
+ return parsed && !parsed.embed
189
+ ? {
190
+ raw: parsed.raw,
191
+ target: parsed.target,
192
+ fragment: parsed.fragment,
193
+ label: parsed.label,
194
+ inTable: parsed.inTable,
195
+ }
196
+ : false;
197
+ },
198
+ },
199
+ ],
200
+ toDOM: (node) => [
201
+ 'span',
202
+ {
203
+ 'data-inkkit-wiki': node.attrs.target,
204
+ 'data-inkkit-fragment': node.attrs.fragment ?? '',
205
+ 'data-inkkit-wiki-raw': node.attrs.raw,
206
+ 'data-inkkit-wiki-table': String(node.attrs.inTable),
207
+ role: 'link',
208
+ tabindex: '0',
209
+ },
210
+ node.attrs.label,
211
+ ],
212
+ parseMarkdown: {
213
+ match: (node) => node.type === 'inkkitWikiLink',
214
+ runner(state, node, type) {
215
+ state.addNode(type, {
216
+ raw: node.raw,
217
+ target: node.target,
218
+ fragment: node.fragment,
219
+ label: node.label,
220
+ inTable: node.inTable,
221
+ });
222
+ },
223
+ },
224
+ toMarkdown: {
225
+ match: (node) => node.type.name === 'inkkit_wiki_link',
226
+ runner(state, node) {
227
+ state.addNode('inkkitWikiLink', undefined, node.attrs.label, {
228
+ raw: node.attrs.raw,
229
+ target: node.attrs.target,
230
+ fragment: node.attrs.fragment,
231
+ label: node.attrs.label,
232
+ inTable: node.attrs.inTable,
233
+ });
234
+ },
235
+ },
236
+ }));
237
+ export function linkedSyntax(wikiLinks, files) {
238
+ const syntax = { text: {} };
239
+ if (wikiLinks)
240
+ syntax.text[91] = {
241
+ name: 'inkkitWikiLink',
242
+ tokenize: linkedTokenizer(false),
243
+ previous: (code) => code !== 33 && code !== 91,
244
+ };
245
+ if (files)
246
+ syntax.text[33] = {
247
+ name: 'inkkitNamedFile',
248
+ tokenize: linkedTokenizer(true),
249
+ };
250
+ const from = {
251
+ enter: {
252
+ inkkitLinkedSyntax(token) {
253
+ const inTable = this.stack.some((node) => node.type === 'tableCell');
254
+ const parsed = parseLinkedSyntax(this.sliceSerialize(token), inTable);
255
+ if (!parsed) {
256
+ this.enter({ type: 'text', value: this.sliceSerialize(token) }, token);
257
+ return;
258
+ }
259
+ if (parsed.embed)
260
+ this.enter({
261
+ type: 'image',
262
+ url: parsed.target +
263
+ (parsed.fragment != null ? `#${parsed.fragment}` : ''),
264
+ alt: parsed.label + (parsed.width != null ? `|${parsed.width}` : ''),
265
+ title: null,
266
+ inkkitFileRaw: parsed.raw,
267
+ inkkitFileTarget: parsed.target,
268
+ inkkitFileFragment: parsed.fragment,
269
+ inkkitFileWidth: parsed.width,
270
+ inkkitFileInTable: inTable,
271
+ }, token);
272
+ else
273
+ this.enter({
274
+ type: 'inkkitWikiLink',
275
+ value: parsed.label,
276
+ raw: parsed.raw,
277
+ target: parsed.target,
278
+ fragment: parsed.fragment,
279
+ label: parsed.label,
280
+ inTable,
281
+ }, token);
282
+ },
283
+ },
284
+ exit: {
285
+ inkkitLinkedSyntax(token) {
286
+ this.exit(token);
287
+ },
288
+ },
289
+ };
290
+ const wiki = (node, _parent, state) => tableForm(String(node.raw), Boolean(node.inTable), state.stack.includes('tableCell'));
291
+ const named = (node, parent, state, info) => {
292
+ if (node.inkkitFileRaw != null)
293
+ return tableForm(String(node.inkkitFileRaw), Boolean(node.inkkitFileInTable), state.stack.includes('tableCell'));
294
+ return defaultHandlers.image(node, parent, state, info);
295
+ };
296
+ // Standard image handling is installed by CommonMark; wrap it only for authored named tokens.
297
+ return $remark('inkkitLinkedSyntax', () => function () {
298
+ const data = this.data();
299
+ (data.micromarkExtensions ??= []).push(syntax);
300
+ (data.fromMarkdownExtensions ??= []).push(from);
301
+ (data.toMarkdownExtensions ??= []).push({
302
+ handlers: {
303
+ inkkitWikiLink: wiki,
304
+ ...(files ? { image: named } : {}),
305
+ },
306
+ });
307
+ });
308
+ }
package/dist/literals.js CHANGED
@@ -47,6 +47,44 @@ function containsUnresolved(node, source) {
47
47
  return ('children' in node &&
48
48
  node.children.some((child) => containsUnresolved(child, source)));
49
49
  }
50
+ function containsUnsupportedLinked(node, source) {
51
+ const start = node.position?.start.offset, end = node.position?.end.offset;
52
+ if (start == null || end == null || node.type === 'definition')
53
+ return false;
54
+ const supported = [];
55
+ const visit = (child) => {
56
+ if ([
57
+ 'inkkitWikiLink',
58
+ 'image',
59
+ 'code',
60
+ 'inlineCode',
61
+ 'link',
62
+ 'inkkitCommentInline',
63
+ 'inkkitCommentBlock',
64
+ ].includes(child.type)) {
65
+ const from = child.position?.start.offset, to = child.position?.end.offset;
66
+ if (from != null && to != null)
67
+ supported.push({ from, to });
68
+ return;
69
+ }
70
+ if ('children' in child)
71
+ child.children.forEach(visit);
72
+ };
73
+ visit(node);
74
+ for (const match of source
75
+ .slice(start, end)
76
+ .matchAll(/!?\[\[[^\r\n]*?\]\]/g)) {
77
+ const from = start + match.index, to = from + match[0].length;
78
+ let escapes = 0;
79
+ while (from - escapes > 0 && source[from - escapes - 1] === '\\')
80
+ escapes++;
81
+ if (escapes % 2)
82
+ continue;
83
+ if (!supported.some((span) => span.from <= from && span.to >= to))
84
+ return true;
85
+ }
86
+ return false;
87
+ }
50
88
  // Keep syntax we cannot faithfully edit as literal text, without making the rest
51
89
  // of the document a source editor or allowing HTML and image network requests.
52
90
  export function createLiteralPreservation(images) {
@@ -66,7 +104,8 @@ export function createLiteralPreservation(images) {
66
104
  if (start == null ||
67
105
  end == null ||
68
106
  (!containsUnsupported(node, images) &&
69
- !containsUnresolved(node, source)))
107
+ !containsUnresolved(node, source) &&
108
+ !containsUnsupportedLinked(node, source)))
70
109
  return node;
71
110
  let raw = source.slice(start, end);
72
111
  // Definition indentation is restored by the footnote serializer.
package/dist/mermaid.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { editorLabels } from './labels.js';
2
- import mermaid from 'mermaid';
3
2
  import DOMPurify from 'dompurify';
4
3
  import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
5
4
  import { Decoration, DecorationSet } from '@milkdown/kit/prose/view';
@@ -56,6 +55,7 @@ export function renderDiagram(source) {
56
55
  if (previous)
57
56
  return previous;
58
57
  const rendered = queue.then(async () => {
58
+ const { default: mermaid } = await import('mermaid');
59
59
  mermaid.initialize({
60
60
  startOnLoad: false,
61
61
  securityLevel: 'strict',
@@ -0,0 +1,10 @@
1
+ import type { Node } from '@milkdown/kit/prose/model';
2
+ import type { DocumentContext, FileAdapter, FilePresentation, FileReference, ImageAdapter, PortableImage } from './types.js';
3
+ export interface PortableFile {
4
+ reference: FileReference;
5
+ presentation: FilePresentation;
6
+ label: string;
7
+ description: string;
8
+ }
9
+ export declare function portableFile(node: Node, adapter: FileAdapter | undefined, context: DocumentContext, signal: AbortSignal): Promise<PortableFile | undefined>;
10
+ export declare function portableFileImage(file: PortableFile, files: FileAdapter, legacy: ImageAdapter | undefined, context: DocumentContext, signal: AbortSignal): Promise<PortableImage>;
@@ -0,0 +1,36 @@
1
+ import { resolveFile, withFileSignal } from './files.js';
2
+ import { fileReference } from './linked-syntax.js';
3
+ export async function portableFile(node, adapter, context, signal) {
4
+ if (!adapter)
5
+ return undefined;
6
+ const reference = fileReference(node);
7
+ let presentation;
8
+ try {
9
+ presentation = await resolveFile(adapter, reference, { ...context, signal });
10
+ }
11
+ catch (error) {
12
+ signal.throwIfAborted();
13
+ presentation = {
14
+ kind: 'error',
15
+ message: error instanceof Error ? error.message : 'The file is unavailable.',
16
+ };
17
+ }
18
+ const label = presentation.label || reference.label || reference.reference || 'File';
19
+ const kind = presentation.kind;
20
+ const description = kind === 'missing' || kind === 'error'
21
+ ? `[File: ${label} (${kind === 'missing' ? 'unavailable' : 'error'})]`
22
+ : `[${kind === 'pdf' ? 'PDF' : kind[0].toUpperCase() + kind.slice(1)}: ${label}]`;
23
+ return { reference, presentation, label, description };
24
+ }
25
+ export async function portableFileImage(file, files, legacy, context, signal) {
26
+ signal.throwIfAborted();
27
+ const image = files.exportImage
28
+ ? await withFileSignal(files.exportImage(file.reference, { ...context, signal }), signal)
29
+ : file.reference.kind === 'path' && legacy
30
+ ? await withFileSignal(legacy.exportImage(file.reference.reference, { ...context }), signal)
31
+ : undefined;
32
+ signal.throwIfAborted();
33
+ if (!image)
34
+ throw new Error('The image has no portable export adapter.');
35
+ return image;
36
+ }
package/dist/preserve.js CHANGED
@@ -266,6 +266,10 @@ function blockForm(ctx, node) {
266
266
  }
267
267
  function semanticSignature(doc) {
268
268
  const clean = (value) => {
269
+ if (value.type === 'inkkit_wiki_link' && value.attrs) {
270
+ const { raw: _raw, inTable: _inTable, ...attrs } = value.attrs;
271
+ value = { ...value, attrs };
272
+ }
269
273
  const content = value.content;
270
274
  if (!content)
271
275
  return value;
@@ -305,6 +309,9 @@ function semanticSignature(doc) {
305
309
  'label',
306
310
  'spread',
307
311
  'authoredFence',
312
+ 'inkkitFileRaw',
313
+ 'inkkitFileWidth',
314
+ 'inkkitFileInTable',
308
315
  ].includes(key)
309
316
  ? undefined
310
317
  : value));
package/dist/print.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Fragment, Schema } from '@milkdown/kit/prose/model';
2
- import { type DocumentContext, type ImageAdapter, type PrintableDocument } from './types.js';
2
+ import { type DocumentContext, type ImageAdapter, type FileAdapter, type PrintableDocument } from './types.js';
3
3
  type PrintableContent = Pick<PrintableDocument, 'html' | 'styles' | 'assets' | 'warnings'>;
4
4
  export declare function printableText(text: string): PrintableContent;
5
5
  export declare function hasAuthoredImagesInLiterals(content: Fragment, parse: (source: string) => unknown): boolean;
6
- export declare function printableMarkdown(content: Fragment, schema: Schema, adapter: ImageAdapter | undefined, context: DocumentContext): Promise<PrintableContent>;
6
+ export declare function printableMarkdown(content: Fragment, schema: Schema, adapter: ImageAdapter | undefined, context: DocumentContext, files?: FileAdapter, signal?: AbortSignal): Promise<PrintableContent>;
7
7
  export {};
package/dist/print.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { clipboardContent } from './clipboard.js';
2
2
  import { shareableFragment } from './comments.js';
3
- import { splitAlt } from './images.js';
3
+ import { fileReference } from './linked-syntax.js';
4
4
  import { sourceFrontmatter } from './literals.js';
5
5
  import { diagramImage, renderDiagram } from './mermaid.js';
6
+ import { portableFile, portableFileImage } from './portable-files.js';
7
+ import { withFileSignal } from './files.js';
6
8
  import { InkKitError, } from './types.js';
7
9
  const styles = `
8
10
  :root { color-scheme: light; font: 11pt/1.5 Arial, sans-serif; }
@@ -124,7 +126,8 @@ function matchesSignature(bytes, mimeType) {
124
126
  return false;
125
127
  }
126
128
  }
127
- async function validatedImage(input) {
129
+ async function validatedImage(input, signal) {
130
+ signal.throwIfAborted();
128
131
  const image = {
129
132
  bytes: new Uint8Array(input.bytes),
130
133
  mimeType: input.mimeType.toLowerCase(),
@@ -136,18 +139,23 @@ async function validatedImage(input) {
136
139
  await new Promise((resolve, reject) => {
137
140
  const decoder = new Image();
138
141
  const timeout = setTimeout(() => {
139
- decoder.onload = decoder.onerror = null;
140
- decoder.removeAttribute('src');
141
- reject(new Error('The portable image could not be decoded.'));
142
+ decoder.src = '';
143
+ finish(new Error('The portable image could not be decoded.'));
142
144
  }, 10000);
143
145
  const finish = (error) => {
144
146
  clearTimeout(timeout);
145
147
  decoder.onload = decoder.onerror = null;
148
+ signal.removeEventListener('abort', cancelled);
146
149
  if (error)
147
150
  reject(error);
148
151
  else
149
152
  resolve();
150
153
  };
154
+ const cancelled = () => {
155
+ decoder.src = '';
156
+ finish(new DOMException('Image export cancelled', 'AbortError'));
157
+ };
158
+ signal.addEventListener('abort', cancelled, { once: true });
151
159
  decoder.onload = () => finish(decoder.naturalWidth > 0 && decoder.naturalHeight > 0
152
160
  ? undefined
153
161
  : new Error('The portable image has no usable dimensions.'));
@@ -272,7 +280,8 @@ function cleanSemantics(container, localIDs) {
272
280
  for (const comment of comments)
273
281
  comment.parentNode?.removeChild(comment);
274
282
  }
275
- export async function printableMarkdown(content, schema, adapter, context) {
283
+ export async function printableMarkdown(content, schema, adapter, context, files, signal = new AbortController().signal) {
284
+ signal.throwIfAborted();
276
285
  content = shareableFragment(content);
277
286
  const container = document.createElement('div');
278
287
  container.innerHTML = clipboardContent(content, schema).html;
@@ -280,10 +289,7 @@ export async function printableMarkdown(content, schema, adapter, context) {
280
289
  const tasks = [];
281
290
  content.descendants((node) => {
282
291
  if (node.type.name === 'image')
283
- references.push({
284
- reference: String(node.attrs.src ?? ''),
285
- ...splitAlt(String(node.attrs.alt ?? '')),
286
- });
292
+ references.push(node);
287
293
  if (node.type.name === 'list_item')
288
294
  tasks.push(node.attrs.checked ?? null);
289
295
  });
@@ -320,33 +326,58 @@ export async function printableMarkdown(content, schema, adapter, context) {
320
326
  reference.replaceChildren(link);
321
327
  }
322
328
  const images = new Map();
329
+ const warnings = [];
323
330
  for (const [index, element] of [
324
331
  ...container.querySelectorAll('img'),
325
332
  ].entries()) {
326
- const reference = references[index];
333
+ const node = references[index];
327
334
  try {
328
- if (!adapter || !reference)
335
+ if (!node)
329
336
  throw new Error('The authored image is unavailable.');
330
- const image = await validatedImage(await adapter.exportImage(reference.reference, { ...context }));
337
+ const reference = fileReference(node);
338
+ const file = files
339
+ ? await portableFile(node, files, context, signal)
340
+ : undefined;
341
+ if (file && file.presentation.kind !== 'image') {
342
+ const fallback = document.createElement('span');
343
+ fallback.textContent = file.description;
344
+ element.replaceWith(fallback);
345
+ const unavailable = file.presentation.kind === 'missing' ||
346
+ file.presentation.kind === 'error';
347
+ warnings.push({
348
+ code: unavailable ? 'attachment-unavailable' : 'attachment-fallback',
349
+ message: unavailable
350
+ ? `${file.label} is unavailable; its description is printed.`
351
+ : `${file.label} is printed as a ${file.presentation.kind === 'pdf' ? 'PDF' : file.presentation.kind} description.`,
352
+ });
353
+ continue;
354
+ }
355
+ if (!file && !adapter)
356
+ throw new Error('The authored image is unavailable.');
357
+ const image = await validatedImage(file
358
+ ? await portableFileImage(file, files, adapter, context, signal)
359
+ : await withFileSignal(adapter.exportImage(reference.reference, { ...context }), signal), signal);
360
+ signal.throwIfAborted();
331
361
  images.set(element, image);
332
362
  element.setAttribute('src', dataURL(image));
333
- element.setAttribute('alt', reference.alt);
363
+ element.setAttribute('alt', file?.label || reference.label || '');
334
364
  if (reference.width != null && reference.width > 0)
335
365
  element.setAttribute('width', String(Math.min(4096, reference.width)));
336
366
  }
337
367
  catch (error) {
368
+ signal.throwIfAborted();
338
369
  throw new InkKitError('image-unavailable', error instanceof Error
339
370
  ? error.message
340
371
  : 'The authored image is unavailable.');
341
372
  }
342
373
  }
343
- const warnings = [];
344
374
  for (const code of container.querySelectorAll('pre[data-language="mermaid"] > code')) {
345
375
  const source = code.textContent ?? '';
346
376
  try {
347
- await renderDiagram(source);
377
+ await withFileSignal(renderDiagram(source), signal);
348
378
  }
349
379
  catch (error) {
380
+ signal.throwIfAborted();
350
381
  const message = `${error instanceof Error ? error.message : 'This diagram could not be rendered.'} The source is printed below.`;
351
382
  warnings.push({ code: 'diagram-unavailable', message });
352
383
  const warning = document.createElement('p');
@@ -355,7 +386,7 @@ export async function printableMarkdown(content, schema, adapter, context) {
355
386
  continue;
356
387
  }
357
388
  try {
358
- const image = await validatedImage(await diagramImage(source));
389
+ const image = await validatedImage(await withFileSignal(diagramImage(source), signal), signal);
359
390
  const figure = document.createElement('figure');
360
391
  figure.className = 'inkkit-print-diagram';
361
392
  const element = document.createElement('img');
@@ -373,5 +404,6 @@ export async function printableMarkdown(content, schema, adapter, context) {
373
404
  }
374
405
  cleanSemantics(container, localIDs);
375
406
  const assets = [...container.querySelectorAll('img')].map((element) => images.get(element));
407
+ signal.throwIfAborted();
376
408
  return documentOutput(container, assets, warnings);
377
409
  }
package/dist/style.css CHANGED
@@ -235,6 +235,45 @@
235
235
  background: var(--_inkkit-accent);
236
236
  }
237
237
 
238
+ .inkkit-root .ProseMirror .inkkit-file {
239
+ vertical-align: middle;
240
+ border: 1px solid var(--_inkkit-rule);
241
+ border-radius: 4px;
242
+ padding: 4px;
243
+ white-space: normal;
244
+ }
245
+
246
+ .inkkit-root .ProseMirror .inkkit-file audio,
247
+ .inkkit-root .ProseMirror .inkkit-file video,
248
+ .inkkit-root .ProseMirror .inkkit-file iframe {
249
+ display: block;
250
+ max-width: 100%;
251
+ }
252
+
253
+ .inkkit-root .ProseMirror .inkkit-file video {
254
+ max-height: 480px;
255
+ }
256
+
257
+ .inkkit-root .ProseMirror .inkkit-file iframe {
258
+ width: min(600px, 100%);
259
+ height: 360px;
260
+ border: 0;
261
+ }
262
+
263
+ .inkkit-root .ProseMirror .inkkit-file-label {
264
+ overflow-wrap: anywhere;
265
+ }
266
+
267
+ .inkkit-root .ProseMirror .inkkit-file-control {
268
+ margin-inline-start: 0.5em;
269
+ font: inherit;
270
+ color: var(--_inkkit-text);
271
+ background: var(--_inkkit-code-bg);
272
+ border: 1px solid var(--_inkkit-rule);
273
+ border-radius: 3px;
274
+ cursor: pointer;
275
+ }
276
+
238
277
  .inkkit-root .ProseMirror {
239
278
  /* The viewport, not the wrapper Milkdown adds, so a click below the text still places the caret. */
240
279
  min-height: var(--inkkit-min-height, 100%);
@@ -680,7 +719,7 @@
680
719
  display: none;
681
720
  }
682
721
 
683
- .inkkit-root[data-inkkit-comments-visible='true'] .inkkit-comment {
722
+ .inkkit-root [data-inkkit-comments-visible='true'] .inkkit-comment {
684
723
  display: inline;
685
724
  white-space: pre-wrap;
686
725
  font-family: var(--_inkkit-mono);
@@ -688,7 +727,7 @@
688
727
  background: var(--_inkkit-code-bg);
689
728
  }
690
729
 
691
- .inkkit-root[data-inkkit-comments-visible='true'] .inkkit-comment-block {
730
+ .inkkit-root [data-inkkit-comments-visible='true'] .inkkit-comment-block {
692
731
  display: block;
693
732
  padding: calc(var(--inkkit-font-size, 15px) * 0.5);
694
733
  }
@@ -707,7 +746,6 @@
707
746
  }
708
747
 
709
748
  .inkkit-root .inkkit-comment,
710
- .inkkit-root[data-inkkit-comments-visible='true'] .inkkit-comment,
711
749
  .inkkit-root [data-inkkit-callout-toggle],
712
750
  .inkkit-root .ProseMirror pre .copy,
713
751
  .inkkit-root .milkdown-table-block .handle,