A stylesheet language written in TypeScript that compiles to CSS
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ast.ts 10.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. import Env, { EnvError } from './env';
  2. import Token from './token';
  3. export namespace AST {
  4. export type Child = Rule | RuleSet | Application;
  5. export interface Opts {
  6. depth: number;
  7. prettyPrint: boolean;
  8. }
  9. const spaces = (depth: number): string => {
  10. return Array(depth)
  11. .fill(' ')
  12. .join('');
  13. };
  14. export class Node {
  15. public compile(_env: Env, _opts: Opts): string | EnvError {
  16. return new EnvError(1, "shouldn't call directly");
  17. }
  18. }
  19. export class Literal extends Node {
  20. public value: Token;
  21. public constructor(value: Token) {
  22. super();
  23. this.value = value;
  24. }
  25. public compile(_env: Env, _opts: Opts) {
  26. return this.value.value;
  27. }
  28. }
  29. export class Identifier extends Node {
  30. public name: Token;
  31. public constructor(name: Token) {
  32. super();
  33. this.name = name;
  34. }
  35. public compile(env: Env, opts: Opts): string | EnvError {
  36. const value = env.get(this.name);
  37. if (value instanceof EnvError) return value;
  38. return value.compile(env, opts);
  39. }
  40. }
  41. export class Selector extends Node {
  42. public name: Token;
  43. public parents: Selector[];
  44. public constructor(name: Token, parents: Selector[] = []) {
  45. super();
  46. this.name = name;
  47. this.parents = parents;
  48. }
  49. public getLineages(): string[] {
  50. if (this.parents.length === 0) return [this.name.value];
  51. return Array.prototype.concat(
  52. ...this.parents.map((parent) => {
  53. return parent.getLineages().map((lineage) => {
  54. if (this.name.value.match(/^&/)) {
  55. return lineage + this.name.value.slice(1);
  56. }
  57. return lineage + ' ' + this.name.value;
  58. });
  59. })
  60. );
  61. }
  62. }
  63. export class Property extends Node {
  64. public name: Token;
  65. public constructor(name: Token) {
  66. super();
  67. this.name = name;
  68. }
  69. public compile(_env: Env, _opts: Opts): string | EnvError {
  70. return this.name.value;
  71. }
  72. }
  73. export class Binding extends Node {
  74. public identifier: Identifier;
  75. public value: Node;
  76. public constructor(identifier: Identifier, value: Node) {
  77. super();
  78. this.identifier = identifier;
  79. this.value = value;
  80. }
  81. }
  82. export class Let extends Node {
  83. public bindings: Binding[];
  84. public children: Node[];
  85. public constructor(bindings: Binding[], children: Node[]) {
  86. super();
  87. this.bindings = bindings;
  88. this.children = children;
  89. }
  90. public compile(env: Env, opts: Opts): string | EnvError {
  91. this.bindings.forEach((binding) => {
  92. env.set(binding.identifier.name.value, binding.value);
  93. });
  94. const children = this.children.map((child) => child.compile(env, opts));
  95. const childrenError = children.find((node) => node instanceof EnvError);
  96. if (childrenError instanceof EnvError) return childrenError;
  97. return children.join(opts.prettyPrint ? '\n' : '');
  98. }
  99. }
  100. export class MediaQueryPredicate extends Node {
  101. public property: Property;
  102. public value: Literal | Identifier | Application;
  103. public constructor(
  104. property: Property,
  105. value: Literal | Identifier | Application
  106. ) {
  107. super();
  108. this.property = property;
  109. this.value = value;
  110. }
  111. public compile(env: Env, opts: Opts) {
  112. const wordSpacer = opts.prettyPrint ? ' ' : '';
  113. const property = this.property.compile(env, opts);
  114. const value = this.value.compile(env, opts);
  115. return `${property}:${wordSpacer}${value}`;
  116. }
  117. }
  118. export class MediaQuery extends Node {
  119. public predicate: MediaQueryPredicate;
  120. public children: Node[];
  121. public constructor(predicate: MediaQueryPredicate, children: Node[]) {
  122. super();
  123. this.predicate = predicate;
  124. this.children = children;
  125. }
  126. public compile(env: Env, opts: Opts): string | EnvError {
  127. const lineSpacer = opts.prettyPrint ? '\n' : '';
  128. const wordSpacer = opts.prettyPrint ? ' ' : '';
  129. const predicate = this.predicate.compile(env, opts);
  130. const children = this.children.map((child) =>
  131. child.compile(env, { ...opts, depth: opts.depth + 2 })
  132. );
  133. const childrenError = children.find((node) => node instanceof EnvError);
  134. if (childrenError instanceof EnvError) return childrenError;
  135. return `@media(${predicate})${wordSpacer}{${lineSpacer}${children.join(
  136. lineSpacer
  137. )}${lineSpacer}}`;
  138. }
  139. }
  140. export class Keyframes extends Node {
  141. public name: Literal;
  142. public children: Node[];
  143. public constructor(name: Literal, children: Node[]) {
  144. super();
  145. this.name = name;
  146. this.children = children;
  147. }
  148. public compile(env: Env, opts: Opts) {
  149. const lineSpacer = opts.prettyPrint ? '\n' : '';
  150. const wordSpacer = opts.prettyPrint ? ' ' : '';
  151. const name = this.name.compile(env, opts);
  152. const children = this.children.map((child) =>
  153. child.compile(env, { ...opts, depth: opts.depth + 2 })
  154. );
  155. const childrenError = children.find((node) => node instanceof EnvError);
  156. if (childrenError instanceof EnvError) return childrenError;
  157. return `@keyframes ${name}${wordSpacer}{${lineSpacer}${children.join(
  158. lineSpacer
  159. )}${lineSpacer}}`;
  160. }
  161. }
  162. export class Application extends Node {
  163. public name: Literal;
  164. public arguments: Node[];
  165. public constructor(name: Literal, args: Node[]) {
  166. super();
  167. this.name = name;
  168. this.arguments = args;
  169. }
  170. public compile(env: Env, opts: Opts) {
  171. const wordSpacer = opts.prettyPrint ? ' ' : '';
  172. const [lParen, rParen, comma] = ['(', ')', ','];
  173. const compiledArguments = this.arguments
  174. .map((arg) => arg.compile(env, opts))
  175. .join(`${comma}${wordSpacer}`);
  176. return `${this.name.value.value}${lParen}${compiledArguments}${rParen}`;
  177. }
  178. }
  179. export class Mixin extends Node {
  180. public name: Token;
  181. public parameters: Identifier[];
  182. public children: Child[];
  183. public constructor(
  184. name: Token,
  185. parameters: Identifier[],
  186. children: Child[]
  187. ) {
  188. super();
  189. this.name = name;
  190. this.parameters = parameters;
  191. this.children = children;
  192. }
  193. public compile(env: Env, _opts: Opts) {
  194. env.set(this.name.value, this);
  195. return '';
  196. }
  197. }
  198. export class Rule extends Node {
  199. public property: Property;
  200. public value: Literal | Identifier | Application;
  201. public constructor(
  202. property: Property,
  203. value: Literal | Identifier | Application
  204. ) {
  205. super();
  206. this.property = property;
  207. this.value = value;
  208. }
  209. public compile(env: Env, opts: Opts): string | EnvError {
  210. const wordSpacer = opts.prettyPrint ? ' ' : '';
  211. const indentSpacer = opts.prettyPrint ? ' ' + spaces(opts.depth) : '';
  212. const property = this.property.compile(env, opts);
  213. if (property instanceof EnvError) return property;
  214. const value = this.value.compile(env, opts);
  215. if (value instanceof EnvError) return value;
  216. return `${indentSpacer}${property}:${wordSpacer}${value};`;
  217. }
  218. }
  219. export class RuleSet extends Node {
  220. public selectors: Selector[];
  221. public children: Child[];
  222. public constructor(selectors: Selector[], children: Child[] = []) {
  223. super();
  224. this.selectors = selectors;
  225. this.children = children;
  226. }
  227. public compile(env: Env, opts: Opts): string | EnvError {
  228. const lineSpacer = opts.prettyPrint ? '\n' : '';
  229. const wordSpacer = opts.prettyPrint ? ' ' : '';
  230. const indentSpacer = opts.prettyPrint ? spaces(opts.depth) : '';
  231. const [lBrace, rBrace] = ['{', '}'];
  232. const lineages = ([] as string[]).concat(
  233. ...this.selectors.map((sel) => sel.getLineages())
  234. );
  235. const rules: (String | EnvError)[] = [];
  236. const children: (String | EnvError)[] = [];
  237. for (const child of this.children) {
  238. if (child instanceof Rule) {
  239. rules.push(child.compile(env, opts));
  240. } else if (child instanceof RuleSet) {
  241. children.push(child.compile(env, opts));
  242. } else if (child instanceof Application) {
  243. const mixin = env.get(child.name.value);
  244. if (mixin instanceof EnvError) return mixin;
  245. if (mixin instanceof AST.Mixin) {
  246. if (mixin.parameters.length !== child.arguments.length) {
  247. return new EnvError(
  248. child.name.value.line,
  249. `Expected ${mixin.parameters.length} arguments but received ${
  250. child.arguments.length
  251. }`
  252. );
  253. }
  254. const mixinEnv = new Env(env);
  255. mixin.parameters.forEach((param, index) => {
  256. mixinEnv.set(param.name.value, child.arguments[index]);
  257. });
  258. mixin.children.forEach((mixinChild) => {
  259. if (mixinChild instanceof Rule) {
  260. rules.push(mixinChild.compile(mixinEnv, opts));
  261. } else if (mixinChild instanceof RuleSet) {
  262. mixinChild.selectors.map((sel) => {
  263. sel.parents = this.selectors;
  264. });
  265. children.push(mixinChild.compile(mixinEnv, opts));
  266. } else {
  267. console.log(mixinChild);
  268. }
  269. });
  270. }
  271. }
  272. }
  273. const rulesError = rules.find((node) => node instanceof EnvError);
  274. if (rulesError instanceof EnvError) return rulesError;
  275. const childrenError = children.find((node) => node instanceof EnvError);
  276. if (childrenError instanceof EnvError) return childrenError;
  277. const compiledRules = rules.join(lineSpacer);
  278. const compiledChildren = children.join(lineSpacer);
  279. const lineage = lineages.join(`,${wordSpacer}`);
  280. const frontMatter = `${indentSpacer}${lineage}${wordSpacer}${lBrace}${lineSpacer}`;
  281. const backMatter = `${lineSpacer}${indentSpacer}${rBrace}${
  282. compiledChildren.length ? lineSpacer : ''
  283. }`;
  284. return rules.length
  285. ? `${frontMatter}${compiledRules}${backMatter}${compiledChildren}`
  286. : compiledChildren;
  287. }
  288. }
  289. }