index.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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 styleSearch = require('style-search');
  7. const validateOptions = require('../../utils/validateOptions');
  8. const ruleName = 'selector-attribute-brackets-space-inside';
  9. const messages = ruleMessages(ruleName, {
  10. expectedOpening: 'Expected single space after "["',
  11. rejectedOpening: 'Unexpected whitespace after "["',
  12. expectedClosing: 'Expected single space before "]"',
  13. rejectedClosing: 'Unexpected whitespace before "]"',
  14. });
  15. const meta = {
  16. url: 'https://stylelint.io/user-guide/rules/selector-attribute-brackets-space-inside',
  17. fixable: true,
  18. deprecated: true,
  19. };
  20. /** @type {import('stylelint').Rule} */
  21. const rule = (primary, _secondaryOptions, context) => {
  22. return (root, result) => {
  23. const validOptions = validateOptions(result, ruleName, {
  24. actual: primary,
  25. possible: ['always', 'never'],
  26. });
  27. if (!validOptions) {
  28. return;
  29. }
  30. root.walkRules((ruleNode) => {
  31. if (!isStandardSyntaxRule(ruleNode)) {
  32. return;
  33. }
  34. if (!ruleNode.selector.includes('[')) {
  35. return;
  36. }
  37. const selector = ruleNode.raws.selector ? ruleNode.raws.selector.raw : ruleNode.selector;
  38. let hasFixed;
  39. const fixedSelector = parseSelector(selector, result, ruleNode, (selectorTree) => {
  40. selectorTree.walkAttributes((attributeNode) => {
  41. const attributeSelectorString = attributeNode.toString();
  42. styleSearch({ source: attributeSelectorString, target: '[' }, (match) => {
  43. const nextCharIsSpace = attributeSelectorString[match.startIndex + 1] === ' ';
  44. const index = attributeNode.sourceIndex + match.startIndex + 1;
  45. if (nextCharIsSpace && primary === 'never') {
  46. if (context.fix) {
  47. hasFixed = true;
  48. fixBefore(attributeNode);
  49. return;
  50. }
  51. complain(messages.rejectedOpening, index);
  52. }
  53. if (!nextCharIsSpace && primary === 'always') {
  54. if (context.fix) {
  55. hasFixed = true;
  56. fixBefore(attributeNode);
  57. return;
  58. }
  59. complain(messages.expectedOpening, index);
  60. }
  61. });
  62. styleSearch({ source: attributeSelectorString, target: ']' }, (match) => {
  63. const prevCharIsSpace = attributeSelectorString[match.startIndex - 1] === ' ';
  64. const index = attributeNode.sourceIndex + match.startIndex - 1;
  65. if (prevCharIsSpace && primary === 'never') {
  66. if (context.fix) {
  67. hasFixed = true;
  68. fixAfter(attributeNode);
  69. return;
  70. }
  71. complain(messages.rejectedClosing, index);
  72. }
  73. if (!prevCharIsSpace && primary === 'always') {
  74. if (context.fix) {
  75. hasFixed = true;
  76. fixAfter(attributeNode);
  77. return;
  78. }
  79. complain(messages.expectedClosing, index);
  80. }
  81. });
  82. });
  83. });
  84. if (hasFixed && fixedSelector) {
  85. if (!ruleNode.raws.selector) {
  86. ruleNode.selector = fixedSelector;
  87. } else {
  88. ruleNode.raws.selector.raw = fixedSelector;
  89. }
  90. }
  91. /**
  92. * @param {string} message
  93. * @param {number} index
  94. */
  95. function complain(message, index) {
  96. report({
  97. message,
  98. index,
  99. result,
  100. ruleName,
  101. node: ruleNode,
  102. });
  103. }
  104. });
  105. };
  106. /**
  107. * @param {import('postcss-selector-parser').Attribute} attributeNode
  108. */
  109. function fixBefore(attributeNode) {
  110. const spacesAttribute = attributeNode.raws.spaces && attributeNode.raws.spaces.attribute;
  111. const rawAttrBefore = spacesAttribute && spacesAttribute.before;
  112. /** @type {{ attrBefore: string, setAttrBefore: (fixed: string) => void }} */
  113. const { attrBefore, setAttrBefore } = rawAttrBefore
  114. ? {
  115. attrBefore: rawAttrBefore,
  116. setAttrBefore(fixed) {
  117. spacesAttribute.before = fixed;
  118. },
  119. }
  120. : {
  121. attrBefore:
  122. (attributeNode.spaces.attribute && attributeNode.spaces.attribute.before) || '',
  123. setAttrBefore(fixed) {
  124. if (!attributeNode.spaces.attribute) attributeNode.spaces.attribute = {};
  125. attributeNode.spaces.attribute.before = fixed;
  126. },
  127. };
  128. if (primary === 'always') {
  129. setAttrBefore(attrBefore.replace(/^\s*/, ' '));
  130. } else if (primary === 'never') {
  131. setAttrBefore(attrBefore.replace(/^\s*/, ''));
  132. }
  133. }
  134. /**
  135. * @param {import('postcss-selector-parser').Attribute} attributeNode
  136. */
  137. function fixAfter(attributeNode) {
  138. const key = attributeNode.operator
  139. ? attributeNode.insensitive
  140. ? 'insensitive'
  141. : 'value'
  142. : 'attribute';
  143. const rawSpaces = attributeNode.raws.spaces && attributeNode.raws.spaces[key];
  144. const rawAfter = rawSpaces && rawSpaces.after;
  145. const spaces = attributeNode.spaces[key];
  146. /** @type {{ after: string, setAfter: (fixed: string) => void }} */
  147. const { after, setAfter } = rawAfter
  148. ? {
  149. after: rawAfter,
  150. setAfter(fixed) {
  151. rawSpaces.after = fixed;
  152. },
  153. }
  154. : {
  155. after: (spaces && spaces.after) || '',
  156. setAfter(fixed) {
  157. if (!attributeNode.spaces[key]) attributeNode.spaces[key] = {};
  158. // @ts-expect-error -- TS2532: Object is possibly 'undefined'.
  159. attributeNode.spaces[key].after = fixed;
  160. },
  161. };
  162. if (primary === 'always') {
  163. setAfter(after.replace(/\s*$/, ' '));
  164. } else if (primary === 'never') {
  165. setAfter(after.replace(/\s*$/, ''));
  166. }
  167. }
  168. };
  169. rule.ruleName = ruleName;
  170. rule.messages = messages;
  171. rule.meta = meta;
  172. module.exports = rule;