diff --git a/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts b/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts index 63411d9bf8..2eba697968 100644 --- a/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts +++ b/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts @@ -39,6 +39,7 @@ export class HTMLMarkdownConverter { this.turndownService.addRule('span', { filter: 'span', replacement: function (content, node) { + let escapedText = escapeAngleBrackets(node.textContent); // There are certain properties that either don't have equivalents in markdown or whose transformations // don't have actions defined in WYSIWYG yet. To unblock users, leaving these elements alone (including their child elements) // Note: the initial list was generated from our TSG Jupyter Book @@ -74,7 +75,7 @@ export class HTMLMarkdownConverter { beginString = '' + beginString; endString += ''; } - return beginString + content + endString; + return beginString + escapedText + endString; } }); this.turndownService.addRule('img', { @@ -99,6 +100,7 @@ export class HTMLMarkdownConverter { const notebookLink = node.href ? URI.parse(node.href) : URI.file(node.title); const notebookFolder = this.notebookUri ? path.join(path.dirname(this.notebookUri.fsPath), path.sep) : ''; let relativePath = findPathRelativeToContent(notebookFolder, notebookLink); + node.innerText = escapeAngleBrackets(node.innerText); if (relativePath) { return `[${node.innerText}](${relativePath})`; } @@ -112,6 +114,7 @@ export class HTMLMarkdownConverter { .replace(/^\n+/, '') // remove leading newlines .replace(/\n+$/, '\n') // replace trailing newlines with just a single one .replace(/\n/gm, '\n '); // indent + content = escapeAngleBrackets(content); let prefix = options.bulletListMarker + ' '; let parent = node.parentNode; let nestedCount = 0; @@ -131,6 +134,72 @@ export class HTMLMarkdownConverter { ); } }); + this.turndownService.addRule('p', { + filter: 'p', + replacement: function (content, node) { + node.childNodes.forEach(c => { + if (c.nodeType === Node.TEXT_NODE) { + c.nodeValue = escapeAngleBrackets(c.textContent); + } else if (c.nodeType === Node.ELEMENT_NODE) { + c.innerText = escapeAngleBrackets(c.textContent); + } + }); + return '\n\n' + node.innerHTML.replace(/</gi, '<').replace(/>/gi, '>').replace(/ /gi, '') + '\n\n'; + } + }); + this.turndownService.addRule('heading', { + filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + replacement: function (content, node, options) { + let hLevel = Number(node.nodeName.charAt(1)); + let escapedText = escapeAngleBrackets(content); + if (options.headingStyle === 'setext' && hLevel < 3) { + let underline = '#'.repeat(hLevel); + return '\n\n' + escapedText + '\n' + underline + '\n\n'; + } else { + return '\n\n' + '#'.repeat(hLevel) + ' ' + escapedText + '\n\n'; + } + } + }); + this.turndownService.addRule('bold', { + filter: ['strong', 'b'], + replacement: function (content, node, options) { + content = escapeAngleBrackets(content); + if (!content.trim()) { return ''; } + return options.strongDelimiter + content + options.strongDelimiter; + } + }); + this.turndownService.addRule('italicize', { + filter: ['em', 'i'], + replacement: function (content, node, options) { + content = escapeAngleBrackets(content); + if (!content.trim()) { return ''; } + return options.emDelimiter + content + options.emDelimiter; + } + }); + this.turndownService.addRule('code', { + filter: function (node) { + let hasSiblings = node.previousSibling || node.nextSibling; + let isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings; + + return node.nodeName === 'CODE' && !isCodeBlock; + }, + replacement: function (content) { + content = escapeAngleBrackets(content); + if (!content.trim()) { return ''; } + + let delimiter = '`'; + let leadingSpace = ''; + let trailingSpace = ''; + let matches = content.match(/`+/gm); + if (matches) { + if (/^`/.test(content)) { leadingSpace = ' '; } + if (/`$/.test(content)) { trailingSpace = ' '; } + while (matches.indexOf(delimiter) !== -1) { delimiter = delimiter + '`'; } + } + + return delimiter + leadingSpace + content + trailingSpace + delimiter; + } + }); } } @@ -150,3 +219,16 @@ export function findPathRelativeToContent(notebookFolder: string, contentPath: U } return ''; } + +export function escapeAngleBrackets(textContent: string): string { + let text: string = textContent; + if (text.includes('') || text.includes('') || (text.includes('style') && !text.includes('