@aicayzer/inkkit 0.1.7 → 0.3.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
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
 
@@ -35008,7 +35008,7 @@ var associationAttrs = {
35008
35008
  identifier: { default: "" }
35009
35009
  };
35010
35010
  /** Whether an element reads as `text`, with whitespace collapsed as shown. */
35011
- function showsText$1(dom, text) {
35011
+ function showsText(dom, text) {
35012
35012
  const shown = (value) => value.replace(/\s+/g, " ").trim();
35013
35013
  return shown(dom.textContent ?? "") === shown(text);
35014
35014
  }
@@ -35033,7 +35033,7 @@ var references = [
35033
35033
  const url = dom.getAttribute("data-inkkit-url") ?? "";
35034
35034
  const label = dom.getAttribute("data-inkkit-label") ?? "";
35035
35035
  const title = dom.getAttribute("data-inkkit-title");
35036
- if (!isSafeLinkTarget(url) || !showsText$1(dom, definitionText(label, url, title))) return false;
35036
+ if (!isSafeLinkTarget(url) || !showsText(dom, definitionText(label, url, title))) return false;
35037
35037
  return {
35038
35038
  label,
35039
35039
  identifier: dom.getAttribute("data-inkkit-reference-definition") ?? "",
@@ -35085,7 +35085,7 @@ var references = [
35085
35085
  tag: "sup[data-inkkit-footnote-reference]",
35086
35086
  getAttrs: (dom) => {
35087
35087
  const label = dom.getAttribute("data-inkkit-label") ?? "";
35088
- return showsText$1(dom, label) ? {
35088
+ return showsText(dom, label) ? {
35089
35089
  label,
35090
35090
  identifier: dom.getAttribute("data-inkkit-footnote-reference") ?? ""
35091
35091
  } : false;
@@ -35529,7 +35529,7 @@ var wikiLinkSchema = $node("inkkit_wiki_link", () => ({
35529
35529
  tag: "span[data-inkkit-wiki]",
35530
35530
  getAttrs(dom) {
35531
35531
  const parsed = parseLinkedSyntax(dom.getAttribute("data-inkkit-wiki-raw") ?? "", dom.getAttribute("data-inkkit-wiki-table") === "true");
35532
- return parsed && !parsed.embed && showsWikiTarget(parsed) && showsText$1(dom, parsed.label) ? {
35532
+ return parsed && !parsed.embed && showsWikiTarget(parsed) && showsText(dom, parsed.label) ? {
35533
35533
  raw: parsed.raw,
35534
35534
  target: parsed.target,
35535
35535
  fragment: parsed.fragment,
@@ -43687,27 +43687,9 @@ function remarkComments() {
43687
43687
  });
43688
43688
  return (tree) => {
43689
43689
  const visit = (node, inline, parentType) => {
43690
- if (parentType === "listItem" && node.type === "inkkitCommentBlock") return {
43691
- type: "paragraph",
43692
- children: [{
43693
- ...node,
43694
- type: "inkkitCommentInline",
43695
- block: true
43696
- }],
43697
- position: node.position
43698
- };
43699
43690
  if (node.type === "html") {
43700
43691
  const spans = htmlSpans(node.value);
43701
- if (spans.length === 1 && spans[0].start === 0 && spans[0].end === node.value.length) return parentType === "listItem" ? {
43702
- type: "paragraph",
43703
- children: [{
43704
- type: "inkkitCommentInline",
43705
- syntax: "html",
43706
- value: spans[0].value,
43707
- position: node.position
43708
- }],
43709
- position: node.position
43710
- } : {
43692
+ if (spans.length === 1 && spans[0].start === 0 && spans[0].end === node.value.length) return {
43711
43693
  type: inline ? "inkkitCommentInline" : "inkkitCommentBlock",
43712
43694
  syntax: "html",
43713
43695
  value: spans[0].value,
@@ -43736,6 +43718,7 @@ function commentNode(inline) {
43736
43718
  marks: "",
43737
43719
  code: true,
43738
43720
  defining: true,
43721
+ createGapCursor: !inline,
43739
43722
  attrs: {
43740
43723
  syntax: { default: "html" },
43741
43724
  block: { default: !inline }
@@ -44482,7 +44465,7 @@ function isHTMLElement(node) {
44482
44465
  return isElement(node) && node.namespaceURI === "http://www.w3.org/1999/xhtml";
44483
44466
  }
44484
44467
  //#endregion
44485
- //#region node_modules/.pnpm/prosemirror-drop-indicator@0.1.4/node_modules/prosemirror-drop-indicator/dist/index.js
44468
+ //#region node_modules/.pnpm/prosemirror-drop-indicator@0.1.4_patch_hash=4afd7e652c59a5f25f8ecc7b74d6370bde7273ee46a28766df1490663f928940/node_modules/prosemirror-drop-indicator/dist/index.js
44486
44469
  function getTargetsByView(view) {
44487
44470
  const stack = [[-1, view.state.doc]];
44488
44471
  const targets = [];
@@ -44638,6 +44621,8 @@ function createDropIndicatorView(view, getTarget, options) {
44638
44621
  let hasDragOverEvent = false;
44639
44622
  const scheduleHide = () => {
44640
44623
  if (hideId) clearTimeout(hideId);
44624
+ prevX = void 0;
44625
+ prevY = void 0;
44641
44626
  hasDragOverEvent = false;
44642
44627
  hideId = setTimeout(() => {
44643
44628
  if (hasDragOverEvent) return;
@@ -44678,6 +44663,7 @@ function createDropIndicatorView(view, getTarget, options) {
44678
44663
  dom.addEventListener("drop", scheduleHide);
44679
44664
  dom.addEventListener("dragleave", scheduleHide);
44680
44665
  const destroy = () => {
44666
+ if (hideId) clearTimeout(hideId);
44681
44667
  dom.removeEventListener("dragover", handleDragOver);
44682
44668
  dom.removeEventListener("dragend", scheduleHide);
44683
44669
  dom.removeEventListener("drop", scheduleHide);
@@ -44924,50 +44910,63 @@ function blockDropRange(view, x, y) {
44924
44910
  * callout, where typing would change text the reader can't see or didn't
44925
44911
  * choose.
44926
44912
  */
44927
- function caretAfter(doc, pos) {
44913
+ function caretAfter(doc, pos, folds = /* @__PURE__ */ new Map()) {
44914
+ const hides = (node) => isComment(node) || node.type.name === "inkkit_callout" && (folds.get(node) ?? node.attrs.fold === "-");
44915
+ const concealedDepth = ($pos) => {
44916
+ for (let depth = 1; depth <= $pos.depth; depth++) if (hides($pos.node(depth))) return depth;
44917
+ return -1;
44918
+ };
44919
+ const concealed = ($pos) => concealedDepth($pos) >= 0;
44928
44920
  const $pos = doc.resolve(pos);
44929
44921
  const before = $pos.nodeBefore;
44930
44922
  if (before?.isTextblock && !concealed(doc.resolve(pos - 1))) return TextSelection.create(doc, pos - 1);
44931
- if (GapCursor.valid($pos)) return new GapCursor($pos);
44923
+ const gap = GapCursor;
44924
+ const forward = before && !showsText(before) ? shownText(doc, pos, 1) : null;
44925
+ if (forward) return forward;
44926
+ if (gap.valid($pos)) return new GapCursor($pos);
44932
44927
  const [first, second] = before && !showsText(before) ? [1, -1] : [-1, 1];
44933
44928
  return shownText(doc, pos, first) ?? shownText(doc, pos, second) ?? Selection.near($pos);
44934
- }
44935
- function hides(node) {
44936
- return isComment(node) || node.type.name === "inkkit_callout" && node.attrs.fold === "-";
44937
- }
44938
- function concealedDepth($pos) {
44939
- for (let depth = 1; depth <= $pos.depth; depth++) if (hides($pos.node(depth))) return depth;
44940
- return -1;
44941
- }
44942
- function concealed($pos) {
44943
- return concealedDepth($pos) >= 0;
44944
- }
44945
- function showsText(node) {
44946
- if (hides(node)) return false;
44947
- if (node.isTextblock) return true;
44948
- let found = false;
44949
- node.descendants((child) => {
44950
- if (found || hides(child)) return false;
44951
- if (child.isTextblock) found = true;
44952
- return !found;
44953
- });
44954
- return found;
44955
- }
44956
- function shownText(doc, pos, dir) {
44957
- let $from = doc.resolve(pos);
44958
- for (;;) {
44959
- const found = TextSelection.findFrom($from, dir, true);
44960
- if (!found) return null;
44961
- const depth = concealedDepth(found.$head);
44962
- if (depth < 0) return found;
44963
- $from = doc.resolve(dir > 0 ? found.$head.after(depth) : found.$head.before(depth));
44929
+ function showsText(node) {
44930
+ if (hides(node)) return false;
44931
+ if (node.isTextblock) return true;
44932
+ let found = false;
44933
+ node.descendants((child) => {
44934
+ if (found || hides(child)) return false;
44935
+ if (child.isTextblock) found = true;
44936
+ return !found;
44937
+ });
44938
+ return found;
44964
44939
  }
44940
+ function shownText(doc, pos, dir) {
44941
+ let $from = doc.resolve(pos);
44942
+ for (;;) {
44943
+ const found = TextSelection.findFrom($from, dir, true);
44944
+ if (!found) return null;
44945
+ const depth = concealedDepth(found.$head);
44946
+ if (depth < 0) return found;
44947
+ $from = doc.resolve(dir > 0 ? found.$head.after(depth) : found.$head.before(depth));
44948
+ }
44949
+ }
44950
+ }
44951
+ /** Capture reader folding before dispatch, then map it to the new document. */
44952
+ function calloutFolds(view, tr) {
44953
+ const folds = /* @__PURE__ */ new Map();
44954
+ view.state.doc.descendants((node, pos) => {
44955
+ if (node.type.name !== "inkkit_callout") return;
44956
+ const dom = view.nodeDOM(pos);
44957
+ if (!(dom instanceof HTMLElement)) return;
44958
+ const mapped = tr.mapping.mapResult(pos, 1);
44959
+ if (mapped.deleted) return;
44960
+ const next = tr.doc.nodeAt(mapped.pos);
44961
+ if (next?.type === node.type) folds.set(next, dom.getAttribute("data-inkkit-folded") === "true");
44962
+ });
44963
+ return folds;
44965
44964
  }
44966
44965
  /**
44967
44966
  * Puts the caret just after what the transaction's last step inserted, so
44968
44967
  * typing continues after pasted or dropped content instead of replacing it.
44969
44968
  */
44970
- function caretAfterInsertion(tr) {
44969
+ function caretAfterInsertion(tr, view) {
44971
44970
  const map = tr.mapping.maps[tr.mapping.maps.length - 1];
44972
44971
  let end;
44973
44972
  map?.forEach((_from, _to, _newFrom, newTo) => {
@@ -44975,7 +44974,7 @@ function caretAfterInsertion(tr) {
44975
44974
  });
44976
44975
  if (end == null) return tr;
44977
44976
  const $end = tr.doc.resolve(end);
44978
- return tr.setSelection($end.parent.inlineContent ? TextSelection.create(tr.doc, end) : caretAfter(tr.doc, end));
44977
+ return tr.setSelection($end.parent.inlineContent ? TextSelection.create(tr.doc, end) : caretAfter(tr.doc, end, view && calloutFolds(view, tr)));
44979
44978
  }
44980
44979
  //#endregion
44981
44980
  //#region src/code-fences.ts
@@ -45768,6 +45767,18 @@ function retainText(raw, before, after, inTable = false, written, beside = [fals
45768
45767
  const read = decodedOffsets(raw, true);
45769
45768
  if (read?.decoded !== before) return after;
45770
45769
  const { offsets } = read;
45770
+ const lineStart = after.lastIndexOf("\n", prefix - 1) + 1;
45771
+ if (lineStart > 0 && written !== void 0) {
45772
+ const canonical = decodedOffsets(written, true);
45773
+ if (canonical?.decoded === after) {
45774
+ const opening = written.slice(canonical.offsets[lineStart]);
45775
+ const marker = /^(?:\d+)?\\[#+.\->)]/.exec(opening)?.[0];
45776
+ if (marker && prefix - lineStart <= decodedOffsets(marker).decoded.length) {
45777
+ prefix = lineStart;
45778
+ return raw.slice(0, offsets[prefix]) + written.slice(canonical.offsets[prefix], canonical.offsets[after.length - suffix]) + raw.slice(offsets[before.length - suffix]);
45779
+ }
45780
+ }
45781
+ }
45771
45782
  if (written !== void 0 && (beside[0] && prefix === 0 || beside[1] && suffix === 0)) {
45772
45783
  const writtenOffsets = decodedOffsets(written);
45773
45784
  if (writtenOffsets && writtenOffsets.decoded === after) return raw.slice(0, offsets[prefix]) + written.slice(writtenOffsets.offsets[prefix], writtenOffsets.offsets[after.length - suffix]) + raw.slice(offsets[before.length - suffix]);
@@ -48200,6 +48211,7 @@ var calloutSchema = $node("inkkit_callout", (ctx) => ({
48200
48211
  group: "block",
48201
48212
  content: "block+",
48202
48213
  defining: true,
48214
+ createGapCursor: true,
48203
48215
  attrs: {
48204
48216
  kind: { default: "NOTE" },
48205
48217
  marker: { default: "[!NOTE]" },
@@ -53851,7 +53863,7 @@ function pasteSpreadsheet(ctx, input, at) {
53851
53863
  }
53852
53864
  let grid;
53853
53865
  try {
53854
- grid = input.html?.match(/<table[\s>]/i) ? htmlGrid(view.state.schema, input.html) : input.text.includes(" ") ? textGrid(view.state.schema, input.text) : void 0;
53866
+ 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;
53855
53867
  } catch (error) {
53856
53868
  if (inTable || !input.html) throw error;
53857
53869
  if (!standaloneTable(input.html)) return false;
@@ -53869,7 +53881,7 @@ function pasteSpreadsheet(ctx, input, at) {
53869
53881
  if (cursor < input.html.length) children.push(schema.text(input.html.slice(cursor)));
53870
53882
  const content = importableSlice(ctx, new Slice(Fragment.from(schema.nodes.literal_markdown.create(null, children)), 0, 0)).content;
53871
53883
  const selection = view.state.selection;
53872
- 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)));
53884
+ 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)));
53873
53885
  return true;
53874
53886
  }
53875
53887
  if (!grid) return false;
@@ -53884,7 +53896,7 @@ function pasteSpreadsheet(ctx, input, at) {
53884
53896
  if (node === table) tableStart = pos + 1;
53885
53897
  });
53886
53898
  if (tableStart < 0) reject();
53887
- tr.setSelection(caretAfter(tr.doc, tableStart - 1 + table.nodeSize));
53899
+ tr.setSelection(caretAfter(tr.doc, tableStart - 1 + table.nodeSize, calloutFolds(view, tr)));
53888
53900
  dispatchTableOperation(view, tr);
53889
53901
  return true;
53890
53902
  }
@@ -53929,6 +53941,12 @@ function standaloneTable(html) {
53929
53941
  function reject() {
53930
53942
  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.");
53931
53943
  }
53944
+ function looksLikeGrid(input) {
53945
+ const rows = input.replace(/\r\n?/g, "\n").replace(/\n$/, "").split("\n");
53946
+ if (rows.length < 2) return false;
53947
+ const widths = rows.map((row) => row.split(" ").length);
53948
+ return widths[0] > 1 && widths.every((width) => width === widths[0]) && rows.some((row) => row.split(" ")[0].trim().length > 0);
53949
+ }
53932
53950
  function textGrid(schema, input) {
53933
53951
  const text = input.replace(/\r\n?/g, "\n").replace(/\n$/, "");
53934
53952
  const rows = [[]];
@@ -54119,7 +54137,10 @@ var PasteController = class {
54119
54137
  return mapped?.operation === operation ? mapped : operation;
54120
54138
  }
54121
54139
  cancelPending() {
54140
+ const operation = this.active;
54141
+ if (!operation) return;
54122
54142
  this.active = void 0;
54143
+ operation.controller.abort();
54123
54144
  this.options.onPendingChanged?.();
54124
54145
  }
54125
54146
  destroy() {
@@ -54131,6 +54152,26 @@ var PasteController = class {
54131
54152
  const current = this.options.context();
54132
54153
  if (this.active !== operation || current.documentId !== context.documentId || current.generation !== context.generation) throw new InkKitError("stale-document", "The document changed while images were importing.");
54133
54154
  }
54155
+ async importImage(image, context, operation) {
54156
+ this.assertContext(context, operation);
54157
+ const signal = operation.controller.signal;
54158
+ let abort;
54159
+ const cancelled = new Promise((_, reject) => {
54160
+ abort = () => reject(new InkKitError(this.dead ? "destroyed" : "stale-document", "The image import was cancelled."));
54161
+ signal.addEventListener("abort", abort, { once: true });
54162
+ });
54163
+ try {
54164
+ return await Promise.race([new Promise((resolve) => {
54165
+ resolve(this.options.adapter.importImage(image, {
54166
+ ...context,
54167
+ signal
54168
+ }));
54169
+ }), cancelled]);
54170
+ } finally {
54171
+ signal.removeEventListener("abort", abort);
54172
+ this.assertContext(context, operation);
54173
+ }
54174
+ }
54134
54175
  commit(apply) {
54135
54176
  return this.options.commit ? this.options.commit(apply) : apply();
54136
54177
  }
@@ -54145,7 +54186,7 @@ var PasteController = class {
54145
54186
  const blocks = Fragment.fromArray(topLevel(view.state.schema, slice.content));
54146
54187
  const tr = view.state.tr.replaceWith(range.from, range.to, blocks);
54147
54188
  const end = tr.mapping.map(range.to);
54148
- this.dispatch(view, tr.setSelection(caretAfter(tr.doc, end)).scrollIntoView());
54189
+ this.dispatch(view, tr.setSelection(caretAfter(tr.doc, end, calloutFolds(view, tr))).scrollIntoView());
54149
54190
  }
54150
54191
  /** Imports the input at the selection, or in place of `target`. */
54151
54192
  async run(input, files = [], target) {
@@ -54162,9 +54203,12 @@ var PasteController = class {
54162
54203
  from: exit,
54163
54204
  to: exit
54164
54205
  };
54165
- const operation = target ? { ...target } : {
54166
- from: view.state.selection.from,
54167
- to: view.state.selection.to
54206
+ const operation = {
54207
+ ...target ?? {
54208
+ from: view.state.selection.from,
54209
+ to: view.state.selection.to
54210
+ },
54211
+ controller: new AbortController()
54168
54212
  };
54169
54213
  const inCode = () => view.state.doc.resolve(this.anchor(operation).from).parent.type.spec.code;
54170
54214
  this.active = operation;
@@ -54213,14 +54257,14 @@ var PasteController = class {
54213
54257
  if (!reference) {
54214
54258
  const captured = images.find((image) => image.source === source) ?? dataImage(source) ?? positional[index++];
54215
54259
  if (!captured || !this.options.adapter) throw new Error("The pasted image is unavailable.");
54216
- reference = (await this.options.adapter.importImage(captured, { ...context })).reference;
54260
+ reference = (await this.importImage(captured, context, operation)).reference;
54217
54261
  this.assertContext(context, operation);
54218
54262
  imported.set(source, reference);
54219
54263
  }
54220
54264
  element.setAttribute("src", reference);
54221
54265
  element.removeAttribute("srcset");
54222
54266
  } catch (error) {
54223
- if (error instanceof InkKitError) throw error;
54267
+ if (error instanceof InkKitError && error.code !== "image-unavailable") throw error;
54224
54268
  element.replaceWith(document.createTextNode(`[${alt}: image unavailable]`));
54225
54269
  this.options.onError?.(coded(error, "image-unavailable", error instanceof Error ? error.message : "The pasted image is unavailable."));
54226
54270
  }
@@ -54265,7 +54309,7 @@ var PasteController = class {
54265
54309
  if (input.text) nodes.push(schema.nodes.paragraph.create(null, schema.text(input.text)));
54266
54310
  for (const image of images) try {
54267
54311
  if (!this.options.adapter) throw new InkKitError("image-unavailable", "No image adapter is configured.");
54268
- const imported = await this.options.adapter.importImage(image, { ...context });
54312
+ const imported = await this.importImage(image, context, operation);
54269
54313
  this.assertContext(context, operation);
54270
54314
  const imageNode = schema.nodes.image?.create({
54271
54315
  src: imported.reference,
@@ -54312,8 +54356,8 @@ var PasteController = class {
54312
54356
  });
54313
54357
  return;
54314
54358
  }
54315
- 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());
54316
- 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());
54359
+ 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());
54360
+ 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());
54317
54361
  } finally {
54318
54362
  if (this.active === operation) {
54319
54363
  this.active = void 0;
@@ -55315,17 +55359,35 @@ function navigateFormattedHeading(view, requested, context) {
55315
55359
  }
55316
55360
  //#endregion
55317
55361
  //#region src/tasks.ts
55362
+ function onTaskMarker(event) {
55363
+ const target = event.target;
55364
+ if (!(target instanceof HTMLElement) || !target.matches("li[data-item-type=\"task\"]")) return false;
55365
+ const marker = getComputedStyle(target, "::before");
55366
+ const rect = target.getBoundingClientRect();
55367
+ const left = rect.left + parseFloat(marker.left);
55368
+ const top = rect.top + parseFloat(marker.top);
55369
+ return event.clientX >= left && event.clientX <= left + parseFloat(marker.width) && event.clientY >= top && event.clientY <= top + parseFloat(marker.height);
55370
+ }
55318
55371
  var taskListPlugin = $prose(() => new Plugin({
55319
55372
  key: new PluginKey("taskToggle"),
55320
- props: { handleClickOn(view, _pos, node, nodePos, event) {
55321
- if (!view.editable) return false;
55322
- if (node.type.name !== "list_item" || node.attrs.checked == null) return false;
55323
- if (!event.target.matches("li[data-item-type=\"task\"]")) return false;
55324
- view.dispatch(view.state.tr.setNodeMarkup(nodePos, void 0, {
55325
- ...node.attrs,
55326
- checked: !node.attrs.checked
55327
- }));
55328
- return true;
55373
+ props: { handleDOMEvents: {
55374
+ mousedown(view, event) {
55375
+ if (!view.editable || event.button !== 0 || !onTaskMarker(event)) return false;
55376
+ event.preventDefault();
55377
+ return true;
55378
+ },
55379
+ click(view, event) {
55380
+ if (!view.editable || event.button !== 0 || !onTaskMarker(event)) return false;
55381
+ const nodePos = view.posAtDOM(event.target, 0) - 1;
55382
+ const node = view.state.doc.nodeAt(nodePos);
55383
+ if (node?.type.name !== "list_item" || node.attrs.checked == null) return false;
55384
+ event.preventDefault();
55385
+ view.dispatch(view.state.tr.setNodeMarkup(nodePos, void 0, {
55386
+ ...node.attrs,
55387
+ checked: !node.attrs.checked
55388
+ }));
55389
+ return true;
55390
+ }
55329
55391
  } }
55330
55392
  }));
55331
55393
  function listItemsInSelection(view) {
@@ -56219,7 +56281,7 @@ var sourceGroupDelay = 500;
56219
56281
  var sourceStepLimit = 100;
56220
56282
  /**
56221
56283
  * The editor. Create it with `InkKitEditor.mount`, load a document, and read it back with
56222
- * `snapshot`. The reference is https://github.com/aicayzer/inkkit/blob/main/docs/api.md.
56284
+ * `snapshot`. The reference is https://inkkit.cyzr.me/reference/editor.
56223
56285
  */
56224
56286
  var InkKitEditor = class InkKitEditor {
56225
56287
  options;
@@ -57157,6 +57219,11 @@ var InkKitEditor = class InkKitEditor {
57157
57219
  ...result
57158
57220
  };
57159
57221
  }
57222
+ /** Cancels the pending image paste without changing the document or editability. */
57223
+ cancelPendingImports(expectedGeneration) {
57224
+ this.assertReady(expectedGeneration);
57225
+ this.pasteController.cancelPending();
57226
+ }
57160
57227
  /** Pastes content the host read from the native clipboard. Capture it once; don't read the
57161
57228
  * clipboard again while this runs. */
57162
57229
  async paste(input, expectedGeneration) {
@@ -57440,7 +57507,7 @@ var InkKitEditor = class InkKitEditor {
57440
57507
  return true;
57441
57508
  }
57442
57509
  }));
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, {
57510
+ }).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-D6evOlOs.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, {
57444
57511
  capture: () => instance.adapterCapture(),
57445
57512
  active: () => instance.ready && !instance.destroyed && !instance.literalSurface,
57446
57513
  labels: {
@@ -58786,6 +58853,7 @@ var InkKitEditor = class InkKitEditor {
58786
58853
  "printableSnapshot",
58787
58854
  "clipboardSnapshot",
58788
58855
  "paste",
58856
+ "cancelPendingImports",
58789
58857
  "pasteAsPlainText",
58790
58858
  "commandState",
58791
58859
  "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-DkL-wDqy.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-DrVCeBzK.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-DkL-wDqy.js";
1
+ import { r as InkKitError, t as InkKitEditor } from "./chunks/core-DrVCeBzK.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;
@@ -321,6 +321,8 @@ export declare class InkKitEditor {
321
321
  /** Prepares the document or the selection for the native clipboard. Rejects with `stale-document`
322
322
  * if the document changes first. */
323
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;
324
326
  /** Pastes content the host read from the native clipboard. Capture it once; don't read the
325
327
  * clipboard again while this runs. */
326
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-DkL-wDqy.js";
1
+ import { n as registerMermaid, r as InkKitError, t as InkKitEditor } from "./chunks/core-DrVCeBzK.js";
2
2
  //#region src/index.ts
3
3
  registerMermaid(async () => (await import("mermaid")).default);
4
4
  //#endregion
package/dist/style.css CHANGED
@@ -81,7 +81,10 @@
81
81
  overflow-x: auto;
82
82
  }
83
83
  .inkkit-root .ProseMirror table {
84
- border-collapse: collapse;
84
+ border-collapse: separate;
85
+ border-spacing: 0;
86
+ border: 1px solid var(--_inkkit-rule);
87
+ border-radius: var(--inkkit-table-radius, 4px);
85
88
  width: 100%;
86
89
  }
87
90
  .inkkit-root .ProseMirror td,
@@ -91,9 +94,39 @@
91
94
  min-width: calc(var(--inkkit-font-size, 15px) * 4);
92
95
  padding: calc(var(--inkkit-font-size, 15px) * 0.4)
93
96
  calc(var(--inkkit-font-size, 15px) * 0.6);
94
- border: 1px solid var(--_inkkit-rule);
97
+ border: 0 solid var(--_inkkit-rule);
98
+ border-width: 0 1px 1px 0;
95
99
  vertical-align: top;
96
100
  }
101
+ /* Round only the perimeter; keep controls and focus rings outside the table
102
+ unclipped, and leave every internal cell division square. */
103
+ .inkkit-root .ProseMirror tr > :last-child {
104
+ border-right-width: 0;
105
+ }
106
+ .inkkit-root .ProseMirror tr:last-child > * {
107
+ border-bottom-width: 0;
108
+ }
109
+ .inkkit-root .ProseMirror tr:first-child > :first-child {
110
+ border-top-left-radius: max(0px, calc(var(--inkkit-table-radius, 4px) - 1px));
111
+ }
112
+ .inkkit-root .ProseMirror tr:first-child > :last-child {
113
+ border-top-right-radius: max(
114
+ 0px,
115
+ calc(var(--inkkit-table-radius, 4px) - 1px)
116
+ );
117
+ }
118
+ .inkkit-root .ProseMirror tr:last-child > :first-child {
119
+ border-bottom-left-radius: max(
120
+ 0px,
121
+ calc(var(--inkkit-table-radius, 4px) - 1px)
122
+ );
123
+ }
124
+ .inkkit-root .ProseMirror tr:last-child > :last-child {
125
+ border-bottom-right-radius: max(
126
+ 0px,
127
+ calc(var(--inkkit-table-radius, 4px) - 1px)
128
+ );
129
+ }
97
130
  .inkkit-root .ProseMirror th {
98
131
  background: var(--_inkkit-code-bg);
99
132
  font-weight: 600;
@@ -107,6 +140,7 @@
107
140
  position: absolute;
108
141
  inset: 0;
109
142
  z-index: 2;
143
+ border-radius: inherit;
110
144
  content: '';
111
145
  background: color-mix(in srgb, var(--_inkkit-accent) 25%, transparent);
112
146
  pointer-events: none;
@@ -154,9 +188,11 @@
154
188
  transform: translate(-50%, -50%);
155
189
  border-radius: 4px;
156
190
  box-shadow: 0 0 0 1px var(--_inkkit-rule);
157
- cursor: grab;
158
191
  touch-action: none;
159
192
  }
193
+ .inkkit-root .inkkit-table-controls .inkkit-table-grip:not(:disabled) {
194
+ cursor: grab;
195
+ }
160
196
  .inkkit-root .inkkit-table-grip[data-inkkit-axis='column'] {
161
197
  width: 24px;
162
198
  height: 14px;
@@ -252,6 +288,10 @@
252
288
  outline-offset: 2px;
253
289
  border-radius: 6px;
254
290
  }
291
+ /* A gap or node selection draws its own indicator, not a native text caret. */
292
+ .inkkit-root .ProseMirror.ProseMirror-hideselection {
293
+ caret-color: transparent;
294
+ }
255
295
  .inkkit-root .ProseMirror-gapcursor {
256
296
  display: none;
257
297
  pointer-events: none;
@@ -659,7 +699,11 @@
659
699
  width: var(--_inkkit-task-size);
660
700
  height: var(--_inkkit-task-size);
661
701
  box-sizing: border-box;
662
- border: 1.5px solid color-mix(in srgb, CanvasText 35%, transparent);
702
+ border: 1.5px solid
703
+ var(
704
+ --inkkit-checkbox-border-color,
705
+ color-mix(in srgb, CanvasText 35%, transparent)
706
+ );
663
707
  border-radius: 4px;
664
708
  cursor: pointer;
665
709
  }
@@ -802,7 +846,7 @@
802
846
  }
803
847
 
804
848
  .inkkit-root .inkkit-reference-definition {
805
- font-family: ui-monospace, monospace;
849
+ font-family: var(--_inkkit-mono);
806
850
  font-size: calc(var(--inkkit-font-size, 15px) * 0.8);
807
851
  opacity: 0.7;
808
852
  }
@@ -957,11 +1001,13 @@
957
1001
  background: var(--inkkit-menu-background, light-dark(#fff, #252529));
958
1002
  color: var(--inkkit-menu-text, CanvasText);
959
1003
  box-shadow: var(--inkkit-menu-shadow, 0 4px 18px #0003);
960
- font: 14px / 24px var(--inkkit-font-family, system-ui, sans-serif);
1004
+ font: calc(var(--inkkit-font-size, 15px) * 14 / 15) /
1005
+ calc(var(--inkkit-font-size, 15px) * 1.6)
1006
+ var(--inkkit-font-family, system-ui, sans-serif);
961
1007
  }
962
1008
  .inkkit-root .inkkit-suggestions [role='option'] {
963
1009
  box-sizing: border-box;
964
- height: 32px;
1010
+ min-height: 32px;
965
1011
  display: flex;
966
1012
  align-items: center;
967
1013
  gap: 8px;
@@ -1021,8 +1067,9 @@
1021
1067
  }
1022
1068
  .inkkit-root .inkkit-code-language:focus-visible,
1023
1069
  .inkkit-root .inkkit-copy:focus-visible {
1024
- outline: 2px solid var(--_inkkit-accent);
1025
- outline-offset: 1px;
1070
+ outline: var(--inkkit-focus-width, 2px) solid
1071
+ var(--inkkit-focus-color, var(--_inkkit-accent));
1072
+ outline-offset: var(--inkkit-focus-offset, 1px);
1026
1073
  }
1027
1074
  .inkkit-root .inkkit-code-language-menu {
1028
1075
  position: fixed;
@@ -1072,3 +1119,7 @@
1072
1119
  .inkkit-root .inkkit-code-language-list button[aria-pressed='true'] {
1073
1120
  background: color-mix(in srgb, CanvasText 8%, transparent);
1074
1121
  }
1122
+
1123
+ .inkkit-root .milkdown-drop-indicator {
1124
+ background: var(--inkkit-drop-indicator-color, var(--_inkkit-accent));
1125
+ }
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.7",
3
+ "version": "0.3.0",
4
4
  "description": "A preserving Markdown editor for offline web views and the web",
5
5
  "license": "MIT",
6
6
  "type": "module",