index.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. 'use strict';
  2. const atRuleParamIndex = require('../../utils/atRuleParamIndex');
  3. const declarationValueIndex = require('../../utils/declarationValueIndex');
  4. const functionArgumentsSearch = require('../../utils/functionArgumentsSearch');
  5. const getAtRuleParams = require('../../utils/getAtRuleParams');
  6. const getDeclarationValue = require('../../utils/getDeclarationValue');
  7. const isStandardSyntaxUrl = require('../../utils/isStandardSyntaxUrl');
  8. const isStandardSyntaxDeclaration = require('../../utils/isStandardSyntaxDeclaration');
  9. const optionsMatches = require('../../utils/optionsMatches');
  10. const report = require('../../utils/report');
  11. const ruleMessages = require('../../utils/ruleMessages');
  12. const validateOptions = require('../../utils/validateOptions');
  13. const ruleName = 'function-url-quotes';
  14. const messages = ruleMessages(ruleName, {
  15. expected: (functionName) => `Expected quotes around "${functionName}" function argument`,
  16. rejected: (functionName) => `Unexpected quotes around "${functionName}" function argument`,
  17. });
  18. const meta = {
  19. url: 'https://stylelint.io/user-guide/rules/function-url-quotes',
  20. fixable: true,
  21. };
  22. /** @type {import('stylelint').Rule} */
  23. const rule = (primary, secondaryOptions, context) => {
  24. return (root, result) => {
  25. const validOptions = validateOptions(
  26. result,
  27. ruleName,
  28. {
  29. actual: primary,
  30. possible: ['always', 'never'],
  31. },
  32. {
  33. actual: secondaryOptions,
  34. possible: {
  35. except: ['empty'],
  36. },
  37. optional: true,
  38. },
  39. );
  40. if (!validOptions) {
  41. return;
  42. }
  43. const exceptEmpty = optionsMatches(secondaryOptions, 'except', 'empty');
  44. const emptyArgumentPatterns = new Set(['', "''", '""']);
  45. root.walkAtRules(checkAtRuleParams);
  46. root.walkDecls(checkDeclParams);
  47. /**
  48. * @param {import('postcss').Declaration} decl
  49. */
  50. function checkDeclParams(decl) {
  51. if (!isStandardSyntaxDeclaration(decl)) return;
  52. const value = getDeclarationValue(decl);
  53. const startIndex = declarationValueIndex(decl);
  54. const parsed = functionArgumentsSearch(value, /^url$/i, (args, index, funcNode) => {
  55. checkArgs(decl, args, startIndex + index, funcNode);
  56. });
  57. if (context.fix) {
  58. decl.value = parsed.toString();
  59. }
  60. }
  61. /**
  62. * @param {import('postcss').AtRule} atRule
  63. */
  64. function checkAtRuleParams(atRule) {
  65. const params = getAtRuleParams(atRule);
  66. const startIndex = atRuleParamIndex(atRule);
  67. const parsed = functionArgumentsSearch(params, /^url$/i, (args, index, funcNode) => {
  68. checkArgs(atRule, args, startIndex + index, funcNode);
  69. });
  70. if (context.fix) {
  71. atRule.params = parsed.toString();
  72. }
  73. }
  74. /**
  75. * @param {import('postcss-value-parser').FunctionNode} funcNode
  76. */
  77. function addQuotes(funcNode) {
  78. for (const argNode of funcNode.nodes) {
  79. if (argNode.type === 'word') {
  80. argNode.value = `"${argNode.value}"`;
  81. }
  82. }
  83. }
  84. /**
  85. * @param {import('postcss-value-parser').FunctionNode} funcNode
  86. */
  87. function removeQuotes(funcNode) {
  88. for (const argNode of funcNode.nodes) {
  89. if (argNode.type === 'string') {
  90. // NOTE: We can ignore this error because the test passes.
  91. // @ts-expect-error -- TS2322: Type '"word"' is not assignable to type '"string"'.
  92. argNode.type = 'word';
  93. }
  94. }
  95. }
  96. /**
  97. * @param {import('postcss').Declaration | import('postcss').AtRule} node
  98. * @param {string} args
  99. * @param {number} index
  100. * @param {import('postcss-value-parser').FunctionNode} funcNode
  101. */
  102. function checkArgs(node, args, index, funcNode) {
  103. const functionName = funcNode.value.toLowerCase();
  104. let shouldHasQuotes = primary === 'always';
  105. const leftTrimmedArgs = args.trimStart();
  106. if (!isStandardSyntaxUrl(leftTrimmedArgs)) {
  107. return;
  108. }
  109. const complaintIndex = index + args.length - leftTrimmedArgs.length;
  110. const complaintEndIndex = index + args.length;
  111. const hasQuotes = leftTrimmedArgs.startsWith("'") || leftTrimmedArgs.startsWith('"');
  112. if (exceptEmpty && emptyArgumentPatterns.has(args.trim())) {
  113. shouldHasQuotes = !shouldHasQuotes;
  114. }
  115. if (shouldHasQuotes) {
  116. if (hasQuotes) {
  117. return;
  118. }
  119. if (context.fix) {
  120. addQuotes(funcNode);
  121. return;
  122. }
  123. complain(messages.expected(functionName), node, complaintIndex, complaintEndIndex);
  124. } else {
  125. if (!hasQuotes) {
  126. return;
  127. }
  128. if (context.fix) {
  129. removeQuotes(funcNode);
  130. return;
  131. }
  132. complain(messages.rejected(functionName), node, complaintIndex, complaintEndIndex);
  133. }
  134. }
  135. /**
  136. * @param {string} message
  137. * @param {import('postcss').Node} node
  138. * @param {number} index
  139. * @param {number} endIndex
  140. */
  141. function complain(message, node, index, endIndex) {
  142. report({
  143. message,
  144. node,
  145. index,
  146. endIndex,
  147. result,
  148. ruleName,
  149. });
  150. }
  151. };
  152. };
  153. rule.ruleName = ruleName;
  154. rule.messages = messages;
  155. rule.meta = meta;
  156. module.exports = rule;