@milkdown/preset-gfm 6.1.3 → 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.
Files changed (72) hide show
  1. package/lib/auto-link.d.ts.map +1 -1
  2. package/lib/footnote/definition.d.ts +1 -5
  3. package/lib/footnote/definition.d.ts.map +1 -1
  4. package/lib/footnote/reference.d.ts +1 -5
  5. package/lib/footnote/reference.d.ts.map +1 -1
  6. package/lib/index.d.ts +1 -3
  7. package/lib/index.d.ts.map +1 -1
  8. package/lib/index.es.js +1671 -16
  9. package/lib/index.es.js.map +1 -1
  10. package/lib/strike-through.d.ts +1 -5
  11. package/lib/strike-through.d.ts.map +1 -1
  12. package/lib/table/command.d.ts +1 -1
  13. package/lib/table/command.d.ts.map +1 -1
  14. package/lib/table/nodes/cell-selection.d.ts +38 -0
  15. package/lib/table/nodes/cell-selection.d.ts.map +1 -0
  16. package/lib/table/nodes/column-resizing.d.ts +10 -0
  17. package/lib/table/nodes/column-resizing.d.ts.map +1 -0
  18. package/lib/table/nodes/commands.d.ts +30 -0
  19. package/lib/table/nodes/commands.d.ts.map +1 -0
  20. package/lib/table/nodes/copy-paste.d.ts +13 -0
  21. package/lib/table/nodes/copy-paste.d.ts.map +1 -0
  22. package/lib/table/nodes/fix-tables.d.ts +6 -0
  23. package/lib/table/nodes/fix-tables.d.ts.map +1 -0
  24. package/lib/table/nodes/index.d.ts +5 -23
  25. package/lib/table/nodes/index.d.ts.map +1 -1
  26. package/lib/table/nodes/schema.d.ts +3 -1
  27. package/lib/table/nodes/schema.d.ts.map +1 -1
  28. package/lib/table/nodes/table-editing.d.ts +9 -0
  29. package/lib/table/nodes/table-editing.d.ts.map +1 -0
  30. package/lib/table/nodes/table-map.d.ts +44 -0
  31. package/lib/table/nodes/table-map.d.ts.map +1 -0
  32. package/lib/table/nodes/table-view.d.ts +15 -0
  33. package/lib/table/nodes/table-view.d.ts.map +1 -0
  34. package/lib/table/nodes/types.d.ts +15 -0
  35. package/lib/table/nodes/types.d.ts.map +1 -0
  36. package/lib/table/nodes/util.d.ts +16 -0
  37. package/lib/table/nodes/util.d.ts.map +1 -0
  38. package/lib/table/operator-plugin/actions.d.ts +1 -1
  39. package/lib/table/operator-plugin/actions.d.ts.map +1 -1
  40. package/lib/table/operator-plugin/calc-pos.d.ts.map +1 -1
  41. package/lib/table/operator-plugin/helper.d.ts +1 -1
  42. package/lib/table/operator-plugin/helper.d.ts.map +1 -1
  43. package/lib/table/operator-plugin/index.d.ts +1 -1
  44. package/lib/table/operator-plugin/index.d.ts.map +1 -1
  45. package/lib/table/operator-plugin/widget.d.ts +4 -4
  46. package/lib/table/operator-plugin/widget.d.ts.map +1 -1
  47. package/lib/table/utils.d.ts +4 -4
  48. package/lib/table/utils.d.ts.map +1 -1
  49. package/lib/task-list-item.d.ts +1 -5
  50. package/lib/task-list-item.d.ts.map +1 -1
  51. package/package.json +6 -6
  52. package/src/auto-link.ts +4 -3
  53. package/src/table/command.ts +3 -3
  54. package/src/table/nodes/cell-selection.ts +352 -0
  55. package/src/table/nodes/column-resizing.ts +260 -0
  56. package/src/table/nodes/commands.ts +551 -0
  57. package/src/table/nodes/copy-paste.ts +306 -0
  58. package/src/table/nodes/fix-tables.ts +117 -0
  59. package/src/table/nodes/index.ts +7 -1
  60. package/src/table/nodes/schema.ts +100 -2
  61. package/src/table/nodes/table-editing.ts +275 -0
  62. package/src/table/nodes/table-map.ts +280 -0
  63. package/src/table/nodes/table-view.ts +76 -0
  64. package/src/table/nodes/types.ts +16 -0
  65. package/src/table/nodes/util.ts +107 -0
  66. package/src/table/operator-plugin/actions.ts +4 -4
  67. package/src/table/operator-plugin/calc-pos.ts +2 -1
  68. package/src/table/operator-plugin/helper.ts +2 -1
  69. package/src/table/operator-plugin/index.ts +1 -1
  70. package/src/table/operator-plugin/widget.ts +4 -14
  71. package/src/table/utils.ts +5 -2
  72. package/src/task-list-item.ts +2 -1
@@ -0,0 +1,275 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { keydownHandler } from '@milkdown/prose/keymap';
3
+ import { Fragment, ResolvedPos, Slice } from '@milkdown/prose/model';
4
+ import { Command, EditorState, Plugin, PluginKey, Selection, TextSelection, Transaction } from '@milkdown/prose/state';
5
+ import { EditorView } from '@milkdown/prose/view';
6
+
7
+ import { CellSelection, drawCellSelection, normalizeSelection } from './cell-selection';
8
+ import { clipCells, fitSlice, insertCells, pastedCells } from './copy-paste';
9
+ import { fixTables } from './fix-tables';
10
+ import { tableNodeTypes } from './schema';
11
+ import { TableMap } from './table-map';
12
+ import { cellAround, inSameTable, isInTable, nextCell, selectionCell } from './util';
13
+
14
+ export const tableEditingKey = new PluginKey('selectingCells');
15
+
16
+ function domInCell(view: EditorView, dom: EventTarget | null) {
17
+ for (; dom && dom != view.dom; dom = (dom as Element).parentNode as Element)
18
+ if ((dom as Element).nodeName == 'TD' || (dom as Element).nodeName == 'TH') return dom;
19
+ return;
20
+ }
21
+
22
+ function cellUnderMouse(view: EditorView, event: MouseEvent) {
23
+ const mousePos = view.posAtCoords({ left: event.clientX, top: event.clientY });
24
+ if (!mousePos) return null;
25
+ return mousePos ? cellAround(view.state.doc.resolve(mousePos.pos)) : null;
26
+ }
27
+
28
+ function handleMouseDown(view: EditorView, event: Event) {
29
+ const startEvent = event as MouseEvent;
30
+ if (startEvent.ctrlKey || startEvent.metaKey) return;
31
+
32
+ const startDOMCell = domInCell(view, startEvent.target as Element);
33
+ let $anchor;
34
+ if (startEvent.shiftKey && view.state.selection instanceof CellSelection) {
35
+ // Adding to an existing cell selection
36
+ setCellSelection(view.state.selection.$anchorCell, startEvent);
37
+ startEvent.preventDefault();
38
+ } else if (
39
+ startEvent.shiftKey &&
40
+ startDOMCell &&
41
+ ($anchor = cellAround(view.state.selection.$anchor)) != null &&
42
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
43
+ cellUnderMouse(view, startEvent)!.pos != $anchor.pos
44
+ ) {
45
+ // Adding to a selection that starts in another cell (causing a
46
+ // cell selection to be created).
47
+ setCellSelection($anchor, startEvent);
48
+ startEvent.preventDefault();
49
+ } else if (!startDOMCell) {
50
+ // Not in a cell, let the default behavior happen.
51
+ return;
52
+ }
53
+
54
+ // Create and dispatch a cell selection between the given anchor and
55
+ // the position under the mouse.
56
+ function setCellSelection($anchor: ResolvedPos, event: MouseEvent) {
57
+ let $head = cellUnderMouse(view, event);
58
+ const starting = tableEditingKey.getState(view.state) == null;
59
+ if (!$head || !inSameTable($anchor, $head)) {
60
+ if (starting) $head = $anchor;
61
+ else return;
62
+ }
63
+ const selection = new CellSelection($anchor, $head);
64
+ if (starting || !view.state.selection.eq(selection)) {
65
+ const tr = view.state.tr.setSelection(selection);
66
+ if (starting) tr.setMeta(tableEditingKey, $anchor.pos);
67
+ view.dispatch(tr);
68
+ }
69
+ }
70
+
71
+ // Stop listening to mouse motion events.
72
+ function stop() {
73
+ view.root.removeEventListener('mouseup', stop);
74
+ view.root.removeEventListener('dragstart', stop);
75
+ view.root.removeEventListener('mousemove', move);
76
+ if (tableEditingKey.getState(view.state) != null) view.dispatch(view.state.tr.setMeta(tableEditingKey, -1));
77
+ }
78
+
79
+ function move(event: Event) {
80
+ const anchor = tableEditingKey.getState(view.state);
81
+ let $anchor;
82
+ if (anchor != null) {
83
+ // Continuing an existing cross-cell selection
84
+ $anchor = view.state.doc.resolve(anchor);
85
+ } else if (domInCell(view, event.target) != startDOMCell) {
86
+ // Moving out of the initial cell -- start a new cell selection
87
+ $anchor = cellUnderMouse(view, startEvent);
88
+ if (!$anchor) return stop();
89
+ }
90
+ if ($anchor) setCellSelection($anchor, event as MouseEvent);
91
+ }
92
+ view.root.addEventListener('mouseup', stop);
93
+ view.root.addEventListener('dragstart', stop);
94
+ view.root.addEventListener('mousemove', move);
95
+ }
96
+
97
+ function handleTripleClick(view: EditorView, pos: number) {
98
+ const doc = view.state.doc,
99
+ $cell = cellAround(doc.resolve(pos));
100
+ if (!$cell) return false;
101
+ view.dispatch(view.state.tr.setSelection(new CellSelection($cell)));
102
+ return true;
103
+ }
104
+
105
+ function maybeSetSelection(
106
+ state: EditorState,
107
+ dispatch: undefined | ((tr: Transaction) => void),
108
+ selection: Selection,
109
+ ) {
110
+ if (selection.eq(state.selection)) return false;
111
+ if (dispatch) dispatch(state.tr.setSelection(selection).scrollIntoView());
112
+ return true;
113
+ }
114
+
115
+ function atEndOfCell(view: EditorView, axis: string, dir: number) {
116
+ if (!(view.state.selection instanceof TextSelection)) return null;
117
+ const { $head } = view.state.selection;
118
+ for (let d = $head.depth - 1; d >= 0; d--) {
119
+ const parent = $head.node(d),
120
+ index = dir < 0 ? $head.index(d) : $head.indexAfter(d);
121
+ if (index != (dir < 0 ? 0 : parent.childCount)) return null;
122
+ if (parent.type.spec['tableRole'] == 'cell' || parent.type.spec['tableRole'] == 'header_cell') {
123
+ const cellPos = $head.before(d);
124
+ const dirStr = axis == 'vert' ? (dir > 0 ? 'down' : 'up') : dir > 0 ? 'right' : 'left';
125
+ return view.endOfTextblock(dirStr) ? cellPos : null;
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+
131
+ function arrow(axis: string, dir: number): Command {
132
+ return (state, dispatch, view) => {
133
+ const sel = state.selection;
134
+ if (sel instanceof CellSelection) {
135
+ return maybeSetSelection(state, dispatch, Selection.near(sel.$headCell, dir));
136
+ }
137
+ if (axis != 'horiz' && !sel.empty) return false;
138
+ const end = atEndOfCell(view as EditorView, axis, dir);
139
+ if (end == null) return false;
140
+ if (axis == 'horiz') {
141
+ return maybeSetSelection(state, dispatch, Selection.near(state.doc.resolve(sel.head + dir), dir));
142
+ } else {
143
+ const $cell = state.doc.resolve(end),
144
+ $next = nextCell($cell, axis, dir);
145
+ let newSel;
146
+ if ($next) newSel = Selection.near($next, 1);
147
+ else if (dir < 0) newSel = Selection.near(state.doc.resolve($cell.before(-1)), -1);
148
+ else newSel = Selection.near(state.doc.resolve($cell.after(-1)), 1);
149
+ return maybeSetSelection(state, dispatch, newSel);
150
+ }
151
+ };
152
+ }
153
+
154
+ function shiftArrow(axis: string, dir: number): Command {
155
+ return (state, dispatch, view) => {
156
+ let sel = state.selection;
157
+ if (!(sel instanceof CellSelection)) {
158
+ const end = atEndOfCell(view as EditorView, axis, dir);
159
+ if (end == null) return false;
160
+ sel = new CellSelection(state.doc.resolve(end));
161
+ }
162
+ const $head = nextCell((sel as CellSelection).$headCell, axis, dir);
163
+ if (!$head) return false;
164
+ return maybeSetSelection(state, dispatch, new CellSelection((sel as CellSelection).$anchorCell, $head));
165
+ };
166
+ }
167
+
168
+ function deleteCellSelection(state: EditorState, dispatch?: (tr: Transaction) => void) {
169
+ const sel = state.selection;
170
+ if (!(sel instanceof CellSelection)) return false;
171
+ if (dispatch) {
172
+ const tr = state.tr,
173
+ baseContent = tableNodeTypes(state.schema).cell.createAndFill().content;
174
+ sel.forEachCell((cell, pos) => {
175
+ if (!cell.content.eq(baseContent))
176
+ tr.replace(
177
+ tr.mapping.map(pos + 1),
178
+ tr.mapping.map(pos + cell.nodeSize - 1),
179
+ new Slice(baseContent, 0, 0),
180
+ );
181
+ });
182
+ if (tr.docChanged) dispatch(tr);
183
+ }
184
+ return true;
185
+ }
186
+
187
+ const handleKeyDown = keydownHandler({
188
+ ArrowLeft: arrow('horiz', -1),
189
+ ArrowRight: arrow('horiz', 1),
190
+ ArrowUp: arrow('vert', -1),
191
+ ArrowDown: arrow('vert', 1),
192
+
193
+ 'Shift-ArrowLeft': shiftArrow('horiz', -1),
194
+ 'Shift-ArrowRight': shiftArrow('horiz', 1),
195
+ 'Shift-ArrowUp': shiftArrow('vert', -1),
196
+ 'Shift-ArrowDown': shiftArrow('vert', 1),
197
+
198
+ Backspace: deleteCellSelection,
199
+ 'Mod-Backspace': deleteCellSelection,
200
+ Delete: deleteCellSelection,
201
+ 'Mod-Delete': deleteCellSelection,
202
+ });
203
+
204
+ export function handlePaste(view: EditorView, _: Event, slice: Slice) {
205
+ if (!isInTable(view.state)) return false;
206
+ let cells = pastedCells(slice);
207
+ const sel = view.state.selection;
208
+ if (sel instanceof CellSelection) {
209
+ if (!cells)
210
+ cells = {
211
+ width: 1,
212
+ height: 1,
213
+ rows: [Fragment.from(fitSlice(tableNodeTypes(view.state.schema).cell, slice))],
214
+ };
215
+ const table = sel.$anchorCell.node(-1),
216
+ start = sel.$anchorCell.start(-1);
217
+ const rect = TableMap.get(table).rectBetween(sel.$anchorCell.pos - start, sel.$headCell.pos - start);
218
+ cells = clipCells(cells, rect.right - rect.left, rect.bottom - rect.top);
219
+ insertCells(view.state, view.dispatch, start, rect, cells);
220
+ return true;
221
+ } else if (cells) {
222
+ const $cell = selectionCell(view.state) as ResolvedPos,
223
+ start = $cell.start(-1);
224
+ insertCells(view.state, view.dispatch, start, TableMap.get($cell.node(-1)).findCell($cell.pos - start), cells);
225
+ return true;
226
+ } else {
227
+ return false;
228
+ }
229
+ }
230
+
231
+ export function tableEditing({ allowTableNodeSelection = false } = {}) {
232
+ return new Plugin({
233
+ key: tableEditingKey,
234
+
235
+ // This piece of state is used to remember when a mouse-drag
236
+ // cell-selection is happening, so that it can continue even as
237
+ // transactions (which might move its anchor cell) come in.
238
+ state: {
239
+ init() {
240
+ return null;
241
+ },
242
+ apply(tr, cur) {
243
+ const set = tr.getMeta(tableEditingKey);
244
+ if (set != null) return set == -1 ? null : set;
245
+ if (cur == null || !tr.docChanged) return cur;
246
+ const { deleted, pos } = tr.mapping.mapResult(cur);
247
+ return deleted ? null : pos;
248
+ },
249
+ },
250
+
251
+ props: {
252
+ decorations: drawCellSelection,
253
+
254
+ handleDOMEvents: {
255
+ mousedown: handleMouseDown,
256
+ },
257
+
258
+ createSelectionBetween(view) {
259
+ if (tableEditingKey.getState(view.state) != null) return view.state.selection;
260
+
261
+ return null;
262
+ },
263
+
264
+ handleTripleClick,
265
+
266
+ handleKeyDown,
267
+
268
+ handlePaste,
269
+ },
270
+
271
+ appendTransaction(_, oldState, state) {
272
+ return normalizeSelection(state, fixTables(state, oldState), allowTableNodeSelection);
273
+ },
274
+ });
275
+ }
@@ -0,0 +1,280 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+
3
+ import { Attrs, Node } from '@milkdown/prose/model';
4
+
5
+ const cache = new WeakMap<Node, TableMap>();
6
+ const readFromCache = (key: Node) => cache.get(key);
7
+ const addToCache = (key: Node, value: TableMap) => {
8
+ cache.set(key, value);
9
+ return value;
10
+ };
11
+
12
+ export class Rect {
13
+ public tableStart?: number;
14
+ public map?: TableMap;
15
+ public table?: Node;
16
+ constructor(public left: number, public top: number, public right: number, public bottom: number) {}
17
+ }
18
+
19
+ // ::- A table map describes the structore of a given table. To avoid
20
+ // recomputing them all the time, they are cached per table node. To
21
+ // be able to do that, positions saved in the map are relative to the
22
+ // start of the table, rather than the start of the document.
23
+ export class TableMap {
24
+ constructor(public width: number, public height: number, public map: number[], public problems?: Problem[]) {
25
+ // :: number The width of the table
26
+ this.width = width;
27
+ // :: number The table's height
28
+ this.height = height;
29
+ // :: [number] A width * height array with the start position of
30
+ // the cell covering that part of the table in each slot
31
+ this.map = map;
32
+ // An optional array of problems (cell overlap or non-rectangular
33
+ // shape) for the table, used by the table normalizer.
34
+ this.problems = problems;
35
+ }
36
+
37
+ // Find the dimensions of the cell at the given position.
38
+ findCell(pos: number): Rect {
39
+ for (let i = 0; i < this.map.length; i++) {
40
+ const curPos = this.map[i];
41
+ if (curPos != pos) continue;
42
+ const left = i % this.width,
43
+ top = (i / this.width) | 0;
44
+ let right = left + 1,
45
+ bottom = top + 1;
46
+ for (let j = 1; right < this.width && this.map[i + j] == curPos; j++) right++;
47
+ for (let j = 1; bottom < this.height && this.map[i + this.width * j] == curPos; j++) bottom++;
48
+ return new Rect(left, top, right, bottom);
49
+ }
50
+ throw new RangeError('No cell with offset ' + pos + ' found');
51
+ }
52
+
53
+ colCount(pos: number): number {
54
+ for (let i = 0; i < this.map.length; i++) if (this.map[i] == pos) return i % this.width;
55
+ throw new RangeError('No cell with offset ' + pos + ' found');
56
+ }
57
+
58
+ // :: (number, string, number) → ?number
59
+ // Find the next cell in the given direction, starting from the cell
60
+ // at `pos`, if any.
61
+ nextCell(pos: number, axis: string, dir: number): number | undefined {
62
+ const { left, right, top, bottom } = this.findCell(pos);
63
+ if (axis == 'horiz') {
64
+ if (dir < 0 ? left == 0 : right == this.width) return undefined;
65
+ return this.map[top * this.width + (dir < 0 ? left - 1 : right)];
66
+ } else {
67
+ if (dir < 0 ? top == 0 : bottom == this.height) return undefined;
68
+ return this.map[left + this.width * (dir < 0 ? top - 1 : bottom)];
69
+ }
70
+ }
71
+
72
+ // :: (number, number) → Rect
73
+ // Get the rectangle spanning the two given cells.
74
+ rectBetween(a: number, b: number): Rect {
75
+ const { left: leftA, right: rightA, top: topA, bottom: bottomA } = this.findCell(a);
76
+ const { left: leftB, right: rightB, top: topB, bottom: bottomB } = this.findCell(b);
77
+ return new Rect(
78
+ Math.min(leftA, leftB),
79
+ Math.min(topA, topB),
80
+ Math.max(rightA, rightB),
81
+ Math.max(bottomA, bottomB),
82
+ );
83
+ }
84
+
85
+ // :: (Rect) → [number]
86
+ // Return the position of all cells that have the top left corner in
87
+ // the given rectangle.
88
+ cellsInRect(rect: Rect): number[] {
89
+ const result: number[] = [],
90
+ seen: Record<number, boolean> = {};
91
+ for (let row = rect.top; row < rect.bottom; row++) {
92
+ for (let col = rect.left; col < rect.right; col++) {
93
+ const index = row * this.width + col,
94
+ pos = this.map[index] as number;
95
+ if (seen[pos]) continue;
96
+ seen[pos] = true;
97
+ if (
98
+ (col != rect.left || !col || this.map[index - 1] != pos) &&
99
+ (row != rect.top || !row || this.map[index - this.width] != pos)
100
+ )
101
+ result.push(pos);
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+
107
+ // :: (number, number, Node) → number
108
+ // Return the position at which the cell at the given row and column
109
+ // starts, or would start, if a cell started there.
110
+ positionAt(row: number, col: number, table: Node): number {
111
+ for (let i = 0, rowStart = 0; ; i++) {
112
+ const rowEnd = rowStart + table.child(i).nodeSize;
113
+ if (i == row) {
114
+ let index = col + row * this.width;
115
+ const rowEndIndex = (row + 1) * this.width;
116
+ // Skip past cells from previous rows (via rowspan)
117
+ while (index < rowEndIndex && (this.map[index] as number) < rowStart) index++;
118
+ return index == rowEndIndex ? rowEnd - 1 : (this.map[index] as number);
119
+ }
120
+ rowStart = rowEnd;
121
+ }
122
+ }
123
+
124
+ // Find the table map for the given table node.
125
+ static get(table: Node): TableMap {
126
+ return readFromCache(table) || addToCache(table, computeMap(table));
127
+ }
128
+ }
129
+
130
+ export type Problem =
131
+ | {
132
+ type: 'missing';
133
+ row: number;
134
+ n: number;
135
+ }
136
+ | {
137
+ type: 'overlong_rowspan';
138
+ pos: number;
139
+ n: number;
140
+ }
141
+ | {
142
+ type: 'collision';
143
+ row: number;
144
+ pos: number;
145
+ n: number;
146
+ }
147
+ | {
148
+ type: 'colwidth mismatch';
149
+ pos: number;
150
+ colwidth: boolean;
151
+ };
152
+
153
+ // Compute a table map.
154
+ function computeMap(table: Node) {
155
+ if (table.type.spec['tableRole'] != 'table') throw new RangeError('Not a table node: ' + table.type.name);
156
+ const width = findWidth(table),
157
+ height = table.childCount;
158
+ const map: number[] = [];
159
+ const colWidths: number[] = [];
160
+ let mapPos = 0,
161
+ problems: Problem[] | undefined = undefined;
162
+ for (let i = 0, e = width * height; i < e; i++) map[i] = 0;
163
+
164
+ for (let row = 0, pos = 0; row < height; row++) {
165
+ const rowNode = table.child(row);
166
+ pos++;
167
+ for (let i = 0; ; i++) {
168
+ while (mapPos < map.length && map[mapPos] != 0) mapPos++;
169
+ if (i == rowNode.childCount) break;
170
+ const cellNode = rowNode.child(i),
171
+ { colspan, rowspan, colwidth } = cellNode.attrs;
172
+ for (let h = 0; h < rowspan; h++) {
173
+ if (h + row >= height) {
174
+ (problems || (problems = [])).push({
175
+ type: 'overlong_rowspan',
176
+ pos,
177
+ n: rowspan - h,
178
+ });
179
+ break;
180
+ }
181
+ const start = mapPos + h * width;
182
+ for (let w = 0; w < colspan; w++) {
183
+ if (map[start + w] == 0) map[start + w] = pos;
184
+ else
185
+ (problems || (problems = [])).push({
186
+ type: 'collision',
187
+ row,
188
+ pos,
189
+ n: colspan - w,
190
+ });
191
+ const colW = colwidth && colwidth[w];
192
+ if (colW) {
193
+ const widthIndex = ((start + w) % width) * 2,
194
+ prev = colWidths[widthIndex];
195
+ if (prev == null || (prev != colW && colWidths[widthIndex + 1] == 1)) {
196
+ colWidths[widthIndex] = colW;
197
+ colWidths[widthIndex + 1] = 1;
198
+ } else if (prev == colW) {
199
+ colWidths[widthIndex + 1]++;
200
+ }
201
+ }
202
+ }
203
+ }
204
+ mapPos += colspan;
205
+ pos += cellNode.nodeSize;
206
+ }
207
+ const expectedPos = (row + 1) * width;
208
+ let missing = 0;
209
+ while (mapPos < expectedPos) if (map[mapPos++] == 0) missing++;
210
+ if (missing) (problems || (problems = [])).push({ type: 'missing', row, n: missing });
211
+ pos++;
212
+ }
213
+
214
+ const tableMap = new TableMap(width, height, map, problems);
215
+ let badWidths = false;
216
+
217
+ // For columns that have defined widths, but whose widths disagree
218
+ // between rows, fix up the cells whose width doesn't match the
219
+ // computed one.
220
+ for (let i = 0; !badWidths && i < colWidths.length; i += 2)
221
+ if (colWidths[i] != null && (colWidths[i + 1] as number) < height) badWidths = true;
222
+ if (badWidths) findBadColWidths(tableMap, colWidths, table);
223
+
224
+ return tableMap;
225
+ }
226
+
227
+ function findWidth(table: Node) {
228
+ let width = -1,
229
+ hasRowSpan = false;
230
+ for (let row = 0; row < table.childCount; row++) {
231
+ const rowNode = table.child(row);
232
+ let rowWidth = 0;
233
+ if (hasRowSpan)
234
+ for (let j = 0; j < row; j++) {
235
+ const prevRow = table.child(j);
236
+ for (let i = 0; i < prevRow.childCount; i++) {
237
+ const cell = prevRow.child(i);
238
+ if (j + cell.attrs['rowspan'] > row) rowWidth += cell.attrs['colspan'];
239
+ }
240
+ }
241
+ for (let i = 0; i < rowNode.childCount; i++) {
242
+ const cell = rowNode.child(i);
243
+ rowWidth += cell.attrs['colspan'];
244
+ if (cell.attrs['rowspan'] > 1) hasRowSpan = true;
245
+ }
246
+ if (width == -1) width = rowWidth;
247
+ else if (width != rowWidth) width = Math.max(width, rowWidth);
248
+ }
249
+ return width;
250
+ }
251
+
252
+ function findBadColWidths(map: TableMap, colWidths: number[], table: Node) {
253
+ if (!map.problems) map.problems = [];
254
+ for (let i = 0, seen: Record<number, boolean> = {}; i < map.map.length; i++) {
255
+ const pos = map.map[i] as number;
256
+ if (seen[pos]) continue;
257
+ seen[pos] = true;
258
+ const node = table.nodeAt(pos) as Node;
259
+ let updated = null;
260
+ for (let j = 0; j < node.attrs['colspan']; j++) {
261
+ const col = (i + j) % map.width,
262
+ colWidth = colWidths[col * 2];
263
+ if (colWidth != null && (!node.attrs['colwidth'] || node.attrs['colwidth'][j] != colWidth))
264
+ (updated || (updated = freshColWidth(node.attrs)))[j] = colWidth;
265
+ }
266
+ if (updated)
267
+ map.problems.unshift({
268
+ type: 'colwidth mismatch',
269
+ pos,
270
+ colwidth: updated,
271
+ });
272
+ }
273
+ }
274
+
275
+ function freshColWidth(attrs: Attrs) {
276
+ if (attrs['colwidth']) return attrs['colwidth'].slice();
277
+ const result = [];
278
+ for (let i = 0; i < attrs['colspan']; i++) result.push(0);
279
+ return result;
280
+ }
@@ -0,0 +1,76 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+ import { Node } from '@milkdown/prose/model';
3
+ import { NodeView } from '@milkdown/prose/view';
4
+
5
+ /* Copyright 2021, Milkdown by Mirone. */
6
+ export class TableView implements NodeView {
7
+ public dom: HTMLElement;
8
+ public contentDOM: HTMLElement;
9
+ public table: HTMLTableElement;
10
+ public colgroup: HTMLTableColElement;
11
+
12
+ constructor(public node: Node, public cellMinWidth: number) {
13
+ this.node = node;
14
+ this.cellMinWidth = cellMinWidth;
15
+ this.dom = document.createElement('div');
16
+ this.dom.className = 'tableWrapper';
17
+ this.table = this.dom.appendChild(document.createElement('table'));
18
+ this.colgroup = this.table.appendChild(document.createElement('colgroup'));
19
+ updateColumns(node, this.colgroup, this.table, cellMinWidth);
20
+ this.contentDOM = this.table.appendChild(document.createElement('tbody'));
21
+ }
22
+
23
+ update(node: Node) {
24
+ if (node.type != this.node.type) return false;
25
+ this.node = node;
26
+ updateColumns(node, this.colgroup, this.table, this.cellMinWidth);
27
+ return true;
28
+ }
29
+
30
+ ignoreMutation(record: MutationRecord) {
31
+ return record.type == 'attributes' && (record.target == this.table || this.colgroup.contains(record.target));
32
+ }
33
+ }
34
+
35
+ export function updateColumns(
36
+ node: Node,
37
+ colgroup: HTMLTableColElement,
38
+ table: HTMLTableElement,
39
+ cellMinWidth: number,
40
+ overrideCol?: number,
41
+ overrideValue?: number,
42
+ ) {
43
+ let totalWidth = 0,
44
+ fixedWidth = true;
45
+ let nextDOM = colgroup.firstChild;
46
+ const row = node.firstChild as Node;
47
+ for (let i = 0, col = 0; i < row.childCount; i++) {
48
+ const { colspan, colwidth } = row.child(i).attrs;
49
+ for (let j = 0; j < colspan; j++, col++) {
50
+ const hasWidth = overrideCol == col ? overrideValue : colwidth && colwidth[j];
51
+ const cssWidth = hasWidth ? hasWidth + 'px' : '';
52
+ totalWidth += hasWidth || cellMinWidth;
53
+ if (!hasWidth) fixedWidth = false;
54
+ if (!nextDOM) {
55
+ colgroup.appendChild(document.createElement('col')).style.width = cssWidth;
56
+ } else {
57
+ if ((nextDOM as HTMLElement).style.width != cssWidth) (nextDOM as HTMLElement).style.width = cssWidth;
58
+ nextDOM = nextDOM.nextSibling;
59
+ }
60
+ }
61
+ }
62
+
63
+ while (nextDOM) {
64
+ const after = nextDOM.nextSibling;
65
+ nextDOM.parentNode?.removeChild(nextDOM);
66
+ nextDOM = after;
67
+ }
68
+
69
+ if (fixedWidth) {
70
+ table.style.width = totalWidth + 'px';
71
+ table.style.minWidth = '';
72
+ } else {
73
+ table.style.width = '';
74
+ table.style.minWidth = totalWidth + 'px';
75
+ }
76
+ }
@@ -0,0 +1,16 @@
1
+ /* Copyright 2021, Milkdown by Mirone. */
2
+
3
+ export type getFromDOM<T> = (dom: Element) => T;
4
+ export type setDOMAttr = <Value>(value: Value, attrs: Record<string, unknown>) => void;
5
+
6
+ export interface CellAttributes<T = unknown> {
7
+ default: T;
8
+ getFromDOM?: getFromDOM<T>;
9
+ setDOMAttr?: setDOMAttr;
10
+ }
11
+
12
+ export interface TableNodesOptions {
13
+ tableGroup?: string;
14
+ cellContent: string;
15
+ cellAttributes: { [key: string]: CellAttributes };
16
+ }