From 7c199fe73ad05e87629be2376ef54ac704499bee Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Tue, 12 Nov 2024 15:00:57 +0100 Subject: [PATCH 1/4] rename asCSSPropertyValue to asCSSStringValue and use for generated CSS with `font-family` and `content` --- src/vs/base/browser/cssValue.ts | 10 ++++++-- .../platform/theme/browser/iconsStyleSheet.ts | 24 ++++++++++++------- .../terminal/browser/terminalService.ts | 2 +- .../decorations/browser/decorationsService.ts | 6 ++--- .../themes/browser/fileIconThemeData.ts | 10 ++++---- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/vs/base/browser/cssValue.ts b/src/vs/base/browser/cssValue.ts index c758b629a71..45bf221249f 100644 --- a/src/vs/base/browser/cssValue.ts +++ b/src/vs/base/browser/cssValue.ts @@ -20,8 +20,14 @@ export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt return dflt; } -export function asCSSPropertyValue(value: string) { - return `'${value.replace(/'/g, '%27')}'`; +/** + * Create a CSS string value from a string. CSS string value is composed of any number of Unicode characters surrounded by either double (") or single (') quotes + * Strings are used in numerous CSS properties, such as content, font-family, and quotes. + * + * https://developer.mozilla.org/en-US/docs/Web/CSS/string + */ +export function asCSSStringValue(value: string) { + return `'${value.replace(/'/g, '\\27')}'`; } /** diff --git a/src/vs/platform/theme/browser/iconsStyleSheet.ts b/src/vs/platform/theme/browser/iconsStyleSheet.ts index 9133d54c5c7..d0073b856a7 100644 --- a/src/vs/platform/theme/browser/iconsStyleSheet.ts +++ b/src/vs/platform/theme/browser/iconsStyleSheet.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { asCSSPropertyValue, asCSSUrl } from '../../../base/browser/cssValue.js'; +import { asCSSStringValue, asCSSUrl } from '../../../base/browser/cssValue.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../base/common/themables.js'; @@ -40,20 +40,28 @@ export function getIconsStyleSheet(themeService: IThemeService | undefined): IIc continue; } + const fontCharacterCSSValue = asCSSStringValue(definition.fontCharacter); + const fontContribution = definition.font; const fontFamilyVar = `--vscode-icon-${contribution.id}-font-family`; const contentVar = `--vscode-icon-${contribution.id}-content`; if (fontContribution) { + const fontFamilyCSSValue = asCSSStringValue(fontContribution.id); + usedFontIds[fontContribution.id] = fontContribution.definition; rootAttribs.push( - `${fontFamilyVar}: ${asCSSPropertyValue(fontContribution.id)};`, - `${contentVar}: '${definition.fontCharacter}';`, + `${fontFamilyVar}: ${fontFamilyCSSValue};`, + `${contentVar}: ${fontCharacterCSSValue};`, ); - rules.push(`.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; font-family: ${asCSSPropertyValue(fontContribution.id)}; }`); + rules.push(`.codicon-${contribution.id}:before { content: ${fontCharacterCSSValue}; font-family: ${fontFamilyCSSValue}; }`); } else { - rootAttribs.push(`${contentVar}: '${definition.fontCharacter}'; ${fontFamilyVar}: 'codicon';`); - rules.push(`.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; }`); + rootAttribs.push( + `${fontFamilyVar}: 'codicon';`, + `${contentVar}: ${fontCharacterCSSValue};` + ); + rules.push(`.codicon-${contribution.id}:before { content: ${fontCharacterCSSValue}; }`); } + } for (const id in usedFontIds) { @@ -61,10 +69,10 @@ export function getIconsStyleSheet(themeService: IThemeService | undefined): IIc const fontWeight = definition.weight ? `font-weight: ${definition.weight};` : ''; const fontStyle = definition.style ? `font-style: ${definition.style};` : ''; const src = definition.src.map(l => `${asCSSUrl(l.location)} format('${l.format}')`).join(', '); - rules.push(`@font-face { src: ${src}; font-family: ${asCSSPropertyValue(id)};${fontWeight}${fontStyle} font-display: block; }`); + rules.push(`@font-face { src: ${src}; font-family: ${asCSSStringValue(id)};${fontWeight}${fontStyle} font-display: block; }`); } - rules.push(`:root { ${rootAttribs.join(' ')} }`); + rules.push(`:root { ${rootAttribs.join('\n')} }`); return rules.join('\n'); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 028b6d9ddd2..d61338cdc50 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -1270,7 +1270,7 @@ class TerminalEditorStyle extends Themable { if (def) { css += ( `.monaco-workbench .terminal-tab.codicon-${icon.id}::before` + - `{content: '${def.fontCharacter}' !important; font-family: ${cssJs.asCSSPropertyValue(def.font?.id ?? 'codicon')} !important;}` + `{content: ${cssJs.asCSSStringValue(def.fontCharacter)} !important; font-family: ${cssJs.asCSSStringValue(def.font?.id ?? 'codicon')} !important;}` ); } } diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 49213f302d3..66b84d1a1f5 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -11,7 +11,7 @@ import { IDisposable, toDisposable, DisposableStore } from '../../../../base/com import { isThenable } from '../../../../base/common/async.js'; import { LinkedList } from '../../../../base/common/linkedList.js'; import { createStyleSheet, createCSSRule, removeCSSRulesContainingSelector } from '../../../../base/browser/dom.js'; -import { asCSSPropertyValue } from '../../../../base/browser/cssValue.js'; +import { asCSSStringValue } from '../../../../base/browser/cssValue.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { isFalsyOrWhitespace } from '../../../../base/common/strings.js'; @@ -137,9 +137,9 @@ class DecorationRule { } createCSSRule( `.${this.iconBadgeClassName}::after`, - `content: '${definition.fontCharacter}'; + `content: ${asCSSStringValue(definition.fontCharacter)}; color: ${icon.color ? getColor(icon.color.id) : getColor(color)}; - font-family: ${asCSSPropertyValue(definition.font?.id ?? 'codicon')}; + font-family: ${asCSSStringValue(definition.font?.id ?? 'codicon')}; font-size: 16px; margin-right: 14px; font-weight: normal; diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index 4aefe5336d2..78d914cc308 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -10,7 +10,7 @@ import * as resources from '../../../../base/common/resources.js'; import * as Json from '../../../../base/common/json.js'; import { ExtensionData, IThemeExtensionPoint, IWorkbenchFileIconTheme } from '../common/workbenchThemeService.js'; import { getParseErrorMessage } from '../../../../base/common/jsonErrorMessages.js'; -import { asCSSUrl } from '../../../../base/browser/cssValue.js'; +import { asCSSStringValue, asCSSUrl } from '../../../../base/browser/cssValue.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IExtensionResourceLoaderService } from '../../../../platform/extensionResourceLoader/common/extensionResourceLoader.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; @@ -398,14 +398,14 @@ export class FileIconThemeLoader { const defaultFontSize = this.tryNormalizeFontSize(fonts[0].size) || '150%'; fonts.forEach(font => { const src = font.src.map(l => `${asCSSUrl(resolvePath(l.path))} format('${l.format}')`).join(', '); - cssRules.push(`@font-face { src: ${src}; font-family: '${font.id}'; font-weight: ${font.weight}; font-style: ${font.style}; font-display: block; }`); + cssRules.push(`@font-face { src: ${src}; font-family: ${asCSSStringValue(font.id)}; font-weight: ${font.weight}; font-style: ${font.style}; font-display: block; }`); const fontSize = this.tryNormalizeFontSize(font.size); if (fontSize !== undefined && fontSize !== defaultFontSize) { fontSizes.set(font.id, fontSize); } }); - cssRules.push(`.show-file-icons .file-icon::before, .show-file-icons .folder-icon::before, .show-file-icons .rootfolder-icon::before { font-family: '${fonts[0].id}'; font-size: ${defaultFontSize}; }`); + cssRules.push(`.show-file-icons .file-icon::before, .show-file-icons .folder-icon::before, .show-file-icons .rootfolder-icon::before { font-family: ${asCSSStringValue(fonts[0].id)}; font-size: ${defaultFontSize}; }`); } // Use emQuads to prevent the icon from collapsing to zero height for image icons @@ -423,14 +423,14 @@ export class FileIconThemeLoader { body.push(`color: ${definition.fontColor};`); } if (definition.fontCharacter) { - body.push(`content: '${definition.fontCharacter}';`); + body.push(`content: ${asCSSStringValue(definition.fontCharacter)};`); } const fontSize = definition.fontSize ?? (definition.fontId ? fontSizes.get(definition.fontId) : undefined); if (fontSize) { body.push(`font-size: ${fontSize};`); } if (definition.fontId) { - body.push(`font-family: ${definition.fontId};`); + body.push(`font-family: ${asCSSStringValue(definition.fontId)};`); } if (showLanguageModeIcons) { body.push(`background-image: unset;`); // potentially set by the language default From faa58459aa6a6d3ae32d1caa4f61216c3b28150f Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Tue, 12 Nov 2024 16:15:08 +0100 Subject: [PATCH 2/4] more validation --- src/vs/base/browser/cssValue.ts | 5 ++++- .../platform/theme/browser/iconsStyleSheet.ts | 2 +- src/vs/platform/theme/common/iconRegistry.ts | 6 +++--- .../terminal/browser/terminalService.ts | 2 +- .../decorations/browser/decorationsService.ts | 2 +- .../themes/browser/fileIconThemeData.ts | 19 ++++++++++--------- .../themes/browser/productIconThemeData.ts | 10 +++++----- .../themes/common/fileIconThemeSchema.ts | 9 ++++++--- .../themes/common/iconExtensionPoint.ts | 12 +++++++++--- .../themes/common/productIconThemeSchema.ts | 2 ++ 10 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/vs/base/browser/cssValue.ts b/src/vs/base/browser/cssValue.ts index 45bf221249f..6488be76601 100644 --- a/src/vs/base/browser/cssValue.ts +++ b/src/vs/base/browser/cssValue.ts @@ -26,7 +26,10 @@ export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt * * https://developer.mozilla.org/en-US/docs/Web/CSS/string */ -export function asCSSStringValue(value: string) { +export function asCSSStringValue(value: string, escapeBackslash = true): string { + if (escapeBackslash) { + value = value.replace(/\\/g, '\\\\'); + } return `'${value.replace(/'/g, '\\27')}'`; } diff --git a/src/vs/platform/theme/browser/iconsStyleSheet.ts b/src/vs/platform/theme/browser/iconsStyleSheet.ts index d0073b856a7..535887a31b3 100644 --- a/src/vs/platform/theme/browser/iconsStyleSheet.ts +++ b/src/vs/platform/theme/browser/iconsStyleSheet.ts @@ -40,7 +40,7 @@ export function getIconsStyleSheet(themeService: IThemeService | undefined): IIc continue; } - const fontCharacterCSSValue = asCSSStringValue(definition.fontCharacter); + const fontCharacterCSSValue = asCSSStringValue(definition.fontCharacter, false); const fontContribution = definition.font; const fontFamilyVar = `--vscode-icon-${contribution.id}-font-family`; diff --git a/src/vs/platform/theme/common/iconRegistry.ts b/src/vs/platform/theme/common/iconRegistry.ts index 279b30ba113..5dee6ce44d6 100644 --- a/src/vs/platform/theme/common/iconRegistry.ts +++ b/src/vs/platform/theme/common/iconRegistry.ts @@ -26,15 +26,15 @@ export const Extensions = { export type IconDefaults = ThemeIcon | IconDefinition; export interface IconDefinition { - font?: IconFontContribution; // undefined for the default font (codicon) - fontCharacter: string; + readonly font?: IconFontContribution; // undefined for the default font (codicon) + readonly fontCharacter: string; } export interface IconContribution { readonly id: string; description: string | undefined; - deprecationMessage?: string; + readonly deprecationMessage?: string; readonly defaults: IconDefaults; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index d61338cdc50..1f1bbfbb19c 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -1270,7 +1270,7 @@ class TerminalEditorStyle extends Themable { if (def) { css += ( `.monaco-workbench .terminal-tab.codicon-${icon.id}::before` + - `{content: ${cssJs.asCSSStringValue(def.fontCharacter)} !important; font-family: ${cssJs.asCSSStringValue(def.font?.id ?? 'codicon')} !important;}` + `{content: ${cssJs.asCSSStringValue(def.fontCharacter, false)} !important; font-family: ${cssJs.asCSSStringValue(def.font?.id ?? 'codicon')} !important;}` ); } } diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 66b84d1a1f5..7c16ea91031 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -137,7 +137,7 @@ class DecorationRule { } createCSSRule( `.${this.iconBadgeClassName}::after`, - `content: ${asCSSStringValue(definition.fontCharacter)}; + `content: ${asCSSStringValue(definition.fontCharacter, false)}; color: ${icon.color ? getColor(icon.color.id) : getColor(color)}; font-family: ${asCSSStringValue(definition.font?.id ?? 'codicon')}; font-size: 16px; diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index 78d914cc308..fb0152fe970 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -15,6 +15,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { IExtensionResourceLoaderService } from '../../../../platform/extensionResourceLoader/common/extensionResourceLoader.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { mainWindow } from '../../../../base/browser/window.js'; +import { fontCharacterRegex, fontColorRegex, fontSizeRegex } from '../common/productIconThemeSchema.js'; export class FileIconThemeData implements IWorkbenchFileIconTheme { @@ -152,11 +153,11 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { } interface IconDefinition { - iconPath: string; - fontColor: string; - fontCharacter: string; - fontSize: string; - fontId: string; + readonly iconPath: string; + readonly fontColor: string; + readonly fontCharacter: string; + readonly fontSize: string; + readonly fontId: string; } interface FontDefinition { @@ -419,14 +420,14 @@ export class FileIconThemeLoader { cssRules.push(`${selectors.join(', ')} { content: '${emQuad}'; background-image: ${asCSSUrl(resolvePath(definition.iconPath))}; }`); } else if (definition.fontCharacter || definition.fontColor) { const body = []; - if (definition.fontColor) { + if (definition.fontColor && definition.fontColor.match(fontColorRegex)) { body.push(`color: ${definition.fontColor};`); } - if (definition.fontCharacter) { - body.push(`content: ${asCSSStringValue(definition.fontCharacter)};`); + if (definition.fontCharacter && definition.fontCharacter.match(fontCharacterRegex)) { + body.push(`content: ${asCSSStringValue(definition.fontCharacter, false)};`); } const fontSize = definition.fontSize ?? (definition.fontId ? fontSizes.get(definition.fontId) : undefined); - if (fontSize) { + if (fontSize && fontSize.match(fontSizeRegex)) { body.push(`font-size: ${fontSize};`); } if (definition.fontId) { diff --git a/src/vs/workbench/services/themes/browser/productIconThemeData.ts b/src/vs/workbench/services/themes/browser/productIconThemeData.ts index 0781c91a3a0..8961bfe3b2b 100644 --- a/src/vs/workbench/services/themes/browser/productIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/productIconThemeData.ts @@ -11,7 +11,7 @@ import * as Json from '../../../../base/common/json.js'; import { ExtensionData, IThemeExtensionPoint, IWorkbenchProductIconTheme, ThemeSettingDefaults } from '../common/workbenchThemeService.js'; import { getParseErrorMessage } from '../../../../base/common/jsonErrorMessages.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { fontIdRegex, fontWeightRegex, fontStyleRegex, fontFormatRegex } from '../common/productIconThemeSchema.js'; +import { fontIdRegex, fontWeightRegex, fontStyleRegex, fontFormatRegex, fontCharacterRegex } from '../common/productIconThemeSchema.js'; import { isObject, isString } from '../../../../base/common/types.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IconDefinition, getIconRegistry, IconContribution, IconFontDefinition, IconFontSource } from '../../../../platform/theme/common/iconRegistry.js'; @@ -200,8 +200,8 @@ function _loadProductIconThemeDocument(fileService: IExtensionResourceLoaderServ const sanitizedFonts: Map = new Map(); for (const font of contentValue.fonts) { - if (isString(font.id) && font.id.match(fontIdRegex)) { - const fontId = font.id; + const fontId = font.id; + if (isString(fontId) && fontId.match(fontIdRegex)) { let fontWeight = undefined; if (isString(font.weight) && font.weight.match(fontWeightRegex)) { @@ -245,7 +245,7 @@ function _loadProductIconThemeDocument(fileService: IExtensionResourceLoaderServ for (const iconId in contentValue.iconDefinitions) { const definition = contentValue.iconDefinitions[iconId]; - if (isString(definition.fontCharacter)) { + if (isString(definition.fontCharacter) && definition.fontCharacter.match(fontCharacterRegex)) { const fontId = definition.fontId ?? primaryFontId; const fontDefinition = sanitizedFonts.get(fontId); if (fontDefinition) { @@ -256,7 +256,7 @@ function _loadProductIconThemeDocument(fileService: IExtensionResourceLoaderServ warnings.push(nls.localize('error.icon.font', 'Skipping icon definition \'{0}\'. Unknown font.', iconId)); } } else { - warnings.push(nls.localize('error.icon.fontCharacter', 'Skipping icon definition \'{0}\'. Unknown fontCharacter.', iconId)); + warnings.push(nls.localize('error.icon.fontCharacter', 'Skipping icon definition \'{0}\'. Unknown fontCharacter. Must use a sing; character or a \\ followed by a Unicode code points in hexadecimal.', iconId)); } } return { iconDefinitions }; diff --git a/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts b/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts index 4d18093f41e..685c3375f00 100644 --- a/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts +++ b/src/vs/workbench/services/themes/common/fileIconThemeSchema.ts @@ -7,7 +7,7 @@ import * as nls from '../../../../nls.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Extensions as JSONExtensions, IJSONContributionRegistry } from '../../../../platform/jsonschemas/common/jsonContributionRegistry.js'; import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; -import { fontWeightRegex, fontStyleRegex, fontSizeRegex, fontIdRegex } from './productIconThemeSchema.js'; +import { fontWeightRegex, fontStyleRegex, fontSizeRegex, fontIdRegex, fontCharacterRegex, fontColorRegex } from './productIconThemeSchema.js'; const schemaId = 'vscode://schemas/icon-theme'; const schema: IJSONSchema = { @@ -208,12 +208,15 @@ const schema: IJSONSchema = { }, fontCharacter: { type: 'string', - description: nls.localize('schema.fontCharacter', 'When using a glyph font: The character in the font to use.') + description: nls.localize('schema.fontCharacter', 'When using a glyph font: The character in the font to use.'), + pattern: fontCharacterRegex, + patternErrorMessage: nls.localize('schema.fontCharacter.formatError', 'The fontCharacter must be a single letter or a backslash and followed by unicode code points in hexadecimal.') }, fontColor: { type: 'string', format: 'color-hex', - description: nls.localize('schema.fontColor', 'When using a glyph font: The color to use.') + description: nls.localize('schema.fontColor', 'When using a glyph font: The color to use.'), + pattern: fontColorRegex }, fontSize: { type: 'string', diff --git a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts index 29588b66234..0a58dbcdbbf 100644 --- a/src/vs/workbench/services/themes/common/iconExtensionPoint.ts +++ b/src/vs/workbench/services/themes/common/iconExtensionPoint.ts @@ -11,6 +11,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import * as resources from '../../../../base/common/resources.js'; import { IExtensionDescription } from '../../../../platform/extensions/common/extensions.js'; import { extname, posix } from '../../../../base/common/path.js'; +import { fontCharacterRegex } from './productIconThemeSchema.js'; interface IIconExtensionPoint { [id: string]: { @@ -53,7 +54,9 @@ const iconConfigurationExtPoint = ExtensionsRegistry.registerExtensionPoint Date: Tue, 12 Nov 2024 16:26:42 +0100 Subject: [PATCH 3/4] polish --- src/vs/base/browser/cssValue.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/base/browser/cssValue.ts b/src/vs/base/browser/cssValue.ts index 6488be76601..1191e4bca9d 100644 --- a/src/vs/base/browser/cssValue.ts +++ b/src/vs/base/browser/cssValue.ts @@ -21,8 +21,8 @@ export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt } /** - * Create a CSS string value from a string. CSS string value is composed of any number of Unicode characters surrounded by either double (") or single (') quotes - * Strings are used in numerous CSS properties, such as content, font-family, and quotes. + * Creates a CSS string value from a string. A CSS string value is composed of any number of Unicode characters surrounded by either double (") or single (') quotes. + * CSS strings are used in numerous CSS properties, such as content, font-family, and quotes. * * https://developer.mozilla.org/en-US/docs/Web/CSS/string */ From b46d303f3cbf7155b9ab70eb23d877d19eacc276 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 15 Nov 2024 11:59:17 +0100 Subject: [PATCH 4/4] update --- src/vs/base/browser/cssValue.ts | 26 +++++++++++----- .../editor/common/services/getIconClasses.ts | 12 ++++---- .../platform/theme/browser/iconsStyleSheet.ts | 4 +-- .../themes/browser/fileIconThemeData.ts | 30 +++++++++---------- .../themes/common/productIconThemeSchema.ts | 2 +- 5 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/vs/base/browser/cssValue.ts b/src/vs/base/browser/cssValue.ts index 490bc811f50..f099ead8ce0 100644 --- a/src/vs/base/browser/cssValue.ts +++ b/src/vs/base/browser/cssValue.ts @@ -27,10 +27,26 @@ export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt return dflt; } -export function value(value: string): CssFragment { - const out = value.replaceAll(/[^_\-a-z0-9#]/gi, ''); +export function sizeValue(value: string): CssFragment { + const out = value.replaceAll(/[^\w.%+-]/gi, ''); if (out !== value) { - console.warn(`CSS value ${value} modified to ${out} to be safe for CSS`); + console.warn(`CSS size ${value} modified to ${out} to be safe for CSS`); + } + return asFragment(out); +} + +export function hexColorValue(value: string): CssFragment { + const out = value.replaceAll(/[^[0-9a-fA-F#]]/gi, ''); + if (out !== value) { + console.warn(`CSS hex color ${value} modified to ${out} to be safe for CSS`); + } + return asFragment(out); +} + +export function identValue(value: string): CssFragment { + const out = value.replaceAll(/[^_\-a-z0-9]/gi, ''); + if (out !== value) { + console.warn(`CSS ident value ${value} modified to ${out} to be safe for CSS`); } return asFragment(out); } @@ -39,10 +55,6 @@ export function stringValue(value: string): CssFragment { return asFragment(`'${value.replaceAll(/'/g, '\\000027')}'`); } -export function selector(value: string): CssFragment { - return asFragment(value); -} - /** * returns url('...') */ diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 61fd63dc827..cb60d8b7bf5 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -35,13 +35,13 @@ export function getIconClasses(modelService: IModelService, languageService: ILa } else { const match = resource.path.match(fileIconDirectoryRegex); if (match) { - name = cssEscape(match[2].toLowerCase()); + name = fileIconSelectorEscape(match[2].toLowerCase()); if (match[1]) { - classes.push(`${cssEscape(match[1].toLowerCase())}-name-dir-icon`); // parent directory + classes.push(`${fileIconSelectorEscape(match[1].toLowerCase())}-name-dir-icon`); // parent directory } } else { - name = cssEscape(resource.authority.toLowerCase()); + name = fileIconSelectorEscape(resource.authority.toLowerCase()); } } @@ -77,7 +77,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa // Detected Mode const detectedLanguageId = detectLanguageId(modelService, languageService, resource); if (detectedLanguageId) { - classes.push(`${cssEscape(detectedLanguageId)}-lang-file-icon`); + classes.push(`${fileIconSelectorEscape(detectedLanguageId)}-lang-file-icon`); } } } @@ -85,7 +85,7 @@ export function getIconClasses(modelService: IModelService, languageService: ILa } export function getIconClassesForLanguageId(languageId: string): string[] { - return ['file-icon', `${cssEscape(languageId)}-lang-file-icon`]; + return ['file-icon', `${fileIconSelectorEscape(languageId)}-lang-file-icon`]; } function detectLanguageId(modelService: IModelService, languageService: ILanguageService, resource: uri): string | null { @@ -122,6 +122,6 @@ function detectLanguageId(modelService: IModelService, languageService: ILanguag return languageService.guessLanguageIdByFilepathOrFirstLine(resource); } -function cssEscape(str: string): string { +export function fileIconSelectorEscape(str: string): string { return str.replace(/[\s]/g, '/'); // HTML class names can not contain certain whitespace characters (https://dom.spec.whatwg.org/#interface-domtokenlist), use / instead, which doesn't exist in file names. } diff --git a/src/vs/platform/theme/browser/iconsStyleSheet.ts b/src/vs/platform/theme/browser/iconsStyleSheet.ts index 42c2d3b3bab..7215f066d45 100644 --- a/src/vs/platform/theme/browser/iconsStyleSheet.ts +++ b/src/vs/platform/theme/browser/iconsStyleSheet.ts @@ -58,8 +58,8 @@ export function getIconsStyleSheet(themeService: IThemeService | undefined): IIc for (const id in usedFontIds) { const definition = usedFontIds[id]; - const fontWeight = definition.weight ? css.inline`font-weight: ${css.value(definition.weight)};` : css.inline``; - const fontStyle = definition.style ? css.inline`font-style: ${css.value(definition.style)};` : css.inline``; + const fontWeight = definition.weight ? css.inline`font-weight: ${css.identValue(definition.weight)};` : css.inline``; + const fontStyle = definition.style ? css.inline`font-style: ${css.identValue(definition.style)};` : css.inline``; const src = new css.Builder(); for (const l of definition.src) { diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index 4e200f24739..79bebd3ad1c 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -13,9 +13,9 @@ import { getParseErrorMessage } from '../../../../base/common/jsonErrorMessages. import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IExtensionResourceLoaderService } from '../../../../platform/extensionResourceLoader/common/extensionResourceLoader.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { mainWindow } from '../../../../base/browser/window.js'; import { fontCharacterRegex, fontColorRegex, fontSizeRegex } from '../common/productIconThemeSchema.js'; import * as css from '../../../../base/browser/cssValue.js'; +import { fileIconSelectorEscape } from '../../../../editor/common/services/getIconClasses.js'; export class FileIconThemeData implements IWorkbenchFileIconTheme { @@ -257,12 +257,12 @@ export class FileIconThemeLoader { } if (associations) { - let qualifier = css.selector(`.show-file-icons`); + let qualifier = css.inline`.show-file-icons`; if (baseThemeClassName) { qualifier = css.inline`${baseThemeClassName} ${qualifier}`; } - const expanded = css.selector('.monaco-tl-twistie.collapsible:not(.collapsed) + .monaco-tl-contents'); + const expanded = css.inline`.monaco-tl-twistie.collapsible:not(.collapsed) + .monaco-tl-contents`; if (associations.folder) { addSelector(css.inline`${qualifier} .folder-icon::before`, associations.folder); @@ -352,7 +352,7 @@ export class FileIconThemeLoader { for (let i = 0; i < segments.length; i++) { selectors.push(css.inline`.${classSelectorPart(segments.slice(i).join('.'))}-ext-file-icon`); } - selectors.push(css.selector('.ext-file-icon')); // extra segment to increase file-ext score + selectors.push(css.inline`.ext-file-icon`); // extra segment to increase file-ext score } addSelector(css.inline`${qualifier} ${selectors.join('')}.file-icon::before`, fileExtensions[key]); result.hasFileIcons = true; @@ -365,13 +365,13 @@ export class FileIconThemeLoader { const selectors = new css.Builder(); const fileName = handleParentFolder(key.toLowerCase(), selectors); selectors.push(css.inline`.${classSelectorPart(fileName)}-name-file-icon`); - selectors.push(css.selector('.name-file-icon')); // extra segment to increase file-name score + selectors.push(css.inline`.name-file-icon`); // extra segment to increase file-name score const segments = fileName.split('.'); if (segments.length) { for (let i = 1; i < segments.length; i++) { selectors.push(css.inline`.${classSelectorPart(segments.slice(i).join('.'))}-ext-file-icon`); } - selectors.push(css.selector('.ext-file-icon')); // extra segment to increase file-ext score + selectors.push(css.inline`.ext-file-icon`); // extra segment to increase file-ext score } addSelector(css.inline`${qualifier} ${selectors.join('')}.file-icon::before`, fileNames[key]); result.hasFileIcons = true; @@ -381,9 +381,9 @@ export class FileIconThemeLoader { } } collectSelectors(iconThemeDocument); - collectSelectors(iconThemeDocument.light, css.selector('.vs')); - collectSelectors(iconThemeDocument.highContrast, css.selector('.hc-black')); - collectSelectors(iconThemeDocument.highContrast, css.selector('.hc-light')); + collectSelectors(iconThemeDocument.light, css.inline`.vs`); + collectSelectors(iconThemeDocument.highContrast, css.inline`.hc-black`); + collectSelectors(iconThemeDocument.highContrast, css.inline`.hc-light`); if (!result.hasFileIcons && !result.hasFolderIcons) { return result; @@ -400,14 +400,14 @@ export class FileIconThemeLoader { fonts.forEach(font => { const fontSrcs = new css.Builder(); fontSrcs.push(...font.src.map(l => css.inline`${css.asCSSUrl(resolvePath(l.path))} format(${css.stringValue(l.format)})`)); - cssRules.push(css.inline`@font-face { src: ${fontSrcs.join(', ')}; font-family: ${css.stringValue(font.id)}; font-weight: ${css.value(font.weight)}; font-style: ${css.value(font.style)}; font-display: block; }`); + cssRules.push(css.inline`@font-face { src: ${fontSrcs.join(', ')}; font-family: ${css.stringValue(font.id)}; font-weight: ${css.identValue(font.weight)}; font-style: ${css.identValue(font.style)}; font-display: block; }`); const fontSize = this.tryNormalizeFontSize(font.size); if (fontSize !== undefined && fontSize !== defaultFontSize) { fontSizes.set(font.id, fontSize); } }); - cssRules.push(css.inline`.show-file-icons .file-icon::before, .show-file-icons .folder-icon::before, .show-file-icons .rootfolder-icon::before { font-family: ${css.stringValue(fonts[0].id)}; font-size: ${css.value(defaultFontSize)}; }`); + cssRules.push(css.inline`.show-file-icons .file-icon::before, .show-file-icons .folder-icon::before, .show-file-icons .rootfolder-icon::before { font-family: ${css.stringValue(fonts[0].id)}; font-size: ${css.identValue(defaultFontSize)}; }`); } // Use emQuads to prevent the icon from collapsing to zero height for image icons @@ -422,14 +422,14 @@ export class FileIconThemeLoader { } else if (definition.fontCharacter || definition.fontColor) { const body = new css.Builder(); if (definition.fontColor && definition.fontColor.match(fontColorRegex)) { - body.push(css.inline`color: ${css.value(definition.fontColor)};`); + body.push(css.inline`color: ${css.hexColorValue(definition.fontColor)};`); } - if (definition.fontCharacter && definition.fontColor.match(fontCharacterRegex)) { + if (definition.fontCharacter && definition.fontCharacter.match(fontCharacterRegex)) { body.push(css.inline`content: ${css.stringValue(definition.fontCharacter)};`); } const fontSize = definition.fontSize ?? (definition.fontId ? fontSizes.get(definition.fontId) : undefined); if (fontSize && fontSize.match(fontSizeRegex)) { - body.push(css.inline`font-size: ${css.value(fontSize)};`); + body.push(css.inline`font-size: ${css.sizeValue(fontSize)};`); } if (definition.fontId) { body.push(css.inline`font-family: ${css.stringValue(definition.fontId)};`); @@ -493,6 +493,6 @@ function handleParentFolder(key: string, selectors: css.Builder): string { } function classSelectorPart(str: string): css.CssFragment { - str = str.replace(/[\s]/g, '/'); // HTML class names can not contain certain whitespace characters (https://dom.spec.whatwg.org/#interface-domtokenlist), use / instead, which doesn't exist in file names. + str = fileIconSelectorEscape(str); return css.className(str); } diff --git a/src/vs/workbench/services/themes/common/productIconThemeSchema.ts b/src/vs/workbench/services/themes/common/productIconThemeSchema.ts index 964a51df3e5..766a8f546e5 100644 --- a/src/vs/workbench/services/themes/common/productIconThemeSchema.ts +++ b/src/vs/workbench/services/themes/common/productIconThemeSchema.ts @@ -12,7 +12,7 @@ import { iconsSchemaId } from '../../../../platform/theme/common/iconRegistry.js export const fontIdRegex = '^([\\w_-]+)$'; export const fontStyleRegex = '^(normal|italic|(oblique[ \\w\\s-]+))$'; export const fontWeightRegex = '^(normal|bold|lighter|bolder|(\\d{0-1000}))$'; -export const fontSizeRegex = '^([\\w .%_-]+)$'; +export const fontSizeRegex = '^([\\w_.%+-]+)$'; export const fontFormatRegex = '^woff|woff2|truetype|opentype|embedded-opentype|svg$'; export const fontCharacterRegex = '^([^\\\\]|\\\\[a-fA-F0-9]+)$'; export const fontColorRegex = '^#[0-9a-fA-F]{0,6}$';