Skip to content

Commit f69523b

Browse files
committed
implement indent
1 parent fa12979 commit f69523b

4 files changed

Lines changed: 616 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ module.exports = {
164164
| [no-ember-super-in-es-classes](docs/rules/no-ember-super-in-es-classes.md) | disallow use of `this._super` in ES class methods || 🔧 | |
165165
| [no-empty-glimmer-component-classes](docs/rules/no-empty-glimmer-component-classes.md) | disallow empty backing classes for Glimmer components || | |
166166
| [no-tracked-properties-from-args](docs/rules/no-tracked-properties-from-args.md) | disallow creating @tracked properties from this.args || | |
167+
| [template-indent](docs/rules/template-indent.md) | enforce consistent indentation | | 🔧 | |
167168

168169
### jQuery
169170

docs/rules/template-indent.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# ember/template-indent
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
This rule extends the base [eslint indent](https://eslint.org/docs/latest/rules/indent) rule, but only applies the indents to Glimmer Nodes.
8+
9+
Otherwise, it receives the same options as the original and can run together with the base rule.
10+
11+
## Configuration
12+
13+
<!-- begin auto-generated rule options list -->
14+
15+
| Name | Type | Default |
16+
| :--------------- | :------- | :------ |
17+
| `ignoreComments` | Boolean | `false` |
18+
| `ignoredNodes` | String[] | |
19+
20+
<!-- end auto-generated rule options list -->
21+
22+
## Rule Details
23+
24+
Enforce consistent indentation for fcct templates
25+
26+
```js
27+
const rules = {
28+
'ember/template-indent': [
29+
'error',
30+
2, // or 'tab'
31+
{
32+
ignoreComments: false,
33+
ignoredNodes: []
34+
}
35+
]
36+
};
37+
```
38+
39+
## Examples
40+
41+
Examples of **incorrect** code for this rule:
42+
43+
```gjs
44+
// my-octane-component.gjs
45+
<template>
46+
<div>
47+
48+
</div>
49+
</template>
50+
}
51+
```
52+
53+
Examples of **correct** code for this rule:
54+
55+
```gjs
56+
// my-component.gjs
57+
<template>
58+
<div>
59+
60+
</div>
61+
</template>
62+
```
63+
64+
## References
65+
66+
- [eslint indent](https://eslint.org/docs/latest/rules/indent)

lib/rules/template-indent.js

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

0 commit comments

Comments
 (0)