Skip to content

Commit d1185db

Browse files
committed
fix: Emit block style composition declarations in a def file.
1 parent 63b06e8 commit d1185db

File tree

2 files changed

+142
-20
lines changed

2 files changed

+142
-20
lines changed

packages/@css-blocks/core/src/BlockCompiler/BlockDefinitionCompiler.ts

+80-16
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import { Dictionary } from "async";
12
import { postcss } from "opticss";
23

3-
import { AttributeSelector, BlockExport, BlockReference, BlockSyntaxVersion, ClassSelector, Declaration, DefinitionAST, DefinitionRoot, ForeignAttributeSelector, GlobalDeclaration, LocalBlockExport, Mapper, Name, Rename, Rule, ScopeSelector, Selector, Visitor, builders, map as mapToDefinition, visit } from "../BlockParser/ast";
4+
import { AttributeSelector, BlockExport, BlockReference, BlockSyntaxVersion, ClassSelector, Declaration, DefinitionAST, DefinitionRoot, ForeignAttributeSelector, GlobalDeclaration, KeyCompoundSelector, LocalBlockExport, Mapper, Name, Rename, Rule, ScopeSelector, Selector, Visitor, builders, map as mapToDefinition, visit } from "../BlockParser/ast";
45
import { BLOCK_GLOBAL } from "../BlockSyntax";
5-
import { Block, BlockClass, Style, isAttrValue, isBlockClass } from "../BlockTree";
6+
import { AttrValue, Block, BlockClass, Style, isAttrValue, isBlockClass } from "../BlockTree";
67
import { ResolvedConfiguration } from "../configuration";
78

89
export const INLINE_DEFINITION_FILE = Symbol("Inline Definition");
@@ -191,34 +192,35 @@ export class BlockDefinitionCompiler {
191192
for (let style of block.all(true)) {
192193
definitionRoot.children.push(this.styleToRule(style, reservedClassNames));
193194
}
195+
definitionRoot.children.push(...this.complexCompositions(block));
194196
return definitionRoot;
195197
}
196198

197199
styleToRule(style: Style, reservedClassNames: Set<string>): Rule<DefinitionAST> {
198200
let selectors = new Array<Selector<DefinitionAST>>();
199201
let blockClass: BlockClass = isAttrValue(style) ? style.blockClass : style;
200-
let elementSelector: ClassSelector | ScopeSelector;
201-
if (blockClass.isRoot) {
202-
elementSelector = builders.scopeSelector();
203-
} else {
204-
elementSelector = builders.classSelector(blockClass.name);
205-
}
206202
if (isAttrValue(style)) {
207-
let attributeSelector: AttributeSelector;
208-
if (style.isPresenceRule) {
209-
attributeSelector = builders.attributeSelector(style.attribute.name);
210-
} else {
211-
attributeSelector = builders.attributeSelector(style.attribute.name, style.value);
212-
}
213-
selectors.push(builders.keyCompoundSelector(elementSelector, [attributeSelector]));
203+
selectors.push(attributeSelectors(blockClass, [style]));
214204
} else {
215-
selectors.push(elementSelector);
205+
selectors.push(elementSelector(blockClass));
216206
}
217207
let declarations = new Array<Declaration>();
218208
if (isBlockClass(style) && style.isRoot) {
219209
declarations.push(builders.declaration("block-id", `"${style.block.guid}"`));
220210
declarations.push(builders.declaration("block-name", `"${style.block.name}"`));
221211
}
212+
213+
let compositions = new Array<string>();
214+
for (let composition of blockClass.composedStyles()) {
215+
if (composition.conditions.length === 0 && blockClass === style) {
216+
compositions.push(composition.path);
217+
} else if (composition.conditions.length === 1 && composition.conditions[0] === style) {
218+
compositions.push(composition.path);
219+
}
220+
}
221+
if (compositions.length > 0) {
222+
declarations.push(builders.declaration("composes", compositions.join(", ")));
223+
}
222224
declarations.push(builders.declaration("block-class", style.cssClass(this.config, reservedClassNames)));
223225
declarations.push(builders.declaration("block-interface-index", style.index.toString()));
224226
let aliasValues = new Array(...style.getStyleAliases());
@@ -227,6 +229,48 @@ export class BlockDefinitionCompiler {
227229
}
228230
return builders.rule(selectors, declarations);
229231
}
232+
/**
233+
* Simple compositions (which apply to a single block class or attribute) are
234+
* processed when we generate the rule for that style. The complex
235+
* compositions which apply to the intersection of more than one attribute
236+
* require the generation of ruleset that targets all of those attributes
237+
* together.
238+
*/
239+
complexCompositions(block: Block): Array<Rule<DefinitionAST>> {
240+
let complexCompositions: Dictionary<{ blockClass: BlockClass; attributes: AttrValue[]; paths: Array<string> }> = {};
241+
for (let blockClass of block.classes) {
242+
// Compositions can have any number of attributes and we need to collate the
243+
// styles being composed for each unique set of attributes. To do this, we
244+
// generate a unique key for each unique set of attributes and store the data
245+
// we need against it.
246+
for (let composition of blockClass.composedStyles()) {
247+
if (composition.conditions.length > 1) {
248+
let key = composition.conditions.map(c => c.index).sort().join(" ");
249+
if (!complexCompositions[key]) {
250+
complexCompositions[key] = {
251+
blockClass,
252+
attributes: composition.conditions,
253+
paths: [composition.path],
254+
};
255+
} else {
256+
complexCompositions[key].paths.push(composition.path);
257+
}
258+
}
259+
}
260+
}
261+
// once we've collated all the compositions by the attributes we generate
262+
// a rule for each distinct set of attributes and put a composes declaration
263+
// in it.
264+
let rules = new Array<Rule<DefinitionAST>>();
265+
for (let key of Object.keys(complexCompositions)) {
266+
let composition = complexCompositions[key];
267+
let selector = attributeSelectors(composition.blockClass, composition.attributes);
268+
let declarations = new Array<Declaration>();
269+
declarations.push(builders.declaration("composes", composition.paths.join(", ")));
270+
rules.push(builders.rule([selector], declarations));
271+
}
272+
return rules;
273+
}
230274

231275
blockReferences(root: postcss.Root, block: Block): void {
232276
block.eachBlockReference((name, _block) => {
@@ -244,3 +288,23 @@ export class BlockDefinitionCompiler {
244288
throw new Error("Method not implemented.");
245289
}
246290
}
291+
292+
function elementSelector(blockClass: BlockClass): ClassSelector | ScopeSelector {
293+
if (blockClass.isRoot) {
294+
return builders.scopeSelector();
295+
} else {
296+
return builders.classSelector(blockClass.name);
297+
}
298+
}
299+
300+
function attributeSelectors(blockClass: BlockClass, attributes: Array<AttrValue>): KeyCompoundSelector<DefinitionAST> {
301+
let attributeSelectors = new Array<AttributeSelector>();
302+
for (let style of attributes) {
303+
if (style.isPresenceRule) {
304+
attributeSelectors.push(builders.attributeSelector(style.attribute.name));
305+
} else {
306+
attributeSelectors.push(builders.attributeSelector(style.attribute.name, style.value));
307+
}
308+
}
309+
return builders.keyCompoundSelector(elementSelector(blockClass), attributeSelectors);
310+
}

packages/@css-blocks/core/test/block-definition-test.ts

+62-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { assert } from "chai";
2-
import { only, suite, test } from "mocha-typescript";
2+
import { suite, test } from "mocha-typescript";
33
import { postcss } from "opticss";
44

55
import * as cssBlocks from "../src";
@@ -45,7 +45,6 @@ export class BlockFactoryTests extends BEMProcessor {
4545
});
4646
}
4747

48-
@only
4948
@test async "can generate a definition"() {
5049
let { imports, factory } = setupImporting();
5150
let filename = "test-block.block.css";
@@ -77,7 +76,6 @@ export class BlockFactoryTests extends BEMProcessor {
7776
`));
7877
}
7978

80-
@only
8179
@test async "can generate a definition with block-global declarations."() {
8280
let { imports, factory } = setupImporting();
8381
let filename = "test-block.block.css";
@@ -98,7 +96,6 @@ export class BlockFactoryTests extends BEMProcessor {
9896
`));
9997
}
10098

101-
@only
10299
@test async "can generate a definition with a block reference"() {
103100
let { imports, factory } = setupImporting();
104101
imports.registerSource(
@@ -141,4 +138,65 @@ export class BlockFactoryTests extends BEMProcessor {
141138
@block bar, (bip, baz as zab) from "./bar.css";
142139
:scope { block-id: "${block.guid}"; block-name: "test-block"; block-class: test-block; block-interface-index: 0 }`));
143140
}
141+
142+
@test async "can generate a definition with style composition"() {
143+
let { imports, factory } = setupImporting();
144+
imports.registerSource(
145+
"foo.block.css",
146+
`:scope[oceanic] { color: blue; }
147+
:scope[forrest] { color: green; }
148+
`,
149+
);
150+
imports.registerSource(
151+
"bar/bip.block.css",
152+
`.orange { color: orange; }`,
153+
);
154+
imports.registerSource(
155+
"bar/baz.block.css",
156+
`:scope { color: red; }`,
157+
);
158+
imports.registerSource(
159+
"bar.block.css",
160+
`@block bip from "./bar/bip.block.css";
161+
@export (default as bip) from "./bar/bip.block.css";
162+
@export (default as baz) from "./bar/baz.block.css";
163+
.lemon {
164+
color: yellow;
165+
}`,
166+
);
167+
let filename = "test-block.block.css";
168+
imports.registerSource(
169+
filename,
170+
`@block foo from "./foo.block.css";
171+
@block bar, (bip, baz as zab) from "./bar.block.css";
172+
:scope {
173+
composes: foo[oceanic];
174+
}
175+
.nav {
176+
composes: foo, bip.orange;
177+
}
178+
.nav[open] {
179+
composes: bar.lemon;
180+
}
181+
.nav[position=top] {
182+
composes: foo[forrest];
183+
}
184+
.nav[position=top][open] {
185+
composes: foo[oceanic], foo[forrest];
186+
}
187+
`,
188+
);
189+
let {block, definitionResult} = await compileBlockWithDefinition(factory, filename);
190+
assert.deepEqual(
191+
clean(definitionResult.css),
192+
clean(`@block-syntax-version 1;
193+
@block foo from "./foo.css";
194+
@block bar, (bip, baz as zab) from "./bar.css";
195+
:scope { block-id: "${block.guid}"; block-name: "test-block"; composes: foo[oceanic]; block-class: test-block; block-interface-index: 0 }
196+
.nav { composes: foo, bip.orange; block-class: test-block__nav; block-interface-index: 1 }
197+
.nav[open] { composes: bar.lemon; block-class: test-block__nav--open; block-interface-index: 3 }
198+
.nav[position="top"] { composes: foo[forrest]; block-class: test-block__nav--position-top; block-interface-index: 5 }
199+
.nav[position="top"][open] { composes: foo[oceanic], foo[forrest] }
200+
`));
201+
}
144202
}

0 commit comments

Comments
 (0)