Skip to content

feat: Require the :scope pseudo for root states. #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ We can easily conceptualize the `RewriteMapping` data for each element in develo
```javascript
// For Element 1:
// - `.class-0` is always applied
// - [state|active] is *only* applied when `isActive` is true
// - `.class-0[state|active]` is *only* applied when `isActive` is true
const el1Classes = [
"block__class-0",
isActive && "block__class-0--active"
Expand Down
4 changes: 2 additions & 2 deletions packages/@css-blocks/broccoli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"author": "Adam Miller <[email protected]>",
"license": "MIT",
"keywords": [
"css-blocks",
"@css-blocks/core",
"css blocks",
"broccoli-plugin"
],
Expand Down Expand Up @@ -46,4 +46,4 @@
"recursive-readdir": "^2.2.2",
"walk-sync": "^0.3.2"
}
}
}
2 changes: 1 addition & 1 deletion packages/@css-blocks/broccoli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ class BroccoliCSSBlocks extends BroccoliPlugin {

// Run optimization and compute StyleMapping.
let optimized = await optimizer.optimize(this.output);
let styleMapping = new StyleMapping(optimized.styleMapping, blocks, options, this.analyzer.analyses());
let styleMapping = new StyleMapping<keyof TemplateTypes>(optimized.styleMapping, blocks, options, this.analyzer.analyses());

// Attach all computed data to our magic shared memory transport object...
this.transport.mapping = styleMapping;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ function printRulesetConflict(prop: string, rule: Ruleset) {
for (let node of nodes) {
let line = node.source.start && `:${node.source.start.line}`;
let column = node.source.start && `:${node.source.start.column}`;
out.push(` ${rule.style.block.name}${rule.style.asSource()} (${rule.file}${line}${column})`);
out.push(` ${rule.style.asSource(true)} (${rule.file}${line}${column})`);
}
return out.join("\n");
}
Expand Down
21 changes: 16 additions & 5 deletions packages/@css-blocks/core/src/BlockCompiler/ConflictResolver.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { assertNever } from "@opticss/util";
import { CompoundSelector, ParsedSelector, parseSelector, postcss, postcssSelectorParser as selectorParser } from "opticss";

import { getBlockNode } from "../BlockParser";
import { isAttributeNode, isClassNode, isRootNode, toAttrToken } from "../BlockParser";
import { RESOLVE_RE } from "../BlockSyntax";
import { Block, Style } from "../BlockTree";
import { Block, BlockClass, Style } from "../BlockTree";
import { ResolvedConfiguration } from "../configuration";
import * as errors from "../errors";
import { QueryKeySelector } from "../query";
Expand Down Expand Up @@ -88,11 +88,22 @@ export class ConflictResolver {
let parsedSelectors = block.getParsedSelectors(rule);
parsedSelectors.forEach((sel) => {
let key = sel.key;
let obj: Style | null = null;
let container: BlockClass | null;

// Fetch the associated `Style`. If does not exist (ex: malformed selector), skip.
let blockNode = getBlockNode(key);
if (!blockNode) { return; }
let obj: Style | null = block.nodeAndTypeToStyle(blockNode);
for (let node of key.nodes) {
if (isRootNode(node)) {
container = obj = block.rootClass;
}
if (isClassNode(node)) {
container = obj = block.getClass(node.value);
}
else if (isAttributeNode(node)) {
obj = container!.getAttributeValue(toAttrToken(node));
}
}

if (!obj) { return; }

// Fetch the set of Style conflicts. If the Style has already
Expand Down
104 changes: 46 additions & 58 deletions packages/@css-blocks/core/src/BlockParser/block-intermediates.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,52 @@
import { assertNever, firstOfType, whatever } from "@opticss/util";
import { CompoundSelector, postcssSelectorParser as selectorParser } from "opticss";
import { assertNever, whatever } from "@opticss/util";
import { postcssSelectorParser as selectorParser } from "opticss";

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

export enum BlockType {
root = 1,
block = 1,
root,
attribute,
class,
classAttribute,
}

export type NodeAndType = {
blockType: BlockType.attribute | BlockType.classAttribute;
export type RootAttributeNode = {
blockName?: string;
blockType: BlockType.attribute;
node: selectorParser.Attribute;
} | {
blockType: BlockType.root | BlockType.class;
node: selectorParser.ClassName | selectorParser.Pseudo;
};

export type BlockNodeAndType = NodeAndType & {
export type ClassAttributeNode = {
blockName?: string;
blockType: BlockType.classAttribute;
node: selectorParser.Attribute;
};

export type AttributeNode = RootAttributeNode | ClassAttributeNode;

export type RootClassNode = {
blockName?: string;
blockType: BlockType.root;
node: selectorParser.Pseudo;
};

export type BlockClassNode = {
blockName?: string;
blockType: BlockType.class;
node: selectorParser.ClassName;
};

export type ClassNode = RootClassNode | BlockClassNode;

export type BlockNode = {
blockName?: string;
blockType: BlockType.block;
node: selectorParser.Tag;
};

export type NodeAndType = AttributeNode | ClassNode | BlockNode;

/** Extract an Attribute's value from a `selectorParser` attribute selector */
function attrValue(attr: selectorParser.Attribute): string {
if (attr.value) {
Expand All @@ -34,7 +59,6 @@ function attrValue(attr: selectorParser.Attribute): string {
/** Extract an Attribute's name from an attribute selector */
export function toAttrToken(attr: selectorParser.Attribute): AttrToken {
return {
type: "attribute",
namespace: attr.namespaceString,
name: attr.attribute,
value: attrValue(attr),
Expand All @@ -51,6 +75,7 @@ export function toAttrToken(attr: selectorParser.Attribute): AttrToken {
export function blockTypeName(t: BlockType, options?: { plural: boolean }): string {
let isPlural = options && options.plural;
switch (t) {
case BlockType.block: return isPlural ? "external blocks" : "external block";
case BlockType.root: return isPlural ? "block roots" : "block root";
case BlockType.attribute: return isPlural ? "root-level states" : "root-level state";
case BlockType.class: return isPlural ? "classes" : "class";
Expand All @@ -59,12 +84,20 @@ export function blockTypeName(t: BlockType, options?: { plural: boolean }): stri
}
}

/**
* Test if the provided node representation is an external block.
* @param object The NodeAndType's descriptor object.
*/
export function isExternalBlock(object: NodeAndType): boolean {
return object.blockType === BlockType.block;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should name the anonymous interfaces in NodeAndType and then this (and the other related isXXX checks) should become typeguards.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmmkay refactored 👍 (Even though its going away...soon...hopefully...)


/**
* Test if the provided node representation is a root level object, aka: operating
* on the root element.
* @param object The CompoundSelector's descriptor object.
* @param object The NodeAndType's descriptor object.
*/
export function isRootLevelObject(object: NodeAndType): boolean {
export function isRootLevelObject(object: NodeAndType): object is RootAttributeNode | RootClassNode {
return object.blockType === BlockType.root || object.blockType === BlockType.attribute;
}

Expand All @@ -73,7 +106,7 @@ export function isRootLevelObject(object: NodeAndType): boolean {
* on an element contained by the root, not the root itself.
* @param object The CompoundSelector's descriptor object.
*/
export function isClassLevelObject(object: NodeAndType): boolean {
export function isClassLevelObject(object: NodeAndType): object is ClassAttributeNode | BlockClassNode {
return object.blockType === BlockType.class || object.blockType === BlockType.classAttribute;
}

Expand All @@ -94,48 +127,3 @@ export const isClassNode = selectorParser.isClassName;
export function isAttributeNode(node: selectorParser.Node): node is selectorParser.Attribute {
return selectorParser.isAttribute(node) && node.namespace === STATE_NAMESPACE;
}

/**
* Similar to assertBlockObject except it doesn't check for well-formedness
* and doesn't ensure that you get a block object when not a legal selector.
* @param sel The `CompoundSelector` to search.
* @return Returns the block's name, type and node.
*/
export function getBlockNode(sel: CompoundSelector): BlockNodeAndType | null {
let blockName = sel.nodes.find(n => n.type === selectorParser.TAG);
let r = firstOfType(sel.nodes, isRootNode);
if (r) {
return {
blockName: blockName && blockName.value,
blockType: BlockType.root,
node: r,
};
}
let s = firstOfType(sel.nodes, isAttributeNode);
if (s) {
let prev = s.prev();
if (prev && isClassNode(prev)) {
return {
blockName: blockName && blockName.value,
blockType: BlockType.classAttribute,
node: s,
};
} else {
return {
blockName: blockName && blockName.value,
blockType: BlockType.attribute,
node: s,
};
}
}
let c = firstOfType(sel.nodes, isClassNode);
if (c) {
return {
blockName: blockName && blockName.value,
blockType: BlockType.class,
node: c,
};
} else {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import { postcss } from "opticss";
import { postcss, postcssSelectorParser as selectorParser } from "opticss";

import { Block } from "../../BlockTree";
import * as errors from "../../errors";
import { selectorSourceLocation as loc } from "../../SourceLocation";
import {
BlockType,
getBlockNode,
toAttrToken,
} from "../block-intermediates";
import { isAttributeNode, toAttrToken } from "../block-intermediates";

/**
* Verify that the external block referenced by a `rule` selects an Attribute that
Expand All @@ -24,42 +20,47 @@ export async function assertForeignGlobalAttribute(root: postcss.Root, block: Bl

parsedSelectors.forEach((iSel) => {

iSel.eachCompoundSelector((compoundSel) => {

let obj = getBlockNode(compoundSel);
iSel.eachCompoundSelector((sel) => {

// Only test rules that are block references (this is validated in parse-styles and shouldn't happen).
// If node isn't selecting a block, move on
if (!obj || !obj.blockName) { return; }
let blockName = sel.nodes.find(n => n.type === selectorParser.TAG);
if (!blockName || !blockName.value) { return; }

// If selecting something other than an attribute on external block, throw.
if (obj.blockType !== BlockType.attribute) {
throw new errors.InvalidBlockSyntax(
`Only global states from other blocks can be used in selectors: ${rule.selector}`,
loc(file, rule, obj.node));
}
for (let node of sel.nodes) {

// If referenced block does not exist, throw.
let otherBlock = block.getReferencedBlock(obj.blockName!);
if (!otherBlock) {
throw new errors.InvalidBlockSyntax(
`No block named ${obj.blockName} found: ${rule.selector}`,
loc(file, rule, obj.node));
}
if (node.type === selectorParser.TAG) { continue; }

// If state referenced does not exist on external block, throw
let otherAttr = otherBlock.rootClass.getAttributeValue(toAttrToken(obj.node));
if (!otherAttr) {
throw new errors.InvalidBlockSyntax(
`No state ${obj.node.toString()} found in : ${rule.selector}`,
loc(file, rule, obj.node));
}
// If selecting something other than an attribute on external block, throw.
if (!isAttributeNode(node)) {
throw new errors.InvalidBlockSyntax(
`Only global states from other blocks can be used in selectors: ${rule.selector}`,
loc(file, rule, node));
}

// If referenced block does not exist, throw.
let otherBlock = block.getReferencedBlock(blockName.value);
if (!otherBlock) {
throw new errors.InvalidBlockSyntax(
`No block named ${blockName.value} found: ${rule.selector}`,
loc(file, rule, node));
}

// If state referenced does not exist on external block, throw
let otherAttr = otherBlock.rootClass.getAttributeValue(toAttrToken(node));
if (!otherAttr) {
throw new errors.InvalidBlockSyntax(
`No state ${node.toString()} found in : ${rule.selector}`,
loc(file, rule, node));
}

// If external state is not set as global, throw.
if (!otherAttr.isGlobal) {
throw new errors.InvalidBlockSyntax(
`${node.toString()} is not global: ${rule.selector}`,
loc(file, rule, node));
}

// If external state is not set as global, throw.
if (!otherAttr.isGlobal) {
throw new errors.InvalidBlockSyntax(
`${obj.node.toString()} is not global: ${rule.selector}`,
loc(file, rule, obj.node));
}
});
});
Expand Down
Loading