Skip to content

Commit

Permalink
Optimize expressions.
Browse files Browse the repository at this point in the history
  • Loading branch information
nsaunders committed Jun 19, 2024
1 parent f072c28 commit c927c28
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 84 deletions.
110 changes: 29 additions & 81 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,64 +81,6 @@ export type Condition<S> =
| { or: Condition<S>[]; and?: undefined; not?: undefined }
| { not: Condition<S>; and?: undefined; or?: undefined };

type LogicalExpression<S> =
| { tag: "just"; value: S }
| {
tag: "and" | "or";
left: LogicalExpression<S>;
right: LogicalExpression<S>;
}
| { tag: "not"; value: LogicalExpression<S> };

function conditionToLogicalExpression<S>(
condition: Condition<S>,
): LogicalExpression<S> {
if (
typeof condition !== "object" ||
condition === null ||
(!("and" in condition) && !("or" in condition) && !("not" in condition))
) {
return { tag: "just", value: condition as S };
}

if (condition.and) {
const [first, ...rest] = condition.and;
if (first) {
return rest.reduce<LogicalExpression<S>>(
(acc, curr) => ({
tag: "and",
left: acc,
right: conditionToLogicalExpression(curr),
}),
conditionToLogicalExpression(first),
);
}
}

if (condition.or) {
const [first, ...rest] = condition.or;
if (first) {
return rest.reduce<LogicalExpression<S>>(
(acc, curr) => ({
tag: "or",
left: acc,
right: conditionToLogicalExpression(curr),
}),
conditionToLogicalExpression(first),
);
}
}

if (condition.not) {
return {
tag: "not",
value: conditionToLogicalExpression(condition.not),
};
}

throw new Error(`Invalid condition: ${JSON.stringify(condition)}`);
}

/**
* Represents a hook implementation consisting of either a basic CSS selector or an at-rule.
*
Expand Down Expand Up @@ -308,33 +250,39 @@ export function createLocalConditions<
] as Condition<HookId>);

return (function buildExpression(
condition: LogicalExpression<HookId>,
condition: Condition<HookId>,
valueIfTrue: string,
valueIfFalse: string,
): string {
switch (condition.tag) {
case "just":
return `var(--${condition.value}-1, ${valueIfTrue}) var(--${condition.value}-0, ${valueIfFalse})`;
case "and":
return buildExpression(
condition.left,
buildExpression(condition.right, valueIfTrue, valueIfFalse),
valueIfFalse,
);
case "or":
return buildExpression(
condition.left,
valueIfTrue,
buildExpression(condition.right, valueIfTrue, valueIfFalse),
);
case "not":
return buildExpression(condition.value, valueIfFalse, valueIfTrue);
if (typeof condition === "string") {
return `var(--${condition}-1, ${valueIfTrue}) var(--${condition}-0, ${valueIfFalse})`;
}
})(
conditionToLogicalExpression<HookId>(condition),
valueIfTrue,
valueIfFalse,
);
if (condition.and) {
const [head, ...tail] = condition.and;
if (!head) {
return valueIfTrue;
}
if (tail.length === 0) {
return buildExpression(head, valueIfTrue, valueIfFalse);
}
return buildExpression(
head,
buildExpression({ and: tail }, valueIfTrue, valueIfFalse),
valueIfFalse,
);
}
if (condition.or) {
return buildExpression(
{ and: condition.or.map(not => ({ not })) },
valueIfFalse,
valueIfTrue,
);
}
if (condition.not) {
return buildExpression(condition.not, valueIfFalse, valueIfTrue);
}
throw new Error(`Invalid condition: ${JSON.stringify(condition)}`);
})(condition, valueIfTrue, valueIfFalse);
},
};
}
Expand Down
57 changes: 54 additions & 3 deletions packages/react/test/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ function hex(value: string) {

async function withBrowser<T>(f: (browser: Browser) => Promise<T>): Promise<T> {
const browser = await puppeteer.launch();
const result = await f(browser);
await browser.close();
return result;
try {
return await f(browser);
} finally {
await browser.close();
}
}

async function withPage<T>(f: (page: Page) => Promise<T>): Promise<T> {
Expand Down Expand Up @@ -300,6 +302,55 @@ test('reusable "not" condition', async () => {
assert.strictEqual(hex(hoverStyle.color), "#ff0000");
});

test("reusable complex condition", async () => {
const { StyleSheet, hooks } = createHooks(["&.a", "&.b", "&.c"]);
const Box = createComponent({
conditions: createConditions(hooks, {
condition: {
or: [
{ and: [{ not: { or: ["&.a", "&.b", "&.c"] } }] },
{ and: ["&.a", "&.b", { not: "&.c" }] },
{ and: [{ not: "&.a" }, "&.b", "&.c"] },
],
},
}),
styleProps: createStyleProps(["color"]),
});
await withPage(async page => {
await renderContent(
page,
<>
<StyleSheet />
{["", "a", "b", "c", "a b", "b c", "a c", "a b c"].map(
(className, index) => (
<Box
key={index}
id={`case${index}`}
className={className}
color="#000000"
condition:color="#009900"
/>
),
)}
</>,
);
for (const caseId of [1, 2, 3, 6, 7]) {
assert.strictEqual(
hex((await queryComputedStyle(page, `#case${caseId}`)).color),
"#000000",
`case ${caseId}`,
);
}
for (const caseId of [0, 4, 5]) {
assert.strictEqual(
hex((await queryComputedStyle(page, `#case${caseId}`)).color),
"#009900",
`case ${caseId}`,
);
}
});
});

test('inline "and" condition', async () => {
const { StyleSheet, hooks } = createHooks(["&:enabled", "&:hover"]);
const Box = createComponent({
Expand Down

0 comments on commit c927c28

Please sign in to comment.