Skip to content

Commit

Permalink
Revamp custom property reference resolution (#44)
Browse files Browse the repository at this point in the history
Reworks how the StyleX native shim parses and resolves values. Replaes
the previous implementation of CSS variable resolution.

Close #44
  • Loading branch information
vincentriemer authored and necolas committed Mar 10, 2024
1 parent 5597943 commit 73b1b6a
Show file tree
Hide file tree
Showing 13 changed files with 740 additions and 189 deletions.
81 changes: 81 additions & 0 deletions flow-typed/npm/postcss-value-parser_vx.x.x.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/

type PostCSSValueASTNode =
| {
type: 'word' | 'unicode-range',
value: string,
sourceIndex: number,
sourceEndIndex: number
}
| {
type: 'string' | 'comment',
value: string,
quote: '"' | "'",
sourceIndex: number,
sourceEndIndex: number,
unclosed?: boolean
}
| {
type: 'comment',
value: string,
sourceIndex: number,
sourceEndIndex: number,
unclosed?: boolean
}
| {
type: 'div',
value: ',' | '/' | ':',
sourceIndex: number,
sourceEndIndex: number,
before: '' | ' ' | ' ' | ' ',
after: '' | ' ' | ' ' | ' '
}
| {
type: 'space',
value: ' ' | ' ' | ' ',
sourceIndex: number,
sourceEndIndex: number
}
| {
type: 'function',
value: string,
before: '' | ' ' | ' ' | ' ',
after: '' | ' ' | ' ' | ' ',
nodes: Array<PostCSSValueASTNode>,
unclosed?: boolean,
sourceIndex: number,
sourceEndIndex: number
};

declare interface PostCSSValueAST {
nodes: Array<PostCSSValueASTNode>;
walk(
callback: (PostCSSValueASTNode, number, PostCSSValueAST) => ?false,
bubble?: boolean
): void;
}

type PostCSSValueParser = {
(string): PostCSSValueAST,
unit(string): { number: string, unit: string } | false,
stringify(
nodes: PostCSSValueAST | PostCSSValueASTNode | PostCSSValueAST['nodes'],
custom?: (PostCSSValueASTNode) => string
): string,
walk(
ast: PostCSSValueAST,
callback: (PostCSSValueASTNode, number, PostCSSValueAST) => ?false,
bubble?: boolean
): void
};

declare module 'postcss-value-parser' {
declare module.exports: PostCSSValueParser;
}
3 changes: 2 additions & 1 deletion package-lock.json

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

3 changes: 2 additions & 1 deletion packages/react-strict-dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
},
"dependencies": {
"@stylexjs/stylex": "0.5.1",
"css-mediaquery": "0.1.2"
"css-mediaquery": "0.1.2",
"postcss-value-parser": "^4.1.0"
},
"devDependencies": {
"@stylexjs/babel-plugin": "0.5.1"
Expand Down

This file was deleted.

6 changes: 3 additions & 3 deletions packages/react-strict-dom/src/native/stylex/CSSMediaQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@ export class CSSMediaQuery {
}

query: string;
matchedStyle: { [string]: mixed };
matchedStyle: { +[string]: mixed };

constructor(query: string, matchedStyle: { [string]: mixed }) {
constructor(query: string, matchedStyle: { +[string]: mixed }) {
this.query = query.replace(MQ_PREFIX, '');
this.matchedStyle = matchedStyle;
}

resolve(matchObject: MatchObject): { [string]: mixed } {
resolve(matchObject: MatchObject): { +[string]: mixed } {
const { width, height, direction } = matchObject;
const matches = mediaQuery.match(this.query, {
width,
Expand Down
106 changes: 106 additions & 0 deletions packages/react-strict-dom/src/native/stylex/customProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/

import { CSSUnparsedValue } from './typed-om/CSSUnparsedValue';
import { CSSVariableReferenceValue } from './typed-om/CSSVariableReferenceValue';

export type MutableCustomProperties = { [string]: string | number };
export type CustomProperties = $ReadOnly<MutableCustomProperties>;

function camelize(s: string): string {
return s.replace(/-./g, (x) => x.toUpperCase()[1]);
}

function normalizeVariableName(name: string): string {
if (!name.startsWith('--')) {
throw new Error("Invalid variable name, must begin with '--'");
}
return camelize(name.substring(2));
}

export function stringContainsVariables(input: string): boolean {
return input.includes('var(');
}

function resolveVariableReferenceValue(
propName: string,
variable: CSSVariableReferenceValue,
propertyRegistry: CustomProperties
) {
const variableName = normalizeVariableName(variable.variable);
const fallbackValue = variable.fallback;

let variableValue: string | number | null = propertyRegistry[variableName];

// Perform variable resolution on the variable's resolved value if it itself
// contains variables
if (
typeof variableValue === 'string' &&
stringContainsVariables(variableValue)
) {
variableValue = resolveVariableReferences(
propName,
CSSUnparsedValue.parse(propName, variableValue),
propertyRegistry
);
}

if (variableValue != null) {
return variableValue;
} else if (fallbackValue != null) {
const resolvedFallback = resolveVariableReferences(
propName,
fallbackValue,
propertyRegistry
);
if (resolvedFallback != null) {
return resolvedFallback;
}
}

console.error(
`React Strict DOM: Unrecognized custom property "${variable.variable}"`
);
return null;
}

// Takes a CSSUnparsedValue and registry of variable values and resolves it down to a string
export function resolveVariableReferences(
propName: string,
propValue: CSSUnparsedValue,
propertyRegistry: CustomProperties
): string | number | null {
const result: Array<string | number> = [];
for (const value of propValue.values()) {
if (value instanceof CSSVariableReferenceValue) {
const resolvedValue = resolveVariableReferenceValue(
propName,
value,
propertyRegistry
);
if (resolvedValue == null) {
// Failure to resolve a css variable in a value means the entire value is unparsable so we bail out and
// resolve the entire value as null
return null;
}
result.push(resolvedValue);
} else {
result.push(value);
}
}

// special case for signular number value
if (result.length === 1 && typeof result[0] === 'number') {
return result[0];
}

// consider empty string as a null value
const output = result.join('').trim();
return output === '' ? null : output;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

type InlineStyle = {
[key: string]: mixed
+[key: string]: mixed
};

type StylesArray<+T> = T | $ReadOnlyArray<StylesArray<T>>;
Expand Down
Loading

0 comments on commit 73b1b6a

Please sign in to comment.