Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions goldens/public-api/platform-browser/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ export class By {
// @public
export function createApplication(options?: ApplicationConfig, context?: BootstrapContext): Promise<ApplicationRef>;

// @public
export class CssVarNamespacer {
namespace(name: string): string;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<CssVarNamespacer, never>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<CssVarNamespacer>;
}

// @public
export function disableDebugTools(): void;

Expand Down Expand Up @@ -158,6 +167,9 @@ export const platformBrowser: (extraProviders?: StaticProvider[]) => PlatformRef
// @public
export function provideClientHydration(...features: HydrationFeature<HydrationFeatureKind>[]): EnvironmentProviders;

// @public
export function provideCssVarNamespacing(namespace: string): EnvironmentProviders;

// @public
export function provideProtractorTestingSupport(options?: {
usePendingTasksForStability?: boolean;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ hostAttrs: [2, "--camel-case", "foo", "--kebab-case", "foo"],
hostBindings: function MyDirective_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵstyleProp("--camelCase", ctx.value)("--kebab-case", ctx.value);
i0.ɵɵstyleProp("--%NS%camelCase", ctx.value)("--%NS%kebab-case", ctx.value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
if (rf & 1) {
i0.ɵɵelement(0, "div", 0);
} if (rf & 2) {
i0.ɵɵstyleProp("--camelCase", ctx.value)("--kebab-case", ctx.value);
i0.ɵɵstyleProp("--%NS%camelCase", ctx.value)("--%NS%kebab-case", ctx.value);
}
}
7 changes: 4 additions & 3 deletions packages/compiler/src/render3/view/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {ConstantPool} from '../../constant_pool';
import * as core from '../../core';
import * as o from '../../output/output_ast';
import {ParseError, ParseSourceSpan} from '../../parse_util';
import {ShadowCss} from '../../shadow_css';
import {namespaceCssVariables, ShadowCss} from '../../shadow_css';
import {CompilationJobKind, TemplateCompilationMode} from '../../template/pipeline/src/compilation';
import {emitHostBindingFunction, emitTemplateFn, transform} from '../../template/pipeline/src/emit';
import {ingestComponent, ingestHostBinding} from '../../template/pipeline/src/ingest';
Expand Down Expand Up @@ -268,10 +268,11 @@ export function compileComponentFromMetadata(
let hasStyles = !!meta.externalStyles?.length;
// e.g. `styles: [str1, str2]`
if (meta.styles && meta.styles.length) {
const namespacedStyles = meta.styles.map((s) => namespaceCssVariables(s));
const styleValues =
meta.encapsulation == core.ViewEncapsulation.Emulated
? compileStyles(meta.styles, CONTENT_ATTR, HOST_ATTR)
: meta.styles;
? compileStyles(namespacedStyles, CONTENT_ATTR, HOST_ATTR)
: namespacedStyles;
const styleNodes = styleValues.reduce((result, style) => {
if (style.trim().length > 0) {
result.push(constantPool.getConstLiteral(o.literal(style)));
Expand Down
35 changes: 35 additions & 0 deletions packages/compiler/src/shadow_css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.dev/license
*/
import * as chars from './chars';
import {getInvalidCssGlobalError} from './util';

/**
* The following set contains all keywords that can be used in the animation css shorthand
Expand Down Expand Up @@ -1033,6 +1034,40 @@ const _cssCommaInPlaceholderReGlobal = new RegExp(COMMA_IN_PLACEHOLDER, 'g');
const _cssSemiInPlaceholderReGlobal = new RegExp(SEMI_IN_PLACEHOLDER, 'g');
const _cssColonInPlaceholderReGlobal = new RegExp(COLON_IN_PLACEHOLDER, 'g');

// Matches any CSS variable name, defined by a double-hyphen followed by any valid ident.
// https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
const _cssVariableRe = /(var\(\s*)?(--(?:[a-zA-Z0-9_-]|[^\x00-\x7F])+)(\s*:)?/g;

/**
* Transforms CSS variables within a stylesheet to include a namespace placeholder.
*
* E.g. `--foo: bar;` becomes `--%NS%foo: bar;`
* E.g. `color: var(--foo);` becomes `color: var(--%NS%foo);`
*
* If a variable is prefixed with `--global--`, it is NOT namespaced and the prefix is removed.
* E.g. `--global--mycolor: red;` becomes `--mycolor: red;`
*/
export function namespaceCssVariables(cssText: string): string {
return cssText.replace(_cssVariableRe, (match, leadingVar, varName, trailingColon) => {
if (!leadingVar && !trailingColon) {
return match;
}

if (varName.startsWith('--global-') && !varName.startsWith('--global--')) {
throw new Error(getInvalidCssGlobalError(varName));
}

let result;
if (varName.startsWith('--global--')) {
result = `--${varName.substring('--global--'.length)}`;
} else {
result = `--%NS%${varName.substring('--'.length)}`;
}

return (leadingVar || '') + result + (trailingColon || '');
});
}

export class CssRule {
constructor(
public selector: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import * as ir from '../../ir';

import type {CompilationJob} from '../compilation';
import {getInvalidCssGlobalError} from '../../../../util';

const STYLE_DOT = 'style.';
const CLASS_DOT = 'class.';
Expand Down Expand Up @@ -40,6 +41,15 @@ export function parseHostStyleProperties(job: CompilationJob): void {

if (!isCssCustomProperty(op.name)) {
op.name = hyphenate(op.name);
} else {
if (op.name.startsWith('--global-') && !op.name.startsWith('--global--')) {
throw new Error(getInvalidCssGlobalError(op.name));
}
if (op.name.startsWith('--global--')) {
op.name = '--' + op.name.substring('--global--'.length);
} else {
op.name = '--%NS%' + op.name.substring('--'.length);
}
}

const {property, suffix} = parseProperty(op.name);
Expand Down
16 changes: 14 additions & 2 deletions packages/compiler/src/template_parser/binding_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {InterpolatedAttributeToken, InterpolatedTextToken} from '../ml_parser/to
import {ParseError, ParseErrorLevel, ParseSourceSpan} from '../parse_util';
import {ElementSchemaRegistry} from '../schema/element_schema_registry';
import {CssSelector} from '../directive_matching';
import {splitAtColon, splitAtPeriod} from '../util';
import {getInvalidCssGlobalError, splitAtColon, splitAtPeriod} from '../util';
import {MATH_ML_NAMESPACE, SVG_NAMESPACE} from '../template/pipeline/src/namespaces';

const PROPERTY_PARTS_SEPARATOR = '.';
Expand Down Expand Up @@ -594,7 +594,19 @@ export class BindingParser {
securityContexts = [SecurityContext.NONE];
} else if (parts[0] == STYLE_PREFIX) {
unit = parts.length > 2 ? parts[2] : null;
boundPropertyName = parts[1];
const boundName = parts[1];
if (!boundName.startsWith('--')) {
boundPropertyName = boundName;
} else {
if (boundName.startsWith('--global-') && !boundName.startsWith('--global--')) {
this._reportError(getInvalidCssGlobalError(boundName), boundProp.sourceSpan);
}
if (boundName.startsWith('--global--')) {
boundPropertyName = '--' + boundName.substring('--global--'.length);
} else {
boundPropertyName = '--%NS%' + boundName.substring('--'.length);
}
}
bindingType = BindingType.Style;
securityContexts = [SecurityContext.STYLE];
} else if (parts[0] == ANIMATE_PREFIX) {
Expand Down
11 changes: 11 additions & 0 deletions packages/compiler/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,14 @@ export function getJitStandaloneDefaultForVersion(version: string): boolean {
// All other Angular versions (v19+) default to true.
return true;
}

/**
* Formats the compile-time error message for invalid global CSS variable names
* (i.e., those starting with `--global-` with a single hyphen instead of two).
*/
export function getInvalidCssGlobalError(varName: string): string {
return (
`CSS variable "${varName}" has a single hyphen after "--global". ` +
`Use two hyphens ("--global--${varName.substring('--global-'.length)}") to opt-out of namespacing.`
);
}
Loading
Loading