@chasemoskal/magical
Version:
web toolkit for lit apps
406 lines (386 loc) • 10.8 kB
text/typescript
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, " ")
}