@aicayzer/inkkit 0.0.2 → 0.0.4

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/editor.js CHANGED
@@ -20,10 +20,13 @@ import { imageView } from './images.js';
20
20
  import { tablePlugins, tableCommand } from './tables.js';
21
21
  import { InkKitError, } from './types.js';
22
22
  import { highlightPlugin } from './highlight.js';
23
+ import { highlightKeymap, toggleHighlightCommand } from './inline-highlight.js';
24
+ import { commentSelectionContent, setCommentVisibility } from './comments.js';
23
25
  import { PasteController } from './paste.js';
24
26
  import { selectionPlugin } from './selection.js';
25
27
  import { search, SearchQuery, getSearchState, setSearchState, } from 'prosemirror-search';
26
28
  import { taskListPlugin, toggleTaskList } from './tasks.js';
29
+ import { isMermaid, mermaidPreview } from './mermaid.js';
27
30
  const shortcutCommands = {
28
31
  heading1: ['heading', 1],
29
32
  heading2: ['heading', 2],
@@ -32,6 +35,7 @@ const shortcutCommands = {
32
35
  bold: ['bold'],
33
36
  italic: ['italic'],
34
37
  strikethrough: ['strikethrough'],
38
+ highlight: ['highlight'],
35
39
  code: ['code'],
36
40
  codeBlock: ['codeBlock'],
37
41
  quote: ['quote'],
@@ -43,6 +47,7 @@ const markNames = {
43
47
  strong: 'bold',
44
48
  emphasis: 'italic',
45
49
  strike_through: 'strikethrough',
50
+ highlight: 'highlight',
46
51
  inlineCode: 'code',
47
52
  link: 'link',
48
53
  };
@@ -338,10 +343,27 @@ export class InkKitEditor {
338
343
  : this.plain.value.slice(this.plain.selectionStart, this.plain.selectionEnd);
339
344
  const pre = document.createElement('pre');
340
345
  pre.textContent = text;
341
- return { text, html: pre.outerHTML, markdown: text, images: [] };
346
+ return {
347
+ text,
348
+ html: pre.outerHTML,
349
+ markdown: text,
350
+ images: [],
351
+ diagrams: [],
352
+ };
342
353
  }
343
354
  const { doc, schema, selection } = this.editor.ctx.get(editorViewCtx).state;
344
- const content = all ? doc.content : selection.content().content;
355
+ const completeDiagrams = new Set();
356
+ doc.descendants((node, pos) => {
357
+ if (isMermaid(node) &&
358
+ (all ||
359
+ (selection.from <= pos + 1 &&
360
+ selection.to >= pos + node.nodeSize - 1))) {
361
+ completeDiagrams.add(node.textContent.replace(/\r\n?/g, '\n'));
362
+ }
363
+ });
364
+ const content = all
365
+ ? doc.content
366
+ : commentSelectionContent(doc, selection.from, selection.to, selection.content().content);
345
367
  const valid = content.firstChild?.isInline
346
368
  ? Fragment.from(schema.nodes.paragraph.create(null, content))
347
369
  : content;
@@ -349,14 +371,16 @@ export class InkKitEditor {
349
371
  try {
350
372
  markdown = all
351
373
  ? snapshot.text
352
- : selectionMarkdown(this.editor.ctx, doc, valid);
374
+ : completeDiagrams.size
375
+ ? new Preservation(this.editor.ctx, snapshot.text).serialize(doc.type.create(null, selectionContent(doc, valid)))
376
+ : selectionMarkdown(this.editor.ctx, doc, valid);
353
377
  }
354
378
  catch (error) {
355
379
  throw new InkKitError('preservation', error instanceof Error
356
380
  ? error.message
357
381
  : 'Cannot copy the selected Markdown');
358
382
  }
359
- const result = await portableClipboard(all ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context());
383
+ const result = await portableClipboard(all ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context(), completeDiagrams);
360
384
  const metadata = referenceMetadata(this.editor.ctx, doc, content);
361
385
  if (metadata != null)
362
386
  result.html = withReferenceMetadata(result.html, metadata);
@@ -365,6 +389,8 @@ export class InkKitEditor {
365
389
  throw new InkKitError('stale-document', 'Document changed');
366
390
  if (result.images.some((image) => image.error))
367
391
  this.events.error?.(new InkKitError('image-unavailable', 'Some copied images were unavailable; their descriptions were retained'));
392
+ if (result.diagrams?.some((diagram) => diagram.error))
393
+ this.events.error?.(new InkKitError('diagram-unavailable', 'Some diagrams could not be exported; their source was retained'));
368
394
  return result;
369
395
  }
370
396
  async paste(input) {
@@ -375,6 +401,10 @@ export class InkKitEditor {
375
401
  }
376
402
  await this.pasteController.paste(input);
377
403
  }
404
+ setCommentsVisible(visible) {
405
+ this.assertAlive();
406
+ setCommentVisibility(this.editor.ctx.get(editorViewCtx), visible);
407
+ }
378
408
  pasteAsPlainText(text) {
379
409
  this.assertCurrent();
380
410
  if (this.formatType === 'txt') {
@@ -485,6 +515,7 @@ export class InkKitEditor {
485
515
  unbind(emphasisKeymap);
486
516
  unbind(inlineCodeKeymap);
487
517
  unbind(strikethroughKeymap);
518
+ unbind(highlightKeymap);
488
519
  unbind(headingKeymap, ['DowngradeHeading']);
489
520
  unbind(paragraphKeymap);
490
521
  unbind(blockquoteKeymap);
@@ -516,6 +547,7 @@ export class InkKitEditor {
516
547
  .use(codeCopyPlugin((text) => events.copy(text)))
517
548
  .use(placeholderPlugin)
518
549
  .use(highlightPlugin)
550
+ .use(mermaidPreview((error) => events.error?.(error)))
519
551
  .use(selectionPlugin)
520
552
  .use($prose(() => search()))
521
553
  .use(options.images ? imageView(options.images) : [])
@@ -580,11 +612,11 @@ export class InkKitEditor {
580
612
  if (instance.formatType === 'txt')
581
613
  return;
582
614
  const view = instance.editor.ctx.get(editorViewCtx);
583
- const fragment = view.state.selection.content().content;
615
+ const fragment = commentSelectionContent(view.state.doc, view.state.selection.from, view.state.selection.to, view.state.selection.content().content);
584
616
  const expanded = selectionContent(view.state.doc, fragment);
585
617
  let hasImages = false;
586
618
  expanded.descendants((node) => {
587
- if (node.type.name === 'image')
619
+ if (node.type.name === 'image' || isMermaid(node))
588
620
  hasImages = true;
589
621
  });
590
622
  if (!hasImages) {
@@ -622,8 +654,10 @@ export class InkKitEditor {
622
654
  void instance
623
655
  .clipboardSnapshot(false)
624
656
  .then(async (content) => {
625
- if (cut && content.images.some((image) => image.error))
626
- throw new InkKitError('image-unavailable', 'The selected images could not be cut safely');
657
+ if (cut &&
658
+ (content.images.some((image) => image.error) ||
659
+ content.diagrams?.some((diagram) => diagram.error)))
660
+ throw new InkKitError('image-unavailable', 'The selected content could not be cut safely');
627
661
  await events.clipboard(content);
628
662
  if (cut) {
629
663
  instance.assertCurrent();
@@ -640,7 +674,7 @@ export class InkKitEditor {
640
674
  container.textContent = clipboardText(expanded);
641
675
  event.clipboardData?.setData('text/plain', container.textContent ?? '');
642
676
  event.clipboardData?.setData('text/html', container.outerHTML);
643
- events.error?.(new InkKitError('image-unavailable', 'The host must provide a clipboard handler to copy images'));
677
+ events.error?.(new InkKitError('image-unavailable', 'The host must provide a clipboard handler for portable images and diagrams'));
644
678
  }
645
679
  };
646
680
  root.addEventListener('copy', instance.copyHandler, true);
@@ -1011,6 +1045,9 @@ export class InkKitEditor {
1011
1045
  case 'strikethrough':
1012
1046
  run(toggleStrikethroughCommand.key);
1013
1047
  break;
1048
+ case 'highlight':
1049
+ run(toggleHighlightCommand.key);
1050
+ break;
1014
1051
  case 'code':
1015
1052
  this.toggleInlineCode();
1016
1053
  break;
package/dist/index.d.ts CHANGED
@@ -2,4 +2,4 @@ export { InkKitEditor } from './editor.js';
2
2
  export type { EditorEvents, CaretState, Mark, Block, FormatCommand, Keymap, } from './editor.js';
3
3
  export type { TableCommand } from './tables.js';
4
4
  export { InkKitError } from './types.js';
5
- export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, } from './types.js';
5
+ export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, ClipboardDiagram, } from './types.js';
@@ -0,0 +1,6 @@
1
+ export declare const remarkInlineHighlightPlugin: import("@milkdown/kit/utils").$Remark<"inkkitInlineHighlight", unknown>;
2
+ export declare const highlightSchema: import("@milkdown/kit/utils").$MarkSchema<"highlight">;
3
+ export declare const toggleHighlightCommand: import("@milkdown/kit/utils").$Command<unknown>;
4
+ export declare const highlightInputRule: import("@milkdown/kit/utils").$InputRule;
5
+ export declare const highlightKeymap: import("@milkdown/kit/utils").$UserKeymap<"highlightKeymap", "ToggleHighlight">;
6
+ export declare const inlineHighlight: (import("@milkdown/kit/utils").$Command<unknown> | import("@milkdown/kit/utils").$Ctx<unknown, "inkkitInlineHighlight"> | import("@milkdown/kit/utils").$Ctx<import("@milkdown/kit/utils").GetMarkSchema, "highlight"> | import("@milkdown/kit/utils").$Ctx<import("@milkdown/kit/utils").KeymapConfig<"ToggleHighlight">, "highlightKeymapKeymap"> | import("@milkdown/kit/utils").$InputRule | import("@milkdown/kit/utils").$Mark | import("@milkdown/kit/utils").$Shortcut | import("@milkdown/kit/ctx").MilkdownPlugin)[];
@@ -0,0 +1,112 @@
1
+ import { commandsCtx, remarkCtx } from '@milkdown/kit/core';
2
+ import { markRule } from '@milkdown/kit/prose';
3
+ import { toggleMark } from '@milkdown/kit/prose/commands';
4
+ import { InputRule } from '@milkdown/kit/prose/inputrules';
5
+ import { EditorState, TextSelection } from '@milkdown/kit/prose/state';
6
+ import { $command, $inputRule, $markSchema, $remark, $useKeymap, } from '@milkdown/kit/utils';
7
+ import { highlightMark } from 'micromark-extension-highlight-mark';
8
+ import { highlightMarkFromMarkdown, highlightMarkToMarkdown, } from 'mdast-util-highlight-mark';
9
+ // The current serializer's attention handling encodes ambiguous whitespace and
10
+ // punctuation boundaries; the highlight provider predates that API.
11
+ const highlight = (node, _parent, state, info) => {
12
+ const exit = state.enter('phrasing');
13
+ const value = state.containerPhrasing({ type: 'root', children: [node] }, info);
14
+ exit();
15
+ return value;
16
+ };
17
+ highlight.attention = () => ({
18
+ construct: 'highlight',
19
+ markers: ['='],
20
+ sizes: [2],
21
+ });
22
+ highlight.peek = () => '=';
23
+ function remarkInlineHighlight() {
24
+ const data = this.data();
25
+ (data.micromarkExtensions ??= []).push(highlightMark());
26
+ (data.fromMarkdownExtensions ??= []).push(highlightMarkFromMarkdown);
27
+ (data.toMarkdownExtensions ??= []).push({
28
+ ...highlightMarkToMarkdown,
29
+ handlers: { highlight },
30
+ });
31
+ }
32
+ export const remarkInlineHighlightPlugin = $remark('inkkitInlineHighlight', () => remarkInlineHighlight);
33
+ export const highlightSchema = $markSchema('highlight', () => ({
34
+ priority: 45,
35
+ parseDOM: [{ tag: 'mark' }],
36
+ toDOM: () => ['mark', 0],
37
+ parseMarkdown: {
38
+ match: (node) => node.type === 'highlight',
39
+ runner(state, node, type) {
40
+ state.openMark(type);
41
+ state.next(node.children);
42
+ state.closeMark(type);
43
+ },
44
+ },
45
+ toMarkdown: {
46
+ match: (mark) => mark.type.name === 'highlight',
47
+ runner(state, mark, node) {
48
+ if (node.isText && /^\s*$/.test(node.text ?? '')) {
49
+ return;
50
+ }
51
+ state.withMark(mark, 'highlight');
52
+ },
53
+ },
54
+ }));
55
+ export const toggleHighlightCommand = $command('ToggleHighlight', (ctx) => () => toggleMark(highlightSchema.type(ctx)));
56
+ export const highlightInputRule = $inputRule((ctx) => {
57
+ const pattern = /==([^\n]+)==$/;
58
+ const rule = markRule(pattern, highlightSchema.type(ctx));
59
+ const apply = rule.handler;
60
+ return new InputRule(pattern, (state, match, start, end) => {
61
+ const parentStart = state.selection.$from.start();
62
+ const prefix = state.selection.$from.parent.textBetween(0, start - parentStart);
63
+ const source = prefix + match[0];
64
+ const tree = ctx.get(remarkCtx).parse(source);
65
+ let valid = false;
66
+ const walk = (node) => {
67
+ if (node.type === 'highlight' &&
68
+ node.position?.start.offset === prefix.length &&
69
+ node.position.end.offset === source.length)
70
+ valid = true;
71
+ if ('children' in node)
72
+ node.children.forEach(walk);
73
+ };
74
+ walk(tree);
75
+ if (!valid)
76
+ return null;
77
+ const { from, to } = state.selection;
78
+ const typed = match[0].slice(from - start);
79
+ if (typed.length <= 1 && state.selection.empty)
80
+ return apply(state, match, start, end);
81
+ // Milkdown's mark rule expects only the final character to be absent.
82
+ // Prepare buffered input without applying plugins or dispatching twice.
83
+ const tr = state.tr.insertText(typed.slice(0, -1), from, to);
84
+ const preparedEnd = from + typed.length - 1;
85
+ const prepared = EditorState.create({
86
+ schema: state.schema,
87
+ doc: tr.doc,
88
+ selection: TextSelection.create(tr.doc, preparedEnd),
89
+ storedMarks: state.storedMarks,
90
+ });
91
+ const transformed = apply(prepared, match, start, preparedEnd);
92
+ if (!transformed)
93
+ return null;
94
+ transformed.steps.forEach((step) => tr.step(step));
95
+ tr.setSelection(TextSelection.create(tr.doc, transformed.selection.from, transformed.selection.to));
96
+ tr.setStoredMarks(transformed.storedMarks);
97
+ return tr;
98
+ });
99
+ });
100
+ export const highlightKeymap = $useKeymap('highlightKeymap', {
101
+ ToggleHighlight: {
102
+ shortcuts: ['Mod-Shift-h'],
103
+ command: (ctx) => () => ctx.get(commandsCtx).call(toggleHighlightCommand.key),
104
+ },
105
+ });
106
+ export const inlineHighlight = [
107
+ remarkInlineHighlightPlugin,
108
+ highlightSchema,
109
+ toggleHighlightCommand,
110
+ highlightInputRule,
111
+ highlightKeymap,
112
+ ].flat();
@@ -1,6 +1,8 @@
1
1
  import type { Literal } from 'mdast';
2
+ import { type CommentSpan } from './comments.js';
2
3
  interface InkKitLiteral extends Literal {
3
4
  type: 'inkkitLiteral';
5
+ comments?: CommentSpan[];
4
6
  }
5
7
  declare module 'mdast' {
6
8
  interface BlockContentMap {
package/dist/literals.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { $node, $remark } from '@milkdown/kit/utils';
2
+ import { commentSource, literalCommentSpans, } from './comments.js';
2
3
  const unsupported = new Set(['html', 'image', 'imageReference']);
3
4
  function restoreInlineBreaks(node) {
4
5
  if ((node.type === 'paragraph' || node.type === 'heading') &&
@@ -68,7 +69,12 @@ export function createLiteralPreservation(images) {
68
69
  // Definition indentation is restored by the footnote serializer.
69
70
  for (let index = 0; index < depth; index++)
70
71
  raw = raw.replace(/\r?\n(?: {4}|\t)/g, '\n');
71
- return { type: 'inkkitLiteral', value: raw, position: node.position };
72
+ return {
73
+ type: 'inkkitLiteral',
74
+ value: raw,
75
+ comments: literalCommentSpans(raw),
76
+ position: node.position,
77
+ };
72
78
  };
73
79
  const blocks = [];
74
80
  if (frontmatter)
@@ -105,6 +111,8 @@ export function createLiteralPreservation(images) {
105
111
  });
106
112
  continue;
107
113
  }
114
+ if (node.type === 'inkkitLiteral')
115
+ node.comments = literalCommentSpans(node.value);
108
116
  blocks.push(preserveBlock(node));
109
117
  }
110
118
  tree.children = blocks;
@@ -113,7 +121,7 @@ export function createLiteralPreservation(images) {
113
121
  export const preserveLiterals = createLiteralPreservation(false);
114
122
  export const literalBlock = $node('literal_markdown', () => ({
115
123
  group: 'block',
116
- content: 'text*',
124
+ content: '(text | comment_inline)*',
117
125
  marks: '',
118
126
  code: true,
119
127
  defining: true,
@@ -129,14 +137,33 @@ export const literalBlock = $node('literal_markdown', () => ({
129
137
  match: (node) => node.type === 'inkkitLiteral',
130
138
  runner: (state, node, type) => {
131
139
  state.openNode(type);
132
- state.addText(String(node.value ?? ''));
140
+ const value = String(node.value ?? '');
141
+ let cursor = 0;
142
+ for (const span of node.comments ?? []) {
143
+ if (span.start > cursor)
144
+ state.addText(value.slice(cursor, span.start));
145
+ state.openNode(state.schema.nodes.comment_inline, {
146
+ syntax: span.syntax,
147
+ block: Boolean(span.block),
148
+ });
149
+ if (span.value)
150
+ state.addText(span.value);
151
+ state.closeNode();
152
+ cursor = span.end;
153
+ }
154
+ if (cursor < value.length)
155
+ state.addText(value.slice(cursor));
133
156
  state.closeNode();
134
157
  },
135
158
  },
136
159
  toMarkdown: {
137
160
  match: (node) => node.type.name === 'literal_markdown',
138
161
  runner: (state, node) => {
139
- state.addNode('html', undefined, node.textContent);
162
+ let source = '';
163
+ node.forEach((child) => {
164
+ source += child.isText ? child.text : commentSource(child);
165
+ });
166
+ state.addNode('html', undefined, source);
140
167
  },
141
168
  },
142
169
  }));
@@ -0,0 +1,8 @@
1
+ import type { Node as ProseNode } from '@milkdown/kit/prose/model';
2
+ import { type PortableImage } from './types.js';
3
+ export declare function isMermaid(node: ProseNode): boolean;
4
+ export declare function validateDiagram(source: string): void;
5
+ export declare function safeDiagramSVG(svg: string): string;
6
+ export declare function renderDiagram(source: string): Promise<string>;
7
+ export declare function diagramImage(source: string): Promise<PortableImage>;
8
+ export declare function mermaidPreview(onError: (error: Error) => void): import("@milkdown/kit/utils").$Prose;
@@ -0,0 +1,216 @@
1
+ import mermaid from 'mermaid';
2
+ import DOMPurify from 'dompurify';
3
+ import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
4
+ import { Decoration, DecorationSet } from '@milkdown/kit/prose/view';
5
+ import { $prose } from '@milkdown/kit/utils';
6
+ import { InkKitError } from './types.js';
7
+ export function isMermaid(node) {
8
+ return node.type.name === 'code_block' && node.attrs.language === 'mermaid';
9
+ }
10
+ export function validateDiagram(source) {
11
+ if (source.length > 30000)
12
+ throw new Error('The diagram exceeds the 30,000 character rendering limit.');
13
+ if (!/^\s*(?:%%[^\n]*\n\s*)*(?:flowchart|graph|sequenceDiagram|classDiagram|stateDiagram(?:-v2)?|erDiagram|pie)\b/.test(source))
14
+ throw new Error('This diagram type is not supported. The source is retained.');
15
+ // Mermaid can load image bytes before sanitising its result. Reject resource
16
+ // and configuration syntax before calling the renderer, including entities.
17
+ if (/%%\{|^\s*---|<\s*[a-z!/?]|&(?:#\w+|[a-z]+);|@\{|\$\$|(?:https?|data|file|javascript|vbscript|blob):|\/\/|\b(?:click|callback|href|link|image|icon|fontawesome|style|classDef|linkStyle)\b|url\s*\(|@import|\\/im.test(source))
18
+ throw new Error('HTML, actions, resources and diagram configuration are disabled. The source is retained.');
19
+ }
20
+ let sequence = 0;
21
+ let queue = Promise.resolve();
22
+ const cache = new Map();
23
+ export function safeDiagramSVG(svg) {
24
+ const clean = DOMPurify.sanitize(svg, {
25
+ USE_PROFILES: { svg: true, svgFilters: true },
26
+ FORBID_TAGS: ['foreignObject', 'image', 'a', 'script', 'animate', 'set'],
27
+ FORBID_ATTR: ['href', 'xlink:href'],
28
+ });
29
+ const template = document.createElement('template');
30
+ template.innerHTML = clean;
31
+ const root = template.content.querySelector('svg');
32
+ if (!root)
33
+ throw new Error('The renderer did not produce a diagram.');
34
+ for (const element of [root, ...root.querySelectorAll('*')]) {
35
+ for (const attribute of [...element.attributes]) {
36
+ if (/^on/i.test(attribute.name) ||
37
+ /(?:https?|data|file|javascript|blob):|@import|url\s*\(\s*[^#]|\\/i.test(attribute.value))
38
+ element.removeAttribute(attribute.name);
39
+ }
40
+ if (element.tagName.toLowerCase() === 'style' &&
41
+ /@import|(?:https?|data|file):|url\s*\(\s*[^#]|\\/i.test(element.textContent ?? ''))
42
+ element.remove();
43
+ }
44
+ root.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
45
+ return root.outerHTML;
46
+ }
47
+ export function renderDiagram(source) {
48
+ try {
49
+ validateDiagram(source);
50
+ }
51
+ catch (error) {
52
+ return Promise.reject(error);
53
+ }
54
+ const previous = cache.get(source);
55
+ if (previous)
56
+ return previous;
57
+ const rendered = queue.then(async () => {
58
+ mermaid.initialize({
59
+ startOnLoad: false,
60
+ securityLevel: 'strict',
61
+ htmlLabels: false,
62
+ suppressErrorRendering: true,
63
+ fontFamily: 'Arial, sans-serif',
64
+ altFontFamily: 'Arial, sans-serif',
65
+ layout: 'dagre',
66
+ look: 'classic',
67
+ theme: 'default',
68
+ maxTextSize: 30000,
69
+ maxEdges: 500,
70
+ flowchart: { htmlLabels: false },
71
+ sequence: {
72
+ actorFontFamily: 'Arial, sans-serif',
73
+ noteFontFamily: 'Arial, sans-serif',
74
+ messageFontFamily: 'Arial, sans-serif',
75
+ },
76
+ secure: [
77
+ 'securityLevel',
78
+ 'htmlLabels',
79
+ 'theme',
80
+ 'themeCSS',
81
+ 'themeVariables',
82
+ 'fontFamily',
83
+ 'dompurifyConfig',
84
+ 'flowchart',
85
+ 'startOnLoad',
86
+ 'maxTextSize',
87
+ 'maxEdges',
88
+ 'secure',
89
+ 'altFontFamily',
90
+ 'layout',
91
+ 'look',
92
+ 'sequence',
93
+ 'class',
94
+ 'state',
95
+ 'er',
96
+ 'pie',
97
+ 'suppressErrorRendering',
98
+ ],
99
+ });
100
+ const holder = document.createElement('div');
101
+ holder.style.cssText =
102
+ 'position:fixed;left:-100000px;top:0;width:1200px;pointer-events:none';
103
+ holder.setAttribute('aria-hidden', 'true');
104
+ document.body.append(holder);
105
+ try {
106
+ const result = await mermaid.render(`inkkitDiagram${++sequence}`, source, holder);
107
+ return safeDiagramSVG(result.svg);
108
+ }
109
+ finally {
110
+ holder.remove();
111
+ }
112
+ });
113
+ queue = rendered.catch(() => undefined);
114
+ cache.set(source, rendered);
115
+ void rendered.catch(() => {
116
+ if (cache.get(source) === rendered)
117
+ cache.delete(source);
118
+ });
119
+ if (cache.size > 64)
120
+ cache.delete(cache.keys().next().value);
121
+ return rendered;
122
+ }
123
+ export async function diagramImage(source) {
124
+ const svg = await renderDiagram(source);
125
+ const parsed = new DOMParser().parseFromString(svg, 'image/svg+xml').documentElement;
126
+ const bounds = (parsed.getAttribute('viewBox') ?? '')
127
+ .split(/[\s,]+/)
128
+ .map(Number);
129
+ const width = bounds[2] || Number.parseFloat(parsed.getAttribute('width') ?? '');
130
+ const height = bounds[3] || Number.parseFloat(parsed.getAttribute('height') ?? '');
131
+ if (!(width > 0 && height > 0 && Number.isFinite(width + height)))
132
+ throw new Error('The rendered diagram has no usable dimensions.');
133
+ const scale = Math.min(2, 4096 / width, 4096 / height);
134
+ const canvas = document.createElement('canvas');
135
+ canvas.width = Math.max(1, Math.ceil(width * scale));
136
+ canvas.height = Math.max(1, Math.ceil(height * scale));
137
+ const context = canvas.getContext('2d');
138
+ if (!context)
139
+ throw new Error('Portable diagram output is unavailable.');
140
+ parsed.setAttribute('width', String(width));
141
+ parsed.setAttribute('height', String(height));
142
+ const image = new Image();
143
+ const url = URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(parsed)], {
144
+ type: 'image/svg+xml',
145
+ }));
146
+ try {
147
+ await new Promise((resolve, reject) => {
148
+ image.onload = () => resolve();
149
+ image.onerror = () => reject(new Error('The diagram image could not be rendered.'));
150
+ image.src = url;
151
+ });
152
+ context.fillStyle = '#ffffff';
153
+ context.fillRect(0, 0, canvas.width, canvas.height);
154
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
155
+ const data = canvas.toDataURL('image/png').split(',')[1];
156
+ if (!data)
157
+ throw new Error('The diagram image could not be exported.');
158
+ return {
159
+ bytes: Uint8Array.from(atob(data), (character) => character.charCodeAt(0)),
160
+ mimeType: 'image/png',
161
+ filename: 'diagram.png',
162
+ };
163
+ }
164
+ finally {
165
+ URL.revokeObjectURL(url);
166
+ }
167
+ }
168
+ export function mermaidPreview(onError) {
169
+ return $prose(() => new Plugin({
170
+ key: new PluginKey('mermaidPreview'),
171
+ props: {
172
+ decorations(state) {
173
+ const widgets = [];
174
+ state.doc.descendants((node, pos) => {
175
+ if (!isMermaid(node))
176
+ return true;
177
+ const source = node.textContent;
178
+ widgets.push(Decoration.widget(pos + node.nodeSize, () => {
179
+ const preview = document.createElement('div');
180
+ preview.className = 'inkkit-mermaid-preview';
181
+ preview.contentEditable = 'false';
182
+ preview.textContent = 'Rendering diagram…';
183
+ let active = true;
184
+ preview.cancel =
185
+ () => {
186
+ active = false;
187
+ };
188
+ void renderDiagram(source).then((svg) => {
189
+ if (active) {
190
+ preview.innerHTML = svg;
191
+ preview.dataset.state = 'ready';
192
+ }
193
+ }, (error) => {
194
+ if (!active)
195
+ return;
196
+ const message = error instanceof Error
197
+ ? error.message
198
+ : 'The diagram could not be rendered.';
199
+ preview.textContent = `Diagram unavailable: ${message}`;
200
+ preview.dataset.state = 'error';
201
+ onError(new InkKitError('diagram-unavailable', message));
202
+ });
203
+ return preview;
204
+ }, {
205
+ key: `mermaid:${source}`,
206
+ side: -1,
207
+ ignoreSelection: true,
208
+ destroy: (node) => node.cancel?.(),
209
+ }));
210
+ return false;
211
+ });
212
+ return DecorationSet.create(state.doc, widgets);
213
+ },
214
+ },
215
+ }));
216
+ }
package/dist/paste.js CHANGED
@@ -136,6 +136,23 @@ export class PasteController {
136
136
  const template = document.createElement('template');
137
137
  template.innerHTML = input.html;
138
138
  const container = template.content;
139
+ for (const warning of container.querySelectorAll('[data-inkkit-diagram-error]'))
140
+ warning.remove();
141
+ for (const diagram of container.querySelectorAll('figure[data-inkkit-mermaid]')) {
142
+ const source = diagram.getAttribute('data-inkkit-mermaid') ?? '';
143
+ if (source.length > 30000)
144
+ continue;
145
+ const pre = document.createElement('pre');
146
+ pre.dataset.language = 'mermaid';
147
+ const fence = diagram.getAttribute('data-inkkit-fence');
148
+ if (fence)
149
+ pre.setAttribute('data-inkkit-fence', fence);
150
+ const code = document.createElement('code');
151
+ code.className = 'language-mermaid';
152
+ code.textContent = source;
153
+ pre.append(code);
154
+ diagram.replaceWith(pre);
155
+ }
139
156
  if (markdown == null) {
140
157
  // Native destinations can keep node metadata after dropping the
141
158
  // Markdown wrapper and its definitions. Import their visible HTML.
@@ -179,6 +196,7 @@ export class PasteController {
179
196
  'H5',
180
197
  'H6',
181
198
  'STRONG',
199
+ 'MARK',
182
200
  'B',
183
201
  'EM',
184
202
  'I',
@@ -300,8 +318,7 @@ export class PasteController {
300
318
  const slot = /^inkkit-clipboard-image:(\d+)$/.exec(String(node.attrs.src));
301
319
  const position = slot ? Number(slot[1]) : imageIndex++;
302
320
  const element = elements.some((element) => element.hasAttribute('data-inkkit-image-slot'))
303
- ? elements.find((element) => Number(element.getAttribute('data-inkkit-image-slot')) ===
304
- position)
321
+ ? elements.find((element) => Number(element.getAttribute('data-inkkit-authored-image-slot') ?? element.getAttribute('data-inkkit-image-slot')) === position)
305
322
  : elements[position];
306
323
  if (!element || !container.contains(element))
307
324
  return schema.text(`[${node.attrs.alt || 'Image'}: image unavailable]`);