From 77ff6eb03bcfb11d3bad26346f0b5ed593cb2a99 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 21 May 2021 14:42:50 -0700 Subject: [PATCH] Add image specific link normalizer (#124400) --- .../src/features/preview.ts | 6 +- .../src/features/previewContentProvider.ts | 2 +- .../src/markdownEngine.ts | 102 ++++++++++-------- .../src/util/resources.ts | 20 ---- 4 files changed, 63 insertions(+), 67 deletions(-) diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index 23ce0b495b0..9c678e4a6ec 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -11,7 +11,7 @@ import { Logger } from '../logger'; import { MarkdownContributionProvider } from '../markdownExtensions'; import { Disposable } from '../util/dispose'; import { isMarkdownFile } from '../util/file'; -import { normalizeResource, WebviewResourceProvider } from '../util/resources'; +import { WebviewResourceProvider } from '../util/resources'; import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor'; import { MarkdownPreviewConfigurationManager } from './previewConfig'; import { MarkdownContentProvider, MarkdownContentProviderOutput } from './previewContentProvider'; @@ -423,7 +423,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { baseRoots.push(vscode.Uri.file(path.dirname(this._resource.fsPath))); } - return baseRoots.map(root => normalizeResource(this._resource, root)); + return baseRoots; } @@ -456,7 +456,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { //#region WebviewResourceProvider asWebviewUri(resource: vscode.Uri) { - return this._webviewPanel.webview.asWebviewUri(normalizeResource(this._resource, resource)); + return this._webviewPanel.webview.asWebviewUri(resource); } get cspSource() { diff --git a/extensions/markdown-language-features/src/features/previewContentProvider.ts b/extensions/markdown-language-features/src/features/previewContentProvider.ts index 474b1864ed7..e2b28c092cb 100644 --- a/extensions/markdown-language-features/src/features/previewContentProvider.ts +++ b/extensions/markdown-language-features/src/features/previewContentProvider.ts @@ -81,7 +81,7 @@ export class MarkdownContentProvider { const nonce = new Date().getTime() + '' + new Date().getMilliseconds(); const csp = this.getCsp(resourceProvider, sourceUri, nonce); - const body = await this.engine.render(markdownDocument); + const body = await this.engine.render(markdownDocument, resourceProvider); const html = ` diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 7e62db9824b..346547f3da9 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { MarkdownIt, Token } from 'markdown-it'; -import * as path from 'path'; import * as vscode from 'vscode'; import { MarkdownContributionProvider as MarkdownContributionProvider } from './markdownExtensions'; import { Slugifier } from './slugify'; import { SkinnyTextDocument } from './tableOfContentsProvider'; import { hash } from './util/hash'; -import { isOfScheme, MarkdownFileExtensions, Schemes } from './util/links'; +import { isOfScheme, Schemes } from './util/links'; +import { WebviewResourceProvider } from './util/resources'; const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g; @@ -62,12 +62,13 @@ export interface RenderOutput { interface RenderEnv { containingImages: { src: string }[]; + currentDocument: vscode.Uri | undefined; + resourceProvider: WebviewResourceProvider | undefined; } export class MarkdownEngine { private md?: Promise; - private currentDocument?: vscode.Uri; private _slugCount = new Map(); private _tokenCache = new TokenCache(); @@ -113,7 +114,7 @@ export class MarkdownEngine { this.addLineNumberRenderer(md, renderName); } - this.addImageStabilizer(md); + this.addImageRenderer(md); this.addFencedRenderer(md); this.addLinkNormalizer(md); this.addLinkValidator(md); @@ -138,8 +139,6 @@ export class MarkdownEngine { return cached; } - this.currentDocument = document.uri; - const tokens = this.tokenizeString(document.getText(), engine); this._tokenCache.update(document, config, tokens); return tokens; @@ -151,7 +150,7 @@ export class MarkdownEngine { return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {}); } - public async render(input: SkinnyTextDocument | string): Promise { + public async render(input: SkinnyTextDocument | string, resourceProvider?: WebviewResourceProvider): Promise { const config = this.getConfig(typeof input === 'string' ? undefined : input.uri); const engine = await this.getEngine(config); @@ -160,7 +159,9 @@ export class MarkdownEngine { : this.tokenizeDocument(input, config, engine); const env: RenderEnv = { - containingImages: [] + containingImages: [], + currentDocument: typeof input === 'string' ? undefined : input.uri, + resourceProvider, }; const html = engine.renderer.render(tokens, { @@ -210,7 +211,7 @@ export class MarkdownEngine { }; } - private addImageStabilizer(md: MarkdownIt): void { + private addImageRenderer(md: MarkdownIt): void { const original = md.renderer.rules.image; md.renderer.rules.image = (tokens: Token[], idx: number, options: any, env: RenderEnv, self: any) => { const token = tokens[idx]; @@ -221,6 +222,11 @@ export class MarkdownEngine { env.containingImages?.push({ src }); const imgHash = hash(src); token.attrSet('id', `image-hash-${imgHash}`); + + if (!token.attrGet('data-src')) { + token.attrSet('src', this.toResourceUri(src, env.currentDocument, env.resourceProvider)); + token.attrSet('data-src', src); + } } if (original) { @@ -252,40 +258,6 @@ export class MarkdownEngine { return normalizeLink(vscode.Uri.parse(link).with({ scheme: vscode.env.uriScheme }).toString()); } - // Support file:// links - if (isOfScheme(Schemes.file, link)) { - // Ensure link is relative by prepending `/` so that it uses the element URI - // when resolving the absolute URL - return normalizeLink('/' + link.replace(/^file:/, 'file')); - } - - // If original link doesn't look like a url with a scheme, assume it must be a link to a file in workspace - if (!/^[a-z\-]+:/i.test(link)) { - // Use a fake scheme for parsing - let uri = vscode.Uri.parse('markdown-link:' + link); - - // Relative paths should be resolved correctly inside the preview but we need to - // handle absolute paths specially (for images) to resolve them relative to the workspace root - if (uri.path[0] === '/') { - const root = vscode.workspace.getWorkspaceFolder(this.currentDocument!); - if (root) { - uri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({ - scheme: 'markdown-link', - fragment: uri.fragment, - query: uri.query, - }); - } - } - - const extname = path.extname(uri.fsPath); - - if (uri.fragment && (extname === '' || MarkdownFileExtensions.includes(extname))) { - uri = uri.with({ - fragment: this.slugifier.fromHeading(uri.fragment).value - }); - } - return normalizeLink(uri.toString(true).replace(/^markdown-link:/, '')); - } } catch (e) { // noop } @@ -343,6 +315,50 @@ export class MarkdownEngine { return old_render(tokens, idx, options, env, self); }; } + + private toResourceUri(href: string, currentDocument: vscode.Uri | undefined, resourceProvider: WebviewResourceProvider | undefined): string { + try { + // Support file:// links + if (isOfScheme(Schemes.file, href)) { + const uri = vscode.Uri.parse(href); + if (resourceProvider) { + return resourceProvider.asWebviewUri(uri).toString(true); + } + // Not sure how to resolve this + return href; + } + + // If original link doesn't look like a url with a scheme, assume it must be a link to a file in workspace + if (!/^[a-z\-]+:/i.test(href)) { + // Use a fake scheme for parsing + let uri = vscode.Uri.parse('markdown-link:' + href); + + // Relative paths should be resolved correctly inside the preview but we need to + // handle absolute paths specially to resolve them relative to the workspace root + if (uri.path[0] === '/' && currentDocument) { + const root = vscode.workspace.getWorkspaceFolder(currentDocument); + if (root) { + uri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({ + fragment: uri.fragment, + query: uri.query, + }); + + if (resourceProvider) { + return resourceProvider.asWebviewUri(uri).toString(true); + } else { + uri = uri.with({ scheme: 'markdown-link' }); + } + } + } + + return uri.toString(true).replace(/^markdown-link:/, ''); + } + + return href; + } catch { + return href; + } + } } async function getMarkdownOptions(md: () => MarkdownIt) { diff --git a/extensions/markdown-language-features/src/util/resources.ts b/extensions/markdown-language-features/src/util/resources.ts index 063c410b39e..f1f2d0886ab 100644 --- a/extensions/markdown-language-features/src/util/resources.ts +++ b/extensions/markdown-language-features/src/util/resources.ts @@ -11,23 +11,3 @@ export interface WebviewResourceProvider { readonly cspSource: string; } -export function normalizeResource( - base: vscode.Uri, - resource: vscode.Uri -): vscode.Uri { - // If we have a windows path and are loading a workspace with an authority, - // make sure we use a unc path with an explicit localhost authority. - // - // Otherwise, the `` rule will insert the authority into the resolved resource - // URI incorrectly. - if (base.authority && !resource.authority) { - const driveMatch = resource.path.match(/^\/(\w):\//); - if (driveMatch) { - return vscode.Uri.file(`\\\\localhost\\${driveMatch[1]}$\\${resource.fsPath.replace(/^\w:\\/, '')}`).with({ - fragment: resource.fragment, - query: resource.query - }); - } - } - return resource; -}