@aicayzer/inkkit 0.0.5 → 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.
- package/README.md +2 -0
- package/dist/dialect.js +14 -2
- package/dist/editor.d.ts +2 -5
- package/dist/editor.js +10 -1
- package/dist/index.d.ts +1 -1
- package/dist/paste.js +56 -6
- package/dist/preserve.js +38 -11
- package/dist/spreadsheet.d.ts +3 -0
- package/dist/spreadsheet.js +275 -0
- package/dist/table-alignment.d.ts +2 -0
- package/dist/table-alignment.js +46 -0
- package/dist/tables.d.ts +15 -4
- package/dist/tables.js +168 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ const snapshot = editor.snapshot(1)
|
|
|
35
35
|
|
|
36
36
|
Markdown includes CommonMark, task lists, strikethrough, autolinks, GFM tables, editable footnotes and reference-style links, bounded callouts, highlights, author comments and offline Mermaid diagrams. Unsupported constructs remain literal Markdown. TXT mode treats all text literally. Images require a host adapter. RTF files remain outside the package scope.
|
|
37
37
|
|
|
38
|
+
Table controls include complete row/column movement and explicit stable text or numeric sorting. Ordinary spreadsheet paste preserves cell boundaries, replaces selected cells and grows supported tables. Hosts call these operations through the public facade; each is undoable.
|
|
39
|
+
|
|
38
40
|
`await editor.printableSnapshot(1)` returns a complete printable HTML document, print styles and portable image bytes. It excludes author comments, includes folded content and reports unavailable assets or a changed document. Client apps own Print/PDF commands, dialogs and file generation.
|
|
39
41
|
|
|
40
42
|
See [supported syntax](docs/supported-syntax.md), [the host API](docs/api.md), [preservation and clipboard behavior](docs/preservation.md), [development](docs/development.md), and [releases](docs/releasing.md).
|
package/dist/dialect.js
CHANGED
|
@@ -3,6 +3,7 @@ import { commonmark, hardbreakSchema, imageSchema, paragraphSchema, remarkInline
|
|
|
3
3
|
import { extendListItemSchemaForTask, strikethroughAttr, strikethroughInputRule, strikethroughKeymap, strikethroughSchema, toggleStrikethroughCommand, wrapInTaskListInputRule, tableSchema, tableHeaderRowSchema, tableHeaderSchema, tableRowSchema, tableCellSchema, tableEditingPlugin, tableKeymap, goToNextTableCellCommand, goToPrevTableCellCommand, insertTableCommand, addRowBeforeCommand, addRowAfterCommand, addColBeforeCommand, addColAfterCommand, selectRowCommand, selectColCommand, deleteSelectedCellsCommand, setAlignCommand, exitTable, } from '@milkdown/kit/preset/gfm';
|
|
4
4
|
import { $remark } from '@milkdown/kit/utils';
|
|
5
5
|
import { autolinkInputRule } from './autolink.js';
|
|
6
|
+
import { normalizeTableAlignment, normalizeTableColumnAlignment, } from './table-alignment.js';
|
|
6
7
|
import { preservingCodeBlocks, remarkCodeFences, fencedCodeHandler, } from './code-fences.js';
|
|
7
8
|
import { literalBlock, createLiteralPreservation } from './literals.js';
|
|
8
9
|
import { references, remarkReferencesPlugin } from './references.js';
|
|
@@ -86,6 +87,17 @@ function remarkDialect() {
|
|
|
86
87
|
add('toMarkdownExtensions', gfmTaskListItemToMarkdown());
|
|
87
88
|
add('toMarkdownExtensions', gfmAutolinkLiteralToMarkdown());
|
|
88
89
|
add('toMarkdownExtensions', gfmTableToMarkdown());
|
|
90
|
+
add('toMarkdownExtensions', {
|
|
91
|
+
handlers: {
|
|
92
|
+
text(node, parent, state, info) {
|
|
93
|
+
const value = defaultHandlers.text(node, parent, state, info);
|
|
94
|
+
// GFM trims literal whitespace at cell edges on reopening.
|
|
95
|
+
return state.stack.includes('tableCell')
|
|
96
|
+
? value.replace(/^[ \t]+|[ \t]+$/g, (spaces) => spaces.replaceAll(' ', ' ').replaceAll('\t', '	'))
|
|
97
|
+
: value;
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
});
|
|
89
101
|
add('toMarkdownExtensions', { handlers: { link, code: fencedCodeHandler } });
|
|
90
102
|
}
|
|
91
103
|
export const remarkDialectPlugin = $remark('remarkDialect', () => remarkDialect);
|
|
@@ -209,7 +221,7 @@ export const tables = [
|
|
|
209
221
|
return false;
|
|
210
222
|
return {
|
|
211
223
|
...attrs,
|
|
212
|
-
alignment: dom
|
|
224
|
+
alignment: normalizeTableAlignment(dom),
|
|
213
225
|
};
|
|
214
226
|
},
|
|
215
227
|
}
|
|
@@ -231,7 +243,7 @@ export const tables = [
|
|
|
231
243
|
return false;
|
|
232
244
|
return {
|
|
233
245
|
...attrs,
|
|
234
|
-
alignment: dom
|
|
246
|
+
alignment: normalizeTableColumnAlignment(dom),
|
|
235
247
|
};
|
|
236
248
|
},
|
|
237
249
|
}
|
package/dist/editor.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type TableCommand } from './tables.js';
|
|
1
|
+
import { type TableCommand, type TableOptions } from './tables.js';
|
|
2
2
|
import { type DocumentInput, type DocumentSnapshot, type ImageAdapter, type ClipboardInput, type ClipboardOutput, type PrintableDocument } from './types.js';
|
|
3
3
|
export type Mark = 'bold' | 'italic' | 'strikethrough' | 'highlight' | 'code' | 'link';
|
|
4
4
|
export type Block = {
|
|
@@ -70,10 +70,7 @@ export declare class InkKitEditor {
|
|
|
70
70
|
paste(input: ClipboardInput): Promise<void>;
|
|
71
71
|
setCommentsVisible(visible: boolean): void;
|
|
72
72
|
pasteAsPlainText(text: string): void;
|
|
73
|
-
table(command: TableCommand, options?:
|
|
74
|
-
rows?: number;
|
|
75
|
-
columns?: number;
|
|
76
|
-
}): boolean;
|
|
73
|
+
table(command: TableCommand, options?: TableOptions): boolean;
|
|
77
74
|
destroy(): Promise<void>;
|
|
78
75
|
private keymapPlugin;
|
|
79
76
|
static mount(root: HTMLElement, events: EditorEvents, options?: {
|
package/dist/editor.js
CHANGED
|
@@ -17,7 +17,7 @@ import { Preservation } from './preserve.js';
|
|
|
17
17
|
import { normaliseLabel, referenceDefinitions, footnoteDefinitions, } from './references.js';
|
|
18
18
|
import { selectionMarkdown, selectionContent, referenceMetadata, withReferenceMetadata, } from './reference-clipboard.js';
|
|
19
19
|
import { imageView } from './images.js';
|
|
20
|
-
import { tablePlugins, tableCommand } from './tables.js';
|
|
20
|
+
import { tablePlugins, tableCommand, configureTableMovement, } from './tables.js';
|
|
21
21
|
import { InkKitError, } from './types.js';
|
|
22
22
|
import { highlightPlugin } from './highlight.js';
|
|
23
23
|
import { highlightKeymap, toggleHighlightCommand } from './inline-highlight.js';
|
|
@@ -589,6 +589,15 @@ export class InkKitEditor {
|
|
|
589
589
|
.use($prose(() => search()))
|
|
590
590
|
.use(options.images ? imageView(options.images) : [])
|
|
591
591
|
.create();
|
|
592
|
+
configureTableMovement(instance.editor.ctx, () => {
|
|
593
|
+
try {
|
|
594
|
+
instance.assertCurrent();
|
|
595
|
+
return instance.formatType === 'md';
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
});
|
|
592
601
|
instance.plain = document.createElement('textarea');
|
|
593
602
|
instance.plain.className = 'inkkit-plain';
|
|
594
603
|
instance.plain.setAttribute('aria-label', 'Plain text editor');
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { InkKitEditor } from './editor.js';
|
|
2
2
|
export type { EditorEvents, CaretState, Mark, Block, FormatCommand, Keymap, } from './editor.js';
|
|
3
|
-
export type { TableCommand } from './tables.js';
|
|
3
|
+
export type { TableCommand, TableOptions } from './tables.js';
|
|
4
4
|
export { InkKitError } from './types.js';
|
|
5
5
|
export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, ClipboardDiagram, PrintableDocument, PrintableWarning, } from './types.js';
|
package/dist/paste.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { editorViewCtx, parserCtx } from '@milkdown/kit/core';
|
|
2
|
-
import { DOMParser as ProseParser, Fragment, Slice, } from '@milkdown/kit/prose/model';
|
|
2
|
+
import { DOMParser as ProseParser, DOMSerializer, Fragment, Slice, } from '@milkdown/kit/prose/model';
|
|
3
3
|
import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
|
|
4
4
|
import { $prose } from '@milkdown/kit/utils';
|
|
5
|
+
import { pasteSpreadsheet } from './spreadsheet.js';
|
|
5
6
|
import { avoidReferenceCollisions, markdownFromHTML, } from './reference-clipboard.js';
|
|
6
7
|
import { InkKitError, } from './types.js';
|
|
7
8
|
export class PasteController {
|
|
@@ -114,6 +115,10 @@ export class PasteController {
|
|
|
114
115
|
const plainText = input.plainText ||
|
|
115
116
|
this.options.literalText?.() ||
|
|
116
117
|
view.state.selection.$from.parent.type.spec.code;
|
|
118
|
+
if (!plainText &&
|
|
119
|
+
input.markdown == null &&
|
|
120
|
+
pasteSpreadsheet(ctx, { ...input, images }))
|
|
121
|
+
return;
|
|
117
122
|
const markdown = plainText
|
|
118
123
|
? undefined
|
|
119
124
|
: (input.markdown ??
|
|
@@ -136,6 +141,16 @@ export class PasteController {
|
|
|
136
141
|
const template = document.createElement('template');
|
|
137
142
|
template.innerHTML = input.html;
|
|
138
143
|
const container = template.content;
|
|
144
|
+
// Preserve whole unsupported tables before rewriting their descendants,
|
|
145
|
+
// so comments retain their source and ordinary-copy privacy.
|
|
146
|
+
for (const table of [...container.querySelectorAll('table')]) {
|
|
147
|
+
if (table.parentElement?.closest('table'))
|
|
148
|
+
continue;
|
|
149
|
+
if (schema.nodes.table && !unsupportedClipboardTable(table))
|
|
150
|
+
continue;
|
|
151
|
+
const preserved = ctx.get(parserCtx)(table.outerHTML);
|
|
152
|
+
table.replaceWith(DOMSerializer.fromSchema(schema).serializeFragment(preserved.content));
|
|
153
|
+
}
|
|
139
154
|
for (const warning of container.querySelectorAll('[data-inkkit-diagram-error]'))
|
|
140
155
|
warning.remove();
|
|
141
156
|
for (const diagram of container.querySelectorAll('figure[data-inkkit-mermaid]')) {
|
|
@@ -252,11 +267,7 @@ export class PasteController {
|
|
|
252
267
|
if (table.parentElement?.closest('table'))
|
|
253
268
|
continue;
|
|
254
269
|
const rows = [...table.querySelectorAll('tr')];
|
|
255
|
-
const
|
|
256
|
-
const unsupported = !schema.nodes.table ||
|
|
257
|
-
widths.length === 0 ||
|
|
258
|
-
widths.some((width) => !width || width !== widths[0]) ||
|
|
259
|
-
table.querySelector('table,[rowspan]:not([rowspan="1"]),[colspan]:not([colspan="1"]),td > p ~ p,th > p ~ p,td > div,th > div,td > ul,td > ol,td > pre,td > blockquote,th > ul,th > ol,th > pre,th > blockquote');
|
|
270
|
+
const unsupported = !schema.nodes.table || unsupportedClipboardTable(table);
|
|
260
271
|
if (!unsupported && !table.querySelector('th')) {
|
|
261
272
|
for (const cell of rows[0]?.querySelectorAll(':scope > td') ?? []) {
|
|
262
273
|
const heading = document.createElement('th');
|
|
@@ -422,6 +433,45 @@ export class PasteController {
|
|
|
422
433
|
}
|
|
423
434
|
}
|
|
424
435
|
}
|
|
436
|
+
function unsupportedClipboardTable(table) {
|
|
437
|
+
const rows = [...table.querySelectorAll('tr')];
|
|
438
|
+
const widths = rows.map((row) => row.querySelectorAll(':scope > th,:scope > td').length);
|
|
439
|
+
const supported = new Set([
|
|
440
|
+
'THEAD',
|
|
441
|
+
'TBODY',
|
|
442
|
+
'TFOOT',
|
|
443
|
+
'TR',
|
|
444
|
+
'TH',
|
|
445
|
+
'TD',
|
|
446
|
+
'P',
|
|
447
|
+
'STRONG',
|
|
448
|
+
'B',
|
|
449
|
+
'EM',
|
|
450
|
+
'I',
|
|
451
|
+
'S',
|
|
452
|
+
'DEL',
|
|
453
|
+
'STRIKE',
|
|
454
|
+
'CODE',
|
|
455
|
+
'A',
|
|
456
|
+
'MARK',
|
|
457
|
+
'SPAN',
|
|
458
|
+
'SUP',
|
|
459
|
+
'FONT',
|
|
460
|
+
'IMG',
|
|
461
|
+
]);
|
|
462
|
+
return (widths.length === 0 ||
|
|
463
|
+
widths.some((width) => !width || width !== widths[0]) ||
|
|
464
|
+
!!document.createTreeWalker(table, NodeFilter.SHOW_COMMENT).nextNode() ||
|
|
465
|
+
[...table.querySelectorAll('*')].some((element) => !supported.has(element.tagName)) ||
|
|
466
|
+
[...table.querySelectorAll('p')].some((paragraph) => {
|
|
467
|
+
const cell = paragraph.parentElement;
|
|
468
|
+
return (!cell ||
|
|
469
|
+
!['TH', 'TD'].includes(cell.tagName) ||
|
|
470
|
+
cell.children.length !== 1 ||
|
|
471
|
+
[...cell.childNodes].some((node) => node !== paragraph && !!node.textContent?.trim()));
|
|
472
|
+
}) ||
|
|
473
|
+
!!table.querySelector('[rowspan]:not([rowspan="1"]),[colspan]:not([colspan="1"])'));
|
|
474
|
+
}
|
|
425
475
|
function dataImage(source) {
|
|
426
476
|
const match = /^data:(image\/(?:png|jpeg|gif|webp|avif|bmp|tiff));base64,([A-Za-z0-9+/=\s]+)$/.exec(source);
|
|
427
477
|
if (!match)
|
package/dist/preserve.js
CHANGED
|
@@ -2,16 +2,14 @@ import { parserCtx, remarkCtx } from '@milkdown/kit/core';
|
|
|
2
2
|
import { serialize } from './dialect.js';
|
|
3
3
|
import { restoreCodeEndings, finalCodeEnding } from './code-fences.js';
|
|
4
4
|
function signature(node) {
|
|
5
|
-
return JSON.stringify(node, (key, value) => ['position', 'data', '
|
|
6
|
-
? undefined
|
|
7
|
-
: value);
|
|
5
|
+
return JSON.stringify(node, (key, value) => ['position', 'data', 'marker'].includes(key) ? undefined : value);
|
|
8
6
|
}
|
|
9
7
|
function ownShape(node) {
|
|
10
8
|
return JSON.stringify(node, (key, value) => ['position', 'data', 'children', 'value', 'marker'].includes(key)
|
|
11
9
|
? undefined
|
|
12
10
|
: value);
|
|
13
11
|
}
|
|
14
|
-
function retainText(raw, before, after, quoteIndent = false) {
|
|
12
|
+
function retainText(raw, before, after, quoteIndent = false, inTable = false) {
|
|
15
13
|
before = before.replace(/\r\n?/g, '\n');
|
|
16
14
|
after = after.replace(/\r\n?/g, '\n');
|
|
17
15
|
let prefix = 0;
|
|
@@ -64,7 +62,7 @@ function retainText(raw, before, after, quoteIndent = false) {
|
|
|
64
62
|
}
|
|
65
63
|
if (decoded !== before) {
|
|
66
64
|
// Continuation indentation is excluded from a quoted paragraph's text.
|
|
67
|
-
return quoteIndent ? after : retainText(raw, before, after, true);
|
|
65
|
+
return quoteIndent ? after : retainText(raw, before, after, true, inTable);
|
|
68
66
|
}
|
|
69
67
|
const inserted = after
|
|
70
68
|
.slice(prefix, after.length - suffix)
|
|
@@ -73,7 +71,9 @@ function retainText(raw, before, after, quoteIndent = false) {
|
|
|
73
71
|
const at = prefix + index;
|
|
74
72
|
const delimiter = (character === '%' || character === '=') &&
|
|
75
73
|
(after[at - 1] === character || after[at + 1] === character);
|
|
76
|
-
return delimiter ||
|
|
74
|
+
return delimiter ||
|
|
75
|
+
/[\\`*_[\]<>]/.test(character) ||
|
|
76
|
+
(inTable && character === '|')
|
|
77
77
|
? `\\${character}`
|
|
78
78
|
: character;
|
|
79
79
|
})
|
|
@@ -108,7 +108,7 @@ function retainBoundaryWhitespace(raw, canonical) {
|
|
|
108
108
|
return raw;
|
|
109
109
|
}
|
|
110
110
|
// Patch changed leaves so an adjacent edit retains authored delimiters and labels.
|
|
111
|
-
function retainSource(before, after, source, canonical, lineEnding) {
|
|
111
|
+
function retainSource(before, after, source, canonical, lineEnding, inTable = false) {
|
|
112
112
|
const start = before.position?.start.offset;
|
|
113
113
|
const end = before.position?.end.offset;
|
|
114
114
|
const newStart = after.position?.start.offset;
|
|
@@ -163,7 +163,7 @@ function retainSource(before, after, source, canonical, lineEnding) {
|
|
|
163
163
|
destination[3]);
|
|
164
164
|
}
|
|
165
165
|
if (before.type === 'text' && after.type === 'text')
|
|
166
|
-
return retainBoundaryWhitespace(retainText(raw, before.value, after.value), canonical.slice(newStart, newEnd));
|
|
166
|
+
return retainBoundaryWhitespace(retainText(raw, before.value, after.value, false, inTable), canonical.slice(newStart, newEnd));
|
|
167
167
|
if (signature(before) === signature(after) &&
|
|
168
168
|
ownShape(before) === ownShape(after))
|
|
169
169
|
return raw;
|
|
@@ -173,13 +173,40 @@ function retainSource(before, after, source, canonical, lineEnding) {
|
|
|
173
173
|
after.children &&
|
|
174
174
|
before.children.length === after.children.length) {
|
|
175
175
|
let output = '', previous = start;
|
|
176
|
+
const retained = new Set();
|
|
176
177
|
before.children.forEach((child, index) => {
|
|
178
|
+
const incoming = after.children[index];
|
|
179
|
+
const movedIndex = before.type === 'table' || before.type === 'tableRow'
|
|
180
|
+
? before.children.findIndex((candidate, candidateIndex) => !retained.has(candidateIndex) &&
|
|
181
|
+
signature(candidate) === signature(incoming) &&
|
|
182
|
+
ownShape(candidate) === ownShape(incoming))
|
|
183
|
+
: -1;
|
|
184
|
+
const original = movedIndex < 0 ? child : before.children[movedIndex];
|
|
185
|
+
if (movedIndex >= 0)
|
|
186
|
+
retained.add(movedIndex);
|
|
177
187
|
const childStart = child.position?.start.offset, childEnd = child.position?.end.offset;
|
|
178
188
|
if (childStart == null || childEnd == null)
|
|
179
189
|
throw new PreservationError('A source token has no location');
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
let retainedSource = retainSource(original, incoming, source, canonical, lineEnding, inTable || before.type === 'tableCell');
|
|
191
|
+
if (before.type === 'tableRow' && movedIndex >= 0) {
|
|
192
|
+
// Cell positions own different pipe boundaries at the row edges.
|
|
193
|
+
const originalFrom = original.children?.[0]?.position?.start.offset;
|
|
194
|
+
const originalTo = original.children?.at(-1)?.position?.end.offset;
|
|
195
|
+
const from = child.children?.[0]?.position?.start.offset;
|
|
196
|
+
const to = child.children?.at(-1)?.position?.end.offset;
|
|
197
|
+
retainedSource =
|
|
198
|
+
originalFrom != null &&
|
|
199
|
+
originalTo != null &&
|
|
200
|
+
from != null &&
|
|
201
|
+
to != null
|
|
202
|
+
? source.slice(childStart, from) +
|
|
203
|
+
source.slice(originalFrom, originalTo) +
|
|
204
|
+
source.slice(to, childEnd)
|
|
205
|
+
: canonical
|
|
206
|
+
.slice(incoming.position.start.offset, incoming.position.end.offset)
|
|
207
|
+
.replaceAll('\n', lineEnding);
|
|
208
|
+
}
|
|
209
|
+
output += source.slice(previous, childStart) + retainedSource;
|
|
183
210
|
previous = childEnd;
|
|
184
211
|
});
|
|
185
212
|
return output + source.slice(previous, end);
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { editorViewCtx } from '@milkdown/kit/core';
|
|
2
|
+
import { DOMParser as ProseParser, Fragment, } from '@milkdown/kit/prose/model';
|
|
3
|
+
import { isInTable, selectedRect } from '@milkdown/kit/prose/tables';
|
|
4
|
+
import { InkKitError } from './types.js';
|
|
5
|
+
import { literalCommentSpans } from './comments.js';
|
|
6
|
+
import { normalizeTableAlignment } from './table-alignment.js';
|
|
7
|
+
import { dispatchTableOperation, editableTable, selectTableRectangle, } from './tables.js';
|
|
8
|
+
export function pasteSpreadsheet(ctx, input) {
|
|
9
|
+
const view = ctx.get(editorViewCtx);
|
|
10
|
+
if (input.plainText || input.markdown != null)
|
|
11
|
+
return false;
|
|
12
|
+
const inTable = isInTable(view.state);
|
|
13
|
+
if (input.html &&
|
|
14
|
+
/data-inkkit-(?:markdown|reference|footnote|comment)/i.test(input.html)) {
|
|
15
|
+
if (inTable && /<table[\s>]/i.test(input.html))
|
|
16
|
+
reject();
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (input.images?.length || (input.html && /<img[\s>]/i.test(input.html))) {
|
|
20
|
+
if (!input.html || !/<table[\s>]/i.test(input.html))
|
|
21
|
+
return false;
|
|
22
|
+
const template = document.createElement('template');
|
|
23
|
+
template.innerHTML = input.html;
|
|
24
|
+
for (const image of template.content.querySelectorAll('img'))
|
|
25
|
+
image.replaceWith(document.createTextNode(image.getAttribute('alt') ?? 'Image'));
|
|
26
|
+
try {
|
|
27
|
+
htmlGrid(view.state.schema, template.innerHTML);
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
if (!inTable && !standaloneTable(input.html))
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
let grid;
|
|
36
|
+
try {
|
|
37
|
+
grid = input.html?.match(/<table[\s>]/i)
|
|
38
|
+
? htmlGrid(view.state.schema, input.html)
|
|
39
|
+
: input.text.includes('\t')
|
|
40
|
+
? textGrid(view.state.schema, input.text)
|
|
41
|
+
: undefined;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (inTable || !input.html)
|
|
45
|
+
throw error;
|
|
46
|
+
if (!standaloneTable(input.html))
|
|
47
|
+
return false;
|
|
48
|
+
const schema = view.state.schema;
|
|
49
|
+
const children = [];
|
|
50
|
+
let cursor = 0;
|
|
51
|
+
for (const span of literalCommentSpans(input.html)) {
|
|
52
|
+
if (span.start > cursor)
|
|
53
|
+
children.push(schema.text(input.html.slice(cursor, span.start)));
|
|
54
|
+
children.push(schema.nodes.comment_inline.create({ syntax: span.syntax, block: Boolean(span.block) }, span.value ? schema.text(span.value) : null));
|
|
55
|
+
cursor = span.end;
|
|
56
|
+
}
|
|
57
|
+
if (cursor < input.html.length)
|
|
58
|
+
children.push(schema.text(input.html.slice(cursor)));
|
|
59
|
+
const literal = schema.nodes.literal_markdown.create(null, children);
|
|
60
|
+
const selection = view.state.selection;
|
|
61
|
+
const tr = selection.$from.parent.type.name === 'paragraph' &&
|
|
62
|
+
selection.empty &&
|
|
63
|
+
selection.$from.parent.content.size === 0
|
|
64
|
+
? view.state.tr.replaceWith(selection.$from.before(), selection.$from.after(), literal)
|
|
65
|
+
: view.state.tr.replaceSelectionWith(literal);
|
|
66
|
+
dispatchTableOperation(view, tr);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
if (!grid)
|
|
70
|
+
return false;
|
|
71
|
+
if (!inTable) {
|
|
72
|
+
const schema = view.state.schema;
|
|
73
|
+
const rows = grid.cells.map((row, index) => schema.nodes[index ? 'table_row' : 'table_header_row'].create(null, row.map((content, column) => schema.nodes[index ? 'table_cell' : 'table_header'].create({ alignment: grid.alignment?.[column] ?? 'left' }, schema.nodes.paragraph.create(null, content)))));
|
|
74
|
+
const table = schema.nodes.table.create(null, rows);
|
|
75
|
+
const selection = view.state.selection;
|
|
76
|
+
const tr = selection.$from.parent.type.name === 'paragraph' &&
|
|
77
|
+
selection.empty &&
|
|
78
|
+
selection.$from.parent.content.size === 0
|
|
79
|
+
? view.state.tr.replaceWith(selection.$from.before(), selection.$from.after(), table)
|
|
80
|
+
: view.state.tr.replaceSelectionWith(table);
|
|
81
|
+
let tableStart = -1;
|
|
82
|
+
tr.doc.descendants((node, pos) => {
|
|
83
|
+
if (node === table)
|
|
84
|
+
tableStart = pos + 1;
|
|
85
|
+
});
|
|
86
|
+
if (tableStart < 0)
|
|
87
|
+
reject();
|
|
88
|
+
selectTableRectangle(tr, tableStart, table, 0, 0, grid.cells.length, grid.width);
|
|
89
|
+
dispatchTableOperation(view, tr);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
const rect = selectedRect(view.state);
|
|
93
|
+
if (!editableTable(rect.table))
|
|
94
|
+
reject();
|
|
95
|
+
const width = Math.max(rect.map.width, rect.left + grid.width);
|
|
96
|
+
const height = Math.max(rect.map.height, rect.top + grid.cells.length);
|
|
97
|
+
if (width > 100 || height > 100)
|
|
98
|
+
reject();
|
|
99
|
+
const schema = view.state.schema;
|
|
100
|
+
const alignment = Array.from({ length: width }, (_, column) => column < rect.map.width
|
|
101
|
+
? rect.table.firstChild.child(column).attrs.alignment
|
|
102
|
+
: 'left');
|
|
103
|
+
const rows = [];
|
|
104
|
+
for (let row = 0; row < height; row++) {
|
|
105
|
+
const cells = [];
|
|
106
|
+
for (let column = 0; column < width; column++) {
|
|
107
|
+
const existing = row < rect.map.height && column < rect.map.width
|
|
108
|
+
? rect.table.child(row).child(column)
|
|
109
|
+
: undefined;
|
|
110
|
+
const incoming = grid.cells[row - rect.top]?.[column - rect.left];
|
|
111
|
+
const cleared = row >= rect.top &&
|
|
112
|
+
row < rect.bottom &&
|
|
113
|
+
column >= rect.left &&
|
|
114
|
+
column < rect.right;
|
|
115
|
+
if (existing && !incoming && !cleared)
|
|
116
|
+
cells.push(existing);
|
|
117
|
+
else {
|
|
118
|
+
const type = schema.nodes[row ? 'table_cell' : 'table_header'];
|
|
119
|
+
cells.push(type.create({ ...(existing?.attrs ?? {}), alignment: alignment[column] }, schema.nodes.paragraph.create(null, incoming)));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
rows.push(schema.nodes[row ? 'table_row' : 'table_header_row'].create(rect.table.maybeChild(row)?.attrs, cells));
|
|
123
|
+
}
|
|
124
|
+
const table = rect.table.copy(Fragment.fromArray(rows));
|
|
125
|
+
const tr = view.state.tr.replaceWith(rect.tableStart - 1, rect.tableStart - 1 + rect.table.nodeSize, table);
|
|
126
|
+
selectTableRectangle(tr, rect.tableStart, table, rect.top, rect.left, rect.top + grid.cells.length, rect.left + grid.width);
|
|
127
|
+
dispatchTableOperation(view, tr);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
function standaloneTable(html) {
|
|
131
|
+
const template = document.createElement('template');
|
|
132
|
+
template.innerHTML = html;
|
|
133
|
+
for (const metadata of template.content.querySelectorAll('meta[charset]'))
|
|
134
|
+
metadata.remove();
|
|
135
|
+
const elements = [...template.content.childNodes].filter((node) => node.nodeType !== 3 || node.textContent?.trim());
|
|
136
|
+
return elements.length === 1 && elements[0] instanceof HTMLTableElement;
|
|
137
|
+
}
|
|
138
|
+
function reject() {
|
|
139
|
+
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.');
|
|
140
|
+
}
|
|
141
|
+
function textGrid(schema, input) {
|
|
142
|
+
const text = input.replace(/\r\n?/g, '\n').replace(/\n$/, '');
|
|
143
|
+
const rows = [[]];
|
|
144
|
+
let value = '', quoted = false, closed = false, start = true;
|
|
145
|
+
for (let index = 0; index <= text.length; index++) {
|
|
146
|
+
const character = text[index];
|
|
147
|
+
if (quoted) {
|
|
148
|
+
if (character == null)
|
|
149
|
+
reject();
|
|
150
|
+
if (character === '"') {
|
|
151
|
+
if (text[index + 1] === '"') {
|
|
152
|
+
value += '"';
|
|
153
|
+
index++;
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
quoted = false;
|
|
157
|
+
closed = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else
|
|
161
|
+
value += character;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (character === '"' && start) {
|
|
165
|
+
quoted = true;
|
|
166
|
+
start = false;
|
|
167
|
+
}
|
|
168
|
+
else if (character === '\t' || character === '\n' || character == null) {
|
|
169
|
+
rows.at(-1).push(value);
|
|
170
|
+
value = '';
|
|
171
|
+
start = true;
|
|
172
|
+
closed = false;
|
|
173
|
+
if (character === '\n')
|
|
174
|
+
rows.push([]);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
if (closed)
|
|
178
|
+
reject();
|
|
179
|
+
value += character;
|
|
180
|
+
start = false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const width = Math.max(...rows.map((row) => row.length));
|
|
184
|
+
if (width > 100 || rows.length > 100)
|
|
185
|
+
reject();
|
|
186
|
+
if (rows.some((row) => row.some((value) => /[\n\t]/.test(value))))
|
|
187
|
+
reject();
|
|
188
|
+
return {
|
|
189
|
+
width,
|
|
190
|
+
cells: rows.map((row) => Array.from({ length: width }, (_, column) => {
|
|
191
|
+
const value = row[column] ?? '';
|
|
192
|
+
return value ? Fragment.from(schema.text(value)) : Fragment.empty;
|
|
193
|
+
})),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function htmlGrid(schema, html) {
|
|
197
|
+
const template = document.createElement('template');
|
|
198
|
+
template.innerHTML = html;
|
|
199
|
+
const fragment = template.content;
|
|
200
|
+
for (const metadata of fragment.querySelectorAll('meta[charset]'))
|
|
201
|
+
metadata.remove();
|
|
202
|
+
const table = fragment.querySelector('table');
|
|
203
|
+
if (!table || fragment.querySelectorAll('table').length !== 1)
|
|
204
|
+
reject();
|
|
205
|
+
const wrapper = table.parentElement;
|
|
206
|
+
if (wrapper && wrapper.tagName !== 'BODY')
|
|
207
|
+
reject();
|
|
208
|
+
for (const sibling of fragment.childNodes)
|
|
209
|
+
if (sibling !== table &&
|
|
210
|
+
(sibling.textContent?.trim() || sibling.nodeType !== 3))
|
|
211
|
+
reject();
|
|
212
|
+
const rows = [...table.querySelectorAll('tr')];
|
|
213
|
+
if (document.createTreeWalker(table, NodeFilter.SHOW_COMMENT).nextNode())
|
|
214
|
+
reject();
|
|
215
|
+
for (const element of table.children) {
|
|
216
|
+
if (!['TR', 'THEAD', 'TBODY', 'TFOOT'].includes(element.tagName))
|
|
217
|
+
reject();
|
|
218
|
+
if (element.tagName !== 'TR')
|
|
219
|
+
for (const row of element.children)
|
|
220
|
+
if (row.tagName !== 'TR')
|
|
221
|
+
reject();
|
|
222
|
+
}
|
|
223
|
+
const width = rows[0]?.children.length ?? 0;
|
|
224
|
+
if (!width || width > 100 || !rows.length || rows.length > 100)
|
|
225
|
+
reject();
|
|
226
|
+
const parser = ProseParser.fromSchema(schema);
|
|
227
|
+
const inline = new Set([
|
|
228
|
+
'STRONG',
|
|
229
|
+
'B',
|
|
230
|
+
'EM',
|
|
231
|
+
'I',
|
|
232
|
+
'S',
|
|
233
|
+
'DEL',
|
|
234
|
+
'STRIKE',
|
|
235
|
+
'CODE',
|
|
236
|
+
'A',
|
|
237
|
+
'MARK',
|
|
238
|
+
'SPAN',
|
|
239
|
+
]);
|
|
240
|
+
const cells = rows.map((row) => {
|
|
241
|
+
if (row.children.length !== width)
|
|
242
|
+
reject();
|
|
243
|
+
return [...row.children].map((cell) => {
|
|
244
|
+
if (!['TH', 'TD'].includes(cell.tagName))
|
|
245
|
+
reject();
|
|
246
|
+
if ((cell.hasAttribute('rowspan') &&
|
|
247
|
+
cell.getAttribute('rowspan') !== '1') ||
|
|
248
|
+
(cell.hasAttribute('colspan') && cell.getAttribute('colspan') !== '1'))
|
|
249
|
+
reject();
|
|
250
|
+
for (const element of cell.querySelectorAll('*'))
|
|
251
|
+
if (!inline.has(element.tagName) &&
|
|
252
|
+
!(element.tagName === 'P' &&
|
|
253
|
+
element.parentElement === cell &&
|
|
254
|
+
cell.children.length === 1))
|
|
255
|
+
reject();
|
|
256
|
+
const walker = document.createTreeWalker(cell, NodeFilter.SHOW_COMMENT);
|
|
257
|
+
if (walker.nextNode())
|
|
258
|
+
reject();
|
|
259
|
+
if (/[\n\r\t]/.test(cell.textContent ?? ''))
|
|
260
|
+
reject();
|
|
261
|
+
for (const link of cell.querySelectorAll('a'))
|
|
262
|
+
if (!link.hasAttribute('href'))
|
|
263
|
+
reject();
|
|
264
|
+
const parsed = parser.parse(cell, { preserveWhitespace: true });
|
|
265
|
+
if (parsed.childCount === 0)
|
|
266
|
+
return Fragment.empty;
|
|
267
|
+
if (parsed.childCount !== 1 ||
|
|
268
|
+
parsed.firstChild.type.name !== 'paragraph')
|
|
269
|
+
reject();
|
|
270
|
+
return parsed.firstChild.content;
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
const alignment = [...rows[0].children].map((cell) => normalizeTableAlignment(cell) ?? 'left');
|
|
274
|
+
return { width, cells, alignment };
|
|
275
|
+
}
|
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
+
}
|