{"id":49484085,"url":"https://github.com/johanrd/html-validate-ember","last_synced_at":"2026-05-12T00:01:55.784Z","repository":{"id":354919546,"uuid":"1225416685","full_name":"johanrd/html-validate-ember","owner":"johanrd","description":"Plugin to run HTML-validate on ember templates","archived":false,"fork":false,"pushed_at":"2026-05-07T22:12:57.000Z","size":453,"stargazers_count":3,"open_issues_count":8,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-08T00:19:49.483Z","etag":null,"topics":["accessibilty","aria","ember","glimmer","html","lint","validation"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/johanrd.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-30T08:56:37.000Z","updated_at":"2026-05-07T08:34:15.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/johanrd/html-validate-ember","commit_stats":null,"previous_names":["johanrd/html-validate-ember"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/johanrd/html-validate-ember","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johanrd%2Fhtml-validate-ember","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johanrd%2Fhtml-validate-ember/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johanrd%2Fhtml-validate-ember/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johanrd%2Fhtml-validate-ember/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/johanrd","download_url":"https://codeload.github.com/johanrd/html-validate-ember/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/johanrd%2Fhtml-validate-ember/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32917885,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-11T17:09:15.040Z","status":"ssl_error","status_checked_at":"2026-05-11T17:08:45.420Z","response_time":120,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["accessibilty","aria","ember","glimmer","html","lint","validation"],"created_at":"2026-05-01T00:02:29.553Z","updated_at":"2026-05-12T00:01:55.777Z","avatar_url":"https://github.com/johanrd.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"./assets/logo.png\" alt=\"HTML-validate ember\" width=\"480\" /\u003e\n\u003c/p\u003e\n\n[HTML-validate](https://html-validate.org) transformer for Ember templates — `.gts`, `.gjs`, and classic `.hbs`.\n\nLint your templates against html-validate's HTML5 spec checks, accessibility rules, content-model rules, and form-correctness rules — with diagnostics pointing at exact source positions.\n\n## Install\n\n`html-validate` is a peer dependency, so install **both**:\n\n```sh\npnpm add --save-dev html-validate html-validate-ember\n```\n\n## Configure\n\nCreate `.htmlvalidate.json` at your project root:\n\n```json\n{\n  \"extends\": [\"html-validate:recommended\", \"html-validate-ember:gts-recommended\"],\n  \"plugins\": [\"html-validate-ember\"],\n  \"transform\": {\n    \"^.*\\\\.(gts|gjs|hbs)$\": \"html-validate-ember\"\n  }\n}\n```\n\n### Two presets\n\nPick whichever fits your project:\n\n- **`html-validate-ember:gts-recommended`** *(recommended for most projects)* — everything in `:recommended` plus Ember/Glimmer style conventions baked in (`void-style: selfclosing` to match `ember-template-lint`'s `self-closing-void-elements`, etc.). Use this if you want the plugin to \"just work\" the way an Ember dev expects.\n- **`html-validate-ember:recommended`** *(minimal)* — only the rule disables that are *required* for the transformer to behave correctly: `no-trailing-whitespace` (mustache lines blank to whitespace), `no-self-closing` (some emit paths preserve a self-closing `/\u003e`), `attr-quotes` (rewritten attributes use double quotes). No stylistic opinions. Pick this if you'd rather keep all html-validate defaults and only opt into the transformer essentials.\n\n## Run\n\nThe bundled `validate-gts` CLI accepts any mix of `.gts` / `.gjs` / `.hbs` files and directories. Directories are walked recursively. Exits non-zero when any file has errors.\n\nThe recommended pattern is to wire it into `package.json` scripts:\n\n```json\n\"scripts\": {\n  \"lint:html:templates\": \"validate-gts --glint app/templates\",\n  \"lint:html:components\": \"validate-gts --glint app/components\",\n  \"lint:html\": \"validate-gts --glint app/templates app/components\"\n}\n```\n\nThen:\n\n```sh\npnpm lint:html\n```\n\nWire it into CI alongside your existing lint scripts; non-zero exit fails the build.\n\nFor ad-hoc one-off runs, use `pnpm exec`:\n\n```sh\npnpm exec validate-gts app/components/foo.gts            # single file\npnpm exec validate-gts app/templates                     # directory (walked recursively)\npnpm exec validate-gts --glint app/templates             # enable Glint type extraction\npnpm exec validate-gts --quiet app                       # only show summary\n```\n\n\n## Supported formats\n\n| Format | What it is | Glint integration |\n|---|---|---|\n| `.gts` | Template-imports + TypeScript (Ember's modern default) | ✅ full (component → element resolution, attribute type narrowing, splatted-root literal extraction) |\n| `.gjs` | Template-imports + JavaScript | ✅ same machinery as `.gts` (Glint understands both) |\n| `.hbs` | Classic separate template file | ⚠️ no Glint integration. Built-in Ember components (`\u003cInput\u003e`/`\u003cTextarea\u003e`/`\u003cLinkTo\u003e`) substitute to their rendered native tag (`\u003cinput\u003e`/`\u003ctextarea\u003e`/`\u003ca\u003e`) so content-model rules apply; other components blank transparently (open/close tags removed; children float into the parent's content model). Static-text resolution applies (`{{t 'Key'}}`, `{{if cond 'a' 'b'}}`). |\n\n\n## Progressive enhancement\n\nThe plugin works at every level — opt in to more accuracy as your project's typing investment grows:\n\n1. **Bare** (no project config, no `--glint`): bundled `:gts-recommended` preset, components blank transparently (children float into the parent's content model), static-text resolution via t-helper / if-helper / top-level consts.\n2. **+ project `.htmlvalidate.json`**: your rules / extends / transform overrides apply. Bundled CLI loads + merges them.\n3. **+ `--glint`** (requires `@glint/ember-tsc` installed): `Signature['Element']` resolves component invocations to native tags. Type-narrowed `@arg` values flow into attribute enum checks. Splatted-root literal attributes propagate (e.g. `\u003cMySlider /\u003e` substitutes to `\u003cinput type='range' min='0' max='100' /\u003e` with the actual literal values from the imported component's template).\n\nGlint silently no-ops when `@glint/ember-tsc` isn't installed, so you can flip `--glint` on and off without breaking anything.\n\n## What it catches\n\nThese are real bugs found in a 300-file Ember codebase that **ember-template-lint** and **eslint-plugin-ember** don't catch. They're HTML5 spec / a11y issues, not Ember/Glimmer-specific patterns.\n\n### Block element inside a `\u003cp\u003e` silently closes the paragraph\n\n```hbs\n\u003cp class='text-sm text-gray-600'\u003e\n  Some explanation\n  \u003cbutton\u003e?\u003c/button\u003e\n  \u003cdiv popover\u003e...\u003c/div\u003e   {{!-- ← parser auto-closes \u003cp\u003e here --}}\n\u003c/p\u003e                        {{!-- ← stray \u003c/p\u003e, doesn't match anything --}}\n```\n\n```\ntemplates/page.gts:42: error [no-implicit-close] Element \u003cp\u003e is implicitly closed by parent \u003c/div\u003e\ntemplates/page.gts:74: error [close-order] Stray end tag '\u003c/p\u003e'\n```\n\nThe browser silently rewrites the DOM; your screen-reader tree doesn't match what you wrote.\n\n### Hardcoded duplicate IDs across copy-pasted form rows\n\n```hbs\n\u003ctd\u003e\n  \u003cinput type='number' name='price' id='price' value='1000' /\u003e  {{!-- 1× --}}\n\u003c/td\u003e\n\u003ctd\u003e\n  \u003cinput type='number' name='price' id='price' value='2000' /\u003e  {{!-- 2× --}}\n\u003c/td\u003e\n{{!-- 6 more identical rows --}}\n```\n\n```\ntemplates/pricing-table.gts:128: error [no-dup-id] Duplicate ID \"price\"\ntemplates/pricing-table.gts:128: error [form-dup-name] Duplicate form control name \"price\"\n…16 more\n```\n\nThe author copy-pasted a pricing-tier column without parameterizing the id. `aria-describedby=\"price-currency\"` resolves to whichever DOM node the browser sees first.\n\n### `\u003cmenu\u003e` used as a styled sidebar\n\n```hbs\n\u003cmenu class='shadow-md flex flex-col px-4 ...'\u003e\n  \u003cdiv\u003e...\u003c/div\u003e\n  \u003cinput type='search' /\u003e\n  \u003cDatePicker /\u003e\n\u003c/menu\u003e\n```\n\n```\ntemplates/sidebar.gts:18: error [element-permitted-content] \u003cdiv\u003e not permitted under \u003cmenu\u003e\ntemplates/sidebar.gts:23: error [element-permitted-content] \u003cinput\u003e not permitted under \u003cmenu\u003e\n```\n\n`\u003cmenu\u003e` only accepts `\u003cli\u003e` per HTML5. Catches a common pattern of using `\u003cmenu role='menu'\u003e` for popovers/sidebars (~50 sites in the audited app).\n\n### Block element inside phrasing parents\n\n```hbs\n\u003cspan class='flex gap-2'\u003e\n  \u003cp class='rounded-full ...'\u003eAutomatisert\u003c/p\u003e   {{!-- ← \u003cp\u003e is flow content --}}\n\u003c/span\u003e\n\n\u003cbutton\u003e\n  \u003cdiv class='truncate'\u003eDaily total\u003c/div\u003e  {{!-- ← \u003cdiv\u003e is flow content --}}\n  \u003cdiv class='font-semibold'\u003e...\u003c/div\u003e\n\u003c/button\u003e\n\n\u003ch1\u003e\n  \u003cdiv class='skeleton'\u003e\u003c/div\u003e                   {{!-- ← \u003cdiv\u003e is flow content --}}\n\u003c/h1\u003e\n\n\u003clabel for='X'\u003e\n  \u003cdiv\u003eLabel text\u003c/div\u003e                          {{!-- ← \u003cdiv\u003e is flow content --}}\n\u003c/label\u003e\n```\n\n```\ntemplates/page.gts:88: error [element-permitted-content] \u003cp\u003e not permitted under \u003cspan\u003e\ncomponents/tile.gts:41: error [element-permitted-content] \u003cdiv\u003e not permitted under \u003cbutton\u003e\ntemplates/page.gts:14: error [element-permitted-content] \u003cdiv\u003e not permitted under \u003ch1\u003e\ncomponents/dialog.gts:32: error [element-permitted-content] \u003cdiv\u003e not permitted under \u003clabel\u003e\n```\n\nReplace inner `\u003cdiv\u003e`/`\u003cp\u003e` with `\u003cspan class='block'\u003e` — keeps Tailwind classes, fixes the spec violation.\n\n### `\u003cdt\u003e` / `\u003cdd\u003e` outside `\u003cdl\u003e`, or `\u003cdl\u003e` nested in `\u003cdl\u003e`\n\n```hbs\n{{!-- Used as a key/value row inside \u003cdetails\u003e\u003csummary\u003e: --}}\n\u003cdetails\u003e\n  \u003csummary\u003e\n    \u003cdiv class='flex justify-between'\u003e\n      \u003cdt\u003eTotal\u003c/dt\u003e             {{!-- ← needs \u003cdl\u003e ancestor --}}\n      \u003cdd\u003e1,234\u003c/dd\u003e             {{!-- ← needs \u003cdl\u003e ancestor --}}\n    \u003c/div\u003e\n  \u003c/summary\u003e\n\u003c/details\u003e\n\n{{!-- Or nested incorrectly: --}}\n\u003cdl\u003e\n  \u003cdt\u003e{{row.key}}\u003c/dt\u003e\n  \u003cdl\u003e{{row.value}}\u003c/dl\u003e         {{!-- ← author meant \u003cdd\u003e --}}\n\u003c/dl\u003e\n```\n\n```\ncomponents/details-row.gts:22: error [element-required-ancestor] \u003cdt\u003e requires \"dl \u003e dt\" ancestor\ntemplates/item-detail.gts:104: error [element-permitted-content] \u003cdl\u003e not permitted under \u003cdl\u003e\n```\n\n### Icon-only `\u003cbutton\u003e` without an accessible name\n\n```hbs\n\u003cbutton type='button' commandfor='Modal' command='close'\u003e\n  \u003csvg\u003e...\u003c/svg\u003e   {{!-- ← screen readers announce \"button\" --}}\n\u003c/button\u003e\n```\n\n```\ntemplates/item-detail.gts:188: error [text-content] \u003cbutton\u003e must have accessible text\n```\n\nAdd `aria-label='Close'` (or use the `\u003cbutton title='...'\u003e` \"subjective\" form — html-validate flags `title` as discouraged but accepts it).\n\n### `\u003cform\u003e` missing a submit button\n\n```hbs\n\u003cform {{on 'submit' this.handleSubmit}}\u003e\n  \u003ctextarea name='message'\u003e\u003c/textarea\u003e\n  \u003cbutton type='button'\u003eCancel\u003c/button\u003e   {{!-- only button is type='button' --}}\n\u003c/form\u003e\n```\n\n```\ncomponents/chat-form.gts:24: error [wcag/h32] \u003cform\u003e element must have a submit button\n```\n\n### Camel-case attribute names\n\nHTML5 attribute names are case-insensitive and lowercase canonical. Camel-case in source compiles down to lowercase in the DOM, so attribute selectors silently fail.\n\n```hbs\n\u003cdiv data-test-userMenuList\u003e\u003c/div\u003e   {{!-- ← lowercased to data-test-usermenulist --}}\n```\n\n```\ntemplates/page.gts:42: error [attr-case] Attribute \"data-test-userMenuList\" should be lowercase\n```\n\n### Duplicate CSS class\n\n```hbs\n\u003cdiv class='flex w-full focus:bg-blue-500 focus:text-white rounded-md flex grow gap-2'\u003e\n                                                                  ↑↑↑↑\n                                                                  duplicate \"flex\"\n```\n\n```\ncomponents/menu-button.gts:18: error [no-dup-class] Class \"flex\" duplicated\n```\n\n### Invalid attribute values\n\n```hbs\n\u003cimg alt='Logo' width='100px' /\u003e   {{!-- ← width must be unitless integer --}}\n\u003cinput type='checkbox' readonly /\u003e  {{!-- ← readonly only valid on text-like inputs --}}\n```\n\n```\ncomponents/footer.gts:12: error [attribute-allowed-values] Attribute \"width\" has invalid value \"100px\"\ntemplates/admin.gts:18: error [input-attributes] Attribute \"readonly\" not allowed on \u003cinput type=\"checkbox\"\u003e\n```\n\n---\n\n## Inline errors in VS Code\n\nInstall the official extension: [html-validate.vscode-html-validate](https://marketplace.visualstudio.com/items?itemName=html-validate.vscode-html-validate).\n\nThe extension only validates files whose **VS Code language ID** is in its `html-validate.validate` allow-list. The default list is `[\"html\", \"javascript\", \"markdown\", \"vue\", \"vue-html\"]` — none of which match `.gts` / `.gjs` / `.hbs`. Add the Ember/Glimmer language IDs to your project's `.vscode/settings.json`:\n\n```json\n{\n  \"html-validate.validate\": [\n    \"html\",\n    \"javascript\",\n    \"markdown\",\n    \"vue\",\n    \"vue-html\",\n    \"glimmer-ts\",\n    \"glimmer-js\",\n    \"handlebars\"\n  ]\n}\n```\n\nThe Glimmer language IDs (`glimmer-ts` for `.gts`, `glimmer-js` for `.gjs`) are registered by the Glimmer / Glint extensions:\n- [`lifeart.vscode-glimmer-syntax`](https://marketplace.visualstudio.com/items?itemName=lifeart.vscode-glimmer-syntax) (syntax highlighting + grammars)\n- [`typed-ember.glint2-vscode`](https://marketplace.visualstudio.com/items?itemName=typed-ember.glint2-vscode) (Glint language service)\n\nEither one is enough. (`gts` / `gjs` shown in the language picker are display *aliases* — the actual ID is `glimmer-ts` / `glimmer-js`. Same trick Vue uses with `vue` / `vue-html`.) `handlebars` is a built-in language ID.\n\nAfter adding the setting, **reload the VS Code window** (Cmd+Shift+P → \"Developer: Reload Window\"). The html-validate extension activates lazily — if you don't see \"HTML-Validate\" in the Output dropdown, run **\"Developer: Show Running Extensions\"** to confirm it loaded, and check **\"Workspaces: Manage Workspace Trust\"** (the extension refuses to run in untrusted workspaces).\n\n\u003e **If you installed via `pnpm install file:/path/to/html-validate-ember`** and you've updated the local source: pnpm's `file:`-dep cache doesn't always invalidate on source-content changes. Bump the version in `html-validate-ember/package.json` (or run `pnpm install --force` in the consuming project), then reload VS Code. Stale plugin code looks like phantom diagnostics that don't reproduce when running `validate-gts` from the terminal.\n\n## Glint integration (opt-in)\n\nWhen `@glint/ember-tsc` is installed in your project, the transformer can extract TypeScript type information for two patterns:\n\n### 1. String-literal-union narrowing in attribute positions\n\n```ts\ninterface PopoverSig {\n  Args: { mode: 'auto' | 'manual' | 'hint' }\n}\n\nclass Popover extends Component\u003cPopoverSig\u003e {\n  \u003ctemplate\u003e\n    \u003cdiv popover={{@mode}}\u003e...\u003c/div\u003e\n  \u003c/template\u003e\n}\n```\n\nWithout Glint: `\u003cdiv popover=\"\"\u003e` (DynamicValue, no enum check).\nWith Glint: html-validate sees `popover=\"auto\"` (or whichever union member is **not** in html-validate's enum, surfacing a typing bug if you've declared an invalid value).\n\n### 2. Component → element substitution\n\nWhen a component declares `Signature['Element']`, the transformer substitutes the invocation with the corresponding native tag, so content-model rules apply correctly:\n\n```ts\nclass MyButton extends Component\u003c{\n  Element: HTMLButtonElement\n  Args: { onClick: () =\u003e void }\n}\u003e { /* ... */ }\n```\n\n```hbs\n\u003cMyButton @onClick={{this.foo}} /\u003e\n```\n\n→ html-validate sees a `\u003cbutton\u003e`, can apply `no-implicit-button-type` / `text-content` rules accordingly. (The transformer adds DynamicValue placeholders for `type` and label content so those rules don't FP-fire on substituted self-closing components — the actual button has its `type` and label set internally.)\n\nFor components with `Element: unknown` (typically yield-only components) the transformer treats the invocation as transparent — children float into the parent's content model.\n\n### Enabling Glint\n\nPass `--glint` to the bundled CLI or set `HVE_GLINT=1` in the environment:\n\n```sh\nnpx validate-gts --glint app\n# or, when invoking html-validate directly:\nHVE_GLINT=1 npx html-validate 'app/**/*.gts'\n```\n\nGlint integration adds significant per-file overhead (TS program rebuild + module rewrite + TypeChecker calls). The static-resolution path is the default for that reason; turn Glint on for design-system-style codebases with strict typing discipline.\n\n### Caching\n\nGlint results are content-addressed and cached on disk under `node_modules/.cache/html-validate-ember/glint/`. The cache key includes file SHA + tsconfig SHA + plugin version, so:\n\n- **Repeat runs** (CI, pre-commit, IDE re-validation) skip the entire Glint pipeline for unchanged files. On the audited 207-file codebase: ~464s cold → ~2.5s warm.\n- **Plugin upgrades** invalidate all entries naturally (version is in the key).\n- **Tsconfig changes** invalidate per-project entries.\n\nSet `HVE_NO_CACHE=1` to bypass the cache (e.g. when debugging the Glint pipeline). Set `HVE_DEBUG=1` to print per-file skip reasons during preload (non-gts/gjs, read error, rewrite returned empty/error) — useful when you see a non-zero \"skipped\" count and want to know what fell out.\n\n## Silencing rules\n\nThree layers, broadest to narrowest:\n\n1. **Project config** — disable any rule in `.htmlvalidate.json`:\n   ```json\n   {\n     \"rules\": {\n       \"aria-label-misuse\": [\"error\", { \"allowAnyNamable\": true }],\n       \"no-inline-style\": \"off\"\n     }\n   }\n   ```\n\n2. **File-level disable** — html-validate directives at the top of a file work normally:\n   ```hbs\n   {{!-- [html-validate-disable no-dup-id] --}}\n   ```\n\n3. **Per-element directive** — Glimmer comment containing the directive:\n   ```hbs\n   {{!-- [html-validate-disable-next no-dup-id] --}}\n   \u003cdiv id={{this.id}}\u003ex\u003c/div\u003e\n   ```\n\n   Use the long form `{{!-- ... --}}`, not `{{! ... }}`. The transformer rewrites the long form to a `\u003c!-- --\u003e` HTML comment in place; the short form is too short to fit `\u003c!-- --\u003e` while preserving byte length.\n\n   **Inline reason / link** — append `-- text` *inside* the brackets (html-validate's directive parser splits on `--` after the rule name):\n   ```hbs\n   {{!-- [html-validate-disable-next unique-landmark -- pending https://github.com/w3c/html-aria/issues/579] --}}\n   \u003cheader\u003e...\u003c/header\u003e\n   ```\n\n   Trailing text *after* the closing `]` does **not** parse — html-validate raises `parser-error: Missing end bracket \"]\" on directive`. The reason has to live inside the brackets.\n\n   Variants (per html-validate's [inline-config docs](https://html-validate.org/usage/inline-config.html)):\n   - `html-validate-disable rule` — disables `rule` for the rest of the file (or until re-enabled with `html-validate-enable`).\n   - `html-validate-disable-next rule` — disables for the next element only.\n   - `html-validate-disable-block rule` — disables for all siblings and descendants of the directive's parent that follow the directive. Useful for scoping to e.g. a whole `\u003cdialog\u003e` subtree:\n     ```hbs\n     \u003cdialog\u003e\n       {{!-- [html-validate-disable-block unique-landmark -- pending https://github.com/w3c/html-aria/issues/579] --}}\n       \u003cheader\u003e...\u003c/header\u003e\n       \u003csection\u003e...\u003c/section\u003e\n     \u003c/dialog\u003e\n     ```\n\n## How positions work\n\n`content-tag` gives byte offsets for each `\u003ctemplate\u003e` block's content. We compute `line:column` for the block's start, attach as the `Source` base, and emit length-equivalent HTML so byte positions inside the template match positions inside the original `.gts`. html-validate adds reported positions to the base.\n\nNo SourceMap machinery — same approach `html-validate-vue` and `html-validate-angular` use.\n\n## Multipass branch validation\n\n`{{#if}}/{{else}}` (and `{{else if}}` chains) are validated **per branch by default**. The transformer enumerates branch combinations, yields one html-validate `Source` per combination, and html-validate validates each independently. Errors from every branch surface — including the un-selected branch under single-pass.\n\nEnumeration is capped at the first **10 conditional branches per template** to bound work; \"conditional branch\" here means any block helper with both a program and an `{{else}}` clause. The common forms are `{{#if/else}}`, `{{#unless/else}}`, and `{{#each/else}}` (empty fallback), but custom block helpers (`{{#my-helper x}}A{{else}}B{{/my-helper}}`) count too. Surplus conditional branches fall back to the single-branch heuristic, which can hide errors in their unselected arms. Override with `--max-conditional-branches=N` on `validate-gts`, or `HVE_MAX_CONDITIONAL_BRANCHES=N` when invoking html-validate directly. Set `N=0` to disable multipass and use the single-branch heuristic everywhere.\n\nEnumeration is **tree-aware**: branches are organized by nesting, and choosing one arm of a branch only enumerates that arm's nested branches. This matches the runtime DOM — choices inside a blanked arm can't affect what html-validate sees — and turns 2^N worst case into far fewer calls on nested templates. A 6-deep `{{#if}}/{{else}}` chain produces 7 distinct passes rather than 64. Pure-sibling branches still scale 2^N, so the cap matters most there.\n\nThe bundled `validate-gts` CLI dedupes identical messages by `(line, column, ruleId, message)` before printing, so an error stable across branches (e.g., a misnested element *outside* the if/else) is reported once even though it lives in every pass. The dedupe util is also exported as `dedupeMultipassReport` from `lib/multipass-dedupe.js` for custom consumers.\n\n## Known limitations\n\n- **Static-string scope.** `{{NAME}}` resolves against same-file `const NAME = '...'` declarations and one-level-deep `import { NAME } from './sibling'` (relative paths only — package and path-aliased imports are skipped). `{{this.field}}` resolves against same-file class-field initializers (`field = '...'` or `field: T = '...'`). What's not resolved: transitive re-exports (`export { X } from './...'` chains), default imports, namespace imports, and getters returning literals — Glint narrows some of these to string-literal types when `--glint` is on, which the blanker picks up through a separate code path.\n- **`no-implicit-button-type` fires on every untyped `\u003cbutton\u003e`** regardless of `\u003cform\u003e` ancestry — that's html-validate's strict design (default `type=submit` is non-obvious). The plugin doesn't try to soften it; if you'd rather only flag buttons that actually live inside a `\u003cform\u003e` at runtime (where the default-submit matters), disable the rule project-wide and rely on review / a custom lint:\n  ```json\n  { \"rules\": { \"no-implicit-button-type\": \"off\" } }\n  ```\n  Static \"is this `\u003cbutton\u003e` inside a `\u003cform\u003e`?\" detection is feasible in principle (walk ancestors at the AST + chain into PascalCase wrappers) but adds the same per-Source-suppression caveat as `wcag/h32`: a button inside a wrapper component that someone else's template wraps in `\u003cform\u003e` would be silenced in the wrong direction. We left it untouched.\n- **TS-flavored block-param types are stripped, not parsed.** `@glimmer/syntax`'s parser doesn't understand `{{#each items as |item: T|}}`-style annotations (or the comma separators that come with multi-param lists). The transformer pre-strips them to whitespace before Glimmer parses, with balanced-bracket scanning so unions (`A | B`), object types (`{ a: number }`), parenthesized types (`(A | B)[]`), generics (`Map\u003cstring, number\u003e`), arrays (`T[]`), and qualified names (`NS.Type`) all work. Length-preserving — AST offsets after the strip match original source. The strip only operates inside `as |…|` ranges of mustache openers; type literals appearing elsewhere in the template aren't touched (and Glimmer wouldn't accept them there anyway).\n\n## Future work\n\n- **Custom rules** — html-validate plugins can ship their own rules. Candidates: `ember-prefer-glimmer-comment-directive` (flag `\u003c!-- [html-validate-disable …] --\u003e` and suggest `{{!-- … --}}`), `ember-component-naming` (enforce PascalCase / dotted invocations).\n- **Piecewise string-builder** in `blank.ts` — current `split('')` / `join('')` is O(n) per `\u003ctemplate\u003e` block; not a bottleneck on real codebases (sub-second for 1000-line files).\n\n## Inspecting what gets emitted\n\nWhen debugging a false positive:\n\n```sh\nnode node_modules/html-validate-ember/dump-blanked.js path/to/file.gts\n```\n\nPrints the original `\u003ctemplate\u003e` body and the length-equivalent HTML the transformer hands to html-validate. False positives are usually traceable to the blanker losing or mis-emitting structure.\n\n## Contributing\n\nPRs welcome. Run `npm test` for the unit + integration suite.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohanrd%2Fhtml-validate-ember","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjohanrd%2Fhtml-validate-ember","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohanrd%2Fhtml-validate-ember/lists"}