├── .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]
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 |
--------------------------------------------------------------------------------