@aicayzer/inkkit 0.0.1 → 0.0.2
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 -1
- package/dist/clipboard.js +22 -1
- package/dist/dialect.js +4 -2
- package/dist/editor.d.ts +4 -0
- package/dist/editor.js +183 -8
- package/dist/literals.d.ts +3 -0
- package/dist/literals.js +32 -17
- package/dist/paste.js +60 -5
- package/dist/preserve.js +234 -8
- package/dist/reference-clipboard.d.ts +8 -0
- package/dist/reference-clipboard.js +197 -0
- package/dist/references.d.ts +14 -0
- package/dist/references.js +367 -0
- package/dist/style.css +27 -0
- package/dist/types.d.ts +1 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -33,8 +33,10 @@ editor.loadDocument({
|
|
|
33
33
|
const snapshot = editor.snapshot(1)
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Markdown includes CommonMark, task lists, strikethrough, autolinks and
|
|
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.
|
|
37
37
|
|
|
38
38
|
See [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). Releases 0.0.2 and 0.0.3 are authorised for sequential delivery; targets from 0.0.4 remain provisional.
|
|
41
|
+
|
|
40
42
|
MIT licensed. InkKit builds on the public PadPad and Memos editors; see [NOTICE](NOTICE).
|
package/dist/clipboard.js
CHANGED
|
@@ -5,11 +5,20 @@ import { $prose } from '@milkdown/kit/utils';
|
|
|
5
5
|
// punctuation or trailing spaces here leaks source syntax into other apps.
|
|
6
6
|
export function clipboardText(content, missingImages = new Set()) {
|
|
7
7
|
const blocks = [];
|
|
8
|
-
content.forEach((node) =>
|
|
8
|
+
content.forEach((node) => {
|
|
9
|
+
if (node.type.name !== 'reference_definition')
|
|
10
|
+
blocks.push(nodeText(node, missingImages));
|
|
11
|
+
});
|
|
9
12
|
const inline = content.firstChild?.isInline ?? false;
|
|
10
13
|
return blocks.join(inline ? '' : '\n\n');
|
|
11
14
|
}
|
|
12
15
|
function nodeText(node, missingImages) {
|
|
16
|
+
if (node.type.name === 'reference_definition')
|
|
17
|
+
return '';
|
|
18
|
+
if (node.type.name === 'footnote_reference')
|
|
19
|
+
return `[${node.attrs.label}]`;
|
|
20
|
+
if (node.type.name === 'footnote_definition')
|
|
21
|
+
return `[${node.attrs.label}] ${clipboardText(node.content, missingImages)}`;
|
|
13
22
|
if (node.isText)
|
|
14
23
|
return node.text ?? '';
|
|
15
24
|
if (node.type.name === 'hardbreak' || node.type.name === 'hard_break')
|
|
@@ -73,6 +82,17 @@ export function clipboardContent(content, schema) {
|
|
|
73
82
|
],
|
|
74
83
|
}, serializer.marks);
|
|
75
84
|
container.append(safe.serializeFragment(content));
|
|
85
|
+
for (const definition of container.querySelectorAll('[data-inkkit-reference-definition]'))
|
|
86
|
+
definition.remove();
|
|
87
|
+
for (const reference of container.querySelectorAll('[data-inkkit-footnote-reference]')) {
|
|
88
|
+
reference.removeAttribute('tabindex');
|
|
89
|
+
reference.removeAttribute('role');
|
|
90
|
+
}
|
|
91
|
+
for (const definition of container.querySelectorAll('[data-inkkit-footnote-definition]')) {
|
|
92
|
+
const label = document.createElement('span');
|
|
93
|
+
label.textContent = `[${definition.getAttribute('data-inkkit-label')}] `;
|
|
94
|
+
definition.prepend(label);
|
|
95
|
+
}
|
|
76
96
|
return { text: clipboardText(content), html: container.innerHTML };
|
|
77
97
|
}
|
|
78
98
|
export const visibleClipboard = $prose(() => new Plugin({
|
|
@@ -100,6 +120,7 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
|
|
|
100
120
|
return { element, reference, alt };
|
|
101
121
|
});
|
|
102
122
|
await Promise.all(captured.map(async ({ element, reference, alt }, index) => {
|
|
123
|
+
element.setAttribute('data-inkkit-image-slot', String(index));
|
|
103
124
|
try {
|
|
104
125
|
if (!adapter)
|
|
105
126
|
throw new Error('The image is unavailable.');
|
package/dist/dialect.js
CHANGED
|
@@ -4,6 +4,7 @@ import { extendListItemSchemaForTask, strikethroughAttr, strikethroughInputRule,
|
|
|
4
4
|
import { $remark } from '@milkdown/kit/utils';
|
|
5
5
|
import { autolinkInputRule } from './autolink.js';
|
|
6
6
|
import { literalBlock, createLiteralPreservation } from './literals.js';
|
|
7
|
+
import { references, remarkReferencesPlugin } from './references.js';
|
|
7
8
|
import { gfmTableFromMarkdown, gfmTableToMarkdown } from 'mdast-util-gfm-table';
|
|
8
9
|
import { gfmTable } from 'micromark-extension-gfm-table';
|
|
9
10
|
import { defaultHandlers, } from 'mdast-util-to-markdown';
|
|
@@ -63,7 +64,6 @@ const link = (node, parent, state, info) => writesBare(node, parent)
|
|
|
63
64
|
link.peek = (node, parent, state) => writesBare(node, parent)
|
|
64
65
|
? node.url[0]
|
|
65
66
|
: defaultHandlers.link.peek(node, parent, state);
|
|
66
|
-
// Footnotes remain literal until the editor supports editing their definitions.
|
|
67
67
|
function remarkDialect() {
|
|
68
68
|
const data = this.data();
|
|
69
69
|
const add = (key, value) => {
|
|
@@ -85,7 +85,7 @@ function remarkDialect() {
|
|
|
85
85
|
add('toMarkdownExtensions', { handlers: { link } });
|
|
86
86
|
}
|
|
87
87
|
export const remarkDialectPlugin = $remark('remarkDialect', () => remarkDialect);
|
|
88
|
-
//
|
|
88
|
+
// Reference links retain their authored form instead of expanding to inline links.
|
|
89
89
|
const commonmarkWithLiteralReferences = commonmark.filter((plugin) => !remarkInlineLinkPlugin.includes(plugin));
|
|
90
90
|
// The preset drops the final empty paragraph, which makes trailing spacers shrink on each reload.
|
|
91
91
|
const preserveSpacerParagraphs = paragraphSchema.extendSchema((base) => (ctx) => {
|
|
@@ -251,9 +251,11 @@ export const tables = [
|
|
|
251
251
|
].flat();
|
|
252
252
|
export function createDialect(images) {
|
|
253
253
|
return [
|
|
254
|
+
remarkReferencesPlugin,
|
|
254
255
|
// Protect unsupported inline HTML before the empty-line plugin consumes break nodes.
|
|
255
256
|
createLiteralPreservation(images),
|
|
256
257
|
commonmarkWithLiteralReferences,
|
|
258
|
+
references,
|
|
257
259
|
preserveSpacerParagraphs,
|
|
258
260
|
visibleSoftbreaks,
|
|
259
261
|
normalizedImages,
|
package/dist/editor.d.ts
CHANGED
|
@@ -52,6 +52,7 @@ export declare class InkKitEditor {
|
|
|
52
52
|
private plain;
|
|
53
53
|
private root;
|
|
54
54
|
private clickHandler?;
|
|
55
|
+
private footnoteKeyHandler?;
|
|
55
56
|
private copyHandler?;
|
|
56
57
|
private originalSource;
|
|
57
58
|
private generation;
|
|
@@ -103,4 +104,7 @@ export declare class InkKitEditor {
|
|
|
103
104
|
private quote;
|
|
104
105
|
private unquote;
|
|
105
106
|
format(command: FormatCommand, arg?: string | number): void;
|
|
107
|
+
editReferenceDefinition(label: string, destination: string, title?: string): boolean;
|
|
108
|
+
insertFootnote(label?: string): void;
|
|
109
|
+
navigateFootnote(target: 'definition' | 'reference'): boolean;
|
|
106
110
|
}
|
package/dist/editor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Editor, defaultValueCtx, editorViewCtx, editorViewOptionsCtx, remarkStringifyOptionsCtx, rootCtx, } from '@milkdown/kit/core';
|
|
2
|
-
import { visibleClipboard, portableClipboard, clipboardText } from './clipboard.js';
|
|
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';
|
|
5
5
|
import { cursor } from '@milkdown/kit/plugin/cursor';
|
|
@@ -9,11 +9,13 @@ import { NodeRange, Fragment, Slice, } from '@milkdown/kit/prose/model';
|
|
|
9
9
|
import { keydownHandler, keymap } from '@milkdown/kit/prose/keymap';
|
|
10
10
|
import { liftListItem } from '@milkdown/kit/prose/schema-list';
|
|
11
11
|
import { findWrapping, liftTarget } from '@milkdown/kit/prose/transform';
|
|
12
|
-
import { AllSelection, Plugin, PluginKey, Selection, TextSelection, } from '@milkdown/kit/prose/state';
|
|
12
|
+
import { AllSelection, NodeSelection, Plugin, PluginKey, Selection, TextSelection, } from '@milkdown/kit/prose/state';
|
|
13
13
|
import { $prose, callCommand, replaceAll, } from '@milkdown/kit/utils';
|
|
14
14
|
import { codeCopyPlugin, placeholderPlugin } from './decorations.js';
|
|
15
15
|
import { createDialect, serialize, stringifyOptions } from './dialect.js';
|
|
16
16
|
import { Preservation } from './preserve.js';
|
|
17
|
+
import { normaliseLabel, referenceDefinitions, footnoteDefinitions, } from './references.js';
|
|
18
|
+
import { selectionMarkdown, selectionContent, referenceMetadata, withReferenceMetadata, } from './reference-clipboard.js';
|
|
17
19
|
import { imageView } from './images.js';
|
|
18
20
|
import { tablePlugins, tableCommand } from './tables.js';
|
|
19
21
|
import { InkKitError, } from './types.js';
|
|
@@ -240,6 +242,7 @@ export class InkKitEditor {
|
|
|
240
242
|
plain;
|
|
241
243
|
root;
|
|
242
244
|
clickHandler;
|
|
245
|
+
footnoteKeyHandler;
|
|
243
246
|
copyHandler;
|
|
244
247
|
originalSource = '';
|
|
245
248
|
generation = 0;
|
|
@@ -346,14 +349,17 @@ export class InkKitEditor {
|
|
|
346
349
|
try {
|
|
347
350
|
markdown = all
|
|
348
351
|
? snapshot.text
|
|
349
|
-
:
|
|
352
|
+
: selectionMarkdown(this.editor.ctx, doc, valid);
|
|
350
353
|
}
|
|
351
354
|
catch (error) {
|
|
352
355
|
throw new InkKitError('preservation', error instanceof Error
|
|
353
356
|
? error.message
|
|
354
357
|
: 'Cannot copy the selected Markdown');
|
|
355
358
|
}
|
|
356
|
-
const result = await portableClipboard(content, schema, markdown, this.options.images, this.context());
|
|
359
|
+
const result = await portableClipboard(all ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context());
|
|
360
|
+
const metadata = referenceMetadata(this.editor.ctx, doc, content);
|
|
361
|
+
if (metadata != null)
|
|
362
|
+
result.html = withReferenceMetadata(result.html, metadata);
|
|
357
363
|
this.assertCurrent(snapshot.generation);
|
|
358
364
|
if (snapshot.documentId !== this.documentId || epoch !== this.documentEpoch)
|
|
359
365
|
throw new InkKitError('stale-document', 'Document changed');
|
|
@@ -423,6 +429,8 @@ export class InkKitEditor {
|
|
|
423
429
|
this.pasteController.destroy();
|
|
424
430
|
if (this.clickHandler)
|
|
425
431
|
this.root.removeEventListener('click', this.clickHandler);
|
|
432
|
+
if (this.footnoteKeyHandler)
|
|
433
|
+
this.root.removeEventListener('keydown', this.footnoteKeyHandler, true);
|
|
426
434
|
if (this.copyHandler) {
|
|
427
435
|
this.root.removeEventListener('copy', this.copyHandler, true);
|
|
428
436
|
this.root.removeEventListener('cut', this.copyHandler, true);
|
|
@@ -433,7 +441,14 @@ export class InkKitEditor {
|
|
|
433
441
|
// The app's bindings, replaced whole whenever they change; the plugin stays.
|
|
434
442
|
keymapPlugin = $prose(() => new Plugin({
|
|
435
443
|
key: new PluginKey('appKeymap'),
|
|
436
|
-
props: {
|
|
444
|
+
props: {
|
|
445
|
+
handleKeyDown: (view, event) => {
|
|
446
|
+
if (event.altKey && event.key === 'Enter') {
|
|
447
|
+
return this.navigateFootnote(event.shiftKey ? 'reference' : 'definition');
|
|
448
|
+
}
|
|
449
|
+
return this.keys(view, event);
|
|
450
|
+
},
|
|
451
|
+
},
|
|
437
452
|
}));
|
|
438
453
|
static async mount(root, events, options = {}) {
|
|
439
454
|
const instance = new InkKitEditor(events, options);
|
|
@@ -520,7 +535,40 @@ export class InkKitEditor {
|
|
|
520
535
|
instance.plain.addEventListener('compositionend', () => {
|
|
521
536
|
instance.plainComposing = false;
|
|
522
537
|
});
|
|
538
|
+
const activateFootnote = (event) => {
|
|
539
|
+
const element = event.target.closest('[data-inkkit-footnote-reference]');
|
|
540
|
+
if (!element)
|
|
541
|
+
return false;
|
|
542
|
+
const identifier = element.getAttribute('data-inkkit-footnote-reference');
|
|
543
|
+
const view = instance.editor.ctx.get(editorViewCtx);
|
|
544
|
+
let position;
|
|
545
|
+
view.state.doc.descendants((node, pos) => {
|
|
546
|
+
if (position == null &&
|
|
547
|
+
node.type.name === 'footnote_reference' &&
|
|
548
|
+
node.attrs.identifier === identifier)
|
|
549
|
+
position = pos;
|
|
550
|
+
});
|
|
551
|
+
if (position == null)
|
|
552
|
+
return false;
|
|
553
|
+
event.preventDefault();
|
|
554
|
+
try {
|
|
555
|
+
view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, position)));
|
|
556
|
+
instance.navigateFootnote('definition');
|
|
557
|
+
}
|
|
558
|
+
catch (error) {
|
|
559
|
+
events.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
560
|
+
}
|
|
561
|
+
return true;
|
|
562
|
+
};
|
|
563
|
+
instance.footnoteKeyHandler = (event) => {
|
|
564
|
+
if ((event.key === 'Enter' || event.key === ' ') &&
|
|
565
|
+
activateFootnote(event))
|
|
566
|
+
event.stopPropagation();
|
|
567
|
+
};
|
|
568
|
+
root.addEventListener('keydown', instance.footnoteKeyHandler, true);
|
|
523
569
|
instance.clickHandler = (event) => {
|
|
570
|
+
if (activateFootnote(event))
|
|
571
|
+
return;
|
|
524
572
|
const anchor = event.target.closest('a[href]');
|
|
525
573
|
if (anchor && event.metaKey) {
|
|
526
574
|
event.preventDefault();
|
|
@@ -533,13 +581,38 @@ export class InkKitEditor {
|
|
|
533
581
|
return;
|
|
534
582
|
const view = instance.editor.ctx.get(editorViewCtx);
|
|
535
583
|
const fragment = view.state.selection.content().content;
|
|
584
|
+
const expanded = selectionContent(view.state.doc, fragment);
|
|
536
585
|
let hasImages = false;
|
|
537
|
-
|
|
586
|
+
expanded.descendants((node) => {
|
|
538
587
|
if (node.type.name === 'image')
|
|
539
588
|
hasImages = true;
|
|
540
589
|
});
|
|
541
|
-
if (!hasImages)
|
|
590
|
+
if (!hasImages) {
|
|
591
|
+
if (!event.clipboardData || fragment.size === 0)
|
|
592
|
+
return;
|
|
593
|
+
event.preventDefault();
|
|
594
|
+
event.stopPropagation();
|
|
595
|
+
try {
|
|
596
|
+
instance.snapshot();
|
|
597
|
+
const content = expanded;
|
|
598
|
+
const valid = fragment.firstChild?.isInline
|
|
599
|
+
? Fragment.from(view.state.schema.nodes.paragraph.create(null, fragment))
|
|
600
|
+
: fragment;
|
|
601
|
+
const markdown = selectionMarkdown(instance.editor.ctx, view.state.doc, valid);
|
|
602
|
+
const output = clipboardContent(content, view.state.schema);
|
|
603
|
+
event.clipboardData.setData('text/plain', output.text);
|
|
604
|
+
const metadata = referenceMetadata(instance.editor.ctx, view.state.doc, valid);
|
|
605
|
+
event.clipboardData.setData('text/html', metadata != null
|
|
606
|
+
? withReferenceMetadata(output.html, metadata)
|
|
607
|
+
: output.html);
|
|
608
|
+
if (event.type === 'cut')
|
|
609
|
+
view.dispatch(view.state.tr.deleteSelection().scrollIntoView());
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
events.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
613
|
+
}
|
|
542
614
|
return;
|
|
615
|
+
}
|
|
543
616
|
event.preventDefault();
|
|
544
617
|
event.stopPropagation();
|
|
545
618
|
if (events.clipboard) {
|
|
@@ -564,7 +637,7 @@ export class InkKitEditor {
|
|
|
564
637
|
}
|
|
565
638
|
else {
|
|
566
639
|
const container = document.createElement('div');
|
|
567
|
-
container.textContent = clipboardText(
|
|
640
|
+
container.textContent = clipboardText(expanded);
|
|
568
641
|
event.clipboardData?.setData('text/plain', container.textContent ?? '');
|
|
569
642
|
event.clipboardData?.setData('text/html', container.outerHTML);
|
|
570
643
|
events.error?.(new InkKitError('image-unavailable', 'The host must provide a clipboard handler to copy images'));
|
|
@@ -961,6 +1034,17 @@ export class InkKitEditor {
|
|
|
961
1034
|
toggleTaskList(this.editor.ctx);
|
|
962
1035
|
break;
|
|
963
1036
|
case 'link': {
|
|
1037
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1038
|
+
const reference = view.state.selection.$from
|
|
1039
|
+
.marks()
|
|
1040
|
+
.find((mark) => mark.type.name === 'link' && mark.attrs.identifier) ??
|
|
1041
|
+
view.state.doc
|
|
1042
|
+
.nodeAt(view.state.selection.from)
|
|
1043
|
+
?.marks.find((mark) => mark.type.name === 'link' && mark.attrs.identifier);
|
|
1044
|
+
if (reference && typeof arg === 'string') {
|
|
1045
|
+
this.editReferenceDefinition(String(reference.attrs.identifier), arg);
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
964
1048
|
const payload = typeof arg === 'string' ? { href: arg } : {};
|
|
965
1049
|
const toggle = () => run(toggleLinkCommand.key, payload);
|
|
966
1050
|
if (!state.marks.includes('link') ||
|
|
@@ -971,4 +1055,95 @@ export class InkKitEditor {
|
|
|
971
1055
|
}
|
|
972
1056
|
this.focus();
|
|
973
1057
|
}
|
|
1058
|
+
editReferenceDefinition(label, destination, title) {
|
|
1059
|
+
this.assertCurrent();
|
|
1060
|
+
if (this.formatType === 'txt')
|
|
1061
|
+
return false;
|
|
1062
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1063
|
+
const definitions = referenceDefinitions(view.state.doc);
|
|
1064
|
+
const definition = definitions.get(normaliseLabel(label)) ??
|
|
1065
|
+
[...definitions.values()].find(({ node }) => normaliseLabel(String(node.attrs.label)) === normaliseLabel(label));
|
|
1066
|
+
if (!definition)
|
|
1067
|
+
return false;
|
|
1068
|
+
view.dispatch(view.state.tr.setNodeMarkup(definition.pos, undefined, {
|
|
1069
|
+
...definition.node.attrs,
|
|
1070
|
+
url: destination,
|
|
1071
|
+
title: title ?? definition.node.attrs.title,
|
|
1072
|
+
}));
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
insertFootnote(label) {
|
|
1076
|
+
this.assertCurrent();
|
|
1077
|
+
if (this.formatType === 'txt')
|
|
1078
|
+
return;
|
|
1079
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1080
|
+
const definitions = footnoteDefinitions(view.state.doc);
|
|
1081
|
+
if (label == null) {
|
|
1082
|
+
let number = 1;
|
|
1083
|
+
const labels = new Set(definitions.keys());
|
|
1084
|
+
for (const match of this.snapshot().text.matchAll(/\[\^([^\]\r\n]+)\]/g))
|
|
1085
|
+
labels.add(normaliseLabel(match[1]));
|
|
1086
|
+
view.state.doc.descendants((node) => {
|
|
1087
|
+
if (node.type.name === 'footnote_reference')
|
|
1088
|
+
labels.add(String(node.attrs.identifier));
|
|
1089
|
+
});
|
|
1090
|
+
while (labels.has(normaliseLabel(String(number))))
|
|
1091
|
+
number++;
|
|
1092
|
+
label = String(number);
|
|
1093
|
+
}
|
|
1094
|
+
if (!label || /[\s\[\]\\]/.test(label) || label.length > 999)
|
|
1095
|
+
throw new Error('A footnote label must be 1–999 characters without whitespace, brackets or backslashes');
|
|
1096
|
+
const identifier = normaliseLabel(label);
|
|
1097
|
+
const reference = view.state.schema.nodes.footnote_reference.create({
|
|
1098
|
+
label,
|
|
1099
|
+
identifier,
|
|
1100
|
+
});
|
|
1101
|
+
const tr = view.state.tr.insert(view.state.selection.to, reference);
|
|
1102
|
+
if (!definitions.has(identifier)) {
|
|
1103
|
+
const definition = view.state.schema.nodes.footnote_definition.create({ label, identifier }, view.state.schema.nodes.paragraph.create());
|
|
1104
|
+
const position = tr.doc.content.size;
|
|
1105
|
+
tr.insert(position, definition).setSelection(TextSelection.create(tr.doc, position + 2));
|
|
1106
|
+
}
|
|
1107
|
+
view.dispatch(tr.scrollIntoView());
|
|
1108
|
+
this.focus();
|
|
1109
|
+
}
|
|
1110
|
+
navigateFootnote(target) {
|
|
1111
|
+
this.assertCurrent();
|
|
1112
|
+
if (this.formatType === 'txt')
|
|
1113
|
+
return false;
|
|
1114
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1115
|
+
const { $from } = view.state.selection;
|
|
1116
|
+
let identifier;
|
|
1117
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
1118
|
+
const node = $from.node(depth);
|
|
1119
|
+
if (node.type.name === 'footnote_definition')
|
|
1120
|
+
identifier = String(node.attrs.identifier);
|
|
1121
|
+
}
|
|
1122
|
+
const at = view.state.doc.nodeAt(view.state.selection.from);
|
|
1123
|
+
const adjacent = at?.type.name === 'footnote_reference' ? at : $from.nodeBefore;
|
|
1124
|
+
if (adjacent?.type.name === 'footnote_reference')
|
|
1125
|
+
identifier = String(adjacent.attrs.identifier);
|
|
1126
|
+
if (!identifier)
|
|
1127
|
+
return false;
|
|
1128
|
+
let position;
|
|
1129
|
+
if (target === 'definition') {
|
|
1130
|
+
const found = footnoteDefinitions(view.state.doc).get(identifier);
|
|
1131
|
+
if (found)
|
|
1132
|
+
position = found.pos + 2;
|
|
1133
|
+
}
|
|
1134
|
+
else
|
|
1135
|
+
view.state.doc.descendants((node, pos) => {
|
|
1136
|
+
if (position == null &&
|
|
1137
|
+
node.type.name === 'footnote_reference' &&
|
|
1138
|
+
node.attrs.identifier === identifier)
|
|
1139
|
+
position = pos;
|
|
1140
|
+
});
|
|
1141
|
+
if (position == null)
|
|
1142
|
+
return false;
|
|
1143
|
+
view.dispatch(view.state.tr
|
|
1144
|
+
.setSelection(TextSelection.near(view.state.doc.resolve(position)))
|
|
1145
|
+
.scrollIntoView());
|
|
1146
|
+
this.focus();
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
974
1149
|
}
|
package/dist/literals.d.ts
CHANGED
package/dist/literals.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { $node, $remark } from '@milkdown/kit/utils';
|
|
2
|
-
const unsupported = new Set([
|
|
3
|
-
'html',
|
|
4
|
-
'image',
|
|
5
|
-
'imageReference',
|
|
6
|
-
'definition',
|
|
7
|
-
'linkReference',
|
|
8
|
-
]);
|
|
2
|
+
const unsupported = new Set(['html', 'image', 'imageReference']);
|
|
9
3
|
function restoreInlineBreaks(node) {
|
|
10
4
|
if ((node.type === 'paragraph' || node.type === 'heading') &&
|
|
11
5
|
node.children.length > 1) {
|
|
@@ -37,6 +31,18 @@ function containsUnsupported(node, images, inline = false) {
|
|
|
37
31
|
('children' in node &&
|
|
38
32
|
node.children.some((child) => containsUnsupported(child, images, inline || node.type === 'paragraph' || node.type === 'heading'))));
|
|
39
33
|
}
|
|
34
|
+
function containsUnresolved(node, source) {
|
|
35
|
+
if (node.type === 'text') {
|
|
36
|
+
const start = node.position?.start.offset;
|
|
37
|
+
const end = node.position?.end.offset;
|
|
38
|
+
if (start == null || end == null)
|
|
39
|
+
return false;
|
|
40
|
+
const raw = source.slice(start, end);
|
|
41
|
+
return /(?<!\\)\[\^[^\]\n]+\]|(?<!\\)\[[^\]\n]+\](?:\[[^\]\n]*\])?/.test(raw);
|
|
42
|
+
}
|
|
43
|
+
return ('children' in node &&
|
|
44
|
+
node.children.some((child) => containsUnresolved(child, source)));
|
|
45
|
+
}
|
|
40
46
|
// Keep syntax we cannot faithfully edit as literal text, without making the rest
|
|
41
47
|
// of the document a source editor or allowing HTML and image network requests.
|
|
42
48
|
export function createLiteralPreservation(images) {
|
|
@@ -46,6 +52,24 @@ export function createLiteralPreservation(images) {
|
|
|
46
52
|
const source = String(file.value).replace(/^\uFEFF/, '');
|
|
47
53
|
const frontmatter = /^(---|\+\+\+)\r?\n[\s\S]*?\r?\n\1(?:\r?\n|$)/.exec(source);
|
|
48
54
|
const frontmatterEnd = frontmatter?.[0].length ?? 0;
|
|
55
|
+
const preserveBlock = (node, depth = 0) => {
|
|
56
|
+
if (node.type === 'footnoteDefinition') {
|
|
57
|
+
node.children = node.children.map((child) => preserveBlock(child, depth + 1));
|
|
58
|
+
return node;
|
|
59
|
+
}
|
|
60
|
+
const start = node.position?.start.offset;
|
|
61
|
+
const end = node.position?.end.offset;
|
|
62
|
+
if (start == null ||
|
|
63
|
+
end == null ||
|
|
64
|
+
(!containsUnsupported(node, images) &&
|
|
65
|
+
!containsUnresolved(node, source)))
|
|
66
|
+
return node;
|
|
67
|
+
let raw = source.slice(start, end);
|
|
68
|
+
// Definition indentation is restored by the footnote serializer.
|
|
69
|
+
for (let index = 0; index < depth; index++)
|
|
70
|
+
raw = raw.replace(/\r?\n(?: {4}|\t)/g, '\n');
|
|
71
|
+
return { type: 'inkkitLiteral', value: raw, position: node.position };
|
|
72
|
+
};
|
|
49
73
|
const blocks = [];
|
|
50
74
|
if (frontmatter)
|
|
51
75
|
blocks.push({
|
|
@@ -81,16 +105,7 @@ export function createLiteralPreservation(images) {
|
|
|
81
105
|
});
|
|
82
106
|
continue;
|
|
83
107
|
}
|
|
84
|
-
|
|
85
|
-
const footnote = node.type !== 'code' && /\[\^[^\]]+\]/.test(raw);
|
|
86
|
-
if (containsUnsupported(node, images) || footnote)
|
|
87
|
-
blocks.push({
|
|
88
|
-
type: 'inkkitLiteral',
|
|
89
|
-
value: raw,
|
|
90
|
-
position: node.position,
|
|
91
|
-
});
|
|
92
|
-
else
|
|
93
|
-
blocks.push(node);
|
|
108
|
+
blocks.push(preserveBlock(node));
|
|
94
109
|
}
|
|
95
110
|
tree.children = blocks;
|
|
96
111
|
});
|
package/dist/paste.js
CHANGED
|
@@ -2,6 +2,7 @@ import { editorViewCtx, parserCtx } from '@milkdown/kit/core';
|
|
|
2
2
|
import { DOMParser as ProseParser, Fragment, Slice, } from '@milkdown/kit/prose/model';
|
|
3
3
|
import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
|
|
4
4
|
import { $prose } from '@milkdown/kit/utils';
|
|
5
|
+
import { avoidReferenceCollisions, markdownFromHTML, } from './reference-clipboard.js';
|
|
5
6
|
import { InkKitError, } from './types.js';
|
|
6
7
|
export class PasteController {
|
|
7
8
|
options;
|
|
@@ -113,6 +114,10 @@ export class PasteController {
|
|
|
113
114
|
const plainText = input.plainText ||
|
|
114
115
|
this.options.literalText?.() ||
|
|
115
116
|
view.state.selection.$from.parent.type.spec.code;
|
|
117
|
+
const markdown = plainText
|
|
118
|
+
? undefined
|
|
119
|
+
: (input.markdown ??
|
|
120
|
+
(input.html ? markdownFromHTML(input.html) : undefined));
|
|
116
121
|
if (plainText) {
|
|
117
122
|
if (view.state.selection.$from.parent.type.spec.code) {
|
|
118
123
|
const anchor = operation;
|
|
@@ -131,6 +136,21 @@ export class PasteController {
|
|
|
131
136
|
const template = document.createElement('template');
|
|
132
137
|
template.innerHTML = input.html;
|
|
133
138
|
const container = template.content;
|
|
139
|
+
if (markdown == null) {
|
|
140
|
+
// Native destinations can keep node metadata after dropping the
|
|
141
|
+
// Markdown wrapper and its definitions. Import their visible HTML.
|
|
142
|
+
for (const element of container.querySelectorAll('[data-inkkit-reference],[data-inkkit-footnote-reference],[data-inkkit-footnote-definition]')) {
|
|
143
|
+
for (const name of [
|
|
144
|
+
'data-inkkit-reference',
|
|
145
|
+
'data-inkkit-footnote-reference',
|
|
146
|
+
'data-inkkit-footnote-definition',
|
|
147
|
+
'data-inkkit-label',
|
|
148
|
+
'data-inkkit-reference-type',
|
|
149
|
+
'data-inkkit-reference-content',
|
|
150
|
+
])
|
|
151
|
+
element.removeAttribute(name);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
134
154
|
// Native HTML clipboards add encoding metadata. Obsidian's reading
|
|
135
155
|
// view also copies its title and collapse controls with document HTML.
|
|
136
156
|
for (const metadata of container.querySelectorAll('meta[charset]'))
|
|
@@ -183,6 +203,7 @@ export class PasteController {
|
|
|
183
203
|
'TD',
|
|
184
204
|
'IMG',
|
|
185
205
|
'SPAN',
|
|
206
|
+
'SUP',
|
|
186
207
|
'DIV',
|
|
187
208
|
'FONT',
|
|
188
209
|
'SECTION',
|
|
@@ -271,9 +292,33 @@ export class PasteController {
|
|
|
271
292
|
: 'The pasted image is unavailable.'));
|
|
272
293
|
}
|
|
273
294
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
295
|
+
if (markdown != null) {
|
|
296
|
+
const parsed = ctx.get(parserCtx)(markdown);
|
|
297
|
+
let imageIndex = 0;
|
|
298
|
+
const hydrate = (node) => {
|
|
299
|
+
if (node.type.name === 'image') {
|
|
300
|
+
const slot = /^inkkit-clipboard-image:(\d+)$/.exec(String(node.attrs.src));
|
|
301
|
+
const position = slot ? Number(slot[1]) : imageIndex++;
|
|
302
|
+
const element = elements.some((element) => element.hasAttribute('data-inkkit-image-slot'))
|
|
303
|
+
? elements.find((element) => Number(element.getAttribute('data-inkkit-image-slot')) ===
|
|
304
|
+
position)
|
|
305
|
+
: elements[position];
|
|
306
|
+
if (!element || !container.contains(element))
|
|
307
|
+
return schema.text(`[${node.attrs.alt || 'Image'}: image unavailable]`);
|
|
308
|
+
return node.type.create({ ...node.attrs, src: element.getAttribute('src') }, null, node.marks);
|
|
309
|
+
}
|
|
310
|
+
if (node.isText)
|
|
311
|
+
return node;
|
|
312
|
+
const children = [];
|
|
313
|
+
node.forEach((child) => children.push(hydrate(child)));
|
|
314
|
+
return node.copy(Fragment.fromArray(children));
|
|
315
|
+
};
|
|
316
|
+
slice = new Slice(hydrate(parsed).content, 0, 0);
|
|
317
|
+
}
|
|
318
|
+
else
|
|
319
|
+
slice = ProseParser.fromSchema(schema).parseSlice(container, {
|
|
320
|
+
preserveWhitespace: true,
|
|
321
|
+
});
|
|
277
322
|
}
|
|
278
323
|
else if (images.length) {
|
|
279
324
|
const nodes = [];
|
|
@@ -310,7 +355,8 @@ export class PasteController {
|
|
|
310
355
|
else {
|
|
311
356
|
const selection = view.state.selection;
|
|
312
357
|
const text = input.text.trim();
|
|
313
|
-
if (
|
|
358
|
+
if (markdown == null &&
|
|
359
|
+
!selection.empty &&
|
|
314
360
|
/^https?:\/\/\S+$/.test(text) &&
|
|
315
361
|
schema.marks.link) {
|
|
316
362
|
const anchor = operation;
|
|
@@ -320,13 +366,22 @@ export class PasteController {
|
|
|
320
366
|
.removeStoredMark(mark));
|
|
321
367
|
return;
|
|
322
368
|
}
|
|
323
|
-
const doc = ctx.get(parserCtx)(input.text);
|
|
369
|
+
const doc = ctx.get(parserCtx)(markdown ?? input.text);
|
|
324
370
|
slice = new Slice(doc.content, 0, 0);
|
|
325
371
|
}
|
|
326
372
|
this.assertContext(context, operation);
|
|
327
373
|
if (!view.editable || this.options.editable?.() === false)
|
|
328
374
|
return;
|
|
329
375
|
const anchor = operation;
|
|
376
|
+
if (!plainText) {
|
|
377
|
+
const valid = slice.content.firstChild?.isInline
|
|
378
|
+
? Fragment.from(schema.nodes.paragraph.create(null, slice.content))
|
|
379
|
+
: slice.content;
|
|
380
|
+
const remapped = avoidReferenceCollisions(schema.topNodeType.create(null, valid), view.state.doc);
|
|
381
|
+
slice = new Slice(slice.content.firstChild?.isInline
|
|
382
|
+
? remapped.firstChild.content
|
|
383
|
+
: remapped.content, slice.openStart, slice.openEnd);
|
|
384
|
+
}
|
|
330
385
|
const insertion = view.state.doc.resolve(anchor.from);
|
|
331
386
|
// An isolated block such as a table cannot fit inside an empty paragraph.
|
|
332
387
|
// Replace that empty block rather than retaining a spurious spacer.
|