Browse Source

Initial commit

master
Dylan Baker 5 years ago
commit
baed12e24e
17 changed files with 2486 additions and 0 deletions
  1. 2
    0
      .gitignore
  2. 6
    0
      .prettierrc
  3. 776
    0
      package-lock.json
  4. 26
    0
      package.json
  5. 309
    0
      src/ast.ts
  6. 36
    0
      src/compiler.ts
  7. 35
    0
      src/env.ts
  8. 13
    0
      src/error.ts
  9. 147
    0
      src/lexer.ts
  10. 87
    0
      src/moss.ts
  11. 341
    0
      src/parser.ts
  12. 171
    0
      src/tests/compiler.test.ts
  13. 188
    0
      src/tests/lexer.test.ts
  14. 289
    0
      src/tests/parser.test.ts
  15. 31
    0
      src/token.ts
  16. 23
    0
      tsconfig.json
  17. 6
    0
      tslint.json

+ 2
- 0
.gitignore View File

@@ -0,0 +1,2 @@
1
+dist/
2
+node_modules/

+ 6
- 0
.prettierrc View File

@@ -0,0 +1,6 @@
1
+{
2
+  "singleQuote": true,
3
+  "trailingComma": "es5",
4
+  "arrowParens": "always",
5
+  "semi": true
6
+}

+ 776
- 0
package-lock.json View File

@@ -0,0 +1,776 @@
1
+{
2
+  "name": "moss",
3
+  "version": "0.1.0",
4
+  "lockfileVersion": 1,
5
+  "requires": true,
6
+  "dependencies": {
7
+    "@types/minimist": {
8
+      "version": "1.2.0",
9
+      "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.0.tgz",
10
+      "integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=",
11
+      "dev": true
12
+    },
13
+    "@types/node": {
14
+      "version": "10.12.18",
15
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz",
16
+      "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==",
17
+      "dev": true
18
+    },
19
+    "@types/tape": {
20
+      "version": "4.2.33",
21
+      "resolved": "https://registry.npmjs.org/@types/tape/-/tape-4.2.33.tgz",
22
+      "integrity": "sha512-ltfyuY5BIkYlGuQfwqzTDT8f0q8Z5DGppvUnWGs39oqDmMd6/UWhNpX3ZMh/VYvfxs3rFGHMrLC/eGRdLiDGuw==",
23
+      "dev": true,
24
+      "requires": {
25
+        "@types/node": "*"
26
+      }
27
+    },
28
+    "ansi-regex": {
29
+      "version": "2.1.1",
30
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
31
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
32
+      "dev": true
33
+    },
34
+    "ansi-styles": {
35
+      "version": "2.2.1",
36
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
37
+      "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
38
+      "dev": true
39
+    },
40
+    "argparse": {
41
+      "version": "1.0.10",
42
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
43
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
44
+      "dev": true,
45
+      "requires": {
46
+        "sprintf-js": "~1.0.2"
47
+      }
48
+    },
49
+    "arrify": {
50
+      "version": "1.0.1",
51
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
52
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
53
+      "dev": true
54
+    },
55
+    "babel-code-frame": {
56
+      "version": "6.26.0",
57
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
58
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
59
+      "dev": true,
60
+      "requires": {
61
+        "chalk": "^1.1.3",
62
+        "esutils": "^2.0.2",
63
+        "js-tokens": "^3.0.2"
64
+      },
65
+      "dependencies": {
66
+        "chalk": {
67
+          "version": "1.1.3",
68
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
69
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
70
+          "dev": true,
71
+          "requires": {
72
+            "ansi-styles": "^2.2.1",
73
+            "escape-string-regexp": "^1.0.2",
74
+            "has-ansi": "^2.0.0",
75
+            "strip-ansi": "^3.0.0",
76
+            "supports-color": "^2.0.0"
77
+          }
78
+        }
79
+      }
80
+    },
81
+    "balanced-match": {
82
+      "version": "1.0.0",
83
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
84
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
85
+      "dev": true
86
+    },
87
+    "brace-expansion": {
88
+      "version": "1.1.11",
89
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
90
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
91
+      "dev": true,
92
+      "requires": {
93
+        "balanced-match": "^1.0.0",
94
+        "concat-map": "0.0.1"
95
+      }
96
+    },
97
+    "buffer-from": {
98
+      "version": "1.1.1",
99
+      "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
100
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
101
+      "dev": true
102
+    },
103
+    "builtin-modules": {
104
+      "version": "1.1.1",
105
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
106
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
107
+      "dev": true
108
+    },
109
+    "chalk": {
110
+      "version": "2.4.2",
111
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
112
+      "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
113
+      "dev": true,
114
+      "requires": {
115
+        "ansi-styles": "^3.2.1",
116
+        "escape-string-regexp": "^1.0.5",
117
+        "supports-color": "^5.3.0"
118
+      },
119
+      "dependencies": {
120
+        "ansi-styles": {
121
+          "version": "3.2.1",
122
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
123
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
124
+          "dev": true,
125
+          "requires": {
126
+            "color-convert": "^1.9.0"
127
+          }
128
+        },
129
+        "supports-color": {
130
+          "version": "5.5.0",
131
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
132
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
133
+          "dev": true,
134
+          "requires": {
135
+            "has-flag": "^3.0.0"
136
+          }
137
+        }
138
+      }
139
+    },
140
+    "color-convert": {
141
+      "version": "1.9.3",
142
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
143
+      "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
144
+      "dev": true,
145
+      "requires": {
146
+        "color-name": "1.1.3"
147
+      }
148
+    },
149
+    "color-name": {
150
+      "version": "1.1.3",
151
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
152
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
153
+      "dev": true
154
+    },
155
+    "commander": {
156
+      "version": "2.19.0",
157
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
158
+      "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==",
159
+      "dev": true
160
+    },
161
+    "concat-map": {
162
+      "version": "0.0.1",
163
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
164
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
165
+      "dev": true
166
+    },
167
+    "core-util-is": {
168
+      "version": "1.0.2",
169
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
170
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
171
+      "dev": true
172
+    },
173
+    "deep-equal": {
174
+      "version": "1.0.1",
175
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
176
+      "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
177
+      "dev": true
178
+    },
179
+    "define-properties": {
180
+      "version": "1.1.3",
181
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
182
+      "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
183
+      "dev": true,
184
+      "requires": {
185
+        "object-keys": "^1.0.12"
186
+      }
187
+    },
188
+    "defined": {
189
+      "version": "1.0.0",
190
+      "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
191
+      "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
192
+      "dev": true
193
+    },
194
+    "diff": {
195
+      "version": "3.5.0",
196
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
197
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
198
+      "dev": true
199
+    },
200
+    "es-abstract": {
201
+      "version": "1.13.0",
202
+      "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz",
203
+      "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==",
204
+      "dev": true,
205
+      "requires": {
206
+        "es-to-primitive": "^1.2.0",
207
+        "function-bind": "^1.1.1",
208
+        "has": "^1.0.3",
209
+        "is-callable": "^1.1.4",
210
+        "is-regex": "^1.0.4",
211
+        "object-keys": "^1.0.12"
212
+      }
213
+    },
214
+    "es-to-primitive": {
215
+      "version": "1.2.0",
216
+      "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
217
+      "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
218
+      "dev": true,
219
+      "requires": {
220
+        "is-callable": "^1.1.4",
221
+        "is-date-object": "^1.0.1",
222
+        "is-symbol": "^1.0.2"
223
+      }
224
+    },
225
+    "escape-string-regexp": {
226
+      "version": "1.0.5",
227
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
228
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
229
+      "dev": true
230
+    },
231
+    "esprima": {
232
+      "version": "4.0.1",
233
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
234
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
235
+      "dev": true
236
+    },
237
+    "esutils": {
238
+      "version": "2.0.2",
239
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
240
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
241
+      "dev": true
242
+    },
243
+    "for-each": {
244
+      "version": "0.3.3",
245
+      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
246
+      "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
247
+      "dev": true,
248
+      "requires": {
249
+        "is-callable": "^1.1.3"
250
+      }
251
+    },
252
+    "fs.realpath": {
253
+      "version": "1.0.0",
254
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
255
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
256
+      "dev": true
257
+    },
258
+    "function-bind": {
259
+      "version": "1.1.1",
260
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
261
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
262
+      "dev": true
263
+    },
264
+    "glob": {
265
+      "version": "7.1.3",
266
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
267
+      "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
268
+      "dev": true,
269
+      "requires": {
270
+        "fs.realpath": "^1.0.0",
271
+        "inflight": "^1.0.4",
272
+        "inherits": "2",
273
+        "minimatch": "^3.0.4",
274
+        "once": "^1.3.0",
275
+        "path-is-absolute": "^1.0.0"
276
+      }
277
+    },
278
+    "has": {
279
+      "version": "1.0.3",
280
+      "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
281
+      "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
282
+      "dev": true,
283
+      "requires": {
284
+        "function-bind": "^1.1.1"
285
+      }
286
+    },
287
+    "has-ansi": {
288
+      "version": "2.0.0",
289
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
290
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
291
+      "dev": true,
292
+      "requires": {
293
+        "ansi-regex": "^2.0.0"
294
+      }
295
+    },
296
+    "has-flag": {
297
+      "version": "3.0.0",
298
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
299
+      "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
300
+      "dev": true
301
+    },
302
+    "has-symbols": {
303
+      "version": "1.0.0",
304
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
305
+      "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=",
306
+      "dev": true
307
+    },
308
+    "inflight": {
309
+      "version": "1.0.6",
310
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
311
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
312
+      "dev": true,
313
+      "requires": {
314
+        "once": "^1.3.0",
315
+        "wrappy": "1"
316
+      }
317
+    },
318
+    "inherits": {
319
+      "version": "2.0.3",
320
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
321
+      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
322
+      "dev": true
323
+    },
324
+    "is-callable": {
325
+      "version": "1.1.4",
326
+      "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
327
+      "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==",
328
+      "dev": true
329
+    },
330
+    "is-date-object": {
331
+      "version": "1.0.1",
332
+      "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
333
+      "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=",
334
+      "dev": true
335
+    },
336
+    "is-regex": {
337
+      "version": "1.0.4",
338
+      "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
339
+      "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
340
+      "dev": true,
341
+      "requires": {
342
+        "has": "^1.0.1"
343
+      }
344
+    },
345
+    "is-symbol": {
346
+      "version": "1.0.2",
347
+      "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
348
+      "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
349
+      "dev": true,
350
+      "requires": {
351
+        "has-symbols": "^1.0.0"
352
+      }
353
+    },
354
+    "js-tokens": {
355
+      "version": "3.0.2",
356
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
357
+      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=",
358
+      "dev": true
359
+    },
360
+    "js-yaml": {
361
+      "version": "3.12.1",
362
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz",
363
+      "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==",
364
+      "dev": true,
365
+      "requires": {
366
+        "argparse": "^1.0.7",
367
+        "esprima": "^4.0.0"
368
+      }
369
+    },
370
+    "make-error": {
371
+      "version": "1.3.5",
372
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
373
+      "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==",
374
+      "dev": true
375
+    },
376
+    "minimatch": {
377
+      "version": "3.0.4",
378
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
379
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
380
+      "dev": true,
381
+      "requires": {
382
+        "brace-expansion": "^1.1.7"
383
+      }
384
+    },
385
+    "minimist": {
386
+      "version": "1.2.0",
387
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
388
+      "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
389
+      "dev": true
390
+    },
391
+    "mkdirp": {
392
+      "version": "0.5.1",
393
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
394
+      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
395
+      "dev": true,
396
+      "requires": {
397
+        "minimist": "0.0.8"
398
+      },
399
+      "dependencies": {
400
+        "minimist": {
401
+          "version": "0.0.8",
402
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
403
+          "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
404
+          "dev": true
405
+        }
406
+      }
407
+    },
408
+    "object-inspect": {
409
+      "version": "1.6.0",
410
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz",
411
+      "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==",
412
+      "dev": true
413
+    },
414
+    "object-keys": {
415
+      "version": "1.0.12",
416
+      "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz",
417
+      "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==",
418
+      "dev": true
419
+    },
420
+    "once": {
421
+      "version": "1.4.0",
422
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
423
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
424
+      "dev": true,
425
+      "requires": {
426
+        "wrappy": "1"
427
+      }
428
+    },
429
+    "path-is-absolute": {
430
+      "version": "1.0.1",
431
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
432
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
433
+      "dev": true
434
+    },
435
+    "path-parse": {
436
+      "version": "1.0.6",
437
+      "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
438
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
439
+      "dev": true
440
+    },
441
+    "process-nextick-args": {
442
+      "version": "2.0.0",
443
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
444
+      "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
445
+      "dev": true
446
+    },
447
+    "re-emitter": {
448
+      "version": "1.1.3",
449
+      "resolved": "https://registry.npmjs.org/re-emitter/-/re-emitter-1.1.3.tgz",
450
+      "integrity": "sha1-+p4xn/3u6zWycpbvDz03TawvUqc=",
451
+      "dev": true
452
+    },
453
+    "resolve": {
454
+      "version": "1.9.0",
455
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz",
456
+      "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==",
457
+      "dev": true,
458
+      "requires": {
459
+        "path-parse": "^1.0.6"
460
+      }
461
+    },
462
+    "resumer": {
463
+      "version": "0.0.0",
464
+      "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz",
465
+      "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=",
466
+      "dev": true,
467
+      "requires": {
468
+        "through": "~2.3.4"
469
+      }
470
+    },
471
+    "safe-buffer": {
472
+      "version": "5.1.2",
473
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
474
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
475
+      "dev": true
476
+    },
477
+    "semver": {
478
+      "version": "5.6.0",
479
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
480
+      "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
481
+      "dev": true
482
+    },
483
+    "source-map": {
484
+      "version": "0.6.1",
485
+      "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
486
+      "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
487
+      "dev": true
488
+    },
489
+    "source-map-support": {
490
+      "version": "0.5.10",
491
+      "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.10.tgz",
492
+      "integrity": "sha512-YfQ3tQFTK/yzlGJuX8pTwa4tifQj4QS2Mj7UegOu8jAz59MqIiMGPXxQhVQiIMNzayuUSF/jEuVnfFF5JqybmQ==",
493
+      "dev": true,
494
+      "requires": {
495
+        "buffer-from": "^1.0.0",
496
+        "source-map": "^0.6.0"
497
+      }
498
+    },
499
+    "split": {
500
+      "version": "1.0.1",
501
+      "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
502
+      "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
503
+      "dev": true,
504
+      "requires": {
505
+        "through": "2"
506
+      }
507
+    },
508
+    "sprintf-js": {
509
+      "version": "1.0.3",
510
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
511
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
512
+      "dev": true
513
+    },
514
+    "string.prototype.trim": {
515
+      "version": "1.1.2",
516
+      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz",
517
+      "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=",
518
+      "dev": true,
519
+      "requires": {
520
+        "define-properties": "^1.1.2",
521
+        "es-abstract": "^1.5.0",
522
+        "function-bind": "^1.0.2"
523
+      }
524
+    },
525
+    "strip-ansi": {
526
+      "version": "3.0.1",
527
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
528
+      "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
529
+      "dev": true,
530
+      "requires": {
531
+        "ansi-regex": "^2.0.0"
532
+      }
533
+    },
534
+    "supports-color": {
535
+      "version": "2.0.0",
536
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
537
+      "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=",
538
+      "dev": true
539
+    },
540
+    "tap-dot": {
541
+      "version": "2.0.0",
542
+      "resolved": "https://registry.npmjs.org/tap-dot/-/tap-dot-2.0.0.tgz",
543
+      "integrity": "sha512-7N1yPcRDgdfHCUbG6lZ0hXo53NyXhKIjJNhqKBixl9HVEG4QasG16Nlvr8wRnqr2ZRYVWmbmxwF3NOBbTLtQLQ==",
544
+      "dev": true,
545
+      "requires": {
546
+        "chalk": "^1.1.1",
547
+        "tap-out": "^1.3.2",
548
+        "through2": "^2.0.0"
549
+      },
550
+      "dependencies": {
551
+        "chalk": {
552
+          "version": "1.1.3",
553
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
554
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
555
+          "dev": true,
556
+          "requires": {
557
+            "ansi-styles": "^2.2.1",
558
+            "escape-string-regexp": "^1.0.2",
559
+            "has-ansi": "^2.0.0",
560
+            "strip-ansi": "^3.0.0",
561
+            "supports-color": "^2.0.0"
562
+          }
563
+        },
564
+        "isarray": {
565
+          "version": "1.0.0",
566
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
567
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
568
+          "dev": true
569
+        },
570
+        "readable-stream": {
571
+          "version": "2.3.6",
572
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
573
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
574
+          "dev": true,
575
+          "requires": {
576
+            "core-util-is": "~1.0.0",
577
+            "inherits": "~2.0.3",
578
+            "isarray": "~1.0.0",
579
+            "process-nextick-args": "~2.0.0",
580
+            "safe-buffer": "~5.1.1",
581
+            "string_decoder": "~1.1.1",
582
+            "util-deprecate": "~1.0.1"
583
+          }
584
+        },
585
+        "string_decoder": {
586
+          "version": "1.1.1",
587
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
588
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
589
+          "dev": true,
590
+          "requires": {
591
+            "safe-buffer": "~5.1.0"
592
+          }
593
+        },
594
+        "through2": {
595
+          "version": "2.0.5",
596
+          "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
597
+          "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
598
+          "dev": true,
599
+          "requires": {
600
+            "readable-stream": "~2.3.6",
601
+            "xtend": "~4.0.1"
602
+          }
603
+        },
604
+        "xtend": {
605
+          "version": "4.0.1",
606
+          "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
607
+          "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
608
+          "dev": true
609
+        }
610
+      }
611
+    },
612
+    "tap-out": {
613
+      "version": "1.4.2",
614
+      "resolved": "https://registry.npmjs.org/tap-out/-/tap-out-1.4.2.tgz",
615
+      "integrity": "sha1-yQfsG/lAURHQiCY+kvVgi4jLs3o=",
616
+      "dev": true,
617
+      "requires": {
618
+        "re-emitter": "^1.0.0",
619
+        "readable-stream": "^2.0.0",
620
+        "split": "^1.0.0",
621
+        "trim": "0.0.1"
622
+      },
623
+      "dependencies": {
624
+        "isarray": {
625
+          "version": "1.0.0",
626
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
627
+          "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
628
+          "dev": true
629
+        },
630
+        "readable-stream": {
631
+          "version": "2.3.6",
632
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
633
+          "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
634
+          "dev": true,
635
+          "requires": {
636
+            "core-util-is": "~1.0.0",
637
+            "inherits": "~2.0.3",
638
+            "isarray": "~1.0.0",
639
+            "process-nextick-args": "~2.0.0",
640
+            "safe-buffer": "~5.1.1",
641
+            "string_decoder": "~1.1.1",
642
+            "util-deprecate": "~1.0.1"
643
+          }
644
+        },
645
+        "string_decoder": {
646
+          "version": "1.1.1",
647
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
648
+          "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
649
+          "dev": true,
650
+          "requires": {
651
+            "safe-buffer": "~5.1.0"
652
+          }
653
+        }
654
+      }
655
+    },
656
+    "tape": {
657
+      "version": "4.9.2",
658
+      "resolved": "https://registry.npmjs.org/tape/-/tape-4.9.2.tgz",
659
+      "integrity": "sha512-lPXKRKILZ1kZaUy5ynWKs8ATGSUO7HAFHCFnBam6FaGSqPdOwMWbxXHq4EXFLE8WRTleo/YOMXkaUTRmTB1Fiw==",
660
+      "dev": true,
661
+      "requires": {
662
+        "deep-equal": "~1.0.1",
663
+        "defined": "~1.0.0",
664
+        "for-each": "~0.3.3",
665
+        "function-bind": "~1.1.1",
666
+        "glob": "~7.1.2",
667
+        "has": "~1.0.3",
668
+        "inherits": "~2.0.3",
669
+        "minimist": "~1.2.0",
670
+        "object-inspect": "~1.6.0",
671
+        "resolve": "~1.7.1",
672
+        "resumer": "~0.0.0",
673
+        "string.prototype.trim": "~1.1.2",
674
+        "through": "~2.3.8"
675
+      },
676
+      "dependencies": {
677
+        "resolve": {
678
+          "version": "1.7.1",
679
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz",
680
+          "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==",
681
+          "dev": true,
682
+          "requires": {
683
+            "path-parse": "^1.0.5"
684
+          }
685
+        }
686
+      }
687
+    },
688
+    "through": {
689
+      "version": "2.3.8",
690
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
691
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
692
+      "dev": true
693
+    },
694
+    "trim": {
695
+      "version": "0.0.1",
696
+      "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
697
+      "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=",
698
+      "dev": true
699
+    },
700
+    "ts-node": {
701
+      "version": "7.0.1",
702
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz",
703
+      "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==",
704
+      "dev": true,
705
+      "requires": {
706
+        "arrify": "^1.0.0",
707
+        "buffer-from": "^1.1.0",
708
+        "diff": "^3.1.0",
709
+        "make-error": "^1.1.1",
710
+        "minimist": "^1.2.0",
711
+        "mkdirp": "^0.5.1",
712
+        "source-map-support": "^0.5.6",
713
+        "yn": "^2.0.0"
714
+      }
715
+    },
716
+    "tslib": {
717
+      "version": "1.9.3",
718
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
719
+      "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
720
+      "dev": true
721
+    },
722
+    "tslint": {
723
+      "version": "5.12.1",
724
+      "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.12.1.tgz",
725
+      "integrity": "sha512-sfodBHOucFg6egff8d1BvuofoOQ/nOeYNfbp7LDlKBcLNrL3lmS5zoiDGyOMdT7YsEXAwWpTdAHwOGOc8eRZAw==",
726
+      "dev": true,
727
+      "requires": {
728
+        "babel-code-frame": "^6.22.0",
729
+        "builtin-modules": "^1.1.1",
730
+        "chalk": "^2.3.0",
731
+        "commander": "^2.12.1",
732
+        "diff": "^3.2.0",
733
+        "glob": "^7.1.1",
734
+        "js-yaml": "^3.7.0",
735
+        "minimatch": "^3.0.4",
736
+        "resolve": "^1.3.2",
737
+        "semver": "^5.3.0",
738
+        "tslib": "^1.8.0",
739
+        "tsutils": "^2.27.2"
740
+      }
741
+    },
742
+    "tsutils": {
743
+      "version": "2.29.0",
744
+      "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
745
+      "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
746
+      "dev": true,
747
+      "requires": {
748
+        "tslib": "^1.8.1"
749
+      }
750
+    },
751
+    "typescript": {
752
+      "version": "3.2.2",
753
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.2.tgz",
754
+      "integrity": "sha512-VCj5UiSyHBjwfYacmDuc/NOk4QQixbE+Wn7MFJuS0nRuPQbof132Pw4u53dm264O8LPc2MVsc7RJNml5szurkg==",
755
+      "dev": true
756
+    },
757
+    "util-deprecate": {
758
+      "version": "1.0.2",
759
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
760
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
761
+      "dev": true
762
+    },
763
+    "wrappy": {
764
+      "version": "1.0.2",
765
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
766
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
767
+      "dev": true
768
+    },
769
+    "yn": {
770
+      "version": "2.0.0",
771
+      "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz",
772
+      "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=",
773
+      "dev": true
774
+    }
775
+  }
776
+}

+ 26
- 0
package.json View File

@@ -0,0 +1,26 @@
1
+{
2
+  "name": "moss",
3
+  "version": "0.1.0",
4
+  "description": "A CSS preprocessor with a Lisp-inspired syntax",
5
+  "scripts": {
6
+    "test": "ts-node ./node_modules/.bin/tape ./src/tests/**/*.test.ts | tap-dot",
7
+    "prettier": "prettier --write ./**/*.ts"
8
+  },
9
+  "bin": {
10
+    "moss": "./dist/moss.js"
11
+  },
12
+  "keywords": [],
13
+  "author": "Dylan Baker <dylanbaker.ct@gmail.com>",
14
+  "license": "MIT",
15
+  "devDependencies": {
16
+    "@types/minimist": "^1.2.0",
17
+    "@types/node": "^10.12.18",
18
+    "@types/tape": "^4.2.33",
19
+    "minimist": "^1.2.0",
20
+    "tap-dot": "^2.0.0",
21
+    "tape": "^4.9.2",
22
+    "ts-node": "^7.0.1",
23
+    "tslint": "^5.12.1",
24
+    "typescript": "^3.2.2"
25
+  }
26
+}

+ 309
- 0
src/ast.ts View File

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

+ 36
- 0
src/compiler.ts View File

@@ -0,0 +1,36 @@
1
+import { AST } from './ast';
2
+import Env, { EnvError } from './env';
3
+
4
+export interface CompilerOpts {
5
+  prettyPrint?: boolean;
6
+  env?: Env;
7
+}
8
+
9
+export default class Compiler {
10
+  private tree: AST.Node[];
11
+  private prettyPrint: boolean;
12
+  private env: Env;
13
+
14
+  public constructor(
15
+    parserResult: { tree: AST.Node[]; module?: string },
16
+    opts: CompilerOpts
17
+  ) {
18
+    this.tree = parserResult.tree;
19
+    this.prettyPrint = opts.prettyPrint || false;
20
+    this.env = opts.env || new Env();
21
+  }
22
+
23
+  public compile(): string | EnvError {
24
+    const result = this.tree.map((node) =>
25
+      node.compile(this.env, {
26
+        depth: 0,
27
+        prettyPrint: this.prettyPrint,
28
+      })
29
+    );
30
+
31
+    const resultError = result.find((el) => el instanceof EnvError);
32
+    if (resultError !== undefined) return resultError;
33
+
34
+    return result.join(this.prettyPrint ? '\n' : '');
35
+  }
36
+}

+ 35
- 0
src/env.ts View File

@@ -0,0 +1,35 @@
1
+import { AST } from './ast';
2
+import { Error } from './error';
3
+import Token from './token';
4
+
5
+export class EnvError implements Error {
6
+  public line: number;
7
+  public message: string;
8
+  public module?: string;
9
+
10
+  public constructor(line: number, message: string, module?: string) {
11
+    this.line = line;
12
+    this.message = message;
13
+    this.module = module;
14
+  }
15
+}
16
+
17
+export default class Env {
18
+  public data: { [name: string]: AST.Node };
19
+
20
+  public constructor(parent = {}) {
21
+    this.data = Object.assign({}, parent);
22
+  }
23
+
24
+  public get(name: Token): AST.Node | EnvError {
25
+    if (this.data[name.value]) return this.data[name.value];
26
+    return new EnvError(
27
+      name.line,
28
+      `Reference to unbound variable ${name.value}`
29
+    );
30
+  }
31
+
32
+  public set(name: string, value: AST.Node): void {
33
+    this.data[name] = value;
34
+  }
35
+}

+ 13
- 0
src/error.ts View File

@@ -0,0 +1,13 @@
1
+export interface Error {
2
+  line: number;
3
+  message: string;
4
+  module?: string;
5
+}
6
+
7
+export const isError = (obj: any): obj is Error => {
8
+  try {
9
+    return 'line' in obj && 'message' in obj;
10
+  } catch {
11
+    return false;
12
+  }
13
+};

+ 147
- 0
src/lexer.ts View File

@@ -0,0 +1,147 @@
1
+import { Error } from './error';
2
+import Token, { TokenTypes } from './token';
3
+
4
+export interface LexerError {}
5
+
6
+export class LexerError implements Error {
7
+  public line: number;
8
+  public message: string;
9
+  public module?: string;
10
+
11
+  public constructor(line: number, message: string, module?: string) {
12
+    this.line = line;
13
+    this.message = message;
14
+    this.module = module;
15
+  }
16
+}
17
+
18
+export type LexerResult = { tokens: Token[]; module?: string } | LexerError;
19
+
20
+export default class Lexer {
21
+  private source: string;
22
+  private module?: string;
23
+  private line: number;
24
+  private position: number;
25
+  private tokens: Token[];
26
+  private allowWhitespace: boolean;
27
+
28
+  public constructor(source: string, module?: string) {
29
+    this.source = source;
30
+    this.module = module;
31
+    this.line = 1;
32
+    this.position = 0;
33
+    this.tokens = [];
34
+    this.allowWhitespace = true;
35
+  }
36
+
37
+  public scan(): LexerResult {
38
+    while (this.position < this.source.length) {
39
+      const result = this.getToken();
40
+      if (!(result instanceof Token)) return result;
41
+      if (result.type !== TokenTypes.WHITESPACE && result.type !== TokenTypes.COMMENT) {
42
+        this.tokens.push(result);
43
+      }
44
+
45
+      if (result.type !== TokenTypes.LITERAL) {
46
+        this.position += result.value.length;
47
+      }
48
+
49
+      if (this.hasSigil(result)) {
50
+        this.position += 1;
51
+      }
52
+    }
53
+
54
+    this.tokens.push(this.token(TokenTypes.EOF, 'eof'));
55
+
56
+    return {
57
+      tokens: this.tokens,
58
+      module: this.module,
59
+    };
60
+  }
61
+
62
+  private getToken(): Token | LexerError {
63
+    const c = this.currentChar();
64
+    const source = this.source.slice(this.position);
65
+    if (c === '(') {
66
+      this.allowWhitespace = true;
67
+      return this.token(TokenTypes.LPAREN, '(');
68
+    } else if (c === ')') {
69
+      this.allowWhitespace = true;
70
+      return this.token(TokenTypes.RPAREN, ')');
71
+    } else if (c === ',') {
72
+      return this.token(TokenTypes.COMMA, ',');
73
+    } else if (c === ':') {
74
+      const match = source.match(/^:([a-z][a-zA-Z0-9_-]*)/);
75
+      if (match === null) {
76
+        return this.error(
77
+          `Unexpected character ${this.source[this.position + 1]}`
78
+        );
79
+      }
80
+      return this.token(TokenTypes.PROPERTY, match[1]);
81
+    } else if (c === '$') {
82
+      const match = source.match(/^\$([a-z][a-zA-Z0-9_-]*)/);
83
+      if (match === null) {
84
+        return this.error(
85
+          `Unexpected character ${this.source[this.position + 1]}`
86
+        );
87
+      }
88
+      return this.token(TokenTypes.IDENTIFIER, match[1]);
89
+    } else if (c === '@') {
90
+      this.allowWhitespace = false;
91
+      const match = source.match(/^\@([a-z][a-zA-Z0-9_-]*)/);
92
+      if (match === null) {
93
+        return this.error(
94
+          `Unexpected character ${this.source[this.position + 1]}`
95
+        );
96
+      }
97
+      return this.token(TokenTypes.FUNCTION_NAME, match[1]);
98
+    } else if (c.match(/^\s/)) {
99
+      if (c === '\n') this.line += 1;
100
+      return this.token(TokenTypes.WHITESPACE, c);
101
+    } else if (c === ';') {
102
+      while (this.currentChar() !== '\n') {
103
+        this.position++;
104
+      }
105
+      return this.token(TokenTypes.COMMENT, '');
106
+    } else {
107
+      let literal = '';
108
+      const endPattern = this.allowWhitespace ? /^[\(\)\n\,]/ : /^[\(\)\n\, ]/;
109
+      while (
110
+        this.position < this.source.length &&
111
+        !this.source.slice(this.position).match(/^ [\:\$][a-z]/) &&
112
+        !this.source.slice(this.position).match(endPattern) &&
113
+        !(this.currentChar() === ' ' && this.nextChar() === '(')
114
+      ) {
115
+        literal += this.currentChar();
116
+        this.position += 1;
117
+      }
118
+      return this.token(TokenTypes.LITERAL, literal);
119
+    }
120
+
121
+    return this.error(`Unexpected character ${c}`);
122
+  }
123
+
124
+  private token(type: TokenTypes, value: string): Token {
125
+    return new Token(type, value, this.line, this.module);
126
+  }
127
+
128
+  private error(message: string): LexerError {
129
+    return new LexerError(this.line, message, this.module);
130
+  }
131
+
132
+  private currentChar(): string {
133
+    return this.source[this.position];
134
+  }
135
+
136
+  private nextChar(): string {
137
+    return this.source[this.position + 1];
138
+  }
139
+
140
+  private hasSigil(token: Token): boolean {
141
+    return [
142
+      TokenTypes.IDENTIFIER,
143
+      TokenTypes.PROPERTY,
144
+      TokenTypes.FUNCTION_NAME,
145
+    ].includes(token.type);
146
+  }
147
+}

+ 87
- 0
src/moss.ts View File

@@ -0,0 +1,87 @@
1
+#!/usr/bin/env node
2
+import * as minimist from 'minimist';
3
+import * as fs from 'fs';
4
+import * as path from 'path';
5
+
6
+import Compiler from './compiler';
7
+import { isError } from './error';
8
+import { EnvError } from './env';
9
+import Lexer, { LexerError } from './lexer';
10
+import Parser, { ParserError } from './parser';
11
+
12
+// import { inspect } from 'util';
13
+// const print = (obj: any) => {
14
+//   console.log(inspect(obj, { depth: null }));
15
+// };
16
+
17
+const rawArgs = minimist(process.argv.slice(2));
18
+
19
+const args = {
20
+  eval: rawArgs.e || rawArgs.eval,
21
+  file: rawArgs.f || rawArgs.file,
22
+  help: rawArgs.h || rawArgs.help,
23
+  output: rawArgs.o || rawArgs.output,
24
+  pretty: rawArgs.p || rawArgs.pretty,
25
+};
26
+
27
+const helpText = `Usage: moss [options]
28
+    -e, --eval <source>    Evaluate a string of source code
29
+    -f, --file <path>      Evaluate a file containing source code
30
+    -h, --help             Print this message
31
+    -o, --output <path>    Write output to a file
32
+    -p, --pretty           Pretty print the output`;
33
+
34
+const moss = (
35
+  source: string,
36
+  file?: string
37
+): LexerError | ParserError | EnvError | string => {
38
+  const lexer = new Lexer(source, file);
39
+  const tokens = lexer.scan();
40
+  if (tokens instanceof LexerError) {
41
+    return tokens;
42
+  } else {
43
+    const parser = new Parser(tokens);
44
+    const tree = parser.parse();
45
+    if (tree instanceof ParserError) {
46
+      return tree;
47
+    } else {
48
+      const compiler = new Compiler(tree, { prettyPrint: args.pretty });
49
+      return compiler.compile();
50
+    }
51
+  }
52
+};
53
+
54
+const run = () => {
55
+  if (args.help) {
56
+    return helpText;
57
+  } else if (args.file) {
58
+    const filepath = path.join(process.cwd(), args.file);
59
+    if (filepath !== null) {
60
+      const source = fs.readFileSync(filepath, 'utf-8');
61
+      return moss(source, args.file);
62
+    }
63
+    return '';
64
+  } else if (args.eval) {
65
+    return moss(args.eval);
66
+  }
67
+
68
+  return helpText;
69
+};
70
+
71
+const output = run();
72
+
73
+if (isError(output)) {
74
+  const moduleAndLine = `${output.module ? output.module : '-e'}:${
75
+    output.line
76
+  }`;
77
+  console.log(`Error ${moduleAndLine}
78
+${output.message}`);
79
+} else {
80
+  if (output.length) {
81
+    if (args.output) {
82
+      fs.writeFileSync(path.join(process.cwd(), args.output), output);
83
+    } else {
84
+      console.log(output);
85
+    }
86
+  }
87
+}

+ 341
- 0
src/parser.ts View File

@@ -0,0 +1,341 @@
1
+import { AST } from './ast';
2
+import { Error } from './error';
3
+import Token, { TokenTypes } from './token';
4
+
5
+export class ParserError implements Error {
6
+  public message: string;
7
+  public line: number;
8
+  public module?: string;
9
+
10
+  public constructor(message: string, line: number, module?: string) {
11
+    this.message = message;
12
+    this.line = line;
13
+    this.module = module;
14
+  }
15
+}
16
+
17
+export type ParserResult = { tree: AST.Node[]; module?: string } | ParserError;
18
+
19
+export default class Parser {
20
+  private tokens: Token[];
21
+  private module?: string;
22
+  private tree: AST.Node[];
23
+  private position: number;
24
+
25
+  public constructor(lexerResult: { tokens: Token[]; module?: string }) {
26
+    this.tokens = lexerResult.tokens;
27
+    this.module = lexerResult.module;
28
+    this.tree = [];
29
+    this.position = 0;
30
+  }
31
+
32
+  public parse(): ParserResult {
33
+    while (this.currentToken().type != TokenTypes.EOF) {
34
+      const expr = this.expr();
35
+      if (expr instanceof ParserError) return expr;
36
+      this.tree.push(expr);
37
+    }
38
+
39
+    this.eat(TokenTypes.EOF);
40
+
41
+    return {
42
+      tree: this.tree,
43
+      module: this.module,
44
+    };
45
+  }
46
+
47
+  private expr(): AST.Node | ParserError {
48
+    const currentToken = this.currentToken();
49
+    switch (currentToken.type) {
50
+      case TokenTypes.LITERAL:
51
+      case TokenTypes.IDENTIFIER:
52
+        return this.value();
53
+      case TokenTypes.LPAREN:
54
+        if (
55
+          this.nextToken() &&
56
+          this.nextToken().type === TokenTypes.FUNCTION_NAME
57
+        ) {
58
+          switch (this.nextToken().value) {
59
+            case 'let':
60
+              return this.let();
61
+            case 'media':
62
+              return this.mediaQuery();
63
+            case 'keyframes':
64
+              return this.keyframes();
65
+            case 'mixin':
66
+              return this.mixin();
67
+            default:
68
+              return this.application();
69
+          }
70
+          return this.error(
71
+            `Undefined function '${this.nextToken().value}`,
72
+            this.nextToken()
73
+          );
74
+        }
75
+        return this.ruleSet();
76
+      default:
77
+        return this.error(`Unexpected ${currentToken.type}`, currentToken);
78
+    }
79
+
80
+    return this.error(`Unexpected ${currentToken.type}`, currentToken);
81
+  }
82
+
83
+  private let(): AST.Let | ParserError {
84
+    const lParen = this.eat(TokenTypes.LPAREN);
85
+    if (lParen instanceof ParserError) return lParen;
86
+
87
+    const letToken = this.eat(TokenTypes.FUNCTION_NAME);
88
+    if (letToken instanceof ParserError) return letToken;
89
+
90
+    const bindings: AST.Binding[] = [];
91
+
92
+    while (this.currentToken().type === TokenTypes.IDENTIFIER) {
93
+      const identifier = this.identifier();
94
+      if (identifier instanceof ParserError) return identifier;
95
+      const value = this.value();
96
+      if (value instanceof ParserError) return value;
97
+      bindings.push(new AST.Binding(identifier, value));
98
+    }
99
+
100
+    const children: AST.Node[] = [];
101
+
102
+    while (this.currentToken().type === TokenTypes.LPAREN) {
103
+      const body = this.expr();
104
+      if (body instanceof ParserError) return body;
105
+      children.push(body);
106
+    }
107
+
108
+    const rParen = this.eat(TokenTypes.RPAREN);
109
+    if (rParen instanceof ParserError) return rParen;
110
+
111
+    return new AST.Let(bindings, children);
112
+  }
113
+
114
+  private mediaQuery(): AST.MediaQuery | ParserError {
115
+    const lParen = this.eat(TokenTypes.LPAREN);
116
+    if (lParen instanceof ParserError) return lParen;
117
+
118
+    const mediaToken = this.eat(TokenTypes.FUNCTION_NAME);
119
+    if (mediaToken instanceof ParserError) return mediaToken;
120
+
121
+    const propertyToken = this.eat(TokenTypes.PROPERTY);
122
+    if (propertyToken instanceof ParserError) return propertyToken;
123
+    const property = new AST.Property(propertyToken);
124
+    const value = this.value();
125
+    if (value instanceof ParserError) return value;
126
+
127
+    const predicate = new AST.MediaQueryPredicate(property, value);
128
+
129
+    const children: AST.Node[] = [];
130
+
131
+    while (this.currentToken().type === TokenTypes.LPAREN) {
132
+      const body = this.expr();
133
+      if (body instanceof ParserError) return body;
134
+      children.push(body);
135
+    }
136
+
137
+    const rParen = this.eat(TokenTypes.RPAREN);
138
+    if (rParen instanceof ParserError) return rParen;
139
+
140
+    return new AST.MediaQuery(predicate, children);
141
+  }
142
+
143
+  private keyframes(): AST.Keyframes | ParserError {
144
+    const lParen = this.eat(TokenTypes.LPAREN);
145
+    if (lParen instanceof ParserError) return lParen;
146
+
147
+    const keyframesToken = this.eat(TokenTypes.FUNCTION_NAME);
148
+    if (keyframesToken instanceof ParserError) return keyframesToken;
149
+
150
+    const name = this.literal();
151
+    if (name instanceof ParserError) return name;
152
+
153
+    const children: AST.Node[] = [];
154
+
155
+    while (this.currentToken().type === TokenTypes.LPAREN) {
156
+      const body = this.expr();
157
+      if (body instanceof ParserError) return body;
158
+      children.push(body);
159
+    }
160
+
161
+    const rParen = this.eat(TokenTypes.RPAREN);
162
+    if (rParen instanceof ParserError) return rParen;
163
+
164
+    return new AST.Keyframes(name, children);
165
+  }
166
+
167
+  private mixin(): AST.Mixin | ParserError {
168
+    const lParen = this.eat(TokenTypes.LPAREN);
169
+    if (lParen instanceof ParserError) return lParen;
170
+
171
+    const mixinToken = this.eat(TokenTypes.FUNCTION_NAME);
172
+    if (mixinToken instanceof ParserError) return mixinToken;
173
+
174
+    const name = this.eat(TokenTypes.LITERAL);
175
+    if (name instanceof ParserError) return name;
176
+
177
+    const rules: AST.Rule[] = [];
178
+
179
+    const openParen = this.eat(TokenTypes.LPAREN);
180
+    if (openParen instanceof ParserError) return openParen;
181
+
182
+    const parameters: AST.Identifier[] = [];
183
+
184
+    while (this.currentToken().type === TokenTypes.IDENTIFIER) {
185
+      const param = this.identifier();
186
+      if (param instanceof ParserError) return param;
187
+      parameters.push(param);
188
+    }
189
+
190
+    const closeParen = this.eat(TokenTypes.RPAREN);
191
+    if (closeParen instanceof ParserError) return closeParen;
192
+
193
+    while (this.currentToken().type === TokenTypes.PROPERTY) {
194
+      const property = this.property();
195
+      if (property instanceof ParserError) return property;
196
+      const value = this.value();
197
+      if (value instanceof ParserError) return value;
198
+      rules.push(new AST.Rule(property, value));
199
+    }
200
+
201
+    const rParen = this.eat(TokenTypes.RPAREN);
202
+    if (rParen instanceof ParserError) return rParen;
203
+
204
+    return new AST.Mixin(name, parameters, rules);
205
+  }
206
+
207
+  private application(): AST.Application | ParserError {
208
+    const lParen = this.eat(TokenTypes.LPAREN);
209
+    if (lParen instanceof ParserError) return lParen;
210
+
211
+    const functionToken = this.eat(TokenTypes.FUNCTION_NAME);
212
+    if (functionToken instanceof ParserError) return functionToken;
213
+
214
+    const functionName = new AST.Literal(functionToken);
215
+
216
+    const args: AST.Node[] = [];
217
+    while (this.currentToken().type !== TokenTypes.RPAREN) {
218
+      const argument = this.expr();
219
+      if (argument instanceof ParserError) return argument;
220
+
221
+      args.push(argument);
222
+    }
223
+
224
+    const rParen = this.eat(TokenTypes.RPAREN);
225
+    if (rParen instanceof ParserError) return rParen;
226
+
227
+    return new AST.Application(functionName, args);
228
+  }
229
+
230
+  private ruleSet(parents: AST.Selector[] = []): AST.RuleSet | ParserError {
231
+    const lParen = this.eat(TokenTypes.LPAREN);
232
+    if (lParen instanceof ParserError) return lParen;
233
+
234
+    const selectors: AST.Selector[] = [];
235
+
236
+    const selector = this.selector(parents);
237
+    if (selector instanceof ParserError) return selector;
238
+    selectors.push(selector);
239
+
240
+    while (this.currentToken().type === TokenTypes.COMMA) {
241
+      this.eat(TokenTypes.COMMA);
242
+      const selector = this.selector(parents);
243
+      if (selector instanceof ParserError) return selector;
244
+      selectors.push(selector);
245
+    }
246
+
247
+    const rules: AST.Rule[] = [];
248
+
249
+    while (this.currentToken().type === TokenTypes.PROPERTY) {
250
+      const propertyToken = this.eat(TokenTypes.PROPERTY);
251
+      if (propertyToken instanceof ParserError) return propertyToken;
252
+
253
+      const property = new AST.Property(propertyToken);
254
+
255
+      const value = this.value();
256
+      if (value instanceof ParserError) return value;
257
+
258
+      rules.push(new AST.Rule(property, value));
259
+    }
260
+
261
+    const children: AST.RuleSet[] = [];
262
+
263
+    while (this.currentToken().type === TokenTypes.LPAREN) {
264
+      const ruleSet = this.ruleSet(selectors);
265
+      if (ruleSet instanceof ParserError) return ruleSet;
266
+      children.push(ruleSet);
267
+    }
268
+
269
+    const rParen = this.eat(TokenTypes.RPAREN);
270
+    if (rParen instanceof ParserError) return rParen;
271
+
272
+    return new AST.RuleSet(selectors, rules, children);
273
+  }
274
+
275
+  private value():
276
+    | AST.Literal
277
+    | AST.Identifier
278
+    | AST.Application
279
+    | ParserError {
280
+    const type = this.currentToken().type;
281
+    switch (type) {
282
+      case TokenTypes.LITERAL:
283
+        return this.literal();
284
+      case TokenTypes.IDENTIFIER:
285
+        return this.identifier();
286
+      case TokenTypes.LPAREN:
287
+        if (this.nextToken().type === TokenTypes.FUNCTION_NAME)
288
+          return this.application();
289
+      default:
290
+        return this.error(
291
+          `Unexpected ${type}`,
292
+          this.currentToken()
293
+        );
294
+    }
295
+  }
296
+
297
+  private literal(): AST.Literal | ParserError {
298
+    const literal = this.eat(TokenTypes.LITERAL);
299
+    if (literal instanceof ParserError) return literal;
300
+    return new AST.Literal(literal);
301
+  }
302
+
303
+  private property(): AST.Property | ParserError {
304
+    const property = this.eat(TokenTypes.PROPERTY);
305
+    if (property instanceof ParserError) return property;
306
+    return new AST.Property(property);
307
+  }
308
+
309
+  private identifier(): AST.Identifier | ParserError {
310
+    const identifier = this.eat(TokenTypes.IDENTIFIER);
311
+    if (identifier instanceof ParserError) return identifier;
312
+    return new AST.Identifier(identifier);
313
+  }
314
+
315
+  private selector(parents: AST.Selector[] = []): AST.Selector | ParserError {
316
+    const literal = this.literal();
317
+    if (literal instanceof ParserError) return literal;
318
+    return new AST.Selector(literal.value, parents);
319
+  }
320
+
321
+  private eat(type: TokenTypes): Token | ParserError {
322
+    const token = this.currentToken();
323
+    if (type === token.type) {
324
+      this.position += 1;
325
+      return token;
326
+    }
327
+    return this.error(`Unexpected ${token.type}; expected ${type}`, token);
328
+  }
329
+
330
+  private currentToken(): Token {
331
+    return this.tokens[this.position];
332
+  }
333
+
334
+  private nextToken(): Token {
335
+    return this.tokens[this.position + 1];
336
+  }
337
+
338
+  private error(message: string, token: Token) {
339
+    return new ParserError(message, token.line, token.module);
340
+  }
341
+}

+ 171
- 0
src/tests/compiler.test.ts View File

@@ -0,0 +1,171 @@
1
+import * as test from 'tape';
2
+
3
+import { AST } from '../ast';
4
+import Compiler, { CompilerOpts } from '../compiler';
5
+import Env, { EnvError } from '../env';
6
+import Lexer, { LexerError, LexerResult } from '../lexer';
7
+import Parser, { ParserError, ParserResult } from '../parser';
8
+import Token, { TokenTypes } from '../token';
9
+
10
+const defaultOpts = {
11
+  prettyPrint: false,
12
+  env: new Env(),
13
+};
14
+
15
+const compile = (source: string, opts: CompilerOpts = defaultOpts)=> {
16
+  const { prettyPrint, env } = opts;
17
+  const lexerResult: LexerResult = new Lexer(source).scan();
18
+  if (!(lexerResult instanceof LexerError)) {
19
+    const parserResult: ParserResult = new Parser(lexerResult).parse();
20
+    if (!(parserResult instanceof ParserError)) {
21
+      return new Compiler(parserResult, { prettyPrint, env }).compile();
22
+    }
23
+
24
+    return parserResult;
25
+  }
26
+
27
+  return lexerResult;
28
+};
29
+
30
+test('compiles literal', (t) => {
31
+  t.plan(1);
32
+  const result = compile('#FF0000');
33
+  t.equal(result, '#FF0000');
34
+});
35
+
36
+test('compiles expression', (t) => {
37
+  t.plan(1);
38
+  const result = compile('(div :color blue)');
39
+  t.equal(result, 'div{color:blue;}');
40
+});
41
+
42
+test('compiles expression with pretty printing', (t) => {
43
+  t.plan(1);
44
+  const result = compile('(div :color blue)', { prettyPrint: true });
45
+  t.equal(result, 'div {\n  color: blue;\n}');
46
+});
47
+
48
+test('compiles nested expression', (t) => {
49
+  t.plan(1);
50
+  const result = compile('(div :color blue (span :color red))');
51
+  t.equal(result, 'div{color:blue;}div span{color:red;}');
52
+});
53
+
54
+test('omits empty sets', (t) => {
55
+  t.plan(1);
56
+  const result = compile('(div (span :color blue))');
57
+  t.equal(result, 'div span{color:blue;}');
58
+});
59
+
60
+test('pretty printing separates multiple selectors', (t) => {
61
+  t.plan(1);
62
+  const result = compile('(div, p :color blue)', { prettyPrint: true });
63
+  t.equal(result, 'div, p {\n  color: blue;\n}');
64
+});
65
+
66
+test('compile identifier', (t) => {
67
+  t.plan(1);
68
+  const result = compile('(@let $red #FF0000 (div :color $red))');
69
+  t.equal(result, 'div{color:#FF0000;}');
70
+});
71
+
72
+test('undefined variable throws an error', (t) => {
73
+  t.plan(1);
74
+  const result = compile('(@let $red #FF0000)(div :color $blue)');
75
+  t.true(result instanceof EnvError);
76
+});
77
+
78
+test('pretty printing does not return an extra newline at the end', (t) => {
79
+  t.plan(1);
80
+  const result = compile('(div :color blue)', { prettyPrint: true });
81
+  t.equal(result, 'div {\n  color: blue;\n}');
82
+});
83
+
84
+test('compiles media query', (t) => {
85
+  t.plan(1);
86
+  const result = compile(
87
+    '(@media :min-width 1000px (div :flex-direction row))'
88
+  );
89
+  t.equal(result, '@media(min-width:1000px){div{flex-direction:row;}}');
90
+});
91
+
92
+test('compiles pretty media query', (t) => {
93
+  t.plan(1);
94
+  const result = compile(
95
+    '(@media :min-width 1000px (div :flex-direction row))',
96
+    { prettyPrint: true }
97
+  );
98
+  t.equal(
99
+    result,
100
+    '@media(min-width: 1000px) {\n  div {\n    flex-direction: row;\n  }\n}'
101
+  );
102
+});
103
+
104
+test('compiles keyframe animation', (t) => {
105
+  t.plan(1);
106
+  const result = compile('(@keyframes fade (0% :opacity 1) (100% :opacity 2))');
107
+  t.equal(result, '@keyframes fade{0%{opacity:1;}100%{opacity:2;}}');
108
+});
109
+
110
+test('compiles pretty keyframe animation', (t) => {
111
+  t.plan(1);
112
+  const result = compile(
113
+    '(@keyframes fade (0% :opacity 1) (100% :opacity 2))',
114
+    { prettyPrint: true }
115
+  );
116
+  t.equal(
117
+    result,
118
+    '@keyframes fade {\n  0% {\n    opacity: 1;\n  }\n  100% {\n    opacity: 2;\n  }\n}'
119
+  );
120
+});
121
+
122
+test('does not append extra semicolons', (t) => {
123
+  t.plan(1);
124
+  const result = compile('(div :color blue :font-size 16px :display flex)');
125
+  t.equal(result, 'div{color:blue;font-size:16px;display:flex;}');
126
+});
127
+
128
+test('deeply nested rules', (t) => {
129
+  t.plan(1);
130
+  const result = compile('(body (main (div (ul (li (a :color pink))))))');
131
+  t.equal(result, 'body main div ul li a{color:pink;}');
132
+});
133
+
134
+test('compile function applications', (t) => {
135
+  t.plan(2);
136
+  t.equal(compile('(@rgba 1 2 3 4)'), 'rgba(1,2,3,4)');
137
+  t.equal(compile('(div :color (@rgba 1 2 3 4))'), 'div{color:rgba(1,2,3,4);}');
138
+});
139
+
140
+test('identifiers in function applications', (t) => {
141
+  t.plan(1);
142
+  t.equal(compile('(@let $red 30 $green 40 $blue 50 (div :color (@rgb $red $green $blue)))'), 'div{color:rgb(30,40,50);}');
143
+});
144
+
145
+test('ampersand refers to parent selector', (t) => {
146
+  t.plan(1);
147
+  t.equal(compile('(div :color green (&:hover :color blue))'), 'div{color:green;}div:hover{color:blue;}');
148
+});
149
+
150
+test('compiles mixins', (t) => {
151
+  t.plan(2);
152
+  const env = new Env();
153
+  const result = compile('(@mixin centered ($width) :max-width $width :margin auto)', {
154
+    env: env,
155
+  });
156
+  t.equal(result, '');
157
+  t.deepEqual(env.get(new Token(TokenTypes.LITERAL, 'centered', 1)), new AST.Mixin(
158
+    new Token(TokenTypes.LITERAL, 'centered', 1),
159
+    [new  AST.Identifier(new Token(TokenTypes.IDENTIFIER, 'width', 1))],
160
+    [
161
+      new AST.Rule(
162
+        new AST.Property(new Token(TokenTypes.PROPERTY, 'max-width', 1)),
163
+        new AST.Identifier(new Token(TokenTypes.IDENTIFIER, 'width', 1)),
164
+      ),
165
+      new AST.Rule(
166
+        new AST.Property(new Token(TokenTypes.PROPERTY, 'margin', 1)),
167
+        new AST.Literal(new Token(TokenTypes.LITERAL, 'auto', 1)),
168
+      )
169
+    ]
170
+  ));
171
+});

+ 188
- 0
src/tests/lexer.test.ts View File

@@ -0,0 +1,188 @@
1
+import * as test from 'tape';
2
+
3
+import Lexer, { LexerError, LexerResult } from '../lexer';
4
+import Token, { TokenTypes as TT } from '../token';
5
+
6
+const scan = (source: string): LexerResult => {
7
+  return new Lexer(source).scan();
8
+};
9
+
10
+const EOF = (line: number = 1) => new Token(TT.EOF, 'eof', line);
11
+
12
+test('scans punctuation', (t) => {
13
+  t.plan(2);
14
+  const result: LexerResult = scan('(),');
15
+  t.false(result instanceof LexerError);
16
+  if (!(result instanceof LexerError)) {
17
+    t.deepEqual(result.tokens.map((t) => t.type), [
18
+      TT.LPAREN,
19
+      TT.RPAREN,
20
+      TT.COMMA,
21
+      TT.EOF,
22
+    ]);
23
+  }
24
+});
25
+
26
+test('scans property', (t) => {
27
+  t.plan(2);
28
+  const result: LexerResult = scan(':property');
29
+  t.false(result instanceof LexerError);
30
+  if (!(result instanceof LexerError)) {
31
+    t.deepEqual(result.tokens, [new Token(TT.PROPERTY, 'property', 1), EOF()]);
32
+  }
33
+});
34
+
35
+test('scans identifier', (t) => {
36
+  t.plan(2);
37
+  const result: LexerResult = scan('$identifier');
38
+  t.false(result instanceof LexerError);
39
+  if (!(result instanceof LexerError)) {
40
+    t.deepEqual(result.tokens, [
41
+      new Token(TT.IDENTIFIER, 'identifier', 1),
42
+      EOF(),
43
+    ]);
44
+  }
45
+});
46
+
47
+test('scans function name', (t) => {
48
+  t.plan(2);
49
+  const result: LexerResult = scan('@function_name');
50
+  t.false(result instanceof LexerError);
51
+  if (!(result instanceof LexerError)) {
52
+    t.deepEqual(result.tokens, [
53
+      new Token(TT.FUNCTION_NAME, 'function_name', 1),
54
+      EOF(),
55
+    ]);
56
+  }
57
+});
58
+
59
+test('scans literal', (t) => {
60
+  t.plan(2);
61
+  const result: LexerResult = scan('literal');
62
+  t.false(result instanceof LexerError);
63
+  if (!(result instanceof LexerError)) {
64
+    t.deepEqual(result.tokens, [new Token(TT.LITERAL, 'literal', 1), EOF()]);
65
+  }
66
+});
67
+
68
+test('scans literal followed by parens', (t) => {
69
+  t.plan(2);
70
+  const result: LexerResult = scan('literal(literal)literal (');
71
+  t.false(result instanceof LexerError);
72
+  if (!(result instanceof LexerError)) {
73
+    t.deepEqual(result.tokens, [
74
+      new Token(TT.LITERAL, 'literal', 1),
75
+      new Token(TT.LPAREN, '(', 1),
76
+      new Token(TT.LITERAL, 'literal', 1),
77
+      new Token(TT.RPAREN, ')', 1),
78
+      new Token(TT.LITERAL, 'literal', 1),
79
+      new Token(TT.LPAREN, '(', 1),
80
+      EOF(),
81
+    ]);
82
+  }
83
+});
84
+
85
+test('scans literal followed by property', (t) => {
86
+  t.plan(2);
87
+  const result: LexerResult = scan('literal :property');
88
+  t.false(result instanceof LexerError);
89
+  if (!(result instanceof LexerError)) {
90
+    t.deepEqual(result.tokens, [
91
+      new Token(TT.LITERAL, 'literal', 1),
92
+      new Token(TT.PROPERTY, 'property', 1),
93
+      EOF(),
94
+    ]);
95
+  }
96
+});
97
+
98
+test('scans simple expression', (t) => {
99
+  t.plan(2);
100
+  const result: LexerResult = scan('(div :color blue)');
101
+  t.false(result instanceof LexerError);
102
+  if (!(result instanceof LexerError)) {
103
+    t.deepEqual(result.tokens, [
104
+      new Token(TT.LPAREN, '(', 1),
105
+      new Token(TT.LITERAL, 'div', 1),
106
+      new Token(TT.PROPERTY, 'color', 1),
107
+      new Token(TT.LITERAL, 'blue', 1),
108
+      new Token(TT.RPAREN, ')', 1),
109
+      EOF(),
110
+    ]);
111
+  }
112
+});
113
+
114
+test('keeps track of line numbers', (t) => {
115
+  t.plan(2);
116
+  const result: LexerResult = scan('line1\nline2');
117
+  t.false(result instanceof LexerError);
118
+  if (!(result instanceof LexerError)) {
119
+    t.deepEqual(result.tokens, [
120
+      new Token(TT.LITERAL, 'line1', 1),
121
+      new Token(TT.LITERAL, 'line2', 2),
122
+      EOF(2),
123
+    ]);
124
+  }
125
+});
126
+
127
+test('scans literal followed by identifier', (t) => {
128
+  t.plan(2);
129
+  const result: LexerResult = scan('literal $identifier');
130
+  t.false(result instanceof LexerError);
131
+  if (!(result instanceof LexerError)) {
132
+    t.deepEqual(result.tokens, [
133
+      new Token(TT.LITERAL, 'literal', 1),
134
+      new Token(TT.IDENTIFIER, 'identifier', 1),
135
+      EOF(),
136
+    ]);
137
+  }
138
+});
139
+
140
+test('allow dashes', (t) => {
141
+  t.plan(2);
142
+  const result: LexerResult = scan(
143
+    'literal-dash $identifier-dash @function-dash'
144
+  );
145
+  t.false(result instanceof LexerError);
146
+  if (!(result instanceof LexerError)) {
147
+    t.deepEqual(result.tokens, [
148
+      new Token(TT.LITERAL, 'literal-dash', 1),
149
+      new Token(TT.IDENTIFIER, 'identifier-dash', 1),
150
+      new Token(TT.FUNCTION_NAME, 'function-dash', 1),
151
+      EOF(),
152
+    ]);
153
+  }
154
+});
155
+
156
+test('disallow whitespace in function arguments', (t) => {
157
+  t.plan(2);
158
+  const result: LexerResult = scan('(@rgba 0 0 0 0)');
159
+  t.false(result instanceof LexerError);
160
+  if (!(result instanceof LexerError)) {
161
+    t.deepEqual(result.tokens, [
162
+      new Token(TT.LPAREN, '(', 1),
163
+      new Token(TT.FUNCTION_NAME, 'rgba', 1),
164
+      new Token(TT.LITERAL, '0', 1),
165
+      new Token(TT.LITERAL, '0', 1),
166
+      new Token(TT.LITERAL, '0', 1),
167
+      new Token(TT.LITERAL, '0', 1),
168
+      new Token(TT.RPAREN, ')', 1),
169
+      EOF(),
170
+    ]);
171
+  }
172
+});
173
+
174
+test('ignores comments', (t) => {
175
+  t.plan(2);
176
+  const result: LexerResult = scan('; div with blue text\n(div :color blue)');
177
+  t.false(result instanceof LexerError);
178
+  if (!(result instanceof LexerError)) {
179
+    t.deepEqual(result.tokens, [
180
+      new Token(TT.LPAREN, '(', 2),
181
+      new Token(TT.LITERAL, 'div', 2),
182
+      new Token(TT.PROPERTY, 'color', 2),
183
+      new Token(TT.LITERAL, 'blue', 2),
184
+      new Token(TT.RPAREN, ')', 2),
185
+      EOF(2),
186
+    ]);
187
+  }
188
+});

+ 289
- 0
src/tests/parser.test.ts View File

@@ -0,0 +1,289 @@
1
+import * as test from 'tape';
2
+
3
+import { AST } from '../ast';
4
+import Lexer, { LexerError, LexerResult } from '../lexer';
5
+import Parser, { ParserError, ParserResult } from '../parser';
6
+import Token, { TokenTypes } from '../token';
7
+
8
+const parse = (source: string) => {
9
+  const lexerResult: LexerResult = new Lexer(source).scan();
10
+  if (!(lexerResult instanceof LexerError)) {
11
+    const parserResult: ParserResult = new Parser(lexerResult).parse();
12
+    return parserResult;
13
+  }
14
+
15
+  return lexerResult;
16
+};
17
+
18
+const literalToken = (value: string): Token => {
19
+  return new Token(TokenTypes.LITERAL, value, 1);
20
+};
21
+
22
+const propertyToken = (value: string): Token => {
23
+  return new Token(TokenTypes.PROPERTY, value, 1);
24
+};
25
+
26
+const identifierToken = (value: string): Token => {
27
+  return new Token(TokenTypes.IDENTIFIER, value, 1);
28
+};
29
+
30
+const literalNode = (value: string): AST.Literal => {
31
+  return new AST.Literal(literalToken(value));
32
+};
33
+
34
+const identifierNode = (value: string): AST.Identifier => {
35
+  return new AST.Identifier(identifierToken(value));
36
+};
37
+
38
+const property = (value: string): AST.Property => {
39
+  const token = new Token(TokenTypes.PROPERTY, value, 1);
40
+  return new AST.Property(token);
41
+};
42
+
43
+const functionName = (value: string): AST.Literal => {
44
+  const token = new Token(TokenTypes.FUNCTION_NAME, value, 1);
45
+  return new AST.Literal(token);
46
+};
47
+
48
+test('parses a literal', (t) => {
49
+  t.plan(2);
50
+  const result = parse('#FF0000');
51
+  t.false(result instanceof ParserError);
52
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
53
+    t.deepEqual(result.tree, [literalNode('#FF0000')]);
54
+  }
55
+});
56
+
57
+test('parses a identifier', (t) => {
58
+  t.plan(2);
59
+  const result = parse('$blue');
60
+  t.false(result instanceof ParserError);
61
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
62
+    t.deepEqual(result.tree, [identifierNode('blue')]);
63
+  }
64
+});
65
+
66
+test('parses an expression', (t) => {
67
+  t.plan(2);
68
+  const result = parse('(.para :color black)');
69
+  t.false(result instanceof ParserError);
70
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
71
+    t.deepEqual(result.tree, [
72
+      new AST.RuleSet(
73
+        [new AST.Selector(literalToken('.para'))],
74
+        [new AST.Rule(property('color'), literalNode('black'))]
75
+      ),
76
+    ]);
77
+  }
78
+});
79
+
80
+test('parses an expression with multiple selectors', (t) => {
81
+  t.plan(2);
82
+  const result = parse('(.header, .footer :color black)');
83
+  t.false(result instanceof ParserError);
84
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
85
+    t.deepEqual(result.tree, [
86
+      new AST.RuleSet(
87
+        [
88
+          new AST.Selector(literalToken('.header')),
89
+          new AST.Selector(literalToken('.footer')),
90
+        ],
91
+        [new AST.Rule(property('color'), literalNode('black'))]
92
+      ),
93
+    ]);
94
+  }
95
+});
96
+
97
+test('parses an expression with children', (t) => {
98
+  t.plan(2);
99
+  const result = parse(
100
+    '(body :color black (div :color blue (span :color red)))'
101
+  );
102
+  t.false(result instanceof ParserError);
103
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
104
+    t.deepEqual(result.tree, [
105
+      new AST.RuleSet(
106
+        [new AST.Selector(literalToken('body'))],
107
+        [new AST.Rule(property('color'), literalNode('black'))],
108
+        [
109
+          new AST.RuleSet(
110
+            [
111
+              new AST.Selector(literalToken('div'), [
112
+                new AST.Selector(literalToken('body')),
113
+              ]),
114
+            ],
115
+            [new AST.Rule(property('color'), literalNode('blue'))],
116
+            [
117
+              new AST.RuleSet(
118
+                [
119
+                  new AST.Selector(literalToken('span'), [
120
+                    new AST.Selector(literalToken('div'), [
121
+                      new AST.Selector(literalToken('body')),
122
+                    ]),
123
+                  ]),
124
+                ],
125
+                [new AST.Rule(property('color'), literalNode('red'))]
126
+              ),
127
+            ]
128
+          ),
129
+        ]
130
+      ),
131
+    ]);
132
+  }
133
+});
134
+
135
+test('parses a `let` call', (t) => {
136
+  t.plan(2);
137
+  const result = parse('(@let $blue #0000FF $red #FF0000)');
138
+  t.false(result instanceof ParserError);
139
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
140
+    t.deepEqual(result.tree, [
141
+      new AST.Let(
142
+        [
143
+          new AST.Binding(identifierNode('blue'), literalNode('#0000FF')),
144
+          new AST.Binding(identifierNode('red'), literalNode('#FF0000')),
145
+        ],
146
+        []
147
+      ),
148
+    ]);
149
+  }
150
+});
151
+
152
+test('parses a `let` call with body', (t) => {
153
+  t.plan(2);
154
+  const result = parse(
155
+    '(@let $blue #0000FF $red #FF0000 (div :background $blue :color $red))'
156
+  );
157
+  t.false(result instanceof ParserError);
158
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
159
+    t.deepEqual(result.tree, [
160
+      new AST.Let(
161
+        [
162
+          new AST.Binding(identifierNode('blue'), literalNode('#0000FF')),
163
+          new AST.Binding(identifierNode('red'), literalNode('#FF0000')),
164
+        ],
165
+        [
166
+          new AST.RuleSet(
167
+            [new AST.Selector(literalToken('div'))],
168
+            [
169
+              new AST.Rule(property('background'), identifierNode('blue')),
170
+              new AST.Rule(property('color'), identifierNode('red')),
171
+            ]
172
+          ),
173
+        ]
174
+      ),
175
+    ]);
176
+  }
177
+});
178
+
179
+test('parses multiple rulesets in a let block', (t) => {
180
+  t.plan(2);
181
+  const result = parse(
182
+    '(@let $blue #0000FF $red #FF0000 (div :background $blue) (span :color $red))'
183
+  );
184
+  t.false(result instanceof ParserError);
185
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
186
+    t.deepEqual(result.tree, [
187
+      new AST.Let(
188
+        [
189
+          new AST.Binding(identifierNode('blue'), literalNode('#0000FF')),
190
+          new AST.Binding(identifierNode('red'), literalNode('#FF0000')),
191
+        ],
192
+        [
193
+          new AST.RuleSet(
194
+            [new AST.Selector(literalToken('div'))],
195
+            [new AST.Rule(property('background'), identifierNode('blue'))]
196
+          ),
197
+          new AST.RuleSet(
198
+            [new AST.Selector(literalToken('span'))],
199
+            [new AST.Rule(property('color'), identifierNode('red'))]
200
+          ),
201
+        ]
202
+      ),
203
+    ]);
204
+  }
205
+});
206
+
207
+test('parses media query', (t) => {
208
+  t.plan(2);
209
+  const result = parse('(@media :min-width 1000px (div :flex-direction row))');
210
+  t.false(result instanceof ParserError);
211
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
212
+    t.deepEqual(result.tree, [
213
+      new AST.MediaQuery(
214
+        new AST.MediaQueryPredicate(
215
+          property('min-width'),
216
+          literalNode('1000px')
217
+        ),
218
+        [
219
+          new AST.RuleSet(
220
+            [new AST.Selector(literalToken('div'))],
221
+            [new AST.Rule(property('flex-direction'), literalNode('row'))]
222
+          ),
223
+        ]
224
+      ),
225
+    ]);
226
+  }
227
+});
228
+
229
+test('parses keyframes', (t) => {
230
+  t.plan(2);
231
+  const result = parse('(@keyframes fade (0% :opacity 1) (100% :opacity 0))');
232
+  t.false(result instanceof ParserError);
233
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
234
+    t.deepEqual(result.tree, [
235
+      new AST.Keyframes(literalNode('fade'), [
236
+        new AST.RuleSet(
237
+          [new AST.Selector(literalToken('0%'))],
238
+          [new AST.Rule(property('opacity'), literalNode('1'))]
239
+        ),
240
+        new AST.RuleSet(
241
+          [new AST.Selector(literalToken('100%'))],
242
+          [new AST.Rule(property('opacity'), literalNode('0'))]
243
+        ),
244
+      ]),
245
+    ]);
246
+  }
247
+});
248
+
249
+test('parses function call', (t) => {
250
+  t.plan(2);
251
+  const result = parse('(@rgba 255 0 0 0)');
252
+  t.false(result instanceof ParserError);
253
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
254
+    t.deepEqual(result.tree, [
255
+      new AST.Application(functionName('rgba'), [
256
+        literalNode('255'),
257
+        literalNode('0'),
258
+        literalNode('0'),
259
+        literalNode('0'),
260
+      ]),
261
+    ]);
262
+  }
263
+});
264
+
265
+test('parses mixin', (t) => {
266
+  t.plan(2);
267
+  const result = parse('(@mixin centered ($width) :max-width $width :margin auto)');
268
+  t.false(result instanceof ParserError);
269
+  if (!(result instanceof LexerError) && !(result instanceof ParserError)) {
270
+    t.deepEqual(result.tree, [
271
+      new AST.Mixin(
272
+        literalToken('centered'),
273
+        [
274
+          identifierNode('width'),
275
+        ],
276
+        [
277
+          new AST.Rule(
278
+            new AST.Property(propertyToken('max-width')),
279
+            identifierNode('width'),
280
+          ),
281
+          new AST.Rule(
282
+            new AST.Property(propertyToken('margin')),
283
+            literalNode('auto')
284
+          ),
285
+        ]
286
+      ),
287
+    ]);
288
+  }
289
+});

+ 31
- 0
src/token.ts View File

@@ -0,0 +1,31 @@
1
+export enum TokenTypes {
2
+  COMMA = 'comma',
3
+  COMMENT = 'comment',
4
+  EOF = 'eof',
5
+  FUNCTION_NAME = 'function name',
6
+  IDENTIFIER = 'identifier',
7
+  LITERAL = 'literal',
8
+  LPAREN = 'lparen',
9
+  PROPERTY = 'property',
10
+  RPAREN = 'rparen',
11
+  WHITESPACE = 'whitespace',
12
+}
13
+
14
+export default class Token {
15
+  public type: TokenTypes;
16
+  public value: string;
17
+  public line: number;
18
+  public module?: string;
19
+
20
+  public constructor(
21
+    type: TokenTypes,
22
+    value: string,
23
+    line: number,
24
+    module?: string
25
+  ) {
26
+    this.type = type;
27
+    this.value = value;
28
+    this.line = line;
29
+    this.module = module;
30
+  }
31
+}

+ 23
- 0
tsconfig.json View File

@@ -0,0 +1,23 @@
1
+{
2
+  "exclude": [
3
+    "src/tests"
4
+  ],
5
+  "compilerOptions": {
6
+    "alwaysStrict": true,
7
+    "forceConsistentCasingInFileNames": true,
8
+    "module": "commonjs",
9
+    "noImplicitAny": true,
10
+    "noImplicitReturns": true,
11
+    "noImplicitThis": true,
12
+    "noUnusedLocals": true,
13
+    "noUnusedParameters": true,
14
+    "outDir": "./dist",
15
+    "rootDir": "./src",
16
+    "strict": true,
17
+    "strictFunctionTypes": true,
18
+    "strictNullChecks": true,
19
+    "strictPropertyInitialization": true,
20
+    "target": "es5",
21
+    "lib": ["es2017"]
22
+  }
23
+}

+ 6
- 0
tslint.json View File

@@ -0,0 +1,6 @@
1
+{
2
+    "defaultSeverity": "error",
3
+    "jsRules": {},
4
+    "rules": {},
5
+    "rulesDirectory": []
6
+}

Loading…
Cancel
Save