@aicayzer/inkkit 0.0.3 → 0.0.5
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 +4 -2
- 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.d.ts +2 -1
- package/dist/editor.js +68 -8
- package/dist/index.d.ts +1 -1
- package/dist/literals.d.ts +1 -0
- package/dist/literals.js +7 -4
- 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/print.d.ts +7 -0
- package/dist/print.js +377 -0
- package/dist/style.css +16 -0
- package/dist/types.d.ts +21 -1
- package/package.json +3 -1
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/print.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Fragment, Schema } from '@milkdown/kit/prose/model';
|
|
2
|
+
import { type DocumentContext, type ImageAdapter, type PrintableDocument } from './types.js';
|
|
3
|
+
type PrintableContent = Pick<PrintableDocument, 'html' | 'styles' | 'assets' | 'warnings'>;
|
|
4
|
+
export declare function printableText(text: string): PrintableContent;
|
|
5
|
+
export declare function hasAuthoredImagesInLiterals(content: Fragment, parse: (source: string) => unknown): boolean;
|
|
6
|
+
export declare function printableMarkdown(content: Fragment, schema: Schema, adapter: ImageAdapter | undefined, context: DocumentContext): Promise<PrintableContent>;
|
|
7
|
+
export {};
|