@aicayzer/inkkit 0.1.6 → 0.2.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.
package/README.md CHANGED
@@ -1,48 +1,32 @@
1
- # InkKit
1
+ # Inkkit
2
2
 
3
- A Markdown editor for the web and for apps that host a web view. It edits formatted text and keeps your Markdown as you wrote it: a document you open and leave is saved unchanged, and an edit rewrites only the blocks it touches.
3
+ A Markdown editor for web pages and for apps that host a web view. People edit formatted text, and your app saves the Markdown as it was written: an unedited document is saved unchanged, and an edit rewrites only the blocks it touches.
4
4
 
5
- - Formatted and source editing, with one undo history
6
- - Tables, task lists, footnotes, callouts, highlights and reference links
7
- - Mermaid diagram previews, offline
8
- - Copy as readable text or HTML, or copy the Markdown itself
9
- - Images and files through your own storage adapter
10
- - Your app owns the toolbar, menus, files and appearance
11
-
12
- ## How it works
5
+ <details>
6
+ <summary>How Inkkit preserves Markdown</summary>
13
7
 
14
8
  <picture>
15
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/aicayzer/inkkit/main/docs/images/how-inkkit-works-dark.svg">
16
- <img src="https://raw.githubusercontent.com/aicayzer/inkkit/main/docs/images/how-inkkit-works.svg" width="800" alt="An ordinary Markdown editor rebuilds the whole file when it saves, so list markers, spacing and escaping change. InkKit loads the file and edits it formatted with Milkdown. A preservation layer between Milkdown and the file saves a note you open and leave byte for byte unchanged. After an edit, it keeps unchanged blocks as their original bytes, writes only edited blocks with Milkdown, and reads the result back. If it reopens as the same document, the edit is kept and saved. If not, the edit is refused and the note stays as it was. Your app owns the files, storage, toolbar and appearance.">
9
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/how-inkkit-works-dark.svg">
10
+ <img src="docs/images/how-inkkit-works.svg" alt="Inkkit keeps unchanged source and checks edited blocks before your app saves them.">
17
11
  </picture>
18
12
 
19
- A formatted Markdown editor usually rebuilds the whole file when it saves, so opening and saving a note can change list markers, spacing and escaping that you never touched. InkKit adds a preservation layer between [Milkdown](https://milkdown.dev) and the file. A note that you open and leave is saved byte for byte unchanged, and after an edit only the blocks you edited are rewritten. InkKit reads the result of each edit back and refuses the edit if it would not reopen as the same document.
20
-
21
- For the exact guarantees, see [Preservation](https://github.com/aicayzer/inkkit/blob/main/docs/preservation.md).
13
+ </details>
22
14
 
23
- ## Install
15
+ ## Get started
24
16
 
25
17
  ```sh
26
18
  npm install @aicayzer/inkkit
27
19
  ```
28
20
 
29
- An app that does not render diagrams can import from `@aicayzer/inkkit/core` instead. That entry leaves Mermaid out of the app's bundle and keeps diagrams as editable source. See [Builds without diagrams](https://github.com/aicayzer/inkkit/blob/main/docs/api.md#builds-without-diagrams).
30
-
31
- ## Supported engines
32
-
33
- InkKit supports Safari and `WKWebView` 17.5 or later (iOS 17.5, macOS 14.5), Chrome and Edge 123 or later, and Firefox 125 or later. The host page must declare UTF-8. See [Supported engines](https://github.com/aicayzer/inkkit/blob/main/docs/integration.md#supported-engines) for the APIs behind these versions.
34
-
35
- ## Use
36
-
37
21
  ```ts
38
22
  import { InkKitEditor } from '@aicayzer/inkkit'
39
23
  import '@aicayzer/inkkit/style.css'
40
24
 
41
25
  const editor = await InkKitEditor.mount(document.getElementById('editor')!, {
42
- changed({ generation, revision, text }) {},
43
- commandStateChanged({ caret }) {},
44
- openLink(href) {},
45
- copy(text) {},
26
+ changed() {},
27
+ error(error) {
28
+ console.error(error.code, error.message)
29
+ },
46
30
  })
47
31
 
48
32
  editor.loadDocument({
@@ -55,17 +39,9 @@ editor.loadDocument({
55
39
  const { text } = editor.snapshot(1)
56
40
  ```
57
41
 
58
- ## Built with InkKit
59
-
60
- - [Memos](https://github.com/aicayzer/memos): a small Mac app for memos, in one floating window
61
- - [PadPad](https://github.com/aicayzer/padpad): a small Mac app for quick text and files
62
-
63
- ## Learn more
42
+ ## Documentation
64
43
 
65
- - [Supported syntax](https://github.com/aicayzer/inkkit/blob/main/docs/supported-syntax.md)
66
- - [Integration guide](https://github.com/aicayzer/inkkit/blob/main/docs/integration.md)
67
- - [API reference](https://github.com/aicayzer/inkkit/blob/main/docs/api.md)
68
- - [All documentation](https://github.com/aicayzer/inkkit/blob/main/docs/README.md)
44
+ [inkkit.cyzr.me](https://inkkit.cyzr.me) covers how Inkkit works, compatibility, integration guides and the API reference.
69
45
 
70
46
  ## License
71
47
 
@@ -22734,7 +22734,7 @@ var SerializerState = class extends Stack {
22734
22734
  #closeMark;
22735
22735
  };
22736
22736
  //#endregion
22737
- //#region node_modules/.pnpm/prosemirror-view@1.42.6_patch_hash=a3414754d6cb6cd32517617a4affee5f7895ca0b3b976186951d43106adf18b3/node_modules/prosemirror-view/dist/index.js
22737
+ //#region node_modules/.pnpm/prosemirror-view@1.42.6_patch_hash=808819d35255b79fc131ebc4873ab2fbcd47cd8f5bc05cd11e8caab02ecee548/node_modules/prosemirror-view/dist/index.js
22738
22738
  var domIndex = function(node) {
22739
22739
  for (var index = 0;; index++) {
22740
22740
  node = node.previousSibling;
@@ -25232,6 +25232,17 @@ editHandlers.keydown = (view, _event) => {
25232
25232
  view.input.lastKeyCodeTime = Date.now();
25233
25233
  if (android && chrome && event.keyCode == 13) return;
25234
25234
  if (event.keyCode != 229) view.domObserver.forceFlush();
25235
+ if (event.keyCode != 229 && ![
25236
+ 16,
25237
+ 17,
25238
+ 18,
25239
+ 91,
25240
+ 92,
25241
+ 93
25242
+ ].includes(event.keyCode)) {
25243
+ view.domObserver.flush();
25244
+ view.input.lastFocus = 0;
25245
+ }
25235
25246
  if (ios && event.keyCode == 13 && !event.ctrlKey && !event.altKey && !event.metaKey) {
25236
25247
  let now = Date.now();
25237
25248
  view.input.lastIOSEnter = now;
@@ -31243,6 +31254,18 @@ function mermaidPreview(onError) {
31243
31254
  }));
31244
31255
  }
31245
31256
  //#endregion
31257
+ //#region src/literal-text.ts
31258
+ /** Literal lines stay in their textblock, including headings and table cells. */
31259
+ function literalTextNodes(schema, text, marks, isHTML) {
31260
+ const nodes = [];
31261
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
31262
+ lines.forEach((line, index) => {
31263
+ if (index) nodes.push(schema.nodes.hardbreak.create({ isHTML }, null, index === lines.length - 1 && line === "" ? [] : marks));
31264
+ if (line) nodes.push(schema.text(line, marks));
31265
+ });
31266
+ return nodes;
31267
+ }
31268
+ //#endregion
31246
31269
  //#region node_modules/.pnpm/rope-sequence@1.3.4/node_modules/rope-sequence/dist/index.js
31247
31270
  var GOOD_LEAF_SIZE = 200;
31248
31271
  var RopeSequence = function RopeSequence() {};
@@ -31697,6 +31720,13 @@ A command function that redoes the last undone change, if any.
31697
31720
  */
31698
31721
  var redo = buildCommand(true, true);
31699
31722
  /**
31723
+ The amount of redoable events available in a given editor state.
31724
+ */
31725
+ function redoDepth(state) {
31726
+ let hist = historyKey.getState(state);
31727
+ return hist ? hist.undone.eventCount : 0;
31728
+ }
31729
+ /**
31700
31730
  Returns true if the given transaction was generated by the history
31701
31731
  plugin.
31702
31732
  */
@@ -34978,7 +35008,7 @@ var associationAttrs = {
34978
35008
  identifier: { default: "" }
34979
35009
  };
34980
35010
  /** Whether an element reads as `text`, with whitespace collapsed as shown. */
34981
- function showsText$1(dom, text) {
35011
+ function showsText(dom, text) {
34982
35012
  const shown = (value) => value.replace(/\s+/g, " ").trim();
34983
35013
  return shown(dom.textContent ?? "") === shown(text);
34984
35014
  }
@@ -35003,7 +35033,7 @@ var references = [
35003
35033
  const url = dom.getAttribute("data-inkkit-url") ?? "";
35004
35034
  const label = dom.getAttribute("data-inkkit-label") ?? "";
35005
35035
  const title = dom.getAttribute("data-inkkit-title");
35006
- if (!isSafeLinkTarget(url) || !showsText$1(dom, definitionText(label, url, title))) return false;
35036
+ if (!isSafeLinkTarget(url) || !showsText(dom, definitionText(label, url, title))) return false;
35007
35037
  return {
35008
35038
  label,
35009
35039
  identifier: dom.getAttribute("data-inkkit-reference-definition") ?? "",
@@ -35055,7 +35085,7 @@ var references = [
35055
35085
  tag: "sup[data-inkkit-footnote-reference]",
35056
35086
  getAttrs: (dom) => {
35057
35087
  const label = dom.getAttribute("data-inkkit-label") ?? "";
35058
- return showsText$1(dom, label) ? {
35088
+ return showsText(dom, label) ? {
35059
35089
  label,
35060
35090
  identifier: dom.getAttribute("data-inkkit-footnote-reference") ?? ""
35061
35091
  } : false;
@@ -35499,7 +35529,7 @@ var wikiLinkSchema = $node("inkkit_wiki_link", () => ({
35499
35529
  tag: "span[data-inkkit-wiki]",
35500
35530
  getAttrs(dom) {
35501
35531
  const parsed = parseLinkedSyntax(dom.getAttribute("data-inkkit-wiki-raw") ?? "", dom.getAttribute("data-inkkit-wiki-table") === "true");
35502
- return parsed && !parsed.embed && showsWikiTarget(parsed) && showsText$1(dom, parsed.label) ? {
35532
+ return parsed && !parsed.embed && showsWikiTarget(parsed) && showsText(dom, parsed.label) ? {
35503
35533
  raw: parsed.raw,
35504
35534
  target: parsed.target,
35505
35535
  fragment: parsed.fragment,
@@ -43706,6 +43736,7 @@ function commentNode(inline) {
43706
43736
  marks: "",
43707
43737
  code: true,
43708
43738
  defining: true,
43739
+ createGapCursor: !inline,
43709
43740
  attrs: {
43710
43741
  syntax: { default: "html" },
43711
43742
  block: { default: !inline }
@@ -44118,7 +44149,7 @@ function nodeText(node, missingImages, fileDescriptions) {
44118
44149
  }
44119
44150
  return clipboardText(node.content, missingImages, fileDescriptions);
44120
44151
  }
44121
- function clipboardContent(content, schema) {
44152
+ function clipboardDOM(content, schema) {
44122
44153
  content = shareableFragment(content);
44123
44154
  const container = document.createElement("div");
44124
44155
  const serializer = DOMSerializer.fromSchema(schema);
@@ -44148,14 +44179,30 @@ function clipboardContent(content, schema) {
44148
44179
  label.textContent = `[${definition.getAttribute("data-inkkit-label")}] `;
44149
44180
  definition.prepend(label);
44150
44181
  }
44182
+ return container;
44183
+ }
44184
+ function clipboardContent(content, schema) {
44151
44185
  return {
44152
44186
  text: clipboardText(content),
44153
- html: container.innerHTML
44187
+ html: clipboardDOM(content, schema).innerHTML
44188
+ };
44189
+ }
44190
+ function clipboardSerializer(schema) {
44191
+ const serializer = DOMSerializer.fromSchema(schema);
44192
+ const safe = new DOMSerializer(serializer.nodes, serializer.marks);
44193
+ safe.serializeFragment = (content, options = {}, target) => {
44194
+ const output = target ?? (options.document ?? document).createDocumentFragment();
44195
+ output.append(...clipboardDOM(content, schema).childNodes);
44196
+ return output;
44154
44197
  };
44198
+ return safe;
44155
44199
  }
44156
- var visibleClipboard = $prose(() => new Plugin({
44200
+ var visibleClipboard = $prose((ctx) => new Plugin({
44157
44201
  key: new PluginKey("visibleClipboard"),
44158
- props: { clipboardTextSerializer: (slice) => clipboardText(slice.content) }
44202
+ props: {
44203
+ clipboardSerializer: clipboardSerializer(ctx.get(schemaCtx)),
44204
+ clipboardTextSerializer: (slice) => clipboardText(slice.content)
44205
+ }
44159
44206
  }));
44160
44207
  /** Export a frozen document fragment; presentation URLs never enter the clipboard. */
44161
44208
  async function portableClipboard(content, schema, markdown, adapter, context, completeDiagrams, files, signal = new AbortController().signal) {
@@ -44878,50 +44925,63 @@ function blockDropRange(view, x, y) {
44878
44925
  * callout, where typing would change text the reader can't see or didn't
44879
44926
  * choose.
44880
44927
  */
44881
- function caretAfter(doc, pos) {
44928
+ function caretAfter(doc, pos, folds = /* @__PURE__ */ new Map()) {
44929
+ const hides = (node) => isComment(node) || node.type.name === "inkkit_callout" && (folds.get(node) ?? node.attrs.fold === "-");
44930
+ const concealedDepth = ($pos) => {
44931
+ for (let depth = 1; depth <= $pos.depth; depth++) if (hides($pos.node(depth))) return depth;
44932
+ return -1;
44933
+ };
44934
+ const concealed = ($pos) => concealedDepth($pos) >= 0;
44882
44935
  const $pos = doc.resolve(pos);
44883
44936
  const before = $pos.nodeBefore;
44884
44937
  if (before?.isTextblock && !concealed(doc.resolve(pos - 1))) return TextSelection.create(doc, pos - 1);
44885
- if (GapCursor.valid($pos)) return new GapCursor($pos);
44938
+ const gap = GapCursor;
44939
+ const forward = before && !showsText(before) ? shownText(doc, pos, 1) : null;
44940
+ if (forward) return forward;
44941
+ if (gap.valid($pos)) return new GapCursor($pos);
44886
44942
  const [first, second] = before && !showsText(before) ? [1, -1] : [-1, 1];
44887
44943
  return shownText(doc, pos, first) ?? shownText(doc, pos, second) ?? Selection.near($pos);
44888
- }
44889
- function hides(node) {
44890
- return isComment(node) || node.type.name === "inkkit_callout" && node.attrs.fold === "-";
44891
- }
44892
- function concealedDepth($pos) {
44893
- for (let depth = 1; depth <= $pos.depth; depth++) if (hides($pos.node(depth))) return depth;
44894
- return -1;
44895
- }
44896
- function concealed($pos) {
44897
- return concealedDepth($pos) >= 0;
44898
- }
44899
- function showsText(node) {
44900
- if (hides(node)) return false;
44901
- if (node.isTextblock) return true;
44902
- let found = false;
44903
- node.descendants((child) => {
44904
- if (found || hides(child)) return false;
44905
- if (child.isTextblock) found = true;
44906
- return !found;
44907
- });
44908
- return found;
44909
- }
44910
- function shownText(doc, pos, dir) {
44911
- let $from = doc.resolve(pos);
44912
- for (;;) {
44913
- const found = TextSelection.findFrom($from, dir, true);
44914
- if (!found) return null;
44915
- const depth = concealedDepth(found.$head);
44916
- if (depth < 0) return found;
44917
- $from = doc.resolve(dir > 0 ? found.$head.after(depth) : found.$head.before(depth));
44944
+ function showsText(node) {
44945
+ if (hides(node)) return false;
44946
+ if (node.isTextblock) return true;
44947
+ let found = false;
44948
+ node.descendants((child) => {
44949
+ if (found || hides(child)) return false;
44950
+ if (child.isTextblock) found = true;
44951
+ return !found;
44952
+ });
44953
+ return found;
44918
44954
  }
44955
+ function shownText(doc, pos, dir) {
44956
+ let $from = doc.resolve(pos);
44957
+ for (;;) {
44958
+ const found = TextSelection.findFrom($from, dir, true);
44959
+ if (!found) return null;
44960
+ const depth = concealedDepth(found.$head);
44961
+ if (depth < 0) return found;
44962
+ $from = doc.resolve(dir > 0 ? found.$head.after(depth) : found.$head.before(depth));
44963
+ }
44964
+ }
44965
+ }
44966
+ /** Capture reader folding before dispatch, then map it to the new document. */
44967
+ function calloutFolds(view, tr) {
44968
+ const folds = /* @__PURE__ */ new Map();
44969
+ view.state.doc.descendants((node, pos) => {
44970
+ if (node.type.name !== "inkkit_callout") return;
44971
+ const dom = view.nodeDOM(pos);
44972
+ if (!(dom instanceof HTMLElement)) return;
44973
+ const mapped = tr.mapping.mapResult(pos, 1);
44974
+ if (mapped.deleted) return;
44975
+ const next = tr.doc.nodeAt(mapped.pos);
44976
+ if (next?.type === node.type) folds.set(next, dom.getAttribute("data-inkkit-folded") === "true");
44977
+ });
44978
+ return folds;
44919
44979
  }
44920
44980
  /**
44921
44981
  * Puts the caret just after what the transaction's last step inserted, so
44922
44982
  * typing continues after pasted or dropped content instead of replacing it.
44923
44983
  */
44924
- function caretAfterInsertion(tr) {
44984
+ function caretAfterInsertion(tr, view) {
44925
44985
  const map = tr.mapping.maps[tr.mapping.maps.length - 1];
44926
44986
  let end;
44927
44987
  map?.forEach((_from, _to, _newFrom, newTo) => {
@@ -44929,7 +44989,7 @@ function caretAfterInsertion(tr) {
44929
44989
  });
44930
44990
  if (end == null) return tr;
44931
44991
  const $end = tr.doc.resolve(end);
44932
- return tr.setSelection($end.parent.inlineContent ? TextSelection.create(tr.doc, end) : caretAfter(tr.doc, end));
44992
+ return tr.setSelection($end.parent.inlineContent ? TextSelection.create(tr.doc, end) : caretAfter(tr.doc, end, view && calloutFolds(view, tr)));
44933
44993
  }
44934
44994
  //#endregion
44935
44995
  //#region src/code-fences.ts
@@ -45099,13 +45159,16 @@ function lineBreakHTML(value) {
45099
45159
  "<br/>"
45100
45160
  ].includes(value.trim());
45101
45161
  }
45102
- function restoreInlineBreaks(node) {
45103
- if (node.type === "paragraph" && node.children.length > 1 || node.type === "heading" || node.type === "tableCell") node.children = node.children.map((child) => child.type === "html" && lineBreakHTML(child.value) ? {
45104
- type: "break",
45105
- data: { inkkitHTMLBreak: true },
45106
- position: child.position
45107
- } : child);
45108
- if ("children" in node) node.children.forEach(restoreInlineBreaks);
45162
+ function restoreInlineBreaks(node, inline = false) {
45163
+ const restore = node.type === "paragraph" && node.children.length > 1 || node.type === "heading" || node.type === "tableCell" || inline;
45164
+ if ("children" in node) {
45165
+ node.children = node.children.map((child) => restore && child.type === "html" && lineBreakHTML(child.value) ? {
45166
+ type: "break",
45167
+ data: { inkkitHTMLBreak: true },
45168
+ position: child.position
45169
+ } : child);
45170
+ node.children.forEach((child) => restoreInlineBreaks(child, restore));
45171
+ }
45109
45172
  }
45110
45173
  function containsUnsupported(node, images, inline = false) {
45111
45174
  if (!inline && node.type === "html" && lineBreakHTML(node.value)) return false;
@@ -46010,6 +46073,12 @@ function retainSource(before, after, source, canonical, lineEnding, inTable = fa
46010
46073
  }, source, canonical, lineEnding, inTable);
46011
46074
  const first = before.children[0];
46012
46075
  if (spacer(first)) return retained;
46076
+ const gap = source.slice(first.position.end.offset, before.children[1].position.start.offset);
46077
+ if (!/\r?\n[ \t]*\r?\n/.test(gap)) {
46078
+ const from = first.position.start.offset - start;
46079
+ const to = before.children[1].position.start.offset - start;
46080
+ return retained.startsWith(source.slice(start, start + to)) ? retained.slice(0, from) + retained.slice(to) : retained;
46081
+ }
46013
46082
  const from = first.position.start.offset - start;
46014
46083
  const to = first.position.end.offset - start;
46015
46084
  return retained.startsWith(source.slice(start, before.children[1].position.start.offset)) ? retained.slice(0, from) + "<br />" + retained.slice(to) : retained;
@@ -48145,6 +48214,7 @@ var calloutSchema = $node("inkkit_callout", (ctx) => ({
48145
48214
  group: "block",
48146
48215
  content: "block+",
48147
48216
  defining: true,
48217
+ createGapCursor: true,
48148
48218
  attrs: {
48149
48219
  kind: { default: "NOTE" },
48150
48220
  marker: { default: "[!NOTE]" },
@@ -50190,7 +50260,23 @@ function padShortRows(node) {
50190
50260
  if ("children" in node) node.children.forEach(padShortRows);
50191
50261
  }
50192
50262
  var remarkDialectPlugin = $remark("remarkDialect", () => remarkDialect);
50193
- var commonmarkWithLiteralReferences = commonmark.filter((plugin) => !remarkInlineLinkPlugin.includes(plugin) && plugin !== hardbreakClearMarkPlugin && !hardbreakKeymap.some((part) => part === plugin) && plugin !== syncHeadingIdPlugin && plugin !== createCodeBlockInputRule);
50263
+ var preservingSoftBreaks = $remark("preservingSoftBreaks", () => () => (tree) => {
50264
+ const walk = (parent) => {
50265
+ parent.children = parent.children.flatMap((child) => {
50266
+ if ("children" in child) walk(child);
50267
+ if (child.type !== "text" || !/[\r\n]/.test(child.value)) return [child];
50268
+ return child.value.split(/(\r?\n|\r)/).flatMap((value) => !value ? [] : /^(?:\r?\n|\r)$/.test(value) ? [{
50269
+ type: "break",
50270
+ data: { isInline: true }
50271
+ }] : [{
50272
+ type: "text",
50273
+ value
50274
+ }]);
50275
+ });
50276
+ };
50277
+ walk(tree);
50278
+ });
50279
+ var commonmarkWithLiteralReferences = commonmark.filter((plugin) => !remarkInlineLinkPlugin.includes(plugin) && !remarkLineBreak.includes(plugin) && plugin !== hardbreakClearMarkPlugin && !hardbreakKeymap.some((part) => part === plugin) && plugin !== syncHeadingIdPlugin && plugin !== createCodeBlockInputRule);
50194
50280
  var breaksWithoutMarks = $prose((ctx) => new Plugin({
50195
50281
  key: new PluginKey("inkkitBreaksWithoutMarks"),
50196
50282
  appendTransaction(transactions, oldState, newState) {
@@ -50200,9 +50286,24 @@ var breaksWithoutMarks = $prose((ctx) => new Plugin({
50200
50286
  const end = oldState.doc.content.findDiffEnd(newState.doc.content).b;
50201
50287
  const type = hardbreakSchema.type(ctx);
50202
50288
  const tr = newState.tr;
50289
+ newState.doc.nodesBetween(start, Math.max(start, end), (node, pos) => {
50290
+ if (!node.isTextblock || !node.firstChild) return;
50291
+ let count = 0;
50292
+ for (let index = 0; index < node.childCount; index++) {
50293
+ const child = node.child(index);
50294
+ if (child.type !== type || !child.attrs.isInline || child.attrs.isHTML) break;
50295
+ let previous = pos + 1 + count;
50296
+ for (let transaction = transactions.length - 1; transaction >= 0; transaction--) previous = transactions[transaction].mapping.invert().map(previous, 1);
50297
+ const before = oldState.doc.nodeAt(previous);
50298
+ if (before?.type !== type || !before.attrs.isInline || before.attrs.isHTML || oldState.doc.resolve(previous).parentOffset === 0) break;
50299
+ count += child.nodeSize;
50300
+ }
50301
+ if (count) tr.delete(tr.mapping.map(pos + 1), tr.mapping.map(pos + 1 + count));
50302
+ return false;
50303
+ });
50203
50304
  newState.doc.nodesBetween(start, Math.max(start, end), (node, pos) => {
50204
50305
  if (node.type !== type) return;
50205
- for (const mark of node.marks) if (mark.type.spec.code) tr.removeMark(pos, pos + 1, mark.type);
50306
+ for (const mark of node.marks) if (mark.type.spec.code) tr.removeMark(tr.mapping.map(pos), tr.mapping.map(pos + 1), mark.type);
50206
50307
  });
50207
50308
  return tr.steps.length ? tr : null;
50208
50309
  }
@@ -50464,6 +50565,7 @@ function createDialect(images, wikiLinks = false, files = false) {
50464
50565
  wikiLinks || files ? linkedSyntax(wikiLinks, files) : [],
50465
50566
  createLiteralPreservation(images || files),
50466
50567
  commonmarkWithLiteralReferences,
50568
+ preservingSoftBreaks,
50467
50569
  $node("doc", () => ({
50468
50570
  ...docSchema.schema,
50469
50571
  attrs: {
@@ -51419,7 +51521,8 @@ var ImageView = class {
51419
51521
  return;
51420
51522
  }
51421
51523
  const presentation = state.presentation;
51422
- if (presentation.kind === "missing" || presentation.kind === "error") this.card(label, presentation.kind === "missing" ? this.runtime.labels.fileMissing : this.runtime.labels.fileError, true);
51524
+ if (presentation.kind === "missing") this.card(label, this.runtime.labels.fileMissing, true);
51525
+ else if (presentation.kind === "error") this.card(label, presentation.message || this.runtime.labels.fileError, true);
51423
51526
  else if (presentation.kind === "image") {
51424
51527
  this.renderImage(presentation.url, label, width);
51425
51528
  this.card(label);
@@ -53763,7 +53866,7 @@ function pasteSpreadsheet(ctx, input, at) {
53763
53866
  }
53764
53867
  let grid;
53765
53868
  try {
53766
- grid = input.html?.match(/<table[\s>]/i) ? htmlGrid(view.state.schema, input.html) : input.text.includes(" ") ? textGrid(view.state.schema, input.text) : void 0;
53869
+ grid = input.html?.match(/<table[\s>]/i) ? htmlGrid(view.state.schema, input.html) : !input.html && input.text.includes(" ") && (inTable || looksLikeGrid(input.text)) ? textGrid(view.state.schema, input.text) : void 0;
53767
53870
  } catch (error) {
53768
53871
  if (inTable || !input.html) throw error;
53769
53872
  if (!standaloneTable(input.html)) return false;
@@ -53781,7 +53884,7 @@ function pasteSpreadsheet(ctx, input, at) {
53781
53884
  if (cursor < input.html.length) children.push(schema.text(input.html.slice(cursor)));
53782
53885
  const content = importableSlice(ctx, new Slice(Fragment.from(schema.nodes.literal_markdown.create(null, children)), 0, 0)).content;
53783
53886
  const selection = view.state.selection;
53784
- dispatchTableOperation(view, at ? caretAfterInsertion(view.state.tr.replaceWith(at.from, at.to, content)) : selection.$from.parent.type.name === "paragraph" && selection.empty && selection.$from.parent.content.size === 0 ? view.state.tr.replaceWith(selection.$from.before(), selection.$from.after(), content) : content.childCount === 1 ? view.state.tr.replaceSelectionWith(content.firstChild) : view.state.tr.replaceSelection(new Slice(content, 0, 0)));
53887
+ dispatchTableOperation(view, at ? caretAfterInsertion(view.state.tr.replaceWith(at.from, at.to, content), view) : selection.$from.parent.type.name === "paragraph" && selection.empty && selection.$from.parent.content.size === 0 ? view.state.tr.replaceWith(selection.$from.before(), selection.$from.after(), content) : content.childCount === 1 ? view.state.tr.replaceSelectionWith(content.firstChild) : view.state.tr.replaceSelection(new Slice(content, 0, 0)));
53785
53888
  return true;
53786
53889
  }
53787
53890
  if (!grid) return false;
@@ -53796,7 +53899,7 @@ function pasteSpreadsheet(ctx, input, at) {
53796
53899
  if (node === table) tableStart = pos + 1;
53797
53900
  });
53798
53901
  if (tableStart < 0) reject();
53799
- tr.setSelection(caretAfter(tr.doc, tableStart - 1 + table.nodeSize));
53902
+ tr.setSelection(caretAfter(tr.doc, tableStart - 1 + table.nodeSize, calloutFolds(view, tr)));
53800
53903
  dispatchTableOperation(view, tr);
53801
53904
  return true;
53802
53905
  }
@@ -53841,6 +53944,12 @@ function standaloneTable(html) {
53841
53944
  function reject() {
53842
53945
  throw new InkKitError("preservation", "This spreadsheet cannot be represented safely in a Markdown table. Paste as plain text outside the table to retain its source.");
53843
53946
  }
53947
+ function looksLikeGrid(input) {
53948
+ const rows = input.replace(/\r\n?/g, "\n").replace(/\n$/, "").split("\n");
53949
+ if (rows.length < 2) return false;
53950
+ const widths = rows.map((row) => row.split(" ").length);
53951
+ return widths[0] > 1 && widths.every((width) => width === widths[0]) && rows.some((row) => row.split(" ")[0].trim().length > 0);
53952
+ }
53844
53953
  function textGrid(schema, input) {
53845
53954
  const text = input.replace(/\r\n?/g, "\n").replace(/\n$/, "");
53846
53955
  const rows = [[]];
@@ -54031,7 +54140,10 @@ var PasteController = class {
54031
54140
  return mapped?.operation === operation ? mapped : operation;
54032
54141
  }
54033
54142
  cancelPending() {
54143
+ const operation = this.active;
54144
+ if (!operation) return;
54034
54145
  this.active = void 0;
54146
+ operation.controller.abort();
54035
54147
  this.options.onPendingChanged?.();
54036
54148
  }
54037
54149
  destroy() {
@@ -54043,6 +54155,26 @@ var PasteController = class {
54043
54155
  const current = this.options.context();
54044
54156
  if (this.active !== operation || current.documentId !== context.documentId || current.generation !== context.generation) throw new InkKitError("stale-document", "The document changed while images were importing.");
54045
54157
  }
54158
+ async importImage(image, context, operation) {
54159
+ this.assertContext(context, operation);
54160
+ const signal = operation.controller.signal;
54161
+ let abort;
54162
+ const cancelled = new Promise((_, reject) => {
54163
+ abort = () => reject(new InkKitError(this.dead ? "destroyed" : "stale-document", "The image import was cancelled."));
54164
+ signal.addEventListener("abort", abort, { once: true });
54165
+ });
54166
+ try {
54167
+ return await Promise.race([new Promise((resolve) => {
54168
+ resolve(this.options.adapter.importImage(image, {
54169
+ ...context,
54170
+ signal
54171
+ }));
54172
+ }), cancelled]);
54173
+ } finally {
54174
+ signal.removeEventListener("abort", abort);
54175
+ this.assertContext(context, operation);
54176
+ }
54177
+ }
54046
54178
  commit(apply) {
54047
54179
  return this.options.commit ? this.options.commit(apply) : apply();
54048
54180
  }
@@ -54057,12 +54189,13 @@ var PasteController = class {
54057
54189
  const blocks = Fragment.fromArray(topLevel(view.state.schema, slice.content));
54058
54190
  const tr = view.state.tr.replaceWith(range.from, range.to, blocks);
54059
54191
  const end = tr.mapping.map(range.to);
54060
- this.dispatch(view, tr.setSelection(caretAfter(tr.doc, end)).scrollIntoView());
54192
+ this.dispatch(view, tr.setSelection(caretAfter(tr.doc, end, calloutFolds(view, tr))).scrollIntoView());
54061
54193
  }
54062
54194
  /** Imports the input at the selection, or in place of `target`. */
54063
54195
  async run(input, files = [], target) {
54064
54196
  if (this.dead) throw new InkKitError("destroyed", "The editor was destroyed.");
54065
54197
  if (this.pending) throw new InkKitError("operation-pending", "An image paste is still pending.");
54198
+ input = this.options.restoreCopy?.(input) ?? input;
54066
54199
  const context = { ...this.options.context() };
54067
54200
  const ctx = this.options.ctx();
54068
54201
  const view = ctx.get(editorViewCtx);
@@ -54073,9 +54206,12 @@ var PasteController = class {
54073
54206
  from: exit,
54074
54207
  to: exit
54075
54208
  };
54076
- const operation = target ? { ...target } : {
54077
- from: view.state.selection.from,
54078
- to: view.state.selection.to
54209
+ const operation = {
54210
+ ...target ?? {
54211
+ from: view.state.selection.from,
54212
+ to: view.state.selection.to
54213
+ },
54214
+ controller: new AbortController()
54079
54215
  };
54080
54216
  const inCode = () => view.state.doc.resolve(this.anchor(operation).from).parent.type.spec.code;
54081
54217
  this.active = operation;
@@ -54103,8 +54239,12 @@ var PasteController = class {
54103
54239
  this.dispatch(view, caretAfterInsertion(view.state.tr.insertText(input.text, anchor.from, anchor.to)).scrollIntoView());
54104
54240
  return;
54105
54241
  }
54106
- const paragraphs = input.text.replace(/\r\n?/g, "\n").split("\n").map((line) => schema.nodes.paragraph.create(null, line ? schema.text(line) : void 0));
54107
- slice = new Slice(Fragment.fromArray(paragraphs), 1, 1);
54242
+ const anchor = this.anchor(operation);
54243
+ const start = view.state.doc.resolve(anchor.from);
54244
+ const end = view.state.doc.resolve(anchor.to);
54245
+ const marks = view.state.storedMarks ?? (anchor.from === anchor.to ? start.marks() : start.marksAcross(end)) ?? [];
54246
+ const nodes = literalTextNodes(schema, input.text, marks, htmlBreakAt(start));
54247
+ slice = new Slice(Fragment.from(schema.nodes.paragraph.create(null, nodes)), 1, 1);
54108
54248
  } else if (input.html) {
54109
54249
  const container = visibleContent(ctx, input.html);
54110
54250
  const imported = /* @__PURE__ */ new Map();
@@ -54120,14 +54260,14 @@ var PasteController = class {
54120
54260
  if (!reference) {
54121
54261
  const captured = images.find((image) => image.source === source) ?? dataImage(source) ?? positional[index++];
54122
54262
  if (!captured || !this.options.adapter) throw new Error("The pasted image is unavailable.");
54123
- reference = (await this.options.adapter.importImage(captured, { ...context })).reference;
54263
+ reference = (await this.importImage(captured, context, operation)).reference;
54124
54264
  this.assertContext(context, operation);
54125
54265
  imported.set(source, reference);
54126
54266
  }
54127
54267
  element.setAttribute("src", reference);
54128
54268
  element.removeAttribute("srcset");
54129
54269
  } catch (error) {
54130
- if (error instanceof InkKitError) throw error;
54270
+ if (error instanceof InkKitError && error.code !== "image-unavailable") throw error;
54131
54271
  element.replaceWith(document.createTextNode(`[${alt}: image unavailable]`));
54132
54272
  this.options.onError?.(coded(error, "image-unavailable", error instanceof Error ? error.message : "The pasted image is unavailable."));
54133
54273
  }
@@ -54172,7 +54312,7 @@ var PasteController = class {
54172
54312
  if (input.text) nodes.push(schema.nodes.paragraph.create(null, schema.text(input.text)));
54173
54313
  for (const image of images) try {
54174
54314
  if (!this.options.adapter) throw new InkKitError("image-unavailable", "No image adapter is configured.");
54175
- const imported = await this.options.adapter.importImage(image, { ...context });
54315
+ const imported = await this.importImage(image, context, operation);
54176
54316
  this.assertContext(context, operation);
54177
54317
  const imageNode = schema.nodes.image?.create({
54178
54318
  src: imported.reference,
@@ -54219,8 +54359,8 @@ var PasteController = class {
54219
54359
  });
54220
54360
  return;
54221
54361
  }
54222
- if (anchor.from === anchor.to && insertion.parent.type.name === "paragraph" && insertion.parent.content.size === 0 && slice.content.firstChild?.isBlock && slice.content.firstChild.type.spec.isolating) this.dispatch(view, caretAfterInsertion(view.state.tr.replaceRange(insertion.before(), insertion.after(), new Slice(slice.content, 0, 0))).scrollIntoView());
54223
- else this.dispatch(view, caretAfterInsertion(view.state.tr.replaceRange(anchor.from, anchor.to, source == null ? slice : withEdgeSpaces(slice, source, view.state.doc.resolve(anchor.from), view.state.doc.resolve(anchor.to)))).scrollIntoView());
54362
+ if (anchor.from === anchor.to && insertion.parent.type.name === "paragraph" && insertion.parent.content.size === 0 && slice.content.firstChild?.isBlock && slice.content.firstChild.type.spec.isolating) this.dispatch(view, caretAfterInsertion(view.state.tr.replaceRange(insertion.before(), insertion.after(), new Slice(slice.content, 0, 0)), view).scrollIntoView());
54363
+ else this.dispatch(view, caretAfterInsertion(view.state.tr.replaceRange(anchor.from, anchor.to, source == null ? slice : withEdgeSpaces(slice, source, view.state.doc.resolve(anchor.from), view.state.doc.resolve(anchor.to))), view).scrollIntoView());
54224
54364
  } finally {
54225
54365
  if (this.active === operation) {
54226
54366
  this.active = void 0;
@@ -55095,14 +55235,7 @@ function replacementSlice(state, from, to, value) {
55095
55235
  const nodes = [];
55096
55236
  if (start.parent.type.spec.code) {
55097
55237
  if (value) nodes.push(state.schema.text(value, marks));
55098
- } else {
55099
- const isHTML = htmlBreakAt(start);
55100
- const lines = value.split("\n");
55101
- lines.forEach((line, index) => {
55102
- if (index) nodes.push(state.schema.nodes.hardbreak.create({ isHTML }, null, index === lines.length - 1 && line === "" ? [] : marks));
55103
- if (line) nodes.push(state.schema.text(line, marks));
55104
- });
55105
- }
55238
+ } else nodes.push(...literalTextNodes(state.schema, value, marks, htmlBreakAt(start)));
55106
55239
  return new Slice(Fragment.fromArray(nodes), 0, 0);
55107
55240
  }
55108
55241
  function replaceFormatted(view, search, replacement, preflight, all = false) {
@@ -55229,17 +55362,35 @@ function navigateFormattedHeading(view, requested, context) {
55229
55362
  }
55230
55363
  //#endregion
55231
55364
  //#region src/tasks.ts
55365
+ function onTaskMarker(event) {
55366
+ const target = event.target;
55367
+ if (!(target instanceof HTMLElement) || !target.matches("li[data-item-type=\"task\"]")) return false;
55368
+ const marker = getComputedStyle(target, "::before");
55369
+ const rect = target.getBoundingClientRect();
55370
+ const left = rect.left + parseFloat(marker.left);
55371
+ const top = rect.top + parseFloat(marker.top);
55372
+ return event.clientX >= left && event.clientX <= left + parseFloat(marker.width) && event.clientY >= top && event.clientY <= top + parseFloat(marker.height);
55373
+ }
55232
55374
  var taskListPlugin = $prose(() => new Plugin({
55233
55375
  key: new PluginKey("taskToggle"),
55234
- props: { handleClickOn(view, _pos, node, nodePos, event) {
55235
- if (!view.editable) return false;
55236
- if (node.type.name !== "list_item" || node.attrs.checked == null) return false;
55237
- if (!event.target.matches("li[data-item-type=\"task\"]")) return false;
55238
- view.dispatch(view.state.tr.setNodeMarkup(nodePos, void 0, {
55239
- ...node.attrs,
55240
- checked: !node.attrs.checked
55241
- }));
55242
- return true;
55376
+ props: { handleDOMEvents: {
55377
+ mousedown(view, event) {
55378
+ if (!view.editable || event.button !== 0 || !onTaskMarker(event)) return false;
55379
+ event.preventDefault();
55380
+ return true;
55381
+ },
55382
+ click(view, event) {
55383
+ if (!view.editable || event.button !== 0 || !onTaskMarker(event)) return false;
55384
+ const nodePos = view.posAtDOM(event.target, 0) - 1;
55385
+ const node = view.state.doc.nodeAt(nodePos);
55386
+ if (node?.type.name !== "list_item" || node.attrs.checked == null) return false;
55387
+ event.preventDefault();
55388
+ view.dispatch(view.state.tr.setNodeMarkup(nodePos, void 0, {
55389
+ ...node.attrs,
55390
+ checked: !node.attrs.checked
55391
+ }));
55392
+ return true;
55393
+ }
55243
55394
  } }
55244
55395
  }));
55245
55396
  function listItemsInSelection(view) {
@@ -55929,6 +56080,15 @@ function caretState(state) {
55929
56080
  quoted: quoted($from, $to)
55930
56081
  };
55931
56082
  }
56083
+ function terminalSelection(doc) {
56084
+ const $end = doc.resolve(doc.content.size);
56085
+ const gap = GapCursor;
56086
+ if (isImageBlock(doc.lastChild) && gap.valid($end)) return new GapCursor($end);
56087
+ const text = Selection.findFrom($end, -1, true);
56088
+ if (text) return text;
56089
+ if (gap.valid($end)) return new GapCursor($end);
56090
+ throw new InkKitError("preservation", "Document has no safe writing position");
56091
+ }
55932
56092
  function quoted($from, $to) {
55933
56093
  return $from.blockRange($to, (node) => node.type.name === "blockquote") != null;
55934
56094
  }
@@ -56124,7 +56284,7 @@ var sourceGroupDelay = 500;
56124
56284
  var sourceStepLimit = 100;
56125
56285
  /**
56126
56286
  * The editor. Create it with `InkKitEditor.mount`, load a document, and read it back with
56127
- * `snapshot`. The reference is https://github.com/aicayzer/inkkit/blob/main/docs/api.md.
56287
+ * `snapshot`. The reference is https://inkkit.cyzr.me/reference/editor.
56128
56288
  */
56129
56289
  var InkKitEditor = class InkKitEditor {
56130
56290
  options;
@@ -56141,6 +56301,8 @@ var InkKitEditor = class InkKitEditor {
56141
56301
  typedSteps = [];
56142
56302
  undoneSteps = [];
56143
56303
  formattedUndone = 0;
56304
+ formattedRedoLimit;
56305
+ historyDirection;
56144
56306
  lastSourceInput = 0;
56145
56307
  syncingSource = false;
56146
56308
  mode = "formatted";
@@ -56158,6 +56320,52 @@ var InkKitEditor = class InkKitEditor {
56158
56320
  auxClickHandler;
56159
56321
  untrackLinkModifier;
56160
56322
  footnoteKeyHandler;
56323
+ localCopies = /* @__PURE__ */ new Map();
56324
+ rememberCopy(output, markdown, content, privateSource = false, actualCopy = false) {
56325
+ let comments = privateSource;
56326
+ content.descendants((node) => {
56327
+ const labelled = (label) => typeof label === "string" && literalCommentSpans(label).length > 0;
56328
+ if (isComment(node) || /^(?:reference|footnote)_/.test(node.type.name) && labelled(node.attrs.label) || node.marks.some((mark) => mark.type.name === "link" && (labelled(mark.attrs.label) || commentProvenance(mark.attrs.referenceContent)))) comments = true;
56329
+ });
56330
+ if (!comments) {
56331
+ if (actualCopy) this.localCopies.clear();
56332
+ return output;
56333
+ }
56334
+ const key = JSON.stringify([
56335
+ output.text,
56336
+ output.html,
56337
+ markdown
56338
+ ]);
56339
+ const previous = this.localCopies.get(key);
56340
+ if (previous) {
56341
+ output.html = previous.html;
56342
+ if (actualCopy) this.localCopies.clear();
56343
+ this.localCopies.set(key, previous);
56344
+ return output;
56345
+ }
56346
+ const wrapper = document.createElement("div");
56347
+ const token = Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, "0")).join("");
56348
+ wrapper.setAttribute("data-inkkit-local-copy", token);
56349
+ wrapper.innerHTML = output.html;
56350
+ output.html = wrapper.outerHTML;
56351
+ if (actualCopy) this.localCopies.clear();
56352
+ this.localCopies.set(key, {
56353
+ text: output.text,
56354
+ html: output.html,
56355
+ markdown
56356
+ });
56357
+ return output;
56358
+ }
56359
+ retainCopy(html) {
56360
+ for (const [key, copy] of this.localCopies) if (copy.html !== html) this.localCopies.delete(key);
56361
+ }
56362
+ restoreCopy(input) {
56363
+ const copy = [...this.localCopies.values()].find((copy) => copy.text === input.text && copy.html === input.html);
56364
+ return copy && input.markdown == null && !input.plainText && copy.text === input.text && copy.html === input.html ? {
56365
+ ...input,
56366
+ markdown: copy.markdown
56367
+ } : input;
56368
+ }
56161
56369
  copyHandler;
56162
56370
  originalSource = "";
56163
56371
  reportingSuppressed = false;
@@ -56373,7 +56581,9 @@ var InkKitEditor = class InkKitEditor {
56373
56581
  revision: this.session.mark(),
56374
56582
  lastMarkdown: this.lastMarkdown,
56375
56583
  literalMatch: this.literalMatch,
56376
- textScope: this.textScope
56584
+ textScope: this.textScope,
56585
+ formattedRedoLimit: this.formattedRedoLimit,
56586
+ formattedUndone: this.formattedUndone
56377
56587
  };
56378
56588
  }
56379
56589
  finish(call, before, failure) {
@@ -56395,6 +56605,8 @@ var InkKitEditor = class InkKitEditor {
56395
56605
  this.lastMarkdown = before.lastMarkdown;
56396
56606
  this.literalMatch = before.literalMatch;
56397
56607
  this.textScope = before.textScope;
56608
+ this.formattedRedoLimit = before.formattedRedoLimit;
56609
+ this.formattedUndone = before.formattedUndone;
56398
56610
  }
56399
56611
  /** Reports a refused edit to the facade call that made it, or else to `events.error`. */
56400
56612
  refuse(error) {
@@ -56535,7 +56747,7 @@ var InkKitEditor = class InkKitEditor {
56535
56747
  table: this.literalSurface ? void 0 : tableContext(state),
56536
56748
  commands: {
56537
56749
  undo: mutable && (!!this.pendingSource || undo(state)),
56538
- redo: mutable && (this.undoneSteps.length > 0 || !this.pendingSource && redo(state)),
56750
+ redo: mutable && (this.undoneSteps.length > 0 || !this.pendingSource && this.formattedRedoLimit !== 0 && redo(state)),
56539
56751
  insertText: mutable,
56540
56752
  paste: mutable,
56541
56753
  replace: mutable,
@@ -56705,7 +56917,9 @@ var InkKitEditor = class InkKitEditor {
56705
56917
  this.assertAlive();
56706
56918
  expectString(text, "text");
56707
56919
  this.assertMutation(expectedGeneration);
56708
- return this.applySource(text, true);
56920
+ const changed = this.applySource(text, true);
56921
+ if (changed) this.localCopies.clear();
56922
+ return changed;
56709
56923
  }
56710
56924
  applySource(text, isolated, from, to, native = false) {
56711
56925
  if (!this.writable) throw new InkKitError("read-only", "The editor is read-only");
@@ -56724,6 +56938,7 @@ var InkKitEditor = class InkKitEditor {
56724
56938
  const previous = this.pendingSource;
56725
56939
  const continuing = previous && !previous.closed && !closed && now - this.lastSourceInput <= sourceGroupDelay;
56726
56940
  if (previous && !continuing) this.rememberTypedStep(previous);
56941
+ this.formattedRedoLimit = 0;
56727
56942
  this.lastSourceInput = now;
56728
56943
  this.undoneSteps = [];
56729
56944
  this.formattedUndone = 0;
@@ -56851,7 +57066,7 @@ var InkKitEditor = class InkKitEditor {
56851
57066
  }
56852
57067
  }
56853
57068
  let tr = view.state.tr.replaceWith(0, view.state.doc.content.size, parsed.content).setDocAttribute(sourceAttribute, provenance).setMeta(sourceReplacement, true);
56854
- tr.setSelection(Selection.atEnd(tr.doc));
57069
+ tr.setSelection(terminalSelection(tr.doc));
56855
57070
  if (isolated) tr = closeHistory(tr);
56856
57071
  view.dispatch(tr);
56857
57072
  if (isolated) view.dispatch(closeHistory(view.state.tr).setMeta("addToHistory", false));
@@ -56862,7 +57077,7 @@ var InkKitEditor = class InkKitEditor {
56862
57077
  this.assertMutation(expectedGeneration);
56863
57078
  if (this.literalSurface && this.stepTypedSource("undo")) return true;
56864
57079
  const view = this.editor.ctx.get(editorViewCtx);
56865
- const undone = !this.literalSurface && undoSmartPunctuation(view.state, view.dispatch) || undo(view.state, view.dispatch);
57080
+ const undone = !this.literalSurface && undoSmartPunctuation(view.state, view.dispatch) || this.stepFormattedHistory("undo");
56866
57081
  if (undone) {
56867
57082
  if (this.literalSurface && this.undoneSteps.length) this.formattedUndone++;
56868
57083
  else this.undoneSteps = [];
@@ -56875,14 +57090,30 @@ var InkKitEditor = class InkKitEditor {
56875
57090
  const view = this.editor.ctx.get(editorViewCtx);
56876
57091
  if (this.literalSurface) {
56877
57092
  if (this.formattedUndone > 0) {
56878
- this.formattedUndone--;
56879
- if (redo(view.state, view.dispatch)) return true;
57093
+ if (this.formattedRedoLimit !== 0 && redo(view.state)) {
57094
+ const redone = this.stepFormattedHistory("redo");
57095
+ if (redone) this.formattedUndone--;
57096
+ return redone;
57097
+ }
56880
57098
  this.formattedUndone = 0;
56881
57099
  }
56882
57100
  if (this.stepTypedSource("redo")) return true;
56883
57101
  if (this.pendingSource) return false;
56884
57102
  }
56885
- return redo(view.state, view.dispatch);
57103
+ return this.stepFormattedHistory("redo");
57104
+ }
57105
+ stepFormattedHistory(direction) {
57106
+ if (direction === "redo" && this.formattedRedoLimit === 0) return false;
57107
+ const view = this.editor.ctx.get(editorViewCtx);
57108
+ const before = view.state;
57109
+ const previous = this.historyDirection;
57110
+ this.historyDirection = direction;
57111
+ try {
57112
+ (direction === "undo" ? undo : redo)(before, view.dispatch);
57113
+ return view.state !== before;
57114
+ } finally {
57115
+ this.historyDirection = previous;
57116
+ }
56886
57117
  }
56887
57118
  replaceLiteralSelection(text, isolated = true, retainEndings = false) {
56888
57119
  const value = this.plain.value;
@@ -56985,16 +57216,22 @@ var InkKitEditor = class InkKitEditor {
56985
57216
  const metadata = referenceMetadata(this.editor.ctx, selectedDoc ?? doc, content);
56986
57217
  if (metadata != null) result.html = withReferenceMetadata(result.html, metadata);
56987
57218
  this.assertCaptured(captured);
57219
+ this.rememberCopy(result, markdown, content, selectedSource != null && selectedSource.markdown !== selectedSource.shareable);
56988
57220
  return {
56989
57221
  ...version,
56990
57222
  ...result
56991
57223
  };
56992
57224
  }
57225
+ /** Cancels the pending image paste without changing the document or editability. */
57226
+ cancelPendingImports(expectedGeneration) {
57227
+ this.assertReady(expectedGeneration);
57228
+ this.pasteController.cancelPending();
57229
+ }
56993
57230
  /** Pastes content the host read from the native clipboard. Capture it once; don't read the
56994
57231
  * clipboard again while this runs. */
56995
57232
  async paste(input, expectedGeneration) {
56996
57233
  this.assertAlive();
56997
- input = clipboardInput(input);
57234
+ input = this.restoreCopy(clipboardInput(input));
56998
57235
  this.assertMutation(expectedGeneration);
56999
57236
  if (this.literalSurface) {
57000
57237
  this.pasteAsPlainText(this.mode === "source" && this.formatType === "md" ? input.markdown ?? input.text : input.text);
@@ -57027,14 +57264,9 @@ var InkKitEditor = class InkKitEditor {
57027
57264
  else {
57028
57265
  const { schema } = view.state;
57029
57266
  const marks = view.state.storedMarks ?? (view.state.selection.empty ? view.state.selection.$from.marks() : view.state.selection.$from.marksAcross(view.state.selection.$to)) ?? [];
57030
- const children = [];
57031
57267
  const isHTML = !inHiddenComment && htmlBreakAt(view.state.selection.$from);
57032
57268
  const lines = text.replace(/\r\n?/g, "\n").split("\n");
57033
- lines.forEach((line, index) => {
57034
- const finalBreak = index === lines.length - 1 && line === "";
57035
- if (index) children.push(schema.nodes.hardbreak.create({ isHTML }, null, finalBreak ? [] : marks));
57036
- if (line) children.push(schema.text(line, marks));
57037
- });
57269
+ const children = literalTextNodes(schema, text, marks, isHTML);
57038
57270
  if (!children.length) {
57039
57271
  view.dispatch(view.state.tr.deleteSelection());
57040
57272
  return;
@@ -57077,12 +57309,14 @@ var InkKitEditor = class InkKitEditor {
57077
57309
  async destroy() {
57078
57310
  if (this.destroyed) return;
57079
57311
  this.destroyed = true;
57312
+ this.localCopies.clear();
57080
57313
  this.ready = false;
57081
57314
  this.session.close();
57082
57315
  this.pendingSource = void 0;
57083
57316
  this.typedSteps = [];
57084
57317
  this.undoneSteps = [];
57085
57318
  this.formattedUndone = 0;
57319
+ this.formattedRedoLimit = void 0;
57086
57320
  this.cancelExports();
57087
57321
  this.mediaPolicy();
57088
57322
  this.pasteController.destroy();
@@ -57122,6 +57356,7 @@ var InkKitEditor = class InkKitEditor {
57122
57356
  if (portable && this.events.clipboard) this.clipboardSnapshot("selection").then(async (content) => {
57123
57357
  if (cut) refuseUnportableCut(content);
57124
57358
  await writeClipboard(this.events, content);
57359
+ this.retainCopy(content.html);
57125
57360
  if (cut) {
57126
57361
  this.assertCut(captured);
57127
57362
  this.plain.setSelectionRange(from, to);
@@ -57131,6 +57366,7 @@ var InkKitEditor = class InkKitEditor {
57131
57366
  else {
57132
57367
  const output = clipboardContent(parsed.content, parsed.type.schema);
57133
57368
  if (!event.clipboardData) return;
57369
+ this.rememberCopy(output, selected.markdown, parsed.content, selected.markdown !== selected.shareable, true);
57134
57370
  event.clipboardData.setData("text/plain", output.text);
57135
57371
  event.clipboardData.setData("text/html", output.html);
57136
57372
  if (portable) {
@@ -57155,6 +57391,7 @@ var InkKitEditor = class InkKitEditor {
57155
57391
  instance.ownsRootClass = !root.classList.contains("inkkit-root");
57156
57392
  root.classList.add("inkkit-root");
57157
57393
  instance.pasteController = new PasteController({
57394
+ restoreCopy: (input) => instance.restoreCopy(input),
57158
57395
  ctx: () => instance.editor.ctx,
57159
57396
  context: () => instance.context(),
57160
57397
  adapter: options.images,
@@ -57258,6 +57495,14 @@ var InkKitEditor = class InkKitEditor {
57258
57495
  return;
57259
57496
  }
57260
57497
  }
57498
+ if (instance.formattedRedoLimit !== void 0) {
57499
+ const depth = redoDepth(next);
57500
+ if (depth === 0) instance.formattedRedoLimit = void 0;
57501
+ else if (next !== this.state) {
57502
+ if (instance.historyDirection === "undo") instance.formattedRedoLimit = Math.min(depth, instance.formattedRedoLimit + 1);
57503
+ else if (instance.historyDirection === "redo") instance.formattedRedoLimit = Math.max(0, instance.formattedRedoLimit - 1);
57504
+ }
57505
+ }
57261
57506
  this.updateState(next);
57262
57507
  },
57263
57508
  handleScrollToSelection: () => {
@@ -57265,7 +57510,7 @@ var InkKitEditor = class InkKitEditor {
57265
57510
  return true;
57266
57511
  }
57267
57512
  }));
57268
- }).use(labelsCtx).use(instance.pasteController.plugin).use(visibleClipboard).use($prose(() => instance.suggestions.plugin())).use(instance.keymapPlugin).use(createDialect(Boolean(options.images || options.files), Boolean(options.wikiLinks), Boolean(options.files))).use(codeControlCtx).use(codeControlsPlugin(() => instance.viewport().rect)).use(tablePlugins).use(tableBlock).use(instance.changePlugin).use(instance.historyInputPlugin).use(history).use(clipboard).use(cursor).use(taskListPlugin).use(quoteBackspace).use(calloutEnter).use(literalEnter).use(literalReread).use(listItemBackspace).use(listItemShiftTab).use(nestedItemEnter).use(rejoinLists).use(movedListItems).use(textInputPolicy).use(events.copy ? codeCopyPlugin((text) => events.copy?.(text)) : []).use(placeholderPlugin).use(options.rendering?.codeHighlighting === false ? [] : (await import("./highlight-CeNqcHAb.js")).highlightPlugin).use(options.rendering?.diagramPreview === false || !diagramsAvailable() ? [] : mermaidPreview((error) => instance.report(error))).use(selectionPlugin).use($prose(() => search())).use(instance.policyPlugin).use(options.wikiLinks ? wikiLinkView(options.wikiLinks, {
57513
+ }).use(labelsCtx).use(instance.pasteController.plugin).use(visibleClipboard).use($prose(() => instance.suggestions.plugin())).use(instance.keymapPlugin).use(createDialect(Boolean(options.images || options.files), Boolean(options.wikiLinks), Boolean(options.files))).use(codeControlCtx).use(codeControlsPlugin(() => instance.viewport().rect)).use(tablePlugins).use(tableBlock).use(instance.changePlugin).use(instance.historyInputPlugin).use(history).use(clipboard).use(cursor).use(taskListPlugin).use(quoteBackspace).use(calloutEnter).use(literalEnter).use(literalReread).use(listItemBackspace).use(listItemShiftTab).use(nestedItemEnter).use(rejoinLists).use(movedListItems).use(textInputPolicy).use(events.copy ? codeCopyPlugin((text) => events.copy?.(text)) : []).use(placeholderPlugin).use(options.rendering?.codeHighlighting === false ? [] : (await import("./highlight-AI86evan.js")).highlightPlugin).use(options.rendering?.diagramPreview === false || !diagramsAvailable() ? [] : mermaidPreview((error) => instance.report(error))).use(selectionPlugin).use($prose(() => search())).use(instance.policyPlugin).use(options.wikiLinks ? wikiLinkView(options.wikiLinks, {
57269
57514
  capture: () => instance.adapterCapture(),
57270
57515
  active: () => instance.ready && !instance.destroyed && !instance.literalSurface,
57271
57516
  labels: {
@@ -57345,6 +57590,7 @@ var InkKitEditor = class InkKitEditor {
57345
57590
  event.preventDefault();
57346
57591
  instance.paste({
57347
57592
  text: event.clipboardData.getData("text/plain"),
57593
+ html: event.clipboardData.getData("text/html"),
57348
57594
  markdown: event.clipboardData.getData("text/markdown") || void 0
57349
57595
  }).catch((error) => instance.report(error));
57350
57596
  });
@@ -57414,11 +57660,13 @@ var InkKitEditor = class InkKitEditor {
57414
57660
  instance.snapshot();
57415
57661
  const content = expanded;
57416
57662
  const valid = fragment.firstChild?.isInline ? Fragment.from(view.state.schema.nodes.paragraph.create(null, fragment)) : fragment;
57417
- selectionMarkdown(instance.editor.ctx, view.state.doc, valid);
57663
+ const markdown = selectionMarkdown(instance.editor.ctx, view.state.doc, valid);
57418
57664
  const output = clipboardContent(content, view.state.schema);
57419
- event.clipboardData.setData("text/plain", output.text);
57420
57665
  const metadata = referenceMetadata(instance.editor.ctx, view.state.doc, valid);
57421
- event.clipboardData.setData("text/html", metadata != null ? withReferenceMetadata(output.html, metadata) : output.html);
57666
+ if (metadata != null) output.html = withReferenceMetadata(output.html, metadata);
57667
+ instance.rememberCopy(output, markdown, fragment, false, true);
57668
+ event.clipboardData.setData("text/plain", output.text);
57669
+ event.clipboardData.setData("text/html", output.html);
57422
57670
  if (event.type === "cut" && instance.writable) view.dispatch(view.state.tr.deleteSelection().scrollIntoView());
57423
57671
  } catch (error) {
57424
57672
  instance.report(error);
@@ -57434,6 +57682,7 @@ var InkKitEditor = class InkKitEditor {
57434
57682
  instance.clipboardSnapshot("selection").then(async (content) => {
57435
57683
  if (cut) refuseUnportableCut(content);
57436
57684
  await writeClipboard(events, content);
57685
+ instance.retainCopy(content.html);
57437
57686
  if (cut) {
57438
57687
  instance.assertCut(captured);
57439
57688
  view.dispatch(view.state.tr.deleteRange(from, to).scrollIntoView());
@@ -57481,6 +57730,7 @@ var InkKitEditor = class InkKitEditor {
57481
57730
  this.typedSteps = [];
57482
57731
  this.undoneSteps = [];
57483
57732
  this.formattedUndone = 0;
57733
+ this.formattedRedoLimit = void 0;
57484
57734
  this.mode = "formatted";
57485
57735
  this.plainComposing = false;
57486
57736
  this.beforePlainInput = void 0;
@@ -57860,6 +58110,7 @@ var InkKitEditor = class InkKitEditor {
57860
58110
  }])[0]);
57861
58111
  }
57862
58112
  load(markdown, preservation) {
58113
+ this.localCopies.clear();
57863
58114
  this.originalSource = markdown;
57864
58115
  const { ctx } = this.editor;
57865
58116
  const view = ctx.get(editorViewCtx);
@@ -57876,9 +58127,7 @@ var InkKitEditor = class InkKitEditor {
57876
58127
  this.preservation = preservation;
57877
58128
  this.carried = void 0;
57878
58129
  this.lastMarkdown = markdown;
57879
- const { doc } = view.state;
57880
- const $end = doc.resolve(doc.content.size);
57881
- const end = isImageBlock(doc.lastChild) ? new GapCursor($end) : Selection.findFrom($end, -1, true) ?? Selection.atEnd(doc);
58130
+ const end = terminalSelection(view.state.doc);
57882
58131
  view.dispatch(setSearchState(view.state.tr.setSelection(end), new SearchQuery({ search: "" })));
57883
58132
  }
57884
58133
  /** Selects the next match and returns whether there was one. */
@@ -58607,6 +58856,7 @@ var InkKitEditor = class InkKitEditor {
58607
58856
  "printableSnapshot",
58608
58857
  "clipboardSnapshot",
58609
58858
  "paste",
58859
+ "cancelPendingImports",
58610
58860
  "pasteAsPlainText",
58611
58861
  "commandState",
58612
58862
  "setEditable",
@@ -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-nIObsA9D.js";
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-C612uj1Q.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,5 +1,5 @@
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, EditorLabels, Heading, TableCommand, TableOptions, BlockMoveDirection, DocumentContext, DocumentInput, DocumentSnapshot, DocumentVersion, EditorErrorCode, ImageAdapter, 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 type { DocumentFormat, 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';
5
5
  export type { SlashMenuOptions, SlashCommandName, SlashCommand } from './types.js';
package/dist/core.js CHANGED
@@ -1,2 +1,2 @@
1
- import { r as InkKitError, t as InkKitEditor } from "./chunks/core-nIObsA9D.js";
1
+ import { r as InkKitError, t as InkKitEditor } from "./chunks/core-C612uj1Q.js";
2
2
  export { InkKitEditor, InkKitError };
package/dist/editor.d.ts CHANGED
@@ -143,7 +143,7 @@ export interface EditorEvents {
143
143
  export declare const editorEvents: readonly ["changed", "commandStateChanged", "openLink", "copy", "error", "clipboard"];
144
144
  /**
145
145
  * The editor. Create it with `InkKitEditor.mount`, load a document, and read it back with
146
- * `snapshot`. The reference is https://github.com/aicayzer/inkkit/blob/main/docs/api.md.
146
+ * `snapshot`. The reference is https://inkkit.cyzr.me/reference/editor.
147
147
  */
148
148
  export declare class InkKitEditor {
149
149
  private readonly options;
@@ -160,6 +160,8 @@ export declare class InkKitEditor {
160
160
  private typedSteps;
161
161
  private undoneSteps;
162
162
  private formattedUndone;
163
+ private formattedRedoLimit?;
164
+ private historyDirection?;
163
165
  private lastSourceInput;
164
166
  private syncingSource;
165
167
  private mode;
@@ -177,6 +179,10 @@ export declare class InkKitEditor {
177
179
  private auxClickHandler?;
178
180
  private untrackLinkModifier?;
179
181
  private footnoteKeyHandler?;
182
+ private readonly localCopies;
183
+ private rememberCopy;
184
+ private retainCopy;
185
+ private restoreCopy;
180
186
  private copyHandler?;
181
187
  private originalSource;
182
188
  private reportingSuppressed;
@@ -298,6 +304,7 @@ export declare class InkKitEditor {
298
304
  undo(expectedGeneration?: number): boolean;
299
305
  /** Returns whether a history step applied. */
300
306
  redo(expectedGeneration?: number): boolean;
307
+ private stepFormattedHistory;
301
308
  private replaceLiteralSelection;
302
309
  /** The complete current text, which is what a host saves. It throws rather than return partial
303
310
  * text. */
@@ -314,6 +321,8 @@ export declare class InkKitEditor {
314
321
  /** Prepares the document or the selection for the native clipboard. Rejects with `stale-document`
315
322
  * if the document changes first. */
316
323
  clipboardSnapshot(scope?: ClipboardScope, expectedGeneration?: number): Promise<ClipboardOutput>;
324
+ /** Cancels the pending image paste without changing the document or editability. */
325
+ cancelPendingImports(expectedGeneration?: number): void;
317
326
  /** Pastes content the host read from the native clipboard. Capture it once; don't read the
318
327
  * clipboard again while this runs. */
319
328
  paste(input: ClipboardInput, expectedGeneration?: number): Promise<void>;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as registerMermaid, r as InkKitError, t as InkKitEditor } from "./chunks/core-nIObsA9D.js";
1
+ import { n as registerMermaid, r as InkKitError, t as InkKitEditor } from "./chunks/core-C612uj1Q.js";
2
2
  //#region src/index.ts
3
3
  registerMermaid(async () => (await import("mermaid")).default);
4
4
  //#endregion
package/dist/style.css CHANGED
@@ -154,9 +154,11 @@
154
154
  transform: translate(-50%, -50%);
155
155
  border-radius: 4px;
156
156
  box-shadow: 0 0 0 1px var(--_inkkit-rule);
157
- cursor: grab;
158
157
  touch-action: none;
159
158
  }
159
+ .inkkit-root .inkkit-table-controls .inkkit-table-grip:not(:disabled) {
160
+ cursor: grab;
161
+ }
160
162
  .inkkit-root .inkkit-table-grip[data-inkkit-axis='column'] {
161
163
  width: 24px;
162
164
  height: 14px;
@@ -252,6 +254,10 @@
252
254
  outline-offset: 2px;
253
255
  border-radius: 6px;
254
256
  }
257
+ /* A gap or node selection draws its own indicator, not a native text caret. */
258
+ .inkkit-root .ProseMirror.ProseMirror-hideselection {
259
+ caret-color: transparent;
260
+ }
255
261
  .inkkit-root .ProseMirror-gapcursor {
256
262
  display: none;
257
263
  pointer-events: none;
package/dist/types.d.ts CHANGED
@@ -51,6 +51,10 @@ export interface PortableImage {
51
51
  mimeType: string;
52
52
  filename?: string;
53
53
  }
54
+ /** The document and cancellation signal for one image import. */
55
+ export interface ImageImportContext extends DocumentContext {
56
+ signal: AbortSignal;
57
+ }
54
58
  /** Stores, shows and exports the images a host owns. References stay opaque to the editor. */
55
59
  export interface ImageAdapter {
56
60
  /** The display URL for `reference`, or `undefined` when the image is unavailable. */
@@ -58,7 +62,7 @@ export interface ImageAdapter {
58
62
  url: string;
59
63
  } | undefined;
60
64
  /** Stores the bytes and returns the reference to write into the Markdown. */
61
- importImage(input: CapturedImage, context: DocumentContext): Promise<{
65
+ importImage(input: CapturedImage, context: ImageImportContext): Promise<{
62
66
  reference: string;
63
67
  }>;
64
68
  /** The stored bytes and type of `reference`, for portable clipboard and print output. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicayzer/inkkit",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "A preserving Markdown editor for offline web views and the web",
5
5
  "license": "MIT",
6
6
  "type": "module",