@aicayzer/inkkit 0.0.3 → 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/clipboard.d.ts +1 -1
- package/dist/clipboard.js +54 -2
- package/dist/code-fences.d.ts +8 -0
- package/dist/code-fences.js +166 -0
- package/dist/dialect.js +4 -1
- package/dist/editor.js +30 -7
- package/dist/index.d.ts +1 -1
- package/dist/mermaid.d.ts +8 -0
- package/dist/mermaid.js +216 -0
- package/dist/paste.js +18 -2
- package/dist/preserve.js +42 -5
- package/dist/style.css +16 -0
- package/dist/types.d.ts +7 -1
- package/package.json +3 -1
package/dist/clipboard.d.ts
CHANGED
|
@@ -8,4 +8,4 @@ export declare function clipboardText(content: Fragment, missingImages?: Readonl
|
|
|
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): Promise<ClipboardOutput>;
|
|
11
|
+
export declare function portableClipboard(content: Fragment, schema: Schema, markdown: string, adapter: ImageAdapter | undefined, context: DocumentContext, completeDiagrams?: ReadonlySet<string>): Promise<ClipboardOutput>;
|
package/dist/clipboard.js
CHANGED
|
@@ -2,6 +2,7 @@ import { DOMSerializer, } from '@milkdown/kit/prose/model';
|
|
|
2
2
|
import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
|
|
3
3
|
import { $prose } from '@milkdown/kit/utils';
|
|
4
4
|
import { shareableFragment } from './comments.js';
|
|
5
|
+
import { diagramImage } from './mermaid.js';
|
|
5
6
|
// Clipboard text is the visible document, not Markdown serialization: escaping
|
|
6
7
|
// punctuation or trailing spaces here leaks source syntax into other apps.
|
|
7
8
|
export function clipboardText(content, missingImages = new Set()) {
|
|
@@ -110,7 +111,7 @@ export const visibleClipboard = $prose(() => new Plugin({
|
|
|
110
111
|
},
|
|
111
112
|
}));
|
|
112
113
|
/** Export a frozen document fragment; presentation URLs never enter the clipboard. */
|
|
113
|
-
export async function portableClipboard(content, schema, markdown, adapter, context) {
|
|
114
|
+
export async function portableClipboard(content, schema, markdown, adapter, context, completeDiagrams) {
|
|
114
115
|
content = shareableFragment(content);
|
|
115
116
|
const base = clipboardContent(content, schema);
|
|
116
117
|
const container = document.createElement('div');
|
|
@@ -130,6 +131,7 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
|
|
|
130
131
|
});
|
|
131
132
|
await Promise.all(captured.map(async ({ element, reference, alt }, index) => {
|
|
132
133
|
element.setAttribute('data-inkkit-image-slot', String(index));
|
|
134
|
+
element.setAttribute('data-inkkit-authored-image-slot', String(index));
|
|
133
135
|
try {
|
|
134
136
|
if (!adapter)
|
|
135
137
|
throw new Error('The image is unavailable.');
|
|
@@ -152,6 +154,55 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
|
|
|
152
154
|
images[index] = { reference, alt, error: message };
|
|
153
155
|
}
|
|
154
156
|
}));
|
|
157
|
+
const diagrams = [];
|
|
158
|
+
for (const code of container.querySelectorAll('pre[data-language="mermaid"] > code')) {
|
|
159
|
+
const source = code.textContent ?? '';
|
|
160
|
+
if (completeDiagrams && !completeDiagrams.has(source))
|
|
161
|
+
continue;
|
|
162
|
+
const result = { source };
|
|
163
|
+
diagrams.push(result);
|
|
164
|
+
try {
|
|
165
|
+
const image = await diagramImage(source);
|
|
166
|
+
let binary = '';
|
|
167
|
+
for (const byte of image.bytes)
|
|
168
|
+
binary += String.fromCharCode(byte);
|
|
169
|
+
const figure = document.createElement('figure');
|
|
170
|
+
figure.setAttribute('data-inkkit-mermaid', source);
|
|
171
|
+
const fence = code.parentElement.getAttribute('data-inkkit-fence');
|
|
172
|
+
if (fence)
|
|
173
|
+
figure.setAttribute('data-inkkit-fence', fence);
|
|
174
|
+
const element = document.createElement('img');
|
|
175
|
+
element.src = `data:image/png;base64,${btoa(binary)}`;
|
|
176
|
+
element.alt = `Mermaid diagram: ${source}`;
|
|
177
|
+
figure.append(element);
|
|
178
|
+
code.parentElement.replaceWith(figure);
|
|
179
|
+
result.image = image;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
result.error =
|
|
183
|
+
error instanceof Error
|
|
184
|
+
? error.message
|
|
185
|
+
: 'The diagram could not be exported.';
|
|
186
|
+
const warning = document.createElement('p');
|
|
187
|
+
warning.setAttribute('data-inkkit-diagram-error', '');
|
|
188
|
+
warning.textContent = `Diagram unavailable: ${result.error}`;
|
|
189
|
+
code.parentElement.before(warning);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Native attachment slots follow DOM order, including generated diagrams.
|
|
193
|
+
const orderedImages = [];
|
|
194
|
+
for (const element of container.querySelectorAll('img')) {
|
|
195
|
+
const diagram = element.closest('[data-inkkit-mermaid]');
|
|
196
|
+
const item = diagram
|
|
197
|
+
? {
|
|
198
|
+
reference: '',
|
|
199
|
+
alt: element.alt,
|
|
200
|
+
image: diagrams.find((entry) => entry.source === diagram.getAttribute('data-inkkit-mermaid')).image,
|
|
201
|
+
}
|
|
202
|
+
: images[Number(element.getAttribute('data-inkkit-image-slot'))];
|
|
203
|
+
element.setAttribute('data-inkkit-image-slot', String(orderedImages.length));
|
|
204
|
+
orderedImages.push(item);
|
|
205
|
+
}
|
|
155
206
|
// Scriptable links and event attributes are not part of semantic rich text.
|
|
156
207
|
for (const element of container.querySelectorAll('*')) {
|
|
157
208
|
for (const attribute of [...element.attributes])
|
|
@@ -165,6 +216,7 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
|
|
|
165
216
|
text: clipboardText(content, missing),
|
|
166
217
|
html: container.innerHTML,
|
|
167
218
|
markdown,
|
|
168
|
-
images,
|
|
219
|
+
images: [...orderedImages, ...images.filter((image) => image.error)],
|
|
220
|
+
diagrams,
|
|
169
221
|
};
|
|
170
222
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Options } from 'mdast-util-to-markdown';
|
|
2
|
+
import type { Ctx } from '@milkdown/kit/ctx';
|
|
3
|
+
import type { Node as ProseNode } from '@milkdown/kit/prose/model';
|
|
4
|
+
export declare const remarkCodeFences: import("@milkdown/kit/utils").$Remark<"codeFences", unknown>;
|
|
5
|
+
export declare const preservingCodeBlocks: import("@milkdown/kit/utils").$NodeSchema<"code_block">;
|
|
6
|
+
export declare const fencedCodeHandler: NonNullable<Options['handlers']>['code'];
|
|
7
|
+
export declare function restoreCodeEndings(ctx: Ctx, doc: ProseNode, markdown: string): string;
|
|
8
|
+
export declare function finalCodeEnding(doc: ProseNode): string | undefined;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { codeBlockSchema } from '@milkdown/kit/preset/commonmark';
|
|
2
|
+
import { $remark } from '@milkdown/kit/utils';
|
|
3
|
+
import { defaultHandlers } from 'mdast-util-to-markdown';
|
|
4
|
+
import { remarkCtx } from '@milkdown/kit/core';
|
|
5
|
+
function readFence(value) {
|
|
6
|
+
if (typeof value !== 'object' || value == null)
|
|
7
|
+
return null;
|
|
8
|
+
const fence = value;
|
|
9
|
+
return typeof fence.marker === 'string' &&
|
|
10
|
+
/^(?:`{3,}|~{3,})$/.test(fence.marker) &&
|
|
11
|
+
fence.marker.length <= 30000 &&
|
|
12
|
+
typeof fence.info === 'string' &&
|
|
13
|
+
!/[\r\n]/.test(fence.info) &&
|
|
14
|
+
typeof fence.close === 'string' &&
|
|
15
|
+
(!fence.close ||
|
|
16
|
+
new RegExp(`^${fence.marker[0]}{${fence.marker.length},}$`).test(fence.close)) &&
|
|
17
|
+
typeof fence.trailing === 'string' &&
|
|
18
|
+
/^[ \t]*$/.test(fence.trailing) &&
|
|
19
|
+
['\n', '\r\n'].includes(fence.ending)
|
|
20
|
+
? fence
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
23
|
+
export const remarkCodeFences = $remark('codeFences', () => () => (tree, file) => {
|
|
24
|
+
const source = String(file.value).replace(/^\uFEFF/, '');
|
|
25
|
+
const visit = (node) => {
|
|
26
|
+
if (node.type === 'code') {
|
|
27
|
+
const raw = source.slice(node.position?.start.offset, node.position?.end.offset);
|
|
28
|
+
const opening = /^(`{3,}|~{3,})([^\r\n]*)(\r?\n)/.exec(raw);
|
|
29
|
+
if (opening) {
|
|
30
|
+
const close = /(?:^|\n)[ \t>]*(`{3,}|~{3,})([ \t]*)\r?$/.exec(raw);
|
|
31
|
+
const matching = close &&
|
|
32
|
+
close[1][0] === opening[1][0] &&
|
|
33
|
+
close[1].length >= opening[1].length;
|
|
34
|
+
node.data = {
|
|
35
|
+
...node.data,
|
|
36
|
+
inkkitFence: {
|
|
37
|
+
marker: opening[1],
|
|
38
|
+
info: opening[2],
|
|
39
|
+
close: matching ? close[1] : '',
|
|
40
|
+
trailing: matching ? close[2] : '',
|
|
41
|
+
ending: opening[3],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if ('children' in node)
|
|
47
|
+
node.children.forEach(visit);
|
|
48
|
+
};
|
|
49
|
+
visit(tree);
|
|
50
|
+
});
|
|
51
|
+
export const preservingCodeBlocks = codeBlockSchema.extendSchema((base) => (ctx) => {
|
|
52
|
+
const schema = base(ctx);
|
|
53
|
+
return {
|
|
54
|
+
...schema,
|
|
55
|
+
attrs: { ...schema.attrs, authoredFence: { default: null } },
|
|
56
|
+
parseDOM: [
|
|
57
|
+
{
|
|
58
|
+
tag: 'pre',
|
|
59
|
+
preserveWhitespace: 'full',
|
|
60
|
+
getAttrs: (dom) => {
|
|
61
|
+
let authoredFence = null;
|
|
62
|
+
try {
|
|
63
|
+
authoredFence = readFence(JSON.parse(dom.getAttribute('data-inkkit-fence') ?? 'null'));
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* Ordinary code stays usable without provenance. */
|
|
67
|
+
}
|
|
68
|
+
return { language: dom.dataset.language ?? '', authoredFence };
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
toDOM: (node) => [
|
|
73
|
+
'pre',
|
|
74
|
+
{
|
|
75
|
+
'data-language': node.attrs.language,
|
|
76
|
+
...(node.attrs.authoredFence
|
|
77
|
+
? { 'data-inkkit-fence': JSON.stringify(node.attrs.authoredFence) }
|
|
78
|
+
: {}),
|
|
79
|
+
},
|
|
80
|
+
['code', 0],
|
|
81
|
+
],
|
|
82
|
+
parseMarkdown: {
|
|
83
|
+
match: (node) => node.type === 'code',
|
|
84
|
+
runner: (state, node, type) => {
|
|
85
|
+
state.openNode(type, {
|
|
86
|
+
language: node.lang ?? '',
|
|
87
|
+
authoredFence: readFence(node.data?.inkkitFence),
|
|
88
|
+
});
|
|
89
|
+
if (node.value)
|
|
90
|
+
state.addText(String(node.value).replace(/\r\n?/g, '\n'));
|
|
91
|
+
state.closeNode();
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
toMarkdown: {
|
|
95
|
+
match: (node) => node.type.name === 'code_block',
|
|
96
|
+
runner: (state, node) => {
|
|
97
|
+
state.addNode('code', undefined, node.textContent, {
|
|
98
|
+
lang: node.attrs.language,
|
|
99
|
+
data: { inkkitFence: node.attrs.authoredFence },
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
export const fencedCodeHandler = (node, parent, state, info) => {
|
|
106
|
+
const fence = readFence(node.data?.inkkitFence);
|
|
107
|
+
if (!fence)
|
|
108
|
+
return defaultHandlers.code(node, parent, state, info);
|
|
109
|
+
const character = fence.marker[0];
|
|
110
|
+
let length = fence.marker.length;
|
|
111
|
+
const body = node.value.replace(/\r\n?/g, '\n');
|
|
112
|
+
for (const line of body.split('\n')) {
|
|
113
|
+
const run = new RegExp(`^ {0,3}(${character}+)[ \\t]*$`).exec(line);
|
|
114
|
+
if (run)
|
|
115
|
+
length = Math.max(length, run[1].length + 1);
|
|
116
|
+
}
|
|
117
|
+
const close = fence.close
|
|
118
|
+
? character.repeat(Math.max(length, fence.close.length)) + fence.trailing
|
|
119
|
+
: '';
|
|
120
|
+
return (character.repeat(length) +
|
|
121
|
+
fence.info +
|
|
122
|
+
fence.ending +
|
|
123
|
+
body.replaceAll('\n', fence.ending) +
|
|
124
|
+
(close ? (body ? fence.ending : '') + close : ''));
|
|
125
|
+
};
|
|
126
|
+
export function restoreCodeEndings(ctx, doc, markdown) {
|
|
127
|
+
const endings = [];
|
|
128
|
+
doc.descendants((node) => {
|
|
129
|
+
if (node.type.name === 'code_block')
|
|
130
|
+
endings.push(readFence(node.attrs.authoredFence)?.ending ?? '\n');
|
|
131
|
+
});
|
|
132
|
+
const patches = [];
|
|
133
|
+
let index = 0;
|
|
134
|
+
const visit = (node) => {
|
|
135
|
+
if (node.type === 'code') {
|
|
136
|
+
const ending = endings[index++];
|
|
137
|
+
const from = node.position?.start.offset, to = node.position?.end.offset;
|
|
138
|
+
if (ending === '\r\n' && from != null && to != null)
|
|
139
|
+
patches.push({
|
|
140
|
+
from,
|
|
141
|
+
to,
|
|
142
|
+
text: markdown
|
|
143
|
+
.slice(from, to)
|
|
144
|
+
.replace(/\r\n?/g, '\n')
|
|
145
|
+
.replaceAll('\n', ending),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if ('children' in node)
|
|
149
|
+
node.children.forEach(visit);
|
|
150
|
+
};
|
|
151
|
+
const processor = ctx.get(remarkCtx);
|
|
152
|
+
visit(processor.runSync(processor.parse(markdown), { value: markdown }));
|
|
153
|
+
for (const patch of patches.reverse())
|
|
154
|
+
markdown =
|
|
155
|
+
markdown.slice(0, patch.from) + patch.text + markdown.slice(patch.to);
|
|
156
|
+
return markdown;
|
|
157
|
+
}
|
|
158
|
+
export function finalCodeEnding(doc) {
|
|
159
|
+
let last = doc.lastChild;
|
|
160
|
+
while (last) {
|
|
161
|
+
if (last.type.name === 'code_block')
|
|
162
|
+
return readFence(last.attrs.authoredFence)?.ending;
|
|
163
|
+
last = last.lastChild;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
package/dist/dialect.js
CHANGED
|
@@ -3,6 +3,7 @@ import { commonmark, hardbreakSchema, imageSchema, paragraphSchema, remarkInline
|
|
|
3
3
|
import { extendListItemSchemaForTask, strikethroughAttr, strikethroughInputRule, strikethroughKeymap, strikethroughSchema, toggleStrikethroughCommand, wrapInTaskListInputRule, tableSchema, tableHeaderRowSchema, tableHeaderSchema, tableRowSchema, tableCellSchema, tableEditingPlugin, tableKeymap, goToNextTableCellCommand, goToPrevTableCellCommand, insertTableCommand, addRowBeforeCommand, addRowAfterCommand, addColBeforeCommand, addColAfterCommand, selectRowCommand, selectColCommand, deleteSelectedCellsCommand, setAlignCommand, exitTable, } from '@milkdown/kit/preset/gfm';
|
|
4
4
|
import { $remark } from '@milkdown/kit/utils';
|
|
5
5
|
import { autolinkInputRule } from './autolink.js';
|
|
6
|
+
import { preservingCodeBlocks, remarkCodeFences, fencedCodeHandler, } from './code-fences.js';
|
|
6
7
|
import { literalBlock, createLiteralPreservation } from './literals.js';
|
|
7
8
|
import { references, remarkReferencesPlugin } from './references.js';
|
|
8
9
|
import { callouts, remarkCalloutsPlugin } from './callouts.js';
|
|
@@ -85,7 +86,7 @@ function remarkDialect() {
|
|
|
85
86
|
add('toMarkdownExtensions', gfmTaskListItemToMarkdown());
|
|
86
87
|
add('toMarkdownExtensions', gfmAutolinkLiteralToMarkdown());
|
|
87
88
|
add('toMarkdownExtensions', gfmTableToMarkdown());
|
|
88
|
-
add('toMarkdownExtensions', { handlers: { link } });
|
|
89
|
+
add('toMarkdownExtensions', { handlers: { link, code: fencedCodeHandler } });
|
|
89
90
|
}
|
|
90
91
|
export const remarkDialectPlugin = $remark('remarkDialect', () => remarkDialect);
|
|
91
92
|
// Reference links retain their authored form instead of expanding to inline links.
|
|
@@ -254,6 +255,7 @@ export const tables = [
|
|
|
254
255
|
].flat();
|
|
255
256
|
export function createDialect(images) {
|
|
256
257
|
return [
|
|
258
|
+
remarkCodeFences,
|
|
257
259
|
remarkReferencesPlugin,
|
|
258
260
|
remarkCalloutsPlugin,
|
|
259
261
|
remarkCommentsPlugin,
|
|
@@ -264,6 +266,7 @@ export function createDialect(images) {
|
|
|
264
266
|
callouts,
|
|
265
267
|
comments,
|
|
266
268
|
inlineHighlight,
|
|
269
|
+
preservingCodeBlocks,
|
|
267
270
|
preserveSpacerParagraphs,
|
|
268
271
|
visibleSoftbreaks,
|
|
269
272
|
normalizedImages,
|
package/dist/editor.js
CHANGED
|
@@ -26,6 +26,7 @@ import { PasteController } from './paste.js';
|
|
|
26
26
|
import { selectionPlugin } from './selection.js';
|
|
27
27
|
import { search, SearchQuery, getSearchState, setSearchState, } from 'prosemirror-search';
|
|
28
28
|
import { taskListPlugin, toggleTaskList } from './tasks.js';
|
|
29
|
+
import { isMermaid, mermaidPreview } from './mermaid.js';
|
|
29
30
|
const shortcutCommands = {
|
|
30
31
|
heading1: ['heading', 1],
|
|
31
32
|
heading2: ['heading', 2],
|
|
@@ -342,9 +343,24 @@ export class InkKitEditor {
|
|
|
342
343
|
: this.plain.value.slice(this.plain.selectionStart, this.plain.selectionEnd);
|
|
343
344
|
const pre = document.createElement('pre');
|
|
344
345
|
pre.textContent = text;
|
|
345
|
-
return {
|
|
346
|
+
return {
|
|
347
|
+
text,
|
|
348
|
+
html: pre.outerHTML,
|
|
349
|
+
markdown: text,
|
|
350
|
+
images: [],
|
|
351
|
+
diagrams: [],
|
|
352
|
+
};
|
|
346
353
|
}
|
|
347
354
|
const { doc, schema, selection } = this.editor.ctx.get(editorViewCtx).state;
|
|
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
|
+
});
|
|
348
364
|
const content = all
|
|
349
365
|
? doc.content
|
|
350
366
|
: commentSelectionContent(doc, selection.from, selection.to, selection.content().content);
|
|
@@ -355,14 +371,16 @@ export class InkKitEditor {
|
|
|
355
371
|
try {
|
|
356
372
|
markdown = all
|
|
357
373
|
? snapshot.text
|
|
358
|
-
:
|
|
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);
|
|
359
377
|
}
|
|
360
378
|
catch (error) {
|
|
361
379
|
throw new InkKitError('preservation', error instanceof Error
|
|
362
380
|
? error.message
|
|
363
381
|
: 'Cannot copy the selected Markdown');
|
|
364
382
|
}
|
|
365
|
-
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);
|
|
366
384
|
const metadata = referenceMetadata(this.editor.ctx, doc, content);
|
|
367
385
|
if (metadata != null)
|
|
368
386
|
result.html = withReferenceMetadata(result.html, metadata);
|
|
@@ -371,6 +389,8 @@ export class InkKitEditor {
|
|
|
371
389
|
throw new InkKitError('stale-document', 'Document changed');
|
|
372
390
|
if (result.images.some((image) => image.error))
|
|
373
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'));
|
|
374
394
|
return result;
|
|
375
395
|
}
|
|
376
396
|
async paste(input) {
|
|
@@ -527,6 +547,7 @@ export class InkKitEditor {
|
|
|
527
547
|
.use(codeCopyPlugin((text) => events.copy(text)))
|
|
528
548
|
.use(placeholderPlugin)
|
|
529
549
|
.use(highlightPlugin)
|
|
550
|
+
.use(mermaidPreview((error) => events.error?.(error)))
|
|
530
551
|
.use(selectionPlugin)
|
|
531
552
|
.use($prose(() => search()))
|
|
532
553
|
.use(options.images ? imageView(options.images) : [])
|
|
@@ -595,7 +616,7 @@ export class InkKitEditor {
|
|
|
595
616
|
const expanded = selectionContent(view.state.doc, fragment);
|
|
596
617
|
let hasImages = false;
|
|
597
618
|
expanded.descendants((node) => {
|
|
598
|
-
if (node.type.name === 'image')
|
|
619
|
+
if (node.type.name === 'image' || isMermaid(node))
|
|
599
620
|
hasImages = true;
|
|
600
621
|
});
|
|
601
622
|
if (!hasImages) {
|
|
@@ -633,8 +654,10 @@ export class InkKitEditor {
|
|
|
633
654
|
void instance
|
|
634
655
|
.clipboardSnapshot(false)
|
|
635
656
|
.then(async (content) => {
|
|
636
|
-
if (cut &&
|
|
637
|
-
|
|
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');
|
|
638
661
|
await events.clipboard(content);
|
|
639
662
|
if (cut) {
|
|
640
663
|
instance.assertCurrent();
|
|
@@ -651,7 +674,7 @@ export class InkKitEditor {
|
|
|
651
674
|
container.textContent = clipboardText(expanded);
|
|
652
675
|
event.clipboardData?.setData('text/plain', container.textContent ?? '');
|
|
653
676
|
event.clipboardData?.setData('text/html', container.outerHTML);
|
|
654
|
-
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'));
|
|
655
678
|
}
|
|
656
679
|
};
|
|
657
680
|
root.addEventListener('copy', instance.copyHandler, true);
|
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,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.
|
|
@@ -301,8 +318,7 @@ export class PasteController {
|
|
|
301
318
|
const slot = /^inkkit-clipboard-image:(\d+)$/.exec(String(node.attrs.src));
|
|
302
319
|
const position = slot ? Number(slot[1]) : imageIndex++;
|
|
303
320
|
const element = elements.some((element) => element.hasAttribute('data-inkkit-image-slot'))
|
|
304
|
-
? elements.find((element) => Number(element.getAttribute('data-inkkit-image-slot')) ===
|
|
305
|
-
position)
|
|
321
|
+
? elements.find((element) => Number(element.getAttribute('data-inkkit-authored-image-slot') ?? element.getAttribute('data-inkkit-image-slot')) === position)
|
|
306
322
|
: elements[position];
|
|
307
323
|
if (!element || !container.contains(element))
|
|
308
324
|
return schema.text(`[${node.attrs.alt || 'Image'}: image unavailable]`);
|
package/dist/preserve.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parserCtx, remarkCtx } from '@milkdown/kit/core';
|
|
2
2
|
import { serialize } from './dialect.js';
|
|
3
|
+
import { restoreCodeEndings, finalCodeEnding } from './code-fences.js';
|
|
3
4
|
function signature(node) {
|
|
4
5
|
return JSON.stringify(node, (key, value) => ['position', 'data', 'url', 'title', 'marker'].includes(key)
|
|
5
6
|
? undefined
|
|
@@ -115,6 +116,40 @@ function retainSource(before, after, source, canonical, lineEnding) {
|
|
|
115
116
|
if (start == null || end == null || newStart == null || newEnd == null)
|
|
116
117
|
throw new PreservationError('A source token has no location');
|
|
117
118
|
const raw = source.slice(start, end);
|
|
119
|
+
if (before.type === 'code' &&
|
|
120
|
+
after.type === 'code' &&
|
|
121
|
+
before.lang === after.lang) {
|
|
122
|
+
const opening = /^([ \t>]*)(`{3,}|~{3,})([^\r\n]*)(\r?\n)/.exec(raw);
|
|
123
|
+
if (opening) {
|
|
124
|
+
const lines = raw.slice(opening[0].length).split(/\r?\n/);
|
|
125
|
+
const closing = /^([ \t>]*)(`{3,}|~{3,})([ \t]*)$/.exec(lines.at(-1) ?? '');
|
|
126
|
+
const originalLine = (before.value ?? '').replace(/\r\n?/g, '\n').split('\n')[0] ?? '';
|
|
127
|
+
const first = lines[0] ?? '';
|
|
128
|
+
const gutter = first.endsWith(originalLine)
|
|
129
|
+
? first.slice(0, first.length - originalLine.length)
|
|
130
|
+
: opening[1];
|
|
131
|
+
const body = (after.value ?? '').replace(/\r\n?/g, '\n').split('\n');
|
|
132
|
+
const character = opening[2][0];
|
|
133
|
+
let length = opening[2].length;
|
|
134
|
+
for (const line of body) {
|
|
135
|
+
const run = new RegExp(`^ {0,3}(${character}+)[ \\t]*$`).exec(line);
|
|
136
|
+
if (run)
|
|
137
|
+
length = Math.max(length, run[1].length + 1);
|
|
138
|
+
}
|
|
139
|
+
const fence = character.repeat(length);
|
|
140
|
+
return (opening[1] +
|
|
141
|
+
fence +
|
|
142
|
+
opening[3] +
|
|
143
|
+
lineEnding +
|
|
144
|
+
body.map((line) => gutter + line).join(lineEnding) +
|
|
145
|
+
(closing
|
|
146
|
+
? lineEnding +
|
|
147
|
+
closing[1] +
|
|
148
|
+
character.repeat(Math.max(length, closing[2].length)) +
|
|
149
|
+
closing[3]
|
|
150
|
+
: ''));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
118
153
|
if (before.type === 'definition' &&
|
|
119
154
|
after.type === 'definition' &&
|
|
120
155
|
before.label === after.label &&
|
|
@@ -193,7 +228,7 @@ export class PreservationError extends Error {
|
|
|
193
228
|
}
|
|
194
229
|
}
|
|
195
230
|
function form(text) {
|
|
196
|
-
return text.replace(/\n+$/, '');
|
|
231
|
+
return text.replace(/\r\n?/g, '\n').replace(/\n+$/, '');
|
|
197
232
|
}
|
|
198
233
|
function blockForm(ctx, node) {
|
|
199
234
|
// An empty document is empty Markdown, but an empty block inside a document
|
|
@@ -219,7 +254,7 @@ function semanticSignature(doc) {
|
|
|
219
254
|
continue;
|
|
220
255
|
const token = {
|
|
221
256
|
...child,
|
|
222
|
-
text,
|
|
257
|
+
text: text.replace(/\r\n?/g, '\n'),
|
|
223
258
|
marks: index === 1 ? child.marks : undefined,
|
|
224
259
|
};
|
|
225
260
|
const previous = tokens.at(-1);
|
|
@@ -242,6 +277,7 @@ function semanticSignature(doc) {
|
|
|
242
277
|
'id',
|
|
243
278
|
'label',
|
|
244
279
|
'spread',
|
|
280
|
+
'authoredFence',
|
|
245
281
|
].includes(key)
|
|
246
282
|
? undefined
|
|
247
283
|
: value));
|
|
@@ -398,16 +434,17 @@ export class Preservation {
|
|
|
398
434
|
output += retainSource(block.ast, positionalNode(currentRawTree, currentTree.children[index]), body, canonical, this.lineEnding);
|
|
399
435
|
}
|
|
400
436
|
else
|
|
401
|
-
output += text.replaceAll('\n', this.lineEnding);
|
|
437
|
+
output += restoreCodeEndings(this.ctx, doc.type.create(null, [doc.child(index)]), text.replaceAll('\n', this.lineEnding));
|
|
402
438
|
previous = position;
|
|
403
439
|
});
|
|
404
440
|
if (previous === this.blocks.length - 1 && this.blocks.length)
|
|
405
441
|
output += this.source.slice(this.blocks[previous].end);
|
|
406
442
|
else if (output)
|
|
407
|
-
output += this.lineEnding;
|
|
443
|
+
output += finalCodeEnding(doc) ?? this.lineEnding;
|
|
408
444
|
const parse = this.ctx.get(parserCtx);
|
|
409
445
|
const reopened = parse(output);
|
|
410
|
-
if (serialize(this.ctx, reopened) ===
|
|
446
|
+
if (serialize(this.ctx, reopened).replace(/\r\n?/g, '\n') ===
|
|
447
|
+
canonical.replace(/\r\n?/g, '\n') &&
|
|
411
448
|
semanticSignature(reopened) === semanticSignature(doc))
|
|
412
449
|
return output;
|
|
413
450
|
throw new PreservationError('The edited Markdown cannot be reopened without changing its content');
|
package/dist/style.css
CHANGED
|
@@ -652,3 +652,19 @@ body {
|
|
|
652
652
|
display: block !important;
|
|
653
653
|
}
|
|
654
654
|
}
|
|
655
|
+
/* Diagram previews are presentation; the authored code remains editable. */
|
|
656
|
+
.inkkit-mermaid-preview {
|
|
657
|
+
padding: 16px;
|
|
658
|
+
overflow: auto;
|
|
659
|
+
background: #fff;
|
|
660
|
+
color: #222;
|
|
661
|
+
}
|
|
662
|
+
.inkkit-mermaid-preview svg {
|
|
663
|
+
display: block;
|
|
664
|
+
max-width: 100%;
|
|
665
|
+
height: auto;
|
|
666
|
+
}
|
|
667
|
+
.inkkit-mermaid-preview[data-state='error'] {
|
|
668
|
+
color: #9b2424;
|
|
669
|
+
font-size: 0.9em;
|
|
670
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface DocumentSnapshot extends DocumentInput {
|
|
|
14
14
|
revision: number;
|
|
15
15
|
dirty: boolean;
|
|
16
16
|
}
|
|
17
|
-
export type EditorErrorCode = 'not-ready' | 'stale-document' | 'composition' | 'operation-pending' | 'destroyed' | 'preservation' | 'image-unavailable';
|
|
17
|
+
export type EditorErrorCode = 'not-ready' | 'stale-document' | 'composition' | 'operation-pending' | 'destroyed' | 'preservation' | 'image-unavailable' | 'diagram-unavailable';
|
|
18
18
|
export declare class InkKitError extends Error {
|
|
19
19
|
readonly code: EditorErrorCode;
|
|
20
20
|
constructor(code: EditorErrorCode, message: string);
|
|
@@ -57,4 +57,10 @@ export interface ClipboardOutput {
|
|
|
57
57
|
html: string;
|
|
58
58
|
markdown: string;
|
|
59
59
|
images: ClipboardImage[];
|
|
60
|
+
diagrams?: ClipboardDiagram[];
|
|
61
|
+
}
|
|
62
|
+
export interface ClipboardDiagram {
|
|
63
|
+
source: string;
|
|
64
|
+
image?: PortableImage;
|
|
65
|
+
error?: string;
|
|
60
66
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aicayzer/inkkit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "A preserving Markdown editor for offline web views and the web",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@milkdown/kit": "7.22.1",
|
|
49
|
+
"dompurify": "3.4.16",
|
|
49
50
|
"lowlight": "^3.3.0",
|
|
50
51
|
"mdast-util-from-markdown": "^2.1.0",
|
|
51
52
|
"mdast-util-gfm-autolink-literal": "^2.0.1",
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
"mdast-util-gfm-task-list-item": "^2.0.0",
|
|
56
57
|
"mdast-util-highlight-mark": "1.2.2",
|
|
57
58
|
"mdast-util-to-markdown": "2.2.0",
|
|
59
|
+
"mermaid": "12.1.0",
|
|
58
60
|
"micromark-extension-gfm-autolink-literal": "^2.1.0",
|
|
59
61
|
"micromark-extension-gfm-footnote": "^2.1.0",
|
|
60
62
|
"micromark-extension-gfm-strikethrough": "^2.1.0",
|