@aicayzer/inkkit 0.0.2 → 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.
@@ -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,6 +1,8 @@
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' {
6
8
  interface BlockContentMap {
package/dist/literals.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { $node, $remark } from '@milkdown/kit/utils';
2
+ import { commentSource, literalCommentSpans, } from './comments.js';
2
3
  const unsupported = new Set(['html', 'image', 'imageReference']);
3
4
  function restoreInlineBreaks(node) {
4
5
  if ((node.type === 'paragraph' || node.type === 'heading') &&
@@ -68,7 +69,12 @@ export function createLiteralPreservation(images) {
68
69
  // Definition indentation is restored by the footnote serializer.
69
70
  for (let index = 0; index < depth; index++)
70
71
  raw = raw.replace(/\r?\n(?: {4}|\t)/g, '\n');
71
- return { type: 'inkkitLiteral', value: raw, position: node.position };
72
+ return {
73
+ type: 'inkkitLiteral',
74
+ value: raw,
75
+ comments: literalCommentSpans(raw),
76
+ position: node.position,
77
+ };
72
78
  };
73
79
  const blocks = [];
74
80
  if (frontmatter)
@@ -105,6 +111,8 @@ export function createLiteralPreservation(images) {
105
111
  });
106
112
  continue;
107
113
  }
114
+ if (node.type === 'inkkitLiteral')
115
+ node.comments = literalCommentSpans(node.value);
108
116
  blocks.push(preserveBlock(node));
109
117
  }
110
118
  tree.children = blocks;
@@ -113,7 +121,7 @@ export function createLiteralPreservation(images) {
113
121
  export const preserveLiterals = createLiteralPreservation(false);
114
122
  export const literalBlock = $node('literal_markdown', () => ({
115
123
  group: 'block',
116
- content: 'text*',
124
+ content: '(text | comment_inline)*',
117
125
  marks: '',
118
126
  code: true,
119
127
  defining: true,
@@ -129,14 +137,33 @@ export const literalBlock = $node('literal_markdown', () => ({
129
137
  match: (node) => node.type === 'inkkitLiteral',
130
138
  runner: (state, node, type) => {
131
139
  state.openNode(type);
132
- 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));
133
156
  state.closeNode();
134
157
  },
135
158
  },
136
159
  toMarkdown: {
137
160
  match: (node) => node.type.name === 'literal_markdown',
138
161
  runner: (state, node) => {
139
- 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);
140
167
  },
141
168
  },
142
169
  }));
package/dist/paste.js CHANGED
@@ -179,6 +179,7 @@ export class PasteController {
179
179
  'H5',
180
180
  'H6',
181
181
  'STRONG',
182
+ 'MARK',
182
183
  'B',
183
184
  'EM',
184
185
  'I',
package/dist/preserve.js CHANGED
@@ -10,7 +10,7 @@ function ownShape(node) {
10
10
  ? undefined
11
11
  : value);
12
12
  }
13
- function retainText(raw, before, after) {
13
+ function retainText(raw, before, after, quoteIndent = false) {
14
14
  before = before.replace(/\r\n?/g, '\n');
15
15
  after = after.replace(/\r\n?/g, '\n');
16
16
  let prefix = 0;
@@ -26,6 +26,14 @@ function retainText(raw, before, after) {
26
26
  const offsets = [0];
27
27
  let decoded = '';
28
28
  for (let index = 0; index < raw.length;) {
29
+ if (index > 0 && raw[index - 1] === '\n') {
30
+ const gutter = (quoteIndent ? /^(?:[ \t]*>[ \t]*)+/ : /^(?:[ \t]*>[ \t]?)+/).exec(raw.slice(index));
31
+ if (gutter) {
32
+ index += gutter[0].length;
33
+ offsets[offsets.length - 1] = index;
34
+ continue;
35
+ }
36
+ }
29
37
  const escaped = /^\\([!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~])/.exec(raw.slice(index));
30
38
  const entity = /^&(?:#[xX][\da-fA-F]+|#\d+|[A-Za-z][A-Za-z\d]+);/.exec(raw.slice(index));
31
39
  let value;
@@ -53,11 +61,22 @@ function retainText(raw, before, after) {
53
61
  for (let part = 0; part < value.length; part++)
54
62
  offsets.push(index);
55
63
  }
56
- if (decoded !== before)
57
- return after;
64
+ if (decoded !== before) {
65
+ // Continuation indentation is excluded from a quoted paragraph's text.
66
+ return quoteIndent ? after : retainText(raw, before, after, true);
67
+ }
58
68
  const inserted = after
59
69
  .slice(prefix, after.length - suffix)
60
- .replace(/[\\`*_[\]<>]/g, '\\$&');
70
+ .split('')
71
+ .map((character, index) => {
72
+ const at = prefix + index;
73
+ const delimiter = (character === '%' || character === '=') &&
74
+ (after[at - 1] === character || after[at + 1] === character);
75
+ return delimiter || /[\\`*_[\]<>]/.test(character)
76
+ ? `\\${character}`
77
+ : character;
78
+ })
79
+ .join('');
61
80
  return (raw.slice(0, offsets[prefix]) +
62
81
  inserted +
63
82
  raw.slice(offsets[before.length - suffix]));
@@ -218,6 +237,7 @@ function semanticSignature(doc) {
218
237
  [
219
238
  'referenceContent',
220
239
  'referenceType',
240
+ 'listPlaceholder',
221
241
  'marker',
222
242
  'id',
223
243
  'label',
@@ -1,6 +1,7 @@
1
1
  import { Fragment } from '@milkdown/kit/prose/model';
2
2
  import { serialize } from './dialect.js';
3
3
  import { normaliseLabel } from './references.js';
4
+ import { shareableFragment } from './comments.js';
4
5
  function key(node) {
5
6
  if (node.type.name === 'reference_definition')
6
7
  return `link:${node.attrs.identifier}`;
@@ -70,6 +71,8 @@ export function selectionMarkdown(ctx, doc, content) {
70
71
  return serialize(ctx, doc.type.create(null, selectionContent(doc, content)));
71
72
  }
72
73
  export function referenceMetadata(ctx, doc, content) {
74
+ doc = doc.copy(shareableFragment(doc.content));
75
+ content = shareableFragment(content);
73
76
  let references = false, index = 0;
74
77
  const rewrite = (node) => {
75
78
  if (key(node) ||
@@ -121,7 +124,8 @@ function allLabels(doc) {
121
124
  }
122
125
  const rawReferences = /(?<![\\!])\[([^\]]+)\](?:\[([^\]]*)\])?(?!\()/g;
123
126
  function isCode(node) {
124
- return (node.type.name === 'code_block' ||
127
+ return (node.type.name.startsWith('comment_') ||
128
+ node.type.name === 'code_block' ||
125
129
  node.marks.some((mark) => mark.type.name === 'inlineCode'));
126
130
  }
127
131
  // Rename only the incoming document. Definitions belong to the insertion's undo transaction.
package/dist/style.css CHANGED
@@ -562,3 +562,93 @@ body {
562
562
  font-size: 0.8em;
563
563
  opacity: 0.7;
564
564
  }
565
+
566
+ .ProseMirror mark {
567
+ background: color-mix(in srgb, #e5c041 38%, transparent);
568
+ color: inherit;
569
+ }
570
+
571
+ .ProseMirror .inkkit-callout {
572
+ --callout-color: var(--accent);
573
+ border-inline-start: 3px solid var(--callout-color);
574
+ padding: 0.65rem 1rem;
575
+ background: color-mix(in srgb, var(--callout-color) 7%, transparent);
576
+ }
577
+
578
+ .ProseMirror [data-inkkit-callout='TIP'] {
579
+ --callout-color: #30a46c;
580
+ }
581
+ .ProseMirror [data-inkkit-callout='IMPORTANT'] {
582
+ --callout-color: #8e4ec6;
583
+ }
584
+ .ProseMirror [data-inkkit-callout='WARNING'] {
585
+ --callout-color: #d28b00;
586
+ }
587
+ .ProseMirror [data-inkkit-callout='CAUTION'] {
588
+ --callout-color: #e5484d;
589
+ }
590
+
591
+ .inkkit-callout-header {
592
+ display: flex;
593
+ align-items: baseline;
594
+ gap: 0.75rem;
595
+ }
596
+
597
+ [data-inkkit-callout-toggle] {
598
+ color: var(--muted);
599
+ background: transparent;
600
+ border: 0;
601
+ padding: 0.25rem;
602
+ cursor: pointer;
603
+ }
604
+
605
+ .inkkit-callout[data-inkkit-folded='true'] > .inkkit-callout-body {
606
+ display: none;
607
+ }
608
+
609
+ .inkkit-comment {
610
+ display: none;
611
+ }
612
+
613
+ [data-inkkit-comments-visible='true'] .inkkit-comment {
614
+ display: inline;
615
+ white-space: pre-wrap;
616
+ font-family: var(--mono);
617
+ color: var(--muted);
618
+ background: var(--code-bg);
619
+ }
620
+
621
+ [data-inkkit-comments-visible='true'] .inkkit-comment-block {
622
+ display: block;
623
+ padding: 0.5rem;
624
+ }
625
+
626
+ @media print {
627
+ html,
628
+ body,
629
+ #editor,
630
+ .ProseMirror {
631
+ height: auto;
632
+ min-height: 0;
633
+ overflow: visible;
634
+ }
635
+
636
+ .milkdown-table-block .table-wrapper,
637
+ .ProseMirror pre {
638
+ overflow: visible;
639
+ }
640
+
641
+ .inkkit-comment,
642
+ [data-inkkit-comments-visible='true'] .inkkit-comment,
643
+ [data-inkkit-callout-toggle],
644
+ .ProseMirror pre .copy,
645
+ .milkdown-table-block .handle,
646
+ .milkdown-table-block .button-group,
647
+ .milkdown-table-block .line-handle {
648
+ display: none !important;
649
+ }
650
+
651
+ .inkkit-callout[data-inkkit-folded='true'] > .inkkit-callout-body {
652
+ display: block !important;
653
+ }
654
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicayzer/inkkit",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "A preserving Markdown editor for offline web views and the web",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,18 +47,22 @@
47
47
  "dependencies": {
48
48
  "@milkdown/kit": "7.22.1",
49
49
  "lowlight": "^3.3.0",
50
+ "mdast-util-from-markdown": "^2.1.0",
50
51
  "mdast-util-gfm-autolink-literal": "^2.0.1",
51
52
  "mdast-util-gfm-footnote": "^2.1.0",
52
53
  "mdast-util-gfm-strikethrough": "^2.0.0",
53
54
  "mdast-util-gfm-table": "^2.0.0",
54
55
  "mdast-util-gfm-task-list-item": "^2.0.0",
56
+ "mdast-util-highlight-mark": "1.2.2",
55
57
  "mdast-util-to-markdown": "2.2.0",
56
58
  "micromark-extension-gfm-autolink-literal": "^2.1.0",
57
59
  "micromark-extension-gfm-footnote": "^2.1.0",
58
60
  "micromark-extension-gfm-strikethrough": "^2.1.0",
59
61
  "micromark-extension-gfm-table": "^2.1.1",
60
62
  "micromark-extension-gfm-task-list-item": "^2.1.0",
63
+ "micromark-extension-highlight-mark": "1.2.0",
61
64
  "micromark-util-normalize-identifier": "^2.0.1",
65
+ "micromark-util-types": "^2.0.3",
62
66
  "prosemirror-search": "^1.1.1"
63
67
  },
64
68
  "devDependencies": {