@aicayzer/inkkit 0.0.7 → 0.0.9

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/dist/images.d.ts CHANGED
@@ -23,6 +23,7 @@ declare class ImageView implements NodeView {
23
23
  update(node: ProseNode): boolean;
24
24
  stopEvent(event: Event): boolean;
25
25
  ignoreMutation(): boolean;
26
+ private cancelResize;
26
27
  destroy(): void;
27
28
  private render;
28
29
  private startResize;
package/dist/images.js CHANGED
@@ -30,6 +30,7 @@ class ImageView {
30
30
  this.adapter = adapter;
31
31
  this.dom = document.createElement('span');
32
32
  this.dom.className = 'image';
33
+ this.view.dom.addEventListener('inkkit-cancel-resize', this.cancelResize);
33
34
  this.render();
34
35
  }
35
36
  update(node) {
@@ -47,7 +48,13 @@ class ImageView {
47
48
  ignoreMutation() {
48
49
  return true;
49
50
  }
51
+ cancelResize = () => {
52
+ this.stopResize?.();
53
+ this.stopResize = undefined;
54
+ this.render();
55
+ };
50
56
  destroy() {
57
+ this.view.dom.removeEventListener('inkkit-cancel-resize', this.cancelResize);
51
58
  this.stopResize?.();
52
59
  this.stopResize = undefined;
53
60
  }
@@ -91,6 +98,10 @@ class ImageView {
91
98
  const limit = this.view.dom.clientWidth;
92
99
  handle.setPointerCapture?.(event.pointerId);
93
100
  const move = (moved) => {
101
+ if (!this.view.editable) {
102
+ this.cancelResize();
103
+ return;
104
+ }
94
105
  const next = Math.round(Math.min(limit, Math.max(minimumWidth, startWidth + (moved.clientX - startX))));
95
106
  this.width = next;
96
107
  image.style.width = `${next}px`;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { InkKitEditor } from './editor.js';
2
- export type { EditorEvents, EditingMode, CaretState, Mark, Block, FormatCommand, Keymap, } from './editor.js';
2
+ export type { EditorEvents, EditorOptions, TextInputPreferences, CommandState, EditingMode, CaretState, Mark, Block, FormatCommand, Keymap, } from './editor.js';
3
3
  export type { TableCommand, TableOptions } from './tables.js';
4
4
  export type { Heading } from './outline.js';
5
5
  export { InkKitError } from './types.js';
6
- export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, ClipboardDiagram, PrintableDocument, PrintableWarning, } from './types.js';
6
+ export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, ClipboardDiagram, PrintableDocument, PrintableWarning, TextRange, ReadableTextSnapshot, TextRect, ViewportInsets, ViewportOptions, ViewportSnapshot, } from './types.js';
7
+ export type { EditorLabels } from './labels.js';
@@ -0,0 +1,28 @@
1
+ import type { Ctx } from '@milkdown/kit/ctx';
2
+ /** Built-in editor text. Hosts can override individual labels at mount time. */
3
+ export interface EditorLabels {
4
+ formattedEditor: string;
5
+ plainTextEditor: string;
6
+ sourceEditor: string;
7
+ placeholder: string;
8
+ copyCode: string;
9
+ expandCallout: string;
10
+ collapseCallout: string;
11
+ diagramRendering: string;
12
+ /** Prefix shown before the renderer's diagnostic message. */
13
+ diagramUnavailable: string;
14
+ /** Prefix followed by the authored footnote label. */
15
+ footnote: string;
16
+ tableAddRow: string;
17
+ tableAddColumn: string;
18
+ tableDeleteRow: string;
19
+ tableDeleteColumn: string;
20
+ tableAlignLeft: string;
21
+ tableAlignCenter: string;
22
+ tableAlignRight: string;
23
+ tableMoveColumn: string;
24
+ tableMoveRow: string;
25
+ }
26
+ export declare const defaultLabels: Readonly<EditorLabels>;
27
+ export declare const labelsCtx: import("@milkdown/kit/utils").$Ctx<Readonly<EditorLabels>, "inkkitLabels">;
28
+ export declare function editorLabels(ctx: Ctx): Readonly<EditorLabels>;
package/dist/labels.js ADDED
@@ -0,0 +1,26 @@
1
+ import { $ctx } from '@milkdown/kit/utils';
2
+ export const defaultLabels = {
3
+ formattedEditor: 'Formatted Markdown editor',
4
+ plainTextEditor: 'Plain text editor',
5
+ sourceEditor: 'Markdown source editor',
6
+ placeholder: 'Start typing…',
7
+ copyCode: 'Copy',
8
+ expandCallout: 'Expand',
9
+ collapseCallout: 'Collapse',
10
+ diagramRendering: 'Rendering diagram…',
11
+ diagramUnavailable: 'Diagram unavailable:',
12
+ footnote: 'Footnote',
13
+ tableAddRow: 'Add row',
14
+ tableAddColumn: 'Add column',
15
+ tableDeleteRow: 'Delete row',
16
+ tableDeleteColumn: 'Delete column',
17
+ tableAlignLeft: 'Align left',
18
+ tableAlignCenter: 'Align center',
19
+ tableAlignRight: 'Align right',
20
+ tableMoveColumn: 'Move column',
21
+ tableMoveRow: 'Move row',
22
+ };
23
+ export const labelsCtx = $ctx(defaultLabels, 'inkkitLabels');
24
+ export function editorLabels(ctx) {
25
+ return ctx.isInjected(labelsCtx.key) ? ctx.get(labelsCtx.key) : defaultLabels;
26
+ }
package/dist/mermaid.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { editorLabels } from './labels.js';
1
2
  import mermaid from 'mermaid';
2
3
  import DOMPurify from 'dompurify';
3
4
  import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
@@ -166,7 +167,7 @@ export async function diagramImage(source) {
166
167
  }
167
168
  }
168
169
  export function mermaidPreview(onError) {
169
- return $prose(() => new Plugin({
170
+ return $prose((ctx) => new Plugin({
170
171
  key: new PluginKey('mermaidPreview'),
171
172
  props: {
172
173
  decorations(state) {
@@ -179,7 +180,9 @@ export function mermaidPreview(onError) {
179
180
  const preview = document.createElement('div');
180
181
  preview.className = 'inkkit-mermaid-preview';
181
182
  preview.contentEditable = 'false';
182
- preview.textContent = 'Rendering diagram…';
183
+ preview.textContent = editorLabels(ctx).diagramRendering;
184
+ preview.setAttribute('role', 'status');
185
+ preview.setAttribute('aria-live', 'polite');
183
186
  let active = true;
184
187
  preview.cancel =
185
188
  () => {
@@ -196,7 +199,7 @@ export function mermaidPreview(onError) {
196
199
  const message = error instanceof Error
197
200
  ? error.message
198
201
  : 'The diagram could not be rendered.';
199
- preview.textContent = `Diagram unavailable: ${message}`;
202
+ preview.textContent = `${editorLabels(ctx).diagramUnavailable} ${message}`;
200
203
  preview.dataset.state = 'error';
201
204
  onError(new InkKitError('diagram-unavailable', message));
202
205
  });
package/dist/paste.d.ts CHANGED
@@ -5,6 +5,7 @@ export interface PasteControllerOptions {
5
5
  context: () => DocumentContext;
6
6
  adapter?: ImageAdapter;
7
7
  onError?: (error: unknown) => void;
8
+ onPendingChanged?: () => void;
8
9
  editable?: () => boolean;
9
10
  literalText?: () => boolean;
10
11
  }
package/dist/paste.js CHANGED
@@ -70,6 +70,7 @@ export class PasteController {
70
70
  }
71
71
  cancelPending() {
72
72
  this.active = undefined;
73
+ this.options.onPendingChanged?.();
73
74
  }
74
75
  destroy() {
75
76
  this.dead = true;
@@ -101,6 +102,7 @@ export class PasteController {
101
102
  to: view.state.selection.to,
102
103
  };
103
104
  this.active = operation;
105
+ this.options.onPendingChanged?.();
104
106
  try {
105
107
  const images = [...(input.images ?? [])];
106
108
  for (const file of files)
@@ -428,8 +430,10 @@ export class PasteController {
428
430
  .scrollIntoView());
429
431
  }
430
432
  finally {
431
- if (this.active === operation)
433
+ if (this.active === operation) {
432
434
  this.active = undefined;
435
+ this.options.onPendingChanged?.();
436
+ }
433
437
  }
434
438
  }
435
439
  }
@@ -1,3 +1,4 @@
1
+ import { editorLabels } from './labels.js';
1
2
  import { $node, $remark, $prose } from '@milkdown/kit/utils';
2
3
  import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
3
4
  import { linkSchema } from '@milkdown/kit/preset/commonmark';
@@ -247,7 +248,7 @@ export const referenceDefinition = $node('reference_definition', () => ({
247
248
  },
248
249
  },
249
250
  }));
250
- export const footnoteReference = $node('footnote_reference', () => ({
251
+ export const footnoteReference = $node('footnote_reference', (ctx) => ({
251
252
  inline: true,
252
253
  group: 'inline',
253
254
  atom: true,
@@ -268,7 +269,7 @@ export const footnoteReference = $node('footnote_reference', () => ({
268
269
  class: 'inkkit-footnote-reference',
269
270
  tabindex: '0',
270
271
  role: 'button',
271
- 'aria-label': `Footnote ${node.attrs.label}`,
272
+ 'aria-label': `${editorLabels(ctx).footnote} ${node.attrs.label}`,
272
273
  'data-inkkit-footnote-reference': node.attrs.identifier,
273
274
  'data-inkkit-label': node.attrs.label,
274
275
  },
@@ -292,7 +293,7 @@ export const footnoteReference = $node('footnote_reference', () => ({
292
293
  },
293
294
  },
294
295
  }));
295
- export const footnoteDefinition = $node('footnote_definition', () => ({
296
+ export const footnoteDefinition = $node('footnote_definition', (ctx) => ({
296
297
  group: 'block',
297
298
  content: 'block+',
298
299
  defining: true,
@@ -310,7 +311,7 @@ export const footnoteDefinition = $node('footnote_definition', () => ({
310
311
  'div',
311
312
  {
312
313
  class: 'inkkit-footnote-definition',
313
- 'aria-label': `Footnote ${node.attrs.label}`,
314
+ 'aria-label': `${editorLabels(ctx).footnote} ${node.attrs.label}`,
314
315
  'data-inkkit-footnote-definition': node.attrs.identifier,
315
316
  'data-inkkit-label': node.attrs.label,
316
317
  },
package/dist/search.d.ts CHANGED
@@ -10,5 +10,6 @@ export interface LiteralReplacement extends TextMatch {
10
10
  }
11
11
  export declare function findLiteral(text: string, search: string, from?: number): TextMatch | undefined;
12
12
  export declare function replaceLiteral(text: string, search: string, replacement: string, selection: TextMatch, all?: boolean): LiteralReplacement;
13
- export declare function findFormatted(view: EditorView, text: string): void;
13
+ export declare function findFormatted(view: EditorView, text: string): boolean;
14
14
  export declare function replaceFormatted(view: EditorView, search: string, replacement: string, preflight: (doc: ProseNode) => void, all?: boolean): number;
15
+ export declare function replaceFormattedRange(view: EditorView, from: number, to: number, replacement: string, preflight: (doc: ProseNode) => void): boolean;
package/dist/search.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Fragment, Slice } from '@milkdown/kit/prose/model';
2
2
  import { closeHistory } from '@milkdown/kit/prose/history';
3
- import { Selection, TextSelection } from '@milkdown/kit/prose/state';
3
+ import { Selection, TextSelection, } from '@milkdown/kit/prose/state';
4
4
  import { SearchQuery, setSearchState, getSearchState } from 'prosemirror-search';
5
5
  import { InkKitError } from './types.js';
6
6
  function escapedPattern(text) {
@@ -82,21 +82,44 @@ export function findFormatted(view, text) {
82
82
  const query = formattedQuery(text);
83
83
  const previous = getSearchState(view.state)?.query.search;
84
84
  let tr = setSearchState(view.state.tr, query);
85
+ let found = false;
85
86
  if (text) {
86
87
  const start = previous === query.search
87
88
  ? view.state.selection.to
88
89
  : view.state.selection.from;
89
90
  const match = query.findNext(view.state, start) ?? query.findNext(view.state, 0);
90
- if (match)
91
+ if (match) {
92
+ found = true;
91
93
  tr = tr
92
94
  .setSelection(TextSelection.create(tr.doc, match.from, match.to))
93
95
  .scrollIntoView();
96
+ }
94
97
  else
95
98
  tr = tr.setSelection(Selection.near(view.state.selection.$to));
96
99
  }
97
100
  else if (previous)
98
101
  tr = tr.setSelection(Selection.near(view.state.selection.$to));
99
102
  view.dispatch(tr);
103
+ return found;
104
+ }
105
+ function replacementSlice(state, from, to, value) {
106
+ const start = state.doc.resolve(from), end = state.doc.resolve(to);
107
+ const marks = start.marksAcross(end) ?? start.marks();
108
+ const nodes = [];
109
+ if (start.parent.type.spec.code) {
110
+ if (value)
111
+ nodes.push(state.schema.text(value, marks));
112
+ }
113
+ else {
114
+ const lines = value.split('\n');
115
+ lines.forEach((line, index) => {
116
+ if (index)
117
+ nodes.push(state.schema.nodes.hardbreak.create({ isHTML: index === lines.length - 1 && line === '' }, null, marks));
118
+ if (line)
119
+ nodes.push(state.schema.text(line, marks));
120
+ });
121
+ }
122
+ return new Slice(Fragment.fromArray(nodes), 0, 0);
100
123
  }
101
124
  export function replaceFormatted(view, search, replacement, preflight, all = false) {
102
125
  if (!search)
@@ -132,26 +155,7 @@ export function replaceFormatted(view, search, replacement, preflight, all = fal
132
155
  return 0;
133
156
  let tr = setSearchState(state.tr, query);
134
157
  for (const match of [...changed].reverse()) {
135
- const from = state.doc.resolve(match.from);
136
- const to = state.doc.resolve(match.to);
137
- const marks = from.marksAcross(to) ?? from.marks();
138
- let content = Fragment.empty;
139
- if (from.parent.type.spec.code) {
140
- if (value)
141
- content = Fragment.from(state.schema.text(value, marks));
142
- }
143
- else {
144
- const nodes = [];
145
- const lines = value.split('\n');
146
- lines.forEach((line, index) => {
147
- if (index)
148
- nodes.push(state.schema.nodes.hardbreak.create({ isHTML: index === lines.length - 1 && line === '' }, null, marks));
149
- if (line)
150
- nodes.push(state.schema.text(line, marks));
151
- });
152
- content = Fragment.fromArray(nodes);
153
- }
154
- tr.replace(match.from, match.to, new Slice(content, 0, 0));
158
+ tr.replace(match.from, match.to, replacementSlice(state, match.from, match.to, value));
155
159
  }
156
160
  if (!all) {
157
161
  const match = changed[0];
@@ -169,3 +173,30 @@ export function replaceFormatted(view, search, replacement, preflight, all = fal
169
173
  view.dispatch(closeHistory(view.state.tr).setMeta('addToHistory', false));
170
174
  return changed.length;
171
175
  }
176
+ export function replaceFormattedRange(view, from, to, replacement, preflight) {
177
+ const start = view.state.doc.resolve(from);
178
+ const end = view.state.doc.resolve(to);
179
+ let textOnly = start.parent === end.parent && start.parent.inlineContent;
180
+ view.state.doc.nodesBetween(from, to, (node) => {
181
+ if (!node.isText && !node.isTextblock)
182
+ textOnly = false;
183
+ });
184
+ if (!textOnly)
185
+ throw new InkKitError('invalid-range', 'Replacement requires literal text within one block');
186
+ const value = replacement.replace(/\r\n?/g, '\n');
187
+ if (view.state.doc.textBetween(from, to) === value)
188
+ return false;
189
+ const tr = closeHistory(view.state.tr.replace(from, to, replacementSlice(view.state, from, to, value)));
190
+ tr.setSelection(TextSelection.create(tr.doc, from, tr.mapping.map(to, 1)));
191
+ try {
192
+ preflight(tr.doc);
193
+ }
194
+ catch (error) {
195
+ if (error instanceof InkKitError)
196
+ throw error;
197
+ throw new InkKitError('preservation', error instanceof Error ? error.message : 'Cannot preserve replacement');
198
+ }
199
+ view.dispatch(tr);
200
+ view.dispatch(closeHistory(view.state.tr).setMeta('addToHistory', false));
201
+ return true;
202
+ }
package/dist/selection.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
2
2
  import { $prose } from '@milkdown/kit/utils';
3
3
  /** The stylesheet paints the selection under this name. */
4
- const name = 'selection';
4
+ const name = 'inkkit-selection';
5
+ const ranges = new Map();
5
6
  /** The selected text within each block. */
6
7
  function covered(state) {
7
8
  const { from, to, empty, visible } = state.selection;
@@ -27,21 +28,32 @@ function domRange(view, span) {
27
28
  range.setEnd(end.node, end.offset);
28
29
  return range;
29
30
  }
30
- function paint(view, highlight) {
31
- highlight.clear();
32
- for (const span of covered(view.state) ?? [])
33
- highlight.add(domRange(view, span));
31
+ function paint(view) {
32
+ ranges.set(view, (covered(view.state) ?? []).map((span) => domRange(view, span)));
33
+ refresh();
34
+ }
35
+ function refresh() {
36
+ if (ranges.size === 0) {
37
+ CSS.highlights.delete(name);
38
+ return;
39
+ }
40
+ const highlight = new Highlight();
41
+ for (const selection of ranges.values())
42
+ for (const range of selection)
43
+ highlight.add(range);
44
+ CSS.highlights.set(name, highlight);
34
45
  }
35
46
  // Paint only selected text, without WebKit filling the surrounding margins.
36
47
  export const selectionPlugin = $prose(() => new Plugin({
37
48
  key: new PluginKey('selection'),
38
49
  view(view) {
39
- const highlight = new Highlight();
40
- CSS.highlights.set(name, highlight);
41
- paint(view, highlight);
50
+ paint(view);
42
51
  return {
43
- update: (view) => paint(view, highlight),
44
- destroy: () => CSS.highlights.delete(name),
52
+ update: (view) => paint(view),
53
+ destroy: () => {
54
+ ranges.delete(view);
55
+ refresh();
56
+ },
45
57
  };
46
58
  },
47
59
  }));
package/dist/source.d.ts CHANGED
@@ -14,4 +14,3 @@ export declare function sourceSelection(source: string, start: number, end: numb
14
14
  markdown: string;
15
15
  shareable: string;
16
16
  };
17
- export declare function revealSourceCaret(plain: HTMLTextAreaElement): void;
package/dist/source.js CHANGED
@@ -55,63 +55,3 @@ export function sourceSelection(source, start, end) {
55
55
  shareable += source.slice(cursor, to);
56
56
  return { markdown: source.slice(from, to), shareable };
57
57
  }
58
- export function revealSourceCaret(plain) {
59
- if (!plain.clientWidth || !plain.clientHeight)
60
- return;
61
- const document = plain.ownerDocument;
62
- const style = document.defaultView.getComputedStyle(plain);
63
- const mirror = document.createElement('div');
64
- for (const property of [
65
- 'font',
66
- 'font-family',
67
- 'font-size',
68
- 'font-weight',
69
- 'font-style',
70
- 'font-stretch',
71
- 'font-variant',
72
- 'line-height',
73
- 'letter-spacing',
74
- 'word-spacing',
75
- 'tab-size',
76
- 'padding',
77
- 'text-indent',
78
- 'text-transform',
79
- 'direction',
80
- 'word-break',
81
- ])
82
- mirror.style.setProperty(property, style.getPropertyValue(property));
83
- Object.assign(mirror.style, {
84
- position: 'fixed',
85
- visibility: 'hidden',
86
- left: '-10000px',
87
- top: '0',
88
- width: `${plain.clientWidth}px`,
89
- boxSizing: 'border-box',
90
- whiteSpace: plain.wrap === 'off' ? 'pre' : 'pre-wrap',
91
- overflowWrap: plain.wrap === 'off' ? 'normal' : 'break-word',
92
- });
93
- mirror.setAttribute('aria-hidden', 'true');
94
- const caret = document.createElement('span');
95
- caret.setAttribute('data-inkkit-source-caret', '');
96
- caret.textContent = plain.value.slice(plain.selectionStart) || '\u200b';
97
- mirror.append(document.createTextNode(plain.value.slice(0, plain.selectionStart)), caret);
98
- document.body.append(mirror);
99
- try {
100
- // Textareas expose selection offsets but no caret geometry; matching their layout also accounts for wrapped lines.
101
- const rect = caret.getClientRects()[0] ?? caret.getBoundingClientRect();
102
- const top = rect.top - mirror.getBoundingClientRect().top;
103
- const lineHeight = Math.max(rect.height, Number.parseFloat(style.lineHeight) || rect.height);
104
- const insetTop = Number.parseFloat(style.paddingTop) || 0;
105
- const insetBottom = Number.parseFloat(style.paddingBottom) || 0;
106
- if (plain.selectionStart === 0)
107
- plain.scrollTop = 0;
108
- else if (top < plain.scrollTop + insetTop)
109
- plain.scrollTop = Math.max(0, top - insetTop);
110
- else if (top + lineHeight >
111
- plain.scrollTop + plain.clientHeight - insetBottom)
112
- plain.scrollTop = Math.max(0, top + lineHeight - plain.clientHeight + insetBottom);
113
- }
114
- finally {
115
- mirror.remove();
116
- }
117
- }