diff --git a/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts b/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts index 35530f0ae1..83916d8dad 100644 --- a/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts +++ b/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts @@ -7,6 +7,7 @@ import TurndownService = require('turndown'); import { URI } from 'vs/base/common/uri'; import * as path from 'vs/base/common/path'; import * as turndownPluginGfm from 'sql/workbench/contrib/notebook/browser/turndownPluginGfm'; +import { replaceInvalidLinkPath } from 'sql/workbench/contrib/notebook/common/utils'; // These replacements apply only to text. Here's how it's handled from Turndown: // if (node.nodeType === 3) { @@ -29,7 +30,6 @@ const markdownReplacements = [ [/ is escaped [/>/g, '\\>'], // Added to ensure sample text like is escaped ]; - export class HTMLMarkdownConverter { private turndownService: TurndownService; @@ -310,6 +310,8 @@ export function findPathRelativeToContent(notebookFolder: string, contentPath: U let relativePath = contentPath.fragment ? path.relative(notebookFolder, contentPath.fsPath).concat('#', contentPath.fragment) : path.relative(notebookFolder, contentPath.fsPath); //if path contains whitespaces then it's not identified as a link relativePath = relativePath.replace(/\s/g, '%20'); + // if relativePath contains improper directory format due to marked js parsing returning an invalid path (ex. ....\) then we need to replace it to ensure the directories are formatted properly (ex. ..\..\) + relativePath = replaceInvalidLinkPath(relativePath); if (relativePath.startsWith(path.join('..', path.sep) || path.join('.', path.sep))) { return relativePath; } else { diff --git a/src/sql/workbench/contrib/notebook/browser/outputs/notebookMarkdown.ts b/src/sql/workbench/contrib/notebook/browser/outputs/notebookMarkdown.ts index a3fba2dda6..75598f4f1b 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/notebookMarkdown.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/notebookMarkdown.ts @@ -13,6 +13,7 @@ import { defaultGenerator } from 'vs/base/common/idGenerator'; import { revive } from 'vs/base/common/marshalling'; import { ImageMimeTypes } from 'sql/workbench/services/notebook/common/contracts'; import { IMarkdownStringWithCellAttachments, MarkdownRenderOptionsWithCellAttachments } from 'sql/workbench/contrib/notebook/browser/cellViews/interfaces'; +import { replaceInvalidLinkPath } from 'sql/workbench/contrib/notebook/common/utils'; // Based off of HtmlContentRenderer export class NotebookMarkdownRenderer { @@ -240,6 +241,10 @@ export class NotebookMarkdownRenderer { } else if (href.charAt(0) === '/') { return base.replace(/(:\/*[^/]*)[\s\S]*/, '$1') + href; } else if (href.slice(0, 2) === '..') { + // we need to format invalid href formats (ex. ....\file to ..\..\file) + // in order to resolve to an absolute link + // Issue tracked here: https://github.com/markedjs/marked/issues/2135 + href = replaceInvalidLinkPath(href); return path.join(base, href); } else { return base + href; diff --git a/src/sql/workbench/contrib/notebook/common/utils.ts b/src/sql/workbench/contrib/notebook/common/utils.ts new file mode 100644 index 0000000000..3a04880f75 --- /dev/null +++ b/src/sql/workbench/contrib/notebook/common/utils.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isWindows } from 'vs/base/common/platform'; + +/** + * Due to marked js parsing returning an invalid path (ex. ....\) we must format the path to ensure directories are formatted properly (ex. ..\..\). + * Issue tracked here: https://github.com/markedjs/marked/issues/2135 + * The function only formats the path for Windows platform (in which the invalid form occurs) and checks to see if the path is invalid based on the leading periods. + * We use the first slash of path and create a relative path format (..\) string based on amount of leading periods + * and then concatenate the relative path format string to rest of path after slash + * @param href is the relative path + * @returns properly formatted relative path + */ +export function replaceInvalidLinkPath(href: string): string { + if (isWindows && href.startsWith('...')) { + let slashIndex = href.indexOf('\\'); + href = '..\\'.repeat(slashIndex / 2) + href.substring(slashIndex + 1); + return href; + } else { + // returns original path since it is valid + return href; + } +} diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookMarkdown.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookMarkdown.test.ts index 51bd875fc7..ad92a0f017 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookMarkdown.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookMarkdown.test.ts @@ -53,15 +53,22 @@ suite('NotebookMarkdownRenderer', () => { }); test('link from relative file path', () => { - notebookMarkdownRenderer.setNotebookURI(URI.parse('maddy/temp/file1.txt')); + notebookMarkdownRenderer.setNotebookURI(URI.parse(`foo/temp/file1.txt`)); let result: HTMLElement = notebookMarkdownRenderer.renderMarkdown({ value: `[Link to relative path](../test/.build/someimageurl)`, isTrusted: true }); if (process.platform === 'win32') { - assert.strictEqual(result.innerHTML, `

Link to relative path

`); + assert.strictEqual(result.innerHTML, `

Link to relative path

`); } else { - assert.strictEqual(result.innerHTML, `

Link to relative path

`); + assert.strictEqual(result.innerHTML, `

Link to relative path

`); } }); + // marked js test that alters the relative path requiring regex replace to resolve path properly + // Issue tracked here: https://github.com/markedjs/marked/issues/2135 + test('marked js compiles relative link incorrectly', () => { + const markedPath = marked.parse('..\\..\\test.ipynb'); + assert.strict(markedPath, '

....\test.ipynb

'); + }); + test('cell attachment image', () => { let result: HTMLElement = notebookMarkdownRenderer.renderMarkdown({ value: `![altText](attachment:ads.png)`, isTrusted: true }, { cellAttachments: JSON.parse('{"ads.png":{"image/png":"iVBORw0KGgoAAAANSUhEUgAAAggg=="}}') }); assert.strictEqual(result.innerHTML, `

altText

`, 'Cell attachment basic test failed when trusted');