Skip to content

Commit

Permalink
Handle variables inside colors (#5165)
Browse files Browse the repository at this point in the history
Fixed variables like `--color: rgb(var(--black), 0, 0, 0)`
  • Loading branch information
alexanderby committed Mar 7, 2021
1 parent 326bc7d commit c994e30
Show file tree
Hide file tree
Showing 2 changed files with 263 additions and 10 deletions.
94 changes: 85 additions & 9 deletions src/inject/dynamic-theme/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class VariablesStore {
private initialVarTypes = new Map<string, number>();
private changedTypeVars = new Set<string>();
private typeChangeSubscriptions = new Map<string, Set<() => void>>();
private unstableVarValues = new Map<string, string>();

clear() {
this.varTypes.clear();
Expand All @@ -46,6 +47,7 @@ export class VariablesStore {
this.initialVarTypes.clear();
this.changedTypeVars.clear();
this.typeChangeSubscriptions.clear();
this.unstableVarValues.clear();
}

private isVarType(varName: string, typeNum: number) {
Expand Down Expand Up @@ -130,11 +132,19 @@ export class VariablesStore {
const property = varNameWrapper(varName);
let modifiedValue: string;
if (isVarDependant(sourceValue)) {
modifiedValue = replaceCSSVariablesNames(
sourceValue,
(v) => varNameWrapper(v),
(fallback) => colorModifier(fallback, theme),
);
if (isConstructedColorVar(sourceValue)) {
let value = insertVarValues(sourceValue, this.unstableVarValues);
if (!value) {
value = typeNum === VAR_TYPE_BGCOLOR ? '#ffffff' : '#000000';
}
modifiedValue = colorModifier(value, theme);
} else {
modifiedValue = replaceCSSVariablesNames(
sourceValue,
(v) => varNameWrapper(v),
(fallback) => colorModifier(fallback, theme),
);
}
} else {
modifiedValue = colorModifier(sourceValue, theme);
}
Expand Down Expand Up @@ -193,6 +203,18 @@ export class VariablesStore {
}

getModifierForVarDependant(property: string, sourceValue: string): CSSValueModifier {
if (sourceValue.match(/^\s*(rgb|hsl)a?\(/)) {
const isBg = property.startsWith('background');
const isText = property === 'color';
return (theme) => {
let value = insertVarValues(sourceValue, this.unstableVarValues);
if (!value) {
value = isBg ? '#ffffff' : '#000000';
}
const modifier = isBg ? tryModifyBgColor : isText ? tryModifyTextColor : tryModifyBorderColor;
return modifier(value, theme);
};
}
if (property === 'background-color') {
return (theme) => {
return replaceCSSVariablesNames(
Expand Down Expand Up @@ -244,6 +266,23 @@ export class VariablesStore {
};
}
if (property.startsWith('border')) {
if (sourceValue.endsWith(')')) {
const colorTypeMatch = sourceValue.match(/((rgb|hsl)a?)\(/);
if (colorTypeMatch) {
const index = colorTypeMatch.index;
return (theme) => {
const value = insertVarValues(sourceValue, this.unstableVarValues);
if (!value) {
return sourceValue;
}
const beginning = sourceValue.substring(0, index);
const color = sourceValue.substring(index, sourceValue.length);
const inserted = insertVarValues(color, this.unstableVarValues);
const modified = tryModifyBorderColor(inserted, theme);
return `${beginning}${modified}`;
};
}
}
return (theme) => {
return replaceCSSVariablesNames(
sourceValue,
Expand All @@ -270,15 +309,17 @@ export class VariablesStore {

private collectVariables(rules: CSSRuleList) {
iterateVariables(rules, (varName, value) => {
this.unstableVarValues.set(varName, value);

if (isVarDependant(value) && isConstructedColorVar(value)) {
this.unknownColorVars.add(varName);
this.definedVars.add(varName);
}
if (this.definedVars.has(varName)) {
return;
}
this.definedVars.add(varName);

if (isVarDependant(value)) {
return;
}

const color = tryParseColor(value);
if (color) {
this.unknownColorVars.add(varName);
Expand Down Expand Up @@ -491,6 +532,10 @@ function isVarDependant(value: string) {
return value.includes('var(');
}

function isConstructedColorVar(value: string) {
return value.match(/^\s*(rgb|hsl)a?\(/);
}

function tryModifyBgColor(color: string, theme: Theme) {
const rgb = tryParseColor(color);
return rgb ? modifyBackgroundColor(rgb, theme) : color;
Expand All @@ -505,3 +550,34 @@ function tryModifyBorderColor(color: string, theme: Theme) {
const rgb = tryParseColor(color);
return rgb ? modifyBorderColor(rgb, theme) : color;
}

function insertVarValues(source: string, varValues: Map<string, string>, stack = new Set<string>()) {
let containsUnresolvedVar = false;
const matchReplacer = (match: string) => {
const {name, fallback} = getVariableNameAndFallback(match);
if (stack.has(name)) {
containsUnresolvedVar = true;
return null;
}
stack.add(name);
const varValue = varValues.get(name) || fallback;
let inserted: string = null;
if (varValue) {
if (isVarDependant(varValue)) {
inserted = insertVarValues(varValue, varValues, stack);
} else {
inserted = varValue;
}
}
if (!inserted) {
containsUnresolvedVar = true;
return null;
}
return inserted;
};
const replaced = replaceVariablesMatches(source, matchReplacer);
if (containsUnresolvedVar) {
return null;
}
return replaced;
}
179 changes: 178 additions & 1 deletion tests/inject/dynamic/variables.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ afterEach(() => {
document.documentElement.removeAttribute('style');
});

describe('CSS Variables Override', () => {
describe('CSS VARIABLES OVERRIDE', () => {
it('should override style with variables', () => {
container.innerHTML = multiline(
'<style>',
Expand Down Expand Up @@ -300,6 +300,183 @@ describe('CSS Variables Override', () => {
expect(getComputedStyle(container.querySelector('h1')).backgroundColor).toBe('rgb(0, 0, 0)');
});

it('should handle variables inside color values (constructed colors)', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --bg: 255, 255, 255;',
' --text: 0, 0, 0;',
' }',
'</style>',
'<style>',
' h1 {',
' background: rgb(var(--bg));',
' color: rgb(var(--text));',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('h1')).backgroundColor).toBe('rgb(0, 0, 0)');
});

it('should use fallback when variable inside a color not found', async () => {
container.innerHTML = multiline(
'<style>',
' h1 {',
' background: rgb(var(--bg, 255, 0, 0));',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')).backgroundColor).toBe('rgb(204, 0, 0)');
});

it('should handle variables in constructed colors that refer to other vars', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --v255: 255;',
' --red: var(--v255), 0, 0;',
' --bg: var(--unknown, var(--red));',
' --text: var(--red);',
' }',
'</style>',
'<style>',
' h1 {',
' background: rgb(var(--bg));',
' color: rgb(var(--text));',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')).color).toBe('rgb(255, 26, 26)');
expect(getComputedStyle(container.querySelector('h1')).backgroundColor).toBe('rgb(204, 0, 0)');
});

it('should handle variables that refer to constructed colors', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --red: 255, 0, 0;',
' --blue: 0, 0, 255;',
' --rgb-blue: rgb(var(--blue));',
' --bg: rgb(var(--red));',
' --text: var(--rgb-blue);',
' }',
'</style>',
'<style>',
' h1 {',
' background: var(--bg);',
' color: var(--text);',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')).backgroundColor).toBe('rgb(204, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')).color).toBe('rgb(51, 125, 255)');
});

it('should handle variables that refer to constructed colors asynchronously', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --red: 255, 0, 0;',
' --bg: rgb(var(--red));',
' }',
' h1 {',
' color: var(--text);',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const anotherStyle = document.createElement('style');
anotherStyle.textContent = multiline(
':root {',
' --blue: 0, 0, 255;',
' --rgb-blue: rgb(var(--blue));',
' --text: var(--rgb-blue);',
'}',
'h1 {',
' background: var(--bg);',
'}',
);
container.append(anotherStyle);
await timeout(0);
expect(getComputedStyle(container.querySelector('h1')).backgroundColor).toBe('rgb(204, 0, 0)');
expect(getComputedStyle(container.querySelector('h1')).color).toBe('rgb(51, 125, 255)');
});

it('should handle variables that are both contructed and usual colors', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --red: 255, 0, 0;',
' --bg: green;',
' }',
' h1 {',
' --bg: rgb(var(--red));',
' background: var(--bg)',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')).backgroundColor).toBe('rgb(204, 0, 0)');
});

it('should handle cyclic references in constructed colors', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --bg: var(--text);',
' --text: var(--bg);',
' }',
'</style>',
'<style>',
' h1 {',
' background: rgb(var(--bg));',
' color: rgb(var(--text));',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
expect(getComputedStyle(container.querySelector('h1')).color).toBe('rgb(255, 255, 255)');
expect(getComputedStyle(container.querySelector('h1')).backgroundColor).toBe('rgb(0, 0, 0)');
});

it('should handle variables inside border color values', async () => {
container.innerHTML = multiline(
'<style>',
' :root {',
' --red: 255, 0, 0;',
' }',
'</style>',
'<style>',
' h1 {',
' border: 1px solid rgb(var(--red));',
' }',
'</style>',
'<h1>Colors with variables inside</h1>',
);
createOrUpdateDynamicTheme(theme, null, false);
const elementStyle = getComputedStyle(container.querySelector('h1'));
if (isFirefox) {
expect(elementStyle.borderTopColor).toBe('rgb(179, 0, 0)');
expect(elementStyle.borderRightColor).toBe('rgb(179, 0, 0)');
expect(elementStyle.borderBottomColor).toBe('rgb(179, 0, 0)');
expect(elementStyle.borderLeftColor).toBe('rgb(179, 0, 0)');
} else {
expect(elementStyle.borderColor).toBe('rgb(179, 0, 0)');
}
});

it('should handle media variables', () => {
container.innerHTML = multiline(
'<style>',
Expand Down

0 comments on commit c994e30

Please sign in to comment.