Skip to content

Commit 0d6e76a

Browse files
author
Timothy Lindvall
committed
feat: Definition ingestion and parsing into block.
- Parse AST from definitions, build a Block. - Check that definitions declare an ID that matches comment header from Compiled CSS file. - Check that definitions declare a block class. - Check that non-definition files don't declare a block-id or block-class. - Update stripQuotes() to allow strings without quotes.
1 parent 983e7c6 commit 0d6e76a

File tree

7 files changed

+178
-19
lines changed

7 files changed

+178
-19
lines changed

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

+58-15
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { RawSourceMap } from "source-map";
66

77
import { Block } from "../BlockTree";
88
import { Options, ResolvedConfiguration, resolveConfiguration } from "../configuration";
9-
import { FileIdentifier, ImportedFile, Importer } from "../importing";
9+
import { FileIdentifier, ImportedCompiledCssFile, ImportedFile, Importer } from "../importing";
1010
import { PromiseQueue } from "../util/PromiseQueue";
1111

1212
import { BlockParser, ParsedSource } from "./BlockParser";
@@ -183,26 +183,31 @@ export class BlockFactory {
183183
private async _getBlockPromiseAsync(identifier: FileIdentifier): Promise<Block> {
184184
try {
185185
let file = await this.importer.import(identifier, this.configuration);
186+
187+
let block;
186188
if (file.type === "ImportedCompiledCssFile") {
187-
// TODO: Process ImportedCompiledCssFile type.
188-
throw new Error("Imported Compiled CSS files aren't supported yet.");
189+
block = await this._reconstituteCompiledCssSource(file);
189190
} else {
190-
let block = await this._importAndPreprocessBlock(file);
191-
debug(`Finalizing Block object for "${block.identifier}"`);
191+
block = await this._importAndPreprocessBlock(file);
192+
}
192193

193-
// last check to make sure we don't return a new instance
194-
if (this.blocks[block.identifier]) {
195-
return this.blocks[block.identifier];
196-
}
194+
debug(`Finalizing Block object for "${block.identifier}"`);
197195

198-
// Ensure this block name is unique.
199-
block.setName(this.getUniqueBlockName(block.name));
196+
// last check to make sure we don't return a new instance
197+
if (this.blocks[block.identifier]) {
198+
return this.blocks[block.identifier];
199+
}
200200

201-
// if the block has any errors, surface them here unless we're in fault tolerant mode.
202-
this._surfaceBlockErrors(block);
203-
this.blocks[block.identifier] = block;
204-
return block;
201+
// Ensure this block name is unique.
202+
// Only need to run this on ImportedFile types.
203+
if (!file.type || file.type === "ImportedFile") {
204+
block.setName(this.getUniqueBlockName(block.name));
205205
}
206+
207+
// if the block has any errors, surface them here unless we're in fault tolerant mode.
208+
this._surfaceBlockErrors(block);
209+
this.blocks[block.identifier] = block;
210+
return block;
206211
} catch (error) {
207212
if (this.preprocessQueue.activeJobCount > 0) {
208213
debug(`Block error. Currently there are ${this.preprocessQueue.activeJobCount} preprocessing jobs. waiting.`);
@@ -295,6 +300,44 @@ export class BlockFactory {
295300
return this.parser.parseSource(source);
296301
}
297302

303+
private async _reconstituteCompiledCssSource(file: ImportedCompiledCssFile): Promise<Block> {
304+
// Maybe we already have this block in cache?
305+
if (this.blocks[file.identifier]) {
306+
debug(`Using pre-compiled Block for "${file.identifier}"`);
307+
return this.blocks[file.identifier];
308+
}
309+
310+
// NOTE: If we had to upgrade the syntax version of a definition file, here's where'd we do that.
311+
// But this isn't a thing we need to do until we have multiple syntax versions.
312+
313+
// NOTE: No need to run preprocessor - we assume that Compiled CSS has already been preprocessed.
314+
// Parse the definition file into an AST
315+
const definitionAst = this.postcssImpl.parse(file.definitionContents);
316+
317+
// Parse the CSS contents into an AST
318+
const cssContentsAst = this.postcssImpl.parse(file.cssContents);
319+
320+
// TODO: Sourcemaps?
321+
322+
// Sanity check! Did we actually get contents for both ASTs?
323+
if (!definitionAst || !definitionAst.nodes) {
324+
throw new Error(`Unable to parse definition file into AST!\nIdentifier: ${file.identifier}`);
325+
}
326+
327+
if (!cssContentsAst || !cssContentsAst.nodes) {
328+
throw new Error(`Unable to parse CSS contents into AST!\nIdentifier: ${file.identifier}`);
329+
}
330+
331+
// Construct a Block out of the definition file.
332+
const block = this.parser.parseDefinitionSource(definitionAst, file.identifier, file.blockId);
333+
334+
// Merge the rules from the CSS contents into the Block.
335+
// TODO: Actually merge the CSS rules in. (^_^")
336+
337+
// And we're done!
338+
return block;
339+
}
340+
298341
/**
299342
* Similar to getBlock(), this imports and parses a block data file. However, this
300343
* method parses a block relative to another block.

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

+23-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import { Block } from "../BlockTree";
55
import { Options, ResolvedConfiguration, resolveConfiguration } from "../configuration";
66
import { FileIdentifier } from "../importing";
77

8+
import { assertBlockClassDeclared } from "./features/assert-block-class-declared";
9+
import { assertBlockIdsMatch } from "./features/assert-block-ids-match";
810
import { assertForeignGlobalAttribute } from "./features/assert-foreign-global-attribute";
911
import { composeBlock } from "./features/composes-block";
1012
import { constructBlock } from "./features/construct-block";
13+
import { disallowDefinitionRules } from "./features/disallow-dfn-rules";
1114
import { disallowImportant } from "./features/disallow-important";
1215
import { discoverName } from "./features/discover-name";
1316
import { exportBlocks } from "./features/export-blocks";
@@ -52,26 +55,44 @@ export class BlockParser {
5255
return block;
5356
}
5457

58+
public async parseDefinitionSource(root: postcss.Root, identifier: string, expectedId: string) {
59+
return await this.parse(root, identifier, undefined, true, expectedId);
60+
}
61+
5562
/**
5663
* Main public interface of `BlockParser`. Given a PostCSS AST, returns a promise
5764
* for the new `Block` object.
5865
* @param root PostCSS AST
5966
* @param sourceFile Source file name
6067
* @param defaultName Name of block
6168
*/
62-
public async parse(root: postcss.Root, identifier: string, name: string): Promise<Block> {
69+
public async parse(root: postcss.Root, identifier: string, name?: string, isDfnFile = false, expectedId?: string): Promise<Block> {
6370
let importer = this.config.importer;
6471
let debugIdent = importer.debugIdentifier(identifier, this.config);
6572
let sourceFile = importer.filesystemPath(identifier, this.config) || debugIdent;
6673
let configuration = this.factory.configuration;
6774
debug(`Begin parse: "${debugIdent}"`);
6875

6976
// Discover the block's preferred name.
70-
name = await discoverName(configuration, root, name, sourceFile);
77+
name = await discoverName(configuration, root, sourceFile, isDfnFile, name);
7178

7279
// Create our new Block object and save reference to the raw AST.
7380
let block = new Block(name, identifier, root);
7481

82+
if (isDfnFile) {
83+
// Rules only checked when parsing definition files...
84+
// Assert that the block-id rule in :scope is declared and matches
85+
// header comment in Compiled CSS.
86+
debug(" - Assert Block IDs Match");
87+
await assertBlockIdsMatch(root, debugIdent, expectedId);
88+
debug(" - Assert Block Class Declared");
89+
await assertBlockClassDeclared(root, debugIdent);
90+
} else {
91+
// If not a definition file, it shouldn't have rules that can
92+
// only be in definition files.
93+
debug(" - Disallow Definition-Only Declarations");
94+
await disallowDefinitionRules(configuration, root, sourceFile);
95+
}
7596
// Throw if we encounter any `!important` decls.
7697
debug(` - Disallow Important`);
7798
await disallowImportant(configuration, root, block, sourceFile);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { postcss } from "opticss";
2+
3+
import { CLASS_NAME_IDENT } from "../../BlockSyntax";
4+
import * as errors from "../../errors";
5+
import { FileIdentifier } from "../../importing";
6+
import { stripQuotes } from "../utils";
7+
8+
export async function assertBlockClassDeclared(root: postcss.Root, identifier: FileIdentifier): Promise<postcss.Root> {
9+
let foundClassDecl = false;
10+
11+
root.walkRules(":scope", (rule) => {
12+
rule.walkDecls("block-class", (decl) => {
13+
const classVal = stripQuotes(decl.value);
14+
if (!CLASS_NAME_IDENT.exec(classVal)) {
15+
throw new errors.InvalidBlockSyntax(
16+
`Illegal block class. '${decl.value}' is not a legal CSS identifier.\nIdentifier: ${identifier}`,
17+
);
18+
}
19+
});
20+
});
21+
22+
if (!foundClassDecl) {
23+
throw new errors.InvalidBlockSyntax(
24+
`Expected block-class to be declared in definition data.\nIdentifier: ${identifier}`,
25+
);
26+
}
27+
28+
return root;
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { postcss } from "opticss";
2+
3+
import * as errors from "../../errors";
4+
import { FileIdentifier } from "../../importing";
5+
import { stripQuotes } from "../utils";
6+
7+
export async function assertBlockIdsMatch(root: postcss.Root, identifier: FileIdentifier, expected?: string): Promise<postcss.Root> {
8+
let foundIdDecl = false;
9+
10+
if (!expected) {
11+
throw new Error(`No expected ID provided.\nIdentifier: ${identifier}`);
12+
}
13+
14+
root.walkRules(":scope", (rule) => {
15+
rule.walkDecls("block-id", (decl) => {
16+
if (stripQuotes(decl.value) !== expected) {
17+
throw new errors.InvalidBlockSyntax(
18+
`Expected block-id property in definition data to match header in Compiled CSS.\nIdentifier: ${identifier}`,
19+
);
20+
}
21+
foundIdDecl = true;
22+
});
23+
});
24+
25+
if (!foundIdDecl) {
26+
throw new errors.InvalidBlockSyntax(
27+
`Expected block-id to be declared in definition data.\nIdentifier: ${identifier}`,
28+
);
29+
}
30+
31+
return root;
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { postcss } from "opticss";
2+
3+
import { Configuration } from "../../configuration";
4+
import * as errors from "../../errors";
5+
import { sourceRange } from "../../SourceLocation";
6+
7+
export async function disallowDefinitionRules(configuration: Configuration, root: postcss.Root, file: string): Promise<postcss.Root> {
8+
root.walkRules((rule) => {
9+
rule.walkDecls((decl) => {
10+
if (decl.prop === "block-id") {
11+
throw new errors.InvalidBlockSyntax(
12+
`block-id is disallowed in source block files.`,
13+
sourceRange(configuration, root, file, decl),
14+
);
15+
} else if (decl.prop === "block-class") {
16+
throw new errors.InvalidBlockSyntax(
17+
`block-id is disallowed in source block files.`,
18+
sourceRange(configuration, root, file, decl),
19+
);
20+
}
21+
});
22+
});
23+
24+
return root;
25+
}

packages/@css-blocks/core/src/BlockParser/features/discover-name.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Configuration } from "../../configuration";
55
import * as errors from "../../errors";
66
import { sourceRange } from "../../SourceLocation";
77

8-
export async function discoverName(configuration: Configuration, root: postcss.Root, defaultName: string, file: string): Promise<string> {
8+
export async function discoverName(configuration: Configuration, root: postcss.Root, file: string, isDfnFile: boolean, defaultName?: string): Promise<string> {
99

1010
// Eagerly fetch custom `block-name` from the root block rule.
1111
root.walkRules(":scope", (rule) => {
@@ -22,5 +22,14 @@ export async function discoverName(configuration: Configuration, root: postcss.R
2222
});
2323
});
2424

25+
// We expect to have a block name by this point. Either we should have found one in the source
26+
// or inferred one from the filename. Definition files must include a blokc-name.
27+
if (!defaultName || defaultName.trim() === "") {
28+
if (isDfnFile) {
29+
throw new errors.InvalidBlockSyntax(`block-name is expected to be declared in definition file's :scope rule.\nIdentifier: ${file}`);
30+
} else {
31+
throw new errors.CssBlockError(`Unable to find or infer a block name.\nIdentifier: ${file}`);
32+
}
33+
}
2534
return defaultName;
2635
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
* @return Result
55
*/
66
export function stripQuotes(str: string): string {
7-
return str.replace(/^(["'])(.+)\1$/, "$2");
7+
return str.replace(/^(["']?)(.+)\1$/, "$2");
88
}

0 commit comments

Comments
 (0)