@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/README.md
CHANGED
|
@@ -33,10 +33,10 @@ 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. Unsupported constructs remain literal Markdown. TXT mode treats all text literally. Images require a host adapter. RTF files remain outside the package scope.
|
|
36
|
+
Markdown includes CommonMark, task lists, strikethrough, autolinks, GFM tables, editable footnotes and reference-style links, bounded callouts, highlights and author comments. Unsupported constructs remain literal Markdown. TXT mode treats all text literally. Images require a host adapter. RTF files remain outside the package scope.
|
|
37
37
|
|
|
38
|
-
See [the host API](docs/api.md), [preservation and clipboard behavior](docs/preservation.md), [development](docs/development.md), and [releases](docs/releasing.md).
|
|
38
|
+
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
39
|
|
|
40
|
-
Planned work and release scope are tracked in the [InkKit Project](https://github.com/users/aicayzer/projects/3) and [milestones](https://github.com/aicayzer/inkkit/milestones).
|
|
40
|
+
Planned work and release scope are tracked in the [InkKit Project](https://github.com/users/aicayzer/projects/3) and [milestones](https://github.com/aicayzer/inkkit/milestones). Release 0.0.2 is published and verified; 0.0.3 delivery is authorised. Targets from 0.0.4 remain provisional.
|
|
41
41
|
|
|
42
42
|
MIT licensed. InkKit builds on the public PadPad and Memos editors; see [NOTICE](NOTICE).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Parent, RootContent } from 'mdast';
|
|
2
|
+
interface Callout extends Parent {
|
|
3
|
+
type: 'inkkitCallout';
|
|
4
|
+
kind: string;
|
|
5
|
+
marker: string;
|
|
6
|
+
fold: string;
|
|
7
|
+
title: string;
|
|
8
|
+
titleSource: string;
|
|
9
|
+
listPlaceholder: boolean;
|
|
10
|
+
children: RootContent[];
|
|
11
|
+
}
|
|
12
|
+
declare module 'mdast' {
|
|
13
|
+
interface BlockContentMap {
|
|
14
|
+
inkkitCallout: Callout;
|
|
15
|
+
}
|
|
16
|
+
interface RootContentMap {
|
|
17
|
+
inkkitCallout: Callout;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export declare const remarkCalloutsPlugin: import("@milkdown/kit/utils").$Remark<"inkkitCallouts", unknown>;
|
|
21
|
+
export declare const calloutSchema: import("@milkdown/kit/utils").$Node;
|
|
22
|
+
export declare const calloutView: import("@milkdown/kit/utils").$Prose;
|
|
23
|
+
export declare const callouts: (import("@milkdown/kit/utils").$Node | import("@milkdown/kit/utils").$Prose)[];
|
|
24
|
+
export {};
|
package/dist/callouts.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { $node, $prose, $remark } from '@milkdown/kit/utils';
|
|
2
|
+
import { remarkCtx } from '@milkdown/kit/core';
|
|
3
|
+
import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
|
|
4
|
+
import { defaultHandlers } from 'mdast-util-to-markdown';
|
|
5
|
+
const kinds = new Set(['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION']);
|
|
6
|
+
function plainHeader(processor, source) {
|
|
7
|
+
const node = processor.parse(source).children[0];
|
|
8
|
+
return node?.type === 'paragraph' &&
|
|
9
|
+
node.children.length === 1 &&
|
|
10
|
+
node.children[0]?.type === 'text'
|
|
11
|
+
? node.children[0].value
|
|
12
|
+
: undefined;
|
|
13
|
+
}
|
|
14
|
+
const calloutHandler = (untyped, parent, state, info) => {
|
|
15
|
+
const node = untyped;
|
|
16
|
+
const body = defaultHandlers.blockquote({ type: 'blockquote', children: node.children }, parent, state, info);
|
|
17
|
+
const title = node.titleSource ?? node.title;
|
|
18
|
+
return `> ${node.marker}${node.fold}${title ? ` ${title}` : ''}\n${body}`;
|
|
19
|
+
};
|
|
20
|
+
function remarkCallouts() {
|
|
21
|
+
const processor = this;
|
|
22
|
+
const data = this.data();
|
|
23
|
+
(data.toMarkdownExtensions ??= []).push({
|
|
24
|
+
handlers: { inkkitCallout: calloutHandler },
|
|
25
|
+
});
|
|
26
|
+
return (tree, file) => {
|
|
27
|
+
const source = String(file.value).replace(/^\uFEFF/, '');
|
|
28
|
+
const visit = (node, parent, index) => {
|
|
29
|
+
if (node.type === 'blockquote') {
|
|
30
|
+
const start = node.position?.start.offset, end = node.position?.end.offset;
|
|
31
|
+
const raw = start == null || end == null ? '' : source.slice(start, end);
|
|
32
|
+
const header = /^>[ \t]*([\s\S]*?)(?:\r?\n|$)/.exec(raw)?.[1] ?? '';
|
|
33
|
+
if (header.startsWith('[!')) {
|
|
34
|
+
const match = /^(\[!([A-Za-z]+)\])([+-]?)(?:[ \t]+([^\r\n]*))?$/.exec(header);
|
|
35
|
+
const first = node.children[0];
|
|
36
|
+
const decodedHeader = plainHeader(processor, header);
|
|
37
|
+
// A bounded header occupies one unformatted line; other variants stay source.
|
|
38
|
+
if (!match ||
|
|
39
|
+
decodedHeader == null ||
|
|
40
|
+
!kinds.has(match[2].toUpperCase()) ||
|
|
41
|
+
/^[+-](?:[ \t]|[+-])/.test(match[4] ?? '') ||
|
|
42
|
+
first?.type !== 'paragraph' ||
|
|
43
|
+
first.children[0]?.type !== 'text')
|
|
44
|
+
return {
|
|
45
|
+
type: 'inkkitLiteral',
|
|
46
|
+
value: raw,
|
|
47
|
+
position: node.position,
|
|
48
|
+
};
|
|
49
|
+
const text = first.children[0];
|
|
50
|
+
// Entities can decode to newlines without ending the authored header line.
|
|
51
|
+
const ending = /^\r?\n/.exec(text.value.slice(decodedHeader.length));
|
|
52
|
+
const lineEnd = ending ? decodedHeader.length : -1;
|
|
53
|
+
if (lineEnd < 0 && first.children.length > 1)
|
|
54
|
+
return {
|
|
55
|
+
type: 'inkkitLiteral',
|
|
56
|
+
value: raw,
|
|
57
|
+
position: node.position,
|
|
58
|
+
};
|
|
59
|
+
const children = [...node.children];
|
|
60
|
+
if (lineEnd >= 0) {
|
|
61
|
+
const remaining = text.value.slice(lineEnd + ending[0].length);
|
|
62
|
+
const firstStart = text.position?.start.offset;
|
|
63
|
+
const token = firstStart == null
|
|
64
|
+
? ''
|
|
65
|
+
: source.slice(firstStart, text.position?.end.offset);
|
|
66
|
+
const boundary = /\r?\n[ \t]*>[ \t]?/.exec(token);
|
|
67
|
+
const bodyStart = firstStart == null || !boundary
|
|
68
|
+
? undefined
|
|
69
|
+
: firstStart + boundary.index + boundary[0].length;
|
|
70
|
+
const point = bodyStart == null
|
|
71
|
+
? undefined
|
|
72
|
+
: {
|
|
73
|
+
line: source.slice(0, bodyStart).split('\n').length,
|
|
74
|
+
column: bodyStart - source.lastIndexOf('\n', bodyStart - 1),
|
|
75
|
+
offset: bodyStart,
|
|
76
|
+
};
|
|
77
|
+
children[0] = {
|
|
78
|
+
...first,
|
|
79
|
+
children: [
|
|
80
|
+
...(remaining
|
|
81
|
+
? [
|
|
82
|
+
{
|
|
83
|
+
...text,
|
|
84
|
+
value: remaining,
|
|
85
|
+
position: point && text.position
|
|
86
|
+
? { start: point, end: text.position.end }
|
|
87
|
+
: undefined,
|
|
88
|
+
},
|
|
89
|
+
]
|
|
90
|
+
: []),
|
|
91
|
+
...first.children.slice(1),
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
else
|
|
96
|
+
children.shift();
|
|
97
|
+
if (!children.length)
|
|
98
|
+
children.push({ type: 'paragraph', children: [] });
|
|
99
|
+
return {
|
|
100
|
+
type: 'inkkitCallout',
|
|
101
|
+
kind: match[2].toUpperCase(),
|
|
102
|
+
marker: match[1],
|
|
103
|
+
fold: match[3],
|
|
104
|
+
title: decodedHeader
|
|
105
|
+
.slice(header.length - (match[4]?.length ?? 0))
|
|
106
|
+
.trimEnd(),
|
|
107
|
+
titleSource: match[4]?.trimEnd() ?? '',
|
|
108
|
+
listPlaceholder: parent?.type === 'listItem' && index === 0,
|
|
109
|
+
children: children.map((child) => visit(child)),
|
|
110
|
+
position: node.position,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if ('children' in node)
|
|
115
|
+
node.children = node.children.map((child, index) => visit(child, node, index));
|
|
116
|
+
return node;
|
|
117
|
+
};
|
|
118
|
+
tree.children = tree.children.map((node) => visit(node));
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export const remarkCalloutsPlugin = $remark('inkkitCallouts', () => remarkCallouts);
|
|
122
|
+
export const calloutSchema = $node('inkkit_callout', (ctx) => ({
|
|
123
|
+
group: 'block',
|
|
124
|
+
content: 'block+',
|
|
125
|
+
defining: true,
|
|
126
|
+
attrs: {
|
|
127
|
+
kind: { default: 'NOTE' },
|
|
128
|
+
marker: { default: '[!NOTE]' },
|
|
129
|
+
fold: { default: '' },
|
|
130
|
+
title: { default: '' },
|
|
131
|
+
titleSource: { default: null },
|
|
132
|
+
listPlaceholder: { default: false },
|
|
133
|
+
},
|
|
134
|
+
parseDOM: [
|
|
135
|
+
{
|
|
136
|
+
tag: 'blockquote[data-inkkit-callout]',
|
|
137
|
+
priority: 100,
|
|
138
|
+
contentElement: '.inkkit-callout-body',
|
|
139
|
+
getAttrs: (dom) => {
|
|
140
|
+
const kind = (dom.getAttribute('data-inkkit-callout') ?? '').toUpperCase();
|
|
141
|
+
const marker = dom.getAttribute('data-inkkit-marker') ?? `[!${kind}]`;
|
|
142
|
+
const fold = dom.getAttribute('data-inkkit-fold') ?? '';
|
|
143
|
+
const titleSource = dom.getAttribute('data-inkkit-title');
|
|
144
|
+
const placeholder = dom.getAttribute('data-inkkit-list-placeholder');
|
|
145
|
+
const previous = dom.previousElementSibling;
|
|
146
|
+
const titleHeader = titleSource == null
|
|
147
|
+
? undefined
|
|
148
|
+
: plainHeader(ctx.get(remarkCtx), `${marker} ${titleSource}`);
|
|
149
|
+
if (!kinds.has(kind) ||
|
|
150
|
+
!/^\[![A-Za-z]+\]$/.test(marker) ||
|
|
151
|
+
marker.slice(2, -1).toUpperCase() !== kind ||
|
|
152
|
+
!['', '+', '-'].includes(fold) ||
|
|
153
|
+
(titleSource != null &&
|
|
154
|
+
(/[\r\n]/.test(titleSource) || titleHeader == null)))
|
|
155
|
+
return false;
|
|
156
|
+
return {
|
|
157
|
+
kind,
|
|
158
|
+
marker,
|
|
159
|
+
fold,
|
|
160
|
+
title: titleHeader == null
|
|
161
|
+
? (dom.querySelector('.inkkit-callout-title')?.textContent ?? '')
|
|
162
|
+
: titleHeader.slice(marker.length + 1).trimEnd(),
|
|
163
|
+
titleSource,
|
|
164
|
+
listPlaceholder: placeholder == null
|
|
165
|
+
? dom.parentElement?.tagName === 'LI' &&
|
|
166
|
+
(!previous ||
|
|
167
|
+
(previous.tagName === 'P' &&
|
|
168
|
+
!previous.textContent &&
|
|
169
|
+
!previous.childElementCount))
|
|
170
|
+
: placeholder === 'true',
|
|
171
|
+
};
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
toDOM: (node) => [
|
|
176
|
+
'blockquote',
|
|
177
|
+
{
|
|
178
|
+
class: 'inkkit-callout',
|
|
179
|
+
'data-inkkit-callout': node.attrs.kind,
|
|
180
|
+
'data-inkkit-marker': node.attrs.marker,
|
|
181
|
+
'data-inkkit-fold': node.attrs.fold,
|
|
182
|
+
'data-inkkit-title': node.attrs.titleSource ?? node.attrs.title,
|
|
183
|
+
'data-inkkit-list-placeholder': String(node.attrs.listPlaceholder),
|
|
184
|
+
},
|
|
185
|
+
[
|
|
186
|
+
'strong',
|
|
187
|
+
{ class: 'inkkit-callout-title' },
|
|
188
|
+
node.attrs.title ||
|
|
189
|
+
String(node.attrs.kind)
|
|
190
|
+
.toLowerCase()
|
|
191
|
+
.replace(/^./, (letter) => letter.toUpperCase()),
|
|
192
|
+
],
|
|
193
|
+
['div', { class: 'inkkit-callout-body' }, 0],
|
|
194
|
+
],
|
|
195
|
+
parseMarkdown: {
|
|
196
|
+
match: (node) => node.type === 'inkkitCallout',
|
|
197
|
+
runner(state, node, type) {
|
|
198
|
+
state.openNode(type, {
|
|
199
|
+
kind: node.kind,
|
|
200
|
+
marker: node.marker,
|
|
201
|
+
fold: node.fold,
|
|
202
|
+
title: node.title,
|
|
203
|
+
titleSource: node.titleSource,
|
|
204
|
+
listPlaceholder: node.listPlaceholder,
|
|
205
|
+
});
|
|
206
|
+
state.next(node.children);
|
|
207
|
+
state.closeNode();
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
toMarkdown: {
|
|
211
|
+
match: (node) => node.type.name === 'inkkit_callout',
|
|
212
|
+
runner(state, node) {
|
|
213
|
+
const parent = state.top();
|
|
214
|
+
const first = parent?.children?.[0];
|
|
215
|
+
// Lists require a leading paragraph even when their authored first block is a callout.
|
|
216
|
+
if (node.attrs.listPlaceholder &&
|
|
217
|
+
parent?.type === 'listItem' &&
|
|
218
|
+
parent.children?.length === 1 &&
|
|
219
|
+
first?.type === 'paragraph' &&
|
|
220
|
+
first.children?.length === 1 &&
|
|
221
|
+
first.children[0]?.type === 'html' &&
|
|
222
|
+
first.children[0].value === '<br />')
|
|
223
|
+
parent.children = [];
|
|
224
|
+
state.openNode('inkkitCallout', undefined, { ...node.attrs });
|
|
225
|
+
state.next(node.content);
|
|
226
|
+
state.closeNode();
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
}));
|
|
230
|
+
export const calloutView = $prose(() => new Plugin({
|
|
231
|
+
key: new PluginKey('inkkitCalloutView'),
|
|
232
|
+
props: {
|
|
233
|
+
nodeViews: {
|
|
234
|
+
inkkit_callout(node) {
|
|
235
|
+
const dom = document.createElement('blockquote');
|
|
236
|
+
dom.className = 'inkkit-callout';
|
|
237
|
+
dom.setAttribute('data-inkkit-callout', String(node.attrs.kind));
|
|
238
|
+
const header = document.createElement('div');
|
|
239
|
+
header.className = 'inkkit-callout-header';
|
|
240
|
+
header.contentEditable = 'false';
|
|
241
|
+
const title = document.createElement('strong');
|
|
242
|
+
title.className = 'inkkit-callout-title';
|
|
243
|
+
const contentDOM = document.createElement('div');
|
|
244
|
+
contentDOM.className = 'inkkit-callout-body';
|
|
245
|
+
let folded = node.attrs.fold === '-';
|
|
246
|
+
const button = document.createElement('button');
|
|
247
|
+
button.type = 'button';
|
|
248
|
+
button.setAttribute('data-inkkit-callout-toggle', '');
|
|
249
|
+
const render = () => {
|
|
250
|
+
title.textContent =
|
|
251
|
+
node.attrs.title ||
|
|
252
|
+
String(node.attrs.kind)
|
|
253
|
+
.toLowerCase()
|
|
254
|
+
.replace(/^./, (letter) => letter.toUpperCase());
|
|
255
|
+
dom.setAttribute('data-inkkit-folded', String(folded));
|
|
256
|
+
button.hidden = !node.attrs.fold;
|
|
257
|
+
button.setAttribute('aria-expanded', String(!folded));
|
|
258
|
+
button.setAttribute('aria-label', `${folded ? 'Expand' : 'Collapse'} ${title.textContent}`);
|
|
259
|
+
button.textContent = folded ? 'Expand' : 'Collapse';
|
|
260
|
+
};
|
|
261
|
+
const toggle = () => {
|
|
262
|
+
folded = !folded;
|
|
263
|
+
render();
|
|
264
|
+
};
|
|
265
|
+
button.addEventListener('click', toggle);
|
|
266
|
+
button.addEventListener('keydown', (event) => {
|
|
267
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
268
|
+
event.preventDefault();
|
|
269
|
+
event.stopPropagation();
|
|
270
|
+
toggle();
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
header.append(title, button);
|
|
274
|
+
dom.append(header, contentDOM);
|
|
275
|
+
render();
|
|
276
|
+
return {
|
|
277
|
+
dom,
|
|
278
|
+
contentDOM,
|
|
279
|
+
update(next) {
|
|
280
|
+
if (next.type !== node.type)
|
|
281
|
+
return false;
|
|
282
|
+
if (next.attrs.fold !== node.attrs.fold)
|
|
283
|
+
folded = next.attrs.fold === '-';
|
|
284
|
+
node = next;
|
|
285
|
+
render();
|
|
286
|
+
return true;
|
|
287
|
+
},
|
|
288
|
+
stopEvent: (event) => header.contains(event.target),
|
|
289
|
+
ignoreMutation: (mutation) => mutation.type !== 'selection' &&
|
|
290
|
+
!contentDOM.contains(mutation.target),
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
}));
|
|
296
|
+
export const callouts = [calloutSchema, calloutView].flat();
|
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
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
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
|
+
import { shareableFragment } from './comments.js';
|
|
5
|
+
import { diagramImage } from './mermaid.js';
|
|
4
6
|
// Clipboard text is the visible document, not Markdown serialization: escaping
|
|
5
7
|
// punctuation or trailing spaces here leaks source syntax into other apps.
|
|
6
8
|
export function clipboardText(content, missingImages = new Set()) {
|
|
9
|
+
content = shareableFragment(content);
|
|
7
10
|
const blocks = [];
|
|
8
11
|
content.forEach((node) => {
|
|
9
12
|
if (node.type.name !== 'reference_definition')
|
|
@@ -13,6 +16,11 @@ export function clipboardText(content, missingImages = new Set()) {
|
|
|
13
16
|
return blocks.join(inline ? '' : '\n\n');
|
|
14
17
|
}
|
|
15
18
|
function nodeText(node, missingImages) {
|
|
19
|
+
if (node.type.name === 'inkkit_callout')
|
|
20
|
+
return `${node.attrs.title ||
|
|
21
|
+
String(node.attrs.kind)
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/^./, (letter) => letter.toUpperCase())}\n${clipboardText(node.content, missingImages)}`;
|
|
16
24
|
if (node.type.name === 'reference_definition')
|
|
17
25
|
return '';
|
|
18
26
|
if (node.type.name === 'footnote_reference')
|
|
@@ -67,6 +75,7 @@ function nodeText(node, missingImages) {
|
|
|
67
75
|
return clipboardText(node.content, missingImages);
|
|
68
76
|
}
|
|
69
77
|
export function clipboardContent(content, schema) {
|
|
78
|
+
content = shareableFragment(content);
|
|
70
79
|
const container = document.createElement('div');
|
|
71
80
|
const serializer = DOMSerializer.fromSchema(schema);
|
|
72
81
|
// Construct image elements without a source so detached clipboard DOM never
|
|
@@ -102,7 +111,8 @@ export const visibleClipboard = $prose(() => new Plugin({
|
|
|
102
111
|
},
|
|
103
112
|
}));
|
|
104
113
|
/** Export a frozen document fragment; presentation URLs never enter the clipboard. */
|
|
105
|
-
export async function portableClipboard(content, schema, markdown, adapter, context) {
|
|
114
|
+
export async function portableClipboard(content, schema, markdown, adapter, context, completeDiagrams) {
|
|
115
|
+
content = shareableFragment(content);
|
|
106
116
|
const base = clipboardContent(content, schema);
|
|
107
117
|
const container = document.createElement('div');
|
|
108
118
|
container.innerHTML = base.html;
|
|
@@ -121,6 +131,7 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
|
|
|
121
131
|
});
|
|
122
132
|
await Promise.all(captured.map(async ({ element, reference, alt }, index) => {
|
|
123
133
|
element.setAttribute('data-inkkit-image-slot', String(index));
|
|
134
|
+
element.setAttribute('data-inkkit-authored-image-slot', String(index));
|
|
124
135
|
try {
|
|
125
136
|
if (!adapter)
|
|
126
137
|
throw new Error('The image is unavailable.');
|
|
@@ -143,6 +154,55 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
|
|
|
143
154
|
images[index] = { reference, alt, error: message };
|
|
144
155
|
}
|
|
145
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
|
+
}
|
|
146
206
|
// Scriptable links and event attributes are not part of semantic rich text.
|
|
147
207
|
for (const element of container.querySelectorAll('*')) {
|
|
148
208
|
for (const attribute of [...element.attributes])
|
|
@@ -156,6 +216,7 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
|
|
|
156
216
|
text: clipboardText(content, missing),
|
|
157
217
|
html: container.innerHTML,
|
|
158
218
|
markdown,
|
|
159
|
-
images,
|
|
219
|
+
images: [...orderedImages, ...images.filter((image) => image.error)],
|
|
220
|
+
diagrams,
|
|
160
221
|
};
|
|
161
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
|
+
}
|