@milkdown/preset-gfm 6.1.5 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/auto-link.d.ts.map +1 -1
- package/lib/footnote/definition.d.ts +1 -5
- package/lib/footnote/definition.d.ts.map +1 -1
- package/lib/footnote/reference.d.ts +1 -5
- package/lib/footnote/reference.d.ts.map +1 -1
- package/lib/index.d.ts +1 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.es.js +1671 -16
- package/lib/index.es.js.map +1 -1
- package/lib/strike-through.d.ts +1 -5
- package/lib/strike-through.d.ts.map +1 -1
- package/lib/table/command.d.ts +1 -1
- package/lib/table/command.d.ts.map +1 -1
- package/lib/table/nodes/cell-selection.d.ts +38 -0
- package/lib/table/nodes/cell-selection.d.ts.map +1 -0
- package/lib/table/nodes/column-resizing.d.ts +10 -0
- package/lib/table/nodes/column-resizing.d.ts.map +1 -0
- package/lib/table/nodes/commands.d.ts +30 -0
- package/lib/table/nodes/commands.d.ts.map +1 -0
- package/lib/table/nodes/copy-paste.d.ts +13 -0
- package/lib/table/nodes/copy-paste.d.ts.map +1 -0
- package/lib/table/nodes/fix-tables.d.ts +6 -0
- package/lib/table/nodes/fix-tables.d.ts.map +1 -0
- package/lib/table/nodes/index.d.ts +5 -23
- package/lib/table/nodes/index.d.ts.map +1 -1
- package/lib/table/nodes/schema.d.ts +3 -1
- package/lib/table/nodes/schema.d.ts.map +1 -1
- package/lib/table/nodes/table-editing.d.ts +9 -0
- package/lib/table/nodes/table-editing.d.ts.map +1 -0
- package/lib/table/nodes/table-map.d.ts +44 -0
- package/lib/table/nodes/table-map.d.ts.map +1 -0
- package/lib/table/nodes/table-view.d.ts +15 -0
- package/lib/table/nodes/table-view.d.ts.map +1 -0
- package/lib/table/nodes/types.d.ts +15 -0
- package/lib/table/nodes/types.d.ts.map +1 -0
- package/lib/table/nodes/util.d.ts +16 -0
- package/lib/table/nodes/util.d.ts.map +1 -0
- package/lib/table/operator-plugin/actions.d.ts +1 -1
- package/lib/table/operator-plugin/actions.d.ts.map +1 -1
- package/lib/table/operator-plugin/calc-pos.d.ts.map +1 -1
- package/lib/table/operator-plugin/helper.d.ts +1 -1
- package/lib/table/operator-plugin/helper.d.ts.map +1 -1
- package/lib/table/operator-plugin/index.d.ts +1 -1
- package/lib/table/operator-plugin/index.d.ts.map +1 -1
- package/lib/table/operator-plugin/widget.d.ts +4 -4
- package/lib/table/operator-plugin/widget.d.ts.map +1 -1
- package/lib/table/utils.d.ts +4 -4
- package/lib/table/utils.d.ts.map +1 -1
- package/lib/task-list-item.d.ts +1 -5
- package/lib/task-list-item.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/auto-link.ts +4 -3
- package/src/table/command.ts +3 -3
- package/src/table/nodes/cell-selection.ts +352 -0
- package/src/table/nodes/column-resizing.ts +260 -0
- package/src/table/nodes/commands.ts +551 -0
- package/src/table/nodes/copy-paste.ts +306 -0
- package/src/table/nodes/fix-tables.ts +117 -0
- package/src/table/nodes/index.ts +7 -1
- package/src/table/nodes/schema.ts +100 -2
- package/src/table/nodes/table-editing.ts +275 -0
- package/src/table/nodes/table-map.ts +280 -0
- package/src/table/nodes/table-view.ts +76 -0
- package/src/table/nodes/types.ts +16 -0
- package/src/table/nodes/util.ts +107 -0
- package/src/table/operator-plugin/actions.ts +4 -4
- package/src/table/operator-plugin/calc-pos.ts +2 -1
- package/src/table/operator-plugin/helper.ts +2 -1
- package/src/table/operator-plugin/index.ts +1 -1
- package/src/table/operator-plugin/widget.ts +4 -14
- package/src/table/utils.ts +5 -2
- package/src/task-list-item.ts +2 -1
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/* Copyright 2021, Milkdown by Mirone. */
|
|
2
|
+
|
|
3
|
+
import { Fragment, Node, NodeType, Schema, Slice } from '@milkdown/prose/model';
|
|
4
|
+
import { EditorState, Transaction } from '@milkdown/prose/state';
|
|
5
|
+
import { Transform } from '@milkdown/prose/transform';
|
|
6
|
+
|
|
7
|
+
import { CellSelection } from './cell-selection';
|
|
8
|
+
import { tableNodeTypes } from './schema';
|
|
9
|
+
import { Rect, TableMap } from './table-map';
|
|
10
|
+
import { removeColSpan, setAttr } from './util';
|
|
11
|
+
|
|
12
|
+
// Utilities to help with copying and pasting table cells
|
|
13
|
+
|
|
14
|
+
// : (Slice) → ?{width: number, height: number, rows: [Fragment]}
|
|
15
|
+
// Get a rectangular area of cells from a slice, or null if the outer
|
|
16
|
+
// nodes of the slice aren't table cells or rows.
|
|
17
|
+
export function pastedCells(slice: Slice) {
|
|
18
|
+
if (!slice.size) return null;
|
|
19
|
+
let { content, openStart, openEnd } = slice;
|
|
20
|
+
while (
|
|
21
|
+
content.childCount == 1 &&
|
|
22
|
+
((openStart > 0 && openEnd > 0) || (content.firstChild as Node).type.spec['tableRole'] == 'table')
|
|
23
|
+
) {
|
|
24
|
+
openStart--;
|
|
25
|
+
openEnd--;
|
|
26
|
+
content = (content.firstChild as Node).content;
|
|
27
|
+
}
|
|
28
|
+
const first = content.firstChild as Node,
|
|
29
|
+
role = first.type.spec['tableRole'];
|
|
30
|
+
const schema = first.type.schema,
|
|
31
|
+
rows = [];
|
|
32
|
+
if (role == 'row') {
|
|
33
|
+
for (let i = 0; i < content.childCount; i++) {
|
|
34
|
+
let cells = content.child(i).content;
|
|
35
|
+
const left = i ? 0 : Math.max(0, openStart - 1);
|
|
36
|
+
const right = i < content.childCount - 1 ? 0 : Math.max(0, openEnd - 1);
|
|
37
|
+
if (left || right) cells = fitSlice(tableNodeTypes(schema).row, new Slice(cells, left, right)).content;
|
|
38
|
+
rows.push(cells);
|
|
39
|
+
}
|
|
40
|
+
} else if (role == 'cell' || role == 'header_cell') {
|
|
41
|
+
rows.push(
|
|
42
|
+
openStart || openEnd
|
|
43
|
+
? fitSlice(tableNodeTypes(schema).row, new Slice(content, openStart, openEnd)).content
|
|
44
|
+
: content,
|
|
45
|
+
);
|
|
46
|
+
} else {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return ensureRectangular(schema, rows);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// : (Schema, [Fragment]) → {width: number, height: number, rows: [Fragment]}
|
|
53
|
+
// Compute the width and height of a set of cells, and make sure each
|
|
54
|
+
// row has the same number of cells.
|
|
55
|
+
export type R = { width: number; height: number; rows: Fragment[] };
|
|
56
|
+
function ensureRectangular(schema: Schema, rows: Fragment[]): R {
|
|
57
|
+
const widths: number[] = [];
|
|
58
|
+
for (let i = 0; i < rows.length; i++) {
|
|
59
|
+
const row = rows[i] as Fragment;
|
|
60
|
+
for (let j = row.childCount - 1; j >= 0; j--) {
|
|
61
|
+
const { rowspan, colspan } = row.child(j).attrs;
|
|
62
|
+
for (let r = i; r < i + rowspan; r++) widths[r] = (widths[r] || 0) + colspan;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
let width = 0;
|
|
66
|
+
for (let r = 0; r < widths.length; r++) width = Math.max(width, widths[r] as number);
|
|
67
|
+
for (let r = 0; r < widths.length; r++) {
|
|
68
|
+
if (r >= rows.length) rows.push(Fragment.empty);
|
|
69
|
+
if ((widths[r] as number) < width) {
|
|
70
|
+
const empty = tableNodeTypes(schema).cell.createAndFill(),
|
|
71
|
+
cells = [];
|
|
72
|
+
for (let i = widths[r] as number; i < width; i++) cells.push(empty);
|
|
73
|
+
rows[r] = (rows[r] as Fragment).append(Fragment.from(cells));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { height: rows.length, width, rows };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function fitSlice(nodeType: NodeType, slice: Slice) {
|
|
80
|
+
const node = nodeType.createAndFill() as Node;
|
|
81
|
+
const tr = new Transform(node).replace(0, node.content.size, slice);
|
|
82
|
+
return tr.doc;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Clip or extend (repeat) the given set of cells to cover the given
|
|
86
|
+
// width and height. Will clip rowspan/colspan cells at the edges when
|
|
87
|
+
// they stick out.
|
|
88
|
+
export function clipCells({ width, height, rows }: R, newWidth: number, newHeight: number): R {
|
|
89
|
+
if (width != newWidth) {
|
|
90
|
+
const added: number[] = [],
|
|
91
|
+
newRows: Fragment[] = [];
|
|
92
|
+
for (let row = 0; row < rows.length; row++) {
|
|
93
|
+
const frag = rows[row] as Fragment,
|
|
94
|
+
cells = [];
|
|
95
|
+
for (let col = added[row] || 0, i = 0; col < newWidth; i++) {
|
|
96
|
+
let cell = frag.child(i % frag.childCount);
|
|
97
|
+
if (col + cell.attrs['colspan'] > newWidth)
|
|
98
|
+
cell = cell.type.create(
|
|
99
|
+
removeColSpan(cell.attrs, cell.attrs['colspan'], col + cell.attrs['colspan'] - newWidth),
|
|
100
|
+
cell.content,
|
|
101
|
+
);
|
|
102
|
+
cells.push(cell);
|
|
103
|
+
col += cell.attrs['colspan'];
|
|
104
|
+
for (let j = 1; j < cell.attrs['rowspan']; j++)
|
|
105
|
+
added[row + j] = (added[row + j] || 0) + cell.attrs['colspan'];
|
|
106
|
+
}
|
|
107
|
+
newRows.push(Fragment.from(cells));
|
|
108
|
+
}
|
|
109
|
+
rows = newRows;
|
|
110
|
+
width = newWidth;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (height != newHeight) {
|
|
114
|
+
const newRows = [];
|
|
115
|
+
for (let row = 0, i = 0; row < newHeight; row++, i++) {
|
|
116
|
+
const cells = [],
|
|
117
|
+
source = rows[i % height] as Fragment;
|
|
118
|
+
for (let j = 0; j < source.childCount; j++) {
|
|
119
|
+
let cell = source.child(j);
|
|
120
|
+
if (row + cell.attrs['rowspan'] > newHeight)
|
|
121
|
+
cell = cell.type.create(
|
|
122
|
+
setAttr(cell.attrs, 'rowspan', Math.max(1, newHeight - cell.attrs['rowspan'])),
|
|
123
|
+
cell.content,
|
|
124
|
+
);
|
|
125
|
+
cells.push(cell);
|
|
126
|
+
}
|
|
127
|
+
newRows.push(Fragment.from(cells));
|
|
128
|
+
}
|
|
129
|
+
rows = newRows;
|
|
130
|
+
height = newHeight;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { width, height, rows };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Make sure a table has at least the given width and height. Return
|
|
137
|
+
// true if something was changed.
|
|
138
|
+
function growTable(
|
|
139
|
+
tr: Transaction,
|
|
140
|
+
map: TableMap,
|
|
141
|
+
table: Node,
|
|
142
|
+
start: number,
|
|
143
|
+
width: number,
|
|
144
|
+
height: number,
|
|
145
|
+
mapFrom: number,
|
|
146
|
+
) {
|
|
147
|
+
const schema = tr.doc.type.schema,
|
|
148
|
+
types = tableNodeTypes(schema);
|
|
149
|
+
let empty, emptyHead;
|
|
150
|
+
if (width > map.width) {
|
|
151
|
+
for (let row = 0, rowEnd = 0; row < map.height; row++) {
|
|
152
|
+
const rowNode = table.child(row);
|
|
153
|
+
rowEnd += rowNode.nodeSize;
|
|
154
|
+
const cells = [];
|
|
155
|
+
let add;
|
|
156
|
+
if (rowNode.lastChild == null || rowNode.lastChild.type == types.cell)
|
|
157
|
+
add = empty || (empty = types.cell.createAndFill());
|
|
158
|
+
else add = emptyHead || (emptyHead = types.header_cell.createAndFill());
|
|
159
|
+
for (let i = map.width; i < width; i++) cells.push(add);
|
|
160
|
+
tr.insert(tr.mapping.slice(mapFrom).map(rowEnd - 1 + start), cells);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (height > map.height) {
|
|
164
|
+
const cells = [];
|
|
165
|
+
for (let i = 0, start = (map.height - 1) * map.width; i < Math.max(map.width, width); i++) {
|
|
166
|
+
const header =
|
|
167
|
+
i >= map.width ? false : (table.nodeAt(map.map[start + i] as number) as Node).type == types.header_cell;
|
|
168
|
+
cells.push(
|
|
169
|
+
header
|
|
170
|
+
? emptyHead || (emptyHead = types.header_cell.createAndFill())
|
|
171
|
+
: empty || (empty = types.cell.createAndFill()),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const emptyRow = types.row.create(null, Fragment.from(cells)),
|
|
176
|
+
rows = [];
|
|
177
|
+
for (let i = map.height; i < height; i++) rows.push(emptyRow);
|
|
178
|
+
tr.insert(tr.mapping.slice(mapFrom).map(start + table.nodeSize - 2), rows);
|
|
179
|
+
}
|
|
180
|
+
return !!(empty || emptyHead);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Make sure the given line (left, top) to (right, top) doesn't cross
|
|
184
|
+
// any rowspan cells by splitting cells that cross it. Return true if
|
|
185
|
+
// something changed.
|
|
186
|
+
function isolateHorizontal(
|
|
187
|
+
tr: Transaction,
|
|
188
|
+
map: TableMap,
|
|
189
|
+
table: Node,
|
|
190
|
+
start: number,
|
|
191
|
+
left: number,
|
|
192
|
+
right: number,
|
|
193
|
+
top: number,
|
|
194
|
+
mapFrom: number,
|
|
195
|
+
) {
|
|
196
|
+
if (top == 0 || top == map.height) return false;
|
|
197
|
+
let found = false;
|
|
198
|
+
for (let col = left; col < right; col++) {
|
|
199
|
+
const index = top * map.width + col,
|
|
200
|
+
pos = map.map[index] as number;
|
|
201
|
+
if (map.map[index - map.width] == pos) {
|
|
202
|
+
found = true;
|
|
203
|
+
const cell = table.nodeAt(pos) as Node;
|
|
204
|
+
const { top: cellTop, left: cellLeft } = map.findCell(pos);
|
|
205
|
+
tr.setNodeMarkup(
|
|
206
|
+
tr.mapping.slice(mapFrom).map(pos + start),
|
|
207
|
+
null,
|
|
208
|
+
setAttr(cell.attrs, 'rowspan', top - cellTop),
|
|
209
|
+
);
|
|
210
|
+
tr.insert(
|
|
211
|
+
tr.mapping.slice(mapFrom).map(map.positionAt(top, cellLeft, table)),
|
|
212
|
+
cell.type.createAndFill(setAttr(cell.attrs, 'rowspan', cellTop + cell.attrs['rowspan'] - top)) as Node,
|
|
213
|
+
);
|
|
214
|
+
col += cell.attrs['colspan'] - 1;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return found;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Make sure the given line (left, top) to (left, bottom) doesn't
|
|
221
|
+
// cross any colspan cells by splitting cells that cross it. Return
|
|
222
|
+
// true if something changed.
|
|
223
|
+
function isolateVertical(
|
|
224
|
+
tr: Transaction,
|
|
225
|
+
map: TableMap,
|
|
226
|
+
table: Node,
|
|
227
|
+
start: number,
|
|
228
|
+
top: number,
|
|
229
|
+
bottom: number,
|
|
230
|
+
left: number,
|
|
231
|
+
mapFrom: number,
|
|
232
|
+
) {
|
|
233
|
+
if (left == 0 || left == map.width) return false;
|
|
234
|
+
let found = false;
|
|
235
|
+
for (let row = top; row < bottom; row++) {
|
|
236
|
+
const index = row * map.width + left,
|
|
237
|
+
pos = map.map[index] as number;
|
|
238
|
+
if (map.map[index - 1] == pos) {
|
|
239
|
+
found = true;
|
|
240
|
+
const cell = table.nodeAt(pos) as Node,
|
|
241
|
+
cellLeft = map.colCount(pos);
|
|
242
|
+
const updatePos = tr.mapping.slice(mapFrom).map(pos + start);
|
|
243
|
+
tr.setNodeMarkup(
|
|
244
|
+
updatePos,
|
|
245
|
+
null,
|
|
246
|
+
removeColSpan(cell.attrs, left - cellLeft, cell.attrs['colspan'] - (left - cellLeft)),
|
|
247
|
+
);
|
|
248
|
+
tr.insert(
|
|
249
|
+
updatePos + cell.nodeSize,
|
|
250
|
+
cell.type.createAndFill(removeColSpan(cell.attrs, 0, left - cellLeft)) as Node,
|
|
251
|
+
);
|
|
252
|
+
row += cell.attrs['rowspan'] - 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return found;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Insert the given set of cells (as returned by `pastedCells`) into a
|
|
259
|
+
// table, at the position pointed at by rect.
|
|
260
|
+
export function insertCells(
|
|
261
|
+
state: EditorState,
|
|
262
|
+
dispatch: (tr: Transaction) => void,
|
|
263
|
+
tableStart: number,
|
|
264
|
+
rect: Rect,
|
|
265
|
+
cells: R,
|
|
266
|
+
) {
|
|
267
|
+
let table = (tableStart ? state.doc.nodeAt(tableStart - 1) : state.doc) as Node,
|
|
268
|
+
map = TableMap.get(table);
|
|
269
|
+
const { top, left } = rect;
|
|
270
|
+
const right = left + cells.width,
|
|
271
|
+
bottom = top + cells.height;
|
|
272
|
+
const tr = state.tr;
|
|
273
|
+
let mapFrom = 0;
|
|
274
|
+
function recomp() {
|
|
275
|
+
table = (tableStart ? tr.doc.nodeAt(tableStart - 1) : tr.doc) as Node;
|
|
276
|
+
map = TableMap.get(table);
|
|
277
|
+
mapFrom = tr.mapping.maps.length;
|
|
278
|
+
}
|
|
279
|
+
// Prepare the table to be large enough and not have any cells
|
|
280
|
+
// crossing the boundaries of the rectangle that we want to
|
|
281
|
+
// insert into. If anything about it changes, recompute the table
|
|
282
|
+
// map so that subsequent operations can see the current shape.
|
|
283
|
+
if (growTable(tr, map, table, tableStart, right, bottom, mapFrom)) recomp();
|
|
284
|
+
if (isolateHorizontal(tr, map, table, tableStart, left, right, top, mapFrom)) recomp();
|
|
285
|
+
if (isolateHorizontal(tr, map, table, tableStart, left, right, bottom, mapFrom)) recomp();
|
|
286
|
+
if (isolateVertical(tr, map, table, tableStart, top, bottom, left, mapFrom)) recomp();
|
|
287
|
+
if (isolateVertical(tr, map, table, tableStart, top, bottom, right, mapFrom)) recomp();
|
|
288
|
+
|
|
289
|
+
for (let row = top; row < bottom; row++) {
|
|
290
|
+
const from = map.positionAt(row, left, table),
|
|
291
|
+
to = map.positionAt(row, right, table);
|
|
292
|
+
tr.replace(
|
|
293
|
+
tr.mapping.slice(mapFrom).map(from + tableStart),
|
|
294
|
+
tr.mapping.slice(mapFrom).map(to + tableStart),
|
|
295
|
+
new Slice(cells.rows[row - top] as Fragment, 0, 0),
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
recomp();
|
|
299
|
+
tr.setSelection(
|
|
300
|
+
new CellSelection(
|
|
301
|
+
tr.doc.resolve(tableStart + map.positionAt(top, left, table)),
|
|
302
|
+
tr.doc.resolve(tableStart + map.positionAt(bottom - 1, right - 1, table)),
|
|
303
|
+
),
|
|
304
|
+
);
|
|
305
|
+
dispatch(tr);
|
|
306
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/* Copyright 2021, Milkdown by Mirone. */
|
|
2
|
+
|
|
3
|
+
import { Node } from '@milkdown/prose/model';
|
|
4
|
+
import { EditorState, PluginKey, Transaction } from '@milkdown/prose/state';
|
|
5
|
+
|
|
6
|
+
import { tableNodeTypes } from './schema';
|
|
7
|
+
import { Problem, TableMap } from './table-map';
|
|
8
|
+
import { removeColSpan, setAttr } from './util';
|
|
9
|
+
|
|
10
|
+
export const fixTablesKey = new PluginKey('fix-tables');
|
|
11
|
+
|
|
12
|
+
// Helper for iterating through the nodes in a document that changed
|
|
13
|
+
// compared to the given previous document. Useful for avoiding
|
|
14
|
+
// duplicate work on each transaction.
|
|
15
|
+
function changedDescendants(old: Node, cur: Node, offset: number, f: (node: Node, pos: number) => void | boolean) {
|
|
16
|
+
const oldSize = old.childCount,
|
|
17
|
+
curSize = cur.childCount;
|
|
18
|
+
outer: for (let i = 0, j = 0; i < curSize; i++) {
|
|
19
|
+
const child = cur.child(i);
|
|
20
|
+
for (let scan = j, e = Math.min(oldSize, i + 3); scan < e; scan++) {
|
|
21
|
+
if (old.child(scan) == child) {
|
|
22
|
+
j = scan + 1;
|
|
23
|
+
offset += child.nodeSize;
|
|
24
|
+
continue outer;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
f(child, offset);
|
|
28
|
+
if (j < oldSize && old.child(j).sameMarkup(child)) changedDescendants(old.child(j), child, offset + 1, f);
|
|
29
|
+
else child.nodesBetween(0, child.content.size, f, offset + 1);
|
|
30
|
+
offset += child.nodeSize;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// :: (EditorState, ?EditorState) → ?Transaction
|
|
35
|
+
// Inspect all tables in the given state's document and return a
|
|
36
|
+
// transaction that fixes them, if necessary. If `oldState` was
|
|
37
|
+
// provided, that is assumed to hold a previous, known-good state,
|
|
38
|
+
// which will be used to avoid re-scanning unchanged parts of the
|
|
39
|
+
// document.
|
|
40
|
+
export function fixTables(state: EditorState, oldState: EditorState) {
|
|
41
|
+
let tr: undefined | Transaction;
|
|
42
|
+
const check = (node: Node, pos: number) => {
|
|
43
|
+
if (node.type.spec['tableRole'] == 'table') tr = fixTable(state, node, pos, tr);
|
|
44
|
+
};
|
|
45
|
+
if (!oldState) state.doc.descendants(check);
|
|
46
|
+
else if (oldState.doc != state.doc) changedDescendants(oldState.doc, state.doc, 0, check);
|
|
47
|
+
return tr;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// : (EditorState, Node, number, ?Transaction) → ?Transaction
|
|
51
|
+
// Fix the given table, if necessary. Will append to the transaction
|
|
52
|
+
// it was given, if non-null, or create a new one if necessary.
|
|
53
|
+
export function fixTable(state: EditorState, table: Node, tablePos: number, tr?: Transaction) {
|
|
54
|
+
const map = TableMap.get(table);
|
|
55
|
+
if (!map.problems) return tr;
|
|
56
|
+
if (!tr) tr = state.tr;
|
|
57
|
+
|
|
58
|
+
// Track which rows we must add cells to, so that we can adjust that
|
|
59
|
+
// when fixing collisions.
|
|
60
|
+
const mustAdd = [];
|
|
61
|
+
for (let i = 0; i < map.height; i++) mustAdd.push(0);
|
|
62
|
+
for (let i = 0; i < map.problems.length; i++) {
|
|
63
|
+
const prob = map.problems[i] as Problem;
|
|
64
|
+
if (prob.type == 'collision') {
|
|
65
|
+
const cell = table.nodeAt(prob.pos) as Node;
|
|
66
|
+
for (let j = 0; j < cell.attrs['rowspan']; j++) mustAdd[prob.row + j] += prob.n;
|
|
67
|
+
tr.setNodeMarkup(
|
|
68
|
+
tr.mapping.map(tablePos + 1 + prob.pos),
|
|
69
|
+
null,
|
|
70
|
+
removeColSpan(cell.attrs, cell.attrs['colspan'] - prob.n, prob.n),
|
|
71
|
+
);
|
|
72
|
+
} else if (prob.type == 'missing') {
|
|
73
|
+
mustAdd[prob.row] += prob.n;
|
|
74
|
+
} else if (prob.type == 'overlong_rowspan') {
|
|
75
|
+
const cell = table.nodeAt(prob.pos) as Node;
|
|
76
|
+
tr.setNodeMarkup(
|
|
77
|
+
tr.mapping.map(tablePos + 1 + prob.pos),
|
|
78
|
+
null,
|
|
79
|
+
setAttr(cell.attrs, 'rowspan', cell.attrs['rowspan'] - prob.n),
|
|
80
|
+
);
|
|
81
|
+
} else if (prob.type == 'colwidth mismatch') {
|
|
82
|
+
const cell = table.nodeAt(prob.pos) as Node;
|
|
83
|
+
tr.setNodeMarkup(
|
|
84
|
+
tr.mapping.map(tablePos + 1 + prob.pos),
|
|
85
|
+
null,
|
|
86
|
+
setAttr(cell.attrs, 'colwidth', prob.colwidth),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let first, last;
|
|
91
|
+
for (let i = 0; i < mustAdd.length; i++)
|
|
92
|
+
if (mustAdd[i]) {
|
|
93
|
+
if (first == null) first = i;
|
|
94
|
+
last = i;
|
|
95
|
+
}
|
|
96
|
+
// Add the necessary cells, using a heuristic for whether to add the
|
|
97
|
+
// cells at the start or end of the rows (if it looks like a 'bite'
|
|
98
|
+
// was taken out of the table, add cells at the start of the row
|
|
99
|
+
// after the bite. Otherwise add them at the end).
|
|
100
|
+
for (let i = 0, pos = tablePos + 1; i < map.height; i++) {
|
|
101
|
+
const row = table.child(i);
|
|
102
|
+
const end = pos + row.nodeSize;
|
|
103
|
+
const add = mustAdd[i] as number;
|
|
104
|
+
if (add > 0) {
|
|
105
|
+
let tableNodeType = 'cell';
|
|
106
|
+
if (row.firstChild) {
|
|
107
|
+
tableNodeType = row.firstChild.type.spec['tableRole'];
|
|
108
|
+
}
|
|
109
|
+
const nodes = [];
|
|
110
|
+
for (let j = 0; j < add; j++) nodes.push(tableNodeTypes(state.schema)[tableNodeType].createAndFill());
|
|
111
|
+
const side = (i == 0 || first == i - 1) && last == i ? pos + 1 : end - 1;
|
|
112
|
+
tr.insert(tr.mapping.map(side), nodes);
|
|
113
|
+
}
|
|
114
|
+
pos = end;
|
|
115
|
+
}
|
|
116
|
+
return tr.setMeta(fixTablesKey, { fixTables: true });
|
|
117
|
+
}
|
package/src/table/nodes/index.ts
CHANGED
|
@@ -3,13 +3,15 @@ import { createCmd, createCmdKey, MarkdownNode, schemaCtx } from '@milkdown/core
|
|
|
3
3
|
import { InputRule } from '@milkdown/prose/inputrules';
|
|
4
4
|
import { NodeType } from '@milkdown/prose/model';
|
|
5
5
|
import { Plugin, PluginKey, Selection, TextSelection } from '@milkdown/prose/state';
|
|
6
|
-
import { columnResizing, goToNextCell, tableEditing } from '@milkdown/prose/tables';
|
|
7
6
|
import { createPlugin, createShortcut } from '@milkdown/utils';
|
|
8
7
|
|
|
9
8
|
import { exitTable } from '../command';
|
|
10
9
|
import { operatorPlugin } from '../operator-plugin';
|
|
11
10
|
import { createTable } from '../utils';
|
|
11
|
+
import { columnResizing } from './column-resizing';
|
|
12
|
+
import { goToNextCell } from './commands';
|
|
12
13
|
import { schema } from './schema';
|
|
14
|
+
import { tableEditing } from './table-editing';
|
|
13
15
|
|
|
14
16
|
export const SupportedKeys = {
|
|
15
17
|
NextCell: 'NextCell',
|
|
@@ -195,3 +197,7 @@ export const table = createPlugin<Keys, Record<string, unknown>, keyof typeof sc
|
|
|
195
197
|
},
|
|
196
198
|
};
|
|
197
199
|
});
|
|
200
|
+
|
|
201
|
+
export * from './cell-selection';
|
|
202
|
+
export * from './commands';
|
|
203
|
+
export * from './util';
|
|
@@ -1,5 +1,103 @@
|
|
|
1
1
|
/* Copyright 2021, Milkdown by Mirone. */
|
|
2
|
-
import {
|
|
2
|
+
import { Attrs, Node, NodeSpec, Schema } from '@milkdown/prose/model';
|
|
3
|
+
|
|
4
|
+
import { TableNodesOptions } from './types';
|
|
5
|
+
|
|
6
|
+
function getCellAttrs(dom: HTMLElement, extraAttrs: Attrs) {
|
|
7
|
+
const widthAttr = dom.getAttribute('data-colwidth');
|
|
8
|
+
const widths = widthAttr && /^\d+(,\d+)*$/.test(widthAttr) ? widthAttr.split(',').map((s) => Number(s)) : null;
|
|
9
|
+
const colspan = Number(dom.getAttribute('colspan') || 1);
|
|
10
|
+
const result = {
|
|
11
|
+
colspan,
|
|
12
|
+
rowspan: Number(dom.getAttribute('rowspan') || 1),
|
|
13
|
+
colwidth: widths && widths.length == colspan ? widths : null,
|
|
14
|
+
};
|
|
15
|
+
for (const prop in extraAttrs) {
|
|
16
|
+
const getter = extraAttrs[prop].getFromDOM;
|
|
17
|
+
const value = getter && getter(dom);
|
|
18
|
+
if (value != null) result[prop as keyof typeof result] = value;
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function setCellAttrs(node: Node, extraAttrs: Attrs) {
|
|
24
|
+
const attrs: Record<string, unknown> = {};
|
|
25
|
+
if (node.attrs['colspan'] != 1) attrs['colspan'] = node.attrs['colspan'];
|
|
26
|
+
if (node.attrs['rowspan'] != 1) attrs['rowspan'] = node.attrs['rowspan'];
|
|
27
|
+
if (node.attrs['colwidth']) attrs['data-colwidth'] = node.attrs['colwidth'].join(',');
|
|
28
|
+
for (const prop in extraAttrs) {
|
|
29
|
+
const setter = extraAttrs[prop].setDOMAttr;
|
|
30
|
+
if (setter) setter(node.attrs[prop], attrs);
|
|
31
|
+
}
|
|
32
|
+
return attrs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function tableNodesSpecCreator(options: TableNodesOptions) {
|
|
36
|
+
const extraAttrs: Attrs = options.cellAttributes || {};
|
|
37
|
+
const cellAttrs: Record<string, unknown> = {
|
|
38
|
+
colspan: { default: 1 },
|
|
39
|
+
rowspan: { default: 1 },
|
|
40
|
+
colwidth: { default: null },
|
|
41
|
+
};
|
|
42
|
+
for (const prop in extraAttrs) cellAttrs[prop] = { default: extraAttrs[prop].default };
|
|
43
|
+
const finalAttrs = cellAttrs as Attrs;
|
|
44
|
+
|
|
45
|
+
const schema: Record<'table' | 'table_row' | 'table_cell' | 'table_header', NodeSpec> = {
|
|
46
|
+
table: {
|
|
47
|
+
content: 'table_row+',
|
|
48
|
+
tableRole: 'table',
|
|
49
|
+
isolating: true,
|
|
50
|
+
group: options.tableGroup,
|
|
51
|
+
parseDOM: [{ tag: 'table' }],
|
|
52
|
+
toDOM() {
|
|
53
|
+
return ['table', ['tbody', 0]];
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
table_row: {
|
|
57
|
+
content: '(table_cell | table_header)*',
|
|
58
|
+
tableRole: 'row',
|
|
59
|
+
parseDOM: [{ tag: 'tr' }],
|
|
60
|
+
toDOM() {
|
|
61
|
+
return ['tr', 0];
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
table_cell: {
|
|
65
|
+
content: options.cellContent,
|
|
66
|
+
attrs: finalAttrs,
|
|
67
|
+
tableRole: 'cell',
|
|
68
|
+
isolating: true,
|
|
69
|
+
parseDOM: [{ tag: 'td', getAttrs: (dom) => getCellAttrs(dom as HTMLElement, extraAttrs) }],
|
|
70
|
+
toDOM(node) {
|
|
71
|
+
return ['td', setCellAttrs(node, extraAttrs), 0];
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
table_header: {
|
|
75
|
+
content: options.cellContent,
|
|
76
|
+
attrs: finalAttrs,
|
|
77
|
+
tableRole: 'header_cell',
|
|
78
|
+
isolating: true,
|
|
79
|
+
parseDOM: [{ tag: 'th', getAttrs: (dom) => getCellAttrs(dom as HTMLElement, extraAttrs) }],
|
|
80
|
+
toDOM(node) {
|
|
81
|
+
return ['th', setCellAttrs(node, extraAttrs), 0];
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return schema;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function tableNodeTypes(schema: Schema) {
|
|
90
|
+
let result = schema.cached['tableNodeTypes'];
|
|
91
|
+
if (!result) {
|
|
92
|
+
result = schema.cached['tableNodeTypes'] = {};
|
|
93
|
+
for (const name in schema.nodes) {
|
|
94
|
+
const type = schema.nodes[name],
|
|
95
|
+
role = type?.spec['tableRole'];
|
|
96
|
+
if (role) result[role] = type;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
3
101
|
|
|
4
102
|
export const schema = tableNodesSpecCreator({
|
|
5
103
|
tableGroup: 'block',
|
|
@@ -9,7 +107,7 @@ export const schema = tableNodesSpecCreator({
|
|
|
9
107
|
default: 'left',
|
|
10
108
|
getFromDOM: (dom) => (dom as HTMLElement).style.textAlign || 'left',
|
|
11
109
|
setDOMAttr: (value, attrs) => {
|
|
12
|
-
attrs
|
|
110
|
+
attrs['style'] = `text-align: ${value || 'left'}`;
|
|
13
111
|
},
|
|
14
112
|
},
|
|
15
113
|
},
|