@aicayzer/inkkit 0.0.4 → 0.0.6

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.
@@ -0,0 +1,46 @@
1
+ export function normalizeTableAlignment(element) {
2
+ const alignment = (element.style.textAlign ||
3
+ element.getAttribute('align') ||
4
+ '')
5
+ .trim()
6
+ .toLowerCase();
7
+ if (alignment === 'left' || alignment === 'center' || alignment === 'right')
8
+ return alignment;
9
+ if (alignment !== 'start' && alignment !== 'end')
10
+ return null;
11
+ let direction = 'ltr';
12
+ for (let current = element; current; current = current.parentElement) {
13
+ const value = (current.style.direction || current.getAttribute('dir') || '')
14
+ .trim()
15
+ .toLowerCase();
16
+ if (value === 'ltr' || value === 'rtl') {
17
+ direction = value;
18
+ break;
19
+ }
20
+ if (value === 'auto') {
21
+ const first = [...(current.textContent ?? '')].find((character) => /[\u061c\u200e\u200f]|\p{Letter}/u.test(character));
22
+ if (first) {
23
+ direction =
24
+ /[\u0590-\u08ff\u200f\ufb1d-\ufdff\ufe70-\ufeff\u{10800}-\u{10fff}\u{1e800}-\u{1edff}\u{1e900}-\u{1e95f}]/u.test(first)
25
+ ? 'rtl'
26
+ : 'ltr';
27
+ break;
28
+ }
29
+ }
30
+ }
31
+ return alignment === 'start'
32
+ ? direction === 'rtl'
33
+ ? 'right'
34
+ : 'left'
35
+ : direction === 'rtl'
36
+ ? 'left'
37
+ : 'right';
38
+ }
39
+ export function normalizeTableColumnAlignment(element) {
40
+ if (!(element instanceof HTMLTableCellElement))
41
+ return normalizeTableAlignment(element);
42
+ const table = element.closest('table');
43
+ const header = table?.rows[0]?.cells[element.cellIndex];
44
+ // GFM represents alignment once per column, independently of cell direction.
45
+ return normalizeTableAlignment(header ?? element);
46
+ }
package/dist/tables.d.ts CHANGED
@@ -1,7 +1,18 @@
1
1
  import type { Ctx } from '@milkdown/kit/ctx';
2
- export type TableCommand = 'insert' | 'addRowBefore' | 'addRowAfter' | 'addColumnBefore' | 'addColumnAfter' | 'deleteRow' | 'deleteColumn' | 'deleteTable' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'exit';
3
- export declare const tablePlugins: (((ctx: Ctx) => () => void) | import("@milkdown/kit/ctx").MilkdownPlugin)[];
4
- export declare function tableCommand(ctx: Ctx, command: TableCommand, options?: {
2
+ import { type Node as ProseNode } from '@milkdown/kit/prose/model';
3
+ import type { EditorView } from '@milkdown/kit/prose/view';
4
+ import type { Transaction } from '@milkdown/kit/prose/state';
5
+ export type TableCommand = 'insert' | 'addRowBefore' | 'addRowAfter' | 'addColumnBefore' | 'addColumnAfter' | 'deleteRow' | 'deleteColumn' | 'deleteTable' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'moveRowUp' | 'moveRowDown' | 'moveColumnLeft' | 'moveColumnRight' | 'sortRows' | 'exit';
6
+ export interface TableOptions {
5
7
  rows?: number;
6
8
  columns?: number;
7
- }): boolean;
9
+ column?: number;
10
+ order?: 'ascending' | 'descending';
11
+ comparison?: 'text' | 'number';
12
+ }
13
+ export declare const tablePlugins: (((ctx: Ctx) => () => void) | import("@milkdown/kit/ctx").MilkdownPlugin)[];
14
+ export declare function tableCommand(ctx: Ctx, command: TableCommand, options?: TableOptions): boolean;
15
+ export declare function dispatchTableOperation(view: EditorView, tr: Transaction): void;
16
+ export declare function configureTableMovement(ctx: Ctx, canEdit: () => boolean): void;
17
+ export declare function editableTable(table: ProseNode): boolean;
18
+ export declare function selectTableRectangle(tr: Transaction, tableStart: number, table: ProseNode, top: number, left: number, bottom: number, right: number): void;
package/dist/tables.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { commandsCtx, editorViewCtx } from '@milkdown/kit/core';
2
2
  import { tableBlock, tableBlockConfig, } from '@milkdown/kit/component/table-block';
3
- import { insertTableCommand, exitTable } from '@milkdown/kit/preset/gfm';
4
- import { addRowAfter, addRowBefore, addColumnAfter, addColumnBefore, deleteRow, deleteColumn, deleteTable, goToNextCell, selectedRect, TableMap, isInTable, } from '@milkdown/kit/prose/tables';
3
+ import { insertTableCommand, exitTable, moveRowCommand, moveColCommand, } from '@milkdown/kit/preset/gfm';
4
+ import { addRowAfter, addRowBefore, addColumnAfter, addColumnBefore, deleteRow, deleteColumn, deleteTable, goToNextCell, selectedRect, TableMap, isInTable, CellSelection, } from '@milkdown/kit/prose/tables';
5
+ import { Fragment } from '@milkdown/kit/prose/model';
6
+ import { closeHistory } from '@milkdown/kit/prose/history';
5
7
  import { keymap } from '@milkdown/kit/prose/keymap';
6
8
  import { $prose } from '@milkdown/kit/utils';
9
+ import { clipboardText } from './clipboard.js';
7
10
  const labels = {
8
11
  add_row: 'Add row',
9
12
  add_col: 'Add column',
@@ -16,6 +19,8 @@ const labels = {
16
19
  row_drag_handle: 'Move row',
17
20
  };
18
21
  export const tablePlugins = [
22
+ moveRowCommand,
23
+ moveColCommand,
19
24
  tableBlockConfig,
20
25
  (ctx) => () => {
21
26
  ctx.update(tableBlockConfig.key, (config) => ({
@@ -42,6 +47,8 @@ export function tableCommand(ctx, command, options) {
42
47
  }
43
48
  if (command === 'exit')
44
49
  return commands.call(exitTable.key);
50
+ if (command.startsWith('move') || command === 'sortRows')
51
+ return refineTable(view, command, options);
45
52
  if (command === 'alignLeft' ||
46
53
  command === 'alignCenter' ||
47
54
  command === 'alignRight') {
@@ -78,3 +85,162 @@ export function tableCommand(ctx, command, options) {
78
85
  };
79
86
  return actions[command](view.state, view.dispatch);
80
87
  }
88
+ export function dispatchTableOperation(view, tr) {
89
+ view.dispatch(closeHistory(tr).scrollIntoView());
90
+ view.dispatch(closeHistory(view.state.tr).setMeta('addToHistory', false));
91
+ }
92
+ export function configureTableMovement(ctx, canEdit) {
93
+ const commands = ctx.get(commandsCtx);
94
+ for (const [key, rowMovement] of [
95
+ [moveRowCommand.key, true],
96
+ [moveColCommand.key, false],
97
+ ]) {
98
+ commands.remove(key);
99
+ commands.create(key, (options = {}) => () => {
100
+ const view = ctx.get(editorViewCtx);
101
+ if (!canEdit() ||
102
+ !view.editable ||
103
+ view.composing ||
104
+ !isInTable(view.state))
105
+ return false;
106
+ const rect = selectedRect(view.state);
107
+ const from = options.from ?? 0, to = options.to ?? 0;
108
+ const length = rowMovement ? rect.map.height : rect.map.width;
109
+ if (!editableTable(rect.table) ||
110
+ (options.pos != null && options.pos !== rect.tableStart) ||
111
+ !Number.isInteger(from) ||
112
+ !Number.isInteger(to) ||
113
+ from < 0 ||
114
+ to < 0 ||
115
+ from >= length ||
116
+ to >= length ||
117
+ from === to ||
118
+ (rowMovement && (from === 0 || to === 0)))
119
+ return false;
120
+ const rows = [];
121
+ rect.table.forEach((row) => rows.push(row));
122
+ if (rowMovement) {
123
+ const moved = rows.splice(from, 1)[0];
124
+ rows.splice(to, 0, moved);
125
+ }
126
+ else {
127
+ for (let index = 0; index < rows.length; index++) {
128
+ const row = rows[index], cells = [];
129
+ row.forEach((cell) => cells.push(cell));
130
+ cells.splice(to, 0, cells.splice(from, 1)[0]);
131
+ rows[index] = row.copy(Fragment.fromArray(cells));
132
+ }
133
+ }
134
+ const table = rect.table.copy(Fragment.fromArray(rows));
135
+ const tr = view.state.tr.replaceWith(rect.tableStart - 1, rect.tableStart - 1 + rect.table.nodeSize, table);
136
+ selectTableRectangle(tr, rect.tableStart, table, rowMovement ? to : 0, rowMovement ? 0 : to, rowMovement ? to + 1 : rect.map.height, rowMovement ? rect.map.width : to + 1);
137
+ dispatchTableOperation(view, tr);
138
+ return true;
139
+ });
140
+ }
141
+ }
142
+ export function editableTable(table) {
143
+ const map = TableMap.get(table);
144
+ if (map.problems?.length || map.width > 100 || map.height > 100)
145
+ return false;
146
+ let valid = table.firstChild?.type.name === 'table_header_row';
147
+ table.forEach((row, _offset, index) => {
148
+ valid &&= row.type.name === (index ? 'table_row' : 'table_header_row');
149
+ valid &&= row.childCount === map.width;
150
+ row.forEach((cell) => {
151
+ valid &&= cell.attrs.colspan === 1 && cell.attrs.rowspan === 1;
152
+ });
153
+ });
154
+ return Boolean(valid);
155
+ }
156
+ function refineTable(view, command, options) {
157
+ if (!isInTable(view.state))
158
+ return false;
159
+ const rect = selectedRect(view.state);
160
+ if (!editableTable(rect.table))
161
+ return false;
162
+ const rows = [];
163
+ rect.table.forEach((row) => rows.push(row));
164
+ let top = rect.top, bottom = rect.bottom, left = rect.left, right = rect.right;
165
+ if (command === 'sortRows') {
166
+ const column = options?.column ?? rect.left;
167
+ if (!Number.isInteger(column) || column < 0 || column >= rect.map.width)
168
+ throw new RangeError('Sort column must identify an existing table column');
169
+ const order = options?.order ?? 'ascending';
170
+ const comparison = options?.comparison ?? 'text';
171
+ if (!['ascending', 'descending'].includes(order))
172
+ throw new RangeError('Sort order must be ascending or descending');
173
+ if (!['text', 'number'].includes(comparison))
174
+ throw new RangeError('Sort comparison must be text or number');
175
+ const body = rows.slice(1).map((row, index) => ({
176
+ row,
177
+ index,
178
+ key: clipboardText(row.child(column).content),
179
+ }));
180
+ if (comparison === 'number' &&
181
+ body.some(({ key }) => {
182
+ const value = key.trim();
183
+ return value !== '' && decimalNumber(value) === undefined;
184
+ }))
185
+ throw new RangeError('Numeric sorting requires finite decimal values or blank cells');
186
+ body.sort((a, b) => compareCells(a.key, b.key, comparison, order) || a.index - b.index);
187
+ if (body.every(({ row }, index) => row === rows[index + 1]))
188
+ return false;
189
+ const activeIndex = top - 1;
190
+ rows.splice(1, rows.length - 1, ...body.map(({ row }) => row));
191
+ top =
192
+ top === 0 ? 0 : body.findIndex(({ index }) => index === activeIndex) + 1;
193
+ bottom = top + 1;
194
+ }
195
+ else if (command === 'moveRowUp' || command === 'moveRowDown') {
196
+ const up = command === 'moveRowUp';
197
+ if (top === 0 || (up ? top === 1 : bottom === rows.length))
198
+ return false;
199
+ const moved = rows.splice(top, bottom - top);
200
+ const destination = top + (up ? -1 : 1);
201
+ rows.splice(destination, 0, ...moved);
202
+ top = destination;
203
+ bottom += up ? -1 : 1;
204
+ }
205
+ else {
206
+ const backward = command === 'moveColumnLeft';
207
+ if (backward ? left === 0 : right === rect.map.width)
208
+ return false;
209
+ for (let index = 0; index < rows.length; index++) {
210
+ const row = rows[index];
211
+ const cells = [];
212
+ row.forEach((cell) => cells.push(cell));
213
+ const moved = cells.splice(left, right - left);
214
+ cells.splice(left + (backward ? -1 : 1), 0, ...moved);
215
+ rows[index] = row.copy(Fragment.fromArray(cells));
216
+ }
217
+ left += backward ? -1 : 1;
218
+ right += backward ? -1 : 1;
219
+ }
220
+ const table = rect.table.copy(Fragment.fromArray(rows));
221
+ const tr = view.state.tr.replaceWith(rect.tableStart - 1, rect.tableStart - 1 + rect.table.nodeSize, table);
222
+ selectTableRectangle(tr, rect.tableStart, table, top, left, bottom, right);
223
+ dispatchTableOperation(view, tr);
224
+ return true;
225
+ }
226
+ export function selectTableRectangle(tr, tableStart, table, top, left, bottom, right) {
227
+ const map = TableMap.get(table);
228
+ tr.setSelection(CellSelection.create(tr.doc, tableStart + map.map[top * map.width + left], tableStart + map.map[(bottom - 1) * map.width + right - 1]));
229
+ }
230
+ function compareCells(a, b, comparison, order) {
231
+ const x = a.trim(), y = b.trim();
232
+ if (!x || !y)
233
+ return x === y ? 0 : x ? -1 : 1;
234
+ const direction = order === 'ascending' ? 1 : -1;
235
+ const text = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
236
+ if (comparison === 'text')
237
+ return direction * text(a, b);
238
+ const nx = decimalNumber(x), ny = decimalNumber(y);
239
+ return direction * (nx < ny ? -1 : nx > ny ? 1 : 0);
240
+ }
241
+ function decimalNumber(value) {
242
+ return /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(value) &&
243
+ Number.isFinite(Number(value))
244
+ ? Number(value)
245
+ : undefined;
246
+ }
package/dist/types.d.ts CHANGED
@@ -64,3 +64,17 @@ export interface ClipboardDiagram {
64
64
  image?: PortableImage;
65
65
  error?: string;
66
66
  }
67
+ export interface PrintableWarning {
68
+ code: 'diagram-unavailable';
69
+ message: string;
70
+ }
71
+ export interface PrintableDocument {
72
+ documentId: string;
73
+ generation: number;
74
+ revision: number;
75
+ format: DocumentFormat;
76
+ html: string;
77
+ styles: string;
78
+ assets: readonly PortableImage[];
79
+ warnings: readonly PrintableWarning[];
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicayzer/inkkit",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "A preserving Markdown editor for offline web views and the web",
5
5
  "license": "MIT",
6
6
  "type": "module",