index.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. 'use strict';
  2. const valueParser = require('postcss-value-parser');
  3. const directionKeywords = new Set(['top', 'right', 'bottom', 'left', 'center']);
  4. const center = '50%';
  5. const horizontal = new Map([
  6. ['right', '100%'],
  7. ['left', '0'],
  8. ]);
  9. const verticalValue = new Map([
  10. ['bottom', '100%'],
  11. ['top', '0'],
  12. ]);
  13. const mathFunctions = new Set(['calc', 'min', 'max', 'clamp']);
  14. const variableFunctions = new Set(['var', 'env', 'constant']);
  15. /**
  16. * @param {valueParser.Node} node
  17. * @return {boolean}
  18. */
  19. function isCommaNode(node) {
  20. return node.type === 'div' && node.value === ',';
  21. }
  22. /**
  23. * @param {valueParser.Node} node
  24. * @return {boolean}
  25. */
  26. function isVariableFunctionNode(node) {
  27. if (node.type !== 'function') {
  28. return false;
  29. }
  30. return variableFunctions.has(node.value.toLowerCase());
  31. }
  32. /**
  33. * @param {valueParser.Node} node
  34. * @return {boolean}
  35. */
  36. function isMathFunctionNode(node) {
  37. if (node.type !== 'function') {
  38. return false;
  39. }
  40. return mathFunctions.has(node.value.toLowerCase());
  41. }
  42. /**
  43. * @param {valueParser.Node} node
  44. * @return {boolean}
  45. */
  46. function isNumberNode(node) {
  47. if (node.type !== 'word') {
  48. return false;
  49. }
  50. const value = parseFloat(node.value);
  51. return !isNaN(value);
  52. }
  53. /**
  54. * @param {valueParser.Node} node
  55. * @return {boolean}
  56. */
  57. function isDimensionNode(node) {
  58. if (node.type !== 'word') {
  59. return false;
  60. }
  61. const parsed = valueParser.unit(node.value);
  62. if (!parsed) {
  63. return false;
  64. }
  65. return parsed.unit !== '';
  66. }
  67. /**
  68. * @param {string} value
  69. * @return {string}
  70. */
  71. function transform(value) {
  72. const parsed = valueParser(value);
  73. /** @type {({start: number, end: number} | {start: null, end: null})[]} */
  74. const ranges = [];
  75. let rangeIndex = 0;
  76. let shouldContinue = true;
  77. parsed.nodes.forEach((node, index) => {
  78. // After comma (`,`) follows next background
  79. if (isCommaNode(node)) {
  80. rangeIndex += 1;
  81. shouldContinue = true;
  82. return;
  83. }
  84. if (!shouldContinue) {
  85. return;
  86. }
  87. // After separator (`/`) follows `background-size` values
  88. // Avoid them
  89. if (node.type === 'div' && node.value === '/') {
  90. shouldContinue = false;
  91. return;
  92. }
  93. if (!ranges[rangeIndex]) {
  94. ranges[rangeIndex] = {
  95. start: null,
  96. end: null,
  97. };
  98. }
  99. // Do not try to be processed `var and `env` function inside background
  100. if (isVariableFunctionNode(node)) {
  101. shouldContinue = false;
  102. ranges[rangeIndex].start = null;
  103. ranges[rangeIndex].end = null;
  104. return;
  105. }
  106. const isPositionKeyword =
  107. (node.type === 'word' &&
  108. directionKeywords.has(node.value.toLowerCase())) ||
  109. isDimensionNode(node) ||
  110. isNumberNode(node) ||
  111. isMathFunctionNode(node);
  112. if (ranges[rangeIndex].start === null && isPositionKeyword) {
  113. ranges[rangeIndex].start = index;
  114. ranges[rangeIndex].end = index;
  115. return;
  116. }
  117. if (ranges[rangeIndex].start !== null) {
  118. if (node.type === 'space') {
  119. return;
  120. } else if (isPositionKeyword) {
  121. ranges[rangeIndex].end = index;
  122. return;
  123. }
  124. return;
  125. }
  126. });
  127. ranges.forEach((range) => {
  128. if (range.start === null) {
  129. return;
  130. }
  131. const nodes = parsed.nodes.slice(range.start, range.end + 1);
  132. if (nodes.length > 3) {
  133. return;
  134. }
  135. const firstNode = nodes[0].value.toLowerCase();
  136. const secondNode =
  137. nodes[2] && nodes[2].value ? nodes[2].value.toLowerCase() : null;
  138. if (nodes.length === 1 || secondNode === 'center') {
  139. if (secondNode) {
  140. nodes[2].value = nodes[1].value = '';
  141. }
  142. const map = new Map([...horizontal, ['center', center]]);
  143. if (map.has(firstNode)) {
  144. nodes[0].value = /** @type {string}*/ (map.get(firstNode));
  145. }
  146. return;
  147. }
  148. if (secondNode !== null) {
  149. if (firstNode === 'center' && directionKeywords.has(secondNode)) {
  150. nodes[0].value = nodes[1].value = '';
  151. if (horizontal.has(secondNode)) {
  152. nodes[2].value = /** @type {string} */ (horizontal.get(secondNode));
  153. }
  154. return;
  155. }
  156. if (horizontal.has(firstNode) && verticalValue.has(secondNode)) {
  157. nodes[0].value = /** @type {string} */ (horizontal.get(firstNode));
  158. nodes[2].value = /** @type {string} */ (verticalValue.get(secondNode));
  159. return;
  160. } else if (verticalValue.has(firstNode) && horizontal.has(secondNode)) {
  161. nodes[0].value = /** @type {string} */ (horizontal.get(secondNode));
  162. nodes[2].value = /** @type {string} */ (verticalValue.get(firstNode));
  163. return;
  164. }
  165. }
  166. });
  167. return parsed.toString();
  168. }
  169. /**
  170. * @type {import('postcss').PluginCreator<void>}
  171. * @return {import('postcss').Plugin}
  172. */
  173. function pluginCreator() {
  174. return {
  175. postcssPlugin: 'postcss-normalize-positions',
  176. OnceExit(css) {
  177. const cache = new Map();
  178. css.walkDecls(
  179. /^(background(-position)?|(-\w+-)?perspective-origin)$/i,
  180. (decl) => {
  181. const value = decl.value;
  182. if (!value) {
  183. return;
  184. }
  185. if (cache.has(value)) {
  186. decl.value = cache.get(value);
  187. return;
  188. }
  189. const result = transform(value);
  190. decl.value = result;
  191. cache.set(value, result);
  192. }
  193. );
  194. },
  195. };
  196. }
  197. pluginCreator.postcss = true;
  198. module.exports = pluginCreator;