@aicayzer/inkkit 0.0.8 → 0.0.10

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.js CHANGED
@@ -17,19 +17,22 @@ import { codeCopyPlugin, placeholderPlugin } from './decorations.js';
17
17
  import { labelsCtx, defaultLabels } from './labels.js';
18
18
  import { createDialect, stringifyOptions } from './dialect.js';
19
19
  import { Preservation } from './preserve.js';
20
- import { sourceAttribute, cleanSourceDoc, editedPlainSource, sourceSelection, revealSourceCaret, rawOffset, normalizedOffset, } from './source.js';
20
+ import { sourceAttribute, cleanSourceDoc, editedPlainSource, sourceSelection, rawOffset, normalizedOffset, } from './source.js';
21
21
  import { normaliseLabel, referenceDefinitions, footnoteDefinitions, } from './references.js';
22
22
  import { selectionMarkdown, selectionContent, referenceMetadata, withReferenceMetadata, } from './reference-clipboard.js';
23
23
  import { imageView } from './images.js';
24
+ import { wikiLinkView } from './wiki-view.js';
25
+ import { withFileSignal } from './files.js';
24
26
  import { tablePlugins, tableCommand, tableCommands, tableAvailability, tableContext, configureTableMovement, } from './tables.js';
25
27
  import { InkKitError, } from './types.js';
28
+ import { readableProjection, projectionPosition, projectionOffset, validateOffsets, formattedRects, literalRects, literalVisibleRanges, intersectsViewport, textRect, } from './text-ranges.js';
26
29
  import { highlightPlugin } from './highlight.js';
27
30
  import { highlightKeymap, toggleHighlightCommand } from './inline-highlight.js';
28
31
  import { commentSelectionContent, setCommentVisibility } from './comments.js';
29
32
  import { PasteController } from './paste.js';
30
33
  import { selectionPlugin } from './selection.js';
31
34
  import { search, SearchQuery, setSearchState } from 'prosemirror-search';
32
- import { findFormatted, findLiteral, replaceFormatted, replaceLiteral, } from './search.js';
35
+ import { findFormatted, findLiteral, replaceFormatted, replaceLiteral, replaceFormattedRange, } from './search.js';
33
36
  import { collectFormattedHeadings, collectSourceHeadings, navigateFormattedHeading, sourceHeadingPosition, } from './outline.js';
34
37
  import { taskListPlugin, toggleTaskList } from './tasks.js';
35
38
  import { isMermaid, mermaidPreview } from './mermaid.js';
@@ -239,6 +242,7 @@ function joinLists(tr, start, index) {
239
242
  if (before && after && isList(before) && after.type === before.type)
240
243
  tr.join($start.posAtIndex(index));
241
244
  }
245
+ let editorSequence = 0;
242
246
  export class InkKitEditor {
243
247
  events;
244
248
  options;
@@ -249,6 +253,7 @@ export class InkKitEditor {
249
253
  revision = 0;
250
254
  documentEpoch = 0;
251
255
  operationSequence = 0;
256
+ exportControllers = new Set();
252
257
  plainComposing = false;
253
258
  plainSearch = '';
254
259
  literalMatch;
@@ -270,6 +275,15 @@ export class InkKitEditor {
270
275
  generation = 0;
271
276
  reportingSuppressed = false;
272
277
  loading = false;
278
+ textIdentity = ++editorSequence;
279
+ textScope;
280
+ viewportContainer;
281
+ viewportInsets = {
282
+ top: 0,
283
+ right: 0,
284
+ bottom: 0,
285
+ left: 0,
286
+ };
273
287
  writable = true;
274
288
  inputPreferences = {};
275
289
  policyEpoch = 0;
@@ -283,6 +297,8 @@ export class InkKitEditor {
283
297
  if (this.loading || view.state.doc.eq(previous.doc))
284
298
  return;
285
299
  this.revision += 1;
300
+ this.cancelExports();
301
+ this.mediaPolicy();
286
302
  this.literalMatch = undefined;
287
303
  let markdown;
288
304
  try {
@@ -300,7 +316,7 @@ export class InkKitEditor {
300
316
  this.plain.value = markdown;
301
317
  if (!this.plainComposing) {
302
318
  this.plain.setSelectionRange(Math.min(from, this.plain.value.length), Math.min(to, this.plain.value.length));
303
- revealSourceCaret(this.plain);
319
+ this.revealLiteralSelection();
304
320
  }
305
321
  }
306
322
  if (markdown === this.lastMarkdown)
@@ -324,6 +340,36 @@ export class InkKitEditor {
324
340
  operationId: `${this.documentEpoch}:${this.generation}:${++this.operationSequence}`,
325
341
  };
326
342
  }
343
+ cancelExports() {
344
+ for (const controller of this.exportControllers)
345
+ controller.abort();
346
+ this.exportControllers.clear();
347
+ }
348
+ async exportContent(snapshot, operation) {
349
+ const epoch = this.documentEpoch;
350
+ const mode = this.editingMode;
351
+ const controller = new AbortController();
352
+ this.exportControllers.add(controller);
353
+ try {
354
+ return await withFileSignal(operation(controller.signal), controller.signal);
355
+ }
356
+ finally {
357
+ this.exportControllers.delete(controller);
358
+ this.assertCurrent(snapshot.generation);
359
+ if (controller.signal.aborted ||
360
+ epoch !== this.documentEpoch ||
361
+ snapshot.documentId !== this.documentId ||
362
+ snapshot.revision !== this.revision ||
363
+ mode !== this.editingMode)
364
+ throw new InkKitError('stale-document', 'Document changed while preparing output');
365
+ }
366
+ }
367
+ mediaPolicy() {
368
+ if (this.editor && this.root)
369
+ this.editor.ctx
370
+ .get(editorViewCtx)
371
+ .dom.dispatchEvent(new Event('inkkit-media-policy'));
372
+ }
327
373
  assertAlive() {
328
374
  if (this.destroyed)
329
375
  throw new InkKitError('destroyed', 'The editor has been destroyed');
@@ -372,6 +418,7 @@ export class InkKitEditor {
372
418
  .get(editorViewCtx)
373
419
  .dom.dispatchEvent(new Event('inkkit-cancel-resize'));
374
420
  this.applyInputPolicy();
421
+ this.mediaPolicy();
375
422
  }
376
423
  finally {
377
424
  this.reportingSuppressed = suppressed;
@@ -574,8 +621,15 @@ export class InkKitEditor {
574
621
  throw new RangeError('Editing mode must be source or formatted');
575
622
  if (this.formatType === 'txt' || this.mode === mode)
576
623
  return false;
624
+ const view = this.editor.ctx.get(editorViewCtx);
625
+ const focused = this.literalSurface
626
+ ? this.plain.ownerDocument.activeElement === this.plain
627
+ : view.hasFocus();
628
+ const container = this.viewportContainer ?? this.root;
629
+ const top = container.scrollTop, left = container.scrollLeft;
577
630
  const text = this.snapshot().text;
578
631
  this.mode = mode;
632
+ this.cancelExports();
579
633
  this.outlineEpoch += 1;
580
634
  this.plainSearch = '';
581
635
  this.literalMatch = undefined;
@@ -591,6 +645,14 @@ export class InkKitEditor {
591
645
  finally {
592
646
  this.reportingSuppressed = suppressed;
593
647
  }
648
+ if (focused) {
649
+ if (this.literalSurface)
650
+ this.plain.focus({ preventScroll: true });
651
+ else
652
+ view.focus();
653
+ }
654
+ container.scrollTop = top;
655
+ container.scrollLeft = left;
594
656
  this.events.stateChanged(this.literalSurface
595
657
  ? { marks: [], block: { type: 'paragraph' }, quoted: false }
596
658
  : caretState(this.editor.ctx.get(editorViewCtx).state));
@@ -606,6 +668,7 @@ export class InkKitEditor {
606
668
  : (this.options.labels?.sourceEditor ?? defaultLabels.sourceEditor));
607
669
  this.editor.ctx.get(editorViewCtx).dom.parentElement.hidden =
608
670
  this.literalSurface;
671
+ this.mediaPolicy();
609
672
  }
610
673
  replaceSource(text, expectedGeneration) {
611
674
  this.assertMutation(expectedGeneration);
@@ -710,26 +773,17 @@ export class InkKitEditor {
710
773
  }
711
774
  async printableSnapshot(expectedGeneration) {
712
775
  const snapshot = this.snapshot(expectedGeneration);
713
- const epoch = this.documentEpoch;
714
776
  const state = this.editor.ctx.get(editorViewCtx).state;
715
- let output;
716
- try {
777
+ const output = await this.exportContent(snapshot, async (signal) => {
717
778
  if (snapshot.format === 'md' &&
718
779
  !this.options.images &&
780
+ !this.options.files &&
719
781
  hasAuthoredImagesInLiterals(state.doc.content, (source) => this.editor.ctx.get(remarkCtx).parse(source)))
720
782
  throw new InkKitError('image-unavailable', 'No image adapter is configured for the authored images.');
721
- output =
722
- snapshot.format === 'txt'
723
- ? printableText(snapshot.text)
724
- : await printableMarkdown(state.doc.content, state.schema, this.options.images, this.context());
725
- }
726
- finally {
727
- this.assertCurrent(snapshot.generation);
728
- if (snapshot.documentId !== this.documentId ||
729
- epoch !== this.documentEpoch ||
730
- snapshot.revision !== this.revision)
731
- throw new InkKitError('stale-document', 'Document changed while preparing printable content');
732
- }
783
+ return snapshot.format === 'txt'
784
+ ? printableText(snapshot.text)
785
+ : printableMarkdown(state.doc.content, state.schema, this.options.images, this.context(), this.options.files, signal);
786
+ });
733
787
  return Object.freeze({
734
788
  documentId: snapshot.documentId,
735
789
  generation: snapshot.generation,
@@ -802,7 +856,7 @@ export class InkKitEditor {
802
856
  ? error.message
803
857
  : 'Cannot copy the selected Markdown');
804
858
  }
805
- const result = await portableClipboard(all || selectedDoc ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context(), completeDiagrams);
859
+ const result = await this.exportContent(snapshot, (signal) => portableClipboard(all || selectedDoc ? content : selectionContent(doc, content), schema, markdown, this.options.images, this.context(), completeDiagrams, this.options.files, signal));
806
860
  const metadata = referenceMetadata(this.editor.ctx, selectedDoc ?? doc, content);
807
861
  if (metadata != null)
808
862
  result.html = withReferenceMetadata(result.html, metadata);
@@ -881,6 +935,8 @@ export class InkKitEditor {
881
935
  return;
882
936
  this.destroyed = true;
883
937
  this.ready = false;
938
+ this.cancelExports();
939
+ this.mediaPolicy();
884
940
  this.pasteController.destroy();
885
941
  if (this.clickHandler)
886
942
  this.root.removeEventListener('click', this.clickHandler);
@@ -1013,11 +1069,13 @@ export class InkKitEditor {
1013
1069
  unbind(codeBlockKeymap);
1014
1070
  unbind(bulletListKeymap);
1015
1071
  unbind(orderedListKeymap);
1016
- // The caret is kept above the fade under the formatting bar.
1017
1072
  ctx.update(editorViewOptionsCtx, (options) => ({
1018
1073
  ...options,
1019
- scrollThreshold: { top: 8, right: 0, bottom: 24, left: 0 },
1020
- scrollMargin: { top: 8, right: 0, bottom: 24, left: 0 },
1074
+ handleScrollToSelection: () => {
1075
+ if (!instance.loading && instance.ready)
1076
+ instance.revealCurrentSelection();
1077
+ return true;
1078
+ },
1021
1079
  }));
1022
1080
  })
1023
1081
  .use(labelsCtx)
@@ -1025,7 +1083,7 @@ export class InkKitEditor {
1025
1083
  .use(instance.pasteController.plugin)
1026
1084
  .use(visibleClipboard)
1027
1085
  .use(instance.keymapPlugin)
1028
- .use(createDialect(Boolean(options.images)))
1086
+ .use(createDialect(Boolean(options.images || options.files), Boolean(options.wikiLinks), Boolean(options.files)))
1029
1087
  .use(tablePlugins)
1030
1088
  .use(instance.changePlugin)
1031
1089
  .use(history)
@@ -1043,7 +1101,57 @@ export class InkKitEditor {
1043
1101
  .use(selectionPlugin)
1044
1102
  .use($prose(() => search()))
1045
1103
  .use(instance.policyPlugin)
1046
- .use(options.images ? imageView(options.images) : [])
1104
+ .use(options.wikiLinks
1105
+ ? wikiLinkView(options.wikiLinks, {
1106
+ capture: () => {
1107
+ const epoch = instance.documentEpoch;
1108
+ const context = instance.context();
1109
+ return {
1110
+ context,
1111
+ isCurrent: () => !instance.destroyed &&
1112
+ epoch === instance.documentEpoch &&
1113
+ context.documentId === instance.documentId &&
1114
+ context.generation === instance.generation,
1115
+ };
1116
+ },
1117
+ active: () => instance.ready &&
1118
+ !instance.destroyed &&
1119
+ !instance.literalSurface,
1120
+ labels: { ...defaultLabels, ...options.labels },
1121
+ error: (error) => events.error?.(error),
1122
+ })
1123
+ : [])
1124
+ .use(options.images || options.files
1125
+ ? imageView(options.images, options.files
1126
+ ? {
1127
+ adapter: options.files,
1128
+ capture: () => {
1129
+ const epoch = instance.documentEpoch;
1130
+ const context = instance.context();
1131
+ return {
1132
+ context,
1133
+ isCurrent: () => !instance.destroyed &&
1134
+ epoch === instance.documentEpoch &&
1135
+ context.documentId === instance.documentId &&
1136
+ context.generation === instance.generation,
1137
+ };
1138
+ },
1139
+ active: () => instance.ready &&
1140
+ !instance.destroyed &&
1141
+ !instance.literalSurface,
1142
+ labels: { ...defaultLabels, ...options.labels },
1143
+ error: (error) => events.error?.(error),
1144
+ }
1145
+ : undefined, () => {
1146
+ try {
1147
+ instance.assertMutation();
1148
+ return true;
1149
+ }
1150
+ catch {
1151
+ return false;
1152
+ }
1153
+ })
1154
+ : [])
1047
1155
  .create();
1048
1156
  configureTableMovement(instance.editor.ctx, () => {
1049
1157
  try {
@@ -1242,7 +1350,14 @@ export class InkKitEditor {
1242
1350
  }
1243
1351
  loadDocument(input) {
1244
1352
  this.assertAlive();
1353
+ const container = this.viewportContainer ?? this.root;
1354
+ const top = container.scrollTop, left = container.scrollLeft;
1355
+ const focused = this.literalSurface
1356
+ ? this.plain.ownerDocument.activeElement === this.plain
1357
+ : this.editor.ctx.get(editorViewCtx).hasFocus();
1245
1358
  this.ready = false;
1359
+ this.cancelExports();
1360
+ this.mediaPolicy();
1246
1361
  this.pasteController.cancelPending();
1247
1362
  this.documentEpoch += 1;
1248
1363
  this.outlineEpoch += 1;
@@ -1256,11 +1371,21 @@ export class InkKitEditor {
1256
1371
  this.formatType = input.format;
1257
1372
  this.revision = 0;
1258
1373
  this.plain.value = input.text;
1374
+ this.plain.setSelectionRange(this.plain.value.length, this.plain.value.length);
1259
1375
  this.load(input.format === 'txt' ? '' : input.text, input.generation);
1260
1376
  this.originalSource = input.text;
1261
1377
  this.lastMarkdown = input.text;
1262
1378
  this.updateSurface();
1263
1379
  this.ready = true;
1380
+ this.mediaPolicy();
1381
+ if (focused) {
1382
+ if (this.literalSurface)
1383
+ this.plain.focus({ preventScroll: true });
1384
+ else
1385
+ this.editor.ctx.get(editorViewCtx).focus();
1386
+ }
1387
+ container.scrollTop = top;
1388
+ container.scrollLeft = left;
1264
1389
  this.events.stateChanged(this.literalSurface
1265
1390
  ? { marks: [], block: { type: 'paragraph' }, quoted: false }
1266
1391
  : caretState(this.editor.ctx.get(editorViewCtx).state));
@@ -1272,11 +1397,19 @@ export class InkKitEditor {
1272
1397
  const wasLiteral = this.literalSurface;
1273
1398
  const previousMode = this.editingMode;
1274
1399
  const previousFormat = this.formatType;
1400
+ const selectedNode = !wasLiteral && view.state.selection instanceof NodeSelection
1401
+ ? view.state.selection.node
1402
+ : undefined;
1275
1403
  const at = wasLiteral
1276
1404
  ? this.plain.selectionStart
1277
- : view.state.selection.from;
1278
- const end = wasLiteral ? this.plain.selectionEnd : at;
1279
- const top = this.root.scrollTop;
1405
+ : view.state.selection.anchor;
1406
+ const end = wasLiteral ? this.plain.selectionEnd : view.state.selection.head;
1407
+ const direction = this.plain.selectionDirection;
1408
+ const container = this.viewportContainer ?? this.root;
1409
+ const top = container.scrollTop, left = container.scrollLeft;
1410
+ const focused = wasLiteral
1411
+ ? document.activeElement === this.plain
1412
+ : view.hasFocus();
1280
1413
  const plainTop = this.plain.scrollTop, plainLeft = this.plain.scrollLeft;
1281
1414
  this.reportingSuppressed = true;
1282
1415
  try {
@@ -1286,10 +1419,24 @@ export class InkKitEditor {
1286
1419
  previousMode === 'source')
1287
1420
  this.setEditingMode('source', input.generation);
1288
1421
  if (this.literalSurface)
1289
- this.plain.setSelectionRange(Math.min(at, this.plain.value.length), Math.min(end, this.plain.value.length));
1290
- else
1291
- view.dispatch(view.state.tr.setSelection(Selection.near(view.state.doc.resolve(Math.min(at, view.state.doc.content.size)))));
1292
- this.root.scrollTop = top;
1422
+ this.plain.setSelectionRange(Math.min(at, this.plain.value.length), Math.min(end, this.plain.value.length), direction);
1423
+ else {
1424
+ const anchor = Math.min(at, view.state.doc.content.size);
1425
+ const node = view.state.doc.nodeAt(anchor);
1426
+ view.dispatch(view.state.tr.setSelection(selectedNode &&
1427
+ node?.type === selectedNode.type &&
1428
+ NodeSelection.isSelectable(node)
1429
+ ? NodeSelection.create(view.state.doc, anchor)
1430
+ : TextSelection.between(view.state.doc.resolve(anchor), view.state.doc.resolve(Math.min(end, view.state.doc.content.size)))));
1431
+ }
1432
+ if (focused) {
1433
+ if (this.literalSurface)
1434
+ this.plain.focus({ preventScroll: true });
1435
+ else
1436
+ view.focus();
1437
+ }
1438
+ container.scrollTop = top;
1439
+ container.scrollLeft = left;
1293
1440
  if (wasLiteral && this.literalSurface) {
1294
1441
  this.plain.scrollTop = plainTop;
1295
1442
  this.plain.scrollLeft = plainLeft;
@@ -1300,6 +1447,301 @@ export class InkKitEditor {
1300
1447
  this.publishCommandState();
1301
1448
  }
1302
1449
  }
1450
+ setViewport(options) {
1451
+ this.assertAlive();
1452
+ const container = options.scrollContainer ?? this.root;
1453
+ if (container !== this.root && !container.contains(this.root))
1454
+ throw new RangeError('The scroll container must be the editor root or an ancestor');
1455
+ const insets = { top: 0, right: 0, bottom: 0, left: 0, ...options.insets };
1456
+ if (Object.values(insets).some((value) => !Number.isFinite(value) || value < 0))
1457
+ throw new RangeError('Viewport insets must be finite non-negative CSS pixel values');
1458
+ this.viewportContainer = container;
1459
+ this.viewportInsets = insets;
1460
+ }
1461
+ viewport() {
1462
+ this.assertAlive();
1463
+ const container = this.viewportContainer ?? this.root;
1464
+ const bounds = container.getBoundingClientRect();
1465
+ const root = this.root.getBoundingClientRect();
1466
+ const insets = this.viewportInsets;
1467
+ const window = this.root.ownerDocument.defaultView;
1468
+ const left = Math.max(bounds.left + container.clientLeft, root.left, 0);
1469
+ const top = Math.max(bounds.top + container.clientTop, root.top, 0);
1470
+ const right = Math.max(left, Math.min(bounds.left + container.clientLeft + container.clientWidth, root.right, window.innerWidth));
1471
+ const bottom = Math.max(top, Math.min(bounds.top + container.clientTop + container.clientHeight, root.bottom, window.innerHeight));
1472
+ let rect = textRect({ left, top, right, bottom });
1473
+ if (this.literalSurface) {
1474
+ const plain = this.plain.getBoundingClientRect();
1475
+ const literalLeft = Math.max(rect.left, plain.left + this.plain.clientLeft);
1476
+ const literalTop = Math.max(rect.top, plain.top + this.plain.clientTop);
1477
+ rect = textRect({
1478
+ left: literalLeft,
1479
+ top: literalTop,
1480
+ right: Math.max(literalLeft, Math.min(rect.right, plain.left + this.plain.clientLeft + this.plain.clientWidth)),
1481
+ bottom: Math.max(literalTop, Math.min(rect.bottom, plain.top + this.plain.clientTop + this.plain.clientHeight)),
1482
+ });
1483
+ }
1484
+ const insetLeft = Math.min(rect.right, rect.left + insets.left);
1485
+ const insetTop = Math.min(rect.bottom, rect.top + insets.top);
1486
+ rect = textRect({
1487
+ left: insetLeft,
1488
+ top: insetTop,
1489
+ right: Math.max(insetLeft, rect.right - insets.right),
1490
+ bottom: Math.max(insetTop, rect.bottom - insets.bottom),
1491
+ });
1492
+ return Object.freeze({
1493
+ rect: Object.freeze(rect),
1494
+ insets: Object.freeze({ ...insets }),
1495
+ scrollTop: container.scrollTop + (this.literalSurface ? this.plain.scrollTop : 0),
1496
+ scrollLeft: container.scrollLeft +
1497
+ (this.literalSurface ? this.plain.scrollLeft : 0),
1498
+ });
1499
+ }
1500
+ readableScope() {
1501
+ const key = `${this.documentEpoch}:${this.revision}:${this.outlineEpoch}:${this.editingMode}`;
1502
+ if (this.textScope?.key !== key)
1503
+ this.textScope = {
1504
+ key,
1505
+ id: `${this.textIdentity}:${key}`,
1506
+ projection: this.literalSurface
1507
+ ? { text: this.plain.value, spans: [] }
1508
+ : readableProjection(this.editor.ctx.get(editorViewCtx).state.doc),
1509
+ };
1510
+ return this.textScope;
1511
+ }
1512
+ textSnapshot(expectedGeneration) {
1513
+ this.assertCurrent(expectedGeneration);
1514
+ const scope = this.readableScope();
1515
+ const selection = this.editor.ctx.get(editorViewCtx).state.selection;
1516
+ return Object.freeze({
1517
+ snapshotId: scope.id,
1518
+ documentId: this.documentId,
1519
+ generation: this.generation,
1520
+ revision: this.revision,
1521
+ format: this.formatType,
1522
+ mode: this.editingMode,
1523
+ text: scope.projection.text,
1524
+ selection: Object.freeze({
1525
+ snapshotId: scope.id,
1526
+ from: this.literalSurface
1527
+ ? this.plain.selectionStart
1528
+ : projectionOffset(scope.projection, selection.from),
1529
+ to: this.literalSurface
1530
+ ? this.plain.selectionEnd
1531
+ : projectionOffset(scope.projection, selection.to),
1532
+ }),
1533
+ });
1534
+ }
1535
+ validateTextRange(range) {
1536
+ this.assertCurrent();
1537
+ const scope = this.readableScope();
1538
+ if (range.snapshotId !== scope.id)
1539
+ throw new InkKitError('stale-document', 'The readable text snapshot changed');
1540
+ validateOffsets(scope.projection.text, range.from, range.to);
1541
+ return scope.projection;
1542
+ }
1543
+ selectTextRange(range, options = {}) {
1544
+ const projection = this.validateTextRange(range);
1545
+ if (this.literalSurface)
1546
+ this.plain.setSelectionRange(range.from, range.to);
1547
+ else {
1548
+ for (const span of projection.spans)
1549
+ if (span.kind === 'embed' &&
1550
+ ((range.from > span.from && range.from < span.to) ||
1551
+ (range.to > span.from && range.to < span.to)))
1552
+ throw new InkKitError('invalid-range', 'Embedded labels must be selected as complete ranges');
1553
+ const view = this.editor.ctx.get(editorViewCtx);
1554
+ const from = projectionPosition(projection, range.from, 1);
1555
+ const to = projectionPosition(projection, range.to, range.from === range.to ? 1 : -1);
1556
+ const embed = projection.spans.find((span) => span.kind === 'embed' &&
1557
+ span.from === range.from &&
1558
+ span.to === range.to);
1559
+ view.dispatch(view.state.tr.setSelection(embed && view.state.doc.nodeAt(embed.start)
1560
+ ? NodeSelection.create(view.state.doc, embed.start)
1561
+ : TextSelection.between(view.state.doc.resolve(from), view.state.doc.resolve(to))));
1562
+ }
1563
+ if (options.focus) {
1564
+ if (this.literalSurface)
1565
+ this.plain.focus({ preventScroll: true });
1566
+ else
1567
+ this.editor.ctx.get(editorViewCtx).focus();
1568
+ }
1569
+ if (options.reveal)
1570
+ this.revealTextRange(range);
1571
+ this.publishCommandState();
1572
+ }
1573
+ replaceTextRange(range, text) {
1574
+ const projection = this.validateTextRange(range);
1575
+ this.assertMutation();
1576
+ if (typeof text !== 'string')
1577
+ throw new TypeError('Replacement must be a string');
1578
+ if (this.literalSurface) {
1579
+ const raw = this.currentText();
1580
+ const from = rawOffset(raw, range.from), to = rawOffset(raw, range.to);
1581
+ const ending = /\r\n|\r|\n/.exec(raw)?.[0] ?? '\n';
1582
+ const value = text.replace(/\r\n?/g, '\n');
1583
+ const replaced = raw.slice(0, from) + value.replaceAll('\n', ending) + raw.slice(to);
1584
+ return this.applySource(replaced, true, range.from, range.from + value.length);
1585
+ }
1586
+ if (projection.spans.some((span) => (span.kind === 'embed' &&
1587
+ range.from > span.from &&
1588
+ range.from < span.to) ||
1589
+ (span.kind === 'embed' &&
1590
+ range.to > span.from &&
1591
+ range.to < span.to) ||
1592
+ (span.to > range.from &&
1593
+ span.from < range.to &&
1594
+ span.kind !== 'text')))
1595
+ throw new InkKitError('invalid-range', 'Replacement cannot remove embedded content or structural separators');
1596
+ const from = projectionPosition(projection, range.from, 1);
1597
+ const to = projectionPosition(projection, range.to, range.from === range.to ? 1 : -1);
1598
+ return replaceFormattedRange(this.editor.ctx.get(editorViewCtx), from, to, text, (doc) => {
1599
+ this.currentText(doc);
1600
+ });
1601
+ }
1602
+ textRangeRects(range) {
1603
+ const projection = this.validateTextRange(range);
1604
+ const rects = this.literalSurface
1605
+ ? literalRects(this.plain, range.from, range.to)
1606
+ : formattedRects(this.editor.ctx.get(editorViewCtx), projection, range.from, range.to);
1607
+ return Object.freeze(rects.map((rect) => Object.freeze(rect)));
1608
+ }
1609
+ visibleTextRanges(snapshotId) {
1610
+ const snapshot = this.textSnapshot();
1611
+ this.validateTextRange({ snapshotId, from: 0, to: 0 });
1612
+ const viewport = this.viewport().rect;
1613
+ if (this.literalSurface)
1614
+ return Object.freeze(literalVisibleRanges(this.plain, viewport).map((range) => Object.freeze({ snapshotId, ...range })));
1615
+ const visible = (rect) => intersectsViewport(rect, viewport);
1616
+ const ranges = [];
1617
+ const projection = this.readableScope().projection;
1618
+ const view = this.editor.ctx.get(editorViewCtx);
1619
+ for (const span of projection.spans) {
1620
+ const current = { text: projection.text, spans: [span] };
1621
+ if (span.kind === 'separator' ||
1622
+ !formattedRects(view, current, span.from, span.to).some(visible))
1623
+ continue;
1624
+ for (let from = span.from; from < span.to;) {
1625
+ const to = from + (snapshot.text.codePointAt(from) > 0xffff ? 2 : 1);
1626
+ if (formattedRects(view, current, from, to).some(visible)) {
1627
+ const previous = ranges.at(-1);
1628
+ if (previous?.to === from)
1629
+ previous.to = to;
1630
+ else
1631
+ ranges.push({ snapshotId, from, to });
1632
+ }
1633
+ from = to;
1634
+ }
1635
+ }
1636
+ return Object.freeze(ranges.map((range) => Object.freeze(range)));
1637
+ }
1638
+ revealTextRange(range) {
1639
+ const projection = this.validateTextRange(range);
1640
+ if (!this.literalSurface) {
1641
+ const view = this.editor.ctx.get(editorViewCtx);
1642
+ const position = projectionPosition(projection, range.from, 1);
1643
+ const at = view.domAtPos(position).node;
1644
+ let element = at instanceof Element ? at : at.parentElement;
1645
+ while (element && this.root.contains(element)) {
1646
+ if (element.matches('.inkkit-callout[data-inkkit-folded="true"]'))
1647
+ element
1648
+ .querySelector(':scope > .inkkit-callout-header [data-inkkit-callout-toggle]')
1649
+ ?.click();
1650
+ element = element.parentElement;
1651
+ }
1652
+ }
1653
+ this.revealRects(this.textRangeRects(range));
1654
+ }
1655
+ revealRects(rects) {
1656
+ if (!rects.length)
1657
+ return;
1658
+ const viewport = this.viewport().rect;
1659
+ const leading = rects[0];
1660
+ const envelope = {
1661
+ top: leading.top,
1662
+ bottom: leading.bottom,
1663
+ left: leading.left,
1664
+ right: leading.right,
1665
+ };
1666
+ for (const rect of rects) {
1667
+ envelope.top = Math.min(envelope.top, rect.top);
1668
+ envelope.bottom = Math.max(envelope.bottom, rect.bottom);
1669
+ envelope.left = Math.min(envelope.left, rect.left);
1670
+ envelope.right = Math.max(envelope.right, rect.right);
1671
+ }
1672
+ const fitsHeight = envelope.bottom - envelope.top <= viewport.height;
1673
+ const fitsWidth = envelope.right - envelope.left <= viewport.width;
1674
+ const rect = {
1675
+ top: fitsHeight ? envelope.top : leading.top,
1676
+ bottom: fitsHeight
1677
+ ? envelope.bottom
1678
+ : Math.min(leading.bottom, leading.top + viewport.height),
1679
+ left: fitsWidth ? envelope.left : leading.left,
1680
+ right: fitsWidth
1681
+ ? envelope.right
1682
+ : Math.min(leading.right, leading.left + viewport.width),
1683
+ };
1684
+ const vertical = viewport.height <= 0
1685
+ ? 0
1686
+ : rect.top < viewport.top
1687
+ ? rect.top - viewport.top
1688
+ : rect.bottom > viewport.bottom
1689
+ ? rect.bottom - viewport.bottom
1690
+ : 0;
1691
+ const horizontal = viewport.width <= 0
1692
+ ? 0
1693
+ : rect.left < viewport.left
1694
+ ? rect.left - viewport.left
1695
+ : rect.right > viewport.right
1696
+ ? rect.right - viewport.right
1697
+ : 0;
1698
+ let remainingTop = vertical, remainingLeft = horizontal;
1699
+ if (this.literalSurface) {
1700
+ const top = this.plain.scrollTop, left = this.plain.scrollLeft;
1701
+ this.plain.scrollTop += vertical;
1702
+ this.plain.scrollLeft += horizontal;
1703
+ remainingTop -= this.plain.scrollTop - top;
1704
+ remainingLeft -= this.plain.scrollLeft - left;
1705
+ }
1706
+ let container = this.viewportContainer ?? this.root;
1707
+ const document = this.root.ownerDocument;
1708
+ while (container &&
1709
+ container !== document.body &&
1710
+ container !== document.documentElement) {
1711
+ const top = container.scrollTop, left = container.scrollLeft;
1712
+ container.scrollTop += remainingTop;
1713
+ container.scrollLeft += remainingLeft;
1714
+ remainingTop -= container.scrollTop - top;
1715
+ remainingLeft -= container.scrollLeft - left;
1716
+ container = container.parentElement;
1717
+ }
1718
+ if (remainingTop || remainingLeft)
1719
+ document.defaultView.scrollBy(remainingLeft, remainingTop);
1720
+ }
1721
+ revealCurrentSelection() {
1722
+ if (this.loading || !this.ready)
1723
+ return;
1724
+ if (this.literalSurface) {
1725
+ this.revealLiteralSelection();
1726
+ return;
1727
+ }
1728
+ const view = this.editor.ctx.get(editorViewCtx);
1729
+ const projection = this.readableScope().projection;
1730
+ const from = projectionOffset(projection, view.state.selection.from);
1731
+ const to = projectionOffset(projection, view.state.selection.to);
1732
+ if (this.composing || this.pasteController.pending) {
1733
+ this.revealRects(formattedRects(view, projection, from, to));
1734
+ return;
1735
+ }
1736
+ this.revealTextRange({
1737
+ snapshotId: this.readableScope().id,
1738
+ from,
1739
+ to,
1740
+ });
1741
+ }
1742
+ revealLiteralSelection() {
1743
+ this.revealRects(literalRects(this.plain, this.plain.selectionStart, this.plain.selectionEnd));
1744
+ }
1303
1745
  load(markdown, generation) {
1304
1746
  this.generation = generation;
1305
1747
  this.originalSource = markdown;
@@ -1319,7 +1761,7 @@ export class InkKitEditor {
1319
1761
  const { doc } = view.state;
1320
1762
  const end = Selection.findFrom(doc.resolve(doc.content.size), -1, true) ??
1321
1763
  Selection.atEnd(doc);
1322
- view.dispatch(setSearchState(view.state.tr.setSelection(end).scrollIntoView(), new SearchQuery({ search: '' })));
1764
+ view.dispatch(setSearchState(view.state.tr.setSelection(end), new SearchQuery({ search: '' })));
1323
1765
  }
1324
1766
  find(text, expectedGeneration) {
1325
1767
  this.assertCurrent(expectedGeneration);
@@ -1349,10 +1791,11 @@ export class InkKitEditor {
1349
1791
  else if (this.plainSearch)
1350
1792
  this.plain.setSelectionRange(this.plain.selectionEnd, this.plain.selectionEnd);
1351
1793
  this.plainSearch = text;
1352
- revealSourceCaret(this.plain);
1794
+ this.revealLiteralSelection();
1353
1795
  return;
1354
1796
  }
1355
- findFormatted(this.editor.ctx.get(editorViewCtx), text);
1797
+ if (findFormatted(this.editor.ctx.get(editorViewCtx), text))
1798
+ this.revealCurrentSelection();
1356
1799
  }
1357
1800
  replace(search, replacement, expectedGeneration) {
1358
1801
  this.assertMutation(expectedGeneration);
@@ -1417,7 +1860,7 @@ export class InkKitEditor {
1417
1860
  const position = sourceHeadingPosition(this.editor.ctx, this.plain.value, heading, this.outlineContext());
1418
1861
  this.plain.setSelectionRange(position, position);
1419
1862
  this.plain.focus({ preventScroll: true });
1420
- revealSourceCaret(this.plain);
1863
+ this.revealLiteralSelection();
1421
1864
  return true;
1422
1865
  }
1423
1866
  return navigateFormattedHeading(this.editor.ctx.get(editorViewCtx), heading, this.outlineContext());