Skip to content

Commit 50e84d1

Browse files
committed
feat: Basic runtime style calculations working.
1 parent cabd495 commit 50e84d1

File tree

4 files changed

+247
-8
lines changed

4 files changed

+247
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../src/AggregateRewriteData.ts

packages/@css-blocks/ember-app/runtime/app/services/css-blocks.ts

+240-3
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,250 @@
22
import Helper from "@ember/component/helper";
33
/// @ts-ignore
44
import Service from "@ember/service";
5+
import type { ObjectDictionary } from "@opticss/util";
56

67
/// @ts-ignore
7-
import { data } from "./-css-blocks-data";
8+
import { data as _data } from "./-css-blocks-data";
9+
import type { AggregateRewriteData, StyleExpression } from "./AggregateRewriteData";
10+
11+
const data: AggregateRewriteData = _data;
12+
13+
type ClassNameExpression = Array<string | number | boolean | null>;
14+
15+
function nextVal(args: ClassNameExpression, type: "number", allowNull: boolean, allowUndefined: false): number | null;
16+
function nextVal(args: ClassNameExpression, type: "number", allowNull: boolean, allowUndefined: boolean): number | null | undefined;
17+
function nextVal(args: ClassNameExpression, type: "string", allowNull: boolean, allowUndefined: boolean): string | null | undefined;
18+
function nextVal(args: ClassNameExpression, type: "string" | "number", allowNull: boolean, allowUndefined: boolean): string | number | boolean | null | undefined {
19+
if (args.length === 0) {
20+
throw new Error("empty argument stack");
21+
}
22+
let v = args.pop();
23+
if (v === undefined) {
24+
if (allowUndefined) {
25+
return undefined;
26+
} else {
27+
throw new Error(`Unexpected undefined value encountered.`);
28+
}
29+
}
30+
if (v === null) {
31+
if (allowNull) {
32+
return v;
33+
} else {
34+
throw new Error(`Unexpected null value encountered.`);
35+
}
36+
}
37+
if (typeof v === type) {
38+
return v;
39+
}
40+
throw new Error(`Expected ${type} got ${v}`);
41+
}
42+
43+
function num(args: ClassNameExpression): number;
44+
function num(args: ClassNameExpression, allowNull: false): number;
45+
function num(args: ClassNameExpression, allowNull: true): number | null;
46+
function num(args: ClassNameExpression, allowNull = false): number | null {
47+
return nextVal(args, "number", allowNull, false);
48+
}
49+
50+
function str(args: ClassNameExpression): string;
51+
function str(args: ClassNameExpression, allowNull: false): string;
52+
function str(args: ClassNameExpression, allowNull: false, allowUndefined: false): string;
53+
function str(args: ClassNameExpression, allowNull: true): string | null;
54+
function str(args: ClassNameExpression, allowNull: true, allowUndefined: false): string | null;
55+
function str(args: ClassNameExpression, allowNull: false, allowUndefined: true): string | undefined;
56+
function str(args: ClassNameExpression, allowNull: true, allowUndefined: true): string | null | undefined;
57+
function str(args: ClassNameExpression, allowNull = false, allowUndefined = false): string | null | undefined {
58+
return nextVal(args, "string", allowNull, allowUndefined);
59+
}
60+
61+
/**
62+
* interprets the next value as a truthy and coerces it to a boolean.
63+
*/
64+
function bool(args: ClassNameExpression): boolean {
65+
return !!args.pop();
66+
}
67+
68+
/**
69+
* Throws an error if the value is null or undefined.
70+
* @param val a value that should not be null or undefined.
71+
* @param msg The error message
72+
*/
73+
function assert(val: unknown, msg: string) {
74+
// I'm using double equals here on purpose for the type coercion.
75+
// tslint:disable-next-line:triple-equals
76+
if (val == undefined) throw new Error(msg);
77+
}
78+
79+
enum Operator {
80+
AND = 1,
81+
OR = 2,
82+
NOT = 3,
83+
}
84+
85+
enum Condition {
86+
static = 1,
87+
toggle = 2,
88+
ternary = 3,
89+
switch = 4,
90+
}
91+
92+
const enum FalsySwitchBehavior {
93+
error = 0,
94+
unset = 1,
95+
default = 2,
96+
}
897

998
// tslint:disable-next-line:no-default-export
1099
export default class CSSBlocksService extends Service {
11-
classNamesFor(_args: Array<string | number | boolean | null>): string {
12-
return data.className;
100+
classNamesFor(argv: Array<string | number | boolean | null>): string {
101+
let args = argv.slice();
102+
console.log(args);
103+
args.reverse(); // pop() is faster than shift()
104+
let rewriteVersion = num(args);
105+
if (rewriteVersion > 1) throw new Error(`The rewrite is newer than expected. Please upgrade @css-blocks/ember-app.`);
106+
107+
let numBlocks = num(args);
108+
// the values in blockStyleIndices map style strings to an index into the array
109+
// stored at the same index of blockStyleIds. That means that
110+
// `blockStyleIds[i][blockStyleIndices[i][":scope"]]` returns the globally
111+
// unique id for the ":scope" style of the runtime selected block.
112+
let blockStyleIndices = new Array<ObjectDictionary<number>>();
113+
// When null, it indicates a missing style. This can happen when the block
114+
// that implements an interface did not fully implement that interface
115+
// because the block it implements has changed since the implementing block
116+
// was precompiled and released.
117+
let blockStyleIds = new Array<Array<number | null>>();
118+
while (numBlocks--) {
119+
let sourceGuid = str(args);
120+
let runtimeGuid = str(args, true); // this won't be non-null until we implement block passing.
121+
let blockIndex = data.blockIds[sourceGuid];
122+
let runtimeBlockIndex = runtimeGuid === null ? blockIndex : data.blockIds[runtimeGuid];
123+
let blockInfo = data.blocks[blockIndex];
124+
blockStyleIndices.push(blockInfo.blockInterfaceStyles);
125+
let styleIds = blockInfo.implementations[runtimeBlockIndex];
126+
assert(styleIds, "unknown implementation");
127+
blockStyleIds.push(styleIds);
128+
}
129+
130+
// Now we build a list of styles ids. these styles are referred to in the
131+
// class name expression by using an index into the `styles` array that
132+
// we're building.
133+
let numStyles = num(args);
134+
let styles = new Array<number | null>();
135+
while (numStyles--) {
136+
let block = num(args);
137+
let style = str(args);
138+
styles.push(blockStyleIds[block][blockStyleIndices[block][style]]);
139+
}
140+
141+
// Now we calculate the runtime javascript state of these styles.
142+
// we start with all of the styles as "off" and can turn them on
143+
// by setting the corresponding index of a `style` entry in `styleStates`
144+
// to true.
145+
let numConditions = num(args);
146+
let styleStates = new Array<boolean>(styles.length);
147+
while (numConditions--) {
148+
let condition = num(args);
149+
switch (condition) {
150+
case Condition.static:
151+
// static styles are always enabled.
152+
styleStates[num(args)] = true;
153+
break;
154+
case Condition.toggle:
155+
// Can enable a single style
156+
let b = bool(args);
157+
let numStyles = num(args);
158+
while (numStyles--) {
159+
let s = num(args);
160+
if (b) {
161+
styleStates[s] = true;
162+
}
163+
}
164+
break;
165+
case Condition.ternary:
166+
// Ternary supports multiple styles being enabled when true
167+
// and multiple values being enabled when false.
168+
let result = bool(args);
169+
let numIfTrue = num(args);
170+
while (numIfTrue--) {
171+
let s = num(args);
172+
if (result) styleStates[s] = true;
173+
}
174+
let numIfFalse = num(args);
175+
while (numIfFalse--) {
176+
let s = num(args);
177+
if (!result) styleStates[s] = true;
178+
}
179+
break;
180+
case Condition.switch:
181+
let falsyBehavior = num(args);
182+
let currentValue = str(args, true, true);
183+
let numValues = num(args);
184+
let found = false;
185+
let legal = new Array<string>();
186+
while (numValues--) {
187+
let v = str(args);
188+
legal.push(v);
189+
let match = (v === currentValue);
190+
found = found || match;
191+
let numStyles = num(args);
192+
while (numStyles--) {
193+
let s = num(args);
194+
if (match) styleStates[s] = true;
195+
}
196+
}
197+
if (!found) {
198+
if (!currentValue) {
199+
if (falsyBehavior === FalsySwitchBehavior.error) {
200+
throw new Error(`A value is required.`);
201+
}
202+
} else {
203+
throw new Error(`"${currentValue} is not a known attribute value. Expected one of: ${legal.join(", ")}`);
204+
}
205+
}
206+
break;
207+
default:
208+
throw new Error(`Unknown condition type ${condition}`);
209+
}
210+
}
211+
let stylesApplied = new Set<number>();
212+
for (let i = 0; i < styles.length; i++) {
213+
if (styleStates[i] && styles[i] !== null) {
214+
stylesApplied.add(styles[i]!);
215+
}
216+
}
217+
// TODO: style inference
218+
let classNameIndices = new Set<number>();
219+
for (let [clsIdx, expr] of data.optimizations) {
220+
if (evaluateExpression(expr, stylesApplied)) {
221+
classNameIndices.add(clsIdx);
222+
}
223+
}
224+
let classNames = new Array<string>();
225+
for (let idx of classNameIndices) {
226+
classNames.push(data.outputClassnames[idx]);
227+
}
228+
let result = classNames.join(" ");
229+
console.log(result);
230+
return result;
231+
}
232+
}
233+
234+
function evaluateExpression(expr: StyleExpression, stylesApplied: Set<number>): boolean {
235+
if (typeof expr === "number") return stylesApplied.has(expr);
236+
if (expr[0] === Operator.AND) {
237+
for (let i = 1; i < expr.length; i++) {
238+
if (!evaluateExpression(expr[i], stylesApplied)) return false;
239+
}
240+
return true;
241+
} else if (expr[0] === Operator.OR) {
242+
for (let i = 1; i < expr.length; i++) {
243+
if (evaluateExpression(expr[i], stylesApplied)) return true;
244+
}
245+
return false;
246+
} else if (expr[0] === Operator.NOT) {
247+
return !evaluateExpression(expr[1], stylesApplied);
248+
} else {
249+
return false;
13250
}
14251
}

packages/@css-blocks/ember/runtime/app/helpers/-css-blocks.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default class CSSBlocksHelper extends Helper {
1212
@service("css-blocks")
1313
cssBlocks!: CSSBlocksService;
1414

15-
compute(...args: Array<string | number | boolean | null>) {
16-
return this.cssBlocks.classNamesFor(args);
15+
compute(...args: [Array<string | number | boolean | null>, object]) {
16+
return this.cssBlocks.classNamesFor(args[0]);
1717
}
1818
}

packages/@css-blocks/ember/src/TemplateAnalyzingRewriter.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ const enum StyleCondition {
3131
}
3232

3333
const enum FalsySwitchBehavior {
34-
error,
35-
unset,
36-
default,
34+
error = 0,
35+
unset = 1,
36+
default = 2,
3737
}
3838

3939
const NOOP_VISITOR = {};
@@ -362,6 +362,7 @@ class HelperInvocationGenerator {
362362
params.push(num(FalsySwitchBehavior.unset));
363363
}
364364
params.push(this.mustacheToStringExpression(this.builders, switchStyle.stringExpression!));
365+
params.push(num(values.length));
365366
for (let value of values) {
366367
let obj = switchStyle.group[value];
367368
params.push(str(value));

0 commit comments

Comments
 (0)