jsx-child-element-spacing.js 2.8 KB

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