提交学习笔记专用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

207 lines
4.9 KiB

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