@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,367 @@
1
+ import { $node, $remark, $prose } from '@milkdown/kit/utils';
2
+ import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
3
+ import { linkSchema } from '@milkdown/kit/preset/commonmark';
4
+ import { normalizeIdentifier } from 'micromark-util-normalize-identifier';
5
+ import { gfmFootnote } from 'micromark-extension-gfm-footnote';
6
+ import { gfmFootnoteFromMarkdown, gfmFootnoteToMarkdown, } from 'mdast-util-gfm-footnote';
7
+ import { defaultHandlers } from 'mdast-util-to-markdown';
8
+ export function normaliseLabel(label) {
9
+ return normalizeIdentifier(label).toLowerCase();
10
+ }
11
+ function definitions(doc, type) {
12
+ const found = new Map();
13
+ doc.descendants((node, pos) => {
14
+ if (node.type.name !== type)
15
+ return true;
16
+ const identifier = normaliseLabel(String(node.attrs.identifier));
17
+ if (!found.has(identifier))
18
+ found.set(identifier, { node, pos });
19
+ return false;
20
+ });
21
+ return found;
22
+ }
23
+ export const referenceDefinitions = (doc) => definitions(doc, 'reference_definition');
24
+ export const footnoteDefinitions = (doc) => definitions(doc, 'footnote_definition');
25
+ function contentShape(value) {
26
+ const clean = (value) => {
27
+ if (Array.isArray(value))
28
+ return value.map(clean);
29
+ if (value && typeof value === 'object')
30
+ return Object.fromEntries(Object.entries(value)
31
+ .filter(([key]) => !['position', 'data', 'marker', 'isMark'].includes(key))
32
+ .sort(([left], [right]) => left.localeCompare(right))
33
+ .map(([key, value]) => [key, clean(value)]));
34
+ return value;
35
+ };
36
+ return JSON.stringify(clean(value));
37
+ }
38
+ const labelPlaceholder = 'inkkit-authored-label';
39
+ const authoredLinkReference = (untyped, parent, state, info) => {
40
+ const node = untyped;
41
+ const full = defaultHandlers.linkReference({
42
+ ...node,
43
+ label: labelPlaceholder,
44
+ identifier: labelPlaceholder,
45
+ referenceType: 'full',
46
+ }, parent, state, info);
47
+ const display = full.slice(1, -labelPlaceholder.length - 3);
48
+ const unchanged = node.referenceContent != null &&
49
+ node.referenceContent === contentShape(node.children);
50
+ if (node.referenceType !== 'full' &&
51
+ (unchanged || normaliseLabel(display) === normaliseLabel(node.identifier)))
52
+ return `[${unchanged ? node.label : display}]${node.referenceType === 'collapsed' ? '[]' : ''}`;
53
+ // Raw entity and escape spelling participates in reference matching.
54
+ return (full.slice(0, -labelPlaceholder.length - 2) +
55
+ `[${node.label ?? node.identifier}]`);
56
+ };
57
+ authoredLinkReference.peek = () => '[';
58
+ const authoredDefinition = (node, parent, state, info) => defaultHandlers
59
+ .definition({ ...node, label: labelPlaceholder, identifier: labelPlaceholder }, parent, state, info)
60
+ .replace(`[${labelPlaceholder}]:`, `[${String(node.label ?? node.identifier)}]:`);
61
+ const footnoteHandlers = gfmFootnoteToMarkdown().handlers;
62
+ const authoredFootnoteReference = (node) => `[^${String(node.label ?? node.identifier)}]`;
63
+ authoredFootnoteReference.peek = () => '[';
64
+ const authoredFootnoteDefinition = (node, parent, state, info) => footnoteHandlers.footnoteDefinition({ ...node, label: labelPlaceholder, identifier: labelPlaceholder }, parent, state, info).replace(`[^${labelPlaceholder}]:`, `[^${String(node.label ?? node.identifier)}]:`);
65
+ function remarkReferences() {
66
+ const data = this.data();
67
+ (data.micromarkExtensions ??= []).push(gfmFootnote());
68
+ (data.fromMarkdownExtensions ??= []).push(gfmFootnoteFromMarkdown());
69
+ (data.toMarkdownExtensions ??= []).push(gfmFootnoteToMarkdown());
70
+ (data.toMarkdownExtensions ??= []).push({
71
+ handlers: {
72
+ linkReference: authoredLinkReference,
73
+ definition: authoredDefinition,
74
+ footnoteReference: authoredFootnoteReference,
75
+ footnoteDefinition: authoredFootnoteDefinition,
76
+ },
77
+ });
78
+ return (tree, file) => {
79
+ const source = String(file.value).replace(/^\uFEFF/, '');
80
+ const targets = new Map();
81
+ const walk = (node, visit) => {
82
+ visit(node);
83
+ if ('children' in node)
84
+ node.children.forEach((child) => walk(child, visit));
85
+ };
86
+ walk(tree, (node) => {
87
+ const start = node.position?.start.offset;
88
+ const end = node.position?.end.offset;
89
+ if (start != null && end != null) {
90
+ const raw = source.slice(start, end);
91
+ if (node.type === 'definition' || node.type === 'footnoteDefinition') {
92
+ const label = /^\[((?:\\[\s\S]|[^\\[\]])*)\]:/.exec(raw)?.[1];
93
+ if (label != null)
94
+ node.label =
95
+ node.type === 'footnoteDefinition' ? label.slice(1) : label;
96
+ }
97
+ else if (node.type === 'footnoteReference')
98
+ node.label = raw.slice(2, -1);
99
+ else if (node.type === 'linkReference') {
100
+ const label = node.referenceType === 'full'
101
+ ? /\[((?:\\[\s\S]|[^\\[\]])*)\]$/.exec(raw)?.[1]
102
+ : raw.slice(1, node.referenceType === 'collapsed' ? -3 : -1);
103
+ if (label != null)
104
+ node.label = label;
105
+ Object.assign(node, { referenceContent: contentShape(node.children) });
106
+ }
107
+ }
108
+ if (node.type === 'definition' && !targets.has(node.identifier))
109
+ targets.set(node.identifier, { url: node.url, title: node.title });
110
+ });
111
+ walk(tree, (node) => {
112
+ if (node.type !== 'linkReference')
113
+ return;
114
+ const target = targets.get(node.identifier);
115
+ if (target)
116
+ Object.assign(node, target);
117
+ });
118
+ };
119
+ }
120
+ export const remarkReferencesPlugin = $remark('inkkitReferences', () => remarkReferences);
121
+ export const referenceLinks = linkSchema.extendSchema((base) => (ctx) => {
122
+ const schema = base(ctx);
123
+ return {
124
+ ...schema,
125
+ // Label formatting belongs inside a reference, where its source determines the identifier.
126
+ priority: 40,
127
+ attrs: {
128
+ ...schema.attrs,
129
+ label: { default: null },
130
+ identifier: { default: null },
131
+ referenceType: { default: null },
132
+ referenceContent: { default: null },
133
+ },
134
+ parseDOM: [
135
+ {
136
+ tag: 'a[href]',
137
+ getAttrs(dom) {
138
+ const element = dom;
139
+ return {
140
+ href: element.getAttribute('href') ?? '',
141
+ title: element.getAttribute('title'),
142
+ label: element.getAttribute('data-inkkit-label'),
143
+ identifier: element.getAttribute('data-inkkit-reference'),
144
+ referenceType: element.getAttribute('data-inkkit-reference-type'),
145
+ referenceContent: element.getAttribute('data-inkkit-reference-content'),
146
+ };
147
+ },
148
+ },
149
+ ],
150
+ toDOM(mark) {
151
+ const dom = schema.toDOM(mark, false);
152
+ const attrs = { ...dom[1] };
153
+ delete attrs.label;
154
+ delete attrs.identifier;
155
+ delete attrs.referenceType;
156
+ delete attrs.referenceContent;
157
+ if (mark.attrs.identifier != null) {
158
+ attrs['data-inkkit-reference'] = mark.attrs.identifier;
159
+ attrs['data-inkkit-label'] = mark.attrs.label;
160
+ attrs['data-inkkit-reference-type'] = mark.attrs.referenceType;
161
+ attrs['data-inkkit-reference-content'] = mark.attrs.referenceContent;
162
+ }
163
+ return ['a', attrs];
164
+ },
165
+ parseMarkdown: {
166
+ match: (node) => node.type === 'link' || node.type === 'linkReference',
167
+ runner(state, node, type) {
168
+ if (node.type === 'link')
169
+ return schema.parseMarkdown.runner(state, node, type);
170
+ state.openMark(type, {
171
+ href: String(node.url ?? ''),
172
+ title: node.title ?? null,
173
+ label: String(node.label ?? node.identifier),
174
+ identifier: String(node.identifier),
175
+ referenceType: String(node.referenceType),
176
+ referenceContent: node.referenceContent ?? null,
177
+ });
178
+ state.next(node.children);
179
+ state.closeMark(type);
180
+ },
181
+ },
182
+ toMarkdown: {
183
+ ...schema.toMarkdown,
184
+ runner(state, mark, node) {
185
+ if (mark.attrs.identifier == null)
186
+ return schema.toMarkdown.runner(state, mark, node);
187
+ state.withMark(mark, 'linkReference', undefined, {
188
+ identifier: mark.attrs.identifier,
189
+ label: mark.attrs.label,
190
+ referenceType: mark.attrs.referenceType,
191
+ referenceContent: mark.attrs.referenceContent,
192
+ });
193
+ },
194
+ },
195
+ };
196
+ });
197
+ const associationAttrs = {
198
+ label: { default: '' },
199
+ identifier: { default: '' },
200
+ };
201
+ export const referenceDefinition = $node('reference_definition', () => ({
202
+ group: 'block',
203
+ atom: true,
204
+ defining: true,
205
+ attrs: {
206
+ ...associationAttrs,
207
+ url: { default: '' },
208
+ title: { default: null },
209
+ },
210
+ parseDOM: [
211
+ {
212
+ tag: '[data-inkkit-reference-definition]',
213
+ getAttrs: (dom) => ({
214
+ label: dom.getAttribute('data-inkkit-label') ?? '',
215
+ identifier: dom.getAttribute('data-inkkit-reference-definition') ?? '',
216
+ url: dom.getAttribute('data-inkkit-url') ?? '',
217
+ title: dom.getAttribute('data-inkkit-title'),
218
+ }),
219
+ },
220
+ ],
221
+ toDOM: (node) => [
222
+ 'div',
223
+ {
224
+ class: 'inkkit-reference-definition',
225
+ 'data-inkkit-reference-definition': node.attrs.identifier,
226
+ 'data-inkkit-label': node.attrs.label,
227
+ 'data-inkkit-url': node.attrs.url,
228
+ 'data-inkkit-title': node.attrs.title,
229
+ },
230
+ `[${node.attrs.label}]: ${node.attrs.url}${node.attrs.title ? ` "${node.attrs.title}"` : ''}`,
231
+ ],
232
+ parseMarkdown: {
233
+ match: (node) => node.type === 'definition',
234
+ runner(state, node, type) {
235
+ state.addNode(type, {
236
+ label: String(node.label ?? node.identifier),
237
+ identifier: String(node.identifier),
238
+ url: String(node.url),
239
+ title: node.title ?? null,
240
+ });
241
+ },
242
+ },
243
+ toMarkdown: {
244
+ match: (node) => node.type.name === 'reference_definition',
245
+ runner: (state, node) => {
246
+ state.addNode('definition', undefined, undefined, { ...node.attrs });
247
+ },
248
+ },
249
+ }));
250
+ export const footnoteReference = $node('footnote_reference', () => ({
251
+ inline: true,
252
+ group: 'inline',
253
+ atom: true,
254
+ selectable: true,
255
+ attrs: associationAttrs,
256
+ parseDOM: [
257
+ {
258
+ tag: 'sup[data-inkkit-footnote-reference]',
259
+ getAttrs: (dom) => ({
260
+ label: dom.getAttribute('data-inkkit-label') ?? '',
261
+ identifier: dom.getAttribute('data-inkkit-footnote-reference') ?? '',
262
+ }),
263
+ },
264
+ ],
265
+ toDOM: (node) => [
266
+ 'sup',
267
+ {
268
+ class: 'inkkit-footnote-reference',
269
+ tabindex: '0',
270
+ role: 'button',
271
+ 'aria-label': `Footnote ${node.attrs.label}`,
272
+ 'data-inkkit-footnote-reference': node.attrs.identifier,
273
+ 'data-inkkit-label': node.attrs.label,
274
+ },
275
+ String(node.attrs.label),
276
+ ],
277
+ parseMarkdown: {
278
+ match: (node) => node.type === 'footnoteReference',
279
+ runner(state, node, type) {
280
+ state.addNode(type, {
281
+ label: String(node.label ?? node.identifier),
282
+ identifier: String(node.identifier),
283
+ });
284
+ },
285
+ },
286
+ toMarkdown: {
287
+ match: (node) => node.type.name === 'footnote_reference',
288
+ runner: (state, node) => {
289
+ state.addNode('footnoteReference', undefined, undefined, {
290
+ ...node.attrs,
291
+ });
292
+ },
293
+ },
294
+ }));
295
+ export const footnoteDefinition = $node('footnote_definition', () => ({
296
+ group: 'block',
297
+ content: 'block+',
298
+ defining: true,
299
+ attrs: associationAttrs,
300
+ parseDOM: [
301
+ {
302
+ tag: '[data-inkkit-footnote-definition]',
303
+ getAttrs: (dom) => ({
304
+ label: dom.getAttribute('data-inkkit-label') ?? '',
305
+ identifier: dom.getAttribute('data-inkkit-footnote-definition') ?? '',
306
+ }),
307
+ },
308
+ ],
309
+ toDOM: (node) => [
310
+ 'div',
311
+ {
312
+ class: 'inkkit-footnote-definition',
313
+ 'aria-label': `Footnote ${node.attrs.label}`,
314
+ 'data-inkkit-footnote-definition': node.attrs.identifier,
315
+ 'data-inkkit-label': node.attrs.label,
316
+ },
317
+ 0,
318
+ ],
319
+ parseMarkdown: {
320
+ match: (node) => node.type === 'footnoteDefinition',
321
+ runner(state, node, type) {
322
+ state.openNode(type, {
323
+ label: String(node.label ?? node.identifier),
324
+ identifier: String(node.identifier),
325
+ });
326
+ state.next(node.children);
327
+ state.closeNode();
328
+ },
329
+ },
330
+ toMarkdown: {
331
+ match: (node) => node.type.name === 'footnote_definition',
332
+ runner(state, node) {
333
+ state.openNode('footnoteDefinition', undefined, { ...node.attrs });
334
+ state.next(node.content);
335
+ state.closeNode();
336
+ },
337
+ },
338
+ }));
339
+ const resolvedReferenceTargets = $prose(() => new Plugin({
340
+ key: new PluginKey('inkkitReferenceTargets'),
341
+ appendTransaction(transactions, _before, state) {
342
+ if (!transactions.some((transaction) => transaction.docChanged))
343
+ return null;
344
+ const targets = referenceDefinitions(state.doc);
345
+ const tr = state.tr;
346
+ state.doc.descendants((node, pos) => {
347
+ for (const mark of node.marks) {
348
+ if (mark.type.name !== 'link' || mark.attrs.identifier == null)
349
+ continue;
350
+ const target = targets.get(normaliseLabel(String(mark.attrs.identifier)))?.node;
351
+ const href = String(target?.attrs.url ?? '');
352
+ const title = target?.attrs.title ?? null;
353
+ if (mark.attrs.href === href && mark.attrs.title === title)
354
+ continue;
355
+ tr.addMark(pos, pos + node.nodeSize, mark.type.create({ ...mark.attrs, href, title }));
356
+ }
357
+ });
358
+ return tr.docChanged ? tr.setMeta('addToHistory', false) : null;
359
+ },
360
+ }));
361
+ export const references = [
362
+ referenceLinks,
363
+ referenceDefinition,
364
+ footnoteReference,
365
+ footnoteDefinition,
366
+ resolvedReferenceTargets,
367
+ ].flat();
package/dist/style.css CHANGED
@@ -535,3 +535,120 @@ body {
535
535
  font-family: var(--mono);
536
536
  font-size: 0.9rem;
537
537
  }
538
+
539
+ .inkkit-footnote-reference {
540
+ cursor: pointer;
541
+ color: var(--accent, currentColor);
542
+ font-size: 0.75em;
543
+ }
544
+
545
+ .inkkit-footnote-reference:focus-visible {
546
+ outline: 2px solid currentColor;
547
+ outline-offset: 2px;
548
+ }
549
+
550
+ .inkkit-footnote-definition {
551
+ border-inline-start: 2px solid var(--muted, currentColor);
552
+ padding-inline-start: 1rem;
553
+ }
554
+
555
+ .inkkit-footnote-definition::before {
556
+ content: '[' attr(data-inkkit-label) ']';
557
+ font-size: 0.8em;
558
+ }
559
+
560
+ .inkkit-reference-definition {
561
+ font-family: ui-monospace, monospace;
562
+ font-size: 0.8em;
563
+ opacity: 0.7;
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/dist/types.d.ts CHANGED
@@ -41,6 +41,7 @@ export interface ImageAdapter {
41
41
  }
42
42
  export interface ClipboardInput {
43
43
  text: string;
44
+ markdown?: string;
44
45
  html?: string;
45
46
  images?: CapturedImage[];
46
47
  plainText?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicayzer/inkkit",
3
- "version": "0.0.1",
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,20 +47,27 @@
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",
52
+ "mdast-util-gfm-footnote": "^2.1.0",
51
53
  "mdast-util-gfm-strikethrough": "^2.0.0",
52
- "mdast-util-gfm-task-list-item": "^2.0.0",
53
54
  "mdast-util-gfm-table": "^2.0.0",
55
+ "mdast-util-gfm-task-list-item": "^2.0.0",
56
+ "mdast-util-highlight-mark": "1.2.2",
54
57
  "mdast-util-to-markdown": "2.2.0",
55
58
  "micromark-extension-gfm-autolink-literal": "^2.1.0",
59
+ "micromark-extension-gfm-footnote": "^2.1.0",
56
60
  "micromark-extension-gfm-strikethrough": "^2.1.0",
57
- "micromark-extension-gfm-task-list-item": "^2.1.0",
58
61
  "micromark-extension-gfm-table": "^2.1.1",
62
+ "micromark-extension-gfm-task-list-item": "^2.1.0",
63
+ "micromark-extension-highlight-mark": "1.2.0",
64
+ "micromark-util-normalize-identifier": "^2.0.1",
65
+ "micromark-util-types": "^2.0.3",
59
66
  "prosemirror-search": "^1.1.1"
60
67
  },
61
68
  "devDependencies": {
62
- "@types/mdast": "^4.0.4",
63
69
  "@types/hast": "^3.0.5",
70
+ "@types/mdast": "^4.0.4",
64
71
  "@types/node": "^26.6.2",
65
72
  "jsdom": "^30.1.0",
66
73
  "prettier": "^3.9.8",