@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/README.md +3 -3
- package/dist/callouts.d.ts +24 -0
- package/dist/callouts.js +296 -0
- package/dist/clipboard.d.ts +1 -1
- package/dist/clipboard.js +63 -2
- package/dist/code-fences.d.ts +8 -0
- package/dist/code-fences.js +166 -0
- package/dist/comments.d.ts +55 -0
- package/dist/comments.js +487 -0
- package/dist/dialect.js +12 -1
- package/dist/editor.d.ts +3 -2
- package/dist/editor.js +46 -9
- package/dist/index.d.ts +1 -1
- package/dist/inline-highlight.d.ts +6 -0
- package/dist/inline-highlight.js +112 -0
- package/dist/literals.d.ts +2 -0
- package/dist/literals.js +31 -4
- package/dist/mermaid.d.ts +8 -0
- package/dist/mermaid.js +216 -0
- package/dist/paste.js +19 -2
- package/dist/preserve.js +66 -9
- package/dist/reference-clipboard.js +5 -1
- package/dist/style.css +106 -0
- package/dist/types.d.ts +7 -1
- package/package.json +7 -1
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 {
|
|
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
|
|
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
|
-
:
|
|
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 &&
|
|
626
|
-
|
|
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
|
|
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();
|
package/dist/literals.d.ts
CHANGED
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/mermaid.js
ADDED
|
@@ -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]`);
|