Не подтверждена Коммит 15a2bf0f создал по автору Matt Bierner's avatar Matt Bierner Зафиксировано автором GitHub
Просмотр файлов

Add helpers for constructing inline css (#233376)

* Add helpers for constructing inline css

Adds some helper functions for constructing inline css

* Add comment and update tests
владелец 4a2d95dd
......@@ -2,9 +2,16 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Color } from '../common/color.js';
import { FileAccess } from '../common/network.js';
import { URI } from '../common/uri.js';
export type CssFragment = string & { readonly __cssFragment: unique symbol };
function asFragment(raw: string): CssFragment {
return raw as CssFragment;
}
export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt: string): string {
if (cssPropertyValue !== undefined) {
const variableMatch = cssPropertyValue.match(/^\s*var\((.+)\)$/);
......@@ -20,16 +27,59 @@ export function asCssValueWithDefault(cssPropertyValue: string | undefined, dflt
return dflt;
}
export function asCSSPropertyValue(value: string) {
return `'${value.replace(/'/g, '%27')}'`;
export function value(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`);
}
return asFragment(out);
}
export function stringValue(value: string): CssFragment {
return asFragment(`'${value.replaceAll(/'/g, '\\000027')}'`);
}
/**
* returns url('...')
*/
export function asCSSUrl(uri: URI | null | undefined): string {
export function asCSSUrl(uri: URI | null | undefined): CssFragment {
if (!uri) {
return `url('')`;
return asFragment(`url('')`);
}
return inline`url(${stringValue(FileAccess.uriToBrowserUri(uri).toString(true))})`;
}
export function className(value: string): CssFragment {
const out = CSS.escape(value);
if (out !== value) {
console.warn(`CSS class name ${value} modified to ${out} to be safe for CSS`);
}
return asFragment(out);
}
type InlineCssTemplateValue = CssFragment | Color;
/**
* Template string tag that that constructs a CSS fragment.
*
* All expressions in the template must be css safe values.
*/
export function inline(strings: TemplateStringsArray, ...values: InlineCssTemplateValue[]): CssFragment {
return asFragment(strings.reduce((result, str, i) => {
const value = values[i] || '';
return result + str + value;
}, ''));
}
export class Builder {
private readonly _parts: CssFragment[] = [];
push(...parts: CssFragment[]): void {
this._parts.push(...parts);
}
join(joiner = '\n'): CssFragment {
return asFragment(this._parts.join(joiner));
}
return `url('${FileAccess.uriToBrowserUri(uri).toString(true).replace(/'/g, '%27')}')`;
}
......@@ -130,7 +130,7 @@ suite('Decoration Render Options', () => {
// single quote must always be escaped/encoded
s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('c:\\files\\foo\\b\'ar.png') });
assertBackground('file:///c:/files/foo/b%27ar.png', 'vscode-file://vscode-app/c:/files/foo/b%27ar.png');
assertBackground('file:///c:/files/foo/b\\000027ar.png', 'vscode-file://vscode-app/c:/files/foo/b\\000027ar.png');
s.removeDecorationType('example');
} else {
// unix file path (used as string)
......@@ -140,12 +140,12 @@ suite('Decoration Render Options', () => {
// single quote must always be escaped/encoded
s.registerDecorationType('test', 'example', { gutterIconPath: URI.file('/Users/foo/b\'ar.png') });
assertBackground('file:///Users/foo/b%27ar.png', 'vscode-file://vscode-app/Users/foo/b%27ar.png');
assertBackground('file:///Users/foo/b\\000027ar.png', 'vscode-file://vscode-app/Users/foo/b\\000027ar.png');
s.removeDecorationType('example');
}
s.registerDecorationType('test', 'example', { gutterIconPath: URI.parse('http://test/pa\'th') });
assert(readStyleSheet(styleSheet).indexOf(`{background:url('http://test/pa%27th') center center no-repeat;}`) > 0);
assert(readStyleSheet(styleSheet).indexOf(`{background:url('http://test/pa\\000027th') center center no-repeat;}`) > 0);
s.removeDecorationType('example');
});
});
......@@ -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 * as css 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';
......@@ -11,7 +11,7 @@ import { getIconRegistry, IconContribution, IconFontDefinition } from '../common
import { IProductIconTheme, IThemeService } from '../common/themeService.js';
export interface IIconsStyleSheet extends IDisposable {
getCSS(): string;
getCSS(): css.CssFragment;
readonly onDidChange: Event<void>;
}
......@@ -28,12 +28,12 @@ export function getIconsStyleSheet(themeService: IThemeService | undefined): IIc
return {
dispose: () => disposable.dispose(),
onDidChange: onDidChangeEmmiter.event,
getCSS() {
getCSS(): css.CssFragment {
const productIconTheme = themeService ? themeService.getProductIconTheme() : new UnthemedProductIconTheme();
const usedFontIds: { [id: string]: IconFontDefinition } = {};
const rules: string[] = [];
const rootAttribs: string[] = [];
const rules = new css.Builder();
const rootAttribs = new css.Builder();
for (const contribution of iconRegistry.getIcons()) {
const definition = productIconTheme.getIcon(contribution);
if (!definition) {
......@@ -41,30 +41,34 @@ export function getIconsStyleSheet(themeService: IThemeService | undefined): IIc
}
const fontContribution = definition.font;
const fontFamilyVar = `--vscode-icon-${contribution.id}-font-family`;
const contentVar = `--vscode-icon-${contribution.id}-content`;
const fontFamilyVar = css.inline`--vscode-icon-${css.className(contribution.id)}-font-family`;
const contentVar = css.inline`--vscode-icon-${css.className(contribution.id)}-content`;
if (fontContribution) {
usedFontIds[fontContribution.id] = fontContribution.definition;
rootAttribs.push(
`${fontFamilyVar}: ${asCSSPropertyValue(fontContribution.id)};`,
`${contentVar}: '${definition.fontCharacter}';`,
css.inline`${fontFamilyVar}: ${css.stringValue(fontContribution.id)};`,
css.inline`${contentVar}: ${css.stringValue(definition.fontCharacter)};`,
);
rules.push(`.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; font-family: ${asCSSPropertyValue(fontContribution.id)}; }`);
rules.push(css.inline`.codicon-${css.className(contribution.id)}:before { content: ${css.stringValue(definition.fontCharacter)}; font-family: ${css.stringValue(fontContribution.id)}; }`);
} else {
rootAttribs.push(`${contentVar}: '${definition.fontCharacter}'; ${fontFamilyVar}: 'codicon';`);
rules.push(`.codicon-${contribution.id}:before { content: '${definition.fontCharacter}'; }`);
rootAttribs.push(css.inline`${contentVar}: ${css.stringValue(definition.fontCharacter)}; ${fontFamilyVar}: 'codicon';`);
rules.push(css.inline`.codicon-${css.className(contribution.id)}:before { content: ${css.stringValue(definition.fontCharacter)}; }`);
}
}
for (const id in usedFontIds) {
const definition = usedFontIds[id];
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; }`);
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 src = new css.Builder();
for (const l of definition.src) {
src.push(css.inline`${css.asCSSUrl(l.location)} format(${css.stringValue(l.format)})`);
}
rules.push(css.inline`@font-face { src: ${src.join(', ')}; font-family: ${css.stringValue(id)};${fontWeight}${fontStyle} font-display: block; }`);
}
rules.push(`:root { ${rootAttribs.join(' ')} }`);
rules.push(css.inline`:root { ${rootAttribs.join(' ')} }`);
return rules.join('\n');
}
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as dom from '../../../../base/browser/dom.js';
import * as cssJs from '../../../../base/browser/cssValue.js';
import * as cssValue from '../../../../base/browser/cssValue.js';
import { DeferredPromise, timeout } from '../../../../base/common/async.js';
import { debounce, memoize } from '../../../../base/common/decorators.js';
import { DynamicListEventMultiplexer, Emitter, Event, IDynamicListEventMultiplexer } from '../../../../base/common/event.js';
......@@ -1258,8 +1258,8 @@ class TerminalEditorStyle extends Themable {
const iconClasses = getUriClasses(instance, colorTheme.type);
if (uri instanceof URI && iconClasses && iconClasses.length > 1) {
css += (
`.monaco-workbench .terminal-tab.${iconClasses[0]}::before` +
`{content: ''; background-image: ${cssJs.asCSSUrl(uri)};}`
cssValue.inline`.monaco-workbench .terminal-tab.${cssValue.className(iconClasses[0])}::before
{content: ''; background-image: ${cssValue.asCSSUrl(uri)};}`
);
}
if (ThemeIcon.isThemeIcon(icon)) {
......@@ -1268,10 +1268,8 @@ class TerminalEditorStyle extends Themable {
if (iconContribution) {
const def = productIconTheme.getIcon(iconContribution);
if (def) {
css += (
`.monaco-workbench .terminal-tab.codicon-${icon.id}::before` +
`{content: '${def.fontCharacter}' !important; font-family: ${cssJs.asCSSPropertyValue(def.font?.id ?? 'codicon')} !important;}`
);
css += cssValue.inline`.monaco-workbench .terminal-tab.codicon-${cssValue.className(icon.id)}::before
{content: ${cssValue.stringValue(def.fontCharacter)} !important; font-family: ${cssValue.stringValue(def.font?.id ?? 'codicon')} !important;}`;
}
}
}
......@@ -1280,7 +1278,7 @@ class TerminalEditorStyle extends Themable {
// Add colors
const iconForegroundColor = colorTheme.getColor(iconForeground);
if (iconForegroundColor) {
css += `.monaco-workbench .show-file-icons .file-icon.terminal-tab::before { color: ${iconForegroundColor}; }`;
css += cssValue.inline`.monaco-workbench .show-file-icons .file-icon.terminal-tab::before { color: ${iconForegroundColor}; }`;
}
css += getColorStyleContent(colorTheme, true);
......
......@@ -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 * as cssValue 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';
......@@ -139,7 +139,7 @@ class DecorationRule {
`.${this.iconBadgeClassName}::after`,
`content: '${definition.fontCharacter}';
color: ${icon.color ? getColor(icon.color.id) : getColor(color)};
font-family: ${asCSSPropertyValue(definition.font?.id ?? 'codicon')};
font-family: ${cssValue.stringValue(definition.font?.id ?? 'codicon')};
font-size: 16px;
margin-right: 14px;
font-weight: normal;
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать