Skip to content

Commit

Permalink
feat(css-blocks): Broke up Block.ts, refactored foundational BlockObj…
Browse files Browse the repository at this point in the history
…ect constructs, added StateGroup concept.

This is a BREAKING CHANGES (obvs).
  • Loading branch information
amiller-gh committed Mar 2, 2018
1 parent 9145568 commit 6824fa8
Show file tree
Hide file tree
Showing 19 changed files with 930 additions and 877 deletions.
915 changes: 82 additions & 833 deletions packages/css-blocks/src/Block/Block.ts

Large diffs are not rendered by default.

178 changes: 178 additions & 0 deletions packages/css-blocks/src/Block/BlockClass.ts
@@ -0,0 +1,178 @@
import { Block } from "./Block";
import { NodeStyle } from "./Style";
import { StateGroup } from "./StateGroup";
import { State } from "./State";
import { Attribute } from "@opticss/element-analysis";
import { OptionsReader } from "../OptionsReader";
import { OutputMode } from "../OutputMode";
import { UNIVERSAL_STATE } from "../BlockSyntax";

/**
* Holds state values to be passed to the StateContainer.
*/
export interface StateInfo {
group?: string;
name: string;
}

/**
* Represents a Class present in the Block.
*/
export class BlockClass extends NodeStyle<BlockClass, Block, StateGroup> {
private _sourceAttribute: Attribute;

protected newChild(name: string): StateGroup { return new StateGroup(name, this); }

get isRoot(): boolean { return this.name === "root"; }

public getGroups(): StateGroup[] { return this.children(); }
public getGroup(name: string, filter?: string): State[] {
let group = this.getChild(name);
if (!group) { return []; }
let states = group.states();
return filter ? states.filter(s => s.name === filter) : states;
}
public ensureGroup(name: string): StateGroup { return this.ensureChild(name); }
public resolveGroup(name: string): StateGroup | null { return this.resolveChild(name); }
public stateGroups(): StateGroup[] { return this.children(); }
public resolveState(group: string, state: string): State | null {
let parent = this.resolveChild(group);
if (parent) {
parent.resolveChild(state);
}
return null;
}

public booleanStates(): State[]{
let res: State[] = [];
for (let group of this.getGroups()) {
let state = group.getState(UNIVERSAL_STATE);
if (!group.hasSubStates && state) {
res.push(state);
}
}
return res;
}

public localName(): string { return this.name; }

/**
* Export as original class name.
* @returns String representing original class.
*/
public asSource(): string { return `.${this.name}`; }

public asSourceAttributes(): Attribute[] {
if (!this._sourceAttribute) {
this._sourceAttribute = new Attribute("class", { constant: this.name });
}
return [this._sourceAttribute];
}

/**
* Export as new class name.
* @param opts Option hash configuring output mode.
* @returns String representing output class.
*/
public cssClass(opts: OptionsReader): string {
switch (opts.outputMode) {
case OutputMode.BEM:
if (this.isRoot) {
return `${this.block.name}`;
} else {
return `${this.block.name}__${this.name}`;
}
default:
throw "this never happens";
}
}

/**
* Return array self and all children.
* @param shallow Pass false to not include children.
* @returns Array of Styles.
*/
all(shallow?: boolean): (State | BlockClass)[] {
let result: (State | BlockClass)[] = [this];
if (!shallow) {
result = result.concat(this.allStates());
}
return result;
}

/**
* Returns all concrete states defined against this class.
* Does not take inheritance into account.
*/
allStates(): State[] {
let result: State[] = [];
for (let stateContainer of this.stateGroups()){
result.push(...stateContainer.states());
}
return result;
}

/**
* Ensure that a `SubState` with the given `subStateName` exists belonging to
* the state named `stateName`. If the state does not exist, it is created.
* @param stateName The State's name to ensure.
* @param subStateName Optional state group for lookup/registration
*/
ensureSubState(groupName: string, subStateName: string): State {
return this.ensureGroup(groupName).ensureState(subStateName);
}

/**
* Group getter. Returns a list of State objects in the requested group that are defined
* against this specific class. This does not take inheritance into account.
* @param stateName State group for lookup or a boolean state name if substate is not provided.
* @param subStateName Optional substate to filter states by.
* @returns An array of all States that were requested.
*/
getState(groupName: string, stateName = UNIVERSAL_STATE): State | null {
let group = this.getChild(groupName);
return group ? group.getState(stateName) || null : null;
}

getGroupsNames(): Set<string> {
return new Set<string>([...this._children.keys()]);
}

/**
* Legacy State getter
* @param info The StateInfo type to lookup, contains `name` and `group`
* @returns The State that was requested, or undefined
*/
_getState(info: StateInfo): State | null {
return info.group ? this.getState(info.group, info.name) : this.getState(info.name);
}

/**
* Legacy state ensurer. Ensure that a `State` with the given `StateInfo` is
* registered with this Block.
* @param info `StateInfo` to verify exists on this `Block`
* @return The `State` object on this `Block`
*/
_ensureState(info: StateInfo): State {
let state = this.ensureGroup(info.group || info.name);
return info.group ? state.ensureState(info.name) : state.ensureState(UNIVERSAL_STATE);
}

/**
* Debug utility to help test States.
* @param options Options to pass to States' asDebug method.
* @return Array of debug strings for these states
*/
debug(opts: OptionsReader): string[] {
let result: string[] = [];
for (let state of this.all()) {
result.push(state.asDebug(opts));
}
return result;
}

}

export function isBlockClass(o: object): o is BlockClass {
return o instanceof BlockClass;
}
178 changes: 178 additions & 0 deletions packages/css-blocks/src/Block/Inheritable.ts
@@ -0,0 +1,178 @@
import { ObjectDictionary } from "@opticss/util";
import { Style, Block, isBlock, isBlockClass, isState } from './index';

import { BlockPath } from "../BlockSyntax";
import { CssBlockError } from "../errors";

export abstract class Inheritable<
Self extends Inheritable<Self, Parent, Child>,
Parent extends Inheritable<any, any, Self> | null = null,
Child extends Inheritable<any, Self, any> | null = null
> {

protected _name: string;
protected _base: Self | undefined;
public _block: Block;
protected _baseName: string;
protected _parent: Parent | undefined;
protected _children: Map<string, Child> = new Map;

/**
* Given a parent that is a base class of this style, retrieve this style's
* base style from it, if it exists. This method does not traverse into base styles.
*/
protected abstract newChild(name: string): Child;

// TODO: Currently only ever returns itself if is a style. Need to get it to look other things up.
public lookup(path: string | BlockPath): Style | undefined {
path = new BlockPath(path);
if (isBlockClass(this) || isState(this)) return this;
return undefined;
}

/**
* Inheritable constructor
* @param name Name for this Inheritable instance.
* @param parent The parent Inheritable of this node.
*/
constructor(name: string, parent?: Parent,) {
this._name = name;
this._parent = parent;
}

public get name(): string { return this._name; }
public get baseName(): string | undefined { return this._baseName; }
public get parent(): Parent | undefined { return this._parent; }

/**
* Get the style that this style inherits from, if any.
*
* This walks down the declared styles of the parent's inheritance chain,
* and attempts to find a matching directly declared style on each.
*
* The result is cached because it never changes and is decidable as soon
* as the style is instantiated.
*/
public get base(): Self | undefined {
if (this._base !== undefined || !this.parent) {
return this._base || undefined;
}
let baseParent: Parent | undefined = this.parent.base;
while (baseParent) {
let cls = baseParent ? baseParent.getChild(this.name) : undefined;
if (cls) {
this._base = cls;
return cls;
}
baseParent = baseParent.base;
}
return this._base = undefined;
}

/**
* traverse parents and return the base block object.
* @returns The base block in this container tree.
*/
public get block(): Block {
if (isBlock(this)) { return this._block = this; }
if (this._block !== undefined) { return this._block; }
if (this.parent) { return this._block = this.parent.block; }
throw new CssBlockError("Tried to access `block` on an orphaned `Style`");
}

setBase(baseName: string, base: Self) {
this._baseName = baseName;
this._base = base;
}

/**
* Compute all block objects that are implied by this block object through
* inheritance. Does not include this object or the styles it implies through
* other relationships to this object.
*
* If nothing is inherited, this returns an empty set.
*/
resolveInheritance(): Self[] {
let inherited = new Array<Self>();
let base: Self | undefined = this.base;
while (base) {
inherited.unshift(base);
base = base.base;
}
return inherited;
}

/**
* Resolves the child with the given name from this node's inheritance
* chain. Returns undefined if the child is not found.
* @param name The name of the child to resolve.
*/
resolveChild(name: string): Child | null {
let state: Child | null = this.getChild(name);
let container: Self | undefined = this.base;
while (!state && container) {
state = container.getChild(name);
container = container.base;
}
return state || null;
}

/**
* Given a parent that is a base class of this style, retrieve this style's
* base style from it, if it exists. This method does not traverse into base styles.
*/
protected getChild(key: string): Child | null {
return this._children.get(key) || null;
}

protected setChild(key: string, value: Child): Child {
this._children.set(key, value);
return value;
}

protected ensureChild(name: string): Child {
if (!this._children.has(name)) {
this.setChild(name, this.newChild(name));
}
return this._children.get(name) as Child;
}

protected children(): Child[]{
return [...this._children.values()];
}

// TODO: Cache this maybe? Convert entire model to only use hash?...
protected childrenHash(): ObjectDictionary<Child> {
let out = {};
for (let [key, value] of this._children.entries() ) {
out[key] = value;
}
return out;
}

}

export abstract class Source<
Self extends Inheritable<Self, null, Child>,
Child extends Inheritable<Child, Self, any>
> extends Inheritable<Self, null, Child> {
public parent: null;
protected _children: Map<string, Child>;
}

export abstract class Node<
Self extends Inheritable<Self, Parent, Child>,
Parent extends Inheritable<Parent, any, Self>,
Child extends Inheritable<Child, Self, any>
> extends Inheritable<Self, Parent, Child> {
public parent: Parent;
protected _children: Map<string, Child>;
}

export abstract class Sink<
Self extends Inheritable<Self, Parent, null>,
Parent extends Inheritable<any, any, Self>
> extends Inheritable<Self, Parent, null> {
public parent: Parent;
protected _children: Map<string, null>;
}
2 changes: 1 addition & 1 deletion packages/css-blocks/src/Block/RulesetContainer.ts
Expand Up @@ -2,7 +2,7 @@ import { MultiMap, TwoKeyMultiMap } from "@opticss/util";
import * as propParser from "css-property-parser";
import * as postcss from "postcss";

import { BLOCK_PROP_NAMES, BlockPath, RESOLVE_RE, SELF_SELECTOR } from "../BlockSyntax";
import { BLOCK_PROP_NAMES, RESOLVE_RE, SELF_SELECTOR, BlockPath } from "../BlockSyntax";
import { sourceLocation } from "../SourceLocation";
import * as errors from "../errors";

Expand Down

0 comments on commit 6824fa8

Please sign in to comment.