mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-26 18:27:38 +01:00
Use custom hovers in rendered markdown for link titles
We were already doing this for chat. I think it makes sense to be consistent
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from '../../nls.js';
|
||||
import { onUnexpectedError } from '../common/errors.js';
|
||||
import { escapeDoubleQuotes, IMarkdownString, MarkdownStringTrustedOptions, parseHrefAndDimensions, removeMarkdownEscapes } from '../common/htmlContent.js';
|
||||
import { markdownEscapeEscapedIcons } from '../common/iconLabels.js';
|
||||
@@ -12,7 +13,7 @@ import { Lazy } from '../common/lazy.js';
|
||||
import { DisposableStore, IDisposable } from '../common/lifecycle.js';
|
||||
import * as marked from '../common/marked/marked.js';
|
||||
import { parse } from '../common/marshalling.js';
|
||||
import { FileAccess, Schemas } from '../common/network.js';
|
||||
import { FileAccess, matchesScheme, Schemas } from '../common/network.js';
|
||||
import { cloneAndChange } from '../common/objects.js';
|
||||
import { dirname, resolvePath } from '../common/resources.js';
|
||||
import { escape } from '../common/strings.js';
|
||||
@@ -101,9 +102,21 @@ const defaultMarkedRenderers = Object.freeze({
|
||||
text = removeMarkdownEscapes(text);
|
||||
}
|
||||
|
||||
title = typeof title === 'string' ? escapeDoubleQuotes(removeMarkdownEscapes(title)) : '';
|
||||
title = typeof title === 'string' ? removeMarkdownEscapes(title) : '';
|
||||
href = removeMarkdownEscapes(href);
|
||||
|
||||
// Try adding a basic title for command uris if none exists
|
||||
if (!title) {
|
||||
try {
|
||||
const uri = URI.parse(href);
|
||||
if (matchesScheme(uri, Schemas.command)) {
|
||||
title = localize('markdown.commandLinkTitle', "Run command: '{0}'", uri.path);
|
||||
}
|
||||
} catch {
|
||||
// Noop
|
||||
}
|
||||
}
|
||||
|
||||
// HTML Encode href
|
||||
href = href.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
@@ -111,7 +124,7 @@ const defaultMarkedRenderers = Object.freeze({
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
return `<a href="${href}" title="${title || href}" draggable="false">${text}</a>`;
|
||||
return `<a href="${escapeDoubleQuotes(href)}" title="${escapeDoubleQuotes(title || href)}" draggable="false">${text}</a>`;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -285,7 +285,7 @@ suite('MarkdownRenderer', () => {
|
||||
});
|
||||
|
||||
const result: HTMLElement = store.add(renderMarkdown(md)).element;
|
||||
assert.strictEqual(result.innerHTML, `<p><a href="" title="command:doFoo" draggable="false" data-href="command:doFoo">command1</a> <a href="" data-href="command:doFoo">command2</a></p>`);
|
||||
assert.strictEqual(result.innerHTML, `<p><a href="" title="Run command: 'doFoo'" draggable="false" data-href="command:doFoo">command1</a> <a href="" data-href="command:doFoo">command2</a></p>`);
|
||||
});
|
||||
|
||||
test('Should remove relative links if there is no base url', () => {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { IRenderedMarkdown, MarkdownRenderOptions, renderMarkdown } from '../../../base/browser/markdownRenderer.js';
|
||||
import { onUnexpectedError } from '../../../base/common/errors.js';
|
||||
import { IMarkdownString, MarkdownStringTrustedOptions } from '../../../base/common/htmlContent.js';
|
||||
import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
|
||||
import { IHoverService } from '../../hover/browser/hover.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { IOpenerService } from '../../opener/common/opener.js';
|
||||
@@ -58,6 +60,7 @@ export class MarkdownRendererService implements IMarkdownRendererService {
|
||||
private _defaultCodeBlockRenderer: IMarkdownCodeBlockRenderer | undefined;
|
||||
|
||||
constructor(
|
||||
@IHoverService private readonly _hoverService: IHoverService,
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
) { }
|
||||
|
||||
@@ -78,12 +81,42 @@ export class MarkdownRendererService implements IMarkdownRendererService {
|
||||
|
||||
const rendered = renderMarkdown(markdown, resolvedOptions, outElement);
|
||||
rendered.element.classList.add('rendered-markdown');
|
||||
return rendered;
|
||||
const hoverDisposables = this.attachCustomHovers(rendered.element);
|
||||
return {
|
||||
element: rendered.element,
|
||||
dispose: () => {
|
||||
rendered.dispose();
|
||||
hoverDisposables.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
setDefaultCodeBlockRenderer(renderer: IMarkdownCodeBlockRenderer): void {
|
||||
this._defaultCodeBlockRenderer = renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the native title tooltips on links with custom hover tooltips
|
||||
*/
|
||||
private attachCustomHovers(element: HTMLElement): IDisposable {
|
||||
const store = new DisposableStore();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const a of element.querySelectorAll('a')) {
|
||||
if (a.title) {
|
||||
const title = a.title;
|
||||
a.title = '';
|
||||
store.add(this._hoverService.setupDelayedHover(a, {
|
||||
content: title,
|
||||
appearance: {
|
||||
compact: true
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return store;
|
||||
}
|
||||
}
|
||||
|
||||
export async function openLinkFromMarkdown(openerService: IOpenerService, link: string, isTrusted: boolean | MarkdownStringTrustedOptions | undefined, skipValidation?: boolean): Promise<boolean> {
|
||||
|
||||
@@ -5,14 +5,8 @@
|
||||
|
||||
import { $ } from '../../../../base/browser/dom.js';
|
||||
import { IRenderedMarkdown, MarkdownRenderOptions } from '../../../../base/browser/markdownRenderer.js';
|
||||
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
|
||||
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
|
||||
import { DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import { IMarkdownRenderer, IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { ILanguageService } from '../../../../editor/common/languages/language.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
|
||||
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
|
||||
import product from '../../../../platform/product/common/product.js';
|
||||
|
||||
export const allowedChatMarkdownHtmlTags = Object.freeze([
|
||||
@@ -62,10 +56,6 @@ export const allowedChatMarkdownHtmlTags = Object.freeze([
|
||||
*/
|
||||
export class ChatContentMarkdownRenderer implements IMarkdownRenderer {
|
||||
constructor(
|
||||
@ILanguageService languageService: ILanguageService,
|
||||
@IOpenerService openerService: IOpenerService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IHoverService private readonly hoverService: IHoverService,
|
||||
@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
|
||||
) { }
|
||||
|
||||
@@ -103,26 +93,6 @@ export class ChatContentMarkdownRenderer implements IMarkdownRenderer {
|
||||
child.replaceWith($('p', undefined, child.textContent));
|
||||
}
|
||||
}
|
||||
return this.attachCustomHover(result);
|
||||
}
|
||||
|
||||
private attachCustomHover(result: IRenderedMarkdown): IRenderedMarkdown {
|
||||
const store = new DisposableStore();
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
result.element.querySelectorAll('a').forEach((element) => {
|
||||
if (element.title) {
|
||||
const title = element.title;
|
||||
element.title = '';
|
||||
store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, title));
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
element: result.element,
|
||||
dispose: () => {
|
||||
result.dispose();
|
||||
store.dispose();
|
||||
}
|
||||
};
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user