Skip to content
Closed
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 @@ -269,10 +269,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
32 changes: 32 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 {namespaceCssVariable} from './util';

/**
* The following set contains all keywords that can be used in the animation css shorthand
Expand Down Expand Up @@ -1033,6 +1034,37 @@ 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) => {
// Check for a leading `var(` or trailing `:` to approximate whether we're operating on a
// real CSS variable, not another piece of syntax that resembles it. For example, this
// guards against:
// - `.foo--bar {}`
// - `/* --foo */`
// - `p { content: "--foo" }`
// - `[data---bar] {}`
// - `[data-status=foo--bar] {}`
// etc.
if (!leadingVar && !trailingColon) {
return match;
}
Comment thread
mattrbeck marked this conversation as resolved.
return (leadingVar ?? '') + namespaceCssVariable(varName) + (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 {namespaceCssVariable} from '../../../../util';

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

if (!isCssCustomProperty(op.name)) {
op.name = hyphenate(op.name);
} else {
op.name = namespaceCssVariable(op.name);
}

const {property, suffix} = parseProperty(op.name);
Expand Down
13 changes: 11 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 {namespaceCssVariable, 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,16 @@ 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 {
try {
boundPropertyName = namespaceCssVariable(boundName);
} catch (e) {
this._reportError((e as Error).message, boundProp.sourceSpan);
}
}
bindingType = BindingType.Style;
securityContexts = [SecurityContext.STYLE];
} else if (parts[0] == ANIMATE_PREFIX) {
Expand Down
26 changes: 26 additions & 0 deletions packages/compiler/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,29 @@ export function getJitStandaloneDefaultForVersion(version: string): boolean {
// All other Angular versions (v19+) default to true.
return true;
}

/**
* Namespaces a CSS variable name and validates its syntax.
*
* @param varName The CSS variable name starting with `--`.
* @throws An Error if the CSS variable is invalid (e.g. has a single hyphen after "--global").
* @returns The namespaced CSS variable name.
*/
export function namespaceCssVariable(varName: string): string {
// TODO: Enforce this in v23. This is a breaking change.
// Enable tests under platform-browser and compiler matching "has a single hyphen".
// g3-only-start
// if (varName.startsWith('--global-') && !varName.startsWith('--global--')) {
// throw new Error(
// `CSS variable "${varName}" has a single hyphen after "--global". ` +
// `Use two hyphens ("--global--${varName.substring('--global-'.length)}") to opt-out of namespacing.`,
// );
// }
// g3-only-end

if (varName.startsWith('--global--')) {
return '--' + varName.substring('--global--'.length);
} else {
return '--%NS%' + varName.substring('--'.length);
}
}
Loading
Loading