jsx-child-element-spacing.js 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. 'use strict';
  2. const docsUrl = require('../util/docsUrl');
  3. const report = require('../util/report');
  4. // This list is taken from https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements
  5. // Note: 'br' is not included because whitespace around br tags is inconsequential to the rendered output
  6. const INLINE_ELEMENTS = new Set([
  7. 'a',
  8. 'abbr',
  9. 'acronym',
  10. 'b',
  11. 'bdo',
  12. 'big',
  13. 'button',
  14. 'cite',
  15. 'code',
  16. 'dfn',
  17. 'em',
  18. 'i',
  19. 'img',
  20. 'input',
  21. 'kbd',
  22. 'label',
  23. 'map',
  24. 'object',
  25. 'q',
  26. 'samp',
  27. 'script',
  28. 'select',
  29. 'small',
  30. 'span',
  31. 'strong',
  32. 'sub',
  33. 'sup',
  34. 'textarea',
  35. 'tt',
  36. 'var',
  37. ]);
  38. const messages = {
  39. spacingAfterPrev: 'Ambiguous spacing after previous element {{element}}',
  40. spacingBeforeNext: 'Ambiguous spacing before next element {{element}}',
  41. };
  42. module.exports = {
  43. meta: {
  44. docs: {
  45. description: 'Enforce or disallow spaces inside of curly braces in JSX attributes and expressions',
  46. category: 'Stylistic Issues',
  47. recommended: false,
  48. url: docsUrl('jsx-child-element-spacing'),
  49. },
  50. fixable: null,
  51. messages,
  52. schema: [],
  53. },
  54. create(context) {
  55. const TEXT_FOLLOWING_ELEMENT_PATTERN = /^\s*\n\s*\S/;
  56. const TEXT_PRECEDING_ELEMENT_PATTERN = /\S\s*\n\s*$/;
  57. const elementName = (node) => (
  58. node.openingElement
  59. && node.openingElement.name
  60. && node.openingElement.name.type === 'JSXIdentifier'
  61. && node.openingElement.name.name
  62. );
  63. const isInlineElement = (node) => (
  64. node.type === 'JSXElement'
  65. && INLINE_ELEMENTS.has(elementName(node))
  66. );
  67. const handleJSX = (node) => {
  68. let lastChild = null;
  69. let child = null;
  70. (node.children.concat([null])).forEach((nextChild) => {
  71. if (
  72. (lastChild || nextChild)
  73. && (!lastChild || isInlineElement(lastChild))
  74. && (child && (child.type === 'Literal' || child.type === 'JSXText'))
  75. && (!nextChild || isInlineElement(nextChild))
  76. && true
  77. ) {
  78. if (lastChild && child.value.match(TEXT_FOLLOWING_ELEMENT_PATTERN)) {
  79. report(context, messages.spacingAfterPrev, 'spacingAfterPrev', {
  80. node: lastChild,
  81. loc: lastChild.loc.end,
  82. data: {
  83. element: elementName(lastChild),
  84. },
  85. });
  86. } else if (nextChild && child.value.match(TEXT_PRECEDING_ELEMENT_PATTERN)) {
  87. report(context, messages.spacingBeforeNext, 'spacingBeforeNext', {
  88. node: nextChild,
  89. loc: nextChild.loc.start,
  90. data: {
  91. element: elementName(nextChild),
  92. },
  93. });
  94. }
  95. }
  96. lastChild = child;
  97. child = nextChild;
  98. });
  99. };
  100. return {
  101. JSXElement: handleJSX,
  102. JSXFragment: handleJSX,
  103. };
  104. },
  105. };