import * as test from 'tape'; import { AST } from '../ast'; import Compiler, { CompilerOpts } from '../compiler'; import Env, { EnvError } from '../env'; import Lexer, { LexerError, LexerResult } from '../lexer'; import Parser, { ParserError, ParserResult } from '../parser'; import Token, { TokenTypes } from '../token'; const defaultOpts = { prettyPrint: false, env: new Env(), }; const compile = (source: string, opts: CompilerOpts = defaultOpts)=> { const { prettyPrint, env } = opts; const lexerResult: LexerResult = new Lexer(source).scan(); if (!(lexerResult instanceof LexerError)) { const parserResult: ParserResult = new Parser(lexerResult).parse(); if (!(parserResult instanceof ParserError)) { return new Compiler(parserResult, { prettyPrint, env }).compile(); } return parserResult; } return lexerResult; }; test('compiles literal', (t) => { t.plan(1); const result = compile('#FF0000'); t.equal(result, '#FF0000'); }); test('compiles expression', (t) => { t.plan(1); const result = compile('(div :color blue)'); t.equal(result, 'div{color:blue;}'); }); test('compiles expression with pretty printing', (t) => { t.plan(1); const result = compile('(div :color blue)', { prettyPrint: true }); t.equal(result, 'div {\n color: blue;\n}'); }); test('compiles nested expression', (t) => { t.plan(1); const result = compile('(div :color blue (span :color red))'); t.equal(result, 'div{color:blue;}div span{color:red;}'); }); test('omits empty sets', (t) => { t.plan(1); const result = compile('(div (span :color blue))'); t.equal(result, 'div span{color:blue;}'); }); test('pretty printing separates multiple selectors', (t) => { t.plan(1); const result = compile('(div, p :color blue)', { prettyPrint: true }); t.equal(result, 'div, p {\n color: blue;\n}'); }); test('compile identifier', (t) => { t.plan(1); const result = compile('(@let $red #FF0000 (div :color $red))'); t.equal(result, 'div{color:#FF0000;}'); }); test('undefined variable throws an error', (t) => { t.plan(1); const result = compile('(@let $red #FF0000)(div :color $blue)'); t.true(result instanceof EnvError); }); test('pretty printing does not return an extra newline at the end', (t) => { t.plan(1); const result = compile('(div :color blue)', { prettyPrint: true }); t.equal(result, 'div {\n color: blue;\n}'); }); test('compiles media query', (t) => { t.plan(1); const result = compile( '(@media :min-width 1000px (div :flex-direction row))' ); t.equal(result, '@media(min-width:1000px){div{flex-direction:row;}}'); }); test('compiles pretty media query', (t) => { t.plan(1); const result = compile( '(@media :min-width 1000px (div :flex-direction row))', { prettyPrint: true } ); t.equal( result, '@media(min-width: 1000px) {\n div {\n flex-direction: row;\n }\n}' ); }); test('compiles keyframe animation', (t) => { t.plan(1); const result = compile('(@keyframes fade (0% :opacity 1) (100% :opacity 2))'); t.equal(result, '@keyframes fade{0%{opacity:1;}100%{opacity:2;}}'); }); test('compiles pretty keyframe animation', (t) => { t.plan(1); const result = compile( '(@keyframes fade (0% :opacity 1) (100% :opacity 2))', { prettyPrint: true } ); t.equal( result, '@keyframes fade {\n 0% {\n opacity: 1;\n }\n 100% {\n opacity: 2;\n }\n}' ); }); test('does not append extra semicolons', (t) => { t.plan(1); const result = compile('(div :color blue :font-size 16px :display flex)'); t.equal(result, 'div{color:blue;font-size:16px;display:flex;}'); }); test('deeply nested rules', (t) => { t.plan(1); const result = compile('(body (main (div (ul (li (a :color pink))))))'); t.equal(result, 'body main div ul li a{color:pink;}'); }); test('compile function applications', (t) => { t.plan(2); t.equal(compile('(@rgba 1 2 3 4)'), 'rgba(1,2,3,4)'); t.equal(compile('(div :color (@rgba 1 2 3 4))'), 'div{color:rgba(1,2,3,4);}'); }); test('identifiers in function applications', (t) => { t.plan(1); t.equal(compile('(@let $red 30 $green 40 $blue 50 (div :color (@rgb $red $green $blue)))'), 'div{color:rgb(30,40,50);}'); }); test('ampersand refers to parent selector', (t) => { t.plan(1); t.equal(compile('(div :color green (&:hover :color blue))'), 'div{color:green;}div:hover{color:blue;}'); }); test('compiles mixins', (t) => { t.plan(2); const env = new Env(); const result = compile('(@mixin centered ($width) :max-width $width :margin auto)', { env: env, }); t.equal(result, ''); t.deepEqual(env.get(new Token(TokenTypes.LITERAL, 'centered', 1)), new AST.Mixin( new Token(TokenTypes.LITERAL, 'centered', 1), [new AST.Identifier(new Token(TokenTypes.IDENTIFIER, 'width', 1))], [ new AST.Rule( new AST.Property(new Token(TokenTypes.PROPERTY, 'max-width', 1)), new AST.Identifier(new Token(TokenTypes.IDENTIFIER, 'width', 1)), ), new AST.Rule( new AST.Property(new Token(TokenTypes.PROPERTY, 'margin', 1)), new AST.Literal(new Token(TokenTypes.LITERAL, 'auto', 1)), ) ] )); });