Skip to content

Commit d042605

Browse files
committed
implement indent
1 parent fa12979 commit d042605

2 files changed

Lines changed: 543 additions & 0 deletions

File tree

lib/rules/template-indent.js

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
const { builtinRules } = require('eslint/use-at-your-own-risk');
2+
3+
const baseRule = builtinRules.get('indent');
4+
const IGNORED_ELEMENTS = new Set(['pre', 'script', 'style', 'textarea']);
5+
6+
/** @type {import('eslint').Rule.RuleModule} */
7+
module.exports = {
8+
ERROR_MESSAGE: baseRule.meta.messages.wrongIndentation,
9+
name: 'indent',
10+
meta: {
11+
type: 'layout',
12+
docs: {
13+
description: 'enforce consistent indentation',
14+
extendsBaseRule: true,
15+
// too opinionated to be recommended
16+
recommended: false,
17+
category: 'Ember Octane',
18+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-indent.md',
19+
},
20+
fixable: 'whitespace',
21+
hasSuggestions: baseRule.meta.hasSuggestions,
22+
schema: baseRule.meta.schema,
23+
messages: baseRule.meta.messages,
24+
},
25+
26+
create: (context) => {
27+
const ctx = Object.create(context, {
28+
report: {
29+
writable: false,
30+
configurable: false,
31+
value: (info) => {
32+
const node = context.sourceCode.getNodeByRangeIndex(info.node.range[0]);
33+
if (!node.type.startsWith('Glimmer')) {
34+
return;
35+
}
36+
context.report(info);
37+
},
38+
},
39+
});
40+
const rules = baseRule.create(ctx);
41+
const sourceCode = context.sourceCode;
42+
43+
function JSXElement(node) {
44+
let closingElement;
45+
let openingElement;
46+
if (node.type === 'GlimmerElementNode') {
47+
const tokens = sourceCode.getTokens(node);
48+
const openEnd = tokens.find((t) => t.value === '>');
49+
const closeStart = tokens.findLast((t) => t.value === '<');
50+
if (!node.selfClosing) {
51+
closingElement = {
52+
type: 'JSXClosingElement',
53+
parent: node,
54+
range: [closeStart.range[0], node.range[1]],
55+
loc: {
56+
start: Object.assign({}, node.loc.start),
57+
end: Object.assign({}, node.loc.end),
58+
},
59+
};
60+
closingElement.loc.start = sourceCode.getLocFromIndex(closeStart.range[0]);
61+
closingElement.name = { ...closingElement, type: 'JSXIdentifier' };
62+
closingElement.name.range = [
63+
closingElement.name.range[0] + 1,
64+
closingElement.name.range[1] - 1,
65+
];
66+
}
67+
68+
openingElement = {
69+
type: 'JSXOpeningElement',
70+
selfClosing: node.selfClosing,
71+
attributes: node.attributes,
72+
parent: node,
73+
range: [node.range[0], openEnd.range[1]],
74+
loc: {
75+
start: Object.assign({}, node.loc.start),
76+
end: Object.assign({}, node.loc.end),
77+
},
78+
};
79+
openingElement.loc.end = sourceCode.getLocFromIndex(openEnd.range[1]);
80+
openingElement.name = { ...openingElement, type: 'JSXIdentifier' };
81+
openingElement.name.range = [
82+
openingElement.name.range[0] + 1,
83+
openingElement.name.range[1] - 1,
84+
];
85+
}
86+
if (node.type === 'GlimmerBlockStatement') {
87+
const tokens = sourceCode.getTokens(node);
88+
let openEndIdx = tokens.findIndex((t) => t.value === '}');
89+
while (tokens[openEndIdx + 1].value === '}') {
90+
openEndIdx += 1;
91+
}
92+
const openEnd = tokens[openEndIdx];
93+
let closeStartIdx = tokens.findLastIndex((t) => t.value === '{');
94+
while (tokens[closeStartIdx - 1].value === '{') {
95+
closeStartIdx -= 1;
96+
}
97+
const closeStart = tokens[closeStartIdx];
98+
closingElement = {
99+
type: 'JSXClosingElement',
100+
parent: node,
101+
range: [closeStart.range[0], node.range[1]],
102+
loc: {
103+
start: Object.assign({}, node.loc.start),
104+
end: Object.assign({}, node.loc.end),
105+
},
106+
};
107+
closingElement.loc.start = sourceCode.getLocFromIndex(closeStart.range[0]);
108+
109+
openingElement = {
110+
type: 'JSXOpeningElement',
111+
attributes: node.params,
112+
parent: node,
113+
range: [node.range[0], openEnd.range[1]],
114+
loc: {
115+
start: Object.assign({}, node.loc.start),
116+
end: Object.assign({}, node.loc.end),
117+
},
118+
};
119+
openingElement.loc.end = sourceCode.getLocFromIndex(openEnd.range[1]);
120+
}
121+
return {
122+
type: 'JSXElement',
123+
openingElement,
124+
closingElement,
125+
children: node.children || node.body,
126+
parent: node.parent,
127+
range: node.range,
128+
loc: node.loc,
129+
};
130+
}
131+
132+
const ignoredStack = new Set();
133+
134+
return Object.assign({}, rules, {
135+
// overwrite the base rule here so we can use our KNOWN_NODES list instead
136+
'*:exit'(node) {
137+
// For nodes we care about, skip the default handling, because it just marks the node as ignored...
138+
if (
139+
!node.type.startsWith('Glimmer') ||
140+
(ignoredStack.size > 0 && !ignoredStack.has(node))
141+
) {
142+
rules['*:exit'](node);
143+
}
144+
if (ignoredStack.has(node)) {
145+
ignoredStack.delete(node);
146+
}
147+
},
148+
'GlimmerTemplate:exit'(node) {
149+
if (!node.parent) {
150+
rules['Program:exit'](node);
151+
}
152+
},
153+
GlimmerElementNode(node) {
154+
if (ignoredStack.size > 0) {
155+
return;
156+
}
157+
if (IGNORED_ELEMENTS.has(node.tag)) {
158+
ignoredStack.add(node);
159+
}
160+
const jsx = JSXElement(node);
161+
rules['JSXElement'](jsx);
162+
rules['JSXOpeningElement'](jsx.openingElement);
163+
if (jsx.closingElement) {
164+
rules['JSXClosingElement'](jsx.closingElement);
165+
}
166+
},
167+
GlimmerAttrNode(node) {
168+
if (ignoredStack.size > 0 || !node.value) {
169+
return;
170+
}
171+
rules['JSXAttribute[value]']({
172+
...node,
173+
type: 'JSXAttribute',
174+
name: {
175+
type: 'JSXIdentifier',
176+
name: node.name,
177+
range: [node.range[0], node.range[0] + node.name.length - 1],
178+
},
179+
});
180+
},
181+
GlimmerTemplate(node) {
182+
if (!node.parent) {
183+
return;
184+
}
185+
const jsx = JSXElement({ ...node, tag: 'template', type: 'GlimmerElementNode' });
186+
rules['JSXElement'](jsx);
187+
},
188+
GlimmerBlockStatement(node) {
189+
const body = [...node.program.body, ...(node.inverse?.body || [])];
190+
rules['JSXElement'](JSXElement({ ...node, body }));
191+
},
192+
});
193+
},
194+
};

0 commit comments

Comments
 (0)