@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.
@@ -0,0 +1,487 @@
1
+ import { $node, $prose, $remark } from '@milkdown/kit/utils';
2
+ import { Fragment } from '@milkdown/kit/prose/model';
3
+ import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
4
+ import { fromMarkdown } from 'mdast-util-from-markdown';
5
+ const lineEnding = (code) => code === -3 || code === -4 || code === -5;
6
+ const whitespace = (code) => code === 32 || code === -1 || code === -2;
7
+ function commentData(effects) {
8
+ let open = false;
9
+ return {
10
+ consume(code) {
11
+ if (!open) {
12
+ effects.enter('inkkitCommentData');
13
+ open = true;
14
+ }
15
+ effects.consume(code);
16
+ },
17
+ close() {
18
+ if (open) {
19
+ effects.exit('inkkitCommentData');
20
+ open = false;
21
+ }
22
+ },
23
+ };
24
+ }
25
+ const inlineComment = function (effects, ok, nok) {
26
+ const data = commentData(effects);
27
+ return start;
28
+ function start(code) {
29
+ effects.enter('inkkitCommentInline');
30
+ data.consume(code);
31
+ return second;
32
+ }
33
+ function second(code) {
34
+ if (code !== 37)
35
+ return nok(code);
36
+ data.consume(code);
37
+ return body;
38
+ }
39
+ function body(code) {
40
+ if (code === null)
41
+ return nok(code);
42
+ if (lineEnding(code)) {
43
+ data.close();
44
+ effects.enter('lineEnding');
45
+ effects.consume(code);
46
+ effects.exit('lineEnding');
47
+ return body;
48
+ }
49
+ data.consume(code);
50
+ return code === 37 ? closing : body;
51
+ }
52
+ function closing(code) {
53
+ if (code !== 37)
54
+ return body(code);
55
+ data.consume(code);
56
+ data.close();
57
+ effects.exit('inkkitCommentInline');
58
+ return ok;
59
+ }
60
+ };
61
+ // A flow construct owns blank lines and Markdown-looking text inside a block comment.
62
+ const blockComment = function (effects, ok, nok) {
63
+ const data = commentData(effects);
64
+ return start;
65
+ function start(code) {
66
+ effects.enter('inkkitCommentBlock');
67
+ data.consume(code);
68
+ return second;
69
+ }
70
+ function second(code) {
71
+ if (code !== 37)
72
+ return nok(code);
73
+ data.consume(code);
74
+ return openingEnd;
75
+ }
76
+ function openingEnd(code) {
77
+ if (whitespace(code)) {
78
+ data.consume(code);
79
+ return openingEnd;
80
+ }
81
+ if (!lineEnding(code))
82
+ return nok(code);
83
+ data.close();
84
+ effects.enter('lineEnding');
85
+ effects.consume(code);
86
+ effects.exit('lineEnding');
87
+ return lineStart;
88
+ }
89
+ function lineStart(code) {
90
+ if (whitespace(code)) {
91
+ data.consume(code);
92
+ return lineStart;
93
+ }
94
+ if (code === 37) {
95
+ data.consume(code);
96
+ return closingSecond;
97
+ }
98
+ return body(code);
99
+ }
100
+ function body(code) {
101
+ if (code === null)
102
+ return nok(code);
103
+ if (lineEnding(code)) {
104
+ data.close();
105
+ effects.enter('lineEnding');
106
+ effects.consume(code);
107
+ effects.exit('lineEnding');
108
+ return lineStart;
109
+ }
110
+ data.consume(code);
111
+ return lineEnding(code) ? lineStart : body;
112
+ }
113
+ function closingSecond(code) {
114
+ if (code !== 37)
115
+ return body(code);
116
+ data.consume(code);
117
+ return closingEnd;
118
+ }
119
+ function closingEnd(code) {
120
+ if (whitespace(code)) {
121
+ data.consume(code);
122
+ return closingEnd;
123
+ }
124
+ if (code !== null && !lineEnding(code))
125
+ return body(code);
126
+ data.close();
127
+ effects.exit('inkkitCommentBlock');
128
+ return ok(code);
129
+ }
130
+ };
131
+ const syntax = {
132
+ text: { 37: { name: 'inkkitCommentInline', tokenize: inlineComment } },
133
+ flow: {
134
+ 37: { name: 'inkkitCommentBlock', tokenize: blockComment, concrete: true },
135
+ },
136
+ };
137
+ const from = {
138
+ enter: {
139
+ inkkitCommentInline(token) {
140
+ this.enter({ type: 'inkkitCommentInline', syntax: 'obsidian', value: '' }, token);
141
+ },
142
+ inkkitCommentBlock(token) {
143
+ this.enter({ type: 'inkkitCommentBlock', syntax: 'obsidian', value: '' }, token);
144
+ },
145
+ },
146
+ exit: {
147
+ inkkitCommentInline(token) {
148
+ const node = this.stack.at(-1);
149
+ node.value = this.sliceSerialize(token).slice(2, -2);
150
+ this.exit(token);
151
+ },
152
+ inkkitCommentBlock(token) {
153
+ const node = this.stack.at(-1);
154
+ node.value = this.sliceSerialize(token)
155
+ .replace(/^%%[^\S\r\n]*\r?\n/, '')
156
+ .replace(/(?:\r?\n)?[^\S\r\n]*%%[^\S\r\n]*$/, '');
157
+ this.exit(token);
158
+ },
159
+ },
160
+ };
161
+ export function commentSource(node) {
162
+ const body = node.textContent;
163
+ return node.attrs.syntax === 'html'
164
+ ? `<!--${body}-->`
165
+ : node.type.name === 'comment_block' || node.attrs.block
166
+ ? `%%\n${body}\n%%`
167
+ : `%%${body}%%`;
168
+ }
169
+ const handler = (node) => {
170
+ const comment = node;
171
+ const value = comment.value ?? '';
172
+ return comment.syntax === 'html'
173
+ ? `<!--${value}-->`
174
+ : comment.type === 'inkkitCommentBlock' || comment.block
175
+ ? `%%\n${value}\n%%`
176
+ : `%%${value}%%`;
177
+ };
178
+ function htmlSpans(value) {
179
+ const spans = [];
180
+ let inTag = false;
181
+ let quote = '';
182
+ for (let index = 0; index < value.length; index++) {
183
+ const character = value[index];
184
+ if (quote) {
185
+ if (character === quote)
186
+ quote = '';
187
+ continue;
188
+ }
189
+ if (inTag && (character === '"' || character === "'")) {
190
+ quote = character;
191
+ continue;
192
+ }
193
+ if (value.startsWith('<!--', index)) {
194
+ const end = value.indexOf('-->', index + 4);
195
+ if (end < 0)
196
+ break;
197
+ spans.push({
198
+ start: index,
199
+ end: end + 3,
200
+ syntax: 'html',
201
+ value: value.slice(index + 4, end),
202
+ });
203
+ index = end + 2;
204
+ inTag = false;
205
+ }
206
+ else if (character === '<')
207
+ inTag = true;
208
+ else if (character === '>')
209
+ inTag = false;
210
+ }
211
+ return spans;
212
+ }
213
+ // Parse preserved source again rather than hiding delimiter-looking strings in code or escapes.
214
+ export function literalCommentSpans(value) {
215
+ const tree = fromMarkdown(value, {
216
+ extensions: [syntax],
217
+ mdastExtensions: [from],
218
+ });
219
+ const spans = [];
220
+ const visit = (node) => {
221
+ const start = node.position?.start.offset;
222
+ const end = node.position?.end.offset;
223
+ if (start != null && end != null) {
224
+ if (node.type === 'inkkitCommentInline' ||
225
+ node.type === 'inkkitCommentBlock')
226
+ spans.push({
227
+ start,
228
+ end,
229
+ syntax: node.syntax,
230
+ value: node.value,
231
+ block: node.type === 'inkkitCommentBlock',
232
+ });
233
+ else if (node.type === 'html')
234
+ spans.push(...htmlSpans(value.slice(start, end)).map((span) => ({
235
+ ...span,
236
+ start: start + span.start,
237
+ end: start + span.end,
238
+ })));
239
+ }
240
+ if ('children' in node)
241
+ node.children.forEach(visit);
242
+ };
243
+ visit(tree);
244
+ return spans.sort((left, right) => left.start - right.start);
245
+ }
246
+ function remarkComments() {
247
+ const data = this.data();
248
+ (data.micromarkExtensions ??= []).push(syntax);
249
+ (data.fromMarkdownExtensions ??= []).push(from);
250
+ (data.toMarkdownExtensions ??= []).push({
251
+ handlers: { inkkitCommentInline: handler, inkkitCommentBlock: handler },
252
+ unsafe: [{ character: '%', inConstruct: 'phrasing', after: '%' }],
253
+ });
254
+ return (tree) => {
255
+ const visit = (node, inline, parentType) => {
256
+ if (parentType === 'listItem' && node.type === 'inkkitCommentBlock')
257
+ return {
258
+ type: 'paragraph',
259
+ children: [{ ...node, type: 'inkkitCommentInline', block: true }],
260
+ position: node.position,
261
+ };
262
+ if (node.type === 'html') {
263
+ const spans = htmlSpans(node.value);
264
+ if (spans.length === 1 &&
265
+ spans[0].start === 0 &&
266
+ spans[0].end === node.value.length)
267
+ return parentType === 'listItem'
268
+ ? {
269
+ type: 'paragraph',
270
+ children: [
271
+ {
272
+ type: 'inkkitCommentInline',
273
+ syntax: 'html',
274
+ value: spans[0].value,
275
+ position: node.position,
276
+ },
277
+ ],
278
+ position: node.position,
279
+ }
280
+ : {
281
+ type: inline ? 'inkkitCommentInline' : 'inkkitCommentBlock',
282
+ syntax: 'html',
283
+ value: spans[0].value,
284
+ position: node.position,
285
+ };
286
+ }
287
+ if ('children' in node)
288
+ node.children = node.children.map((child) => visit(child, node.type === 'paragraph' ||
289
+ node.type === 'heading' ||
290
+ node.type === 'tableCell' ||
291
+ node.type === 'link' ||
292
+ node.type === 'linkReference' ||
293
+ inline, node.type));
294
+ if (node.type === 'paragraph' &&
295
+ parentType !== 'listItem' &&
296
+ node.children.length === 1 &&
297
+ node.children[0]?.type === 'inkkitCommentInline')
298
+ return {
299
+ ...node.children[0],
300
+ type: 'inkkitCommentBlock',
301
+ position: node.position,
302
+ };
303
+ return node;
304
+ };
305
+ tree.children = tree.children.map((node) => visit(node, false));
306
+ };
307
+ }
308
+ export const remarkCommentsPlugin = $remark('remarkComments', () => remarkComments);
309
+ function commentNode(inline) {
310
+ const name = inline ? 'comment_inline' : 'comment_block';
311
+ const mdastType = inline ? 'inkkitCommentInline' : 'inkkitCommentBlock';
312
+ return $node(name, () => ({
313
+ inline,
314
+ group: inline ? 'inline' : 'block',
315
+ content: 'text*',
316
+ marks: '',
317
+ code: true,
318
+ defining: true,
319
+ attrs: { syntax: { default: 'html' }, block: { default: !inline } },
320
+ parseDOM: [
321
+ {
322
+ tag: `${inline ? 'span' : 'div'}[data-inkkit-comment]`,
323
+ preserveWhitespace: 'full',
324
+ getAttrs: (dom) => ({
325
+ syntax: dom.getAttribute('data-inkkit-comment') === 'obsidian'
326
+ ? 'obsidian'
327
+ : 'html',
328
+ }),
329
+ },
330
+ ],
331
+ toDOM: (node) => [
332
+ inline ? 'span' : 'div',
333
+ {
334
+ class: `inkkit-comment inkkit-comment-${inline ? 'inline' : 'block'}`,
335
+ 'data-inkkit-comment': node.attrs.syntax,
336
+ 'aria-label': 'Author comment',
337
+ },
338
+ 0,
339
+ ],
340
+ parseMarkdown: {
341
+ match: (node) => node.type === mdastType,
342
+ runner: (state, node, type) => {
343
+ state.openNode(type, {
344
+ syntax: node.syntax,
345
+ block: inline ? Boolean(node.block) : true,
346
+ });
347
+ if (node.value)
348
+ state.addText(String(node.value));
349
+ state.closeNode();
350
+ },
351
+ },
352
+ toMarkdown: {
353
+ match: (node) => node.type.name === name,
354
+ runner: (state, node) => {
355
+ state.addNode(mdastType, undefined, node.textContent, {
356
+ syntax: node.attrs.syntax,
357
+ block: node.attrs.block,
358
+ });
359
+ },
360
+ },
361
+ }));
362
+ }
363
+ export const commentInline = commentNode(true);
364
+ export const commentBlock = commentNode(false);
365
+ const visibilityKey = new PluginKey('inkkit-comments-visible');
366
+ export const commentVisibility = $prose(() => new Plugin({
367
+ key: visibilityKey,
368
+ state: {
369
+ init: () => false,
370
+ apply: (transaction, value) => transaction.getMeta(visibilityKey) ?? value,
371
+ },
372
+ view(view) {
373
+ const update = () => view.dom.setAttribute('data-inkkit-comments-visible', String(visibilityKey.getState(view.state) ?? false));
374
+ update();
375
+ return { update };
376
+ },
377
+ }));
378
+ export function setCommentVisibility(view, visible) {
379
+ if (visibilityKey.getState(view.state) === visible)
380
+ return;
381
+ view.dispatch(view.state.tr
382
+ .setMeta(visibilityKey, visible)
383
+ .setMeta('addToHistory', false));
384
+ }
385
+ export const comments = [commentInline, commentBlock, commentVisibility].flat();
386
+ export function isComment(node) {
387
+ return (node.type.name === 'comment_inline' || node.type.name === 'comment_block');
388
+ }
389
+ // Authored reference metadata can retain comments after their visible nodes are removed.
390
+ function commentProvenance(value) {
391
+ if (typeof value !== 'string')
392
+ return false;
393
+ const visit = (value) => {
394
+ if (Array.isArray(value))
395
+ return value.some(visit);
396
+ if (!value || typeof value !== 'object')
397
+ return false;
398
+ const node = value;
399
+ return (node.type === 'inkkitCommentInline' ||
400
+ node.type === 'inkkitCommentBlock' ||
401
+ (node.type === 'html' &&
402
+ typeof node.value === 'string' &&
403
+ htmlSpans(node.value).length > 0) ||
404
+ Boolean(node.children?.some(visit)));
405
+ };
406
+ try {
407
+ return visit(JSON.parse(value));
408
+ }
409
+ catch {
410
+ return true;
411
+ }
412
+ }
413
+ export function shareableFragment(content) {
414
+ const filtered = [];
415
+ const labelHasComments = (label) => typeof label === 'string' &&
416
+ (label.includes('%%') || label.includes('<!--')) &&
417
+ literalCommentSpans(label).length > 0;
418
+ content.forEach((node) => {
419
+ if (isComment(node))
420
+ return;
421
+ if (node.type.name === 'reference_definition' &&
422
+ labelHasComments(node.attrs.label))
423
+ return;
424
+ node = node.mark(node.marks.map((mark) => {
425
+ if (mark.type.name !== 'link')
426
+ return mark;
427
+ const clearTarget = labelHasComments(mark.attrs.label);
428
+ if (!clearTarget && !commentProvenance(mark.attrs.referenceContent))
429
+ return mark;
430
+ return mark.type.create({
431
+ ...mark.attrs,
432
+ referenceContent: null,
433
+ ...(clearTarget
434
+ ? { identifier: null, label: null, referenceType: null }
435
+ : {}),
436
+ });
437
+ }));
438
+ if (!node.childCount) {
439
+ filtered.push(node);
440
+ return;
441
+ }
442
+ const inner = shareableFragment(node.content);
443
+ let whitespaceOnly = true, removedComment = false;
444
+ inner.forEach((child) => {
445
+ if (!(child.isText && !child.text.trim()) &&
446
+ child.type.name !== 'hardbreak')
447
+ whitespaceOnly = false;
448
+ });
449
+ node.content.descendants((child) => {
450
+ if (isComment(child)) {
451
+ removedComment = true;
452
+ return false;
453
+ }
454
+ });
455
+ const commentOnly = removedComment && whitespaceOnly;
456
+ const paragraph = () => Fragment.from(node.type.schema.nodes.paragraph.create());
457
+ if ((inner.size === 0 || commentOnly) && node.content.size > 0) {
458
+ if ([
459
+ 'footnote_definition',
460
+ 'list_item',
461
+ 'table_cell',
462
+ 'table_header',
463
+ 'inkkit_callout',
464
+ ].includes(node.type.name))
465
+ filtered.push(node.copy(paragraph()));
466
+ return;
467
+ }
468
+ if (!node.type.validContent(inner)) {
469
+ if (node.type.name === 'list_item')
470
+ filtered.push(node.copy(paragraph().append(inner)));
471
+ return;
472
+ }
473
+ filtered.push(node.copy(inner));
474
+ });
475
+ return Fragment.fromArray(filtered);
476
+ }
477
+ // ProseMirror omits a parent wrapper when a selection is wholly inside its text.
478
+ export function commentSelectionContent(doc, from, to, content) {
479
+ const start = doc.resolve(from);
480
+ const end = doc.resolve(to);
481
+ for (let depth = start.depth; depth > 0; depth--) {
482
+ const node = start.node(depth);
483
+ if (isComment(node) && end.depth >= depth && node === end.node(depth))
484
+ return Fragment.from(node.copy(node.content.cut(from - start.start(depth), to - start.start(depth))));
485
+ }
486
+ return content;
487
+ }
package/dist/dialect.js CHANGED
@@ -4,6 +4,10 @@ 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';
8
+ import { callouts, remarkCalloutsPlugin } from './callouts.js';
9
+ import { inlineHighlight } from './inline-highlight.js';
10
+ import { comments, remarkCommentsPlugin } from './comments.js';
7
11
  import { gfmTableFromMarkdown, gfmTableToMarkdown } from 'mdast-util-gfm-table';
8
12
  import { gfmTable } from 'micromark-extension-gfm-table';
9
13
  import { defaultHandlers, } from 'mdast-util-to-markdown';
@@ -63,7 +67,6 @@ const link = (node, parent, state, info) => writesBare(node, parent)
63
67
  link.peek = (node, parent, state) => writesBare(node, parent)
64
68
  ? node.url[0]
65
69
  : defaultHandlers.link.peek(node, parent, state);
66
- // Footnotes remain literal until the editor supports editing their definitions.
67
70
  function remarkDialect() {
68
71
  const data = this.data();
69
72
  const add = (key, value) => {
@@ -85,7 +88,7 @@ function remarkDialect() {
85
88
  add('toMarkdownExtensions', { handlers: { link } });
86
89
  }
87
90
  export const remarkDialectPlugin = $remark('remarkDialect', () => remarkDialect);
88
- // Keep authored spacer paragraphs; references remain literal instead of becoming inline links.
91
+ // Reference links retain their authored form instead of expanding to inline links.
89
92
  const commonmarkWithLiteralReferences = commonmark.filter((plugin) => !remarkInlineLinkPlugin.includes(plugin));
90
93
  // The preset drops the final empty paragraph, which makes trailing spacers shrink on each reload.
91
94
  const preserveSpacerParagraphs = paragraphSchema.extendSchema((base) => (ctx) => {
@@ -251,9 +254,16 @@ export const tables = [
251
254
  ].flat();
252
255
  export function createDialect(images) {
253
256
  return [
257
+ remarkReferencesPlugin,
258
+ remarkCalloutsPlugin,
259
+ remarkCommentsPlugin,
254
260
  // Protect unsupported inline HTML before the empty-line plugin consumes break nodes.
255
261
  createLiteralPreservation(images),
256
262
  commonmarkWithLiteralReferences,
263
+ references,
264
+ callouts,
265
+ comments,
266
+ inlineHighlight,
257
267
  preserveSpacerParagraphs,
258
268
  visibleSoftbreaks,
259
269
  normalizedImages,
package/dist/editor.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type TableCommand } from './tables.js';
2
2
  import { type DocumentInput, type DocumentSnapshot, type ImageAdapter, type ClipboardInput, type ClipboardOutput } from './types.js';
3
- export type Mark = 'bold' | 'italic' | 'strikethrough' | 'code' | 'link';
3
+ export type Mark = 'bold' | 'italic' | 'strikethrough' | 'highlight' | 'code' | 'link';
4
4
  export type Block = {
5
5
  type: 'paragraph';
6
6
  } | {
@@ -21,7 +21,7 @@ export interface CaretState {
21
21
  /** One quote holds the whole selection; the block is what sits inside it. */
22
22
  quoted: boolean;
23
23
  }
24
- export type FormatCommand = 'heading' | 'paragraph' | 'bold' | 'italic' | 'strikethrough' | 'code' | 'codeBlock' | 'quote' | 'bulletList' | 'orderedList' | 'taskList' | 'link';
24
+ export type FormatCommand = 'heading' | 'paragraph' | 'bold' | 'italic' | 'strikethrough' | 'highlight' | 'code' | 'codeBlock' | 'quote' | 'bulletList' | 'orderedList' | 'taskList' | 'link';
25
25
  /** The bindings the app sets, by shortcut name; each runs a format command. */
26
26
  export type Keymap = Record<string, string[]>;
27
27
  export interface EditorEvents {
@@ -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;
@@ -66,6 +67,7 @@ export declare class InkKitEditor {
66
67
  snapshot(expectedGeneration?: number): DocumentSnapshot;
67
68
  clipboardSnapshot(all?: boolean): Promise<ClipboardOutput>;
68
69
  paste(input: ClipboardInput): Promise<void>;
70
+ setCommentsVisible(visible: boolean): void;
69
71
  pasteAsPlainText(text: string): void;
70
72
  table(command: TableCommand, options?: {
71
73
  rows?: number;
@@ -103,4 +105,7 @@ export declare class InkKitEditor {
103
105
  private quote;
104
106
  private unquote;
105
107
  format(command: FormatCommand, arg?: string | number): void;
108
+ editReferenceDefinition(label: string, destination: string, title?: string): boolean;
109
+ insertFootnote(label?: string): void;
110
+ navigateFootnote(target: 'definition' | 'reference'): boolean;
106
111
  }