123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377 |
- // Because working with row and column-spanning cells is not quite
- // trivial, this code builds up a descriptive structure for a given
- // table node. The structures are cached with the (persistent) table
- // nodes as key, so that they only have to be recomputed when the
- // content of the table changes.
- //
- // This does mean that they have to store table-relative, not
- // document-relative positions. So code that uses them will typically
- // compute the start position of the table and offset positions passed
- // to or gotten from this structure by that amount.
- import { Attrs, Node } from 'prosemirror-model';
- import { CellAttrs } from './util';
- /**
- * @public
- */
- export type ColWidths = number[];
- /**
- * @public
- */
- export type Problem =
- | {
- type: 'colwidth mismatch';
- pos: number;
- colwidth: ColWidths;
- }
- | {
- type: 'collision';
- pos: number;
- row: number;
- n: number;
- }
- | {
- type: 'missing';
- row: number;
- n: number;
- }
- | {
- type: 'overlong_rowspan';
- pos: number;
- n: number;
- };
- let readFromCache: (key: Node) => TableMap | undefined;
- let addToCache: (key: Node, value: TableMap) => TableMap;
- // Prefer using a weak map to cache table maps. Fall back on a
- // fixed-size cache if that's not supported.
- if (typeof WeakMap != 'undefined') {
- // eslint-disable-next-line
- let cache = new WeakMap<Node, TableMap>();
- readFromCache = (key) => cache.get(key);
- addToCache = (key, value) => {
- cache.set(key, value);
- return value;
- };
- } else {
- const cache: (Node | TableMap)[] = [];
- const cacheSize = 10;
- let cachePos = 0;
- readFromCache = (key) => {
- for (let i = 0; i < cache.length; i += 2)
- if (cache[i] == key) return cache[i + 1] as TableMap;
- };
- addToCache = (key, value) => {
- if (cachePos == cacheSize) cachePos = 0;
- cache[cachePos++] = key;
- return (cache[cachePos++] = value);
- };
- }
- /**
- * @public
- */
- export interface Rect {
- left: number;
- top: number;
- right: number;
- bottom: number;
- }
- /**
- * A table map describes the structure of a given table. To avoid
- * recomputing them all the time, they are cached per table node. To
- * be able to do that, positions saved in the map are relative to the
- * start of the table, rather than the start of the document.
- *
- * @public
- */
- export class TableMap {
- constructor(
- /**
- * The number of columns
- */
- public width: number,
- /**
- * The number of rows
- */
- public height: number,
- /**
- * A width * height array with the start position of
- * the cell covering that part of the table in each slot
- */
- public map: number[],
- /**
- * An optional array of problems (cell overlap or non-rectangular
- * shape) for the table, used by the table normalizer.
- */
- public problems: Problem[] | null,
- ) {}
- // Find the dimensions of the cell at the given position.
- findCell(pos: number): Rect {
- for (let i = 0; i < this.map.length; i++) {
- const curPos = this.map[i];
- if (curPos != pos) continue;
- const left = i % this.width;
- const top = (i / this.width) | 0;
- let right = left + 1;
- let bottom = top + 1;
- for (let j = 1; right < this.width && this.map[i + j] == curPos; j++) {
- right++;
- }
- for (
- let j = 1;
- bottom < this.height && this.map[i + this.width * j] == curPos;
- j++
- ) {
- bottom++;
- }
- return { left, top, right, bottom };
- }
- throw new RangeError(`No cell with offset ${pos} found`);
- }
- // Find the left side of the cell at the given position.
- colCount(pos: number): number {
- for (let i = 0; i < this.map.length; i++) {
- if (this.map[i] == pos) {
- return i % this.width;
- }
- }
- throw new RangeError(`No cell with offset ${pos} found`);
- }
- // Find the next cell in the given direction, starting from the cell
- // at `pos`, if any.
- nextCell(pos: number, axis: 'horiz' | 'vert', dir: number): null | number {
- const { left, right, top, bottom } = this.findCell(pos);
- if (axis == 'horiz') {
- if (dir < 0 ? left == 0 : right == this.width) return null;
- return this.map[top * this.width + (dir < 0 ? left - 1 : right)];
- } else {
- if (dir < 0 ? top == 0 : bottom == this.height) return null;
- return this.map[left + this.width * (dir < 0 ? top - 1 : bottom)];
- }
- }
- // Get the rectangle spanning the two given cells.
- rectBetween(a: number, b: number): Rect {
- const {
- left: leftA,
- right: rightA,
- top: topA,
- bottom: bottomA,
- } = this.findCell(a);
- const {
- left: leftB,
- right: rightB,
- top: topB,
- bottom: bottomB,
- } = this.findCell(b);
- return {
- left: Math.min(leftA, leftB),
- top: Math.min(topA, topB),
- right: Math.max(rightA, rightB),
- bottom: Math.max(bottomA, bottomB),
- };
- }
- // Return the position of all cells that have the top left corner in
- // the given rectangle.
- cellsInRect(rect: Rect): number[] {
- const result: number[] = [];
- const seen: Record<number, boolean> = {};
- for (let row = rect.top; row < rect.bottom; row++) {
- for (let col = rect.left; col < rect.right; col++) {
- const index = row * this.width + col;
- const pos = this.map[index];
- if (seen[pos]) continue;
- seen[pos] = true;
- if (
- (col == rect.left && col && this.map[index - 1] == pos) ||
- (row == rect.top && row && this.map[index - this.width] == pos)
- ) {
- continue;
- }
- result.push(pos);
- }
- }
- return result;
- }
- // Return the position at which the cell at the given row and column
- // starts, or would start, if a cell started there.
- positionAt(row: number, col: number, table: Node): number {
- for (let i = 0, rowStart = 0; ; i++) {
- const rowEnd = rowStart + table.child(i).nodeSize;
- if (i == row) {
- let index = col + row * this.width;
- const rowEndIndex = (row + 1) * this.width;
- // Skip past cells from previous rows (via rowspan)
- while (index < rowEndIndex && this.map[index] < rowStart) index++;
- return index == rowEndIndex ? rowEnd - 1 : this.map[index];
- }
- rowStart = rowEnd;
- }
- }
- // Find the table map for the given table node.
- static get(table: Node): TableMap {
- return readFromCache(table) || addToCache(table, computeMap(table));
- }
- }
- // Compute a table map.
- function computeMap(table: Node): TableMap {
- if (table.type.spec.tableRole != 'table')
- throw new RangeError('Not a table node: ' + table.type.name);
- const width = findWidth(table),
- height = table.childCount;
- const map = [];
- let mapPos = 0;
- let problems: Problem[] | null = null;
- const colWidths: ColWidths = [];
- for (let i = 0, e = width * height; i < e; i++) map[i] = 0;
- for (let row = 0, pos = 0; row < height; row++) {
- const rowNode = table.child(row);
- pos++;
- for (let i = 0; ; i++) {
- while (mapPos < map.length && map[mapPos] != 0) mapPos++;
- if (i == rowNode.childCount) break;
- const cellNode = rowNode.child(i);
- const { colspan, rowspan, colwidth } = cellNode.attrs;
- for (let h = 0; h < rowspan; h++) {
- if (h + row >= height) {
- (problems || (problems = [])).push({
- type: 'overlong_rowspan',
- pos,
- n: rowspan - h,
- });
- break;
- }
- const start = mapPos + h * width;
- for (let w = 0; w < colspan; w++) {
- if (map[start + w] == 0) map[start + w] = pos;
- else
- (problems || (problems = [])).push({
- type: 'collision',
- row,
- pos,
- n: colspan - w,
- });
- const colW = colwidth && colwidth[w];
- if (colW) {
- const widthIndex = ((start + w) % width) * 2,
- prev = colWidths[widthIndex];
- if (
- prev == null ||
- (prev != colW && colWidths[widthIndex + 1] == 1)
- ) {
- colWidths[widthIndex] = colW;
- colWidths[widthIndex + 1] = 1;
- } else if (prev == colW) {
- colWidths[widthIndex + 1]++;
- }
- }
- }
- }
- mapPos += colspan;
- pos += cellNode.nodeSize;
- }
- const expectedPos = (row + 1) * width;
- let missing = 0;
- while (mapPos < expectedPos) if (map[mapPos++] == 0) missing++;
- if (missing)
- (problems || (problems = [])).push({ type: 'missing', row, n: missing });
- pos++;
- }
- const tableMap = new TableMap(width, height, map, problems);
- let badWidths = false;
- // For columns that have defined widths, but whose widths disagree
- // between rows, fix up the cells whose width doesn't match the
- // computed one.
- for (let i = 0; !badWidths && i < colWidths.length; i += 2)
- if (colWidths[i] != null && colWidths[i + 1] < height) badWidths = true;
- if (badWidths) findBadColWidths(tableMap, colWidths, table);
- return tableMap;
- }
- function findWidth(table: Node): number {
- let width = -1;
- let hasRowSpan = false;
- for (let row = 0; row < table.childCount; row++) {
- const rowNode = table.child(row);
- let rowWidth = 0;
- if (hasRowSpan)
- for (let j = 0; j < row; j++) {
- const prevRow = table.child(j);
- for (let i = 0; i < prevRow.childCount; i++) {
- const cell = prevRow.child(i);
- if (j + cell.attrs.rowspan > row) rowWidth += cell.attrs.colspan;
- }
- }
- for (let i = 0; i < rowNode.childCount; i++) {
- const cell = rowNode.child(i);
- rowWidth += cell.attrs.colspan;
- if (cell.attrs.rowspan > 1) hasRowSpan = true;
- }
- if (width == -1) width = rowWidth;
- else if (width != rowWidth) width = Math.max(width, rowWidth);
- }
- return width;
- }
- function findBadColWidths(
- map: TableMap,
- colWidths: ColWidths,
- table: Node,
- ): void {
- if (!map.problems) map.problems = [];
- const seen: Record<number, boolean> = {};
- for (let i = 0; i < map.map.length; i++) {
- const pos = map.map[i];
- if (seen[pos]) continue;
- seen[pos] = true;
- const node = table.nodeAt(pos);
- if (!node) {
- throw new RangeError(`No cell with offset ${pos} found`);
- }
- let updated = null;
- const attrs = node.attrs as CellAttrs;
- for (let j = 0; j < attrs.colspan; j++) {
- const col = (i + j) % map.width;
- const colWidth = colWidths[col * 2];
- if (
- colWidth != null &&
- (!attrs.colwidth || attrs.colwidth[j] != colWidth)
- )
- (updated || (updated = freshColWidth(attrs)))[j] = colWidth;
- }
- if (updated)
- map.problems.unshift({
- type: 'colwidth mismatch',
- pos,
- colwidth: updated,
- });
- }
- }
- function freshColWidth(attrs: Attrs): ColWidths {
- if (attrs.colwidth) return attrs.colwidth.slice();
- const result: ColWidths = [];
- for (let i = 0; i < attrs.colspan; i++) result.push(0);
- return result;
- }
|