mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 17:19:48 +01:00
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:
@@ -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) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user