@aicayzer/inkkit 0.0.1 → 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.
- package/README.md +3 -1
- package/dist/clipboard.js +22 -1
- package/dist/dialect.js +4 -2
- package/dist/editor.d.ts +4 -0
- package/dist/editor.js +183 -8
- package/dist/literals.d.ts +3 -0
- package/dist/literals.js +32 -17
- package/dist/paste.js +60 -5
- package/dist/preserve.js +234 -8
- package/dist/reference-clipboard.d.ts +8 -0
- package/dist/reference-clipboard.js +197 -0
- package/dist/references.d.ts +14 -0
- package/dist/references.js +367 -0
- package/dist/style.css +27 -0
- package/dist/types.d.ts +1 -0
- package/package.json +7 -4
package/dist/preserve.js
CHANGED
|
@@ -1,5 +1,172 @@
|
|
|
1
1
|
import { parserCtx, remarkCtx } from '@milkdown/kit/core';
|
|
2
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));|	)';
|
|
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' ? '	' : ' '));
|
|
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
|
+
}
|
|
3
170
|
export class PreservationError extends Error {
|
|
4
171
|
constructor(message, options) {
|
|
5
172
|
super(message, options);
|
|
@@ -16,6 +183,50 @@ function blockForm(ctx, node) {
|
|
|
16
183
|
return '<br />';
|
|
17
184
|
return form(serialize(ctx, node.type.schema.topNodeType.create(null, [node])));
|
|
18
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
|
+
}
|
|
19
230
|
// Edit distance distinguishes replacements from insertions, so replacing a block
|
|
20
231
|
// retains its surrounding whitespace without assigning it a neighbour's source.
|
|
21
232
|
function correspondence(before, after) {
|
|
@@ -86,10 +297,11 @@ export class Preservation {
|
|
|
86
297
|
const processor = ctx.get(remarkCtx);
|
|
87
298
|
const offset = source.startsWith('\uFEFF') ? 1 : 0;
|
|
88
299
|
const body = source.slice(offset);
|
|
89
|
-
const
|
|
300
|
+
const rawTree = processor.parse(source);
|
|
301
|
+
const tree = processor.runSync(structuredClone(rawTree), {
|
|
90
302
|
value: body,
|
|
91
303
|
});
|
|
92
|
-
for (const node of tree.children) {
|
|
304
|
+
for (const [index, node] of tree.children.entries()) {
|
|
93
305
|
let start = node.position?.start.offset;
|
|
94
306
|
const end = node.position?.end.offset;
|
|
95
307
|
if (start == null || end == null)
|
|
@@ -100,13 +312,16 @@ export class Preservation {
|
|
|
100
312
|
if (/^[ \t]*$/.test(source.slice(firstColumn, start)))
|
|
101
313
|
start = firstColumn;
|
|
102
314
|
const text = source.slice(start, sourceEnd);
|
|
103
|
-
const parsed =
|
|
315
|
+
const parsed = this.baseline.childCount === tree.children.length
|
|
316
|
+
? this.baseline.type.create(null, [this.baseline.child(index)])
|
|
317
|
+
: parse(text);
|
|
104
318
|
this.blocks.push({
|
|
105
319
|
start,
|
|
106
320
|
end: sourceEnd,
|
|
107
321
|
form: parsed.childCount === 1
|
|
108
322
|
? blockForm(ctx, parsed.firstChild)
|
|
109
323
|
: form(serialize(ctx, parsed)),
|
|
324
|
+
ast: positionalNode(rawTree, node),
|
|
110
325
|
});
|
|
111
326
|
}
|
|
112
327
|
}
|
|
@@ -135,6 +350,12 @@ export class Preservation {
|
|
|
135
350
|
current.push(fingerprint);
|
|
136
351
|
});
|
|
137
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/, '');
|
|
138
359
|
let output = '';
|
|
139
360
|
let previous;
|
|
140
361
|
current.forEach((text, index) => {
|
|
@@ -151,10 +372,13 @@ export class Preservation {
|
|
|
151
372
|
}
|
|
152
373
|
else
|
|
153
374
|
output += this.lineEnding.repeat(2);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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);
|
|
158
382
|
previous = position;
|
|
159
383
|
});
|
|
160
384
|
if (previous === this.blocks.length - 1 && this.blocks.length)
|
|
@@ -162,7 +386,9 @@ export class Preservation {
|
|
|
162
386
|
else if (output)
|
|
163
387
|
output += this.lineEnding;
|
|
164
388
|
const parse = this.ctx.get(parserCtx);
|
|
165
|
-
|
|
389
|
+
const reopened = parse(output);
|
|
390
|
+
if (serialize(this.ctx, reopened) === canonical &&
|
|
391
|
+
semanticSignature(reopened) === semanticSignature(doc))
|
|
166
392
|
return output;
|
|
167
393
|
throw new PreservationError('The edited Markdown cannot be reopened without changing its content');
|
|
168
394
|
}
|
|
@@ -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)[];
|