@aicayzer/inkkit 0.0.12 → 0.0.14
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/editor.d.ts +1 -0
- package/dist/editor.js +40 -16
- package/dist/images.js +19 -7
- package/dist/paste.d.ts +2 -0
- package/dist/paste.js +19 -12
- package/dist/preserve.d.ts +4 -0
- package/dist/preserve.js +134 -30
- package/dist/style.css +50 -10
- package/dist/text-ranges.d.ts +4 -1
- package/dist/text-ranges.js +26 -12
- package/package.json +1 -1
package/dist/editor.d.ts
CHANGED
|
@@ -181,6 +181,7 @@ export declare class InkKitEditor {
|
|
|
181
181
|
}): void;
|
|
182
182
|
replaceTextRange(range: TextRange, text: string): boolean;
|
|
183
183
|
textRangeRects(range: TextRange): readonly TextRect[];
|
|
184
|
+
textRangesRects(ranges: readonly TextRange[]): readonly (readonly TextRect[])[];
|
|
184
185
|
visibleTextRanges(snapshotId: string): readonly TextRange[];
|
|
185
186
|
revealTextRange(range: TextRange): void;
|
|
186
187
|
private revealRects;
|
package/dist/editor.js
CHANGED
|
@@ -25,7 +25,7 @@ import { wikiLinkView } from './wiki-view.js';
|
|
|
25
25
|
import { withFileSignal } from './files.js';
|
|
26
26
|
import { tablePlugins, tableCommand, tableCommands, tableAvailability, tableContext, configureTableMovement, } from './tables.js';
|
|
27
27
|
import { InkKitError, } from './types.js';
|
|
28
|
-
import { readableProjection, projectionPosition, projectionOffset, validateOffsets, formattedRects,
|
|
28
|
+
import { readableProjection, projectionPosition, projectionOffset, validateOffsets, formattedRects, literalRangesRects, literalVisibleRanges, intersectsViewport, textRect, } from './text-ranges.js';
|
|
29
29
|
import { highlightKeymap, toggleHighlightCommand } from './inline-highlight.js';
|
|
30
30
|
import { commentSelectionContent, setCommentVisibility } from './comments.js';
|
|
31
31
|
import { PasteController } from './paste.js';
|
|
@@ -1089,6 +1089,24 @@ export class InkKitEditor {
|
|
|
1089
1089
|
unbind(orderedListKeymap);
|
|
1090
1090
|
ctx.update(editorViewOptionsCtx, (options) => ({
|
|
1091
1091
|
...options,
|
|
1092
|
+
dispatchTransaction: function (tr) {
|
|
1093
|
+
const next = this.state.apply(tr);
|
|
1094
|
+
if (!instance.loading &&
|
|
1095
|
+
instance.preservation &&
|
|
1096
|
+
!instance.literalSurface &&
|
|
1097
|
+
!next.doc.eq(this.state.doc)) {
|
|
1098
|
+
try {
|
|
1099
|
+
// Validate the final state, including dialect normalisation transactions.
|
|
1100
|
+
instance.currentText(next.doc);
|
|
1101
|
+
}
|
|
1102
|
+
catch (error) {
|
|
1103
|
+
this.updateState(this.state);
|
|
1104
|
+
instance.events.error?.(new InkKitError('preservation', `The edit was not applied because its Markdown could not be preserved: ${error instanceof Error ? error.message : String(error)}`));
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
this.updateState(next);
|
|
1109
|
+
},
|
|
1092
1110
|
handleScrollToSelection: () => {
|
|
1093
1111
|
if (!instance.loading && instance.ready)
|
|
1094
1112
|
instance.revealCurrentSelection();
|
|
@@ -1374,9 +1392,10 @@ export class InkKitEditor {
|
|
|
1374
1392
|
this.assertAlive();
|
|
1375
1393
|
const container = this.viewportContainer ?? this.root;
|
|
1376
1394
|
const top = container.scrollTop, left = container.scrollLeft;
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
|
|
1395
|
+
const active = this.root.ownerDocument.activeElement;
|
|
1396
|
+
if (active === this.plain ||
|
|
1397
|
+
active === this.editor.ctx.get(editorViewCtx).dom)
|
|
1398
|
+
active.blur();
|
|
1380
1399
|
this.ready = false;
|
|
1381
1400
|
this.cancelExports();
|
|
1382
1401
|
this.mediaPolicy();
|
|
@@ -1401,12 +1420,6 @@ export class InkKitEditor {
|
|
|
1401
1420
|
this.updateSurface();
|
|
1402
1421
|
this.ready = true;
|
|
1403
1422
|
this.mediaPolicy();
|
|
1404
|
-
if (focused) {
|
|
1405
|
-
if (this.literalSurface)
|
|
1406
|
-
this.plain.focus({ preventScroll: true });
|
|
1407
|
-
else
|
|
1408
|
-
this.editor.ctx.get(editorViewCtx).focus();
|
|
1409
|
-
}
|
|
1410
1423
|
container.scrollTop = top;
|
|
1411
1424
|
container.scrollLeft = left;
|
|
1412
1425
|
this.events.stateChanged(this.literalSurface
|
|
@@ -1623,11 +1636,17 @@ export class InkKitEditor {
|
|
|
1623
1636
|
});
|
|
1624
1637
|
}
|
|
1625
1638
|
textRangeRects(range) {
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1639
|
+
return this.textRangesRects([range])[0];
|
|
1640
|
+
}
|
|
1641
|
+
textRangesRects(ranges) {
|
|
1642
|
+
this.assertCurrent();
|
|
1643
|
+
const projections = ranges.map((range) => this.validateTextRange(range));
|
|
1644
|
+
if (!ranges.length)
|
|
1645
|
+
return Object.freeze([]);
|
|
1646
|
+
const results = this.literalSurface
|
|
1647
|
+
? literalRangesRects(this.plain, ranges)
|
|
1648
|
+
: ranges.map((range, index) => formattedRects(this.editor.ctx.get(editorViewCtx), projections[index], range.from, range.to));
|
|
1649
|
+
return Object.freeze(results.map((rects) => Object.freeze(rects.map((rect) => Object.freeze(rect)))));
|
|
1631
1650
|
}
|
|
1632
1651
|
visibleTextRanges(snapshotId) {
|
|
1633
1652
|
const snapshot = this.textSnapshot();
|
|
@@ -1763,7 +1782,12 @@ export class InkKitEditor {
|
|
|
1763
1782
|
});
|
|
1764
1783
|
}
|
|
1765
1784
|
revealLiteralSelection() {
|
|
1766
|
-
this.revealRects(
|
|
1785
|
+
this.revealRects(literalRangesRects(this.plain, [
|
|
1786
|
+
{
|
|
1787
|
+
from: this.plain.selectionStart,
|
|
1788
|
+
to: this.plain.selectionEnd,
|
|
1789
|
+
},
|
|
1790
|
+
])[0]);
|
|
1767
1791
|
}
|
|
1768
1792
|
load(markdown, generation) {
|
|
1769
1793
|
this.generation = generation;
|
package/dist/images.js
CHANGED
|
@@ -143,19 +143,28 @@ class ImageView {
|
|
|
143
143
|
this.runtime?.error?.(error instanceof Error ? error : new Error(String(error)));
|
|
144
144
|
}
|
|
145
145
|
card(label, status, retry = false) {
|
|
146
|
+
const footer = document.createElement('span');
|
|
147
|
+
footer.className = 'inkkit-file-footer';
|
|
146
148
|
const text = document.createElement('span');
|
|
147
149
|
text.className = 'inkkit-file-label';
|
|
148
|
-
text.textContent =
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
150
|
+
text.textContent = label;
|
|
151
|
+
text.title = label;
|
|
152
|
+
footer.append(text);
|
|
153
|
+
if (status) {
|
|
154
|
+
const description = document.createElement('span');
|
|
155
|
+
description.className = 'inkkit-file-status';
|
|
156
|
+
description.setAttribute('role', 'status');
|
|
157
|
+
description.textContent = status;
|
|
158
|
+
footer.append(description);
|
|
159
|
+
}
|
|
152
160
|
if (retry && this.runtime)
|
|
153
|
-
|
|
161
|
+
footer.append(this.button(this.runtime.labels.fileRetry, () => this.resolution?.retry()));
|
|
154
162
|
if (this.runtime?.adapter.open) {
|
|
155
163
|
const open = this.button(this.runtime.labels.fileOpen, this.activate);
|
|
156
164
|
open.setAttribute('aria-label', `${this.runtime.labels.fileOpen} ${label}`);
|
|
157
|
-
|
|
165
|
+
footer.append(open);
|
|
158
166
|
}
|
|
167
|
+
this.dom.append(footer);
|
|
159
168
|
}
|
|
160
169
|
failPreview() {
|
|
161
170
|
this.previewFailed = true;
|
|
@@ -284,10 +293,13 @@ class ImageView {
|
|
|
284
293
|
});
|
|
285
294
|
image.src = url;
|
|
286
295
|
this.image = image;
|
|
296
|
+
const preview = document.createElement('span');
|
|
297
|
+
preview.className = 'inkkit-image-preview';
|
|
287
298
|
const handle = document.createElement('span');
|
|
288
299
|
handle.className = 'image-handle';
|
|
289
300
|
handle.addEventListener('pointerdown', this.startResize);
|
|
290
|
-
|
|
301
|
+
preview.append(image, handle);
|
|
302
|
+
this.dom.append(preview);
|
|
291
303
|
}
|
|
292
304
|
startResize = (event) => {
|
|
293
305
|
event.preventDefault();
|
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
|
@@ -8,6 +8,7 @@ import { InkKitError, } from './types.js';
|
|
|
8
8
|
export class PasteController {
|
|
9
9
|
options;
|
|
10
10
|
active;
|
|
11
|
+
key = new PluginKey('inkkitPaste');
|
|
11
12
|
dead = false;
|
|
12
13
|
get pending() {
|
|
13
14
|
return this.active != null;
|
|
@@ -16,16 +17,18 @@ export class PasteController {
|
|
|
16
17
|
constructor(options) {
|
|
17
18
|
this.options = options;
|
|
18
19
|
this.plugin = $prose(() => new Plugin({
|
|
19
|
-
key:
|
|
20
|
+
key: this.key,
|
|
20
21
|
state: {
|
|
21
22
|
init: () => null,
|
|
22
|
-
apply: (transaction) => {
|
|
23
|
-
if (this.active)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
apply: (transaction, previous) => {
|
|
24
|
+
if (!this.active)
|
|
25
|
+
return null;
|
|
26
|
+
const anchor = previous?.operation === this.active ? previous : this.active;
|
|
27
|
+
return {
|
|
28
|
+
operation: this.active,
|
|
29
|
+
from: transaction.mapping.map(anchor.from, 1),
|
|
30
|
+
to: transaction.mapping.map(anchor.to, anchor.from === anchor.to ? 1 : -1),
|
|
31
|
+
};
|
|
29
32
|
},
|
|
30
33
|
},
|
|
31
34
|
props: {
|
|
@@ -68,6 +71,10 @@ export class PasteController {
|
|
|
68
71
|
paste(input) {
|
|
69
72
|
return this.run(input);
|
|
70
73
|
}
|
|
74
|
+
anchor(operation) {
|
|
75
|
+
const mapped = this.key.getState(this.options.ctx().get(editorViewCtx).state);
|
|
76
|
+
return mapped?.operation === operation ? mapped : operation;
|
|
77
|
+
}
|
|
71
78
|
cancelPending() {
|
|
72
79
|
this.active = undefined;
|
|
73
80
|
this.options.onPendingChanged?.();
|
|
@@ -127,7 +134,7 @@ export class PasteController {
|
|
|
127
134
|
(input.html ? markdownFromHTML(input.html) : undefined));
|
|
128
135
|
if (plainText) {
|
|
129
136
|
if (view.state.selection.$from.parent.type.spec.code) {
|
|
130
|
-
const anchor = operation;
|
|
137
|
+
const anchor = this.anchor(operation);
|
|
131
138
|
view.dispatch(view.state.tr.insertText(input.text, anchor.from, anchor.to));
|
|
132
139
|
return;
|
|
133
140
|
}
|
|
@@ -389,8 +396,8 @@ export class PasteController {
|
|
|
389
396
|
!selection.empty &&
|
|
390
397
|
/^https?:\/\/\S+$/.test(text) &&
|
|
391
398
|
schema.marks.link) {
|
|
392
|
-
const anchor = operation;
|
|
393
|
-
const mark = schema.marks.link.create({ href: text
|
|
399
|
+
const anchor = this.anchor(operation);
|
|
400
|
+
const mark = schema.marks.link.create({ href: text });
|
|
394
401
|
view.dispatch(view.state.tr
|
|
395
402
|
.addMark(anchor.from, anchor.to, mark)
|
|
396
403
|
.removeStoredMark(mark));
|
|
@@ -402,7 +409,7 @@ export class PasteController {
|
|
|
402
409
|
this.assertContext(context, operation);
|
|
403
410
|
if (!view.editable || this.options.editable?.() === false)
|
|
404
411
|
return;
|
|
405
|
-
const anchor = operation;
|
|
412
|
+
const anchor = this.anchor(operation);
|
|
406
413
|
if (!plainText) {
|
|
407
414
|
const valid = slice.content.firstChild?.isInline
|
|
408
415
|
? 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/style.css
CHANGED
|
@@ -207,16 +207,26 @@
|
|
|
207
207
|
height: auto;
|
|
208
208
|
border-radius: 6px;
|
|
209
209
|
}
|
|
210
|
+
.inkkit-root .ProseMirror .inkkit-image-preview {
|
|
211
|
+
position: relative;
|
|
212
|
+
display: block;
|
|
213
|
+
width: fit-content;
|
|
214
|
+
max-width: 100%;
|
|
215
|
+
line-height: 0;
|
|
216
|
+
}
|
|
210
217
|
.inkkit-root .ProseMirror .image-absent {
|
|
211
218
|
color: var(--_inkkit-muted);
|
|
212
219
|
font-style: italic;
|
|
213
220
|
}
|
|
214
221
|
.inkkit-root .ProseMirror .image-handle {
|
|
215
222
|
position: absolute;
|
|
216
|
-
right:
|
|
217
|
-
top:
|
|
218
|
-
|
|
219
|
-
|
|
223
|
+
right: 0;
|
|
224
|
+
top: 50%;
|
|
225
|
+
transform: translateY(-50%);
|
|
226
|
+
width: 20px;
|
|
227
|
+
height: 32px;
|
|
228
|
+
max-height: 100%;
|
|
229
|
+
touch-action: none;
|
|
220
230
|
cursor: col-resize;
|
|
221
231
|
opacity: 0;
|
|
222
232
|
}
|
|
@@ -227,22 +237,41 @@
|
|
|
227
237
|
.inkkit-root .ProseMirror .image-handle::after {
|
|
228
238
|
content: '';
|
|
229
239
|
position: absolute;
|
|
230
|
-
right:
|
|
231
|
-
top:
|
|
232
|
-
|
|
240
|
+
right: 8px;
|
|
241
|
+
top: 50%;
|
|
242
|
+
transform: translateY(-50%);
|
|
243
|
+
height: 20px;
|
|
244
|
+
max-height: 100%;
|
|
233
245
|
width: 4px;
|
|
234
246
|
border-radius: 2px;
|
|
235
247
|
background: var(--_inkkit-accent);
|
|
236
248
|
}
|
|
249
|
+
.inkkit-root .ProseMirror[contenteditable='false'] .image-handle {
|
|
250
|
+
display: none;
|
|
251
|
+
}
|
|
237
252
|
|
|
238
253
|
.inkkit-root .ProseMirror .inkkit-file {
|
|
254
|
+
box-sizing: border-box;
|
|
239
255
|
vertical-align: middle;
|
|
240
256
|
border: 1px solid var(--_inkkit-rule);
|
|
241
|
-
border-radius:
|
|
242
|
-
padding:
|
|
257
|
+
border-radius: 6px;
|
|
258
|
+
padding: 8px;
|
|
243
259
|
white-space: normal;
|
|
244
260
|
}
|
|
245
261
|
|
|
262
|
+
.inkkit-root .ProseMirror .inkkit-file-footer {
|
|
263
|
+
display: flex;
|
|
264
|
+
flex-wrap: wrap;
|
|
265
|
+
align-items: center;
|
|
266
|
+
gap: 0.5em;
|
|
267
|
+
font-size: 0.9em;
|
|
268
|
+
line-height: 1.4;
|
|
269
|
+
color: var(--_inkkit-muted);
|
|
270
|
+
}
|
|
271
|
+
.inkkit-root .ProseMirror .inkkit-file-footer:not(:first-child) {
|
|
272
|
+
margin-block-start: 0.5em;
|
|
273
|
+
}
|
|
274
|
+
|
|
246
275
|
.inkkit-root .ProseMirror .inkkit-file audio,
|
|
247
276
|
.inkkit-root .ProseMirror .inkkit-file video,
|
|
248
277
|
.inkkit-root .ProseMirror .inkkit-file iframe {
|
|
@@ -261,12 +290,23 @@
|
|
|
261
290
|
}
|
|
262
291
|
|
|
263
292
|
.inkkit-root .ProseMirror .inkkit-file-label {
|
|
293
|
+
flex: 1 1 auto;
|
|
294
|
+
min-width: 0;
|
|
295
|
+
overflow: hidden;
|
|
296
|
+
text-overflow: ellipsis;
|
|
297
|
+
white-space: nowrap;
|
|
298
|
+
}
|
|
299
|
+
.inkkit-root .ProseMirror .inkkit-file-status {
|
|
300
|
+
flex-basis: 100%;
|
|
264
301
|
overflow-wrap: anywhere;
|
|
265
302
|
}
|
|
266
303
|
|
|
267
304
|
.inkkit-root .ProseMirror .inkkit-file-control {
|
|
268
|
-
|
|
305
|
+
flex: none;
|
|
306
|
+
padding: 0.25em 0.5em;
|
|
269
307
|
font: inherit;
|
|
308
|
+
line-height: 1.4;
|
|
309
|
+
white-space: nowrap;
|
|
270
310
|
color: var(--_inkkit-text);
|
|
271
311
|
background: var(--_inkkit-code-bg);
|
|
272
312
|
border: 1px solid var(--_inkkit-rule);
|
package/dist/text-ranges.d.ts
CHANGED
|
@@ -18,7 +18,10 @@ export declare function projectionOffset(projection: TextProjection, position: n
|
|
|
18
18
|
export declare function validateOffsets(text: string, from: number, to: number): void;
|
|
19
19
|
export declare function textRect(rect: Pick<TextRect, 'left' | 'top' | 'right' | 'bottom'>): TextRect;
|
|
20
20
|
export declare function formattedRects(view: EditorView, projection: TextProjection, from: number, to: number): TextRect[];
|
|
21
|
-
export declare function
|
|
21
|
+
export declare function literalRangesRects(plain: HTMLTextAreaElement, ranges: readonly {
|
|
22
|
+
from: number;
|
|
23
|
+
to: number;
|
|
24
|
+
}[]): TextRect[][];
|
|
22
25
|
export declare function intersectsViewport(rect: Pick<TextRect, 'top' | 'right' | 'bottom' | 'left'>, viewport: TextRect): boolean;
|
|
23
26
|
export declare function literalVisibleRanges(plain: HTMLTextAreaElement, viewport: TextRect): {
|
|
24
27
|
from: number;
|
package/dist/text-ranges.js
CHANGED
|
@@ -110,8 +110,20 @@ export function formattedRects(view, projection, from, to) {
|
|
|
110
110
|
return [textRect(view.coordsAtPos(position))];
|
|
111
111
|
}
|
|
112
112
|
const rects = [];
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
let low = 0;
|
|
114
|
+
let high = projection.spans.length;
|
|
115
|
+
while (low < high) {
|
|
116
|
+
const middle = (low + high) >>> 1;
|
|
117
|
+
if (projection.spans[middle].to <= from)
|
|
118
|
+
low = middle + 1;
|
|
119
|
+
else
|
|
120
|
+
high = middle;
|
|
121
|
+
}
|
|
122
|
+
for (let index = low; index < projection.spans.length; index++) {
|
|
123
|
+
const span = projection.spans[index];
|
|
124
|
+
if (span.from >= to)
|
|
125
|
+
break;
|
|
126
|
+
if (span.kind === 'separator')
|
|
115
127
|
continue;
|
|
116
128
|
if (span.kind === 'embed' || span.kind === 'break') {
|
|
117
129
|
const node = view.nodeDOM(span.start);
|
|
@@ -180,18 +192,20 @@ function withLiteralLayout(plain, measure) {
|
|
|
180
192
|
mirror.remove();
|
|
181
193
|
}
|
|
182
194
|
}
|
|
183
|
-
export function
|
|
195
|
+
export function literalRangesRects(plain, ranges) {
|
|
184
196
|
return (withLiteralLayout(plain, (text) => {
|
|
185
197
|
const range = plain.ownerDocument.createRange();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
198
|
+
return ranges.map(({ from, to }) => {
|
|
199
|
+
range.setStart(text, from);
|
|
200
|
+
range.setEnd(text, from === to ? Math.min(to + 1, text.length) : to);
|
|
201
|
+
return Array.from(range.getClientRects(), (rect) => textRect({
|
|
202
|
+
left: rect.left,
|
|
203
|
+
right: from === to ? rect.left : rect.right,
|
|
204
|
+
top: rect.top,
|
|
205
|
+
bottom: rect.bottom,
|
|
206
|
+
}));
|
|
207
|
+
});
|
|
208
|
+
}) ?? ranges.map(() => []));
|
|
195
209
|
}
|
|
196
210
|
export function intersectsViewport(rect, viewport) {
|
|
197
211
|
return (viewport.width > 0 &&
|