import Env, { EnvError } from './env'; import Token from './token'; export namespace AST { export interface Opts { depth: number; prettyPrint: boolean; } const spaces = (depth: number): string => { return Array(depth) .fill(' ') .join(''); }; export class Node { public compile(_env: Env, _opts: Opts): string | EnvError { return new EnvError(1, "shouldn't call directly"); } } export class Literal extends Node { public value: Token; public constructor(value: Token) { super(); this.value = value; } public compile(_env: Env, _opts: Opts) { return this.value.value; } } export class Identifier extends Node { public name: Token; public constructor(name: Token) { super(); this.name = name; } public compile(env: Env, opts: Opts): string | EnvError { const value = env.get(this.name); if (value instanceof EnvError) return value; return value.compile(env, opts); } } export class Selector extends Node { public name: Token; public parents: Selector[]; public constructor(name: Token, parents: Selector[] = []) { super(); this.name = name; this.parents = parents; } public getLineages(): string[] { if (this.parents.length === 0) return [this.name.value]; return Array.prototype.concat( ...this.parents.map((parent) => { return parent.getLineages().map((lineage) => { if (this.name.value.match(/^&/)) { return lineage + this.name.value.slice(1); } return lineage + ' ' + this.name.value; }); }) ); } } export class Property extends Node { public name: Token; public constructor(name: Token) { super(); this.name = name; } public compile(_env: Env, _opts: Opts): string | EnvError { return this.name.value; } } export class Binding extends Node { public identifier: Identifier; public value: Node; public constructor(identifier: Identifier, value: Node) { super(); this.identifier = identifier; this.value = value; } } export class Let extends Node { public bindings: Binding[]; public children: Node[]; public constructor(bindings: Binding[], children: Node[]) { super(); this.bindings = bindings; this.children = children; } public compile(env: Env, opts: Opts): string | EnvError { this.bindings.forEach((binding) => { env.set(binding.identifier.name.value, binding.value); }); const children = this.children.map((child) => child.compile(env, opts)); const childrenError = children.find((node) => node instanceof EnvError); if (childrenError instanceof EnvError) return childrenError; return children.join(opts.prettyPrint ? '\n' : ''); } } export class MediaQueryPredicate extends Node { public property: Property; public value: Literal | Identifier | Application; public constructor( property: Property, value: Literal | Identifier | Application ) { super(); this.property = property; this.value = value; } public compile(env: Env, opts: Opts) { const wordSpacer = opts.prettyPrint ? ' ' : ''; const property = this.property.compile(env, opts); const value = this.value.compile(env, opts); return `${property}:${wordSpacer}${value}`; } } export class MediaQuery extends Node { public predicate: MediaQueryPredicate; public children: Node[]; public constructor(predicate: MediaQueryPredicate, children: Node[]) { super(); this.predicate = predicate; this.children = children; } public compile(env: Env, opts: Opts): string | EnvError { const lineSpacer = opts.prettyPrint ? '\n' : ''; const wordSpacer = opts.prettyPrint ? ' ' : ''; const predicate = this.predicate.compile(env, opts); const children = this.children.map((child) => child.compile(env, { ...opts, depth: opts.depth + 2 }) ); const childrenError = children.find((node) => node instanceof EnvError); if (childrenError instanceof EnvError) return childrenError; return `@media(${predicate})${wordSpacer}{${lineSpacer}${children.join( lineSpacer )}${lineSpacer}}`; } } export class Keyframes extends Node { public name: Literal; public children: Node[]; public constructor(name: Literal, children: Node[]) { super(); this.name = name; this.children = children; } public compile(env: Env, opts: Opts) { const lineSpacer = opts.prettyPrint ? '\n' : ''; const wordSpacer = opts.prettyPrint ? ' ' : ''; const name = this.name.compile(env, opts); const children = this.children.map((child) => child.compile(env, { ...opts, depth: opts.depth + 2 }) ); const childrenError = children.find((node) => node instanceof EnvError); if (childrenError instanceof EnvError) return childrenError; return `@keyframes ${name}${wordSpacer}{${lineSpacer}${children.join( lineSpacer )}${lineSpacer}}`; } } export class Application extends Node { public name: Literal; public arguments: Node[]; public constructor(name: Literal, args: Node[]) { super(); this.name = name; this.arguments = args; } public compile(env: Env, opts: Opts) { const wordSpacer = opts.prettyPrint ? ' ' : ''; const [lParen, rParen, comma] = ['(', ')', ',']; const compiledArguments = this.arguments .map((arg) => arg.compile(env, opts)) .join(`${comma}${wordSpacer}`); return `${this.name.value.value}${lParen}${compiledArguments}${rParen}`; } } export class Mixin extends Node { public name: Token; public parameters: Identifier[]; public rules: Rule[]; public constructor(name: Token, parameters: Identifier[], rules: Rule[]) { super(); this.name = name; this.parameters = parameters; this.rules = rules; } public compile(env: Env, _opts: Opts) { env.set(this.name.value, this); return ''; } } export class Rule extends Node { public property: Property; public value: Literal | Identifier | Application; public constructor( property: Property, value: Literal | Identifier | Application ) { super(); this.property = property; this.value = value; } public compile(env: Env, opts: Opts): string | EnvError { const wordSpacer = opts.prettyPrint ? ' ' : ''; const indentSpacer = opts.prettyPrint ? ' ' + spaces(opts.depth) : ''; const property = this.property.compile(env, opts); if (property instanceof EnvError) return property; const value = this.value.compile(env, opts); if (value instanceof EnvError) return value; return `${indentSpacer}${property}:${wordSpacer}${value};`; } } export class RuleSet extends Node { public selectors: Selector[]; public rules: Rule[]; public children: RuleSet[]; public constructor( selectors: Selector[], rules: Rule[], children: RuleSet[] = [] ) { super(); this.selectors = selectors; this.rules = rules; this.children = children; } public compile(env: Env, opts: Opts): string | EnvError { const lineSpacer = opts.prettyPrint ? '\n' : ''; const wordSpacer = opts.prettyPrint ? ' ' : ''; const indentSpacer = opts.prettyPrint ? spaces(opts.depth) : ''; const [lBrace, rBrace] = ['{', '}']; const lineages = ([] as string[]).concat( ...this.selectors.map((sel) => sel.getLineages()) ); const rules = this.rules.map((rule: Rule) => rule.compile(env, opts)); const rulesError = rules.find((rule) => rule instanceof EnvError); if (rulesError !== undefined) return rulesError; const compiledRules = rules.join(lineSpacer); const children: Array = this.children.map( (child: RuleSet): string | EnvError => { return child.compile(env, opts); } ); const childrenError = children.find((child) => child instanceof EnvError); if (childrenError !== undefined) return childrenError; const compiledChildren = children.join(lineSpacer); const lineage = lineages.join(`,${wordSpacer}`); const frontMatter = `${indentSpacer}${lineage}${wordSpacer}${lBrace}${lineSpacer}`; const backMatter = `${lineSpacer}${indentSpacer}${rBrace}${ compiledChildren.length ? lineSpacer : '' }`; return rules.length ? `${frontMatter}${compiledRules}${backMatter}${compiledChildren}` : compiledChildren; } } }