@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/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 ? doc.content : selection.content().content;
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
- : serialize(this.editor.ctx, schema.topNodeType.create(null, valid));
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: { handleKeyDown: (view, event) => this.keys(view, event) },
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
- fragment.descendants((node) => {
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(fragment);
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();
@@ -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
- const unsupported = new Set([
3
- 'html',
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
- 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);
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
- state.addText(String(node.value ?? ''));
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
- state.addNode('html', undefined, node.textContent);
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
  }));