├── CHANGELOG.md ├── .gitignore ├── .travis.yml ├── .prettierrc.json ├── .eslintrc ├── .npmignore ├── .editorconfig ├── LICENSE ├── index.js ├── package.json ├── README.md └── index.test.js /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1 2 | * Initial release. 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | yarn-error.log 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: yarn 3 | node_js: 4 | - node 5 | - '12' 6 | - '10' 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-postcss", 4 | "plugin:prettier/recommended", 5 | "prettier" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | yarn-error.log 2 | yarn.lock 3 | 4 | *.test.js 5 | .travis.yml 6 | .editorconfig 7 | 8 | .gitignore 9 | node_modules/ 10 | test/ 11 | gulpfile.js 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2015 xingkui wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | 3 | const bgUrlRegex = /url\([\s'"]?(.*?)[\s'"]?\)/g; 4 | // eslint-disable-next-line security/detect-unsafe-regex 5 | const protocolRegex = /(https?:)?\/\/|data:/g; 6 | 7 | module.exports = postcss.plugin('postcss-shopify-settings-variables', () => { 8 | // Work with options here 9 | 10 | return (root) => { 11 | // Transform CSS AST here 12 | root.walk(function (node) { 13 | if (node.type === 'decl') { 14 | while (node.value.includes('$(')) { 15 | node.value = node.value.replace( 16 | /^([^$]*)(\$\()([^)]+)(\))(.*)$/, 17 | function (match, $1, $2, $3, $4, $5) { 18 | return $1 + '{{ settings.' + $3 + ' }}' + $5; 19 | }, 20 | ); 21 | } 22 | if (bgUrlRegex.test(node.value) && !protocolRegex.test(node.value)) { 23 | node.value = node.value.replace(bgUrlRegex, function (match, $1) { 24 | let urlAndFilters = $1.split('|'); 25 | let newVal = 'url({{ "'; 26 | urlAndFilters.forEach(function (current, index) { 27 | if (index === 0) { 28 | newVal += current.replace(/'|"/g, '').trim(); 29 | newVal += '" | asset_url '; 30 | } else { 31 | newVal += '| ' + current.trim() + ' '; 32 | } 33 | }); 34 | newVal += '}})'; 35 | return newVal; 36 | }); 37 | } 38 | } 39 | }); 40 | }; 41 | }); 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-shopify-settings-variables", 3 | "version": "0.3.1", 4 | "description": "PostCSS plugin for setting variable in shopify css file.", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "shopify", 10 | "liquid" 11 | ], 12 | "author": "xingkui wang ", 13 | "license": "MIT", 14 | "repository": "bit3725/postcss-shopify-settings-variables", 15 | "bugs": { 16 | "url": "https://github.com/bit3725/postcss-shopify-settings-variables/issues" 17 | }, 18 | "homepage": "https://github.com/bit3725/postcss-shopify-settings-variables", 19 | "dependencies": { 20 | "postcss": "^8.2.10" 21 | }, 22 | "devDependencies": { 23 | "@logux/eslint-config": "^33.0.0", 24 | "chai": "^3.0.0", 25 | "eslint": "^6.5.1", 26 | "eslint-config-postcss": "^3.0.7", 27 | "eslint-config-prettier": "^6.11.0", 28 | "eslint-config-standard": "^14.1.0", 29 | "eslint-plugin-es5": "^1.4.1", 30 | "eslint-plugin-import": "^2.18.2", 31 | "eslint-plugin-jest": "^22.19.0", 32 | "eslint-plugin-node": "^10.0.0", 33 | "eslint-plugin-prefer-let": "^1.0.1", 34 | "eslint-plugin-prettier": "^3.1.4", 35 | "eslint-plugin-promise": "^4.2.1", 36 | "eslint-plugin-security": "^1.4.0", 37 | "eslint-plugin-standard": "^4.0.1", 38 | "eslint-plugin-unicorn": "^12.1.0", 39 | "jest": "^24.9.0", 40 | "node-notifier": ">=8.0.1", 41 | "prettier": "2.0.5" 42 | }, 43 | "engines": { 44 | "node": ">=10.0.0" 45 | }, 46 | "jest": { 47 | "testEnvironment": "node" 48 | }, 49 | "scripts": { 50 | "test": "jest && eslint ." 51 | } 52 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS Shopify Settings Variables [![Build Status][ci-img]][ci] 2 | 3 | [PostCSS] plugin to allow use of Shopify specific theme variables in Shopify css files. 4 | 5 | It's common in a Shopify theme css file to use code such as `{{ settings.headline_color }}` as a value of `css property`. Sadly, this will cause some annoying issues such as your code editor will loose syntax highlighting, and more. This happens because such values are invalid form of css. 6 | 7 | With this simple [PostCSS](https://github.com/postcss/postcss) plugin, you can safely use code like `$(headline_color)` instead. This code will be transformed to the syntax Shopify parsers support. 8 | 9 | [PostCSS]: https://github.com/postcss/postcss 10 | [ci-img]: https://travis-ci.org/bit3725/postcss-shopify-settings-variables.svg 11 | [ci]: https://travis-ci.org/bit3725/postcss-shopify-settings-variables 12 | 13 | ```css 14 | .foo { 15 | color: $(headline_color); 16 | font-family: $(regular_websafe_font | replace: '+', ' '); 17 | font-size: $(regular_font_size)px; 18 | border: 1px solid $(border_color); 19 | background: rgba($(settings.header_bg_color), 0.9); 20 | background: url(logo.png); 21 | background: url(logo.png | split: '?' | first); 22 | } 23 | ``` 24 | 25 | Will be transformed to: 26 | 27 | ```css 28 | .foo { 29 | color: {{ settings.headline_color }}; /* Shopify friendly values */ 30 | font-family: {{ settings.regular_websafe_font | replace: '+', ' ' }}; 31 | font-size: {{ settings.regular_font_size }}px; 32 | border: 1px solid {{ settings.border_color }}; 33 | background: rgba({{ settings.header_bg_color }}, 0.9); 34 | background: url({{ "logo.png" | asset_url }}); 35 | background: url({{ "logo.png" | asset_url | split: '?' | first }}); 36 | } 37 | ``` 38 | 39 | ## Usage 40 | 41 | ```js 42 | postcss([ require('postcss-shopify-settings-variables') ]) 43 | ``` 44 | 45 | See [PostCSS] docs for examples for your environment. 46 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | 3 | const plugin = require('./'); 4 | 5 | async function run(input, output, opts) { 6 | let result = await postcss([plugin(opts)]).process(input, { 7 | from: undefined, 8 | }); 9 | expect(result.css).toEqual(output); 10 | expect(result.warnings()).toHaveLength(0); 11 | } 12 | 13 | describe('postcss-shopify-settings-variables', () => { 14 | it('replace single variable in value', async () => { 15 | await run( 16 | 'a{ color: $(headline_color); }', 17 | 'a{ color: {{ settings.headline_color }}; }', 18 | {}, 19 | ); 20 | }); 21 | 22 | it('replace multiple variables in multiple values', async () => { 23 | await run( 24 | 'a{ color: $(headline_color); background-color: $(healine_bg_color); }', 25 | 'a{ color: {{ settings.headline_color }}; background-color: {{ settings.healine_bg_color }}; }', 26 | {}, 27 | ); 28 | }); 29 | 30 | it('replace single variable in value with pixel unit', async () => { 31 | await run( 32 | 'body{ font-size: $(headline_size)px; }', 33 | 'body{ font-size: {{ settings.headline_size }}px; }', 34 | {}, 35 | ); 36 | }); 37 | 38 | it('replace single variable in value with liquid filter', async () => { 39 | await run( 40 | 'a{ font-size: $(headline_size | divided_by: 2)px; }', 41 | 'a{ font-size: {{ settings.headline_size | divided_by: 2 }}px; }', 42 | {}, 43 | ); 44 | }); 45 | 46 | it('replace single variable in value with liquid string filter', async () => { 47 | await run( 48 | "a{ font-family: $(font_family | replace: '+', ' '); }", 49 | "a{ font-family: {{ settings.font_family | replace: '+', ' ' }}; }", 50 | {}, 51 | ); 52 | }); 53 | 54 | it('replace single variable in value which has multiple variables', async () => { 55 | await run( 56 | 'a{ border-bottom: 1px dotted $(border_color); }', 57 | 'a{ border-bottom: 1px dotted {{ settings.border_color }}; }', 58 | {}, 59 | ); 60 | }); 61 | 62 | it('replace single variable in value when there is quotes', async () => { 63 | await run( 64 | 'a{ font-family: "$(headline_google_webfont_font)"; }', 65 | 'a{ font-family: "{{ settings.headline_google_webfont_font }}"; }', 66 | {}, 67 | ); 68 | }); 69 | 70 | it('replace single variable in value when there is parenthesis', async () => { 71 | await run( 72 | 'a{ background: rgba($(header_bg_color), 0.9); }', 73 | 'a{ background: rgba({{ settings.header_bg_color }}, 0.9); }', 74 | {}, 75 | ); 76 | }); 77 | 78 | describe('replace background url with asset_url filter', () => { 79 | it('no quote', async () => { 80 | await run( 81 | 'a{ background: url(logo.png); }', 82 | 'a{ background: url({{ "logo.png" | asset_url }}); }', 83 | {}, 84 | ); 85 | }); 86 | 87 | it('with space', async () => { 88 | await run( 89 | 'a{ background: url( logo.png ); }', 90 | 'a{ background: url({{ "logo.png" | asset_url }}); }', 91 | {}, 92 | ); 93 | }); 94 | 95 | it('single quote', async () => { 96 | await run( 97 | "a{ background: url('logo.png'); }", 98 | 'a{ background: url({{ "logo.png" | asset_url }}); }', 99 | {}, 100 | ); 101 | }); 102 | 103 | it('double quote', async () => { 104 | await run( 105 | 'a{ background: url("logo.png"); }', 106 | 'a{ background: url({{ "logo.png" | asset_url }}); }', 107 | {}, 108 | ); 109 | }); 110 | 111 | it('only replace url', async () => { 112 | await run( 113 | 'a{ background-image: url(logo.png) no-repeat; }', 114 | 'a{ background-image: url({{ "logo.png" | asset_url }}) no-repeat; }', 115 | {}, 116 | ); 117 | }); 118 | 119 | it('multiple url', async () => { 120 | await run( 121 | 'a{ background: url("logo.png"), url(logo@2x.jpg); }', 122 | 'a{ background: url({{ "logo.png" | asset_url }}), url({{ "logo@2x.jpg" | asset_url }}); }', 123 | {}, 124 | ); 125 | }); 126 | 127 | it('not replace url with full path', async () => { 128 | await run( 129 | 'a{ background: url("http://a.com/logo.png"); }', 130 | 'a{ background: url("http://a.com/logo.png"); }', 131 | {}, 132 | ); 133 | }); 134 | 135 | it('not replace url with data uri', async () => { 136 | await run( 137 | 'a{ background: url(data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7); }', 138 | 'a{ background: url(data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7); }', 139 | {}, 140 | ); 141 | }); 142 | }); 143 | 144 | it('replace variable and url together', async () => { 145 | await run( 146 | 'a{ background: $(modal_background_color) url("newsletter_bg.png"); }', 147 | 'a{ background: {{ settings.modal_background_color }} url({{ "newsletter_bg.png" | asset_url }}); }', 148 | {}, 149 | ); 150 | }); 151 | 152 | describe('insert asset_url filter between background url and associated filters', () => { 153 | it('single url with filters', async () => { 154 | await run( 155 | 'a{ background: url(logo.png | split: "?" | first); }', 156 | 'a{ background: url({{ "logo.png" | asset_url | split: "?" | first }}); }', 157 | {}, 158 | ); 159 | }); 160 | 161 | it('variable and single url with filters', async () => { 162 | await run( 163 | 'a{ background: url(logo.png | split: "?" | first) $(modal_background_color); }', 164 | 'a{ background: url({{ "logo.png" | asset_url | split: "?" | first }}) {{ settings.modal_background_color }}; }', 165 | {}, 166 | ); 167 | }); 168 | 169 | it('multiple url with filters', async () => { 170 | await run( 171 | 'a{ background: url(logo.png | split: "?" | first), url("logo@2x.png" | downcase); }', 172 | 'a{ background: url({{ "logo.png" | asset_url | split: "?" | first }}), url({{ "logo@2x.png" | asset_url | downcase }}); }', 173 | {}, 174 | ); 175 | }); 176 | 177 | it('one url with filters, another without filters', async () => { 178 | await run( 179 | "a{ background: url( logo.png ), url('logo@2x.png' | downcase); }", 180 | 'a{ background: url({{ "logo.png" | asset_url }}), url({{ "logo@2x.png" | asset_url | downcase }}); }', 181 | {}, 182 | ); 183 | }); 184 | }); 185 | 186 | it('multiple settings on same line', async () => { 187 | await run( 188 | 'a{ font-family: $(type_header_font_family.family), $(type_header_font_family.fallback_families); }', 189 | 'a{ font-family: {{ settings.type_header_font_family.family }}, {{ settings.type_header_font_family.fallback_families }}; }', 190 | {}, 191 | ); 192 | }); 193 | }); 194 | --------------------------------------------------------------------------------