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 | };
|
---|