src/vs/base/common/htmlContent.ts

255 LOC · 199 covered · 56 uncovered · 47 ranges · 3818 concepts · 18 introducers · 1859 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- htmlContent.ts ×20
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { illegalArgument } from './errors.js';
7 > import { escapeIcons } from './iconLabels.js';
8 > import { Schemas } from './network.js';
9 > import { isEqual } from './resources.js';
10 > import { escapeRegExpCharacters } from './strings.js';
11 > import { URI, UriComponents } from './uri.js';
12 >
13 > export interface MarkdownStringTrustedOptions {
14 > readonly enabledCommands: readonly string[];
15 > }
16 >
17 > export interface IMarkdownString {
18 > readonly value: string;
19 > readonly isTrusted?: boolean | MarkdownStringTrustedOptions;
20 > readonly supportThemeIcons?: boolean;
21 > readonly supportHtml?: boolean;
22 > /** @internal */
23 > readonly supportAlertSyntax?: boolean;
24 > readonly baseUri?: UriComponents;
25 > uris?: { [href: string]: UriComponents };
26 > }
27 >
28 > export const enum MarkdownStringTextNewlineStyle {
29 > Paragraph = 0,
30 > Break = 1,
31 > }
32 >
33 > export class MarkdownString implements IMarkdownString {
34 >
35 > public value: string;
36 > public isTrusted?: boolean | MarkdownStringTrustedOptions;
37 > public supportThemeIcons?: boolean;
38 > public supportHtml?: boolean;
39 > public supportAlertSyntax?: boolean;
40 > public baseUri?: URI;
41 > public uris?: { [href: string]: UriComponents } | undefined;
42 >
43 > public static lift(dto: IMarkdownString): MarkdownString {
44 > const markdownString = new MarkdownString(dto.value, dto); htmlContent.ts ×1
45 > markdownString.uris = dto.uris;
46 > markdownString.baseUri = dto.baseUri ? URI.revive(dto.baseUri) : undefined;
47 > return markdownString;
48 > }
50 > constructor(
51 > value: string = '', htmlContent.ts ×3
52 > isTrustedOrOptions: boolean | { isTrusted?: boolean | MarkdownStringTrustedOptions; supportThemeIcons?: boolean; supportHtml?: boolean; supportAlertSyntax?: boolean } = false,
53 > ) {
54 > this.value = value;
55 > if (typeof this.value !== 'string') {
56 throw illegalArgument('value');
57 }
59 > if (typeof isTrustedOrOptions === 'boolean') {
60 > this.isTrusted = isTrustedOrOptions; htmlContent.ts ×1
61 > this.supportThemeIcons = false;
62 > this.supportHtml = false;
63 > this.supportAlertSyntax = false;
64 > }
65 > else { htmlContent.ts ×1
66 > this.isTrusted = isTrustedOrOptions.isTrusted ?? undefined;
67 > this.supportThemeIcons = isTrustedOrOptions.supportThemeIcons ?? false;
68 > this.supportHtml = isTrustedOrOptions.supportHtml ?? false;
69 > this.supportAlertSyntax = isTrustedOrOptions.supportAlertSyntax ?? false;
70 > }
73 > appendText(value: string, newlineStyle: MarkdownStringTextNewlineStyle = MarkdownStringTextNewlineStyle.Paragraph): MarkdownString {
74 > this.value += escapeMarkdownSyntaxTokens(this.supportThemeIcons ? escapeIcons(value) : value) // CodeQL [SM02383] The Markdown is fully sanitized after being rendered. htmlContent.ts ×1
75 > .replace(/([ \t]+)/g, (_match, g1) => ' '.repeat(g1.length)) // CodeQL [SM02383] The Markdown is fully sanitized after being rendered.
76 > .replace(/\>/gm, '\\>') // CodeQL [SM02383] The Markdown is fully sanitized after being rendered.
77 > .replace(/\n/g, newlineStyle === MarkdownStringTextNewlineStyle.Break ? '\\\n' : '\n\n'); // CodeQL [SM02383] The Markdown is fully sanitized after being rendered.
78 >
79 > return this;
80 > }
82 > appendMarkdown(value: string): MarkdownString {
83 > this.value += value; htmlContent.ts ×1
84 > return this;
85 > }
87 > appendCodeblock(langId: string, code: string): MarkdownString {
88 > this.value += `\n${appendEscapedMarkdownCodeBlockFence(code, langId)}\n`; htmlContent.ts ×3
89 > return this;
90 > }
92 > appendLink(target: URI | string, label: string, title?: string): MarkdownString {
93 > this.value += '['; htmlContent.ts ×4
94 > this.value += this._escape(label, ']');
95 > this.value += '](';
96 > this.value += this._escape(String(target), ')');
97 > if (title) {
98 > this.value += ` "${this._escape(this._escape(title, '"'), ')')}"`; htmlContent.ts ×2
99 > }
100 > this.value += ')'; htmlContent.ts ×4
101 > return this;
102 > }
104 > private _escape(value: string, ch: string): string {
105 > const r = new RegExp(escapeRegExpCharacters(ch), 'g'); htmlContent.ts ×4
106 > return value.replace(r, (match, offset) => {
107 > if (value.charAt(offset - 1) !== '\\') { htmlContent.ts ×2
108 > return `\\${match}`;
109 > } else {
110 > return match;
111 > }
112 > }); htmlContent.ts ×4
113 > }
115 >
116 > export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownString[] | null | undefined): boolean {
117 if (isMarkdownString(oneOrMany)) {
118 return !oneOrMany.value;
119 } else if (Array.isArray(oneOrMany)) {
120 return oneOrMany.every(isEmptyMarkdownString);
121 } else {
122 return true;
123 }
124 }
126 > export function isMarkdownString(thing: unknown): thing is IMarkdownString {
127 > if (thing instanceof MarkdownString) { htmlContent.ts ×3
128 return true;
129 > } else if (thing && typeof thing === 'object') { htmlContent.ts ×3
130 > return typeof (<IMarkdownString>thing).value === 'string'
131 > && (typeof (<IMarkdownString>thing).isTrusted === 'boolean' || typeof (<IMarkdownString>thing).isTrusted === 'object' || (<IMarkdownString>thing).isTrusted === undefined) htmlContent.ts ×1
132 > && (typeof (<IMarkdownString>thing).supportThemeIcons === 'boolean' || (<IMarkdownString>thing).supportThemeIcons === undefined)
133 > && (typeof (<IMarkdownString>thing).supportAlertSyntax === 'boolean' || (<IMarkdownString>thing).supportAlertSyntax === undefined);
135 > return false; htmlContent.ts ×1
136 > }
138 > export function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean {
139 if (a === b) {
140 return true;
141 } else if (!a || !b) {
142 return false;
143 } else {
144 return a.value === b.value
145 && a.isTrusted === b.isTrusted
146 && a.supportThemeIcons === b.supportThemeIcons
147 && a.supportHtml === b.supportHtml
148 && a.supportAlertSyntax === b.supportAlertSyntax
149 && (a.baseUri === b.baseUri || !!a.baseUri && !!b.baseUri && isEqual(URI.from(a.baseUri), URI.from(b.baseUri)));
150 }
151 }
153 > export function escapeMarkdownSyntaxTokens(text: string): string {
154 > // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash htmlContent.ts ×1
155 > return text
156 > .replace(/[\\`*_{}[\]()#+!~]/g, '\\$&') // CodeQL [SM02383] Backslash is escaped in the character class
157 > .replace(/^([ \t]*)-/gm, '$1\\-'); // CodeQL [SM02383] Backslash is escaped in the character class
158 > }
160 > /**
161 > * Escapes only the characters that would break out of markdown link text
162 > * (`[label](url)`) syntax: `\` and `]`. Use this when the escaped string is
163 > * displayed as the visible label of a link, since renderers that extract the
164 > * link text without re-parsing markdown (e.g. the chat inline anchor / skill
165 > * pill) would otherwise show full `escapeMarkdownSyntaxTokens` backslashes
166 > * (`\-`, `\.`, ...) verbatim.
167 > */
168 > export function escapeMarkdownLinkLabel(text: string): string {
169 > return text.replace(/[\\\]]/g, '\\$&'); htmlContent.ts ×1
170 > }
172 > /**
173 > * @see https://github.com/microsoft/vscode/issues/193746
174 > */
175 > export function appendEscapedMarkdownCodeBlockFence(code: string, langId: string) {
176 > const longestFenceLength = htmlContent.ts ×3
177 > code.match(/^`+/gm)?.reduce((a, b) => (a.length > b.length ? a : b)).length ??
179 > const desiredFenceLength = htmlContent.ts ×3
180 > longestFenceLength >= 3 ? longestFenceLength + 1 : 3;
181 >
182 > // the markdown result
183 > return [
184 > `${'`'.repeat(desiredFenceLength)}${langId}`,
185 > code,
186 > `${'`'.repeat(desiredFenceLength)}`,
187 > ].join('\n');
188 > }
190 > /**
191 > * Wraps arbitrary text in a markdown inline code span using a backtick fence
192 > * long enough to safely contain any backtick sequences present in the text.
193 > *
194 > * Backticks inside an inline code span cannot be backslash-escaped per the
195 > * CommonMark spec — the only safe way is to choose a delimiter run longer
196 > * than any run of backticks in the content (and pad with spaces if the
197 > * content begins or ends with a backtick).
198 > */
199 > export function appendEscapedMarkdownInlineCode(text: string): string {
200 > const longestBacktickRun = Math.max(0, ...(text.match(/`+/g) ?? []).map(m => m.length)); htmlContent.ts ×1
201 > const fence = '`'.repeat(longestBacktickRun + 1);
202 > const needsSpace = text.startsWith('`') || text.endsWith('`');
203 > const content = needsSpace ? ` ${text} ` : text;
204 > return `${fence}${content}${fence}`;
205 > }
207 > export function escapeDoubleQuotes(input: string) {
208 return input.replace(/"/g, '&quot;');
209 }
211 > export function removeMarkdownEscapes(text: string): string {
212 if (!text) {
213 return text;
214 }
215 return text.replace(/\\([\\`*_{}[\]()#+\-.!~])/g, '$1');
216 }
218 > export function parseHrefAndDimensions(href: string): { href: string; dimensions: string[] } {
219 const dimensions: string[] = [];
220 const splitted = href.split('|').map(s => s.trim());
221 href = splitted[0];
222 const parameters = splitted[1];
223 if (parameters) {
224 const heightFromParams = /height=(\d+)/.exec(parameters);
225 const widthFromParams = /width=(\d+)/.exec(parameters);
226 const height = heightFromParams ? heightFromParams[1] : '';
227 const width = widthFromParams ? widthFromParams[1] : '';
228 const widthIsFinite = isFinite(parseInt(width));
229 const heightIsFinite = isFinite(parseInt(height));
230 if (widthIsFinite) {
231 dimensions.push(`width="${width}"`);
232 }
233 if (heightIsFinite) {
234 dimensions.push(`height="${height}"`);
235 }
236 }
237 return { href, dimensions };
238 }
240 > export function createMarkdownLink(text: string, href: string, title?: string, escapeTokens = true): string {
241 return `[${escapeTokens ? escapeMarkdownSyntaxTokens(text) : text}](${href}${title ? ` "${escapeMarkdownSyntaxTokens(title)}"` : ''})`;
242 }
244 > export function createMarkdownCommandLink(command: { text: string; id: string; arguments?: unknown[]; tooltip: string }, escapeTokens = true): string {
245 const uri = createCommandUri(command.id, ...(command.arguments || [])).toString();
246 return createMarkdownLink(command.text, uri, command.tooltip, escapeTokens);
247 }
249 > export function createCommandUri(commandId: string, ...commandArgs: unknown[]): URI {
250 > return URI.from({ htmlContent.ts ×1
251 > scheme: Schemas.command,
252 > path: commandId,
253 > query: commandArgs.length ? encodeURIComponent(JSON.stringify(commandArgs)) : undefined,
254 > });
255 > }