├── .eslintignore ├── .gitignore ├── .prettierrc ├── .travis.yml ├── .eslintrc.js ├── LICENSE ├── CHANGELOG.md ├── package.json ├── README.md ├── index.js └── test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/**/*.js 2 | .nyc_output/**/*.js 3 | test.js 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npm-debug.log 3 | logs 4 | *.log* 5 | tmp 6 | .DS_Store 7 | .nyc_output 8 | coverage 9 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true, 4 | "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"] 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - stable 5 | - "20" 6 | 7 | install: 8 | - npm install 9 | - npm install -g codecov 10 | 11 | after_success: 12 | - npm run coverage 13 | - codecov 14 | 15 | # Telegram bot 16 | notifications: 17 | webhooks: https://fathomless-fjord-24024.herokuapp.com/notify 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | }, 5 | extends: 'airbnb', 6 | parser: 'babel-eslint', 7 | rules: { 8 | 'max-len': [ 9 | 'error', 10 | { 11 | code: 150, 12 | }, 13 | ], 14 | 'comma-dangle': 0, 15 | 'no-plusplus': 0, 16 | 'arrow-parens': 0, 17 | 'no-multi-assign': 0, 18 | strict: 0, 19 | 'operator-linebreak': 0, 20 | 'no-console': 0, 21 | 'prefer-destructuring': 0, 22 | 'function-paren-newline': 0, 23 | 'global-require': 0, 24 | 'prefer-spread': 0, 25 | 'prefer-rest-params': 0, 26 | 'prefer-arrow-callback': 0, 27 | 'arrow-body-style': 0, 28 | 'no-restricted-globals': 0, 29 | 'no-restricted-syntax': 0, 30 | 'consistent-return': 0, 31 | 'no-param-reassign': 0, 32 | 'no-underscore-dangle': 0, 33 | 'import/no-unresolved': 0, 34 | 'import/no-dynamic-require': 0, 35 | 'import/no-extraneous-dependencies': 0, 36 | 'import/extensions': ['warn', 'ignorePackages'], 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Dimitri Nicolas 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 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | This project adheres to 4 | [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## 1.5.0 - 2024-07-31 7 | ### Added 8 | - PostCSS 8 support, thanks to @stephanedemotte [issue #11](https://github.com/dimitrinicolas/postcss-inline-media/issues/8) and [pr #12](https://github.com/dimitrinicolas/postcss-inline-media/pull/12). 9 | 10 | ## 1.4.0 - 2020-11-18 11 | ### Added 12 | - The `shorthandValueAddition` option, regarding [issue #3](https://github.com/dimitrinicolas/postcss-inline-media/issues/3). 13 | 14 | ## 1.3.0 - 2018-08-15 15 | ### Added 16 | - Source for new nodes to generate an accurate source map. 17 | 18 | ## 1.2.1 - 2018-08-11 19 | ### Changed 20 | - Simpler nested rules management system. 21 | 22 | ## 1.2.0 - 2018-08-11 23 | ### Added 24 | - Now packing together same media queries and same selectors, respecting css 25 | priority order. 26 | ### Changed 27 | - Now supporting nested rules. 28 | 29 | ## 1.1.2 - 2018-08-03 30 | ### Changed 31 | - Updated PostCSS dependency to v7. 32 | 33 | ## 1.1.1 - 2018-07-06 34 | ### Fixed 35 | - An issue with nested conditions. 36 | 37 | ## 1.1.0 - 2018-06-16 38 | ### Changed 39 | - Shorthand option, `max-width` by default. 40 | 41 | ## 1.0.2 - 2018-06-16 42 | ### Changed 43 | - Dependencies update. 44 | 45 | ## 1.O.1 - 2018-02-05 46 | ### Added 47 | - Nested conditions. 48 | 49 | ## 1.0.0 - 2018-01-30 50 | - Complete rework. 51 | ### Added 52 | - A shorthand for `max-width` media queries. 53 | 54 | ## 0.0.2 - 2017-10-18 55 | ### Changed 56 | - Updated postcss dependency 57 | 58 | ## 0.0.1 - 2017-05-14 59 | - Initial release. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-inline-media", 3 | "version": "1.5.0", 4 | "description": "Media queries shortcut on PostCSS", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/dimitrinicolas/postcss-inline-media.git" 10 | }, 11 | "author": { 12 | "name": "Dimitri NICOLAS", 13 | "email": "dimitri@fivecorp.fr" 14 | }, 15 | "keywords": [ 16 | "front-end", 17 | "postcss", 18 | "inline", 19 | "media", 20 | "query", 21 | "queries", 22 | "shortcut", 23 | "postcss-plugin" 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/dimitrinicolas/postcss-inline-media/issues" 27 | }, 28 | "homepage": "https://github.com/dimitrinicolas/postcss-inline-media", 29 | "scripts": { 30 | "publish": "clean-publish --files .nyc_output coverage", 31 | "lint": "eslint index.js", 32 | "test": "nyc --reporter=lcov --reporter=text ava", 33 | "coverage": "nyc report --reporter=text-lcov | coveralls", 34 | "dev": "nodemon -e js -x \"npm run test\"" 35 | }, 36 | "dependencies": { 37 | "postcss-value-parser": "^4.2.0" 38 | }, 39 | "peerDependencies": { 40 | "postcss": "^8.4.40" 41 | }, 42 | "devDependencies": { 43 | "ava": "^0.25.0", 44 | "ava-postcss-tester": "^1.0.0", 45 | "babel-eslint": "^8.2.6", 46 | "clean-publish": "^1.0.9", 47 | "coveralls": "^3.0.2", 48 | "eslint": "^5.3.0", 49 | "eslint-config-airbnb": "^17.0.0", 50 | "eslint-plugin-import": "^2.13.0", 51 | "eslint-plugin-jsx-a11y": "^6.1.1", 52 | "eslint-plugin-react": "^7.10.0", 53 | "nodemon": "^1.18.3", 54 | "nyc": "^12.0.2", 55 | "postcss-custom-media": "^10.0.8", 56 | "postcss-media-minmax": "^5.0.0", 57 | "postcss-simple-vars": "^7.0.1", 58 | "prettier": "^3.3.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-inline-media [![Build Status][travis badge]][travis link] [![Coverage Status][coveralls badge]][coveralls link] 2 | 3 | A [PostCSS][postcss] plugin that allows you to write media queries properties 4 | on the same line. 5 | 6 | ```pcss 7 | .title { 8 | font-size: 20px @1200 18px @480 16px; 9 | } 10 | ``` 11 | 12 | ## Installation 13 | 14 | ```console 15 | npm install postcss-inline-media 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```js 21 | // PostCSS plugins 22 | postcss([ 23 | require('postcss-inline-media'), 24 | ]); 25 | ``` 26 | 27 | Check out [PostCSS][postcss] docs for the complete installation. 28 | 29 | ### Example 30 | 31 | You can inline media queries just by writing its condition next to an `@` 32 | symbol. 33 | 34 | If you only write a number after the `@`, it will be read as a `max-width` 35 | value in pixels, you can change this shorthand with the `shorthand` and 36 | `shorthandUnit` option of this plugin, e.g.: 37 | 38 | ```js 39 | require('postcss-inline-media')({ 40 | shorthand: 'min-width', 41 | shorthandUnit: 'rem', 42 | }) 43 | ``` 44 | 45 | You can use the `shorthandValueAddition` to modify the shorthand media queries 46 | number values with a relative number to addition to it, e.g.: 47 | 48 | ```js 49 | require('postcss-inline-media')({ 50 | shorthandValueAddition: -1, 51 | }) 52 | ``` 53 | 54 | This file: 55 | 56 | ```pcss 57 | .btn { 58 | margin: 20px 10px @(print) 10px 5px @600 5px 0; 59 | } 60 | ``` 61 | 62 | will output: 63 | 64 | ```pcss 65 | .btn { 66 | margin: 20px 10px; 67 | } 68 | @media (print) { 69 | .btn { 70 | margin: 10px 5px; 71 | } 72 | } 73 | @media (max-width: 600px) { 74 | .btn { 75 | margin: 5px 0; 76 | } 77 | } 78 | ``` 79 | 80 | ### Media queries variables 81 | 82 | You can use 83 | [**postcss-simple-vars**][postcss-simple-vars] as media queries shortcut, put 84 | the `postcss-simple-vars` plugin **after** `postcss-inline-media`. 85 | 86 | ```pcss 87 | $md: (max-width: 900px); 88 | .btn { 89 | padding: 20px @md 10px; 90 | } 91 | ``` 92 | 93 | will output: 94 | 95 | ```pcss 96 | .btn { 97 | padding: 20px; 98 | } 99 | @media (max-width: 900px) { 100 | .btn { 101 | padding: 10px; 102 | } 103 | } 104 | ``` 105 | 106 | ### Nested conditions 107 | 108 | You can nest media queries in parentheses, but you can't set multiples nesting 109 | parentheses on the same CSS property. 110 | 111 | ```pcss 112 | div { 113 | margin: 50px (30px @(print) 20px @(max-width: 800px) 10px) 5px 5px; 114 | } 115 | ``` 116 | 117 | will output: 118 | 119 | ```pcss 120 | div { 121 | margin: 50px 30px 5px 5px; 122 | } 123 | @media print { 124 | div { 125 | margin: 50px 20px 5px 5px; 126 | } 127 | } 128 | @media (max-width: 800px) { 129 | div { 130 | margin: 50px 10px 5px 5px; 131 | } 132 | } 133 | ``` 134 | 135 | ### postcss-media-minmax 136 | 137 | This plugin is compatible with 138 | [**postcss-media-minmax**][postcss-media-minmax], put the `postcss-media-minmax` 139 | plugin **after** `postcss-inline-media`. 140 | 141 | ```pcss 142 | .btn { 143 | padding: 20px @(width <= 500px) 10px; 144 | } 145 | ``` 146 | 147 | ### postcss-custom-media 148 | 149 | You can also use 150 | [**postcss-custom-media**][postcss-custom-media], put the `postcss-custom-media` 151 | plugin **after** `postcss-inline-media`. 152 | 153 | ```pcss 154 | @custom-media --small-viewport (max-width: 30em); 155 | .btn { 156 | padding: 20px @(--small-viewport) 10px; 157 | } 158 | ``` 159 | 160 | ## Related 161 | 162 | - [postcss][postcss] - Transforming styles with JS plugins 163 | - [postcss-simple-vars][postcss-simple-vars] - PostCSS plugin for Sass-like 164 | variables 165 | - [postcss-media-minmax][postcss-media-minmax] - Writing simple and graceful 166 | Media Queries! 167 | - [postcss-custom-media][postcss-custom-media] - PostCSS plugin to transform 168 | - [ava-postcss-tester][ava-postcss-tester] - Simply test your PostCSS plugin 169 | with AVA 170 | 171 | ## License 172 | 173 | This project is licensed under the [MIT license](LICENSE). 174 | 175 | [travis badge]: https://travis-ci.org/dimitrinicolas/postcss-inline-media.svg?branch=master 176 | [travis link]: https://travis-ci.org/dimitrinicolas/postcss-inline-media 177 | [coveralls badge]: https://coveralls.io/repos/github/dimitrinicolas/postcss-inline-media/badge.svg?branch=master 178 | [coveralls link]: https://coveralls.io/github/dimitrinicolas/postcss-inline-media?branch=master 179 | 180 | [postcss]: https://github.com/postcss/postcss 181 | [postcss-simple-vars]: https://github.com/postcss/postcss-simple-vars 182 | [postcss-media-minmax]: https://github.com/postcss/postcss-media-minmax 183 | [postcss-custom-media]: https://github.com/postcss/postcss-custom-media 184 | [ava-postcss-tester]: https://github.com/dimitrinicolas/ava-postcss-tester 185 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const valueParser = require('postcss-value-parser'); 3 | 4 | /** 5 | * get string value of a node 6 | * @param {Node} node 7 | * @returns {string} node value 8 | */ 9 | const getValue = (node) => { 10 | if (node.type === 'function') { 11 | return valueParser.stringify(node); 12 | } 13 | return node.value; 14 | }; 15 | 16 | /** 17 | * Remove extra spaces from strings 18 | * @param {string} str 19 | */ 20 | const cleanString = (str) => { 21 | return str.replace(/\s\s+|^ | $/g, ' ').replace(/^ | $/g, ''); 22 | }; 23 | 24 | /** 25 | * parse nodes 26 | * @param {Node[]} nodes 27 | * @param {object} opts 28 | * @returns {{ base: {string}, queries: {MediaQuery[]} }} 29 | */ 30 | const parse = (nodes, opts) => { 31 | let base = ''; 32 | const queries = []; 33 | let nestedQuery = false; 34 | 35 | for (const node of nodes) { 36 | if (node.type === 'function' && node.value === '') { 37 | const nested = parse(node.nodes, Object.assign({}, opts, { base })); 38 | 39 | for (const query of nested.queries) { 40 | query.value = base + query.value; 41 | queries.push(query); 42 | } 43 | 44 | base += nested.base; 45 | nestedQuery = true; 46 | } else if (node.type === 'function' && node.value === '@') { 47 | queries[queries.length] = { 48 | media: '(', 49 | value: '', 50 | }; 51 | queries[queries.length - 1].media = '('; 52 | for (const item of node.nodes) { 53 | if (item.type === 'function') { 54 | queries[queries.length - 1].media += valueParser.stringify(item); 55 | } else { 56 | queries[queries.length - 1].media += 57 | (item.before || '') + item.value + (item.after || ''); 58 | } 59 | } 60 | queries[queries.length - 1].media += ')'; 61 | } else if (node.type === 'word' && /^@/gi.test(node.value)) { 62 | queries[queries.length] = { 63 | media: '(', 64 | value: '', 65 | }; 66 | let media = node.value.replace(/^@/gi, ''); 67 | if (Number.isNaN(parseInt(media, 10))) { 68 | media = `$${media}`; 69 | } else { 70 | media = `(${opts.shorthand}: ${ 71 | parseInt(media, 10) + opts.shorthandValueAddition 72 | }${opts.shorthandUnit})`; 73 | } 74 | queries[queries.length - 1].media = media; 75 | } else if (queries.length > 0) { 76 | if (nestedQuery) { 77 | for (const query of queries) { 78 | query.value += getValue(node); 79 | } 80 | } else { 81 | queries[queries.length - 1].value += getValue(node); 82 | } 83 | } 84 | 85 | if (nestedQuery) { 86 | if (['word', 'space'].indexOf(node.type) !== -1) { 87 | base += getValue(node); 88 | } else if ( 89 | node.type === 'function' && 90 | ['', '@'].indexOf(node.value) === -1 91 | ) { 92 | base += getValue(node); 93 | } 94 | } 95 | 96 | if (!nestedQuery && queries.length < 1) { 97 | base += getValue(node); 98 | } 99 | } 100 | 101 | return { 102 | base: cleanString(base), 103 | queries: queries.map(({ media, value }) => { 104 | return { 105 | media, 106 | value: cleanString(value), 107 | }; 108 | }), 109 | }; 110 | }; 111 | 112 | /** 113 | * Query class 114 | * @param {string} selector 115 | * @param {string} prop 116 | * @param {object} source 117 | * @param {object} content 118 | */ 119 | class Query { 120 | constructor(selector, prop, source, content) { 121 | this.selector = cleanString(selector); 122 | this.prop = cleanString(prop); 123 | this.source = source; 124 | 125 | const queries = content.queries.map(({ media, value }) => { 126 | const special = 127 | media.indexOf(':') !== -1 || 128 | media.indexOf('--') !== -1 || 129 | media.indexOf('>') !== -1 || 130 | media.indexOf('<') !== -1; 131 | const parenthesis = media.replace(/^\(/gi, '').indexOf('(') !== -1; 132 | if ((special && parenthesis) || (!special && !parenthesis)) { 133 | media = media.replace(/^\(/gi, '').replace(/\)$/gi, ''); 134 | } 135 | media = media.replace(/ or /gi, ','); 136 | 137 | return { 138 | media, 139 | value: cleanString(value), 140 | }; 141 | }); 142 | 143 | this.content = { 144 | base: content.base, 145 | queries, 146 | }; 147 | } 148 | } 149 | 150 | /** 151 | * RulePack class 152 | * @param {string} rule 153 | * @param {string} selector 154 | */ 155 | class RulePack { 156 | constructor(rule, selector) { 157 | this.rules = [rule]; 158 | this.parent = rule.parent; 159 | this.selector = selector; 160 | this.queries = []; 161 | } 162 | 163 | addQuery(rule, { prop, content }) { 164 | this.rules.push(rule); 165 | content.queries.forEach(({ media, value }) => { 166 | let foundMedia = false; 167 | this.queries.forEach((item) => { 168 | if (item.media === media && !foundMedia) { 169 | foundMedia = true; 170 | item.queries.push({ 171 | prop, 172 | value, 173 | }); 174 | } 175 | }); 176 | if (!foundMedia) { 177 | this.queries.push({ 178 | media, 179 | queries: [ 180 | { 181 | prop, 182 | value, 183 | }, 184 | ], 185 | }); 186 | } 187 | }); 188 | } 189 | } 190 | 191 | module.exports = (opts = {}) => { 192 | const inlineMedia = (root) => { 193 | const mediaQueries = []; 194 | 195 | root.walk((rule) => { 196 | if (rule.type !== 'decl') return; 197 | const value = rule.value; 198 | if (/@/gi.test(value)) { 199 | const content = parse(valueParser(value).nodes, { 200 | shorthand: 201 | typeof opts.shorthand === 'string' ? opts.shorthand : 'max-width', 202 | shorthandUnit: 203 | typeof opts.shorthandUnit === 'string' ? opts.shorthandUnit : 'px', 204 | shorthandValueAddition: 205 | typeof opts.shorthandValueAddition === 'number' && 206 | !isNaN(opts.shorthandValueAddition) 207 | ? opts.shorthandValueAddition 208 | : 0, 209 | }); 210 | 211 | const query = new Query( 212 | rule.parent.selector, 213 | rule.prop, 214 | rule.source, 215 | content, 216 | ); 217 | if ( 218 | mediaQueries.length && 219 | mediaQueries[mediaQueries.length - 1].parent === rule.parent && 220 | mediaQueries[mediaQueries.length - 1].selector === query.selector 221 | ) { 222 | mediaQueries[mediaQueries.length - 1].addQuery(rule, query); 223 | } else { 224 | const pack = new RulePack(rule, query.selector); 225 | pack.addQuery(rule, query); 226 | mediaQueries.push(pack); 227 | } 228 | 229 | if (content.base !== '') { 230 | rule.parent.insertBefore(rule, { 231 | prop: rule.prop, 232 | value: content.base, 233 | }); 234 | } 235 | } 236 | }); 237 | 238 | mediaQueries.forEach((mq) => { 239 | const nodeRoot = mq.parent.parent; 240 | 241 | mq.queries.forEach(({ media, queries, source }) => { 242 | const atRule = postcss.atRule({ 243 | name: 'media', 244 | params: media, 245 | }); 246 | 247 | const mediaRule = postcss.rule({ 248 | selector: mq.selector, 249 | }); 250 | queries.forEach(({ prop, value }) => { 251 | mediaRule.append({ 252 | prop, 253 | value, 254 | }); 255 | }); 256 | atRule.append(mediaRule); 257 | atRule.source = source; 258 | 259 | nodeRoot.append(atRule); 260 | }); 261 | 262 | mq.rules.forEach((rule) => { 263 | rule.remove(); 264 | }); 265 | }); 266 | }; 267 | 268 | return { 269 | postcssPlugin: 'postcss-inline-media', 270 | Once: inlineMedia, 271 | }; 272 | }; 273 | 274 | module.exports.postcss = true; 275 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import PostcssTester from 'ava-postcss-tester'; 3 | 4 | import postcss from 'postcss'; 5 | import postcssSimpleVars from 'postcss-simple-vars'; 6 | import postcssCustomMedia from 'postcss-custom-media'; 7 | import postcssMediaMinMax from 'postcss-media-minmax'; 8 | 9 | import postcssInlineMedia from '.'; 10 | 11 | const tester = new PostcssTester({ 12 | postcss, 13 | plugin: postcssInlineMedia, 14 | }); 15 | 16 | test('@(max-width: 800px)', async (t) => { 17 | const input = /* scss */ ` 18 | div { margin: 20px @(max-width: 800px) 10px; } 19 | `; 20 | const output = /* scss */ ` 21 | div { margin: 20px; } 22 | @media (max-width: 800px) { 23 | div { margin: 10px; } 24 | } 25 | `; 26 | tester.test(input, output, t); 27 | }); 28 | 29 | test('no default value', async (t) => { 30 | const input = /* scss */ ` 31 | div { margin: @(max-width: 800px) 10px; } 32 | `; 33 | const output = /* scss */ ` 34 | div { } 35 | @media (max-width: 800px) { 36 | div { margin: 10px } 37 | } 38 | `; 39 | tester.test(input, output, t); 40 | }); 41 | 42 | test('or operator replacement', async (t) => { 43 | const input = /* scss */ ` 44 | div { margin: 20px @(print or tv) 10px; } 45 | `; 46 | const output = /* scss */ ` 47 | div { margin: 20px; } 48 | @media print,tv { 49 | div { margin: 10px; } 50 | } 51 | `; 52 | tester.test(input, output, t); 53 | }); 54 | 55 | test('@Number shorthand', async (t) => { 56 | const input = /* scss */ ` 57 | div { margin: 20px @800 10px; } 58 | `; 59 | const output = /* scss */ ` 60 | div { margin: 20px; } 61 | @media (max-width: 800px) { 62 | div { margin: 10px; } 63 | } 64 | `; 65 | tester.test(input, output, t); 66 | }); 67 | 68 | test('Custom shorthand option', async (t) => { 69 | const input = /* scss */ ` 70 | div { margin: 20px @800 10px; } 71 | `; 72 | const output = /* scss */ ` 73 | div { margin: 20px; } 74 | @media (min-width: 800px) { 75 | div { margin: 10px; } 76 | } 77 | `; 78 | tester.test(input, output, t, { 79 | pluginOptions: { 80 | shorthand: 'min-width', 81 | }, 82 | }); 83 | }); 84 | 85 | test('Custom shorthandUnit option', async (t) => { 86 | const input = /* scss */ ` 87 | div { margin: 20px @30 10px; } 88 | `; 89 | const output = /* scss */ ` 90 | div { margin: 20px; } 91 | @media (max-width: 30em) { 92 | div { margin: 10px; } 93 | } 94 | `; 95 | tester.test(input, output, t, { 96 | pluginOptions: { 97 | shorthandUnit: 'em', 98 | }, 99 | }); 100 | }); 101 | 102 | test('Custom shorthandValueAddition option', async (t) => { 103 | const input = /* scss */ ` 104 | div { margin: 20px @30 10px; } 105 | `; 106 | const output = /* scss */ ` 107 | div { margin: 20px; } 108 | @media (max-width: 29px) { 109 | div { margin: 10px; } 110 | } 111 | `; 112 | tester.test(input, output, t, { 113 | pluginOptions: { 114 | shorthandValueAddition: -1, 115 | }, 116 | }); 117 | }); 118 | 119 | test('multiples conditions', async (t) => { 120 | const input = /* scss */ ` 121 | div { margin: 20px @800 10px @600 5px; } 122 | `; 123 | const output = /* scss */ ` 124 | div { margin: 20px; } 125 | @media (max-width: 800px) { 126 | div { margin: 10px; } 127 | } 128 | @media (max-width: 600px) { 129 | div { margin: 5px; } 130 | } 131 | `; 132 | tester.test(input, output, t); 133 | }); 134 | 135 | test('postcss-simple-vars', async (t) => { 136 | const input = /* scss */ ` 137 | $media: (print); 138 | div { margin: 20px @media 10px; } 139 | `; 140 | const output = /* scss */ ` 141 | div { margin: 20px; } 142 | @media (print) { 143 | div { margin: 10px; } 144 | } 145 | `; 146 | tester.test(input, output, t, { 147 | pluginsAfter: [postcssSimpleVars()], 148 | }); 149 | }); 150 | 151 | test('simple nested condition', async (t) => { 152 | const input = /* scss */ ` 153 | div { margin: 20px (15px @800 10px); } 154 | `; 155 | const output = /* scss */ ` 156 | div { margin: 20px 15px; } 157 | @media (max-width: 800px) { 158 | div { margin: 20px 10px; } 159 | } 160 | `; 161 | tester.test(input, output, t); 162 | }); 163 | 164 | test('complex nested condition', async (t) => { 165 | const input = /* scss */ ` 166 | div { 167 | margin: 20px (15px @(print) 10px @(max-width: 800px) 7px) 5px 5px; 168 | } 169 | `; 170 | const output = /* scss */ ` 171 | div { margin: 20px 15px 5px 5px; } 172 | @media print { 173 | div { margin: 20px 10px 5px 5px; } 174 | } 175 | @media (max-width: 800px) { 176 | div { margin: 20px 7px 5px 5px; } 177 | } 178 | `; 179 | tester.test(input, output, t); 180 | }); 181 | 182 | test('complex nested condition with function node', async (t) => { 183 | const input = /* scss */ ` 184 | div { margin: 20px (15px @(print) 10px) 7px func(8px); } 185 | `; 186 | const output = /* scss */ ` 187 | div { margin: 20px 15px 7px func(8px); } 188 | @media print { 189 | div { margin: 20px 10px 7px func(8px); } 190 | } 191 | `; 192 | tester.test(input, output, t); 193 | }); 194 | 195 | test('postcss-custom-media', async (t) => { 196 | const input = /* scss */ ` 197 | @custom-media --small-viewport (max-width: 30em); 198 | div { 199 | margin: 20px @(--small-viewport) 10px; 200 | } 201 | `; 202 | const output = /* scss */ ` 203 | div { margin: 20px; } 204 | @media (max-width: 30em) { 205 | div { margin: 10px; } 206 | } 207 | `; 208 | tester.test(input, output, t, { 209 | pluginsAfter: [postcssCustomMedia()], 210 | }); 211 | }); 212 | 213 | test('postcss-media-minmax', async (t) => { 214 | const input = /* scss */ ` 215 | div { margin: 20px @(width >= 500px) 10px; } 216 | `; 217 | const output = /* scss */ ` 218 | div { margin: 20px; } 219 | @media (min-width: 500px) { 220 | div { margin: 10px; } 221 | } 222 | `; 223 | tester.test(input, output, t, { 224 | pluginsAfter: [postcssMediaMinMax()], 225 | }); 226 | }); 227 | 228 | test('postcss-media-minmax many', async (t) => { 229 | const input = /* scss */ ` 230 | div { 231 | margin: 20px @(screen and (width >= 500px) and (width <= 1200px)) 10px; 232 | } 233 | `; 234 | const output = /* scss */ ` 235 | div { margin: 20px; } 236 | @media screen and (min-width: 500px) and (max-width: 1200px) { 237 | div { margin: 10px; } 238 | } 239 | `; 240 | tester.test(input, output, t, { 241 | pluginsAfter: [postcssMediaMinMax()], 242 | }); 243 | }); 244 | 245 | test('nested rules', async (t) => { 246 | const input = /* scss */ ` 247 | div { 248 | margin: 20px @900 10px @600 5px; 249 | padding: 20px @900 10px; 250 | header { 251 | span { color: black @900 red; } 252 | } 253 | } 254 | span { color: black @800 red; } 255 | `; 256 | const output = /* scss */ ` 257 | div { 258 | margin: 20px; 259 | padding: 20px; 260 | header { 261 | span { color: black; } 262 | @media (max-width: 900px) { 263 | span { color: red; } 264 | } 265 | } 266 | } 267 | span { color: black; } 268 | @media (max-width: 900px) { 269 | div { 270 | margin: 10px; 271 | padding: 10px; 272 | } 273 | } 274 | @media (max-width: 600px) { 275 | div { margin: 5px; } 276 | } 277 | @media (max-width: 800px) { 278 | span { color: red; } 279 | } 280 | `; 281 | tester.test(input, output, t); 282 | }); 283 | 284 | test('nested pseudo element', async (t) => { 285 | const input = /* scss */ ` 286 | div { 287 | margin: 20px @900 10px; 288 | padding: @600 10px; 289 | &::before { 290 | color: black @900 red; 291 | } 292 | } 293 | `; 294 | const output = /* scss */ ` 295 | div { 296 | margin: 20px; 297 | &::before { 298 | color: black; 299 | } 300 | @media (max-width: 900px) { 301 | &::before { 302 | color: red; 303 | } 304 | } 305 | } 306 | @media (max-width: 900px) { 307 | div { margin: 10px; } 308 | } 309 | @media (max-width: 600px) { 310 | div { padding: 10px; } 311 | } 312 | `; 313 | tester.test(input, output, t); 314 | }); 315 | 316 | test('nested unknown rule type', async (t) => { 317 | const input = /* scss */ ` 318 | div { 319 | margin: 20px @900 10px; 320 | -something { 321 | color: black @900 red; 322 | } 323 | } 324 | `; 325 | const output = /* scss */ ` 326 | div { 327 | margin: 20px; 328 | -something { 329 | color: black; 330 | } 331 | @media (max-width: 900px) { 332 | -something { 333 | color: red; 334 | } 335 | } 336 | } 337 | @media (max-width: 900px) { 338 | div { 339 | margin: 10px; 340 | } 341 | } 342 | `; 343 | tester.test(input, output, t); 344 | }); 345 | --------------------------------------------------------------------------------