Skip to content

Commit 282f90d

Browse files
committed
fix: Extract StyleEvaluator, StyleResolver classes from runtime service.
1 parent 89dff62 commit 282f90d

File tree

5 files changed

+587
-257
lines changed

5 files changed

+587
-257
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
import { AggregateRewriteData } from "./AggregateRewriteData";
2+
3+
export type ClassNameExpression = Array<string | number | boolean | null>;
4+
type Defined<T> = T extends undefined ? never : T;
5+
6+
/**
7+
* Throws an error if the value is null or undefined.
8+
* @param val a value that should not be null or undefined.
9+
* @param msg The error message
10+
*/
11+
function assert<T>(val: T, msg: string): asserts val is Defined<T> {
12+
// I'm using double equals here on purpose for the type coercion.
13+
// tslint:disable-next-line:triple-equals
14+
if (val == undefined) throw new Error(msg);
15+
}
16+
17+
const enum Condition {
18+
static = 1,
19+
toggle = 2,
20+
ternary = 3,
21+
switch = 4,
22+
}
23+
24+
const enum FalsySwitchBehavior {
25+
error = 0,
26+
unset = 1,
27+
default = 2,
28+
}
29+
30+
export class StyleEvaluator {
31+
data: AggregateRewriteData;
32+
args: ClassNameExpression;
33+
index = 0;
34+
// the values in blockStyleIndices map style strings to an index into the array
35+
// stored at the same index of blockStyleIds. That means that
36+
// `blockStyleIds[i][blockStyleIndices[i][":scope"]]` returns the globally
37+
// unique id for the ":scope" style of the runtime selected block.
38+
blockStyleIndices: Array<Record<string, number>> = [];
39+
// When null, it indicates a missing style. This can happen when the block
40+
// that implements an interface did not fully implement that interface
41+
// because the block it implements has changed since the implementing block
42+
// was precompiled and released.
43+
blockStyleIds: Array<Array<number | null>> = [];
44+
styles: Array<number | null> = [];
45+
constructor(data: AggregateRewriteData, args: ClassNameExpression) {
46+
this.data = data;
47+
this.args = args;
48+
}
49+
50+
evaluateBlocks() {
51+
let numBlocks = this.num();
52+
while (numBlocks--) {
53+
let sourceGuid = this.str();
54+
let runtimeGuid = this.str(true); // this won't be non-null until we implement block passing.
55+
let blockIndex = this.data.blockIds[sourceGuid];
56+
assert(blockIndex, `unknown block ${sourceGuid}`);
57+
let runtimeBlockIndex = runtimeGuid === null ? blockIndex : this.data.blockIds[runtimeGuid];
58+
let blockInfo = this.data.blocks[blockIndex];
59+
this.blockStyleIndices.push(blockInfo.blockInterfaceStyles);
60+
let styleIds = blockInfo.implementations[runtimeBlockIndex];
61+
assert(styleIds, "unknown implementation");
62+
this.blockStyleIds.push(styleIds);
63+
}
64+
}
65+
66+
evaluateStyles() {
67+
// Now we build a list of styles ids. these styles are referred to in the
68+
// class name expression by using an index into the `styles` array that
69+
// we're building.
70+
let numStyles = this.num();
71+
while (numStyles--) {
72+
let block = this.num();
73+
let style = this.str();
74+
this.styles.push(this.blockStyleIds[block][this.blockStyleIndices[block][style]]);
75+
}
76+
}
77+
evaluateToggleCondition(styleStates: Array<boolean>) {
78+
// Can enable a single style
79+
let b = this.bool();
80+
let numStyles = this.num();
81+
while (numStyles--) {
82+
let s = this.num();
83+
if (b) {
84+
styleStates[s] = true;
85+
}
86+
}
87+
}
88+
89+
evaluate(): Set<number> {
90+
let rewriteVersion = this.num();
91+
if (rewriteVersion > 0) throw new Error(`The rewrite schema is newer than expected. Please upgrade @css-blocks/ember-app.`);
92+
93+
this.evaluateBlocks();
94+
this.evaluateStyles();
95+
96+
// Now we calculate the runtime javascript state of these styles.
97+
// we start with all of the styles as "off" and can turn them on
98+
// by setting the corresponding index of a `style` entry in `styleStates`
99+
// to true.
100+
let numConditions = this.num();
101+
let styleStates = new Array<boolean>(this.styles.length);
102+
while (numConditions--) {
103+
let condition = this.num();
104+
switch (condition) {
105+
case Condition.static:
106+
// static styles are always enabled.
107+
styleStates[this.num()] = true;
108+
break;
109+
case Condition.toggle:
110+
this.evaluateToggleCondition(styleStates);
111+
break;
112+
case Condition.ternary:
113+
this.evaluateTernaryCondition(styleStates);
114+
break;
115+
case Condition.switch:
116+
this.evaluateSwitchCondition(styleStates);
117+
break;
118+
default:
119+
throw new Error(`Unknown condition type ${condition}`);
120+
}
121+
}
122+
123+
let stylesApplied = new Set<number>();
124+
for (let i = 0; i < this.styles.length; i++) {
125+
if (styleStates[i] && this.styles[i] !== null) {
126+
stylesApplied.add(this.styles[i]!);
127+
}
128+
}
129+
130+
return stylesApplied;
131+
}
132+
evaluateSwitchCondition(styleStates: Array<boolean>) {
133+
let falsyBehavior = this.num();
134+
let currentValue = this.str(true, true);
135+
let numValues = this.num();
136+
let found = false;
137+
let legal: Array<String> = [];
138+
while (numValues--) {
139+
let v = this.str();
140+
legal.push(v);
141+
let match = (v === currentValue);
142+
found = found || match;
143+
let numStyles = this.num();
144+
while (numStyles--) {
145+
let s = this.num();
146+
if (match) styleStates[s] = true;
147+
}
148+
}
149+
if (!found) {
150+
if (!currentValue) {
151+
if (falsyBehavior === FalsySwitchBehavior.error) {
152+
throw new Error(`A value is required.`);
153+
}
154+
} else {
155+
throw new Error(`"${currentValue} is not a known attribute value. Expected one of: ${legal.join(", ")}`);
156+
}
157+
}
158+
}
159+
evaluateTernaryCondition(styleStates: Array<boolean>) {
160+
// Ternary supports multiple styles being enabled when true
161+
// and multiple values being enabled when false.
162+
let result = this.bool();
163+
let numIfTrue = this.num();
164+
while (numIfTrue--) {
165+
let s = this.num();
166+
if (result) styleStates[s] = true;
167+
}
168+
let numIfFalse = this.num();
169+
while (numIfFalse--) {
170+
let s = this.num();
171+
if (!result) styleStates[s] = true;
172+
}
173+
}
174+
175+
nextVal(type: "number", allowNull: boolean, allowUndefined: false): number | null;
176+
nextVal(type: "number", allowNull: boolean, allowUndefined: boolean): number | null | undefined;
177+
nextVal(type: "string", allowNull: boolean, allowUndefined: boolean): string | null | undefined;
178+
nextVal(type: "string" | "number", allowNull: boolean, allowUndefined: boolean): string | number | boolean | null | undefined {
179+
if (this.args.length === 0) {
180+
throw new Error("empty argument stack");
181+
}
182+
let v = this.args[this.index++];
183+
if (v === undefined) {
184+
if (allowUndefined) {
185+
return undefined;
186+
} else {
187+
throw new Error(`Unexpected undefined value encountered.`);
188+
}
189+
}
190+
if (v === null) {
191+
if (allowNull) {
192+
return v;
193+
} else {
194+
throw new Error(`Unexpected null value encountered.`);
195+
}
196+
}
197+
if (typeof v === type) {
198+
return v;
199+
}
200+
throw new Error(`Expected ${type} got ${v}`);
201+
}
202+
203+
num(): number;
204+
num(allowNull: false): number;
205+
num(allowNull: true): number | null;
206+
num(allowNull = false): number | null {
207+
return this.nextVal("number", allowNull, false);
208+
}
209+
210+
str(): string;
211+
str(allowNull: false): string;
212+
str(allowNull: true): string | null;
213+
str(allowNull: false, allowUndefined: false): string;
214+
str(allowNull: false, allowUndefined: true): string | undefined;
215+
str(allowNull: true, allowUndefined: false): string | null;
216+
str(allowNull: true, allowUndefined: true): string | null | undefined;
217+
str(allowNull = false, allowUndefined = false): string | null | undefined {
218+
return this.nextVal("string", allowNull, allowUndefined);
219+
}
220+
221+
/**
222+
* interprets the next value as a truthy and coerces it to a boolean.
223+
*/
224+
bool(): boolean {
225+
return !!this.args[this.index++];
226+
}
227+
}

0 commit comments

Comments
 (0)