@aicayzer/inkkit 0.0.17 → 0.1.0

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.
Files changed (81) hide show
  1. package/NOTICE +4 -2
  2. package/README.md +12 -6
  3. package/dist/THIRD-PARTY-NOTICES.txt +2742 -0
  4. package/dist/chunks/core-vtCr4MBS.js +56503 -0
  5. package/dist/chunks/highlight-Daj2fv_u.js +14122 -0
  6. package/dist/core.d.ts +4 -0
  7. package/dist/core.js +2 -0
  8. package/dist/editor.d.ts +261 -48
  9. package/dist/index.d.ts +1 -7
  10. package/dist/index.js +5 -2
  11. package/dist/style.css +242 -221
  12. package/dist/types.d.ts +143 -18
  13. package/package.json +26 -15
  14. package/dist/autolink.d.ts +0 -1
  15. package/dist/autolink.js +0 -18
  16. package/dist/callouts.d.ts +0 -26
  17. package/dist/callouts.js +0 -311
  18. package/dist/clipboard.d.ts +0 -11
  19. package/dist/clipboard.js +0 -261
  20. package/dist/code-fences.d.ts +0 -8
  21. package/dist/code-fences.js +0 -166
  22. package/dist/comments.d.ts +0 -55
  23. package/dist/comments.js +0 -494
  24. package/dist/decorations.d.ts +0 -2
  25. package/dist/decorations.js +0 -67
  26. package/dist/dialect.d.ts +0 -9
  27. package/dist/dialect.js +0 -358
  28. package/dist/editor.js +0 -2441
  29. package/dist/files.d.ts +0 -40
  30. package/dist/files.js +0 -133
  31. package/dist/highlight.d.ts +0 -1
  32. package/dist/highlight.js +0 -136
  33. package/dist/image-blocks.d.ts +0 -71
  34. package/dist/image-blocks.js +0 -286
  35. package/dist/images.d.ts +0 -51
  36. package/dist/images.js +0 -398
  37. package/dist/inline-highlight.d.ts +0 -6
  38. package/dist/inline-highlight.js +0 -112
  39. package/dist/labels.d.ts +0 -36
  40. package/dist/labels.js +0 -34
  41. package/dist/linked-syntax.d.ts +0 -48
  42. package/dist/linked-syntax.js +0 -320
  43. package/dist/literals.d.ts +0 -19
  44. package/dist/literals.js +0 -211
  45. package/dist/mermaid.d.ts +0 -8
  46. package/dist/mermaid.js +0 -321
  47. package/dist/outline.d.ts +0 -22
  48. package/dist/outline.js +0 -79
  49. package/dist/paste-html.d.ts +0 -23
  50. package/dist/paste-html.js +0 -441
  51. package/dist/paste.d.ts +0 -26
  52. package/dist/paste.js +0 -482
  53. package/dist/portable-files.d.ts +0 -10
  54. package/dist/portable-files.js +0 -36
  55. package/dist/preserve.d.ts +0 -21
  56. package/dist/preserve.js +0 -757
  57. package/dist/print.d.ts +0 -7
  58. package/dist/print.js +0 -409
  59. package/dist/reference-clipboard.d.ts +0 -8
  60. package/dist/reference-clipboard.js +0 -201
  61. package/dist/references.d.ts +0 -14
  62. package/dist/references.js +0 -368
  63. package/dist/search.d.ts +0 -15
  64. package/dist/search.js +0 -202
  65. package/dist/selection.d.ts +0 -1
  66. package/dist/selection.js +0 -59
  67. package/dist/source.d.ts +0 -16
  68. package/dist/source.js +0 -57
  69. package/dist/spreadsheet.d.ts +0 -3
  70. package/dist/spreadsheet.js +0 -275
  71. package/dist/table-alignment.d.ts +0 -2
  72. package/dist/table-alignment.js +0 -46
  73. package/dist/tables.d.ts +0 -27
  74. package/dist/tables.js +0 -322
  75. package/dist/tasks.d.ts +0 -3
  76. package/dist/tasks.js +0 -51
  77. package/dist/text-ranges.d.ts +0 -29
  78. package/dist/text-ranges.js +0 -256
  79. package/dist/types.js +0 -8
  80. package/dist/wiki-view.d.ts +0 -21
  81. package/dist/wiki-view.js +0 -78
package/dist/preserve.js DELETED
@@ -1,757 +0,0 @@
1
- import { parserCtx, remarkCtx } from '@milkdown/kit/core';
2
- import { serialize } from './dialect.js';
3
- import { restoreCodeEndings, finalCodeEnding } from './code-fences.js';
4
- import { sourceFrontmatter } from './literals.js';
5
- function signature(node) {
6
- return JSON.stringify(node, (key, value) => ['position', 'data', 'marker'].includes(key) ? undefined : 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, quoteIndent = false, inTable = false) {
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
- if (index > 0 && raw[index - 1] === '\n') {
30
- const gutter = (quoteIndent ? /^(?:[ \t]*>[ \t]*)+/ : /^(?:[ \t]*>[ \t]?)+/).exec(raw.slice(index));
31
- if (gutter) {
32
- index += gutter[0].length;
33
- offsets[offsets.length - 1] = index;
34
- continue;
35
- }
36
- }
37
- const escaped = /^\\([!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~])/.exec(raw.slice(index));
38
- const entity = /^&(?:#[xX][\da-fA-F]+|#\d+|[A-Za-z][A-Za-z\d]+);/.exec(raw.slice(index));
39
- let value;
40
- let length;
41
- if (escaped) {
42
- value = escaped[1];
43
- length = escaped[0].length;
44
- }
45
- else if (entity) {
46
- const element = document.createElement('textarea');
47
- element.innerHTML = entity[0];
48
- value = element.value;
49
- length = entity[0].length;
50
- }
51
- else if (raw.slice(index, index + 2) === '\r\n') {
52
- value = '\n';
53
- length = 2;
54
- }
55
- else {
56
- value = raw[index];
57
- length = 1;
58
- }
59
- decoded += value;
60
- index += length;
61
- for (let part = 0; part < value.length; part++)
62
- offsets.push(index);
63
- }
64
- if (decoded !== before) {
65
- // Continuation indentation is excluded from a quoted paragraph's text.
66
- return quoteIndent ? after : retainText(raw, before, after, true, inTable);
67
- }
68
- const inserted = after
69
- .slice(prefix, after.length - suffix)
70
- .split('')
71
- .map((character, index) => {
72
- const at = prefix + index;
73
- const delimiter = (character === '%' || character === '=') &&
74
- (after[at - 1] === character || after[at + 1] === character);
75
- return delimiter ||
76
- /[\\`*_[\]<>]/.test(character) ||
77
- (inTable && character === '|')
78
- ? `\\${character}`
79
- : character;
80
- })
81
- .join('');
82
- return (raw.slice(0, offsets[prefix]) +
83
- inserted +
84
- raw.slice(offsets[before.length - suffix]));
85
- }
86
- function retainBoundaryWhitespace(raw, canonical) {
87
- // A middle-of-line space becomes indentation or trimmed text at a block edge.
88
- const token = '(?:[ \\t]|&#(?:[xX]0*(?:20|9)|0*(?:32|9));|&Tab;)';
89
- const decoded = (value) => (value.match(new RegExp(token, 'g')) ?? [])
90
- .map((value) => value === '\t' || /(?:9;|Tab;)$/.test(value) ? '\t' : ' ')
91
- .join('');
92
- const encode = (value) => value.replace(/[ \t]/g, (value) => (value === '\t' ? '&#x9;' : '&#x20;'));
93
- const start = /^[ \t]+/.exec(raw)?.[0];
94
- const rawStart = new RegExp(`^${token}+`).exec(raw)?.[0];
95
- const encodedStart = new RegExp(`^${token}+`).exec(canonical)?.[0];
96
- if (start &&
97
- rawStart &&
98
- encodedStart?.includes('&') &&
99
- decoded(rawStart) === decoded(encodedStart))
100
- raw = encode(start) + raw.slice(start.length);
101
- const end = /[ \t]+$/.exec(raw)?.[0];
102
- const rawEnd = new RegExp(`${token}+$`).exec(raw)?.[0];
103
- const encodedEnd = new RegExp(`${token}+$`).exec(canonical)?.[0];
104
- if (end &&
105
- rawEnd &&
106
- encodedEnd?.includes('&') &&
107
- decoded(rawEnd) === decoded(encodedEnd))
108
- raw = raw.slice(0, -end.length) + encode(end);
109
- return raw;
110
- }
111
- // Patch changed leaves so an adjacent edit retains authored delimiters and labels.
112
- function retainSource(before, after, source, canonical, lineEnding, inTable = false) {
113
- const start = before.position?.start.offset;
114
- const end = before.position?.end.offset;
115
- const newStart = after.position?.start.offset;
116
- const newEnd = after.position?.end.offset;
117
- if (start == null || end == null || newStart == null || newEnd == null)
118
- throw new PreservationError('A source token has no location');
119
- const raw = source.slice(start, end);
120
- if (before.type === 'code' &&
121
- after.type === 'code' &&
122
- before.lang === after.lang) {
123
- const opening = /^([ \t>]*)(`{3,}|~{3,})([^\r\n]*)(\r?\n)/.exec(raw);
124
- if (opening) {
125
- const lines = raw.slice(opening[0].length).split(/\r?\n/);
126
- const closing = /^([ \t>]*)(`{3,}|~{3,})([ \t]*)$/.exec(lines.at(-1) ?? '');
127
- const originalLine = (before.value ?? '').replace(/\r\n?/g, '\n').split('\n')[0] ?? '';
128
- const first = lines[0] ?? '';
129
- const gutter = first.endsWith(originalLine)
130
- ? first.slice(0, first.length - originalLine.length)
131
- : opening[1];
132
- const body = (after.value ?? '').replace(/\r\n?/g, '\n').split('\n');
133
- const character = opening[2][0];
134
- let length = opening[2].length;
135
- for (const line of body) {
136
- const run = new RegExp(`^ {0,3}(${character}+)[ \\t]*$`).exec(line);
137
- if (run)
138
- length = Math.max(length, run[1].length + 1);
139
- }
140
- const fence = character.repeat(length);
141
- return (opening[1] +
142
- fence +
143
- opening[3] +
144
- lineEnding +
145
- body.map((line) => gutter + line).join(lineEnding) +
146
- (closing
147
- ? lineEnding +
148
- closing[1] +
149
- character.repeat(Math.max(length, closing[2].length)) +
150
- closing[3]
151
- : ''));
152
- }
153
- }
154
- if (before.type === 'definition' &&
155
- after.type === 'definition' &&
156
- before.label === after.label &&
157
- before.title === after.title) {
158
- const destination = /^(\[[\s\S]*?\]:[ \t]*)(<[^>\n]*>|[^\s]+)([\s\S]*)$/.exec(raw);
159
- if (destination)
160
- return (destination[1] +
161
- (destination[2].startsWith('<')
162
- ? `<${after.url}>`
163
- : String(after.url)) +
164
- destination[3]);
165
- }
166
- if (before.type === 'text' && after.type === 'text')
167
- return retainBoundaryWhitespace(withEnding(retainText(raw, before.value, after.value, false, inTable), lineEnding), canonical.slice(newStart, newEnd));
168
- if (signature(before) === signature(after) &&
169
- ownShape(before) === ownShape(after))
170
- return raw;
171
- if (before.type === after.type &&
172
- ownShape(before) === ownShape(after) &&
173
- before.children &&
174
- after.children &&
175
- before.children.length === after.children.length) {
176
- let output = '', previous = start;
177
- const retained = new Set();
178
- before.children.forEach((child, index) => {
179
- const incoming = after.children[index];
180
- const movedIndex = before.type === 'table' || before.type === 'tableRow'
181
- ? before.children.findIndex((candidate, candidateIndex) => !retained.has(candidateIndex) &&
182
- signature(candidate) === signature(incoming) &&
183
- ownShape(candidate) === ownShape(incoming))
184
- : -1;
185
- const original = movedIndex < 0 ? child : before.children[movedIndex];
186
- if (movedIndex >= 0)
187
- retained.add(movedIndex);
188
- const childStart = child.position?.start.offset, childEnd = child.position?.end.offset;
189
- if (childStart == null || childEnd == null)
190
- throw new PreservationError('A source token has no location');
191
- let retainedSource = retainSource(original, incoming, source, canonical, lineEnding, inTable || before.type === 'tableCell');
192
- if (before.type === 'tableRow' && movedIndex >= 0) {
193
- // Cell positions own different pipe boundaries at the row edges.
194
- const originalFrom = original.children?.[0]?.position?.start.offset;
195
- const originalTo = original.children?.at(-1)?.position?.end.offset;
196
- const from = child.children?.[0]?.position?.start.offset;
197
- const to = child.children?.at(-1)?.position?.end.offset;
198
- retainedSource =
199
- originalFrom != null &&
200
- originalTo != null &&
201
- from != null &&
202
- to != null
203
- ? source.slice(childStart, from) +
204
- source.slice(originalFrom, originalTo) +
205
- source.slice(to, childEnd)
206
- : withEnding(canonical.slice(incoming.position.start.offset, incoming.position.end.offset), lineEnding);
207
- }
208
- output += source.slice(previous, childStart) + retainedSource;
209
- previous = childEnd;
210
- });
211
- return output + source.slice(previous, end);
212
- }
213
- if (before.type === after.type &&
214
- ownShape(before) === ownShape(after) &&
215
- before.children &&
216
- after.children) {
217
- let prefix = 0, suffix = 0;
218
- while (prefix < before.children.length &&
219
- prefix < after.children.length &&
220
- signature(before.children[prefix]) === signature(after.children[prefix]))
221
- prefix++;
222
- while (suffix < before.children.length - prefix &&
223
- suffix < after.children.length - prefix &&
224
- signature(before.children[before.children.length - 1 - suffix]) ===
225
- signature(after.children[after.children.length - 1 - suffix]))
226
- suffix++;
227
- const from = prefix
228
- ? before.children[prefix - 1].position?.end.offset
229
- : start;
230
- const to = suffix
231
- ? before.children[before.children.length - suffix].position?.start.offset
232
- : end;
233
- const newFrom = prefix
234
- ? after.children[prefix - 1].position?.end.offset
235
- : newStart;
236
- const newTo = suffix
237
- ? after.children[after.children.length - suffix].position?.start.offset
238
- : newEnd;
239
- if (from != null && to != null && newFrom != null && newTo != null)
240
- return retainBoundaryWhitespace(source.slice(start, from) +
241
- withEnding(canonical.slice(newFrom, newTo), lineEnding) +
242
- source.slice(to, end), canonical.slice(newStart, newEnd));
243
- }
244
- return withEnding(canonical.slice(newStart, newEnd), lineEnding);
245
- }
246
- // Canonical Markdown keeps authored CRLF inside literal blocks and code fences.
247
- function withEnding(text, lineEnding) {
248
- return lineEnding === '\n' ? text : text.replace(/\r?\n/g, lineEnding);
249
- }
250
- function positionalNode(tree, transformed) {
251
- return (tree.children.find((node) => node.position?.start.offset === transformed.position?.start.offset &&
252
- node.position?.end.offset === transformed.position?.end.offset) ?? transformed);
253
- }
254
- export class PreservationError extends Error {
255
- constructor(message, options) {
256
- super(message, options);
257
- this.name = 'PreservationError';
258
- }
259
- }
260
- function form(text) {
261
- return text.replace(/\r\n?/g, '\n').replace(/\n+$/, '');
262
- }
263
- function blockForm(ctx, node) {
264
- // An empty document is empty Markdown, but an empty block inside a document
265
- // is authored spacing and must remain a real block when read again.
266
- if (node.type.name === 'paragraph' && node.content.size === 0)
267
- return '<br />';
268
- return form(serialize(ctx, node.type.schema.topNodeType.create(null, [node])));
269
- }
270
- function semanticSignature(doc) {
271
- const clean = (value) => {
272
- if (value.type === 'inkkit_wiki_link' && value.attrs) {
273
- const { raw: _raw, inTable: _inTable, ...attrs } = value.attrs;
274
- value = { ...value, attrs };
275
- }
276
- const content = value.content;
277
- if (!content)
278
- return value;
279
- const tokens = [];
280
- for (const child of content) {
281
- if (child.type !== 'text') {
282
- tokens.push(clean(child));
283
- continue;
284
- }
285
- const parts = /^(\s*)([\s\S]*?)(\s*)$/.exec(String(child.text));
286
- for (const [index, text] of parts.slice(1).entries()) {
287
- if (!text)
288
- continue;
289
- const token = {
290
- ...child,
291
- text: text.replace(/\r\n?/g, '\n'),
292
- marks: index === 1 ? child.marks : undefined,
293
- };
294
- const previous = tokens.at(-1);
295
- if (previous?.type === 'text' &&
296
- JSON.stringify(previous.marks) === JSON.stringify(token.marks))
297
- previous.text = String(previous.text) + text;
298
- else
299
- tokens.push(token);
300
- }
301
- }
302
- return { ...value, content: tokens };
303
- };
304
- const json = JSON.parse(JSON.stringify(doc.toJSON(), (key, value) =>
305
- // Fresh list items default to loose even when their Markdown reopens tight.
306
- [
307
- 'referenceContent',
308
- 'referenceType',
309
- 'listPlaceholder',
310
- 'marker',
311
- 'id',
312
- 'label',
313
- 'spread',
314
- 'authoredFence',
315
- 'inkkitFileRaw',
316
- 'inkkitFileWidth',
317
- 'inkkitFileInTable',
318
- ].includes(key)
319
- ? undefined
320
- : value));
321
- return JSON.stringify(clean(json));
322
- }
323
- // Milkdown derives heading DOM IDs; Markdown stores the level and content.
324
- function withoutHeadingId(node) {
325
- return node.type.name === 'heading' && node.attrs.id
326
- ? node.type.create({ ...node.attrs, id: '' }, node.content, node.marks)
327
- : node;
328
- }
329
- const MATCHING_BUDGET = 1_000_000;
330
- const SIMILARITY_BUDGET = 4_000_000;
331
- // Unchanged blocks are anchored first, so a long document with many scattered
332
- // edits is matched as many short gaps rather than one wide span.
333
- export function correspondence(before, after, resemblance = true) {
334
- const result = new Array(after.length);
335
- const work = {
336
- budget: MATCHING_BUDGET,
337
- similarity: resemblance ? SIMILARITY_BUDGET : 0,
338
- };
339
- const spans = [{ b: 0, a: 0, bEnd: before.length, aEnd: after.length }];
340
- for (let span = spans.pop(); span; span = spans.pop()) {
341
- let { b, a, bEnd, aEnd } = span;
342
- while (b < bEnd && a < aEnd && before[b] === after[a])
343
- result[a++] = b++;
344
- while (b < bEnd && a < aEnd && before[bEnd - 1] === after[aEnd - 1])
345
- result[--aEnd] = --bEnd;
346
- if (b === bEnd || a === aEnd)
347
- continue;
348
- work.budget -= bEnd - b + (aEnd - a);
349
- if (work.budget < 0)
350
- throw tooManyBlocks();
351
- const anchors = uniqueAnchors(before, after, b, bEnd, a, aEnd);
352
- if (!anchors.length) {
353
- align(before, after, b, bEnd, a, aEnd, result, work);
354
- continue;
355
- }
356
- for (const anchor of anchors) {
357
- result[anchor.a] = anchor.b;
358
- spans.push({ b, a, bEnd: anchor.b, aEnd: anchor.a });
359
- b = anchor.b + 1;
360
- a = anchor.a + 1;
361
- }
362
- spans.push({ b, a, bEnd, aEnd });
363
- }
364
- return result;
365
- }
366
- function tooManyBlocks() {
367
- return new PreservationError('The edit changes too many blocks to preserve safely');
368
- }
369
- // Blocks that occur exactly once on each side, in the longest subsequence whose
370
- // order the two sides share. A repeated block is never an anchor: which copy it
371
- // belongs to is not known until its neighbours are placed.
372
- function uniqueAnchors(before, after, b, bEnd, a, aEnd) {
373
- const once = (list, from, to) => {
374
- const at = new Map();
375
- for (let i = from; i < to; i++)
376
- at.set(list[i], at.has(list[i]) ? -1 : i);
377
- return at;
378
- };
379
- const beforeAt = once(before, b, bEnd);
380
- const afterAt = once(after, a, aEnd);
381
- const pairs = [];
382
- for (let i = a; i < aEnd; i++) {
383
- const match = beforeAt.get(after[i]);
384
- if (match != null && match >= 0 && afterAt.get(after[i]) === i)
385
- pairs.push({ b: match, a: i });
386
- }
387
- // Patience sorting: tails[k] ends the best increasing subsequence of length k + 1.
388
- const tails = [];
389
- const previous = new Array(pairs.length);
390
- for (const [index, pair] of pairs.entries()) {
391
- let low = 0;
392
- let high = tails.length;
393
- while (low < high) {
394
- const middle = (low + high) >> 1;
395
- if (pairs[tails[middle]].b < pair.b)
396
- low = middle + 1;
397
- else
398
- high = middle;
399
- }
400
- previous[index] = low ? tails[low - 1] : -1;
401
- tails[low] = index;
402
- }
403
- const anchors = [];
404
- let at = tails.length ? tails[tails.length - 1] : -1;
405
- while (at >= 0) {
406
- anchors.push(pairs[at]);
407
- at = previous[at];
408
- }
409
- return anchors.reverse();
410
- }
411
- // Replacing a block costs more than inserting or deleting one, so an unchanged
412
- // block keeps its own copy rather than pairing with a deleted neighbour, and
413
- // less than both, so an edited block still keeps its surrounding whitespace.
414
- const INDEL = 2;
415
- const REPLACE = 3;
416
- function align(before, after, b, bEnd, a, aEnd, result, work) {
417
- const rows = bEnd - b + 1;
418
- const columns = aEnd - a + 1;
419
- const offset = Math.abs(rows - columns);
420
- let width = Math.max(1, offset);
421
- for (;;) {
422
- const costs = [];
423
- const get = (i, j) => i < 0 || j < 0 || Math.abs(i - j) > width
424
- ? Infinity
425
- : (costs[i]?.[j - Math.max(0, i - width)] ?? Infinity);
426
- for (let i = 0; i < rows; i++) {
427
- const from = Math.max(0, i - width);
428
- const to = Math.min(columns - 1, i + width);
429
- work.budget -= to - from + 1;
430
- if (work.budget < 0)
431
- throw tooManyBlocks();
432
- const row = new Uint32Array(to - from + 1);
433
- costs.push(row);
434
- for (let j = from; j <= to; j++) {
435
- const change = before[b + i - 1] === after[a + j - 1] ? 0 : REPLACE;
436
- row[j - from] =
437
- i === 0
438
- ? j * INDEL
439
- : j === 0
440
- ? i * INDEL
441
- : Math.min(get(i - 1, j) + INDEL, get(i, j - 1) + INDEL, get(i - 1, j - 1) + change);
442
- }
443
- }
444
- // A path that leaves the band takes at least width + 1 insertions or
445
- // deletions away from the diagonal and width + 1 - offset back to its end,
446
- // so a path inside the band that costs no more is optimal. Widen only when
447
- // needed, charging every attempt to the same work budget.
448
- if (width < Math.max(rows, columns) - 1 &&
449
- get(rows - 1, columns - 1) > INDEL * (2 * (width + 1) - offset)) {
450
- width *= 2;
451
- continue;
452
- }
453
- const unchanged = [];
454
- let i = rows - 1;
455
- let j = columns - 1;
456
- while (i || j) {
457
- const same = i && j && before[b + i - 1] === after[a + j - 1];
458
- if (i && j && get(i, j) === get(i - 1, j - 1) + (same ? 0 : REPLACE)) {
459
- result[a + j - 1] = b + i - 1;
460
- if (same)
461
- unchanged.push({ b: b + i - 1, a: a + j - 1 });
462
- i--;
463
- j--;
464
- }
465
- else if (i && get(i, j) === get(i - 1, j) + INDEL)
466
- i--;
467
- else
468
- j--;
469
- }
470
- unchanged.push({ b: b - 1, a: a - 1 });
471
- unchanged.reverse().push({ b: bEnd, a: aEnd });
472
- for (let index = 1; index < unchanged.length; index++)
473
- pairSimilar(before, after, unchanged[index - 1].b + 1, unchanged[index].b, unchanged[index - 1].a + 1, unchanged[index].a, result, work);
474
- return;
475
- }
476
- }
477
- // Between unchanged blocks, edit distance prices every replacement alike, so
478
- // it cannot tell which original an edited block came from. Pair each with the
479
- // original it most resembles, keeping their order: typing in a block then keeps
480
- // its own source even when a neighbour was inserted or deleted beside it.
481
- function pairSimilar(before, after, b, bEnd, a, aEnd, result, work) {
482
- const removed = bEnd - b;
483
- const added = aEnd - a;
484
- if (!removed || !added || (removed === 1 && added === 1))
485
- return;
486
- // Resemblance decides; among equally resembling pairings, more pairs keep
487
- // more replaced blocks in place with their surrounding whitespace.
488
- const bonus = 1 / (Math.min(removed, added) + 1);
489
- // This is a refinement with its own budget of characters compared. When it
490
- // runs out, the alignment's pairing stands rather than the edit failing.
491
- if (removed * added > work.similarity)
492
- return;
493
- const weights = new Float64Array(removed * added);
494
- for (let x = 0; x < removed; x++)
495
- for (let y = 0; y < added; y++) {
496
- const shared = similarity(before[b + x], after[a + y]);
497
- work.similarity -= shared + 1;
498
- if (work.similarity < 0)
499
- return;
500
- weights[x * added + y] = shared + bonus;
501
- }
502
- const best = Array.from({ length: removed + 1 }, () => new Float64Array(added + 1));
503
- for (let x = 1; x <= removed; x++)
504
- for (let y = 1; y <= added; y++)
505
- best[x][y] = Math.max(best[x - 1][y], best[x][y - 1], best[x - 1][y - 1] + weights[(x - 1) * added + y - 1]);
506
- for (let index = a; index < aEnd; index++)
507
- result[index] = undefined;
508
- for (let x = removed, y = added; x && y;) {
509
- if (best[x][y] ===
510
- best[x - 1][y - 1] + weights[(x - 1) * added + y - 1]) {
511
- result[a + --y] = b + --x;
512
- }
513
- else if (best[x][y] === best[x - 1][y])
514
- x--;
515
- else
516
- y--;
517
- }
518
- }
519
- // Characters an edit left in place at the start and end of a block.
520
- function similarity(left, right) {
521
- const limit = Math.min(left.length, right.length);
522
- let prefix = 0;
523
- while (prefix < limit && left[prefix] === right[prefix])
524
- prefix++;
525
- let suffix = 0;
526
- while (suffix < limit - prefix &&
527
- left[left.length - 1 - suffix] === right[right.length - 1 - suffix])
528
- suffix++;
529
- return prefix + suffix;
530
- }
531
- /** Retains the loaded source of blocks the edit leaves alone. */
532
- export class Preservation {
533
- ctx;
534
- source;
535
- baseline;
536
- blocks = [];
537
- lineEnding;
538
- originalForms = new Set();
539
- forms = new WeakMap();
540
- textBlockEdits = new WeakMap();
541
- constructor(ctx, source) {
542
- this.ctx = ctx;
543
- this.source = source;
544
- try {
545
- const parse = ctx.get(parserCtx);
546
- this.baseline = parse(source);
547
- this.lineEnding =
548
- source.includes('\r\n') && !source.replaceAll('\r\n', '').includes('\n')
549
- ? '\r\n'
550
- : '\n';
551
- const processor = ctx.get(remarkCtx);
552
- const offset = source.startsWith('\uFEFF') ? 1 : 0;
553
- const body = source.slice(offset);
554
- const rawTree = processor.parse(source);
555
- const tree = processor.runSync(structuredClone(rawTree), {
556
- value: body,
557
- });
558
- for (const [index, node] of tree.children.entries()) {
559
- let start = node.position?.start.offset;
560
- const end = node.position?.end.offset;
561
- if (start == null || end == null)
562
- throw new PreservationError('A source block has no location');
563
- start += offset;
564
- const sourceEnd = end + offset;
565
- const firstColumn = source.lastIndexOf('\n', start - 1) + 1;
566
- if (/^[ \t]*$/.test(source.slice(firstColumn, start)))
567
- start = firstColumn;
568
- const text = source.slice(start, sourceEnd);
569
- const parsed = this.baseline.childCount === tree.children.length
570
- ? this.baseline.type.create(null, [this.baseline.child(index)])
571
- : parse(text);
572
- this.blocks.push({
573
- start,
574
- end: sourceEnd,
575
- form: parsed.childCount === 1
576
- ? blockForm(ctx, parsed.firstChild)
577
- : form(serialize(ctx, parsed)),
578
- ast: positionalNode(rawTree, node),
579
- });
580
- }
581
- for (const block of this.blocks)
582
- this.originalForms.add(block.form);
583
- }
584
- catch (cause) {
585
- if (cause instanceof PreservationError)
586
- throw cause;
587
- throw new PreservationError('Cannot read Markdown for preservation', {
588
- cause,
589
- });
590
- }
591
- }
592
- retainBlock(block, after, canonical) {
593
- const offset = this.source.startsWith('\uFEFF') ? 1 : 0;
594
- const start = block.ast.position?.start.offset;
595
- if (start == null)
596
- throw new PreservationError('A source block has no location');
597
- return (this.source.slice(block.start, start + offset) +
598
- retainSource(block.ast, after, this.source.slice(offset), canonical, this.lineEnding));
599
- }
600
- independentTextBlocks(doc) {
601
- if (this.baseline.childCount !== this.blocks.length ||
602
- doc.childCount !== this.blocks.length ||
603
- !doc.sameMarkup(this.baseline))
604
- return undefined;
605
- let output = '', previous = 0;
606
- for (let index = 0; index < doc.childCount; index++) {
607
- const node = doc.child(index);
608
- const before = this.baseline.child(index);
609
- const block = this.blocks[index];
610
- if (node.eq(before))
611
- continue;
612
- let retained = this.textBlockEdits.get(node);
613
- if (retained?.index !== index) {
614
- const raw = this.source.slice(block.start, block.end);
615
- const expected = withoutHeadingId(node);
616
- // References and HTML/comments can depend on other blocks. Restrict
617
- // local validation to text blocks without these context-sensitive forms.
618
- const contextual = /[\[<]|%%/;
619
- const textOnly = (block) => {
620
- let valid = true;
621
- block.forEach((child) => {
622
- if (!child.isText)
623
- valid = false;
624
- });
625
- return valid;
626
- };
627
- if (!['paragraph', 'heading'].includes(node.type.name) ||
628
- !expected.sameMarkup(withoutHeadingId(before)) ||
629
- !textOnly(node) ||
630
- !textOnly(before) ||
631
- contextual.test(raw) ||
632
- (index > 0 &&
633
- !/\r?\n[ \t]*\r?\n/.test(this.source.slice(this.blocks[index - 1].end, block.start))) ||
634
- (index + 1 < this.blocks.length &&
635
- !/\r?\n[ \t]*\r?\n/.test(this.source.slice(block.end, this.blocks[index + 1].start))))
636
- return undefined;
637
- const canonical = blockForm(this.ctx, node);
638
- if (contextual.test(canonical) ||
639
- (canonical !== block.form && this.originalForms.has(canonical)))
640
- return undefined;
641
- const tree = this.ctx.get(remarkCtx).parse(canonical);
642
- if (tree.children.length !== 1)
643
- return undefined;
644
- const text = this.retainBlock(block, tree.children[0], canonical);
645
- const reopened = this.ctx.get(parserCtx)(text);
646
- if (reopened.childCount !== 1)
647
- return undefined;
648
- if (!withoutHeadingId(reopened.firstChild).eq(expected))
649
- return undefined;
650
- retained = { index, text };
651
- this.textBlockEdits.set(node, retained);
652
- }
653
- output += this.source.slice(previous, block.start) + retained.text;
654
- previous = block.end;
655
- }
656
- return output + this.source.slice(previous);
657
- }
658
- serialize(doc) {
659
- if (doc.eq(this.baseline))
660
- return this.source;
661
- try {
662
- const independent = this.independentTextBlocks(doc);
663
- if (independent !== undefined)
664
- return independent;
665
- const canonical = serialize(this.ctx, doc);
666
- if (canonical === '')
667
- return this.source.startsWith('\uFEFF') ? '\uFEFF' : '';
668
- const current = [];
669
- doc.forEach((node) => {
670
- let fingerprint = this.forms.get(node);
671
- if (fingerprint == null) {
672
- fingerprint = blockForm(this.ctx, node);
673
- this.forms.set(node, fingerprint);
674
- }
675
- current.push(fingerprint);
676
- });
677
- const forms = this.blocks.map((block) => block.form);
678
- const resembling = correspondence(forms, current);
679
- const processor = this.ctx.get(remarkCtx);
680
- const currentRawTree = processor.parse(canonical);
681
- const currentTree = processor.runSync(structuredClone(currentRawTree), {
682
- value: canonical,
683
- });
684
- const parse = this.ctx.get(parserCtx);
685
- const normalized = (text) => text.replace(/\r\n?/g, '\n');
686
- const reconstruct = (positions) => {
687
- let output = '';
688
- let previous;
689
- current.forEach((text, index) => {
690
- const position = positions[index];
691
- const block = position == null ? undefined : this.blocks[position];
692
- if (index === 0) {
693
- if (position === 0 && block)
694
- output += this.source.slice(0, block.start);
695
- else if (this.source.startsWith('\uFEFF'))
696
- output += '\uFEFF';
697
- }
698
- else if (previous != null && position === previous + 1 && block) {
699
- output += this.source.slice(this.blocks[previous].end, block.start);
700
- }
701
- else
702
- output += this.lineEnding.repeat(2);
703
- if (block && block.form === text)
704
- output += this.source.slice(block.start, block.end);
705
- else if (block && currentTree.children.length === current.length) {
706
- output += this.retainBlock(block, positionalNode(currentRawTree, currentTree.children[index]), canonical);
707
- }
708
- else
709
- output += restoreCodeEndings(this.ctx, doc.type.create(null, [doc.child(index)]), withEnding(text, this.lineEnding));
710
- previous = position;
711
- });
712
- if (previous === this.blocks.length - 1 && this.blocks.length)
713
- output += this.source.slice(this.blocks[previous].end);
714
- else if (output)
715
- output += finalCodeEnding(doc) ?? this.lineEnding;
716
- // A `---` rule that opens the file reads back as front matter when
717
- // another `---` line follows, whether it is new or moved there.
718
- const bom = output.startsWith('\uFEFF') ? 1 : 0;
719
- if (doc.firstChild?.type.name === 'hr' &&
720
- sourceFrontmatter(output.slice(bom))?.startsWith('---'))
721
- output = output.slice(0, bom) + '***' + output.slice(bom + 3);
722
- const reopened = parse(output);
723
- if (semanticSignature(reopened) === semanticSignature(doc)) {
724
- const actual = normalized(serialize(this.ctx, reopened));
725
- if (actual === normalized(canonical))
726
- return output;
727
- // Parsing may normalize loose-list spacing. Compare both reconstructed
728
- // source and intended canonical source after that same normalization.
729
- if (actual === normalized(serialize(this.ctx, parse(canonical))))
730
- return output;
731
- }
732
- return undefined;
733
- };
734
- // Pairing by resemblance can leave a block in canonical form beside an
735
- // authored neighbour it then merges with, such as two lists. The
736
- // alignment's own pairing does not depend on resemblance, so try it too.
737
- const output = reconstruct(resembling) ??
738
- (() => {
739
- const aligned = correspondence(forms, current, false);
740
- // Unmatched blocks are holes, which every() would skip.
741
- return current.every((_, index) => aligned[index] === resembling[index])
742
- ? undefined
743
- : reconstruct(aligned);
744
- })();
745
- if (output !== undefined)
746
- return output;
747
- throw new PreservationError('The edited Markdown cannot be reopened without changing its content');
748
- }
749
- catch (cause) {
750
- if (cause instanceof PreservationError)
751
- throw cause;
752
- throw new PreservationError('Cannot preserve the edited Markdown', {
753
- cause,
754
- });
755
- }
756
- }
757
- }