├── .editorconfig ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .tape.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── example.js ├── index.js ├── package.json └── test ├── basic.css └── basic.expect.css /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = tab 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | 13 | [*.{json,md,yml}] 14 | indent_size = 2 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: master 6 | pull_request: 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [10.x, 12.x, 14.x, 16.x, 17.x] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | yarn.lock 4 | .* 5 | !.appveyor.yml 6 | !.editorconfig 7 | !.gitignore 8 | !.tape.js 9 | !.github 10 | *.log* 11 | *.result.css 12 | -------------------------------------------------------------------------------- /.tape.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'basic': { 3 | message: 'supports basic usage' 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changes to Media() 2 | 3 | ### 3.0.0 (May 30, 2017) 4 | 5 | - Updated: Support for PostCSS v6. 6 | - Updated: Support for Node v4. 7 | 8 | ### 2.0.0 (January 8, 2017) 9 | 10 | - Changed: Append complete `@media` at-rules with cloned rules and declarations after rules. 11 | 12 | ### 1.0.0 (January 3, 2017) 13 | 14 | - Initial version 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Media() 2 | 3 | You want to help? You rock! Now, take a moment to be sure your contributions 4 | make sense to everyone else. 5 | 6 | ## Reporting Issues 7 | 8 | Found a problem? Want a new feature? 9 | 10 | - See if your issue or idea has [already been reported]. 11 | - Provide a [reduced test case] or a [live example]. 12 | 13 | Remember, a bug is a _demonstrable problem_ caused by _our_ code. 14 | 15 | ## Submitting Pull Requests 16 | 17 | Pull requests are the greatest contributions, so be sure they are focused in 18 | scope and avoid unrelated commits. 19 | 20 | 1. To begin; [fork this project], clone your fork, and add our upstream. 21 | ```bash 22 | # Clone your fork of the repo into the current directory 23 | git clone git@github.com:YOUR_USER/postcss-media-fn.git 24 | 25 | # Navigate to the newly cloned directory 26 | cd postcss-media-fn 27 | 28 | # Assign the original repo to a remote called "upstream" 29 | git remote add upstream git@github.com:jonathantneal/postcss-media-fn.git 30 | 31 | # Install the tools necessary for testing 32 | npm install 33 | ``` 34 | 35 | 2. Create a branch for your feature or fix: 36 | ```bash 37 | # Move into a new branch for your feature 38 | git checkout -b feature/thing 39 | ``` 40 | ```bash 41 | # Move into a new branch for your fix 42 | git checkout -b fix/something 43 | ``` 44 | 45 | 3. If your code follows our practices, then push your feature branch: 46 | ```bash 47 | # Test current code 48 | npm test 49 | ``` 50 | ```bash 51 | # Push the branch for your new feature 52 | git push origin feature/thing 53 | ``` 54 | ```bash 55 | # Or, push the branch for your update 56 | git push origin update/something 57 | ``` 58 | 59 | That’s it! Now [open a pull request] with a clear title and description. 60 | 61 | [already been reported]: issues 62 | [fork this project]: fork 63 | [live example]: https://codepen.io/pen 64 | [open a pull request]: https://help.github.com/articles/using-pull-requests/ 65 | [reduced test case]: https://css-tricks.com/reduced-test-cases/ 66 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # CC0 1.0 Universal 2 | 3 | ## Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an “owner”) of an original work of 8 | authorship and/or a database (each, a “Work”). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific works 12 | (“Commons”) that the public can reliably and without fear of later claims of 13 | infringement build upon, modify, incorporate in other works, reuse and 14 | redistribute as freely as possible in any form whatsoever and for any purposes, 15 | including without limitation commercial purposes. These owners may contribute 16 | to the Commons to promote the ideal of a free culture and the further 17 | production of creative, cultural and scientific works, or to gain reputation or 18 | greater distribution for their Work in part through the use and efforts of 19 | others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation of 22 | additional consideration or compensation, the person associating CC0 with a 23 | Work (the “Affirmer”), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and 25 | publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights (“Copyright and 31 | Related Rights”). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 1. the right to reproduce, adapt, distribute, perform, display, 34 | communicate, and translate a Work; 35 | 2. moral rights retained by the original author(s) and/or performer(s); 36 | 3. publicity and privacy rights pertaining to a person’s image or likeness 37 | depicted in a Work; 38 | 4. rights protecting against unfair competition in regards to a Work, 39 | subject to the limitations in paragraph 4(i), below; 40 | 5. rights protecting the extraction, dissemination, use and reuse of data 41 | in a Work; 42 | 6. database rights (such as those arising under Directive 96/9/EC of the 43 | European Parliament and of the Council of 11 March 1996 on the legal 44 | protection of databases, and under any national implementation thereof, 45 | including any amended or successor version of such directive); and 46 | 7. other similar, equivalent or corresponding rights throughout the world 47 | based on applicable law or treaty, and any national implementations 48 | thereof. 49 | 50 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 51 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 52 | unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright 53 | and Related Rights and associated claims and causes of action, whether now 54 | known or unknown (including existing as well as future claims and causes of 55 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 56 | duration provided by applicable law or treaty (including future time 57 | extensions), (iii) in any current or future medium and for any number of 58 | copies, and (iv) for any purpose whatsoever, including without limitation 59 | commercial, advertising or promotional purposes (the “Waiver”). Affirmer makes 60 | the Waiver for the benefit of each member of the public at large and to the 61 | detriment of Affirmer’s heirs and successors, fully intending that such Waiver 62 | shall not be subject to revocation, rescission, cancellation, termination, or 63 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 64 | by the public as contemplated by Affirmer’s express Statement of Purpose. 65 | 66 | 3. Public License Fallback. Should any part of the Waiver for any reason be 67 | judged legally invalid or ineffective under applicable law, then the Waiver 68 | shall be preserved to the maximum extent permitted taking into account 69 | Affirmer’s express Statement of Purpose. In addition, to the extent the Waiver 70 | is so judged Affirmer hereby grants to each affected person a royalty-free, non 71 | transferable, non sublicensable, non exclusive, irrevocable and unconditional 72 | license to exercise Affirmer’s Copyright and Related Rights in the Work (i) in 73 | all territories worldwide, (ii) for the maximum duration provided by applicable 74 | law or treaty (including future time extensions), (iii) in any current or 75 | future medium and for any number of copies, and (iv) for any purpose 76 | whatsoever, including without limitation commercial, advertising or promotional 77 | purposes (the “License”). The License shall be deemed effective as of the date 78 | CC0 was applied by Affirmer to the Work. Should any part of the License for any 79 | reason be judged legally invalid or ineffective under applicable law, such 80 | partial invalidity or ineffectiveness shall not invalidate the remainder of the 81 | License, and in such case Affirmer hereby affirms that he or she will not (i) 82 | exercise any of his or her remaining Copyright and Related Rights in the Work 83 | or (ii) assert any associated claims and causes of action with respect to the 84 | Work, in either case contrary to Affirmer’s express Statement of Purpose. 85 | 86 | 4. Limitations and Disclaimers. 87 | 1. No trademark or patent rights held by Affirmer are waived, abandoned, 88 | surrendered, licensed or otherwise affected by this document. 89 | 2. Affirmer offers the Work as-is and makes no representations or 90 | warranties of any kind concerning the Work, express, implied, statutory 91 | or otherwise, including without limitation warranties of title, 92 | merchantability, fitness for a particular purpose, non infringement, or 93 | the absence of latent or other defects, accuracy, or the present or 94 | absence of errors, whether or not discoverable, all to the greatest 95 | extent permissible under applicable law. 96 | 3. Affirmer disclaims responsibility for clearing rights of other persons 97 | that may apply to the Work or any use thereof, including without 98 | limitation any person’s Copyright and Related Rights in the Work. 99 | Further, Affirmer disclaims responsibility for obtaining any necessary 100 | consents, permissions or other rights required for any use of the Work. 101 | 4. Affirmer understands and acknowledges that Creative Commons is not a 102 | party to this document and has no duty or obligation with respect to 103 | this CC0 or use of the Work. 104 | 105 | For more information, please see 106 | https://creativecommons.org/publicdomain/zero/1.0/. 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Media() [PostCSS Logo][postcss] 2 | 3 | [![NPM Version][npm-img]][npm-url] 4 | [![Build Status][cli-img]][cli-url] 5 | [![Licensing][lic-img]][lic-url] 6 | [![Gitter Chat][git-img]][git-url] 7 | 8 | [Media()] lets you use the `media()` function to assign responsive values to a declaration, following the [CSS Media Expressions] specification. 9 | 10 | ```css 11 | /* before */ 12 | 13 | h1 { 14 | font-size: media( 15 | 16px, 16 | (min-width: 600px) 20px, 17 | (min-width: 1000px) 40px, 18 | (min-width: 1400px) 60px 19 | ); 20 | } 21 | 22 | 23 | /* after */ 24 | 25 | h1 { 26 | font-size: 16px; 27 | } 28 | 29 | @media (min-width: 600px) { 30 | h1 { 31 | font-size: 20px; 32 | } 33 | } 34 | 35 | @media (min-width: 1000px) { 36 | h1 { 37 | font-size: 40px; 38 | } 39 | } 40 | 41 | @media (min-width: 1400px) { 42 | h1 { 43 | font-size: 60px; 44 | } 45 | } 46 | ``` 47 | 48 | ## Usage 49 | 50 | Add [Media()] to your build tool: 51 | 52 | ```bash 53 | npm install postcss-media-fn --save-dev 54 | ``` 55 | 56 | #### Node 57 | 58 | ```js 59 | require('postcss-media-fn').process(YOUR_CSS, { /* options */ }); 60 | ``` 61 | 62 | #### PostCSS 63 | 64 | Add [PostCSS] to your build tool: 65 | 66 | ```bash 67 | npm install postcss --save-dev 68 | ``` 69 | 70 | Load [Media()] as a PostCSS plugin: 71 | 72 | ```js 73 | postcss([ 74 | require('postcss-media-fn')({ /* options */ }) 75 | ]).process(YOUR_CSS, /* options */); 76 | ``` 77 | 78 | #### Gulp 79 | 80 | Add [Gulp PostCSS] to your build tool: 81 | 82 | ```bash 83 | npm install gulp-postcss --save-dev 84 | ``` 85 | 86 | Enable [Media()] within your Gulpfile: 87 | 88 | ```js 89 | var postcss = require('gulp-postcss'); 90 | 91 | gulp.task('css', function () { 92 | return gulp.src('./src/*.css').pipe( 93 | postcss([ 94 | require('postcss-media-fn')({ /* options */ }) 95 | ]) 96 | ).pipe( 97 | gulp.dest('.') 98 | ); 99 | }); 100 | ``` 101 | 102 | #### Grunt 103 | 104 | Add [Grunt PostCSS] to your build tool: 105 | 106 | ```bash 107 | npm install grunt-postcss --save-dev 108 | ``` 109 | 110 | Enable [Media()] within your Gruntfile: 111 | 112 | ```js 113 | grunt.loadNpmTasks('grunt-postcss'); 114 | 115 | grunt.initConfig({ 116 | postcss: { 117 | options: { 118 | use: [ 119 | require('postcss-media-fn')({ /* options */ }) 120 | ] 121 | }, 122 | dist: { 123 | src: '*.css' 124 | } 125 | } 126 | }); 127 | ``` 128 | 129 | [cli-url]: https://github.com/csstools/postcss-media-fn/actions/workflows/ci.yaml 130 | [cli-img]: https://github.com/csstools/postcss-media-fn/actions/workflows/ci.yaml/badge.svg 131 | [git-url]: https://gitter.im/postcss/postcss 132 | [git-img]: https://img.shields.io/badge/chat-gitter-blue.svg 133 | [lic-url]: LICENSE.md 134 | [lic-img]: https://img.shields.io/npm/l/postcss-media-fn.svg 135 | [npm-url]: https://www.npmjs.com/package/postcss-media-fn 136 | [npm-img]: https://img.shields.io/npm/v/postcss-media-fn.svg 137 | 138 | [CSS Media Expressions]: https://jonathantneal.github.io/media-expressions-spec/ 139 | [Gulp PostCSS]: https://github.com/postcss/gulp-postcss 140 | [Grunt PostCSS]: https://github.com/nDmitry/grunt-postcss 141 | [Media()]: https://github.com/jonathantneal/postcss-media-fn 142 | [PostCSS]: https://github.com/postcss/postcss 143 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Add your CSS code here. 3 | * See https://github.com/jonathantneal/postcss-media-fn#features for more examples 4 | */ 5 | 6 | const css = ` 7 | h1 { 8 | font-size: media( 9 | 16px, 10 | (min-width: 600px) 20px, 11 | (min-width: 1000px) 40px, 12 | (min-width: 1400px) 60px 13 | ); 14 | } 15 | `; 16 | 17 | /* 18 | * Add your process configuration here; see 19 | * http://api.postcss.org/global.html#processOptions for more details. 20 | */ 21 | 22 | const processOptions = {}; 23 | 24 | /* 25 | * Process the CSS and log it to the console. 26 | */ 27 | 28 | require('postcss-media-fn').process(css, {}, processOptions).then(result => { 29 | console.log(result.css); 30 | }); 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const { parse } = require('postcss-values-parser'); 3 | 4 | // local tooling 5 | const mediaFnRegExp = /media\([^)]+\)/i; 6 | const isMediaFn = (node) => node.name === 'media'; 7 | const isResponsiveValue = (value) => value.length > 1; 8 | const isNonResponsiveValue = (value) => value.length === 1; 9 | 10 | /** 11 | * Use `media()` to assign responsive values. 12 | * @returns {import('postcss').Plugin} 13 | */ 14 | module.exports = function creator() { 15 | return { 16 | postcssPlugin: 'postcss-media-fn', 17 | Rule(rule) { 18 | const newAtRules = []; 19 | 20 | rule.walkDecls( 21 | (decl) => { 22 | if (!mediaFnRegExp.test(decl.value)) { 23 | return; 24 | } 25 | 26 | const valueAST = parse(decl.value); 27 | valueAST.walkFuncs( 28 | (node) => { 29 | if (isMediaFn(node)) { 30 | // all values 31 | const allValues = node.nodes.reduce( 32 | (values, childNode) => { 33 | // if the node is a dividing comma 34 | if (childNode.value === ',') { 35 | // create a new values sub-group 36 | values.push([]); 37 | } else { 38 | // otherwise, assign the stringified node to the last values sub-group 39 | values[values.length - 1].push(childNode.raws.before + childNode.toString() + childNode.raws.after); 40 | } 41 | return values; 42 | }, 43 | [[]] 44 | ); 45 | 46 | // responsive values 47 | const responsiveValues = allValues.filter(isResponsiveValue); 48 | 49 | // non-responsive values 50 | const nonResponsiveValues = allValues.filter(isNonResponsiveValue); 51 | 52 | // for each responsive value 53 | responsiveValues.forEach( 54 | (value) => { 55 | const prop = value.pop().trim() 56 | const media = value.join('').trim() 57 | 58 | // add new @media at-rule, rule, and declaration to list 59 | newAtRules.push( 60 | postcss.atRule({ 61 | name: 'media', 62 | params: media, 63 | source: decl.source 64 | }).append( 65 | rule.clone({ 66 | raws: { 67 | before: decl.raws.before 68 | } 69 | }).removeAll().append( 70 | decl.clone({ 71 | value: prop, 72 | raws: {} 73 | }) 74 | ) 75 | ) 76 | ); 77 | } 78 | ); 79 | 80 | // if there non-responsive values 81 | if (nonResponsiveValues.length) { 82 | // re-assign the first non-responsive value to the declaration 83 | node.type = 'word'; 84 | node.value = nonResponsiveValues.shift().join(''); 85 | } else { 86 | // otherwise, remove the node 87 | node.remove() 88 | } 89 | } 90 | } 91 | ); 92 | 93 | const newValue = valueAST.toString() 94 | 95 | // if the value has changed 96 | if (decl.value !== newValue) { 97 | // if the new value is empty 98 | if (!newValue) { 99 | // remove the declaration 100 | decl.remove(); 101 | } else { 102 | // otherwise, update the declaration value 103 | decl.value = newValue.trim(); 104 | } 105 | } 106 | } 107 | ); 108 | 109 | if (newAtRules.length) { 110 | rule.parent.insertAfter(rule, newAtRules); 111 | 112 | if (!rule.nodes.length) { 113 | rule.remove(); 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | module.exports.postcss = true; 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-media-fn", 3 | "version": "3.0.0", 4 | "description": "Use `media()` to assign responsive values", 5 | "author": "Jonathan Neal ", 6 | "license": "CC0-1.0", 7 | "repository": "csstools/postcss-media-fn", 8 | "homepage": "https://github.com/csstools/postcss-media-fn#readme", 9 | "bugs": "https://github.com/csstools/postcss-media-fn/issues", 10 | "main": "index.js", 11 | "files": [ 12 | "example.js", 13 | "index.js" 14 | ], 15 | "scripts": { 16 | "lint:js": "eslint . --cache", 17 | "lint": "npm run lint:js", 18 | "pretest": "npm run lint", 19 | "test": "postcss-tape", 20 | "prepublishOnly": "npm test" 21 | }, 22 | "engines": { 23 | "node": "^10 || ^12 || >=14" 24 | }, 25 | "peerDependencies": { 26 | "postcss": "^8.3" 27 | }, 28 | "dependencies": { 29 | "postcss-values-parser": "^6.0.1" 30 | }, 31 | "devDependencies": { 32 | "eslint": "7.32.0", 33 | "postcss": "8.3.11", 34 | "postcss-tape": "6.0.1" 35 | }, 36 | "eslintConfig": { 37 | "env": { 38 | "es6": true, 39 | "node": true 40 | }, 41 | "extends": "eslint:recommended" 42 | }, 43 | "keywords": [ 44 | "postcss", 45 | "css", 46 | "postcss-plugin", 47 | "media", 48 | "function", 49 | "method", 50 | "responsive", 51 | "query", 52 | "custom", 53 | "multiple", 54 | "values" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /test/basic.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: media( 3 | 10px, 4 | (min-width: 600px) 20px, 5 | (min-width: 800px) 30px, 6 | (min-width: 1000px) 40px, 7 | (min-width: 1200px) 50px, 8 | (min-width: 1400px) 60px 9 | ); 10 | line-height: media(1); 11 | margin-bottom: media( 12 | (min-width: 800px) 20px, 13 | (min-width: 1400px) 40px 14 | ); 15 | } 16 | 17 | h2 { 18 | font-size: 10px; 19 | } 20 | 21 | h3 { 22 | font-size: media( 23 | (min-width: 600px) 20px, 24 | (min-width: 800px) 40px 25 | ); 26 | } 27 | 28 | h4 { 29 | font-size: 10px; 30 | } 31 | 32 | h5 { 33 | font-size: media( 34 | 20px, 35 | 40px 36 | ); 37 | } 38 | 39 | h6 { 40 | color: media( 41 | rgba(0, 0, 0), 42 | rgba(1, 2, 3) 43 | ); 44 | } 45 | 46 | div { 47 | color: media( 48 | rgba(0, 0, 0), 49 | (min-width: 600px) rgba(1, 2, 3) 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /test/basic.expect.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: 10px; 3 | line-height: 1; 4 | } 5 | @media (min-width: 600px) { 6 | h1 { 7 | font-size: 20px; 8 | } 9 | } 10 | @media (min-width: 800px) { 11 | h1 { 12 | font-size: 30px; 13 | } 14 | } 15 | @media (min-width: 1000px) { 16 | h1 { 17 | font-size: 40px; 18 | } 19 | } 20 | @media (min-width: 1200px) { 21 | h1 { 22 | font-size: 50px; 23 | } 24 | } 25 | @media (min-width: 1400px) { 26 | h1 { 27 | font-size: 60px; 28 | } 29 | } 30 | @media (min-width: 800px) { 31 | h1 { 32 | margin-bottom: 20px; 33 | } 34 | } 35 | @media (min-width: 1400px) { 36 | h1 { 37 | margin-bottom: 40px; 38 | } 39 | } 40 | 41 | h2 { 42 | font-size: 10px; 43 | } 44 | 45 | @media (min-width: 600px) { 46 | h3 { 47 | font-size: 20px; 48 | } 49 | } 50 | 51 | @media (min-width: 800px) { 52 | h3 { 53 | font-size: 40px; 54 | } 55 | } 56 | 57 | h4 { 58 | font-size: 10px; 59 | } 60 | 61 | h5 { 62 | font-size: 20px; 63 | } 64 | 65 | h6 { 66 | color: rgba(0, 0, 0); 67 | } 68 | 69 | div { 70 | color: rgba(0, 0, 0); 71 | } 72 | 73 | @media (min-width: 600px) { 74 | div { 75 | color: rgba(1, 2, 3); 76 | } 77 | } 78 | --------------------------------------------------------------------------------