@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 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 GFM tables. Unsupported constructs remain literal Markdown. TXT mode treats all text literally. Images require a host adapter. Footnote editing and RTF files are not included in 0.0.1.
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) => blocks.push(nodeText(node, missingImages)));
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
- // Keep authored spacer paragraphs; references remain literal instead of becoming inline links.
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
- : serialize(this.editor.ctx, schema.topNodeType.create(null, valid));
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: { handleKeyDown: (view, event) => this.keys(view, event) },
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
- fragment.descendants((node) => {
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(fragment);
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
  }
@@ -3,6 +3,9 @@ interface InkKitLiteral extends Literal {
3
3
  type: 'inkkitLiteral';
4
4
  }
5
5
  declare module 'mdast' {
6
+ interface BlockContentMap {
7
+ inkkitLiteral: InkKitLiteral;
8
+ }
6
9
  interface RootContentMap {
7
10
  inkkitLiteral: InkKitLiteral;
8
11
  }
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
- const raw = source.slice(start, end);
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
- slice = ProseParser.fromSchema(schema).parseSlice(container, {
275
- preserveWhitespace: true,
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 (!selection.empty &&
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.