Files
datascape/assets/editor/tables.js
T

374 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
window.EditorTables = (function () {
function repeat(ch, n) {
var s = '';
for (var i = 0; i < n; i++) s += ch;
return s;
}
var segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
// Graphemes rendered two columns wide: emoji-presentation characters, anything
// forced to emoji presentation via VS16, and East Asian wide/fullwidth ranges.
var WIDE = /\p{Emoji_Presentation}|️|[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/u;
var ZERO = /^[\p{Mn}\p{Me}\p{Cf}]+$/u;
function displayWidth(s) {
var w = 0;
Array.from(segmenter.segment(s), function (seg) {
var g = seg.segment;
if (!ZERO.test(g)) w += WIDE.test(g) ? 2 : 1;
});
return w;
}
function padLeft(s, w) {
return repeat(' ', w - displayWidth(s)) + s;
}
function padRight(s, w) {
return s + repeat(' ', w - displayWidth(s));
}
function padCenter(s, w) {
var total = w - displayWidth(s);
var left = Math.floor(total / 2);
return repeat(' ', left) + s + repeat(' ', total - left);
}
function parseTableRow(line) {
var trimmed = line.trim();
if (trimmed.charAt(0) === '|') trimmed = trimmed.slice(1);
if (trimmed.charAt(trimmed.length - 1) === '|') trimmed = trimmed.slice(0, -1);
return trimmed.split('|').map(function (c) { return c.trim(); });
}
function isSeparatorRow(cells) {
return cells.every(function (c) { return /^:?-+:?$/.test(c); });
}
function parseAlignment(cell) {
var left = cell.charAt(0) === ':';
var right = cell.charAt(cell.length - 1) === ':';
if (left && right) return 'center';
if (left) return 'left';
if (right) return 'right';
return null;
}
function makeSepCell(width, align) {
if (align === 'left') return ':' + repeat('-', width - 1);
if (align === 'center') return ':' + repeat('-', width - 2) + ':';
if (align === 'right') return repeat('-', width - 1) + ':';
return repeat('-', width);
}
function findTableRange(text, cursorPos) {
var lines = text.split('\n');
var charCount = 0;
var cursorLine = 0;
for (var i = 0; i < lines.length; i++) {
if (charCount + lines[i].length >= cursorPos && charCount <= cursorPos) {
cursorLine = i;
}
charCount += lines[i].length + 1;
}
if (!/^\|.*\|$/.test(lines[cursorLine].trim())) return null;
var start = cursorLine;
while (start > 0 && /^\|.*\|$/.test(lines[start - 1].trim())) start--;
var end = cursorLine;
while (end < lines.length - 1 && /^\|.*\|$/.test(lines[end + 1].trim())) end++;
if (end - start < 1) return null;
return { start: start, end: end, lines: lines, cursorLine: cursorLine };
}
function formatTableText(text, cursorPos) {
var range = findTableRange(text, cursorPos);
if (!range) return null;
var rows = [];
var sepIndex = -1;
var alignments = [];
for (var i = range.start; i <= range.end; i++) {
var cells = parseTableRow(range.lines[i]);
if (sepIndex === -1 && isSeparatorRow(cells)) {
sepIndex = rows.length;
alignments = cells.map(function (c) { return parseAlignment(c); });
}
rows.push(cells);
}
if (sepIndex === -1) return null;
var colCount = 0;
rows.forEach(function (r) { if (r.length > colCount) colCount = r.length; });
while (alignments.length < colCount) alignments.push(null);
rows = rows.map(function (r) {
while (r.length < colCount) r.push('');
return r;
});
var widths = [];
for (var c = 0; c < colCount; c++) {
var max = 3;
for (var r = 0; r < rows.length; r++) {
if (r === sepIndex) continue;
var len = displayWidth(rows[r][c]);
if (len > max) max = len;
}
widths.push(max);
}
var formatted = rows.map(function (row, ri) {
var cells = row.map(function (cell, ci) {
var w = widths[ci];
if (ri === sepIndex) return makeSepCell(w, alignments[ci]);
var align = alignments[ci];
if (align === 'right') return padLeft(cell, w);
if (align === 'center') return padCenter(cell, w);
return padRight(cell, w);
});
return '| ' + cells.join(' | ') + ' |';
});
var beforeTable = range.lines.slice(0, range.start).join('\n');
var afterTable = range.lines.slice(range.end + 1).join('\n');
var parts = [];
if (beforeTable) parts.push(beforeTable);
parts.push(formatted.join('\n'));
if (afterTable) parts.push(afterTable);
var newText = parts.join('\n');
var oldBeforeLen = 0;
for (var i = 0; i < range.start; i++) oldBeforeLen += range.lines[i].length + 1;
var cursorInTable = cursorPos - oldBeforeLen;
var newTableText = formatted.join('\n');
var newCursor = (beforeTable ? beforeTable.length + 1 : 0) + Math.min(cursorInTable, newTableText.length);
return { text: newText, cursor: newCursor };
}
function getCursorColumn(text, cursorPos) {
var range = findTableRange(text, cursorPos);
if (!range) return null;
var lines = text.split('\n');
var charCount = 0;
for (var i = 0; i < range.cursorLine; i++) charCount += lines[i].length + 1;
var lineOffset = cursorPos - charCount;
var line = lines[range.cursorLine];
var col = -1;
for (var c = 0; c < line.length; c++) {
if (line.charAt(c) === '|') {
col++;
if (c >= lineOffset) return Math.max(col - 1, 0);
}
}
return Math.max(col, 0);
}
function setColumnAlignment(text, cursorPos, align) {
var range = findTableRange(text, cursorPos);
if (!range) return null;
var colIdx = getCursorColumn(text, cursorPos);
if (colIdx === null) return null;
var rows = [];
var sepIndex = -1;
for (var i = range.start; i <= range.end; i++) {
var cells = parseTableRow(range.lines[i]);
if (sepIndex === -1 && isSeparatorRow(cells)) sepIndex = rows.length;
rows.push(cells);
}
if (sepIndex === -1 || colIdx >= rows[sepIndex].length) return null;
var cell = rows[sepIndex][colIdx].replace(/:/g, '-');
if (align === 'left') cell = ':' + cell.slice(1);
else if (align === 'center') cell = ':' + cell.slice(1, -1) + ':';
else if (align === 'right') cell = cell.slice(0, -1) + ':';
rows[sepIndex][colIdx] = cell;
var newLines = range.lines.slice();
for (var i = 0; i < rows.length; i++) {
newLines[range.start + i] = '| ' + rows[i].join(' | ') + ' |';
}
return formatTableText(newLines.join('\n'), cursorPos);
}
function insertColumn(text, cursorPos) {
var range = findTableRange(text, cursorPos);
if (!range) return null;
var colIdx = getCursorColumn(text, cursorPos);
if (colIdx === null) return null;
var newLines = range.lines.slice();
var sepIndex = -1;
for (var i = range.start; i <= range.end; i++) {
var cells = parseTableRow(range.lines[i]);
var isSep = sepIndex === -1 && isSeparatorRow(cells);
if (isSep) sepIndex = i;
cells.splice(colIdx, 0, isSep ? '---' : '');
newLines[i] = '| ' + cells.join(' | ') + ' |';
}
return formatTableText(newLines.join('\n'), cursorPos);
}
function deleteColumn(text, cursorPos) {
var range = findTableRange(text, cursorPos);
if (!range) return null;
var colIdx = getCursorColumn(text, cursorPos);
if (colIdx === null) return null;
var newLines = range.lines.slice();
for (var i = range.start; i <= range.end; i++) {
var cells = parseTableRow(range.lines[i]);
if (cells.length <= 1) return null;
cells.splice(colIdx, 1);
newLines[i] = '| ' + cells.join(' | ') + ' |';
}
return formatTableText(newLines.join('\n'), cursorPos);
}
function insertRow(text, cursorPos) {
var range = findTableRange(text, cursorPos);
if (!range) return null;
var colCount = 0;
for (var i = range.start; i <= range.end; i++) {
var cells = parseTableRow(range.lines[i]);
if (cells.length > colCount) colCount = cells.length;
}
var emptyCells = [];
for (var c = 0; c < colCount; c++) emptyCells.push('');
var newLines = range.lines.slice();
newLines.splice(range.cursorLine + 1, 0, '| ' + emptyCells.join(' | ') + ' |');
return formatTableText(newLines.join('\n'), cursorPos);
}
function insertRowBelow(text, cursorPos) {
var range = findTableRange(text, cursorPos);
if (!range) return null;
var result = insertRow(text, cursorPos);
if (!result) return null;
var lines = result.text.split('\n');
var cursor = 0;
for (var i = 0; i <= range.cursorLine; i++) cursor += lines[i].length + 1;
cursor += 2; // skip leading '| '
return { text: result.text, cursor: cursor };
}
function deleteRow(text, cursorPos) {
var range = findTableRange(text, cursorPos);
if (!range) return null;
var sepIndex = -1;
for (var i = range.start; i <= range.end; i++) {
if (isSeparatorRow(parseTableRow(range.lines[i]))) { sepIndex = i; break; }
}
if (range.cursorLine === range.start || range.cursorLine === sepIndex) return null;
var newLines = range.lines.slice();
newLines.splice(range.cursorLine, 1);
var newCursor = cursorPos;
if (range.cursorLine < newLines.length) {
var charCount = 0;
for (var i = 0; i < range.cursorLine; i++) charCount += newLines[i].length + 1;
newCursor = charCount;
}
return formatTableText(newLines.join('\n'), Math.min(newCursor, newLines.join('\n').length));
}
// Absolute cursor offset at the start of the `col`-th cell's content on
// `lineIdx` of a *formatted* table (i.e. just past the "| " / " | " that
// opens the cell). Clamps to the last cell if col overflows the row.
function cellCursor(text, lineIdx, col) {
var lines = text.split('\n');
var offset = 0;
for (var i = 0; i < lineIdx; i++) offset += lines[i].length + 1;
var line = lines[lineIdx] || '';
var pipes = [];
for (var c = 0; c < line.length; c++) if (line.charAt(c) === '|') pipes.push(c);
if (pipes.length === 0) return Math.min(offset, text.length);
var p = pipes[Math.min(col, pipes.length - 1)];
return Math.min(offset + p + 2, text.length);
}
// Shared engine for the Enter / Tab / Shift-Tab cell navigation. Reformats
// the table and drops the cursor into the target cell, appending an empty
// row when navigation runs past the last row. Returns null when the cursor
// is not in a table (so the caller can fall back to the editor default).
function moveInTable(text, cursorPos, mode) {
var range = findTableRange(text, cursorPos);
if (!range) return null;
var colIdx = getCursorColumn(text, cursorPos);
if (colIdx === null) return null;
var sepRel = -1, colCount = 0;
for (var i = range.start; i <= range.end; i++) {
var cells = parseTableRow(range.lines[i]);
if (cells.length > colCount) colCount = cells.length;
if (sepRel === -1 && isSeparatorRow(cells)) sepRel = i - range.start;
}
if (sepRel === -1) return null;
var lastRel = range.end - range.start;
var rowRel = range.cursorLine - range.start;
var targetRow = rowRel, targetCol = colIdx;
if (mode === 'enter') {
targetRow = rowRel + 1;
if (targetRow === sepRel) targetRow++;
} else if (mode === 'tab') {
targetCol = colIdx + 1;
if (targetCol >= colCount) {
targetCol = 0;
targetRow = rowRel + 1;
if (targetRow === sepRel) targetRow++;
}
} else if (mode === 'shifttab') {
targetCol = colIdx - 1;
if (targetCol < 0) {
targetCol = colCount - 1;
targetRow = rowRel - 1;
if (targetRow === sepRel) targetRow--;
}
if (targetRow < 0) return null;
}
var lines = range.lines.slice();
if (targetRow > lastRel) {
var emptyCells = [];
for (var c = 0; c < colCount; c++) emptyCells.push('');
var emptyLine = '| ' + emptyCells.join(' | ') + ' |';
while (lastRel < targetRow) {
lines.splice(range.start + lastRel + 1, 0, emptyLine);
lastRel++;
}
}
var tableStartOffset = 0;
for (var i = 0; i < range.start; i++) tableStartOffset += lines[i].length + 1;
var formatted = formatTableText(lines.join('\n'), tableStartOffset);
if (!formatted) return null;
return {
text: formatted.text,
cursor: cellCursor(formatted.text, range.start + targetRow, targetCol),
};
}
function nextRowSameColumn(text, cursorPos) { return moveInTable(text, cursorPos, 'enter'); }
function nextCell(text, cursorPos) { return moveInTable(text, cursorPos, 'tab'); }
function prevCell(text, cursorPos) { return moveInTable(text, cursorPos, 'shifttab'); }
return {
formatTableText: formatTableText,
nextRowSameColumn: nextRowSameColumn,
nextCell: nextCell,
prevCell: prevCell,
setColumnAlignment: setColumnAlignment,
insertColumn: insertColumn,
deleteColumn: deleteColumn,
insertRow: insertRow,
insertRowBelow: insertRowBelow,
deleteRow: deleteRow,
};
})();