Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(css-blocks): Broke up Block.ts, refactored foundational BlockObj…
…ect constructs, added StateGroup concept. This is a BREAKING CHANGES (obvs).
- Loading branch information
1 parent
9145568
commit 6824fa8
Showing
19 changed files
with
930 additions
and
877 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.