@aicayzer/inkkit 0.6.0 → 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;
@@ -48733,6 +48792,11 @@ function formattedQuery(text) {
48733
48792
  const from = state.doc.resolve(match.from);
48734
48793
  const to = state.doc.resolve(match.to);
48735
48794
  if (from.parent !== to.parent || !from.parent.inlineContent) return false;
48795
+ if ([
48796
+ "comment_inline",
48797
+ "comment_block",
48798
+ "reference_definition"
48799
+ ].includes(from.parent.type.name)) return false;
48736
48800
  const content = state.doc.slice(match.from, match.to).content;
48737
48801
  let textOnly = true;
48738
48802
  content.forEach((node) => {
@@ -48742,6 +48806,22 @@ function formattedQuery(text) {
48742
48806
  }
48743
48807
  });
48744
48808
  }
48809
+ function formattedMatches(state, text) {
48810
+ if (!text) return [];
48811
+ const query = formattedQuery(text);
48812
+ const matches = [];
48813
+ if (!query.valid) return matches;
48814
+ for (let position = 0;;) {
48815
+ const match = query.findNext(state, position);
48816
+ if (!match) break;
48817
+ matches.push({
48818
+ from: match.from,
48819
+ to: match.to
48820
+ });
48821
+ position = match.to;
48822
+ }
48823
+ return matches;
48824
+ }
48745
48825
  function findFormatted(view, text) {
48746
48826
  const query = formattedQuery(text);
48747
48827
  const previous = getSearchState(view.state)?.query.search;
@@ -48847,6 +48927,33 @@ var EditorSearch = class {
48847
48927
  restore(match) {
48848
48928
  this.literalMatch = match;
48849
48929
  }
48930
+ matches(text, snapshotId, projection) {
48931
+ const raw = this.runtime.literal() ? this.runtime.currentText() : void 0;
48932
+ const matches = raw === void 0 ? formattedMatches(this.runtime.view().state, text) : literalMatches(raw, text);
48933
+ let rawPosition = 0, normalizedPosition = 0, spanIndex = 0;
48934
+ const offset = (position) => {
48935
+ if (raw !== void 0) {
48936
+ while (rawPosition < position) {
48937
+ if (raw[rawPosition] !== "\n" || raw[rawPosition - 1] !== "\r") normalizedPosition++;
48938
+ rawPosition++;
48939
+ }
48940
+ return normalizedPosition;
48941
+ }
48942
+ while (spanIndex < projection.spans.length && projection.spans[spanIndex].end < position) spanIndex++;
48943
+ const span = projection.spans[spanIndex];
48944
+ if (!span) return projection.text.length;
48945
+ if (position < span.start) return span.from;
48946
+ return span.kind === "text" ? span.from + Math.max(0, position - span.start) : position === span.start ? span.from : span.to;
48947
+ };
48948
+ return Object.freeze(matches.map((match) => {
48949
+ const start = raw !== void 0 && raw[match.from] === "\n" && raw[match.from - 1] === "\r" ? match.from - 1 : match.from;
48950
+ return Object.freeze({
48951
+ snapshotId,
48952
+ from: offset(start),
48953
+ to: offset(match.to)
48954
+ });
48955
+ }));
48956
+ }
48850
48957
  find(text) {
48851
48958
  if (this.runtime.literal()) {
48852
48959
  const from = this.plainSearch === text ? this.runtime.element().selectionEnd : this.runtime.element().selectionStart;
@@ -53285,6 +53392,29 @@ var EditorDocument = class {
53285
53392
  //#region src/editor-history.ts
53286
53393
  var sourceGroupDelay = 500;
53287
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
+ }
53288
53418
  var SourceHistory = class {
53289
53419
  runtime;
53290
53420
  pendingSource;
@@ -53315,6 +53445,11 @@ var SourceHistory = class {
53315
53445
  this.undoneSteps = [];
53316
53446
  this.formattedUndone = 0;
53317
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
+ }
53318
53453
  canUndo(state) {
53319
53454
  return !!this.pendingSource || undo(state);
53320
53455
  }
@@ -57233,7 +57368,7 @@ async function createMilkdownEditor(root, options, runtime) {
57233
57368
  return true;
57234
57369
  }
57235
57370
  }));
57236
- }).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-ByA5v5o7.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, {
57237
57372
  capture: () => runtime.capture(),
57238
57373
  active: () => runtime.active(),
57239
57374
  labels: {
@@ -58792,6 +58927,11 @@ var PasteController = class {
58792
58927
  },
58793
58928
  drop: (view, event) => {
58794
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
+ }
58795
58935
  if (!data?.getData("text/html") || !view.editable || view.dragging) return false;
58796
58936
  event.preventDefault();
58797
58937
  const target = blockDropRange(view, event.clientX, event.clientY);
@@ -58800,7 +58940,7 @@ var PasteController = class {
58800
58940
  text: data.getData("text/plain"),
58801
58941
  html: data.getData("text/html"),
58802
58942
  plainText: this.options.literalText?.() ?? false
58803
- }, [...data.files].filter((file) => file.type.startsWith("image/")), target).catch((error) => options.onError?.(error));
58943
+ }, imageFiles(data.files), target).catch((error) => options.onError?.(error));
58804
58944
  return true;
58805
58945
  }
58806
58946
  },
@@ -58827,7 +58967,7 @@ var PasteController = class {
58827
58967
  }).catch((error) => options.onError?.(error));
58828
58968
  return true;
58829
58969
  }
58830
- const files = [...data.files].filter((file) => file.type.startsWith("image/"));
58970
+ const files = imageFiles(data.files);
58831
58971
  this.run({
58832
58972
  text,
58833
58973
  html,
@@ -58921,6 +59061,7 @@ var PasteController = class {
58921
59061
  };
58922
59062
  const rich = this.options.richText?.();
58923
59063
  if (rich) {
59064
+ if (!this.options.adapter && (files.length || input.images?.length)) throw imagesRefused();
58924
59065
  const anchor = operation;
58925
59066
  const state = view.state;
58926
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());
@@ -58943,6 +59084,12 @@ var PasteController = class {
58943
59084
  this.assertContext(context, operation);
58944
59085
  const schema = view.state.schema;
58945
59086
  let slice;
59087
+ const placeholders = [];
59088
+ const placeholder = (name) => {
59089
+ const text = `[${name}: image unavailable]`;
59090
+ placeholders.push(text);
59091
+ return text;
59092
+ };
58946
59093
  let source;
58947
59094
  const plainText = input.plainText || this.options.literalText?.() || inCode();
58948
59095
  if (!plainText && input.markdown == null && this.commit(() => pasteSpreadsheet(ctx, {
@@ -58984,8 +59131,8 @@ var PasteController = class {
58984
59131
  element.removeAttribute("srcset");
58985
59132
  } catch (error) {
58986
59133
  if (error instanceof InkKitError && error.code !== "image-unavailable") throw error;
58987
- element.replaceWith(document.createTextNode(`[${alt}: image unavailable]`));
58988
- 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."));
58989
59136
  }
58990
59137
  }
58991
59138
  let imageIndex = 0;
@@ -58995,7 +59142,7 @@ var PasteController = class {
58995
59142
  const position = slot ? Number(slot[1]) : imageIndex++;
58996
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];
58997
59144
  if (!element || !container.contains(element)) {
58998
- const text = schema.text(`[${node.attrs.alt || "Image"}: image unavailable]`);
59145
+ const text = schema.text(placeholder(node.attrs.alt || "Image"));
58999
59146
  return node.isBlock ? schema.nodes.paragraph.create(null, text) : text;
59000
59147
  }
59001
59148
  return node.type.create({
@@ -59024,6 +59171,7 @@ var PasteController = class {
59024
59171
  }
59025
59172
  }
59026
59173
  } else if (images.length) {
59174
+ if (!this.options.adapter) throw imagesRefused();
59027
59175
  const nodes = [];
59028
59176
  if (input.text) nodes.push(schema.nodes.paragraph.create(null, schema.text(input.text)));
59029
59177
  for (const image of images) try {
@@ -59038,7 +59186,7 @@ var PasteController = class {
59038
59186
  nodes.push(schema.nodes.paragraph.create(null, imageNode));
59039
59187
  } catch (error) {
59040
59188
  if (error instanceof InkKitError && error.code !== "image-unavailable") throw error;
59041
- 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"))));
59042
59190
  this.options.onError?.(coded(error, "image-unavailable", error instanceof Error ? error.message : "The pasted image is unavailable."));
59043
59191
  }
59044
59192
  slice = new Slice(Fragment.fromArray(nodes), 1, 1);
@@ -59055,6 +59203,10 @@ var PasteController = class {
59055
59203
  source = markdown ?? input.text;
59056
59204
  slice = joinable(ctx.get(parserCtx)(source).content);
59057
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
+ }
59058
59210
  this.assertContext(context, operation);
59059
59211
  if (!view.editable || this.options.editable?.() === false) return;
59060
59212
  const anchor = this.anchor(operation);
@@ -59320,6 +59472,12 @@ function dropRange(doc, range) {
59320
59472
  * keeps the provider's code-language behaviour; invalid metadata pastes plain
59321
59473
  * text.
59322
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
+ }
59323
59481
  function claimsPaste(view, event) {
59324
59482
  const data = event.clipboardData;
59325
59483
  if (!data || !view.editable) return false;
@@ -59377,6 +59535,210 @@ function dataImage(source) {
59377
59535
  };
59378
59536
  }
59379
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
59380
59742
  //#region src/session.ts
59381
59743
  /**
59382
59744
  * The loaded document's identity and counters. Each load must bring a greater `generation` than
@@ -59950,6 +60312,9 @@ var InkKitEditor = class InkKitEditor {
59950
60312
  ownsRootClass = false;
59951
60313
  activation;
59952
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();
59953
60318
  loading = false;
59954
60319
  call;
59955
60320
  writable = true;
@@ -60354,6 +60719,14 @@ var InkKitEditor = class InkKitEditor {
60354
60719
  this.suggestions?.refresh();
60355
60720
  if (!this.ready || this.loading || this.reportingSuppressed || this.destroyed) return;
60356
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
+ }
60357
60730
  }
60358
60731
  /** What the editor can do now, for enabling host controls. Unlike `snapshot`, it works during
60359
60732
  * composition. */
@@ -60434,7 +60807,7 @@ var InkKitEditor = class InkKitEditor {
60434
60807
  expectOneOf(mode, editingModes, "mode");
60435
60808
  this.assertCurrent(expectedGeneration);
60436
60809
  if (!this.document.handler.sourceMode || this.mode === mode) return false;
60437
- return this.source.setMode(mode, {
60810
+ const switched = this.source.setMode(mode, {
60438
60811
  ctx: this.editor.ctx,
60439
60812
  wasSource: this.literalSurface,
60440
60813
  container: this.geometry.container,
@@ -60460,6 +60833,8 @@ var InkKitEditor = class InkKitEditor {
60460
60833
  },
60461
60834
  reveal: () => this.revealCurrentSelection()
60462
60835
  });
60836
+ if (switched) for (const session of this.findSessions) session.reapply();
60837
+ return switched;
60463
60838
  }
60464
60839
  updateSurface() {
60465
60840
  this.source.element.hidden = !this.literalSurface;
@@ -60489,6 +60864,19 @@ var InkKitEditor = class InkKitEditor {
60489
60864
  this.assertMutation(expectedGeneration);
60490
60865
  return this.history.redo();
60491
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
+ }
60492
60880
  /** The complete current document, which is what a host saves. It throws rather than return a
60493
60881
  * partial document. */
60494
60882
  snapshot(expectedGeneration) {
@@ -60627,6 +61015,7 @@ var InkKitEditor = class InkKitEditor {
60627
61015
  if (this.destroyed) return;
60628
61016
  this.diagnostics.destroy(this.editor.ctx);
60629
61017
  this.destroyed = true;
61018
+ this.findSessions.clear();
60630
61019
  this.clipboard.clear();
60631
61020
  this.ready = false;
60632
61021
  this.session.close();
@@ -60691,6 +61080,7 @@ var InkKitEditor = class InkKitEditor {
60691
61080
  ctx: () => instance.editor.ctx,
60692
61081
  context: () => instance.context(),
60693
61082
  adapter: options.images,
61083
+ files: !!options.files,
60694
61084
  onError: (error) => instance.report(error),
60695
61085
  commit: (apply) => instance.facadeCall(apply),
60696
61086
  literalText: () => instance.literalSurface,
@@ -60816,6 +61206,7 @@ var InkKitEditor = class InkKitEditor {
60816
61206
  this.mode = "formatted";
60817
61207
  this.source.reset();
60818
61208
  this.search.reset();
61209
+ if (!this.reloading) for (const session of this.findSessions) session.reset();
60819
61210
  this.document.handler = handler;
60820
61211
  this.source.setSourceValue(handler.richText ? "" : input.text);
60821
61212
  this.source.element.setSelectionRange(this.source.element.value.length, this.source.element.value.length);
@@ -60847,11 +61238,14 @@ var InkKitEditor = class InkKitEditor {
60847
61238
  const previous = this.document.handler;
60848
61239
  const saved = captureReloadSurface(view, this.source.element, this.geometry.container, wasLiteral);
60849
61240
  this.reportingSuppressed = true;
61241
+ this.reloading = true;
60850
61242
  try {
60851
61243
  this.loadDocument(document);
60852
61244
  if (previous.sourceMode && this.document.handler.sourceMode && previousMode === "source") this.setEditingMode("source", input.generation);
60853
61245
  restoreReloadSurface(view, this.source.element, () => this.literalSurface, saved);
61246
+ for (const session of this.findSessions) session.reapply();
60854
61247
  } finally {
61248
+ this.reloading = false;
60855
61249
  this.reportingSuppressed = false;
60856
61250
  this.publishCommandState();
60857
61251
  }
@@ -60970,6 +61364,14 @@ var InkKitEditor = class InkKitEditor {
60970
61364
  this.lastMarkdown = markdown;
60971
61365
  view.dispatch(initialDocumentSelection(view.state));
60972
61366
  }
61367
+ /** Returns every non-overlapping match without changing selection, focus or highlights. */
61368
+ searchMatches(text, expectedGeneration) {
61369
+ this.assertAlive();
61370
+ expectString(text, "text");
61371
+ this.assertCurrent(expectedGeneration);
61372
+ const scope = this.text.scope();
61373
+ return this.search.matches(text, scope.id, scope.projection);
61374
+ }
60973
61375
  /** Selects the next match and returns whether there was one. */
60974
61376
  find(text, expectedGeneration) {
60975
61377
  this.assertAlive();
@@ -60996,6 +61398,24 @@ var InkKitEditor = class InkKitEditor {
60996
61398
  const count = this.search.replace(search, replacement, true);
60997
61399
  return count && replacement !== search && this.refusedRichEdit(doc) ? 0 : count;
60998
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
+ }
60999
61419
  outlineContext() {
61000
61420
  const { documentId, generation, revision, key } = this.session.capture();
61001
61421
  return {
@@ -61305,7 +61725,10 @@ var InkKitEditor = class InkKitEditor {
61305
61725
  "replaceSource",
61306
61726
  "undo",
61307
61727
  "redo",
61728
+ "discardUndone",
61308
61729
  "find",
61730
+ "searchMatches",
61731
+ "findSession",
61309
61732
  "replace",
61310
61733
  "replaceAll",
61311
61734
  "format",
@@ -61399,4 +61822,45 @@ var InkKitEditor = class InkKitEditor {
61399
61822
  }
61400
61823
  };
61401
61824
  //#endregion
61402
- 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-C37056dR.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-C37056dR.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>;
@@ -371,12 +384,20 @@ export declare class InkKitEditor<F extends DocumentFormat = TextDocumentFormat>
371
384
  private revealCurrentSelection;
372
385
  private revealLiteralSelection;
373
386
  private load;
387
+ /** Returns every non-overlapping match without changing selection, focus or highlights. */
388
+ searchMatches(text: string, expectedGeneration?: number): readonly TextRange[];
374
389
  /** Selects the next match and returns whether there was one. */
375
390
  find(text: string, expectedGeneration?: number): boolean;
376
391
  /** Replaces the selected match, or the next one, and returns whether text changed. */
377
392
  replace(search: string, replacement: string, expectedGeneration?: number): boolean;
378
393
  /** Replaces every non-overlapping match once and returns how many. */
379
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;
380
401
  private outlineContext;
381
402
  /** The outline in document order. Entries go stale when the document changes. */
382
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-C37056dR.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.0",
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": {