@aicayzer/inkkit 0.0.13 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/comments.js +7 -0
- package/dist/dialect.js +10 -19
- package/dist/editor.d.ts +4 -0
- package/dist/editor.js +82 -28
- package/dist/image-blocks.d.ts +71 -0
- package/dist/image-blocks.js +286 -0
- package/dist/images.d.ts +3 -1
- package/dist/images.js +64 -23
- package/dist/paste.d.ts +2 -0
- package/dist/paste.js +27 -15
- package/dist/preserve.d.ts +4 -0
- package/dist/preserve.js +134 -30
- package/dist/search.d.ts +3 -3
- package/dist/search.js +2 -2
- package/dist/style.css +144 -10
- package/dist/text-ranges.d.ts +4 -1
- package/dist/text-ranges.js +36 -13
- package/package.json +1 -1
package/dist/images.js
CHANGED
|
@@ -3,6 +3,7 @@ import { closeHistory } from '@milkdown/kit/prose/history';
|
|
|
3
3
|
import { $view } from '@milkdown/kit/utils';
|
|
4
4
|
import { FileResolution } from './files.js';
|
|
5
5
|
import { fileReference } from './linked-syntax.js';
|
|
6
|
+
import { imageBlockSchema } from './image-blocks.js';
|
|
6
7
|
/** A width rides in the alt text, as ``. Other Markdown readers
|
|
7
8
|
* show the picture and keep the number without requiring a private extension. */
|
|
8
9
|
export function splitAlt(alt) {
|
|
@@ -23,6 +24,8 @@ class ImageView {
|
|
|
23
24
|
runtime;
|
|
24
25
|
canMutate;
|
|
25
26
|
dom;
|
|
27
|
+
/** The card or picture, which a standalone image's block row holds. */
|
|
28
|
+
element;
|
|
26
29
|
image;
|
|
27
30
|
media;
|
|
28
31
|
pdf;
|
|
@@ -37,8 +40,15 @@ class ImageView {
|
|
|
37
40
|
this.adapter = adapter;
|
|
38
41
|
this.runtime = runtime;
|
|
39
42
|
this.canMutate = canMutate;
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
43
|
+
this.element = document.createElement('span');
|
|
44
|
+
this.element.className = 'image';
|
|
45
|
+
if (node.isBlock) {
|
|
46
|
+
this.dom = document.createElement('div');
|
|
47
|
+
this.dom.className = 'inkkit-image-block';
|
|
48
|
+
this.dom.append(this.element);
|
|
49
|
+
}
|
|
50
|
+
else
|
|
51
|
+
this.dom = this.element;
|
|
42
52
|
this.view.dom.addEventListener('inkkit-cancel-resize', this.cancelResize);
|
|
43
53
|
this.view.dom.addEventListener('inkkit-media-policy', this.mediaPolicy);
|
|
44
54
|
if (runtime) {
|
|
@@ -143,19 +153,34 @@ class ImageView {
|
|
|
143
153
|
this.runtime?.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
144
154
|
}
|
|
145
155
|
card(label, status, retry = false) {
|
|
156
|
+
const footer = document.createElement('span');
|
|
157
|
+
footer.className = 'inkkit-file-footer';
|
|
158
|
+
const name = document.createElement('span');
|
|
159
|
+
name.className = 'inkkit-file-name';
|
|
146
160
|
const text = document.createElement('span');
|
|
147
161
|
text.className = 'inkkit-file-label';
|
|
148
|
-
text.textContent =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.dom.append(text);
|
|
152
|
-
if (retry && this.runtime)
|
|
153
|
-
this.dom.append(this.button(this.runtime.labels.fileRetry, () => this.resolution?.retry()));
|
|
162
|
+
text.textContent = label;
|
|
163
|
+
text.title = label;
|
|
164
|
+
name.append(text);
|
|
154
165
|
if (this.runtime?.adapter.open) {
|
|
155
|
-
const open = this.button(
|
|
166
|
+
const open = this.button('', this.activate);
|
|
167
|
+
open.classList.add('inkkit-file-open');
|
|
156
168
|
open.setAttribute('aria-label', `${this.runtime.labels.fileOpen} ${label}`);
|
|
157
|
-
this.
|
|
169
|
+
open.title = this.runtime.labels.fileOpen;
|
|
170
|
+
open.append(openArrow());
|
|
171
|
+
name.append(open);
|
|
158
172
|
}
|
|
173
|
+
footer.append(name);
|
|
174
|
+
if (status) {
|
|
175
|
+
const description = document.createElement('span');
|
|
176
|
+
description.className = 'inkkit-file-status';
|
|
177
|
+
description.setAttribute('role', 'status');
|
|
178
|
+
description.textContent = status;
|
|
179
|
+
footer.append(description);
|
|
180
|
+
}
|
|
181
|
+
if (retry && this.runtime)
|
|
182
|
+
footer.append(this.button(this.runtime.labels.fileRetry, () => this.resolution?.retry()));
|
|
183
|
+
this.element.append(footer);
|
|
159
184
|
}
|
|
160
185
|
failPreview() {
|
|
161
186
|
this.previewFailed = true;
|
|
@@ -174,11 +199,11 @@ class ImageView {
|
|
|
174
199
|
}
|
|
175
200
|
: splitAlt(String(this.node.attrs.alt ?? ''));
|
|
176
201
|
this.width = width;
|
|
177
|
-
this.
|
|
202
|
+
this.element.textContent = '';
|
|
178
203
|
this.image = undefined;
|
|
179
204
|
if (this.runtime) {
|
|
180
|
-
this.
|
|
181
|
-
this.
|
|
205
|
+
this.element.classList.add('inkkit-file');
|
|
206
|
+
this.element.oncontextmenu = (event) => {
|
|
182
207
|
if (!this.runtime?.adapter.contextMenu || !this.active())
|
|
183
208
|
return;
|
|
184
209
|
const captured = this.runtime.capture();
|
|
@@ -196,11 +221,11 @@ class ImageView {
|
|
|
196
221
|
const label = state.status === 'resolved'
|
|
197
222
|
? state.presentation.label || alt || reference.reference
|
|
198
223
|
: alt || reference.reference;
|
|
199
|
-
this.
|
|
200
|
-
this.
|
|
224
|
+
this.element.setAttribute('aria-label', label);
|
|
225
|
+
this.element.dataset.inkkitFileState = this.previewFailed
|
|
201
226
|
? 'error'
|
|
202
227
|
: state.status;
|
|
203
|
-
this.
|
|
228
|
+
this.element.dataset.inkkitFileKind =
|
|
204
229
|
state.status === 'resolved' ? state.presentation.kind : 'file';
|
|
205
230
|
if (this.previewFailed) {
|
|
206
231
|
this.card(label, this.runtime.labels.filePreviewUnavailable, true);
|
|
@@ -238,7 +263,7 @@ class ImageView {
|
|
|
238
263
|
});
|
|
239
264
|
media.src = presentation.url;
|
|
240
265
|
this.media = media;
|
|
241
|
-
this.
|
|
266
|
+
this.element.append(media);
|
|
242
267
|
this.card(label);
|
|
243
268
|
}
|
|
244
269
|
else if (presentation.kind === 'pdf') {
|
|
@@ -254,7 +279,7 @@ class ImageView {
|
|
|
254
279
|
});
|
|
255
280
|
pdf.src = presentation.url;
|
|
256
281
|
this.pdf = pdf;
|
|
257
|
-
this.
|
|
282
|
+
this.element.append(pdf);
|
|
258
283
|
this.card(label, this.runtime.labels.filePDFPreview);
|
|
259
284
|
}
|
|
260
285
|
else
|
|
@@ -263,11 +288,11 @@ class ImageView {
|
|
|
263
288
|
}
|
|
264
289
|
const presentation = this.adapter?.presentation(String(this.node.attrs.src ?? ''));
|
|
265
290
|
if (!presentation) {
|
|
266
|
-
this.
|
|
267
|
-
this.
|
|
291
|
+
this.element.classList.add('image-absent');
|
|
292
|
+
this.element.textContent = alt || String(this.node.attrs.src ?? '');
|
|
268
293
|
return;
|
|
269
294
|
}
|
|
270
|
-
this.
|
|
295
|
+
this.element.classList.remove('image-absent');
|
|
271
296
|
this.renderImage(presentation.url, alt, width);
|
|
272
297
|
}
|
|
273
298
|
renderImage(url, alt, width) {
|
|
@@ -284,10 +309,14 @@ class ImageView {
|
|
|
284
309
|
});
|
|
285
310
|
image.src = url;
|
|
286
311
|
this.image = image;
|
|
312
|
+
const preview = document.createElement('span');
|
|
313
|
+
preview.className = 'inkkit-image-preview';
|
|
314
|
+
preview.append(image);
|
|
315
|
+
// The grip sits on the whole card, so it centres on the picture and its name together.
|
|
287
316
|
const handle = document.createElement('span');
|
|
288
317
|
handle.className = 'image-handle';
|
|
289
318
|
handle.addEventListener('pointerdown', this.startResize);
|
|
290
|
-
this.
|
|
319
|
+
this.element.append(preview, handle);
|
|
291
320
|
}
|
|
292
321
|
startResize = (event) => {
|
|
293
322
|
event.preventDefault();
|
|
@@ -352,6 +381,18 @@ class ImageView {
|
|
|
352
381
|
return this.view.editable && !this.view.composing && this.canMutate();
|
|
353
382
|
}
|
|
354
383
|
}
|
|
384
|
+
const svg = 'http://www.w3.org/2000/svg';
|
|
385
|
+
function openArrow() {
|
|
386
|
+
const icon = document.createElementNS(svg, 'svg');
|
|
387
|
+
icon.setAttribute('viewBox', '0 0 12 12');
|
|
388
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
389
|
+
icon.setAttribute('focusable', 'false');
|
|
390
|
+
const path = document.createElementNS(svg, 'path');
|
|
391
|
+
path.setAttribute('d', 'M4 2.5h5.5V8M9.5 2.5 2.5 9.5');
|
|
392
|
+
icon.append(path);
|
|
393
|
+
return icon;
|
|
394
|
+
}
|
|
355
395
|
export function imageView(adapter, runtime, canMutate = () => true) {
|
|
356
|
-
|
|
396
|
+
const view = (schema) => $view(schema.node, () => (node, view, getPos) => new ImageView(node, view, getPos, adapter, runtime, canMutate));
|
|
397
|
+
return [view(imageSchema), view(imageBlockSchema)];
|
|
357
398
|
}
|
package/dist/paste.d.ts
CHANGED
|
@@ -12,11 +12,13 @@ export interface PasteControllerOptions {
|
|
|
12
12
|
export declare class PasteController {
|
|
13
13
|
private readonly options;
|
|
14
14
|
private active?;
|
|
15
|
+
private readonly key;
|
|
15
16
|
private dead;
|
|
16
17
|
get pending(): boolean;
|
|
17
18
|
readonly plugin: import("@milkdown/kit/utils").$Prose;
|
|
18
19
|
constructor(options: PasteControllerOptions);
|
|
19
20
|
paste(input: ClipboardInput): Promise<void>;
|
|
21
|
+
private anchor;
|
|
20
22
|
cancelPending(): void;
|
|
21
23
|
destroy(): void;
|
|
22
24
|
private assertContext;
|
package/dist/paste.js
CHANGED
|
@@ -3,11 +3,13 @@ import { DOMParser as ProseParser, DOMSerializer, Fragment, Slice, } from '@milk
|
|
|
3
3
|
import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
|
|
4
4
|
import { $prose } from '@milkdown/kit/utils';
|
|
5
5
|
import { pasteSpreadsheet } from './spreadsheet.js';
|
|
6
|
+
import { isImage } from './image-blocks.js';
|
|
6
7
|
import { avoidReferenceCollisions, markdownFromHTML, } from './reference-clipboard.js';
|
|
7
8
|
import { InkKitError, } from './types.js';
|
|
8
9
|
export class PasteController {
|
|
9
10
|
options;
|
|
10
11
|
active;
|
|
12
|
+
key = new PluginKey('inkkitPaste');
|
|
11
13
|
dead = false;
|
|
12
14
|
get pending() {
|
|
13
15
|
return this.active != null;
|
|
@@ -16,16 +18,18 @@ export class PasteController {
|
|
|
16
18
|
constructor(options) {
|
|
17
19
|
this.options = options;
|
|
18
20
|
this.plugin = $prose(() => new Plugin({
|
|
19
|
-
key:
|
|
21
|
+
key: this.key,
|
|
20
22
|
state: {
|
|
21
23
|
init: () => null,
|
|
22
|
-
apply: (transaction) => {
|
|
23
|
-
if (this.active)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
apply: (transaction, previous) => {
|
|
25
|
+
if (!this.active)
|
|
26
|
+
return null;
|
|
27
|
+
const anchor = previous?.operation === this.active ? previous : this.active;
|
|
28
|
+
return {
|
|
29
|
+
operation: this.active,
|
|
30
|
+
from: transaction.mapping.map(anchor.from, 1),
|
|
31
|
+
to: transaction.mapping.map(anchor.to, anchor.from === anchor.to ? 1 : -1),
|
|
32
|
+
};
|
|
29
33
|
},
|
|
30
34
|
},
|
|
31
35
|
props: {
|
|
@@ -68,6 +72,10 @@ export class PasteController {
|
|
|
68
72
|
paste(input) {
|
|
69
73
|
return this.run(input);
|
|
70
74
|
}
|
|
75
|
+
anchor(operation) {
|
|
76
|
+
const mapped = this.key.getState(this.options.ctx().get(editorViewCtx).state);
|
|
77
|
+
return mapped?.operation === operation ? mapped : operation;
|
|
78
|
+
}
|
|
71
79
|
cancelPending() {
|
|
72
80
|
this.active = undefined;
|
|
73
81
|
this.options.onPendingChanged?.();
|
|
@@ -127,7 +135,7 @@ export class PasteController {
|
|
|
127
135
|
(input.html ? markdownFromHTML(input.html) : undefined));
|
|
128
136
|
if (plainText) {
|
|
129
137
|
if (view.state.selection.$from.parent.type.spec.code) {
|
|
130
|
-
const anchor = operation;
|
|
138
|
+
const anchor = this.anchor(operation);
|
|
131
139
|
view.dispatch(view.state.tr.insertText(input.text, anchor.from, anchor.to));
|
|
132
140
|
return;
|
|
133
141
|
}
|
|
@@ -327,14 +335,18 @@ export class PasteController {
|
|
|
327
335
|
const parsed = ctx.get(parserCtx)(markdown);
|
|
328
336
|
let imageIndex = 0;
|
|
329
337
|
const hydrate = (node) => {
|
|
330
|
-
if (node
|
|
338
|
+
if (isImage(node)) {
|
|
331
339
|
const slot = /^inkkit-clipboard-image:(\d+)$/.exec(String(node.attrs.src));
|
|
332
340
|
const position = slot ? Number(slot[1]) : imageIndex++;
|
|
333
341
|
const element = elements.some((element) => element.hasAttribute('data-inkkit-image-slot'))
|
|
334
342
|
? elements.find((element) => Number(element.getAttribute('data-inkkit-authored-image-slot') ?? element.getAttribute('data-inkkit-image-slot')) === position)
|
|
335
343
|
: elements[position];
|
|
336
|
-
if (!element || !container.contains(element))
|
|
337
|
-
|
|
344
|
+
if (!element || !container.contains(element)) {
|
|
345
|
+
const text = schema.text(`[${node.attrs.alt || 'Image'}: image unavailable]`);
|
|
346
|
+
return node.isBlock
|
|
347
|
+
? schema.nodes.paragraph.create(null, text)
|
|
348
|
+
: text;
|
|
349
|
+
}
|
|
338
350
|
return node.type.create({ ...node.attrs, src: element.getAttribute('src') }, null, node.marks);
|
|
339
351
|
}
|
|
340
352
|
if (node.isText)
|
|
@@ -389,8 +401,8 @@ export class PasteController {
|
|
|
389
401
|
!selection.empty &&
|
|
390
402
|
/^https?:\/\/\S+$/.test(text) &&
|
|
391
403
|
schema.marks.link) {
|
|
392
|
-
const anchor = operation;
|
|
393
|
-
const mark = schema.marks.link.create({ href: text
|
|
404
|
+
const anchor = this.anchor(operation);
|
|
405
|
+
const mark = schema.marks.link.create({ href: text });
|
|
394
406
|
view.dispatch(view.state.tr
|
|
395
407
|
.addMark(anchor.from, anchor.to, mark)
|
|
396
408
|
.removeStoredMark(mark));
|
|
@@ -402,7 +414,7 @@ export class PasteController {
|
|
|
402
414
|
this.assertContext(context, operation);
|
|
403
415
|
if (!view.editable || this.options.editable?.() === false)
|
|
404
416
|
return;
|
|
405
|
-
const anchor = operation;
|
|
417
|
+
const anchor = this.anchor(operation);
|
|
406
418
|
if (!plainText) {
|
|
407
419
|
const valid = slice.content.firstChild?.isInline
|
|
408
420
|
? Fragment.from(schema.nodes.paragraph.create(null, slice.content))
|
package/dist/preserve.d.ts
CHANGED
|
@@ -10,7 +10,11 @@ export declare class Preservation {
|
|
|
10
10
|
private readonly baseline;
|
|
11
11
|
private readonly blocks;
|
|
12
12
|
private readonly lineEnding;
|
|
13
|
+
private readonly originalForms;
|
|
13
14
|
private readonly forms;
|
|
15
|
+
private readonly textBlockEdits;
|
|
14
16
|
constructor(ctx: Ctx, source: string);
|
|
17
|
+
private retainBlock;
|
|
18
|
+
private independentTextBlocks;
|
|
15
19
|
serialize(doc: ProseNode): string;
|
|
16
20
|
}
|
package/dist/preserve.js
CHANGED
|
@@ -317,6 +317,12 @@ function semanticSignature(doc) {
|
|
|
317
317
|
: value));
|
|
318
318
|
return JSON.stringify(clean(json));
|
|
319
319
|
}
|
|
320
|
+
// Milkdown derives heading DOM IDs; Markdown stores the level and content.
|
|
321
|
+
function withoutHeadingId(node) {
|
|
322
|
+
return node.type.name === 'heading' && node.attrs.id
|
|
323
|
+
? node.type.create({ ...node.attrs, id: '' }, node.content, node.marks)
|
|
324
|
+
: node;
|
|
325
|
+
}
|
|
320
326
|
// Edit distance distinguishes replacements from insertions, so replacing a block
|
|
321
327
|
// retains its surrounding whitespace without assigning it a neighbour's source.
|
|
322
328
|
function correspondence(before, after) {
|
|
@@ -337,32 +343,52 @@ function correspondence(before, after) {
|
|
|
337
343
|
}
|
|
338
344
|
const rows = endBefore - head + 1;
|
|
339
345
|
const columns = endAfter - head + 1;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
346
|
+
let budget = 1_000_000;
|
|
347
|
+
let width = Math.max(1, Math.abs(rows - columns));
|
|
348
|
+
for (;;) {
|
|
349
|
+
const costs = [];
|
|
350
|
+
const get = (i, j) => i < 0 || j < 0 || Math.abs(i - j) > width
|
|
351
|
+
? width + 1
|
|
352
|
+
: (costs[i]?.[j - Math.max(0, i - width)] ?? width + 1);
|
|
353
|
+
for (let i = 0; i < rows; i++) {
|
|
354
|
+
const from = Math.max(0, i - width);
|
|
355
|
+
const to = Math.min(columns - 1, i + width);
|
|
356
|
+
budget -= to - from + 1;
|
|
357
|
+
if (budget < 0)
|
|
358
|
+
throw new PreservationError('The edit changes too many blocks to preserve safely');
|
|
359
|
+
const row = new Uint32Array(to - from + 1);
|
|
360
|
+
costs.push(row);
|
|
361
|
+
for (let j = from; j <= to; j++) {
|
|
362
|
+
const change = before[head + i - 1] === after[head + j - 1] ? 0 : 1;
|
|
363
|
+
row[j - from] =
|
|
364
|
+
i === 0
|
|
365
|
+
? j
|
|
366
|
+
: j === 0
|
|
367
|
+
? i
|
|
368
|
+
: Math.min(get(i - 1, j) + 1, get(i, j - 1) + 1, get(i - 1, j - 1) + change);
|
|
369
|
+
}
|
|
351
370
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (i && j && costs[i][j] === costs[i - 1][j - 1] + change) {
|
|
358
|
-
result[head + j - 1] = head + i - 1;
|
|
359
|
-
i--;
|
|
360
|
-
j--;
|
|
371
|
+
// A path with at most width edits cannot leave this diagonal band.
|
|
372
|
+
// Widen only when needed, charging every attempt to the same work budget.
|
|
373
|
+
if (get(rows - 1, columns - 1) > width) {
|
|
374
|
+
width *= 2;
|
|
375
|
+
continue;
|
|
361
376
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
j
|
|
377
|
+
let i = rows - 1;
|
|
378
|
+
let j = columns - 1;
|
|
379
|
+
while (i || j) {
|
|
380
|
+
const change = i && j && before[head + i - 1] === after[head + j - 1] ? 0 : 1;
|
|
381
|
+
if (i && j && get(i, j) === get(i - 1, j - 1) + change) {
|
|
382
|
+
result[head + j - 1] = head + i - 1;
|
|
383
|
+
i--;
|
|
384
|
+
j--;
|
|
385
|
+
}
|
|
386
|
+
else if (i && get(i, j) === get(i - 1, j) + 1)
|
|
387
|
+
i--;
|
|
388
|
+
else
|
|
389
|
+
j--;
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
366
392
|
}
|
|
367
393
|
return result;
|
|
368
394
|
}
|
|
@@ -373,7 +399,9 @@ export class Preservation {
|
|
|
373
399
|
baseline;
|
|
374
400
|
blocks = [];
|
|
375
401
|
lineEnding;
|
|
402
|
+
originalForms = new Set();
|
|
376
403
|
forms = new WeakMap();
|
|
404
|
+
textBlockEdits = new WeakMap();
|
|
377
405
|
constructor(ctx, source) {
|
|
378
406
|
this.ctx = ctx;
|
|
379
407
|
this.source = source;
|
|
@@ -414,6 +442,8 @@ export class Preservation {
|
|
|
414
442
|
ast: positionalNode(rawTree, node),
|
|
415
443
|
});
|
|
416
444
|
}
|
|
445
|
+
for (const block of this.blocks)
|
|
446
|
+
this.originalForms.add(block.form);
|
|
417
447
|
}
|
|
418
448
|
catch (cause) {
|
|
419
449
|
if (cause instanceof PreservationError)
|
|
@@ -423,10 +453,79 @@ export class Preservation {
|
|
|
423
453
|
});
|
|
424
454
|
}
|
|
425
455
|
}
|
|
456
|
+
retainBlock(block, after, canonical) {
|
|
457
|
+
const offset = this.source.startsWith('\uFEFF') ? 1 : 0;
|
|
458
|
+
const start = block.ast.position?.start.offset;
|
|
459
|
+
if (start == null)
|
|
460
|
+
throw new PreservationError('A source block has no location');
|
|
461
|
+
return (this.source.slice(block.start, start + offset) +
|
|
462
|
+
retainSource(block.ast, after, this.source.slice(offset), canonical, this.lineEnding));
|
|
463
|
+
}
|
|
464
|
+
independentTextBlocks(doc) {
|
|
465
|
+
if (this.baseline.childCount !== this.blocks.length ||
|
|
466
|
+
doc.childCount !== this.blocks.length ||
|
|
467
|
+
!doc.sameMarkup(this.baseline))
|
|
468
|
+
return undefined;
|
|
469
|
+
let output = '', previous = 0;
|
|
470
|
+
for (let index = 0; index < doc.childCount; index++) {
|
|
471
|
+
const node = doc.child(index);
|
|
472
|
+
const before = this.baseline.child(index);
|
|
473
|
+
const block = this.blocks[index];
|
|
474
|
+
if (node.eq(before))
|
|
475
|
+
continue;
|
|
476
|
+
let retained = this.textBlockEdits.get(node);
|
|
477
|
+
if (retained?.index !== index) {
|
|
478
|
+
const raw = this.source.slice(block.start, block.end);
|
|
479
|
+
const expected = withoutHeadingId(node);
|
|
480
|
+
// References and HTML/comments can depend on other blocks. Restrict
|
|
481
|
+
// local validation to text blocks without these context-sensitive forms.
|
|
482
|
+
const contextual = /[\[<]|%%/;
|
|
483
|
+
const textOnly = (block) => {
|
|
484
|
+
let valid = true;
|
|
485
|
+
block.forEach((child) => {
|
|
486
|
+
if (!child.isText)
|
|
487
|
+
valid = false;
|
|
488
|
+
});
|
|
489
|
+
return valid;
|
|
490
|
+
};
|
|
491
|
+
if (!['paragraph', 'heading'].includes(node.type.name) ||
|
|
492
|
+
!expected.sameMarkup(withoutHeadingId(before)) ||
|
|
493
|
+
!textOnly(node) ||
|
|
494
|
+
!textOnly(before) ||
|
|
495
|
+
contextual.test(raw) ||
|
|
496
|
+
(index > 0 &&
|
|
497
|
+
!/\r?\n[ \t]*\r?\n/.test(this.source.slice(this.blocks[index - 1].end, block.start))) ||
|
|
498
|
+
(index + 1 < this.blocks.length &&
|
|
499
|
+
!/\r?\n[ \t]*\r?\n/.test(this.source.slice(block.end, this.blocks[index + 1].start))))
|
|
500
|
+
return undefined;
|
|
501
|
+
const canonical = blockForm(this.ctx, node);
|
|
502
|
+
if (contextual.test(canonical) ||
|
|
503
|
+
(canonical !== block.form && this.originalForms.has(canonical)))
|
|
504
|
+
return undefined;
|
|
505
|
+
const tree = this.ctx.get(remarkCtx).parse(canonical);
|
|
506
|
+
if (tree.children.length !== 1)
|
|
507
|
+
return undefined;
|
|
508
|
+
const text = this.retainBlock(block, tree.children[0], canonical);
|
|
509
|
+
const reopened = this.ctx.get(parserCtx)(text);
|
|
510
|
+
if (reopened.childCount !== 1)
|
|
511
|
+
return undefined;
|
|
512
|
+
if (!withoutHeadingId(reopened.firstChild).eq(expected))
|
|
513
|
+
return undefined;
|
|
514
|
+
retained = { index, text };
|
|
515
|
+
this.textBlockEdits.set(node, retained);
|
|
516
|
+
}
|
|
517
|
+
output += this.source.slice(previous, block.start) + retained.text;
|
|
518
|
+
previous = block.end;
|
|
519
|
+
}
|
|
520
|
+
return output + this.source.slice(previous);
|
|
521
|
+
}
|
|
426
522
|
serialize(doc) {
|
|
427
523
|
if (doc.eq(this.baseline))
|
|
428
524
|
return this.source;
|
|
429
525
|
try {
|
|
526
|
+
const independent = this.independentTextBlocks(doc);
|
|
527
|
+
if (independent !== undefined)
|
|
528
|
+
return independent;
|
|
430
529
|
const canonical = serialize(this.ctx, doc);
|
|
431
530
|
if (canonical === '')
|
|
432
531
|
return this.source.startsWith('\uFEFF') ? '\uFEFF' : '';
|
|
@@ -445,7 +544,6 @@ export class Preservation {
|
|
|
445
544
|
const currentTree = processor.runSync(structuredClone(currentRawTree), {
|
|
446
545
|
value: canonical,
|
|
447
546
|
});
|
|
448
|
-
const body = this.source.replace(/^\uFEFF/, '');
|
|
449
547
|
let output = '';
|
|
450
548
|
let previous;
|
|
451
549
|
current.forEach((text, index) => {
|
|
@@ -465,7 +563,7 @@ export class Preservation {
|
|
|
465
563
|
if (block && block.form === text)
|
|
466
564
|
output += this.source.slice(block.start, block.end);
|
|
467
565
|
else if (block && currentTree.children.length === current.length) {
|
|
468
|
-
output +=
|
|
566
|
+
output += this.retainBlock(block, positionalNode(currentRawTree, currentTree.children[index]), canonical);
|
|
469
567
|
}
|
|
470
568
|
else
|
|
471
569
|
output += restoreCodeEndings(this.ctx, doc.type.create(null, [doc.child(index)]), text.replaceAll('\n', this.lineEnding));
|
|
@@ -477,10 +575,16 @@ export class Preservation {
|
|
|
477
575
|
output += finalCodeEnding(doc) ?? this.lineEnding;
|
|
478
576
|
const parse = this.ctx.get(parserCtx);
|
|
479
577
|
const reopened = parse(output);
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
578
|
+
const normalized = (text) => text.replace(/\r\n?/g, '\n');
|
|
579
|
+
if (semanticSignature(reopened) === semanticSignature(doc)) {
|
|
580
|
+
const actual = normalized(serialize(this.ctx, reopened));
|
|
581
|
+
if (actual === normalized(canonical))
|
|
582
|
+
return output;
|
|
583
|
+
// Parsing may normalize loose-list spacing. Compare both reconstructed
|
|
584
|
+
// source and intended canonical source after that same normalization.
|
|
585
|
+
if (actual === normalized(serialize(this.ctx, parse(canonical))))
|
|
586
|
+
return output;
|
|
587
|
+
}
|
|
484
588
|
throw new PreservationError('The edited Markdown cannot be reopened without changing its content');
|
|
485
589
|
}
|
|
486
590
|
catch (cause) {
|
package/dist/search.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type Transaction } from '@milkdown/kit/prose/state';
|
|
2
2
|
import type { EditorView } from '@milkdown/kit/prose/view';
|
|
3
3
|
export interface TextMatch {
|
|
4
4
|
from: number;
|
|
@@ -11,5 +11,5 @@ export interface LiteralReplacement extends TextMatch {
|
|
|
11
11
|
export declare function findLiteral(text: string, search: string, from?: number): TextMatch | undefined;
|
|
12
12
|
export declare function replaceLiteral(text: string, search: string, replacement: string, selection: TextMatch, all?: boolean): LiteralReplacement;
|
|
13
13
|
export declare function findFormatted(view: EditorView, text: string): boolean;
|
|
14
|
-
export declare function replaceFormatted(view: EditorView, search: string, replacement: string, preflight: (
|
|
15
|
-
export declare function replaceFormattedRange(view: EditorView, from: number, to: number, replacement: string, preflight: (
|
|
14
|
+
export declare function replaceFormatted(view: EditorView, search: string, replacement: string, preflight: (tr: Transaction) => void, all?: boolean): number;
|
|
15
|
+
export declare function replaceFormattedRange(view: EditorView, from: number, to: number, replacement: string, preflight: (tr: Transaction) => void): boolean;
|
package/dist/search.js
CHANGED
|
@@ -162,7 +162,7 @@ export function replaceFormatted(view, search, replacement, preflight, all = fal
|
|
|
162
162
|
tr = tr.setSelection(TextSelection.create(tr.doc, match.from, tr.mapping.map(match.to, 1)));
|
|
163
163
|
}
|
|
164
164
|
try {
|
|
165
|
-
preflight(tr
|
|
165
|
+
preflight(tr);
|
|
166
166
|
}
|
|
167
167
|
catch (error) {
|
|
168
168
|
if (error instanceof InkKitError)
|
|
@@ -189,7 +189,7 @@ export function replaceFormattedRange(view, from, to, replacement, preflight) {
|
|
|
189
189
|
const tr = closeHistory(view.state.tr.replace(from, to, replacementSlice(view.state, from, to, value)));
|
|
190
190
|
tr.setSelection(TextSelection.create(tr.doc, from, tr.mapping.map(to, 1)));
|
|
191
191
|
try {
|
|
192
|
-
preflight(tr
|
|
192
|
+
preflight(tr);
|
|
193
193
|
}
|
|
194
194
|
catch (error) {
|
|
195
195
|
if (error instanceof InkKitError)
|