@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,403 @@
1
+ import { parserCtx, remarkCtx } from '@milkdown/kit/core';
2
+ import { serialize } from './dialect.js';
3
+ function signature(node) {
4
+ return JSON.stringify(node, (key, value) => ['position', 'data', 'url', 'title', 'marker'].includes(key)
5
+ ? undefined
6
+ : value);
7
+ }
8
+ function ownShape(node) {
9
+ return JSON.stringify(node, (key, value) => ['position', 'data', 'children', 'value', 'marker'].includes(key)
10
+ ? undefined
11
+ : value);
12
+ }
13
+ function retainText(raw, before, after) {
14
+ before = before.replace(/\r\n?/g, '\n');
15
+ after = after.replace(/\r\n?/g, '\n');
16
+ let prefix = 0;
17
+ while (prefix < before.length &&
18
+ prefix < after.length &&
19
+ before[prefix] === after[prefix])
20
+ prefix++;
21
+ let suffix = 0;
22
+ while (suffix < before.length - prefix &&
23
+ suffix < after.length - prefix &&
24
+ before[before.length - 1 - suffix] === after[after.length - 1 - suffix])
25
+ suffix++;
26
+ const offsets = [0];
27
+ let decoded = '';
28
+ for (let index = 0; index < raw.length;) {
29
+ const escaped = /^\\([!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~])/.exec(raw.slice(index));
30
+ const entity = /^&(?:#[xX][\da-fA-F]+|#\d+|[A-Za-z][A-Za-z\d]+);/.exec(raw.slice(index));
31
+ let value;
32
+ let length;
33
+ if (escaped) {
34
+ value = escaped[1];
35
+ length = escaped[0].length;
36
+ }
37
+ else if (entity) {
38
+ const element = document.createElement('textarea');
39
+ element.innerHTML = entity[0];
40
+ value = element.value;
41
+ length = entity[0].length;
42
+ }
43
+ else if (raw.slice(index, index + 2) === '\r\n') {
44
+ value = '\n';
45
+ length = 2;
46
+ }
47
+ else {
48
+ value = raw[index];
49
+ length = 1;
50
+ }
51
+ decoded += value;
52
+ index += length;
53
+ for (let part = 0; part < value.length; part++)
54
+ offsets.push(index);
55
+ }
56
+ if (decoded !== before)
57
+ return after;
58
+ const inserted = after
59
+ .slice(prefix, after.length - suffix)
60
+ .replace(/[\\`*_[\]<>]/g, '\\$&');
61
+ return (raw.slice(0, offsets[prefix]) +
62
+ inserted +
63
+ raw.slice(offsets[before.length - suffix]));
64
+ }
65
+ function retainBoundaryWhitespace(raw, canonical) {
66
+ // A middle-of-line space becomes indentation or trimmed text at a block edge.
67
+ const token = '(?:[ \\t]|&#(?:[xX]0*(?:20|9)|0*(?:32|9));|&Tab;)';
68
+ const decoded = (value) => (value.match(new RegExp(token, 'g')) ?? [])
69
+ .map((value) => value === '\t' || /(?:9;|Tab;)$/.test(value) ? '\t' : ' ')
70
+ .join('');
71
+ const encode = (value) => value.replace(/[ \t]/g, (value) => (value === '\t' ? '&#x9;' : '&#x20;'));
72
+ const start = /^[ \t]+/.exec(raw)?.[0];
73
+ const rawStart = new RegExp(`^${token}+`).exec(raw)?.[0];
74
+ const encodedStart = new RegExp(`^${token}+`).exec(canonical)?.[0];
75
+ if (start &&
76
+ rawStart &&
77
+ encodedStart?.includes('&') &&
78
+ decoded(rawStart) === decoded(encodedStart))
79
+ raw = encode(start) + raw.slice(start.length);
80
+ const end = /[ \t]+$/.exec(raw)?.[0];
81
+ const rawEnd = new RegExp(`${token}+$`).exec(raw)?.[0];
82
+ const encodedEnd = new RegExp(`${token}+$`).exec(canonical)?.[0];
83
+ if (end &&
84
+ rawEnd &&
85
+ encodedEnd?.includes('&') &&
86
+ decoded(rawEnd) === decoded(encodedEnd))
87
+ raw = raw.slice(0, -end.length) + encode(end);
88
+ return raw;
89
+ }
90
+ // Patch changed leaves so an adjacent edit retains authored delimiters and labels.
91
+ function retainSource(before, after, source, canonical, lineEnding) {
92
+ const start = before.position?.start.offset;
93
+ const end = before.position?.end.offset;
94
+ const newStart = after.position?.start.offset;
95
+ const newEnd = after.position?.end.offset;
96
+ if (start == null || end == null || newStart == null || newEnd == null)
97
+ throw new PreservationError('A source token has no location');
98
+ const raw = source.slice(start, end);
99
+ if (before.type === 'definition' &&
100
+ after.type === 'definition' &&
101
+ before.label === after.label &&
102
+ before.title === after.title) {
103
+ const destination = /^(\[[\s\S]*?\]:[ \t]*)(<[^>\n]*>|[^\s]+)([\s\S]*)$/.exec(raw);
104
+ if (destination)
105
+ return (destination[1] +
106
+ (destination[2].startsWith('<')
107
+ ? `<${after.url}>`
108
+ : String(after.url)) +
109
+ destination[3]);
110
+ }
111
+ if (before.type === 'text' && after.type === 'text')
112
+ return retainBoundaryWhitespace(retainText(raw, before.value, after.value), canonical.slice(newStart, newEnd));
113
+ if (signature(before) === signature(after) &&
114
+ ownShape(before) === ownShape(after))
115
+ return raw;
116
+ if (before.type === after.type &&
117
+ ownShape(before) === ownShape(after) &&
118
+ before.children &&
119
+ after.children &&
120
+ before.children.length === after.children.length) {
121
+ let output = '', previous = start;
122
+ before.children.forEach((child, index) => {
123
+ const childStart = child.position?.start.offset, childEnd = child.position?.end.offset;
124
+ if (childStart == null || childEnd == null)
125
+ throw new PreservationError('A source token has no location');
126
+ output +=
127
+ source.slice(previous, childStart) +
128
+ retainSource(child, after.children[index], source, canonical, lineEnding);
129
+ previous = childEnd;
130
+ });
131
+ return output + source.slice(previous, end);
132
+ }
133
+ if (before.type === after.type &&
134
+ ownShape(before) === ownShape(after) &&
135
+ before.children &&
136
+ after.children) {
137
+ let prefix = 0, suffix = 0;
138
+ while (prefix < before.children.length &&
139
+ prefix < after.children.length &&
140
+ signature(before.children[prefix]) === signature(after.children[prefix]))
141
+ prefix++;
142
+ while (suffix < before.children.length - prefix &&
143
+ suffix < after.children.length - prefix &&
144
+ signature(before.children[before.children.length - 1 - suffix]) ===
145
+ signature(after.children[after.children.length - 1 - suffix]))
146
+ suffix++;
147
+ const from = prefix
148
+ ? before.children[prefix - 1].position?.end.offset
149
+ : start;
150
+ const to = suffix
151
+ ? before.children[before.children.length - suffix].position?.start.offset
152
+ : end;
153
+ const newFrom = prefix
154
+ ? after.children[prefix - 1].position?.end.offset
155
+ : newStart;
156
+ const newTo = suffix
157
+ ? after.children[after.children.length - suffix].position?.start.offset
158
+ : newEnd;
159
+ if (from != null && to != null && newFrom != null && newTo != null)
160
+ return retainBoundaryWhitespace(source.slice(start, from) +
161
+ canonical.slice(newFrom, newTo).replaceAll('\n', lineEnding) +
162
+ source.slice(to, end), canonical.slice(newStart, newEnd));
163
+ }
164
+ return canonical.slice(newStart, newEnd).replaceAll('\n', lineEnding);
165
+ }
166
+ function positionalNode(tree, transformed) {
167
+ return (tree.children.find((node) => node.position?.start.offset === transformed.position?.start.offset &&
168
+ node.position?.end.offset === transformed.position?.end.offset) ?? transformed);
169
+ }
170
+ export class PreservationError extends Error {
171
+ constructor(message, options) {
172
+ super(message, options);
173
+ this.name = 'PreservationError';
174
+ }
175
+ }
176
+ function form(text) {
177
+ return text.replace(/\n+$/, '');
178
+ }
179
+ function blockForm(ctx, node) {
180
+ // An empty document is empty Markdown, but an empty block inside a document
181
+ // is authored spacing and must remain a real block when read again.
182
+ if (node.type.name === 'paragraph' && node.content.size === 0)
183
+ return '<br />';
184
+ return form(serialize(ctx, node.type.schema.topNodeType.create(null, [node])));
185
+ }
186
+ function semanticSignature(doc) {
187
+ const clean = (value) => {
188
+ const content = value.content;
189
+ if (!content)
190
+ return value;
191
+ const tokens = [];
192
+ for (const child of content) {
193
+ if (child.type !== 'text') {
194
+ tokens.push(clean(child));
195
+ continue;
196
+ }
197
+ const parts = /^(\s*)([\s\S]*?)(\s*)$/.exec(String(child.text));
198
+ for (const [index, text] of parts.slice(1).entries()) {
199
+ if (!text)
200
+ continue;
201
+ const token = {
202
+ ...child,
203
+ text,
204
+ marks: index === 1 ? child.marks : undefined,
205
+ };
206
+ const previous = tokens.at(-1);
207
+ if (previous?.type === 'text' &&
208
+ JSON.stringify(previous.marks) === JSON.stringify(token.marks))
209
+ previous.text = String(previous.text) + text;
210
+ else
211
+ tokens.push(token);
212
+ }
213
+ }
214
+ return { ...value, content: tokens };
215
+ };
216
+ const json = JSON.parse(JSON.stringify(doc.toJSON(), (key, value) =>
217
+ // Fresh list items default to loose even when their Markdown reopens tight.
218
+ [
219
+ 'referenceContent',
220
+ 'referenceType',
221
+ 'marker',
222
+ 'id',
223
+ 'label',
224
+ 'spread',
225
+ ].includes(key)
226
+ ? undefined
227
+ : value));
228
+ return JSON.stringify(clean(json));
229
+ }
230
+ // Edit distance distinguishes replacements from insertions, so replacing a block
231
+ // retains its surrounding whitespace without assigning it a neighbour's source.
232
+ function correspondence(before, after) {
233
+ const result = new Array(after.length);
234
+ let head = 0;
235
+ while (head < before.length &&
236
+ head < after.length &&
237
+ before[head] === after[head]) {
238
+ result[head] = head;
239
+ head++;
240
+ }
241
+ let endBefore = before.length;
242
+ let endAfter = after.length;
243
+ while (endBefore > head &&
244
+ endAfter > head &&
245
+ before[endBefore - 1] === after[endAfter - 1]) {
246
+ result[--endAfter] = --endBefore;
247
+ }
248
+ const rows = endBefore - head + 1;
249
+ const columns = endAfter - head + 1;
250
+ if (rows * columns > 1_000_000)
251
+ throw new PreservationError('The edit changes too many blocks to preserve safely');
252
+ const costs = Array.from({ length: rows }, () => new Uint32Array(columns));
253
+ for (let i = 0; i < rows; i++)
254
+ costs[i][0] = i;
255
+ for (let j = 0; j < columns; j++)
256
+ costs[0][j] = j;
257
+ for (let i = 1; i < rows; i++) {
258
+ for (let j = 1; j < columns; j++) {
259
+ const change = before[head + i - 1] === after[head + j - 1] ? 0 : 1;
260
+ costs[i][j] = Math.min(costs[i - 1][j] + 1, costs[i][j - 1] + 1, costs[i - 1][j - 1] + change);
261
+ }
262
+ }
263
+ let i = rows - 1;
264
+ let j = columns - 1;
265
+ while (i || j) {
266
+ const change = i && j && before[head + i - 1] === after[head + j - 1] ? 0 : 1;
267
+ if (i && j && costs[i][j] === costs[i - 1][j - 1] + change) {
268
+ result[head + j - 1] = head + i - 1;
269
+ i--;
270
+ j--;
271
+ }
272
+ else if (i && costs[i][j] === costs[i - 1][j] + 1)
273
+ i--;
274
+ else
275
+ j--;
276
+ }
277
+ return result;
278
+ }
279
+ /** Retains the loaded source of blocks the edit leaves alone. */
280
+ export class Preservation {
281
+ ctx;
282
+ source;
283
+ baseline;
284
+ blocks = [];
285
+ lineEnding;
286
+ forms = new WeakMap();
287
+ constructor(ctx, source) {
288
+ this.ctx = ctx;
289
+ this.source = source;
290
+ try {
291
+ const parse = ctx.get(parserCtx);
292
+ this.baseline = parse(source);
293
+ this.lineEnding =
294
+ source.includes('\r\n') && !source.replaceAll('\r\n', '').includes('\n')
295
+ ? '\r\n'
296
+ : '\n';
297
+ const processor = ctx.get(remarkCtx);
298
+ const offset = source.startsWith('\uFEFF') ? 1 : 0;
299
+ const body = source.slice(offset);
300
+ const rawTree = processor.parse(source);
301
+ const tree = processor.runSync(structuredClone(rawTree), {
302
+ value: body,
303
+ });
304
+ for (const [index, node] of tree.children.entries()) {
305
+ let start = node.position?.start.offset;
306
+ const end = node.position?.end.offset;
307
+ if (start == null || end == null)
308
+ throw new PreservationError('A source block has no location');
309
+ start += offset;
310
+ const sourceEnd = end + offset;
311
+ const firstColumn = source.lastIndexOf('\n', start - 1) + 1;
312
+ if (/^[ \t]*$/.test(source.slice(firstColumn, start)))
313
+ start = firstColumn;
314
+ const text = source.slice(start, sourceEnd);
315
+ const parsed = this.baseline.childCount === tree.children.length
316
+ ? this.baseline.type.create(null, [this.baseline.child(index)])
317
+ : parse(text);
318
+ this.blocks.push({
319
+ start,
320
+ end: sourceEnd,
321
+ form: parsed.childCount === 1
322
+ ? blockForm(ctx, parsed.firstChild)
323
+ : form(serialize(ctx, parsed)),
324
+ ast: positionalNode(rawTree, node),
325
+ });
326
+ }
327
+ }
328
+ catch (cause) {
329
+ if (cause instanceof PreservationError)
330
+ throw cause;
331
+ throw new PreservationError('Cannot read Markdown for preservation', {
332
+ cause,
333
+ });
334
+ }
335
+ }
336
+ serialize(doc) {
337
+ if (doc.eq(this.baseline))
338
+ return this.source;
339
+ try {
340
+ const canonical = serialize(this.ctx, doc);
341
+ if (canonical === '')
342
+ return this.source.startsWith('\uFEFF') ? '\uFEFF' : '';
343
+ const current = [];
344
+ doc.forEach((node) => {
345
+ let fingerprint = this.forms.get(node);
346
+ if (fingerprint == null) {
347
+ fingerprint = blockForm(this.ctx, node);
348
+ this.forms.set(node, fingerprint);
349
+ }
350
+ current.push(fingerprint);
351
+ });
352
+ const positions = correspondence(this.blocks.map((block) => block.form), current);
353
+ const processor = this.ctx.get(remarkCtx);
354
+ const currentRawTree = processor.parse(canonical);
355
+ const currentTree = processor.runSync(structuredClone(currentRawTree), {
356
+ value: canonical,
357
+ });
358
+ const body = this.source.replace(/^\uFEFF/, '');
359
+ let output = '';
360
+ let previous;
361
+ current.forEach((text, index) => {
362
+ const position = positions[index];
363
+ const block = position == null ? undefined : this.blocks[position];
364
+ if (index === 0) {
365
+ if (position === 0 && block)
366
+ output += this.source.slice(0, block.start);
367
+ else if (this.source.startsWith('\uFEFF'))
368
+ output += '\uFEFF';
369
+ }
370
+ else if (previous != null && position === previous + 1 && block) {
371
+ output += this.source.slice(this.blocks[previous].end, block.start);
372
+ }
373
+ else
374
+ output += this.lineEnding.repeat(2);
375
+ if (block && block.form === text)
376
+ output += this.source.slice(block.start, block.end);
377
+ else if (block && currentTree.children.length === current.length) {
378
+ output += retainSource(block.ast, positionalNode(currentRawTree, currentTree.children[index]), body, canonical, this.lineEnding);
379
+ }
380
+ else
381
+ output += text.replaceAll('\n', this.lineEnding);
382
+ previous = position;
383
+ });
384
+ if (previous === this.blocks.length - 1 && this.blocks.length)
385
+ output += this.source.slice(this.blocks[previous].end);
386
+ else if (output)
387
+ output += this.lineEnding;
388
+ const parse = this.ctx.get(parserCtx);
389
+ const reopened = parse(output);
390
+ if (serialize(this.ctx, reopened) === canonical &&
391
+ semanticSignature(reopened) === semanticSignature(doc))
392
+ return output;
393
+ throw new PreservationError('The edited Markdown cannot be reopened without changing its content');
394
+ }
395
+ catch (cause) {
396
+ if (cause instanceof PreservationError)
397
+ throw cause;
398
+ throw new PreservationError('Cannot preserve the edited Markdown', {
399
+ cause,
400
+ });
401
+ }
402
+ }
403
+ }
@@ -0,0 +1,8 @@
1
+ import type { Ctx } from '@milkdown/kit/ctx';
2
+ import { Fragment, type Node } from '@milkdown/kit/prose/model';
3
+ export declare function selectionContent(doc: Node, content: Fragment): Fragment;
4
+ export declare function selectionMarkdown(ctx: Ctx, doc: Node, content: Fragment): string;
5
+ export declare function referenceMetadata(ctx: Ctx, doc: Node, content: Fragment): string | undefined;
6
+ export declare function avoidReferenceCollisions(incoming: Node, destination: Node): Node;
7
+ export declare function markdownFromHTML(html: string): string | undefined;
8
+ export declare function withReferenceMetadata(html: string, markdown: string): string;
@@ -0,0 +1,197 @@
1
+ import { Fragment } from '@milkdown/kit/prose/model';
2
+ import { serialize } from './dialect.js';
3
+ import { normaliseLabel } from './references.js';
4
+ function key(node) {
5
+ if (node.type.name === 'reference_definition')
6
+ return `link:${node.attrs.identifier}`;
7
+ if (node.type.name === 'footnote_definition')
8
+ return `footnote:${node.attrs.identifier}`;
9
+ return undefined;
10
+ }
11
+ function used(content) {
12
+ const result = new Set();
13
+ content.descendants((node) => {
14
+ if (node.type.name === 'footnote_reference')
15
+ result.add(`footnote:${node.attrs.identifier}`);
16
+ for (const mark of node.marks)
17
+ if (mark.type.name === 'link' && mark.attrs.identifier)
18
+ result.add(`link:${mark.attrs.identifier}`);
19
+ });
20
+ return result;
21
+ }
22
+ export function selectionContent(doc, content) {
23
+ const definitions = new Map();
24
+ doc.descendants((node) => {
25
+ const id = key(node);
26
+ if (id && !definitions.has(id))
27
+ definitions.set(id, node);
28
+ });
29
+ const required = used(content);
30
+ for (const id of required) {
31
+ const definition = definitions.get(id);
32
+ if (definition)
33
+ for (const dependency of used(definition.content))
34
+ required.add(dependency);
35
+ }
36
+ const complete = (node) => {
37
+ const id = key(node);
38
+ if (id && required.has(id) && definitions.has(id))
39
+ return definitions.get(id);
40
+ if (node.isText)
41
+ return node;
42
+ const children = [];
43
+ node.forEach((child) => children.push(complete(child)));
44
+ return node.copy(Fragment.fromArray(children));
45
+ };
46
+ const selected = [];
47
+ content.forEach((node) => selected.push(complete(node)));
48
+ content = Fragment.fromArray(selected);
49
+ const included = new Set();
50
+ content.descendants((node) => {
51
+ const id = key(node);
52
+ if (id)
53
+ included.add(id);
54
+ });
55
+ const extra = [];
56
+ for (const id of required) {
57
+ if (included.has(id))
58
+ continue;
59
+ const definition = definitions.get(id);
60
+ if (!definition)
61
+ continue;
62
+ included.add(id);
63
+ extra.push(definition);
64
+ for (const dependency of used(definition.content))
65
+ required.add(dependency);
66
+ }
67
+ return content.append(Fragment.fromArray(extra));
68
+ }
69
+ export function selectionMarkdown(ctx, doc, content) {
70
+ return serialize(ctx, doc.type.create(null, selectionContent(doc, content)));
71
+ }
72
+ export function referenceMetadata(ctx, doc, content) {
73
+ let references = false, index = 0;
74
+ const rewrite = (node) => {
75
+ if (key(node) ||
76
+ node.type.name === 'footnote_reference' ||
77
+ node.marks.some((mark) => mark.type.name === 'link' && mark.attrs.identifier))
78
+ references = true;
79
+ if (node.type.name === 'image')
80
+ return node.type.create({ ...node.attrs, src: `inkkit-clipboard-image:${index++}` }, null, node.marks);
81
+ if (node.isText)
82
+ return node;
83
+ const children = [];
84
+ node.forEach((child) => children.push(rewrite(child)));
85
+ return node.copy(Fragment.fromArray(children));
86
+ };
87
+ const valid = content.firstChild?.isInline
88
+ ? Fragment.from(doc.type.schema.nodes.paragraph.create(null, content))
89
+ : content;
90
+ const copied = rewrite(doc.type.create(null, selectionContent(doc, valid)));
91
+ return references ? serialize(ctx, copied) : undefined;
92
+ }
93
+ function allLabels(doc) {
94
+ const labels = new Map([
95
+ ['link', new Set()],
96
+ ['footnote', new Set()],
97
+ ]);
98
+ doc.descendants((node) => {
99
+ if (isCode(node))
100
+ return false;
101
+ const id = key(node);
102
+ if (id) {
103
+ const separator = id.indexOf(':');
104
+ labels.get(id.slice(0, separator)).add(id.slice(separator + 1));
105
+ }
106
+ if (node.type.name === 'footnote_reference')
107
+ labels.get('footnote').add(node.attrs.identifier);
108
+ for (const mark of node.marks)
109
+ if (mark.type.name === 'link' && mark.attrs.identifier)
110
+ labels.get('link').add(mark.attrs.identifier);
111
+ if (node.isText) {
112
+ for (const match of node.text.matchAll(rawReferences))
113
+ labels
114
+ .get(match[1].startsWith('^') ? 'footnote' : 'link')
115
+ .add(normaliseLabel(match[1].startsWith('^')
116
+ ? match[1].slice(1)
117
+ : match[2] || match[1]));
118
+ }
119
+ });
120
+ return labels;
121
+ }
122
+ const rawReferences = /(?<![\\!])\[([^\]]+)\](?:\[([^\]]*)\])?(?!\()/g;
123
+ function isCode(node) {
124
+ return (node.type.name === 'code_block' ||
125
+ node.marks.some((mark) => mark.type.name === 'inlineCode'));
126
+ }
127
+ // Rename only the incoming document. Definitions belong to the insertion's undo transaction.
128
+ export function avoidReferenceCollisions(incoming, destination) {
129
+ const occupied = allLabels(destination);
130
+ const incomingLabels = allLabels(incoming);
131
+ const replacements = new Map();
132
+ for (const [kind, labels] of incomingLabels) {
133
+ const reserved = new Set([...occupied.get(kind), ...labels]);
134
+ for (const identifier of labels) {
135
+ if (!occupied.get(kind).has(identifier))
136
+ continue;
137
+ let suffix = 2;
138
+ while (reserved.has(normaliseLabel(`${identifier}-${suffix}`)))
139
+ suffix++;
140
+ const label = `${identifier}-${suffix}`;
141
+ reserved.add(normaliseLabel(label));
142
+ replacements.set(`${kind}:${identifier}`, label);
143
+ }
144
+ }
145
+ if (!replacements.size)
146
+ return incoming;
147
+ const rewrite = (node) => {
148
+ if (isCode(node))
149
+ return node;
150
+ const kind = node.type.name.startsWith('footnote_') ? 'footnote' : 'link';
151
+ const label = replacements.get(`${kind}:${node.attrs.identifier}`);
152
+ const attrs = label
153
+ ? { ...node.attrs, label, identifier: normaliseLabel(label) }
154
+ : node.attrs;
155
+ const marks = node.marks.map((mark) => {
156
+ const replacement = replacements.get(`link:${mark.attrs.identifier}`);
157
+ return replacement && mark.type.name === 'link'
158
+ ? mark.type.create({
159
+ ...mark.attrs,
160
+ label: replacement,
161
+ identifier: normaliseLabel(replacement),
162
+ referenceType: 'full',
163
+ })
164
+ : mark;
165
+ });
166
+ if (node.isText) {
167
+ const text = node.text.replace(rawReferences, (raw, display, reference) => {
168
+ const footnote = display.startsWith('^')
169
+ ? display.slice(1)
170
+ : undefined;
171
+ const replacement = replacements.get(`${footnote ? 'footnote' : 'link'}:${normaliseLabel(footnote ?? (reference || display))}`);
172
+ return replacement
173
+ ? footnote
174
+ ? `[^${replacement}]`
175
+ : `[${display}][${replacement}]`
176
+ : raw;
177
+ });
178
+ return node.type.schema.text(text, marks);
179
+ }
180
+ const children = [];
181
+ node.forEach((child) => children.push(rewrite(child)));
182
+ return node.type.create(attrs, children, marks);
183
+ };
184
+ return rewrite(incoming);
185
+ }
186
+ export function markdownFromHTML(html) {
187
+ const template = document.createElement('template');
188
+ template.innerHTML = html;
189
+ const element = template.content.querySelector('[data-inkkit-markdown]');
190
+ return element?.getAttribute('data-inkkit-markdown') ?? undefined;
191
+ }
192
+ export function withReferenceMetadata(html, markdown) {
193
+ const container = document.createElement('div');
194
+ container.setAttribute('data-inkkit-markdown', markdown);
195
+ container.innerHTML = html;
196
+ return container.outerHTML;
197
+ }
@@ -0,0 +1,14 @@
1
+ import type { Node as ProseNode } from '@milkdown/kit/prose/model';
2
+ export declare function normaliseLabel(label: string): string;
3
+ export interface LocatedDefinition {
4
+ node: ProseNode;
5
+ pos: number;
6
+ }
7
+ export declare const referenceDefinitions: (doc: ProseNode) => Map<string, LocatedDefinition>;
8
+ export declare const footnoteDefinitions: (doc: ProseNode) => Map<string, LocatedDefinition>;
9
+ export declare const remarkReferencesPlugin: import("@milkdown/kit/utils").$Remark<"inkkitReferences", unknown>;
10
+ export declare const referenceLinks: import("@milkdown/kit/utils").$MarkSchema<"link">;
11
+ export declare const referenceDefinition: import("@milkdown/kit/utils").$Node;
12
+ export declare const footnoteReference: import("@milkdown/kit/utils").$Node;
13
+ export declare const footnoteDefinition: import("@milkdown/kit/utils").$Node;
14
+ export declare const references: (import("@milkdown/kit/utils").$Ctx<import("@milkdown/kit/utils").GetMarkSchema, "link"> | import("@milkdown/kit/utils").$Mark | import("@milkdown/kit/utils").$Node | import("@milkdown/kit/utils").$Prose)[];