Skip to content

Commit

Permalink
feat(template-api): Rewrite static classes as static instead of as dy…
Browse files Browse the repository at this point in the history
…namic.
  • Loading branch information
chriseppstein committed Nov 20, 2017
1 parent 5fed46c commit 237b042
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 29 deletions.
47 changes: 46 additions & 1 deletion packages/template-api/src/Selectable.ts
Expand Up @@ -223,6 +223,11 @@ export abstract class AttributeBase implements HasNamespace {
return this._value;
}

isNamed(name: string, ns: string | null = null): boolean {
return this.namespaceURL === ns &&
this.name === name;
}

constants(condition?: AttributeValue): Set<string> {
if (condition === undefined && this._constants !== undefined) {
return this._constants;
Expand Down Expand Up @@ -306,7 +311,35 @@ export abstract class AttributeBase implements HasNamespace {
}
}

isAmbiguous(condition = this.value): boolean {
isStatic(value: string | null, condition = this.value): boolean | undefined {
if (isAbsent(condition)) {
return value === null ? true : undefined;
} else if (isConstant(condition)) {
return condition.constant === value ? true : undefined;
} else if (isChoice(condition)) {
let constants = this.constants(condition);
if (value && constants.has(value)) {
return false;
} else if (!value) {
return condition.oneOf.find((c => isAbsent(c))) ? false : undefined;
} else {
return undefined;
}
} else if (isUnknown(condition) || isUnknownIdentifier(condition)) {
return false;
} else if (isSet(condition)) {
return condition.allOf.reduce<undefined | boolean>((prev, a) => {
let r = this.isStatic(value, a);
if (r === undefined) return prev;
if (prev === undefined) return r;
return prev && r;
}, undefined);
} else {
return false;
}
}

isAmbiguous(condition = this.value): condition is ValueUnknown | ValueStartsWith | ValueEndsWith | ValueStartsAndEndsWith | AttributeValueChoice | AttributeValueSet {
if (isUnknown(condition)) {
return true;
} else if (isUnknownIdentifier(condition)) {
Expand Down Expand Up @@ -528,6 +561,18 @@ export abstract class TagnameBase implements HasNamespace {
return this._value;
}

isStatic(): boolean {
if (isConstant(this.value)) {
return true;
} else if (isTagnameValueChoice(this.value)) {
return false;
} else if (isUnknown(this.value)) {
return false;
} else {
return assertNever(this.value);
}
}

valueToString(): string {
if (isUnknown(this.value)) {
return "???";
Expand Down
98 changes: 73 additions & 25 deletions packages/template-api/src/StyleMapping.ts
Expand Up @@ -3,7 +3,9 @@ import {
} from "typescript-collections";
import {
IdentityDictionary,
assertNever
assertNever,
ItemType,
unionInto,
} from "@opticss/util";
import {
Attribute as SelectableAttribute,
Expand All @@ -19,10 +21,18 @@ import {
isUnknown,
isUnknownIdentifier,
} from './Selectable';
import { BooleanExpression, AndExpression, isOrExpression } from "./BooleanExpression";
import { BooleanExpression, AndExpression, OrExpression, isOrExpression, isAndExpression, isNotExpression } from "./BooleanExpression";
import { TemplateIntegrationOptions } from "./TemplateIntegrationOptions";

export type RewriteableAttrName = "id" | "class";
export interface RewriteInformation<InfoType> {
id: InfoType;
class: InfoType;
}

export type RewriteableAttrName = keyof RewriteInformation<any>;

export const REWRITE_ATTRS = new Array<RewriteableAttrName>("id", "class");
Object.freeze(REWRITE_ATTRS);

export interface DynamicExpressions {
[outputValue: string]: BooleanExpression<number> | undefined;
Expand All @@ -37,26 +47,14 @@ export interface RewriteMapping {
/**
* output attributes that are always on the element independent of any dynamic changes.
*/
staticAttributes: {
/**
* The id attribute will have at most one value unless the element
* analysis is invalid.
*/
id?: string[];
class?: string[];
};
staticAttributes: RewriteInformation<string[]>;

/**
* The numbers in the boolean expressions represents indexes into the inputAttributes array.
* For attributes that are not whitespace delimited (E.g. id) only one value
* will evaluate to true unless the element analysis itself is invalid.
*/
dynamicAttributes: {
/**
* At most, one id value will evaluate to true unless the element analysis
* is invalid.
*/
id?: DynamicExpressions;
class?: DynamicExpressions;
};
dynamicAttributes: RewriteInformation<DynamicExpressions>;
}

export interface SimpleTagname {
Expand Down Expand Up @@ -164,7 +162,7 @@ export class StyleMapping {
}
}
}
getRewriteOf(from: SimpleAttribute): SimpleAttribute | undefined {
private getRewriteOf(from: SimpleAttribute): SimpleAttribute | undefined {
return this.replacedAttributes.getValue(from);
}
private getInputs(element: ElementInfo): Array<SimpleTagname | SimpleAttribute> {
Expand Down Expand Up @@ -193,12 +191,11 @@ export class StyleMapping {
}
return inputs;
}

rewriteMapping(element: ElementInfo): RewriteMapping {
let inputs = this.getInputs(element);
let dynamicAttributes = {
id: <DynamicExpressions>{},
class: <DynamicExpressions>{}
};
let staticAttributes: RewriteInformation<Set<string>> = {id: new Set<string>(), class: new Set<string>()};
let dynamicAttributes: RewriteMapping['dynamicAttributes'] = {id: {}, class: {}};
for (let i = 0; i < inputs.length; i++) {
let input = inputs[i];
if (isSimpleTagname(input)) continue;
Expand Down Expand Up @@ -239,10 +236,14 @@ export class StyleMapping {
dynamicAttributes[this.toRewritableAttrName(linked.to.name)][linked.to.value] = dynExpr;
}
}
for (let key of REWRITE_ATTRS) {
let extracted = extractStatic(element, inputs, dynamicAttributes[key]);
unionInto(staticAttributes[key], extracted);
}
}
return {
inputs,
staticAttributes: {id: [], class: []},
staticAttributes: {id: [...staticAttributes.id], class: [...staticAttributes.class]},
dynamicAttributes
};
}
Expand Down Expand Up @@ -336,3 +337,50 @@ function attributeMultiDictionary<V>(
function attrToKey(attr: SimpleAttribute): string {
return `${attr.ns || ''}|${attr.name}=${attr.value}`;
}

function extractStatic(element: ElementInfo, inputs: RewriteMapping["inputs"], dyn: DynamicExpressions): Array<string> {
let result = new Array<string>();
for (let v of Object.keys(dyn)) {
let expr = dyn[v]!;
let staticValue = isStatic(expr, inputs, element);
if (staticValue === true) {
result.push(v);
delete dyn[v];
}
}
return result;
}

function isStatic(
value: BooleanExpression<number> | number,
inputs: RewriteMapping["inputs"],
element: ElementInfo
): boolean | undefined {
if (typeof value === 'number') {
return isStaticOnElement(inputs[value], element);
} else if (isAndExpression(value) || isOrExpression(value)) {
let values = ((<AndExpression<number>>value).and || (<OrExpression<number>>value).or);
return values.reduce<undefined|boolean>(((prev, a) => {
let result = isStatic(a, inputs, element);
if (prev === undefined) return result;
if (result === undefined) return prev;
return prev && result;
}), undefined);
} else if (isNotExpression(value)) {
return isStatic(value.not, inputs, element);
} else {
return assertNever(value);
}
}

function isStaticOnElement(input: ItemType<RewriteMapping["inputs"]>, element: ElementInfo): boolean | undefined {
if (isSimpleTagname(input)) {
return element.tagname.isStatic();
} else if (isSimpleAttribute(input)) {
let attribute = element.attributes.find(a => a.isNamed(input.name, input.ns));
if (!attribute) throw new Error("internal error");
return attribute.isStatic(input.value) !== false;
} else {
return assertNever(input);
}
}
48 changes: 45 additions & 3 deletions packages/template-api/test/style-mapping-test.ts
@@ -1,5 +1,5 @@
import { and } from '../src/BooleanExpression';
import { Attribute, AttributeValue, Element, Tagname, Value as v } from '../src/Selectable';
import { Attribute, Element, Tagname, Value as v } from '../src/Selectable';
import { SimpleAttribute, StyleMapping } from '../src/StyleMapping';
import {
assert,
Expand Down Expand Up @@ -44,21 +44,41 @@ function element(tag: string, ...attrs: Array<Attribute>): Element {

@suite("StyleMapping")
export class StyleMappingTest {
@test "can rewrite an attribute"() {
@test "can rewrite a static attribute"() {
let mapping = new StyleMapping(normalizeTemplateOptions({}));
mapping.rewriteAttribute(sClass("test"), sClass("a"));
let rewrite = mapping.rewriteMapping(element("?", attr("class", "test")));
let staticClasses = rewrite.staticAttributes.class;
assert(staticClasses.find(c => c === "a"), "rewritten class is missing");
assert.isUndefined(staticClasses.find(c => c === "test"), "rewritten source class is present");
assert.deepEqual(rewrite.dynamicAttributes, {id: {}, class: {}});
}
@test "can rewrite a dynamic attribute"() {
let mapping = new StyleMapping(normalizeTemplateOptions({}));
mapping.rewriteAttribute(sClass("test"), sClass("a"));
let rewrite = mapping.rewriteMapping(element("?", attr("class", "(test|---)")));
assertNotNull(rewrite).and(rewrite => {
let dyn = rewrite.dynamicAttributes.class!["a"];
assertDefined(dyn).and(dyn => {
assert.deepEqual(dyn, and(0));
});
});
}
@test "can link an attribute"() {
@test "can link a static attribute"() {
let mapping = new StyleMapping(normalizeTemplateOptions({}));
mapping.linkAttributes(sClass("a"), [{existing: [sClass("test")], unless: []}]);
let rewrite = mapping.rewriteMapping(element("?", attr("class", "test")));
assertNotNull(rewrite).and(rewrite => {
assert.deepEqual(rewrite.inputs[0], {name: "class", value: "test"});
let staticClasses = rewrite.staticAttributes.class;
assert(staticClasses.find(c => c === "a"), "linked class is missing");
assert(staticClasses.find(c => c === "test"), "source class is missing");
});
}
@test "can link a dynamic attribute"() {
let mapping = new StyleMapping(normalizeTemplateOptions({}));
mapping.linkAttributes(sClass("a"), [{existing: [sClass("test")], unless: []}]);
let rewrite = mapping.rewriteMapping(element("?", attr("class", "(test|---)")));
assertNotNull(rewrite).and(rewrite => {
assert.deepEqual(rewrite.inputs[0], {name: "class", value: "test"});
let dyn = rewrite.dynamicAttributes.class!["a"];
Expand All @@ -81,4 +101,26 @@ export class StyleMappingTest {
assert.equal(rewrite.staticAttributes.class!.length, 0);
});
}
@test "static attributes are static in the rewrite"() {
let mapping = new StyleMapping(normalizeTemplateOptions({}));
mapping.linkAttributes(sClass("a"), [{existing: [sClass("s_a")], unless: []}]);
mapping.linkAttributes(sClass("a"), [{existing: [sClass("s_b")], unless: []}]);
mapping.attributeIsObsolete(sClass("s_a"));
let rewrite = mapping.rewriteMapping(element("?", attr("class", 's_a')));
assert.deepEqual(rewrite.dynamicAttributes, { id: {}, class: {}});
assert.deepEqual(rewrite.staticAttributes, {id: [], class: ['a']});
}
@test "classes can be both static and dynamic"() {
let mapping = new StyleMapping(normalizeTemplateOptions({}));
mapping.linkAttributes(sClass("a"), [{existing: [sClass("s_a")], unless: []}]);
mapping.linkAttributes(sClass("b"), [{existing: [sClass("s_b")], unless: []}]);
mapping.attributeIsObsolete(sClass("s_a"));
mapping.attributeIsObsolete(sClass("s_b"));
let rewrite = mapping.rewriteMapping(element("?", attr("class", 's_a (s_b|---)')));
let inputs = (<SimpleAttribute[]>rewrite.inputs).map(a => a.value);

assert.deepEqual(inputs, ["s_a", "s_b"]);
assert.deepEqual(rewrite.staticAttributes, {id: [], class: ['a']});
assert.deepEqual(rewrite.dynamicAttributes, { id: {}, class: {'b': {and: [1]}}});
}
}

0 comments on commit 237b042

Please sign in to comment.