├── .gitignore ├── test ├── fixtures │ ├── with-content │ │ ├── actual.css │ │ └── expected.css │ ├── default-options │ │ ├── actual.css │ │ └── expected.css │ ├── given-options │ │ ├── actual.css │ │ └── expected.css │ ├── filter-file-option │ │ ├── actual.css │ │ └── expected.css │ └── filter-rule-option │ │ ├── actual.css │ │ └── expected.css └── index.js ├── .travis.yml ├── .eslintrc ├── package.json ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /test/fixtures/with-content/actual.css: -------------------------------------------------------------------------------- 1 | .hero:after { 2 | width: 50vw; 3 | content: 'this is a hero'; 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | 5 | script: 6 | - npm run lint 7 | - npm test 8 | -------------------------------------------------------------------------------- /test/fixtures/with-content/expected.css: -------------------------------------------------------------------------------- 1 | .hero:after { 2 | width: 50vw; 3 | content: 'this is a hero'; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/default-options/actual.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | width: 50vw; 3 | height: 50vh; 4 | margin-top: calc((360/1080) * 100vh); 5 | padding: 1rem; 6 | color: green; 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/given-options/actual.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | width: 50vw; 3 | height: 50vh; 4 | margin-top: calc((360/1080) * 100vh); 5 | padding: 1rem; 6 | color: green; 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/filter-file-option/actual.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | width: 50vw; 3 | height: 50vh; 4 | margin-top: calc((360/1080) * 100vh); 5 | padding: 1rem; 6 | color: green; 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/filter-file-option/expected.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | width: 50vw; 3 | height: 50vh; 4 | margin-top: calc((360/1080) * 100vh); 5 | padding: 1rem; 6 | color: green; 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/filter-rule-option/actual.css: -------------------------------------------------------------------------------- 1 | .icon-book::before { 2 | width: 2vw; 3 | height: 2vh; 4 | } 5 | .icon-book::after { 6 | content: 'book'; 7 | width: 2vw; 8 | height: 2vh; 9 | } 10 | -------------------------------------------------------------------------------- /test/fixtures/given-options/expected.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | width: 50vw; 3 | height: 50vh; 4 | margin-top: calc((360/1080) * 100vh); 5 | padding: 1rem; 6 | color: green; 7 | content: 'viewport-units-buggyfill; margin-top: calc((360/1080) * 100vh)'; 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/filter-rule-option/expected.css: -------------------------------------------------------------------------------- 1 | .icon-book::before { 2 | width: 2vw; 3 | height: 2vh; 4 | content: 'viewport-units-buggyfill; width: 2vw; height: 2vh'; 5 | } 6 | .icon-book::after { 7 | content: 'book'; 8 | width: 2vw; 9 | height: 2vh; 10 | } 11 | -------------------------------------------------------------------------------- /test/fixtures/default-options/expected.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | width: 50vw; 3 | height: 50vh; 4 | margin-top: calc((360/1080) * 100vh); 5 | padding: 1rem; 6 | color: green; 7 | content: 'viewport-units-buggyfill; width: 50vw; height: 50vh; margin-top: calc((360/1080) * 100vh)'; 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "rules": { 8 | "comma-dangle": ["error", { 9 | "arrays": "always-multiline", 10 | "objects": "always-multiline", 11 | "functions": "ignore", 12 | }] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-viewport-units", 3 | "version": "0.1.6", 4 | "description": "Automatically append `content` property for viewport-units-buggyfill", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint index.js test/", 8 | "test": "mocha test/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/springuper/postcss-viewport-units.git" 13 | }, 14 | "keywords": [ 15 | "postcss", 16 | "viewport-units-buggyfill", 17 | "content" 18 | ], 19 | "author": "springuper@gmail.com", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/springuper/postcss-viewport-units/issues" 23 | }, 24 | "homepage": "https://github.com/springuper/postcss-viewport-units#readme", 25 | "dependencies": { 26 | "postcss": "^5.2.8" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^3.12.2", 30 | "eslint-config-airbnb-base": "^11.0.0", 31 | "eslint-plugin-import": "^2.2.0", 32 | "mocha": "^3.2.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | 3 | const regViewportUnit = /\d(vw|vh|vmax|vmin)\b/; 4 | const CONTENT_PROP = 'content'; 5 | const PREFIX = 'viewport-units-buggyfill'; 6 | 7 | module.exports = postcss.plugin('postcss-viewport-units', options => (root, result) => { 8 | const opts = options || {}; 9 | const test = opts.test || (value => regViewportUnit.test(value)); 10 | const { silence = false } = opts; 11 | 12 | if (opts.filterFile && !opts.filterFile(root.source.input.file || '')) return; 13 | 14 | root.walkRules((rule) => { 15 | let hasContent; 16 | const viewportUnitDecls = []; 17 | if (opts.filterRule && !opts.filterRule(rule)) return; 18 | 19 | rule.nodes.slice(0).forEach((decl) => { 20 | if (decl.prop === CONTENT_PROP && !hasContent) { 21 | hasContent = true; 22 | } 23 | 24 | if (test(decl.value)) { 25 | if (opts.onlyCalc && decl.value.indexOf('calc') !== 0) return; 26 | viewportUnitDecls.push(`${decl.prop}: ${decl.value}`); 27 | } 28 | }); 29 | 30 | if (viewportUnitDecls.length === 0) return; 31 | 32 | if (hasContent) { 33 | if (!silence) { 34 | rule.warn(result, `'${rule.selector}' already has a 'content' property, give up to overwrite it.`); 35 | } 36 | } else { 37 | rule.append({ 38 | prop: CONTENT_PROP, 39 | value: `'${[PREFIX].concat(viewportUnitDecls).join('; ')}'`, 40 | }); 41 | } 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-viewport-units 2 | 3 | [![Build Status](https://travis-ci.org/springuper/postcss-viewport-units.svg?branch=master)](https://travis-ci.org/springuper/postcss-viewport-units) 4 | [![npm version](https://badge.fury.io/js/postcss-viewport-units.svg)](https://badge.fury.io/js/postcss-viewport-units) 5 | 6 | Automatically append `content` property for [viewport-units-buggyfill](https://github.com/rodneyrehm/viewport-units-buggyfill). 7 | 8 | ## Install 9 | 10 | ```bash 11 | $ npm install postcss-viewport-units 12 | ``` 13 | 14 | ## Usage 15 | 16 | See [PostCSS](https://github.com/postcss/postcss#usage) usage section for detail. 17 | 18 | ### Options 19 | 20 | `onlyCalc`(`[Boolean]`): if `ture`, only process `calc` values. it's `false` by default, which means, all values including `vw` `vh` `vmin` `vmax` are processed. 21 | 22 | `test`(`[Function]`): used to judge whether current value of declaration should be considered including viewport units, the default is `value => /\d(vw|vh|vmax|vmin)\b/.test(value)`. 23 | 24 | `filterFile`(`[Function]`): used to filter out files which need be processed, you can refer to [test case](test/index.js) to see how it works. 25 | 26 | `filterRule`(`[Function]`): used to filter out rules which need be processed, you can refer to [test case](test/index.js) to see how it works. 27 | 28 | `silence`(`[Boolean]`): if `true`, will not print warning even though there is a `content` property. it's `false` by default. 29 | 30 | ## License 31 | 32 | MIT. 33 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const assert = require('assert'); 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const plugin = require('../'); 6 | 7 | function compare(filename, warnings, options) { 8 | const actualFilePath = path.resolve(__dirname, `fixtures/${filename}/actual.css`); 9 | const actual = fs.readFileSync(actualFilePath, { encoding: 'utf8' }); 10 | const expected = fs.readFileSync( 11 | path.resolve(__dirname, `fixtures/${filename}/expected.css`), 12 | { encoding: 'utf8' } 13 | ); 14 | 15 | return postcss([plugin(options)]) 16 | .process(actual, { from: actualFilePath }) 17 | .then((result) => { 18 | assert.equal(result.css, expected); 19 | if (warnings) { 20 | assert.deepEqual(result.warnings().map(item => item.text), warnings); 21 | } 22 | if (options && options.silence === true) { 23 | assert.equal(result.warnings().length, 0); 24 | } 25 | return result; 26 | }); 27 | } 28 | 29 | describe('postcss-viewport-units', () => { 30 | it('should automatically append `content` property', () => compare('default-options')); 31 | 32 | it('should only process `calc` value if `options.onlyCalc` is true', 33 | () => compare('given-options', null, { onlyCalc: true })); 34 | 35 | it('should give a warning if there is already a `content` property', () => ( 36 | compare('with-content', [ 37 | '\'.hero:after\' already has a \'content\' property, give up to overwrite it.', 38 | ]) 39 | )); 40 | 41 | it('should not give a warning when `options.silence` is true even though there is already a `content` property', 42 | () => compare('with-content', null, { silence: true })); 43 | 44 | it('should not continue to process if `options.filterFile` returns `false`', () => compare( 45 | 'filter-file-option', 46 | null, 47 | { filterFile: file => !file.includes('filter-file-option') } 48 | )); 49 | 50 | it('should only continue to process valid rules if `options.filterRule` is specified', () => compare( 51 | 'filter-rule-option', 52 | null, 53 | { filterRule: rule => !rule.selector.includes('::after') } 54 | )); 55 | }); 56 | --------------------------------------------------------------------------------