|  | 
|  | 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 | +      // too opinionated to be recommended | 
|  | 15 | +      extendsBaseRule: true, | 
|  | 16 | +      recommended: true, | 
|  | 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 rules = baseRule.create(context); | 
|  | 28 | +    const sourceCode = context.sourceCode; | 
|  | 29 | + | 
|  | 30 | +    function JSXElement(node) { | 
|  | 31 | +      let closingElement; | 
|  | 32 | +      let openingElement; | 
|  | 33 | +      if (node.type === 'GlimmerElementNode') { | 
|  | 34 | +        const tokens = sourceCode.getTokens(node); | 
|  | 35 | +        const openEnd = tokens.find(t => t.value === '>'); | 
|  | 36 | +        const closeStart = tokens.findLast(t => t.value === '<'); | 
|  | 37 | +        if (!node.selfClosing) { | 
|  | 38 | +          closingElement = { | 
|  | 39 | +            type: 'JSXClosingElement', | 
|  | 40 | +            parent: node, | 
|  | 41 | +            range: [closeStart.range[0], node.range[1]], | 
|  | 42 | +            loc: { | 
|  | 43 | +              start: Object.assign({}, node.loc.start), | 
|  | 44 | +              end: Object.assign({}, node.loc.end), | 
|  | 45 | +            }, | 
|  | 46 | +          }; | 
|  | 47 | +          closingElement.loc.start = sourceCode.getLocFromIndex(closeStart.range[0]); | 
|  | 48 | +          closingElement.name = { ...closingElement, type: 'JSXIdentifier' }; | 
|  | 49 | +          closingElement.name.range = [ | 
|  | 50 | +            closingElement.name.range[0] + 1, | 
|  | 51 | +            closingElement.name.range[1] - 1, | 
|  | 52 | +          ]; | 
|  | 53 | +        } | 
|  | 54 | + | 
|  | 55 | +        openingElement = { | 
|  | 56 | +          type: 'JSXOpeningElement', | 
|  | 57 | +          selfClosing: node.selfClosing, | 
|  | 58 | +          attributes: node.attributes, | 
|  | 59 | +          parent: node, | 
|  | 60 | +          range: [node.range[0], openEnd.range[1]], | 
|  | 61 | +          loc: { | 
|  | 62 | +            start: Object.assign({}, node.loc.start), | 
|  | 63 | +            end: Object.assign({}, node.loc.end), | 
|  | 64 | +          }, | 
|  | 65 | +        }; | 
|  | 66 | +        openingElement.loc.end = sourceCode.getLocFromIndex(openEnd.range[1]); | 
|  | 67 | +        openingElement.name = { ...openingElement, type: 'JSXIdentifier' }; | 
|  | 68 | +        openingElement.name.range = [openingElement.name.range[0] + 1, openingElement.name.range[1] - 1]; | 
|  | 69 | +      } | 
|  | 70 | +      if (node.type === 'GlimmerBlockStatement') { | 
|  | 71 | +        const tokens = sourceCode.getTokens(node); | 
|  | 72 | +        let openEndIdx = tokens.findIndex(t => t.value === '}'); | 
|  | 73 | +        while (tokens[openEndIdx + 1].value === '}') { | 
|  | 74 | +          openEndIdx += 1; | 
|  | 75 | +        } | 
|  | 76 | +        const openEnd = tokens[openEndIdx]; | 
|  | 77 | +        let closeStartIdx = tokens.findLastIndex(t => t.value === '{'); | 
|  | 78 | +        while (tokens[closeStartIdx - 1].value === '{') { | 
|  | 79 | +          closeStartIdx -= 1; | 
|  | 80 | +        } | 
|  | 81 | +        const closeStart = tokens[closeStartIdx]; | 
|  | 82 | +        closingElement = { | 
|  | 83 | +          type: 'JSXClosingElement', | 
|  | 84 | +          parent: node, | 
|  | 85 | +          range: [closeStart.range[0], node.range[1]], | 
|  | 86 | +          loc: { | 
|  | 87 | +            start: Object.assign({}, node.loc.start), | 
|  | 88 | +            end: Object.assign({}, node.loc.end), | 
|  | 89 | +          }, | 
|  | 90 | +        }; | 
|  | 91 | +        closingElement.loc.start = sourceCode.getLocFromIndex(closeStart.range[0]); | 
|  | 92 | + | 
|  | 93 | +        openingElement = { | 
|  | 94 | +          type: 'JSXOpeningElement', | 
|  | 95 | +          attributes: node.params, | 
|  | 96 | +          parent: node, | 
|  | 97 | +          range: [node.range[0], openEnd.range[1]], | 
|  | 98 | +          loc: { | 
|  | 99 | +            start: Object.assign({}, node.loc.start), | 
|  | 100 | +            end: Object.assign({}, node.loc.end), | 
|  | 101 | +          }, | 
|  | 102 | +        }; | 
|  | 103 | +        openingElement.loc.end = sourceCode.getLocFromIndex(openEnd.range[1]); | 
|  | 104 | +      } | 
|  | 105 | +      return { | 
|  | 106 | +        type: 'JSXElement', | 
|  | 107 | +        openingElement, | 
|  | 108 | +        closingElement, | 
|  | 109 | +        children: node.children || node.body, | 
|  | 110 | +        parent: node.parent, | 
|  | 111 | +        range: node.range, | 
|  | 112 | +        loc: node.loc, | 
|  | 113 | +      }; | 
|  | 114 | +    } | 
|  | 115 | + | 
|  | 116 | +    const ignoredStack = new Set(); | 
|  | 117 | + | 
|  | 118 | +    return Object.assign({}, rules, { | 
|  | 119 | +      // overwrite the base rule here so we can use our KNOWN_NODES list instead | 
|  | 120 | +      '*:exit'(node) { | 
|  | 121 | +        // For nodes we care about, skip the default handling, because it just marks the node as ignored... | 
|  | 122 | +        if ( | 
|  | 123 | +          !node.type.startsWith('Glimmer') || | 
|  | 124 | +          ignoredStack.size > 0 && !ignoredStack.has(node) | 
|  | 125 | +        ) { | 
|  | 126 | +          rules['*:exit'](node); | 
|  | 127 | +        } | 
|  | 128 | +        if (ignoredStack.has(node)) { | 
|  | 129 | +          ignoredStack.delete(node); | 
|  | 130 | +        } | 
|  | 131 | +      }, | 
|  | 132 | +      'GlimmerTemplate:exit'(node) { | 
|  | 133 | +        if (!node.parent) { | 
|  | 134 | +          rules['Program:exit'](node); | 
|  | 135 | +        } | 
|  | 136 | +      }, | 
|  | 137 | +      GlimmerElementNode(node) { | 
|  | 138 | +        if (ignoredStack.size > 0) { | 
|  | 139 | +          return; | 
|  | 140 | +        } | 
|  | 141 | +        if (IGNORED_ELEMENTS.has(node.tag)) { | 
|  | 142 | +          ignoredStack.add(node); | 
|  | 143 | +        } | 
|  | 144 | +        const jsx = JSXElement(node); | 
|  | 145 | +        rules['JSXElement'](jsx); | 
|  | 146 | +        rules['JSXOpeningElement'](jsx.openingElement); | 
|  | 147 | +        if (jsx.closingElement) { | 
|  | 148 | +          rules['JSXClosingElement'](jsx.closingElement); | 
|  | 149 | +        } | 
|  | 150 | +      }, | 
|  | 151 | +      GlimmerAttrNode(node) { | 
|  | 152 | +        if (ignoredStack.size > 0 || !node.value) { | 
|  | 153 | +          return; | 
|  | 154 | +        } | 
|  | 155 | +        rules['JSXAttribute[value]']({ | 
|  | 156 | +          ...node, | 
|  | 157 | +          type: 'JSXAttribute', | 
|  | 158 | +          name: { | 
|  | 159 | +            type: 'JSXIdentifier', | 
|  | 160 | +            name: node.name, | 
|  | 161 | +            range: [node.range[0], node.range[0] + node.name.length - 1], | 
|  | 162 | +          }, | 
|  | 163 | +        }); | 
|  | 164 | +      }, | 
|  | 165 | +      GlimmerTemplate(node) { | 
|  | 166 | +        if (!node.parent) { | 
|  | 167 | +          return; | 
|  | 168 | +        } | 
|  | 169 | +        const jsx = JSXElement({ ...node, tag: 'template', type: 'GlimmerElementNode' }); | 
|  | 170 | +        rules['JSXElement'](jsx); | 
|  | 171 | +      }, | 
|  | 172 | +      GlimmerBlockStatement(node) { | 
|  | 173 | +        const body = [...node.program.body, ...(node.inverse?.body || [])]; | 
|  | 174 | +        rules['JSXElement'](JSXElement({ ...node, body })); | 
|  | 175 | +      }, | 
|  | 176 | +    }); | 
|  | 177 | +  }, | 
|  | 178 | +}; | 
0 commit comments