diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 9ae71214a..3a341f379 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -234,7 +234,188 @@ const definitionLists = { } }; -Markdown.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] }); +const spanTable = { + name : 'spanTable', + level : 'block', // Is this a block-level or inline-level tokenizer? + start(src) { return src.match(/^\n *([^\n ].*\|.*)\n/)?.index; }, // Hint to Marked.js to stop and check for a match + tokenizer(src, tokens) { + //const regex = this.tokenizer.rules.block.table; + const regex = new RegExp('^ *([^\\n ].*\\|.*\\n(?:.*?[^\\s].*\\n)*?)' // Header + + ' {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)\\|?' // Align + + '(?:\\n *((?:(?!\\n| {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})' // Cells + + '(?:\\n+|$)| {0,3}#{1,6} | {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|!--)).*(?:\\n|$))*)\\n*|$)'); // Cells + const cap = regex.exec(src); + + if(cap) { + const item = { + type : 'spanTable', + header : cap[1].replace(/\n$/, '').split('\n'), + align : cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), + rows : cap[3] ? cap[3].replace(/\n$/, '').split('\n') : [] + }; + + // 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); + } + + // Get main table cells + l = item.rows.length; + for (i = 0; i < l; i++) { + item.rows[i] = splitCells(item.rows[i], colCount); + } + + // 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.inlineTokens(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.inlineTokens(row[k].text, row[k].tokens); + } + } + return item; + } + } + }, + renderer(token) { + let i, j, row, cell, col, text; + let output = ``; + output += ``; + for (i = 0; i < token.header.length; i++) { + row = token.header[i]; + let col = 0; + output += ``; + for (j = 0; j < row.length; j++) { + cell = row[j]; + text = this.parseInline(cell.tokens); + output += getTableCell(text, cell.colspan, 'th', token.align[col]); + col += cell.colspan; + } + output += ``; + } + output += ``; + if(token.rows.length) { + output += ``; + for (i = 0; i < token.rows.length; i++) { + row = token.rows[i]; + col = 0; + output += ``; + for (j = 0; j < row.length; j++) { + cell = row[j]; + text = this.parseInline(cell.tokens); + output += getTableCell(text, cell.colspan, 'td', token.align[col]); + col += cell.colspan; + } + output += ``; + } + output += ``; + } + output += `
`; + return output; + } +}; + +const getTableCell = (text, colspan, type, align)=>{ + const tag = `<${type}` + + `${colspan > 1 ? ` colspan=${colspan}` : ''}` + + `${align ? ` align=${align}` : ''}>`; + return `${tag + text}\n`; +}; + +const splitCells = (tableRow, count)=>{ + // ensure that every cell-delimiting pipe has a space + // before it to distinguish it from an escaped pipe + const row = tableRow.replace(/(\|+)/g, (match, p1, offset, str)=>{ + let escaped = false, + curr = offset; + while (--curr >= 0 && str[curr] === '\\') escaped = !escaped; + if(escaped) { + // odd number of slashes means | is escaped + // so we leave it and the slashes alone + return p1; + } else { + // add space before unescaped | + return ` ${p1}`; + } + }); + + const cells = row.split(/(?: \||(?<=\|)\|)(?=[^\|]|$)/g); + let i = 0; + + // First/last cell in a row cannot be empty if it has no leading/trailing pipe + if(!cells[0].trim()) { cells.shift(); } + if(!cells[cells.length - 1].trim()) { cells.pop(); } + + let numCols = 0; + + for (; i < cells.length; i++) { + const trimmedCell = cells[i].split(/(? count) { + cells.splice(count); + } else { + while (numCols < count) { + cells.push({ + colspan : 1, + text : '' + }); + numCols += 1; + } + } + return cells; +}; + +Markdown.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists, spanTable] }); Markdown.use(mustacheInjectBlock); Markdown.use({ smartypants: true });