You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					315 lines
				
				9.8 KiB
			
		
		
			
		
	
	
					315 lines
				
				9.8 KiB
			| 
								 
											3 years ago
										 
									 | 
							
								// 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.
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								let readFromCache, addToCache;
							 | 
						||
| 
								 | 
							
								// 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();
							 | 
						||
| 
								 | 
							
								  readFromCache = (key) => cache.get(key);
							 | 
						||
| 
								 | 
							
								  addToCache = (key, value) => {
							 | 
						||
| 
								 | 
							
								    cache.set(key, value);
							 | 
						||
| 
								 | 
							
								    return value;
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								} else {
							 | 
						||
| 
								 | 
							
								  let cache = [],
							 | 
						||
| 
								 | 
							
								    cacheSize = 10,
							 | 
						||
| 
								 | 
							
								    cachePos = 0;
							 | 
						||
| 
								 | 
							
								  readFromCache = (key) => {
							 | 
						||
| 
								 | 
							
								    for (let i = 0; i < cache.length; i += 2)
							 | 
						||
| 
								 | 
							
								      if (cache[i] == key) return cache[i + 1];
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								  addToCache = (key, value) => {
							 | 
						||
| 
								 | 
							
								    if (cachePos == cacheSize) cachePos = 0;
							 | 
						||
| 
								 | 
							
								    cache[cachePos++] = key;
							 | 
						||
| 
								 | 
							
								    return (cache[cachePos++] = value);
							 | 
						||
| 
								 | 
							
								  };
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								export class Rect {
							 | 
						||
| 
								 | 
							
								  constructor(left, top, right, bottom) {
							 | 
						||
| 
								 | 
							
								    this.left = left;
							 | 
						||
| 
								 | 
							
								    this.top = top;
							 | 
						||
| 
								 | 
							
								    this.right = right;
							 | 
						||
| 
								 | 
							
								    this.bottom = bottom;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// ::- A table map describes the structore 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.
							 | 
						||
| 
								 | 
							
								export class TableMap {
							 | 
						||
| 
								 | 
							
								  constructor(width, height, map, problems) {
							 | 
						||
| 
								 | 
							
								    // :: number The width of the table
							 | 
						||
| 
								 | 
							
								    this.width = width;
							 | 
						||
| 
								 | 
							
								    // :: number The table's height
							 | 
						||
| 
								 | 
							
								    this.height = height;
							 | 
						||
| 
								 | 
							
								    // :: [number] A width * height array with the start position of
							 | 
						||
| 
								 | 
							
								    // the cell covering that part of the table in each slot
							 | 
						||
| 
								 | 
							
								    this.map = map;
							 | 
						||
| 
								 | 
							
								    // An optional array of problems (cell overlap or non-rectangular
							 | 
						||
| 
								 | 
							
								    // shape) for the table, used by the table normalizer.
							 | 
						||
| 
								 | 
							
								    this.problems = problems;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // :: (number) → Rect
							 | 
						||
| 
								 | 
							
								  // Find the dimensions of the cell at the given position.
							 | 
						||
| 
								 | 
							
								  findCell(pos) {
							 | 
						||
| 
								 | 
							
								    for (let i = 0; i < this.map.length; i++) {
							 | 
						||
| 
								 | 
							
								      let curPos = this.map[i];
							 | 
						||
| 
								 | 
							
								      if (curPos != pos) continue;
							 | 
						||
| 
								 | 
							
								      let left = i % this.width,
							 | 
						||
| 
								 | 
							
								        top = (i / this.width) | 0;
							 | 
						||
| 
								 | 
							
								      let right = left + 1,
							 | 
						||
| 
								 | 
							
								        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 new Rect(left, top, right, bottom);
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    throw new RangeError('No cell with offset ' + pos + ' found');
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // :: (number) → number
							 | 
						||
| 
								 | 
							
								  // Find the left side of the cell at the given position.
							 | 
						||
| 
								 | 
							
								  colCount(pos) {
							 | 
						||
| 
								 | 
							
								    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');
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // :: (number, string, number) → ?number
							 | 
						||
| 
								 | 
							
								  // Find the next cell in the given direction, starting from the cell
							 | 
						||
| 
								 | 
							
								  // at `pos`, if any.
							 | 
						||
| 
								 | 
							
								  nextCell(pos, axis, dir) {
							 | 
						||
| 
								 | 
							
								    let { 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)];
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // :: (number, number) → Rect
							 | 
						||
| 
								 | 
							
								  // Get the rectangle spanning the two given cells.
							 | 
						||
| 
								 | 
							
								  rectBetween(a, b) {
							 | 
						||
| 
								 | 
							
								    let {
							 | 
						||
| 
								 | 
							
								      left: leftA,
							 | 
						||
| 
								 | 
							
								      right: rightA,
							 | 
						||
| 
								 | 
							
								      top: topA,
							 | 
						||
| 
								 | 
							
								      bottom: bottomA,
							 | 
						||
| 
								 | 
							
								    } = this.findCell(a);
							 | 
						||
| 
								 | 
							
								    let {
							 | 
						||
| 
								 | 
							
								      left: leftB,
							 | 
						||
| 
								 | 
							
								      right: rightB,
							 | 
						||
| 
								 | 
							
								      top: topB,
							 | 
						||
| 
								 | 
							
								      bottom: bottomB,
							 | 
						||
| 
								 | 
							
								    } = this.findCell(b);
							 | 
						||
| 
								 | 
							
								    return new Rect(
							 | 
						||
| 
								 | 
							
								      Math.min(leftA, leftB),
							 | 
						||
| 
								 | 
							
								      Math.min(topA, topB),
							 | 
						||
| 
								 | 
							
								      Math.max(rightA, rightB),
							 | 
						||
| 
								 | 
							
								      Math.max(bottomA, bottomB),
							 | 
						||
| 
								 | 
							
								    );
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // :: (Rect) → [number]
							 | 
						||
| 
								 | 
							
								  // Return the position of all cells that have the top left corner in
							 | 
						||
| 
								 | 
							
								  // the given rectangle.
							 | 
						||
| 
								 | 
							
								  cellsInRect(rect) {
							 | 
						||
| 
								 | 
							
								    let result = [],
							 | 
						||
| 
								 | 
							
								      seen = {};
							 | 
						||
| 
								 | 
							
								    for (let row = rect.top; row < rect.bottom; row++) {
							 | 
						||
| 
								 | 
							
								      for (let col = rect.left; col < rect.right; col++) {
							 | 
						||
| 
								 | 
							
								        let index = row * this.width + col,
							 | 
						||
| 
								 | 
							
								          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)
							 | 
						||
| 
								 | 
							
								        )
							 | 
						||
| 
								 | 
							
								          result.push(pos);
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    return result;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // :: (number, number, Node) → number
							 | 
						||
| 
								 | 
							
								  // Return the position at which the cell at the given row and column
							 | 
						||
| 
								 | 
							
								  // starts, or would start, if a cell started there.
							 | 
						||
| 
								 | 
							
								  positionAt(row, col, table) {
							 | 
						||
| 
								 | 
							
								    for (let i = 0, rowStart = 0; ; i++) {
							 | 
						||
| 
								 | 
							
								      let rowEnd = rowStart + table.child(i).nodeSize;
							 | 
						||
| 
								 | 
							
								      if (i == row) {
							 | 
						||
| 
								 | 
							
								        let index = col + row * this.width,
							 | 
						||
| 
								 | 
							
								          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;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  // :: (Node) → TableMap
							 | 
						||
| 
								 | 
							
								  // Find the table map for the given table node.
							 | 
						||
| 
								 | 
							
								  static get(table) {
							 | 
						||
| 
								 | 
							
								    return readFromCache(table) || addToCache(table, computeMap(table));
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								// Compute a table map.
							 | 
						||
| 
								 | 
							
								function computeMap(table) {
							 | 
						||
| 
								 | 
							
								  if (table.type.spec.tableRole != 'table')
							 | 
						||
| 
								 | 
							
								    throw new RangeError('Not a table node: ' + table.type.name);
							 | 
						||
| 
								 | 
							
								  let width = findWidth(table),
							 | 
						||
| 
								 | 
							
								    height = table.childCount;
							 | 
						||
| 
								 | 
							
								  let map = [],
							 | 
						||
| 
								 | 
							
								    mapPos = 0,
							 | 
						||
| 
								 | 
							
								    problems = null,
							 | 
						||
| 
								 | 
							
								    colWidths = [];
							 | 
						||
| 
								 | 
							
								  for (let i = 0, e = width * height; i < e; i++) map[i] = 0;
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  for (let row = 0, pos = 0; row < height; row++) {
							 | 
						||
| 
								 | 
							
								    let rowNode = table.child(row);
							 | 
						||
| 
								 | 
							
								    pos++;
							 | 
						||
| 
								 | 
							
								    for (let i = 0; ; i++) {
							 | 
						||
| 
								 | 
							
								      while (mapPos < map.length && map[mapPos] != 0) mapPos++;
							 | 
						||
| 
								 | 
							
								      if (i == rowNode.childCount) break;
							 | 
						||
| 
								 | 
							
								      let cellNode = rowNode.child(i),
							 | 
						||
| 
								 | 
							
								        { 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;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								        let 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,
							 | 
						||
| 
								 | 
							
								            });
							 | 
						||
| 
								 | 
							
								          let colW = colwidth && colwidth[w];
							 | 
						||
| 
								 | 
							
								          if (colW) {
							 | 
						||
| 
								 | 
							
								            let 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;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    let expectedPos = (row + 1) * width,
							 | 
						||
| 
								 | 
							
								      missing = 0;
							 | 
						||
| 
								 | 
							
								    while (mapPos < expectedPos) if (map[mapPos++] == 0) missing++;
							 | 
						||
| 
								 | 
							
								    if (missing)
							 | 
						||
| 
								 | 
							
								      (problems || (problems = [])).push({ type: 'missing', row, n: missing });
							 | 
						||
| 
								 | 
							
								    pos++;
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								  let tableMap = new TableMap(width, height, map, problems),
							 | 
						||
| 
								 | 
							
								    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) {
							 | 
						||
| 
								 | 
							
								  let width = -1,
							 | 
						||
| 
								 | 
							
								    hasRowSpan = false;
							 | 
						||
| 
								 | 
							
								  for (let row = 0; row < table.childCount; row++) {
							 | 
						||
| 
								 | 
							
								    let rowNode = table.child(row),
							 | 
						||
| 
								 | 
							
								      rowWidth = 0;
							 | 
						||
| 
								 | 
							
								    if (hasRowSpan)
							 | 
						||
| 
								 | 
							
								      for (let j = 0; j < row; j++) {
							 | 
						||
| 
								 | 
							
								        let prevRow = table.child(j);
							 | 
						||
| 
								 | 
							
								        for (let i = 0; i < prevRow.childCount; i++) {
							 | 
						||
| 
								 | 
							
								          let cell = prevRow.child(i);
							 | 
						||
| 
								 | 
							
								          if (j + cell.attrs.rowspan > row) rowWidth += cell.attrs.colspan;
							 | 
						||
| 
								 | 
							
								        }
							 | 
						||
| 
								 | 
							
								      }
							 | 
						||
| 
								 | 
							
								    for (let i = 0; i < rowNode.childCount; i++) {
							 | 
						||
| 
								 | 
							
								      let 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, colWidths, table) {
							 | 
						||
| 
								 | 
							
								  if (!map.problems) map.problems = [];
							 | 
						||
| 
								 | 
							
								  for (let i = 0, seen = {}; i < map.map.length; i++) {
							 | 
						||
| 
								 | 
							
								    let pos = map.map[i];
							 | 
						||
| 
								 | 
							
								    if (seen[pos]) continue;
							 | 
						||
| 
								 | 
							
								    seen[pos] = true;
							 | 
						||
| 
								 | 
							
								    let node = table.nodeAt(pos),
							 | 
						||
| 
								 | 
							
								      updated = null;
							 | 
						||
| 
								 | 
							
								    for (let j = 0; j < node.attrs.colspan; j++) {
							 | 
						||
| 
								 | 
							
								      let col = (i + j) % map.width,
							 | 
						||
| 
								 | 
							
								        colWidth = colWidths[col * 2];
							 | 
						||
| 
								 | 
							
								      if (
							 | 
						||
| 
								 | 
							
								        colWidth != null &&
							 | 
						||
| 
								 | 
							
								        (!node.attrs.colwidth || node.attrs.colwidth[j] != colWidth)
							 | 
						||
| 
								 | 
							
								      )
							 | 
						||
| 
								 | 
							
								        (updated || (updated = freshColWidth(node.attrs)))[j] = colWidth;
							 | 
						||
| 
								 | 
							
								    }
							 | 
						||
| 
								 | 
							
								    if (updated)
							 | 
						||
| 
								 | 
							
								      map.problems.unshift({
							 | 
						||
| 
								 | 
							
								        type: 'colwidth mismatch',
							 | 
						||
| 
								 | 
							
								        pos,
							 | 
						||
| 
								 | 
							
								        colwidth: updated,
							 | 
						||
| 
								 | 
							
								      });
							 | 
						||
| 
								 | 
							
								  }
							 | 
						||
| 
								 | 
							
								}
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								function freshColWidth(attrs) {
							 | 
						||
| 
								 | 
							
								  if (attrs.colwidth) return attrs.colwidth.slice();
							 | 
						||
| 
								 | 
							
								  let result = [];
							 | 
						||
| 
								 | 
							
								  for (let i = 0; i < attrs.colspan; i++) result.push(0);
							 | 
						||
| 
								 | 
							
								  return result;
							 | 
						||
| 
								 | 
							
								}
							 |