UNPKG

5.03 kBJavaScriptView Raw
1'use strict';
2const valueParser = require('postcss-value-parser');
3const browserslist = require('browserslist');
4const convert = require('./lib/convert.js');
5
6const LENGTH_UNITS = new Set([
7 'em',
8 'ex',
9 'ch',
10 'rem',
11 'vw',
12 'vh',
13 'vmin',
14 'vmax',
15 'cm',
16 'mm',
17 'q',
18 'in',
19 'pt',
20 'pc',
21 'px',
22]);
23
24// These properties only accept percentages, so no point in trying to transform
25const notALength = new Set([
26 'descent-override',
27 'ascent-override',
28 'font-stretch',
29 'size-adjust',
30 'line-gap-override',
31]);
32
33// Can't change the unit on these properties when they're 0
34const keepWhenZero = new Set([
35 'stroke-dashoffset',
36 'stroke-width',
37 'line-height',
38]);
39
40// Can't remove the % on these properties when they're 0 on IE 11
41const keepZeroPercent = new Set(['max-height', 'height', 'min-width']);
42
43/**
44 * Numbers without digits after the dot are technically invalid,
45 * but in that case css-value-parser returns the dot as part of the unit,
46 * so we use this to remove the dot.
47 *
48 * @param {string} item
49 * @return {string}
50 */
51function stripLeadingDot(item) {
52 if (item.charCodeAt(0) === '.'.charCodeAt(0)) {
53 return item.slice(1);
54 } else {
55 return item;
56 }
57}
58
59/**
60 * @param {valueParser.Node} node
61 * @param {Options} opts
62 * @param {boolean} keepZeroUnit
63 * @return {void}
64 */
65function parseWord(node, opts, keepZeroUnit) {
66 const pair = valueParser.unit(node.value);
67 if (pair) {
68 const num = Number(pair.number);
69 const u = stripLeadingDot(pair.unit);
70 if (num === 0) {
71 node.value =
72 0 +
73 (keepZeroUnit || (!LENGTH_UNITS.has(u.toLowerCase()) && u !== '%')
74 ? u
75 : '');
76 } else {
77 node.value = convert(num, u, opts);
78
79 if (
80 typeof opts.precision === 'number' &&
81 u.toLowerCase() === 'px' &&
82 pair.number.includes('.')
83 ) {
84 const precision = Math.pow(10, opts.precision);
85 node.value =
86 Math.round(parseFloat(node.value) * precision) / precision + u;
87 }
88 }
89 }
90}
91
92/**
93 * @param {valueParser.WordNode} node
94 * @return {void}
95 */
96function clampOpacity(node) {
97 const pair = valueParser.unit(node.value);
98 if (!pair) {
99 return;
100 }
101 let num = Number(pair.number);
102 if (num > 1) {
103 node.value = pair.unit === '%' ? num + pair.unit : 1 + pair.unit;
104 } else if (num < 0) {
105 node.value = 0 + pair.unit;
106 }
107}
108
109/**
110 * @param {import('postcss').Declaration} decl
111 * @param {string[]} browsers
112 * @return {boolean}
113 */
114function shouldKeepZeroUnit(decl, browsers) {
115 const { parent } = decl;
116 const lowerCasedProp = decl.prop.toLowerCase();
117 return (
118 (decl.value.includes('%') &&
119 keepZeroPercent.has(lowerCasedProp) &&
120 browsers.includes('ie 11')) ||
121 (parent &&
122 parent.parent &&
123 parent.parent.type === 'atrule' &&
124 /** @type {import('postcss').AtRule} */ (
125 parent.parent
126 ).name.toLowerCase() === 'keyframes' &&
127 lowerCasedProp === 'stroke-dasharray') ||
128 keepWhenZero.has(lowerCasedProp)
129 );
130}
131/**
132 * @param {Options} opts
133 * @param {string[]} browsers
134 * @param {import('postcss').Declaration} decl
135 * @return {void}
136 */
137function transform(opts, browsers, decl) {
138 const lowerCasedProp = decl.prop.toLowerCase();
139 if (
140 lowerCasedProp.includes('flex') ||
141 lowerCasedProp.indexOf('--') === 0 ||
142 notALength.has(lowerCasedProp)
143 ) {
144 return;
145 }
146
147 decl.value = valueParser(decl.value)
148 .walk((node) => {
149 const lowerCasedValue = node.value.toLowerCase();
150
151 if (node.type === 'word') {
152 parseWord(node, opts, shouldKeepZeroUnit(decl, browsers));
153 if (
154 lowerCasedProp === 'opacity' ||
155 lowerCasedProp === 'shape-image-threshold'
156 ) {
157 clampOpacity(node);
158 }
159 } else if (node.type === 'function') {
160 if (
161 lowerCasedValue === 'calc' ||
162 lowerCasedValue === 'min' ||
163 lowerCasedValue === 'max' ||
164 lowerCasedValue === 'clamp' ||
165 lowerCasedValue === 'hsl' ||
166 lowerCasedValue === 'hsla'
167 ) {
168 valueParser.walk(node.nodes, (n) => {
169 if (n.type === 'word') {
170 parseWord(n, opts, true);
171 }
172 });
173 return false;
174 }
175 if (lowerCasedValue === 'url') {
176 return false;
177 }
178 }
179 })
180 .toString();
181}
182
183const plugin = 'postcss-convert-values';
184/**
185 * @typedef {{precision: boolean | number, angle?: boolean, time?: boolean, length?: boolean} & browserslist.Options} Options */
186/**
187 * @type {import('postcss').PluginCreator<Options>}
188 * @param {Options} opts
189 * @return {import('postcss').Plugin}
190 */
191function pluginCreator(opts = { precision: false }) {
192 const browsers = browserslist(null, {
193 stats: opts.stats,
194 path: __dirname,
195 env: opts.env,
196 });
197
198 return {
199 postcssPlugin: plugin,
200 OnceExit(css) {
201 css.walkDecls((decl) => transform(opts, browsers, decl));
202 },
203 };
204}
205
206pluginCreator.postcss = true;
207module.exports = pluginCreator;