Skip to content

Commit

Permalink
feat: Block Object asSource methods take optional Block scope.
Browse files Browse the repository at this point in the history
  • Loading branch information
amiller-gh committed Apr 19, 2018
1 parent b953602 commit 370dfd1
Show file tree
Hide file tree
Showing 15 changed files with 112 additions and 126 deletions.
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
// - `:scope[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
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
92 changes: 34 additions & 58 deletions packages/@css-blocks/core/src/BlockParser/block-intermediates.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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";

Expand All @@ -11,21 +11,42 @@ export enum BlockType {
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;
} | {
blockType: BlockType.block;
node: selectorParser.Tag;
};

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 Down Expand Up @@ -76,7 +97,7 @@ export function isExternalBlock(object: NodeAndType): boolean {
* on the root element.
* @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 @@ -85,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 @@ -106,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
Expand Up @@ -4,7 +4,6 @@ import { Block, BlockClass, Style } from "../../BlockTree";
import * as errors from "../../errors";
import { selectorSourceLocation as loc, sourceLocation } from "../../SourceLocation";
import {
BlockNodeAndType,
BlockType,
NodeAndType,
blockTypeName,
Expand Down Expand Up @@ -238,7 +237,7 @@ function assertValidSelector(block: Block, rule: postcss.Rule, selector: ParsedS
* @param rule The full `postcss.Rule` for nice error reporting.
* @return Returns the block's name, type and node.
*/
function assertBlockObject(block: Block, sel: CompoundSelector, rule: postcss.Rule, file: string): BlockNodeAndType {
function assertBlockObject(block: Block, sel: CompoundSelector, rule: postcss.Rule, file: string): NodeAndType {

// If selecting a block or tag, check that the referenced block has been imported.
// Otherwise, referencing a tag name is not allowed in blocks, throw an error.
Expand Down Expand Up @@ -325,7 +324,7 @@ function assertBlockObject(block: Block, sel: CompoundSelector, rule: postcss.Ru
}
if (!found) {
throw new errors.InvalidBlockSyntax(
`States without an explicit :scope or class selector are not yet supported: ${rule.selector}`,
`States without an explicit :scope or class selector are not supported: ${rule.selector}`,
loc(file, rule, n),
);
} else if (found.blockType === BlockType.class || found.blockType === BlockType.classAttribute) {
Expand Down
11 changes: 9 additions & 2 deletions packages/@css-blocks/core/src/BlockTree/AttrValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,15 @@ export class AttrValue extends Style<AttrValue, Block, Attribute, never> {
return this._sourceAttributes.slice();
}

asSource(): string {
return this.parent.asSource(this.value);
/**
* Export as original AttrValue name.
* @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`.
* @returns String representing original AttrValue path.
*/
asSource(scope?: Block | boolean): string {
let namespace = this.attribute.namespace ? `${this.attribute.namespace}|` : "";
let value = (this.value && this.value !== ATTR_PRESENT) ? `=${this.value}` : "";
return this.attribute.blockClass.asSource(scope) + `[${namespace}${this.parent.name}${value}]`;
}

public cssClass(config: ResolvedConfiguration): string {
Expand Down
18 changes: 4 additions & 14 deletions packages/@css-blocks/core/src/BlockTree/Attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,25 +121,15 @@ export class Attribute extends Inheritable<Attribute, Block, BlockClass, AttrVal
return resolved;
}

/**
* @returns The bare Attribute selector with no qualifying `BlockClass` name.
*/
unqualifiedSource(value?: string): string {
let namespace = this.token.namespace ? `${this.token.namespace}|` : "";
value = (value && value !== ATTR_PRESENT) ? `=${value}` : "";
return `[${namespace}${this.token.name}${value}]`;
}

/**
* Retrieve this Attribute's selector as it appears in the Block source code.
*
* @param value If provided, it is used as the Attribute's value whether or not
* it is allowed by the known AttrValues (this is useful for constructing
* error messages).
* @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`.
* @returns The Attribute's attribute selector.
*/
asSource(value?: string): string {
return this.blockClass.asSource() + this.unqualifiedSource(value);
asSource(scope?: Block | boolean): string {
let namespace = this.token.namespace ? `${this.token.namespace}|` : "";
return this.blockClass.asSource(scope) + `[${namespace}${this.token.name}]`;
}

/**
Expand Down
15 changes: 14 additions & 1 deletion packages/@css-blocks/core/src/BlockTree/BlockClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,22 @@ export class BlockClass extends Style<BlockClass, Block, Block, Attribute> {

/**
* Export as original class name.
* @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`.
* @returns String representing original class.
*/
public asSource(): string { return this.isRoot ? ROOT_CLASS : `.${this.name}`; }
public asSource(scope?: Block | boolean): string {
let blockName = this.block.name;

if (scope instanceof Block) {
blockName = scope.getReferencedBlockLocalName(this.block) || blockName;
}

if (scope && scope !== this.block) {
return this.isRoot ? blockName : `${blockName}.${this.name}`;
}

return this.isRoot ? ROOT_CLASS : `.${this.name}`;
}

/**
* Emit analysis attributes for the class value this
Expand Down
3 changes: 2 additions & 1 deletion packages/@css-blocks/core/src/BlockTree/Style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ export abstract class Style<

/**
* Return the source selector this `Style` was read from.
* @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`.
* @returns The source selector.
*/
public abstract asSource(): string;
public abstract asSource(scope?: Root | boolean): string;

/**
* Return an attribute for analysis using the authored source syntax.
Expand Down
2 changes: 1 addition & 1 deletion packages/@css-blocks/core/test/global-states-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class BlockInheritance extends BEMProcessor {
);
});
}
@test "Global state usage must be specify a global state if present"() {
@test "Global state usage must specify a global state, not just a block name."() {
let { imports, config } = setupImporting();
imports.registerSource(
"app.block.css",
Expand Down
2 changes: 1 addition & 1 deletion packages/@css-blocks/core/test/syntax-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ export class StraightJacket extends BEMProcessor {
[state|foo] { display: block; }`;
return assertError(
cssBlocks.InvalidBlockSyntax,
"States without an explicit :scope or class selector are not yet supported: [state|foo]" +
"States without an explicit :scope or class selector are not supported: [state|foo]" +
" (foo/bar/illegal-class-combinator.css:2:21)",
this.process(filename, inputCSS));
}
Expand Down
Loading

0 comments on commit 370dfd1

Please sign in to comment.