index.js 5.4 KB

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