index.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. 'use strict';
  2. const isStandardSyntaxRule = require('../../utils/isStandardSyntaxRule');
  3. const parseSelector = require('../../utils/parseSelector');
  4. const report = require('../../utils/report');
  5. const ruleMessages = require('../../utils/ruleMessages');
  6. const validateOptions = require('../../utils/validateOptions');
  7. const ruleName = 'selector-pseudo-class-parentheses-space-inside';
  8. const messages = ruleMessages(ruleName, {
  9. expectedOpening: 'Expected single space after "("',
  10. rejectedOpening: 'Unexpected whitespace after "("',
  11. expectedClosing: 'Expected single space before ")"',
  12. rejectedClosing: 'Unexpected whitespace before ")"',
  13. });
  14. const meta = {
  15. url: 'https://stylelint.io/user-guide/rules/selector-pseudo-class-parentheses-space-inside',
  16. fixable: true,
  17. deprecated: true,
  18. };
  19. /** @type {import('stylelint').Rule} */
  20. const rule = (primary, _secondaryOptions, context) => {
  21. return (root, result) => {
  22. const validOptions = validateOptions(result, ruleName, {
  23. actual: primary,
  24. possible: ['always', 'never'],
  25. });
  26. if (!validOptions) {
  27. return;
  28. }
  29. root.walkRules((ruleNode) => {
  30. if (!isStandardSyntaxRule(ruleNode)) {
  31. return;
  32. }
  33. if (!ruleNode.selector.includes('(')) {
  34. return;
  35. }
  36. let hasFixed = false;
  37. const selector = ruleNode.raws.selector ? ruleNode.raws.selector.raw : ruleNode.selector;
  38. const fixedSelector = parseSelector(selector, result, ruleNode, (selectorTree) => {
  39. selectorTree.walkPseudos((pseudoNode) => {
  40. if (!pseudoNode.length) {
  41. return;
  42. }
  43. const paramString = pseudoNode.map((node) => String(node)).join(',');
  44. const nextCharIsSpace = paramString.startsWith(' ');
  45. const openIndex = pseudoNode.sourceIndex + pseudoNode.value.length + 1;
  46. if (nextCharIsSpace && primary === 'never') {
  47. if (context.fix) {
  48. hasFixed = true;
  49. setFirstNodeSpaceBefore(pseudoNode, '');
  50. } else {
  51. complain(messages.rejectedOpening, openIndex);
  52. }
  53. }
  54. if (!nextCharIsSpace && primary === 'always') {
  55. if (context.fix) {
  56. hasFixed = true;
  57. setFirstNodeSpaceBefore(pseudoNode, ' ');
  58. } else {
  59. complain(messages.expectedOpening, openIndex);
  60. }
  61. }
  62. const prevCharIsSpace = paramString.endsWith(' ');
  63. const closeIndex = openIndex + paramString.length - 1;
  64. if (prevCharIsSpace && primary === 'never') {
  65. if (context.fix) {
  66. hasFixed = true;
  67. setLastNodeSpaceAfter(pseudoNode, '');
  68. } else {
  69. complain(messages.rejectedClosing, closeIndex);
  70. }
  71. }
  72. if (!prevCharIsSpace && primary === 'always') {
  73. if (context.fix) {
  74. hasFixed = true;
  75. setLastNodeSpaceAfter(pseudoNode, ' ');
  76. } else {
  77. complain(messages.expectedClosing, closeIndex);
  78. }
  79. }
  80. });
  81. });
  82. if (hasFixed && fixedSelector) {
  83. if (!ruleNode.raws.selector) {
  84. ruleNode.selector = fixedSelector;
  85. } else {
  86. ruleNode.raws.selector.raw = fixedSelector;
  87. }
  88. }
  89. /**
  90. * @param {string} message
  91. * @param {number} index
  92. */
  93. function complain(message, index) {
  94. report({
  95. message,
  96. index,
  97. result,
  98. ruleName,
  99. node: ruleNode,
  100. });
  101. }
  102. });
  103. };
  104. };
  105. /**
  106. * @param {import('postcss-selector-parser').Container} node
  107. * @param {string} value
  108. * @returns {void}
  109. */
  110. function setFirstNodeSpaceBefore(node, value) {
  111. const target = node.first;
  112. if (target.type === 'selector') {
  113. setFirstNodeSpaceBefore(target, value);
  114. } else {
  115. target.spaces.before = value;
  116. }
  117. }
  118. /**
  119. * @param {import('postcss-selector-parser').Container} node
  120. * @param {string} value
  121. * @returns {void}
  122. */
  123. function setLastNodeSpaceAfter(node, value) {
  124. const target = node.last;
  125. if (target.type === 'selector') {
  126. setLastNodeSpaceAfter(target, value);
  127. } else {
  128. target.spaces.after = value;
  129. }
  130. }
  131. rule.ruleName = ruleName;
  132. rule.messages = messages;
  133. rule.meta = meta;
  134. module.exports = rule;