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