├── CNAME ├── .gitignore ├── .prettierignore ├── src ├── pixel-unit-regexp.ts ├── utils.ts ├── types.ts ├── prop-list-matcher.ts └── index.ts ├── .editorconfig ├── .prettierrc.json ├── jest.config.js ├── example ├── index.js ├── main-viewport.css └── main.css ├── CONTRIBUTING.md ├── .fatherrc.ts ├── tsconfig.json ├── package.json ├── README.md └── spec └── px-to-viewport.spec.ts /CNAME: -------------------------------------------------------------------------------- 1 | v8-doc.com -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .changelog 3 | /lib 4 | dist 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | package.json 3 | 4 | # fixtures 5 | **/fixtures/** 6 | 7 | # templates 8 | **/templates/** 9 | -------------------------------------------------------------------------------- /src/pixel-unit-regexp.ts: -------------------------------------------------------------------------------- 1 | function getUnitRegexp(unit: string) { 2 | return new RegExp( 3 | `"[^"]+"|'[^']+'|url\\([^\\)]+\\)|(\\d*\\.?\\d+)${unit}`, 4 | 'g', 5 | ); 6 | } 7 | 8 | export { getUnitRegexp }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "semi": true, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "arrowParens": "always", 8 | "overrides": [ 9 | { 10 | "files": ".prettierrc", 11 | "options": { 12 | "parser": "json" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // module.exports = { 2 | // moduleNameMapper(memo) { 3 | // return Object.assign(memo, { 4 | // '^react$': require.resolve('react'), 5 | // '^react-dom$': require.resolve('react-dom'), 6 | // }); 7 | // }, 8 | // }; 9 | 10 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 11 | module.exports = { 12 | preset: 'ts-jest', 13 | testEnvironment: 'node', 14 | }; 15 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var postcss = require('postcss'); 3 | var { postcssPxToViewport } = require('../lib'); 4 | var css = fs.readFileSync('main.css', 'utf8'); 5 | 6 | var processedCss = postcss(postcssPxToViewport({})).process(css).css; 7 | 8 | fs.writeFile('main-viewport.css', processedCss, function(err) { 9 | if (err) { 10 | throw err; 11 | } 12 | console.log('File with viewport units written.'); 13 | }); 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to plugin 2 | 3 | ## Set up 4 | 5 | Install dev deps after git clone the repo. 6 | 7 | ```bash 8 | # npm is not allowed. 9 | $ yarn 10 | ``` 11 | 12 | ## Build 13 | 14 | Transform with babel and rollup. 15 | 16 | ```bash 17 | $ yarn build 18 | 19 | # Build and monitor file changes 20 | $ yarn build --watch 21 | 22 | ``` 23 | 24 | ## Dev Plugin 25 | 26 | ```bash 27 | # This Step must only be executed in Build 28 | $ yarn dev 29 | ``` 30 | 31 | ## Debug 32 | 33 | TODO 34 | 35 | ## Test 36 | 37 | ```bash 38 | $ yarn test 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'father'; 2 | 3 | export default defineConfig({ 4 | cjs: { 5 | output: 'lib', 6 | platform: 'node', 7 | transformer: 'babel', 8 | }, 9 | }); 10 | 11 | // export default [ 12 | // { 13 | // target: 'node', 14 | // cjs: { 15 | // output: 'lib', 16 | // platform: 'node', 17 | // transformer: 'babel', 18 | // }, 19 | // disableTypeCheck: false, 20 | // }, 21 | // ]; 22 | // export default [ 23 | // { 24 | // target: 'node', 25 | // cjs: { type: 'babel', lazy: true }, 26 | // disableTypeCheck: false, 27 | // }, 28 | // ]; 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitReturns": true, 12 | "suppressImplicitAnyIndexErrors": true, 13 | "sourceMap": true, 14 | "allowSyntheticDefaultImports": true, 15 | "declaration": true, 16 | "skipLibCheck": true, 17 | "jsx": "react-jsx", 18 | "allowJs": true, 19 | "baseUrl": "./" 20 | }, 21 | "include": ["src/**/*.tsx", "src/**/*.ts"], 22 | "exclude": ["node_modules", "lib", "es", "dist", "typings"] 23 | } 24 | -------------------------------------------------------------------------------- /example/main-viewport.css: -------------------------------------------------------------------------------- 1 | .class { 2 | margin: -3.125vw 0.5vh; 3 | padding: 5vmin 2.96875vw 1px; 4 | border: 0.9375vw solid black; 5 | border-bottom-width: 1px; 6 | font-size: 4.375vw; 7 | line-height: 6.25vw; 8 | } 9 | .class2 { 10 | border: 1px solid black; 11 | margin-bottom: 1px; 12 | font-size: 6.25vw; 13 | line-height: 9.375vw; 14 | } 15 | @media (min-width: 750px) { 16 | .class3 { 17 | font-size: 16px; 18 | line-height: 22px; 19 | } 20 | } 21 | 22 | .class4-ignore::before { 23 | content: ''; 24 | width: 10px; 25 | padding: 3.125vw; 26 | height: 10px; 27 | border: solid 2px #000; 28 | } 29 | .class5-bad-ignore { 30 | /* px-to-viewport-ignore */ 31 | width: 3.125vw; 32 | /* px-to-viewport-ignore */ 33 | height: 3.125vw; 34 | /* px-to-viewport-ignore-next */ 35 | } 36 | 37 | @keyframes move { 38 | 0% { 39 | transform: translate(0, 0); 40 | } 41 | /* px-to-viewport-ignore-next */ 42 | 50% { 43 | transform: translate(10px, -10px); 44 | } 45 | 100% { 46 | transform: translate(10px, -10px); /* px-to-viewport-ignore */ 47 | } 48 | } 49 | 50 | /* 51 | .class { 52 | font-size: 16px; 53 | } 54 | */ 55 | -------------------------------------------------------------------------------- /example/main.css: -------------------------------------------------------------------------------- 1 | .class { 2 | margin: -10px 0.5vh; 3 | padding: 5vmin 9.5px 1px; 4 | border: 3px solid black; 5 | border-bottom-width: 1px; 6 | font-size: 14px; 7 | line-height: 20px; 8 | } 9 | .class2 { 10 | border: 1px solid black; 11 | margin-bottom: 1px; 12 | font-size: 20px; 13 | line-height: 30px; 14 | } 15 | @media (min-width: 750px) { 16 | .class3 { 17 | font-size: 16px; 18 | line-height: 22px; 19 | } 20 | } 21 | 22 | .class4-ignore::before { 23 | content: ''; 24 | /* px-to-viewport-ignore-next */ 25 | width: 10px; 26 | padding: 10px; 27 | height: 10px; /* px-to-viewport-ignore */ 28 | border: solid 2px #000; /* px-to-viewport-ignore */ 29 | } 30 | .class5-bad-ignore { 31 | /* px-to-viewport-ignore */ 32 | width: 10px; 33 | /* px-to-viewport-ignore */ 34 | height: 10px; 35 | /* px-to-viewport-ignore-next */ 36 | } 37 | 38 | @keyframes move { 39 | 0% { 40 | transform: translate(0, 0); 41 | } 42 | /* px-to-viewport-ignore-next */ 43 | 50% { 44 | transform: translate(10px, -10px); 45 | } 46 | 100% { 47 | transform: translate(10px, -10px); /* px-to-viewport-ignore */ 48 | } 49 | } 50 | 51 | /* 52 | .class { 53 | font-size: 16px; 54 | } 55 | */ 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-px-to-viewport-8-plugin", 3 | "version": "1.2.5", 4 | "main": "lib/index.js", 5 | "module": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "description": "css-vw", 8 | "authors": { 9 | "name": "lkxian888", 10 | "email": "lkxian888@163.com" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/lkxian888/postcss-px-to-viewport-8-plugin" 15 | }, 16 | "scripts": { 17 | "build": "father build", 18 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 19 | "test": "jest spec/*.spec.js" 20 | }, 21 | "lint-staged": { 22 | "*.{js,jsx,less,md,json}": [ 23 | "prettier --write", 24 | "git add" 25 | ] 26 | }, 27 | "devDependencies": { 28 | "@types/jest": "^25.1.3", 29 | "@types/node": "^13.7.7", 30 | "@types/object-assign": "^4.0.30", 31 | "father": "^4.1.3", 32 | "father-build": "^1.17.2", 33 | "jest": "^25.4.0", 34 | "lint-staged": "^10.0.8", 35 | "postcss": "^8.3.8", 36 | "prettier": "^1.19.1", 37 | "ts-jest": "^29.0.5", 38 | "yorkie": "^2.0.0" 39 | }, 40 | "gitHooks": { 41 | "pre-commit": "lint-staged" 42 | }, 43 | "files": [ 44 | "lib", 45 | "es", 46 | "README.md" 47 | ], 48 | "dependencies": { 49 | "object-assign": "^4.1.1" 50 | }, 51 | "directories": { 52 | "example": "example" 53 | }, 54 | "keywords": [ 55 | "postcss-px-to-viewport-8-plugin", 56 | "viewport", 57 | "postcss", 58 | "postcss-plugin", 59 | "css", 60 | "px", 61 | "vw", 62 | "vh", 63 | "vmin", 64 | "vmax" 65 | ], 66 | "author": "lkxian888", 67 | "license": "MIT" 68 | } 69 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { OptionType, ParentExtendType } from './types'; 2 | 3 | export const getUnit = (prop: string | string[], opts: OptionType) => { 4 | return prop.indexOf('font') === -1 ? opts.viewportUnit : opts.fontViewportUnit; 5 | }; 6 | 7 | export const createPxReplace = ( 8 | opts: OptionType, 9 | viewportUnit: string | number, 10 | viewportSize: number, 11 | ) => { 12 | return function (m: any, $1: string) { 13 | if (!$1) return m; 14 | const pixels = parseFloat($1); 15 | if (pixels <= opts.minPixelValue!) return m; 16 | const parsedVal = toFixed((pixels / viewportSize) * 100, opts.unitPrecision!); 17 | return parsedVal === 0 ? '0' : `${parsedVal}${viewportUnit}`; 18 | }; 19 | }; 20 | 21 | export const toFixed = (number: number, precision: number) => { 22 | const multiplier = Math.pow(10, precision + 1); 23 | const wholeNumber = Math.floor(number * multiplier); 24 | return (Math.round(wholeNumber / 10) * 10) / multiplier; 25 | }; 26 | 27 | export const blacklistedSelector = (blacklist: string[], selector: string) => { 28 | if (typeof selector !== 'string') return; 29 | return blacklist.some((regex) => { 30 | if (typeof regex === 'string') return selector.indexOf(regex) !== -1; 31 | return selector.match(regex); 32 | }); 33 | }; 34 | 35 | export const isExclude = (reg: RegExp, file: string) => { 36 | if (Object.prototype.toString.call(reg) !== '[object RegExp]') { 37 | throw new Error('options.exclude should be RegExp.'); 38 | } 39 | return file.match(reg) !== null; 40 | }; 41 | 42 | export const declarationExists = (decls: ParentExtendType[], prop: string, value: string) => { 43 | return decls?.some((decl: ParentExtendType) => { 44 | return decl.prop === prop && decl.value === value; 45 | }); 46 | }; 47 | 48 | export const validateParams = (params: string, mediaQuery: boolean) => { 49 | return !params || (params && mediaQuery); 50 | }; 51 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Rule } from 'postcss'; 2 | 3 | export type OptionType = { 4 | // /* px-to-viewport-ignore-next */ 在另一行上,阻止下一行上的转换 5 | // /* px-to-viewport-ignore */ 在右边的属性之后,防止在同一行上转换 6 | 7 | /** 8 | * 需要转换的单位,默认为"px" 9 | */ 10 | unitToConvert?: string; 11 | /** 12 | * 设计稿的视口宽度 13 | * 支持传入函数,函数的参数为当前处理的文件路径 14 | */ 15 | viewportWidth?: number | ((filePath: string) => number|undefined); 16 | /** 17 | * 设计稿的视口高度 18 | */ 19 | viewportHeight?: number; // not now used; TODO: need for different units and math for different properties 20 | /** 21 | * 单位转换后保留的精度 22 | */ 23 | unitPrecision?: number; 24 | /** 25 | * 希望使用的视口单位 26 | */ 27 | viewportUnit?: string; 28 | /** 29 | * 字体使用的视口单位 30 | */ 31 | fontViewportUnit?: string; // vmin is more suitable. 32 | /** 33 | * 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位 34 | * 如果传入的值为字符串的话,只要选择器中含有传入值就会被匹配:例如 selectorBlackList 为 ['body'] 的话, 那么 .body-class 就会被忽略 35 | * 如果传入的值为正则表达式的话,那么就会依据CSS选择器是否匹配该正则:例如 selectorBlackList 为 [/^body$/] , 那么 body 会被忽略,而 .body 不会 36 | */ 37 | selectorBlackList?: string[]; 38 | /** 39 | * 能转化为vw的属性列表 40 | * 传入特定的CSS属性 41 | * 可以传入通配符""去匹配所有属性,例如:[''] 42 | * 在属性的前或后添加"*",可以匹配特定的属性. (例如['position'] 会匹配 background-position-y) 43 | * 在特定属性前加 "!",将不转换该属性的单位 . 例如: ['*', '!letter-spacing'],将不转换letter-spacing 44 | * "!" 和 ""可以组合使用, 例如: ['', '!font*'],将不转换font-size以及font-weight等属性 45 | */ 46 | propList?: string[]; 47 | /** 48 | * 设置最小的转换数值,如果为1的话,只有大于1的值会被转换 49 | */ 50 | minPixelValue?: number; 51 | /** 52 | * 媒体查询里的单位是否需要转换单位 53 | */ 54 | mediaQuery?: boolean; 55 | /** 56 | * 是否直接更换属性值,而不添加备用属性 57 | */ 58 | replace?: boolean; 59 | /** 60 | * 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件 61 | * 如果值是一个正则表达式,那么匹配这个正则的文件会被忽略 62 | * 如果传入的值是一个数组,那么数组里的值必须为正则 63 | */ 64 | exclude?: RegExp | RegExp[]; 65 | /** 66 | * 如果设置了include,那将只有匹配到的文件才会被转换 67 | * 如果值是一个正则表达式,将包含匹配的文件,否则将排除该文件 68 | * 如果传入的值是一个数组,那么数组里的值必须为正则 69 | */ 70 | include?: RegExp | RegExp[]; 71 | /** 72 | * 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape) 73 | */ 74 | landscape?: boolean; 75 | /** 76 | * 横屏时使用的单位 77 | */ 78 | landscapeUnit?: string; 79 | /** 80 | * 横屏时使用的视口宽度 81 | * 支持传入函数,函数的参数为当前处理的文件路径 82 | */ 83 | landscapeWidth?: number | ((filePath: string) => number|undefined); 84 | 85 | }; 86 | 87 | export type ParentExtendType = { prop: string; value: string; params: string }; 88 | 89 | export type ParentType = { 90 | parent: Rule['parent'] & ParentExtendType; 91 | }; 92 | 93 | export type RuleType = Omit & ParentType; 94 | -------------------------------------------------------------------------------- /src/prop-list-matcher.ts: -------------------------------------------------------------------------------- 1 | const filterPropList = { 2 | exact(list: string[]) { 3 | return list.filter((m: string) => { 4 | return m.match(/^[^\*\!]+$/); 5 | }); 6 | }, 7 | contain(list: string[]) { 8 | return list 9 | .filter(m => { 10 | return m.match(/^\*.+\*$/); 11 | }) 12 | .map(m => { 13 | return m.substr(1, m.length - 2); 14 | }); 15 | }, 16 | endWith(list: string[]) { 17 | return list 18 | .filter(m => { 19 | return m.match(/^\*[^\*]+$/); 20 | }) 21 | .map(m => { 22 | return m.substr(1); 23 | }); 24 | }, 25 | startWith(list: string[]) { 26 | return list 27 | .filter(m => { 28 | return m.match(/^[^\*\!]+\*$/); 29 | }) 30 | .map(m => { 31 | return m.substr(0, m.length - 1); 32 | }); 33 | }, 34 | notExact(list: string[]) { 35 | return list 36 | .filter(m => { 37 | return m.match(/^\![^\*].*$/); 38 | }) 39 | .map(m => { 40 | return m.substr(1); 41 | }); 42 | }, 43 | notContain(list: string[]) { 44 | return list 45 | .filter(m => { 46 | return m.match(/^\!\*.+\*$/); 47 | }) 48 | .map(m => { 49 | return m.substr(2, m.length - 3); 50 | }); 51 | }, 52 | notEndWith(list: string[]) { 53 | return list 54 | .filter(m => { 55 | return m.match(/^\!\*[^\*]+$/); 56 | }) 57 | .map(m => { 58 | return m.substr(2); 59 | }); 60 | }, 61 | notStartWith(list: string[]) { 62 | return list 63 | .filter(m => { 64 | return m.match(/^\![^\*]+\*$/); 65 | }) 66 | .map(m => { 67 | return m.substr(1, m.length - 2); 68 | }); 69 | }, 70 | }; 71 | 72 | const createPropListMatcher = (propList: string[]) => { 73 | const hasWild = propList.indexOf('*') > -1; 74 | const matchAll = hasWild && propList.length === 1; 75 | const lists = { 76 | exact: filterPropList.exact(propList), 77 | contain: filterPropList.contain(propList), 78 | startWith: filterPropList.startWith(propList), 79 | endWith: filterPropList.endWith(propList), 80 | notExact: filterPropList.notExact(propList), 81 | notContain: filterPropList.notContain(propList), 82 | notStartWith: filterPropList.notStartWith(propList), 83 | notEndWith: filterPropList.notEndWith(propList), 84 | }; 85 | return function(prop: string) { 86 | if (matchAll) return true; 87 | return ( 88 | (hasWild || 89 | lists.exact.indexOf(prop) > -1 || 90 | lists.contain.some(m => { 91 | return prop.indexOf(m) > -1; 92 | }) || 93 | lists.startWith.some(m => { 94 | return prop.indexOf(m) === 0; 95 | }) || 96 | lists.endWith.some(m => { 97 | return prop.indexOf(m) === prop.length - m.length; 98 | })) && 99 | !( 100 | lists.notExact.indexOf(prop) > -1 || 101 | lists.notContain.some(m => { 102 | return prop.indexOf(m) > -1; 103 | }) || 104 | lists.notStartWith.some(m => { 105 | return prop.indexOf(m) === 0; 106 | }) || 107 | lists.notEndWith.some(m => { 108 | return prop.indexOf(m) === prop.length - m.length; 109 | }) 110 | ) 111 | ); 112 | }; 113 | }; 114 | export { filterPropList, createPropListMatcher }; 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-px-to-viewport-8-plugin 2 | 3 | 将 px 单位转换为视口单位的 (vw, vh, vmin, vmax) 的 [PostCSS](https://github.com/postcss/postcss) 插件. 4 | 5 | ## 问题 6 | 7 | 使用 [postcss-px-to-viewport](https://github.com/evrone/postcss-px-to-viewport) 控制台报以下代码 8 | 9 | ```js 10 | postcss-px-to-viewport: postcss.plugin was deprecated. Migration guide: https://evilmartians.com/chronicles/postcss-8-plugin-migration 11 | 12 | ``` 13 | 14 | ## 解决 15 | 16 | `postcss-px-to-viewport` 替换 `postcss-px-to-viewport-8-plugin` 17 | 18 | 注意对应库版本 19 | 20 | ```js 21 | "postcss": "^8.3.8", // 8.0.0版本都不会转单位 22 | "postcss-loader": "^6.1.1", 23 | ``` 24 | 25 | ## 简介 26 | 27 | 如果你的样式需要做根据视口大小来调整宽度,这个脚本可以将你 CSS 中的 px 单位转化为 vw,1vw 等于 1/100 视口宽度。 28 | 29 | ## 输入 30 | 31 | ```css 32 | .class { 33 | margin: -10px 0.5vh; 34 | padding: 5vmin 9.5px 1px; 35 | border: 3px solid black; 36 | border-bottom-width: 1px; 37 | font-size: 14px; 38 | line-height: 20px; 39 | } 40 | 41 | .class2 { 42 | padding-top: 10px; /* px-to-viewport-ignore */ 43 | /* px-to-viewport-ignore-next */ 44 | padding-bottom: 10px; 45 | /* Any other comment */ 46 | border: 1px solid black; 47 | margin-bottom: 1px; 48 | font-size: 20px; 49 | line-height: 30px; 50 | } 51 | 52 | @media (min-width: 750px) { 53 | .class3 { 54 | font-size: 16px; 55 | line-height: 22px; 56 | } 57 | } 58 | ``` 59 | 60 | ## 输出 61 | 62 | ```css 63 | .class { 64 | margin: -3.125vw 0.5vh; 65 | padding: 5vmin 2.96875vw 1px; 66 | border: 0.9375vw solid black; 67 | border-bottom-width: 1px; 68 | font-size: 4.375vw; 69 | line-height: 6.25vw; 70 | } 71 | 72 | .class2 { 73 | padding-top: 10px; 74 | padding-bottom: 10px; 75 | /* Any other comment */ 76 | border: 1px solid black; 77 | margin-bottom: 1px; 78 | font-size: 6.25vw; 79 | line-height: 9.375vw; 80 | } 81 | 82 | @media (min-width: 750px) { 83 | .class3 { 84 | font-size: 16px; 85 | line-height: 22px; 86 | } 87 | } 88 | ``` 89 | 90 | ## 安装 91 | 92 | ```js 93 | 94 | npm install postcss-px-to-viewport-8-plugin -D 95 | or 96 | yarn add postcss-px-to-viewport-8-plugin -D 97 | ``` 98 | 99 | ## 配置参数使用与 [postcss-px-to-viewport](https://www.npmjs.com/package/postcss-px-to-viewport) 一致 100 | 101 | **默认选项:** 102 | 103 | ``` 104 | { 105 | unitToConvert: 'px', 106 | viewportWidth: 320, 107 | unitPrecision: 5, 108 | propList: ['*'], 109 | viewportUnit: 'vw', 110 | fontViewportUnit: 'vw', 111 | selectorBlackList: [], 112 | minPixelValue: 1, 113 | mediaQuery: false, 114 | replace: true, 115 | exclude: [], 116 | landscape: false, 117 | landscapeUnit: 'vw', 118 | landscapeWidth: 568 119 | } 120 | ``` 121 | 122 | ## API 说明 123 | 124 | | 参数 | 说明 | 类型 | 默认值 | 125 | | :-- | --- | --- | --- | 126 | | `unitToConvert` | 需要转换的单位,默认为 px | `string` | px | 127 | | `viewportWidth` | 设计稿的视口宽度,如传入函数,函数的参数为当前处理的文件路径,函数返回 `undefind` 跳过转换 | `number \| Function` | 320 | 128 | | `unitPrecision` | 单位转换后保留的精度 | `number` | 5 | 129 | | `propList` | 能转化为 vw 的属性列表 | `string[]` | ['*'] | 130 | | `viewportUnit` | 希望使用的视口单位 | `string` | vw | 131 | | `fontViewportUnit` | 字体使用的视口单位 | `string` | vw | 132 | | `selectorBlackList` | 需要忽略的 CSS 选择器,不会转为视口单位,使用原有的 px 等单位 | `string[]` | [] | 133 | | `minPixelValue` | 设置最小的转换数值,如果为 1 的话,只有大于 1 的值会被转换 | `number` | 1 | 134 | | `mediaQuery` | 媒体查询里的单位是否需要转换单位 | `boolean` | false | 135 | | `replace` | 是否直接更换属性值,而不添加备用属性 | `boolean` | true | 136 | | `landscape` | 是否添加根据 `landscapeWidth` 生成的媒体查询条件 `@media (orientation: landscape)` | `boolean` | false | 137 | | `landscapeUnit` | 横屏时使用的单位 | `string` | vw | 138 | | `landscapeWidth` | 横屏时使用的视口宽度,,如传入函数,函数的参数为当前处理的文件路径,函数返回 `undefind` 跳过转换 | `number` | 568 | 139 | | `exclude` | 忽略某些文件夹下的文件或特定文件,例如 node_modules 下的文件,如果值是一个正则表达式,那么匹配这个正则的文件会被忽略,如果传入的值是一个数组,那么数组里的值必须为正则 | `Regexp` | undefined | 140 | | `include` | 需要转换的文件,例如只转换 'src/mobile' 下的文件 (`include: /\/src\/mobile\//`),如果值是一个正则表达式,将包含匹配的文件,否则将排除该文件, 如果传入的值是一个数组,那么数组里的值必须为正则 | `Regexp` | undefined | 141 | 142 | ## 补充说明 143 | 144 | - `propList` (Array) 能转化为 vw 的属性列表 145 | - 传入特定的 CSS 属性; 146 | - 可以传入通配符"_"去匹配所有属性,例如:['_']; 147 | - 在属性的前或后添加"*",可以匹配特定的属性. (例如['*position\*'] 会匹配 background-position-y) 148 | - 在特定属性前加 "!",将不转换该属性的单位 . 例如: ['*', '!letter-spacing'],将不转换 letter-spacing 149 | - "!" 和 "_"可以组合使用, 例如: ['_', '!font\*'],将不转换 font-size 以及 font-weight 等属性 150 | - `selectorBlackList` (Array) 需要忽略的 CSS 选择器,不会转为视口单位,使用原有的 px 等单位。 151 | 152 | - 如果传入的值为字符串的话,只要选择器中含有传入值就会被匹配 153 | - 例如 `selectorBlackList` 为 `['body']` 的话, 那么 `.body-class` 就会被忽略 154 | - 如果传入的值为正则表达式的话,那么就会依据 CSS 选择器是否匹配该正则 155 | - 例如 `selectorBlackList` 为 `[/^body$/]` , 那么 `body` 会被忽略,而 `.body` 不会 156 | 157 | - 你可以使用特殊的注释来忽略单行的转换: 158 | 159 | - `/* px-to-viewport-ignore-next */` — 在单独的行上,防止在下一行上进行转换。 160 | - `/* px-to-viewport-ignore */` — 在右边的属性之后,防止在同一行上进行转换。 161 | 162 | Example: 163 | 164 | ```css 165 | /* example input: */ 166 | .class { 167 | /* px-to-viewport-ignore-next */ 168 | width: 10px; 169 | padding: 10px; 170 | height: 10px; /* px-to-viewport-ignore */ 171 | border: solid 2px #000; /* px-to-viewport-ignore */ 172 | } 173 | 174 | /* example output: */ 175 | .class { 176 | width: 10px; 177 | padding: 3.125vw; 178 | height: 10px; 179 | border: solid 2px #000; 180 | } 181 | ``` 182 | 183 | There are several more reasons why your pixels may not convert, the following options may affect this: `propList`, `selectorBlackList`, `minPixelValue`, `mediaQuery`, `exclude`, `include`. 184 | 185 | ## 与 PostCss 配置文件一起使用 186 | 187 | **在`postcss.config.js`文件添加如下配置** 188 | 189 | ```js 190 | module.exports = { 191 | plugins: { 192 | ... 193 | 'postcss-px-to-viewport-8-plugin': { 194 | viewportWidth: 1920, 195 | exclude: [/node_modules/], 196 | unitToConvert: 'px', 197 | ... 198 | } 199 | } 200 | } 201 | ``` 202 | 203 | ## vite 使用 204 | 205 | **在`vite.config.ts`文件添加如下配置** 206 | 207 | ```ts 208 | import { defineConfig } from 'vite'; 209 | import postcsspxtoviewport8plugin from 'postcss-px-to-viewport-8-plugin'; 210 | 211 | export default defineConfig({ 212 | css: { 213 | postcss: { 214 | plugins: [ 215 | postcsspxtoviewport8plugin({ 216 | unitToConvert: 'px', 217 | viewportWidth: file => { 218 | let num = 1920; 219 | if (file.indexOf('m_') !== -1) { 220 | num = 375; 221 | } 222 | return num; 223 | }, 224 | unitPrecision: 5, // 单位转换后保留的精度 225 | propList: ['*'], // 能转化为vw的属性列表 226 | viewportUnit: 'vw', // 希望使用的视口单位 227 | fontViewportUnit: 'vw', // 字体使用的视口单位 228 | selectorBlackList: [], // 需要忽略的CSS选择器,不会转为视口单位,使用原有的px等单位。 229 | minPixelValue: 1, // 设置最小的转换数值,如果为1的话,只有大于1的值会被转换 230 | mediaQuery: true, // 媒体查询里的单位是否需要转换单位 231 | replace: true, // 是否直接更换属性值,而不添加备用属性 232 | exclude: [/node_modules\/ant-design-vue/], // 忽略某些文件夹下的文件或特定文件,例如 'node_modules' 下的文件 233 | include: [], // 如果设置了include,那将只有匹配到的文件才会被转换 234 | landscape: false, // 是否添加根据 landscapeWidth 生成的媒体查询条件 @media (orientation: landscape) 235 | landscapeUnit: 'vw', // 横屏时使用的单位 236 | landscapeWidth: 1024, // 横屏时使用的视口宽度 237 | }), 238 | ], 239 | }, 240 | }, 241 | }); 242 | ``` 243 | 244 | ## 作者 245 | 246 | - [lkxian888](https://github.com/lkxian888) 247 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { getUnitRegexp } from './pixel-unit-regexp'; 2 | import { createPropListMatcher } from './prop-list-matcher'; 3 | import { OptionType, ParentExtendType, RuleType } from './types'; 4 | import { 5 | blacklistedSelector, 6 | createPxReplace, 7 | declarationExists, 8 | getUnit, 9 | isExclude, 10 | validateParams, 11 | } from './utils'; 12 | import objectAssign from 'object-assign'; 13 | 14 | import { AtRule, Root, Rule } from 'postcss'; 15 | import postcss from 'postcss'; 16 | 17 | const defaults: Required> = { 18 | unitToConvert: 'px', 19 | viewportWidth: 320, 20 | viewportHeight: 568, // not now used; TODO: need for different units and math for different properties 21 | unitPrecision: 5, 22 | viewportUnit: 'vw', 23 | fontViewportUnit: 'vw', // vmin is more suitable. 24 | selectorBlackList: [], 25 | propList: ['*'], 26 | minPixelValue: 1, 27 | mediaQuery: false, 28 | replace: true, 29 | landscape: false, 30 | landscapeUnit: 'vw', 31 | landscapeWidth: 568, 32 | }; 33 | 34 | const ignoreNextComment = 'px-to-viewport-ignore-next'; 35 | const ignorePrevComment = 'px-to-viewport-ignore'; 36 | 37 | const postcssPxToViewport = (options: OptionType) => { 38 | const opts = objectAssign({}, defaults, options); 39 | 40 | const pxRegex = getUnitRegexp(opts.unitToConvert); 41 | const satisfyPropList = createPropListMatcher(opts.propList); 42 | const landscapeRules: AtRule[] = []; 43 | 44 | return { 45 | postcssPlugin: 'postcss-px-to-viewport', 46 | Once(css: Root, { result }: { result: any }) { 47 | // @ts-ignore 补充类型 48 | css.walkRules((rule: RuleType) => { 49 | // Add exclude option to ignore some files like 'node_modules' 50 | const file = rule.source?.input.file || ''; 51 | if (opts.exclude && file) { 52 | if (Object.prototype.toString.call(opts.exclude) === '[object RegExp]') { 53 | if (isExclude(opts.exclude as RegExp, file)) return; 54 | } else if ( 55 | // Object.prototype.toString.call(opts.exclude) === '[object Array]' && 56 | opts.exclude instanceof Array 57 | ) { 58 | for (let i = 0; i < opts.exclude.length; i++) { 59 | if (isExclude(opts.exclude[i], file)) return; 60 | } 61 | } else { 62 | throw new Error('options.exclude should be RegExp or Array.'); 63 | } 64 | } 65 | 66 | if (blacklistedSelector(opts.selectorBlackList, rule.selector)) return; 67 | 68 | if (opts.landscape && !rule.parent?.params) { 69 | const landscapeRule = rule.clone().removeAll(); 70 | rule.walkDecls((decl) => { 71 | if (decl.value.indexOf(opts.unitToConvert) === -1) return; 72 | if (!satisfyPropList(decl.prop)) return; 73 | let landscapeWidth; 74 | if (typeof opts.landscapeWidth === 'function') { 75 | const num = opts.landscapeWidth(file); 76 | if (!num) return; 77 | landscapeWidth = num; 78 | } else { 79 | landscapeWidth = opts.landscapeWidth; 80 | } 81 | 82 | landscapeRule.append( 83 | decl.clone({ 84 | value: decl.value.replace( 85 | pxRegex, 86 | createPxReplace(opts, opts.landscapeUnit, landscapeWidth), 87 | ), 88 | }), 89 | ); 90 | }); 91 | 92 | if (landscapeRule.nodes.length > 0) { 93 | landscapeRules.push((landscapeRule as unknown) as AtRule); 94 | } 95 | } 96 | 97 | if (!validateParams(rule.parent?.params, opts.mediaQuery)) return; 98 | 99 | rule.walkDecls((decl, i) => { 100 | if (decl.value.indexOf(opts.unitToConvert) === -1) return; 101 | if (!satisfyPropList(decl.prop)) return; 102 | 103 | const prev = decl.prev(); 104 | // prev declaration is ignore conversion comment at same line 105 | if (prev && prev.type === 'comment' && prev.text === ignoreNextComment) { 106 | // remove comment 107 | prev.remove(); 108 | return; 109 | } 110 | const next = decl.next(); 111 | // next declaration is ignore conversion comment at same line 112 | if (next && next.type === 'comment' && next.text === ignorePrevComment) { 113 | if (/\n/.test(next.raws.before!)) { 114 | result.warn( 115 | `Unexpected comment /* ${ignorePrevComment} */ must be after declaration at same line.`, 116 | { node: next }, 117 | ); 118 | } else { 119 | // remove comment 120 | next.remove(); 121 | return; 122 | } 123 | } 124 | 125 | let unit; 126 | let size; 127 | const { params } = rule.parent; 128 | 129 | if (opts.landscape && params && params.indexOf('landscape') !== -1) { 130 | unit = opts.landscapeUnit; 131 | 132 | if (typeof opts.landscapeWidth === 'function') { 133 | const num = opts.landscapeWidth(file); 134 | if (!num) return; 135 | size = num; 136 | } else { 137 | size = opts.landscapeWidth; 138 | } 139 | } else { 140 | unit = getUnit(decl.prop, opts); 141 | if (typeof opts.viewportWidth === 'function') { 142 | const num = opts.viewportWidth(file); 143 | if (!num) return; 144 | size = num; 145 | } else { 146 | size = opts.viewportWidth; 147 | } 148 | } 149 | 150 | const value = decl.value.replace(pxRegex, createPxReplace(opts, unit!, size)); 151 | 152 | if (declarationExists((decl.parent as unknown) as ParentExtendType[], decl.prop, value)) 153 | return; 154 | 155 | if (opts.replace) { 156 | decl.value = value; 157 | } else { 158 | decl.parent?.insertAfter(i, decl.clone({ value })); 159 | } 160 | }); 161 | }); 162 | 163 | // if (landscapeRules.length > 0) { 164 | // const landscapeRoot = new AtRule({ 165 | // params: '(orientation: landscape)', 166 | // name: 'media', 167 | // }); 168 | 169 | // landscapeRules.forEach((rule) => { 170 | // landscapeRoot.append(rule); 171 | // }); 172 | // css.append(landscapeRoot); 173 | // } 174 | }, 175 | // https://www.postcss.com.cn/docs/writing-a-postcss-plugin 176 | // Declaration Rule RuleExit OnceExit 177 | // There two types or listeners: enter and exit. 178 | // Once, Root, AtRule, and Rule will be called before processing children. 179 | // OnceExit, RootExit, AtRuleExit, and RuleExit after processing all children inside node. 180 | OnceExit(css: Root, { AtRule }: { AtRule: any }) { 181 | // 在 Once里跑这段逻辑,设置横屏时,打包后到生产环境竖屏样式会覆盖横屏样式,所以 OnceExit再执行。 182 | if (landscapeRules.length > 0) { 183 | const landscapeRoot = new AtRule({ 184 | params: '(orientation: landscape)', 185 | name: 'media', 186 | }); 187 | 188 | landscapeRules.forEach(function(rule) { 189 | landscapeRoot.append(rule); 190 | }); 191 | css.append(landscapeRoot); 192 | } 193 | }, 194 | }; 195 | }; 196 | 197 | postcssPxToViewport.postcss = true; 198 | module.exports = postcssPxToViewport; 199 | export default postcssPxToViewport; 200 | -------------------------------------------------------------------------------- /spec/px-to-viewport.spec.ts: -------------------------------------------------------------------------------- 1 | // To run tests, run these commands from the project root: 2 | // 1. `npm install` 3 | // 2. `npm test` 4 | 5 | /* global describe, it, expect */ 6 | 7 | var postcss = require('postcss'); 8 | var pxToViewport = require('../lib'); 9 | var basicCSS = '.rule { font-size: 15px }'; 10 | var { filterPropList } = require('../src/prop-list-matcher'); 11 | 12 | describe('px-to-viewport', function() { 13 | it('should work on the readme example', function() { 14 | var input = 15 | 'h1 { margin: 0 0 20px; font-size: 32px; line-height: 2; letter-spacing: 1px; }'; 16 | var output = 17 | 'h1 { margin: 0 0 6.25vw; font-size: 10vw; line-height: 2; letter-spacing: 1px; }'; 18 | var processed = postcss(pxToViewport()).process(input).css; 19 | 20 | expect(processed).toBe(output); 21 | }); 22 | 23 | it('should replace the px unit with vw', function() { 24 | var processed = postcss(pxToViewport()).process(basicCSS).css; 25 | var expected = '.rule { font-size: 4.6875vw }'; 26 | 27 | expect(processed).toBe(expected); 28 | }); 29 | 30 | it('should handle < 1 values and values without a leading 0', function() { 31 | var rules = '.rule { margin: 0.5rem .5px -0.2px -.2em }'; 32 | var expected = '.rule { margin: 0.5rem 0.15625vw -0.0625vw -.2em }'; 33 | var options = { 34 | minPixelValue: 0, 35 | }; 36 | var processed = postcss(pxToViewport(options)).process(rules).css; 37 | 38 | expect(processed).toBe(expected); 39 | }); 40 | 41 | it('should remain unitless if 0', function() { 42 | var expected = '.rule { font-size: 0px; font-size: 0; }'; 43 | var processed = postcss(pxToViewport()).process(expected).css; 44 | 45 | expect(processed).toBe(expected); 46 | }); 47 | 48 | it('should not add properties that already exist', function() { 49 | var expected = '.rule { font-size: 16px; font-size: 5vw; }'; 50 | var processed = postcss(pxToViewport()).process(expected).css; 51 | 52 | expect(processed).toBe(expected); 53 | }); 54 | 55 | it('should not replace units inside mediaQueries by default', function() { 56 | var expected = '@media (min-width: 500px) { .rule { font-size: 16px } }'; 57 | var processed = postcss(pxToViewport()).process( 58 | '@media (min-width: 500px) { .rule { font-size: 16px } }', 59 | ).css; 60 | 61 | expect(processed).toBe(expected); 62 | }); 63 | }); 64 | 65 | describe('value parsing', function() { 66 | it('should not replace values in double quotes or single quotes', function() { 67 | var options = { 68 | propList: ['*'], 69 | }; 70 | var rules = 71 | '.rule { content: \'16px\'; font-family: "16px"; font-size: 16px; }'; 72 | var expected = 73 | '.rule { content: \'16px\'; font-family: "16px"; font-size: 5vw; }'; 74 | var processed = postcss(pxToViewport(options)).process(rules).css; 75 | 76 | expect(processed).toBe(expected); 77 | }); 78 | 79 | it('should not replace values in `url()`', function() { 80 | var rules = '.rule { background: url(16px.jpg); font-size: 16px; }'; 81 | var expected = '.rule { background: url(16px.jpg); font-size: 5vw; }'; 82 | var processed = postcss(pxToViewport()).process(rules).css; 83 | 84 | expect(processed).toBe(expected); 85 | }); 86 | 87 | it('should not replace values with an uppercase P or X', function() { 88 | var rules = 89 | '.rule { margin: 12px calc(100% - 14PX); height: calc(100% - 20px); font-size: 12Px; line-height: 16px; }'; 90 | var expected = 91 | '.rule { margin: 3.75vw calc(100% - 14PX); height: calc(100% - 6.25vw); font-size: 12Px; line-height: 5vw; }'; 92 | var processed = postcss(pxToViewport()).process(rules).css; 93 | 94 | expect(processed).toBe(expected); 95 | }); 96 | }); 97 | 98 | describe('unitToConvert', function() { 99 | it('should ignore non px values by default', function() { 100 | var expected = '.rule { font-size: 2em }'; 101 | var processed = postcss(pxToViewport()).process(expected).css; 102 | 103 | expect(processed).toBe(expected); 104 | }); 105 | 106 | it('should convert only values described in options', function() { 107 | var rules = '.rule { font-size: 5em; line-height: 2px }'; 108 | var expected = '.rule { font-size: 1.5625vw; line-height: 2px }'; 109 | var options = { 110 | unitToConvert: 'em', 111 | }; 112 | var processed = postcss(pxToViewport(options)).process(rules).css; 113 | 114 | expect(processed).toBe(expected); 115 | }); 116 | }); 117 | 118 | describe('viewportWidth', function() { 119 | it('should should replace using 320px by default', function() { 120 | var expected = '.rule { font-size: 4.6875vw }'; 121 | var processed = postcss(pxToViewport()).process(basicCSS).css; 122 | 123 | expect(processed).toBe(expected); 124 | }); 125 | 126 | it('should replace using viewportWidth from options', function() { 127 | var expected = '.rule { font-size: 3.125vw }'; 128 | var options = { 129 | viewportWidth: 480, 130 | }; 131 | var processed = postcss(pxToViewport(options)).process(basicCSS).css; 132 | 133 | expect(processed).toBe(expected); 134 | }); 135 | }); 136 | 137 | describe('unitPrecision', function() { 138 | it('should replace using a decimal of 2 places', function() { 139 | var expected = '.rule { font-size: 4.69vw }'; 140 | var options = { 141 | unitPrecision: 2, 142 | }; 143 | var processed = postcss(pxToViewport(options)).process(basicCSS).css; 144 | 145 | expect(processed).toBe(expected); 146 | }); 147 | }); 148 | 149 | describe('viewportUnit', function() { 150 | it('should replace using unit from options', function() { 151 | var rules = '.rule { margin-top: 15px }'; 152 | var expected = '.rule { margin-top: 4.6875vh }'; 153 | var options = { 154 | viewportUnit: 'vh', 155 | }; 156 | var processed = postcss(pxToViewport(options)).process(rules).css; 157 | 158 | expect(processed).toBe(expected); 159 | }); 160 | }); 161 | 162 | describe('fontViewportUnit', function() { 163 | it('should replace only font-size using unit from options', function() { 164 | var rules = '.rule { margin-top: 15px; font-size: 8px; }'; 165 | var expected = '.rule { margin-top: 4.6875vw; font-size: 2.5vmax; }'; 166 | var options = { 167 | fontViewportUnit: 'vmax', 168 | }; 169 | var processed = postcss(pxToViewport(options)).process(rules).css; 170 | 171 | expect(processed).toBe(expected); 172 | }); 173 | }); 174 | 175 | describe('selectorBlackList', function() { 176 | it('should ignore selectors in the selector black list', function() { 177 | var rules = '.rule { font-size: 15px } .rule2 { font-size: 15px }'; 178 | var expected = '.rule { font-size: 4.6875vw } .rule2 { font-size: 15px }'; 179 | var options = { 180 | selectorBlackList: ['.rule2'], 181 | }; 182 | var processed = postcss(pxToViewport(options)).process(rules).css; 183 | 184 | expect(processed).toBe(expected); 185 | }); 186 | 187 | it('should ignore every selector with `body$`', function() { 188 | var rules = 189 | 'body { font-size: 16px; } .class-body$ { font-size: 16px; } .simple-class { font-size: 16px; }'; 190 | var expected = 191 | 'body { font-size: 5vw; } .class-body$ { font-size: 16px; } .simple-class { font-size: 5vw; }'; 192 | var options = { 193 | selectorBlackList: ['body$'], 194 | }; 195 | var processed = postcss(pxToViewport(options)).process(rules).css; 196 | 197 | expect(processed).toBe(expected); 198 | }); 199 | 200 | it('should only ignore exactly `body`', function() { 201 | var rules = 202 | 'body { font-size: 16px; } .class-body { font-size: 16px; } .simple-class { font-size: 16px; }'; 203 | var expected = 204 | 'body { font-size: 16px; } .class-body { font-size: 5vw; } .simple-class { font-size: 5vw; }'; 205 | var options = { 206 | selectorBlackList: [/^body$/], 207 | }; 208 | var processed = postcss(pxToViewport(options)).process(rules).css; 209 | 210 | expect(processed).toBe(expected); 211 | }); 212 | }); 213 | 214 | describe('mediaQuery', function() { 215 | it('should replace px inside media queries if opts.mediaQuery', function() { 216 | var options = { 217 | mediaQuery: true, 218 | }; 219 | var processed = postcss(pxToViewport(options)).process( 220 | '@media (min-width: 500px) { .rule { font-size: 16px } }', 221 | ).css; 222 | var expected = '@media (min-width: 500px) { .rule { font-size: 5vw } }'; 223 | 224 | expect(processed).toBe(expected); 225 | }); 226 | 227 | it('should not replace px inside media queries if not opts.mediaQuery', function() { 228 | var options = { 229 | mediaQuery: false, 230 | }; 231 | var processed = postcss(pxToViewport(options)).process( 232 | '@media (min-width: 500px) { .rule { font-size: 16px } }', 233 | ).css; 234 | var expected = '@media (min-width: 500px) { .rule { font-size: 16px } }'; 235 | 236 | expect(processed).toBe(expected); 237 | }); 238 | 239 | it('should replace px inside media queries if it has params orientation landscape and landscape option', function() { 240 | var options = { 241 | mediaQuery: true, 242 | landscape: true, 243 | }; 244 | var processed = postcss(pxToViewport(options)).process( 245 | '@media (orientation-landscape) and (min-width: 500px) { .rule { font-size: 16px } }', 246 | ).css; 247 | var expected = 248 | '@media (orientation-landscape) and (min-width: 500px) { .rule { font-size: 2.8169vw } }'; 249 | 250 | expect(processed).toBe(expected); 251 | }); 252 | }); 253 | 254 | describe('propList', function() { 255 | it('should only replace properties in the prop list', function() { 256 | var css = 257 | '.rule { font-size: 16px; margin: 16px; margin-left: 5px; padding: 5px; padding-right: 16px }'; 258 | var expected = 259 | '.rule { font-size: 5vw; margin: 5vw; margin-left: 5px; padding: 5px; padding-right: 5vw }'; 260 | var options = { 261 | propList: ['*font*', 'margin*', '!margin-left', '*-right', 'pad'], 262 | }; 263 | var processed = postcss(pxToViewport(options)).process(css).css; 264 | 265 | expect(processed).toBe(expected); 266 | }); 267 | 268 | it('should only replace properties in the prop list with wildcard', function() { 269 | var css = 270 | '.rule { font-size: 16px; margin: 16px; margin-left: 5px; padding: 5px; padding-right: 16px }'; 271 | var expected = 272 | '.rule { font-size: 16px; margin: 5vw; margin-left: 5px; padding: 5px; padding-right: 16px }'; 273 | var options = { 274 | propList: ['*', '!margin-left', '!*padding*', '!font*'], 275 | }; 276 | var processed = postcss(pxToViewport(options)).process(css).css; 277 | 278 | expect(processed).toBe(expected); 279 | }); 280 | 281 | it('should replace all properties when prop list is not given', function() { 282 | var rules = '.rule { margin: 16px; font-size: 15px }'; 283 | var expected = '.rule { margin: 5vw; font-size: 4.6875vw }'; 284 | var processed = postcss(pxToViewport()).process(rules).css; 285 | 286 | expect(processed).toBe(expected); 287 | }); 288 | }); 289 | 290 | describe('minPixelValue', function() { 291 | it('should not replace values below minPixelValue', function() { 292 | var options = { 293 | propWhiteList: [], 294 | minPixelValue: 2, 295 | }; 296 | var rules = 297 | '.rule { border: 1px solid #000; font-size: 16px; margin: 1px 10px; }'; 298 | var expected = 299 | '.rule { border: 1px solid #000; font-size: 5vw; margin: 1px 3.125vw; }'; 300 | var processed = postcss(pxToViewport(options)).process(rules).css; 301 | 302 | expect(processed).toBe(expected); 303 | }); 304 | }); 305 | 306 | describe('exclude', function() { 307 | var rules = 308 | '.rule { border: 1px solid #000; font-size: 16px; margin: 1px 10px; }'; 309 | var covered = 310 | '.rule { border: 1px solid #000; font-size: 5vw; margin: 1px 3.125vw; }'; 311 | it('when using regex at the time, the style should not be overwritten.', function() { 312 | var options = { 313 | exclude: /\/node_modules\//, 314 | }; 315 | var processed = postcss(pxToViewport(options)).process(rules, { 316 | from: '/node_modules/main.css', 317 | }).css; 318 | 319 | expect(processed).toBe(rules); 320 | }); 321 | 322 | it('when using regex at the time, the style should be overwritten.', function() { 323 | var options = { 324 | exclude: /\/node_modules\//, 325 | }; 326 | var processed = postcss(pxToViewport(options)).process(rules, { 327 | from: '/example/main.css', 328 | }).css; 329 | 330 | expect(processed).toBe(covered); 331 | }); 332 | 333 | it('when using array at the time, the style should not be overwritten.', function() { 334 | var options = { 335 | exclude: [/\/node_modules\//, /\/exclude\//], 336 | }; 337 | var processed = postcss(pxToViewport(options)).process(rules, { 338 | from: '/exclude/main.css', 339 | }).css; 340 | 341 | expect(processed).toBe(rules); 342 | }); 343 | 344 | it('when using array at the time, the style should be overwritten.', function() { 345 | var options = { 346 | exclude: [/\/node_modules\//, /\/exclude\//], 347 | }; 348 | var processed = postcss(pxToViewport(options)).process(rules, { 349 | from: '/example/main.css', 350 | }).css; 351 | 352 | expect(processed).toBe(covered); 353 | }); 354 | }); 355 | 356 | describe('include', function() { 357 | var rules = 358 | '.rule { border: 1px solid #000; font-size: 16px; margin: 1px 10px; }'; 359 | var covered = 360 | '.rule { border: 1px solid #000; font-size: 5vw; margin: 1px 3.125vw; }'; 361 | it('when using regex at the time, the style should not be overwritten.', function() { 362 | var options = { 363 | include: /\/mobile\//, 364 | }; 365 | var processed = postcss(pxToViewport(options)).process(rules, { 366 | from: '/pc/main.css', 367 | }).css; 368 | 369 | expect(processed).toBe(rules); 370 | }); 371 | 372 | it('when using regex at the time, the style should be overwritten.', function() { 373 | var options = { 374 | include: /\/mobile\//, 375 | }; 376 | var processed = postcss(pxToViewport(options)).process(rules, { 377 | from: '/mobile/main.css', 378 | }).css; 379 | 380 | expect(processed).toBe(covered); 381 | }); 382 | 383 | it('when using array at the time, the style should not be overwritten.', function() { 384 | var options = { 385 | include: [/\/flexible\//, /\/mobile\//], 386 | }; 387 | var processed = postcss(pxToViewport(options)).process(rules, { 388 | from: '/pc/main.css', 389 | }).css; 390 | 391 | expect(processed).toBe(rules); 392 | }); 393 | 394 | it('when using array at the time, the style should be overwritten.', function() { 395 | var options = { 396 | include: [/\/flexible\//, /\/mobile\//], 397 | }; 398 | var processed = postcss(pxToViewport(options)).process(rules, { 399 | from: '/flexible/main.css', 400 | }).css; 401 | 402 | expect(processed).toBe(covered); 403 | }); 404 | }); 405 | 406 | describe('include-and-exclude', function() { 407 | var rules = 408 | '.rule { border: 1px solid #000; font-size: 16px; margin: 1px 10px; }'; 409 | var covered = 410 | '.rule { border: 1px solid #000; font-size: 5vw; margin: 1px 3.125vw; }'; 411 | 412 | it('when using regex at the time, the style should not be overwritten.', function() { 413 | var options = { 414 | include: /\/mobile\//, 415 | exclude: /\/not-transform\//, 416 | }; 417 | var processed = postcss(pxToViewport(options)).process(rules, { 418 | from: '/mobile/not-transform/main.css', 419 | }).css; 420 | 421 | expect(processed).toBe(rules); 422 | }); 423 | 424 | it('when using regex at the time, the style should be overwritten.', function() { 425 | var options = { 426 | include: /\/mobile\//, 427 | exclude: /\/not-transform\//, 428 | }; 429 | var processed = postcss(pxToViewport(options)).process(rules, { 430 | from: '/mobile/style/main.css', 431 | }).css; 432 | 433 | expect(processed).toBe(covered); 434 | }); 435 | 436 | it('when using array at the time, the style should not be overwritten.', function() { 437 | var options = { 438 | include: [/\/flexible\//, /\/mobile\//], 439 | exclude: [/\/not-transform\//, /pc/], 440 | }; 441 | var processed = postcss(pxToViewport(options)).process(rules, { 442 | from: '/flexible/not-transform/main.css', 443 | }).css; 444 | 445 | expect(processed).toBe(rules); 446 | }); 447 | 448 | it('when using regex at the time, the style should be overwritten.', function() { 449 | var options = { 450 | include: [/\/flexible\//, /\/mobile\//], 451 | exclude: [/\/not-transform\//, /pc/], 452 | }; 453 | var processed = postcss(pxToViewport(options)).process(rules, { 454 | from: '/mobile/style/main.css', 455 | }).css; 456 | 457 | expect(processed).toBe(covered); 458 | }); 459 | }); 460 | 461 | describe('regex', function() { 462 | var rules = 463 | '.rule { border: 1px solid #000; font-size: 16px; margin: 1px 10px; }'; 464 | var covered = 465 | '.rule { border: 1px solid #000; font-size: 5vw; margin: 1px 3.125vw; }'; 466 | 467 | it('when using regex at the time, the style should not be overwritten.', function() { 468 | var options = { 469 | exclude: /pc/, 470 | }; 471 | var processed = postcss(pxToViewport(options)).process(rules, { 472 | from: '/pc-project/main.css', 473 | }).css; 474 | 475 | expect(processed).toBe(rules); 476 | }); 477 | 478 | it('when using regex at the time, the style should be overwritten.', function() { 479 | var options = { 480 | exclude: /\/pc\//, 481 | }; 482 | var processed = postcss(pxToViewport(options)).process(rules, { 483 | from: '/pc-project/main.css', 484 | }).css; 485 | 486 | expect(processed).toBe(covered); 487 | }); 488 | 489 | it('when using regex at the time, the style should not be overwritten.', function() { 490 | var options = { 491 | include: /\/pc\//, 492 | }; 493 | var processed = postcss(pxToViewport(options)).process(rules, { 494 | from: '/pc-project/main.css', 495 | }).css; 496 | 497 | expect(processed).toBe(rules); 498 | }); 499 | 500 | it('when using regex at the time, the style should be overwritten.', function() { 501 | var options = { 502 | include: /pc/, 503 | }; 504 | var processed = postcss(pxToViewport(options)).process(rules, { 505 | from: '/pc-project/main.css', 506 | }).css; 507 | 508 | expect(processed).toBe(covered); 509 | }); 510 | }); 511 | 512 | describe('replace', function() { 513 | it('should leave fallback pixel unit with root em value', function() { 514 | var options = { 515 | replace: false, 516 | }; 517 | var processed = postcss(pxToViewport(options)).process(basicCSS).css; 518 | var expected = '.rule { font-size: 15px; font-size: 4.6875vw }'; 519 | 520 | expect(processed).toBe(expected); 521 | }); 522 | }); 523 | 524 | describe('filter-prop-list', function() { 525 | it('should find "exact" matches from propList', function() { 526 | var propList = [ 527 | 'font-size', 528 | 'margin', 529 | '!padding', 530 | '*border*', 531 | '*', 532 | '*y', 533 | '!*font*', 534 | ]; 535 | var expected = 'font-size,margin'; 536 | expect(filterPropList.exact(propList).join()).toBe(expected); 537 | }); 538 | 539 | it('should find "contain" matches from propList and reduce to string', function() { 540 | var propList = [ 541 | 'font-size', 542 | '*margin*', 543 | '!padding', 544 | '*border*', 545 | '*', 546 | '*y', 547 | '!*font*', 548 | ]; 549 | var expected = 'margin,border'; 550 | expect(filterPropList.contain(propList).join()).toBe(expected); 551 | }); 552 | 553 | it('should find "start" matches from propList and reduce to string', function() { 554 | var propList = [ 555 | 'font-size', 556 | '*margin*', 557 | '!padding', 558 | 'border*', 559 | '*', 560 | '*y', 561 | '!*font*', 562 | ]; 563 | var expected = 'border'; 564 | expect(filterPropList.startWith(propList).join()).toBe(expected); 565 | }); 566 | 567 | it('should find "end" matches from propList and reduce to string', function() { 568 | var propList = [ 569 | 'font-size', 570 | '*margin*', 571 | '!padding', 572 | 'border*', 573 | '*', 574 | '*y', 575 | '!*font*', 576 | ]; 577 | var expected = 'y'; 578 | expect(filterPropList.endWith(propList).join()).toBe(expected); 579 | }); 580 | 581 | it('should find "not" matches from propList and reduce to string', function() { 582 | var propList = [ 583 | 'font-size', 584 | '*margin*', 585 | '!padding', 586 | 'border*', 587 | '*', 588 | '*y', 589 | '!*font*', 590 | ]; 591 | var expected = 'padding'; 592 | expect(filterPropList.notExact(propList).join()).toBe(expected); 593 | }); 594 | 595 | it('should find "not contain" matches from propList and reduce to string', function() { 596 | var propList = [ 597 | 'font-size', 598 | '*margin*', 599 | '!padding', 600 | '!border*', 601 | '*', 602 | '*y', 603 | '!*font*', 604 | ]; 605 | var expected = 'font'; 606 | expect(filterPropList.notContain(propList).join()).toBe(expected); 607 | }); 608 | 609 | it('should find "not start" matches from propList and reduce to string', function() { 610 | var propList = [ 611 | 'font-size', 612 | '*margin*', 613 | '!padding', 614 | '!border*', 615 | '*', 616 | '*y', 617 | '!*font*', 618 | ]; 619 | var expected = 'border'; 620 | expect(filterPropList.notStartWith(propList).join()).toBe(expected); 621 | }); 622 | 623 | it('should find "not end" matches from propList and reduce to string', function() { 624 | var propList = [ 625 | 'font-size', 626 | '*margin*', 627 | '!padding', 628 | '!border*', 629 | '*', 630 | '!*y', 631 | '!*font*', 632 | ]; 633 | var expected = 'y'; 634 | expect(filterPropList.notEndWith(propList).join()).toBe(expected); 635 | }); 636 | }); 637 | 638 | describe('landscape', function() { 639 | it('should add landscape atRule', function() { 640 | var css = 641 | '.rule { font-size: 16px; margin: 16px; margin-left: 5px; padding: 5px; padding-right: 16px }'; 642 | var expected = 643 | '.rule { font-size: 5vw; margin: 5vw; margin-left: 1.5625vw; padding: 1.5625vw; padding-right: 5vw }@media (orientation: landscape) {.rule { font-size: 2.8169vw; margin: 2.8169vw; margin-left: 0.88028vw; padding: 0.88028vw; padding-right: 2.8169vw } }'; 644 | var options = { 645 | landscape: true, 646 | }; 647 | var processed = postcss(pxToViewport(options)).process(css).css; 648 | 649 | expect(processed).toBe(expected); 650 | }); 651 | 652 | it('should add landscape atRule with specified landscapeUnits', function() { 653 | var css = 654 | '.rule { font-size: 16px; margin: 16px; margin-left: 5px; padding: 5px; padding-right: 16px }'; 655 | var expected = 656 | '.rule { font-size: 5vw; margin: 5vw; margin-left: 1.5625vw; padding: 1.5625vw; padding-right: 5vw }@media (orientation: landscape) {.rule { font-size: 2.8169vh; margin: 2.8169vh; margin-left: 0.88028vh; padding: 0.88028vh; padding-right: 2.8169vh } }'; 657 | var options = { 658 | landscape: true, 659 | landscapeUnit: 'vh', 660 | }; 661 | var processed = postcss(pxToViewport(options)).process(css).css; 662 | 663 | expect(processed).toBe(expected); 664 | }); 665 | 666 | it('should not add landscape atRule in mediaQueries', function() { 667 | var css = '@media (min-width: 500px) { .rule { font-size: 16px } }'; 668 | var expected = '@media (min-width: 500px) { .rule { font-size: 5vw } }'; 669 | var options = { 670 | landscape: true, 671 | mediaQuery: true, 672 | }; 673 | var processed = postcss(pxToViewport(options)).process(css).css; 674 | 675 | expect(processed).toBe(expected); 676 | }); 677 | 678 | it('should not replace values inside landscape atRule', function() { 679 | var options = { 680 | replace: false, 681 | landscape: true, 682 | }; 683 | var processed = postcss(pxToViewport(options)).process(basicCSS).css; 684 | var expected = 685 | '.rule { font-size: 15px; font-size: 4.6875vw }@media (orientation: landscape) {.rule { font-size: 2.64085vw } }'; 686 | 687 | expect(processed).toBe(expected); 688 | }); 689 | 690 | it('should add landscape atRule with specified landscapeWidth', function() { 691 | var options = { 692 | landscape: true, 693 | landscapeWidth: 768, 694 | }; 695 | var processed = postcss(pxToViewport(options)).process(basicCSS).css; 696 | var expected = 697 | '.rule { font-size: 4.6875vw }@media (orientation: landscape) {.rule { font-size: 1.95313vw } }'; 698 | 699 | expect(processed).toBe(expected); 700 | }); 701 | 702 | it('should not add landscape atRule if it has no nodes', function() { 703 | var css = '.rule { font-size: 15vw }'; 704 | var options = { 705 | landscape: true, 706 | }; 707 | var processed = postcss(pxToViewport(options)).process(css).css; 708 | var expected = '.rule { font-size: 15vw }'; 709 | 710 | expect(processed).toBe(expected); 711 | }); 712 | }); 713 | 714 | describe('/* px-to-viewport-ignore */ & /* px-to-viewport-ignore-next */', function() { 715 | it('should ignore right-commented', function() { 716 | var css = 717 | '.rule { font-size: 15px; /* simple comment */ width: 100px; /* px-to-viewport-ignore */ height: 50px; }'; 718 | var expected = 719 | '.rule { font-size: 4.6875vw; /* simple comment */ width: 100px; height: 15.625vw; }'; 720 | 721 | var processed = postcss(pxToViewport()).process(css).css; 722 | 723 | expect(processed).toBe(expected); 724 | }); 725 | 726 | it('should ignore right-commented in multiline-css', function() { 727 | var css = 728 | '.rule {\n font-size: 15px;\n width: 100px; /*px-to-viewport-ignore*/\n height: 50px;\n}'; 729 | var expected = 730 | '.rule {\n font-size: 4.6875vw;\n width: 100px;\n height: 15.625vw;\n}'; 731 | 732 | var processed = postcss(pxToViewport()).process(css).css; 733 | 734 | expect(processed).toBe(expected); 735 | }); 736 | 737 | it('should ignore before-commented in multiline-css', function() { 738 | var css = 739 | '.rule {\n font-size: 15px;\n /*px-to-viewport-ignore-next*/\n width: 100px;\n /*px-to-viewport-ignore*/\n height: 50px;\n}'; 740 | var expected = 741 | '.rule {\n font-size: 4.6875vw;\n width: 100px;\n /*px-to-viewport-ignore*/\n height: 15.625vw;\n}'; 742 | 743 | var processed = postcss(pxToViewport()).process(css).css; 744 | 745 | expect(processed).toBe(expected); 746 | }); 747 | }); 748 | --------------------------------------------------------------------------------