Skip to content

Commit b9c4938

Browse files
committed
feat: Per block namespaces.
RFC: #332 Don't require the 'state' namespace for state attributes in the block file. When referring to global states, use that external block's local namespace identifier in the state attribute's namespace.
1 parent 3dcd620 commit b9c4938

32 files changed

+539
-549
lines changed

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

+15-15
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { assertNever } from "@opticss/util";
22
import { CompoundSelector, postcssSelectorParser as selectorParser } from "opticss";
33

4-
import { ATTR_PRESENT, AttrToken, ROOT_CLASS, STATE_NAMESPACE } from "../BlockSyntax";
4+
import { ATTR_PRESENT, AttrToken, ROOT_CLASS } from "../BlockSyntax";
55
import { AttrValue, Block, BlockClass } from "../BlockTree";
66

77
export enum BlockType {
8-
block = 1,
98
root,
109
attribute,
1110
class,
@@ -40,13 +39,7 @@ export type BlockClassNode = {
4039

4140
export type ClassNode = RootClassNode | BlockClassNode;
4241

43-
export type BlockNode = {
44-
blockName?: string;
45-
blockType: BlockType.block;
46-
node: selectorParser.Tag;
47-
};
48-
49-
export type NodeAndType = AttributeNode | ClassNode | BlockNode;
42+
export type NodeAndType = AttributeNode | ClassNode;
5043

5144
/** Extract an Attribute's value from a `selectorParser` attribute selector */
5245
function attrValue(attr: selectorParser.Attribute): string {
@@ -76,7 +69,6 @@ export function toAttrToken(attr: selectorParser.Attribute): AttrToken {
7669
export function blockTypeName(t: BlockType, options?: { plural: boolean }): string {
7770
let isPlural = options && options.plural;
7871
switch (t) {
79-
case BlockType.block: return isPlural ? "external blocks" : "external block";
8072
case BlockType.root: return isPlural ? "block roots" : "block root";
8173
case BlockType.attribute: return isPlural ? "root-level states" : "root-level state";
8274
case BlockType.class: return isPlural ? "classes" : "class";
@@ -89,8 +81,8 @@ export function blockTypeName(t: BlockType, options?: { plural: boolean }): stri
8981
* Test if the provided node representation is an external block.
9082
* @param object The NodeAndType's descriptor object.
9183
*/
92-
export function isExternalBlock(object: NodeAndType): boolean {
93-
return object.blockType === BlockType.block;
84+
export function isExternalBlock(object: NodeAndType): object is RootAttributeNode | RootClassNode {
85+
return (object.blockType === BlockType.attribute && !!object.blockName);
9486
}
9587

9688
/**
@@ -122,13 +114,16 @@ export function isRootNode(node: unknown): node is selectorParser.Pseudo {
122114

123115
export const isClassNode = selectorParser.isClassName;
124116

117+
export const RESERVED_NAMESPACES = new Set<string | undefined | true>(["html", "math", "svg"]);
118+
Object.freeze(RESERVED_NAMESPACES);
119+
125120
/**
126121
* Check if given selector node is an attribute selector
127122
* @param node The selector to test.
128123
* @return True if attribute selector, false if not.
129124
*/
130125
export function isAttributeNode(node: selectorParser.Node): node is selectorParser.Attribute {
131-
return selectorParser.isAttribute(node) && node.namespace === STATE_NAMESPACE;
126+
return selectorParser.isAttribute(node) && !RESERVED_NAMESPACES.has(node.namespace);
132127
}
133128

134129
/**
@@ -152,7 +147,12 @@ export function getStyleTargets(block: Block, sel: CompoundSelector): StyleTarge
152147

153148
for (let node of sel.nodes) {
154149
if (isRootNode(node)) {
155-
blockClass = block.rootClass;
150+
let nextNode = node.next();
151+
if (nextNode && isAttributeNode(nextNode) && typeof nextNode.namespace === "string") {
152+
break;
153+
} else {
154+
blockClass = block.rootClass;
155+
}
156156
}
157157
else if (isClassNode(node)) {
158158
blockClass = block.ensureClass(node.value);
@@ -161,7 +161,7 @@ export function getStyleTargets(block: Block, sel: CompoundSelector): StyleTarge
161161
// The fact that a base class exists for all state selectors is
162162
// validated in `assertBlockObject`. BlockClass may be undefined
163163
// here if parsing a global state.
164-
if (!blockClass) { continue; }
164+
if (!blockClass) { break; }
165165
blockAttrs.push(blockClass.ensureAttributeValue(toAttrToken(node)));
166166
}
167167
}

packages/@css-blocks/core/src/BlockParser/features/assert-foreign-global-attribute.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { postcss, postcssSelectorParser as selectorParser } from "opticss";
22

3+
import { ROOT_CLASS } from "../../BlockSyntax";
34
import { Block } from "../../BlockTree";
45
import { Configuration } from "../../configuration";
56
import * as errors from "../../errors";
@@ -25,22 +26,28 @@ export async function assertForeignGlobalAttribute(configuration: Configuration,
2526

2627
// Only test rules that are block references (this is validated in parse-styles and shouldn't happen).
2728
// If node isn't selecting a block, move on
28-
let blockName = sel.nodes.find(n => n.type === selectorParser.TAG);
29-
if (!blockName || !blockName.value) { return; }
29+
let blockName = sel.nodes.find(n => isAttributeNode(n) && n.namespace) as selectorParser.Attribute | undefined;
30+
31+
if (!blockName || !blockName.namespace) { return; }
32+
33+
if (blockName.namespace === true) {
34+
// universal namespace selector was already validated; it won't occur here.
35+
return;
36+
}
3037

3138
for (let node of sel.nodes) {
3239

33-
if (node.type === selectorParser.TAG) { continue; }
40+
if ( node.type === selectorParser.PSEUDO && node.value === ROOT_CLASS) { continue; }
3441

35-
// If selecting something other than an attribute on external block, throw.
42+
// If selecting something other than an attribute on external attribute, throw.
3643
if (!isAttributeNode(node)) {
3744
throw new errors.InvalidBlockSyntax(
38-
`Only global states from other blocks can be used in selectors: ${rule.selector}`,
45+
`Illegal global state selector: ${rule.selector}`,
3946
range(configuration, block.stylesheet, file, rule, node));
4047
}
4148

4249
// If referenced block does not exist, throw.
43-
let otherBlock = block.getReferencedBlock(blockName.value);
50+
let otherBlock = block.getReferencedBlock(blockName.namespace);
4451
if (!otherBlock) {
4552
throw new errors.InvalidBlockSyntax(
4653
`No Block named "${blockName.value}" found in scope: ${rule.selector}`,

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

+41-45
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export async function constructBlock(configuration: Configuration, root: postcss
7676

7777
// If this is an external Style, move on. These are validated
7878
// in `assert-foreign-global-attribute`.
79-
let blockName = sel.nodes.find(n => n.type === selectorParser.TAG);
79+
let blockName = sel.nodes.find(n => isAttributeNode(n) && n.namespace );
8080
if (blockName) {
8181
sel = sel.next && sel.next.selector;
8282
continue;
@@ -226,24 +226,19 @@ function assertValidSelector(configuration: Configuration, block: Block, rule: p
226226
*/
227227
function assertBlockObject(configuration: Configuration, block: Block, sel: CompoundSelector, rule: postcss.Rule, file: string): NodeAndType {
228228

229-
// If selecting a block or tag, check that the referenced block has been imported.
230-
// Otherwise, referencing a tag name is not allowed in blocks, throw an error.
231-
let blockName = sel.nodes.find(selectorParser.isTag);
232-
if (blockName) {
233-
let refBlock = block.getReferencedBlock(blockName.value);
234-
if (!refBlock) {
235-
throw new errors.InvalidBlockSyntax(
236-
`Tag name selectors are not allowed: ${rule.selector}`,
237-
range(configuration, block.stylesheet, file, rule, blockName),
238-
);
239-
}
229+
let tagNode = sel.nodes.find(selectorParser.isTag);
230+
if (tagNode) {
231+
throw new errors.InvalidBlockSyntax(
232+
`Tag name selectors are not allowed: ${rule.selector}`,
233+
range(configuration, block.stylesheet, file, rule, tagNode),
234+
);
240235
}
241236

242237
// Targeting attributes that are not state selectors is not allowed in blocks, throw.
243238
let nonStateAttribute = sel.nodes.find(n => selectorParser.isAttribute(n) && !isAttributeNode(n));
244239
if (nonStateAttribute) {
245240
throw new errors.InvalidBlockSyntax(
246-
`Cannot select attributes other than states: ${rule.selector}`,
241+
`Cannot select attributes in the \`${selectorParser.isAttribute(nonStateAttribute) && nonStateAttribute.namespaceString}\` namespace: ${rule.selector}`,
247242
range(configuration, block.stylesheet, file, rule, nonStateAttribute),
248243
);
249244
}
@@ -264,23 +259,6 @@ function assertBlockObject(configuration: Configuration, block: Block, sel: Comp
264259
// Test each node in selector
265260
let result = sel.nodes.reduce<NodeAndType | null>(
266261
(found, n) => {
267-
268-
// If this is an external Block reference, indicate we have encountered it.
269-
// If this is not the first BlockType encountered, throw the appropriate error.
270-
if (n.type === selectorParser.TAG) {
271-
if (found === null) {
272-
found = {
273-
blockType: BlockType.block,
274-
node: n,
275-
};
276-
} else {
277-
throw new errors.InvalidBlockSyntax(
278-
`External Block ${n} must be the first selector in "${rule.selector}"`,
279-
range(configuration, block.stylesheet, file, rule, sel.nodes[0]),
280-
);
281-
}
282-
}
283-
284262
// If selecting the root element, indicate we have encountered it. If this
285263
// is not the first BlockType encountered, throw the appropriate error
286264
if (isRootNode(n)) {
@@ -314,10 +292,19 @@ function assertBlockObject(configuration: Configuration, block: Block, sel: Comp
314292
`States without an explicit :scope or class selector are not supported: ${rule.selector}`,
315293
range(configuration, block.stylesheet, file, rule, n),
316294
);
317-
} else if (found.blockType === BlockType.class || found.blockType === BlockType.classAttribute) {
295+
}
296+
if (found.blockType === BlockType.class || found.blockType === BlockType.classAttribute) {
318297
found = { node: n, blockType: BlockType.classAttribute };
319-
} else if (found.blockType === BlockType.block || found.blockType === BlockType.root || found.blockType === BlockType.attribute) {
320-
found = { node: n, blockType: BlockType.attribute };
298+
} else if (found.blockType === BlockType.root || found.blockType === BlockType.attribute) {
299+
if (n.namespace === true) {
300+
throw new errors.InvalidBlockSyntax(
301+
`The "any namespace" selector is not supported: ${rule.selector}`,
302+
range(configuration, block.stylesheet, file, rule, n),
303+
);
304+
}
305+
// XXX this is where we drop the ref to the other attribute nodes,
306+
// XXX potentially causing the interface to not be fully discovered
307+
found = { node: n, blockType: BlockType.attribute, blockName: n.namespace };
321308
}
322309
}
323310

@@ -348,9 +335,9 @@ function assertBlockObject(configuration: Configuration, block: Block, sel: Comp
348335
}
349336
}
350337
return found;
351-
},
338+
},
352339
null,
353-
);
340+
);
354341

355342
// If no rules found in selector, we have a problem. Throw.
356343
if (!result) {
@@ -360,24 +347,33 @@ function assertBlockObject(configuration: Configuration, block: Block, sel: Comp
360347
}
361348

362349
if (isExternalBlock(result)) {
363-
let external = block.getReferencedBlock(result.node.value!);
364-
if (!external) { throw new errors.InvalidBlockSyntax(``, range(configuration, block.stylesheet, file, rule, sel.nodes[0])); }
350+
let blockName: string | undefined;
351+
if (result.blockType === BlockType.attribute) {
352+
blockName = result.blockName!;
353+
} else {
354+
blockName = result.node.value;
355+
}
356+
let external = block.getReferencedBlock(blockName);
357+
if (!external) {
358+
throw new errors.InvalidBlockSyntax(`A block named "${blockName}" does not exist in this context.`,
359+
range(configuration, block.stylesheet, file, rule, sel.nodes[0]));
360+
}
365361
let globalStates = external.rootClass.allAttributeValues().filter((a) => a.isGlobal);
366362
if (!globalStates.length) {
367363
throw new errors.InvalidBlockSyntax(
368-
`External Block '${result.node.value}' has no global states.`,
364+
`External Block '${blockName}' has no global states.`,
369365
range(configuration, block.stylesheet, file, rule, sel.nodes[0]));
370366
}
371-
throw new errors.InvalidBlockSyntax(
372-
`Missing global state selector on external Block '${result.node.value}'. Did you mean one of: ${globalStates.map((s) => s.asSource()).join(" ")}`,
373-
range(configuration, block.stylesheet, file, rule, sel.nodes[0]));
367+
if (result.blockType !== BlockType.attribute) {
368+
throw new errors.InvalidBlockSyntax(
369+
`Missing global state selector on external Block '${blockName}'. Did you mean one of: ${globalStates.map((s) => s.asSource()).join(" ")}`,
370+
range(configuration, block.stylesheet, file, rule, sel.nodes[0]));
371+
}
372+
return result;
374373
}
375374

376375
// Otherwise, return the block, type and associated node.
377376
else {
378-
return {
379-
blockName: blockName && blockName.value,
380-
...result,
381-
};
377+
return result;
382378
}
383379
}

packages/@css-blocks/core/src/BlockSyntax/BlockPath.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
CLASS_NAME_IDENT,
66
DEFAULT_EXPORT,
77
ROOT_CLASS,
8-
STATE_NAMESPACE,
98
} from "./BlockSyntax";
109

1110
interface BlockToken {
@@ -39,7 +38,7 @@ const isAttribute = (token?: Partial<Token>): token is AttrToken => !!token &&
3938
const isQuoted = (token?: Partial<AttrToken>): boolean => !!token && !!token.quoted;
4039
const isIdent = (ident?: string): boolean => !ident || CLASS_NAME_IDENT.test(ident);
4140
const hasName = (token?: Partial<Token>): boolean => !!token && !!token.name;
42-
const isValidNamespace = (token?: Partial<AttrToken>): boolean => !!token && (token.namespace === undefined || token.namespace === STATE_NAMESPACE);
41+
const isValidNamespace = (token?: Partial<AttrToken>): boolean => !!token && (token.namespace === undefined);
4342

4443
const ATTR_BEGIN = "[";
4544
const ATTR_END = "]";
@@ -54,7 +53,7 @@ const SEPARATORS = new Set([CLASS_BEGIN, ATTR_BEGIN, PSEUDO_BEGIN]);
5453

5554
export const ERRORS = {
5655
whitespace: "Whitespace is only allowed in quoted attribute values",
57-
namespace: "State attribute selectors are required to use a valid namespace.",
56+
namespace: "Namespaces aren't used for states in a block path.",
5857
noname: "Block path segments must include a valid name",
5958
unclosedAttribute: "Unclosed attribute selector",
6059
mismatchedQuote: "No closing quote found in Block path",

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

-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,6 @@ import regexpu = require("regexpu-core");
66
*/
77
export const CLASS_NAME_IDENT = new RegExp(regexpu("^(-?(?:\\\\.|[A-Za-z_\\u{0080}-\\u{10ffff}])(?:\\\\.|[A-Za-z0-9_\\-\\u{0080}-\\u{10ffff}])*)$", "u"));
88

9-
// State Attribute Namespace
10-
export const STATE_NAMESPACE = "state";
11-
129
// Prop Names
1310
export const EXTENDS = "extends";
1411
export const IMPLEMENTS = "implements";

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class Attribute extends Inheritable<Attribute, Block, BlockClass, AttrVal
2424
private _sourceAttributes: Attr[] | undefined;
2525

2626
protected get ChildConstructor(): typeof AttrValue { return AttrValue; }
27-
protected tokenToUid(token: AttrToken): string { return `${token.namespace}|${token.name}`; }
27+
protected tokenToUid(token: AttrToken): string { return token.name; }
2828

2929
public get name(): string { return this.token.name; }
3030
public get namespace(): string | null { return this.token.namespace || null; }

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

+5-5
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export class Block
111111
* | <attribute-selector> // reference to attribute in this sub-block
112112
* block-selector -> 'root'
113113
* class-selector -> <ident>
114-
* attribute-selector -> '[state|' <ident> ']'
114+
* attribute-selector -> '[' <ident> ']'
115115
* ident -> regex:[a-zA-Z_-][a-zA-Z0-9]*
116116
* A single dot by itself returns the current block.
117117
* @returns The Style referenced at the supplied path.
@@ -154,7 +154,7 @@ export class Block
154154
* | <attribute-selector> // reference to attribute in this sub-block
155155
* block-selector -> 'root'
156156
* class-selector -> <ident>
157-
* attribute-selector -> '[state|' <ident> ']'
157+
* attribute-selector -> '[' <ident> ']'
158158
* ident -> regex:[a-zA-Z_-][a-zA-Z0-9]*
159159
* A single dot by itself returns the current block.
160160
* @returns The Style referenced at the supplied path.
@@ -394,10 +394,10 @@ export class Block
394394
}
395395

396396
nodeAsStyle(node: selectorParser.Node): [Styles, number] | null {
397-
if (selectorParser.isTag(node)) {
398-
let otherBlock = this.getReferencedBlock(node.value);
397+
let next = node.next();
398+
if (isRootNode(node) && next && isAttributeNode(next) && typeof next.namespace === "string") {
399+
let otherBlock = this.getReferencedBlock(next.namespace);
399400
if (otherBlock) {
400-
let next = node.next();
401401
if (next && isClassNode(next)) {
402402
let klass = otherBlock.getClass(next.value);
403403
if (klass) {

packages/@css-blocks/core/test/Block/lookup-test.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ export class LookupTests {
1313
}
1414
@test "finds a state attribute"() {
1515
let block = new Block("test", "test.block.css");
16-
let attr = block.rootClass.ensureAttributeValue("[state|foo]");
17-
let found = block.lookup("[state|foo]");
16+
let attr = block.rootClass.ensureAttributeValue("[foo]");
17+
let found = block.lookup("[foo]");
1818
assert.deepEqual(attr, found);
1919
}
2020
@test "finds a state attribute with a value"() {
2121
let block = new Block("test", "test.block.css");
22-
let attr = block.rootClass.ensureAttributeValue("[state|foo=bar]");
23-
let found = block.lookup("[state|foo=bar]");
22+
let attr = block.rootClass.ensureAttributeValue("[foo=bar]");
23+
let found = block.lookup("[foo=bar]");
2424
assert.deepEqual(attr, found);
2525
}
2626
@test "invalid namespaces throw"() {
@@ -39,15 +39,15 @@ export class LookupTests {
3939
@test "finds a class with state attribute"() {
4040
let block = new Block("test", "test.block.css");
4141
let klass = block.ensureClass("foo");
42-
let attr = klass.ensureAttributeValue("[state|a]");
43-
let found = block.lookup(".foo[state|a]");
42+
let attr = klass.ensureAttributeValue("[a]");
43+
let found = block.lookup(".foo[a]");
4444
assert.deepEqual(attr, found);
4545
}
4646
@test "finds an class state attribute value"() {
4747
let block = new Block("test", "test.block.css");
4848
let klass = block.ensureClass("foo");
49-
let attr = klass.ensureAttributeValue("[state|b=a]");
50-
let found = block.lookup(".foo[state|b=a]");
49+
let attr = klass.ensureAttributeValue("[b=a]");
50+
let found = block.lookup(".foo[b=a]");
5151
assert.deepEqual(attr, found);
5252
}
5353
@test "finds referenced blocks"() {

0 commit comments

Comments
 (0)