@aicayzer/inkkit 0.1.6 → 0.1.7

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,4 +1,4 @@
1
- # InkKit
1
+ # Inkkit
2
2
 
3
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.
4
4
 
@@ -13,10 +13,10 @@ A Markdown editor for the web and for apps that host a web view. It edits format
13
13
 
14
14
  <picture>
15
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.">
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.">
17
17
  </picture>
18
18
 
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.
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
20
 
21
21
  For the exact guarantees, see [Preservation](https://github.com/aicayzer/inkkit/blob/main/docs/preservation.md).
22
22
 
@@ -30,7 +30,7 @@ An app that does not render diagrams can import from `@aicayzer/inkkit/core` ins
30
30
 
31
31
  ## Supported engines
32
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.
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
34
 
35
35
  ## Use
36
36
 
@@ -55,7 +55,7 @@ editor.loadDocument({
55
55
  const { text } = editor.snapshot(1)
56
56
  ```
57
57
 
58
- ## Built with InkKit
58
+ ## Built with Inkkit
59
59
 
60
60
  - [Memos](https://github.com/aicayzer/memos): a small Mac app for memos, in one floating window
61
61
  - [PadPad](https://github.com/aicayzer/padpad): a small Mac app for quick text and files
@@ -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
  */
@@ -44118,7 +44148,7 @@ function nodeText(node, missingImages, fileDescriptions) {
44118
44148
  }
44119
44149
  return clipboardText(node.content, missingImages, fileDescriptions);
44120
44150
  }
44121
- function clipboardContent(content, schema) {
44151
+ function clipboardDOM(content, schema) {
44122
44152
  content = shareableFragment(content);
44123
44153
  const container = document.createElement("div");
44124
44154
  const serializer = DOMSerializer.fromSchema(schema);
@@ -44148,14 +44178,30 @@ function clipboardContent(content, schema) {
44148
44178
  label.textContent = `[${definition.getAttribute("data-inkkit-label")}] `;
44149
44179
  definition.prepend(label);
44150
44180
  }
44181
+ return container;
44182
+ }
44183
+ function clipboardContent(content, schema) {
44151
44184
  return {
44152
44185
  text: clipboardText(content),
44153
- html: container.innerHTML
44186
+ html: clipboardDOM(content, schema).innerHTML
44154
44187
  };
44155
44188
  }
44156
- var visibleClipboard = $prose(() => new Plugin({
44189
+ function clipboardSerializer(schema) {
44190
+ const serializer = DOMSerializer.fromSchema(schema);
44191
+ const safe = new DOMSerializer(serializer.nodes, serializer.marks);
44192
+ safe.serializeFragment = (content, options = {}, target) => {
44193
+ const output = target ?? (options.document ?? document).createDocumentFragment();
44194
+ output.append(...clipboardDOM(content, schema).childNodes);
44195
+ return output;
44196
+ };
44197
+ return safe;
44198
+ }
44199
+ var visibleClipboard = $prose((ctx) => new Plugin({
44157
44200
  key: new PluginKey("visibleClipboard"),
44158
- props: { clipboardTextSerializer: (slice) => clipboardText(slice.content) }
44201
+ props: {
44202
+ clipboardSerializer: clipboardSerializer(ctx.get(schemaCtx)),
44203
+ clipboardTextSerializer: (slice) => clipboardText(slice.content)
44204
+ }
44159
44205
  }));
44160
44206
  /** Export a frozen document fragment; presentation URLs never enter the clipboard. */
44161
44207
  async function portableClipboard(content, schema, markdown, adapter, context, completeDiagrams, files, signal = new AbortController().signal) {
@@ -45099,13 +45145,16 @@ function lineBreakHTML(value) {
45099
45145
  "<br/>"
45100
45146
  ].includes(value.trim());
45101
45147
  }
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);
45148
+ function restoreInlineBreaks(node, inline = false) {
45149
+ const restore = node.type === "paragraph" && node.children.length > 1 || node.type === "heading" || node.type === "tableCell" || inline;
45150
+ if ("children" in node) {
45151
+ node.children = node.children.map((child) => restore && child.type === "html" && lineBreakHTML(child.value) ? {
45152
+ type: "break",
45153
+ data: { inkkitHTMLBreak: true },
45154
+ position: child.position
45155
+ } : child);
45156
+ node.children.forEach((child) => restoreInlineBreaks(child, restore));
45157
+ }
45109
45158
  }
45110
45159
  function containsUnsupported(node, images, inline = false) {
45111
45160
  if (!inline && node.type === "html" && lineBreakHTML(node.value)) return false;
@@ -46010,6 +46059,12 @@ function retainSource(before, after, source, canonical, lineEnding, inTable = fa
46010
46059
  }, source, canonical, lineEnding, inTable);
46011
46060
  const first = before.children[0];
46012
46061
  if (spacer(first)) return retained;
46062
+ const gap = source.slice(first.position.end.offset, before.children[1].position.start.offset);
46063
+ if (!/\r?\n[ \t]*\r?\n/.test(gap)) {
46064
+ const from = first.position.start.offset - start;
46065
+ const to = before.children[1].position.start.offset - start;
46066
+ return retained.startsWith(source.slice(start, start + to)) ? retained.slice(0, from) + retained.slice(to) : retained;
46067
+ }
46013
46068
  const from = first.position.start.offset - start;
46014
46069
  const to = first.position.end.offset - start;
46015
46070
  return retained.startsWith(source.slice(start, before.children[1].position.start.offset)) ? retained.slice(0, from) + "<br />" + retained.slice(to) : retained;
@@ -50190,7 +50245,23 @@ function padShortRows(node) {
50190
50245
  if ("children" in node) node.children.forEach(padShortRows);
50191
50246
  }
50192
50247
  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);
50248
+ var preservingSoftBreaks = $remark("preservingSoftBreaks", () => () => (tree) => {
50249
+ const walk = (parent) => {
50250
+ parent.children = parent.children.flatMap((child) => {
50251
+ if ("children" in child) walk(child);
50252
+ if (child.type !== "text" || !/[\r\n]/.test(child.value)) return [child];
50253
+ return child.value.split(/(\r?\n|\r)/).flatMap((value) => !value ? [] : /^(?:\r?\n|\r)$/.test(value) ? [{
50254
+ type: "break",
50255
+ data: { isInline: true }
50256
+ }] : [{
50257
+ type: "text",
50258
+ value
50259
+ }]);
50260
+ });
50261
+ };
50262
+ walk(tree);
50263
+ });
50264
+ var commonmarkWithLiteralReferences = commonmark.filter((plugin) => !remarkInlineLinkPlugin.includes(plugin) && !remarkLineBreak.includes(plugin) && plugin !== hardbreakClearMarkPlugin && !hardbreakKeymap.some((part) => part === plugin) && plugin !== syncHeadingIdPlugin && plugin !== createCodeBlockInputRule);
50194
50265
  var breaksWithoutMarks = $prose((ctx) => new Plugin({
50195
50266
  key: new PluginKey("inkkitBreaksWithoutMarks"),
50196
50267
  appendTransaction(transactions, oldState, newState) {
@@ -50200,9 +50271,24 @@ var breaksWithoutMarks = $prose((ctx) => new Plugin({
50200
50271
  const end = oldState.doc.content.findDiffEnd(newState.doc.content).b;
50201
50272
  const type = hardbreakSchema.type(ctx);
50202
50273
  const tr = newState.tr;
50274
+ newState.doc.nodesBetween(start, Math.max(start, end), (node, pos) => {
50275
+ if (!node.isTextblock || !node.firstChild) return;
50276
+ let count = 0;
50277
+ for (let index = 0; index < node.childCount; index++) {
50278
+ const child = node.child(index);
50279
+ if (child.type !== type || !child.attrs.isInline || child.attrs.isHTML) break;
50280
+ let previous = pos + 1 + count;
50281
+ for (let transaction = transactions.length - 1; transaction >= 0; transaction--) previous = transactions[transaction].mapping.invert().map(previous, 1);
50282
+ const before = oldState.doc.nodeAt(previous);
50283
+ if (before?.type !== type || !before.attrs.isInline || before.attrs.isHTML || oldState.doc.resolve(previous).parentOffset === 0) break;
50284
+ count += child.nodeSize;
50285
+ }
50286
+ if (count) tr.delete(tr.mapping.map(pos + 1), tr.mapping.map(pos + 1 + count));
50287
+ return false;
50288
+ });
50203
50289
  newState.doc.nodesBetween(start, Math.max(start, end), (node, pos) => {
50204
50290
  if (node.type !== type) return;
50205
- for (const mark of node.marks) if (mark.type.spec.code) tr.removeMark(pos, pos + 1, mark.type);
50291
+ 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
50292
  });
50207
50293
  return tr.steps.length ? tr : null;
50208
50294
  }
@@ -50464,6 +50550,7 @@ function createDialect(images, wikiLinks = false, files = false) {
50464
50550
  wikiLinks || files ? linkedSyntax(wikiLinks, files) : [],
50465
50551
  createLiteralPreservation(images || files),
50466
50552
  commonmarkWithLiteralReferences,
50553
+ preservingSoftBreaks,
50467
50554
  $node("doc", () => ({
50468
50555
  ...docSchema.schema,
50469
50556
  attrs: {
@@ -51419,7 +51506,8 @@ var ImageView = class {
51419
51506
  return;
51420
51507
  }
51421
51508
  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);
51509
+ if (presentation.kind === "missing") this.card(label, this.runtime.labels.fileMissing, true);
51510
+ else if (presentation.kind === "error") this.card(label, presentation.message || this.runtime.labels.fileError, true);
51423
51511
  else if (presentation.kind === "image") {
51424
51512
  this.renderImage(presentation.url, label, width);
51425
51513
  this.card(label);
@@ -54063,6 +54151,7 @@ var PasteController = class {
54063
54151
  async run(input, files = [], target) {
54064
54152
  if (this.dead) throw new InkKitError("destroyed", "The editor was destroyed.");
54065
54153
  if (this.pending) throw new InkKitError("operation-pending", "An image paste is still pending.");
54154
+ input = this.options.restoreCopy?.(input) ?? input;
54066
54155
  const context = { ...this.options.context() };
54067
54156
  const ctx = this.options.ctx();
54068
54157
  const view = ctx.get(editorViewCtx);
@@ -54103,8 +54192,12 @@ var PasteController = class {
54103
54192
  this.dispatch(view, caretAfterInsertion(view.state.tr.insertText(input.text, anchor.from, anchor.to)).scrollIntoView());
54104
54193
  return;
54105
54194
  }
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);
54195
+ const anchor = this.anchor(operation);
54196
+ const start = view.state.doc.resolve(anchor.from);
54197
+ const end = view.state.doc.resolve(anchor.to);
54198
+ const marks = view.state.storedMarks ?? (anchor.from === anchor.to ? start.marks() : start.marksAcross(end)) ?? [];
54199
+ const nodes = literalTextNodes(schema, input.text, marks, htmlBreakAt(start));
54200
+ slice = new Slice(Fragment.from(schema.nodes.paragraph.create(null, nodes)), 1, 1);
54108
54201
  } else if (input.html) {
54109
54202
  const container = visibleContent(ctx, input.html);
54110
54203
  const imported = /* @__PURE__ */ new Map();
@@ -55095,14 +55188,7 @@ function replacementSlice(state, from, to, value) {
55095
55188
  const nodes = [];
55096
55189
  if (start.parent.type.spec.code) {
55097
55190
  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
- }
55191
+ } else nodes.push(...literalTextNodes(state.schema, value, marks, htmlBreakAt(start)));
55106
55192
  return new Slice(Fragment.fromArray(nodes), 0, 0);
55107
55193
  }
55108
55194
  function replaceFormatted(view, search, replacement, preflight, all = false) {
@@ -55929,6 +56015,15 @@ function caretState(state) {
55929
56015
  quoted: quoted($from, $to)
55930
56016
  };
55931
56017
  }
56018
+ function terminalSelection(doc) {
56019
+ const $end = doc.resolve(doc.content.size);
56020
+ const gap = GapCursor;
56021
+ if (isImageBlock(doc.lastChild) && gap.valid($end)) return new GapCursor($end);
56022
+ const text = Selection.findFrom($end, -1, true);
56023
+ if (text) return text;
56024
+ if (gap.valid($end)) return new GapCursor($end);
56025
+ throw new InkKitError("preservation", "Document has no safe writing position");
56026
+ }
55932
56027
  function quoted($from, $to) {
55933
56028
  return $from.blockRange($to, (node) => node.type.name === "blockquote") != null;
55934
56029
  }
@@ -56141,6 +56236,8 @@ var InkKitEditor = class InkKitEditor {
56141
56236
  typedSteps = [];
56142
56237
  undoneSteps = [];
56143
56238
  formattedUndone = 0;
56239
+ formattedRedoLimit;
56240
+ historyDirection;
56144
56241
  lastSourceInput = 0;
56145
56242
  syncingSource = false;
56146
56243
  mode = "formatted";
@@ -56158,6 +56255,52 @@ var InkKitEditor = class InkKitEditor {
56158
56255
  auxClickHandler;
56159
56256
  untrackLinkModifier;
56160
56257
  footnoteKeyHandler;
56258
+ localCopies = /* @__PURE__ */ new Map();
56259
+ rememberCopy(output, markdown, content, privateSource = false, actualCopy = false) {
56260
+ let comments = privateSource;
56261
+ content.descendants((node) => {
56262
+ const labelled = (label) => typeof label === "string" && literalCommentSpans(label).length > 0;
56263
+ 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;
56264
+ });
56265
+ if (!comments) {
56266
+ if (actualCopy) this.localCopies.clear();
56267
+ return output;
56268
+ }
56269
+ const key = JSON.stringify([
56270
+ output.text,
56271
+ output.html,
56272
+ markdown
56273
+ ]);
56274
+ const previous = this.localCopies.get(key);
56275
+ if (previous) {
56276
+ output.html = previous.html;
56277
+ if (actualCopy) this.localCopies.clear();
56278
+ this.localCopies.set(key, previous);
56279
+ return output;
56280
+ }
56281
+ const wrapper = document.createElement("div");
56282
+ const token = Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, "0")).join("");
56283
+ wrapper.setAttribute("data-inkkit-local-copy", token);
56284
+ wrapper.innerHTML = output.html;
56285
+ output.html = wrapper.outerHTML;
56286
+ if (actualCopy) this.localCopies.clear();
56287
+ this.localCopies.set(key, {
56288
+ text: output.text,
56289
+ html: output.html,
56290
+ markdown
56291
+ });
56292
+ return output;
56293
+ }
56294
+ retainCopy(html) {
56295
+ for (const [key, copy] of this.localCopies) if (copy.html !== html) this.localCopies.delete(key);
56296
+ }
56297
+ restoreCopy(input) {
56298
+ const copy = [...this.localCopies.values()].find((copy) => copy.text === input.text && copy.html === input.html);
56299
+ return copy && input.markdown == null && !input.plainText && copy.text === input.text && copy.html === input.html ? {
56300
+ ...input,
56301
+ markdown: copy.markdown
56302
+ } : input;
56303
+ }
56161
56304
  copyHandler;
56162
56305
  originalSource = "";
56163
56306
  reportingSuppressed = false;
@@ -56373,7 +56516,9 @@ var InkKitEditor = class InkKitEditor {
56373
56516
  revision: this.session.mark(),
56374
56517
  lastMarkdown: this.lastMarkdown,
56375
56518
  literalMatch: this.literalMatch,
56376
- textScope: this.textScope
56519
+ textScope: this.textScope,
56520
+ formattedRedoLimit: this.formattedRedoLimit,
56521
+ formattedUndone: this.formattedUndone
56377
56522
  };
56378
56523
  }
56379
56524
  finish(call, before, failure) {
@@ -56395,6 +56540,8 @@ var InkKitEditor = class InkKitEditor {
56395
56540
  this.lastMarkdown = before.lastMarkdown;
56396
56541
  this.literalMatch = before.literalMatch;
56397
56542
  this.textScope = before.textScope;
56543
+ this.formattedRedoLimit = before.formattedRedoLimit;
56544
+ this.formattedUndone = before.formattedUndone;
56398
56545
  }
56399
56546
  /** Reports a refused edit to the facade call that made it, or else to `events.error`. */
56400
56547
  refuse(error) {
@@ -56535,7 +56682,7 @@ var InkKitEditor = class InkKitEditor {
56535
56682
  table: this.literalSurface ? void 0 : tableContext(state),
56536
56683
  commands: {
56537
56684
  undo: mutable && (!!this.pendingSource || undo(state)),
56538
- redo: mutable && (this.undoneSteps.length > 0 || !this.pendingSource && redo(state)),
56685
+ redo: mutable && (this.undoneSteps.length > 0 || !this.pendingSource && this.formattedRedoLimit !== 0 && redo(state)),
56539
56686
  insertText: mutable,
56540
56687
  paste: mutable,
56541
56688
  replace: mutable,
@@ -56705,7 +56852,9 @@ var InkKitEditor = class InkKitEditor {
56705
56852
  this.assertAlive();
56706
56853
  expectString(text, "text");
56707
56854
  this.assertMutation(expectedGeneration);
56708
- return this.applySource(text, true);
56855
+ const changed = this.applySource(text, true);
56856
+ if (changed) this.localCopies.clear();
56857
+ return changed;
56709
56858
  }
56710
56859
  applySource(text, isolated, from, to, native = false) {
56711
56860
  if (!this.writable) throw new InkKitError("read-only", "The editor is read-only");
@@ -56724,6 +56873,7 @@ var InkKitEditor = class InkKitEditor {
56724
56873
  const previous = this.pendingSource;
56725
56874
  const continuing = previous && !previous.closed && !closed && now - this.lastSourceInput <= sourceGroupDelay;
56726
56875
  if (previous && !continuing) this.rememberTypedStep(previous);
56876
+ this.formattedRedoLimit = 0;
56727
56877
  this.lastSourceInput = now;
56728
56878
  this.undoneSteps = [];
56729
56879
  this.formattedUndone = 0;
@@ -56851,7 +57001,7 @@ var InkKitEditor = class InkKitEditor {
56851
57001
  }
56852
57002
  }
56853
57003
  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));
57004
+ tr.setSelection(terminalSelection(tr.doc));
56855
57005
  if (isolated) tr = closeHistory(tr);
56856
57006
  view.dispatch(tr);
56857
57007
  if (isolated) view.dispatch(closeHistory(view.state.tr).setMeta("addToHistory", false));
@@ -56862,7 +57012,7 @@ var InkKitEditor = class InkKitEditor {
56862
57012
  this.assertMutation(expectedGeneration);
56863
57013
  if (this.literalSurface && this.stepTypedSource("undo")) return true;
56864
57014
  const view = this.editor.ctx.get(editorViewCtx);
56865
- const undone = !this.literalSurface && undoSmartPunctuation(view.state, view.dispatch) || undo(view.state, view.dispatch);
57015
+ const undone = !this.literalSurface && undoSmartPunctuation(view.state, view.dispatch) || this.stepFormattedHistory("undo");
56866
57016
  if (undone) {
56867
57017
  if (this.literalSurface && this.undoneSteps.length) this.formattedUndone++;
56868
57018
  else this.undoneSteps = [];
@@ -56875,14 +57025,30 @@ var InkKitEditor = class InkKitEditor {
56875
57025
  const view = this.editor.ctx.get(editorViewCtx);
56876
57026
  if (this.literalSurface) {
56877
57027
  if (this.formattedUndone > 0) {
56878
- this.formattedUndone--;
56879
- if (redo(view.state, view.dispatch)) return true;
57028
+ if (this.formattedRedoLimit !== 0 && redo(view.state)) {
57029
+ const redone = this.stepFormattedHistory("redo");
57030
+ if (redone) this.formattedUndone--;
57031
+ return redone;
57032
+ }
56880
57033
  this.formattedUndone = 0;
56881
57034
  }
56882
57035
  if (this.stepTypedSource("redo")) return true;
56883
57036
  if (this.pendingSource) return false;
56884
57037
  }
56885
- return redo(view.state, view.dispatch);
57038
+ return this.stepFormattedHistory("redo");
57039
+ }
57040
+ stepFormattedHistory(direction) {
57041
+ if (direction === "redo" && this.formattedRedoLimit === 0) return false;
57042
+ const view = this.editor.ctx.get(editorViewCtx);
57043
+ const before = view.state;
57044
+ const previous = this.historyDirection;
57045
+ this.historyDirection = direction;
57046
+ try {
57047
+ (direction === "undo" ? undo : redo)(before, view.dispatch);
57048
+ return view.state !== before;
57049
+ } finally {
57050
+ this.historyDirection = previous;
57051
+ }
56886
57052
  }
56887
57053
  replaceLiteralSelection(text, isolated = true, retainEndings = false) {
56888
57054
  const value = this.plain.value;
@@ -56985,6 +57151,7 @@ var InkKitEditor = class InkKitEditor {
56985
57151
  const metadata = referenceMetadata(this.editor.ctx, selectedDoc ?? doc, content);
56986
57152
  if (metadata != null) result.html = withReferenceMetadata(result.html, metadata);
56987
57153
  this.assertCaptured(captured);
57154
+ this.rememberCopy(result, markdown, content, selectedSource != null && selectedSource.markdown !== selectedSource.shareable);
56988
57155
  return {
56989
57156
  ...version,
56990
57157
  ...result
@@ -56994,7 +57161,7 @@ var InkKitEditor = class InkKitEditor {
56994
57161
  * clipboard again while this runs. */
56995
57162
  async paste(input, expectedGeneration) {
56996
57163
  this.assertAlive();
56997
- input = clipboardInput(input);
57164
+ input = this.restoreCopy(clipboardInput(input));
56998
57165
  this.assertMutation(expectedGeneration);
56999
57166
  if (this.literalSurface) {
57000
57167
  this.pasteAsPlainText(this.mode === "source" && this.formatType === "md" ? input.markdown ?? input.text : input.text);
@@ -57027,14 +57194,9 @@ var InkKitEditor = class InkKitEditor {
57027
57194
  else {
57028
57195
  const { schema } = view.state;
57029
57196
  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
57197
  const isHTML = !inHiddenComment && htmlBreakAt(view.state.selection.$from);
57032
57198
  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
- });
57199
+ const children = literalTextNodes(schema, text, marks, isHTML);
57038
57200
  if (!children.length) {
57039
57201
  view.dispatch(view.state.tr.deleteSelection());
57040
57202
  return;
@@ -57077,12 +57239,14 @@ var InkKitEditor = class InkKitEditor {
57077
57239
  async destroy() {
57078
57240
  if (this.destroyed) return;
57079
57241
  this.destroyed = true;
57242
+ this.localCopies.clear();
57080
57243
  this.ready = false;
57081
57244
  this.session.close();
57082
57245
  this.pendingSource = void 0;
57083
57246
  this.typedSteps = [];
57084
57247
  this.undoneSteps = [];
57085
57248
  this.formattedUndone = 0;
57249
+ this.formattedRedoLimit = void 0;
57086
57250
  this.cancelExports();
57087
57251
  this.mediaPolicy();
57088
57252
  this.pasteController.destroy();
@@ -57122,6 +57286,7 @@ var InkKitEditor = class InkKitEditor {
57122
57286
  if (portable && this.events.clipboard) this.clipboardSnapshot("selection").then(async (content) => {
57123
57287
  if (cut) refuseUnportableCut(content);
57124
57288
  await writeClipboard(this.events, content);
57289
+ this.retainCopy(content.html);
57125
57290
  if (cut) {
57126
57291
  this.assertCut(captured);
57127
57292
  this.plain.setSelectionRange(from, to);
@@ -57131,6 +57296,7 @@ var InkKitEditor = class InkKitEditor {
57131
57296
  else {
57132
57297
  const output = clipboardContent(parsed.content, parsed.type.schema);
57133
57298
  if (!event.clipboardData) return;
57299
+ this.rememberCopy(output, selected.markdown, parsed.content, selected.markdown !== selected.shareable, true);
57134
57300
  event.clipboardData.setData("text/plain", output.text);
57135
57301
  event.clipboardData.setData("text/html", output.html);
57136
57302
  if (portable) {
@@ -57155,6 +57321,7 @@ var InkKitEditor = class InkKitEditor {
57155
57321
  instance.ownsRootClass = !root.classList.contains("inkkit-root");
57156
57322
  root.classList.add("inkkit-root");
57157
57323
  instance.pasteController = new PasteController({
57324
+ restoreCopy: (input) => instance.restoreCopy(input),
57158
57325
  ctx: () => instance.editor.ctx,
57159
57326
  context: () => instance.context(),
57160
57327
  adapter: options.images,
@@ -57258,6 +57425,14 @@ var InkKitEditor = class InkKitEditor {
57258
57425
  return;
57259
57426
  }
57260
57427
  }
57428
+ if (instance.formattedRedoLimit !== void 0) {
57429
+ const depth = redoDepth(next);
57430
+ if (depth === 0) instance.formattedRedoLimit = void 0;
57431
+ else if (next !== this.state) {
57432
+ if (instance.historyDirection === "undo") instance.formattedRedoLimit = Math.min(depth, instance.formattedRedoLimit + 1);
57433
+ else if (instance.historyDirection === "redo") instance.formattedRedoLimit = Math.max(0, instance.formattedRedoLimit - 1);
57434
+ }
57435
+ }
57261
57436
  this.updateState(next);
57262
57437
  },
57263
57438
  handleScrollToSelection: () => {
@@ -57265,7 +57440,7 @@ var InkKitEditor = class InkKitEditor {
57265
57440
  return true;
57266
57441
  }
57267
57442
  }));
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, {
57443
+ }).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-TSUdUlMi.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
57444
  capture: () => instance.adapterCapture(),
57270
57445
  active: () => instance.ready && !instance.destroyed && !instance.literalSurface,
57271
57446
  labels: {
@@ -57345,6 +57520,7 @@ var InkKitEditor = class InkKitEditor {
57345
57520
  event.preventDefault();
57346
57521
  instance.paste({
57347
57522
  text: event.clipboardData.getData("text/plain"),
57523
+ html: event.clipboardData.getData("text/html"),
57348
57524
  markdown: event.clipboardData.getData("text/markdown") || void 0
57349
57525
  }).catch((error) => instance.report(error));
57350
57526
  });
@@ -57414,11 +57590,13 @@ var InkKitEditor = class InkKitEditor {
57414
57590
  instance.snapshot();
57415
57591
  const content = expanded;
57416
57592
  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);
57593
+ const markdown = selectionMarkdown(instance.editor.ctx, view.state.doc, valid);
57418
57594
  const output = clipboardContent(content, view.state.schema);
57419
- event.clipboardData.setData("text/plain", output.text);
57420
57595
  const metadata = referenceMetadata(instance.editor.ctx, view.state.doc, valid);
57421
- event.clipboardData.setData("text/html", metadata != null ? withReferenceMetadata(output.html, metadata) : output.html);
57596
+ if (metadata != null) output.html = withReferenceMetadata(output.html, metadata);
57597
+ instance.rememberCopy(output, markdown, fragment, false, true);
57598
+ event.clipboardData.setData("text/plain", output.text);
57599
+ event.clipboardData.setData("text/html", output.html);
57422
57600
  if (event.type === "cut" && instance.writable) view.dispatch(view.state.tr.deleteSelection().scrollIntoView());
57423
57601
  } catch (error) {
57424
57602
  instance.report(error);
@@ -57434,6 +57612,7 @@ var InkKitEditor = class InkKitEditor {
57434
57612
  instance.clipboardSnapshot("selection").then(async (content) => {
57435
57613
  if (cut) refuseUnportableCut(content);
57436
57614
  await writeClipboard(events, content);
57615
+ instance.retainCopy(content.html);
57437
57616
  if (cut) {
57438
57617
  instance.assertCut(captured);
57439
57618
  view.dispatch(view.state.tr.deleteRange(from, to).scrollIntoView());
@@ -57481,6 +57660,7 @@ var InkKitEditor = class InkKitEditor {
57481
57660
  this.typedSteps = [];
57482
57661
  this.undoneSteps = [];
57483
57662
  this.formattedUndone = 0;
57663
+ this.formattedRedoLimit = void 0;
57484
57664
  this.mode = "formatted";
57485
57665
  this.plainComposing = false;
57486
57666
  this.beforePlainInput = void 0;
@@ -57860,6 +58040,7 @@ var InkKitEditor = class InkKitEditor {
57860
58040
  }])[0]);
57861
58041
  }
57862
58042
  load(markdown, preservation) {
58043
+ this.localCopies.clear();
57863
58044
  this.originalSource = markdown;
57864
58045
  const { ctx } = this.editor;
57865
58046
  const view = ctx.get(editorViewCtx);
@@ -57876,9 +58057,7 @@ var InkKitEditor = class InkKitEditor {
57876
58057
  this.preservation = preservation;
57877
58058
  this.carried = void 0;
57878
58059
  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);
58060
+ const end = terminalSelection(view.state.doc);
57882
58061
  view.dispatch(setSearchState(view.state.tr.setSelection(end), new SearchQuery({ search: "" })));
57883
58062
  }
57884
58063
  /** Selects the next match and returns whether there was one. */
@@ -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-DkL-wDqy.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.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-DkL-wDqy.js";
2
2
  export { InkKitEditor, InkKitError };
package/dist/editor.d.ts CHANGED
@@ -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. */
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-DkL-wDqy.js";
2
2
  //#region src/index.ts
3
3
  registerMermaid(async () => (await import("mermaid")).default);
4
4
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicayzer/inkkit",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "A preserving Markdown editor for offline web views and the web",
5
5
  "license": "MIT",
6
6
  "type": "module",