mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 05:19:39 +00:00
Rewrite how we handle links in the md preview
Try to simplify how we resolve links: - Move most logic out of the preview itself. - Simplify the amount of rewriting we do in the markdown engine
This commit is contained in:
parent
7f5a4a3f5b
commit
36aa903d5a
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -314,7 +314,7 @@
|
|||
"watch": "npm run build-preview && gulp watch-extension:markdown-language-features",
|
||||
"vscode:prepublish": "npm run build-ext && npm run build-preview",
|
||||
"build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:markdown-language-features ./tsconfig.json",
|
||||
"build-preview": "webpack --mode development"
|
||||
"build-preview": "webpack --mode production"
|
||||
},
|
||||
"dependencies": {
|
||||
"highlight.js": "9.15.8",
|
||||
|
|
|
@ -19,7 +19,7 @@ const settings = getSettings();
|
|||
const vscode = acquireVsCodeApi();
|
||||
|
||||
// Set VS Code state
|
||||
let state = getData<{ line: number, fragment: string }>('data-state');
|
||||
let state = getData<{ line: number; fragment: string; }>('data-state');
|
||||
vscode.setState(state);
|
||||
|
||||
const messaging = createPosterForVsCode(vscode);
|
||||
|
@ -67,7 +67,7 @@ const onUpdateView = (() => {
|
|||
})();
|
||||
|
||||
let updateImageSizes = throttle(() => {
|
||||
const imageInfo: { id: string, height: number, width: number }[] = [];
|
||||
const imageInfo: { id: string, height: number, width: number; }[] = [];
|
||||
let images = document.getElementsByTagName('img');
|
||||
if (images) {
|
||||
let i;
|
||||
|
@ -129,6 +129,8 @@ document.addEventListener('dblclick', event => {
|
|||
}
|
||||
});
|
||||
|
||||
const passThroughLinkSchemes = ['http:', 'https:', 'mailto:', 'vscode:', 'vscode-insiders'];
|
||||
|
||||
document.addEventListener('click', event => {
|
||||
if (!event) {
|
||||
return;
|
||||
|
@ -138,20 +140,25 @@ document.addEventListener('click', event => {
|
|||
while (node) {
|
||||
if (node.tagName && node.tagName === 'A' && node.href) {
|
||||
if (node.getAttribute('href').startsWith('#')) {
|
||||
break;
|
||||
return;
|
||||
}
|
||||
if (node.href.startsWith('file://') || node.href.startsWith('vscode-resource:') || node.href.startsWith(settings.webviewResourceRoot)) {
|
||||
const [path, fragment] = node.href
|
||||
.replace(/^file:\/\//i, '')
|
||||
.replace(/^vscode-resource:\/\/[^\/]+\//i, '')
|
||||
.replace(new RegExp(`^${escapeRegExp(settings.webviewResourceRoot)}`))
|
||||
.split('#');
|
||||
messaging.postMessage('clickLink', { path, fragment });
|
||||
|
||||
// Pass through known schemes
|
||||
if (passThroughLinkSchemes.some(scheme => node.href.startsWith(scheme))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hrefText = node.getAttribute('data-href') || node.getAttribute('href');
|
||||
|
||||
// If original link doesn't look like a url, delegate back to VS Code to resolve
|
||||
if (!/^[a-z\-]+:/i.test(hrefText)) {
|
||||
messaging.postMessage('openLink', { href: hrefText });
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
return;
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
@ -170,6 +177,3 @@ window.addEventListener('scroll', throttle(() => {
|
|||
}
|
||||
}, 50));
|
||||
|
||||
function escapeRegExp(text: string) {
|
||||
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ interface WebviewMessage {
|
|||
|
||||
interface CacheImageSizesMessage extends WebviewMessage {
|
||||
readonly type: 'cacheImageSizes';
|
||||
readonly body: { id: string, width: number, height: number }[];
|
||||
readonly body: { id: string, width: number, height: number; }[];
|
||||
}
|
||||
|
||||
interface RevealLineMessage extends WebviewMessage {
|
||||
|
@ -43,10 +43,9 @@ interface DidClickMessage extends WebviewMessage {
|
|||
}
|
||||
|
||||
interface ClickLinkMessage extends WebviewMessage {
|
||||
readonly type: 'clickLink';
|
||||
readonly type: 'openLink';
|
||||
readonly body: {
|
||||
readonly path: string;
|
||||
readonly fragment?: string;
|
||||
readonly href: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -88,7 +87,7 @@ export class MarkdownPreview extends Disposable {
|
|||
private forceUpdate = false;
|
||||
private isScrolling = false;
|
||||
private _disposed: boolean = false;
|
||||
private imageInfo: { id: string, width: number, height: number }[] = [];
|
||||
private imageInfo: { id: string, width: number, height: number; }[] = [];
|
||||
private scrollToFragment: string | undefined;
|
||||
|
||||
public static async revive(
|
||||
|
@ -202,8 +201,8 @@ export class MarkdownPreview extends Disposable {
|
|||
this.onDidClickPreview(e.body.line);
|
||||
break;
|
||||
|
||||
case 'clickLink':
|
||||
this.onDidClickPreviewLink(e.body.path, e.body.fragment);
|
||||
case 'openLink':
|
||||
this.onDidClickPreviewLink(e.body.href);
|
||||
break;
|
||||
|
||||
case 'showPreviewSecuritySelector':
|
||||
|
@ -536,12 +535,19 @@ export class MarkdownPreview extends Disposable {
|
|||
this.editor.webview.html = html;
|
||||
}
|
||||
|
||||
private async onDidClickPreviewLink(path: string, fragment: string | undefined) {
|
||||
this.scrollToFragment = undefined;
|
||||
private async onDidClickPreviewLink(href: string) {
|
||||
let [hrefPath, fragment] = href.split('#');
|
||||
|
||||
// We perviously already resolve absolute paths.
|
||||
// Now make sure we handle relative file paths
|
||||
if (hrefPath[0] !== '/') {
|
||||
hrefPath = path.join(path.dirname(this.resource.path), hrefPath);
|
||||
}
|
||||
|
||||
const config = vscode.workspace.getConfiguration('markdown', this.resource);
|
||||
const openLinks = config.get<string>('preview.openMarkdownLinks', 'inPreview');
|
||||
if (openLinks === 'inPreview') {
|
||||
const markdownLink = await resolveLinkToMarkdownFile(path);
|
||||
const markdownLink = await resolveLinkToMarkdownFile(hrefPath);
|
||||
if (markdownLink) {
|
||||
if (fragment) {
|
||||
this.scrollToFragment = fragment;
|
||||
|
@ -551,10 +557,10 @@ export class MarkdownPreview extends Disposable {
|
|||
}
|
||||
}
|
||||
|
||||
vscode.commands.executeCommand('_markdown.openDocumentLink', { path, fragment, fromResource: this.resource });
|
||||
vscode.commands.executeCommand('_markdown.openDocumentLink', { path: hrefPath, fragment, fromResource: this.resource });
|
||||
}
|
||||
|
||||
private async onCacheImageSizes(imageInfo: { id: string, width: number, height: number }[]) {
|
||||
private async onCacheImageSizes(imageInfo: { id: string, width: number, height: number; }[]) {
|
||||
this.imageInfo = imageInfo;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,13 +4,13 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import { MarkdownIt, Token } from 'markdown-it';
|
||||
import * as path from 'path';
|
||||
import { MarkdownIt, Token } from 'markdown-it';
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownContributionProvider as MarkdownContributionProvider } from './markdownExtensions';
|
||||
import { Slugifier } from './slugify';
|
||||
import { SkinnyTextDocument } from './tableOfContentsProvider';
|
||||
import { getUriForLinkWithKnownExternalScheme } from './util/links';
|
||||
import { Schemes, isOfScheme } from './util/links';
|
||||
|
||||
const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g;
|
||||
|
||||
|
@ -105,10 +105,10 @@ export class MarkdownEngine {
|
|||
|
||||
this.addImageStabilizer(md);
|
||||
this.addFencedRenderer(md);
|
||||
|
||||
this.addLinkNormalizer(md);
|
||||
this.addLinkValidator(md);
|
||||
this.addNamedHeaders(md);
|
||||
this.addLinkRenderer(md);
|
||||
return md;
|
||||
});
|
||||
}
|
||||
|
@ -143,6 +143,7 @@ export class MarkdownEngine {
|
|||
public async render(input: SkinnyTextDocument | string): Promise<string> {
|
||||
const config = this.getConfig(typeof input === 'string' ? undefined : input.uri);
|
||||
const engine = await this.getEngine(config);
|
||||
|
||||
const tokens = typeof input === 'string'
|
||||
? this.tokenizeString(input, engine)
|
||||
: this.tokenizeDocument(input, config, engine);
|
||||
|
@ -226,36 +227,28 @@ export class MarkdownEngine {
|
|||
const normalizeLink = md.normalizeLink;
|
||||
md.normalizeLink = (link: string) => {
|
||||
try {
|
||||
const externalSchemeUri = getUriForLinkWithKnownExternalScheme(link);
|
||||
if (externalSchemeUri) {
|
||||
// set true to skip encoding
|
||||
return normalizeLink(externalSchemeUri.toString(true));
|
||||
}
|
||||
// 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);
|
||||
|
||||
// Assume it must be an relative or absolute file path
|
||||
// Use a fake scheme to avoid parse warnings
|
||||
let uri = vscode.Uri.parse(`vscode-resource:${link}`);
|
||||
|
||||
if (uri.path) {
|
||||
// Assume it must be a file
|
||||
const fragment = uri.fragment;
|
||||
// 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.file(path.join(root.uri.fsPath, uri.path));
|
||||
uri = uri.with({
|
||||
path: path.join(root.uri.fsPath, uri.path),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
uri = vscode.Uri.file(path.join(path.dirname(this.currentDocument!.path), uri.path));
|
||||
}
|
||||
|
||||
if (fragment) {
|
||||
if (uri.fragment) {
|
||||
uri = uri.with({
|
||||
fragment: this.slugifier.fromHeading(fragment).value
|
||||
fragment: this.slugifier.fromHeading(uri.fragment).value
|
||||
});
|
||||
}
|
||||
return normalizeLink(uri.with({ scheme: 'vscode-resource' }).toString(true));
|
||||
} else if (!uri.path && uri.fragment) {
|
||||
return `#${this.slugifier.fromHeading(uri.fragment).value}`;
|
||||
return normalizeLink(uri.toString(true).replace(/^markdown-link:/, ''));
|
||||
}
|
||||
} catch (e) {
|
||||
// noop
|
||||
|
@ -268,7 +261,7 @@ export class MarkdownEngine {
|
|||
const validateLink = md.validateLink;
|
||||
md.validateLink = (link: string) => {
|
||||
// support file:// links
|
||||
return validateLink(link) || link.startsWith('file:') || /^data:image\/.*?;/.test(link);
|
||||
return validateLink(link) || isOfScheme(Schemes.file, link) || /^data:image\/.*?;/.test(link);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -296,6 +289,22 @@ export class MarkdownEngine {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
private addLinkRenderer(md: any): void {
|
||||
const old_render = md.renderer.rules.link_open || ((tokens: any, idx: number, options: any, _env: any, self: any) => {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
});
|
||||
|
||||
md.renderer.rules.link_open = (tokens: any, idx: number, options: any, env: any, self: any) => {
|
||||
const token = tokens[idx];
|
||||
const hrefIndex = token.attrIndex('href');
|
||||
if (hrefIndex >= 0) {
|
||||
const href = token.attrs[hrefIndex][1];
|
||||
token.attrPush(['data-href', href]);
|
||||
}
|
||||
return old_render(tokens, idx, options, env, self);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function getMarkdownOptions(md: () => MarkdownIt) {
|
||||
|
|
|
@ -5,14 +5,30 @@
|
|||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
const knownSchemes = ['http:', 'https:', 'file:', 'mailto:', 'data:', `${vscode.env.uriScheme}:`, 'vscode:', 'vscode-insiders:', 'vscode-resource:'];
|
||||
export const Schemes = {
|
||||
http: 'http:',
|
||||
https: 'https:',
|
||||
file: 'file:',
|
||||
mailto: 'mailto:',
|
||||
data: 'data:',
|
||||
vscode: 'vscode:',
|
||||
'vscode-insiders': 'vscode-insiders:',
|
||||
'vscode-resource': 'vscode-resource',
|
||||
} as const;
|
||||
|
||||
export function getUriForLinkWithKnownExternalScheme(
|
||||
link: string,
|
||||
): vscode.Uri | undefined {
|
||||
if (knownSchemes.some(knownScheme => link.toLowerCase().startsWith(knownScheme))) {
|
||||
const knownSchemes = [
|
||||
...Object.values(Schemes),
|
||||
`${vscode.env.uriScheme}:`
|
||||
] as const;
|
||||
|
||||
export function getUriForLinkWithKnownExternalScheme(link: string): vscode.Uri | undefined {
|
||||
if (knownSchemes.some(knownScheme => isOfScheme(knownScheme, link))) {
|
||||
return vscode.Uri.parse(link);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isOfScheme(scheme: string, link: string): boolean {
|
||||
return link.toLowerCase().startsWith(scheme);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue