@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/README.md
CHANGED
|
@@ -33,10 +33,12 @@ editor.loadDocument({
|
|
|
33
33
|
const snapshot = editor.snapshot(1)
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Markdown includes CommonMark, task lists, strikethrough, autolinks, GFM tables, editable footnotes and reference-style links, bounded callouts, highlights and
|
|
36
|
+
Markdown includes CommonMark, task lists, strikethrough, autolinks, GFM tables, editable footnotes and reference-style links, bounded callouts, highlights, author comments and offline Mermaid diagrams. Unsupported constructs remain literal Markdown. TXT mode treats all text literally. Images require a host adapter. RTF files remain outside the package scope.
|
|
37
|
+
|
|
38
|
+
`await editor.printableSnapshot(1)` returns a complete printable HTML document, print styles and portable image bytes. It excludes author comments, includes folded content and reports unavailable assets or a changed document. Client apps own Print/PDF commands, dialogs and file generation.
|
|
37
39
|
|
|
38
40
|
See [supported syntax](docs/supported-syntax.md), [the host API](docs/api.md), [preservation and clipboard behavior](docs/preservation.md), [development](docs/development.md), and [releases](docs/releasing.md).
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
Delivery scope and release status are tracked in the [InkKit Project](https://github.com/users/aicayzer/projects/3) and [milestones](https://github.com/aicayzer/inkkit/milestones).
|
|
41
43
|
|
|
42
44
|
MIT licensed. InkKit builds on the public PadPad and Memos editors; see [NOTICE](NOTICE).
|
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.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type TableCommand } from './tables.js';
|
|
2
|
-
import { type DocumentInput, type DocumentSnapshot, type ImageAdapter, type ClipboardInput, type ClipboardOutput } from './types.js';
|
|
2
|
+
import { type DocumentInput, type DocumentSnapshot, type ImageAdapter, type ClipboardInput, type ClipboardOutput, type PrintableDocument } from './types.js';
|
|
3
3
|
export type Mark = 'bold' | 'italic' | 'strikethrough' | 'highlight' | 'code' | 'link';
|
|
4
4
|
export type Block = {
|
|
5
5
|
type: 'paragraph';
|
|
@@ -65,6 +65,7 @@ export declare class InkKitEditor {
|
|
|
65
65
|
private assertCurrent;
|
|
66
66
|
private plainSource;
|
|
67
67
|
snapshot(expectedGeneration?: number): DocumentSnapshot;
|
|
68
|
+
printableSnapshot(expectedGeneration?: number): Promise<PrintableDocument>;
|
|
68
69
|
clipboardSnapshot(all?: boolean): Promise<ClipboardOutput>;
|
|
69
70
|
paste(input: ClipboardInput): Promise<void>;
|
|
70
71
|
setCommentsVisible(visible: boolean): void;
|
package/dist/editor.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Editor, defaultValueCtx, editorViewCtx, editorViewOptionsCtx, remarkStringifyOptionsCtx, rootCtx, } from '@milkdown/kit/core';
|
|
1
|
+
import { Editor, defaultValueCtx, editorViewCtx, editorViewOptionsCtx, remarkCtx, remarkStringifyOptionsCtx, rootCtx, } from '@milkdown/kit/core';
|
|
2
2
|
import { visibleClipboard, portableClipboard, clipboardText, clipboardContent, } from './clipboard.js';
|
|
3
3
|
import { clipboard } from '@milkdown/kit/plugin/clipboard';
|
|
4
4
|
import { history } from '@milkdown/kit/plugin/history';
|
|
@@ -26,6 +26,8 @@ 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';
|
|
30
|
+
import { hasAuthoredImagesInLiterals, printableMarkdown, printableText, } from './print.js';
|
|
29
31
|
const shortcutCommands = {
|
|
30
32
|
heading1: ['heading', 1],
|
|
31
33
|
heading2: ['heading', 2],
|
|
@@ -333,6 +335,42 @@ export class InkKitEditor {
|
|
|
333
335
|
dirty: text !== this.originalSource,
|
|
334
336
|
};
|
|
335
337
|
}
|
|
338
|
+
async printableSnapshot(expectedGeneration) {
|
|
339
|
+
const snapshot = this.snapshot(expectedGeneration);
|
|
340
|
+
const epoch = this.documentEpoch;
|
|
341
|
+
const state = this.editor.ctx.get(editorViewCtx).state;
|
|
342
|
+
let output;
|
|
343
|
+
try {
|
|
344
|
+
if (snapshot.format === 'md' &&
|
|
345
|
+
!this.options.images &&
|
|
346
|
+
hasAuthoredImagesInLiterals(state.doc.content, (source) => this.editor.ctx.get(remarkCtx).parse(source)))
|
|
347
|
+
throw new InkKitError('image-unavailable', 'No image adapter is configured for the authored images.');
|
|
348
|
+
output =
|
|
349
|
+
snapshot.format === 'txt'
|
|
350
|
+
? printableText(snapshot.text)
|
|
351
|
+
: await printableMarkdown(state.doc.content, state.schema, this.options.images, this.context());
|
|
352
|
+
}
|
|
353
|
+
finally {
|
|
354
|
+
this.assertCurrent(snapshot.generation);
|
|
355
|
+
if (snapshot.documentId !== this.documentId ||
|
|
356
|
+
epoch !== this.documentEpoch ||
|
|
357
|
+
snapshot.revision !== this.revision)
|
|
358
|
+
throw new InkKitError('stale-document', 'Document changed while preparing printable content');
|
|
359
|
+
}
|
|
360
|
+
return Object.freeze({
|
|
361
|
+
documentId: snapshot.documentId,
|
|
362
|
+
generation: snapshot.generation,
|
|
363
|
+
revision: snapshot.revision,
|
|
364
|
+
format: snapshot.format,
|
|
365
|
+
html: output.html,
|
|
366
|
+
styles: output.styles,
|
|
367
|
+
assets: Object.freeze(output.assets.map((asset) => Object.freeze({
|
|
368
|
+
...asset,
|
|
369
|
+
bytes: new Uint8Array(asset.bytes),
|
|
370
|
+
}))),
|
|
371
|
+
warnings: Object.freeze(output.warnings.map((warning) => Object.freeze({ ...warning }))),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
336
374
|
async clipboardSnapshot(all = true) {
|
|
337
375
|
const snapshot = this.snapshot();
|
|
338
376
|
const epoch = this.documentEpoch;
|
|
@@ -342,9 +380,24 @@ export class InkKitEditor {
|
|
|
342
380
|
: this.plain.value.slice(this.plain.selectionStart, this.plain.selectionEnd);
|
|
343
381
|
const pre = document.createElement('pre');
|
|
344
382
|
pre.textContent = text;
|
|
345
|
-
return {
|
|
383
|
+
return {
|
|
384
|
+
text,
|
|
385
|
+
html: pre.outerHTML,
|
|
386
|
+
markdown: text,
|
|
387
|
+
images: [],
|
|
388
|
+
diagrams: [],
|
|
389
|
+
};
|
|
346
390
|
}
|
|
347
391
|
const { doc, schema, selection } = this.editor.ctx.get(editorViewCtx).state;
|
|
392
|
+
const completeDiagrams = new Set();
|
|
393
|
+
doc.descendants((node, pos) => {
|
|
394
|
+
if (isMermaid(node) &&
|
|
395
|
+
(all ||
|
|
396
|
+
(selection.from <= pos + 1 &&
|
|
397
|
+
selection.to >= pos + node.nodeSize - 1))) {
|
|
398
|
+
completeDiagrams.add(node.textContent.replace(/\r\n?/g, '\n'));
|
|
399
|
+
}
|
|
400
|
+
});
|
|
348
401
|
const content = all
|
|
349
402
|
? doc.content
|
|
350
403
|
: commentSelectionContent(doc, selection.from, selection.to, selection.content().content);
|
|
@@ -355,14 +408,16 @@ export class InkKitEditor {
|
|
|
355
408
|
try {
|
|
356
409
|
markdown = all
|
|
357
410
|
? snapshot.text
|
|
358
|
-
:
|
|
411
|
+
: completeDiagrams.size
|
|
412
|
+
? new Preservation(this.editor.ctx, snapshot.text).serialize(doc.type.create(null, selectionContent(doc, valid)))
|
|
413
|
+
: selectionMarkdown(this.editor.ctx, doc, valid);
|
|
359
414
|
}
|
|
360
415
|
catch (error) {
|
|
361
416
|
throw new InkKitError('preservation', error instanceof Error
|
|
362
417
|
? error.message
|
|
363
418
|
: 'Cannot copy the selected Markdown');
|
|
364
419
|
}
|
|
365
|
-
const result = await portableClipboard(all ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context());
|
|
420
|
+
const result = await portableClipboard(all ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context(), completeDiagrams);
|
|
366
421
|
const metadata = referenceMetadata(this.editor.ctx, doc, content);
|
|
367
422
|
if (metadata != null)
|
|
368
423
|
result.html = withReferenceMetadata(result.html, metadata);
|
|
@@ -371,6 +426,8 @@ export class InkKitEditor {
|
|
|
371
426
|
throw new InkKitError('stale-document', 'Document changed');
|
|
372
427
|
if (result.images.some((image) => image.error))
|
|
373
428
|
this.events.error?.(new InkKitError('image-unavailable', 'Some copied images were unavailable; their descriptions were retained'));
|
|
429
|
+
if (result.diagrams?.some((diagram) => diagram.error))
|
|
430
|
+
this.events.error?.(new InkKitError('diagram-unavailable', 'Some diagrams could not be exported; their source was retained'));
|
|
374
431
|
return result;
|
|
375
432
|
}
|
|
376
433
|
async paste(input) {
|
|
@@ -527,6 +584,7 @@ export class InkKitEditor {
|
|
|
527
584
|
.use(codeCopyPlugin((text) => events.copy(text)))
|
|
528
585
|
.use(placeholderPlugin)
|
|
529
586
|
.use(highlightPlugin)
|
|
587
|
+
.use(mermaidPreview((error) => events.error?.(error)))
|
|
530
588
|
.use(selectionPlugin)
|
|
531
589
|
.use($prose(() => search()))
|
|
532
590
|
.use(options.images ? imageView(options.images) : [])
|
|
@@ -595,7 +653,7 @@ export class InkKitEditor {
|
|
|
595
653
|
const expanded = selectionContent(view.state.doc, fragment);
|
|
596
654
|
let hasImages = false;
|
|
597
655
|
expanded.descendants((node) => {
|
|
598
|
-
if (node.type.name === 'image')
|
|
656
|
+
if (node.type.name === 'image' || isMermaid(node))
|
|
599
657
|
hasImages = true;
|
|
600
658
|
});
|
|
601
659
|
if (!hasImages) {
|
|
@@ -633,8 +691,10 @@ export class InkKitEditor {
|
|
|
633
691
|
void instance
|
|
634
692
|
.clipboardSnapshot(false)
|
|
635
693
|
.then(async (content) => {
|
|
636
|
-
if (cut &&
|
|
637
|
-
|
|
694
|
+
if (cut &&
|
|
695
|
+
(content.images.some((image) => image.error) ||
|
|
696
|
+
content.diagrams?.some((diagram) => diagram.error)))
|
|
697
|
+
throw new InkKitError('image-unavailable', 'The selected content could not be cut safely');
|
|
638
698
|
await events.clipboard(content);
|
|
639
699
|
if (cut) {
|
|
640
700
|
instance.assertCurrent();
|
|
@@ -651,7 +711,7 @@ export class InkKitEditor {
|
|
|
651
711
|
container.textContent = clipboardText(expanded);
|
|
652
712
|
event.clipboardData?.setData('text/plain', container.textContent ?? '');
|
|
653
713
|
event.clipboardData?.setData('text/html', container.outerHTML);
|
|
654
|
-
events.error?.(new InkKitError('image-unavailable', 'The host must provide a clipboard handler
|
|
714
|
+
events.error?.(new InkKitError('image-unavailable', 'The host must provide a clipboard handler for portable images and diagrams'));
|
|
655
715
|
}
|
|
656
716
|
};
|
|
657
717
|
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, PrintableDocument, PrintableWarning, } from './types.js';
|
package/dist/literals.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ declare module 'mdast' {
|
|
|
12
12
|
inkkitLiteral: InkKitLiteral;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
export declare function sourceFrontmatter(source: string): string | undefined;
|
|
15
16
|
export declare function createLiteralPreservation(images: boolean): import("@milkdown/kit/utils").$Remark<"preserveLiterals", unknown>;
|
|
16
17
|
export declare const preserveLiterals: import("@milkdown/kit/utils").$Remark<"preserveLiterals", unknown>;
|
|
17
18
|
export declare const literalBlock: import("@milkdown/kit/utils").$Node;
|
package/dist/literals.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { $node, $remark } from '@milkdown/kit/utils';
|
|
2
2
|
import { commentSource, literalCommentSpans, } from './comments.js';
|
|
3
3
|
const unsupported = new Set(['html', 'image', 'imageReference']);
|
|
4
|
+
export function sourceFrontmatter(source) {
|
|
5
|
+
return /^(---|\+\+\+)\r?\n[\s\S]*?\r?\n\1(?:\r?\n|$)/.exec(source)?.[0];
|
|
6
|
+
}
|
|
4
7
|
function restoreInlineBreaks(node) {
|
|
5
8
|
if ((node.type === 'paragraph' || node.type === 'heading') &&
|
|
6
9
|
node.children.length > 1) {
|
|
@@ -51,8 +54,8 @@ export function createLiteralPreservation(images) {
|
|
|
51
54
|
restoreInlineBreaks(tree);
|
|
52
55
|
// Micromark's positions exclude an initial byte-order mark.
|
|
53
56
|
const source = String(file.value).replace(/^\uFEFF/, '');
|
|
54
|
-
const frontmatter =
|
|
55
|
-
const frontmatterEnd = frontmatter?.
|
|
57
|
+
const frontmatter = sourceFrontmatter(source);
|
|
58
|
+
const frontmatterEnd = frontmatter?.length ?? 0;
|
|
56
59
|
const preserveBlock = (node, depth = 0) => {
|
|
57
60
|
if (node.type === 'footnoteDefinition') {
|
|
58
61
|
node.children = node.children.map((child) => preserveBlock(child, depth + 1));
|
|
@@ -80,13 +83,13 @@ export function createLiteralPreservation(images) {
|
|
|
80
83
|
if (frontmatter)
|
|
81
84
|
blocks.push({
|
|
82
85
|
type: 'inkkitLiteral',
|
|
83
|
-
value: frontmatter
|
|
86
|
+
value: frontmatter.trimEnd(),
|
|
84
87
|
position: {
|
|
85
88
|
start: { line: 1, column: 1, offset: 0 },
|
|
86
89
|
end: {
|
|
87
90
|
line: 1,
|
|
88
91
|
column: 1,
|
|
89
|
-
offset: frontmatter
|
|
92
|
+
offset: frontmatter.trimEnd().length,
|
|
90
93
|
},
|
|
91
94
|
},
|
|
92
95
|
});
|
|
@@ -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;
|