@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/tables.d.ts CHANGED
@@ -1,8 +1,17 @@
1
1
  import type { Ctx } from '@milkdown/kit/ctx';
2
2
  import { type Node as ProseNode } from '@milkdown/kit/prose/model';
3
3
  import type { EditorView } from '@milkdown/kit/prose/view';
4
- import type { Transaction } from '@milkdown/kit/prose/state';
4
+ import type { EditorState, Transaction } from '@milkdown/kit/prose/state';
5
5
  export type TableCommand = 'insert' | 'addRowBefore' | 'addRowAfter' | 'addColumnBefore' | 'addColumnAfter' | 'deleteRow' | 'deleteColumn' | 'deleteTable' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'moveRowUp' | 'moveRowDown' | 'moveColumnLeft' | 'moveColumnRight' | 'sortRows' | 'exit';
6
+ export declare const tableCommands: readonly TableCommand[];
7
+ export declare function tableContext(state: EditorState): {
8
+ row: number;
9
+ column: number;
10
+ rows: number;
11
+ columns: number;
12
+ } | undefined;
13
+ /** Checks the same default operations without dispatching a transaction. */
14
+ export declare function tableAvailability(ctx: Ctx): Record<TableCommand, boolean>;
6
15
  export interface TableOptions {
7
16
  rows?: number;
8
17
  columns?: number;
package/dist/tables.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { editorLabels } from './labels.js';
1
2
  import { commandsCtx, editorViewCtx } from '@milkdown/kit/core';
2
3
  import { tableBlock, tableBlockConfig, } from '@milkdown/kit/component/table-block';
3
4
  import { insertTableCommand, exitTable, moveRowCommand, moveColCommand, } from '@milkdown/kit/preset/gfm';
@@ -7,17 +8,92 @@ import { closeHistory } from '@milkdown/kit/prose/history';
7
8
  import { keymap } from '@milkdown/kit/prose/keymap';
8
9
  import { $prose } from '@milkdown/kit/utils';
9
10
  import { clipboardText } from './clipboard.js';
10
- const labels = {
11
- add_row: 'Add row',
12
- add_col: 'Add column',
13
- delete_row: 'Delete row',
14
- delete_col: 'Delete column',
15
- align_col_left: 'Align left',
16
- align_col_center: 'Align center',
17
- align_col_right: 'Align right',
18
- col_drag_handle: 'Move column',
19
- row_drag_handle: 'Move row',
11
+ export const tableCommands = [
12
+ 'insert',
13
+ 'addRowBefore',
14
+ 'addRowAfter',
15
+ 'addColumnBefore',
16
+ 'addColumnAfter',
17
+ 'deleteRow',
18
+ 'deleteColumn',
19
+ 'deleteTable',
20
+ 'alignLeft',
21
+ 'alignCenter',
22
+ 'alignRight',
23
+ 'moveRowUp',
24
+ 'moveRowDown',
25
+ 'moveColumnLeft',
26
+ 'moveColumnRight',
27
+ 'sortRows',
28
+ 'exit',
29
+ ];
30
+ export function tableContext(state) {
31
+ if (!isInTable(state))
32
+ return undefined;
33
+ const rect = selectedRect(state);
34
+ return {
35
+ row: rect.top,
36
+ column: rect.left,
37
+ rows: rect.map.height,
38
+ columns: rect.map.width,
39
+ };
40
+ }
41
+ /** Checks the same default operations without dispatching a transaction. */
42
+ export function tableAvailability(ctx) {
43
+ const view = ctx.get(editorViewCtx);
44
+ const state = view.state;
45
+ const commands = ctx.get(commandsCtx);
46
+ const available = Object.fromEntries(tableCommands.map((name) => [name, false]));
47
+ available.insert = commands.get(insertTableCommand.key)({ row: 3, col: 2 })(state);
48
+ const actions = {
49
+ addRowBefore,
50
+ addRowAfter,
51
+ addColumnBefore,
52
+ addColumnAfter,
53
+ deleteRow,
54
+ deleteColumn,
55
+ deleteTable,
56
+ };
57
+ for (const [name, action] of Object.entries(actions))
58
+ available[name] = action(state);
59
+ available.exit = commands.get(exitTable.key)()(state);
60
+ if (!isInTable(state))
61
+ return available;
62
+ const rect = selectedRect(state);
63
+ available.alignLeft = available.alignCenter = available.alignRight = true;
64
+ if (!editableTable(rect.table))
65
+ return available;
66
+ available.moveRowUp = rect.top > 1;
67
+ available.moveRowDown = rect.top > 0 && rect.bottom < rect.map.height;
68
+ available.moveColumnLeft = rect.left > 0;
69
+ available.moveColumnRight = rect.right < rect.map.width;
70
+ const body = [];
71
+ rect.table.forEach((row, _offset, index) => {
72
+ if (index)
73
+ body.push({ key: clipboardText(row.child(rect.left).content), index });
74
+ });
75
+ const sorted = [...body].sort((a, b) => compareCells(a.key, b.key, 'text', 'ascending') || a.index - b.index);
76
+ available.sortRows = sorted.some((row, index) => row !== body[index]);
77
+ return available;
78
+ }
79
+ const tableLabels = {
80
+ add_row: 'tableAddRow',
81
+ add_col: 'tableAddColumn',
82
+ delete_row: 'tableDeleteRow',
83
+ delete_col: 'tableDeleteColumn',
84
+ align_col_left: 'tableAlignLeft',
85
+ align_col_center: 'tableAlignCenter',
86
+ align_col_right: 'tableAlignRight',
87
+ col_drag_handle: 'tableMoveColumn',
88
+ row_drag_handle: 'tableMoveRow',
20
89
  };
90
+ function tableButton(label) {
91
+ const span = document.createElement('span');
92
+ span.setAttribute('role', 'img');
93
+ span.setAttribute('aria-label', label);
94
+ span.textContent = label;
95
+ return span.outerHTML;
96
+ }
21
97
  export const tablePlugins = [
22
98
  moveRowCommand,
23
99
  moveColCommand,
@@ -25,7 +101,7 @@ export const tablePlugins = [
25
101
  (ctx) => () => {
26
102
  ctx.update(tableBlockConfig.key, (config) => ({
27
103
  ...config,
28
- renderButton: (type) => `<span role="img" aria-label="${labels[type]}">${labels[type]}</span>`,
104
+ renderButton: (type) => tableButton(editorLabels(ctx)[tableLabels[type]]),
29
105
  }));
30
106
  },
31
107
  tableBlock,
package/dist/tasks.js CHANGED
@@ -8,6 +8,8 @@ export const taskListPlugin = $prose(() => new Plugin({
8
8
  key: new PluginKey('taskToggle'),
9
9
  props: {
10
10
  handleClickOn(view, _pos, node, nodePos, event) {
11
+ if (!view.editable)
12
+ return false;
11
13
  if (node.type.name !== 'list_item' || node.attrs.checked == null)
12
14
  return false;
13
15
  const target = event.target;
@@ -0,0 +1,26 @@
1
+ import type { Node as ProseNode } from '@milkdown/kit/prose/model';
2
+ import type { EditorView } from '@milkdown/kit/prose/view';
3
+ import { type TextRect } from './types.js';
4
+ export interface TextSpan {
5
+ from: number;
6
+ to: number;
7
+ start: number;
8
+ end: number;
9
+ kind: 'text' | 'break' | 'embed' | 'separator';
10
+ }
11
+ export interface TextProjection {
12
+ text: string;
13
+ spans: TextSpan[];
14
+ }
15
+ export declare function readableProjection(doc: ProseNode): TextProjection;
16
+ export declare function projectionPosition(projection: TextProjection, offset: number, side: -1 | 1): number;
17
+ export declare function projectionOffset(projection: TextProjection, position: number): number;
18
+ export declare function validateOffsets(text: string, from: number, to: number): void;
19
+ export declare function textRect(rect: Pick<TextRect, 'left' | 'top' | 'right' | 'bottom'>): TextRect;
20
+ export declare function formattedRects(view: EditorView, projection: TextProjection, from: number, to: number): TextRect[];
21
+ export declare function literalRects(plain: HTMLTextAreaElement, from: number, to: number): TextRect[];
22
+ export declare function intersectsViewport(rect: Pick<TextRect, 'top' | 'right' | 'bottom' | 'left'>, viewport: TextRect): boolean;
23
+ export declare function literalVisibleRanges(plain: HTMLTextAreaElement, viewport: TextRect): {
24
+ from: number;
25
+ to: number;
26
+ }[];
@@ -0,0 +1,228 @@
1
+ import { InkKitError } from './types.js';
2
+ export function readableProjection(doc) {
3
+ const result = { text: '', spans: [] };
4
+ let blocks = 0;
5
+ const append = (text, start, end, kind) => {
6
+ const from = result.text.length;
7
+ result.text += text;
8
+ result.spans.push({ from, to: result.text.length, start, end, kind });
9
+ };
10
+ const block = (start) => {
11
+ if (blocks++)
12
+ append('\n', result.spans.at(-1)?.end ?? start, start, 'separator');
13
+ };
14
+ doc.descendants((node, position) => {
15
+ const name = node.type.name;
16
+ if (name === 'comment_inline' ||
17
+ name === 'comment_block' ||
18
+ name === 'reference_definition')
19
+ return false;
20
+ if (name === 'inkkit_callout') {
21
+ block(position);
22
+ append(node.attrs.title ||
23
+ String(node.attrs.kind).replace(/^./, (value) => value.toUpperCase()), position, position + 1, 'embed');
24
+ return true;
25
+ }
26
+ if (node.isTextblock) {
27
+ block(position + 1);
28
+ if (!node.content.size)
29
+ append('', position + 1, position + 1, 'text');
30
+ return true;
31
+ }
32
+ if (node.isText) {
33
+ append(node.text ?? '', position, position + node.nodeSize, 'text');
34
+ return false;
35
+ }
36
+ if (name === 'hardbreak' || name === 'hard_break') {
37
+ append('\n', position, position + node.nodeSize, 'break');
38
+ return false;
39
+ }
40
+ if (name === 'image' || name === 'footnote_reference') {
41
+ append(name === 'image'
42
+ ? String(node.attrs.alt ?? '').replace(/\|\d{1,5}$/, '') || 'Image'
43
+ : `[${node.attrs.label}]`, position, position + node.nodeSize, 'embed');
44
+ return false;
45
+ }
46
+ return true;
47
+ });
48
+ return result;
49
+ }
50
+ export function projectionPosition(projection, offset, side) {
51
+ const spans = projection.spans;
52
+ const span = spans.find((span) => (offset > span.from && offset < span.to) ||
53
+ (side > 0 ? offset === span.from : offset === span.to)) ?? (side > 0 ? spans.at(-1) : spans[0]);
54
+ if (!span)
55
+ return 0;
56
+ if (span.kind === 'text')
57
+ return (span.start +
58
+ Math.max(0, Math.min(offset - span.from, span.to - span.from)));
59
+ return offset <= span.from ? span.start : span.end;
60
+ }
61
+ export function projectionOffset(projection, position) {
62
+ for (const span of projection.spans) {
63
+ if (position < span.start)
64
+ return span.from;
65
+ if (position <= span.end)
66
+ return span.kind === 'text'
67
+ ? span.from + Math.max(0, position - span.start)
68
+ : position === span.start
69
+ ? span.from
70
+ : span.to;
71
+ }
72
+ return projection.text.length;
73
+ }
74
+ export function validateOffsets(text, from, to) {
75
+ const split = (offset) => offset > 0 &&
76
+ offset < text.length &&
77
+ /[\uD800-\uDBFF]/.test(text[offset - 1]) &&
78
+ /[\uDC00-\uDFFF]/.test(text[offset]);
79
+ if (!Number.isInteger(from) ||
80
+ !Number.isInteger(to) ||
81
+ from < 0 ||
82
+ to < from ||
83
+ to > text.length ||
84
+ split(from) ||
85
+ split(to))
86
+ throw new InkKitError('invalid-range', 'Range must use valid UTF-16 boundaries in the text snapshot');
87
+ }
88
+ export function textRect(rect) {
89
+ return {
90
+ left: rect.left,
91
+ top: rect.top,
92
+ right: rect.right,
93
+ bottom: rect.bottom,
94
+ width: Math.max(0, rect.right - rect.left),
95
+ height: Math.max(0, rect.bottom - rect.top),
96
+ };
97
+ }
98
+ export function formattedRects(view, projection, from, to) {
99
+ if (from === to) {
100
+ const position = projectionPosition(projection, from, 1);
101
+ const dom = view.domAtPos(position);
102
+ const element = dom.node.nodeType === 1 ? dom.node : dom.node.parentElement;
103
+ if (!element?.getClientRects().length)
104
+ return [];
105
+ return [textRect(view.coordsAtPos(position))];
106
+ }
107
+ const rects = [];
108
+ for (const span of projection.spans) {
109
+ if (span.to <= from || span.from >= to || span.kind === 'separator')
110
+ continue;
111
+ if (span.kind === 'embed' || span.kind === 'break') {
112
+ const node = view.nodeDOM(span.start);
113
+ if (node instanceof Element) {
114
+ const label = node.matches('.inkkit-callout')
115
+ ? (node.querySelector(':scope > .inkkit-callout-header > .inkkit-callout-title') ?? node)
116
+ : node;
117
+ rects.push(...Array.from(label.getClientRects(), textRect));
118
+ }
119
+ continue;
120
+ }
121
+ const start = view.domAtPos(span.start + Math.max(0, from - span.from), 1);
122
+ const end = view.domAtPos(span.start + Math.min(span.to - span.from, to - span.from), -1);
123
+ const range = view.dom.ownerDocument.createRange();
124
+ range.setStart(start.node, start.offset);
125
+ range.setEnd(end.node, end.offset);
126
+ rects.push(...Array.from(range.getClientRects(), textRect));
127
+ }
128
+ return rects.filter((rect) => rect.height > 0 && rect.width >= 0);
129
+ }
130
+ function withLiteralLayout(plain, measure) {
131
+ if (!plain.clientWidth || !plain.clientHeight)
132
+ return undefined;
133
+ const document = plain.ownerDocument;
134
+ const style = document.defaultView.getComputedStyle(plain);
135
+ const mirror = document.createElement('div');
136
+ for (const property of [
137
+ 'font',
138
+ 'font-family',
139
+ 'font-size',
140
+ 'font-weight',
141
+ 'font-style',
142
+ 'font-stretch',
143
+ 'font-variant',
144
+ 'line-height',
145
+ 'letter-spacing',
146
+ 'word-spacing',
147
+ 'tab-size',
148
+ 'padding',
149
+ 'text-indent',
150
+ 'text-transform',
151
+ 'direction',
152
+ 'word-break',
153
+ ])
154
+ mirror.style.setProperty(property, style.getPropertyValue(property));
155
+ const origin = plain.getBoundingClientRect();
156
+ Object.assign(mirror.style, {
157
+ position: 'fixed',
158
+ visibility: 'hidden',
159
+ pointerEvents: 'none',
160
+ left: `${origin.left + plain.clientLeft - plain.scrollLeft}px`,
161
+ top: `${origin.top + plain.clientTop - plain.scrollTop}px`,
162
+ width: `${plain.clientWidth}px`,
163
+ boxSizing: 'border-box',
164
+ whiteSpace: plain.wrap === 'off' ? 'pre' : 'pre-wrap',
165
+ overflowWrap: plain.wrap === 'off' ? 'normal' : 'break-word',
166
+ });
167
+ mirror.setAttribute('aria-hidden', 'true');
168
+ const text = document.createTextNode(plain.value + '\u200b');
169
+ mirror.append(text);
170
+ document.body.append(mirror);
171
+ try {
172
+ return measure(text);
173
+ }
174
+ finally {
175
+ mirror.remove();
176
+ }
177
+ }
178
+ export function literalRects(plain, from, to) {
179
+ return (withLiteralLayout(plain, (text) => {
180
+ const range = plain.ownerDocument.createRange();
181
+ range.setStart(text, from);
182
+ range.setEnd(text, from === to ? Math.min(to + 1, text.length) : to);
183
+ return Array.from(range.getClientRects(), (rect) => textRect({
184
+ left: rect.left,
185
+ right: from === to ? rect.left : rect.right,
186
+ top: rect.top,
187
+ bottom: rect.bottom,
188
+ }));
189
+ }) ?? []);
190
+ }
191
+ export function intersectsViewport(rect, viewport) {
192
+ return (viewport.width > 0 &&
193
+ viewport.height > 0 &&
194
+ rect.bottom > viewport.top &&
195
+ rect.top < viewport.bottom &&
196
+ rect.right >= viewport.left &&
197
+ rect.left < viewport.right);
198
+ }
199
+ export function literalVisibleRanges(plain, viewport) {
200
+ return (withLiteralLayout(plain, (text) => {
201
+ const range = plain.ownerDocument.createRange();
202
+ const ranges = [];
203
+ const visible = (from, to) => {
204
+ range.setStart(text, from);
205
+ range.setEnd(text, to);
206
+ return Array.from(range.getClientRects()).some((rect) => intersectsViewport(rect, viewport));
207
+ };
208
+ for (let start = 0; start < plain.value.length;) {
209
+ const newline = plain.value.indexOf('\n', start);
210
+ const end = newline < 0 ? plain.value.length : newline + 1;
211
+ if (visible(start, end)) {
212
+ for (let from = start; from < end;) {
213
+ const to = from + (plain.value.codePointAt(from) > 0xffff ? 2 : 1);
214
+ if (visible(from, to)) {
215
+ const previous = ranges.at(-1);
216
+ if (previous?.to === from)
217
+ previous.to = to;
218
+ else
219
+ ranges.push({ from, to });
220
+ }
221
+ from = to;
222
+ }
223
+ }
224
+ start = end;
225
+ }
226
+ return ranges;
227
+ }) ?? []);
228
+ }
package/dist/types.d.ts CHANGED
@@ -14,7 +14,7 @@ export interface DocumentSnapshot extends DocumentInput {
14
14
  revision: number;
15
15
  dirty: boolean;
16
16
  }
17
- export type EditorErrorCode = 'not-ready' | 'stale-document' | 'composition' | 'operation-pending' | 'destroyed' | 'preservation' | 'image-unavailable' | 'diagram-unavailable';
17
+ export type EditorErrorCode = 'not-ready' | 'stale-document' | 'composition' | 'read-only' | 'operation-pending' | 'destroyed' | 'preservation' | 'image-unavailable' | 'diagram-unavailable' | 'invalid-range';
18
18
  export declare class InkKitError extends Error {
19
19
  readonly code: EditorErrorCode;
20
20
  constructor(code: EditorErrorCode, message: string);
@@ -78,3 +78,42 @@ export interface PrintableDocument {
78
78
  assets: readonly PortableImage[];
79
79
  warnings: readonly PrintableWarning[];
80
80
  }
81
+ export interface TextRange {
82
+ snapshotId: string;
83
+ from: number;
84
+ to: number;
85
+ }
86
+ export interface ReadableTextSnapshot {
87
+ snapshotId: string;
88
+ documentId: string;
89
+ generation: number;
90
+ revision: number;
91
+ format: DocumentFormat;
92
+ mode: 'source' | 'formatted';
93
+ text: string;
94
+ selection: TextRange;
95
+ }
96
+ export interface TextRect {
97
+ left: number;
98
+ top: number;
99
+ right: number;
100
+ bottom: number;
101
+ width: number;
102
+ height: number;
103
+ }
104
+ export interface ViewportInsets {
105
+ top: number;
106
+ right: number;
107
+ bottom: number;
108
+ left: number;
109
+ }
110
+ export interface ViewportOptions {
111
+ scrollContainer?: HTMLElement;
112
+ insets?: Partial<ViewportInsets>;
113
+ }
114
+ export interface ViewportSnapshot {
115
+ rect: TextRect;
116
+ insets: ViewportInsets;
117
+ scrollTop: number;
118
+ scrollLeft: number;
119
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicayzer/inkkit",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "A preserving Markdown editor for offline web views and the web",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,7 +42,8 @@
42
42
  "dev": "vite playground",
43
43
  "build:playground": "vite build playground --config vite.config.ts",
44
44
  "test:package": "node scripts/test-package.mjs",
45
- "check": "pnpm typecheck && pnpm test && pnpm build && pnpm test:package"
45
+ "check": "pnpm typecheck && pnpm test && pnpm build && pnpm test:package",
46
+ "test:browser": "playwright test --project=chromium"
46
47
  },
47
48
  "dependencies": {
48
49
  "@milkdown/kit": "7.22.1",
@@ -68,6 +69,7 @@
68
69
  "prosemirror-search": "^1.1.1"
69
70
  },
70
71
  "devDependencies": {
72
+ "@playwright/test": "^1.64.0",
71
73
  "@types/hast": "^3.0.5",
72
74
  "@types/mdast": "^4.0.4",
73
75
  "@types/node": "^26.6.2",