diff --git a/package-lock.json b/package-lock.json index 7d6c370c9..62a60c433 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,6 @@ "requires": true, "packages": { "": { - "name": "homebrewery", "version": "3.0.5", "hasInstallScript": true, "license": "MIT", @@ -29,7 +28,8 @@ "jwt-simple": "^0.5.6", "less": "^3.13.1", "lodash": "^4.17.21", - "marked": "3.0.8", + "marked": "4.0.7", + "marked-extended-tables": "^1.0.2", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.29.1", "mongoose": "^6.1.1", @@ -6082,16 +6082,24 @@ } }, "node_modules/marked": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.8.tgz", - "integrity": "sha512-0gVrAjo5m0VZSJb4rpL59K1unJAMb/hm8HRXqasD8VeC8m91ytDPMritgFSlKonfdt+rRYYpP/JfLxgIX8yoSw==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.7.tgz", + "integrity": "sha512-mQrRvV2vRk7DHZsWsYJfAjmBo+lSYPhTJPThaaGpkEfmC+4oefeug2txZniQTieDS0CFpokfVhd7JuS5GtnHhA==", "bin": { - "marked": "bin/marked" + "marked": "bin/marked.js" }, "engines": { "node": ">= 12" } }, + "node_modules/marked-extended-tables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/marked-extended-tables/-/marked-extended-tables-1.0.2.tgz", + "integrity": "sha512-QRc8EgdWNrPXYYMa2tHtlKGrMUvJI9H3DUGTLrBpsevFntignPXPDs/2Aez4tw8pAvxbQ9K7yos6u3gAajPAkA==", + "peerDependencies": { + "marked": "^3.0.0 || ^4.0.0" + } + }, "node_modules/markedLegacy": { "name": "marked", "version": "0.3.19", @@ -14155,9 +14163,15 @@ } }, "marked": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/marked/-/marked-3.0.8.tgz", - "integrity": "sha512-0gVrAjo5m0VZSJb4rpL59K1unJAMb/hm8HRXqasD8VeC8m91ytDPMritgFSlKonfdt+rRYYpP/JfLxgIX8yoSw==" + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.7.tgz", + "integrity": "sha512-mQrRvV2vRk7DHZsWsYJfAjmBo+lSYPhTJPThaaGpkEfmC+4oefeug2txZniQTieDS0CFpokfVhd7JuS5GtnHhA==" + }, + "marked-extended-tables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/marked-extended-tables/-/marked-extended-tables-1.0.2.tgz", + "integrity": "sha512-QRc8EgdWNrPXYYMa2tHtlKGrMUvJI9H3DUGTLrBpsevFntignPXPDs/2Aez4tw8pAvxbQ9K7yos6u3gAajPAkA==", + "requires": {} }, "markedLegacy": { "version": "npm:marked@0.3.19", diff --git a/package.json b/package.json index 4e104bd16..8271d95cb 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,8 @@ "jwt-simple": "^0.5.6", "less": "^3.13.1", "lodash": "^4.17.21", - "marked": "3.0.8", + "marked": "4.0.7", + "marked-extended-tables": "^1.0.2", "markedLegacy": "npm:marked@^0.3.19", "moment": "^2.29.1", "mongoose": "^6.1.1", diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 5003d1053..607f7c428 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -1,7 +1,8 @@ /* eslint-disable max-lines */ const _ = require('lodash'); -const Markdown = require('marked'); -const renderer = new Markdown.Renderer(); +const Marked = require('marked'); +const MarkedExtendedTables = require('marked-extended-tables'); +const renderer = new Marked.Renderer(); //Processes the markdown within an HTML block if it's just a class-wrapper renderer.html = function (html) { @@ -9,7 +10,7 @@ renderer.html = function (html) { const openTag = html.substring(0, html.indexOf('>')+1); html = html.substring(html.indexOf('>')+1); html = html.substring(0, html.lastIndexOf('')); - return `${openTag} ${Markdown(html)} `; + return `${openTag} ${Marked.parse(html)} `; } return html; }; @@ -235,200 +236,10 @@ const 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, item.header[i-1]); - } - - // 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]); - } - - // 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.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.lexer.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.parser.parseInline(cell.tokens); - output += getTableCell(text, cell, '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.parser.parseInline(cell.tokens); - output += getTableCell(text, cell, 'td', token.align[col]); - col += cell.colspan; - } - output += ``; - } - output += ``; - } - output += `
`; - return output; - } -}; - -const getTableCell = (text, cell, type, align)=>{ - if(!cell.rowspan) { - return ''; - } - const tag = `<${type}` - + `${cell.colspan > 1 ? ` colspan=${cell.colspan}` : ''}` - + `${cell.rowspan > 1 ? ` rowspan=${cell.rowspan}` : ''}` - + `${align ? ` align=${align}` : ''}>`; - return `${tag + text}\n`; -}; - -const splitCells = (tableRow, count, prevRow = [])=>{ - const cells = [...tableRow.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; - } - - // Force main cell rows to match header column count - if(numCols > 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 }); +Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] }); +Marked.use(MarkedExtendedTables()); +Marked.use(mustacheInjectBlock); +Marked.use({ smartypants: true }); //Fix local links in the Preview iFrame to link inside the frame renderer.link = function (href, title, text) { @@ -532,11 +343,11 @@ const processStyleTags = (string)=>{ }; module.exports = { - marked : Markdown, + marked : Marked, render : (rawBrewText)=>{ rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n
\n`) .replace(/^(:+)$/gm, (match)=>`${`
`.repeat(match.length)}\n`); - return Markdown( + return Marked.parse( sanatizeScriptTags(rawBrewText), { renderer: renderer } );