index.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. 'use strict';
  2. const atRuleParamIndex = require('../../utils/atRuleParamIndex');
  3. const declarationValueIndex = require('../../utils/declarationValueIndex');
  4. const isStandardSyntaxSelector = require('../../utils/isStandardSyntaxSelector');
  5. const parseSelector = require('../../utils/parseSelector');
  6. const report = require('../../utils/report');
  7. const ruleMessages = require('../../utils/ruleMessages');
  8. const validateOptions = require('../../utils/validateOptions');
  9. const valueParser = require('postcss-value-parser');
  10. const ruleName = 'string-no-newline';
  11. const reNewLine = /\r?\n/;
  12. const messages = ruleMessages(ruleName, {
  13. rejected: 'Unexpected newline in string',
  14. });
  15. const meta = {
  16. url: 'https://stylelint.io/user-guide/rules/list/string-no-newline',
  17. };
  18. /** @type {import('stylelint').Rule} */
  19. const rule = (primary) => {
  20. return (root, result) => {
  21. const validOptions = validateOptions(result, ruleName, { actual: primary });
  22. if (!validOptions) {
  23. return;
  24. }
  25. root.walk((node) => {
  26. switch (node.type) {
  27. case 'atrule':
  28. checkDeclOrAtRule(node, node.params, atRuleParamIndex);
  29. break;
  30. case 'decl':
  31. checkDeclOrAtRule(node, node.value, declarationValueIndex);
  32. break;
  33. case 'rule':
  34. checkRule(node);
  35. break;
  36. }
  37. });
  38. /**
  39. * @param {import('postcss').Rule} ruleNode
  40. * @returns {void}
  41. */
  42. function checkRule(ruleNode) {
  43. // Get out quickly if there are no new line
  44. if (!reNewLine.test(ruleNode.selector)) {
  45. return;
  46. }
  47. if (!isStandardSyntaxSelector(ruleNode.selector)) {
  48. return;
  49. }
  50. parseSelector(ruleNode.selector, result, ruleNode, (selectorTree) => {
  51. selectorTree.walkAttributes((attributeNode) => {
  52. const match = reNewLine.exec(attributeNode.value || '');
  53. if (!match) {
  54. return;
  55. }
  56. const openIndex = [
  57. // length of our attribute
  58. attributeNode.attribute,
  59. // length of our operator , ie '='
  60. attributeNode.operator || '',
  61. // length of the contents before newline
  62. match.input.slice(0, match.index),
  63. ].reduce(
  64. (index, str) => index + str.length,
  65. // index of the start of our attribute node in our source
  66. // plus 1 for the opening quotation mark
  67. attributeNode.sourceIndex + 1,
  68. );
  69. report({
  70. message: messages.rejected,
  71. node: ruleNode,
  72. index: openIndex,
  73. result,
  74. ruleName,
  75. });
  76. });
  77. });
  78. }
  79. /**
  80. * @template {import('postcss').AtRule | import('postcss').Declaration} T
  81. * @param {T} node
  82. * @param {string} value
  83. * @param {(node: T) => number} getIndex
  84. * @returns {void}
  85. */
  86. function checkDeclOrAtRule(node, value, getIndex) {
  87. // Get out quickly if there are no new line
  88. if (!reNewLine.test(value)) {
  89. return;
  90. }
  91. valueParser(value).walk((valueNode) => {
  92. if (valueNode.type !== 'string') {
  93. return;
  94. }
  95. const match = reNewLine.exec(valueNode.value);
  96. if (!match) {
  97. return;
  98. }
  99. const openIndex = [
  100. // length of the quote
  101. valueNode.quote,
  102. // length of the contents before newline
  103. match.input.slice(0, match.index),
  104. ].reduce((index, str) => index + str.length, valueNode.sourceIndex);
  105. report({
  106. message: messages.rejected,
  107. node,
  108. index: getIndex(node) + openIndex,
  109. result,
  110. ruleName,
  111. });
  112. });
  113. }
  114. };
  115. };
  116. rule.ruleName = ruleName;
  117. rule.messages = messages;
  118. rule.meta = meta;
  119. module.exports = rule;