diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts index fab47a1d1c..b26d7c8f2d 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/markdownToolbar.component.ts @@ -236,11 +236,14 @@ export class MarkdownToolbarComponent extends AngularDisposable { needsTransform = false; } else { let linkUrl = linkCalloutResult.insertUnescapedLinkUrl; - const isFile = URI.parse(linkUrl).scheme === 'file'; - if (isFile && !path.isAbsolute(linkUrl)) { - const notebookDirName = path.dirname(this.cellModel?.notebookModel?.notebookUri.fsPath); - const relativePath = (linkUrl).replace(/\\/g, path.posix.sep); - linkUrl = path.resolve(notebookDirName, relativePath); + const isAnchorLink = linkUrl.startsWith('#'); + if (!isAnchorLink) { + const isFile = URI.parse(linkUrl).scheme === 'file'; + if (isFile && !path.isAbsolute(linkUrl)) { + const notebookDirName = path.dirname(this.cellModel?.notebookModel?.notebookUri.fsPath); + const relativePath = (linkUrl).replace(/\\/g, path.posix.sep); + linkUrl = path.resolve(notebookDirName, relativePath); + } } // Otherwise, re-focus on the output element, and insert the link directly. this.output?.nativeElement?.focus(); diff --git a/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts b/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts index 9fbeaaef04..4f1a116335 100644 --- a/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts +++ b/src/sql/workbench/contrib/notebook/browser/htmlMarkdownConverter.ts @@ -132,15 +132,28 @@ export class HTMLMarkdownConverter { this.turndownService.addRule('a', { filter: 'a', replacement: (content, node) => { - //On Windows, if notebook is not trusted then the href attr is removed for all non-web URL links - // href contains either a hyperlink or a URI-encoded absolute path. (See resolveUrls method in notebookMarkdown.ts) - 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); - if (relativePath) { - return `[${node.innerText}](${relativePath})`; + let href = node.href; + let notebookLink: URI | undefined; + const isAnchorLinkInFile = (node.attributes.href?.nodeValue.startsWith('#') || href.includes('#')) && href.startsWith('file://'); + if (isAnchorLinkInFile) { + notebookLink = getUriAnchorLink(node, this.notebookUri); + } else { + //On Windows, if notebook is not trusted then the href attr is removed for all non-web URL links + // href contains either a hyperlink or a URI-encoded absolute path. (See resolveUrls method in notebookMarkdown.ts) + notebookLink = href ? URI.parse(href) : URI.file(node.title); } - return `[${content}](${node.href})`; + const notebookFolder = this.notebookUri ? path.join(path.dirname(this.notebookUri.fsPath), path.sep) : ''; + if (notebookLink.fsPath !== this.notebookUri.fsPath) { + let relativePath = findPathRelativeToContent(notebookFolder, notebookLink); + if (relativePath) { + return `[${node.innerText}](${relativePath})`; + } + } else if (notebookLink?.fragment) { + // if the anchor link is to a section in the same notebook then just add the fragment + return `[${content}](${notebookLink.fragment})`; + } + + return `[${content}](${href})`; } }); // Only nested list case differs from original turndown rule @@ -275,7 +288,7 @@ function blankReplacement(content, node) { export function findPathRelativeToContent(notebookFolder: string, contentPath: URI | undefined): string { if (notebookFolder) { if (contentPath?.scheme === 'file') { - let relativePath = path.relative(notebookFolder, contentPath.fsPath); + 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.startsWith(path.join('..', path.sep) || path.join('.', path.sep))) { @@ -295,3 +308,15 @@ export function addHighlightIfYellowBgExists(node, content: string): string { } return content; } + +export function getUriAnchorLink(node, notebookUri: URI): URI { + const sectionLinkToAnotherFile = node.href.includes('#') && !node.attributes.href?.nodeValue.startsWith('#'); + if (sectionLinkToAnotherFile) { + let absolutePath = !path.isAbsolute(node.attributes.href?.nodeValue) ? path.resolve(path.dirname(notebookUri.fsPath), node.attributes.href?.nodeValue) : node.attributes.href?.nodeValue; + // if section link is different from the current notebook + return URI.file(absolutePath); + } else { + // else build an uri using the current notebookUri + return URI.from({ scheme: 'file', path: notebookUri.path, fragment: node.attributes.href?.nodeValue }); + } +} diff --git a/src/sql/workbench/contrib/notebook/test/browser/htmlMarkdownConverter.test.ts b/src/sql/workbench/contrib/notebook/test/browser/htmlMarkdownConverter.test.ts index cc7f70f8c9..1a10cf0341 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/htmlMarkdownConverter.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/htmlMarkdownConverter.test.ts @@ -143,6 +143,12 @@ suite('HTML Markdown Converter', function (): void { assert.equal(htmlMarkdownConverter.convert(htmlString), '[msft](http://www.microsoft.com/images/msft.png)', 'Basic http link test failed'); htmlString = 'Test msft'; assert.equal(htmlMarkdownConverter.convert(htmlString), 'Test [msft](http://www.microsoft.com/images/msft.png)', 'Basic http link + text test failed'); + htmlString = 'hello'; + assert.equal(htmlMarkdownConverter.convert(htmlString), '[hello](#hello)', 'Basic link to a section failed'); + htmlString = 'hello'; + assert.equal(htmlMarkdownConverter.convert(htmlString), `[hello](.${path.sep}file.md#hello)`, 'Basic anchor link to a section failed'); + htmlString = 'hello'; + assert.equal(htmlMarkdownConverter.convert(htmlString), '[hello](http://www.microsoft.com/images/msft.png#Hello)', 'Http link containing # sign failed'); }); test('Should transform