Merge pull request #233687 from microsoft/aeschli/metropolitan-salmon-496

rename asCSSPropertyValue to asCSSStringValue and use for generated CSS with `font-family` and `content`
This commit is contained in:
Martin Aeschlimann
2024-11-15 12:55:11 +01:00
committed by GitHub
9 changed files with 114 additions and 85 deletions
+18 -2
View File
@@ -27,10 +27,26 @@ export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt
return dflt;
}
export function value(value: string): CssFragment {
export function sizeValue(value: string): CssFragment {
const out = value.replaceAll(/[^\w.%+-]/gi, '');
if (out !== value) {
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 value ${value} modified to ${out} to be safe for CSS`);
console.warn(`CSS ident value ${value} modified to ${out} to be safe for CSS`);
}
return asFragment(out);
}
@@ -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.
}
@@ -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) {
+3 -3
View File
@@ -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;
}
@@ -10,11 +10,12 @@ 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 { 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 {
@@ -236,7 +237,7 @@ export class FileIconThemeLoader {
if (!iconThemeDocument.iconDefinitions) {
return result;
}
const selectorByDefinitionId: { [def: string]: string[] } = {};
const selectorByDefinitionId: { [def: string]: css.Builder } = {};
const coveredLanguages: { [languageId: string]: boolean } = {};
const iconThemeDocumentLocationDirname = resources.dirname(iconThemeDocumentLocation);
@@ -244,32 +245,32 @@ export class FileIconThemeLoader {
return resources.joinPath(iconThemeDocumentLocationDirname, path);
}
function collectSelectors(associations: IconsAssociation | undefined, baseThemeClassName?: string) {
function addSelector(selector: string, defId: string) {
function collectSelectors(associations: IconsAssociation | undefined, baseThemeClassName?: css.CssFragment) {
function addSelector(selector: css.CssFragment, defId: string) {
if (defId) {
let list = selectorByDefinitionId[defId];
if (!list) {
list = selectorByDefinitionId[defId] = [];
list = selectorByDefinitionId[defId] = new css.Builder();
}
list.push(selector);
}
}
if (associations) {
let qualifier = '.show-file-icons';
let qualifier = css.inline`.show-file-icons`;
if (baseThemeClassName) {
qualifier = baseThemeClassName + ' ' + qualifier;
qualifier = css.inline`${baseThemeClassName} ${qualifier}`;
}
const expanded = '.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(`${qualifier} .folder-icon::before`, associations.folder);
addSelector(css.inline`${qualifier} .folder-icon::before`, associations.folder);
result.hasFolderIcons = true;
}
if (associations.folderExpanded) {
addSelector(`${qualifier} ${expanded} .folder-icon::before`, associations.folderExpanded);
addSelector(css.inline`${qualifier} ${expanded} .folder-icon::before`, associations.folderExpanded);
result.hasFolderIcons = true;
}
@@ -277,37 +278,37 @@ export class FileIconThemeLoader {
const rootFolderExpanded = associations.rootFolderExpanded || associations.folderExpanded;
if (rootFolder) {
addSelector(`${qualifier} .rootfolder-icon::before`, rootFolder);
addSelector(css.inline`${qualifier} .rootfolder-icon::before`, rootFolder);
result.hasFolderIcons = true;
}
if (rootFolderExpanded) {
addSelector(`${qualifier} ${expanded} .rootfolder-icon::before`, rootFolderExpanded);
addSelector(css.inline`${qualifier} ${expanded} .rootfolder-icon::before`, rootFolderExpanded);
result.hasFolderIcons = true;
}
if (associations.file) {
addSelector(`${qualifier} .file-icon::before`, associations.file);
addSelector(css.inline`${qualifier} .file-icon::before`, associations.file);
result.hasFileIcons = true;
}
const folderNames = associations.folderNames;
if (folderNames) {
for (const key in folderNames) {
const selectors: string[] = [];
const selectors = new css.Builder();
const name = handleParentFolder(key.toLowerCase(), selectors);
selectors.push(`.${escapeCSS(name)}-name-folder-icon`);
addSelector(`${qualifier} ${selectors.join('')}.folder-icon::before`, folderNames[key]);
selectors.push(css.inline`.${classSelectorPart(name)}-name-folder-icon`);
addSelector(css.inline`${qualifier} ${selectors.join('')}.folder-icon::before`, folderNames[key]);
result.hasFolderIcons = true;
}
}
const folderNamesExpanded = associations.folderNamesExpanded;
if (folderNamesExpanded) {
for (const key in folderNamesExpanded) {
const selectors: string[] = [];
const selectors = new css.Builder();
const name = handleParentFolder(key.toLowerCase(), selectors);
selectors.push(`.${escapeCSS(name)}-name-folder-icon`);
addSelector(`${qualifier} ${expanded} ${selectors.join('')}.folder-icon::before`, folderNamesExpanded[key]);
selectors.push(css.inline`.${classSelectorPart(name)}-name-folder-icon`);
addSelector(css.inline`${qualifier} ${expanded} ${selectors.join('')}.folder-icon::before`, folderNamesExpanded[key]);
result.hasFolderIcons = true;
}
}
@@ -316,7 +317,7 @@ export class FileIconThemeLoader {
if (rootFolderNames) {
for (const key in rootFolderNames) {
const name = key.toLowerCase();
addSelector(`${qualifier} .${escapeCSS(name)}-root-name-folder-icon.rootfolder-icon::before`, rootFolderNames[key]);
addSelector(css.inline`${qualifier} .${classSelectorPart(name)}-root-name-folder-icon.rootfolder-icon::before`, rootFolderNames[key]);
result.hasFolderIcons = true;
}
}
@@ -324,7 +325,7 @@ export class FileIconThemeLoader {
if (rootFolderNamesExpanded) {
for (const key in rootFolderNamesExpanded) {
const name = key.toLowerCase();
addSelector(`${qualifier} ${expanded} .${escapeCSS(name)}-root-name-folder-icon.rootfolder-icon::before`, rootFolderNamesExpanded[key]);
addSelector(css.inline`${qualifier} ${expanded} .${classSelectorPart(name)}-root-name-folder-icon.rootfolder-icon::before`, rootFolderNamesExpanded[key]);
result.hasFolderIcons = true;
}
}
@@ -335,7 +336,7 @@ export class FileIconThemeLoader {
languageIds.jsonc = languageIds.json;
}
for (const languageId in languageIds) {
addSelector(`${qualifier} .${escapeCSS(languageId)}-lang-file-icon.file-icon::before`, languageIds[languageId]);
addSelector(css.inline`${qualifier} .${classSelectorPart(languageId)}-lang-file-icon.file-icon::before`, languageIds[languageId]);
result.hasFileIcons = true;
hasSpecificFileIcons = true;
coveredLanguages[languageId] = true;
@@ -344,16 +345,16 @@ export class FileIconThemeLoader {
const fileExtensions = associations.fileExtensions;
if (fileExtensions) {
for (const key in fileExtensions) {
const selectors: string[] = [];
const selectors = new css.Builder();
const name = handleParentFolder(key.toLowerCase(), selectors);
const segments = name.split('.');
if (segments.length) {
for (let i = 0; i < segments.length; i++) {
selectors.push(`.${escapeCSS(segments.slice(i).join('.'))}-ext-file-icon`);
selectors.push(css.inline`.${classSelectorPart(segments.slice(i).join('.'))}-ext-file-icon`);
}
selectors.push('.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(`${qualifier} ${selectors.join('')}.file-icon::before`, fileExtensions[key]);
addSelector(css.inline`${qualifier} ${selectors.join('')}.file-icon::before`, fileExtensions[key]);
result.hasFileIcons = true;
hasSpecificFileIcons = true;
}
@@ -361,18 +362,18 @@ export class FileIconThemeLoader {
const fileNames = associations.fileNames;
if (fileNames) {
for (const key in fileNames) {
const selectors: string[] = [];
const selectors = new css.Builder();
const fileName = handleParentFolder(key.toLowerCase(), selectors);
selectors.push(`.${escapeCSS(fileName)}-name-file-icon`);
selectors.push('.name-file-icon'); // extra segment to increase file-name score
selectors.push(css.inline`.${classSelectorPart(fileName)}-name-file-icon`);
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(`.${escapeCSS(segments.slice(i).join('.'))}-ext-file-icon`);
selectors.push(css.inline`.${classSelectorPart(segments.slice(i).join('.'))}-ext-file-icon`);
}
selectors.push('.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(`${qualifier} ${selectors.join('')}.file-icon::before`, fileNames[key]);
addSelector(css.inline`${qualifier} ${selectors.join('')}.file-icon::before`, fileNames[key]);
result.hasFileIcons = true;
hasSpecificFileIcons = true;
}
@@ -380,9 +381,9 @@ export class FileIconThemeLoader {
}
}
collectSelectors(iconThemeDocument);
collectSelectors(iconThemeDocument.light, '.vs');
collectSelectors(iconThemeDocument.highContrast, '.hc-black');
collectSelectors(iconThemeDocument.highContrast, '.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;
@@ -390,52 +391,53 @@ export class FileIconThemeLoader {
const showLanguageModeIcons = iconThemeDocument.showLanguageModeIcons === true || (hasSpecificFileIcons && iconThemeDocument.showLanguageModeIcons !== false);
const cssRules: string[] = [];
const cssRules = new css.Builder();
const fonts = iconThemeDocument.fonts;
const fontSizes = new Map<string, string>();
if (Array.isArray(fonts)) {
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; }`);
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.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(`.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(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
const emQuad = '\\2001';
const emQuad = css.stringValue('\\2001');
for (const defId in selectorByDefinitionId) {
const selectors = selectorByDefinitionId[defId];
const definition = iconThemeDocument.iconDefinitions[defId];
if (definition) {
if (definition.iconPath) {
cssRules.push(`${selectors.join(', ')} { content: '${emQuad}'; background-image: ${asCSSUrl(resolvePath(definition.iconPath))}; }`);
cssRules.push(css.inline`${selectors.join(', ')} { content: ${emQuad}; background-image: ${css.asCSSUrl(resolvePath(definition.iconPath))}; }`);
} else if (definition.fontCharacter || definition.fontColor) {
const body = [];
if (definition.fontColor) {
body.push(`color: ${definition.fontColor};`);
const body = new css.Builder();
if (definition.fontColor && definition.fontColor.match(fontColorRegex)) {
body.push(css.inline`color: ${css.hexColorValue(definition.fontColor)};`);
}
if (definition.fontCharacter) {
body.push(`content: '${definition.fontCharacter}';`);
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) {
body.push(`font-size: ${fontSize};`);
if (fontSize && fontSize.match(fontSizeRegex)) {
body.push(css.inline`font-size: ${css.sizeValue(fontSize)};`);
}
if (definition.fontId) {
body.push(`font-family: ${definition.fontId};`);
body.push(css.inline`font-family: ${css.stringValue(definition.fontId)};`);
}
if (showLanguageModeIcons) {
body.push(`background-image: unset;`); // potentially set by the language default
body.push(css.inline`background-image: unset;`); // potentially set by the language default
}
cssRules.push(`${selectors.join(', ')} { ${body.join(' ')} }`);
cssRules.push(css.inline`${selectors.join(', ')} { ${body.join(' ')} }`);
}
}
}
@@ -445,9 +447,9 @@ export class FileIconThemeLoader {
if (!coveredLanguages[languageId]) {
const icon = this.languageService.getIcon(languageId);
if (icon) {
const selector = `.show-file-icons .${escapeCSS(languageId)}-lang-file-icon.file-icon::before`;
cssRules.push(`${selector} { content: '${emQuad}'; background-image: ${asCSSUrl(icon.dark)}; }`);
cssRules.push(`.vs ${selector} { content: '${emQuad}'; background-image: ${asCSSUrl(icon.light)}; }`);
const selector = css.inline`.show-file-icons .${classSelectorPart(languageId)}-lang-file-icon.file-icon::before`;
cssRules.push(css.inline`${selector} { content: ${emQuad}; background-image: ${css.asCSSUrl(icon.dark)}; }`);
cssRules.push(css.inline`.vs ${selector} { content: ${emQuad}; background-image: ${css.asCSSUrl(icon.light)}; }`);
}
}
}
@@ -480,17 +482,17 @@ export class FileIconThemeLoader {
}
}
function handleParentFolder(key: string, selectors: string[]): string {
function handleParentFolder(key: string, selectors: css.Builder): string {
const lastIndexOfSlash = key.lastIndexOf('/');
if (lastIndexOfSlash >= 0) {
const parentFolder = key.substring(0, lastIndexOfSlash);
selectors.push(`.${escapeCSS(parentFolder)}-name-dir-icon`);
selectors.push(css.inline`.${classSelectorPart(parentFolder)}-name-dir-icon`);
return key.substring(lastIndexOfSlash + 1);
}
return key;
}
function escapeCSS(str: string) {
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.
return mainWindow.CSS.escape(str);
function classSelectorPart(str: string): css.CssFragment {
str = fileIconSelectorEscape(str);
return css.className(str);
}
@@ -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<string, IconFontDefinition> = 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 };
@@ -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',
@@ -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<IIco
},
fontCharacter: {
description: nls.localize('contributes.icon.default.fontCharacter', 'The character for the icon in the icon font.'),
type: 'string'
type: 'string',
pattern: fontCharacterRegex,
patternErrorMessage: nls.localize('schema.fontCharacter.formatError', 'The fontCharacter must be a single letter or a backslash followed by unicode code points in hexadecimal.')
}
},
required: ['fontPath', 'fontCharacter'],
@@ -103,14 +106,17 @@ export class IconExtensionPoint {
collector.warn(nls.localize('invalid.icons.default.fontPath.extension', "Expected `contributes.icons.default.fontPath` to have file extension 'woff', woff2' or 'ttf', is '{0}'.", fileExt));
return;
}
if (!defaultIcon.fontCharacter.match(fontCharacterRegex)) {
collector.warn(nls.localize('invalid.icons.default.fontCharacter', 'Expected `contributes.icons.default.fontCharacter` to consist of a single character or a \\ followed by a Unicode code points in hexadecimal.')); return;
}
const extensionLocation = extension.description.extensionLocation;
const iconFontLocation = resources.joinPath(extensionLocation, defaultIcon.fontPath);
const fontId = getFontId(extension.description, defaultIcon.fontPath);
const definition = iconRegistry.registerIconFont(fontId, { src: [{ location: iconFontLocation, format }] });
if (!resources.isEqualOrParent(iconFontLocation, extensionLocation)) {
collector.warn(nls.localize('invalid.icons.default.fontPath.path', "Expected `contributes.icons.default.fontPath` ({0}) to be included inside extension's folder ({0}).", iconFontLocation.path, extensionLocation.path));
return;
}
const fontId = getFontId(extension.description, defaultIcon.fontPath);
const definition = iconRegistry.registerIconFont(fontId, { src: [{ location: iconFontLocation, format }] });
iconRegistry.registerIcon(id, {
fontCharacter: defaultIcon.fontCharacter,
font: {
@@ -12,8 +12,10 @@ 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}$';
const schemaId = 'vscode://schemas/product-icon-theme';
const schema: IJSONSchema = {