Skip to content
This repository was archived by the owner on Oct 14, 2022. It is now read-only.

Commit 6c44a51

Browse files
committedJan 26, 2018
Initial commit
1 parent 7fc982a commit 6c44a51

34 files changed

+13899
-1
lines changed
 

‎.eslintrc.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module.exports = {
2+
"env": {
3+
"browser" : true,
4+
"commonjs": true,
5+
"es6" : true,
6+
"node" : true
7+
},
8+
"extends": "eslint:recommended",
9+
"plugins": [],
10+
"parserOptions": {
11+
"sourceType" : "module"
12+
},
13+
"rules": {
14+
"array-bracket-spacing" : ["error", "never"],
15+
"array-callback-return" : ["error"],
16+
"block-scoped-var" : ["error"],
17+
"block-spacing" : ["error", "always"],
18+
"curly" : ["error"],
19+
"dot-notation" : ["error"],
20+
"eqeqeq" : ["error"],
21+
"indent" : ["error", 4],
22+
"linebreak-style" : ["error", "unix"],
23+
"no-console" : ["warn"],
24+
"no-floating-decimal" : ["error"],
25+
"no-implicit-coercion" : ["error"],
26+
"no-implicit-globals" : ["error"],
27+
"no-loop-func" : ["error"],
28+
"no-return-assign" : ["error"],
29+
"no-template-curly-in-string": ["error"],
30+
"no-unneeded-ternary" : ["error"],
31+
"no-unused-vars" : ["error", { "args": "none" }],
32+
"no-useless-computed-key" : ["error"],
33+
"no-useless-return" : ["error"],
34+
"no-var" : ["error"],
35+
"prefer-const" : ["error"],
36+
"quotes" : ["error", "single"],
37+
"semi" : ["error", "always"]
38+
}
39+
};

‎.gitignore

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Folders
2+
coverage
3+
node_modules
4+
5+
# Files
6+
*.log
7+
8+
# OS
9+
._*
10+
.cache
11+
.DS_Store

‎.travis.yml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
language: node_js
2+
node_js: "stable"
3+
4+
notifications:
5+
email: false
6+
7+
before_install:
8+
- stty cols 80
9+
10+
script: "npm run test-remote"
11+
12+
after_success:
13+
- bash <(curl -s https://codecov.io/bash) -e TRAVIS_NODE_VERSION

‎CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Change Log
2+
3+
## 1.0.0 - 2017-11-26
4+
5+
**Added**
6+
7+
- Initial release

‎LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2017 John Hildenbiddle
3+
Copyright (c) 2018 John Hildenbiddle
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

‎dist/css-vars-ponyfill.esm.js

+979
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/css-vars-ponyfill.esm.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/css-vars-ponyfill.esm.min.js

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/css-vars-ponyfill.esm.min.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/css-vars-ponyfill.js

+958
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/css-vars-ponyfill.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/css-vars-ponyfill.min.js

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎dist/css-vars-ponyfill.min.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎karma.conf.js

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Dependencies
2+
// =============================================================================
3+
const pkg = require('./package');
4+
const saucelabs = require('./saucelabs.config');
5+
6+
7+
// Variables
8+
// =============================================================================
9+
const files = {
10+
serve: './tests/fixtures/**/*.*',
11+
test : './tests/**/*.test.js'
12+
};
13+
14+
15+
// Local config
16+
// =============================================================================
17+
const localConfig = {
18+
// Add browsers via Karma launchers
19+
// https://www.npmjs.com/search?q=karma+launcher
20+
browsers: [
21+
'ChromeHeadless'
22+
],
23+
files: [
24+
'node_modules/babel-polyfill/dist/polyfill.js',
25+
files.test,
26+
// Serve files for accessing in tests via AJAX
27+
// Ex: /base/[files.serve]/path/to/file
28+
{ pattern: files.serve, included: false, served: true, watched: true }
29+
],
30+
preprocessors: {
31+
[files.test]: ['eslint', 'webpack', 'sourcemap']
32+
},
33+
frameworks: ['mocha', 'chai'],
34+
reporters : ['mocha', 'coverage'],
35+
webpack : {
36+
devtool: 'inline-source-map',
37+
module: {
38+
rules: [{
39+
test : /\.js$/,
40+
exclude: [/node_modules/],
41+
use : [{
42+
loader : 'babel-loader',
43+
options: {
44+
presets: [
45+
['env', {
46+
targets: {
47+
browsers: ['ie >= 9']
48+
}
49+
}]
50+
],
51+
plugins: [
52+
['istanbul', {
53+
exclude: [
54+
'**/*.test.js',
55+
'tests/helpers/*'
56+
]
57+
}]
58+
]
59+
},
60+
}]
61+
}]
62+
}
63+
},
64+
webpackMiddleware: {
65+
// From the docs:
66+
// "With noInfo enabled, messages like the webpack bundle information
67+
// that is shown when starting up and after each save, will be hidden.
68+
// Errors and warnings will still be shown."
69+
// https://webpack.js.org/configuration/dev-server/#devserver-noinfo-
70+
noInfo: true
71+
},
72+
// Code coverage
73+
// https://www.npmjs.com/package/karma-coverage
74+
coverageReporter: {
75+
reporters: [
76+
{ type: 'html' },
77+
{ type: 'lcovonly' },
78+
{ type: 'text-summary' }
79+
]
80+
},
81+
// Mocha reporter
82+
// https://www.npmjs.com/package/karma-mocha-reporter
83+
mochaReporter: {
84+
output: 'autowatch'
85+
},
86+
port : 9876,
87+
colors : true,
88+
autoWatch : false,
89+
singleRun : true,
90+
concurrency: Infinity
91+
};
92+
93+
94+
// Remote config
95+
// =============================================================================
96+
const remoteConfig = Object.assign({}, localConfig, {
97+
// SauceLabs browers (see platform configurator below)
98+
// https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/
99+
customLaunchers: {
100+
sl_chrome: {
101+
base : 'SauceLabs',
102+
browserName: 'Chrome',
103+
platform : 'Windows 10',
104+
version : '26.0'
105+
},
106+
sl_edge: {
107+
base : 'SauceLabs',
108+
browserName: 'MicrosoftEdge',
109+
platform : 'Windows 10',
110+
version : '13.10586'
111+
},
112+
sl_firefox: {
113+
base : 'SauceLabs',
114+
browserName: 'Firefox',
115+
platform : 'Windows 10',
116+
version : '30'
117+
},
118+
sl_ie_11: {
119+
base : 'SauceLabs',
120+
browserName: 'Internet Explorer',
121+
platform : 'Windows 10',
122+
version : '11.0'
123+
},
124+
sl_ie_10: {
125+
base : 'SauceLabs',
126+
browserName: 'Internet Explorer',
127+
platform : 'Windows 8',
128+
version : '10.0'
129+
},
130+
sl_ie_9: {
131+
base : 'SauceLabs',
132+
browserName: 'Internet Explorer',
133+
platform : 'Windows 7',
134+
version : '9.0'
135+
},
136+
sl_safari: {
137+
base : 'SauceLabs',
138+
browserName: 'Safari',
139+
platform : 'OS X 10.9',
140+
version : '7.0'
141+
}
142+
},
143+
// Set browsers to customLaunchers
144+
get browsers() {
145+
return Object.keys(this.customLaunchers);
146+
},
147+
// SauceLab settings
148+
sauceLabs: {
149+
username : saucelabs.username || process.env.SAUCE_USERNAME,
150+
accessKey: saucelabs.accessKey || process.env.SAUCE_ACCESS_KEY,
151+
testName : `${pkg.name} (karma)`
152+
}
153+
});
154+
155+
156+
// Export
157+
// =============================================================================
158+
module.exports = function(config) {
159+
const isRemote = Boolean(process.argv.indexOf('--remote') > -1);
160+
const testConfig = isRemote ? remoteConfig : localConfig;
161+
162+
if (isRemote) {
163+
// Disabled source maps to prevent SauceLabs timeouts
164+
// https://github.com/karma-runner/karma-sauce-launcher/issues/95
165+
testConfig.webpack.devtool = '';
166+
testConfig.webpack.module.rules[0].use[0].options.sourceMap = false;
167+
168+
// Add SauceLabs reporter
169+
testConfig.reporters.push('saucelabs');
170+
171+
// Remove text-summary reporter
172+
testConfig.coverageReporter.reporters = testConfig.coverageReporter.reporters.filter(obj => obj.type !== 'text-summary');
173+
}
174+
else {
175+
// eslint-disable-next-line
176+
console.log([
177+
'============================================================\n',
178+
`KARMA: localhost:${testConfig.port}/debug.html\n`,
179+
'============================================================\n'
180+
].join(''));
181+
}
182+
183+
// Logging: LOG_DISABLE, LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG
184+
testConfig.logLevel = config.LOG_WARN;
185+
config.set(testConfig);
186+
};

‎package-lock.json

+9,160
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"name": "css-vars-ponyfill",
3+
"version": "0.0.1",
4+
"description": "CSS custom property (aka \"CSS variable\") ponyfill",
5+
"author": "John Hildenbiddle <http://hildenbiddle.com>",
6+
"license": "MIT",
7+
"homepage": "https://github.com/jhildenbiddle/css-vars-ponyfill",
8+
"repository": {
9+
"type": "git",
10+
"url": "git+https://jhildenbiddle@github.com/jhildenbiddle/css-vars-ponyfill.git"
11+
},
12+
"bugs": {
13+
"url": "https://github.com/jhildenbiddle/css-vars-ponyfill/issues"
14+
},
15+
"keywords": [
16+
"css",
17+
"custom",
18+
"es6",
19+
"javascript",
20+
"js",
21+
"module",
22+
"polyfill",
23+
"ponyfill",
24+
"properties",
25+
"variables"
26+
],
27+
"browserslist": [
28+
"ie >= 9"
29+
],
30+
"main": "dist/css-vars-ponyfill.js",
31+
"module": "dist/css-vars-ponyfill.esm.js",
32+
"unpkg": "dist/css-vars-ponyfill.min.js",
33+
"scripts": {
34+
"prepare": "rimraf dist/* && npm run build",
35+
"build": "rollup -c",
36+
"start": "rimraf coverage/* dist/* && npm run build -- --watch",
37+
"test": "rimraf coverage/* && karma start",
38+
"test-watch": "npm test -- --auto-watch --no-single-run",
39+
"test-remote": "npm test -- --remote",
40+
"preversion": "npm test",
41+
"version": "npm run build && git add -A dist"
42+
},
43+
"devDependencies": {
44+
"axios": "^0.17.1",
45+
"babel-core": "^6.26.0",
46+
"babel-loader": "^7.1.2",
47+
"babel-plugin-external-helpers": "^6.22.0",
48+
"babel-plugin-istanbul": "^4.1.5",
49+
"babel-polyfill": "^6.26.0",
50+
"babel-preset-env": "^1.6.1",
51+
"chai": "^4.1.2",
52+
"create-elms": "^1.0.4",
53+
"eslint": "^4.12.1",
54+
"eslint-plugin-chai-expect": "^1.1.1",
55+
"eslint-plugin-mocha": "^4.11.0",
56+
"get-css-data": "^1.1.0",
57+
"karma": "^2.0.0",
58+
"karma-chai": "^0.1.0",
59+
"karma-chrome-launcher": "^2.2.0",
60+
"karma-coverage": "^1.1.1",
61+
"karma-eslint": "^2.2.0",
62+
"karma-mocha": "^1.3.0",
63+
"karma-mocha-reporter": "^2.2.5",
64+
"karma-sauce-launcher": "^1.2.0",
65+
"karma-sourcemap-loader": "^0.3.7",
66+
"karma-webpack": "^2.0.6",
67+
"lodash.merge": "^4.6.0",
68+
"mocha": "^5.0.0",
69+
"rimraf": "^2.6.2",
70+
"rollup": "^0.54.1",
71+
"rollup-plugin-babel": "^3.0.2",
72+
"rollup-plugin-commonjs": "^8.2.6",
73+
"rollup-plugin-eslint": "^4.0.0",
74+
"rollup-plugin-json": "^2.3.0",
75+
"rollup-plugin-node-resolve": "^3.0.0",
76+
"rollup-plugin-uglify": "^2.0.1",
77+
"uglify-es": "^3.3.4",
78+
"webpack": "^3.10.0"
79+
}
80+
}

‎rollup.config.js

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Dependencies
2+
// =============================================================================
3+
import babel from 'rollup-plugin-babel';
4+
import commonjs from 'rollup-plugin-commonjs';
5+
import eslint from 'rollup-plugin-eslint';
6+
import json from 'rollup-plugin-json';
7+
import merge from 'lodash.merge';
8+
import pkg from './package.json';
9+
import resolve from 'rollup-plugin-node-resolve';
10+
import uglify from 'rollup-plugin-uglify';
11+
import { minify } from 'uglify-es';
12+
13+
const path = require('path');
14+
const entryFile = path.resolve(__dirname, 'src', 'index.js');
15+
const fnName = 'cssVars';
16+
17+
18+
// Constants & Variables
19+
// =============================================================================
20+
const bannerData = [
21+
`${pkg.name}`,
22+
`v${pkg.version}`,
23+
`${pkg.homepage}`,
24+
`(c) ${(new Date()).getFullYear()} ${pkg.author}`,
25+
`${pkg.license} license`
26+
];
27+
const settings = {
28+
eslint: {
29+
exclude : ['node_modules/**', './package.json'],
30+
throwOnWarning: false,
31+
throwOnError : true
32+
},
33+
babel: {
34+
exclude: ['node_modules/**'],
35+
presets: [
36+
['env', {
37+
modules: false,
38+
targets: {
39+
browsers: ['ie >= 9']
40+
}
41+
}]
42+
],
43+
plugins: [
44+
'external-helpers'
45+
]
46+
},
47+
uglify: {
48+
beautify: {
49+
compress: false,
50+
mangle : false,
51+
output: {
52+
beautify: true,
53+
comments: /(?:^!|@(?:license|preserve))/
54+
}
55+
},
56+
minify: {
57+
compress: true,
58+
mangle : true,
59+
output : {
60+
comments: /^!/
61+
}
62+
}
63+
}
64+
};
65+
66+
67+
// Config
68+
// =============================================================================
69+
// Base
70+
const config = {
71+
input : entryFile,
72+
output: {
73+
file : path.resolve(__dirname, 'dist', `${pkg.name}.js`),
74+
name : fnName,
75+
banner : `/*!\n * ${ bannerData.join('\n * ') }\n */`,
76+
sourcemap: true
77+
},
78+
plugins: [
79+
resolve(),
80+
commonjs(),
81+
json(),
82+
eslint(settings.eslint),
83+
babel(settings.babel)
84+
],
85+
watch: {
86+
clearScreen: false
87+
}
88+
};
89+
90+
// Output
91+
// -----------------------------------------------------------------------------
92+
// ES Module
93+
const esm = merge({}, config, {
94+
output: {
95+
file : config.output.file.replace(/\.js$/, '.esm.js'),
96+
format: 'es'
97+
},
98+
plugins: [
99+
uglify(settings.uglify.beautify, minify)
100+
]
101+
});
102+
103+
// ES Module (Minified)
104+
const esmMinified = merge({}, config, {
105+
output: {
106+
file : esm.output.file.replace(/\.js$/, '.min.js'),
107+
format: esm.output.format
108+
},
109+
plugins: [
110+
uglify(settings.uglify.minify, minify)
111+
]
112+
});
113+
114+
// UMD
115+
const umd = merge({}, config, {
116+
output: {
117+
format: 'umd'
118+
},
119+
plugins: [
120+
uglify(settings.uglify.beautify, minify)
121+
]
122+
});
123+
124+
// UMD (Minified)
125+
const umdMinified = merge({}, config, {
126+
output: {
127+
file : umd.output.file.replace(/\.js$/, '.min.js'),
128+
format: umd.output.format
129+
},
130+
plugins: [
131+
uglify(settings.uglify.minify, minify)
132+
]
133+
});
134+
135+
136+
// Exports
137+
// =============================================================================
138+
export default [
139+
esm,
140+
esmMinified,
141+
umd,
142+
umdMinified
143+
];

‎saucelabs.config.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Export
2+
// =============================================================================
3+
module.exports = {
4+
username : null,
5+
accessKey: null
6+
};

‎src/index.js

+249
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
// Dependencies
2+
// =============================================================================
3+
import getCssData from 'get-css-data';
4+
import mergeDeep from './merge-deep';
5+
import transformCss from './transform-css';
6+
import { name as pkgName } from '../package.json';
7+
8+
9+
// Constants & Variables
10+
// =============================================================================
11+
const defaults = {
12+
// Sources
13+
include : 'style,link[rel=stylesheet]',
14+
exclude : '',
15+
// Options
16+
appendStyle: true, // cssVars
17+
onlyLegacy : true, // cssVars
18+
onlyVars : true, // cssVars, transformCss
19+
preserve : true, // transformCss
20+
silent : false, // cssVars
21+
variables : {}, // transformCss
22+
// Callbacks
23+
onSuccess() {}, // cssVars
24+
onError() {}, // cssVars
25+
onWarning() {}, // transformCss
26+
onComplete() {} // cssVars
27+
};
28+
// Regex: CSS variable :root declarations and var() function values
29+
const reCssVars = /(?:(?::root\s*{\s*[^;]*;*\s*)|(?:var\(\s*))(--[^:)]+)(?:\s*[:)])/;
30+
31+
32+
33+
34+
// Functions
35+
// =============================================================================
36+
/**
37+
* Description
38+
*
39+
* @preserve
40+
* @param {object} [options] Options object
41+
* @param {string} [options.include="style,link[rel=stylesheet]"] CSS selector
42+
* matching <link> and <style> nodes to include
43+
* @param {string} [options.exclude=""] CSS selector matching <link> and
44+
* <style> nodes to exclude
45+
* @param {boolean} [options.appendStyle=true] Append <style> node containing
46+
* updated CSS to DOM
47+
* @param {boolean} [options.onlyLegacy=true] Only process CSS variables in
48+
* browsers that lack native support
49+
* @param {boolean} [options.onlyVars=true] Remove declarations that do not
50+
* contain a CSS variable from the return value. Note that
51+
* font-face and keyframe rules require all declarations to be
52+
* returned if a CSS variable is used.
53+
* @param {boolean} [options.preserve=true] Preserve CSS variable definitions
54+
* and functions in the return value, allowing "live" variable
55+
* updates via JavaScript to continue working in browsers with
56+
* native CSS variable support.
57+
* @param {boolean} [options.silent=false] Prevent console warnign and error
58+
* messages
59+
* @param {object} [options.variables={}] CSS variable definitions to include
60+
* during transformation. Can be used to add new override
61+
* exisitng definitions.
62+
* @param {function} [options.onSuccess] Callback after all stylesheets have
63+
* been processed succesfully. Passes 1) a CSS string with CSS
64+
* variable values resolved as an argument. Modifying the CSS
65+
* appended when 'appendStyle' is 'true' can be done by
66+
* returning a string value from this funtion (or 'false' to
67+
* skip).
68+
* @param {function} [options.onError] Callback on each error. Passes 1) an
69+
* error message, and 2) source node reference as arguments.
70+
* @param {function} [options.onWarning] Callback on each warning. Passes 1) a
71+
* warning message as an argument.
72+
* @param {function} [options.onComplete] Callback after all stylesheets have
73+
* been processed succesfully and <style> node containing
74+
* updated CSS has been appended to the DOM (based on
75+
* 'appendStyle' setting. Passes 1) a CSS string with CSS
76+
* variable values resolved, and 2) a reference to the
77+
* appended <style> node.
78+
*
79+
* @example
80+
*
81+
* cssVars({
82+
* include : 'style,link[rel="stylesheet"]', // default
83+
* exclude : '',
84+
* appendStyle: true, // default
85+
* onlyLegacy : true, // default
86+
* onlyVars : true, // default
87+
* preserve : true, // default
88+
* silent : false, // default
89+
* onError(message, node) {
90+
* // ...
91+
* },
92+
* onWarning(message) {
93+
* // ...
94+
* },
95+
* onSuccess(cssText) {
96+
* // ...
97+
* },
98+
* onComplete(cssText, styleNode) {
99+
* // ...
100+
* }
101+
* });
102+
*/
103+
function cssVars(options = {}) {
104+
const settings = mergeDeep(defaults, options);
105+
106+
function handleError(message, sourceNode) {
107+
/* istanbul ignore next */
108+
if (!settings.silent) {
109+
// eslint-disable-next-line
110+
console.error(`${message}\n`, sourceNode);
111+
}
112+
113+
settings.onError(message, sourceNode);
114+
}
115+
116+
function handleWarning(message) {
117+
/* istanbul ignore next */
118+
if (!settings.silent) {
119+
// eslint-disable-next-line
120+
console.warn(message);
121+
}
122+
123+
settings.onWarning(message);
124+
}
125+
126+
// Verify readyState to ensure all <link> and <style> nodes are available
127+
if (document.readyState !== 'loading') {
128+
const hasNativeSupport = window.CSS && window.CSS.supports && window.CSS.supports('(--a: 0)');
129+
130+
// Lacks native support or onlyLegacy 'false'
131+
if (!hasNativeSupport || !settings.onlyLegacy) {
132+
const styleNodeId = pkgName;
133+
134+
getCssData({
135+
include: settings.include,
136+
// Always exclude styleNodeId element, which is the generated
137+
// <style> node containing previously transformed CSS.
138+
exclude: `#${styleNodeId}` + (settings.exclude ? `,${settings.exclude}` : ''),
139+
// This filter does a test on each block of CSS. An additional
140+
// filter is used in the parser to remove individual
141+
// declarations.
142+
filter : settings.onlyVars ? reCssVars : null,
143+
onComplete(cssText, cssArray, nodeArray) {
144+
let styleNode = null;
145+
146+
try {
147+
cssText = transformCss(cssText, {
148+
onlyVars : settings.onlyVars,
149+
preserve : settings.preserve,
150+
variables: settings.variables,
151+
onWarning: handleWarning
152+
});
153+
154+
// Success if an error was not been throw during
155+
// transformation. Store the onSuccess return value,
156+
// which allows modifying cssText before passing to
157+
// onComplete and/or appending to new <style> node.
158+
const returnVal = settings.onSuccess(cssText);
159+
160+
// Set cssText to return value (if provided)
161+
cssText = returnVal === false ? '' : returnVal || cssText;
162+
163+
if (settings.appendStyle) {
164+
styleNode = document.querySelector(`#${styleNodeId}`) || document.createElement('style');
165+
styleNode.setAttribute('id', styleNodeId);
166+
167+
if (styleNode.textContent !== cssText) {
168+
styleNode.textContent = cssText;
169+
}
170+
171+
// Append <style> element to either the <head> or
172+
// <body> based on the position of last stylesheet
173+
// node.
174+
const styleTargetNode = document.querySelector(`body link[rel=stylesheet], body style:not(#${styleNodeId})`) ? document.body : document.head;
175+
const isNewTarget = styleNode.parentNode !== styleTargetNode;
176+
const isLastStyleElm = matchesSelector(styleNode, 'style:last-of-type');
177+
178+
if (isNewTarget || !isLastStyleElm) {
179+
styleTargetNode.appendChild(styleNode);
180+
}
181+
}
182+
}
183+
catch(err) {
184+
let errorThrown = false;
185+
186+
// Iterate cssArray to detect CSS text and node(s)
187+
// responsibile for error.
188+
cssArray.forEach((cssText, i) => {
189+
try {
190+
cssText = transformCss(cssText, settings);
191+
}
192+
catch(err) {
193+
const errorNode = nodeArray[i - 0];
194+
195+
errorThrown = true;
196+
handleError(err.message, errorNode);
197+
}
198+
});
199+
200+
// In the event the error thrown was not due to
201+
// transformCss, handle the original error.
202+
/* istanbul ignore next */
203+
if (!errorThrown) {
204+
handleError(err.message || err);
205+
}
206+
}
207+
208+
settings.onComplete(cssText, styleNode);
209+
},
210+
onError(xhr, node, url) {
211+
const errorMsg = `Unable to process ${url} (${xhr.status} - ${xhr.statusText}}`;
212+
213+
handleError(errorMsg, node);
214+
}
215+
});
216+
}
217+
}
218+
// Delay function until DOMContentLoaded event is fired
219+
/* istanbul ignore next */
220+
else {
221+
document.addEventListener('DOMContentLoaded', function init(evt) {
222+
cssVars(options);
223+
224+
document.removeEventListener('DOMContentLoaded', init);
225+
});
226+
}
227+
}
228+
229+
230+
// Functions (Private)
231+
// =============================================================================
232+
/**
233+
* Ponyfill for native Element.matches method
234+
*
235+
* @param {object} elm - The element to test
236+
* @param {string} selector - The CSS selector to test against
237+
* @returns {boolean}
238+
*/
239+
function matchesSelector(elm, selector) {
240+
/* istanbul ignore next */
241+
const matches = elm.matches || elm.matchesSelector || elm.webkitMatchesSelector || elm.mozMatchesSelector || elm.msMatchesSelector || elm.oMatchesSelector;
242+
243+
return matches.call(elm, selector);
244+
}
245+
246+
247+
// Export
248+
// =============================================================================
249+
export default cssVars;

‎src/merge-deep.js

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Functions
2+
// =============================================================================
3+
/**
4+
* Performs a deep merge of objects and returns new object. Does not modify
5+
* objects (immutable) and merges arrays via concatenation.
6+
*
7+
* @param {...object} objects - Objects to merge
8+
* @returns {object} New object with merged key/values
9+
*/
10+
function mergeDeep(...objects) {
11+
const isObject = obj => obj instanceof Object && obj.constructor === Object;
12+
13+
return objects.reduce((prev, obj) => {
14+
Object.keys(obj).forEach(key => {
15+
const pVal = prev[key];
16+
const oVal = obj[key];
17+
18+
// if (Array.isArray(pVal) && Array.isArray(oVal)) {
19+
// prev[key] = pVal.concat(...oVal);
20+
// }
21+
if (isObject(pVal) && isObject(oVal)) {
22+
prev[key] = mergeDeep(pVal, oVal);
23+
}
24+
else {
25+
prev[key] = oVal;
26+
}
27+
});
28+
29+
return prev;
30+
}, {});
31+
}
32+
33+
34+
// Export
35+
// =============================================================================
36+
export default mergeDeep;

‎src/parse-css.js

+255
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/**
2+
* Based on css parser/compiler by NxChg
3+
* https://github.com/NxtChg/pieces/tree/master/js/css_parser
4+
*/
5+
6+
7+
// Functions
8+
// =============================================================================
9+
/**
10+
* Parses CSS string and generates AST object
11+
*
12+
* @param {string} css The CSS stringt to be converted to an AST
13+
* @returns {object}
14+
*/
15+
function cssParse(css) {
16+
const errors = [];
17+
18+
// Errors
19+
// -------------------------------------------------------------------------
20+
function error(msg) {
21+
throw new Error(`CSS parse error: ${msg}`);
22+
}
23+
24+
// RegEx
25+
// -------------------------------------------------------------------------
26+
// Match regexp and return captures
27+
function match(re) {
28+
const m = re.exec(css);
29+
30+
if (m) {
31+
css = css.slice(m[0].length);
32+
33+
return m;
34+
}
35+
}
36+
37+
function whitespace() {
38+
match(/^\s*/);
39+
}
40+
function open() {
41+
return match(/^{\s*/);
42+
}
43+
function close() {
44+
return match(/^}/);
45+
}
46+
47+
// Comments
48+
// -------------------------------------------------------------------------
49+
function comment() {
50+
whitespace();
51+
52+
if (css[0] !== '/' || css[1] !== '*') { return; }
53+
54+
let i = 2;
55+
while (css[i] && (css[i] !== '*' || css[i + 1] !== '/')) { i++; }
56+
57+
// FIXED
58+
if (!css[i]) { return error('end of comment is missing'); }
59+
60+
const str = css.slice(2, i);
61+
css = css.slice(i + 2);
62+
63+
return { type: 'comment', comment: str };
64+
}
65+
function comments() {
66+
const cmnts = [];
67+
68+
let c;
69+
70+
while ((c = comment())) {
71+
cmnts.push(c);
72+
}
73+
return cmnts;
74+
}
75+
76+
// Selector
77+
// -------------------------------------------------------------------------
78+
function selector() {
79+
whitespace();
80+
while (css[0] === '}') {
81+
error('extra closing bracket');
82+
}
83+
84+
const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/);
85+
86+
if (m)
87+
{ return m[0]
88+
.trim() // remove all comments from selectors
89+
.replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '')
90+
.replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, function(m) {
91+
return m.replace(/,/g, '\u200C');
92+
})
93+
.split(/\s*(?![^(]*\)),\s*/)
94+
.map(function(s) {
95+
return s.replace(/\u200C/g, ',');
96+
}); }
97+
}
98+
99+
// Declarations
100+
// -------------------------------------------------------------------------
101+
function declaration() {
102+
match(/^([;\s]*)+/); // ignore empty declarations + whitespace
103+
104+
const comment_regexp = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g;
105+
106+
let prop = match(/^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/);
107+
if (!prop) { return; }
108+
109+
prop = prop[0].trim();
110+
111+
if (!match(/^:\s*/)) { return error('property missing \':\''); }
112+
113+
// Quotes regex repeats verbatim inside and outside parentheses
114+
const val = match(/^((?:\/\*.*?\*\/|'(?:\\'|.)*?'|"(?:\\"|.)*?"|\((\s*'(?:\\'|.)*?'|"(?:\\"|.)*?"|[^)]*?)\s*\)|[^};])+)/);
115+
116+
const ret = { type: 'declaration', property: prop.replace(comment_regexp, ''), value: val ? val[0].replace(comment_regexp, '').trim() : '' };
117+
118+
match(/^[;\s]*/);
119+
120+
return ret;
121+
}
122+
function declarations() {
123+
if (!open()) { return error('missing \'{\''); }
124+
125+
let d,
126+
decls = comments();
127+
128+
while ((d = declaration())) {
129+
decls.push(d);
130+
decls = decls.concat(comments());
131+
}
132+
133+
if (!close()) { return error('missing \'}\''); }
134+
135+
return decls;
136+
}
137+
138+
// Keyframes
139+
// -------------------------------------------------------------------------
140+
function keyframe() {
141+
whitespace();
142+
143+
const vals = [];
144+
145+
let m;
146+
147+
while ((m = match(/^((\d+\.\d+|\.\d+|\d+)%?|[a-z]+)\s*/))) {
148+
vals.push(m[1]);
149+
match(/^,\s*/);
150+
}
151+
152+
if (vals.length) { return { type: 'keyframe', values: vals, declarations: declarations() }; }
153+
}
154+
function at_keyframes() {
155+
let m = match(/^@([-\w]+)?keyframes\s*/);
156+
157+
if (!m) { return; }
158+
159+
const vendor = m[1];
160+
161+
m = match(/^([-\w]+)\s*/);
162+
if (!m) { return error('@keyframes missing name'); } // identifier
163+
164+
const name = m[1];
165+
166+
if (!open()) { return error('@keyframes missing \'{\''); }
167+
168+
let frame,
169+
frames = comments();
170+
while ((frame = keyframe())) {
171+
frames.push(frame);
172+
frames = frames.concat(comments());
173+
}
174+
175+
if (!close()) { return error('@keyframes missing \'}\''); }
176+
177+
return { type: 'keyframes', name: name, vendor: vendor, keyframes: frames };
178+
}
179+
180+
// @ Rules
181+
// -------------------------------------------------------------------------
182+
function at_page() {
183+
const m = match(/^@page */);
184+
if (m) {
185+
const sel = selector() || [];
186+
return { type: 'page', selectors: sel, declarations: declarations() };
187+
}
188+
}
189+
function at_fontface() {
190+
const m = match(/^@font-face\s*/);
191+
if (m) { return { type: 'font-face', declarations: declarations() }; }
192+
}
193+
function at_supports() {
194+
const m = match(/^@supports *([^{]+)/);
195+
if (m) { return { type: 'supports', supports: m[1].trim(), rules: rules() }; }
196+
}
197+
function at_host() {
198+
const m = match(/^@host\s*/);
199+
if (m) { return { type: 'host', rules: rules() }; }
200+
}
201+
function at_media() {
202+
const m = match(/^@media *([^{]+)/);
203+
if (m) { return { type: 'media', media: m[1].trim(), rules: rules() }; }
204+
}
205+
function at_custom_m() {
206+
const m = match(/^@custom-media\s+(--[^\s]+)\s*([^{;]+);/);
207+
if (m) { return { type: 'custom-media', name: m[1].trim(), media: m[2].trim() }; }
208+
}
209+
function at_document() {
210+
const m = match(/^@([-\w]+)?document *([^{]+)/);
211+
// FIXED
212+
if (m) { return { type: 'document', document: m[2].trim(), vendor: m[1] ? m[1].trim() : null, rules: rules() }; }
213+
}
214+
function at_x() {
215+
const m = match(/^@(import|charset|namespace)\s*([^;]+);/);
216+
if (m) { return { type: m[1], name: m[2].trim() }; }
217+
}
218+
function at_rule() {
219+
whitespace();
220+
if (css[0] === '@') { return at_keyframes() || at_supports() || at_host() || at_media() || at_custom_m() || at_page() || at_document() || at_fontface() || at_x(); }
221+
}
222+
223+
// Rules
224+
// -------------------------------------------------------------------------
225+
function rule() {
226+
const sel = selector() || [];
227+
if (!sel.length) { error('selector missing'); }
228+
229+
const decls = declarations();
230+
231+
return { type: 'rule', selectors: sel, declarations: decls };
232+
}
233+
function rules(core) {
234+
if (!core && !open()) { return error('missing \'{\''); }
235+
236+
let node,
237+
rules = comments();
238+
239+
while (css.length && (core || css[0] !== '}') && (node = at_rule() || rule())) {
240+
rules.push(node);
241+
rules = rules.concat(comments());
242+
}
243+
244+
if (!core && !close()) { return error('missing \'}\''); }
245+
246+
return rules;
247+
}
248+
249+
return { type: 'stylesheet', stylesheet: { rules: rules(true), errors: errors } };
250+
}
251+
252+
253+
// Exports
254+
// =============================================================================
255+
export default cssParse;

‎src/stringify-css.js

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* Based on css parser/compiler by NxChg
3+
* https://github.com/NxtChg/pieces/tree/master/js/css_parser
4+
*/
5+
6+
7+
// Functions
8+
// =============================================================================
9+
/**
10+
* Compiles CSS AST to string
11+
*
12+
* @param {object} tree CSS AST object
13+
* @param {string} [delim=''] CSS rule delimiter
14+
* @param {function} cb Function to be called before each node is processed
15+
* @returns {string}
16+
*/
17+
function stringifyCss(tree, delim = '', cb) {
18+
const renderMethods = {
19+
charset(node) {
20+
return '@charset ' + node.name + ';';
21+
},
22+
comment(node) {
23+
return '';
24+
},
25+
'custom-media'(node) {
26+
return '@custom-media ' + node.name + ' ' + node.media + ';';
27+
},
28+
declaration(node) {
29+
return node.property + ':' + node.value + ';';
30+
},
31+
document(node) {
32+
return '@' + (node.vendor || '') + 'document ' + node.document + '{' + visit(node.rules) + '}';
33+
},
34+
'font-face'(node) {
35+
return '@font-face' + '{' + visit(node.declarations) + '}';
36+
},
37+
host(node) {
38+
return '@host' + '{' + visit(node.rules) + '}';
39+
},
40+
import(node) {
41+
// FIXED
42+
return '@import ' + node.name + ';';
43+
},
44+
keyframe(node) {
45+
return node.values.join(',') + '{' + visit(node.declarations) + '}';
46+
},
47+
keyframes(node) {
48+
return '@' + (node.vendor || '') + 'keyframes ' + node.name + '{' + visit(node.keyframes) + '}';
49+
},
50+
media(node) {
51+
return '@media ' + node.media + '{' + visit(node.rules) + '}';
52+
},
53+
namespace(node) {
54+
return '@namespace ' + node.name + ';';
55+
},
56+
page(node) {
57+
return '@page ' + (node.selectors.length ? node.selectors.join(', ') : '') + '{' + visit(node.declarations) + '}';
58+
},
59+
rule(node) {
60+
const decls = node.declarations;
61+
62+
if (decls.length) {
63+
return node.selectors.join(',') + '{' + visit(decls) + '}';
64+
}
65+
},
66+
supports(node) {
67+
// FIXED
68+
return '@supports ' + node.supports + '{' + visit(node.rules) + '}';
69+
}
70+
};
71+
72+
function visit(nodes) {
73+
let buf = '';
74+
75+
for (let i = 0; i < nodes.length; i++) {
76+
const n = nodes[i];
77+
78+
if (cb) {
79+
cb(n);
80+
}
81+
82+
const txt = renderMethods[n.type](n);
83+
84+
if (txt) {
85+
buf += txt;
86+
87+
if (txt.length && n.selectors) {
88+
buf += delim;
89+
}
90+
}
91+
}
92+
93+
return buf;
94+
}
95+
96+
return visit(tree.stylesheet.rules);
97+
}
98+
99+
100+
// Exports
101+
// =============================================================================
102+
export default stringifyCss;

‎src/transform-css.js

+314
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
/**
2+
* Based on rework-vars by reworkcss
3+
* https://github.com/reworkcss/rework-vars
4+
*/
5+
6+
7+
// Dependencies
8+
// =============================================================================
9+
import balanced from 'balanced-match';
10+
import mergeDeep from './merge-deep';
11+
import parseCss from './parse-css';
12+
import stringifyCss from './stringify-css';
13+
14+
15+
// Constants & Variables
16+
// =============================================================================
17+
const VAR_PROP_IDENTIFIER = '--';
18+
const VAR_FUNC_IDENTIFIER = 'var';
19+
const reVarProp = /^--/;
20+
const reVarVal = /^var(.*)/;
21+
22+
23+
// Functions
24+
// =============================================================================
25+
/**
26+
* Transforms W3C-style CSS variables to static values and returns an updated
27+
* CSS string.
28+
*
29+
* @param {object} cssText CSS containing variable definitions and functions
30+
* @param {object} [options] Options object
31+
* @param {boolean} [options.onlyVars=true] Remove declarations that do not
32+
* contain a CSS variable from the return value. Note that
33+
* @font-face and @keyframe rules require all declarations to
34+
* be returned if a CSS variable is used.
35+
* @param {boolean} [options.preserve=true] Preserve CSS variable definitions
36+
* and functions in the return value, allowing "live" variable
37+
* updates via JavaScript to continue working in browsers with
38+
* native CSS variable support.
39+
* @param {object} [options.variables={}] CSS variable definitions to include
40+
* during transformation. Can be used to add new override
41+
* exisitng definitions.
42+
* @param {function} [options.onWarning] Callback on each transformation
43+
* warning. Passes 1) warningMessage as an argument.
44+
* @returns {string}
45+
*/
46+
function transformVars(cssText, options = {}) {
47+
const defaults = {
48+
onlyVars : true,
49+
preserve : true,
50+
variables: {},
51+
onWarning() {}
52+
};
53+
const map = {};
54+
const settings = mergeDeep(defaults, options);
55+
56+
// Convert cssText to AST (this could throw errors)
57+
const cssTree = parseCss(cssText);
58+
59+
// Remove non-vars
60+
if (settings.onlyVars) {
61+
cssTree.stylesheet.rules = filterVars(cssTree.stylesheet.rules);
62+
}
63+
64+
// Define variables
65+
cssTree.stylesheet.rules.forEach(function(rule) {
66+
const varNameIndices = [];
67+
68+
if (rule.type !== 'rule') {
69+
return;
70+
}
71+
72+
// only variables declared for `:root` are supported
73+
if (rule.selectors.length !== 1 || rule.selectors[0] !== ':root') {
74+
return;
75+
}
76+
77+
rule.declarations.forEach(function(decl, i) {
78+
const prop = decl.property;
79+
const value = decl.value;
80+
81+
if (prop && prop.indexOf(VAR_PROP_IDENTIFIER) === 0) {
82+
map[prop] = value;
83+
varNameIndices.push(i);
84+
}
85+
});
86+
87+
// optionally remove `--*` properties from the rule
88+
if (!settings.preserve) {
89+
for (let i = varNameIndices.length - 1; i >= 0; i--) {
90+
rule.declarations.splice(varNameIndices[i], 1);
91+
}
92+
}
93+
});
94+
95+
// Handle variables defined in settings.variables
96+
if (Object.keys(settings.variables).length) {
97+
const newRule = {
98+
declarations: [],
99+
selectors : [':root'],
100+
type : 'rule'
101+
};
102+
103+
Object.keys(settings.variables).forEach(function(key) {
104+
// Normalize variables by ensuring all start with leading '--'
105+
const varName = `--${key.replace(/^-+/, '')}`;
106+
const varValue = settings.variables[key];
107+
108+
// Update internal map value with settings.variables value
109+
map[varName] = varValue;
110+
111+
// Add new declaration to newRule
112+
newRule.declarations.push({
113+
type : 'declaration',
114+
property: varName,
115+
value : varValue
116+
});
117+
});
118+
119+
// Append new :root ruleset
120+
if (settings.preserve) {
121+
cssTree.stylesheet.rules.push(newRule);
122+
}
123+
}
124+
125+
// Resolve variables
126+
walkCss(cssTree.stylesheet, function(declarations, node) {
127+
let decl;
128+
let resolvedValue;
129+
let value;
130+
131+
for (let i = 0; i < declarations.length; i++) {
132+
decl = declarations[i];
133+
value = decl.value;
134+
135+
// skip comments
136+
if (decl.type !== 'declaration') {
137+
continue;
138+
}
139+
140+
// skip values that don't contain variable functions
141+
if (!value || value.indexOf(VAR_FUNC_IDENTIFIER + '(') === -1) {
142+
continue;
143+
}
144+
145+
resolvedValue = resolveValue(value, map, settings);
146+
147+
if (resolvedValue !== 'undefined') {
148+
if (!settings.preserve) {
149+
decl.value = resolvedValue;
150+
}
151+
else {
152+
declarations.splice(i, 0, {
153+
type : decl.type,
154+
property: decl.property,
155+
value : resolvedValue
156+
});
157+
158+
// skip ahead of preserved declaration
159+
i++;
160+
}
161+
}
162+
}
163+
});
164+
165+
// Return CSS string
166+
return stringifyCss(cssTree);
167+
}
168+
169+
170+
// Functions (Private)
171+
// =============================================================================
172+
/**
173+
* Filters rules recursively, retaining only declarations that contain either a
174+
* CSS variable definition (property) or function (value). Maintains all
175+
* declarations for @font-face and @keyframes rules that contain a CSS
176+
* definition or function.
177+
*
178+
* @param {array} rules
179+
* @returns {array}
180+
*/
181+
function filterVars(rules) {
182+
return rules.filter(rule => {
183+
// Rule, @font-face, @host, @page
184+
if (rule.declarations) {
185+
// @font-face rules require all declarations to be retained if any
186+
// declaration contains a CSS variable definition or value.
187+
// For other rules, any declaration that does not contain a CSS
188+
// variable can be removed.
189+
let declArray = rule.type === 'font-face' ? [] : rule.declarations;
190+
191+
declArray = rule.declarations.filter(d => reVarProp.test(d.property) || reVarVal.test(d.value));
192+
193+
return Boolean(declArray.length);
194+
}
195+
// @keyframes
196+
else if (rule.keyframes) {
197+
// @keyframe rules require all declarations to be retained if any
198+
// declaration contains a CSS variable definition or value.
199+
return Boolean(rule.keyframes.filter(k =>
200+
Boolean(k.declarations.filter(d => reVarProp.test(d.property) || reVarVal.test(d.value)).length)
201+
).length);
202+
}
203+
// @document, @media, @supports
204+
else if (rule.rules) {
205+
rule.rules = filterVars(rule.rules).filter(r => r.declarations.length);
206+
207+
return Boolean(rule.rules.length);
208+
}
209+
210+
return true;
211+
});
212+
}
213+
214+
/**
215+
* Resolve CSS variables in a value
216+
*
217+
* The second argument to a CSS variable function, if provided, is a fallback
218+
* value, which is used as the substitution value when the referenced variable
219+
* is invalid.
220+
*
221+
* var(name[, fallback])
222+
*
223+
* @param {string} value A property value containing a CSS variable function
224+
* @param {object} map A map of variable names and values
225+
* @param {object} settings Settings object passed from transformVars()
226+
* @return {string} A new value with CSS variables substituted or using fallback
227+
*/
228+
function resolveValue(value, map, settings) {
229+
// matches `name[, fallback]`, captures 'name' and 'fallback'
230+
const RE_VAR = /([\w-]+)(?:\s*,\s*)?(.*)?/;
231+
const balancedParens = balanced('(', ')', value);
232+
const varStartIndex = value.indexOf('var(');
233+
const varRef = balanced('(', ')', value.substring(varStartIndex)).body;
234+
const warningIntro = 'CSS transform warning:';
235+
236+
/* istanbul ignore next */
237+
if (!balancedParens) {
238+
settings.onWarning(`${warningIntro} missing closing ")" in the value "${value}"`);
239+
}
240+
241+
/* istanbul ignore next */
242+
if (varRef === '') {
243+
settings.onWarning(`${warningIntro} var() must contain a non-whitespace string`);
244+
}
245+
246+
const varFunc = VAR_FUNC_IDENTIFIER + '(' + varRef + ')';
247+
248+
const varResult = varRef.replace(RE_VAR, function(_, name, fallback) {
249+
const replacement = map[name];
250+
251+
if (!replacement && !fallback) {
252+
settings.onWarning(`${warningIntro} variable "${name}" is undefined`);
253+
}
254+
255+
if (!replacement && fallback) {
256+
return fallback;
257+
}
258+
259+
return replacement;
260+
});
261+
262+
// resolve the variable
263+
value = value.split(varFunc).join(varResult);
264+
265+
// recursively resolve any remaining variables in the value
266+
if (value.indexOf(VAR_FUNC_IDENTIFIER) !== -1) {
267+
value = resolveValue(value, map, settings);
268+
}
269+
270+
return value;
271+
}
272+
273+
/**
274+
* Visit `node` declarations recursively and invoke `fn(declarations, node)`.
275+
*
276+
* Based on rework-visit by reworkcss
277+
* https://github.com/reworkcss/rework-visit
278+
*
279+
* @param {object} node
280+
* @param {function} fn
281+
*/
282+
function walkCss(node, fn){
283+
node.rules.forEach(function(rule){
284+
// @media etc
285+
if (rule.rules) {
286+
walkCss(rule, fn);
287+
288+
return;
289+
}
290+
291+
// keyframes
292+
if (rule.keyframes) {
293+
rule.keyframes.forEach(function(keyframe){
294+
if (keyframe.type === 'keyframe') {
295+
fn(keyframe.declarations, rule);
296+
}
297+
});
298+
299+
return;
300+
}
301+
302+
// @charset, @import etc
303+
if (!rule.declarations) {
304+
return;
305+
}
306+
307+
fn(rule.declarations, node);
308+
});
309+
}
310+
311+
312+
// Exports
313+
// =============================================================================
314+
export default transformVars;

‎tests/.eslintrc.js

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Cascading config (merges with parent config)
2+
// http://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
3+
module.exports = {
4+
"env": {
5+
"mocha": true
6+
},
7+
"plugins": [
8+
"chai-expect",
9+
"mocha"
10+
],
11+
"parserOptions": {
12+
"ecmaVersion": 8,
13+
},
14+
"rules": {
15+
"mocha/no-global-tests" : ["error"],
16+
"mocha/no-identical-title": ["error"],
17+
"mocha/no-mocha-arrows" : ["error"],
18+
"no-console" : "off",
19+
}
20+
};

‎tests/css-vars.test.js

+454
Large diffs are not rendered by default.

‎tests/fixtures/test-declaration.css

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:root {
2+
--color: red;
3+
}

‎tests/fixtures/test-onerror.css

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
:root { --error: red;

‎tests/fixtures/test-parse.css

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/* spacing */
2+
a{height:0}
3+
a{height:0;}
4+
a { height: 0; }
5+
a { height : 0 ; }
6+
a {
7+
height : 0;
8+
}
9+
a
10+
{
11+
height
12+
:
13+
0
14+
;
15+
}
16+
17+
/* selectors */
18+
a,b,c{height:0;}
19+
a, b, c { height: 0; }
20+
a, b,
21+
c
22+
{ height: 0; }
23+
24+
/* quotes */
25+
a {
26+
background: url('path;to;file?a=1&b=2') 50% 50% no-repeat;
27+
}
28+
a {
29+
background: url("path;to;file?a=1&b=2") 50% 50% no-repeat;
30+
}
31+
32+
/* comma-separated */
33+
a[bar="foo,bar"] {
34+
height: 0;
35+
}
36+
a:first-child(.foo,.bar) {
37+
height: 0;
38+
}
39+
40+
/* comments */
41+
a {
42+
margin/*123*/: 1px/*\**/ 2px /*45*/3px\9;
43+
}
44+
45+
/* variables */
46+
:root { --color: red; }
47+
a { color: var(--color); }
48+
49+
/* misc */
50+
foo bar baz {
51+
bizz-buzz: word "ferrets";
52+
*even: 'ie boo'
53+
}
54+
.wtf {
55+
*overflow-x: hidden;
56+
//max-height: 110px;
57+
#height: 18px;
58+
}
59+
60+
/* @charset */
61+
@charset "UTF-8";
62+
63+
/* @custom-media */
64+
@custom-media
65+
--test
66+
(max-width: 1024px)
67+
;
68+
@custom-media --narrow-window (max-width: 1024px);
69+
@custom-media --wide-window screen and (max-width: 1024px);
70+
71+
/* @document */
72+
@document url-prefix() {
73+
.test {
74+
height: 0;
75+
}
76+
}
77+
@-moz-document url-prefix() {
78+
.test1 {
79+
height: 0
80+
}
81+
82+
.test2 {
83+
height: 0;
84+
}
85+
}
86+
87+
/* @font-face */
88+
@font-face {
89+
font-family: "Bitstream Vera Serif Bold";
90+
src: url("http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf");
91+
}
92+
93+
/* @host */
94+
@host {
95+
:scope { height: 0; }
96+
}
97+
98+
/* @import */
99+
@import 'test.css';
100+
@import "test.css" screen, projection;
101+
@import url('test.css');
102+
@import url("test.css") print;
103+
@import url("test.css") projection, tv;
104+
@import url('test.css') screen and (orientation:landscape);
105+
106+
/* @keyframes */
107+
@keyframes test1 {
108+
from { opacity: 0 }
109+
to { opacity: 1; }
110+
}
111+
@-webkit-keyframes test2 {
112+
from { opacity: 0 }
113+
to { opacity: 1; }
114+
}
115+
@keyframes test3 {
116+
0% { opacity: 0 }
117+
30.50% { opacity: 0.3050 }
118+
.68% ,
119+
72%
120+
, 85% { opacity: 0.85 }
121+
100% { opacity: 1 }
122+
}
123+
124+
/* @media */
125+
@media (min-width: 1024px) {
126+
.test { height: 0; }
127+
}
128+
@media screen, print {
129+
.test { height: 0; }
130+
}
131+
132+
/* @namespace */
133+
@namespace svg "http://www.w3.org/2000/svg";
134+
@namespace
135+
"http://www.w3.org/1999/xhtml"
136+
;
137+
138+
/* @page */
139+
@page {
140+
margin: 1cm;
141+
}
142+
@page :first {
143+
margin: 1cm;
144+
}
145+
@page toc, index:blank {
146+
margin: 1cm;
147+
}
148+
@page
149+
toc
150+
,
151+
index:blank
152+
{
153+
margin: 1cm
154+
}
155+
156+
/* @supports */
157+
@supports (display: flex) {
158+
a {
159+
height: 0;
160+
}
161+
}
162+
@supports (display: flex) or (display: box) {
163+
a {
164+
height: 0;
165+
}
166+
}
167+
168+
/* Escaped: Will be remove */
169+
.\3A \`\({}
170+
.\3A \`\({}
171+
.\31 a2b3c{}
172+
#\#fake-id{}
173+
#\---{}
174+
#-a-b-c-{}
175+
#©{}
176+
177+
/* Escaped: Retained (http://mathiasbynens.be/demo/html5-id) */
178+
a[bcd="e\",f"] { height: 0; }
179+
#♥{height:0;}
180+
#©{height:0;}
181+
#“‘’”{height:0;}
182+
#☺☃{height:0;}
183+
#⌘⌥{height:0;}
184+
#𝄞♪♩♫♬{height:0;}
185+
#\?{height:0;}
186+
#\@{height:0;}
187+
#\.{height:0;}
188+
#\3A \){height:0;}
189+
#\3A \`\({height:0;}
190+
#\31 23{height:0;}
191+
#\31 a2b3c{height:0;}
192+
#\<p\>{height:0;}
193+
#\<\>\<\<\<\>\>\<\>{height:0;}
194+
#\+\+\+\+\+\+\+\+\+\+\[\>\+\+\+\+\+\+\+\>\+\+\+\+\+\+\+\+\+\+\>\+\+\+\>\+\<\<\<\<\-\]\>\+\+\.\>\+\.\+\+\+\+\+\+\+\.\.\+\+\+\.\>\+\+\.\<\<\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\.\>\.\+\+\+\.\-\-\-\-\-\-\.\-\-\-\-\-\-\-\-\.\>\+\.\>\.{height:0;}
195+
#\#{height:0;}
196+
#\#\#{height:0;}
197+
#\#\.\#\.\#{height:0;}
198+
#\_{height:0;}
199+
#\.fake\-class{height:0;}
200+
#foo\.bar{height:0;}
201+
#\3A hover{height:0;}
202+
#\3A hover\3A focus\3A active{height:0;}
203+
#\[attr\=value\]{height:0;}
204+
#f\/o\/o{height:0;}
205+
#f\\o\\o{height:0;}
206+
#f\*o\*o{height:0;}
207+
#f\!o\!o{height:0;}
208+
#f\'o\'o{height:0;}
209+
#f\~o\~o{height:0;}
210+
#f\+o\+o{height:0;}

‎tests/fixtures/test-stringify.css

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
a{height:0;}
2+
a{height:0;}
3+
a{height:0;}
4+
a{height:0;}
5+
a{height:0;}
6+
a{height:0;}
7+
8+
a,b,c{height:0;}
9+
a,b,c{height:0;}
10+
a,b,c{height:0;}
11+
12+
a{background:url('path;to;file?a=1&b=2') 50% 50% no-repeat;}
13+
a{background:url("path;to;file?a=1&b=2") 50% 50% no-repeat;}
14+
15+
a[bar="foo,bar"]{height:0;}
16+
a:first-child(.foo,.bar){height:0;}
17+
18+
a{margin:1px 2px 3px\9;}
19+
20+
:root{--color:red;}
21+
a{color:var(--color);}
22+
23+
foo bar baz{bizz-buzz:word "ferrets";*even:'ie boo';}
24+
.wtf{*overflow-x:hidden;//max-height:110px;#height:18px;}
25+
26+
@charset "UTF-8";
27+
28+
@custom-media --test (max-width: 1024px);
29+
@custom-media --narrow-window (max-width: 1024px);
30+
@custom-media --wide-window screen and (max-width: 1024px);
31+
32+
@document url-prefix(){.test{height:0;}}
33+
@-moz-document url-prefix(){.test1{height:0;}.test2{height:0;}}
34+
35+
@font-face{font-family:"Bitstream Vera Serif Bold";src:url("http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf");}
36+
37+
@host{:scope{height:0;}}
38+
39+
@import 'test.css';
40+
@import "test.css" screen, projection;
41+
@import url('test.css');
42+
@import url("test.css") print;
43+
@import url("test.css") projection, tv;
44+
@import url('test.css') screen and (orientation:landscape);
45+
46+
@keyframes test1{from{opacity:0;}to{opacity:1;}}
47+
@-webkit-keyframes test2{from{opacity:0;}to{opacity:1;}}
48+
@keyframes test3{0%{opacity:0;}30.50%{opacity:0.3050;}.68%,72%,85%{opacity:0.85;}100%{opacity:1;}}
49+
50+
@media (min-width: 1024px){.test{height:0;}}
51+
@media screen, print{.test{height:0;}}
52+
53+
@namespace svg "http://www.w3.org/2000/svg";
54+
@namespace "http://www.w3.org/1999/xhtml";
55+
56+
@page {margin:1cm;}
57+
@page :first{margin:1cm;}
58+
@page toc, index:blank{margin:1cm;}
59+
@page toc, index:blank{margin:1cm;}
60+
61+
@supports (display: flex){a{height:0;}}
62+
@supports (display: flex) or (display: box){a{height:0;}}
63+
64+
a[bcd="e\",f"]{height:0;}
65+
#♥{height:0;}
66+
#©{height:0;}
67+
#“‘’”{height:0;}
68+
#☺☃{height:0;}
69+
#⌘⌥{height:0;}
70+
#𝄞♪♩♫♬{height:0;}
71+
#\?{height:0;}
72+
#\@{height:0;}
73+
#\.{height:0;}
74+
#\3A \){height:0;}
75+
#\3A \`\({height:0;}
76+
#\31 23{height:0;}
77+
#\31 a2b3c{height:0;}
78+
#\<p\>{height:0;}
79+
#\<\>\<\<\<\>\>\<\>{height:0;}
80+
#\+\+\+\+\+\+\+\+\+\+\[\>\+\+\+\+\+\+\+\>\+\+\+\+\+\+\+\+\+\+\>\+\+\+\>\+\<\<\<\<\-\]\>\+\+\.\>\+\.\+\+\+\+\+\+\+\.\.\+\+\+\.\>\+\+\.\<\<\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\.\>\.\+\+\+\.\-\-\-\-\-\-\.\-\-\-\-\-\-\-\-\.\>\+\.\>\.{height:0;}
81+
#\#{height:0;}
82+
#\#\#{height:0;}
83+
#\#\.\#\.\#{height:0;}
84+
#\_{height:0;}
85+
#\.fake\-class{height:0;}
86+
#foo\.bar{height:0;}
87+
#\3A hover{height:0;}
88+
#\3A hover\3A focus\3A active{height:0;}
89+
#\[attr\=value\]{height:0;}
90+
#f\/o\/o{height:0;}
91+
#f\\o\\o{height:0;}
92+
#f\*o\*o{height:0;}
93+
#f\!o\!o{height:0;}
94+
#f\'o\'o{height:0;}
95+
#f\~o\~o{height:0;}
96+
#f\+o\+o{height:0;}

‎tests/fixtures/test-value.css

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
p {
2+
color: var(--color);
3+
}

‎tests/helpers/load-fixtures.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Dependencies
2+
// =============================================================================
3+
import axios from 'axios';
4+
5+
6+
// Functions
7+
// =============================================================================
8+
function loadFixtures(options = {}, targetObj = {}) {
9+
const baseUrl = options.base || '';
10+
const urls = Array.isArray(options.urls) ? options.urls : options.urls ? [options.urls] : [];
11+
12+
return new Promise(function(resolve, reject) {
13+
// Load Fixtures
14+
axios.all(urls.map(url => axios.get(`${baseUrl}${url}`)))
15+
.then(axios.spread(function (...responseArr) {
16+
responseArr.forEach((response, i) => {
17+
const key = urls[i];
18+
const val = response.data;
19+
20+
targetObj[key] = val;
21+
});
22+
23+
resolve(targetObj);
24+
}))
25+
.catch(err => {
26+
reject(err);
27+
});
28+
});
29+
}
30+
31+
32+
// Export
33+
// =============================================================================
34+
export default loadFixtures;

‎tests/parse-css.test.js

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Dependencies
2+
// =============================================================================
3+
import loadFixtures from './helpers/load-fixtures';
4+
import parseCss from '../src/parse-css';
5+
import { expect } from 'chai';
6+
7+
8+
// Suite
9+
// =============================================================================
10+
describe('parse-css', function() {
11+
const fixtures = {};
12+
13+
// Hooks
14+
// -------------------------------------------------------------------------
15+
before(async function() {
16+
await loadFixtures({
17+
base: '/base/tests/fixtures/',
18+
urls : ['test-parse.css'],
19+
}, fixtures);
20+
});
21+
22+
// Tests
23+
// -------------------------------------------------------------------------
24+
it('parses CSS to an AST (object)', async function() {
25+
const css = fixtures['test-parse.css'];
26+
const ast = parseCss(css);
27+
28+
expect(ast).to.be.an('object');
29+
expect(ast).to.have.property('type', 'stylesheet');
30+
});
31+
32+
it('throws an error when parsing missing opening bracket', function() {
33+
const css = 'p color: red; }';
34+
const badFn = function() {
35+
parseCss(css);
36+
};
37+
38+
expect(badFn).to.throw(Error, 'missing \'{\'');
39+
});
40+
41+
it('throws an error when parsing missing @rule opening bracket', function() {
42+
const css = '@media screen p color: red; }';
43+
const badFn = function() {
44+
parseCss(css);
45+
};
46+
47+
expect(badFn).to.throw(Error, 'missing \'{\'');
48+
});
49+
50+
it('throws an error when parsing missing closing bracket', function() {
51+
const css = 'p { color: red;';
52+
const badFn = function() {
53+
parseCss(css);
54+
};
55+
56+
expect(badFn).to.throw(Error, 'missing \'}\'');
57+
});
58+
59+
it('throws an error when parsing missing @rule closing bracket', function() {
60+
const css = '@media screen { p { color: red; }';
61+
const badFn = function() {
62+
parseCss(css);
63+
};
64+
65+
expect(badFn).to.throw(Error, 'missing \'}\'');
66+
});
67+
68+
it('throws an error when parsing missing end of comment', function() {
69+
const css = '/* Comment *';
70+
const badFn = function() {
71+
parseCss(css);
72+
};
73+
74+
expect(badFn).to.throw(Error, 'end of comment');
75+
});
76+
77+
it('throws an error when parsing extra closing bracket', function() {
78+
const css = 'p { color: red; }}';
79+
const badFn = function() {
80+
parseCss(css);
81+
};
82+
83+
expect(badFn).to.throw(Error, 'closing bracket');
84+
});
85+
86+
it('throws an error when parsing property missing colon', function() {
87+
const css = 'p { color red; }';
88+
const badFn = function() {
89+
parseCss(css);
90+
};
91+
92+
expect(badFn).to.throw(Error, 'property missing \':\'');
93+
});
94+
95+
it('throws an error when parsing missing selector', function() {
96+
const css = '{ color red; }';
97+
const badFn = function() {
98+
parseCss(css);
99+
};
100+
101+
expect(badFn).to.throw(Error, 'selector missing');
102+
});
103+
104+
it('throws an error when parsing @keyframes with missing name', function() {
105+
const css = '@keyframes { from { opacity: 0; } to { opacity: 1; } }';
106+
const badFn = function() {
107+
parseCss(css);
108+
};
109+
110+
expect(badFn).to.throw(Error, '@keyframes missing name');
111+
});
112+
113+
it('throws an error when parsing @keyframes with missing open bracket', function() {
114+
const css = '@keyframes test from { opacity: 0; } to { opacity: 1; } }';
115+
const badFn = function() {
116+
parseCss(css);
117+
};
118+
119+
expect(badFn).to.throw(Error, '@keyframes missing \'{\'');
120+
});
121+
122+
it('throws an error when parsing @keyframes with missing closing bracket', function() {
123+
const css = '@keyframes test { from { opacity: 0; } to { opacity: 1; }';
124+
const badFn = function() {
125+
parseCss(css);
126+
};
127+
128+
expect(badFn).to.throw(Error, '@keyframes missing \'}\'');
129+
});
130+
});

‎tests/stringify-css.test.js

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Dependencies
2+
// =============================================================================
3+
import loadFixtures from './helpers/load-fixtures';
4+
import parseCss from '../src/parse-css';
5+
import stringifyCss from '../src/stringify-css';
6+
import { expect } from 'chai';
7+
8+
9+
// Suite
10+
// =============================================================================
11+
describe('stringify-css', function() {
12+
const fixtures = {};
13+
14+
// Hooks
15+
// -------------------------------------------------------------------------
16+
before(async function() {
17+
await loadFixtures({
18+
base: '/base/tests/fixtures/',
19+
urls : [
20+
'test-parse.css',
21+
'test-stringify.css'
22+
]
23+
}, fixtures);
24+
});
25+
26+
// Tests
27+
// -------------------------------------------------------------------------
28+
it('converts AST to string', function() {
29+
const cssIn = fixtures['test-parse.css'];
30+
const cssAst = parseCss(cssIn);
31+
const cssOut = stringifyCss(cssAst);
32+
const expectCss = fixtures['test-stringify.css'].replace(/\n/g,'');
33+
34+
expect(cssOut).to.equal(expectCss);
35+
});
36+
37+
it('triggers callback for each node', function() {
38+
const cssIn = 'p { color: red; }';
39+
const cssAst = parseCss(cssIn);
40+
41+
let callbackCount = 0;
42+
43+
stringifyCss(cssAst, '', node => { callbackCount++; });
44+
45+
expect(callbackCount).to.be.above(0);
46+
});
47+
});

‎tests/transform-css.test.js

+326
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
// Dependencies
2+
// =============================================================================
3+
import loadFixtures from './helpers/load-fixtures';
4+
import transformCss from '../src/transform-css';
5+
import { expect } from 'chai';
6+
7+
8+
// Suite
9+
// =============================================================================
10+
describe('transform-css', function() {
11+
const fixtures = {};
12+
13+
// Hooks
14+
// -------------------------------------------------------------------------
15+
before(async function() {
16+
await loadFixtures({
17+
base: '/base/tests/fixtures/',
18+
urls : [
19+
'test-parse.css',
20+
'test-stringify.css'
21+
]
22+
}, fixtures);
23+
});
24+
25+
// Tests: Transforms
26+
// -------------------------------------------------------------------------
27+
describe('Transforms', function() {
28+
it('transforms :root variable', function() {
29+
const cssIn = `
30+
:root { --color: red; }
31+
p { color: var(--color); }
32+
`;
33+
const cssOut = transformCss(cssIn);
34+
const expectCss = `
35+
:root { --color: red; }
36+
p { color: red; color: var(--color); }
37+
`.replace(/\n|\s/g, '');
38+
39+
expect(cssOut).to.equal(expectCss);
40+
});
41+
42+
it('transforms :root variable with comments', function() {
43+
const cssIn = `
44+
:root {
45+
/* 1 */
46+
--color: /* 2 */ red /* 3 */;
47+
/* 4 */
48+
}
49+
p {
50+
/* 5 */
51+
color: /* 6 */ var(--color) /* 7 */;
52+
/* 8 */
53+
}
54+
`;
55+
const cssOut = transformCss(cssIn);
56+
const expectCss = `
57+
:root { --color: red; }
58+
p { color: red; color: var(--color); }
59+
`.replace(/\n|\s/g, '');
60+
61+
expect(cssOut).to.equal(expectCss);
62+
});
63+
64+
it('transforms out-of-order :root variable', function() {
65+
const cssIn = `
66+
p { color: var(--color); }
67+
:root { --color: red; }
68+
`;
69+
const cssOut = transformCss(cssIn);
70+
const expectCss = `
71+
p { color: red; color: var(--color); }
72+
:root { --color: red; }
73+
`.replace(/\n|\s/g, '');
74+
75+
expect(cssOut).to.equal(expectCss);
76+
});
77+
78+
it('transforms chained :root variable', function() {
79+
const cssIn = `
80+
:root { --color1: var(--color2); }
81+
:root { --color2: red; }
82+
p { color: var(--color1); }
83+
`;
84+
const cssOut = transformCss(cssIn);
85+
const expectCss = `
86+
:root { --color1: red; --color1: var(--color2); }
87+
:root { --color2: red; }
88+
p { color: red; color: var(--color1); }
89+
`.replace(/\n|\s/g, '');
90+
91+
expect(cssOut).to.equal(expectCss);
92+
});
93+
94+
it('transforms variable function fallback', function() {
95+
const cssIn = 'p { color: var(--fail, red); }';
96+
const cssOut = transformCss(cssIn);
97+
const expectCss = 'p{color:red;color:var(--fail, red);}';
98+
99+
expect(cssOut).to.equal(expectCss);
100+
});
101+
});
102+
103+
// Tests: Options
104+
// -------------------------------------------------------------------------
105+
describe('Options', function() {
106+
// The 'onlyVars' option is used in this module as well as the index.js
107+
// module. Testing how this options is handled by each module is handled
108+
// in each module's test file.
109+
describe('onlyVars', function() {
110+
it('true (declarations)', function() {
111+
const cssIn = `
112+
/* Comment */
113+
:root { --color: red; }
114+
p { color: var(--color); }
115+
p { color: green; }
116+
`;
117+
const cssOut = transformCss(cssIn, { onlyVars: true }).replace(/\n/g, '');
118+
const expectCss = `
119+
:root{ --color:red; }
120+
p { color: red; color: var(--color); }
121+
`.replace(/\n|\s/g, '');
122+
123+
expect(cssOut).to.equal(expectCss);
124+
});
125+
126+
it('true (@font-face)', function() {
127+
const cssIn = `
128+
:root { --weight: normal; }
129+
@font-face {
130+
font-family: "test1";
131+
font-weight: var(--weight);
132+
}
133+
@font-face {
134+
font-family: "test2";
135+
font-weight: bold;
136+
}
137+
`;
138+
const cssOut = transformCss(cssIn, { onlyVars: true }).replace(/\n/g, '');
139+
const expectCss = `
140+
:root { --weight: normal; }
141+
@font-face {
142+
font-family: "test1";
143+
font-weight: normal;
144+
font-weight: var(--weight);
145+
}
146+
`.replace(/\n|\s/g, '');
147+
148+
expect(cssOut).to.equal(expectCss);
149+
});
150+
151+
it('true (@keyframes)', function() {
152+
const cssIn = `
153+
:root { --color: red; }
154+
@keyframes test1 {
155+
from { color: var(--color); }
156+
to { color: green; }
157+
}
158+
@keyframes test2 {
159+
from { color: red; }
160+
to { color: green; }
161+
}
162+
`;
163+
const cssOut = transformCss(cssIn, { onlyVars: true }).replace(/\n/g, '');
164+
const expectCss = `
165+
:root { --color: red;}
166+
@keyframes test1 {
167+
from { color: red; color: var(--color); }
168+
to { color: green; }
169+
}
170+
`.replace(/\n|\s/g, '').replace(/@keyframes/, '@keyframes ');
171+
172+
expect(cssOut).to.equal(expectCss);
173+
});
174+
175+
it('true (@media)', function() {
176+
const cssIn = `
177+
:root { --color: red; }
178+
@media screen {
179+
p { color: var(--color); }
180+
p { color: green; }
181+
}
182+
`;
183+
const cssOut = transformCss(cssIn, { onlyVars: true }).replace(/\n/g, '');
184+
const expectCss = `
185+
:root { --color: red; }
186+
@media screen {
187+
p { color: red; color: var(--color); }
188+
}
189+
`.replace(/\n|\s/g, '').replace(/@media/, '@media ');
190+
191+
expect(cssOut).to.equal(expectCss);
192+
});
193+
});
194+
195+
describe('preserve', function() {
196+
it('true (default)', function() {
197+
const cssIn = `
198+
:root { --color: red; }
199+
p { color: var(--color); }
200+
`;
201+
const cssOut = transformCss(cssIn).replace(/\n/g, '');
202+
const expectCss = `
203+
:root { --color: red; }
204+
p { color: red; color: var(--color); }
205+
`.replace(/\n|\s/g, '');
206+
207+
expect(cssOut).to.equal(expectCss);
208+
});
209+
210+
it('false', function() {
211+
const cssIn = `
212+
:root { --color: red; --weight: normal; }
213+
p {
214+
color: var(--color);
215+
}
216+
@font-face {
217+
font-family: "test1";
218+
font-weight: var(--weight);
219+
}
220+
@keyframes test1 {
221+
from { color: var(--color); }
222+
to { color: green; }
223+
}
224+
@media screen {
225+
p { color: var(--color); }
226+
}
227+
`;
228+
const cssOut = transformCss(cssIn, { preserve: false }).replace(/\n/g, '');
229+
const expectCss = `
230+
p {
231+
color: red;
232+
}
233+
@font-face {
234+
font-family: "test1";
235+
font-weight: normal;
236+
}
237+
@keyframes test1 {
238+
from { color: red; }
239+
to { color: green; }
240+
}
241+
@media screen {
242+
p { color: red; }
243+
}
244+
`.replace(/\n|\s/g, '').replace(/@keyframes/, '@keyframes ').replace(/@media/, '@media ');
245+
246+
expect(cssOut).to.equal(expectCss);
247+
});
248+
});
249+
250+
describe('variables', function() {
251+
it('No leading --', function() {
252+
const cssIn = ':root{--color1:red}p{color:var(--color1)}p{color:var(--color2)}';
253+
const cssOut = transformCss(cssIn, {
254+
preserve : false,
255+
variables: { color2: 'green' }
256+
}).replace(/\n/g, '');
257+
const expectCss = 'p{color:red;}p{color:green;}';
258+
259+
expect(cssOut).to.equal(expectCss);
260+
});
261+
262+
it('Malformed single -', function() {
263+
const cssIn = ':root{--color1:red}p{color:var(--color1)}p{color:var(--color2)}';
264+
const cssOut = transformCss(cssIn, {
265+
preserve : false,
266+
variables: { '-color2': 'green' }
267+
}).replace(/\n/g, '');
268+
const expectCss = 'p{color:red;}p{color:green;}';
269+
270+
expect(cssOut).to.equal(expectCss);
271+
});
272+
273+
it('Leading --', function() {
274+
const cssIn = ':root{--color1:red}p{color:var(--color1)}p{color:var(--color2)}';
275+
const cssOut = transformCss(cssIn, {
276+
preserve : false,
277+
variables: { '--color2': 'green' }
278+
}).replace(/\n/g, '');
279+
const expectCss = 'p{color:red;}p{color:green;}';
280+
281+
expect(cssOut).to.equal(expectCss);
282+
});
283+
284+
it('Override existing variable', function() {
285+
const cssIn = ':root{--color1:red}p{color:var(--color1)}p{color:var(--color2)}';
286+
const cssOut = transformCss(cssIn, {
287+
preserve : false,
288+
variables: {
289+
'--color1': 'blue',
290+
'--color2': 'green'
291+
}
292+
}).replace(/\n/g, '');
293+
const expectCss = 'p{color:blue;}p{color:green;}';
294+
295+
expect(cssOut).to.equal(expectCss);
296+
});
297+
298+
it('Appends new :root element with vars', function() {
299+
const cssIn = 'p{color:var(--color1)}';
300+
const cssOut = transformCss(cssIn, {
301+
preserve : true,
302+
variables: { color1: 'red' }
303+
}).replace(/\n/g, '');
304+
const expectCss = 'p{color:red;color:var(--color1);}:root{--color1:red;}';
305+
306+
expect(cssOut).to.equal(expectCss);
307+
});
308+
});
309+
});
310+
311+
// Tests: Callbacks
312+
// -------------------------------------------------------------------------
313+
describe('Callbacks', function() {
314+
it('triggers onWarning callback with proper arguments', function() {
315+
let onWarningCount = 0;
316+
317+
transformCss('p { color: var(--fail); }', {
318+
onWarning() {
319+
onWarningCount++;
320+
}
321+
});
322+
323+
expect(onWarningCount).to.equal(1);
324+
});
325+
});
326+
});

0 commit comments

Comments
 (0)
This repository has been archived.