@aicayzer/inkkit 0.0.13 → 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 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, literalRects, literalVisibleRanges, intersectsViewport, textRect, } from './text-ranges.js';
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();
@@ -1618,11 +1636,17 @@ export class InkKitEditor {
1618
1636
  });
1619
1637
  }
1620
1638
  textRangeRects(range) {
1621
- const projection = this.validateTextRange(range);
1622
- const rects = this.literalSurface
1623
- ? literalRects(this.plain, range.from, range.to)
1624
- : formattedRects(this.editor.ctx.get(editorViewCtx), projection, range.from, range.to);
1625
- return Object.freeze(rects.map((rect) => Object.freeze(rect)));
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)))));
1626
1650
  }
1627
1651
  visibleTextRanges(snapshotId) {
1628
1652
  const snapshot = this.textSnapshot();
@@ -1758,7 +1782,12 @@ export class InkKitEditor {
1758
1782
  });
1759
1783
  }
1760
1784
  revealLiteralSelection() {
1761
- this.revealRects(literalRects(this.plain, this.plain.selectionStart, this.plain.selectionEnd));
1785
+ this.revealRects(literalRangesRects(this.plain, [
1786
+ {
1787
+ from: this.plain.selectionStart,
1788
+ to: this.plain.selectionEnd,
1789
+ },
1790
+ ])[0]);
1762
1791
  }
1763
1792
  load(markdown, generation) {
1764
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 = status ? `${label}: ${status}` : label;
149
- if (status)
150
- text.setAttribute('role', 'status');
151
- this.dom.append(text);
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
- this.dom.append(this.button(this.runtime.labels.fileRetry, () => this.resolution?.retry()));
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
- this.dom.append(open);
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
- this.dom.append(image, handle);
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: new PluginKey('inkkitPaste'),
20
+ key: this.key,
20
21
  state: {
21
22
  init: () => null,
22
- apply: (transaction) => {
23
- if (this.active) {
24
- const empty = this.active.from === this.active.to;
25
- this.active.from = transaction.mapping.map(this.active.from, 1);
26
- this.active.to = transaction.mapping.map(this.active.to, empty ? 1 : -1);
27
- }
28
- return null;
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, title: '' });
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))
@@ -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
- if (rows * columns > 1_000_000)
341
- throw new PreservationError('The edit changes too many blocks to preserve safely');
342
- const costs = Array.from({ length: rows }, () => new Uint32Array(columns));
343
- for (let i = 0; i < rows; i++)
344
- costs[i][0] = i;
345
- for (let j = 0; j < columns; j++)
346
- costs[0][j] = j;
347
- for (let i = 1; i < rows; i++) {
348
- for (let j = 1; j < columns; j++) {
349
- const change = before[head + i - 1] === after[head + j - 1] ? 0 : 1;
350
- costs[i][j] = Math.min(costs[i - 1][j] + 1, costs[i][j - 1] + 1, costs[i - 1][j - 1] + change);
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
- let i = rows - 1;
354
- let j = columns - 1;
355
- while (i || j) {
356
- const change = i && j && before[head + i - 1] === after[head + j - 1] ? 0 : 1;
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
- else if (i && costs[i][j] === costs[i - 1][j] + 1)
363
- i--;
364
- else
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 += retainSource(block.ast, positionalNode(currentRawTree, currentTree.children[index]), body, canonical, this.lineEnding);
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
- if (serialize(this.ctx, reopened).replace(/\r\n?/g, '\n') ===
481
- canonical.replace(/\r\n?/g, '\n') &&
482
- semanticSignature(reopened) === semanticSignature(doc))
483
- return output;
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: -3px;
217
- top: 0;
218
- width: 10px;
219
- height: 100%;
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: 3px;
231
- top: 25%;
232
- height: 50%;
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: 4px;
242
- padding: 4px;
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
- margin-inline-start: 0.5em;
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);
@@ -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 literalRects(plain: HTMLTextAreaElement, from: number, to: number): TextRect[];
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;
@@ -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
- for (const span of projection.spans) {
114
- if (span.to <= from || span.from >= to || span.kind === 'separator')
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 literalRects(plain, from, to) {
195
+ export function literalRangesRects(plain, ranges) {
184
196
  return (withLiteralLayout(plain, (text) => {
185
197
  const range = plain.ownerDocument.createRange();
186
- range.setStart(text, from);
187
- range.setEnd(text, from === to ? Math.min(to + 1, text.length) : to);
188
- return Array.from(range.getClientRects(), (rect) => textRect({
189
- left: rect.left,
190
- right: from === to ? rect.left : rect.right,
191
- top: rect.top,
192
- bottom: rect.bottom,
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 &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicayzer/inkkit",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "A preserving Markdown editor for offline web views and the web",
5
5
  "license": "MIT",
6
6
  "type": "module",