import * as test from 'tape'; import { AST } from '../ast'; import Lexer, { LexerError, LexerResult } from '../lexer'; import Parser, { ParserError, ParserResult } from '../parser'; import Token, { TokenTypes } from '../token'; const parse = (source: string) => { const lexerResult: LexerResult = new Lexer(source).scan(); if (!(lexerResult instanceof LexerError)) { const parserResult: ParserResult = new Parser(lexerResult).parse(); return parserResult; } return lexerResult; }; const literalToken = (value: string): Token => { return new Token(TokenTypes.LITERAL, value, 1); }; const propertyToken = (value: string): Token => { return new Token(TokenTypes.PROPERTY, value, 1); }; const identifierToken = (value: string): Token => { return new Token(TokenTypes.IDENTIFIER, value, 1); }; const literalNode = (value: string): AST.Literal => { return new AST.Literal(literalToken(value)); }; const identifierNode = (value: string): AST.Identifier => { return new AST.Identifier(identifierToken(value)); }; const property = (value: string): AST.Property => { const token = new Token(TokenTypes.PROPERTY, value, 1); return new AST.Property(token); }; const functionName = (value: string): AST.Literal => { const token = new Token(TokenTypes.FUNCTION_NAME, value, 1); return new AST.Literal(token); }; test('parses a literal', (t) => { t.plan(2); const result = parse('#FF0000'); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [literalNode('#FF0000')]); } }); test('parses a identifier', (t) => { t.plan(2); const result = parse('$blue'); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [identifierNode('blue')]); } }); test('parses an expression', (t) => { t.plan(2); const result = parse('(.para :color black)'); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [ new AST.RuleSet( [new AST.Selector(literalToken('.para'))], [new AST.Rule(property('color'), literalNode('black'))] ), ]); } }); test('parses an expression with multiple selectors', (t) => { t.plan(2); const result = parse('(.header, .footer :color black)'); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [ new AST.RuleSet( [ new AST.Selector(literalToken('.header')), new AST.Selector(literalToken('.footer')), ], [new AST.Rule(property('color'), literalNode('black'))] ), ]); } }); test('parses an expression with children', (t) => { t.plan(2); const result = parse( '(body :color black (div :color blue (span :color red)))' ); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [ new AST.RuleSet( [new AST.Selector(literalToken('body'))], [ new AST.Rule(property('color'), literalNode('black')), new AST.RuleSet( [ new AST.Selector(literalToken('div'), [ new AST.Selector(literalToken('body')), ]), ], [ new AST.Rule(property('color'), literalNode('blue')), new AST.RuleSet( [ new AST.Selector(literalToken('span'), [ new AST.Selector(literalToken('div'), [ new AST.Selector(literalToken('body')), ]), ]), ], [new AST.Rule(property('color'), literalNode('red'))] ), ] ), ] ), ]); } }); test('parses a `let` call', (t) => { t.plan(2); const result = parse('(@let $blue #0000FF $red #FF0000)'); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [ new AST.Let( [ new AST.Binding(identifierNode('blue'), literalNode('#0000FF')), new AST.Binding(identifierNode('red'), literalNode('#FF0000')), ], [] ), ]); } }); test('parses a `let` call with body', (t) => { t.plan(2); const result = parse( '(@let $blue #0000FF $red #FF0000 (div :background $blue :color $red))' ); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [ new AST.Let( [ new AST.Binding(identifierNode('blue'), literalNode('#0000FF')), new AST.Binding(identifierNode('red'), literalNode('#FF0000')), ], [ new AST.RuleSet( [new AST.Selector(literalToken('div'))], [ new AST.Rule(property('background'), identifierNode('blue')), new AST.Rule(property('color'), identifierNode('red')), ] ), ] ), ]); } }); test('parses multiple rulesets in a let block', (t) => { t.plan(2); const result = parse( '(@let $blue #0000FF $red #FF0000 (div :background $blue) (span :color $red))' ); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [ new AST.Let( [ new AST.Binding(identifierNode('blue'), literalNode('#0000FF')), new AST.Binding(identifierNode('red'), literalNode('#FF0000')), ], [ new AST.RuleSet( [new AST.Selector(literalToken('div'))], [new AST.Rule(property('background'), identifierNode('blue'))] ), new AST.RuleSet( [new AST.Selector(literalToken('span'))], [new AST.Rule(property('color'), identifierNode('red'))] ), ] ), ]); } }); test('parses media query', (t) => { t.plan(2); const result = parse('(@media :min-width 1000px (div :flex-direction row))'); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [ new AST.MediaQuery( new AST.MediaQueryPredicate( property('min-width'), literalNode('1000px') ), [ new AST.RuleSet( [new AST.Selector(literalToken('div'))], [new AST.Rule(property('flex-direction'), literalNode('row'))] ), ] ), ]); } }); test('parses keyframes', (t) => { t.plan(2); const result = parse('(@keyframes fade (0% :opacity 1) (100% :opacity 0))'); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [ new AST.Keyframes(literalNode('fade'), [ new AST.RuleSet( [new AST.Selector(literalToken('0%'))], [new AST.Rule(property('opacity'), literalNode('1'))] ), new AST.RuleSet( [new AST.Selector(literalToken('100%'))], [new AST.Rule(property('opacity'), literalNode('0'))] ), ]), ]); } }); test('parses function call', (t) => { t.plan(2); const result = parse('(@rgba 255 0 0 0)'); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [ new AST.Application(functionName('rgba'), [ literalNode('255'), literalNode('0'), literalNode('0'), literalNode('0'), ]), ]); } }); test('parses mixin', (t) => { t.plan(2); const result = parse( '(@mixin centered ($width) :max-width $width :margin auto)' ); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual(result.tree, [ new AST.Mixin( literalToken('centered'), [identifierNode('width')], [ new AST.Rule( new AST.Property(propertyToken('max-width')), identifierNode('width') ), new AST.Rule( new AST.Property(propertyToken('margin')), literalNode('auto') ), ] ), ]); } }); test('allow rules and children in any order', (t) => { t.plan(2); const result = parse('(div (span :color blue) :color green)'); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual( result.tree[0], new AST.RuleSet( [new AST.Selector(literalToken('div'))], [ new AST.RuleSet( [ new AST.Selector(literalToken('span'), [ new AST.Selector(literalToken('div')), ]), ], [new AST.Rule(property('color'), literalNode('blue'))] ), new AST.Rule(property('color'), literalNode('green')), ] ) ); } }); test('combine rules and children', (t) => { t.plan(2); const result = parse('(div :color green (span :color blue))'); t.false(result instanceof ParserError); if (!(result instanceof LexerError) && !(result instanceof ParserError)) { t.deepEqual( result.tree[0], new AST.RuleSet( [new AST.Selector(literalToken('div'))], [ new AST.Rule(property('color'), literalNode('green')), new AST.RuleSet( [ new AST.Selector(literalToken('span'), [ new AST.Selector(literalToken('div')), ]), ], [new AST.Rule(property('color'), literalNode('blue'))] ), ] ) ); } });