Skip to content

Commit 7a0150d

Browse files
author
Timothy Lindvall
committed
feat: Parse and set block-interface-index
- When processing definition files, recognize and set the interface index on style nodes in the Block. As with block-class, if each style node found has not declared an interface-index by the end of processing, it's an error. - Test coverage for ensuring block-interface-index is declared.
1 parent 2236f4f commit 7a0150d

File tree

4 files changed

+205
-3
lines changed

4 files changed

+205
-3
lines changed

packages/@css-blocks/core/src/BlockParser/BlockParser.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Options, ResolvedConfiguration, resolveConfiguration } from "../configu
66
import { CssBlockError } from "../errors";
77
import { FileIdentifier } from "../importing";
88

9+
import { addInterfaceIndexes } from "./features/add-interface-indexes";
910
import { addPresetSelectors } from "./features/add-preset-selectors";
1011
import { assertForeignGlobalAttribute } from "./features/assert-foreign-global-attribute";
1112
import { composeBlock } from "./features/composes-block";
@@ -152,7 +153,9 @@ export class BlockParser {
152153
// Find any block-class rules and override the class name of the block with its value.
153154
debug(" - Process Preset Block Classes");
154155
await addPresetSelectors(configuration, root, block, debugIdent);
155-
// TODO: Process block-interface-index declarations. (And inherited-styles???)
156+
debug(" - Process Preset Interface Indexes");
157+
await addInterfaceIndexes(configuration, root, block, debugIdent);
158+
// TODO: Process inherited-styles?
156159
}
157160

158161
// Return our fully constructed block.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { postcss } from "opticss";
2+
3+
import { Block } from "../../BlockTree";
4+
import { Configuration } from "../../configuration";
5+
import { CssBlockError } from "../../errors";
6+
import { sourceRange } from "../../SourceLocation";
7+
import { getStyleTargets } from "../block-intermediates";
8+
import { stripQuotes } from "../utils";
9+
10+
/**
11+
* Traverse a definition file's rules and define a preset block-class for each
12+
* rule present. This ensures the block uses the same CSS class as defined in
13+
* the linked Compiled CSS file associated with this definition file.
14+
*
15+
* If a given rule does not have a block-class declared, an error is added to
16+
* the block for the user to correct.
17+
*
18+
* This should only be run on definition files! Standard block files aren't
19+
* allowed to define block-class rules.
20+
*
21+
* @param configuration - The current CSS Blocks configuration.
22+
* @param root - The root of the AST that this block was generated from.
23+
* @param block - The block that's being generated.
24+
* @param file - The definition file this block was generated from.
25+
*/
26+
export function addInterfaceIndexes(configuration: Configuration, root: postcss.Root, block: Block, file: string) {
27+
// For each rule declared in the file...
28+
root.walkRules(rule => {
29+
30+
// Find the block-class declaration...
31+
rule.walkDecls("block-interface-index", decl => {
32+
const val = stripQuotes(decl.value);
33+
34+
// The value of block-interface-index should be numeric.
35+
const parsedIndex = parseInt(val, 10);
36+
if (isNaN(parsedIndex)) {
37+
block.addError(
38+
new CssBlockError(
39+
"block-interface-index must be a number.",
40+
sourceRange(configuration, root, file, decl),
41+
),
42+
);
43+
}
44+
45+
// Set the index on the related style node.
46+
const parsedSelectors = block.getParsedSelectors(rule);
47+
parsedSelectors.forEach(sel => {
48+
const styleTarget = getStyleTargets(block, sel.key);
49+
if (styleTarget.blockAttrs.length > 0) {
50+
styleTarget.blockAttrs[0].index = parsedIndex;
51+
} else if (styleTarget.blockClasses.length > 0) {
52+
styleTarget.blockClasses[0].index = parsedIndex;
53+
} else {
54+
throw new Error(`Couldn\'t find style node corresponding to selector ${sel}. This shouldn't happen.`);
55+
}
56+
});
57+
});
58+
});
59+
60+
// At this point, every style node should have a fixed block-class.
61+
block.all(true).forEach(styleNode => {
62+
if (!styleNode.wasIndexReset) {
63+
block.addError(
64+
new CssBlockError(
65+
`Style node ${styleNode.asSource()} doesn't have a preset interface index after parsing definition file. You may need to declare this style node in the definition file.`,
66+
{
67+
filename: file,
68+
},
69+
),
70+
);
71+
}
72+
});
73+
}

packages/@css-blocks/core/src/BlockTree/Style.ts

+40-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,45 @@ export abstract class Style<
3131

3232
// tslint:disable-next-line:prefer-unknown-to-any
3333
public abstract readonly rulesets: RulesetContainer<any>;
34-
public readonly index: number;
34+
35+
/**
36+
* The interface index for this style node. A default one is provided
37+
* when the style node is constructed, but may be reset to a specific
38+
* value later (such as when reading the block-interface-index decl
39+
* in definition files).
40+
*/
41+
private _index: number;
42+
43+
/**
44+
* Whether the interface index for this style node was set to a specific
45+
* value after it was created. Used for error detection when parsing
46+
* definition files.
47+
*/
48+
private _wasIndexReset = false;
49+
50+
/**
51+
* The interface index for this style node.
52+
*/
53+
get index() {
54+
return this._index;
55+
}
56+
57+
/**
58+
* Sets the interface index for this style node to a specific value.
59+
*/
60+
set index(val: number) {
61+
this._index = val;
62+
this._wasIndexReset = true;
63+
}
64+
65+
/**
66+
* Whether the interface index for this style node has been reset from
67+
* its original value (usually from a declared block-interface-index in
68+
* a definition file).
69+
*/
70+
get wasIndexReset() {
71+
return this._wasIndexReset;
72+
}
3573

3674
/**
3775
* The preset selector for this particular class node.
@@ -44,7 +82,7 @@ export abstract class Style<
4482
*/
4583
constructor(name: string, parent: Parent, index: number) {
4684
super(name, parent);
47-
this.index = index;
85+
this._index = index;
4886
}
4987

5088
/**

packages/@css-blocks/core/test/BlockParser/definition-file-processing-test.ts

+88
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,27 @@ export class DefinitionFileProcessing extends BEMProcessor {
112112
);
113113
}
114114

115+
@test "It errors out if :scope selector doesn't declare a block-interface-index"() {
116+
const registry = new MockImportRegistry();
117+
registerReferencedSources(registry);
118+
119+
const filename = "foo/bar/nav.block.css";
120+
const inputCss = `@block-syntax-version: 1;
121+
:scope { block-id: "7d97e"; block-name: nav; block-class: "nav-7d97e"; }`;
122+
const parseConfig = {
123+
importer: registry.importer(),
124+
};
125+
const mockConfigOpts = {
126+
dfnFiles: [filename],
127+
};
128+
129+
return assertError(
130+
CssBlockError,
131+
"Style node :scope doesn't have a preset interface index after parsing definition file. You may need to declare this style node in the definition file. (foo/bar/nav.block.css)",
132+
this.process(filename, inputCss, parseConfig, mockConfigOpts),
133+
);
134+
}
135+
115136
@test "It errors out if an element doesn't declare a block class"() {
116137
const registry = new MockImportRegistry();
117138
registerReferencedSources(registry);
@@ -135,6 +156,29 @@ export class DefinitionFileProcessing extends BEMProcessor {
135156
);
136157
}
137158

159+
@test "It errors out if an element doesn't declare an interface-index"() {
160+
const registry = new MockImportRegistry();
161+
registerReferencedSources(registry);
162+
163+
const filename = "foo/bar/nav.block.css";
164+
const inputCss = `@block-syntax-version: 1;
165+
:scope { block-id: "7d97e"; block-class: nav-7d97e; block-name: nav; block-interface-index: 0; }
166+
.entry { block-class: nav-7d97e__entry; }
167+
.entry[active] { block-interface-index: 2; block-class: nav-7d97e__entry--active;}`;
168+
const parseConfig = {
169+
importer: registry.importer(),
170+
};
171+
const mockConfigOpts = {
172+
dfnFiles: [filename],
173+
};
174+
175+
return assertError(
176+
CssBlockError,
177+
"Style node .entry doesn't have a preset interface index after parsing definition file. You may need to declare this style node in the definition file. (foo/bar/nav.block.css)",
178+
this.process(filename, inputCss, parseConfig, mockConfigOpts),
179+
);
180+
}
181+
138182
@test "It errors out if a modifier doesn't declare a block class"() {
139183
const registry = new MockImportRegistry();
140184
registerReferencedSources(registry);
@@ -158,6 +202,29 @@ export class DefinitionFileProcessing extends BEMProcessor {
158202
);
159203
}
160204

205+
@test "It errors out if a modifier doesn't declare an interface index"() {
206+
const registry = new MockImportRegistry();
207+
registerReferencedSources(registry);
208+
209+
const filename = "foo/bar/nav.block.css";
210+
const inputCss = `@block-syntax-version: 1;
211+
:scope { block-id: "7d97e"; block-class: nav-7d97e; block-name: nav; block-interface-index: 0; }
212+
.entry { block-interface-index: 1; block-class: nav-7d97e__entry; }
213+
.entry[active] { block-class: nav-7d97e__entry--active; }`;
214+
const parseConfig = {
215+
importer: registry.importer(),
216+
};
217+
const mockConfigOpts = {
218+
dfnFiles: [filename],
219+
};
220+
221+
return assertError(
222+
CssBlockError,
223+
"Style node .entry[active] doesn't have a preset interface index after parsing definition file. You may need to declare this style node in the definition file. (foo/bar/nav.block.css)",
224+
this.process(filename, inputCss, parseConfig, mockConfigOpts),
225+
);
226+
}
227+
161228
@test "It errors out if a declared block-class isn't a valid class name"() {
162229
const registry = new MockImportRegistry();
163230
registerReferencedSources(registry);
@@ -178,4 +245,25 @@ export class DefinitionFileProcessing extends BEMProcessor {
178245
this.process(filename, inputCss, parseConfig, mockConfigOpts),
179246
);
180247
}
248+
249+
@test "It errors out if a declared block-interface-index isn't a valid number"() {
250+
const registry = new MockImportRegistry();
251+
registerReferencedSources(registry);
252+
253+
const filename = "foo/bar/nav.block.css";
254+
const inputCss = `@block-syntax-version: 1;
255+
:scope { block-id: "7d97e"; block-class: nav-7d97e; block-name: nav; block-interface-index: hello; }`;
256+
const parseConfig = {
257+
importer: registry.importer(),
258+
};
259+
const mockConfigOpts = {
260+
dfnFiles: [filename],
261+
};
262+
263+
return assertError(
264+
CssBlockError,
265+
"block-interface-index must be a number. (foo/bar/nav.block.css:2:92)",
266+
this.process(filename, inputCss, parseConfig, mockConfigOpts),
267+
);
268+
}
181269
}

0 commit comments

Comments
 (0)