├── docs ├── .nojekyll ├── assets │ └── img │ │ ├── code.svg │ │ ├── npm.svg │ │ ├── twitter.svg │ │ ├── ie.svg │ │ ├── edge.svg │ │ ├── safari.svg │ │ ├── github.svg │ │ ├── chrome.svg │ │ └── firefox.svg ├── sidebar.md ├── index.html └── index.md ├── tests ├── fixtures │ ├── test-empty.css │ ├── test-onerror.css │ ├── test-skip.css │ ├── test-value.css │ ├── test-declaration.css │ ├── a │ │ ├── b │ │ │ └── test-url.css │ │ └── test-url.css │ ├── test-import.css │ ├── 404.html │ ├── test-urls.css │ ├── test-stringify.css │ └── test-parse.css ├── .eslintrc.js ├── stringify-css.test.js ├── helpers │ ├── inject-test-component.js │ └── create-test-elms.js ├── parse-vars.test.js ├── parse-css.test.js └── transform-css.test.js ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── .vscode ├── settings.json └── launch.json ├── .gitignore ├── .markdownlint.json ├── .babelrc ├── server.js ├── LICENSE ├── src ├── index.d.ts ├── walk-css.js ├── parse-vars.js ├── stringify-css.js ├── transform-css.js └── parse-css.js ├── .eslintrc.js ├── package.json ├── rollup.config.js ├── README.md ├── karma.conf.js └── CHANGELOG.md /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/test-empty.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: jhildenbiddle 2 | -------------------------------------------------------------------------------- /tests/fixtures/test-onerror.css: -------------------------------------------------------------------------------- 1 | :root { --error: red; -------------------------------------------------------------------------------- /tests/fixtures/test-skip.css: -------------------------------------------------------------------------------- 1 | p { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug.node.autoAttach": "off" 3 | } -------------------------------------------------------------------------------- /tests/fixtures/test-value.css: -------------------------------------------------------------------------------- 1 | p { 2 | color: var(--color); 3 | } -------------------------------------------------------------------------------- /tests/fixtures/test-declaration.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color: red; 3 | } -------------------------------------------------------------------------------- /tests/fixtures/a/b/test-url.css: -------------------------------------------------------------------------------- 1 | p { 2 | background: url(image.jpg); 3 | } -------------------------------------------------------------------------------- /tests/fixtures/a/test-url.css: -------------------------------------------------------------------------------- 1 | p { 2 | background: url(image.jpg); 3 | } -------------------------------------------------------------------------------- /tests/fixtures/test-import.css: -------------------------------------------------------------------------------- 1 | @import "test-declaration.css"; 2 | @import "test-value.css"; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | coverage 3 | dist 4 | node_modules 5 | 6 | # Files 7 | *.log 8 | 9 | # OS 10 | ._* 11 | .cache 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD001": false, 4 | "MD004": { "style": "consistent" }, 5 | "MD013": false, 6 | "MD033": false, 7 | "MD036": false 8 | } 9 | -------------------------------------------------------------------------------- /docs/assets/img/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules/**"], 3 | "presets": [ 4 | ["@babel/preset-env", { 5 | "targets": "ie >= 9" 6 | }] 7 | ], 8 | "plugins": [ 9 | "@babel/plugin-transform-object-assign", 10 | "transform-custom-element-classes" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tests/fixtures/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Redirect / 404 9 | 10 | 11 |

Redirect / 404

12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/assets/img/npm.svg: -------------------------------------------------------------------------------- 1 | NPM icon -------------------------------------------------------------------------------- /docs/sidebar.md: -------------------------------------------------------------------------------- 1 | 2 | - [Documentation](/) 3 | - [Changelog](changelog) 4 | - **Links** 5 | - [![Github](assets/img/github.svg)Github](https://github.com/jhildenbiddle/css-vars-ponyfill) 6 | - [![NPM](assets/img/npm.svg)NPM](https://www.npmjs.com/package/css-vars-ponyfill) 7 | - [![Twitter](assets/img/twitter.svg)@jhildenbiddle](http://twitter.com/jhildenbiddle) 8 | -------------------------------------------------------------------------------- /tests/fixtures/test-urls.css: -------------------------------------------------------------------------------- 1 | @import "a/test-url.css"; 2 | @import "a/b/test-url.css"; 3 | 4 | :root { 5 | --color: red; 6 | } 7 | 8 | p { 9 | color: var(--color); 10 | background: url(image.jpg); 11 | background: url('image.jpg'); 12 | background: url("image.jpg"); 13 | background: url(image1.jpg) url('image2.jpg') url("image3.jpg"); 14 | background: url(data:image/gif;base64,IMAGEDATA); 15 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "attach", 10 | "name": "Attach to Chrome", 11 | "port": 9333, 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 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 | 'extends': [ 8 | 'plugin:mocha/recommended' 9 | ], 10 | 'parserOptions': { 11 | 'ecmaVersion': 8, 12 | }, 13 | 'plugins': [ 14 | 'chai-expect', 15 | 'mocha' 16 | ], 17 | 'rules': { 18 | 'mocha/no-hooks-for-single-case': ['off'], 19 | 'mocha/no-top-level-hooks' : ['off'], 20 | 'mocha/no-setup-in-describe' : ['off'], 21 | 'no-console' : ['off'] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /docs/assets/img/twitter.svg: -------------------------------------------------------------------------------- 1 | Twitter icon -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | // ============================================================================= 3 | const browserSync = require('browser-sync').create(); 4 | const compression = require('compression'); 5 | 6 | browserSync.init({ 7 | files: [ 8 | './docs/**/*.*' 9 | ], 10 | ghostMode: { 11 | clicks: false, 12 | forms : false, 13 | scroll: false 14 | }, 15 | open: false, 16 | notify: false, 17 | reloadOnRestart: true, 18 | server: { 19 | baseDir: [ 20 | './docs/' 21 | ], 22 | middleware: [ 23 | compression() 24 | ], 25 | routes: { 26 | '/CHANGELOG.md': './CHANGELOG.md' 27 | } 28 | }, 29 | rewriteRules: [ 30 | // Replace CDN URLs with local paths 31 | { 32 | match : /https?.*\/CHANGELOG.md/g, 33 | replace: '/CHANGELOG.md' 34 | } 35 | ] 36 | }); 37 | -------------------------------------------------------------------------------- /docs/assets/img/ie.svg: -------------------------------------------------------------------------------- 1 | Internet Explorer icon -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 John Hildenbiddle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'css-vars-ponyfill' { 2 | export interface CSSVarsPonyfillOptions { 3 | rootElement?: Document|HTMLElement; 4 | shadowDOM?: boolean; 5 | include?: string; 6 | exclude?: string; 7 | variables?: {[key: string]: string}; 8 | onlyLegacy?: boolean; 9 | preserveStatic?: boolean; 10 | preserveVars?: boolean; 11 | silent?: boolean; 12 | updateDOM?: boolean; 13 | updateURLs?: boolean; 14 | watch?: null|boolean; 15 | onBeforeSend?(xhr: XMLHttpRequest, elm: HTMLLinkElement|HTMLStyleElement, url: string): void; 16 | onError?(message: string, elm: HTMLLinkElement|HTMLStyleElement, xhr: XMLHttpRequest, url: string): void; 17 | onWarning?(message: string): void; 18 | onSuccess?(cssText: string, elm: HTMLLinkElement|HTMLStyleElement, url: string): void; 19 | onComplete?(cssText: string, styleElms: HTMLStyleElement[], cssVariables: {[key: string]: string}, benchmark: number): void; 20 | onFinally?(hasChanged: boolean, hasNativeSupport: boolean, benchmark: number): void; 21 | } 22 | 23 | export default function cssVars(options?: CSSVarsPonyfillOptions): void; 24 | } 25 | -------------------------------------------------------------------------------- /src/walk-css.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on rework-visit by reworkcss 3 | * https://github.com/reworkcss/rework-visit 4 | */ 5 | 6 | 7 | // Functions 8 | // ============================================================================= 9 | /** 10 | * Visit `node` declarations recursively and invoke `fn(declarations, node)`. 11 | * 12 | * @param {object} node 13 | * @param {function} fn 14 | */ 15 | function walkCss(node, fn){ 16 | node.rules.forEach(function(rule){ 17 | // @media etc 18 | if (rule.rules) { 19 | walkCss(rule, fn); 20 | 21 | return; 22 | } 23 | 24 | // keyframes 25 | if (rule.keyframes) { 26 | rule.keyframes.forEach(function(keyframe){ 27 | if (keyframe.type === 'keyframe') { 28 | fn(keyframe.declarations, rule); 29 | } 30 | }); 31 | 32 | return; 33 | } 34 | 35 | // @charset, @import etc 36 | if (!rule.declarations) { 37 | return; 38 | } 39 | 40 | fn(rule.declarations, node); 41 | }); 42 | } 43 | 44 | 45 | // Exports 46 | // ============================================================================= 47 | export default walkCss; 48 | -------------------------------------------------------------------------------- /tests/stringify-css.test.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | // ============================================================================= 3 | import parseCss from '../src/parse-css'; 4 | import stringifyCss from '../src/stringify-css'; 5 | import { expect } from 'chai'; 6 | 7 | 8 | // Suite 9 | // ============================================================================= 10 | describe('stringify-css', function() { 11 | const fixtures = window.__FIXTURES__; 12 | 13 | // Tests 14 | // ------------------------------------------------------------------------- 15 | it('converts AST to string', function() { 16 | const cssIn = fixtures['test-parse.css']; 17 | const cssAst = parseCss(cssIn); 18 | const cssOut = stringifyCss(cssAst); 19 | const expectCss = fixtures['test-stringify.css'].replace(/\n/g,''); 20 | 21 | expect(cssOut).to.equal(expectCss); 22 | }); 23 | 24 | it('triggers callback for each node', function() { 25 | const cssIn = 'p { color: red; }'; 26 | const cssAst = parseCss(cssIn); 27 | 28 | let callbackCount = 0; 29 | 30 | stringifyCss(cssAst, '', node => { callbackCount++; }); 31 | 32 | expect(callbackCount).to.be.above(0); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/helpers/inject-test-component.js: -------------------------------------------------------------------------------- 1 | // Export 2 | // ============================================================================= 3 | module.exports = function injectTestComponent() { 4 | class TestComponent extends HTMLElement { 5 | constructor() { 6 | super(); 7 | this.attachShadow({ mode: 'open' }); 8 | } 9 | 10 | connectedCallback() { 11 | this.shadowRoot.innerHTML = ` 12 | 19 | 20 |

${this.getAttribute('data-text')}

21 | `; 22 | } 23 | } 24 | 25 | window.customElements.define('test-component', TestComponent); 26 | 27 | // createElms({ tag: 'style', text: ':root { --test-component-background: green; }', appendTo: 'head' }); 28 | // createElms([ 29 | // { tag: 'test-component', attr: { 'data-text': 'Custom element' }}, 30 | // { tag: 'p', text: 'Standard element' } 31 | // ], { appendTo: 'body' }); 32 | }; 33 | -------------------------------------------------------------------------------- /tests/helpers/create-test-elms.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | // ============================================================================= 3 | import createElms from 'create-elms'; 4 | 5 | 6 | // Functions 7 | // ============================================================================= 8 | function createTestElms(elmsData, sharedOptions = {}, documentObject) { 9 | elmsData = (Array.isArray(elmsData) ? elmsData : [elmsData]).map(elmData => { 10 | if (typeof elmData === 'object') { 11 | const isHeadElm = elmData.tag && ['link', 'style'].indexOf(elmData.tag) !== -1; 12 | 13 | // Set 'appendTo' values based on tag 14 | elmData.appendTo = elmData.appendTo || sharedOptions.appendTo || (isHeadElm ? 'head' : 'body'); 15 | } 16 | 17 | return elmData; 18 | }); 19 | 20 | // If unspecified, append all elements to 21 | sharedOptions.appendTo = sharedOptions.appendTo || 'body'; 22 | 23 | // Set 'data-test' attribute so test elements can easily identified 24 | sharedOptions.attr = Object.assign(sharedOptions.attr || {}, { 'data-test': '' }); 25 | 26 | return createElms(elmsData, sharedOptions, documentObject); 27 | } 28 | 29 | 30 | // Export 31 | // ============================================================================= 32 | export default createTestElms; 33 | -------------------------------------------------------------------------------- /docs/assets/img/edge.svg: -------------------------------------------------------------------------------- 1 | Microsoft Edge icon -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser' : true, 4 | 'commonjs': true, 5 | 'es6' : true, 6 | 'node' : true 7 | }, 8 | 'extends': [ 9 | 'eslint:recommended' 10 | ], 11 | 'ignorePatterns': [ 12 | 'dist' 13 | ], 14 | 'parserOptions': { 15 | 'sourceType': 'module' 16 | }, 17 | 'plugins': [], 18 | 'rules': { 19 | 'array-bracket-spacing' : ['error', 'never'], 20 | 'array-callback-return' : ['error'], 21 | 'block-scoped-var' : ['error'], 22 | 'block-spacing' : ['error', 'always'], 23 | 'curly' : ['error'], 24 | 'dot-notation' : ['error'], 25 | 'eqeqeq' : ['error'], 26 | 'indent' : ['error', 4, {'SwitchCase': 1}], 27 | 'no-console' : ['warn'], 28 | 'no-floating-decimal' : ['error'], 29 | 'no-implicit-coercion' : ['error'], 30 | 'no-implicit-globals' : ['error'], 31 | 'no-loop-func' : ['error'], 32 | 'no-return-assign' : ['error'], 33 | 'no-template-curly-in-string': ['error'], 34 | 'no-unneeded-ternary' : ['error'], 35 | 'no-unused-vars' : ['error', { 'args': 'none' }], 36 | 'no-useless-computed-key' : ['error'], 37 | 'no-useless-return' : ['error'], 38 | 'no-var' : ['error'], 39 | 'prefer-const' : ['error'], 40 | 'quotes' : ['error', 'single'], 41 | 'semi' : ['error', 'always'] 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: 'Build & Test' 2 | 3 | on: [push, pull_request_target] 4 | 5 | jobs: 6 | build: 7 | name: Build (${{ matrix.os }}) 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [macos-latest, ubuntu-latest, windows-latest] 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Install 16 | run: npm ci --ignore-scripts 17 | - name: Lint 18 | run: npm run lint 19 | - name: Build 20 | run: npm run build 21 | 22 | test: 23 | name: Test 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | - name: Install 29 | run: npm ci --ignore-scripts 30 | - name: Build 31 | run: npm run build 32 | - name: Setup BrowserStack environment 33 | uses: browserstack/github-actions/setup-env@master 34 | with: 35 | username: ${{ secrets.BROWSERSTACK_USERNAME }} 36 | access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} 37 | - name: Start BrowserStack tunnel 38 | uses: browserstack/github-actions/setup-local@master 39 | with: 40 | local-testing: start 41 | local-identifier: random 42 | - name: Run tests on BrowserStack 43 | run: npm run test-remote 44 | - name: Stop BrowserStack tunnel 45 | uses: browserstack/github-actions/setup-local@master 46 | with: 47 | local-testing: stop 48 | - name: Report code coverage 49 | uses: codacy/codacy-coverage-reporter-action@v1 50 | with: 51 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 52 | coverage-reports: coverage/lcov.info 53 | -------------------------------------------------------------------------------- /docs/assets/img/safari.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/assets/img/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/parse-vars.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | // ============================================================================= 3 | import parseCss from './parse-css'; 4 | 5 | 6 | // Functions 7 | // ============================================================================= 8 | /** 9 | * Description 10 | * 11 | * @param {object|string} cssData CSS data to parse 12 | * @param {object} [options] Options object 13 | * @param {object} [options.store={}] CSS variable definitions to include during 14 | * transformation. Can be used to add new override exisitng definitions. 15 | * @param {function} [options.onWarning] Callback on each transformation 16 | * warning. Passes 1) warningMessage as an argument. 17 | * @returns {object} 18 | */ 19 | function parseVars(cssData, options = {}) { 20 | const defaults = { 21 | parseHost: false, 22 | store : {}, 23 | onWarning() {} 24 | }; 25 | const settings = Object.assign({}, defaults, options); 26 | const reVarDeclSelectors = new RegExp(`:${ settings.parseHost ? 'host' : 'root' }$`); 27 | 28 | // Convert CSS string to AST 29 | if (typeof cssData === 'string') { 30 | cssData = parseCss(cssData, settings); 31 | } 32 | 33 | // Define variables 34 | cssData.stylesheet.rules.forEach(function(rule) { 35 | const varNameIndices = []; 36 | 37 | if (rule.type !== 'rule' || !rule.selectors.some(s => reVarDeclSelectors.test(s))) { 38 | return; 39 | } 40 | 41 | rule.declarations.forEach(function(decl, i) { 42 | const prop = decl.property; 43 | const value = decl.value; 44 | 45 | if (prop && prop.indexOf('--') === 0) { 46 | settings.store[prop] = value; 47 | varNameIndices.push(i); 48 | } 49 | }); 50 | }); 51 | 52 | // Return variable store 53 | return settings.store; 54 | } 55 | 56 | 57 | // Exports 58 | // ============================================================================= 59 | export default parseVars; 60 | -------------------------------------------------------------------------------- /tests/fixtures/test-stringify.css: -------------------------------------------------------------------------------- 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 | :root{--var-with\.escaped-dot:blue;} 22 | a{color:var(--color);} 23 | a{background:var(--var-with\.escaped-dot);} 24 | 25 | foo bar baz{bizz-buzz:word "ferrets";*even:'ie boo';} 26 | .wtf{*overflow-x:hidden;//max-height:110px;#height:18px;} 27 | 28 | @charset "UTF-8"; 29 | 30 | @custom-media --test (max-width: 1024px); 31 | @custom-media --narrow-window (max-width: 1024px); 32 | @custom-media --wide-window screen and (max-width: 1024px); 33 | 34 | @document url-prefix(){.test{height:0;}} 35 | @-moz-document url-prefix(){.test1{height:0;}.test2{height:0;}} 36 | 37 | @font-face{font-family:"Bitstream Vera Serif Bold";src:url("http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf");} 38 | 39 | @host{:scope{height:0;}} 40 | 41 | @import 'test.css'; 42 | @import "test.css" screen, projection; 43 | @import url('test.css'); 44 | @import url("test.css") print; 45 | @import url("test.css") projection, tv; 46 | @import url('test.css') screen and (orientation:landscape); 47 | 48 | @keyframes test1{from{opacity:0;}to{opacity:1;}} 49 | @-webkit-keyframes test2{from{opacity:0;}to{opacity:1;}} 50 | @keyframes test3{0%{opacity:0;}30.50%{opacity:0.3050;}.68%,72%,85%{opacity:0.85;}100%{opacity:1;}} 51 | 52 | @media (min-width: 1024px){.test{height:0;}} 53 | @media screen, print{.test{height:0;}} 54 | @media {.test{height:0;}} 55 | @media {.test{height:0;}} 56 | 57 | @namespace svg "http://www.w3.org/2000/svg"; 58 | @namespace "http://www.w3.org/1999/xhtml"; 59 | 60 | @page {margin:1cm;} 61 | @page :first{margin:1cm;} 62 | @page toc, index:blank{margin:1cm;} 63 | @page toc, index:blank{margin:1cm;@top-left-corner{content:"Page " counter(page);}} 64 | 65 | @supports (display: flex){a{height:0;}} 66 | @supports (display: flex) or (display: box){a{height:0;}} 67 | 68 | a[bcd="e\",f"]{height:0;} 69 | #♥{height:0;} 70 | #©{height:0;} 71 | #“‘’”{height:0;} 72 | #☺☃{height:0;} 73 | #⌘⌥{height:0;} 74 | #𝄞♪♩♫♬{height:0;} 75 | #\?{height:0;} 76 | #\@{height:0;} 77 | #\.{height:0;} 78 | #\3A \){height:0;} 79 | #\3A \`\({height:0;} 80 | #\31 23{height:0;} 81 | #\31 a2b3c{height:0;} 82 | #\{height:0;} 83 | #\<\>\<\<\<\>\>\<\>{height:0;} 84 | #\+\+\+\+\+\+\+\+\+\+\[\>\+\+\+\+\+\+\+\>\+\+\+\+\+\+\+\+\+\+\>\+\+\+\>\+\<\<\<\<\-\]\>\+\+\.\>\+\.\+\+\+\+\+\+\+\.\.\+\+\+\.\>\+\+\.\<\<\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\.\>\.\+\+\+\.\-\-\-\-\-\-\.\-\-\-\-\-\-\-\-\.\>\+\.\>\.{height:0;} 85 | #\#{height:0;} 86 | #\#\#{height:0;} 87 | #\#\.\#\.\#{height:0;} 88 | #\_{height:0;} 89 | #\.fake\-class{height:0;} 90 | #foo\.bar{height:0;} 91 | #\3A hover{height:0;} 92 | #\3A hover\3A focus\3A active{height:0;} 93 | #\[attr\=value\]{height:0;} 94 | #f\/o\/o{height:0;} 95 | #f\\o\\o{height:0;} 96 | #f\*o\*o{height:0;} 97 | #f\!o\!o{height:0;} 98 | #f\'o\'o{height:0;} 99 | #f\~o\~o{height:0;} 100 | #f\+o\+o{height:0;} -------------------------------------------------------------------------------- /docs/assets/img/chrome.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/stringify-css.js: -------------------------------------------------------------------------------- 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 | // Preserve ponyfill marker comments 24 | return node.comment.indexOf('__CSSVARSPONYFILL') === 0 ? '/*' + node.comment + '*/' : ''; 25 | }, 26 | 'custom-media'(node) { 27 | return '@custom-media ' + node.name + ' ' + node.media + ';'; 28 | }, 29 | declaration(node) { 30 | return node.property + ':' + node.value + ';'; 31 | }, 32 | document(node) { 33 | return '@' + (node.vendor || '') + 'document ' + node.document + '{' + visit(node.rules) + '}'; 34 | }, 35 | 'font-face'(node) { 36 | return '@font-face' + '{' + visit(node.declarations) + '}'; 37 | }, 38 | host(node) { 39 | return '@host' + '{' + visit(node.rules) + '}'; 40 | }, 41 | import(node) { 42 | // FIXED 43 | return '@import ' + node.name + ';'; 44 | }, 45 | keyframe(node) { 46 | return node.values.join(',') + '{' + visit(node.declarations) + '}'; 47 | }, 48 | keyframes(node) { 49 | return '@' + (node.vendor || '') + 'keyframes ' + node.name + '{' + visit(node.keyframes) + '}'; 50 | }, 51 | media(node) { 52 | return '@media ' + node.media + '{' + visit(node.rules) + '}'; 53 | }, 54 | namespace(node) { 55 | return '@namespace ' + node.name + ';'; 56 | }, 57 | page(node) { 58 | return '@page ' + (node.selectors.length ? node.selectors.join(', ') : '') + '{' + visit(node.declarations) + '}'; 59 | }, 60 | 'page-margin-box'(node) { 61 | return '@' + node.name + '{' + visit(node.declarations) + '}'; 62 | }, 63 | rule(node) { 64 | const decls = node.declarations; 65 | 66 | if (decls.length) { 67 | return node.selectors.join(',') + '{' + visit(decls) + '}'; 68 | } 69 | }, 70 | supports(node) { 71 | // FIXED 72 | return '@supports ' + node.supports + '{' + visit(node.rules) + '}'; 73 | } 74 | }; 75 | 76 | function visit(nodes) { 77 | let buf = ''; 78 | 79 | for (let i = 0; i < nodes.length; i++) { 80 | const n = nodes[i]; 81 | 82 | if (cb) { 83 | cb(n); 84 | } 85 | 86 | const txt = renderMethods[n.type](n); 87 | 88 | if (txt) { 89 | buf += txt; 90 | 91 | if (txt.length && n.selectors) { 92 | buf += delim; 93 | } 94 | } 95 | } 96 | 97 | return buf; 98 | } 99 | 100 | return visit(tree.stylesheet.rules); 101 | } 102 | 103 | 104 | // Exports 105 | // ============================================================================= 106 | export default stringifyCss; 107 | -------------------------------------------------------------------------------- /tests/parse-vars.test.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | // ============================================================================= 3 | import parseVars from '../src/parse-vars'; 4 | import { expect } from 'chai'; 5 | 6 | 7 | // Suite 8 | // ============================================================================= 9 | describe('parse-vars', function() { 10 | // Tests: Parsing 11 | // ------------------------------------------------------------------------- 12 | describe('Parsing', function() { 13 | it('parses from single selector', function() { 14 | const css = ':root { --color: red; }'; 15 | const vars = parseVars(css); 16 | 17 | expect(vars).to.eql({ '--color': 'red' }); 18 | }); 19 | 20 | it('parses from comma-separated selector', function() { 21 | const css = ':root, html { --color: red; }'; 22 | const vars = parseVars(css); 23 | 24 | expect(vars).to.eql({ '--color': 'red' }); 25 | }); 26 | }); 27 | 28 | describe('Ignoring', function() { 29 | it('ignores :root with class selector', function() { 30 | const css = ` 31 | :root { --color: red; } 32 | :root.foo { --color: green; } 33 | `; 34 | const vars = parseVars(css); 35 | 36 | expect(vars).to.eql({ '--color': 'red' }); 37 | }); 38 | 39 | it('ignores :root with attribute selector', function() { 40 | const css = ` 41 | :root { --color: red; } 42 | :root[foo="bar"] { --color: green; } 43 | `; 44 | const vars = parseVars(css); 45 | 46 | expect(vars).to.eql({ '--color': 'red' }); 47 | }); 48 | 49 | it('ignores :root with pseudo selector', function() { 50 | const css = ` 51 | :root { --color: red; } 52 | :root:hover { --color: green; } 53 | :root::hover { --color: blue; } 54 | `; 55 | const vars = parseVars(css); 56 | 57 | expect(vars).to.eql({ '--color': 'red' }); 58 | }); 59 | 60 | it('ignores :root with descendant selector', function() { 61 | const css = ` 62 | :root { --color: red; } 63 | :root #foo { --color: green; } 64 | :root .bar { --color: blue; } 65 | `; 66 | const vars = parseVars(css); 67 | 68 | expect(vars).to.eql({ '--color': 'red' }); 69 | }); 70 | 71 | it('ignores :root with child selector', function() { 72 | const css = ` 73 | :root { --color: red; } 74 | :root > #foo { --color: green; } 75 | :root > .bar { --color: blue; } 76 | `; 77 | const vars = parseVars(css); 78 | 79 | expect(vars).to.eql({ '--color': 'red' }); 80 | }); 81 | 82 | it('ignores :root with sibling selector', function() { 83 | const css = ` 84 | :root { --color: red; } 85 | :root ~ #foo { --color: green; } 86 | :root ~ .bar { --color: blue; } 87 | `; 88 | const vars = parseVars(css); 89 | 90 | expect(vars).to.eql({ '--color': 'red' }); 91 | }); 92 | 93 | it('ignores :root with adjacent sibling selector', function() { 94 | const css = ` 95 | :root { --color: red; } 96 | :root + #foo { --color: green; } 97 | :root + .bar { --color: blue; } 98 | `; 99 | const vars = parseVars(css); 100 | 101 | expect(vars).to.eql({ '--color': 'red' }); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | css-vars-ponyfill - Client-side support for CSS custom properties in legacy and modern browsers 10 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 23 | 24 |
25 | 26 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-vars-ponyfill", 3 | "version": "2.4.9", 4 | "description": "Client-side support for CSS custom properties (aka \"CSS variables\") in legacy and modern browsers", 5 | "author": "John Hildenbiddle ", 6 | "license": "MIT", 7 | "homepage": "https://jhildenbiddle.github.io/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 | "client", 17 | "component", 18 | "css", 19 | "custom properties", 20 | "custom property", 21 | "custom", 22 | "dom", 23 | "es6", 24 | "ie", 25 | "ie9", 26 | "ie10", 27 | "ie11", 28 | "internet explorer", 29 | "javascript", 30 | "js", 31 | "legacy", 32 | "module", 33 | "mutation", 34 | "observer", 35 | "polyfill", 36 | "ponyfill", 37 | "properties", 38 | "property", 39 | "runtime", 40 | "shadow", 41 | "variables", 42 | "vars", 43 | "watch", 44 | "web" 45 | ], 46 | "browserslist": [ 47 | "ie >= 9" 48 | ], 49 | "files": [ 50 | "dist" 51 | ], 52 | "main": "dist/css-vars-ponyfill.js", 53 | "module": "dist/css-vars-ponyfill.esm.js", 54 | "unpkg": "dist/css-vars-ponyfill.min.js", 55 | "types": "dist/css-vars-ponyfill.d.ts", 56 | "sideEffects": false, 57 | "scripts": { 58 | "prepare": "rimraf dist/*.js* && npm run build", 59 | "build": "rollup -c", 60 | "escheck": "es-check es5 ./dist/*ponyfill.js && es-check es5 ./dist/*.esm.js --module", 61 | "prebuild": "cpy src/index.d.ts dist --rename=css-vars-ponyfill.d.ts", 62 | "postbuild": "npm run escheck", 63 | "dev": "npm start | npm run test-debug", 64 | "lint": "eslint . && markdownlint . --ignore node_modules", 65 | "serve": "node server.js", 66 | "start": "rimraf coverage/* && npm run serve & npm run build -- --watch", 67 | "test": "npm run lint && karma start", 68 | "test-debug": "npm run test-watch -- --debug", 69 | "test-watch": "npm run test -- --auto-watch --no-single-run", 70 | "test-remote": "npm run test -- --remote", 71 | "version": "npm run build && npm test" 72 | }, 73 | "dependencies": { 74 | "balanced-match": "^1.0.2", 75 | "get-css-data": "^2.0.2" 76 | }, 77 | "devDependencies": { 78 | "@babel/core": "^7.12.7", 79 | "@babel/plugin-transform-object-assign": "^7.12.1", 80 | "@babel/preset-env": "^7.12.7", 81 | "@rollup/plugin-babel": "^5.3.0", 82 | "@rollup/plugin-commonjs": "^22.0.1", 83 | "@rollup/plugin-json": "^4.0.3", 84 | "@rollup/plugin-node-resolve": "^13.3.0", 85 | "@webcomponents/webcomponentsjs": "^2.5.0", 86 | "babel-loader": "^8.2.1", 87 | "babel-plugin-istanbul": "^6.0.0", 88 | "babel-plugin-transform-custom-element-classes": "^0.1.0", 89 | "browser-sync": "^2.26.13", 90 | "chai": "^4.2.0", 91 | "chai-colors": "^1.0.1", 92 | "compression": "^1.7.4", 93 | "cpy-cli": "^4.1.0", 94 | "create-elms": "^1.0.9", 95 | "es-check": "^7.0.0", 96 | "eslint": "^7.32.0", 97 | "eslint-plugin-chai-expect": "^3.0.0", 98 | "eslint-plugin-mocha": "^10.1.0", 99 | "jsdom": "^20.0.0", 100 | "karma": "^6.4.0", 101 | "karma-browserstack-launcher": "^1.6.0", 102 | "karma-chai": "^0.1.0", 103 | "karma-chrome-launcher": "^3.1.0", 104 | "karma-coverage-istanbul-reporter": "^3.0.2", 105 | "karma-eslint": "^2.2.0", 106 | "karma-file-fixtures-preprocessor": "^3.0.1", 107 | "karma-jsdom-launcher": "^13.0.0", 108 | "karma-mocha": "^2.0.1", 109 | "karma-mocha-reporter": "^2.2.5", 110 | "karma-sourcemap-loader": "^0.3.8", 111 | "karma-webpack": "^5.0.0", 112 | "markdownlint-cli": "^0.32.1", 113 | "mergician": "^1.0.2", 114 | "mocha": "^9.2.2", 115 | "rimraf": "^3.0.2", 116 | "rollup": "^2.33.3", 117 | "rollup-plugin-eslint": "^7.0.0", 118 | "rollup-plugin-terser": "^7.0.2", 119 | "webpack": "^5.74.0" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | // ============================================================================= 3 | const path = require('path'); 4 | 5 | import { babel } from '@rollup/plugin-babel'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | import { eslint } from 'rollup-plugin-eslint'; 8 | import json from '@rollup/plugin-json'; 9 | import mergician from 'mergician'; 10 | import pkg from './package.json'; 11 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 12 | import { terser } from 'rollup-plugin-terser'; 13 | 14 | 15 | // Settings 16 | // ============================================================================= 17 | // Copyright 18 | const currentYear = (new Date()).getFullYear(); 19 | const releaseYear = 2018; 20 | 21 | // Output 22 | const entryFile = path.resolve(__dirname, 'src', 'index.js'); 23 | const outputFile = path.resolve(__dirname, 'dist', `${pkg.name}.js`); 24 | const outputName = 'cssVars'; 25 | 26 | // Banner 27 | const bannerData = [ 28 | `${pkg.name}`, 29 | `v${pkg.version}`, 30 | `${pkg.homepage}`, 31 | `(c) ${releaseYear}${currentYear === releaseYear ? '' : '-' + currentYear} ${pkg.author}`, 32 | `${pkg.license} license` 33 | ]; 34 | 35 | // Plugins 36 | const pluginSettings = { 37 | eslint: { 38 | exclude : ['node_modules/**', './package.json'], 39 | throwOnWarning: false, 40 | throwOnError : true 41 | }, 42 | babel: { 43 | // See .babelrc 44 | babelHelpers: 'bundled' 45 | }, 46 | terser: { 47 | beautify: { 48 | compress: false, 49 | mangle : false, 50 | output: { 51 | beautify: true, 52 | comments: /(?:^!|@(?:license|preserve))/ 53 | } 54 | }, 55 | minify: { 56 | compress: true, 57 | mangle : true, 58 | output : { 59 | comments: new RegExp(pkg.name) 60 | } 61 | } 62 | } 63 | }; 64 | 65 | 66 | // Config 67 | // ============================================================================= 68 | // Base 69 | const config = { 70 | input : entryFile, 71 | output: { 72 | file : outputFile, 73 | name : outputName, 74 | banner : `/*!\n * ${ bannerData.join('\n * ') }\n */`, 75 | sourcemap: true 76 | }, 77 | plugins: [ 78 | nodeResolve(), 79 | commonjs(), 80 | json(), 81 | eslint(pluginSettings.eslint), 82 | babel(pluginSettings.babel) 83 | ], 84 | watch: { 85 | clearScreen: false 86 | } 87 | }; 88 | 89 | // Formats 90 | // ----------------------------------------------------------------------------- 91 | // ES Module 92 | const esm = mergician({}, config, { 93 | output: { 94 | file : config.output.file.replace(/\.js$/, '.esm.js'), 95 | format: 'esm' 96 | }, 97 | plugins: config.plugins.concat([ 98 | terser(pluginSettings.terser.beautify) 99 | ]) 100 | }); 101 | 102 | // ES Module (Minified) 103 | const esmMinified = mergician({}, config, { 104 | output: { 105 | file : esm.output.file.replace(/\.js$/, '.min.js'), 106 | format: esm.output.format 107 | }, 108 | plugins: config.plugins.concat([ 109 | terser(pluginSettings.terser.minify) 110 | ]) 111 | }); 112 | 113 | // UMD 114 | const umd = mergician({}, config, { 115 | output: { 116 | format: 'umd' 117 | }, 118 | plugins: config.plugins.concat([ 119 | terser(pluginSettings.terser.beautify) 120 | ]) 121 | }); 122 | 123 | // UMD (Minified) 124 | const umdMinified = mergician({}, config, { 125 | output: { 126 | file : umd.output.file.replace(/\.js$/, '.min.js'), 127 | format: umd.output.format 128 | }, 129 | plugins: config.plugins.concat([ 130 | terser(pluginSettings.terser.minify) 131 | ]) 132 | }); 133 | 134 | 135 | // Exports 136 | // ============================================================================= 137 | export default [ 138 | esm, 139 | esmMinified, 140 | umd, 141 | umdMinified 142 | ]; 143 | -------------------------------------------------------------------------------- /tests/fixtures/test-parse.css: -------------------------------------------------------------------------------- 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 | :root { --var-with\.escaped-dot: blue; } 48 | a { color: var(--color); } 49 | a { background: var(--var-with\.escaped-dot); } 50 | 51 | /* misc */ 52 | foo bar baz { 53 | bizz-buzz: word "ferrets"; 54 | *even: 'ie boo' 55 | } 56 | .wtf { 57 | *overflow-x: hidden; 58 | //max-height: 110px; 59 | #height: 18px; 60 | } 61 | 62 | /* @charset */ 63 | @charset "UTF-8"; 64 | 65 | /* @custom-media */ 66 | @custom-media 67 | --test 68 | (max-width: 1024px) 69 | ; 70 | @custom-media --narrow-window (max-width: 1024px); 71 | @custom-media --wide-window screen and (max-width: 1024px); 72 | 73 | /* @document */ 74 | @document url-prefix() { 75 | .test { 76 | height: 0; 77 | } 78 | } 79 | @-moz-document url-prefix() { 80 | .test1 { 81 | height: 0 82 | } 83 | 84 | .test2 { 85 | height: 0; 86 | } 87 | } 88 | 89 | /* @font-face */ 90 | @font-face { 91 | font-family: "Bitstream Vera Serif Bold"; 92 | src: url("http://developer.mozilla.org/@api/deki/files/2934/=VeraSeBd.ttf"); 93 | } 94 | 95 | /* @host */ 96 | @host { 97 | :scope { height: 0; } 98 | } 99 | 100 | /* @import */ 101 | @import 'test.css'; 102 | @import "test.css" screen, projection; 103 | @import url('test.css'); 104 | @import url("test.css") print; 105 | @import url("test.css") projection, tv; 106 | @import url('test.css') screen and (orientation:landscape); 107 | 108 | /* @keyframes */ 109 | @keyframes test1 { 110 | from { opacity: 0 } 111 | to { opacity: 1; } 112 | } 113 | @-webkit-keyframes test2 { 114 | from { opacity: 0 } 115 | to { opacity: 1; } 116 | } 117 | @keyframes test3 { 118 | 0% { opacity: 0 } 119 | 30.50% { opacity: 0.3050 } 120 | .68% , 121 | 72% 122 | , 85% { opacity: 0.85 } 123 | 100% { opacity: 1 } 124 | } 125 | 126 | /* @media */ 127 | @media (min-width: 1024px) { 128 | .test { height: 0; } 129 | } 130 | @media screen, print { 131 | .test { height: 0; } 132 | } 133 | @media { 134 | .test { height: 0; } 135 | } 136 | @media{ 137 | .test { height: 0; } 138 | } 139 | 140 | /* @namespace */ 141 | @namespace svg "http://www.w3.org/2000/svg"; 142 | @namespace 143 | "http://www.w3.org/1999/xhtml" 144 | ; 145 | 146 | /* @page */ 147 | @page { 148 | margin: 1cm; 149 | } 150 | @page :first { 151 | margin: 1cm; 152 | } 153 | @page toc, index:blank { 154 | margin: 1cm; 155 | } 156 | @page 157 | toc 158 | , 159 | index:blank 160 | { 161 | margin: 1cm; 162 | @top-left-corner { 163 | content: "Page " counter(page); 164 | } 165 | } 166 | 167 | /* @supports */ 168 | @supports (display: flex) { 169 | a { 170 | height: 0; 171 | } 172 | } 173 | @supports (display: flex) or (display: box) { 174 | a { 175 | height: 0; 176 | } 177 | } 178 | 179 | /* Escaped: Will be remove */ 180 | .\3A \`\({} 181 | .\3A \`\({} 182 | .\31 a2b3c{} 183 | #\#fake-id{} 184 | #\---{} 185 | #-a-b-c-{} 186 | #©{} 187 | 188 | /* Escaped: Retained (http://mathiasbynens.be/demo/html5-id) */ 189 | a[bcd="e\",f"] { height: 0; } 190 | #♥{height:0;} 191 | #©{height:0;} 192 | #“‘’”{height:0;} 193 | #☺☃{height:0;} 194 | #⌘⌥{height:0;} 195 | #𝄞♪♩♫♬{height:0;} 196 | #\?{height:0;} 197 | #\@{height:0;} 198 | #\.{height:0;} 199 | #\3A \){height:0;} 200 | #\3A \`\({height:0;} 201 | #\31 23{height:0;} 202 | #\31 a2b3c{height:0;} 203 | #\{height:0;} 204 | #\<\>\<\<\<\>\>\<\>{height:0;} 205 | #\+\+\+\+\+\+\+\+\+\+\[\>\+\+\+\+\+\+\+\>\+\+\+\+\+\+\+\+\+\+\>\+\+\+\>\+\<\<\<\<\-\]\>\+\+\.\>\+\.\+\+\+\+\+\+\+\.\.\+\+\+\.\>\+\+\.\<\<\+\+\+\+\+\+\+\+\+\+\+\+\+\+\+\.\>\.\+\+\+\.\-\-\-\-\-\-\.\-\-\-\-\-\-\-\-\.\>\+\.\>\.{height:0;} 206 | #\#{height:0;} 207 | #\#\#{height:0;} 208 | #\#\.\#\.\#{height:0;} 209 | #\_{height:0;} 210 | #\.fake\-class{height:0;} 211 | #foo\.bar{height:0;} 212 | #\3A hover{height:0;} 213 | #\3A hover\3A focus\3A active{height:0;} 214 | #\[attr\=value\]{height:0;} 215 | #f\/o\/o{height:0;} 216 | #f\\o\\o{height:0;} 217 | #f\*o\*o{height:0;} 218 | #f\!o\!o{height:0;} 219 | #f\'o\'o{height:0;} 220 | #f\~o\~o{height:0;} 221 | #f\+o\+o{height:0;} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-vars-ponyfill 2 | 3 | [![NPM](https://img.shields.io/npm/v/css-vars-ponyfill.svg?style=flat-square)](https://www.npmjs.com/package/css-vars-ponyfill) 4 | [![GitHub Workflow Status (master)](https://img.shields.io/github/actions/workflow/status/jhildenbiddle/css-vars-ponyfill/test.yml?branch=master&label=checks&style=flat-square)](https://github.com/jhildenbiddle/css-vars-ponyfill/actions?query=branch%3Amaster+) 5 | [![Codacy code quality](https://img.shields.io/codacy/grade/cb3acd7af0a34f3ea2c9f330548e2055/master?style=flat-square)](https://app.codacy.com/gh/jhildenbiddle/css-vars-ponyfill/dashboard?branch=master) 6 | [![Codacy branch coverage](https://img.shields.io/codacy/coverage/cb3acd7af0a34f3ea2c9f330548e2055/master?style=flat-square)](https://app.codacy.com/gh/jhildenbiddle/css-vars-ponyfill/dashboard?branch=master) 7 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://github.com/jhildenbiddle/css-vars-ponyfill/blob/master/LICENSE) 8 | [![jsDelivr](https://data.jsdelivr.com/v1/package/npm/css-vars-ponyfill/badge)](https://www.jsdelivr.com/package/npm/css-vars-ponyfill) 9 | [![Sponsor this project](https://img.shields.io/static/v1?style=flat-square&label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86)](https://github.com/sponsors/jhildenbiddle) 10 | 11 | A [ponyfill](https://ponyfill.com/) that provides client-side support for [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) (aka "CSS variables") in legacy and modern browsers. 12 | 13 | - [Documentation & Demos](https://jhildenbiddle.github.io/css-vars-ponyfill) 14 | 15 | ## Features 16 | 17 | - Client-side transformation of CSS custom properties to static values 18 | - Live updates of runtime values in both modern and legacy browsers 19 | - Transforms ``, ` 103 | ``` 104 | 105 | ```css 106 | /* style.css */ 107 | 108 | :root { 109 | --a: var(--b); /* Chained */ 110 | --b: var(--c); 111 | --c: 10px; 112 | } 113 | 114 | div { 115 | color: var(--color); /* from 313 | include: '[data-include]' 314 | }); 315 | ``` 316 | 317 | ### exclude 318 | 319 | - Type: `string` 320 | - Default: *none* 321 | 322 | CSS selector matching `` and/or ` 351 | include: '[data-exclude]' 352 | }); 353 | ``` 354 | 355 | ### variables 356 | 357 | - Type: `object` 358 | - Default: `{}` 359 | 360 | A collection of custom property name/value pairs to apply to both legacy and modern browsers as `:root`-level custom property declarations (or `:host`-level for shadowDOM elements). Property names can include or omit the leading double-hyphen (`--`). Values specified will override previous values. 361 | 362 | Legacy browsers (and modern browsers when [options.onlyLegacy](#onlylegacy) is `false`) will process these values while generating legacy-compatible CSS. Modern browsers with native support for CSS custom properties will add/update these values using the [setProperty()](https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration/setProperty) method when [options.updateDOM](#updatedom) is `true`. 363 | 364 | **Note:** Although these values are applied to both modern and legacy browsers, ponyfill callbacks like (e.g. [onComplete](#oncomplete)) will only be invoked in legacy browsers (or in modern browsers when [onlyLegacy](#onlylegacy) is `false`). 365 | 366 | **Example** 367 | 368 | ```javascript 369 | cssVars({ 370 | variables: { 371 | '--color1': 'red', // Leading -- included 372 | 'color2' : 'green' // Leading -- omitted 373 | } 374 | }); 375 | ``` 376 | 377 | ### onlyLegacy 378 | 379 | - Type: `boolean` 380 | - Default: `true` 381 | 382 | Determines how the ponyfill handles modern browsers with native CSS custom property support. 383 | 384 | When `true`, the ponyfill will only generate legacy-compatible CSS and invoke associated callbacks in browsers that lack native support for CSS custom properties (i.e. "legacy" browsers). When `false`, the ponyfill will treat all browsers as legacy, generating legacy-compatible CSS and invoking associated callbacks regardless of support for CSS custom properties. 385 | 386 | **Tip:** Setting this value to `false` allows for easier testing and debugging in modern browsers when legacy browsers are not accessible. 387 | 388 | **Example** 389 | 390 | ```javascript 391 | cssVars({ 392 | onlyLegacy: true // default 393 | }); 394 | 395 | cssVars({ 396 | // Treat all browsers as legacy 397 | onlyLegacy: false 398 | }); 399 | 400 | cssVars({ 401 | // Treat Edge 15/16 as legacy 402 | onlyLegacy: !(/Edge\/1[56]\./i.test(navigator.userAgent)) 403 | }); 404 | ``` 405 | 406 | ### preserveStatic 407 | 408 | - Type: `boolean` 409 | - Default: `true` 410 | 411 | Determines if CSS declarations that do not reference a custom property will be preserved in the transformed CSS. 412 | 413 | When `true`, CSS declarations that do not reference a custom property value will be preserved in the transformed CSS. This ensures that the original cascade order is maintained after the transformed CSS is appended to the DOM. It also allows the ponyfill to automatically disable source stylesheets because the styles they contain will be redundant. This provides a significant performance boost by limiting the number of style recalculations necessary when transformed CSS is appended to the DOM. 414 | 415 | When `false`, these declarations will be omitted from the transformed CSS. This can increase performance by reducing the amount of CSS that needs to be processed, but doing so runs the risk of breaking the original cascade when the transformed CSS is appended to the DOM (see example below). 416 | 417 | **Example** 418 | 419 | CSS: 420 | 421 | ```css 422 | :root { 423 | --color: red; 424 | } 425 | 426 | h1 { 427 | font-weight: bold; 428 | } 429 | 430 | p { 431 | margin: 20px; 432 | color: var(--color); 433 | } 434 | ``` 435 | 436 | JavaScript: 437 | 438 | ```javascript 439 | cssVars({ 440 | preserveStatic: true // default 441 | }); 442 | ``` 443 | 444 | Output when `preserveStatic:true` 445 | 446 | ```css 447 | h1 { 448 | font-weight: bold; 449 | } 450 | 451 | p { 452 | margin: 20px; 453 | color: red; 454 | } 455 | ``` 456 | 457 | Output when `preserveStatic:false` 458 | 459 | ```css 460 | p { 461 | color: red; 462 | } 463 | ``` 464 | 465 | **Example: Broken cascade** 466 | 467 | CSS: 468 | 469 | ```css 470 | :root { 471 | --color: red; 472 | } 473 | 474 | p { 475 | color: var(--color); 476 | } 477 | 478 | @media all (min-width: 800px) { 479 | p { 480 | color: blue; 481 | } 482 | } 483 | ``` 484 | 485 | Output when `preserveStatic:false` 486 | 487 | ```css 488 | p { 489 | color: red; 490 | } 491 | ``` 492 | 493 | When the above output is appended to the DOM, the `color: red` declaration overrides the `@media` rule's `color: blue` declaration. This is because the two `p` rules have the same [CSS specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) and the output CSS is appended *after* the source CSS. 494 | 495 | A workaround for this issue is force the ponyfill to include a declaration in the output by using a bogus CSS custom property with a fallback value: 496 | 497 | ```css 498 | @media all (min-width: 800px) { 499 | p { 500 | color: var(--bogus, blue); 501 | } 502 | } 503 | ``` 504 | 505 | The ponyfill will include the declaration when `preserveStatic` is `false` and resolve to its fallback value, maintaining the original cascade. 506 | 507 | ```css 508 | p { 509 | color: red; 510 | } 511 | 512 | @media all (min-width: 800px) { 513 | p { 514 | color: blue; 515 | } 516 | } 517 | ``` 518 | 519 | ### preserveVars 520 | 521 | - Type: `boolean` 522 | - Default: `false` 523 | 524 | Determines if CSS custom property declarations will be preserved in the transformed CSS. 525 | 526 | When `true`, both `:root` and `:host` custom property declarations and `var()` functions will be preserved in the transformed CSS. When `false`, these declarations will be omitted from the transformed CSS. 527 | 528 | **Example** 529 | 530 | CSS: 531 | 532 | ```css 533 | :root { 534 | --color: red; 535 | } 536 | p { 537 | color: var(--color); 538 | } 539 | ``` 540 | 541 | JavaScript: 542 | 543 | ```javascript 544 | cssVars({ 545 | preserveVars: false // default 546 | }); 547 | ``` 548 | 549 | Output when `preserveVars: false` 550 | 551 | ```css 552 | p { 553 | color: red; 554 | } 555 | ``` 556 | 557 | Output when `preserveVars: true` 558 | 559 | ```css 560 | :root { 561 | --color: red; 562 | } 563 | p { 564 | color: red; 565 | color: var(--color); 566 | } 567 | ``` 568 | 569 | ### silent 570 | 571 | - Type: `boolean` 572 | - Default: `false` 573 | 574 | Determines if warning and error messages will be displayed in the console. 575 | 576 | When `true`, messages will not be displayed in the console but will still be available using the [options.onWarning](#onwarning) and [options.onSuccess](#onsuccess) callbacks. When `false`, messages will be displayed in the console for each warning and error encountered while processing CSS. 577 | 578 | **Example** 579 | 580 | CSS: 581 | 582 | ```css 583 | @import "fail.css" 584 | 585 | p { 586 | color: var(--fail); 587 | } 588 | 589 | p { 590 | color: red; 591 | 592 | ``` 593 | 594 | JavaScript: 595 | 596 | ```javascript 597 | cssVars({ 598 | silent: false // default 599 | }); 600 | ``` 601 | 602 | Console: 603 | 604 | ```bash 605 | > cssVars(): "fail.css" 404 (Not Found) 606 | > cssVars(): variable "--fail" is undefined 607 | > cssVars(): parse error: missing "}" 608 | ``` 609 | 610 | ### updateDOM 611 | 612 | - Type: `boolean` 613 | - Default: `true` 614 | 615 | Determines if the ponyfill will update the DOM after processing CSS custom properties. 616 | 617 | When `true`, transformed CSS will be appended to the DOM. For legacy browsers, this is accomplished by appending a ` 647 | 648 | ``` 649 | 650 | ### updateURLs 651 | 652 | - Type: `boolean` 653 | - Default: `true` 654 | 655 | Determines if the ponyfill will convert relative `url()` paths to absolute urls. 656 | 657 | When `true`, the ponyfill will parse each block of external CSS for relative `url()` paths and convert them to absolute URLs. This allows resources (images, fonts, etc.) referenced using paths relative to an external stylesheet to load properly when legacy-compatible CSS is generated and appended to the DOM by the ponyfill. When `false`, the ponyfill will not modify relative `url()` paths. 658 | 659 | **Example** 660 | 661 | CSS: 662 | 663 | ```css 664 | /* http://mydomain.com/path/to/style.css */ 665 | 666 | div { 667 | background-image: url(image.jpg); 668 | } 669 | ``` 670 | 671 | JavaScript: 672 | 673 | ```javascript 674 | cssVars({ 675 | updateURLs: true // default 676 | }); 677 | ``` 678 | 679 | Output when `updateURLs: true` 680 | 681 | ```css 682 | div { 683 | background-image: url(http://mydomain.com/path/to/image.jpg); 684 | } 685 | ``` 686 | 687 | Output when `updateURLs: false` 688 | 689 | ```css 690 | div { 691 | background-image: url(image.jpg); 692 | } 693 | ``` 694 | 695 | ### watch 696 | 697 | - Type: `null`|`boolean` 698 | - Default: `null` 699 | 700 | Determines if a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) will be created to watch for `` and `