Notes/src/tables.js

172 lines
5.0 KiB
JavaScript
Raw Normal View History

2017-11-24 11:18:27 +00:00
var indexOf = Array.prototype.indexOf
var every = Array.prototype.every
2017-11-10 14:21:46 +00:00
var rules = {}
rules.tableCell = {
filter: ['th', 'td'],
replacement: function (content, node) {
if (tableShouldBeSkipped(nodeParentTable(node))) return content;
2017-11-10 14:21:46 +00:00
return cell(content, node)
}
}
rules.tableRow = {
filter: 'tr',
replacement: function (content, node) {
2018-05-24 12:31:40 +01:00
const parentTable = nodeParentTable(node);
if (tableShouldBeSkipped(parentTable)) return content;
2017-11-10 14:21:46 +00:00
var borderCells = ''
var alignMap = { left: ':--', right: '--:', center: ':-:' }
2017-11-24 11:18:27 +00:00
if (isHeadingRow(node)) {
2018-05-24 12:31:40 +01:00
const colCount = tableColCount(parentTable);
for (var i = 0; i < colCount; i++) {
const childNode = colCount >= node.childNodes.length ? null : node.childNodes[i];
2017-11-10 14:21:46 +00:00
var border = '---'
2018-05-24 12:31:40 +01:00
var align = childNode ? (childNode.getAttribute('align') || '').toLowerCase() : '';
2017-11-10 14:21:46 +00:00
if (align) border = alignMap[align] || border
2017-11-10 14:21:46 +00:00
2018-05-24 12:31:40 +01:00
if (childNode) {
borderCells += cell(border, node.childNodes[i])
} else {
borderCells += cell(border, null, i);
}
2017-11-10 14:21:46 +00:00
}
}
return '\n' + content + (borderCells ? '\n' + borderCells : '')
}
}
rules.table = {
// Only convert tables with a heading row.
// Tables with no heading row are kept using `keep` (see below).
filter: function (node) {
return node.nodeName === 'TABLE'
},
replacement: function (content, node) {
if (tableShouldBeSkipped(node)) return content;
// Ensure there are no blank lines
content = content.replace(/\n+/g, '\n')
// If table has no heading, add an empty one so as to get a valid Markdown table
var secondLine = content.trim().split('\n');
if (secondLine.length >= 2) secondLine = secondLine[1]
var secondLineIsDivider = secondLine.indexOf('| ---') === 0
var columnCount = tableColCount(node);
var emptyHeader = ''
if (columnCount && !secondLineIsDivider) {
emptyHeader = '|' + ' |'.repeat(columnCount) + '\n' + '|' + ' --- |'.repeat(columnCount)
}
2018-05-17 01:29:33 +01:00
return '\n\n' + emptyHeader + content + '\n\n'
2017-11-10 14:21:46 +00:00
}
}
rules.tableSection = {
filter: ['thead', 'tbody', 'tfoot'],
replacement: function (content) {
return content
}
}
2017-11-24 11:18:27 +00:00
// A tr is a heading row if:
// - the parent is a THEAD
// - or if its the first child of the TABLE or the first TBODY (possibly
// following a blank THEAD)
// - and every cell is a TH
function isHeadingRow (tr) {
var parentNode = tr.parentNode
return (
parentNode.nodeName === 'THEAD' ||
(
parentNode.firstChild === tr &&
(parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&
every.call(tr.childNodes, function (n) { return n.nodeName === 'TH' })
)
)
}
function isFirstTbody (element) {
var previousSibling = element.previousSibling
return (
element.nodeName === 'TBODY' && (
!previousSibling ||
(
previousSibling.nodeName === 'THEAD' &&
/^\s*$/i.test(previousSibling.textContent)
)
)
)
}
2018-05-24 12:31:40 +01:00
function cell (content, node = null, index = null) {
if (index === null) index = indexOf.call(node.parentNode.childNodes, node)
2017-11-10 14:21:46 +00:00
var prefix = ' '
if (index === 0) prefix = '| '
2018-05-24 12:31:40 +01:00
let filteredContent = content.trim().replace(/\n\r/g, '<br>').replace(/\n/g, "<br>");
filteredContent = filteredContent.replace(/\|+/g, '\\|')
2018-05-21 23:55:53 +01:00
while (filteredContent.length < 3) filteredContent += ' ';
2018-05-24 12:31:40 +01:00
if (node) filteredContent = handleColSpan(filteredContent, node, ' ');
2018-05-21 23:55:53 +01:00
return prefix + filteredContent + ' |'
2017-11-10 14:21:46 +00:00
}
function nodeContainsTable(node) {
if (!node.childNodes) return false;
for (let i = 0; i < node.childNodes.length; i++) {
const child = node.childNodes[i];
if (child.nodeName === 'TABLE') return true;
if (nodeContainsTable(child)) return true;
}
return false;
}
// Various conditions under which a table should be skipped - i.e. each cell
// will be rendered one after the other as if they were paragraphs.
function tableShouldBeSkipped(tableNode) {
if (!tableNode) return true;
if (!tableNode.rows) return true;
if (tableNode.rows.length <= 1 && tableNode.rows[0].childNodes.length <= 1) return true; // Table with only one cell
if (nodeContainsTable(tableNode)) return true;
return false;
}
function nodeParentTable(node) {
let parent = node.parentNode;
while (parent.nodeName !== 'TABLE') {
parent = parent.parentNode;
if (!parent) return null;
}
return parent;
}
2018-05-24 12:31:40 +01:00
function handleColSpan(content, node, emptyChar) {
const colspan = node.getAttribute('colspan') || 1;
for (let i = 1; i < colspan; i++) {
content += ' | ' + emptyChar.repeat(3);
}
return content
}
function tableColCount(node) {
let maxColCount = 0;
for (let i = 0; i < node.rows.length; i++) {
const row = node.rows[i]
const colCount = row.childNodes.length
if (colCount > maxColCount) maxColCount = colCount
}
return maxColCount
}
2017-11-10 14:21:46 +00:00
export default function tables (turndownService) {
turndownService.keep(function (node) {
return node.nodeName === 'TABLE'
})
2017-11-10 14:21:46 +00:00
for (var key in rules) turndownService.addRule(key, rules[key])
}