@aicayzer/inkkit 0.0.0-stage → 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.
@@ -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();
@@ -0,0 +1 @@
1
+ export declare const selectionPlugin: import("@milkdown/kit/utils").$Prose;
@@ -0,0 +1,47 @@
1
+ import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
2
+ import { $prose } from '@milkdown/kit/utils';
3
+ /** The stylesheet paints the selection under this name. */
4
+ const name = 'selection';
5
+ /** The selected text within each block. */
6
+ function covered(state) {
7
+ const { from, to, empty, visible } = state.selection;
8
+ if (empty || !visible)
9
+ return null;
10
+ const text = [];
11
+ state.doc.nodesBetween(from, to, (node, pos) => {
12
+ if (!node.isTextblock)
13
+ return true;
14
+ const start = Math.max(from, pos + 1);
15
+ const end = Math.min(to, pos + node.nodeSize - 1);
16
+ if (end > start)
17
+ text.push({ from: start, to: end });
18
+ return false;
19
+ });
20
+ return text;
21
+ }
22
+ function domRange(view, span) {
23
+ const start = view.domAtPos(span.from);
24
+ const end = view.domAtPos(span.to);
25
+ const range = document.createRange();
26
+ range.setStart(start.node, start.offset);
27
+ range.setEnd(end.node, end.offset);
28
+ return range;
29
+ }
30
+ function paint(view, highlight) {
31
+ highlight.clear();
32
+ for (const span of covered(view.state) ?? [])
33
+ highlight.add(domRange(view, span));
34
+ }
35
+ // Paint only selected text, without WebKit filling the surrounding margins.
36
+ export const selectionPlugin = $prose(() => new Plugin({
37
+ key: new PluginKey('selection'),
38
+ view(view) {
39
+ const highlight = new Highlight();
40
+ CSS.highlights.set(name, highlight);
41
+ paint(view, highlight);
42
+ return {
43
+ update: (view) => paint(view, highlight),
44
+ destroy: () => CSS.highlights.delete(name),
45
+ };
46
+ },
47
+ }));