remark-grid-tables
Version:
This plugin parses custom Markdown syntax to describe tables. It was inspired by [this syntax](https://github.com/smartboyathome/Markdown-GridTables/blob/b4d16d5d254bed4336713d27eb8a37dc0e5f4273/mdx_grid_tables.py).
774 lines (660 loc) • 22.8 kB
JavaScript
;
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var trimEnd = require('lodash.trimend');
var visit = require('unist-util-visit');
var mainLineRegex = new RegExp(/((\+)|(\|)).+((\|)|(\+))/);
var totalMainLineRegex = new RegExp(/^((\+)|(\|)).+((\|)|(\+))$/);
var headerLineRegex = new RegExp(/^\+=[=+]+=\+$/);
var partLineRegex = new RegExp(/\+-[-+]+-\+/);
var separationLineRegex = new RegExp(/^\+-[-+]+-\+$/);
module.exports = plugin;
// A small class helping table generation
var Table = function () {
function Table(linesInfos) {
_classCallCheck(this, Table);
this._parts = [];
this._linesInfos = linesInfos;
this.addPart();
}
_createClass(Table, [{
key: 'lastPart',
value: function lastPart() {
return this._parts[this._parts.length - 1];
}
}, {
key: 'addPart',
value: function addPart() {
this._parts.push(new TablePart(this._linesInfos));
}
}]);
return Table;
}();
var TablePart = function () {
function TablePart(linesInfos) {
_classCallCheck(this, TablePart);
this._rows = [];
this._linesInfos = linesInfos;
this.addRow();
}
_createClass(TablePart, [{
key: 'addRow',
value: function addRow() {
this._rows.push(new TableRow(this._linesInfos));
}
}, {
key: 'removeLastRow',
value: function removeLastRow() {
this._rows.pop();
}
}, {
key: 'lastRow',
value: function lastRow() {
return this._rows[this._rows.length - 1];
}
}, {
key: 'updateWithMainLine',
value: function updateWithMainLine(line, isEndLine) {
// Update last row according to a line.
var mergeChars = isEndLine ? '+|' : '|';
var newCells = [this.lastRow()._cells[0]];
for (var c = 1; c < this.lastRow()._cells.length; c++) {
var cell = this.lastRow()._cells[c];
// Only cells with rowspan equals can be merged
// Test if the char before the cell is a separation character
if (cell._rowspan === newCells[newCells.length - 1]._rowspan && !mergeChars.includes(line[cell._startPosition - 1])) {
newCells[newCells.length - 1].mergeWith(cell);
} else {
newCells.push(cell);
}
}
this.lastRow()._cells = newCells;
}
}, {
key: 'updateWithPartLine',
value: function updateWithPartLine(line) {
// Get cells not finished
var remainingCells = [];
for (var c = 0; c < this.lastRow()._cells.length; c++) {
var cell = this.lastRow()._cells[c];
var partLine = line.substring(cell._startPosition - 1, cell._endPosition + 1);
if (!isSeparationLine(partLine)) {
cell._lines.push(line.substring(cell._startPosition, cell._endPosition));
cell._rowspan += 1;
remainingCells.push(cell);
}
}
// Generate new row
this.addRow();
var newCells = [];
for (var _c = 0; _c < remainingCells.length; _c++) {
var remainingCell = remainingCells[_c];
for (var cc = 0; cc < this.lastRow()._cells.length; cc++) {
var _cell = this.lastRow()._cells[cc];
if (_cell._endPosition < remainingCell._startPosition && !newCells.includes(_cell)) {
newCells.push(_cell);
}
}
newCells.push(remainingCell);
for (var _cc = 0; _cc < this.lastRow()._cells.length; _cc++) {
var _cell2 = this.lastRow()._cells[_cc];
if (_cell2._startPosition > remainingCell._endPosition && !newCells.includes(_cell2)) {
newCells.push(_cell2);
}
}
}
// Remove duplicates
for (var nc = 0; nc < newCells.length; nc++) {
var newCell = newCells[nc];
for (var ncc = 0; ncc < newCells.length; ncc++) {
if (nc !== ncc) {
var other = newCells[ncc];
if (other._startPosition >= newCell._startPosition && other._endPosition <= newCell._endPosition) {
if (other._lines.length === 0) {
newCells.splice(ncc, 1);
ncc -= 1;
if (nc > ncc) {
nc -= 1;
newCell = newCells[nc];
}
}
}
}
}
}
this.lastRow()._cells = newCells;
}
}]);
return TablePart;
}();
var TableRow = function () {
function TableRow(linesInfos) {
_classCallCheck(this, TableRow);
this._linesInfos = linesInfos;
this._cells = [];
for (var i = 0; i < linesInfos.length - 1; i++) {
this._cells.push(new TableCell(linesInfos[i] + 1, linesInfos[i + 1]));
}
}
_createClass(TableRow, [{
key: 'updateContent',
value: function updateContent(line) {
for (var c = 0; c < this._cells.length; c++) {
var cell = this._cells[c];
cell._lines.push(line.substring(cell._startPosition, cell._endPosition));
}
}
}]);
return TableRow;
}();
var TableCell = function () {
function TableCell(startPosition, endPosition) {
_classCallCheck(this, TableCell);
this._startPosition = startPosition;
this._endPosition = endPosition;
this._colspan = 1;
this._rowspan = 1;
this._lines = [];
}
_createClass(TableCell, [{
key: 'mergeWith',
value: function mergeWith(other) {
this._endPosition = other._endPosition;
this._colspan += other._colspan;
var newLines = [];
for (var l = 0; l < this._lines.length; l++) {
newLines.push(this._lines[l] + '|' + other._lines[l]);
}
this._lines = newLines;
}
}]);
return TableCell;
}();
function merge(beforeTable, gridTable, afterTable) {
// get the eaten text
var total = beforeTable.join('\n');
if (total.length) {
total += '\n';
}
total += gridTable.join('\n');
if (afterTable.join('\n').length) {
total += '\n';
}
total += afterTable.join('\n');
return total;
}
function isSeparationLine(line) {
return separationLineRegex.exec(line);
}
function isHeaderLine(line) {
return headerLineRegex.exec(line);
}
function isPartLine(line) {
return partLineRegex.exec(line);
}
function findAll(content, characters) {
var pos = [];
for (var i = 0; i < content.length; i++) {
var char = content[i];
if (characters.includes(char)) {
pos.push(i);
}
}
return pos;
}
function computePlainLineColumnsStartingPositions(line) {
return findAll(line, '+|');
}
function mergeColumnsStartingPositions(allPos) {
// Get all starting positions, allPos is an array of array of positions
var positions = [];
allPos.forEach(function (posRow) {
return posRow.forEach(function (pos) {
if (!positions.includes(pos)) {
positions.push(pos);
}
});
});
return positions.sort(function (a, b) {
return a - b;
});
}
function computeColumnStartingPositions(lines) {
var linesInfo = [];
lines.forEach(function (line) {
if (isHeaderLine(line) || isPartLine(line)) {
linesInfo.push(computePlainLineColumnsStartingPositions(line));
}
});
return mergeColumnsStartingPositions(linesInfo);
}
function extractTable(value, eat, tokenizer) {
// Extract lines before the grid table
var markdownLines = value.split('\n');
var i = 0;
var before = [];
for (; i < markdownLines.length; i++) {
var line = markdownLines[i];
if (isSeparationLine(line)) break;
if (line.length === 0) break;
before.push(line);
}
var possibleGridTable = markdownLines.map(function (line) {
return trimEnd(line);
});
// Extract table
if (!possibleGridTable[i + 1]) return [null, null, null, null];
var lineLength = possibleGridTable[i + 1].length;
var gridTable = [];
var hasHeader = false;
for (; i < possibleGridTable.length; i++) {
var _line = possibleGridTable[i];
var isMainLine = totalMainLineRegex.exec(_line);
// line is in table
if (isMainLine && _line.length === lineLength) {
var _isHeaderLine = headerLineRegex.exec(_line);
if (_isHeaderLine && !hasHeader) hasHeader = true;
// A table can't have 2 headers
else if (_isHeaderLine && hasHeader) {
break;
}
gridTable.push(_line);
} else {
// this line is not in the grid table.
break;
}
}
// if the last line is not a plain line
if (!separationLineRegex.exec(gridTable[gridTable.length - 1])) {
// Remove lines not in the table
for (var j = gridTable.length - 1; j >= 0; j--) {
var isSeparation = separationLineRegex.exec(gridTable[j]);
if (isSeparation) break;
gridTable.pop();
i -= 1;
}
}
// Extract lines after table
var after = [];
for (; i < possibleGridTable.length; i++) {
var _line2 = possibleGridTable[i];
if (_line2.length === 0) break;
after.push(_line2);
}
return [before, gridTable, after, hasHeader];
}
function extractTableContent(lines, linesInfos, hasHeader) {
var table = new Table(linesInfos);
for (var l = 0; l < lines.length; l++) {
var line = lines[l];
// Get if the line separate the head of the table from the body
var matchHeader = hasHeader & isHeaderLine(line) !== null;
// Get if the line close some cells
var isEndLine = matchHeader | isPartLine(line) !== null;
if (isEndLine) {
// It is a header, a plain line or a line with plain line part.
// First, update the last row
table.lastPart().updateWithMainLine(line, isEndLine);
// Create the new row
if (l !== 0) {
if (matchHeader) {
table.addPart();
} else if (isSeparationLine(line)) {
table.lastPart().addRow();
} else {
table.lastPart().updateWithPartLine(line);
}
}
// update the last row
table.lastPart().updateWithMainLine(line, isEndLine);
} else {
// it's a plain line
table.lastPart().updateWithMainLine(line, isEndLine);
table.lastPart().lastRow().updateContent(line);
}
}
// Because the last line is a separation, the last row is always empty
table.lastPart().removeLastRow();
return table;
}
function generateTable(tableContent, now, tokenizer) {
// Generate the gridTable node to insert in the AST
var tableElt = {
type: 'gridTable',
children: [],
data: {
hName: 'table'
}
};
var hasHeader = tableContent._parts.length > 1;
for (var p = 0; p < tableContent._parts.length; p++) {
var part = tableContent._parts[p];
var partElt = {
type: 'tableHeader',
children: [],
data: {
hName: hasHeader && p === 0 ? 'thead' : 'tbody'
}
};
for (var r = 0; r < part._rows.length; r++) {
var row = part._rows[r];
var rowElt = {
type: 'tableRow',
children: [],
data: {
hName: 'tr'
}
};
for (var c = 0; c < row._cells.length; c++) {
var cell = row._cells[c];
var tokenizedContent = tokenizer.tokenizeBlock(cell._lines.map(function (e) {
return e.trim();
}).join('\n'), now);
var cellElt = {
type: 'tableCell',
children: tokenizedContent,
data: {
hName: hasHeader && p === 0 ? 'th' : 'td',
hProperties: {
colspan: cell._colspan,
rowspan: cell._rowspan
}
}
};
var endLine = r + cell._rowspan;
if (cell._rowspan > 1 && endLine - 1 < part._rows.length) {
for (var rs = 1; rs < cell._rowspan; rs++) {
for (var cc = 0; cc < part._rows[r + rs]._cells.length; cc++) {
var other = part._rows[r + rs]._cells[cc];
if (cell._startPosition === other._startPosition && cell._endPosition === other._endPosition && cell._colspan === other._colspan && cell._rowspan === other._rowspan && cell._lines === other._lines) {
part._rows[r + rs]._cells.splice(cc, 1);
}
}
}
}
rowElt.children.push(cellElt);
}
partElt.children.push(rowElt);
}
tableElt.children.push(partElt);
}
return tableElt;
}
function gridTableTokenizer(eat, value, silent) {
var keep = mainLineRegex.exec(value);
if (!keep) return;
var _extractTable = extractTable(value, eat, this),
_extractTable2 = _slicedToArray(_extractTable, 4),
before = _extractTable2[0],
gridTable = _extractTable2[1],
after = _extractTable2[2],
hasHeader = _extractTable2[3];
if (!gridTable || gridTable.length < 3) return;
var now = eat.now();
var linesInfos = computeColumnStartingPositions(gridTable);
var tableContent = extractTableContent(gridTable, linesInfos, hasHeader);
var tableElt = generateTable(tableContent, now, this);
var merged = merge(before, gridTable, after);
// Because we can't add multiples blocs in one eat, I use a temp block
var wrapperBlock = {
type: 'element',
tagName: 'WrapperBlock',
children: []
};
if (before.length) {
var tokensBefore = this.tokenizeBlock(before.join('\n'), now)[0];
wrapperBlock.children.push(tokensBefore);
}
wrapperBlock.children.push(tableElt);
if (after.length) {
var tokensAfter = this.tokenizeBlock(after.join('\n'), now);
if (tokensAfter.length) {
wrapperBlock.children.push(tokensAfter[0]);
}
}
return eat(merged)(wrapperBlock);
}
function deleteWrapperBlock() {
function one(node, index, parent) {
if (!node.children) return;
var newChildren = [];
var replace = false;
for (var c = 0; c < node.children.length; c++) {
var child = node.children[c];
if (child.tagName === 'WrapperBlock' && child.type === 'element') {
replace = true;
for (var cc = 0; cc < child.children.length; cc++) {
newChildren.push(child.children[cc]);
}
} else {
newChildren.push(child);
}
}
if (replace) {
node.children = newChildren;
}
}
return one;
}
function transformer(tree) {
// Remove the temporary block in which we previously wrapped the table parts
visit(tree, deleteWrapperBlock());
}
function createGrid(nbRows, nbCols) {
var grid = [];
for (var i = 0; i < nbRows; i++) {
grid.push([]);
for (var j = 0; j < nbCols; j++) {
grid[i].push({ height: -1, width: -1, hasBottom: true, hasRigth: true });
}
}
return grid;
}
function setWidth(grid, i, j, cols) {
/* To do it, we put enougth space to write the text.
* For multi-cell, we divid it among the cells. */
var tmpWidth = Math.max.apply(Math, _toConsumableArray(Array.from(grid[i][j].value).map(function (x) {
return x.length;
}))) + 2;
grid[i].forEach(function (_, c) {
if (c < cols) {
// To divid
var localWidth = Math.ceil(tmpWidth / (cols - c)); // cols - c will be 1 for the last cell
tmpWidth -= localWidth;
grid[i][j + c].width = localWidth;
}
});
}
function setHeight(grid, i, j, values) {
// To do it, we count the line. Extra length to cell with a pipe
// in the value of the last line, to not be confuse with a border.
grid[i][j].height = values.length;
// Extra line
if (values[values.length - 1].indexOf('|') > 0) {
grid[i][j].height += 1;
}
}
function extractAST(gridNode, grid, nbRows, nbCols, getMD) {
var _this = this;
var i = 0;
/* Fill the grid with value, height and width from the ast */
gridNode.children.forEach(function (th) {
th.children.forEach(function (row) {
row.children.forEach(function (cell, j) {
var X = 0; // x taking colspan and rowspan into account
while (grid[i][j + X].evaluated) {
X++;
}grid[i][j + X].value = _this.all(cell).join('\n\n').split('\n');
setHeight(grid, i, j + X, grid[i][j + X].value);
setWidth(grid, i, j + X, cell.data.hProperties.colspan);
// If it's empty, we fill it up with a useless space
// Otherwise, it will not be parsed.
if (!grid[0][0].value.join('\n')) {
grid[0][0].value = [' '];
grid[0][0].width = 3;
}
// Define the border of each cell
for (var x = 0; x < cell.data.hProperties.rowspan; x++) {
for (var y = 0; y < cell.data.hProperties.colspan; y++) {
// b attribute is for bottom
grid[i + x][j + X + y].hasBottom = x + 1 === cell.data.hProperties.rowspan;
// r attribute is for right
grid[i + x][j + X + y].hasRigth = y + 1 === cell.data.hProperties.colspan;
// set v if a cell has ever been define
grid[i + x][j + X + y].evaluated = ' ';
}
}
});
i++;
});
});
// If they is 2 differents tableHeader, so the first one is a header and
// should be underlined
if (gridNode.children.length > 1) {
grid[gridNode.children[0].children.length - 1][0].isHeader = true;
}
}
function setSize(grid) {
// The idea is the max win
// Set the height of each column
grid.forEach(function (row) {
// Find the max
var maxHeight = Math.max.apply(Math, _toConsumableArray(row.map(function (cell) {
return cell.height;
})));
// Set it to each cell
row.forEach(function (cell) {
cell.height = maxHeight;
});
});
// Set the width of each row
grid[0].forEach(function (_, j) {
// Find the max
var maxWidth = Math.max.apply(Math, _toConsumableArray(grid.map(function (row) {
return row[j].width;
})));
// Set it to each cell
grid.forEach(function (row) {
row[j].width = maxWidth;
});
});
}
function generateBorders(grid, nbRows, nbCols, gridString) {
/** **** Create the borders *******/
// Create the first line
/*
* We have to create the first line manually because
* we process the borders from the attributes bottom
* and right of each cell. For the first line, their
* is no bottom nor right cell.
*
* We only need the right attribute of the first row's
* cells
*/
var first = '+';
grid[0].forEach(function (cell, i) {
first += '-'.repeat(cell.width);
first += cell.hasRigth || i === nbCols - 1 ? '+' : '-';
});
gridString.push(first);
grid.forEach(function (row, i) {
var line = '';
// Cells lines
// The inner of the cell
line = '|';
row.forEach(function (cell) {
cell.y = gridString.length;
cell.x = line.length + 1;
line += ' '.repeat(cell.width);
line += cell.hasRigth ? '|' : ' ';
});
// Add it until the text can fit
for (var t = 0; t < row[0].height; t++) {
gridString.push(line);
}
// "End" line
// It's the last line of the cell. Actually the border.
line = row[0].hasBottom ? '+' : '|';
row.forEach(function (cell, j) {
var char = ' ';
if (cell.hasBottom) {
if (row[0].isHeader) {
char = '=';
} else {
char = '-';
}
}
line += char.repeat(cell.width);
if (cell.hasBottom || j + 1 < nbCols && grid[i][j + 1].hasBottom) {
if (cell.hasRigth || i + 1 < nbRows && grid[i + 1][j].hasRigth) {
line += '+';
} else {
line += row[0].isHeader ? '=' : '-';
}
} else if (cell.hasRigth || i + 1 < nbRows && grid[i + 1][j].hasRigth) {
line += '|';
} else {
line += ' ';
}
});
gridString.push(line);
});
}
function writeText(grid, gridString) {
grid.forEach(function (row) {
row.forEach(function (cell) {
if (cell.value && cell.value[0]) {
for (var tmpCount = 0; tmpCount < cell.value.length; tmpCount++) {
var tmpLine = cell.y + tmpCount;
var line = cell.value[tmpCount];
var lineEdit = gridString[tmpLine];
gridString[tmpLine] = lineEdit.substr(0, cell.x);
gridString[tmpLine] += line;
gridString[tmpLine] += lineEdit.substr(cell.x + line.length);
}
}
});
});
}
function stringifyGridTables(gridNode) {
var gridString = [];
var nbRows = gridNode.children.map(function (th) {
return th.children.length;
}).reduce(function (a, b) {
return a + b;
});
var nbCols = gridNode.children[0].children[0].children.map(function (c) {
return c.data.hProperties.colspan;
}).reduce(function (a, b) {
return a + b;
});
var grid = createGrid(nbRows, nbCols);
/* First, we extract the information
* then, we set the size(2) of the border
* and create it(3).
* Finaly we fill it up.
*/
extractAST.bind(this)(gridNode, grid, nbRows, nbCols);
setSize(grid);
generateBorders(grid, nbRows, nbCols, gridString);
writeText(grid, gridString);
return gridString.join('\n');
}
function plugin() {
var Parser = this.Parser;
// Inject blockTokenizer
var blockTokenizers = Parser.prototype.blockTokenizers;
var blockMethods = Parser.prototype.blockMethods;
blockTokenizers.grid_table = gridTableTokenizer;
blockMethods.splice(blockMethods.indexOf('fencedCode') + 1, 0, 'grid_table');
var Compiler = this.Compiler;
// Stringify
if (Compiler) {
var visitors = Compiler.prototype.visitors;
if (!visitors) return;
visitors.gridTable = stringifyGridTables;
}
return transformer;
}