summaryrefslogtreecommitdiff
path: root/stylis.js
diff options
context:
space:
mode:
Diffstat (limited to 'stylis.js')
-rw-r--r--stylis.js/.github/workflows/main.yaml24
-rw-r--r--stylis.js/.gitignore18
-rw-r--r--stylis.js/LICENSE21
-rw-r--r--stylis.js/README.md138
-rw-r--r--stylis.js/docs/CNAME1
-rw-r--r--stylis.js/docs/assets/logo.svg1
-rw-r--r--stylis.js/docs/assets/stylesheet.css22
-rw-r--r--stylis.js/docs/index.html90
-rw-r--r--stylis.js/index.js7
-rw-r--r--stylis.js/package.json161
-rw-r--r--stylis.js/script/build.js34
-rw-r--r--stylis.js/script/setup.js10
-rw-r--r--stylis.js/src/Enum.js20
-rw-r--r--stylis.js/src/Middleware.js107
-rw-r--r--stylis.js/src/Parser.js174
-rw-r--r--stylis.js/src/Prefixer.js114
-rw-r--r--stylis.js/src/Serializer.js34
-rw-r--r--stylis.js/src/Tokenizer.js218
-rw-r--r--stylis.js/src/Utility.js109
-rw-r--r--stylis.js/test/Middleware.js82
-rw-r--r--stylis.js/test/Parser.js930
-rw-r--r--stylis.js/test/Prefixer.js178
-rw-r--r--stylis.js/test/Tokenizer.js9
23 files changed, 2502 insertions, 0 deletions
diff --git a/stylis.js/.github/workflows/main.yaml b/stylis.js/.github/workflows/main.yaml
new file mode 100644
index 0000000..5960524
--- /dev/null
+++ b/stylis.js/.github/workflows/main.yaml
@@ -0,0 +1,24 @@
+name: main
+on: [push, pull_request]
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version: [11.4.0]
+ steps:
+ - name: checkout
+ uses: actions/checkout@v1
+ - name: setup ${{ matrix.node-version }}
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ matrix.node-version }}
+ - name: test
+ run: |
+ npm install
+ npm test
+ - name: report
+ uses: coverallsapp/github-action@v1.0.1
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ path-to-lcov: ./coverage/lcov.info
diff --git a/stylis.js/.gitignore b/stylis.js/.gitignore
new file mode 100644
index 0000000..315b335
--- /dev/null
+++ b/stylis.js/.gitignore
@@ -0,0 +1,18 @@
+.DS_Store
+Thumbs.db
+Desktop.ini
+node_modules
+npm-debug.log
+*~
+*.pyc
+*.sublime-project
+*.sublime-workspace
+.idea
+*.iml
+.vscode
+*.swp
+*.swo
+coverage
+.nyc_output
+dist/
+package-lock.json
diff --git a/stylis.js/LICENSE b/stylis.js/LICENSE
new file mode 100644
index 0000000..f0b1a9a
--- /dev/null
+++ b/stylis.js/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016-present Sultan Tarimo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/stylis.js/README.md b/stylis.js/README.md
new file mode 100644
index 0000000..f3d48fa
--- /dev/null
+++ b/stylis.js/README.md
@@ -0,0 +1,138 @@
+# STYLIS
+
+[![stylis](https://stylis.js.org/assets/logo.svg)](https://github.com/thysultan/stylis.js)
+
+A Light–weight CSS Preprocessor.
+
+[![Coverage](https://coveralls.io/repos/github/thysultan/stylis.js/badge.svg?branch=master)](https://coveralls.io/github/thysultan/stylis.js)
+[![Size](https://badgen.net/bundlephobia/minzip/stylis)](https://bundlephobia.com/result?p=stylis)
+[![Licence](https://badgen.net/badge/license/MIT/blue)](https://github.com/thysultan/stylis.js/blob/master/LICENSE)
+[![NPM](https://badgen.net/npm/v/dyo)](https://www.npmjs.com/package/stylis)
+
+## Installation
+
+* Use a Direct Download: `<script src=stylis.js></script>`
+* Use a CDN: `<script src=unpkg.com/stylis></script>`
+* Use NPM: `npm install stylis --save`
+
+## Features
+
+- nesting `a { &:hover {} }`
+- selector namespacing
+- vendor prefixing (flex-box, etc...)
+- minification
+- esm module compatible
+- tree-shaking-able
+
+## Abstract Syntax Structure
+
+```js
+const declaration = {
+ value: 'color:red;',
+ type: 'decl',
+ props: 'color',
+ children: 'red',
+ line: 1, column: 1
+}
+
+const comment = {
+ value: '/*@noflip*/',
+ type: 'comm',
+ props: '/',
+ children: '@noflip',
+ line: 1, column: 1
+}
+
+const ruleset = {
+ value: 'h1,h2',
+ type: 'rule',
+ props: ['h1', 'h2'],
+ children: [/* ... */],
+ line: 1, column: 1
+}
+
+const atruleset = {
+ value: '@media (max-width:100), (min-width:100)',
+ type: '@media',
+ props: ['(max-width:100)', '(min-width:100)'],
+ children: [/* ... */],
+ line: 1, column: 1
+}
+```
+
+## Example:
+
+```js
+import {compile, serialize, stringify} from 'stylis'
+
+serialize(compile(`h1{all:unset}`), stringify)
+```
+
+### Compile
+
+```js
+compile('h1{all:unset}') === [{value: 'h1', type: 'rule', props: ['h1'], children: [/* ... */]}]
+compile('--foo:unset;') === [{value: '--foo:unset;', type: 'decl', props: '--foo', children: 'unset'}]
+```
+
+### Tokenize
+
+```js
+tokenize('h1 h2 h3 [h4 h5] fn(args) "a b c"') === ['h1', 'h2', 'h3', '[h4 h5]', 'fn', '(args)', '"a b c"']
+```
+
+### Serialize
+
+```js
+serialize(compile('h1{all:unset}'), stringify)
+```
+
+## Middleware
+
+The middleware helper is a convenient helper utility, that for all intents and purposes you can do without if you intend to implement your own traversal logic. The `stringify` middleware is one such middleware that can be used in conjunction with it.
+
+Elements passed to middlewares have a `root` property that is the immediate root/parent of the current element **in the compiled output**, so it references the parent in the already expanded CSS-like structure. Elements have also `parent` property that is the immediate parent of the current element **from the input structure** (structure representing the input string).
+
+### Traversal
+
+```js
+serialize(compile('h1{all:unset}'), middleware([(element, index, children) => {
+ assert(children === element.root.children && children[index] === element.children)
+}, stringify])) === 'h1{all:unset;}'
+```
+
+The abstract syntax tree also includes an additional `return` property for more niche uses.
+
+### Prefixing
+
+```js
+serialize(compile('h1{all:unset}'), middleware([(element, index, children, callback) => {
+ if (element.type === 'decl' && element.props === 'all' && element.children === 'unset')
+ element.return = 'color:red;' + element.value
+}, stringify])) === 'h1{color:red;all:unset;}'
+```
+
+```js
+serialize(compile('h1{all:unset}'), middleware([(element, index, children, callback) => {
+ if (element.type === 'rule' && element.props.indexOf('h1') > -1)
+ return serialize([{...element, props: ['h2', 'h3']}], callback)
+}, stringify])) === 'h2,h3{all:unset;}h1{all:unset;}'
+```
+
+### Reading
+
+```js
+serialize(compile('h1{all:unset}'), middleware([stringify, (element, index, children) => {
+ assert(element.return === 'h1{all:unset;}')
+}])) === 'h1{all:unset;color:red;}'
+```
+
+The middlewares in [src/Middleware.js](src/Middleware.js) dive into tangible examples of how you might implement a middleware, alternatively you could also create your own middleware system as `compile` returns all the nessessary structure to fork from.
+
+## Benchmark
+
+Stylis is at-least 2X faster than its predecesor.
+
+### License
+
+Stylis is [MIT licensed](./LICENSE).
diff --git a/stylis.js/docs/CNAME b/stylis.js/docs/CNAME
new file mode 100644
index 0000000..26c12af
--- /dev/null
+++ b/stylis.js/docs/CNAME
@@ -0,0 +1 @@
+stylis.js.org
diff --git a/stylis.js/docs/assets/logo.svg b/stylis.js/docs/assets/logo.svg
new file mode 100644
index 0000000..089c03d
--- /dev/null
+++ b/stylis.js/docs/assets/logo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 194 76" version="1.1" xml:space="preserve" x="0px" y="0px" width="194px" height="76px" background-color="#ffffff00"><defs><linearGradient id="gradient1" x1="47.1649%" y1="1.3158%" x2="47.1649%" y2="81.5789%"><stop stop-color="#745dc0" stop-opacity="1" offset="0%"/><stop stop-color="#5497d8" stop-opacity="1" offset="100%"/></linearGradient></defs><path d="M142 62L173.5 62C175.7092 62 179.0539 58.9963 180 57L186.4386 43.1381C188.6816 38.0912 185.5229 34 180 34L170 34C168.8954 34 168 33.3284 168 32.5 168 31.6716 168.8954 31 170 31L187 31 194 16 165 16 154 38C151.7269 43.0334 153.4771 46.9688 159 46.9688L168 46.9688 168 47C169.1046 47 170 47.6716 170 48.5 170 49.3284 169.1046 50 168 50L148 50 142 62ZM65.5 65C62.4624 65 60 67.4624 60 70.5 60 73.5376 62.4624 76 65.5 76L91 76 96 65 65.5 65ZM85 16L70.6721 47.0177C70.2089 48.0205 70.3053 49.5943 70.8876 50.533L76.9458 60.3004C77.528 61.2391 78.8954 62 80 62L98 62 120 16 108 16 93 48 82 48 97 16 85 16ZM140 29L124 62 136 62 152 29 140 29ZM146 16L141 26 149.005 26C151.2142 26 153.8052 24.3979 154.7924 22.4216L156.2126 19.5784C157.1998 17.6021 156.2092 16 154.0001 16L146 16ZM0 62L29.5 62C31.7092 62 35.0539 58.9963 36 57L42.4386 43.1381C44.6816 38.0912 41.5229 34 36 34L26 34C24.8954 34 24 33.3284 24 32.5 24 31.6716 24.8954 31 26 31L43 31 50 16 21 16 10 38C7.7269 43.0334 9.4771 46.9688 15 46.9688L24 46.9688 24 47C25.1046 47 26 47.6716 26 48.5 26 49.3284 25.1046 50 24 50L6 50 0 62ZM134 0L105 62 118 62 147 0 134 0ZM62 0L46.635 34.775C48.3667 36.4388 48.7809 40.118 47.4386 43.1381L38 62 60 62 64 52C64.9879 50.0241 64.7092 48 62.5 48L54 48 62 31 71 31C72.1046 31 72.533 30.001 73 29L79 16 68 16 75 0 62 0Z" stroke="#ffffff00" stroke-width="1" fill="url(#gradient1)"/></svg> \ No newline at end of file
diff --git a/stylis.js/docs/assets/stylesheet.css b/stylis.js/docs/assets/stylesheet.css
new file mode 100644
index 0000000..6909849
--- /dev/null
+++ b/stylis.js/docs/assets/stylesheet.css
@@ -0,0 +1,22 @@
+* { box-sizing: border-box; margin: 0; padding: 0; }
+
+body { background: #F1F1F1; font-family: 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}
+p { color: #535353; line-height: 38px; font-size: 17px; text-align: center; }
+a { color: #332E22; font-size: 19px; text-decoration: none; }
+a[title=github] { font-weight: bold; }
+
+.container > .welcome > *:not(pre):not(div), .wrap { max-width: 600px; width: 100%; margin: 0 auto; padding:0 20px; }
+.header { padding-top: 40px; padding-bottom: 14px; min-height: 160px; text-align: center; }
+.logo { margin-bottom: 20px; display: block; }
+.logo a { display: inline-block; width: 194px; height: 76px; cursor: pointer; }
+
+.nav {display: inline-block; font-weight: bold; }
+.nav li { display: inline-block; }
+.nav li:not(:last-child) a { margin-right: 16px; }
+
+.code { position: relative; display: block; margin: 40px auto 0; }
+.code > div { display: flex; flex-direction: row; overflow: auto; tab-size: 4; height: 100%; }
+.code pre { font: 15px/1.8em menlo,monospace; flex: auto; width: 50%; padding: 26px 30px; outline: none; }
+span.selector { color: #AA0C91; }
+span.value { color: #C41913; }
+span.string { color: #007400; }
diff --git a/stylis.js/docs/index.html b/stylis.js/docs/index.html
new file mode 100644
index 0000000..a6faa1b
--- /dev/null
+++ b/stylis.js/docs/index.html
@@ -0,0 +1,90 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset=utf-8>
+ <meta name=viewport content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'>
+ <meta name=description content='light – weight css preprocessor'>
+ <title>stylis</title>
+ <link rel='stylesheet' href='assets/stylesheet.css'>
+ </head>
+ <body>
+ <div class='header'>
+ <div class='wrap'>
+ <div class='logo'><a class=./><img src='assets/logo.svg'></a></div>
+ <div class='nav'>
+ <ul>
+ <li><a href='https://github.com/thysultan/stylis.js'>Github</a></li>
+ <li><a href='https://npmjs.com/package/stylis'>NPM</a></li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ <div class='container'>
+ <div class='welcome'>
+ <p>light – weight css preprocessor</p>
+ <div class='code'>
+ <div spellcheck='false'>
+<pre contenteditable id='editor'>
+div {
+ display: flex;
+
+ @media screen {
+ color: red;
+ }
+}
+
+div {
+ transform: translateZ(0);
+
+ h1, h2 {
+ color: red;
+ }
+}
+</pre>
+<pre id='output'></pre>
+ </div>
+ </div>
+ </div>
+ </div>
+ <script type=module>
+ import {compile, serialize, middleware, prefixer, stringify} from 'https://unpkg.com/stylis?module'
+
+ function tabbed (select, ranged, content) {
+ ranged.deleteContents()
+ ranged.insertNode(content)
+ ranged.setStartAfter(content)
+ select.removeAllRanges()
+ select.addRange(ranged)
+ }
+
+ // update output preview
+ function update (value) {
+ switch (value.keyCode) {
+ // tab indent
+ case 9: tabbed(value = window.getSelection(value.preventDefault()), value.getRangeAt(0), document.createTextNode('\t'))
+ break
+ // formatting
+ default:
+ value = serialize(compile(`[namespace]{${value.target.textContent}}`), middleware([prefixer, stringify]))
+ value = value.replace(/(;|\})/g, (match, group) => group + (group === '}' ? '\n\n' : '\n'))
+ value = value.replace(/(.*?)\{/g, (match, group) => '<span class=selector>' + group.trim() + '</span> {\n')
+ value = value.replace(/:(.*);/g, (match, group) => ': <span class=value>' + group.trim() + '</span>;')
+ value = value.replace(/^[^@].*;$/gm, (match) => '\t' + match)
+ value = value.replace(/\}\n\n\}/g, '}\n}')
+ value = value.replace(/(.*@.*\{)([^}]+\})(\n\})/g, (match, group1, group2, group3) => group1 + group2.split('\n').join('\n\t') + group3)
+ value = value.replace(/['"`].*?['"`]/g, (match) => '<span class=string>'+ match.trim() +'</span>')
+
+ output.innerHTML = value
+ }
+ }
+
+ const target = document.getElementById('editor')
+ const output = document.getElementById('output')
+
+ target.addEventListener('keydown', update)
+ target.addEventListener('input', update)
+
+ update({target})
+ </script>
+ </body>
+</html>
diff --git a/stylis.js/index.js b/stylis.js/index.js
new file mode 100644
index 0000000..3c12a38
--- /dev/null
+++ b/stylis.js/index.js
@@ -0,0 +1,7 @@
+export * from './src/Enum.js'
+export * from './src/Utility.js'
+export * from './src/Parser.js'
+export * from './src/Prefixer.js'
+export * from './src/Tokenizer.js'
+export * from './src/Serializer.js'
+export * from './src/Middleware.js'
diff --git a/stylis.js/package.json b/stylis.js/package.json
new file mode 100644
index 0000000..989c4b7
--- /dev/null
+++ b/stylis.js/package.json
@@ -0,0 +1,161 @@
+{
+ "name": "stylis",
+ "version": "4.0.3",
+ "license": "MIT",
+ "description": "A Light–weight CSS Preprocessor",
+ "homepage": "https://github.com/thysultan/stylis.js",
+ "author": "Sultan Tarimo <sultantarimo@me.com>",
+ "repository": "https://github.com/thysultan/stylis.js",
+ "bugs": "https://github.com/thysultan/stylis.js/issues",
+ "sideEffects": false,
+ "type": "module",
+ "main": "dist/stylis.cjs",
+ "module": "dist/stylis.mjs",
+ "react-native": "./index.js",
+ "exports": {
+ "import": "./index.js",
+ "require": "./dist/stylis.cjs"
+ },
+ "files": [
+ "index.js",
+ "dist/",
+ "src/"
+ ],
+ "scripts": {
+ "lint": "eslint ./",
+ "pretest": "npm run lint && npm run build",
+ "test": "nyc npm run spec",
+ "spec": "mocha --harmony --require esm script/setup.js --recursive test",
+ "prebuild": "rimraf dist",
+ "build": "rollup --config script/build.js --configSrc ./",
+ "start": "npm run build -- --watch",
+ "prepare": "npm run build",
+ "postversion": "git push --follow-tags && npm publish",
+ "release-major": "npm version major -m '%s'",
+ "release-minor": "npm version minor -m '%s'",
+ "release-patch": "npm version patch -m '%s'"
+ },
+ "devDependencies": {
+ "chai": "4.2.0",
+ "eslint": "6.8.0",
+ "esm": "3.2.25",
+ "mocha": "7.0.0",
+ "nyc": "15.0.0",
+ "rimraf": "3.0.2",
+ "rollup": "1.28.0",
+ "rollup-plugin-size": "0.2.1",
+ "rollup-plugin-terser": "5.1.3",
+ "stylis": "./"
+ },
+ "nyc": {
+ "temp-dir": "./coverage/.nyc_output",
+ "exclude": [
+ "**/dist/",
+ "**/test/",
+ "**/script/"
+ ],
+ "reporter": [
+ "lcov",
+ "text"
+ ]
+ },
+ "esm": {
+ "cjs": true,
+ "cache": false
+ },
+ "eslintIgnore": [
+ "script/",
+ "test/",
+ "dist/",
+ "docs/"
+ ],
+ "eslintConfig": {
+ "env": {
+ "commonjs": true,
+ "browser": true,
+ "node": true,
+ "es6": true
+ },
+ "extends": "eslint:recommended",
+ "parserOptions": {
+ "ecmaVersion": 7,
+ "sourceType": "module",
+ "ecmaFeatures": {
+ "impliedStrict": true
+ }
+ },
+ "rules": {
+ "indent": [
+ "error",
+ "tab",
+ {
+ "SwitchCase": 1
+ }
+ ],
+ "linebreak-style": [
+ "error",
+ "unix"
+ ],
+ "quotes": [
+ "error",
+ "single"
+ ],
+ "semi": [
+ "error",
+ "never"
+ ],
+ "no-cond-assign": [
+ "off"
+ ],
+ "no-redeclare": [
+ "off"
+ ],
+ "no-fallthrough": [
+ "off"
+ ],
+ "no-console": [
+ "off"
+ ],
+ "no-unsafe-finally": [
+ "off"
+ ],
+ "no-shadow-restricted-names": [
+ "error"
+ ],
+ "no-whitespace-before-property": [
+ "error"
+ ],
+ "eol-last": [
+ "error"
+ ],
+ "func-call-spacing": [
+ "error",
+ "never"
+ ],
+ "brace-style": [
+ "error",
+ "1tbs",
+ {
+ "allowSingleLine": true
+ }
+ ],
+ "require-jsdoc": [
+ "error",
+ {
+ "require": {
+ "FunctionDeclaration": true
+ }
+ }
+ ],
+ "no-trailing-spaces": [
+ "error",
+ {
+ "skipBlankLines": true
+ }
+ ],
+ "no-constant-condition": [
+ "off"
+ ]
+ }
+ }
+}
diff --git a/stylis.js/script/build.js b/stylis.js/script/build.js
new file mode 100644
index 0000000..cfd5a7f
--- /dev/null
+++ b/stylis.js/script/build.js
@@ -0,0 +1,34 @@
+import {join} from 'path'
+import {terser} from 'rollup-plugin-terser'
+import size from 'rollup-plugin-size'
+
+const options = {mangle: true, compress: false, toplevel: true}
+const defaults = {
+ onwarn(warning, warn) {
+ switch (warning.code) {
+ case 'CIRCULAR_DEPENDENCY':
+ return
+ default:
+ warn(warning)
+ }
+ },
+ treeshake: {propertyReadSideEffects: false},
+ context: 'this'
+}
+
+export default ({configSrc = './', configInput = join(configSrc, 'index.js')}) => {
+ return [
+ {
+ ...defaults,
+ input: configInput,
+ output: [{file: join(configSrc, 'dist', 'stylis.cjs'), format: 'umd', name: 'stylis', freeze: false, sourcemap: true}],
+ plugins: [terser(options), size()]
+ },
+ {
+ ...defaults,
+ input: configInput,
+ output: [{file: join(configSrc, 'dist', 'stylis.mjs'), format: 'esm', name: 'stylis', freeze: false, sourcemap: true}],
+ plugins: [terser(options), size()]
+ }
+ ]
+}
diff --git a/stylis.js/script/setup.js b/stylis.js/script/setup.js
new file mode 100644
index 0000000..f56729c
--- /dev/null
+++ b/stylis.js/script/setup.js
@@ -0,0 +1,10 @@
+import {expect} from 'chai'
+import {describe, test} from 'mocha'
+
+const {assign} = Object
+const that = () => typeof globalThis == 'object' ? globalThis :
+ typeof global == 'object' ? global :
+ typeof window == 'object' ? window :
+ typeof self == 'object' ? self : Function('return this')()
+
+assign(that(), {expect, describe, test, globalThis: that()})
diff --git a/stylis.js/src/Enum.js b/stylis.js/src/Enum.js
new file mode 100644
index 0000000..0d611ab
--- /dev/null
+++ b/stylis.js/src/Enum.js
@@ -0,0 +1,20 @@
+export var MS = '-ms-'
+export var MOZ = '-moz-'
+export var WEBKIT = '-webkit-'
+
+export var COMMENT = 'comm'
+export var RULESET = 'rule'
+export var DECLARATION = 'decl'
+
+export var PAGE = '@page'
+export var MEDIA = '@media'
+export var IMPORT = '@import'
+export var CHARSET = '@charset'
+export var VIEWPORT = '@viewport'
+export var SUPPORTS = '@supports'
+export var DOCUMENT = '@document'
+export var NAMESPACE = '@namespace'
+export var KEYFRAMES = '@keyframes'
+export var FONT_FACE = '@font-face'
+export var COUNTER_STYLE = '@counter-style'
+export var FONT_FEATURE_VALUES = '@font-feature-values'
diff --git a/stylis.js/src/Middleware.js b/stylis.js/src/Middleware.js
new file mode 100644
index 0000000..cec5f62
--- /dev/null
+++ b/stylis.js/src/Middleware.js
@@ -0,0 +1,107 @@
+import {MS, MOZ, WEBKIT, RULESET, KEYFRAMES, DECLARATION} from './Enum.js'
+import {match, charat, substr, strlen, sizeof, replace, combine} from './Utility.js'
+import {copy, tokenize} from './Tokenizer.js'
+import {serialize} from './Serializer.js'
+import {prefix} from './Prefixer.js'
+
+/**
+ * @param {function[]} collection
+ * @return {function}
+ */
+export function middleware (collection) {
+ var length = sizeof(collection)
+
+ return function (element, index, children, callback) {
+ var output = ''
+
+ for (var i = 0; i < length; i++)
+ output += collection[i](element, index, children, callback) || ''
+
+ return output
+ }
+}
+
+/**
+ * @param {function} callback
+ * @return {function}
+ */
+export function rulesheet (callback) {
+ return function (element) {
+ if (!element.root)
+ if (element = element.return)
+ callback(element)
+ }
+}
+
+/**
+ * @param {object} element
+ * @param {number} index
+ * @param {object[]} children
+ * @param {function} callback
+ */
+export function prefixer (element, index, children, callback) {
+ if (!element.return)
+ switch (element.type) {
+ case DECLARATION: element.return = prefix(element.value, element.length)
+ break
+ case KEYFRAMES:
+ return serialize([copy(replace(element.value, '@', '@' + WEBKIT), element, '')], callback)
+ case RULESET:
+ if (element.length)
+ return combine(element.props, function (value) {
+ switch (match(value, /(::plac\w+|:read-\w+)/)) {
+ // :read-(only|write)
+ case ':read-only': case ':read-write':
+ return serialize([copy(replace(value, /:(read-\w+)/, ':' + MOZ + '$1'), element, '')], callback)
+ // :placeholder
+ case '::placeholder':
+ return serialize([
+ copy(replace(value, /:(plac\w+)/, ':' + WEBKIT + 'input-$1'), element, ''),
+ copy(replace(value, /:(plac\w+)/, ':' + MOZ + '$1'), element, ''),
+ copy(replace(value, /:(plac\w+)/, MS + 'input-$1'), element, '')
+ ], callback)
+ }
+
+ return ''
+ })
+ }
+}
+
+/**
+ * @param {object} element
+ * @param {number} index
+ * @param {object[]} children
+ */
+export function namespace (element) {
+ switch (element.type) {
+ case RULESET:
+ element.props = element.props.map(function (value) {
+ return combine(tokenize(value), function (value, index, children) {
+ switch (charat(value, 0)) {
+ // \f
+ case 12:
+ return substr(value, 1, strlen(value))
+ // \0 ( + > ~
+ case 0: case 40: case 43: case 62: case 126:
+ return value
+ // :
+ case 58:
+ if (children[index + 1] === 'global')
+ children[index + 1] = '', children[index + 2] = '\f' + substr(children[index + 2], index = 1, -1)
+ // \s
+ case 32:
+ return index === 1 ? '' : value
+ default:
+ switch (index) {
+ case 0: element = value
+ return sizeof(children) > 1 ? '' : value
+ case index = sizeof(children) - 1: case 2:
+ return index === 2 ? value + element + element : value + element
+ default:
+ return value
+ }
+ }
+ })
+ })
+ }
+}
diff --git a/stylis.js/src/Parser.js b/stylis.js/src/Parser.js
new file mode 100644
index 0000000..4089514
--- /dev/null
+++ b/stylis.js/src/Parser.js
@@ -0,0 +1,174 @@
+import {COMMENT, RULESET, DECLARATION} from './Enum.js'
+import {abs, trim, from, sizeof, strlen, substr, append, replace} from './Utility.js'
+import {node, char, next, peek, caret, alloc, dealloc, delimit, whitespace, identifier, commenter} from './Tokenizer.js'
+
+/**
+ * @param {string} value
+ * @return {object[]}
+ */
+export function compile (value) {
+ return dealloc(parse('', null, null, null, [''], value = alloc(value), 0, [0], value))
+}
+
+/**
+ * @param {string} value
+ * @param {object} root
+ * @param {object?} parent
+ * @param {string[]} rule
+ * @param {string[]} rules
+ * @param {string[]} rulesets
+ * @param {number[]} pseudo
+ * @param {number[]} points
+ * @param {string[]} declarations
+ * @return {object}
+ */
+export function parse (value, root, parent, rule, rules, rulesets, pseudo, points, declarations) {
+ var index = 0
+ var offset = 0
+ var length = pseudo
+ var atrule = 0
+ var property = 0
+ var previous = 0
+ var variable = 1
+ var scanning = 1
+ var ampersand = 1
+ var character = 0
+ var type = ''
+ var props = rules
+ var children = rulesets
+ var reference = rule
+ var characters = type
+
+ while (scanning)
+ switch (previous = character, character = next()) {
+ // " ' [ (
+ case 34: case 39: case 91: case 40:
+ characters += delimit(character)
+ break
+ // \t \n \r \s
+ case 9: case 10: case 13: case 32:
+ characters += whitespace(previous)
+ break
+ // /
+ case 47:
+ switch (peek()) {
+ case 42: case 47:
+ append(comment(commenter(next(), caret()), root, parent), declarations)
+ break
+ default:
+ characters += '/'
+ }
+ break
+ // {
+ case 123 * variable:
+ points[index++] = strlen(characters) * ampersand
+ // } ; \0
+ case 125 * variable: case 59: case 0:
+ switch (character) {
+ // \0 }
+ case 0: case 125: scanning = 0
+ // ;
+ case 59 + offset:
+ if (property > 0)
+ append(property > 32 ? declaration(characters + ';', rule, parent, length - 1) : declaration(replace(characters, ' ', '') + ';', rule, parent, length - 2), declarations)
+ break
+ // @ ;
+ case 59: characters += ';'
+ // { rule/at-rule
+ default:
+ append(reference = ruleset(characters, root, parent, index, offset, rules, points, type, props = [], children = [], length), rulesets)
+
+ if (character === 123)
+ if (offset === 0)
+ parse(characters, root, reference, reference, props, rulesets, length, points, children)
+ else
+ switch (atrule) {
+ // d m s
+ case 100: case 109: case 115:
+ parse(value, reference, reference, rule && append(ruleset(value, reference, reference, 0, 0, rules, points, type, rules, props = [], length), children), rules, children, length, points, rule ? props : children)
+ break
+ default:
+ parse(characters, reference, reference, reference, [''], children, length, points, children)
+ }
+ }
+
+ index = offset = property = 0, variable = ampersand = 1, type = characters = '', length = pseudo
+ break
+ // :
+ case 58:
+ length = 1 + strlen(characters), property = previous
+ default:
+ switch (characters += from(character), character * variable) {
+ // &
+ case 38:
+ ampersand = offset > 0 ? 1 : (characters += '\f', -1)
+ break
+ // ,
+ case 44:
+ points[index++] = (strlen(characters) - 1) * ampersand, ampersand = 1
+ break
+ // @
+ case 64:
+ // -
+ if (peek() === 45)
+ characters += delimit(next())
+
+ atrule = peek(), offset = strlen(type = characters += identifier(caret())), character++
+ break
+ // -
+ case 45:
+ if (previous === 45 && strlen(characters) == 2)
+ variable = 0
+ }
+ }
+
+ return rulesets
+}
+
+/**
+ * @param {string} value
+ * @param {object} root
+ * @param {object?} parent
+ * @param {number} index
+ * @param {number} offset
+ * @param {string[]} rules
+ * @param {number[]} points
+ * @param {string} type
+ * @param {string[]} props
+ * @param {string[]} children
+ * @param {number} length
+ * @return {object}
+ */
+export function ruleset (value, root, parent, index, offset, rules, points, type, props, children, length) {
+ var post = offset - 1
+ var rule = offset === 0 ? rules : ['']
+ var size = sizeof(rule)
+
+ for (var i = 0, j = 0, k = 0; i < index; ++i)
+ for (var x = 0, y = substr(value, post + 1, post = abs(j = points[i])), z = value; x < size; ++x)
+ if (z = trim(j > 0 ? rule[x] + ' ' + y : replace(y, /&\f/g, rule[x])))
+ props[k++] = z
+
+ return node(value, root, parent, offset === 0 ? RULESET : type, props, children, length)
+}
+
+/**
+ * @param {number} value
+ * @param {object} root
+ * @param {object?} parent
+ * @return {object}
+ */
+export function comment (value, root, parent) {
+ return node(value, root, parent, COMMENT, from(char()), substr(value, 2, -2), 0)
+}
+
+/**
+ * @param {string} value
+ * @param {object} root
+ * @param {object?} parent
+ * @param {number} length
+ * @return {object}
+ */
+export function declaration (value, root, parent, length) {
+ return node(value, root, parent, DECLARATION, substr(value, 0, length), substr(value, length + 1, -1), length)
+}
diff --git a/stylis.js/src/Prefixer.js b/stylis.js/src/Prefixer.js
new file mode 100644
index 0000000..e156c21
--- /dev/null
+++ b/stylis.js/src/Prefixer.js
@@ -0,0 +1,114 @@
+import {MS, MOZ, WEBKIT} from './Enum.js'
+import {hash, charat, strlen, indexof, replace} from './Utility.js'
+
+/**
+ * @param {string} value
+ * @param {number} length
+ * @return {string}
+ */
+export function prefix (value, length) {
+ switch (hash(value, length)) {
+ // animation, animation-(delay|direction|duration|fill-mode|iteration-count|name|play-state|timing-function)
+ case 5737: case 4201: case 3177: case 3433: case 1641: case 4457: case 2921:
+ // text-decoration, filter, clip-path, backface-visibility, column, box-decoration-break
+ case 5572: case 6356: case 5844: case 3191: case 6645: case 3005:
+ // mask, mask-image, mask-(mode|clip|size), mask-(repeat|origin), mask-position, mask-composite,
+ case 6391: case 5879: case 5623: case 6135: case 4599: case 4855:
+ // background-clip, columns, column-(count|fill|gap|rule|rule-color|rule-style|rule-width|span|width)
+ case 4215: case 6389: case 5109: case 5365: case 5621: case 3829:
+ return WEBKIT + value + value
+ // appearance, user-select, transform, hyphens, text-size-adjust
+ case 5349: case 4246: case 4810: case 6968: case 2756:
+ return WEBKIT + value + MOZ + value + MS + value + value
+ // flex, flex-direction
+ case 6828: case 4268:
+ return WEBKIT + value + MS + value + value
+ // order
+ case 6165:
+ return WEBKIT + value + MS + 'flex-' + value + value
+ // align-items
+ case 5187:
+ return WEBKIT + value + replace(value, /(\w+).+(:[^]+)/, WEBKIT + 'box-$1$2' + MS + 'flex-$1$2') + value
+ // align-self
+ case 5443:
+ return WEBKIT + value + MS + 'flex-item-' + replace(value, /flex-|-self/, '') + value
+ // align-content
+ case 4675:
+ return WEBKIT + value + MS + 'flex-line-pack' + replace(value, /align-content|flex-|-self/, '') + value
+ // flex-shrink
+ case 5548:
+ return WEBKIT + value + MS + replace(value, 'shrink', 'negative') + value
+ // flex-basis
+ case 5292:
+ return WEBKIT + value + MS + replace(value, 'basis', 'preferred-size') + value
+ // flex-grow
+ case 6060:
+ return WEBKIT + 'box-' + replace(value, '-grow', '') + WEBKIT + value + MS + replace(value, 'grow', 'positive') + value
+ // transition
+ case 4554:
+ return WEBKIT + replace(value, /([^-])(transform)/g, '$1' + WEBKIT + '$2') + value
+ // cursor
+ case 6187:
+ return replace(replace(replace(value, /(zoom-|grab)/, WEBKIT + '$1'), /(image-set)/, WEBKIT + '$1'), value, '') + value
+ // background, background-image
+ case 5495: case 3959:
+ return replace(value, /(image-set\([^]*)/, WEBKIT + '$1' + '$`$1')
+ // justify-content
+ case 4968:
+ return replace(replace(value, /(.+:)(flex-)?(.*)/, WEBKIT + 'box-pack:$3' + MS + 'flex-pack:$3'), /s.+-b[^;]+/, 'justify') + WEBKIT + value + value
+ // (margin|padding)-inline-(start|end)
+ case 4095: case 3583: case 4068: case 2532:
+ return replace(value, /(.+)-inline(.+)/, WEBKIT + '$1$2') + value
+ // (min|max)?(width|height|inline-size|block-size)
+ case 8116: case 7059: case 5753: case 5535:
+ case 5445: case 5701: case 4933: case 4677:
+ case 5533: case 5789: case 5021: case 4765:
+ // stretch, max-content, min-content, fill-available
+ if (strlen(value) - 1 - length > 6)
+ switch (charat(value, length + 1)) {
+ // (m)ax-content, (m)in-content
+ case 109:
+ return replace(value, /(.+:)(.+)-([^]+)/, '$1' + WEBKIT + '$2-$3' + '$1' + MOZ + '$2-$3') + value
+ // (f)ill-available
+ case 102:
+ return replace(value, /(.+:)(.+)-([^]+)/, '$1' + WEBKIT + '$2-$3' + '$1' + MOZ + '$3') + value
+ // (s)tretch
+ case 115:
+ return prefix(replace(value, 'stretch', 'fill-available'), length) + value
+ }
+ break
+ // position: sticky
+ case 4949:
+ // (s)ticky?
+ if (charat(value, length + 1) !== 115)
+ break
+ // display: (flex|inline-flex|inline-box)
+ case 6444:
+ switch (charat(value, strlen(value) - 3 - (~indexof(value, '!important') && 10))) {
+ // stic(k)y, inline-b(o)x
+ case 107: case 111:
+ return replace(value, value, WEBKIT + value) + value
+ // (inline-)?fl(e)x
+ case 101:
+ return replace(value, /(.+:)([^;!]+)(;|!.+)?/, '$1' + WEBKIT + (charat(value, 14) === 45 ? 'inline-' : '') + 'box$3' + '$1' + WEBKIT + '$2$3' + '$1' + MS + '$2box$3') + value
+ }
+ break
+ // writing-mode
+ case 5936:
+ switch (charat(value, length + 11)) {
+ // vertical-l(r)
+ case 114:
+ return WEBKIT + value + MS + replace(value, /[svh]\w+-[tblr]{2}/, 'tb') + value
+ // vertical-r(l)
+ case 108:
+ return WEBKIT + value + MS + replace(value, /[svh]\w+-[tblr]{2}/, 'tb-rl') + value
+ // horizontal(-)tb
+ case 45:
+ return WEBKIT + value + MS + replace(value, /[svh]\w+-[tblr]{2}/, 'lr') + value
+ }
+
+ return WEBKIT + value + MS + value + value
+ }
+
+ return value
+}
diff --git a/stylis.js/src/Serializer.js b/stylis.js/src/Serializer.js
new file mode 100644
index 0000000..c82f0b4
--- /dev/null
+++ b/stylis.js/src/Serializer.js
@@ -0,0 +1,34 @@
+import {IMPORT, COMMENT, RULESET, DECLARATION} from './Enum.js'
+import {strlen, sizeof} from './Utility.js'
+
+/**
+ * @param {object[]} children
+ * @param {function} callback
+ * @return {string}
+ */
+export function serialize (children, callback) {
+ var output = ''
+ var length = sizeof(children)
+
+ for (var i = 0; i < length; i++)
+ output += callback(children[i], i, children, callback) || ''
+
+ return output
+}
+
+/**
+ * @param {object} element
+ * @param {number} index
+ * @param {object[]} children
+ * @param {function} callback
+ * @return {string}
+ */
+export function stringify (element, index, children, callback) {
+ switch (element.type) {
+ case IMPORT: case DECLARATION: return element.return = element.return || element.value
+ case COMMENT: return ''
+ case RULESET: element.value = element.props.join(',')
+ }
+
+ return strlen(children = serialize(element.children, callback)) ? element.return = element.value + '{' + children + '}' : ''
+}
diff --git a/stylis.js/src/Tokenizer.js b/stylis.js/src/Tokenizer.js
new file mode 100644
index 0000000..d14c136
--- /dev/null
+++ b/stylis.js/src/Tokenizer.js
@@ -0,0 +1,218 @@
+import {from, trim, charat, strlen, substr, append} from './Utility.js'
+
+export var line = 1
+export var column = 1
+export var length = 0
+export var position = 0
+export var character = 0
+export var characters = ''
+
+/**
+ * @param {string} value
+ * @param {object} root
+ * @param {object?} parent
+ * @param {string} type
+ * @param {string[]} props
+ * @param {object[]} children
+ * @param {number} length
+ */
+export function node (value, root, parent, type, props, children, length) {
+ return {value: value, root: root, parent: parent, type: type, props: props, children: children, line: line, column: column, length: length, return: ''}
+}
+
+/**
+ * @param {string} value
+ * @param {object} root
+ * @param {string} type
+ */
+export function copy (value, root, type) {
+ return node(value, root.root, root.parent, type, root.props, root.children, 0)
+}
+
+/**
+ * @return {number}
+ */
+export function char () {
+ return character
+}
+
+/**
+ * @return {number}
+ */
+export function next () {
+ character = position < length ? charat(characters, position++) : 0
+
+ if (column++, character === 10)
+ column = 1, line++
+
+ return character
+}
+
+/**
+ * @return {number}
+ */
+export function peek () {
+ return charat(characters, position)
+}
+
+/**
+ * @return {number}
+ */
+export function caret () {
+ return position
+}
+
+/**
+ * @param {number} begin
+ * @param {number} end
+ * @return {string}
+ */
+export function slice (begin, end) {
+ return substr(characters, begin, end)
+}
+
+/**
+ * @param {number} type
+ * @return {number}
+ */
+export function token (type) {
+ switch (type) {
+ // \0 \t \n \r \s whitespace token
+ case 0: case 9: case 10: case 13: case 32:
+ return 5
+ // ! + , / > @ ~ isolate token
+ case 33: case 43: case 44: case 47: case 62: case 64: case 126:
+ // ; { } / breakpoint token
+ case 59: case 123: case 125:
+ return 4
+ // : accompanied token
+ case 58:
+ return 3
+ // " ' ( [ opening delimit token
+ case 34: case 39: case 40: case 91:
+ return 2
+ // ) ] closing delimit token
+ case 41: case 93:
+ return 1
+ }
+
+ return 0
+}
+
+/**
+ * @param {string} value
+ * @return {any[]}
+ */
+export function alloc (value) {
+ return line = column = 1, length = strlen(characters = value), position = 0, []
+}
+
+/**
+ * @param {any} value
+ * @return {any}
+ */
+export function dealloc (value) {
+ return characters = '', value
+}
+
+/**
+ * @param {number} type
+ * @return {string}
+ */
+export function delimit (type) {
+ return trim(slice(position - 1, delimiter(type === 91 ? type + 2 : type === 40 ? type + 1 : type)))
+}
+
+/**
+ * @param {string} value
+ * @return {string[]}
+ */
+export function tokenize (value) {
+ return dealloc(tokenizer(alloc(value)))
+}
+
+/**
+ * @param {number} type
+ * @return {string}
+ */
+export function whitespace (type) {
+ while (character = peek())
+ if (character < 33)
+ next()
+ else
+ break
+
+ return token(type) > 2 || token(character) > 3 ? '' : ' '
+}
+
+/**
+ * @param {string[]} children
+ * @return {string[]}
+ */
+export function tokenizer (children) {
+ while (next())
+ switch (token(character)) {
+ case 0: append(identifier(position - 1), children)
+ break
+ case 2: append(delimit(character), children)
+ break
+ default: append(from(character), children)
+ }
+
+ return children
+}
+
+/**
+ * @param {number} type
+ * @return {number}
+ */
+export function delimiter (type) {
+ while (next())
+ switch (character) {
+ // ] ) " '
+ case type:
+ return position
+ // " '
+ case 34: case 39:
+ return delimiter(type === 34 || type === 39 ? type : character)
+ // (
+ case 40:
+ if (type === 41)
+ delimiter(type)
+ break
+ // \
+ case 92:
+ next()
+ break
+ }
+
+ return position
+}
+
+/**
+ * @param {number} type
+ * @param {number} index
+ * @return {number}
+ */
+export function commenter (type, index) {
+ while (next())
+ // //
+ if (type + character === 47 + 10)
+ break
+ // /*
+ else if (type + character === 42 + 42 && peek() === 47)
+ break
+
+ return '/*' + slice(index, position - 1) + '*' + from(type === 47 ? type : next())
+}
+
+/**
+ * @param {number} index
+ * @return {string}
+ */
+export function identifier (index) {
+ while (!token(peek()))
+ next()
+
+ return slice(index, position)
+}
diff --git a/stylis.js/src/Utility.js b/stylis.js/src/Utility.js
new file mode 100644
index 0000000..f29590e
--- /dev/null
+++ b/stylis.js/src/Utility.js
@@ -0,0 +1,109 @@
+/**
+ * @param {number}
+ * @return {number}
+ */
+export var abs = Math.abs
+
+/**
+ * @param {number}
+ * @return {string}
+ */
+export var from = String.fromCharCode
+
+/**
+ * @param {string} value
+ * @param {number} length
+ * @return {number}
+ */
+export function hash (value, length) {
+ return (((((((length << 2) ^ charat(value, 0)) << 2) ^ charat(value, 1)) << 2) ^ charat(value, 2)) << 2) ^ charat(value, 3)
+}
+
+/**
+ * @param {string} value
+ * @return {string}
+ */
+export function trim (value) {
+ return value.trim()
+}
+
+/**
+ * @param {string} value
+ * @param {RegExp} pattern
+ * @return {string?}
+ */
+export function match (value, pattern) {
+ return (value = pattern.exec(value)) ? value[0] : value
+}
+
+/**
+ * @param {string} value
+ * @param {(string|RegExp)} pattern
+ * @param {string} replacement
+ * @return {string}
+ */
+export function replace (value, pattern, replacement) {
+ return value.replace(pattern, replacement)
+}
+
+/**
+ * @param {string} value
+ * @param {string} value
+ * @return {number}
+ */
+export function indexof (value, search) {
+ return value.indexOf(search)
+}
+
+/**
+ * @param {string} value
+ * @param {number} index
+ * @return {number}
+ */
+export function charat (value, index) {
+ return value.charCodeAt(index) | 0
+}
+
+/**
+ * @param {string} value
+ * @param {number} begin
+ * @param {number} end
+ * @return {string}
+ */
+export function substr (value, begin, end) {
+ return value.slice(begin, end)
+}
+
+/**
+ * @param {string} value
+ * @return {number}
+ */
+export function strlen (value) {
+ return value.length
+}
+
+/**
+ * @param {any[]} value
+ * @return {number}
+ */
+export function sizeof (value) {
+ return value.length
+}
+
+/**
+ * @param {any} value
+ * @param {any[]} array
+ * @return {any}
+ */
+export function append (value, array) {
+ return array.push(value), value
+}
+
+/**
+ * @param {string[]} array
+ * @param {function} callback
+ * @return {string}
+ */
+export function combine (array, callback) {
+ return array.map(callback).join('')
+}
diff --git a/stylis.js/test/Middleware.js b/stylis.js/test/Middleware.js
new file mode 100644
index 0000000..4d867ad
--- /dev/null
+++ b/stylis.js/test/Middleware.js
@@ -0,0 +1,82 @@
+import {compile, serialize, stringify, middleware, rulesheet, prefixer, namespace} from "../index.js"
+
+const stack = []
+
+describe('Middleware', () => {
+ test('rulesheet', () => {
+ serialize(compile(`@import url('something.com/file.css');.user{ h1 {width:0;} @media{width:1;}@keyframes name{from{width:0;}to{width:1;}}}`), middleware([prefixer, stringify, rulesheet(value => stack.push(value))]))
+ expect(stack).to.deep.equal([
+ `@import url('something.com/file.css');`,
+ `.user h1{width:0;}`,
+ `@media{.user{width:1;}}`,
+ `@-webkit-keyframes name{from{width:0;}to{width:1;}}`,
+ `@keyframes name{from{width:0;}to{width:1;}}`,
+ ])
+ })
+
+ test('namespace', () => {
+ expect(serialize(compile(`.user{width:0; :global(p,a){width:1;} h1 {width:1; h2:last-child {width:2} h2 h3 {width:3}}}`), middleware([namespace, stringify]))).to.equal([
+ `.user{width:0;}`, `p,a{width:1;}`, `h1.user.user{width:1;}`, `h1.user h2:last-child.user{width:2;}`, `h1.user h2 h3.user{width:3;}`
+ ].join(''))
+ })
+
+ test('comments', () => {
+ expect(serialize(compile(`/*@noflip*/ .user{//noflip\n\n}`), middleware([value => value.type === 'comm' ? 'color:red;' : '', stringify]))).to.deep.equal([
+ `color:red;.user{color:red;}`
+ ].join())
+ })
+
+ test('prefixer', () => {
+ expect(serialize(compile(`.user{h1:last-child{clip-path:none;}`), middleware([prefixer, stringify]))).to.equal([
+ `.user h1:last-child{-webkit-clip-path:none;clip-path:none;}`,
+ ].join(''))
+
+ expect(serialize(compile(`@keyframes name{from{transform: rotate(0deg);}to{transform: rotate(360deg);}}`), middleware([prefixer, stringify]))).to.equal([
+ `@-webkit-keyframes name{from{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}to{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}`,
+ `@keyframes name{from{-webkit-transform:rotate(0deg);-moz-transform:rotate(0deg);-ms-transform:rotate(0deg);transform:rotate(0deg);}to{-webkit-transform:rotate(360deg);-moz-transform:rotate(360deg);-ms-transform:rotate(360deg);transform:rotate(360deg);}}`
+ ].join(''))
+
+ expect(serialize(compile(`a:read-only{color:red;}`), middleware([prefixer, stringify]))).to.equal([
+ `a:-moz-read-only{color:red;}`, `a:read-only{color:red;}`
+ ].join(''))
+
+ expect(serialize(compile(`a:read-write{color:red;}`), middleware([prefixer, stringify]))).to.equal([
+ `a:-moz-read-write{color:red;}`, `a:read-write{color:red;}`
+ ].join(''))
+
+ expect(serialize(compile(`a::placeholder{color:red;}`), middleware([prefixer, stringify]))).to.equal([
+ `a::-webkit-input-placeholder{color:red;}`, `a::-moz-placeholder{color:red;}`, `a:-ms-input-placeholder{color:red;}`, `a::placeholder{color:red;}`
+ ].join(''))
+
+ expect(serialize(compile(`textarea::placeholder{font-size:14px;@media{font-size:16px;}}`), middleware([prefixer, stringify]))).to.equal([
+ `textarea::-webkit-input-placeholder{font-size:14px;}textarea::-moz-placeholder{font-size:14px;}textarea:-ms-input-placeholder{font-size:14px;}textarea::placeholder{font-size:14px;}`,
+ `@media{textarea::-webkit-input-placeholder{font-size:16px;}textarea::-moz-placeholder{font-size:16px;}textarea:-ms-input-placeholder{font-size:16px;}textarea::placeholder{font-size:16px;}}`
+ ].join(''))
+
+ expect(serialize(compile(`div:read-write{background-color:red;span{background-color:green;}}`), middleware([prefixer, stringify]))).to.equal([
+ `div:-moz-read-write{background-color:red;}`,
+ `div:read-write{background-color:red;}`,
+ `div:-moz-read-write span{background-color:green;}`,
+ `div:read-write span{background-color:green;}`
+ ].join(''))
+
+ expect(serialize(compile(`div:read-write span{background-color:hotpink;}`), middleware([prefixer, stringify]))).to.equal([
+ `div:-moz-read-write span{background-color:hotpink;}`,
+ `div:read-write span{background-color:hotpink;}`
+ ].join(''))
+
+ expect(serialize(compile(`.read-write:read-write,.read-only:read-only,.placeholder::placeholder{background-color:hotpink;}`), middleware([prefixer, stringify]))).to.equal([
+ `.read-write:-moz-read-write{background-color:hotpink;}`,
+ `.read-only:-moz-read-only{background-color:hotpink;}`,
+ `.placeholder::-webkit-input-placeholder{background-color:hotpink;}`,
+ `.placeholder::-moz-placeholder{background-color:hotpink;}`,
+ `.placeholder:-ms-input-placeholder{background-color:hotpink;}`,
+ `.read-write:read-write,.read-only:read-only,.placeholder::placeholder{background-color:hotpink;}`,
+ ].join(''))
+
+ expect(serialize(compile(`:read-write{background-color:hotpink;}`), middleware([prefixer, stringify]))).to.equal([
+ `:-moz-read-write{background-color:hotpink;}`,
+ `:read-write{background-color:hotpink;}`
+ ].join(''))
+ })
+})
diff --git a/stylis.js/test/Parser.js b/stylis.js/test/Parser.js
new file mode 100644
index 0000000..3bed9a9
--- /dev/null
+++ b/stylis.js/test/Parser.js
@@ -0,0 +1,930 @@
+import {compile, serialize, stringify} from "../index.js"
+
+const stylis = string => serialize(compile(`.user{${string}}`), stringify)
+
+describe('Parser', () => {
+ test('unnested', () => {
+ expect(serialize(compile(`--foo:none;@supports{--bar:none;}`), stringify)).to.equal(`--foo:none;@supports{--bar:none;}`)
+ })
+
+ test('escape', () => {
+ expect(
+ stylis(`
+ height:calc(\\))\t!important;
+ `)
+ ).to.equal(`.user{height:calc(\\))!important;}`)
+ })
+
+ test('calc', () => {
+ expect(
+ stylis(`
+ height:calc( 100vh - 1px );
+ height:calc(
+ 100vh -
+ 1px
+ );
+ `)
+ ).to.equal(`.user{height:calc( 100vh - 1px );height:calc(\n 100vh -\n 1px\n );}`)
+ })
+
+ test('at-rules', () => {
+ expect(
+ stylis(`
+ @-ms-viewport {
+ width:device-width;
+ }
+ @viewport {
+ width:device-width;
+ }
+ @page & {
+ invalid:true;
+ }
+ @page {
+ size:A4 landscape;
+ }
+ @document url(://www.w3.org/),url-prefix(//www.w3.org/),domain(mozilla.org),regexp("https:.*") {
+ body {
+ color: red;
+ }
+ }
+ @viewport {
+ min-width:640px;
+ max-width:800px;
+ }
+ @counter-style list {
+ system:fixed;
+ symbols:url();
+ suffix:" ";
+ }
+ @-moz-document url-prefix() {
+ .selector {
+ color:lime;
+ }
+ }
+ @page {
+ color:red;
+ @bottom-right {
+ content: counter(pages);
+ margin-right: 1cm;
+ }
+ width: none;
+ }
+ `)
+ ).to.equal([
+ `@-ms-viewport{width:device-width;}`,
+ `@viewport{width:device-width;}`,
+ `@page &{invalid:true;}`,
+ `@page{size:A4 landscape;}`,
+ `@document url(://www.w3.org/),url-prefix(//www.w3.org/),domain(mozilla.org),regexp("https:.*"){.user body{color:red;}}`,
+ `@viewport{min-width:640px;max-width:800px;}`,
+ `@counter-style list{system:fixed;symbols:url();suffix:" ";}`,
+ `@-moz-document url-prefix(){.user .selector{color:lime;}}`,
+ `@page{color:red;@bottom-right{content:counter(pages);margin-right:1cm;}width:none;}`
+ ].join(''))
+ })
+
+ test('universal selector', () => {
+ expect(
+ stylis(`
+ * {
+ color:red;
+ }
+ svg {
+ &, & * {
+ fill: currentColor;
+ }
+ }
+ * * {color:hotpink;}
+ `)
+ ).to.equal([
+ `.user *{color:red;}`,
+ `.user svg,.user svg *{fill:currentColor;}`,
+ `.user * *{color:hotpink;}`
+ ].join(''))
+ })
+
+ test('flat', () => {
+ expect(
+ stylis(`
+ color:20px;
+ font-size:20px
+ `)
+ ).to.equal(`.user{color:20px;font-size:20px;}`)
+ })
+
+ test('namespace', () => {
+ expect(
+ stylis(`
+ & {
+ color:red;
+ }
+ `)
+ ).to.equal(`.user{color:red;}`)
+ })
+
+ test('& in a string', () => {
+ expect(
+ stylis(`
+ & [href="https://css-tricks.com?a=1&b=2"] {
+ color:red;
+ }
+ `)
+ ).to.equal(`.user [href="https://css-tricks.com?a=1&b=2"]{color:red;}`)
+ })
+
+ test('comments', () => {
+ expect(
+ stylis(`
+ // line comment
+ // color: red;
+ /**
+ * removes block comments and line comments,
+ * there's a fire in the house // there is
+ */
+ button /*
+ // what's
+ xxx
+ */
+ {color: blue;}
+ // hello
+ button /* 1 */
+ {
+ color: red; /* 2 */
+ }
+ /*! 1 */
+ color: red;
+ /*! 2 */
+ h1 {
+ /*! 1 */
+ color: red;
+ /*! 2 */
+ color: red;
+ /*! 3 */
+ }
+ `)
+ ).to.equal([
+ `.user{color:red;}`,
+ `.user button{color:blue;}.user button{color:red;}`,
+ `.user h1{color:red;color:red;}`
+ ].join(''))
+ })
+
+ test('&', () => {
+ expect(
+ stylis(`
+ & {
+ color:blue;
+ }
+ &&& {
+ color:red;
+ }
+ & + & {
+ color:red;
+ }
+ .wrapper button& {
+ color:red;
+ }
+ &:hover & {
+ color: green;
+ }
+ div:hover & {
+ color: green;
+ }
+ div:hover & {
+ h1 & {
+ color:red;
+ }
+ }
+ `)
+ ).to.equal([
+ `.user{color:blue;}`,
+ `.user.user.user{color:red;}`,
+ `.user+.user{color:red;}`,
+ `.wrapper button.user{color:red;}`,
+ `.user:hover .user{color:green;}`,
+ `div:hover .user{color:green;}`,
+ `h1 div:hover .user{color:red;}`
+ ].join(''))
+ })
+
+ test('&:before', () => {
+ expect(
+ stylis(`
+ &:before{
+ color:blue;
+ }
+ `)
+ ).to.equal(`.user:before{color:blue;}`)
+ })
+
+ test('& :hover', () => {
+ expect(
+ stylis(`
+ & :hover{
+ color:blue;
+ }
+ `)
+ ).to.equal(`.user :hover{color:blue;}`)
+ })
+
+ test('div :hover', () => {
+ expect(
+ stylis(`
+ div :hover {
+ color:blue;
+ }
+ `)
+ ).to.equal(`.user div :hover{color:blue;}`)
+ })
+
+ test('@import', () => {
+ expect(stylis(`@import url('http://example.com');`)).to.equal(`@import url('http://example.com');`)
+ })
+
+ test('@supports', () => {
+ expect(
+ stylis(`
+ @supports (display:block) {
+ color:red;
+ h1 {
+ color:red;
+ h2 {
+ color:blue;
+ }
+ }
+ display:none;
+ }
+ @supports (appearance: none) {
+ color:red;
+ }
+ @supports (backdrop-filter: blur(10px)) {
+ backdrop-filter: blur(10px);
+ }
+ `)
+ ).to.equal([
+ '@supports (display:block){.user{color:red;display:none;}.user h1{color:red;}.user h1 h2{color:blue;}}',
+ '@supports (appearance: none){.user{color:red;}}',
+ '@supports (backdrop-filter: blur(10px)){.user{backdrop-filter:blur(10px);}}'
+ ].join(''))
+ })
+
+ test('@media', () => {
+ expect(
+ stylis(`
+ @media (max-width:600px) {
+ color:red;
+ h1 {
+ color:red;
+ h2 {
+ color:blue;
+ }
+ }
+ display:none;
+ }
+ @media (min-width:576px) {
+ &.card-deck {
+ .card {
+ &:not(:first-child) {
+ margin-left:15px;
+ }
+ &:not(:last-child) {
+ margin-right:15px;
+ }
+ }
+ }
+ }
+ @supports (display:block) {
+ @media (min-width:10px) {
+ background-color:seagreen;
+ }
+ }
+ @media (max-width:600px) {
+ & { color:red }
+ }
+ &:hover {
+ color:orange
+ }
+ `)
+ ).to.equal([
+ '@media (max-width:600px){.user{color:red;display:none;}.user h1{color:red;}.user h1 h2{color:blue;}}',
+ '@media (min-width:576px){.user.card-deck .card:not(:first-child){margin-left:15px;}.user.card-deck .card:not(:last-child){margin-right:15px;}}',
+ '@supports (display:block){@media (min-width:10px){.user{background-color:seagreen;}}}',
+ '@media (max-width:600px){.user{color:red;}}.user:hover{color:orange;}'
+ ].join(''))
+ })
+
+ test('@media specifity', () => {
+ expect(
+ stylis(`
+ > #box-not-working {
+ background:red;
+ padding-left:8px;
+ width:10px;
+ @media only screen and (min-width:10px) {
+ width: calc(10px + 90px * (100vw - 10px) / 90);
+ }
+ @media only screen and (min-width:90px) {
+ width: 90px;
+ }
+ height: 10px;
+ @media only screen and (min-width:10px) {
+ height: calc(10px + 90px * (100vw - 10px) / 90);
+ }
+ @media only screen and (min-width:90px) {
+ height: 90px;
+ }
+ }
+ `)
+ ).to.equal([
+ '.user >#box-not-working{background:red;padding-left:8px;width:10px;height:10px;}',
+ '@media only screen and (min-width:10px){.user >#box-not-working{width:calc(10px + 90px * (100vw - 10px) / 90);}}',
+ '@media only screen and (min-width:90px){.user >#box-not-working{width:90px;}}',
+ '@media only screen and (min-width:10px){.user >#box-not-working{height:calc(10px + 90px * (100vw - 10px) / 90);}}',
+ '@media only screen and (min-width:90px){.user >#box-not-working{height:90px;}}'
+ ].join(''))
+ })
+
+ test('@font-face', () => {
+ expect(
+ stylis(`
+ @font-face {
+ font-family:Pangolin;
+ src:url('Pangolin-Regular.ttf') format('truetype');
+ }
+ `)
+ ).to.equal(
+ `@font-face{font-family:Pangolin;src:url('Pangolin-Regular.ttf') format('truetype');}`
+ )
+ })
+
+ test('multiple selectors', () => {
+ expect(
+ stylis(`
+ span, h1 {
+ color:red;
+ }
+ h1, &:after, &:before {
+ color:red;
+ }
+ `)
+ ).to.equal([
+ `.user span,.user h1{color:red;}`,
+ `.user h1,.user:after,.user:before{color:red;}`
+ ].join(''))
+ })
+
+ test('[title="a,b"] and :matches(a,b)', () => {
+ expect(
+ stylis(`
+ .test:matches(a,b,c), .test {
+ color:blue;
+ }
+ .test[title=","] {
+ color:red;
+ }
+ [title="a,b,c, something"], h1, [title="a,b,c"] {
+ color:red
+ }
+ [title="a"],
+ [title="b"] {
+ color:red;
+ }
+ `)
+ ).to.equal([
+ `.user .test:matches(a,b,c),.user .test{color:blue;}`,
+ `.user .test[title=","]{color:red;}`,
+ `.user [title="a,b,c, something"],.user h1,.user [title="a,b,c"]{color:red;}`,
+ `.user [title="a"],.user [title="b"]{color:red;}`
+ ].join(''))
+ })
+
+ test('quotes', () => {
+ expect(
+ stylis(`
+ .foo:before {
+ content:".hello {world}";
+ content:".hello {world} ' ";
+ content:'.hello {world} " ';
+ }
+ `)
+ ).to.equal(`.user .foo:before{content:".hello {world}";content:".hello {world} ' ";content:'.hello {world} " ';}`)
+ })
+
+ test('quotes - escaping', () => {
+ expect(
+ stylis(`
+ .foo:before {
+ content:"\\"";
+ content:"\\\\\\"";
+
+ content:'\\'';
+ content:'\\\\\\'';
+ }
+ `)
+ ).to.equal(`.user .foo:before{content:"\\"";content:"\\\\\\"";content:'\\'';content:'\\\\\\'';}`)
+ })
+
+ test('remove empty css', () => {
+ expect(
+ stylis(`& { }`)
+ ).to.equal(``)
+ })
+
+ test('urls', () => {
+ expect(
+ stylis(`
+ background:url(http://url.com/});
+ background:url(http://url.com//1234) '('; // sdsd
+ background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAAABCAIAAADsEU8HAAAACXBIW` +
+ `XMAAAsTAAALEwEAmpwYAAAAIklEQVQI12P8//8/Aw4wbdq0rKysAZG1trbGJXv06FH8sgDIJBbBfp+hFAAAAABJRU5ErkJggg==");`
+ )
+ ).to.equal([
+ `.user{background:url(http://url.com/});`,
+ `background:url(http://url.com//1234) '(';`,
+ `background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAAABCAIAAADsEU8HAAAACXBIW`,
+ `XMAAAsTAAALEwEAmpwYAAAAIklEQVQI12P8//8/Aw4wbdq0rKysAZG1trbGJXv06FH8sgDIJBbBfp+hFAAAAABJRU5ErkJggg==");}`
+ ].join(''))
+ })
+
+ test('last semicolon omission', () => {
+ expect(
+ stylis(`
+ .content {
+ color:red
+ }
+ .content {
+ color:blue
+ }
+ `)
+ ).to.equal(
+ [`.user .content{color:red;}`, `.user .content{color:blue;}`].join('')
+ )
+ })
+
+ test(':matches(:not())', () => {
+ expect(
+ stylis(`
+ h1:matches(.a,.b,:not(.c)) {
+ display: none
+ }
+ `)
+ ).to.equal(`.user h1:matches(.a,.b,:not(.c)){display:none;}`)
+ })
+
+ test('@keyframes', () => {
+ expect(serialize(compile(`
+ @-webkit-keyframes slidein {
+ to { transform:translate(20px); }
+ }
+ @keyframes slidein {
+ to { transform:translate(20px); }
+ }
+ @keyframes hahaha {
+ 0%,
+ 1%{t:0}
+ }
+ @keyframes infinite-spinning {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+ }
+ `), stringify)).to.equal([
+ `@-webkit-keyframes slidein{to{transform:translate(20px);}}`,
+ `@keyframes slidein{to{transform:translate(20px);}}`,
+ `@keyframes hahaha{0%,1%{t:0;}}`,
+ `@keyframes infinite-spinning{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}`
+ ].join(''))
+ })
+
+ test('edge cases', () => {
+ expect(
+ stylis(`
+ @media (min-width:537px) {
+ border-bottom:4px solid red;
+ }
+ &::placeholder {
+ color:pink;
+ }
+ .a {color:'red'}
+ .b {color:"red"}
+ .a {color:red;}[role=button]{color:red;}
+ .b {padding:30 3}
+ .c {v-text-anchor: middle;}
+ `)
+ ).to.equal([
+ `@media (min-width:537px){.user{border-bottom:4px solid red;}}`,
+ `.user::placeholder{color:pink;}`,
+ `.user .a{color:'red';}`,
+ `.user .b{color:"red";}`,
+ `.user .a{color:red;}`,
+ `.user [role=button]{color:red;}`,
+ `.user .b{padding:30 3;}`,
+ `.user .c{v-text-anchor:middle;}`
+ ].join(''))
+ })
+
+ test('whitespace', () => {
+ expect(
+ stylis(`
+ @import\r\n "./a.css";
+
+ div {
+ ${'width:0; '}
+ }
+ .foo {
+ color : hotpink;
+ }
+ `)
+ ).to.equal([
+ `@import "./a.css";`,
+ `.user div{width:0;}`,
+ `.user .foo{color:hotpink;}`
+ ].join(''))
+ })
+
+ test('no trailing semi-colons', () => {
+ expect(stylis(`
+ h2 {
+ display:none
+ }
+ div:hover
+ {
+ color:red
+ }
+ `)).to.equal([
+ '.user h2{display:none;}',
+ '.user div:hover{color:red;}'
+ ].join(''))
+ })
+
+ test('multiline declaration', () => {
+ expect(stylis(`
+ html {
+ background:
+ linear-gradient(0deg, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.8)),
+ url(/static/background.svg);
+ }
+ `)).to.equal(`.user html{background:linear-gradient(0deg, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 0.8)),url(/static/background.svg);}`)
+ })
+
+ test('nesting selector multiple levels', () => {
+ expect(
+ stylis(`
+ a {
+ a {
+ a {
+ a {
+ a {
+ a {
+ a {
+ a {
+ a {
+ a {
+ a {
+ a {
+ color:red;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ `)
+ ).to.equal(`.user a a a a a a a a a a a a{color:red;}`)
+ })
+
+ test('nesting @media multiple levels', () => {
+ expect(
+ stylis(`
+ div {
+ @media {
+ a {
+ color:red;
+
+ @media {
+ h1 {
+ color:hotpink;
+ }
+ }
+ }
+ }
+ }
+ `)
+ ).to.equal([`@media{.user div a{color:red;}`, `@media{.user div a h1{color:hotpink;}}}`].join(''))
+ })
+
+ test('noop tail I', () => {
+ expect(stylis(`color:red/**/`)).to.equal(`.user{color:red;}`)
+ })
+
+ test('noop tail II', () => {
+ expect(stylis(`color:red//`,)).to.equal('.user{color:red;}')
+ })
+
+ test('noop tail III', () => {
+ expect(stylis(`color:red[]`)).to.equal(`.user{color:red[];}`)
+ })
+
+ test('noop tail IV', () => {
+ expect(stylis(`color:red()`)).to.equal(`.user{color:red();}`)
+ })
+
+ test('noop tail V', () => {
+ expect(stylis(`color:red''`)).to.equal(`.user{color:red'';}`)
+ })
+
+ test('noop tail VI', () => {
+ expect(stylis(`color:red""`)).to.equal(`.user{color:red"";}`)
+ })
+
+ test('noop tail VII', () => {
+ expect(stylis(`h1{color:red/**}`)).to.equal(`.user h1{color:red;}`)
+ })
+
+ test('context character I', () => {
+ expect(stylis(`.a{color:red;/* } */}`)).to.equal(`.user .a{color:red;}`)
+ })
+
+ test('context character II', () => {
+ expect(stylis(`.a{color:red;/*}*/}`)).to.equal(`.user .a{color:red;}`)
+ })
+
+ test('context character III', () => {
+ expect(stylis(`.a{color:red;/*{*/}`)).to.equal(`.user .a{color:red;}`)
+ })
+
+ test('context character IV', () => {
+ expect(stylis(`.a{/**/color:red}`)).to.equal(`.user .a{color:red;}`)
+ })
+
+ test('context character V', () => {
+ expect(stylis(`.a{color:red;/*//color:blue;*/}`)).to.equal(`.user .a{color:red;}`)
+ })
+
+ test('context character VI', () => {
+ expect(
+ stylis(
+ `background: url("img}.png");.a {background: url("img}.png");}`
+ )
+ ).to.equal([
+ `.user{background:url("img}.png");}`,
+ `.user .a{background:url("img}.png");}`
+ ].join(''))
+ })
+
+ test('context character VII', () => {
+ expect(
+ stylis(`background: url(img}.png);.a {background: url(img}.png);}`)
+ ).to.equal([
+ `.user{background:url(img}.png);}`,
+ `.user .a{background:url(img}.png);}`
+ ].join(''))
+ })
+
+ test('context character VIII', () => {
+ expect(
+ stylis(`background: url[img}.png];.a {background: url[img}.png];}`)
+ ).to.equal([
+ `.user{background:url[img}.png];}`,
+ `.user .a{background:url[img}.png];}`
+ ].join(''))
+ })
+
+ test('`--` in an identifier (#220)', () => {
+ expect(
+ stylis(`
+ .block--modifier {
+ color: hotpink;
+ }
+ .card {
+ color: black;
+ }
+ `)
+ ).to.equal([
+ `.user .block--modifier{color:hotpink;}`,
+ `.user .card{color:black;}`
+ ].join(''))
+ })
+
+ test('comments in rules not increasing depth of consecutive rules (#154)', () => {
+ expect(
+ stylis(`
+ font-size:2rem;
+ .one{color:black;/* foo */}
+ .two{color:black;/* bar */}
+ .three{color:black;/* baz */}
+ `)
+ ).to.equal([
+ '.user{font-size:2rem;}',
+ '.user .one{color:black;}',
+ '.user .two{color:black;}',
+ '.user .three{color:black;}'
+ ].join(''))
+ })
+
+ test('comment in a group of selectors inside a media query (#152)', () => {
+ expect(
+ stylis(`
+ @media (min-width: 400px) {
+ div /* comment */ {
+ border-left:1px solid hotpink;
+ }
+ span {
+ border-top:none;
+ }
+ }
+ `)
+ ).to.equal(`@media (min-width: 400px){.user div{border-left:1px solid hotpink;}.user span{border-top:none;}}`)
+ })
+
+ test('comment - bang at the start (#114)', () => {
+ expect(serialize(compile(`/*! test */body{color:red;}`), stringify)).to.equal('body{color:red;}')
+ })
+
+ test('parenthesis in string literal I (#151)', () => {
+ expect(
+ stylis(`
+ @media only screen and (max-width: 320px){
+ background: url("${'image_(1).jpg'}");
+ }
+
+ @media only screen and (min-width:321px) {
+ background: url("${'image_(1).jpg'}");
+ }
+ `)
+ ).to.equal([
+ `@media only screen and (max-width: 320px){.user{background:url("${'image_(1).jpg'}");}}`,
+ `@media only screen and (min-width:321px){.user{background:url("${'image_(1).jpg'}");}}`
+ ].join(''))
+ })
+
+ test('parenthesis in string literal II (#123)', () => {
+ expect(
+ stylis(`
+ .a {
+ background: url("${'image_(1).jpg'})");
+ }
+
+ .b {
+ background: url("abc");
+ }
+ `)
+ ).to.equal([
+ `.user .a{background:url("image_(1).jpg)");}`,
+ `.user .b{background:url("abc");}`
+ ].join(''))
+ })
+
+ test('parenthesis in string literal III (#128)', () => {
+ expect(
+ stylis(`
+ .icon {
+ background:url("data:image/svg+xml;charset=utf-8,%3Csvg width='12' height='12' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.117.323L8.044 6.398 2.595.323a1.105 1.105 0 0 0-1.562 1.562L6.482 7.96.323 14.119a1.105 1.105 0 0 0 1.562 1.562L7.96 9.608l5.449 6.073a1.103 1.103 0 1 0 1.56-1.562L9.517 8.046l6.159-6.161a1.103 1.103 0 1 0-1.56-1.562z' fill='rgba(85, 85, 85, 0.5)'/%3E%3C/svg%3E");
+ }
+
+ div {
+ background: cyan;
+ }
+ `)
+ ).to.equal([
+ `.user .icon{background:url("data:image/svg+xml;charset=utf-8,%3Csvg width='12' height='12' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M14.117.323L8.044 6.398 2.595.323a1.105 1.105 0 0 0-1.562 1.562L6.482 7.96.323 14.119a1.105 1.105 0 0 0 1.562 1.562L7.96 9.608l5.449 6.073a1.103 1.103 0 1 0 1.56-1.562L9.517 8.046l6.159-6.161a1.103 1.103 0 1 0-1.56-1.562z' fill='rgba(85, 85, 85, 0.5)'/%3E%3C/svg%3E");}`,
+ `.user div{background:cyan;}`
+ ].join(''))
+ })
+
+ test('parenthesis in string literal IV (#116)', () => {
+ expect(
+ stylis(`
+ .a .b .c {
+ width: calc(100% / "func()");
+ }
+
+ .d {
+ background: yellow;
+ }
+ `)
+ ).to.equal([
+ `.user .a .b .c{width:calc(100% / "func()");}`,
+ `.user .d{background:yellow;}`
+ ].join(''))
+ })
+
+ test('nested parenthesis', () => {
+ expect(stylis(`width: calc(calc(1) + 10);`)).to.equal(`.user{width:calc(calc(1) + 10);}`)
+ })
+
+ test('css variables edge cases (#144, #173)', () => {
+ expect(
+ stylis(`
+ --braces: { };
+ --at-keyword-unknown-block: @foobar {};
+ --at-keyword-known-block: @media {};
+ --cdo-at-top-level: <!--;
+ --cdc-at-top-level: -->;
+ --semicolon-not-top-level: (;);
+ --cdo-not-top-level: (<!--);
+ --cdc-not-top-level: (-->);
+ --ampersand-preserved: foo & bar;
+ `)
+ ).to.equal(`.user{` + [
+ `--braces:{};`,
+ `--at-keyword-unknown-block:@foobar{};`,
+ `--at-keyword-known-block:@media{};`,
+ `--cdo-at-top-level:<!--;`,
+ `--cdc-at-top-level:-->;`,
+ `--semicolon-not-top-level:(;);`,
+ `--cdo-not-top-level:(<!--);`,
+ `--cdc-not-top-level:(-->);`,
+ `--ampersand-preserved:foo & bar;`
+ ].join('') +'}')
+ })
+
+ test('does not hang on unterminated block comment (#129)', () => {
+ expect(stylis(`/*`)).to.equal(``)
+ })
+
+ test('does not hang on unterminated function', () => {
+ expect(stylis(`(`)).to.equal(``)
+ })
+
+ test('handles single `/` in a value', () => {
+ expect(stylis(`font: 12px/14px serif;`)).to.equal(`.user{font:12px/14px serif;}`)
+ })
+
+ test('nested', () => {
+ expect(stylis(`
+ div {
+ h2 {
+ color:red;
+ h3 {
+ color:blue;
+ }
+ }
+ }
+
+ .foo & {
+ width:1px;
+ &:hover {
+ color:black;
+ }
+ li {
+ color:white;
+ }
+ }
+
+ h1, div {
+ color:red;
+ h2,
+ &:before {
+ color:red;
+ }
+ color:blue;
+ header {
+ font-size:12px;
+ }
+ @media {
+ color:red;
+ }
+ @media {
+ color:blue;
+ }
+ }
+
+ &.foo {
+ &.bar {
+ color:orange
+ }
+ }
+
+ &.foo {
+ &.bar {
+ &.barbar {
+ color:orange
+ }
+ }
+ }
+ `)).to.equal([
+ `.user div h2{color:red;}`+
+ `.user div h2 h3{color:blue;}`+
+ `.foo .user{width:1px;}`+
+ `.foo .user:hover{color:black;}`+
+ `.foo .user li{color:white;}`+
+ `.user h1,.user div{color:red;color:blue;}`+
+ `.user h1 h2,.user div h2,.user h1:before,.user div:before{color:red;}`+
+ `.user h1 header,.user div header{font-size:12px;}`+
+ `@media{.user h1,.user div{color:red;}}`+
+ `@media{.user h1,.user div{color:blue;}}`+
+ `.user.foo.bar{color:orange;}`+
+ `.user.foo.bar.barbar{color:orange;}`
+ ].join(''))
+ })
+})
diff --git a/stylis.js/test/Prefixer.js b/stylis.js/test/Prefixer.js
new file mode 100644
index 0000000..c744804
--- /dev/null
+++ b/stylis.js/test/Prefixer.js
@@ -0,0 +1,178 @@
+import {prefix} from "../index.js"
+
+describe('Prefixer', () => {
+ test('flex-box', () => {
+ expect(prefix(`display:block;`, 7)).to.equal(['display:block;'].join())
+ expect(prefix(`display:flex!important;`, 7)).to.equal([`display:-webkit-box!important;`, `display:-webkit-flex!important;`, `display:-ms-flexbox!important;`, `display:flex!important;`].join(''))
+ expect(prefix(`display:inline-flex;`, 7)).to.equal([`display:-webkit-inline-box;`, `display:-webkit-inline-flex;`, `display:-ms-inline-flexbox;`, `display:inline-flex;`].join(''))
+ expect(prefix(`display:inline-box;`, 7)).to.equal([`-webkit-display:inline-box;`, `display:inline-box;`].join(''))
+ expect(prefix(`flex:inherit;`, 4)).to.equal([`-webkit-flex:inherit;`, `-ms-flex:inherit;`, `flex:inherit;`].join(''))
+ expect(prefix(`flex-grow:none;`, 9)).to.equal([`-webkit-box-flex:none;`, `-webkit-flex-grow:none;`, `-ms-flex-positive:none;`, `flex-grow:none;`].join(''))
+ expect(prefix(`flex-shrink:none;`, 11)).to.equal([`-webkit-flex-shrink:none;`, `-ms-flex-negative:none;`, `flex-shrink:none;`].join(''))
+ expect(prefix(`flex-basis:none;`, 10)).to.equal([`-webkit-flex-basis:none;`, `-ms-flex-preferred-size:none;`, `flex-basis:none;`].join(''))
+ expect(prefix(`align-self:value;`, 10)).to.equal([`-webkit-align-self:value;`, `-ms-flex-item-align:value;`, `align-self:value;`].join(''))
+ expect(prefix(`align-self:flex-start;`, 10)).to.equal([`-webkit-align-self:flex-start;`, `-ms-flex-item-align:flex-start;`, `align-self:flex-start;`].join(''))
+ expect(prefix(`align-self:flex-end;`, 10)).to.equal([`-webkit-align-self:flex-end;`, `-ms-flex-item-align:flex-end;`, `align-self:flex-end;`].join(''))
+ expect(prefix(`align-content:value;`, 13)).to.equal([`-webkit-align-content:value;`, `-ms-flex-line-pack:value;`, `align-content:value;`].join(''))
+ expect(prefix(`align-content:flex-start;`, 13)).to.equal([`-webkit-align-content:flex-start;`, `-ms-flex-line-pack:flex-start;`, `align-content:flex-start;`].join(''))
+ expect(prefix(`align-content:flex-end;`, 13)).to.equal([`-webkit-align-content:flex-end;`, `-ms-flex-line-pack:flex-end;`, `align-content:flex-end;`].join(''))
+ expect(prefix(`align-items:value;`, 11)).to.equal([`-webkit-align-items:value;`, `-webkit-box-align:value;`, `-ms-flex-align:value;`, `align-items:value;`].join(''))
+ expect(prefix(`justify-content:flex-end;`, 15)).to.equal([`-webkit-box-pack:end;`, `-ms-flex-pack:end;`, `-webkit-justify-content:flex-end;`, `justify-content:flex-end;`].join(''))
+ expect(prefix(`justify-content:flex-start;`, 15)).to.equal([`-webkit-box-pack:start;`, `-ms-flex-pack:start;`, `-webkit-justify-content:flex-start;`, `justify-content:flex-start;`].join(''))
+ expect(prefix(`justify-content:justify;`, 15)).to.equal([`-webkit-box-pack:justify;`, `-ms-flex-pack:justify;`, `-webkit-justify-content:justify;`, `justify-content:justify;`].join(''))
+ expect(prefix(`justify-content:space-between;`, 15)).to.equal([`-webkit-box-pack:justify;`, `-webkit-justify-content:space-between;`, `justify-content:space-between;`].join(''))
+ expect(prefix(`justify-items:center;`, 13)).to.equal([`justify-items:center;`].join(''))
+ expect(prefix(`order:flex;`, 5)).to.equal([`-webkit-order:flex;`, `-ms-flex-order:flex;`, `order:flex;`].join(''))
+ expect(prefix(`flex-direction:column;`, 14)).to.equal([`-webkit-flex-direction:column;`, `-ms-flex-direction:column;`, `flex-direction:column;`].join(''))
+ })
+
+ test('transform', () => {
+ expect(prefix(`transform:rotate(30deg);`, 9)).to.equal([`-webkit-transform:rotate(30deg);`, `-moz-transform:rotate(30deg);`, `-ms-transform:rotate(30deg);`, `transform:rotate(30deg);`].join(''))
+ })
+
+ test('cursor', () => {
+ expect(prefix(`cursor:none;`, 6)).to.equal([`cursor:none;`].join(''))
+ expect(prefix(`cursor:grab;`, 6)).to.equal([`cursor:-webkit-grab;`, `cursor:grab;`].join(''))
+ expect(prefix(`cursor:image-set(url(foo.jpg) 2x), pointer;`, 6)).to.equal([
+ `cursor:-webkit-image-set(url(foo.jpg) 2x), pointer;`,
+ `cursor:image-set(url(foo.jpg) 2x), pointer;`
+ ].join(''))
+ expect(prefix(`cursor:image-set(url(foo.jpg) 2x), grab;`, 6)).to.equal([
+ `cursor:-webkit-image-set(url(foo.jpg) 2x), -webkit-grab;`,
+ `cursor:image-set(url(foo.jpg) 2x), grab;`
+ ].join(''))
+ })
+
+ test('backface-visibility', () => {
+ expect(prefix(`backface-visibility:hidden;`, 19)).to.equal([`-webkit-backface-visibility:hidden;`, `backface-visibility:hidden;`].join(''))
+ })
+
+ test('transition', () => {
+ expect(prefix(`transition:transform 1s,transform all 400ms,text-transform;`, 10)).to.equal([
+ `-webkit-transition:-webkit-transform 1s,-webkit-transform all 400ms,text-transform;`,
+ `transition:transform 1s,transform all 400ms,text-transform;`
+ ].join(''))
+ })
+
+ test('writing-mode', () => {
+ expect(prefix(`writing-mode:none;`, 12)).to.equal([`-webkit-writing-mode:none;`, `-ms-writing-mode:none;`, `writing-mode:none;`].join(''))
+ expect(prefix(`writing-mode:vertical-lr;`, 12)).to.equal([`-webkit-writing-mode:vertical-lr;`, `-ms-writing-mode:tb;`, `writing-mode:vertical-lr;`].join(''))
+ expect(prefix(`writing-mode:vertical-rl;`, 12)).to.equal([`-webkit-writing-mode:vertical-rl;`, `-ms-writing-mode:tb-rl;`, `writing-mode:vertical-rl;`].join(''))
+ expect(prefix(`writing-mode:horizontal-tb;`, 12)).to.equal([`-webkit-writing-mode:horizontal-tb;`, `-ms-writing-mode:lr;`, `writing-mode:horizontal-tb;`].join(''))
+ expect(prefix(`writing-mode:sideways-rl;`, 12)).to.equal([`-webkit-writing-mode:sideways-rl;`, `-ms-writing-mode:tb-rl;`, `writing-mode:sideways-rl;`].join(''))
+ expect(prefix(`writing-mode:sideways-lr;`, 12)).to.equal([`-webkit-writing-mode:sideways-lr;`, `-ms-writing-mode:tb;`, `writing-mode:sideways-lr;`].join(''))
+ })
+
+ test('columns', () => {
+ expect(prefix(`columns:auto;`, 7)).to.equal([`-webkit-columns:auto;`, `columns:auto;`].join(''))
+ expect(prefix(`column-count:auto;`, 12)).to.equal([`-webkit-column-count:auto;`, `column-count:auto;`].join(''))
+ expect(prefix(`column-fill:auto;`, 11)).to.equal([`-webkit-column-fill:auto;`, `column-fill:auto;`].join(''))
+ expect(prefix(`column-gap:auto;`, 10)).to.equal([`-webkit-column-gap:auto;`, `column-gap:auto;`].join(''))
+ expect(prefix(`column-rule:auto;`, 11)).to.equal([`-webkit-column-rule:auto;`, `column-rule:auto;`].join(''))
+ expect(prefix(`column-rule-color:auto;`, 17)).to.equal([`-webkit-column-rule-color:auto;`, `column-rule-color:auto;`].join(''))
+ expect(prefix(`column-rule-style:auto;`, 17)).to.equal([`-webkit-column-rule-style:auto;`, `column-rule-style:auto;`].join(''))
+ expect(prefix(`column-rule-width:auto;`, 17)).to.equal([`-webkit-column-rule-width:auto;`, `column-rule-width:auto;`].join(''))
+ expect(prefix(`column-span:auto;`, 11)).to.equal([`-webkit-column-span:auto;`, `column-span:auto;`].join(''))
+ expect(prefix(`column-width:auto;`, 12)).to.equal([`-webkit-column-width:auto;`, `column-width:auto;`].join(''))
+ })
+
+ test('text', () => {
+ expect(prefix(`text-align:left;`, 10)).to.equal([`text-align:left;`].join(''))
+ expect(prefix(`text-transform:none;`, 14)).to.equal([`text-transform:none;`].join(''))
+ expect(prefix(`text-shadow:none;`, 11)).to.equal([`text-shadow:none;`].join(''))
+ expect(prefix(`text-size-adjust:none;`, 16)).to.equal([`-webkit-text-size-adjust:none;`, `-moz-text-size-adjust:none;`, `-ms-text-size-adjust:none;`, `text-size-adjust:none;`].join(''))
+ expect(prefix(`text-decoration:none;`, 15)).to.equal([`-webkit-text-decoration:none;`, `text-decoration:none;`].join(''))
+ })
+
+ test('mask', () => {
+ expect(prefix(`mask:none;`, 10)).to.equal([`-webkit-mask:none;`, `mask:none;`].join(''))
+ expect(prefix(`mask-image:none;`, 10)).to.equal([`-webkit-mask-image:none;`, `mask-image:none;`].join(''))
+ expect(prefix(`mask-image:linear-gradient(#fff);`, 10)).to.equal([`-webkit-mask-image:linear-gradient(#fff);`, `mask-image:linear-gradient(#fff);`].join(''))
+ expect(prefix(`mask-mode:none;`, 10)).to.equal([`-webkit-mask-mode:none;`, `mask-mode:none;`].join(''))
+ expect(prefix(`mask-clip:none;`, 10)).to.equal([`-webkit-mask-clip:none;`, `mask-clip:none;`].join(''))
+ expect(prefix(`mask-size:none;`, 10)).to.equal([`-webkit-mask-size:none;`, `mask-size:none;`].join(''))
+ expect(prefix(`mask-repeat:none;`, 10)).to.equal([`-webkit-mask-repeat:none;`, `mask-repeat:none;`].join(''))
+ expect(prefix(`mask-origin:none;`, 10)).to.equal([`-webkit-mask-origin:none;`, `mask-origin:none;`].join(''))
+ expect(prefix(`mask-position:none;`, 10)).to.equal([`-webkit-mask-position:none;`, `mask-position:none;`].join(''))
+ expect(prefix(`mask-composite:none;`, 10)).to.equal([`-webkit-mask-composite:none;`, `mask-composite:none;`].join(''))
+ })
+
+ test('filter', () => {
+ expect(prefix(`filter:grayscale(100%);`, 6)).to.equal([`-webkit-filter:grayscale(100%);`, `filter:grayscale(100%);`].join(''))
+ expect(prefix(`fill:red;`, 4)).to.equal([`fill:red;`].join(''))
+ })
+
+ test('position', () => {
+ expect(prefix(`position:relative;`, 8)).to.equal([`position:relative;`].join(''))
+ expect(prefix(`position:sticky;`, 8)).to.equal([`-webkit-position:sticky;`, `position:sticky;`].join(''))
+ })
+
+ test('box', () => {
+ expect(prefix(`box-decoration-break:slice;`, 20)).to.equal([`-webkit-box-decoration-break:slice;`, `box-decoration-break:slice;`].join(''))
+ expect(prefix(`box-sizing:border-box;`, 10)).to.equal([`box-sizing:border-box;`].join(''))
+ })
+
+ test('clip', () => {
+ expect(prefix(`clip-path:none;`, 9)).to.equal([`-webkit-clip-path:none;`, `clip-path:none;`].join(''))
+ })
+
+ test('size', () => {
+ expect(prefix(`width:auto;`, 5)).to.equal([`width:auto;`].join(''))
+ expect(prefix(`width:unset;`, 5)).to.equal([`width:unset;`].join(''))
+ expect(prefix(`width:initial;`, 5)).to.equal([`width:initial;`].join(''))
+ expect(prefix(`width:inherit;`, 5)).to.equal([`width:inherit;`].join(''))
+ expect(prefix(`width:10;`, 5)).to.equal([`width:10;`].join(''))
+ expect(prefix(`width:min();`, 5)).to.equal([`width:min();`].join(''))
+ expect(prefix(`width:var(--foo-content);`, 5)).to.equal([`width:var(--foo-content);`].join(''))
+ expect(prefix(`width:var(-content);`, 5)).to.equal([`width:var(-content);`].join(''))
+ expect(prefix(`width:var(--max-content);`, 5)).to.equal([`width:var(--max-content);`].join(''))
+ expect(prefix(`width:--max-content;`, 5)).to.equal([`width:--max-content;`].join(''))
+ expect(prefix(`width:fit-content;`, 5)).to.equal([`width:-webkit-fit-content;`, `width:-moz-content;`, `width:fit-content;`].join(''))
+ expect(prefix(`min-width:max-content;`, 9)).to.equal([`min-width:-webkit-max-content;`, `min-width:-moz-max-content;`, `min-width:max-content;`].join(''))
+ expect(prefix(`max-width:min-content;`, 9)).to.equal([`max-width:-webkit-min-content;`, `max-width:-moz-min-content;`, `max-width:min-content;`].join(''))
+ expect(prefix(`height:fill-available;`, 6)).to.equal([`height:-webkit-fill-available;`, `height:-moz-available;`, `height:fill-available;`].join(''))
+ expect(prefix(`max-height:fit-content;`, 10)).to.equal([`max-height:-webkit-fit-content;`, `max-height:-moz-content;`, `max-height:fit-content;`].join(''))
+ expect(prefix(`width:stretch;`, 5)).to.equal([`width:-webkit-fill-available;`, `width:-moz-available;`, `width:fill-available;`, `width:stretch;`].join(''))
+ expect(prefix(`width:stretch !important;`, 5)).to.equal([`width:-webkit-fill-available !important;`, `width:-moz-available !important;`, `width:fill-available !important;`, `width:stretch !important;`].join(''))
+ expect(prefix(`min-block-size:max-content;`, 14)).to.equal([`min-block-size:-webkit-max-content;`, `min-block-size:-moz-max-content;`, `min-block-size:max-content;`].join(''))
+ expect(prefix(`min-inline-size:max-content;`, 15)).to.equal([`min-inline-size:-webkit-max-content;`, `min-inline-size:-moz-max-content;`, `min-inline-size:max-content;`].join(''))
+ })
+
+ test('zoom', () => {
+ expect(prefix(`min-zoom:0;`, 8)).to.equal([`min-zoom:0;`].join(''))
+ })
+
+ test('background', () => {
+ expect(prefix(`background:none;`, 10)).to.equal([`background:none;`].join(''))
+ expect(prefix(`background:image-set(url(foo.jpg) 2x);`, 10)).to.equal([`background:-webkit-image-set(url(foo.jpg) 2x);`, `background:image-set(url(foo.jpg) 2x);`].join(''))
+ expect(prefix(`background-image:image-set(url(foo.jpg) 2x);`, 16)).to.equal([`background-image:-webkit-image-set(url(foo.jpg) 2x);`, `background-image:image-set(url(foo.jpg) 2x);`].join(''))
+ })
+
+ test('background-clip', () => {
+ expect(prefix(`background-clip:text;`, 15)).to.equal([`-webkit-background-clip:text;`, `background-clip:text;`].join(''))
+ })
+
+ test('margin-inline', () => {
+ expect(prefix(`margin-inline-start:20px;`, 19)).to.equal([`-webkit-margin-start:20px;`, `margin-inline-start:20px;`].join(''))
+ expect(prefix(`margin-inline-end:20px;`, 17)).to.equal([`-webkit-margin-end:20px;`, `margin-inline-end:20px;`].join(''))
+ })
+
+ test('user-select', () => {
+ expect(prefix(`user-select:none;`, 11)).to.equal([`-webkit-user-select:none;`, `-moz-user-select:none;`, `-ms-user-select:none;`, `user-select:none;`].join(''))
+ })
+
+ test('appearance', () => {
+ expect(prefix(`appearance:none;`, 10)).to.equal([`-webkit-appearance:none;`, `-moz-appearance:none;`, `-ms-appearance:none;`, `appearance:none;`].join(''))
+ })
+
+ test('animation', () => {
+ expect(prefix(`animation:inherit;`, 9)).to.equal([`-webkit-animation:inherit;`, `animation:inherit;`].join(''))
+ expect(prefix(`animation-duration:0.6s;`, 18)).to.equal([`-webkit-animation-duration:0.6s;`, `animation-duration:0.6s;`].join(''))
+ expect(prefix(`animation-name:slidein;`, 14)).to.equal([`-webkit-animation-name:slidein;`, `animation-name:slidein;`].join(''))
+ expect(prefix(`animation-iteration-count:infinite;`, 25)).to.equal([`-webkit-animation-iteration-count:infinite;`, `animation-iteration-count:infinite;`].join(''))
+ expect(prefix(`animation-timing-function:cubic-bezier(0.1,0.7,1.0,0.1);`, 25)).to.equal([
+ `-webkit-animation-timing-function:cubic-bezier(0.1,0.7,1.0,0.1);`,
+ `animation-timing-function:cubic-bezier(0.1,0.7,1.0,0.1);`
+ ].join(''))
+ })
+})
diff --git a/stylis.js/test/Tokenizer.js b/stylis.js/test/Tokenizer.js
new file mode 100644
index 0000000..2be5bb3
--- /dev/null
+++ b/stylis.js/test/Tokenizer.js
@@ -0,0 +1,9 @@
+import {tokenize} from "../index.js"
+
+describe('Tokenizer', () => {
+ test('tokenize', () => {
+ expect(tokenize('h1 h2 (h1 h2) 1 / 3 * 2 + 1 [1 2] "1 2" a')).to.deep.equal([
+ 'h1', ' ', 'h2', ' ', '(h1 h2)', ' ', '1', ' ', '/', ' ', '3', ' ', '*', ' ', '2', ' ', '+', ' ', '1', ' ', '[1 2]', ' ', '"1 2"', ' ', 'a'
+ ])
+ })
+})