|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +const htmlTags = require('html-tags'); |
| 4 | +const svgTags = require('svg-tags'); |
| 5 | +const { mathmlTagNames } = require('mathml-tag-names'); |
| 6 | + |
| 7 | +// Authoritative set of native element tag names. Mirrors the approach |
| 8 | +// established by #2689 (template-no-block-params-for-html-elements), which |
| 9 | +// the maintainer requires for component-vs-element discrimination in this |
| 10 | +// plugin. Heuristic approaches (PascalCase detection, etc.) were explicitly |
| 11 | +// rejected there because a lowercase tag CAN be a component in GJS/GTS when |
| 12 | +// the name is bound in scope (e.g. `const div = MyComponent; <div />`). |
| 13 | +const ELEMENT_TAGS = new Set([...htmlTags, ...svgTags, ...mathmlTagNames]); |
| 14 | + |
| 15 | +/** |
| 16 | + * Returns true if the Glimmer element node is a native HTML / SVG / MathML |
| 17 | + * element — i.e. the tag name is in the authoritative list AND is not |
| 18 | + * shadowed by an in-scope binding. |
| 19 | + * |
| 20 | + * "Native" here means **spec-registered tag name** (in the HTML, SVG, or |
| 21 | + * MathML spec registries, reached via the `html-tags` / `svg-tags` / |
| 22 | + * `mathml-tag-names` packages). It is NOT the same as: |
| 23 | + * |
| 24 | + * - "native accessibility" / "widget-ness" — see `interactive-roles.js` |
| 25 | + * (aria-query widget taxonomy; an ARIA-tree-semantics question) |
| 26 | + * - "native interactive content" / "focus behavior" — see |
| 27 | + * `html-interactive-content.js` (HTML §3.2.5.2.7; an HTML-content-model |
| 28 | + * question about which tags can be nested inside what) |
| 29 | + * - "natively focusable" / sequential-focus — see HTML §6.6.3 |
| 30 | + * |
| 31 | + * This util answers only: "is this tag a first-class built-in element of one |
| 32 | + * of the three markup-language standards, rather than a component invocation |
| 33 | + * or a shadowed local binding?" Callers compose it with the other utils |
| 34 | + * above when they need a more specific question (see e.g. `template-no- |
| 35 | + * noninteractive-tabindex`, which consults both this and |
| 36 | + * `html-interactive-content`). |
| 37 | + * |
| 38 | + * Returns false for: |
| 39 | + * - components (PascalCase, dotted, @-prefixed, this.-prefixed, ::-namespaced — |
| 40 | + * none of these tag names appear in the HTML/SVG/MathML lists) |
| 41 | + * - custom elements (`<my-widget>`) — accepted false negative; the web- |
| 42 | + * components namespace is open and can't be enumerated |
| 43 | + * - scope-bound identifiers (`<div>` when `div` is a local `let` / `const` / |
| 44 | + * import / block-param in the enclosing scope) |
| 45 | + * |
| 46 | + * @param {object} node - GlimmerElementNode |
| 47 | + * @param {object} [sourceCode] - ESLint SourceCode, for scope lookup. When |
| 48 | + * omitted, the scope check is skipped (the result is then list-based only — |
| 49 | + * suitable for unit tests). |
| 50 | + */ |
| 51 | +function isNativeElement(node, sourceCode) { |
| 52 | + if (!node || typeof node.tag !== 'string') { |
| 53 | + return false; |
| 54 | + } |
| 55 | + if (!ELEMENT_TAGS.has(node.tag)) { |
| 56 | + return false; |
| 57 | + } |
| 58 | + if (!sourceCode || !node.parent) { |
| 59 | + return true; |
| 60 | + } |
| 61 | + const scope = sourceCode.getScope(node.parent); |
| 62 | + const firstPart = node.parts && node.parts[0]; |
| 63 | + // Compare by identifier name rather than AST node object identity — object |
| 64 | + // identity isn't guaranteed across parser versions (ember-eslint-parser can |
| 65 | + // produce distinct node objects for the same token depending on how the |
| 66 | + // scope manager walks the tree), but the resolved `.name` is stable. |
| 67 | + if (firstPart && scope.references.some((ref) => ref.identifier?.name === firstPart?.name)) { |
| 68 | + return false; |
| 69 | + } |
| 70 | + return true; |
| 71 | +} |
| 72 | + |
| 73 | +/** |
| 74 | + * Inverse of {@link isNativeElement}. Returns true when the node should NOT |
| 75 | + * be treated as a native HTML element — either because it's a component |
| 76 | + * invocation (PascalCase, dotted, @-prefixed, this.-prefixed, custom element) |
| 77 | + * OR a tag name that's shadowed by a scope binding. |
| 78 | + */ |
| 79 | +function isComponentInvocation(node, sourceCode) { |
| 80 | + return !isNativeElement(node, sourceCode); |
| 81 | +} |
| 82 | + |
| 83 | +module.exports = { isNativeElement, isComponentInvocation, ELEMENT_TAGS }; |
0 commit comments