index.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  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. };
  18. /** @type {import('stylelint').Rule} */
  19. const rule = (primary, _secondaryOptions, context) => {
  20. return (root, result) => {
  21. const validOptions = validateOptions(result, ruleName, {
  22. actual: primary,
  23. possible: ['always', 'never'],
  24. });
  25. if (!validOptions) {
  26. return;
  27. }
  28. root.walkRules((ruleNode) => {
  29. if (!isStandardSyntaxRule(ruleNode)) {
  30. return;
  31. }
  32. if (!ruleNode.selector.includes('[')) {
  33. return;
  34. }
  35. const selector = ruleNode.raws.selector ? ruleNode.raws.selector.raw : ruleNode.selector;
  36. let hasFixed;
  37. const fixedSelector = parseSelector(selector, result, ruleNode, (selectorTree) => {
  38. selectorTree.walkAttributes((attributeNode) => {
  39. const attributeSelectorString = attributeNode.toString();
  40. styleSearch({ source: attributeSelectorString, target: '[' }, (match) => {
  41. const nextCharIsSpace = attributeSelectorString[match.startIndex + 1] === ' ';
  42. const index = attributeNode.sourceIndex + match.startIndex + 1;
  43. if (nextCharIsSpace && primary === 'never') {
  44. if (context.fix) {
  45. hasFixed = true;
  46. fixBefore(attributeNode);
  47. return;
  48. }
  49. complain(messages.rejectedOpening, index);
  50. }
  51. if (!nextCharIsSpace && primary === 'always') {
  52. if (context.fix) {
  53. hasFixed = true;
  54. fixBefore(attributeNode);
  55. return;
  56. }
  57. complain(messages.expectedOpening, index);
  58. }
  59. });
  60. styleSearch({ source: attributeSelectorString, target: ']' }, (match) => {
  61. const prevCharIsSpace = attributeSelectorString[match.startIndex - 1] === ' ';
  62. const index = attributeNode.sourceIndex + match.startIndex - 1;
  63. if (prevCharIsSpace && primary === 'never') {
  64. if (context.fix) {
  65. hasFixed = true;
  66. fixAfter(attributeNode);
  67. return;
  68. }
  69. complain(messages.rejectedClosing, index);
  70. }
  71. if (!prevCharIsSpace && primary === 'always') {
  72. if (context.fix) {
  73. hasFixed = true;
  74. fixAfter(attributeNode);
  75. return;
  76. }
  77. complain(messages.expectedClosing, index);
  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. * @param {import('postcss-selector-parser').Attribute} attributeNode
  106. */
  107. function fixBefore(attributeNode) {
  108. const spacesAttribute = attributeNode.raws.spaces && attributeNode.raws.spaces.attribute;
  109. const rawAttrBefore = spacesAttribute && spacesAttribute.before;
  110. /** @type {{ attrBefore: string, setAttrBefore: (fixed: string) => void }} */
  111. const { attrBefore, setAttrBefore } = rawAttrBefore
  112. ? {
  113. attrBefore: rawAttrBefore,
  114. setAttrBefore(fixed) {
  115. spacesAttribute.before = fixed;
  116. },
  117. }
  118. : {
  119. attrBefore:
  120. (attributeNode.spaces.attribute && attributeNode.spaces.attribute.before) || '',
  121. setAttrBefore(fixed) {
  122. if (!attributeNode.spaces.attribute) attributeNode.spaces.attribute = {};
  123. attributeNode.spaces.attribute.before = fixed;
  124. },
  125. };
  126. if (primary === 'always') {
  127. setAttrBefore(attrBefore.replace(/^\s*/, ' '));
  128. } else if (primary === 'never') {
  129. setAttrBefore(attrBefore.replace(/^\s*/, ''));
  130. }
  131. }
  132. /**
  133. * @param {import('postcss-selector-parser').Attribute} attributeNode
  134. */
  135. function fixAfter(attributeNode) {
  136. const key = attributeNode.operator
  137. ? attributeNode.insensitive
  138. ? 'insensitive'
  139. : 'value'
  140. : 'attribute';
  141. const rawSpaces = attributeNode.raws.spaces && attributeNode.raws.spaces[key];
  142. const rawAfter = rawSpaces && rawSpaces.after;
  143. const spaces = attributeNode.spaces[key];
  144. /** @type {{ after: string, setAfter: (fixed: string) => void }} */
  145. const { after, setAfter } = rawAfter
  146. ? {
  147. after: rawAfter,
  148. setAfter(fixed) {
  149. rawSpaces.after = fixed;
  150. },
  151. }
  152. : {
  153. after: (spaces && spaces.after) || '',
  154. setAfter(fixed) {
  155. if (!attributeNode.spaces[key]) attributeNode.spaces[key] = {};
  156. // @ts-expect-error -- TS2532: Object is possibly 'undefined'.
  157. attributeNode.spaces[key].after = fixed;
  158. },
  159. };
  160. if (primary === 'always') {
  161. setAfter(after.replace(/\s*$/, ' '));
  162. } else if (primary === 'never') {
  163. setAfter(after.replace(/\s*$/, ''));
  164. }
  165. }
  166. };
  167. rule.ruleName = ruleName;
  168. rule.messages = messages;
  169. rule.meta = meta;
  170. module.exports = rule;