├── 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 |${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 | -------------------------------------------------------------------------------- /.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 |{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 | [](https://www.npmjs.com/package/css-vars-ponyfill) 4 | [](https://github.com/jhildenbiddle/css-vars-ponyfill/actions?query=branch%3Amaster+) 5 | [](https://app.codacy.com/gh/jhildenbiddle/css-vars-ponyfill/dashboard?branch=master) 6 | [](https://app.codacy.com/gh/jhildenbiddle/css-vars-ponyfill/dashboard?branch=master) 7 | [](https://github.com/jhildenbiddle/css-vars-ponyfill/blob/master/LICENSE) 8 | [](https://www.jsdelivr.com/package/npm/css-vars-ponyfill) 9 | [](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 `