@aicayzer/inkkit 0.0.18 → 0.1.1

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 (90) hide show
  1. package/NOTICE +4 -2
  2. package/README.md +11 -7
  3. package/dist/THIRD-PARTY-NOTICES.txt +2742 -0
  4. package/dist/chunks/core-CYp5ONPO.js +56519 -0
  5. package/dist/chunks/highlight-BW9W8PtS.js +14122 -0
  6. package/dist/core.d.ts +2 -5
  7. package/dist/core.js +2 -2
  8. package/dist/editor.d.ts +229 -47
  9. package/dist/index.js +5 -3
  10. package/dist/style.css +231 -202
  11. package/dist/types.d.ts +143 -18
  12. package/package.json +16 -13
  13. package/dist/autolink.d.ts +0 -1
  14. package/dist/autolink.js +0 -18
  15. package/dist/callouts.d.ts +0 -25
  16. package/dist/callouts.js +0 -288
  17. package/dist/clipboard.d.ts +0 -11
  18. package/dist/clipboard.js +0 -261
  19. package/dist/code-fences.d.ts +0 -8
  20. package/dist/code-fences.js +0 -171
  21. package/dist/comments.d.ts +0 -76
  22. package/dist/comments.js +0 -545
  23. package/dist/cursor.d.ts +0 -2
  24. package/dist/cursor.js +0 -40
  25. package/dist/decorations.d.ts +0 -2
  26. package/dist/decorations.js +0 -67
  27. package/dist/dialect.d.ts +0 -9
  28. package/dist/dialect.js +0 -446
  29. package/dist/drop-placement.d.ts +0 -19
  30. package/dist/drop-placement.js +0 -47
  31. package/dist/editor.js +0 -2673
  32. package/dist/files.d.ts +0 -40
  33. package/dist/files.js +0 -133
  34. package/dist/highlight.d.ts +0 -1
  35. package/dist/highlight.js +0 -136
  36. package/dist/html-import.d.ts +0 -13
  37. package/dist/html-import.js +0 -477
  38. package/dist/image-blocks.d.ts +0 -86
  39. package/dist/image-blocks.js +0 -329
  40. package/dist/images.d.ts +0 -51
  41. package/dist/images.js +0 -398
  42. package/dist/inline-highlight.d.ts +0 -6
  43. package/dist/inline-highlight.js +0 -112
  44. package/dist/labels.d.ts +0 -36
  45. package/dist/labels.js +0 -34
  46. package/dist/link-safety.d.ts +0 -17
  47. package/dist/link-safety.js +0 -29
  48. package/dist/linked-syntax.d.ts +0 -48
  49. package/dist/linked-syntax.js +0 -320
  50. package/dist/literals.d.ts +0 -20
  51. package/dist/literals.js +0 -216
  52. package/dist/mermaid.d.ts +0 -28
  53. package/dist/mermaid.js +0 -404
  54. package/dist/outline.d.ts +0 -22
  55. package/dist/outline.js +0 -79
  56. package/dist/paste-html.d.ts +0 -31
  57. package/dist/paste-html.js +0 -462
  58. package/dist/paste.d.ts +0 -33
  59. package/dist/paste.js +0 -591
  60. package/dist/portable-files.d.ts +0 -10
  61. package/dist/portable-files.js +0 -36
  62. package/dist/preserve.d.ts +0 -38
  63. package/dist/preserve.js +0 -1396
  64. package/dist/print.d.ts +0 -7
  65. package/dist/print.js +0 -406
  66. package/dist/reference-clipboard.d.ts +0 -8
  67. package/dist/reference-clipboard.js +0 -201
  68. package/dist/references.d.ts +0 -16
  69. package/dist/references.js +0 -418
  70. package/dist/scroll.d.ts +0 -15
  71. package/dist/scroll.js +0 -27
  72. package/dist/search.d.ts +0 -15
  73. package/dist/search.js +0 -202
  74. package/dist/selection.d.ts +0 -1
  75. package/dist/selection.js +0 -59
  76. package/dist/source.d.ts +0 -16
  77. package/dist/source.js +0 -57
  78. package/dist/spreadsheet.d.ts +0 -8
  79. package/dist/spreadsheet.js +0 -289
  80. package/dist/table-alignment.d.ts +0 -2
  81. package/dist/table-alignment.js +0 -46
  82. package/dist/tables.d.ts +0 -27
  83. package/dist/tables.js +0 -344
  84. package/dist/tasks.d.ts +0 -3
  85. package/dist/tasks.js +0 -51
  86. package/dist/text-ranges.d.ts +0 -29
  87. package/dist/text-ranges.js +0 -256
  88. package/dist/types.js +0 -8
  89. package/dist/wiki-view.d.ts +0 -21
  90. package/dist/wiki-view.js +0 -78
package/dist/preserve.js DELETED
@@ -1,1396 +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 { lineBreakHTML, sourceFrontmatter } from './literals.js';
5
- import { normalizeIdentifier } from 'micromark-util-normalize-identifier';
6
- // Line endings are the document's, not the node's: a CRLF source and its LF
7
- // canonical form describe the same text.
8
- function signature(node) {
9
- return JSON.stringify(node, function (key, value) {
10
- if (['position', 'data', 'marker'].includes(key))
11
- return undefined;
12
- const holder = this;
13
- // Every spelling of a line break reads back as the same break.
14
- if (key === 'value' && holder.type === 'html' && lineBreakHTML(value))
15
- return '<br />';
16
- // A row read as ending in empty cells is the row written without them.
17
- if (key === 'children' && holder.type === 'tableRow') {
18
- let end = value.length;
19
- while (end > 0 && !value[end - 1].children?.length)
20
- end--;
21
- return value.slice(0, end);
22
- }
23
- return typeof value === 'string' ? value.replace(/\r\n?/g, '\n') : value;
24
- });
25
- }
26
- function ownShape(node) {
27
- return JSON.stringify(node, (key, value) => ['position', 'data', 'children', 'value', 'marker'].includes(key)
28
- ? undefined
29
- : value);
30
- }
31
- // The character that decides which list an item belongs to: its bullet, or the
32
- // delimiter after its number. Items with different ones start a new list.
33
- function markerAt(text, at) {
34
- const match = /^(?:[-*+]|\d{1,9}[.)])/.exec(text.slice(at, at + 10));
35
- return match
36
- ? { at: at + match[0].length - 1, marker: match[0].at(-1) }
37
- : undefined;
38
- }
39
- function relabel(text, offsets, marker) {
40
- for (const offset of offsets) {
41
- const found = markerAt(text, offset);
42
- if (found &&
43
- found.marker !== marker &&
44
- /[-*+]/.test(found.marker) === /[-*+]/.test(marker))
45
- text = text.slice(0, found.at) + marker + text.slice(found.at + 1);
46
- }
47
- return text;
48
- }
49
- function authoredMarker(list, source) {
50
- const at = list.children?.[0]?.position?.start.offset;
51
- return at == null ? undefined : markerAt(source, at)?.marker;
52
- }
53
- const sameList = (before, after) => before.type === 'list' &&
54
- after.type === 'list' &&
55
- before.ordered === after.ordered;
56
- // Canonical Markdown written into authored source. Items written into an
57
- // authored list take its marker, which they would otherwise split away from.
58
- // A list written beside an authored list takes a marker that list does not
59
- // use, which it would otherwise merge with.
60
- function canonicalSlice(marker, children, canonical, from, to, previous, next) {
61
- let text = canonical.slice(from, to);
62
- const offsets = (list) => (list.children ?? []).map((item) => item.position.start.offset - from);
63
- if (marker)
64
- return relabel(text, children.map((item) => item.position.start.offset - from), marker);
65
- children.forEach((child, index) => {
66
- if (child.type !== 'list') {
67
- previous = undefined;
68
- return;
69
- }
70
- const avoid = [previous, index === children.length - 1 ? next : undefined]
71
- .filter((neighbour) => neighbour?.ordered === child.ordered)
72
- .map((neighbour) => neighbour.marker);
73
- const own = markerAt(canonical, child.children[0].position.start.offset)?.marker;
74
- const choice = [
75
- own,
76
- ...(child.ordered ? ['.', ')'] : ['-', '*', '+']),
77
- ].find((candidate) => candidate && !avoid.includes(candidate));
78
- if (choice)
79
- text = relabel(text, offsets(child), choice);
80
- previous = { ordered: child.ordered, marker: choice ?? own ?? '' };
81
- });
82
- return text;
83
- }
84
- // The authored list a child is, as a neighbour of canonical Markdown.
85
- function authoredNeighbour(before, after, source) {
86
- if (!before || !after || !sameList(before, after))
87
- return undefined;
88
- const marker = authoredMarker(before, source);
89
- return marker ? { ordered: before.ordered, marker } : undefined;
90
- }
91
- function retainText(raw, before, after, quoteIndent = false, inTable = false) {
92
- before = before.replace(/\r\n?/g, '\n');
93
- after = after.replace(/\r\n?/g, '\n');
94
- let prefix = 0;
95
- while (prefix < before.length &&
96
- prefix < after.length &&
97
- before[prefix] === after[prefix])
98
- prefix++;
99
- let suffix = 0;
100
- while (suffix < before.length - prefix &&
101
- suffix < after.length - prefix &&
102
- before[before.length - 1 - suffix] === after[after.length - 1 - suffix])
103
- suffix++;
104
- const offsets = [0];
105
- let decoded = '';
106
- for (let index = 0; index < raw.length;) {
107
- if (index > 0 && raw[index - 1] === '\n') {
108
- const gutter = (quoteIndent ? /^(?:[ \t]*>[ \t]*)+/ : /^(?:[ \t]*>[ \t]?)+/).exec(raw.slice(index));
109
- if (gutter) {
110
- index += gutter[0].length;
111
- offsets[offsets.length - 1] = index;
112
- continue;
113
- }
114
- }
115
- const escaped = /^\\([!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~])/.exec(raw.slice(index));
116
- const entity = /^&(?:#[xX][\da-fA-F]+|#\d+|[A-Za-z][A-Za-z\d]+);/.exec(raw.slice(index));
117
- let value;
118
- let length;
119
- if (escaped) {
120
- value = escaped[1];
121
- length = escaped[0].length;
122
- }
123
- else if (entity) {
124
- const element = document.createElement('textarea');
125
- element.innerHTML = entity[0];
126
- value = element.value;
127
- length = entity[0].length;
128
- }
129
- else if (raw.slice(index, index + 2) === '\r\n') {
130
- value = '\n';
131
- length = 2;
132
- }
133
- else {
134
- value = raw[index];
135
- length = 1;
136
- }
137
- decoded += value;
138
- index += length;
139
- for (let part = 0; part < value.length; part++)
140
- offsets.push(index);
141
- }
142
- if (decoded !== before) {
143
- // Continuation indentation is excluded from a quoted paragraph's text.
144
- return quoteIndent ? after : retainText(raw, before, after, true, inTable);
145
- }
146
- const inserted = after
147
- .slice(prefix, after.length - suffix)
148
- .split('')
149
- .map((character, index) => {
150
- const at = prefix + index;
151
- const delimiter = (character === '%' || character === '=') &&
152
- (after[at - 1] === character || after[at + 1] === character);
153
- return delimiter ||
154
- /[\\`*_[\]<>]/.test(character) ||
155
- (inTable && character === '|')
156
- ? `\\${character}`
157
- : character;
158
- })
159
- .join('');
160
- return (raw.slice(0, offsets[prefix]) +
161
- inserted +
162
- raw.slice(offsets[before.length - suffix]));
163
- }
164
- function retainBoundaryWhitespace(raw, canonical) {
165
- // A middle-of-line space becomes indentation or trimmed text at a block edge.
166
- const token = '(?:[ \\t]|&#(?:[xX]0*(?:20|9)|0*(?:32|9));|&Tab;)';
167
- const decoded = (value) => (value.match(new RegExp(token, 'g')) ?? [])
168
- .map((value) => value === '\t' || /(?:9;|Tab;)$/.test(value) ? '\t' : ' ')
169
- .join('');
170
- const encode = (value) => value.replace(/[ \t]/g, (value) => (value === '\t' ? '&#x9;' : '&#x20;'));
171
- const start = /^[ \t]+/.exec(raw)?.[0];
172
- const rawStart = new RegExp(`^${token}+`).exec(raw)?.[0];
173
- const encodedStart = new RegExp(`^${token}+`).exec(canonical)?.[0];
174
- if (start &&
175
- rawStart &&
176
- encodedStart?.includes('&') &&
177
- decoded(rawStart) === decoded(encodedStart))
178
- raw = encode(start) + raw.slice(start.length);
179
- const end = /[ \t]+$/.exec(raw)?.[0];
180
- const rawEnd = new RegExp(`${token}+$`).exec(raw)?.[0];
181
- const encodedEnd = new RegExp(`${token}+$`).exec(canonical)?.[0];
182
- if (end &&
183
- rawEnd &&
184
- encodedEnd?.includes('&') &&
185
- decoded(rawEnd) === decoded(encodedEnd))
186
- raw = raw.slice(0, -end.length) + encode(end);
187
- return raw;
188
- }
189
- // The pipes GFM splits a row at; an escaped pipe does not split it.
190
- function rowPipes(row) {
191
- const pipes = [];
192
- for (let index = 0; index < row.length; index++)
193
- if (row[index] === '\\' && /[\\|]/.test(row[index + 1] ?? ''))
194
- index++;
195
- else if (row[index] === '|')
196
- pipes.push(index);
197
- const opening = pipes[0] === row.search(/\S/);
198
- const last = row.trimEnd().length - 1;
199
- const closing = pipes.at(-1) === last && (!opening || pipes.length > 1) ? last : undefined;
200
- return {
201
- opening,
202
- closing,
203
- cells: pipes.length + 1 - Number(opening) - Number(closing != null),
204
- };
205
- }
206
- // A source row may stop before the header's last cell; the editor holds the
207
- // rest as empty cells, which stay unwritten until one of them gains content.
208
- // Then the new cells follow the row's own source, before its closing pipe.
209
- function retainShortRow(before, after, retain, source, canonical) {
210
- const count = before.children.length;
211
- const row = retain({ ...after, children: after.children.slice(0, count) });
212
- let filled = after.children.length;
213
- while (filled > count && !after.children[filled - 1].children?.length)
214
- filled--;
215
- if (filled === count)
216
- return row;
217
- const from = after.children[count].position?.start.offset;
218
- const to = after.children[filled - 1].position?.end.offset;
219
- const start = before.position?.start.offset;
220
- const end = before.position?.end.offset;
221
- if (from == null || to == null || start == null || end == null)
222
- throw new PreservationError('A source token has no location');
223
- const kept = rowPipes(row);
224
- let output = (kept.closing == null ? row : row.slice(0, kept.closing)).trimEnd();
225
- // Without an opening pipe, an empty first cell would not be read at all.
226
- if (!kept.opening && !after.children[0].children?.length)
227
- output = `|${output ? ' ' : ''}${output.trimStart()}`;
228
- for (let cell = kept.cells; cell < count; cell++)
229
- output += ' |';
230
- // A cell that matches one of the row's own, such as one moved there, keeps
231
- // that cell's authored text.
232
- const contentOf = (cell) => [
233
- cell.children[0].position.start.offset,
234
- cell.children.at(-1).position.end.offset,
235
- ];
236
- const added = after
237
- .children.slice(count, filled)
238
- .map((cell) => {
239
- const text = canonical.slice(cell.position.start.offset, cell.position.end.offset);
240
- const original = cell.children?.length
241
- ? before.children.find((candidate) => candidate.children?.length &&
242
- signature(candidate) === signature(cell))
243
- : undefined;
244
- if (!original)
245
- return text;
246
- const [cellFrom, cellTo] = contentOf(cell);
247
- const [sourceFrom, sourceTo] = contentOf(original);
248
- const offset = cell.position.start.offset;
249
- return (text.slice(0, cellFrom - offset) +
250
- source.slice(sourceFrom, sourceTo) +
251
- text.slice(cellTo - offset));
252
- })
253
- .join('');
254
- const addedPipes = rowPipes(added);
255
- const closing = kept.closing != null || rowPipes(source.slice(start, end)).closing != null;
256
- return (`${output} ` +
257
- (addedPipes.closing == null
258
- ? added
259
- : added.slice(0, addedPipes.closing)).trimEnd() +
260
- (closing ? ' |' : ''));
261
- }
262
- // A `<br />` line, read as an HTML block or as a paragraph holding only it.
263
- function spacer(node) {
264
- const html = node?.type === 'paragraph' ? node.children?.[0] : node;
265
- return ((node?.type !== 'paragraph' || node.children?.length === 1) &&
266
- html?.type === 'html' &&
267
- lineBreakHTML(String(html.value)));
268
- }
269
- // Patch changed leaves so an adjacent edit retains authored delimiters and labels.
270
- function retainSource(before, after, source, canonical, lineEnding, inTable = false,
271
- // The header's cell count, for the rows of a table.
272
- width) {
273
- const start = before.position?.start.offset;
274
- const end = before.position?.end.offset;
275
- const newStart = after.position?.start.offset;
276
- const newEnd = after.position?.end.offset;
277
- if (start == null || end == null || newStart == null || newEnd == null)
278
- throw new PreservationError('A source token has no location');
279
- const raw = source.slice(start, end);
280
- if (before.type === 'code' &&
281
- after.type === 'code' &&
282
- before.lang === after.lang) {
283
- if ((before.value ?? '') === (after.value ?? ''))
284
- return raw;
285
- const opening = /^([ \t>]*)(`{3,}|~{3,})([^\r\n]*)(\r?\n)/.exec(raw);
286
- if (opening) {
287
- const lines = raw.slice(opening[0].length).split(/\r?\n/);
288
- const closing = /^([ \t>]*)(`{3,}|~{3,})([ \t]*)$/.exec(lines.at(-1) ?? '');
289
- const originalLine = (before.value ?? '').replace(/\r\n?/g, '\n').split('\n')[0] ?? '';
290
- // A fence whose code is empty or starts with a blank line has no line
291
- // to take the indent from but its closing one.
292
- const first = originalLine ? lines[0] : undefined;
293
- const gutter = first === undefined
294
- ? (closing?.[1] ?? opening[1])
295
- : first.endsWith(originalLine)
296
- ? first.slice(0, first.length - originalLine.length)
297
- : opening[1];
298
- // A code block without code is written as a fence with no line inside.
299
- const body = after.value
300
- ? after.value.replace(/\r\n?/g, '\n').split('\n')
301
- : [];
302
- const character = opening[2][0];
303
- let length = opening[2].length;
304
- for (const line of body) {
305
- const run = new RegExp(`^ {0,3}(${character}+)[ \\t]*$`).exec(line);
306
- if (run)
307
- length = Math.max(length, run[1].length + 1);
308
- }
309
- const fence = character.repeat(length);
310
- return (opening[1] +
311
- fence +
312
- opening[3] +
313
- body.map((line) => lineEnding + gutter + line).join('') +
314
- (closing || body.length ? '' : opening[4]) +
315
- (closing
316
- ? lineEnding +
317
- closing[1] +
318
- character.repeat(Math.max(length, closing[2].length)) +
319
- closing[3]
320
- : ''));
321
- }
322
- }
323
- if (before.type === 'definition' &&
324
- after.type === 'definition' &&
325
- before.label === after.label &&
326
- before.title === after.title) {
327
- const destination = /^(\[[\s\S]*?\]:[ \t]*)(<[^>\n]*>|[^\s]+)([\s\S]*)$/.exec(raw);
328
- if (destination)
329
- return (destination[1] +
330
- (destination[2].startsWith('<')
331
- ? `<${after.url}>`
332
- : String(after.url)) +
333
- destination[3]);
334
- }
335
- if (before.type === 'tableRow' &&
336
- after.type === 'tableRow' &&
337
- before.children &&
338
- after.children &&
339
- width != null &&
340
- before.children.length < width &&
341
- after.children.length === width)
342
- return retainShortRow(before, after, (row) => retainSource(before, row, source, canonical, lineEnding, inTable), source, canonical);
343
- if (before.type === 'tableCell' &&
344
- after.type === 'tableCell' &&
345
- !before.children?.length &&
346
- after.children?.length) {
347
- // An empty cell's range is only its pipe and padding, so its new content
348
- // goes inside them rather than through the whole-range boundary check.
349
- const from = after.children[0].position?.start.offset;
350
- const to = after.children.at(-1).position?.end.offset;
351
- if (from == null || to == null)
352
- throw new PreservationError('A source token has no location');
353
- const pipes = rowPipes(raw);
354
- return `${pipes.opening ? '| ' : ''}${canonical.slice(from, to)} ${pipes.closing != null ? '|' : ''}`;
355
- }
356
- if (before.type === 'listItem' &&
357
- after.type === 'listItem' &&
358
- before.children &&
359
- after.children &&
360
- before.children.length === after.children.length + 1 &&
361
- (spacer(before.children[0]) || before.children[0].type === 'paragraph') &&
362
- after.children[0]?.type !== 'paragraph' &&
363
- !spacer(after.children[0])) {
364
- // The item now opens with an empty line before a block, which is written
365
- // as no line. The blocks after it keep their authored source.
366
- const retained = retainSource({ ...before, children: before.children.slice(1) }, { ...after, spread: before.spread }, source, canonical, lineEnding, inTable);
367
- const first = before.children[0];
368
- if (spacer(first))
369
- return retained;
370
- // An emptied first line is kept as the `<br />` line it reads back as.
371
- const from = first.position.start.offset - start;
372
- const to = first.position.end.offset - start;
373
- return retained.startsWith(source.slice(start, before.children[1].position.start.offset))
374
- ? retained.slice(0, from) + '<br />' + retained.slice(to)
375
- : retained;
376
- }
377
- if (before.type === 'text' && after.type === 'text')
378
- return retainBoundaryWhitespace(withEnding(retainText(raw, before.value, after.value, false, inTable), lineEnding), canonical.slice(newStart, newEnd));
379
- if (signature(before) === signature(after) &&
380
- ownShape(before) === ownShape(after))
381
- return raw;
382
- const marker = sameList(before, after)
383
- ? authoredMarker(before, source)
384
- : undefined;
385
- if (before.type === after.type &&
386
- ownShape(before) === ownShape(after) &&
387
- before.children &&
388
- after.children &&
389
- before.children.length === after.children.length) {
390
- let output = '', at = start;
391
- let previous;
392
- const same = (candidate, incoming) => signature(candidate) === signature(incoming) &&
393
- ownShape(candidate) === ownShape(incoming);
394
- const reorderable = before.type === 'table' || before.type === 'tableRow';
395
- // Equal rows or cells can differ in spelling, so each one that stayed in
396
- // place keeps its own source before any moved one is matched.
397
- const retained = new Set(reorderable
398
- ? before.children.flatMap((child, index) => same(child, after.children[index]) ? [index] : [])
399
- : []);
400
- before.children.forEach((child, index) => {
401
- const incoming = after.children[index];
402
- const movedIndex = !reorderable
403
- ? -1
404
- : same(child, incoming)
405
- ? index
406
- : before.children.findIndex((candidate, candidateIndex) => !retained.has(candidateIndex) && same(candidate, incoming));
407
- const original = movedIndex < 0 ? child : before.children[movedIndex];
408
- if (movedIndex >= 0)
409
- retained.add(movedIndex);
410
- const childStart = child.position?.start.offset, childEnd = child.position?.end.offset;
411
- if (childStart == null || childEnd == null)
412
- throw new PreservationError('A source token has no location');
413
- let retainedSource = incoming.type === 'list' && !sameList(original, incoming)
414
- ? withEnding(canonicalSlice(undefined, [incoming], canonical, incoming.position.start.offset, incoming.position.end.offset, previous, authoredNeighbour(before.children[index + 1], after.children[index + 1], source)), lineEnding)
415
- : retainSource(original, incoming, source, canonical, lineEnding, inTable || before.type === 'tableCell', before.type === 'table'
416
- ? before.children[0]?.children?.length
417
- : undefined);
418
- if (before.type === 'tableRow' &&
419
- movedIndex >= 0 &&
420
- movedIndex !== index) {
421
- // Cell positions own different pipe boundaries at the row edges.
422
- const originalFrom = original.children?.[0]?.position?.start.offset;
423
- const originalTo = original.children?.at(-1)?.position?.end.offset;
424
- const from = child.children?.[0]?.position?.start.offset;
425
- const to = child.children?.at(-1)?.position?.end.offset;
426
- retainedSource =
427
- originalFrom != null &&
428
- originalTo != null &&
429
- from != null &&
430
- to != null
431
- ? source.slice(childStart, from) +
432
- source.slice(originalFrom, originalTo) +
433
- source.slice(to, childEnd)
434
- : withEnding(canonical.slice(incoming.position.start.offset, incoming.position.end.offset), lineEnding);
435
- }
436
- if (marker)
437
- retainedSource = relabel(retainedSource, [0], marker);
438
- previous =
439
- incoming.type !== 'list'
440
- ? undefined
441
- : (authoredNeighbour(original, incoming, source) ?? {
442
- ordered: incoming.ordered,
443
- marker: markerAt(retainedSource, /^[ \t]*/.exec(retainedSource)[0].length)?.marker ?? '',
444
- });
445
- output += source.slice(at, childStart) + retainedSource;
446
- at = childEnd;
447
- });
448
- return output + source.slice(at, end);
449
- }
450
- if (before.type === after.type &&
451
- ownShape(before) === ownShape(after) &&
452
- before.children &&
453
- after.children) {
454
- let prefix = 0, suffix = 0;
455
- while (prefix < before.children.length &&
456
- prefix < after.children.length &&
457
- signature(before.children[prefix]) === signature(after.children[prefix]))
458
- prefix++;
459
- while (suffix < before.children.length - prefix &&
460
- suffix < after.children.length - prefix &&
461
- signature(before.children[before.children.length - 1 - suffix]) ===
462
- signature(after.children[after.children.length - 1 - suffix]))
463
- suffix++;
464
- // A cell's range includes its pipe and padding, which would hide a space
465
- // now at the edge of its content from the boundary check.
466
- const cell = before.type === 'tableCell' &&
467
- before.children.length > 0 &&
468
- after.children.length > 0;
469
- const first = cell ? before.children[0].position?.start.offset : start;
470
- const last = cell ? before.children.at(-1).position?.end.offset : end;
471
- const newFirst = cell ? after.children[0].position?.start.offset : newStart;
472
- const newLast = cell ? after.children.at(-1).position?.end.offset : newEnd;
473
- const from = prefix
474
- ? before.children[prefix - 1].position?.end.offset
475
- : first;
476
- const to = suffix
477
- ? before.children[before.children.length - suffix].position?.start.offset
478
- : last;
479
- const newFrom = prefix
480
- ? after.children[prefix - 1].position?.end.offset
481
- : newFirst;
482
- const newTo = suffix
483
- ? after.children[after.children.length - suffix].position?.start.offset
484
- : newLast;
485
- if (first != null &&
486
- last != null &&
487
- newFirst != null &&
488
- newLast != null &&
489
- from != null &&
490
- to != null &&
491
- newFrom != null &&
492
- newTo != null)
493
- return (source.slice(start, first) +
494
- retainBoundaryWhitespace(source.slice(first, from) +
495
- withEnding(canonicalSlice(marker, after.children.slice(prefix, after.children.length - suffix), canonical, newFrom, newTo, authoredNeighbour(before.children[prefix - 1], after.children[prefix - 1], source), authoredNeighbour(before.children[before.children.length - suffix], after.children[after.children.length - suffix], source)), lineEnding) +
496
- source.slice(to, last), canonical.slice(newFirst, newLast)) +
497
- source.slice(last, end));
498
- }
499
- // A list can end after blank lines that its container gave it; keep the
500
- // authored ones rather than add the canonical ones beside them, unless the
501
- // list became tight or loose, which those lines can undo.
502
- const lastAfter = after.children?.at(-1)?.position?.end.offset;
503
- const lastBefore = before.children?.at(-1)?.position?.end.offset;
504
- const trailing = marker != null &&
505
- lastAfter != null &&
506
- lastBefore != null &&
507
- loose(before, source) === loose(after, canonical);
508
- return (withEnding(canonicalSlice(marker, after.children ?? [], canonical, newStart, trailing ? lastAfter : newEnd), lineEnding) + (trailing ? source.slice(lastBefore, end) : ''));
509
- }
510
- // Whether blank lines separate a list's items or the blocks inside one. A
511
- // list's own spread can also count blank lines after its last item.
512
- function loose(list, text) {
513
- const blank = (from, to) => from != null && to != null && /\n[ \t>]*\r?\n/.test(text.slice(from, to));
514
- const gaps = (nodes) => nodes.some((node, index) => index
515
- ? blank(nodes[index - 1].position?.end.offset, node.position?.start.offset)
516
- : false);
517
- const items = list.children ?? [];
518
- return gaps(items) || items.some((item) => gaps(item.children ?? []));
519
- }
520
- // How a reference label is matched to a definition. Footnote labels, which
521
- // start with `^`, are a separate set.
522
- function definitionLabel(label) {
523
- return label.startsWith('^')
524
- ? '^' + normalizeIdentifier(label.slice(1))
525
- : normalizeIdentifier(label);
526
- }
527
- // Whether two nodes have the same blocks, leaving inline content aside.
528
- function sameBlocks(left, right) {
529
- if (left.type !== right.type || left.isTextblock !== right.isTextblock)
530
- return false;
531
- if (left.isTextblock)
532
- return true;
533
- if (left.childCount !== right.childCount)
534
- return false;
535
- for (let index = 0; index < left.childCount; index++)
536
- if (!sameBlocks(left.child(index), right.child(index)))
537
- return false;
538
- return true;
539
- }
540
- // Canonical Markdown keeps authored CRLF inside literal blocks and code fences.
541
- function withEnding(text, lineEnding) {
542
- return lineEnding === '\n' ? text : text.replace(/\r?\n/g, lineEnding);
543
- }
544
- function positionalNode(tree, transformed) {
545
- return (tree.children.find((node) => node.position?.start.offset === transformed.position?.start.offset &&
546
- node.position?.end.offset === transformed.position?.end.offset) ?? transformed);
547
- }
548
- export class PreservationError extends Error {
549
- constructor(message, options) {
550
- super(message, options);
551
- this.name = 'PreservationError';
552
- }
553
- }
554
- function form(text) {
555
- return text.replace(/\r\n?/g, '\n').replace(/\n+$/, '');
556
- }
557
- function blockForm(ctx, node) {
558
- // An empty document is empty Markdown, but an empty block inside a document
559
- // is authored spacing and must remain a real block when read again.
560
- if (node.type.name === 'paragraph' && node.content.size === 0)
561
- return '<br />';
562
- return form(serialize(ctx, node.type.schema.topNodeType.create(null, [node])));
563
- }
564
- export function semanticSignature(doc) {
565
- const clean = (value) => {
566
- if (value.type === 'inkkit_wiki_link' && value.attrs) {
567
- const { raw: _raw, inTable: _inTable, ...attrs } = value.attrs;
568
- value = { ...value, attrs };
569
- }
570
- const content = value.content;
571
- if (!content)
572
- return value;
573
- const tokens = [];
574
- for (const child of content) {
575
- if (child.type !== 'text') {
576
- tokens.push(clean(child));
577
- continue;
578
- }
579
- const parts = /^(\s*)([\s\S]*?)(\s*)$/.exec(String(child.text));
580
- for (const [index, text] of parts.slice(1).entries()) {
581
- if (!text)
582
- continue;
583
- const token = {
584
- ...child,
585
- text: text.replace(/\r\n?/g, '\n'),
586
- marks: index === 1 ? child.marks : undefined,
587
- };
588
- const previous = tokens.at(-1);
589
- if (previous?.type === 'text' &&
590
- JSON.stringify(previous.marks) === JSON.stringify(token.marks))
591
- previous.text = String(previous.text) + text;
592
- else
593
- tokens.push(token);
594
- }
595
- }
596
- return { ...value, content: tokens };
597
- };
598
- const json = JSON.parse(JSON.stringify(doc.toJSON(), (key, value) =>
599
- // Fresh list items default to loose even when their Markdown reopens tight.
600
- [
601
- 'referenceContent',
602
- 'referenceType',
603
- 'marker',
604
- 'id',
605
- 'label',
606
- 'spread',
607
- 'authoredFence',
608
- 'inkkitFileRaw',
609
- 'inkkitFileWidth',
610
- 'inkkitFileInTable',
611
- ].includes(key)
612
- ? undefined
613
- : value));
614
- return JSON.stringify(clean(json));
615
- }
616
- // Milkdown derives heading DOM IDs; Markdown stores the level and content.
617
- function withoutHeadingId(node) {
618
- return node.type.name === 'heading' && node.attrs.id
619
- ? node.type.create({ ...node.attrs, id: '' }, node.content, node.marks)
620
- : node;
621
- }
622
- const MATCHING_BUDGET = 1_000_000;
623
- const SIMILARITY_BUDGET = 4_000_000;
624
- // Unchanged blocks are anchored first, so a long document with many scattered
625
- // edits is matched as many short gaps rather than one wide span.
626
- export function correspondence(before, after) {
627
- const result = new Array(after.length);
628
- const work = { budget: MATCHING_BUDGET, similarity: SIMILARITY_BUDGET };
629
- const spans = [{ b: 0, a: 0, bEnd: before.length, aEnd: after.length }];
630
- for (let span = spans.pop(); span; span = spans.pop()) {
631
- let { b, a, bEnd, aEnd } = span;
632
- while (b < bEnd && a < aEnd && before[b] === after[a])
633
- result[a++] = b++;
634
- while (b < bEnd && a < aEnd && before[bEnd - 1] === after[aEnd - 1])
635
- result[--aEnd] = --bEnd;
636
- if (b === bEnd || a === aEnd)
637
- continue;
638
- work.budget -= bEnd - b + (aEnd - a);
639
- if (work.budget < 0)
640
- throw tooManyBlocks();
641
- const anchors = uniqueAnchors(before, after, b, bEnd, a, aEnd);
642
- if (!anchors.length) {
643
- align(before, after, b, bEnd, a, aEnd, result, work);
644
- continue;
645
- }
646
- for (const anchor of anchors) {
647
- result[anchor.a] = anchor.b;
648
- spans.push({ b, a, bEnd: anchor.b, aEnd: anchor.a });
649
- b = anchor.b + 1;
650
- a = anchor.a + 1;
651
- }
652
- spans.push({ b, a, bEnd, aEnd });
653
- }
654
- return result;
655
- }
656
- function tooManyBlocks() {
657
- return new PreservationError('The edit changes too many blocks to preserve safely');
658
- }
659
- // Blocks that occur exactly once on each side, in the longest subsequence whose
660
- // order the two sides share. A repeated block is never an anchor: which copy it
661
- // belongs to is not known until its neighbours are placed.
662
- function uniqueAnchors(before, after, b, bEnd, a, aEnd) {
663
- const once = (list, from, to) => {
664
- const at = new Map();
665
- for (let i = from; i < to; i++)
666
- at.set(list[i], at.has(list[i]) ? -1 : i);
667
- return at;
668
- };
669
- const beforeAt = once(before, b, bEnd);
670
- const afterAt = once(after, a, aEnd);
671
- const pairs = [];
672
- for (let i = a; i < aEnd; i++) {
673
- const match = beforeAt.get(after[i]);
674
- if (match != null && match >= 0 && afterAt.get(after[i]) === i)
675
- pairs.push({ b: match, a: i });
676
- }
677
- // Patience sorting: tails[k] ends the best increasing subsequence of length k + 1.
678
- const tails = [];
679
- const previous = new Array(pairs.length);
680
- for (const [index, pair] of pairs.entries()) {
681
- let low = 0;
682
- let high = tails.length;
683
- while (low < high) {
684
- const middle = (low + high) >> 1;
685
- if (pairs[tails[middle]].b < pair.b)
686
- low = middle + 1;
687
- else
688
- high = middle;
689
- }
690
- previous[index] = low ? tails[low - 1] : -1;
691
- tails[low] = index;
692
- }
693
- const anchors = [];
694
- let at = tails.length ? tails[tails.length - 1] : -1;
695
- while (at >= 0) {
696
- anchors.push(pairs[at]);
697
- at = previous[at];
698
- }
699
- return anchors.reverse();
700
- }
701
- // Replacing a block costs more than inserting or deleting one, so an unchanged
702
- // block keeps its own copy rather than pairing with a deleted neighbour, and
703
- // less than both, so an edited block still keeps its surrounding whitespace.
704
- const INDEL = 2;
705
- const REPLACE = 3;
706
- function align(before, after, b, bEnd, a, aEnd, result, work) {
707
- const rows = bEnd - b + 1;
708
- const columns = aEnd - a + 1;
709
- const offset = Math.abs(rows - columns);
710
- let width = Math.max(1, offset);
711
- for (;;) {
712
- const costs = [];
713
- const get = (i, j) => i < 0 || j < 0 || Math.abs(i - j) > width
714
- ? Infinity
715
- : (costs[i]?.[j - Math.max(0, i - width)] ?? Infinity);
716
- for (let i = 0; i < rows; i++) {
717
- const from = Math.max(0, i - width);
718
- const to = Math.min(columns - 1, i + width);
719
- work.budget -= to - from + 1;
720
- if (work.budget < 0)
721
- throw tooManyBlocks();
722
- const row = new Uint32Array(to - from + 1);
723
- costs.push(row);
724
- for (let j = from; j <= to; j++) {
725
- const change = before[b + i - 1] === after[a + j - 1] ? 0 : REPLACE;
726
- row[j - from] =
727
- i === 0
728
- ? j * INDEL
729
- : j === 0
730
- ? i * INDEL
731
- : Math.min(get(i - 1, j) + INDEL, get(i, j - 1) + INDEL, get(i - 1, j - 1) + change);
732
- }
733
- }
734
- // A path that leaves the band takes at least width + 1 insertions or
735
- // deletions away from the diagonal and width + 1 - offset back to its end,
736
- // so a path inside the band that costs no more is optimal. Widen only when
737
- // needed, charging every attempt to the same work budget.
738
- if (width < Math.max(rows, columns) - 1 &&
739
- get(rows - 1, columns - 1) > INDEL * (2 * (width + 1) - offset)) {
740
- width *= 2;
741
- continue;
742
- }
743
- const unchanged = [];
744
- let i = rows - 1;
745
- let j = columns - 1;
746
- while (i || j) {
747
- const same = i && j && before[b + i - 1] === after[a + j - 1];
748
- if (i && j && get(i, j) === get(i - 1, j - 1) + (same ? 0 : REPLACE)) {
749
- result[a + j - 1] = b + i - 1;
750
- if (same)
751
- unchanged.push({ b: b + i - 1, a: a + j - 1 });
752
- i--;
753
- j--;
754
- }
755
- else if (i && get(i, j) === get(i - 1, j) + INDEL)
756
- i--;
757
- else
758
- j--;
759
- }
760
- unchanged.push({ b: b - 1, a: a - 1 });
761
- unchanged.reverse().push({ b: bEnd, a: aEnd });
762
- for (let index = 1; index < unchanged.length; index++)
763
- pairSimilar(before, after, unchanged[index - 1].b + 1, unchanged[index].b, unchanged[index - 1].a + 1, unchanged[index].a, result, work);
764
- return;
765
- }
766
- }
767
- // Between unchanged blocks, edit distance prices every replacement alike, so
768
- // it cannot tell which original an edited block came from. Pair each with the
769
- // original it most resembles, keeping their order: typing in a block then keeps
770
- // its own source even when a neighbour was inserted or deleted beside it.
771
- function pairSimilar(before, after, b, bEnd, a, aEnd, result, work) {
772
- const removed = bEnd - b;
773
- const added = aEnd - a;
774
- if (!removed || !added || (removed === 1 && added === 1))
775
- return;
776
- // Resemblance decides; among equally resembling pairings, more pairs keep
777
- // more replaced blocks in place with their surrounding whitespace.
778
- const bonus = 1 / (Math.min(removed, added) + 1);
779
- // This is a refinement with its own budget of characters compared. When it
780
- // runs out, the alignment's pairing stands rather than the edit failing.
781
- if (removed * added > work.similarity)
782
- return;
783
- const weights = new Float64Array(removed * added);
784
- for (let x = 0; x < removed; x++)
785
- for (let y = 0; y < added; y++) {
786
- const shared = similarity(before[b + x], after[a + y]);
787
- work.similarity -= shared + 1;
788
- if (work.similarity < 0)
789
- return;
790
- weights[x * added + y] = shared + bonus;
791
- }
792
- const best = Array.from({ length: removed + 1 }, () => new Float64Array(added + 1));
793
- for (let x = 1; x <= removed; x++)
794
- for (let y = 1; y <= added; y++)
795
- best[x][y] = Math.max(best[x - 1][y], best[x][y - 1], best[x - 1][y - 1] + weights[(x - 1) * added + y - 1]);
796
- for (let index = a; index < aEnd; index++)
797
- result[index] = undefined;
798
- for (let x = removed, y = added; x && y;) {
799
- if (best[x][y] ===
800
- best[x - 1][y - 1] + weights[(x - 1) * added + y - 1]) {
801
- result[a + --y] = b + --x;
802
- }
803
- else if (best[x][y] === best[x - 1][y])
804
- x--;
805
- else
806
- y--;
807
- }
808
- }
809
- // Characters an edit left in place at the start and end of a block.
810
- function similarity(left, right) {
811
- const limit = Math.min(left.length, right.length);
812
- let prefix = 0;
813
- while (prefix < limit && left[prefix] === right[prefix])
814
- prefix++;
815
- let suffix = 0;
816
- while (suffix < limit - prefix &&
817
- left[left.length - 1 - suffix] === right[right.length - 1 - suffix])
818
- suffix++;
819
- return prefix + suffix;
820
- }
821
- /** Retains the loaded source of blocks the edit leaves alone. */
822
- export class Preservation {
823
- ctx;
824
- source;
825
- baseline;
826
- blocks = [];
827
- lineEnding;
828
- originalForms = new Set();
829
- forms = new WeakMap();
830
- blockEdits = new WeakMap();
831
- itemEdits = new WeakMap();
832
- definitionNodes = new Map();
833
- // Undefined when a definition sits inside another block, where appending
834
- // the top-level ones would not reproduce the document.
835
- definitionSources;
836
- constructor(ctx, source) {
837
- this.ctx = ctx;
838
- this.source = source;
839
- try {
840
- const parse = ctx.get(parserCtx);
841
- this.baseline = parse(source);
842
- this.lineEnding =
843
- source.includes('\r\n') && !source.replaceAll('\r\n', '').includes('\n')
844
- ? '\r\n'
845
- : '\n';
846
- const processor = ctx.get(remarkCtx);
847
- const offset = source.startsWith('\uFEFF') ? 1 : 0;
848
- const body = source.slice(offset);
849
- const rawTree = processor.parse(source);
850
- const tree = processor.runSync(structuredClone(rawTree), {
851
- value: body,
852
- });
853
- for (const [index, node] of tree.children.entries()) {
854
- let start = node.position?.start.offset;
855
- const end = node.position?.end.offset;
856
- if (start == null || end == null)
857
- throw new PreservationError('A source block has no location');
858
- start += offset;
859
- const sourceEnd = end + offset;
860
- const firstColumn = source.lastIndexOf('\n', start - 1) + 1;
861
- if (/^[ \t]*$/.test(source.slice(firstColumn, start)))
862
- start = firstColumn;
863
- const text = source.slice(start, sourceEnd);
864
- const parsed = this.baseline.childCount === tree.children.length
865
- ? this.baseline.type.create(null, [this.baseline.child(index)])
866
- : parse(text);
867
- this.blocks.push({
868
- start,
869
- end: sourceEnd,
870
- form: parsed.childCount === 1
871
- ? blockForm(ctx, parsed.firstChild)
872
- : form(serialize(ctx, parsed)),
873
- ast: positionalNode(rawTree, node),
874
- });
875
- }
876
- for (const block of this.blocks)
877
- this.originalForms.add(block.form);
878
- const definition = (node) => node.type === 'definition' || node.type === 'footnoteDefinition';
879
- const nested = (node) => (node.children ?? []).some((child) => definition(child) || nested(child));
880
- const top = rawTree.children;
881
- if (!top.some(nested))
882
- this.definitionSources = top.filter(definition).map((node) => {
883
- const raw = source.slice(node.position.start.offset + offset, node.position.end.offset + offset);
884
- // A footnote reference is never validated alone, so only its label
885
- // matters, and its body need not be parsed on every keystroke.
886
- const label = /^\[((?:\\[\s\S]|[^\\\]])*)\]:/.exec(raw)[1];
887
- return node.type === 'footnoteDefinition'
888
- ? { label: definitionLabel(label), text: `[${label}]: x` }
889
- : { label: definitionLabel(label), text: raw };
890
- });
891
- }
892
- catch (cause) {
893
- if (cause instanceof PreservationError)
894
- throw cause;
895
- throw new PreservationError('Cannot read Markdown for preservation', {
896
- cause,
897
- });
898
- }
899
- }
900
- retainBlock(block, after, canonical) {
901
- const offset = this.source.startsWith('\uFEFF') ? 1 : 0;
902
- const start = block.ast.position?.start.offset;
903
- if (start == null)
904
- throw new PreservationError('A source block has no location');
905
- return (this.source.slice(block.start, start + offset) +
906
- retainSource(block.ast, after, this.source.slice(offset), canonical, this.lineEnding));
907
- }
908
- localEdits(doc) {
909
- if (this.baseline.childCount !== this.blocks.length ||
910
- doc.childCount !== this.blocks.length ||
911
- !doc.sameMarkup(this.baseline))
912
- return undefined;
913
- let output = '', previous = 0;
914
- let run = [];
915
- for (let index = 0; index <= doc.childCount; index++) {
916
- const node = index < doc.childCount ? doc.child(index) : undefined;
917
- if (!node || node.eq(this.baseline.child(index))) {
918
- if (run.length > 1 && !this.pairedInPlace(doc, run))
919
- return undefined;
920
- run = [];
921
- continue;
922
- }
923
- run.push(index);
924
- const block = this.blocks[index];
925
- let retained = this.blockEdits.get(node);
926
- if (retained?.index !== index) {
927
- const text = this.localBlock(index, node);
928
- if (text === undefined)
929
- return undefined;
930
- retained = { index, text };
931
- this.blockEdits.set(node, retained);
932
- }
933
- output += this.source.slice(previous, block.start) + retained.text;
934
- previous = block.end;
935
- }
936
- output += this.source.slice(previous);
937
- // Front matter is found by reading from the start of the file to the next
938
- // delimiter line, which an edited block can supply.
939
- const bom = (text) => text.replace(/^\uFEFF/, '');
940
- return sourceFrontmatter(bom(output)) ===
941
- sourceFrontmatter(bom(this.source))
942
- ? output
943
- : undefined;
944
- }
945
- // Whole-document matching pairs adjacent edited blocks by resemblance, which
946
- // can pair one with its neighbour's original. Keep its result exactly.
947
- pairedInPlace(doc, run) {
948
- if (run.some((index) => ['bullet_list', 'ordered_list'].includes(doc.child(index).type.name)))
949
- return false;
950
- const before = run.map((index) => this.blocks[index].form);
951
- const after = run.map((index) => this.formOf(doc.child(index)));
952
- const result = run.map((_, at) => at);
953
- pairSimilar(before, after, 0, run.length, 0, run.length, result, {
954
- similarity: SIMILARITY_BUDGET,
955
- });
956
- return result.every((position, at) => position === at);
957
- }
958
- // Every definition in the source, in order. A block's links can depend on
959
- // definitions and nothing else outside it, so parsing it with these gives
960
- // the meaning it has in the document.
961
- // The definitions a block's text can refer to: only a bracketed label that
962
- // matches one can resolve, so the others need not be parsed with it.
963
- definitions(text) {
964
- if (this.definitionSources === undefined)
965
- return undefined;
966
- const labels = new Set();
967
- for (const match of text.matchAll(/\[((?:\\[\s\S]|[^\\[\]])*)\]/g))
968
- labels.add(definitionLabel(match[1]));
969
- const sources = this.definitionSources
970
- .filter((source) => labels.has(source.label))
971
- .map((source) => source.text);
972
- const appended = sources.join('\n\n');
973
- let nodes = this.definitionNodes.get(appended);
974
- if (nodes === undefined) {
975
- nodes = appended ? this.ctx.get(parserCtx)(appended).childCount : 0;
976
- this.definitionNodes.set(appended, nodes);
977
- }
978
- return { text: appended, blocks: sources.length, nodes };
979
- }
980
- // The block's text followed by what it can depend on elsewhere in the
981
- // document, or undefined when that cannot be known from the block alone.
982
- inContext(text) {
983
- // HTML and %% comments can span blocks.
984
- if (/<|%%/.test(text))
985
- return undefined;
986
- if (!text.includes('['))
987
- return { text, blocks: 1, nodes: 1 };
988
- const definitions = this.definitions(text);
989
- if (!definitions)
990
- return undefined;
991
- return definitions.text
992
- ? {
993
- text: text + this.lineEnding.repeat(2) + definitions.text,
994
- blocks: 1 + definitions.blocks,
995
- nodes: 1 + definitions.nodes,
996
- }
997
- : { text, blocks: 1, nodes: 1 };
998
- }
999
- // The first block of text as the document would read it in place.
1000
- parseLocally(text) {
1001
- const local = this.inContext(text);
1002
- if (!local)
1003
- return undefined;
1004
- const parsed = this.ctx.get(parserCtx)(local.text);
1005
- return parsed.childCount === local.nodes ? parsed.firstChild : undefined;
1006
- }
1007
- // Canonical Markdown for a block and its source tree, read in the same
1008
- // context, so references in it resolve as they do in the document.
1009
- canonicalTree(text) {
1010
- const local = this.inContext(text);
1011
- if (!local)
1012
- return undefined;
1013
- const tree = this.ctx.get(remarkCtx).parse(local.text);
1014
- return tree.children.length === local.blocks
1015
- ? {
1016
- canonical: local.text,
1017
- node: tree.children[0],
1018
- }
1019
- : undefined;
1020
- }
1021
- // Inline content other than text and line breaks can depend on other blocks
1022
- // or carry attributes that a reparse does not reproduce.
1023
- static plainInline(node) {
1024
- let plain = true;
1025
- node.descendants((child) => {
1026
- if (child.isInline && !child.isText && child.type.name !== 'hardbreak')
1027
- plain = false;
1028
- return plain;
1029
- });
1030
- return plain;
1031
- }
1032
- separated(index) {
1033
- const block = this.blocks[index];
1034
- return ((index === 0 ||
1035
- /\r?\n[ \t]*\r?\n/.test(this.source.slice(this.blocks[index - 1].end, block.start))) &&
1036
- (index + 1 === this.blocks.length ||
1037
- /\r?\n[ \t]*\r?\n/.test(this.source.slice(block.end, this.blocks[index + 1].start))));
1038
- }
1039
- // Validates a changed block on its own when nothing outside it can change
1040
- // what it means, so a keystroke does not serialize and reparse the document.
1041
- localBlock(index, node) {
1042
- const before = this.baseline.child(index);
1043
- const block = this.blocks[index];
1044
- const expected = withoutHeadingId(node);
1045
- if (!expected.sameMarkup(withoutHeadingId(before)) ||
1046
- !this.separated(index))
1047
- return undefined;
1048
- if (node.type.name === 'bullet_list' || node.type.name === 'ordered_list')
1049
- return this.localItems(index, node);
1050
- const code = node.type.name === 'code_block';
1051
- // Indented code is rewritten fenced, and the blocks after it can then read
1052
- // differently.
1053
- if (code &&
1054
- !/^ {0,3}(?:`{3,}|~{3,})/.test(this.source.slice(block.start, block.end)))
1055
- return undefined;
1056
- if (!code &&
1057
- (!['paragraph', 'heading', 'table'].includes(node.type.name) ||
1058
- !Preservation.plainInline(node) ||
1059
- !Preservation.plainInline(before) ||
1060
- !this.inContext(this.source.slice(block.start, block.end))))
1061
- return undefined;
1062
- const form = this.formOf(node);
1063
- if (form !== block.form && this.originalForms.has(form))
1064
- return undefined;
1065
- // Code is literal, so only its fence decides where it ends.
1066
- const canonical = code
1067
- ? (() => {
1068
- const tree = this.ctx.get(remarkCtx).parse(form);
1069
- return tree.children.length === 1
1070
- ? {
1071
- canonical: form,
1072
- node: tree.children[0],
1073
- }
1074
- : undefined;
1075
- })()
1076
- : this.canonicalTree(form);
1077
- if (!canonical)
1078
- return undefined;
1079
- const text = this.retainBlock(block, canonical.node, canonical.canonical);
1080
- const reopened = code
1081
- ? (() => {
1082
- const parsed = this.ctx.get(parserCtx)(text);
1083
- return parsed.childCount === 1 ? parsed.firstChild : undefined;
1084
- })()
1085
- : this.parseLocally(text);
1086
- // The authored fence is presentation the editor keeps from loading.
1087
- const same = (node) => code
1088
- ? node.type === expected.type &&
1089
- node.attrs.language === expected.attrs.language &&
1090
- node.content.eq(expected.content)
1091
- : withoutHeadingId(node).eq(expected);
1092
- return reopened && same(reopened) ? text : undefined;
1093
- }
1094
- formOf(node) {
1095
- let form = this.forms.get(node);
1096
- if (form == null) {
1097
- form = blockForm(this.ctx, node);
1098
- this.forms.set(node, form);
1099
- }
1100
- return form;
1101
- }
1102
- // A list is one block, so typing in one item of a long list validates that
1103
- // item alone, read as the only item of a list with the same marker.
1104
- localItems(index, node) {
1105
- const before = this.baseline.child(index);
1106
- const block = this.blocks[index];
1107
- const list = block.ast;
1108
- const offset = this.source.startsWith('\uFEFF') ? 1 : 0;
1109
- const source = this.source.slice(offset);
1110
- if (list.type !== 'list' ||
1111
- list.children?.length !== node.childCount ||
1112
- before.childCount !== node.childCount)
1113
- return undefined;
1114
- // Whole-document matching would read a list that now matches another one
1115
- // as moved there.
1116
- for (let other = 0; other < this.baseline.childCount; other++) {
1117
- const candidate = this.baseline.child(other);
1118
- if (other !== index &&
1119
- candidate.type === node.type &&
1120
- candidate.childCount === node.childCount &&
1121
- candidate.textContent === node.textContent)
1122
- return undefined;
1123
- }
1124
- const marker = authoredMarker(list, source);
1125
- let output = '';
1126
- let previous = block.start;
1127
- for (let at = 0; at < node.childCount; at++) {
1128
- const item = node.child(at);
1129
- if (item.eq(before.child(at)))
1130
- continue;
1131
- let retained = this.itemEdits.get(item);
1132
- if (retained?.index !== index || retained.item !== at) {
1133
- const text = this.localItem(node, item, before.child(at), list.children[at], source, marker);
1134
- if (text === undefined)
1135
- return undefined;
1136
- retained = { index, item: at, text };
1137
- this.itemEdits.set(item, retained);
1138
- }
1139
- const ast = list.children[at];
1140
- output +=
1141
- this.source.slice(previous, ast.position.start.offset + offset) +
1142
- retained.text;
1143
- previous = ast.position.end.offset + offset;
1144
- }
1145
- return output + this.source.slice(previous, block.end);
1146
- }
1147
- localItem(list, item, before, ast, source, marker) {
1148
- const start = ast.position.start.offset;
1149
- const end = ast.position.end.offset;
1150
- if (!item.sameMarkup(before) ||
1151
- !sameBlocks(item, before) ||
1152
- !Preservation.plainInline(item) ||
1153
- !Preservation.plainInline(before) ||
1154
- // The authored item is compared with the edit, so it must read the same
1155
- // in this context too.
1156
- !this.inContext(source.slice(start, end)))
1157
- return undefined;
1158
- const local = this.canonicalTree(form(serialize(this.ctx, list.type.schema.topNodeType.create(null, [
1159
- list.type.create(list.attrs, [item]),
1160
- ]))));
1161
- const after = local?.node.children?.[0];
1162
- if (!local || local.node.children.length !== 1 || !after)
1163
- return undefined;
1164
- // Whether an item is spread depends on the blank lines around it, which an
1165
- // edit that keeps its blocks does not change.
1166
- let text = retainSource(ast, { ...after, spread: ast.spread }, source, local.canonical, this.lineEnding);
1167
- if (marker)
1168
- text = relabel(text, [0], marker);
1169
- // The marker and the spaces after it set the column that continuation
1170
- // lines, and blocks after the list, are read against.
1171
- const content = ast.children?.[0]?.position?.start.offset;
1172
- if (content == null || !text.startsWith(source.slice(start, content)))
1173
- return undefined;
1174
- // An item with nothing on its first line has a different content column.
1175
- if (/^(?:[-*+]|\d{1,9}[.)])[ \t]*(?:\r?\n|$)/.test(text))
1176
- return undefined;
1177
- const indent = source.slice(source.lastIndexOf('\n', start - 1) + 1, start);
1178
- if (!/^[ \t]*$/.test(indent))
1179
- return undefined;
1180
- const reopened = this.parseLocally(indent + text);
1181
- if (reopened?.type !== list.type || reopened.childCount !== 1)
1182
- return undefined;
1183
- const reread = reopened.firstChild;
1184
- // The label and spacing of an item come from its list.
1185
- const { label: _label, spread: _spread, ...attrs } = reread.attrs;
1186
- const { label: _l, spread: _s, ...expected } = item.attrs;
1187
- return reread.type === item.type &&
1188
- JSON.stringify(attrs) === JSON.stringify(expected) &&
1189
- reread.content.eq(item.content)
1190
- ? text
1191
- : undefined;
1192
- }
1193
- // Two lists of one kind side by side are one list unless their markers
1194
- // differ, so each run of adjacent lists gets markers that alternate. A list
1195
- // written afresh changes first, then an edited list. An unchanged list keeps
1196
- // its source unless a move changed one of its neighbours; otherwise the
1197
- // reopen check refuses.
1198
- separateLists(doc, pieces, kept, positions, current) {
1199
- const kind = (index) => {
1200
- const name = doc.maybeChild(index)?.type.name;
1201
- return name === 'bullet_list' || name === 'ordered_list'
1202
- ? name
1203
- : undefined;
1204
- };
1205
- const marker = (index) => markerAt(pieces[index], /^[ \t]*/.exec(pieces[index])[0].length)
1206
- ?.marker;
1207
- const elsewhere = new Set(current.filter((_, index) => kept[index] !== 'unchanged'));
1208
- const moved = (index) => positions[index] == null && this.originalForms.has(current[index]);
1209
- // Beside its source neighbour, or beside new blocks only, with nothing
1210
- // between them in the source moved elsewhere.
1211
- const inPlace = (index, step) => {
1212
- const own = positions[index];
1213
- let next = index + step;
1214
- while (next >= 0 &&
1215
- next < pieces.length &&
1216
- positions[next] == null &&
1217
- !moved(next))
1218
- next += step;
1219
- if (next >= 0 && next < pieces.length && positions[next] == null)
1220
- return false;
1221
- const other = next < 0
1222
- ? -1
1223
- : next >= pieces.length
1224
- ? this.blocks.length
1225
- : positions[next];
1226
- if ((other - own) * step <= 0)
1227
- return false;
1228
- for (let at = own + step; at !== other; at += step)
1229
- if (elsewhere.has(this.blocks[at].form))
1230
- return false;
1231
- return true;
1232
- };
1233
- // An unchanged list changes only to part from a list with its marker across
1234
- // a boundary that a move disturbed, and one moved on both sides first.
1235
- const cost = (index) => {
1236
- if (kept[index] === 'canonical')
1237
- return 1;
1238
- if (kept[index] === 'edited')
1239
- return 10;
1240
- const own = marker(index);
1241
- const sides = [-1, 1].map((step) => inPlace(index, step));
1242
- const collides = [-1, 1].some((step, side) => !sides[side] &&
1243
- kind(index + step) === kind(index) &&
1244
- marker(index + step) === own);
1245
- return collides
1246
- ? [50, 150][Number(sides[0]) + Number(sides[1])]
1247
- : Infinity;
1248
- };
1249
- for (let start = 0; start < pieces.length;) {
1250
- const list = kind(start);
1251
- let end = start + 1;
1252
- while (list && kind(end) === list)
1253
- end++;
1254
- const run = Array.from({ length: end - start }, (_, i) => start + i);
1255
- start = end;
1256
- if (run.length < 2 ||
1257
- run.every((index, i) => !i || marker(index) !== marker(index - 1)))
1258
- continue;
1259
- const markers = list === 'bullet_list' ? ['-', '*', '+'] : ['.', ')'];
1260
- const options = run.map((index) => {
1261
- const own = marker(index);
1262
- const change = cost(index);
1263
- const choices = own ? [{ marker: own, cost: 0, text: pieces[index] }] : [];
1264
- if (change !== Infinity)
1265
- for (const choice of markers) {
1266
- if (choice === own)
1267
- continue;
1268
- const text = this.relabelList(pieces[index], choice);
1269
- if (text !== undefined)
1270
- choices.push({ marker: choice, cost: change, text });
1271
- }
1272
- return choices;
1273
- });
1274
- // Fewest and cheapest changes, with adjacent markers always different.
1275
- const best = options.map((choices) => choices.map(() => Infinity));
1276
- const from = options.map((choices) => choices.map(() => -1));
1277
- options[0].forEach((choice, j) => (best[0][j] = choice.cost));
1278
- for (let i = 1; i < options.length; i++)
1279
- options[i].forEach((choice, j) => {
1280
- options[i - 1].forEach((previous, k) => {
1281
- const total = best[i - 1][k] + choice.cost;
1282
- if (previous.marker !== choice.marker && total < best[i][j]) {
1283
- best[i][j] = total;
1284
- from[i][j] = k;
1285
- }
1286
- });
1287
- });
1288
- const last = best.at(-1);
1289
- let j = last.indexOf(Math.min(...last));
1290
- if (j < 0 || last[j] === Infinity)
1291
- continue;
1292
- for (let i = options.length - 1; i >= 0; j = from[i][j], i--)
1293
- pieces[run[i]] = options[i][j].text;
1294
- }
1295
- }
1296
- relabelList(text, marker) {
1297
- const processor = this.ctx.get(remarkCtx);
1298
- const read = (text) => processor.parse(text).children;
1299
- const [list, ...rest] = read(text);
1300
- if (list?.type !== 'list' || rest.length)
1301
- return undefined;
1302
- const relabelled = relabel(text, list.children.map((item) => item.position.start.offset), marker);
1303
- const reread = read(relabelled);
1304
- // A bullet can turn an item into a thematic break, such as `* * *`.
1305
- return reread.length === 1 &&
1306
- signature(reread[0]) === signature(list) &&
1307
- markerAt(relabelled, list.children[0].position.start.offset)
1308
- ?.marker === marker
1309
- ? relabelled
1310
- : undefined;
1311
- }
1312
- serialize(doc) {
1313
- if (doc.eq(this.baseline))
1314
- return this.source;
1315
- try {
1316
- const local = this.localEdits(doc);
1317
- if (local !== undefined)
1318
- return local;
1319
- const canonical = serialize(this.ctx, doc);
1320
- if (canonical === '')
1321
- return this.source.startsWith('\uFEFF') ? '\uFEFF' : '';
1322
- const current = [];
1323
- doc.forEach((node) => current.push(this.formOf(node)));
1324
- const forms = this.blocks.map((block) => block.form);
1325
- const processor = this.ctx.get(remarkCtx);
1326
- const currentRawTree = processor.parse(canonical);
1327
- const currentTree = processor.runSync(structuredClone(currentRawTree), {
1328
- value: canonical,
1329
- });
1330
- const parse = this.ctx.get(parserCtx);
1331
- const normalized = (text) => text.replace(/\r\n?/g, '\n');
1332
- const positions = correspondence(forms, current);
1333
- const gaps = [];
1334
- const pieces = [];
1335
- const kept = [];
1336
- let previous;
1337
- current.forEach((text, index) => {
1338
- const position = positions[index];
1339
- const block = position == null ? undefined : this.blocks[position];
1340
- if (index === 0) {
1341
- if (position === 0 && block)
1342
- gaps.push(this.source.slice(0, block.start));
1343
- else
1344
- gaps.push(this.source.startsWith('\uFEFF') ? '\uFEFF' : '');
1345
- }
1346
- else if (previous != null && position === previous + 1 && block) {
1347
- gaps.push(this.source.slice(this.blocks[previous].end, block.start));
1348
- }
1349
- else
1350
- gaps.push(this.lineEnding.repeat(2));
1351
- const kind = block && block.form === text
1352
- ? 'unchanged'
1353
- : block && currentTree.children.length === current.length
1354
- ? 'edited'
1355
- : 'canonical';
1356
- kept.push(kind);
1357
- pieces.push(kind === 'unchanged'
1358
- ? this.source.slice(block.start, block.end)
1359
- : kind === 'edited'
1360
- ? this.retainBlock(block, positionalNode(currentRawTree, currentTree.children[index]), canonical)
1361
- : restoreCodeEndings(this.ctx, doc.type.create(null, [doc.child(index)]), withEnding(text, this.lineEnding)));
1362
- previous = position;
1363
- });
1364
- this.separateLists(doc, pieces, kept, positions, current);
1365
- let output = pieces.map((piece, index) => gaps[index] + piece).join('');
1366
- if (previous === this.blocks.length - 1 && this.blocks.length)
1367
- output += this.source.slice(this.blocks[previous].end);
1368
- else if (output)
1369
- output += finalCodeEnding(doc) ?? this.lineEnding;
1370
- // A `---` rule that opens the file reads back as front matter when
1371
- // another `---` line follows, whether it is new or moved there.
1372
- const bom = output.startsWith('\uFEFF') ? 1 : 0;
1373
- if (doc.firstChild?.type.name === 'hr' &&
1374
- sourceFrontmatter(output.slice(bom))?.startsWith('---'))
1375
- output = output.slice(0, bom) + '***' + output.slice(bom + 3);
1376
- const reopened = parse(output);
1377
- if (semanticSignature(reopened) === semanticSignature(doc)) {
1378
- const actual = normalized(serialize(this.ctx, reopened));
1379
- if (actual === normalized(canonical))
1380
- return output;
1381
- // Parsing may normalize loose-list spacing. Compare both reconstructed
1382
- // source and intended canonical source after that same normalization.
1383
- if (actual === normalized(serialize(this.ctx, parse(canonical))))
1384
- return output;
1385
- }
1386
- throw new PreservationError('The edited Markdown cannot be reopened without changing its content');
1387
- }
1388
- catch (cause) {
1389
- if (cause instanceof PreservationError)
1390
- throw cause;
1391
- throw new PreservationError('Cannot preserve the edited Markdown', {
1392
- cause,
1393
- });
1394
- }
1395
- }
1396
- }