index.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. 'use strict';
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. var _postcss = require('postcss');
  6. var _postcss2 = _interopRequireDefault(_postcss);
  7. var _postcssValueParser = require('postcss-value-parser');
  8. var _postcssValueParser2 = _interopRequireDefault(_postcssValueParser);
  9. var _has = require('has');
  10. var _has2 = _interopRequireDefault(_has);
  11. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  12. /*
  13. * Constants (parser usage)
  14. */
  15. const SINGLE_QUOTE = '\''.charCodeAt(0);
  16. const DOUBLE_QUOTE = '"'.charCodeAt(0);
  17. const BACKSLASH = '\\'.charCodeAt(0);
  18. const NEWLINE = '\n'.charCodeAt(0);
  19. const SPACE = ' '.charCodeAt(0);
  20. const FEED = '\f'.charCodeAt(0);
  21. const TAB = '\t'.charCodeAt(0);
  22. const CR = '\r'.charCodeAt(0);
  23. const WORD_END = /[ \n\t\r\f'"\\]/g;
  24. /*
  25. * Constants (node type strings)
  26. */
  27. const C_STRING = 'string';
  28. const C_ESCAPED_SINGLE_QUOTE = 'escapedSingleQuote';
  29. const C_ESCAPED_DOUBLE_QUOTE = 'escapedDoubleQuote';
  30. const C_SINGLE_QUOTE = 'singleQuote';
  31. const C_DOUBLE_QUOTE = 'doubleQuote';
  32. const C_NEWLINE = 'newline';
  33. const C_SINGLE = 'single';
  34. /*
  35. * Literals
  36. */
  37. const L_SINGLE_QUOTE = `'`;
  38. const L_DOUBLE_QUOTE = `"`;
  39. const L_NEWLINE = `\\\n`;
  40. /*
  41. * Parser nodes
  42. */
  43. const T_ESCAPED_SINGLE_QUOTE = { type: C_ESCAPED_SINGLE_QUOTE, value: `\\'` };
  44. const T_ESCAPED_DOUBLE_QUOTE = { type: C_ESCAPED_DOUBLE_QUOTE, value: `\\"` };
  45. const T_SINGLE_QUOTE = { type: C_SINGLE_QUOTE, value: L_SINGLE_QUOTE };
  46. const T_DOUBLE_QUOTE = { type: C_DOUBLE_QUOTE, value: L_DOUBLE_QUOTE };
  47. const T_NEWLINE = { type: C_NEWLINE, value: L_NEWLINE };
  48. function stringify(ast) {
  49. return ast.nodes.reduce((str, { value }) => {
  50. // Collapse multiple line strings automatically
  51. if (value === L_NEWLINE) {
  52. return str;
  53. }
  54. return str + value;
  55. }, '');
  56. }
  57. function parse(str) {
  58. let code, next, value;
  59. let pos = 0;
  60. let len = str.length;
  61. const ast = {
  62. nodes: [],
  63. types: {
  64. escapedSingleQuote: 0,
  65. escapedDoubleQuote: 0,
  66. singleQuote: 0,
  67. doubleQuote: 0
  68. },
  69. quotes: false
  70. };
  71. while (pos < len) {
  72. code = str.charCodeAt(pos);
  73. switch (code) {
  74. case SPACE:
  75. case TAB:
  76. case CR:
  77. case FEED:
  78. next = pos;
  79. do {
  80. next += 1;
  81. code = str.charCodeAt(next);
  82. } while (code === SPACE || code === NEWLINE || code === TAB || code === CR || code === FEED);
  83. ast.nodes.push({
  84. type: 'space',
  85. value: str.slice(pos, next)
  86. });
  87. pos = next - 1;
  88. break;
  89. case SINGLE_QUOTE:
  90. ast.nodes.push(T_SINGLE_QUOTE);
  91. ast.types[C_SINGLE_QUOTE]++;
  92. ast.quotes = true;
  93. break;
  94. case DOUBLE_QUOTE:
  95. ast.nodes.push(T_DOUBLE_QUOTE);
  96. ast.types[C_DOUBLE_QUOTE]++;
  97. ast.quotes = true;
  98. break;
  99. case BACKSLASH:
  100. next = pos + 1;
  101. if (str.charCodeAt(next) === SINGLE_QUOTE) {
  102. ast.nodes.push(T_ESCAPED_SINGLE_QUOTE);
  103. ast.types[C_ESCAPED_SINGLE_QUOTE]++;
  104. ast.quotes = true;
  105. pos = next;
  106. break;
  107. } else if (str.charCodeAt(next) === DOUBLE_QUOTE) {
  108. ast.nodes.push(T_ESCAPED_DOUBLE_QUOTE);
  109. ast.types[C_ESCAPED_DOUBLE_QUOTE]++;
  110. ast.quotes = true;
  111. pos = next;
  112. break;
  113. } else if (str.charCodeAt(next) === NEWLINE) {
  114. ast.nodes.push(T_NEWLINE);
  115. pos = next;
  116. break;
  117. }
  118. /*
  119. * We need to fall through here to handle the token as
  120. * a whole word. The missing 'break' is intentional.
  121. */
  122. default:
  123. WORD_END.lastIndex = pos + 1;
  124. WORD_END.test(str);
  125. if (WORD_END.lastIndex === 0) {
  126. next = len - 1;
  127. } else {
  128. next = WORD_END.lastIndex - 2;
  129. }
  130. value = str.slice(pos, next + 1);
  131. ast.nodes.push({
  132. type: C_STRING,
  133. value
  134. });
  135. pos = next;
  136. }
  137. pos++;
  138. }
  139. return ast;
  140. }
  141. function changeWrappingQuotes(node, ast) {
  142. const { types } = ast;
  143. if (types[C_SINGLE_QUOTE] || types[C_DOUBLE_QUOTE]) {
  144. return;
  145. }
  146. if (node.quote === L_SINGLE_QUOTE && types[C_ESCAPED_SINGLE_QUOTE] > 0 && !types[C_ESCAPED_DOUBLE_QUOTE]) {
  147. node.quote = L_DOUBLE_QUOTE;
  148. }
  149. if (node.quote === L_DOUBLE_QUOTE && types[C_ESCAPED_DOUBLE_QUOTE] > 0 && !types[C_ESCAPED_SINGLE_QUOTE]) {
  150. node.quote = L_SINGLE_QUOTE;
  151. }
  152. ast.nodes = ast.nodes.reduce((newAst, child) => {
  153. if (child.type === C_ESCAPED_DOUBLE_QUOTE && node.quote === L_SINGLE_QUOTE) {
  154. return [...newAst, T_DOUBLE_QUOTE];
  155. }
  156. if (child.type === C_ESCAPED_SINGLE_QUOTE && node.quote === L_DOUBLE_QUOTE) {
  157. return [...newAst, T_SINGLE_QUOTE];
  158. }
  159. return [...newAst, child];
  160. }, []);
  161. }
  162. function normalize(value, preferredQuote) {
  163. if (!value || !value.length) {
  164. return value;
  165. }
  166. return (0, _postcssValueParser2.default)(value).walk(child => {
  167. if (child.type !== C_STRING) {
  168. return;
  169. }
  170. const ast = parse(child.value);
  171. if (ast.quotes) {
  172. changeWrappingQuotes(child, ast);
  173. } else if (preferredQuote === C_SINGLE) {
  174. child.quote = L_SINGLE_QUOTE;
  175. } else {
  176. child.quote = L_DOUBLE_QUOTE;
  177. }
  178. child.value = stringify(ast);
  179. }).toString();
  180. }
  181. const params = {
  182. rule: 'selector',
  183. decl: 'value',
  184. atrule: 'params'
  185. };
  186. exports.default = _postcss2.default.plugin('postcss-normalize-string', opts => {
  187. const { preferredQuote } = Object.assign({}, {
  188. preferredQuote: 'double'
  189. }, opts);
  190. return css => {
  191. const cache = {};
  192. css.walk(node => {
  193. const { type } = node;
  194. if ((0, _has2.default)(params, type)) {
  195. const param = params[type];
  196. const key = node[param] + '|' + preferredQuote;
  197. if (cache[key]) {
  198. node[param] = cache[key];
  199. return;
  200. }
  201. const result = normalize(node[param], preferredQuote);
  202. node[param] = result;
  203. cache[key] = result;
  204. }
  205. });
  206. };
  207. });
  208. module.exports = exports['default'];