Skip to content

Commit 370dfd1

Browse files
committed
feat: Block Object asSource methods take optional Block scope.
1 parent b953602 commit 370dfd1

15 files changed

+112
-126
lines changed

ARCHITECTURE.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ We can easily conceptualize the `RewriteMapping` data for each element in develo
175175
```javascript
176176
// For Element 1:
177177
// - `.class-0` is always applied
178-
// - `:scope[state|active]` is *only* applied when `isActive` is true
178+
// - `.class-0[state|active]` is *only* applied when `isActive` is true
179179
const el1Classes = [
180180
"block__class-0",
181181
isActive && "block__class-0--active"

packages/@css-blocks/core/src/Analyzer/validations/property-conflict-validator.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ function printRulesetConflict(prop: string, rule: Ruleset) {
117117
for (let node of nodes) {
118118
let line = node.source.start && `:${node.source.start.line}`;
119119
let column = node.source.start && `:${node.source.start.column}`;
120-
out.push(` ${rule.style.block.name}${rule.style.asSource()} (${rule.file}${line}${column})`);
120+
out.push(` ${rule.style.asSource(true)} (${rule.file}${line}${column})`);
121121
}
122122
return out.join("\n");
123123
}

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

+34-58
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { assertNever, firstOfType, whatever } from "@opticss/util";
2-
import { CompoundSelector, postcssSelectorParser as selectorParser } from "opticss";
1+
import { assertNever, whatever } from "@opticss/util";
2+
import { postcssSelectorParser as selectorParser } from "opticss";
33

44
import { ATTR_PRESENT, AttrToken, ROOT_CLASS, STATE_NAMESPACE } from "../BlockSyntax";
55

@@ -11,21 +11,42 @@ export enum BlockType {
1111
classAttribute,
1212
}
1313

14-
export type NodeAndType = {
15-
blockType: BlockType.attribute | BlockType.classAttribute;
14+
export type RootAttributeNode = {
15+
blockName?: string;
16+
blockType: BlockType.attribute;
1617
node: selectorParser.Attribute;
17-
} | {
18-
blockType: BlockType.root | BlockType.class;
19-
node: selectorParser.ClassName | selectorParser.Pseudo;
20-
} | {
21-
blockType: BlockType.block;
22-
node: selectorParser.Tag;
2318
};
2419

25-
export type BlockNodeAndType = NodeAndType & {
20+
export type ClassAttributeNode = {
21+
blockName?: string;
22+
blockType: BlockType.classAttribute;
23+
node: selectorParser.Attribute;
24+
};
25+
26+
export type AttributeNode = RootAttributeNode | ClassAttributeNode;
27+
28+
export type RootClassNode = {
29+
blockName?: string;
30+
blockType: BlockType.root;
31+
node: selectorParser.Pseudo;
32+
};
33+
34+
export type BlockClassNode = {
35+
blockName?: string;
36+
blockType: BlockType.class;
37+
node: selectorParser.ClassName;
38+
};
39+
40+
export type ClassNode = RootClassNode | BlockClassNode;
41+
42+
export type BlockNode = {
2643
blockName?: string;
44+
blockType: BlockType.block;
45+
node: selectorParser.Tag;
2746
};
2847

48+
export type NodeAndType = AttributeNode | ClassNode | BlockNode;
49+
2950
/** Extract an Attribute's value from a `selectorParser` attribute selector */
3051
function attrValue(attr: selectorParser.Attribute): string {
3152
if (attr.value) {
@@ -76,7 +97,7 @@ export function isExternalBlock(object: NodeAndType): boolean {
7697
* on the root element.
7798
* @param object The NodeAndType's descriptor object.
7899
*/
79-
export function isRootLevelObject(object: NodeAndType): boolean {
100+
export function isRootLevelObject(object: NodeAndType): object is RootAttributeNode | RootClassNode {
80101
return object.blockType === BlockType.root || object.blockType === BlockType.attribute;
81102
}
82103

@@ -85,7 +106,7 @@ export function isRootLevelObject(object: NodeAndType): boolean {
85106
* on an element contained by the root, not the root itself.
86107
* @param object The CompoundSelector's descriptor object.
87108
*/
88-
export function isClassLevelObject(object: NodeAndType): boolean {
109+
export function isClassLevelObject(object: NodeAndType): object is ClassAttributeNode | BlockClassNode {
89110
return object.blockType === BlockType.class || object.blockType === BlockType.classAttribute;
90111
}
91112

@@ -106,48 +127,3 @@ export const isClassNode = selectorParser.isClassName;
106127
export function isAttributeNode(node: selectorParser.Node): node is selectorParser.Attribute {
107128
return selectorParser.isAttribute(node) && node.namespace === STATE_NAMESPACE;
108129
}
109-
110-
/**
111-
* Similar to assertBlockObject except it doesn't check for well-formedness
112-
* and doesn't ensure that you get a block object when not a legal selector.
113-
* @param sel The `CompoundSelector` to search.
114-
* @return Returns the block's name, type and node.
115-
*/
116-
export function getBlockNode(sel: CompoundSelector): BlockNodeAndType | null {
117-
let blockName = sel.nodes.find(n => n.type === selectorParser.TAG);
118-
let r = firstOfType(sel.nodes, isRootNode);
119-
if (r) {
120-
return {
121-
blockName: blockName && blockName.value,
122-
blockType: BlockType.root,
123-
node: r,
124-
};
125-
}
126-
let s = firstOfType(sel.nodes, isAttributeNode);
127-
if (s) {
128-
let prev = s.prev();
129-
if (prev && isClassNode(prev)) {
130-
return {
131-
blockName: blockName && blockName.value,
132-
blockType: BlockType.classAttribute,
133-
node: s,
134-
};
135-
} else {
136-
return {
137-
blockName: blockName && blockName.value,
138-
blockType: BlockType.attribute,
139-
node: s,
140-
};
141-
}
142-
}
143-
let c = firstOfType(sel.nodes, isClassNode);
144-
if (c) {
145-
return {
146-
blockName: blockName && blockName.value,
147-
blockType: BlockType.class,
148-
node: c,
149-
};
150-
} else {
151-
return null;
152-
}
153-
}

packages/@css-blocks/core/src/BlockParser/features/construct-block.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Block, BlockClass, Style } from "../../BlockTree";
44
import * as errors from "../../errors";
55
import { selectorSourceLocation as loc, sourceLocation } from "../../SourceLocation";
66
import {
7-
BlockNodeAndType,
87
BlockType,
98
NodeAndType,
109
blockTypeName,
@@ -238,7 +237,7 @@ function assertValidSelector(block: Block, rule: postcss.Rule, selector: ParsedS
238237
* @param rule The full `postcss.Rule` for nice error reporting.
239238
* @return Returns the block's name, type and node.
240239
*/
241-
function assertBlockObject(block: Block, sel: CompoundSelector, rule: postcss.Rule, file: string): BlockNodeAndType {
240+
function assertBlockObject(block: Block, sel: CompoundSelector, rule: postcss.Rule, file: string): NodeAndType {
242241

243242
// If selecting a block or tag, check that the referenced block has been imported.
244243
// Otherwise, referencing a tag name is not allowed in blocks, throw an error.
@@ -325,7 +324,7 @@ function assertBlockObject(block: Block, sel: CompoundSelector, rule: postcss.Ru
325324
}
326325
if (!found) {
327326
throw new errors.InvalidBlockSyntax(
328-
`States without an explicit :scope or class selector are not yet supported: ${rule.selector}`,
327+
`States without an explicit :scope or class selector are not supported: ${rule.selector}`,
329328
loc(file, rule, n),
330329
);
331330
} else if (found.blockType === BlockType.class || found.blockType === BlockType.classAttribute) {

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

+9-2
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,15 @@ export class AttrValue extends Style<AttrValue, Block, Attribute, never> {
7474
return this._sourceAttributes.slice();
7575
}
7676

77-
asSource(): string {
78-
return this.parent.asSource(this.value);
77+
/**
78+
* Export as original AttrValue name.
79+
* @param scope Optional scope to resolve this name relative to. If `true`, return the Block name instead of `:scope`. If a Block object, return with the local name instead of `:scope`.
80+
* @returns String representing original AttrValue path.
81+
*/
82+
asSource(scope?: Block | boolean): string {
83+
let namespace = this.attribute.namespace ? `${this.attribute.namespace}|` : "";
84+
let value = (this.value && this.value !== ATTR_PRESENT) ? `=${this.value}` : "";
85+
return this.attribute.blockClass.asSource(scope) + `[${namespace}${this.parent.name}${value}]`;
7986
}
8087

8188
public cssClass(config: ResolvedConfiguration): string {

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

+4-14
Original file line numberDiff line numberDiff line change
@@ -121,25 +121,15 @@ export class Attribute extends Inheritable<Attribute, Block, BlockClass, AttrVal
121121
return resolved;
122122
}
123123

124-
/**
125-
* @returns The bare Attribute selector with no qualifying `BlockClass` name.
126-
*/
127-
unqualifiedSource(value?: string): string {
128-
let namespace = this.token.namespace ? `${this.token.namespace}|` : "";
129-
value = (value && value !== ATTR_PRESENT) ? `=${value}` : "";
130-
return `[${namespace}${this.token.name}${value}]`;
131-
}
132-
133124
/**
134125
* Retrieve this Attribute's selector as it appears in the Block source code.
135126
*
136-
* @param value If provided, it is used as the Attribute's value whether or not
137-
* it is allowed by the known AttrValues (this is useful for constructing
138-
* error messages).
127+
* @param scope Optional scope to resolve this name relative to. If `true`, return the Block name instead of `:scope`. If a Block object, return with the local name instead of `:scope`.
139128
* @returns The Attribute's attribute selector.
140129
*/
141-
asSource(value?: string): string {
142-
return this.blockClass.asSource() + this.unqualifiedSource(value);
130+
asSource(scope?: Block | boolean): string {
131+
let namespace = this.token.namespace ? `${this.token.namespace}|` : "";
132+
return this.blockClass.asSource(scope) + `[${namespace}${this.token.name}]`;
143133
}
144134

145135
/**

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

+14-1
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,22 @@ export class BlockClass extends Style<BlockClass, Block, Block, Attribute> {
154154

155155
/**
156156
* Export as original class name.
157+
* @param scope Optional scope to resolve this name relative to. If `true`, return the Block name instead of `:scope`. If a Block object, return with the local name instead of `:scope`.
157158
* @returns String representing original class.
158159
*/
159-
public asSource(): string { return this.isRoot ? ROOT_CLASS : `.${this.name}`; }
160+
public asSource(scope?: Block | boolean): string {
161+
let blockName = this.block.name;
162+
163+
if (scope instanceof Block) {
164+
blockName = scope.getReferencedBlockLocalName(this.block) || blockName;
165+
}
166+
167+
if (scope && scope !== this.block) {
168+
return this.isRoot ? blockName : `${blockName}.${this.name}`;
169+
}
170+
171+
return this.isRoot ? ROOT_CLASS : `.${this.name}`;
172+
}
160173

161174
/**
162175
* Emit analysis attributes for the class value this

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ export abstract class Style<
4545

4646
/**
4747
* Return the source selector this `Style` was read from.
48+
* @param scope Optional scope to resolve this name relative to. If `true`, return the Block name instead of `:scope`. If a Block object, return with the local name instead of `:scope`.
4849
* @returns The source selector.
4950
*/
50-
public abstract asSource(): string;
51+
public abstract asSource(scope?: Root | boolean): string;
5152

5253
/**
5354
* Return an attribute for analysis using the authored source syntax.

packages/@css-blocks/core/test/global-states-test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class BlockInheritance extends BEMProcessor {
3333
);
3434
});
3535
}
36-
@test "Global state usage must be specify a global state if present"() {
36+
@test "Global state usage must specify a global state, not just a block name."() {
3737
let { imports, config } = setupImporting();
3838
imports.registerSource(
3939
"app.block.css",

packages/@css-blocks/core/test/syntax-test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ export class StraightJacket extends BEMProcessor {
424424
[state|foo] { display: block; }`;
425425
return assertError(
426426
cssBlocks.InvalidBlockSyntax,
427-
"States without an explicit :scope or class selector are not yet supported: [state|foo]" +
427+
"States without an explicit :scope or class selector are not supported: [state|foo]" +
428428
" (foo/bar/illegal-class-combinator.css:2:21)",
429429
this.process(filename, inputCSS));
430430
}

0 commit comments

Comments
 (0)