@aicayzer/inkkit 0.6.1 → 0.7.0

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.
@@ -28448,6 +28448,7 @@ var editorErrorCodes = [
28448
28448
  "invalid-range",
28449
28449
  "preservation",
28450
28450
  "image-unavailable",
28451
+ "image-unsupported",
28451
28452
  "diagram-unavailable",
28452
28453
  "file-unavailable",
28453
28454
  "clipboard-unavailable",
@@ -33178,6 +33179,13 @@ A command function that redoes the last undone change, if any.
33178
33179
  */
33179
33180
  var redo = buildCommand(true, true);
33180
33181
  /**
33182
+ The amount of undoable events available in a given state.
33183
+ */
33184
+ function undoDepth(state) {
33185
+ let hist = historyKey.getState(state);
33186
+ return hist ? hist.done.eventCount : 0;
33187
+ }
33188
+ /**
33181
33189
  The amount of redoable events available in a given editor state.
33182
33190
  */
33183
33191
  function redoDepth(state) {
@@ -40553,7 +40561,7 @@ function parseLinkedSyntax(raw, inTable = false) {
40553
40561
  let section = "target", hasFragment = false, hasAlias = false;
40554
40562
  for (let index = 0; index < content.length; index++) {
40555
40563
  const character = content[index];
40556
- if (character === "\\") {
40564
+ if (character === "\\" && embed) {
40557
40565
  const escaped = content[++index];
40558
40566
  if (!escaped || !"[]|#\\".includes(escaped)) return void 0;
40559
40567
  parts[section] += escaped;
@@ -40604,7 +40612,7 @@ function linkedTokenizer(embed) {
40604
40612
  function body(code) {
40605
40613
  if (code === null || code < 32 && code !== -2 && code !== -1 && code !== 9 || code === 91) return nok(code);
40606
40614
  consume(code);
40607
- if (code === 92) return escaped;
40615
+ if (code === 92 && embed) return escaped;
40608
40616
  return code === 93 ? closing : body;
40609
40617
  }
40610
40618
  function escaped(code) {
@@ -40621,6 +40629,39 @@ function linkedTokenizer(embed) {
40621
40629
  }
40622
40630
  };
40623
40631
  }
40632
+ /**
40633
+ * Looks ahead from a backslash for a wiki link that starts right after it. CommonMark reads `\[`
40634
+ * as an escaped bracket, which would hide the link, but a backslash before a wiki link is only
40635
+ * text, as it is inside one.
40636
+ */
40637
+ var backslashBeforeWikiLink = {
40638
+ name: "inkkitWikiBackslash",
40639
+ tokenize(effects, ok, nok) {
40640
+ return function start(code) {
40641
+ if (code !== 92) return nok(code);
40642
+ return effects.check(wikiLinkAhead, literal, nok)(code);
40643
+ };
40644
+ function literal(code) {
40645
+ effects.enter("data");
40646
+ effects.consume(code);
40647
+ effects.exit("data");
40648
+ return ok;
40649
+ }
40650
+ }
40651
+ };
40652
+ var wikiLinkAhead = {
40653
+ partial: true,
40654
+ tokenize(effects, ok, nok) {
40655
+ const link = linkedTokenizer(false).call(this, effects, ok, nok);
40656
+ return function start(code) {
40657
+ if (code !== 92) return nok(code);
40658
+ effects.enter("data");
40659
+ effects.consume(code);
40660
+ effects.exit("data");
40661
+ return link;
40662
+ };
40663
+ }
40664
+ };
40624
40665
  function tableForm(raw, authoredInTable, outputInTable) {
40625
40666
  if (authoredInTable === outputInTable) return raw;
40626
40667
  return outputInTable ? toTable(raw) : fromTable(raw);
@@ -40872,14 +40913,14 @@ var referenceTableContext = $prose(() => new Plugin({
40872
40913
  * brackets, is never changed, and that includes the run of every rule when a
40873
40914
  * composition ends.
40874
40915
  */
40875
- var wikiLinkInputRule = $inputRule(() => new InputRule(/\[\[(?:\\.|[^[\]\\\ufffc])*\]\]$/, (state, match, start, end) => {
40916
+ var wikiLinkInputRule = $inputRule(() => new InputRule(/\[\[[^[\]\ufffc]*\]\]$/, (state, match, start, end) => {
40876
40917
  const { schema, doc } = state;
40877
40918
  const type = schema.nodes.inkkit_wiki_link;
40878
40919
  const caret = state.selection.from;
40879
40920
  if (start + match[0].length - 1 !== caret && !(start + match[0].length === caret && composed.getState(state)?.closing === caret)) return null;
40880
40921
  const $start = doc.resolve(start);
40881
40922
  const parent = $start.parent;
40882
- if (!type || !parent.inlineContent || !parent.canReplaceWith($start.index(), $start.index(), type) || /[!\\[]/.test(doc.textBetween(Math.max($start.start(), start - 1), start))) return null;
40923
+ if (!type || !parent.inlineContent || !parent.canReplaceWith($start.index(), $start.index(), type) || /[![]/.test(doc.textBetween(Math.max($start.start(), start - 1), start))) return null;
40883
40924
  let marks;
40884
40925
  let plain = true;
40885
40926
  doc.nodesBetween(start, end, (node) => {
@@ -40908,6 +40949,7 @@ function linkedSyntax(wikiLinks, files) {
40908
40949
  tokenize: linkedTokenizer(false),
40909
40950
  previous: (code) => code !== 33 && code !== 91
40910
40951
  };
40952
+ if (wikiLinks) syntax.text[92] = backslashBeforeWikiLink;
40911
40953
  if (files) syntax.text[33] = {
40912
40954
  name: "inkkitNamedFile",
40913
40955
  tokenize: linkedTokenizer(true)
@@ -40967,7 +41009,7 @@ function linkedSyntax(wikiLinks, files) {
40967
41009
  //#endregion
40968
41010
  //#region src/wiki-suggestions.ts
40969
41011
  function rawName(name) {
40970
- return `[[${name.replace(/[\\[\]|#]/g, "\\$&")}]]`;
41012
+ return `[[${name}]]`;
40971
41013
  }
40972
41014
  function validName(value) {
40973
41015
  if (typeof value !== "string") return false;
@@ -46968,12 +47010,29 @@ function caretState(state) {
46968
47010
  const type = state.schema.marks[name];
46969
47011
  if (type && state.doc.rangeHasMark($from.pos, $to.pos, type)) active.add(mark);
46970
47012
  }
47013
+ const href = active.has("link") ? linkAddress(state, marks) : void 0;
46971
47014
  return {
46972
47015
  marks: [...active],
46973
47016
  block: blockAt($from.parent, $from),
46974
- quoted: quoted($from, $to)
47017
+ quoted: quoted($from, $to),
47018
+ ...href != null ? { href } : {}
46975
47019
  };
46976
47020
  }
47021
+ /** The address of the first link at the caret, or in the selection, in document order. */
47022
+ function linkAddress(state, caretMarks) {
47023
+ const type = state.schema.marks.link;
47024
+ if (!type) return void 0;
47025
+ const { empty } = state.selection;
47026
+ if (empty) return type.isInSet(caretMarks)?.attrs.href;
47027
+ const { $from, $to } = textBounds(state);
47028
+ let href;
47029
+ state.doc.nodesBetween($from.pos, $to.pos, (node) => {
47030
+ if (href != null) return false;
47031
+ href = type.isInSet(node.marks)?.attrs.href;
47032
+ return true;
47033
+ });
47034
+ return href;
47035
+ }
46977
47036
  function terminalSelection(doc) {
46978
47037
  const $end = doc.resolve(doc.content.size);
46979
47038
  const gap = GapCursor;
@@ -53333,6 +53392,29 @@ var EditorDocument = class {
53333
53392
  //#region src/editor-history.ts
53334
53393
  var sourceGroupDelay = 500;
53335
53394
  var sourceStepLimit = 100;
53395
+ var isHistoryState = (value) => typeof value === "object" && value !== null && "done" in value && "undone" in value && typeof value.undone?.constructor?.empty === "object";
53396
+ /**
53397
+ * Empties the formatted document's redo stack and keeps every undo step. ProseMirror's history
53398
+ * clears the stack only when an edit lands, and offers no call for it, so this replaces the
53399
+ * plugin's state through the plugin's own transaction meta. It checks the result against
53400
+ * `undoDepth` and `redoDepth` before it changes anything, so a ProseMirror update that changes
53401
+ * the state's shape fails here, in `test/discard-undone.test.ts`, and not silently.
53402
+ */
53403
+ function discardFormattedRedo(view) {
53404
+ const { state } = view;
53405
+ if (redoDepth(state) === 0) return;
53406
+ const plugin = state.plugins.find((candidate) => /^history\$/.test(candidate.spec.key?.key ?? "") && isHistoryState(candidate.getState(state)));
53407
+ const previous = plugin?.getState(state);
53408
+ if (!plugin || !previous) throw new InkKitError("internal", "The undo history could not be found");
53409
+ const historyState = Object.assign(Object.create(Object.getPrototypeOf(previous)), previous, { undone: previous.undone.constructor.empty });
53410
+ const tr = state.tr.setMeta(plugin.spec.key, {
53411
+ redo: false,
53412
+ historyState
53413
+ });
53414
+ const next = state.apply(tr);
53415
+ if (redoDepth(next) !== 0 || undoDepth(next) !== undoDepth(state)) throw new InkKitError("internal", "The redo steps could not be discarded");
53416
+ view.dispatch(tr);
53417
+ }
53336
53418
  var SourceHistory = class {
53337
53419
  runtime;
53338
53420
  pendingSource;
@@ -53363,6 +53445,11 @@ var SourceHistory = class {
53363
53445
  this.undoneSteps = [];
53364
53446
  this.formattedUndone = 0;
53365
53447
  }
53448
+ /** Drops the redo steps of typed source, and the count of formatted ones that wait behind them. */
53449
+ discardUndone() {
53450
+ this.undoneSteps = [];
53451
+ this.formattedUndone = 0;
53452
+ }
53366
53453
  canUndo(state) {
53367
53454
  return !!this.pendingSource || undo(state);
53368
53455
  }
@@ -57281,7 +57368,7 @@ async function createMilkdownEditor(root, options, runtime) {
57281
57368
  return true;
57282
57369
  }
57283
57370
  }));
57284
- }).use(labelsCtx).use(linkSchemesCtx).use(formatInputCtx).use(richTextGuard).use(runtime.plugins.paste).use(visibleClipboard).use(runtime.plugins.suggestions).use(runtime.plugins.keymap).use(createDialect(Boolean(options.images || options.files), Boolean(options.wikiLinks), Boolean(options.files))).use(codeControlCtx).use(codeControlsPlugin(() => runtime.visibleRect())).use(tablePlugins).use(tableBlock).use(runtime.plugins.change).use(runtime.plugins.historyInput).use(history).use(clipboard).use(cursor).use(taskListPlugin((operation) => runtime.mutate(operation))).use(undoTypedRule).use(quoteBackspace).use(calloutEnter).use(literalEnter).use(literalReread).use(listItemBackspace).use(nestedItemEnter).use(rejoinLists).use(movedListItems).use(textInputPolicy).use(runtime.copy ? codeCopyPlugin((text) => runtime.copy?.(text)) : []).use(placeholderPlugin).use(options.rendering?.codeHighlighting === false ? [] : (await import("./highlight-C-9lbVMU.js")).highlightPlugin).use(options.rendering?.diagramPreview === false || !diagramsAvailable() ? [] : mermaidPreview(runtime.diagrams, (error) => runtime.report(error))).use(selectionPlugin).use($prose(() => search())).use(runtime.plugins.policy).use(options.wikiLinks ? wikiLinkView(options.wikiLinks, {
57371
+ }).use(labelsCtx).use(linkSchemesCtx).use(formatInputCtx).use(richTextGuard).use(runtime.plugins.paste).use(visibleClipboard).use(runtime.plugins.suggestions).use(runtime.plugins.keymap).use(createDialect(Boolean(options.images || options.files), Boolean(options.wikiLinks), Boolean(options.files))).use(codeControlCtx).use(codeControlsPlugin(() => runtime.visibleRect())).use(tablePlugins).use(tableBlock).use(runtime.plugins.change).use(runtime.plugins.historyInput).use(history).use(clipboard).use(cursor).use(taskListPlugin((operation) => runtime.mutate(operation))).use(undoTypedRule).use(quoteBackspace).use(calloutEnter).use(literalEnter).use(literalReread).use(listItemBackspace).use(nestedItemEnter).use(rejoinLists).use(movedListItems).use(textInputPolicy).use(runtime.copy ? codeCopyPlugin((text) => runtime.copy?.(text)) : []).use(placeholderPlugin).use(options.rendering?.codeHighlighting === false ? [] : (await import("./highlight-B_juJyDH.js")).highlightPlugin).use(options.rendering?.diagramPreview === false || !diagramsAvailable() ? [] : mermaidPreview(runtime.diagrams, (error) => runtime.report(error))).use(selectionPlugin).use($prose(() => search())).use(runtime.plugins.policy).use(options.wikiLinks ? wikiLinkView(options.wikiLinks, {
57285
57372
  capture: () => runtime.capture(),
57286
57373
  active: () => runtime.active(),
57287
57374
  labels: {
@@ -58840,6 +58927,11 @@ var PasteController = class {
58840
58927
  },
58841
58928
  drop: (view, event) => {
58842
58929
  const data = event.dataTransfer;
58930
+ if (!this.options.adapter && !this.options.files && !data?.getData("text/html") && view.editable && !view.dragging && imageFiles(data?.files ?? []).length) {
58931
+ event.preventDefault();
58932
+ options.onError?.(imagesRefused());
58933
+ return true;
58934
+ }
58843
58935
  if (!data?.getData("text/html") || !view.editable || view.dragging) return false;
58844
58936
  event.preventDefault();
58845
58937
  const target = blockDropRange(view, event.clientX, event.clientY);
@@ -58848,7 +58940,7 @@ var PasteController = class {
58848
58940
  text: data.getData("text/plain"),
58849
58941
  html: data.getData("text/html"),
58850
58942
  plainText: this.options.literalText?.() ?? false
58851
- }, [...data.files].filter((file) => file.type.startsWith("image/")), target).catch((error) => options.onError?.(error));
58943
+ }, imageFiles(data.files), target).catch((error) => options.onError?.(error));
58852
58944
  return true;
58853
58945
  }
58854
58946
  },
@@ -58875,7 +58967,7 @@ var PasteController = class {
58875
58967
  }).catch((error) => options.onError?.(error));
58876
58968
  return true;
58877
58969
  }
58878
- const files = [...data.files].filter((file) => file.type.startsWith("image/"));
58970
+ const files = imageFiles(data.files);
58879
58971
  this.run({
58880
58972
  text,
58881
58973
  html,
@@ -58969,6 +59061,7 @@ var PasteController = class {
58969
59061
  };
58970
59062
  const rich = this.options.richText?.();
58971
59063
  if (rich) {
59064
+ if (!this.options.adapter && (files.length || input.images?.length)) throw imagesRefused();
58972
59065
  const anchor = operation;
58973
59066
  const state = view.state;
58974
59067
  const slice = input.plainText ? rich.paste({ text: input.text }, state, this.options.linkSchemes ?? /* @__PURE__ */ new Set()) : rich.paste(input, state, this.options.linkSchemes ?? /* @__PURE__ */ new Set());
@@ -58991,6 +59084,12 @@ var PasteController = class {
58991
59084
  this.assertContext(context, operation);
58992
59085
  const schema = view.state.schema;
58993
59086
  let slice;
59087
+ const placeholders = [];
59088
+ const placeholder = (name) => {
59089
+ const text = `[${name}: image unavailable]`;
59090
+ placeholders.push(text);
59091
+ return text;
59092
+ };
58994
59093
  let source;
58995
59094
  const plainText = input.plainText || this.options.literalText?.() || inCode();
58996
59095
  if (!plainText && input.markdown == null && this.commit(() => pasteSpreadsheet(ctx, {
@@ -59032,8 +59131,8 @@ var PasteController = class {
59032
59131
  element.removeAttribute("srcset");
59033
59132
  } catch (error) {
59034
59133
  if (error instanceof InkKitError && error.code !== "image-unavailable") throw error;
59035
- element.replaceWith(document.createTextNode(`[${alt}: image unavailable]`));
59036
- this.options.onError?.(coded(error, "image-unavailable", error instanceof Error ? error.message : "The pasted image is unavailable."));
59134
+ element.replaceWith(document.createTextNode(placeholder(alt)));
59135
+ if (this.options.adapter) this.options.onError?.(coded(error, "image-unavailable", error instanceof Error ? error.message : "The pasted image is unavailable."));
59037
59136
  }
59038
59137
  }
59039
59138
  let imageIndex = 0;
@@ -59043,7 +59142,7 @@ var PasteController = class {
59043
59142
  const position = slot ? Number(slot[1]) : imageIndex++;
59044
59143
  const element = elements.some((element) => element.hasAttribute("data-inkkit-image-slot")) ? elements.find((element) => Number(element.getAttribute("data-inkkit-authored-image-slot") ?? element.getAttribute("data-inkkit-image-slot")) === position) : elements[position];
59045
59144
  if (!element || !container.contains(element)) {
59046
- const text = schema.text(`[${node.attrs.alt || "Image"}: image unavailable]`);
59145
+ const text = schema.text(placeholder(node.attrs.alt || "Image"));
59047
59146
  return node.isBlock ? schema.nodes.paragraph.create(null, text) : text;
59048
59147
  }
59049
59148
  return node.type.create({
@@ -59072,6 +59171,7 @@ var PasteController = class {
59072
59171
  }
59073
59172
  }
59074
59173
  } else if (images.length) {
59174
+ if (!this.options.adapter) throw imagesRefused();
59075
59175
  const nodes = [];
59076
59176
  if (input.text) nodes.push(schema.nodes.paragraph.create(null, schema.text(input.text)));
59077
59177
  for (const image of images) try {
@@ -59086,7 +59186,7 @@ var PasteController = class {
59086
59186
  nodes.push(schema.nodes.paragraph.create(null, imageNode));
59087
59187
  } catch (error) {
59088
59188
  if (error instanceof InkKitError && error.code !== "image-unavailable") throw error;
59089
- nodes.push(schema.nodes.paragraph.create(null, schema.text(`[${image.filename ?? "Image"}: image unavailable]`)));
59189
+ nodes.push(schema.nodes.paragraph.create(null, schema.text(placeholder(image.filename ?? "Image"))));
59090
59190
  this.options.onError?.(coded(error, "image-unavailable", error instanceof Error ? error.message : "The pasted image is unavailable."));
59091
59191
  }
59092
59192
  slice = new Slice(Fragment.fromArray(nodes), 1, 1);
@@ -59103,6 +59203,10 @@ var PasteController = class {
59103
59203
  source = markdown ?? input.text;
59104
59204
  slice = joinable(ctx.get(parserCtx)(source).content);
59105
59205
  }
59206
+ if (!this.options.adapter && placeholders.length) {
59207
+ const shown = slice.content.textBetween(0, slice.content.size, "\n", "\n");
59208
+ if (placeholders.some((text) => shown.includes(text))) throw imagesRefused();
59209
+ }
59106
59210
  this.assertContext(context, operation);
59107
59211
  if (!view.editable || this.options.editable?.() === false) return;
59108
59212
  const anchor = this.anchor(operation);
@@ -59368,6 +59472,12 @@ function dropRange(doc, range) {
59368
59472
  * keeps the provider's code-language behaviour; invalid metadata pastes plain
59369
59473
  * text.
59370
59474
  */
59475
+ function imageFiles(files) {
59476
+ return [...files].filter((file) => file.type.startsWith("image/"));
59477
+ }
59478
+ function imagesRefused() {
59479
+ return new InkKitError("image-unsupported", "The images were not inserted because no image adapter is configured.");
59480
+ }
59371
59481
  function claimsPaste(view, event) {
59372
59482
  const data = event.clipboardData;
59373
59483
  if (!data || !view.editable) return false;
@@ -59425,6 +59535,210 @@ function dataImage(source) {
59425
59535
  };
59426
59536
  }
59427
59537
  //#endregion
59538
+ //#region src/find-session.ts
59539
+ /** The codes of failures that mean the editor cannot answer now, not that the session is wrong. */
59540
+ var transient = [
59541
+ "not-ready",
59542
+ "stale-document",
59543
+ "composition",
59544
+ "operation-pending"
59545
+ ];
59546
+ /** The editor refreshes each open session after every edit, selection change, load and mode change. */
59547
+ var EditorFindSession = class {
59548
+ host;
59549
+ onChange;
59550
+ detach;
59551
+ text = "";
59552
+ reported = "";
59553
+ closed = false;
59554
+ busy = false;
59555
+ cached;
59556
+ constructor(host, onChange, detach) {
59557
+ this.host = host;
59558
+ this.onChange = onChange;
59559
+ this.detach = detach;
59560
+ }
59561
+ state(expectedGeneration) {
59562
+ this.assertOpen();
59563
+ return this.measure(expectedGeneration);
59564
+ }
59565
+ find(query, options, expectedGeneration) {
59566
+ this.assertOpen();
59567
+ return this.command(expectedGeneration, () => this.select(query, options?.direction, expectedGeneration));
59568
+ }
59569
+ next(expectedGeneration) {
59570
+ this.assertOpen();
59571
+ return this.command(expectedGeneration, () => this.select(this.text, "next", expectedGeneration));
59572
+ }
59573
+ previous(expectedGeneration) {
59574
+ this.assertOpen();
59575
+ return this.command(expectedGeneration, () => this.select(this.text, "previous", expectedGeneration));
59576
+ }
59577
+ replace(query, replacement, expectedGeneration) {
59578
+ this.assertOpen();
59579
+ return this.command(expectedGeneration, () => {
59580
+ const changed = this.restoring(expectedGeneration, () => {
59581
+ this.select(query, void 0, expectedGeneration);
59582
+ return this.host.replace(query, replacement, expectedGeneration);
59583
+ });
59584
+ const snapshot = this.host.textSnapshot(expectedGeneration);
59585
+ const matches = this.matches(query, snapshot.snapshotId, expectedGeneration);
59586
+ const following = matches.find((range) => range.from >= snapshot.selection.to) ?? matches[0];
59587
+ if (following) this.host.selectTextRange(following, { reveal: true });
59588
+ return changed;
59589
+ });
59590
+ }
59591
+ replaceAll(query, replacement, expectedGeneration) {
59592
+ this.assertOpen();
59593
+ return this.command(expectedGeneration, () => this.restoring(expectedGeneration, () => {
59594
+ this.select(query, void 0, expectedGeneration);
59595
+ return this.host.replaceAll(query, replacement, expectedGeneration);
59596
+ }));
59597
+ }
59598
+ clear(expectedGeneration) {
59599
+ this.assertOpen();
59600
+ this.command(expectedGeneration, () => {
59601
+ this.text = "";
59602
+ this.host.find("", expectedGeneration);
59603
+ });
59604
+ }
59605
+ close() {
59606
+ if (this.closed) return;
59607
+ this.closed = true;
59608
+ this.detach(this);
59609
+ if (!this.text) return;
59610
+ try {
59611
+ this.host.find("");
59612
+ } catch (error) {
59613
+ if (!(error instanceof InkKitError) || !transient.includes(error.code)) throw error;
59614
+ }
59615
+ }
59616
+ /** Reports the state if it changed. The editor calls this after each of its own updates; nothing
59617
+ * it cannot answer now is reported. */
59618
+ refresh(expectedGeneration) {
59619
+ if (this.closed || this.busy) return;
59620
+ let state;
59621
+ try {
59622
+ state = this.measure(expectedGeneration);
59623
+ } catch (error) {
59624
+ if (error instanceof InkKitError && transient.includes(error.code)) return;
59625
+ throw error;
59626
+ }
59627
+ const key = JSON.stringify([
59628
+ state.documentId,
59629
+ state.generation,
59630
+ state.query,
59631
+ state.current,
59632
+ state.total
59633
+ ]);
59634
+ if (key === this.reported) return;
59635
+ this.reported = key;
59636
+ this.onChange(state);
59637
+ }
59638
+ /** Forgets the query after a new document loads. */
59639
+ reset() {
59640
+ this.text = "";
59641
+ this.cached = void 0;
59642
+ }
59643
+ /** Highlights the query again where the editor dropped its highlights, such as after a reload or
59644
+ * a mode change, and leaves the selection where it was. */
59645
+ reapply() {
59646
+ if (this.closed) return;
59647
+ this.cached = void 0;
59648
+ if (!this.text) return;
59649
+ this.busy = true;
59650
+ try {
59651
+ const { selection } = this.host.textSnapshot();
59652
+ this.host.find(this.text);
59653
+ this.host.selectTextRange(selection);
59654
+ } catch (error) {
59655
+ if (!(error instanceof InkKitError) || !transient.includes(error.code)) throw error;
59656
+ } finally {
59657
+ this.busy = false;
59658
+ }
59659
+ }
59660
+ assertOpen() {
59661
+ if (this.closed) throw new InkKitError("destroyed", "The find session was closed");
59662
+ }
59663
+ /** Runs a command without reporting the editor's intermediate states, then reports the result. */
59664
+ command(expectedGeneration, run) {
59665
+ this.busy = true;
59666
+ let failed = true;
59667
+ try {
59668
+ const result = run();
59669
+ failed = false;
59670
+ return result;
59671
+ } finally {
59672
+ this.busy = false;
59673
+ if (!failed) this.refresh(expectedGeneration);
59674
+ else try {
59675
+ this.refresh(expectedGeneration);
59676
+ } catch {}
59677
+ }
59678
+ }
59679
+ select(query, direction, expectedGeneration) {
59680
+ const snapshot = this.host.textSnapshot(expectedGeneration);
59681
+ const matches = this.matches(query, snapshot.snapshotId, expectedGeneration);
59682
+ const changedQuery = query !== this.text;
59683
+ let index = matches.findIndex((range) => sameRange(range, snapshot.selection));
59684
+ if (changedQuery || index < 0) {
59685
+ if (direction === "previous") {
59686
+ index = -1;
59687
+ for (let at = matches.length - 1; at >= 0; at -= 1) if (matches[at].to <= snapshot.selection.from) {
59688
+ index = at;
59689
+ break;
59690
+ }
59691
+ if (index < 0) index = matches.length - 1;
59692
+ } else {
59693
+ index = matches.findIndex((range) => range.from >= snapshot.selection.from);
59694
+ if (index < 0) index = 0;
59695
+ }
59696
+ } else if (direction) index = (index + (direction === "next" ? 1 : -1) + matches.length) % matches.length;
59697
+ if (changedQuery || !query) this.host.find(query, expectedGeneration);
59698
+ this.text = query;
59699
+ const match = matches[index];
59700
+ if (match) this.host.selectTextRange(match, { reveal: true });
59701
+ return !!match;
59702
+ }
59703
+ /** Runs an edit and, if it fails, puts the query, highlights and selection back. */
59704
+ restoring(expectedGeneration, run) {
59705
+ const before = this.host.textSnapshot(expectedGeneration);
59706
+ const previous = this.text;
59707
+ try {
59708
+ return run();
59709
+ } catch (error) {
59710
+ this.text = previous;
59711
+ this.host.find(previous, expectedGeneration);
59712
+ this.host.selectTextRange(before.selection);
59713
+ throw error;
59714
+ }
59715
+ }
59716
+ measure(expectedGeneration) {
59717
+ const snapshot = this.host.textSnapshot(expectedGeneration);
59718
+ const matches = this.matches(this.text, snapshot.snapshotId, expectedGeneration);
59719
+ return {
59720
+ documentId: snapshot.documentId,
59721
+ generation: snapshot.generation,
59722
+ revision: snapshot.revision,
59723
+ query: this.text,
59724
+ current: matches.findIndex((range) => sameRange(range, snapshot.selection)) + 1,
59725
+ total: matches.length
59726
+ };
59727
+ }
59728
+ matches(query, snapshotId, expectedGeneration) {
59729
+ if (!query) return [];
59730
+ if (this.cached?.snapshotId !== snapshotId || this.cached.query !== query) this.cached = {
59731
+ snapshotId,
59732
+ query,
59733
+ matches: this.host.searchMatches(query, expectedGeneration)
59734
+ };
59735
+ return this.cached.matches;
59736
+ }
59737
+ };
59738
+ function sameRange(left, right) {
59739
+ return left.snapshotId === right.snapshotId && left.from === right.from && left.to === right.to;
59740
+ }
59741
+ //#endregion
59428
59742
  //#region src/session.ts
59429
59743
  /**
59430
59744
  * The loaded document's identity and counters. Each load must bring a greater `generation` than
@@ -59998,6 +60312,9 @@ var InkKitEditor = class InkKitEditor {
59998
60312
  ownsRootClass = false;
59999
60313
  activation;
60000
60314
  reportingSuppressed = false;
60315
+ /** A reload keeps each find session's query; a load starts without one. */
60316
+ reloading = false;
60317
+ findSessions = /* @__PURE__ */ new Set();
60001
60318
  loading = false;
60002
60319
  call;
60003
60320
  writable = true;
@@ -60402,6 +60719,14 @@ var InkKitEditor = class InkKitEditor {
60402
60719
  this.suggestions?.refresh();
60403
60720
  if (!this.ready || this.loading || this.reportingSuppressed || this.destroyed) return;
60404
60721
  this.events.commandStateChanged?.(this.commandState());
60722
+ this.refreshFindSessions();
60723
+ }
60724
+ refreshFindSessions() {
60725
+ for (const session of [...this.findSessions]) try {
60726
+ session.refresh();
60727
+ } catch (error) {
60728
+ this.report(error);
60729
+ }
60405
60730
  }
60406
60731
  /** What the editor can do now, for enabling host controls. Unlike `snapshot`, it works during
60407
60732
  * composition. */
@@ -60482,7 +60807,7 @@ var InkKitEditor = class InkKitEditor {
60482
60807
  expectOneOf(mode, editingModes, "mode");
60483
60808
  this.assertCurrent(expectedGeneration);
60484
60809
  if (!this.document.handler.sourceMode || this.mode === mode) return false;
60485
- return this.source.setMode(mode, {
60810
+ const switched = this.source.setMode(mode, {
60486
60811
  ctx: this.editor.ctx,
60487
60812
  wasSource: this.literalSurface,
60488
60813
  container: this.geometry.container,
@@ -60508,6 +60833,8 @@ var InkKitEditor = class InkKitEditor {
60508
60833
  },
60509
60834
  reveal: () => this.revealCurrentSelection()
60510
60835
  });
60836
+ if (switched) for (const session of this.findSessions) session.reapply();
60837
+ return switched;
60511
60838
  }
60512
60839
  updateSurface() {
60513
60840
  this.source.element.hidden = !this.literalSurface;
@@ -60537,6 +60864,19 @@ var InkKitEditor = class InkKitEditor {
60537
60864
  this.assertMutation(expectedGeneration);
60538
60865
  return this.history.redo();
60539
60866
  }
60867
+ /** Drops the steps that `undo` took off the history, so `redo` has nothing to bring back. For a
60868
+ * host that adds content after an undo and must not let a redo resurrect what was undone.
60869
+ * Returns whether a redo was available. The document, selection, revision and undo steps stay
60870
+ * as they are, and no `changed` event is sent. */
60871
+ discardUndone(expectedGeneration) {
60872
+ this.assertMutation(expectedGeneration);
60873
+ const view = this.editor.ctx.get(editorViewCtx);
60874
+ const available = this.history.canRedo(view.state);
60875
+ this.history.discardUndone();
60876
+ discardFormattedRedo(view);
60877
+ if (available) this.publishCommandState();
60878
+ return available;
60879
+ }
60540
60880
  /** The complete current document, which is what a host saves. It throws rather than return a
60541
60881
  * partial document. */
60542
60882
  snapshot(expectedGeneration) {
@@ -60675,6 +61015,7 @@ var InkKitEditor = class InkKitEditor {
60675
61015
  if (this.destroyed) return;
60676
61016
  this.diagnostics.destroy(this.editor.ctx);
60677
61017
  this.destroyed = true;
61018
+ this.findSessions.clear();
60678
61019
  this.clipboard.clear();
60679
61020
  this.ready = false;
60680
61021
  this.session.close();
@@ -60739,6 +61080,7 @@ var InkKitEditor = class InkKitEditor {
60739
61080
  ctx: () => instance.editor.ctx,
60740
61081
  context: () => instance.context(),
60741
61082
  adapter: options.images,
61083
+ files: !!options.files,
60742
61084
  onError: (error) => instance.report(error),
60743
61085
  commit: (apply) => instance.facadeCall(apply),
60744
61086
  literalText: () => instance.literalSurface,
@@ -60864,6 +61206,7 @@ var InkKitEditor = class InkKitEditor {
60864
61206
  this.mode = "formatted";
60865
61207
  this.source.reset();
60866
61208
  this.search.reset();
61209
+ if (!this.reloading) for (const session of this.findSessions) session.reset();
60867
61210
  this.document.handler = handler;
60868
61211
  this.source.setSourceValue(handler.richText ? "" : input.text);
60869
61212
  this.source.element.setSelectionRange(this.source.element.value.length, this.source.element.value.length);
@@ -60895,11 +61238,14 @@ var InkKitEditor = class InkKitEditor {
60895
61238
  const previous = this.document.handler;
60896
61239
  const saved = captureReloadSurface(view, this.source.element, this.geometry.container, wasLiteral);
60897
61240
  this.reportingSuppressed = true;
61241
+ this.reloading = true;
60898
61242
  try {
60899
61243
  this.loadDocument(document);
60900
61244
  if (previous.sourceMode && this.document.handler.sourceMode && previousMode === "source") this.setEditingMode("source", input.generation);
60901
61245
  restoreReloadSurface(view, this.source.element, () => this.literalSurface, saved);
61246
+ for (const session of this.findSessions) session.reapply();
60902
61247
  } finally {
61248
+ this.reloading = false;
60903
61249
  this.reportingSuppressed = false;
60904
61250
  this.publishCommandState();
60905
61251
  }
@@ -61052,6 +61398,24 @@ var InkKitEditor = class InkKitEditor {
61052
61398
  const count = this.search.replace(search, replacement, true);
61053
61399
  return count && replacement !== search && this.refusedRichEdit(doc) ? 0 : count;
61054
61400
  }
61401
+ /**
61402
+ * Opens a find session, which keeps a query and reports its match count and position to
61403
+ * `onChange` whenever they change. Open one session per editor: the editor has one set of
61404
+ * highlights.
61405
+ */
61406
+ findSession(onChange) {
61407
+ this.assertAlive();
61408
+ if (typeof onChange !== "function") throw invalidArgument("onChange must be a function");
61409
+ const session = new EditorFindSession(this, (state) => {
61410
+ try {
61411
+ onChange(state);
61412
+ } catch (error) {
61413
+ if (typeof globalThis.reportError === "function") globalThis.reportError(error);
61414
+ }
61415
+ }, (closed) => this.findSessions.delete(closed));
61416
+ this.findSessions.add(session);
61417
+ return session;
61418
+ }
61055
61419
  outlineContext() {
61056
61420
  const { documentId, generation, revision, key } = this.session.capture();
61057
61421
  return {
@@ -61361,8 +61725,10 @@ var InkKitEditor = class InkKitEditor {
61361
61725
  "replaceSource",
61362
61726
  "undo",
61363
61727
  "redo",
61728
+ "discardUndone",
61364
61729
  "find",
61365
61730
  "searchMatches",
61731
+ "findSession",
61366
61732
  "replace",
61367
61733
  "replaceAll",
61368
61734
  "format",
@@ -61456,4 +61822,45 @@ var InkKitEditor = class InkKitEditor {
61456
61822
  }
61457
61823
  };
61458
61824
  //#endregion
61459
- export { Decoration as a, PluginKey as c, __toESM as d, $prose as i, ok$1 as l, registerDiagram as n, DecorationSet as o, InkKitError as r, Plugin as s, InkKitEditor as t, __commonJSMin as u };
61825
+ //#region src/link-address.ts
61826
+ var openable = /* @__PURE__ */ new Set([
61827
+ "http",
61828
+ "https",
61829
+ "mailto",
61830
+ "tel"
61831
+ ]);
61832
+ var schemePrefix = /^(?:[a-z][a-z0-9+.-]*:\/\/|(?:mailto|tel):)/i;
61833
+ /**
61834
+ * The address a Link field should write for what the person typed, or `undefined` when it is not
61835
+ * a web, mail or phone address. See the reference, which describes each rule.
61836
+ */
61837
+ function normalizeLinkAddress(input) {
61838
+ const text = input.trim();
61839
+ if (!text || /[\s\u0000-\u001f\u007f]/.test(text)) return void 0;
61840
+ const prefix = schemePrefix.exec(text);
61841
+ let address;
61842
+ if (prefix) address = prefix[0].toLowerCase() + text.slice(prefix[0].length);
61843
+ else if (text.startsWith("/")) return void 0;
61844
+ else if (text.includes("@") && !text.includes("/")) address = `mailto:${text}`;
61845
+ else address = `https://${text}`;
61846
+ const scheme = address.slice(0, address.indexOf(":"));
61847
+ if (scheme === "http" || scheme === "https") try {
61848
+ return new URL(address).hostname ? address : void 0;
61849
+ } catch {
61850
+ return;
61851
+ }
61852
+ if (scheme === "mailto" || scheme === "tel") return address.length > scheme.length + 1 ? address : void 0;
61853
+ }
61854
+ /**
61855
+ * Whether a host may open `address` for the person. Only `http`, `https`, `mailto` and `tel`
61856
+ * qualify, whatever the letter case. An address with no scheme, an unknown scheme such as
61857
+ * `javascript:` or `file:`, or a control character does not.
61858
+ */
61859
+ function canOpenLink(address) {
61860
+ const text = address.replace(/^[ \t\n\f\r]+|[ \t\n\f\r]+$/g, "");
61861
+ if (/[\u0000-\u001f\u007f]/.test(text)) return false;
61862
+ const colon = text.search(/[:/?#\\]|$/);
61863
+ return text[colon] === ":" && openable.has(text.slice(0, colon).toLowerCase());
61864
+ }
61865
+ //#endregion
61866
+ export { InkKitError as a, DecorationSet as c, ok$1 as d, __commonJSMin as f, registerDiagram as i, Plugin as l, normalizeLinkAddress as n, $prose as o, __toESM as p, InkKitEditor as r, Decoration as s, canOpenLink as t, PluginKey as u };
@@ -1,4 +1,4 @@
1
- import { a as Decoration, c as PluginKey, d as __toESM, i as $prose, o as DecorationSet, s as Plugin, u as __commonJSMin } from "./core-CbHV2KPY.js";
1
+ import { c as DecorationSet, f as __commonJSMin, l as Plugin, o as $prose, p as __toESM, s as Decoration, u as PluginKey } from "./core-B7CD5hsd.js";
2
2
  //#region node_modules/.pnpm/highlight.js@11.11.2/node_modules/highlight.js/es/languages/arduino.js
3
3
  /** @type LanguageFn */
4
4
  function cPlusPlus(hljs) {
package/dist/core.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { InkKitEditor } from './editor.js';
2
2
  export type { EditorEvents, EditorOptions, TextInputPreferences, CommandState, EditingMode, CaretState, Mark, Block, FormatCommand, FormatOptions, KeyInput, ImageInsertion, ShortcutName, Keymap, } from './editor.js';
3
3
  export { InkKitError } from './types.js';
4
- export type { DocumentFormat, TextDocumentFormat, TextDocumentInput, RichTextDocumentInput, TextDocumentSnapshot, RichTextDocumentSnapshot, RichTextDocument, RichTextBlock, RichTextListItem, RichTextTableCell, RichTextInline, RichTextMark, FormatLoss, FormatLossCode, ConvertedDocument, FormatPromise, OfferedFormat, EditorLabels, Heading, TableCommand, TableOptions, BlockMoveDirection, DocumentContext, DocumentInput, DocumentSnapshot, DocumentVersion, EditorErrorCode, ImageAdapter, ImageImportContext, WikiLinkReference, WikiLinkAdapter, FileReference, ClientPoint, FileContext, FilePresentation, FileAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardScope, ClipboardImage, ClipboardDiagram, PrintableDocument, PrintableWarning, TextRange, ReadableTextSnapshot, TextRect, ViewportInsets, ViewportOptions, ViewportSnapshot, } from './types.js';
4
+ export { normalizeLinkAddress, canOpenLink } from './link-address.js';
5
+ export type { DocumentFormat, TextDocumentFormat, TextDocumentInput, RichTextDocumentInput, TextDocumentSnapshot, RichTextDocumentSnapshot, RichTextDocument, RichTextBlock, RichTextListItem, RichTextTableCell, RichTextInline, RichTextMark, FormatLoss, FormatLossCode, ConvertedDocument, FormatPromise, OfferedFormat, EditorLabels, Heading, TableCommand, TableOptions, BlockMoveDirection, DocumentContext, DocumentInput, DocumentSnapshot, DocumentVersion, EditorErrorCode, ImageAdapter, ImageImportContext, WikiLinkReference, WikiLinkAdapter, FileReference, ClientPoint, FileContext, FilePresentation, FileAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardScope, ClipboardImage, ClipboardDiagram, PrintableDocument, PrintableWarning, TextRange, FindState, FindSession, ReadableTextSnapshot, TextRect, ViewportInsets, ViewportOptions, ViewportSnapshot, } from './types.js';
5
6
  export type { SlashMenuOptions, SlashCommandName, SlashCommand } from './types.js';
6
7
  export type { DiagnosticReport, DiagnosticEvent, DiagnosticState, DiagnosticPresentation, } from './diagnostics.js';
package/dist/core.js CHANGED
@@ -1,2 +1,2 @@
1
- import { r as InkKitError, t as InkKitEditor } from "./chunks/core-CbHV2KPY.js";
2
- export { InkKitEditor, InkKitError };
1
+ import { a as InkKitError, n as normalizeLinkAddress, r as InkKitEditor, t as canOpenLink } from "./chunks/core-B7CD5hsd.js";
2
+ export { InkKitEditor, InkKitError, canOpenLink, normalizeLinkAddress };
package/dist/editor.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type DiagnosticReport } from './diagnostics.js';
2
2
  import type { SlashMenuOptions } from './types.js';
3
- import { InkKitError, type ClientPoint, type DocumentFormat, type DocumentInput, type DocumentSnapshot, type ConvertedDocument, type TextDocumentFormat, type ImageAdapter, type FileAdapter, type WikiLinkAdapter, type ClipboardInput, type ClipboardOutput, type ClipboardScope, type DocumentVersion, type PrintableDocument, type TextRange, type ReadableTextSnapshot, type TextRect, type ViewportOptions, type ViewportSnapshot, type EditorLabels, type Heading, type TableCommand, type TableOptions, type BlockMoveDirection, type OfferedFormat } from './types.js';
3
+ import { InkKitError, type ClientPoint, type DocumentFormat, type DocumentInput, type DocumentSnapshot, type ConvertedDocument, type TextDocumentFormat, type ImageAdapter, type FileAdapter, type WikiLinkAdapter, type ClipboardInput, type ClipboardOutput, type ClipboardScope, type DocumentVersion, type PrintableDocument, type TextRange, type ReadableTextSnapshot, type FindSession, type FindState, type TextRect, type ViewportOptions, type ViewportSnapshot, type EditorLabels, type Heading, type TableCommand, type TableOptions, type BlockMoveDirection, type OfferedFormat } from './types.js';
4
4
  /** An inline format that `CaretState` can report. */
5
5
  export type Mark = 'bold' | 'italic' | 'strikethrough' | 'highlight' | 'underline' | 'superscript' | 'subscript' | 'code' | 'link';
6
6
  /** The kind of block that holds the start of the selection. Any other block reports `paragraph`. */
@@ -27,6 +27,10 @@ export interface CaretState {
27
27
  block: Block;
28
28
  /** One quote holds the whole selection; the block is what sits inside it. */
29
29
  quoted: boolean;
30
+ /** The address of the link at the caret or, for a range, of the first link in it, as the
31
+ * document holds it. Present exactly when `marks` includes `'link'`. For a reference link, it
32
+ * is the address its definition gives. A host's Link sheet starts from it. */
33
+ href?: string;
30
34
  /** In an RTF document, the alignment of the paragraph that holds the start of the selection. */
31
35
  align?: 'left' | 'center' | 'right' | 'justify';
32
36
  }
@@ -178,6 +182,9 @@ export declare class InkKitEditor<F extends DocumentFormat = TextDocumentFormat>
178
182
  private ownsRootClass;
179
183
  private activation;
180
184
  private reportingSuppressed;
185
+ /** A reload keeps each find session's query; a load starts without one. */
186
+ private reloading;
187
+ private readonly findSessions;
181
188
  private loading;
182
189
  private call?;
183
190
  private writable;
@@ -268,6 +275,7 @@ export declare class InkKitEditor<F extends DocumentFormat = TextDocumentFormat>
268
275
  setTextInputPreferences(preferences: TextInputPreferences): void;
269
276
  private applyInputPolicy;
270
277
  private publishCommandState;
278
+ private refreshFindSessions;
271
279
  /** What the editor can do now, for enabling host controls. Unlike `snapshot`, it works during
272
280
  * composition. */
273
281
  commandState(expectedGeneration?: number): CommandState;
@@ -289,6 +297,11 @@ export declare class InkKitEditor<F extends DocumentFormat = TextDocumentFormat>
289
297
  undo(expectedGeneration?: number): boolean;
290
298
  /** Returns whether a history step applied. */
291
299
  redo(expectedGeneration?: number): boolean;
300
+ /** Drops the steps that `undo` took off the history, so `redo` has nothing to bring back. For a
301
+ * host that adds content after an undo and must not let a redo resurrect what was undone.
302
+ * Returns whether a redo was available. The document, selection, revision and undo steps stay
303
+ * as they are, and no `changed` event is sent. */
304
+ discardUndone(expectedGeneration?: number): boolean;
292
305
  /** The complete current document, which is what a host saves. It throws rather than return a
293
306
  * partial document. */
294
307
  snapshot(expectedGeneration?: number): DocumentSnapshot<F>;
@@ -379,6 +392,12 @@ export declare class InkKitEditor<F extends DocumentFormat = TextDocumentFormat>
379
392
  replace(search: string, replacement: string, expectedGeneration?: number): boolean;
380
393
  /** Replaces every non-overlapping match once and returns how many. */
381
394
  replaceAll(search: string, replacement: string, expectedGeneration?: number): number;
395
+ /**
396
+ * Opens a find session, which keeps a query and reports its match count and position to
397
+ * `onChange` whenever they change. Open one session per editor: the editor has one set of
398
+ * highlights.
399
+ */
400
+ findSession(onChange: (state: FindState) => void): FindSession;
382
401
  private outlineContext;
383
402
  /** The outline in document order. Entries go stale when the document changes. */
384
403
  headings(expectedGeneration?: number): readonly Heading[];
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as registerDiagram, r as InkKitError, t as InkKitEditor } from "./chunks/core-CbHV2KPY.js";
1
+ import { a as InkKitError, i as registerDiagram, n as normalizeLinkAddress, r as InkKitEditor, t as canOpenLink } from "./chunks/core-B7CD5hsd.js";
2
2
  //#region node_modules/.pnpm/dompurify@3.4.17/node_modules/dompurify/dist/purify.es.mjs
3
3
  /*! @license DOMPurify 3.4.17 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.17/LICENSE */
4
4
  function _OverloadYield(e, d) {
@@ -32790,4 +32790,4 @@ function renderPele(source) {
32790
32790
  //#region src/index.ts
32791
32791
  registerDiagram(renderPele);
32792
32792
  //#endregion
32793
- export { InkKitEditor, InkKitError };
32793
+ export { InkKitEditor, InkKitError, canOpenLink, normalizeLinkAddress };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The address a Link field should write for what the person typed, or `undefined` when it is not
3
+ * a web, mail or phone address. See the reference, which describes each rule.
4
+ */
5
+ export declare function normalizeLinkAddress(input: string): string | undefined;
6
+ /**
7
+ * Whether a host may open `address` for the person. Only `http`, `https`, `mailto` and `tel`
8
+ * qualify, whatever the letter case. An address with no scheme, an unknown scheme such as
9
+ * `javascript:` or `file:`, or a control character does not.
10
+ */
11
+ export declare function canOpenLink(address: string): boolean;
package/dist/types.d.ts CHANGED
@@ -120,7 +120,7 @@ export interface RichTextDocumentSnapshot extends RichTextDocumentInput, Documen
120
120
  dirty: boolean;
121
121
  }
122
122
  /** The closed list of failure codes. The API reference describes each one. */
123
- export declare const editorErrorCodes: readonly ['not-ready', 'destroyed', 'stale-document', 'composition', 'operation-pending', 'read-only', 'invalid-argument', 'invalid-range', 'preservation', 'image-unavailable', 'diagram-unavailable', 'file-unavailable', 'clipboard-unavailable', 'slash-command-failed', 'internal'];
123
+ export declare const editorErrorCodes: readonly ['not-ready', 'destroyed', 'stale-document', 'composition', 'operation-pending', 'read-only', 'invalid-argument', 'invalid-range', 'preservation', 'image-unavailable', 'image-unsupported', 'diagram-unavailable', 'file-unavailable', 'clipboard-unavailable', 'slash-command-failed', 'internal'];
124
124
  export type EditorErrorCode = (typeof editorErrorCodes)[number];
125
125
  /** Every failure InkKit reports. Hosts branch on `code`; `message` is for logs. When the failure
126
126
  * started elsewhere, such as in an adapter, the host clipboard or the renderer, `cause` holds the
@@ -310,6 +310,44 @@ export interface ReadableTextSnapshot extends DocumentVersion {
310
310
  text: string;
311
311
  selection: TextRange;
312
312
  }
313
+ /** What a find session shows: how many matches the query has and which one the selection is on. */
314
+ export interface FindState extends DocumentVersion {
315
+ /** The query, or an empty string when there is none. */
316
+ query: string;
317
+ /** The position, from 1, of the match that the selection covers exactly, or 0 when the selection
318
+ * is not on a match. */
319
+ current: number;
320
+ /** The number of matches. */
321
+ total: number;
322
+ }
323
+ /**
324
+ * A find bar's state and commands: the query, the match count, the match the selection is on, and
325
+ * navigation that wraps. Open one with `InkKitEditor.findSession`.
326
+ */
327
+ export interface FindSession {
328
+ /** The current state. It does not call the change handler. */
329
+ state(expectedGeneration?: number): FindState;
330
+ /** Sets the query, highlights its matches and selects one. A new query selects the first match
331
+ * at or after the selection, or the last one before it with `direction: 'previous'`. With the
332
+ * same query, the selection moves to the next or previous match and wraps at the ends; without
333
+ * a direction, a selection on a match stays there. Returns whether there is a match. */
334
+ find(query: string, options?: {
335
+ direction?: 'next' | 'previous';
336
+ }, expectedGeneration?: number): boolean;
337
+ /** Moves to the next match, wrapping at the end. Returns whether there is a match. */
338
+ next(expectedGeneration?: number): boolean;
339
+ /** Moves to the previous match, wrapping at the start. Returns whether there is a match. */
340
+ previous(expectedGeneration?: number): boolean;
341
+ /** Replaces the selected match, or the next one, and selects the match after it. Returns whether
342
+ * the text changed. A match that already reads as the replacement is skipped without a change. */
343
+ replace(query: string, replacement: string, expectedGeneration?: number): boolean;
344
+ /** Replaces every match and returns how many were replaced. */
345
+ replaceAll(query: string, replacement: string, expectedGeneration?: number): number;
346
+ /** Clears the query and the highlights. */
347
+ clear(expectedGeneration?: number): void;
348
+ /** Stops the session: it clears the highlights and its handler is not called again. */
349
+ close(): void;
350
+ }
313
351
  /** A rectangle in CSS pixels in the web view's client coordinate space. */
314
352
  export interface TextRect {
315
353
  left: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicayzer/inkkit",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "A preserving Markdown editor for offline web views and the web",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -55,6 +55,8 @@
55
55
  "test:package": "node scripts/test-package.mjs",
56
56
  "check": "pnpm typecheck && pnpm test && pnpm build && pnpm test:docs && pnpm test:package",
57
57
  "test:browser": "playwright test --project=chromium",
58
+ "test:playground": "playwright test --config playwright.playground.config.ts --project=chromium",
59
+ "measure:browser": "playwright test --config playwright.measure.config.ts",
58
60
  "test:built-playground": "playwright test --config playwright.built.config.ts"
59
61
  },
60
62
  "devDependencies": {