@aicayzer/inkkit 0.0.1 → 0.0.3
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/callouts.d.ts +24 -0
- package/dist/callouts.js +296 -0
- package/dist/clipboard.js +31 -1
- package/dist/comments.d.ts +55 -0
- package/dist/comments.js +487 -0
- package/dist/dialect.js +12 -2
- package/dist/editor.d.ts +7 -2
- package/dist/editor.js +199 -10
- package/dist/inline-highlight.d.ts +6 -0
- package/dist/inline-highlight.js +112 -0
- package/dist/literals.d.ts +5 -0
- package/dist/literals.js +62 -20
- package/dist/paste.js +61 -5
- package/dist/preserve.js +254 -8
- package/dist/reference-clipboard.d.ts +8 -0
- package/dist/reference-clipboard.js +201 -0
- package/dist/references.d.ts +14 -0
- package/dist/references.js +367 -0
- package/dist/style.css +117 -0
- package/dist/types.d.ts +1 -0
- package/package.json +11 -4
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,15 +9,19 @@ 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';
|
|
20
22
|
import { highlightPlugin } from './highlight.js';
|
|
23
|
+
import { highlightKeymap, toggleHighlightCommand } from './inline-highlight.js';
|
|
24
|
+
import { commentSelectionContent, setCommentVisibility } from './comments.js';
|
|
21
25
|
import { PasteController } from './paste.js';
|
|
22
26
|
import { selectionPlugin } from './selection.js';
|
|
23
27
|
import { search, SearchQuery, getSearchState, setSearchState, } from 'prosemirror-search';
|
|
@@ -30,6 +34,7 @@ const shortcutCommands = {
|
|
|
30
34
|
bold: ['bold'],
|
|
31
35
|
italic: ['italic'],
|
|
32
36
|
strikethrough: ['strikethrough'],
|
|
37
|
+
highlight: ['highlight'],
|
|
33
38
|
code: ['code'],
|
|
34
39
|
codeBlock: ['codeBlock'],
|
|
35
40
|
quote: ['quote'],
|
|
@@ -41,6 +46,7 @@ const markNames = {
|
|
|
41
46
|
strong: 'bold',
|
|
42
47
|
emphasis: 'italic',
|
|
43
48
|
strike_through: 'strikethrough',
|
|
49
|
+
highlight: 'highlight',
|
|
44
50
|
inlineCode: 'code',
|
|
45
51
|
link: 'link',
|
|
46
52
|
};
|
|
@@ -240,6 +246,7 @@ export class InkKitEditor {
|
|
|
240
246
|
plain;
|
|
241
247
|
root;
|
|
242
248
|
clickHandler;
|
|
249
|
+
footnoteKeyHandler;
|
|
243
250
|
copyHandler;
|
|
244
251
|
originalSource = '';
|
|
245
252
|
generation = 0;
|
|
@@ -338,7 +345,9 @@ export class InkKitEditor {
|
|
|
338
345
|
return { text, html: pre.outerHTML, markdown: text, images: [] };
|
|
339
346
|
}
|
|
340
347
|
const { doc, schema, selection } = this.editor.ctx.get(editorViewCtx).state;
|
|
341
|
-
const content = all
|
|
348
|
+
const content = all
|
|
349
|
+
? doc.content
|
|
350
|
+
: commentSelectionContent(doc, selection.from, selection.to, selection.content().content);
|
|
342
351
|
const valid = content.firstChild?.isInline
|
|
343
352
|
? Fragment.from(schema.nodes.paragraph.create(null, content))
|
|
344
353
|
: content;
|
|
@@ -346,14 +355,17 @@ export class InkKitEditor {
|
|
|
346
355
|
try {
|
|
347
356
|
markdown = all
|
|
348
357
|
? snapshot.text
|
|
349
|
-
:
|
|
358
|
+
: selectionMarkdown(this.editor.ctx, doc, valid);
|
|
350
359
|
}
|
|
351
360
|
catch (error) {
|
|
352
361
|
throw new InkKitError('preservation', error instanceof Error
|
|
353
362
|
? error.message
|
|
354
363
|
: 'Cannot copy the selected Markdown');
|
|
355
364
|
}
|
|
356
|
-
const result = await portableClipboard(content, schema, markdown, this.options.images, this.context());
|
|
365
|
+
const result = await portableClipboard(all ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context());
|
|
366
|
+
const metadata = referenceMetadata(this.editor.ctx, doc, content);
|
|
367
|
+
if (metadata != null)
|
|
368
|
+
result.html = withReferenceMetadata(result.html, metadata);
|
|
357
369
|
this.assertCurrent(snapshot.generation);
|
|
358
370
|
if (snapshot.documentId !== this.documentId || epoch !== this.documentEpoch)
|
|
359
371
|
throw new InkKitError('stale-document', 'Document changed');
|
|
@@ -369,6 +381,10 @@ export class InkKitEditor {
|
|
|
369
381
|
}
|
|
370
382
|
await this.pasteController.paste(input);
|
|
371
383
|
}
|
|
384
|
+
setCommentsVisible(visible) {
|
|
385
|
+
this.assertAlive();
|
|
386
|
+
setCommentVisibility(this.editor.ctx.get(editorViewCtx), visible);
|
|
387
|
+
}
|
|
372
388
|
pasteAsPlainText(text) {
|
|
373
389
|
this.assertCurrent();
|
|
374
390
|
if (this.formatType === 'txt') {
|
|
@@ -423,6 +439,8 @@ export class InkKitEditor {
|
|
|
423
439
|
this.pasteController.destroy();
|
|
424
440
|
if (this.clickHandler)
|
|
425
441
|
this.root.removeEventListener('click', this.clickHandler);
|
|
442
|
+
if (this.footnoteKeyHandler)
|
|
443
|
+
this.root.removeEventListener('keydown', this.footnoteKeyHandler, true);
|
|
426
444
|
if (this.copyHandler) {
|
|
427
445
|
this.root.removeEventListener('copy', this.copyHandler, true);
|
|
428
446
|
this.root.removeEventListener('cut', this.copyHandler, true);
|
|
@@ -433,7 +451,14 @@ export class InkKitEditor {
|
|
|
433
451
|
// The app's bindings, replaced whole whenever they change; the plugin stays.
|
|
434
452
|
keymapPlugin = $prose(() => new Plugin({
|
|
435
453
|
key: new PluginKey('appKeymap'),
|
|
436
|
-
props: {
|
|
454
|
+
props: {
|
|
455
|
+
handleKeyDown: (view, event) => {
|
|
456
|
+
if (event.altKey && event.key === 'Enter') {
|
|
457
|
+
return this.navigateFootnote(event.shiftKey ? 'reference' : 'definition');
|
|
458
|
+
}
|
|
459
|
+
return this.keys(view, event);
|
|
460
|
+
},
|
|
461
|
+
},
|
|
437
462
|
}));
|
|
438
463
|
static async mount(root, events, options = {}) {
|
|
439
464
|
const instance = new InkKitEditor(events, options);
|
|
@@ -470,6 +495,7 @@ export class InkKitEditor {
|
|
|
470
495
|
unbind(emphasisKeymap);
|
|
471
496
|
unbind(inlineCodeKeymap);
|
|
472
497
|
unbind(strikethroughKeymap);
|
|
498
|
+
unbind(highlightKeymap);
|
|
473
499
|
unbind(headingKeymap, ['DowngradeHeading']);
|
|
474
500
|
unbind(paragraphKeymap);
|
|
475
501
|
unbind(blockquoteKeymap);
|
|
@@ -520,7 +546,40 @@ export class InkKitEditor {
|
|
|
520
546
|
instance.plain.addEventListener('compositionend', () => {
|
|
521
547
|
instance.plainComposing = false;
|
|
522
548
|
});
|
|
549
|
+
const activateFootnote = (event) => {
|
|
550
|
+
const element = event.target.closest('[data-inkkit-footnote-reference]');
|
|
551
|
+
if (!element)
|
|
552
|
+
return false;
|
|
553
|
+
const identifier = element.getAttribute('data-inkkit-footnote-reference');
|
|
554
|
+
const view = instance.editor.ctx.get(editorViewCtx);
|
|
555
|
+
let position;
|
|
556
|
+
view.state.doc.descendants((node, pos) => {
|
|
557
|
+
if (position == null &&
|
|
558
|
+
node.type.name === 'footnote_reference' &&
|
|
559
|
+
node.attrs.identifier === identifier)
|
|
560
|
+
position = pos;
|
|
561
|
+
});
|
|
562
|
+
if (position == null)
|
|
563
|
+
return false;
|
|
564
|
+
event.preventDefault();
|
|
565
|
+
try {
|
|
566
|
+
view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, position)));
|
|
567
|
+
instance.navigateFootnote('definition');
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
events.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
571
|
+
}
|
|
572
|
+
return true;
|
|
573
|
+
};
|
|
574
|
+
instance.footnoteKeyHandler = (event) => {
|
|
575
|
+
if ((event.key === 'Enter' || event.key === ' ') &&
|
|
576
|
+
activateFootnote(event))
|
|
577
|
+
event.stopPropagation();
|
|
578
|
+
};
|
|
579
|
+
root.addEventListener('keydown', instance.footnoteKeyHandler, true);
|
|
523
580
|
instance.clickHandler = (event) => {
|
|
581
|
+
if (activateFootnote(event))
|
|
582
|
+
return;
|
|
524
583
|
const anchor = event.target.closest('a[href]');
|
|
525
584
|
if (anchor && event.metaKey) {
|
|
526
585
|
event.preventDefault();
|
|
@@ -532,14 +591,39 @@ export class InkKitEditor {
|
|
|
532
591
|
if (instance.formatType === 'txt')
|
|
533
592
|
return;
|
|
534
593
|
const view = instance.editor.ctx.get(editorViewCtx);
|
|
535
|
-
const fragment = view.state.selection.content().content;
|
|
594
|
+
const fragment = commentSelectionContent(view.state.doc, view.state.selection.from, view.state.selection.to, view.state.selection.content().content);
|
|
595
|
+
const expanded = selectionContent(view.state.doc, fragment);
|
|
536
596
|
let hasImages = false;
|
|
537
|
-
|
|
597
|
+
expanded.descendants((node) => {
|
|
538
598
|
if (node.type.name === 'image')
|
|
539
599
|
hasImages = true;
|
|
540
600
|
});
|
|
541
|
-
if (!hasImages)
|
|
601
|
+
if (!hasImages) {
|
|
602
|
+
if (!event.clipboardData || fragment.size === 0)
|
|
603
|
+
return;
|
|
604
|
+
event.preventDefault();
|
|
605
|
+
event.stopPropagation();
|
|
606
|
+
try {
|
|
607
|
+
instance.snapshot();
|
|
608
|
+
const content = expanded;
|
|
609
|
+
const valid = fragment.firstChild?.isInline
|
|
610
|
+
? Fragment.from(view.state.schema.nodes.paragraph.create(null, fragment))
|
|
611
|
+
: fragment;
|
|
612
|
+
const markdown = selectionMarkdown(instance.editor.ctx, view.state.doc, valid);
|
|
613
|
+
const output = clipboardContent(content, view.state.schema);
|
|
614
|
+
event.clipboardData.setData('text/plain', output.text);
|
|
615
|
+
const metadata = referenceMetadata(instance.editor.ctx, view.state.doc, valid);
|
|
616
|
+
event.clipboardData.setData('text/html', metadata != null
|
|
617
|
+
? withReferenceMetadata(output.html, metadata)
|
|
618
|
+
: output.html);
|
|
619
|
+
if (event.type === 'cut')
|
|
620
|
+
view.dispatch(view.state.tr.deleteSelection().scrollIntoView());
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
events.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
624
|
+
}
|
|
542
625
|
return;
|
|
626
|
+
}
|
|
543
627
|
event.preventDefault();
|
|
544
628
|
event.stopPropagation();
|
|
545
629
|
if (events.clipboard) {
|
|
@@ -564,7 +648,7 @@ export class InkKitEditor {
|
|
|
564
648
|
}
|
|
565
649
|
else {
|
|
566
650
|
const container = document.createElement('div');
|
|
567
|
-
container.textContent = clipboardText(
|
|
651
|
+
container.textContent = clipboardText(expanded);
|
|
568
652
|
event.clipboardData?.setData('text/plain', container.textContent ?? '');
|
|
569
653
|
event.clipboardData?.setData('text/html', container.outerHTML);
|
|
570
654
|
events.error?.(new InkKitError('image-unavailable', 'The host must provide a clipboard handler to copy images'));
|
|
@@ -938,6 +1022,9 @@ export class InkKitEditor {
|
|
|
938
1022
|
case 'strikethrough':
|
|
939
1023
|
run(toggleStrikethroughCommand.key);
|
|
940
1024
|
break;
|
|
1025
|
+
case 'highlight':
|
|
1026
|
+
run(toggleHighlightCommand.key);
|
|
1027
|
+
break;
|
|
941
1028
|
case 'code':
|
|
942
1029
|
this.toggleInlineCode();
|
|
943
1030
|
break;
|
|
@@ -961,6 +1048,17 @@ export class InkKitEditor {
|
|
|
961
1048
|
toggleTaskList(this.editor.ctx);
|
|
962
1049
|
break;
|
|
963
1050
|
case 'link': {
|
|
1051
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1052
|
+
const reference = view.state.selection.$from
|
|
1053
|
+
.marks()
|
|
1054
|
+
.find((mark) => mark.type.name === 'link' && mark.attrs.identifier) ??
|
|
1055
|
+
view.state.doc
|
|
1056
|
+
.nodeAt(view.state.selection.from)
|
|
1057
|
+
?.marks.find((mark) => mark.type.name === 'link' && mark.attrs.identifier);
|
|
1058
|
+
if (reference && typeof arg === 'string') {
|
|
1059
|
+
this.editReferenceDefinition(String(reference.attrs.identifier), arg);
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
964
1062
|
const payload = typeof arg === 'string' ? { href: arg } : {};
|
|
965
1063
|
const toggle = () => run(toggleLinkCommand.key, payload);
|
|
966
1064
|
if (!state.marks.includes('link') ||
|
|
@@ -971,4 +1069,95 @@ export class InkKitEditor {
|
|
|
971
1069
|
}
|
|
972
1070
|
this.focus();
|
|
973
1071
|
}
|
|
1072
|
+
editReferenceDefinition(label, destination, title) {
|
|
1073
|
+
this.assertCurrent();
|
|
1074
|
+
if (this.formatType === 'txt')
|
|
1075
|
+
return false;
|
|
1076
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1077
|
+
const definitions = referenceDefinitions(view.state.doc);
|
|
1078
|
+
const definition = definitions.get(normaliseLabel(label)) ??
|
|
1079
|
+
[...definitions.values()].find(({ node }) => normaliseLabel(String(node.attrs.label)) === normaliseLabel(label));
|
|
1080
|
+
if (!definition)
|
|
1081
|
+
return false;
|
|
1082
|
+
view.dispatch(view.state.tr.setNodeMarkup(definition.pos, undefined, {
|
|
1083
|
+
...definition.node.attrs,
|
|
1084
|
+
url: destination,
|
|
1085
|
+
title: title ?? definition.node.attrs.title,
|
|
1086
|
+
}));
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
insertFootnote(label) {
|
|
1090
|
+
this.assertCurrent();
|
|
1091
|
+
if (this.formatType === 'txt')
|
|
1092
|
+
return;
|
|
1093
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1094
|
+
const definitions = footnoteDefinitions(view.state.doc);
|
|
1095
|
+
if (label == null) {
|
|
1096
|
+
let number = 1;
|
|
1097
|
+
const labels = new Set(definitions.keys());
|
|
1098
|
+
for (const match of this.snapshot().text.matchAll(/\[\^([^\]\r\n]+)\]/g))
|
|
1099
|
+
labels.add(normaliseLabel(match[1]));
|
|
1100
|
+
view.state.doc.descendants((node) => {
|
|
1101
|
+
if (node.type.name === 'footnote_reference')
|
|
1102
|
+
labels.add(String(node.attrs.identifier));
|
|
1103
|
+
});
|
|
1104
|
+
while (labels.has(normaliseLabel(String(number))))
|
|
1105
|
+
number++;
|
|
1106
|
+
label = String(number);
|
|
1107
|
+
}
|
|
1108
|
+
if (!label || /[\s\[\]\\]/.test(label) || label.length > 999)
|
|
1109
|
+
throw new Error('A footnote label must be 1–999 characters without whitespace, brackets or backslashes');
|
|
1110
|
+
const identifier = normaliseLabel(label);
|
|
1111
|
+
const reference = view.state.schema.nodes.footnote_reference.create({
|
|
1112
|
+
label,
|
|
1113
|
+
identifier,
|
|
1114
|
+
});
|
|
1115
|
+
const tr = view.state.tr.insert(view.state.selection.to, reference);
|
|
1116
|
+
if (!definitions.has(identifier)) {
|
|
1117
|
+
const definition = view.state.schema.nodes.footnote_definition.create({ label, identifier }, view.state.schema.nodes.paragraph.create());
|
|
1118
|
+
const position = tr.doc.content.size;
|
|
1119
|
+
tr.insert(position, definition).setSelection(TextSelection.create(tr.doc, position + 2));
|
|
1120
|
+
}
|
|
1121
|
+
view.dispatch(tr.scrollIntoView());
|
|
1122
|
+
this.focus();
|
|
1123
|
+
}
|
|
1124
|
+
navigateFootnote(target) {
|
|
1125
|
+
this.assertCurrent();
|
|
1126
|
+
if (this.formatType === 'txt')
|
|
1127
|
+
return false;
|
|
1128
|
+
const view = this.editor.ctx.get(editorViewCtx);
|
|
1129
|
+
const { $from } = view.state.selection;
|
|
1130
|
+
let identifier;
|
|
1131
|
+
for (let depth = $from.depth; depth > 0; depth--) {
|
|
1132
|
+
const node = $from.node(depth);
|
|
1133
|
+
if (node.type.name === 'footnote_definition')
|
|
1134
|
+
identifier = String(node.attrs.identifier);
|
|
1135
|
+
}
|
|
1136
|
+
const at = view.state.doc.nodeAt(view.state.selection.from);
|
|
1137
|
+
const adjacent = at?.type.name === 'footnote_reference' ? at : $from.nodeBefore;
|
|
1138
|
+
if (adjacent?.type.name === 'footnote_reference')
|
|
1139
|
+
identifier = String(adjacent.attrs.identifier);
|
|
1140
|
+
if (!identifier)
|
|
1141
|
+
return false;
|
|
1142
|
+
let position;
|
|
1143
|
+
if (target === 'definition') {
|
|
1144
|
+
const found = footnoteDefinitions(view.state.doc).get(identifier);
|
|
1145
|
+
if (found)
|
|
1146
|
+
position = found.pos + 2;
|
|
1147
|
+
}
|
|
1148
|
+
else
|
|
1149
|
+
view.state.doc.descendants((node, pos) => {
|
|
1150
|
+
if (position == null &&
|
|
1151
|
+
node.type.name === 'footnote_reference' &&
|
|
1152
|
+
node.attrs.identifier === identifier)
|
|
1153
|
+
position = pos;
|
|
1154
|
+
});
|
|
1155
|
+
if (position == null)
|
|
1156
|
+
return false;
|
|
1157
|
+
view.dispatch(view.state.tr
|
|
1158
|
+
.setSelection(TextSelection.near(view.state.doc.resolve(position)))
|
|
1159
|
+
.scrollIntoView());
|
|
1160
|
+
this.focus();
|
|
1161
|
+
return true;
|
|
1162
|
+
}
|
|
974
1163
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const remarkInlineHighlightPlugin: import("@milkdown/kit/utils").$Remark<"inkkitInlineHighlight", unknown>;
|
|
2
|
+
export declare const highlightSchema: import("@milkdown/kit/utils").$MarkSchema<"highlight">;
|
|
3
|
+
export declare const toggleHighlightCommand: import("@milkdown/kit/utils").$Command<unknown>;
|
|
4
|
+
export declare const highlightInputRule: import("@milkdown/kit/utils").$InputRule;
|
|
5
|
+
export declare const highlightKeymap: import("@milkdown/kit/utils").$UserKeymap<"highlightKeymap", "ToggleHighlight">;
|
|
6
|
+
export declare const inlineHighlight: (import("@milkdown/kit/utils").$Command<unknown> | import("@milkdown/kit/utils").$Ctx<unknown, "inkkitInlineHighlight"> | import("@milkdown/kit/utils").$Ctx<import("@milkdown/kit/utils").GetMarkSchema, "highlight"> | import("@milkdown/kit/utils").$Ctx<import("@milkdown/kit/utils").KeymapConfig<"ToggleHighlight">, "highlightKeymapKeymap"> | import("@milkdown/kit/utils").$InputRule | import("@milkdown/kit/utils").$Mark | import("@milkdown/kit/utils").$Shortcut | import("@milkdown/kit/ctx").MilkdownPlugin)[];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { commandsCtx, remarkCtx } from '@milkdown/kit/core';
|
|
2
|
+
import { markRule } from '@milkdown/kit/prose';
|
|
3
|
+
import { toggleMark } from '@milkdown/kit/prose/commands';
|
|
4
|
+
import { InputRule } from '@milkdown/kit/prose/inputrules';
|
|
5
|
+
import { EditorState, TextSelection } from '@milkdown/kit/prose/state';
|
|
6
|
+
import { $command, $inputRule, $markSchema, $remark, $useKeymap, } from '@milkdown/kit/utils';
|
|
7
|
+
import { highlightMark } from 'micromark-extension-highlight-mark';
|
|
8
|
+
import { highlightMarkFromMarkdown, highlightMarkToMarkdown, } from 'mdast-util-highlight-mark';
|
|
9
|
+
// The current serializer's attention handling encodes ambiguous whitespace and
|
|
10
|
+
// punctuation boundaries; the highlight provider predates that API.
|
|
11
|
+
const highlight = (node, _parent, state, info) => {
|
|
12
|
+
const exit = state.enter('phrasing');
|
|
13
|
+
const value = state.containerPhrasing({ type: 'root', children: [node] }, info);
|
|
14
|
+
exit();
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
highlight.attention = () => ({
|
|
18
|
+
construct: 'highlight',
|
|
19
|
+
markers: ['='],
|
|
20
|
+
sizes: [2],
|
|
21
|
+
});
|
|
22
|
+
highlight.peek = () => '=';
|
|
23
|
+
function remarkInlineHighlight() {
|
|
24
|
+
const data = this.data();
|
|
25
|
+
(data.micromarkExtensions ??= []).push(highlightMark());
|
|
26
|
+
(data.fromMarkdownExtensions ??= []).push(highlightMarkFromMarkdown);
|
|
27
|
+
(data.toMarkdownExtensions ??= []).push({
|
|
28
|
+
...highlightMarkToMarkdown,
|
|
29
|
+
handlers: { highlight },
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export const remarkInlineHighlightPlugin = $remark('inkkitInlineHighlight', () => remarkInlineHighlight);
|
|
33
|
+
export const highlightSchema = $markSchema('highlight', () => ({
|
|
34
|
+
priority: 45,
|
|
35
|
+
parseDOM: [{ tag: 'mark' }],
|
|
36
|
+
toDOM: () => ['mark', 0],
|
|
37
|
+
parseMarkdown: {
|
|
38
|
+
match: (node) => node.type === 'highlight',
|
|
39
|
+
runner(state, node, type) {
|
|
40
|
+
state.openMark(type);
|
|
41
|
+
state.next(node.children);
|
|
42
|
+
state.closeMark(type);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
toMarkdown: {
|
|
46
|
+
match: (mark) => mark.type.name === 'highlight',
|
|
47
|
+
runner(state, mark, node) {
|
|
48
|
+
if (node.isText && /^\s*$/.test(node.text ?? '')) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
state.withMark(mark, 'highlight');
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
55
|
+
export const toggleHighlightCommand = $command('ToggleHighlight', (ctx) => () => toggleMark(highlightSchema.type(ctx)));
|
|
56
|
+
export const highlightInputRule = $inputRule((ctx) => {
|
|
57
|
+
const pattern = /==([^\n]+)==$/;
|
|
58
|
+
const rule = markRule(pattern, highlightSchema.type(ctx));
|
|
59
|
+
const apply = rule.handler;
|
|
60
|
+
return new InputRule(pattern, (state, match, start, end) => {
|
|
61
|
+
const parentStart = state.selection.$from.start();
|
|
62
|
+
const prefix = state.selection.$from.parent.textBetween(0, start - parentStart);
|
|
63
|
+
const source = prefix + match[0];
|
|
64
|
+
const tree = ctx.get(remarkCtx).parse(source);
|
|
65
|
+
let valid = false;
|
|
66
|
+
const walk = (node) => {
|
|
67
|
+
if (node.type === 'highlight' &&
|
|
68
|
+
node.position?.start.offset === prefix.length &&
|
|
69
|
+
node.position.end.offset === source.length)
|
|
70
|
+
valid = true;
|
|
71
|
+
if ('children' in node)
|
|
72
|
+
node.children.forEach(walk);
|
|
73
|
+
};
|
|
74
|
+
walk(tree);
|
|
75
|
+
if (!valid)
|
|
76
|
+
return null;
|
|
77
|
+
const { from, to } = state.selection;
|
|
78
|
+
const typed = match[0].slice(from - start);
|
|
79
|
+
if (typed.length <= 1 && state.selection.empty)
|
|
80
|
+
return apply(state, match, start, end);
|
|
81
|
+
// Milkdown's mark rule expects only the final character to be absent.
|
|
82
|
+
// Prepare buffered input without applying plugins or dispatching twice.
|
|
83
|
+
const tr = state.tr.insertText(typed.slice(0, -1), from, to);
|
|
84
|
+
const preparedEnd = from + typed.length - 1;
|
|
85
|
+
const prepared = EditorState.create({
|
|
86
|
+
schema: state.schema,
|
|
87
|
+
doc: tr.doc,
|
|
88
|
+
selection: TextSelection.create(tr.doc, preparedEnd),
|
|
89
|
+
storedMarks: state.storedMarks,
|
|
90
|
+
});
|
|
91
|
+
const transformed = apply(prepared, match, start, preparedEnd);
|
|
92
|
+
if (!transformed)
|
|
93
|
+
return null;
|
|
94
|
+
transformed.steps.forEach((step) => tr.step(step));
|
|
95
|
+
tr.setSelection(TextSelection.create(tr.doc, transformed.selection.from, transformed.selection.to));
|
|
96
|
+
tr.setStoredMarks(transformed.storedMarks);
|
|
97
|
+
return tr;
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
export const highlightKeymap = $useKeymap('highlightKeymap', {
|
|
101
|
+
ToggleHighlight: {
|
|
102
|
+
shortcuts: ['Mod-Shift-h'],
|
|
103
|
+
command: (ctx) => () => ctx.get(commandsCtx).call(toggleHighlightCommand.key),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
export const inlineHighlight = [
|
|
107
|
+
remarkInlineHighlightPlugin,
|
|
108
|
+
highlightSchema,
|
|
109
|
+
toggleHighlightCommand,
|
|
110
|
+
highlightInputRule,
|
|
111
|
+
highlightKeymap,
|
|
112
|
+
].flat();
|
package/dist/literals.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import type { Literal } from 'mdast';
|
|
2
|
+
import { type CommentSpan } from './comments.js';
|
|
2
3
|
interface InkKitLiteral extends Literal {
|
|
3
4
|
type: 'inkkitLiteral';
|
|
5
|
+
comments?: CommentSpan[];
|
|
4
6
|
}
|
|
5
7
|
declare module 'mdast' {
|
|
8
|
+
interface BlockContentMap {
|
|
9
|
+
inkkitLiteral: InkKitLiteral;
|
|
10
|
+
}
|
|
6
11
|
interface RootContentMap {
|
|
7
12
|
inkkitLiteral: InkKitLiteral;
|
|
8
13
|
}
|
package/dist/literals.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { $node, $remark } from '@milkdown/kit/utils';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
'image',
|
|
5
|
-
'imageReference',
|
|
6
|
-
'definition',
|
|
7
|
-
'linkReference',
|
|
8
|
-
]);
|
|
2
|
+
import { commentSource, literalCommentSpans, } from './comments.js';
|
|
3
|
+
const unsupported = new Set(['html', 'image', 'imageReference']);
|
|
9
4
|
function restoreInlineBreaks(node) {
|
|
10
5
|
if ((node.type === 'paragraph' || node.type === 'heading') &&
|
|
11
6
|
node.children.length > 1) {
|
|
@@ -37,6 +32,18 @@ function containsUnsupported(node, images, inline = false) {
|
|
|
37
32
|
('children' in node &&
|
|
38
33
|
node.children.some((child) => containsUnsupported(child, images, inline || node.type === 'paragraph' || node.type === 'heading'))));
|
|
39
34
|
}
|
|
35
|
+
function containsUnresolved(node, source) {
|
|
36
|
+
if (node.type === 'text') {
|
|
37
|
+
const start = node.position?.start.offset;
|
|
38
|
+
const end = node.position?.end.offset;
|
|
39
|
+
if (start == null || end == null)
|
|
40
|
+
return false;
|
|
41
|
+
const raw = source.slice(start, end);
|
|
42
|
+
return /(?<!\\)\[\^[^\]\n]+\]|(?<!\\)\[[^\]\n]+\](?:\[[^\]\n]*\])?/.test(raw);
|
|
43
|
+
}
|
|
44
|
+
return ('children' in node &&
|
|
45
|
+
node.children.some((child) => containsUnresolved(child, source)));
|
|
46
|
+
}
|
|
40
47
|
// Keep syntax we cannot faithfully edit as literal text, without making the rest
|
|
41
48
|
// of the document a source editor or allowing HTML and image network requests.
|
|
42
49
|
export function createLiteralPreservation(images) {
|
|
@@ -46,6 +53,29 @@ export function createLiteralPreservation(images) {
|
|
|
46
53
|
const source = String(file.value).replace(/^\uFEFF/, '');
|
|
47
54
|
const frontmatter = /^(---|\+\+\+)\r?\n[\s\S]*?\r?\n\1(?:\r?\n|$)/.exec(source);
|
|
48
55
|
const frontmatterEnd = frontmatter?.[0].length ?? 0;
|
|
56
|
+
const preserveBlock = (node, depth = 0) => {
|
|
57
|
+
if (node.type === 'footnoteDefinition') {
|
|
58
|
+
node.children = node.children.map((child) => preserveBlock(child, depth + 1));
|
|
59
|
+
return node;
|
|
60
|
+
}
|
|
61
|
+
const start = node.position?.start.offset;
|
|
62
|
+
const end = node.position?.end.offset;
|
|
63
|
+
if (start == null ||
|
|
64
|
+
end == null ||
|
|
65
|
+
(!containsUnsupported(node, images) &&
|
|
66
|
+
!containsUnresolved(node, source)))
|
|
67
|
+
return node;
|
|
68
|
+
let raw = source.slice(start, end);
|
|
69
|
+
// Definition indentation is restored by the footnote serializer.
|
|
70
|
+
for (let index = 0; index < depth; index++)
|
|
71
|
+
raw = raw.replace(/\r?\n(?: {4}|\t)/g, '\n');
|
|
72
|
+
return {
|
|
73
|
+
type: 'inkkitLiteral',
|
|
74
|
+
value: raw,
|
|
75
|
+
comments: literalCommentSpans(raw),
|
|
76
|
+
position: node.position,
|
|
77
|
+
};
|
|
78
|
+
};
|
|
49
79
|
const blocks = [];
|
|
50
80
|
if (frontmatter)
|
|
51
81
|
blocks.push({
|
|
@@ -81,16 +111,9 @@ export function createLiteralPreservation(images) {
|
|
|
81
111
|
});
|
|
82
112
|
continue;
|
|
83
113
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
blocks.push({
|
|
88
|
-
type: 'inkkitLiteral',
|
|
89
|
-
value: raw,
|
|
90
|
-
position: node.position,
|
|
91
|
-
});
|
|
92
|
-
else
|
|
93
|
-
blocks.push(node);
|
|
114
|
+
if (node.type === 'inkkitLiteral')
|
|
115
|
+
node.comments = literalCommentSpans(node.value);
|
|
116
|
+
blocks.push(preserveBlock(node));
|
|
94
117
|
}
|
|
95
118
|
tree.children = blocks;
|
|
96
119
|
});
|
|
@@ -98,7 +121,7 @@ export function createLiteralPreservation(images) {
|
|
|
98
121
|
export const preserveLiterals = createLiteralPreservation(false);
|
|
99
122
|
export const literalBlock = $node('literal_markdown', () => ({
|
|
100
123
|
group: 'block',
|
|
101
|
-
content: 'text*',
|
|
124
|
+
content: '(text | comment_inline)*',
|
|
102
125
|
marks: '',
|
|
103
126
|
code: true,
|
|
104
127
|
defining: true,
|
|
@@ -114,14 +137,33 @@ export const literalBlock = $node('literal_markdown', () => ({
|
|
|
114
137
|
match: (node) => node.type === 'inkkitLiteral',
|
|
115
138
|
runner: (state, node, type) => {
|
|
116
139
|
state.openNode(type);
|
|
117
|
-
|
|
140
|
+
const value = String(node.value ?? '');
|
|
141
|
+
let cursor = 0;
|
|
142
|
+
for (const span of node.comments ?? []) {
|
|
143
|
+
if (span.start > cursor)
|
|
144
|
+
state.addText(value.slice(cursor, span.start));
|
|
145
|
+
state.openNode(state.schema.nodes.comment_inline, {
|
|
146
|
+
syntax: span.syntax,
|
|
147
|
+
block: Boolean(span.block),
|
|
148
|
+
});
|
|
149
|
+
if (span.value)
|
|
150
|
+
state.addText(span.value);
|
|
151
|
+
state.closeNode();
|
|
152
|
+
cursor = span.end;
|
|
153
|
+
}
|
|
154
|
+
if (cursor < value.length)
|
|
155
|
+
state.addText(value.slice(cursor));
|
|
118
156
|
state.closeNode();
|
|
119
157
|
},
|
|
120
158
|
},
|
|
121
159
|
toMarkdown: {
|
|
122
160
|
match: (node) => node.type.name === 'literal_markdown',
|
|
123
161
|
runner: (state, node) => {
|
|
124
|
-
|
|
162
|
+
let source = '';
|
|
163
|
+
node.forEach((child) => {
|
|
164
|
+
source += child.isText ? child.text : commentSource(child);
|
|
165
|
+
});
|
|
166
|
+
state.addNode('html', undefined, source);
|
|
125
167
|
},
|
|
126
168
|
},
|
|
127
169
|
}));
|