243 lines
7.3 KiB
JavaScript
243 lines
7.3 KiB
JavaScript
|
|
export function extendedTables({
|
|
interruptPatterns = [],
|
|
skipEmptyRows = true
|
|
} = {}) {
|
|
return {
|
|
extensions: [{
|
|
name: 'spanTable',
|
|
level: 'block', // Is this a block-level or inline-level tokenizer?
|
|
start(src) {
|
|
return src.match(/\n *([^\n ].*\|.*)\n/m)?.index;
|
|
}, // Hint to Marked.js to stop and check for a match
|
|
tokenizer(src, tokens) {
|
|
// const regex = this.tokenizer.rules.block.table;
|
|
let regexString = '^ *([^\\n ].*\\|.*\\n(?: *[^\\s].*\\n)*?)' // Header
|
|
+
|
|
' {0,3}(?:\\| *)?(:?-+(?: *(?:100|[1-9][0-9]?%) *-+)?:? *(?:\\| *:?-+(?: *(?:100|[1-9][0-9]?%) *-+)?:? *)*)(?:\\| *)?' // Align
|
|
+
|
|
'(?:\\n((?:(?! *\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' // Cells
|
|
+
|
|
'(?:\\n+|$)| {0,3}#{1,6}(?:\\s|$)| {0,3}>| {4}[^\\n]| {0,3}(?:`{3,}' +
|
|
'(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n| {0,3}(?:[*+-]|1[.)]) |' +
|
|
'<\\/?(?:address|article|aside|base|basefont|blockquote|body' +
|
|
'|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt' +
|
|
'|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]' +
|
|
'|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem' +
|
|
'|meta|nav|noframes|ol|optgroup|option|p|param|section|source' +
|
|
'|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)' +
|
|
'(?: +|\\n|\\/?>)|<(?:script|pre|style|textarea|!--)endRegex).*(?:\\n|$))*)\\n*|$)'; // Cells
|
|
|
|
regexString = regexString.replace('endRegex', interruptPatterns.map(str =>
|
|
`|(?:${str})`).join(''));
|
|
const widthRegex = / *(?:100|[1-9][0-9]?%) */g;
|
|
const regex = new RegExp(regexString);
|
|
const cap = regex.exec(src);
|
|
|
|
if (cap) {
|
|
const item = {
|
|
type: 'spanTable',
|
|
header: cap[1].replace(/\n$/, '').split('\n'),
|
|
align: cap[2].replace(widthRegex, '').replace(/^ *|\| *$/g, '').split(
|
|
/ *\| */),
|
|
rows: cap[3]?.trim() ? cap[3].replace(/\n[ \t]*$/, '').split('\n') : [],
|
|
width: cap[2].replace(/:/g, '').replace(/-+| /g, '').split('|')
|
|
};
|
|
|
|
// Get first header row to determine how many columns
|
|
item.header[0] = splitCells(item.header[0]);
|
|
|
|
const colCount = item.header[0].reduce((length, header) => {
|
|
return length + header.colspan;
|
|
}, 0);
|
|
|
|
if (colCount === item.align.length) {
|
|
item.raw = cap[0];
|
|
|
|
let i, j, k, row;
|
|
|
|
// Get alignment row (:---:)
|
|
let l = item.align.length;
|
|
|
|
for (i = 0; i < l; i++) {
|
|
if (/^ *-+: *$/.test(item.align[i])) {
|
|
item.align[i] = 'right';
|
|
} else if (/^ *:-+: *$/.test(item.align[i])) {
|
|
item.align[i] = 'center';
|
|
} else if (/^ *:-+ *$/.test(item.align[i])) {
|
|
item.align[i] = 'left';
|
|
} else {
|
|
item.align[i] = null;
|
|
}
|
|
}
|
|
|
|
// Get any remaining header rows
|
|
l = item.header.length;
|
|
for (i = 1; i < l; i++) {
|
|
item.header[i] = splitCells(item.header[i], colCount, item.header[i -
|
|
1], skipEmptyRows);
|
|
}
|
|
|
|
// Get main table cells
|
|
l = item.rows.length;
|
|
for (i = 0; i < l; i++) {
|
|
item.rows[i] = splitCells(item.rows[i], colCount, item.rows[i - 1],
|
|
skipEmptyRows);
|
|
}
|
|
|
|
// header child tokens
|
|
l = item.header.length;
|
|
for (j = 0; j < l; j++) {
|
|
row = item.header[j];
|
|
for (k = 0; k < row.length; k++) {
|
|
row[k].tokens = [];
|
|
this.lexer.inline(row[k].text, row[k].tokens);
|
|
}
|
|
}
|
|
|
|
// cell child tokens
|
|
l = item.rows.length;
|
|
for (j = 0; j < l; j++) {
|
|
row = item.rows[j];
|
|
for (k = 0; k < row.length; k++) {
|
|
row[k].tokens = [];
|
|
this.lexer.inline(row[k].text, row[k].tokens);
|
|
}
|
|
}
|
|
return item;
|
|
}
|
|
}
|
|
},
|
|
renderer(token) {
|
|
|
|
let i, j, row, cell, col, text;
|
|
let output = '<table class="table">';
|
|
output += '<thead class="thead">';
|
|
for (i = 0; i < token.header.length; i++) {
|
|
row = token.header[i];
|
|
let col = 0;
|
|
output += '<tr class="tr">';
|
|
for (j = 0; j < row.length; j++) {
|
|
cell = row[j];
|
|
text = this.parser.parseInline(cell.tokens);
|
|
output += getTableCell(text, cell, 'th', token.align[col], token.width[
|
|
col]);
|
|
col += cell.colspan;
|
|
}
|
|
output += '</tr>';
|
|
}
|
|
output += '</thead>';
|
|
if (token.rows.length) {
|
|
output += '<tbody class="tbody">';
|
|
for (i = 0; i < token.rows.length; i++) {
|
|
row = token.rows[i];
|
|
col = 0;
|
|
if (!row[0].emptyRow) {
|
|
output += '<tr class="tr">';
|
|
for (j = 0; j < row.length; j++) {
|
|
cell = row[j];
|
|
text = this.parser.parseInline(cell.tokens);
|
|
output += getTableCell(text, cell, 'td', token.align[col], token
|
|
.width[col]);
|
|
col += cell.colspan;
|
|
}
|
|
output += '</tr>';
|
|
}
|
|
}
|
|
output += '</tbody>';
|
|
}
|
|
output += '</table>';
|
|
return output;
|
|
}
|
|
}]
|
|
};
|
|
}
|
|
|
|
const getTableCell = (text, cell, type, align, width) => {
|
|
if (!cell.rowspan) {
|
|
return '';
|
|
}
|
|
const tag = `<${type} class="${type}"` +
|
|
`${cell.colspan > 1 ? ` colspan=${cell.colspan}` : ''}` +
|
|
`${cell.rowspan > 1 ? ` rowspan=${cell.rowspan}` : ''}` +
|
|
`${align ? ` align=${align}` : ''}` +
|
|
`${width ? ` width=${width}` : ''}>`;
|
|
return `${tag + text}</${type}>\n`;
|
|
};
|
|
|
|
const splitCells = (tableRow, count, prevRow = [], skipEmptyRows) => {
|
|
const cells = [...tableRow.trim().matchAll(/(?:[^|\\]|\\.?)+(?:\|+|$)/g)].map((x) => x[0]);
|
|
|
|
// Remove first/last cell in a row if whitespace only and no leading/trailing pipe
|
|
if (!cells[0]?.trim()) {
|
|
cells.shift();
|
|
}
|
|
if (!cells[cells.length - 1]?.trim()) {
|
|
cells.pop();
|
|
}
|
|
|
|
let numCols = 0;
|
|
let i, j, trimmedCell, prevCell, prevCols;
|
|
|
|
for (i = 0; i < cells.length; i++) {
|
|
trimmedCell = cells[i].split(/\|+$/)[0];
|
|
cells[i] = {
|
|
rowspan: 1,
|
|
colspan: Math.max(cells[i].length - trimmedCell.length, 1),
|
|
text: trimmedCell.trim().replace(/\\\|/g, '|')
|
|
// display escaped pipes as normal character
|
|
};
|
|
|
|
// Handle Rowspan
|
|
if (trimmedCell.slice(-1) === '^' && prevRow.length) {
|
|
// Find matching cell in previous row
|
|
prevCols = 0;
|
|
for (j = 0; j < prevRow.length; j++) {
|
|
prevCell = prevRow[j];
|
|
if ((prevCols === numCols) && (prevCell.colspan === cells[i].colspan)) {
|
|
// merge into matching cell in previous row (the "target")
|
|
cells[i].rowSpanTarget = prevCell.rowSpanTarget ?? prevCell;
|
|
cells[i].rowSpanTarget.text += ` ${cells[i].text.slice(0, -1)}`;
|
|
cells[i].rowSpanTarget.rowspan += 1;
|
|
cells[i].rowspan = 0;
|
|
break;
|
|
}
|
|
prevCols += prevCell.colspan;
|
|
if (prevCols > numCols) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
numCols += cells[i].colspan;
|
|
}
|
|
|
|
// If all cells have been merged, flag as an empty row
|
|
if (cells.length > 0 && skipEmptyRows && cells.length === cells.filter((cell) => {
|
|
return cell.rowspan === 0;
|
|
}).length) {
|
|
cells[0].emptyRow = true;
|
|
for (i = 0; i < cells.length; i++) {
|
|
cells[i].rowSpanTarget.rowspan -= 1;
|
|
}
|
|
}
|
|
|
|
// Force main cell rows to match header column count
|
|
if (numCols > count) {
|
|
cells.splice(count);
|
|
} else {
|
|
while (numCols < count) {
|
|
cells.push({
|
|
rowspan: 1,
|
|
colspan: 1,
|
|
text: ''
|
|
});
|
|
numCols += 1;
|
|
}
|
|
}
|
|
return cells;
|
|
};
|
|
|
|
|
|
|