UNPKG

@chasemoskal/magical

Version:

web toolkit for lit apps

406 lines (386 loc) 10.8 kB
import {Suite, expect} from "cynic" import {Rules} from "./types.js" import {camelCss, css} from "./camel-css.js" import {compile} from "./compilation/compile.js" import {Token} from "./parsing/ordinary/types.js" import {parse} from "./parsing/ordinary/parse.js" import {tokenize} from "./parsing/ordinary/tokenize.js" /* SEE MDN CSS REFERENCE https://developer.mozilla.org/en-US/docs/Web/CSS/Syntax TODO features - slash-star comments (that remain in the css output) - child selectors with commas: h1 {h2,h3 {}} -- compiles to `h1 :is(h2, h3)` - strip off trailing commas at end of selectors (camel allows trailing commas, css does not) - "^" caret parent reference feature (^:hover) - fully featured - animations and keyframes and stuff like that - import statements - media queries - injection safety */ export default <Suite>{ "ordinary syntax": { "tokenize": { async "returns the correct number of tokens"() { const tokens = [...tokenize(`header { h1 { color: red; } }`)] expect(tokens.length).equals(6) }, async "returns the correct tokens"() { const correctTokenTypes = [ Token.Type.Open, Token.Type.Open, Token.Type.RuleName, Token.Type.RuleValue, Token.Type.Close, Token.Type.Close, ] const tokens = [...tokenize(`header { h1 { color: red; } }`)] expect(tokens.length).equals(correctTokenTypes.length) const correct = correctTokenTypes .every((type, index) => tokens[index].type === type) expect(correct).ok() }, async "returns good tokens for complex source"() { const tokens = [...tokenize(` header { background: yellow; h1 { color: red; } } `)] const correctTokenTypes = [ Token.Type.Open, Token.Type.RuleName, Token.Type.RuleValue, Token.Type.Open, Token.Type.RuleName, Token.Type.RuleValue, Token.Type.Close, Token.Type.Close, ] expect(tokens.length).equals(correctTokenTypes.length) const correct = correctTokenTypes .every((type, index) => tokens[index].type === type) expect(correct).ok() }, }, "parse": { async "flat source code into expressions"() { const tokens = tokenize(` h1 { color: red; text-align: center; } h2 { font-style: italic; } `) const expressions = [...parse(tokens)] expect(expressions.length).equals(2) }, async "nested source code into nested expressions"() { const tokens = tokenize(`header { h1 { color: red; } }`) const expressions = [...parse(tokens)] expect(expressions.length).equals(1) { const [expression1] = expressions const [selector, rules, children] = expression1 expect(selector).equals("header") expect(Object.keys(rules as Rules).length).equals(0) expect(children!.length).equals(1) { const [child] = children! const [selector, rules, children2] = child expect(selector).equals("h1") expect(Object.keys(rules as Rules).length).equals(1) expect(children2!.length).equals(0) } } }, }, "compile": { async "nested source code emits proper css"() { const tokens = tokenize(`header { h1 { color: red; } }`) const expressions = parse(tokens) const css = [...compile(expressions)].join("") const expectedResult = `header h1 { color: red; }` expect(strip(css)).equals(strip(expectedResult)) }, async "parent expression can contain rules and children"() { const tokens = tokenize(` header { background: yellow; h1 { color: red; } } `) const expressions = parse(tokens) const cssBlocks = compile(expressions) const css = [...cssBlocks].join("") const expectedResult = ` header { background: yellow; } header h1 { color: red; } ` expect(strip(css)).equals(strip(expectedResult)) }, async "parent reference (^) is properly replaced"() { const tokens = tokenize(` header { background: yellow; ^:hover { color: red; } } `) const expressions = parse(tokens) const cssBlocks = compile(expressions) const css = [...cssBlocks].join("") const expectedResult = ` header { background: yellow; } header:hover { color: red; } ` expect(strip(css)).equals(strip(expectedResult)) }, async "parent reference (&) is properly replaced"() { const tokens = tokenize(` header { background: yellow; &:hover { color: red; } } `) const expressions = parse(tokens) const cssBlocks = compile(expressions) const css = [...cssBlocks].join("") const expectedResult = ` header { background: yellow; } header:hover { color: red; } ` expect(strip(css)).equals(strip(expectedResult)) }, async "nesting works in various circumstances"() { const tokens = tokenize(` header { background: yellow; &:hover { color: red; } & div { color: green; } span { color: blue; } } `) const expressions = parse(tokens) const cssBlocks = compile(expressions) const css = [...cssBlocks].join("") const expectedResult = ` header { background: yellow; } header:hover { color: red; } header div { color: green; } header span { color: blue; } ` expect(strip(css)).equals(strip(expectedResult)) }, async "media queries work"() { const tokens = tokenize(` @media screen (max-width: 800px) { header { background: yellow; } } `) const expressions = parse(tokens) const cssBlocks = compile(expressions) const css = [...cssBlocks].join("") const expectedResult = ` @media screen (max-width: 800px) { header { background: yellow; } } ` expect(strip(css)).equals(strip(expectedResult)) }, async "nesting inside media query"() { const tokens = tokenize(` @media screen (max-width: 800px) { header { background: yellow; &:hover { color: red; } } } `) const expressions = parse(tokens) const cssBlocks = compile(expressions) const css = [...cssBlocks].join("") const expectedResult = ` @media screen (max-width: 800px) { header { background: yellow; } header:hover { color: red; } } ` expect(strip(css)).equals(strip(expectedResult)) }, // async "slash-slash comments are stripped away from output"() { // const result = strip(camelCss(` // // my comment // h1 { // lol1 // // another comment // color: red; // // yet another comment! // background: linear-gradient( // to bottom, // magenta, // lol // // rofl // pink, // ); // } // rofl2 // h2 // hello // { color: blue; } // `)) // expect(result).equals(strip(` // h1 { // color: red; // background: linear-gradient( // to bottom, // magenta, // pink, // ); // } // h2 { color: blue; } // `)) // }, // async "slash-star comments remain in output"() { // expect(camelCss(strip(` // /* here is my comment that stays in the output */ // h1 { // /* // these comments are multiline // */ // color: red; // background: linear-gradient( // /* lol */to /*hahah*/bottom, // magenta, // pink, // )/*rofl*/; // } // /*h2,*/ // h3 { color: blue; } // h4, // /*h5,*/ // h6 { color: skyblue; } // h7 // /*h8*/ { color: lime; } // `))).equals(strip(` // h1 { // color: red; // background: linear-gradient( // to bottom, // magenta, // pink, // ); // } // h3 { color: blue; } // h4, // h6 { color: skyblue; } // h7 { color: lime; } // `)) // }, // async "fully-featured snippet"() { // expect(strip(camelCss(` // @charset "utf-8"; // @import url("narrow.css") supports(display: flex) screen and (max-width: 400px); // @font-face { // font-family: "Open Sans"; // src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"), // url("/fonts/OpenSans-Regular-webfont.woff") format("woff"); // } // @media screen and (min-width: 900px) { // article { // padding: 1rem 3rem; // } // } // @supports (display: flex) { // @media screen and (min-width: 900px) { // article { // display: flex; // } // } // } // @keyframes slidein { // from { transform: translateX(0%); } // to { transform: translateX(100%); } // } // // this comment disappears // /* this comment remains it the output */ // h1 { // background: black; // em { color: yellow; } // } // `))).equals(strip(` // @charset "utf-8"; // @import url("narrow.css") supports(display: flex) screen and (max-width: 400px); // @font-face { // font-family: "Open Sans"; // src: url("/fonts/OpenSans-Regular-webfont.woff2") format("woff2"), // url("/fonts/OpenSans-Regular-webfont.woff") format("woff"); // } // @media screen and (min-width: 900px) { // article { // padding: 1rem 3rem; // } // } // @supports (display: flex) { // @media screen and (min-width: 900px) { // article { // display: flex; // } // } // } // @keyframes slidein { // from { transform: translateX(0%); } // to { transform: translateX(100%); } // } // /* this comment remains it the output */ // h1 { background: black; } // h1 em { color: yellow; } // `)) // }, // async "media query nesting"() { // expect(strip(camelCss(` // @media (min-width: 900px) { // article { // padding: 1rem 3rem; // h1 { // color: red; // } // } // } // header { // @media (max-width: 500px) { // h2 { // color: cyan; // em { color: green; } // } // } // } // `))).equals(strip(` // @media (min-width: 900px) { // article { padding: 1rem 3rem; } // article h1 { color: red; } // } // @media (max-width: 500px) { // header h2 { color: cyan; } // header h2 em { color: green; } // } // `)) // }, }, "errors": { async "error should be thrown on missing close token"() { expect(() => camelCss(`h1 { color: red;`)).throws() }, }, "bugs": { async "fixed: missing semicolon gives blank output"() { expect(strip(css`h1 { color: red }`)) .equals(strip(`h1 { color: red; }`)) }, }, }, } function strip(text: string) { return text.trim().replaceAll(/\s+/mg, " ") }