Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
; http://editorconfig.org

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true

[*.{go,py,rb,php}]
indent_size = 4

[*.md]
trim_trailing_whitespace = false
14 changes: 0 additions & 14 deletions .eslintrc

This file was deleted.

39 changes: 0 additions & 39 deletions .github/ISSUE_TEMPLATE/bugreport.yml

This file was deleted.

1 change: 0 additions & 1 deletion .github/ISSUE_TEMPLATE/config.yml

This file was deleted.

56 changes: 0 additions & 56 deletions .github/ISSUE_TEMPLATE/new-rule.yml

This file was deleted.

24 changes: 0 additions & 24 deletions .github/workflows/eslint.yml

This file was deleted.

26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Test

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm test
16 changes: 9 additions & 7 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/node_modules
yarn.lock
._*
.DS_Store
Thumbs.db
.idea
*.sublime*
*.lock
.publish
*.local.*
.worktrees/
node_modules/
.idea/
.vscode/
.zed/
docs/
plan/
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@htmlacademy:registry=https://registry.npmjs.org/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24
44 changes: 42 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,45 @@
# Changelog

## 2.0.0 — 2026-05-20

### Breaking

- Migrated to ESM. The plugin now requires Node.js >= 24 and is loaded via ESM `import`. Configs that reference the plugin by name (`plugins: ["linthtml-rules-htmlacademy"]` in `.linthtmlrc`) keep working.
- Renamed `htmlacademy/img-svg-req-dimensions` → `htmlacademy/replaced-elements-req-dimensions`. The renamed rule also covers `<video>` and `<iframe>` (previously only `<img>` and `<svg>`).

### Added

- New rule `htmlacademy/label-req-for`: validates that every `<label>` is properly associated with a form control. A label must either have a non-empty `for` attribute pointing to the `id` of a labelable element (`<input>`, `<select>`, `<textarea>`, `<button>`, `<meter>`, `<output>`, `<progress>`), or contain a labelable descendant. Empty `for=""` is explicitly allowed.
- New rule `htmlacademy/req-submit-button`: requires every `<form>` to contain a submit button. Recognises `<button type="submit">`, `<button>` (defaults to submit per the HTML spec), `<input type="submit">`, and external submitters linked through the `form` attribute. Closes [#40](https://github.com/htmlacademy/linthtml-rules-htmlacademy/issues/40).
- New rule `htmlacademy/boolean-attr-no-value`: disallows assigning a value to HTML boolean attributes (`disabled`, `required`, `checked`, `readonly`, `multiple`, `selected`, `autofocus`, `hidden`, `open`, `async`, `defer`, `autoplay`, `controls`, `loop`, `muted`, `inert`, and others recognised by `@linthtml/dom-utils`). Implements the codeguide rule about boolean attributes being written without a value.
- New rule `htmlacademy/icon-button-aria-label`: a `<button>` without visible text content must have an accessible name via `aria-label`, `aria-labelledby`, or `title`. Covers icon-only buttons that would otherwise be invisible to screen readers. Addresses [#80](https://github.com/htmlacademy/linthtml-rules-htmlacademy/issues/80) — the upstream `button-req-content` rule does not recognise `aria-labelledby`, so this plugin rule explicitly requires an accessible name for content-less buttons.
- New rule `htmlacademy/attr-order`: enforces an attribute order using configurable groups. The default order matches the codeguide policy `class → src/href → data-* → others` and supports glob patterns like `data-*`, `aria-*`, and `*` for the catch-all group. Order inside a group is free.
- New rule `htmlacademy/input-name-unique`: each `<input>` inside a `<form>` must have a unique `name`. Radio and checkbox groups (legitimate same-name patterns) are exempt. Closes [#46](https://github.com/htmlacademy/linthtml-rules-htmlacademy/issues/46).
- New rule `htmlacademy/heading-level`: document headings must not skip levels (`<h1>` → `<h3>` is flagged) and must start with `<h1>`. Returning to a higher level (`<h3>` → `<h2>`) is allowed. Closes [#41](https://github.com/htmlacademy/linthtml-rules-htmlacademy/issues/41).
- New rule `htmlacademy/label-req-text`: `<label>` must contain visible text (or carry an `aria-label`). `<label><input></label>` without any text is reported. Closes [#67](https://github.com/htmlacademy/linthtml-rules-htmlacademy/issues/67).
- New rule `htmlacademy/svg-role-img`: inline `<svg>` must declare itself as content via `role="img"` + `aria-label`/`aria-labelledby`, or as decorative via `aria-hidden="true"`. Bare `<svg>` is flagged. Closes [#53](https://github.com/htmlacademy/linthtml-rules-htmlacademy/issues/53).
- `peerDependency` on `@linthtml/linthtml >= 0.10.0` so consumers get a clear error when the host package is missing.
- `node:test`-based test suite: one `test/invalid/<rule>.test.js` per rule with positive and negative cases (~317 tests across all 39 rules).

### Changed

- `htmlacademy/a-target-rel`: now requires `rel="noreferrer"` (previously required `noopener` + `noreferrer`). Per the HTML spec, `noreferrer` implicitly enables `noopener` behaviour, so one keyword is enough. Modern browsers also default to `noopener` for `target="_blank"` since 2020, which is why the rule is still disabled by default in the shipped config — enable it when you want the requirement explicit in source.
- `htmlacademy/req-webp-in-picture`: accepts `image/avif` as an alternative to `image/webp`.
- `htmlacademy/attribute-allowed-values`: rule body was non-functional due to a property-access bug (`node.name.chars` on a plain string). The rule now correctly validates attribute values.
- `htmlacademy/attr-req-value`: no longer crashes when activated without an `ignore` option — an empty ignore list is assumed.
- `htmlacademy/input-req-label`: end-of-pass issues now use the `htmlacademy/` namespace prefix consistently (previously the second code path reported under `input-req-label` without the namespace).
- `htmlacademy/no-px-size`: extended to cover `<video>` and `<iframe>` in addition to `<img>` and `<svg>`.
- `htmlacademy/charset-position`: fixed inverted logic that silently accepted `<meta name="viewport">` (or any other non-charset `<meta>`) as the first child of `<head>`. The rule now strictly requires the first element to be `<meta>` with a `charset` attribute.
- `htmlacademy/tag-forbid-attr`: each forbidden entry now supports an optional `value` (string or `RegExp`) so attributes can be forbidden only when their value matches, e.g. disallowing `type="text/css"` on `<link>` but allowing other `type` values.
- `htmlacademy/aria-label-misuse`: `aria-label` on `<svg role="img">` (or `role="image"`) is now considered valid usage, matching how content SVG is exposed to assistive technology. Closes [#79](https://github.com/htmlacademy/linthtml-rules-htmlacademy/issues/79).
- Node.js requirement bumped to >= 24.

### Migration notes

- Programmatic consumers (`import` or `require`) must use `import plugin from 'linthtml-rules-htmlacademy'`. Configs in `.linthtmlrc` that reference the plugin by name keep working without changes.
- Replace any usage of `htmlacademy/img-svg-req-dimensions` with `htmlacademy/replaced-elements-req-dimensions` in project-level overrides.
- If you extend `linthtml-config-htmlacademy`, no action is required — the config already references the new rule names.

## 1.0.21
Fixes `req-webp-in-picture` to not check `<picture>` if all `<source>` have attribute `type="image/svg+xml"`.

Expand Down Expand Up @@ -101,7 +141,7 @@ Added a new rule [htmlacademy/req-stylesheet-link](rules/req-stylesheet-link/REA
```html
<head>
<link rel="stylesheet" href="styles/style.css">
</head>
</head>
```

## 1.0.13
Expand All @@ -124,7 +164,7 @@ will not require a name attribute for `<input`> with `type="submit"`
## 1.0.12
Fix `htmlacademy/attr-req-value` rule

### Exceptions
### Exceptions
A single `<option>` in `<select>` may have an empty value for the `value` attribute if it is selected by default.

The following pattern is **not** considered a problem:
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 .html academy
Copyright (c) 2023-2026 HTML Academy

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
93 changes: 86 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,89 @@
# linthtml-rules-htmlacademy
Кастомные правила для linthtml от HTML Academy
# LintHTML Rules for HTML Academy Codeguide

## All rules
Познакомьтесь со всеми правилами в [едином списке всех правил](docs/list-of-rules.md).
[![npm version](https://img.shields.io/npm/v/linthtml-rules-htmlacademy.svg)](https://www.npmjs.com/package/linthtml-rules-htmlacademy)
[![test](https://github.com/htmlacademy/linthtml-rules-htmlacademy/actions/workflows/test.yml/badge.svg)](https://github.com/htmlacademy/linthtml-rules-htmlacademy/actions/workflows/test.yml)
[![license](https://img.shields.io/npm/l/linthtml-rules-htmlacademy.svg)](https://github.com/htmlacademy/linthtml-rules-htmlacademy/blob/main/LICENSE)

## Contributing
Ознакомьтесь с подробными [правилами по содействию](./docs/CONTRIBUTING.md). Мы принимаем любую помощь, начиная с исправления орфографии, заканчивая созданием новых правил для линтера.
Custom [LintHTML](https://linthtml.vercel.app) rules for HTML markup validation according to [HTML Academy Codeguide](https://codeguide.academy).

https://linthtml.vercel.app/developer-guide/plugins
## Requirements

- Node.js >= 24
- `@linthtml/linthtml` >= 0.10.0

## Installation

```bash
npm install -D @linthtml/linthtml linthtml-rules-htmlacademy
```

## Usage

Add the plugin to your `.linthtmlrc` configuration file:

```json
{
"plugins": ["linthtml-rules-htmlacademy"],
"rules": {
"htmlacademy/tag-name-lowercase": true,
"htmlacademy/replaced-elements-req-dimensions": true
}
}
```

Most consumers should extend the curated [`linthtml-config-htmlacademy`](https://github.com/htmlacademy/linthtml-config-htmlacademy) instead — it activates every rule from this package with sensible defaults.

## Rules

| Rule | Description |
| --- | --- |
| [htmlacademy/a-target-rel](rules/a-target-rel/) | Requires `rel="noopener"` on `<a target="_blank">` |
| [htmlacademy/aria-label-misuse](rules/aria-label-misuse/) | Requires `aria-label` usage on specific elements only |
| [htmlacademy/attr-delimiter](rules/attr-delimiter/) | Disallows spaces around `=` in attributes |
| [htmlacademy/attr-order](rules/attr-order/) | Enforces an attribute order using configurable groups (default: `class → src/href → data-* → others`) |
| [htmlacademy/attr-req-value](rules/attr-req-value/) | Disallows empty attribute values except those in `ignore` list |
| [htmlacademy/attribute-allowed-values](rules/attribute-allowed-values/) | Validates attribute values against allowed values |
| [htmlacademy/ban-url-spaces](rules/ban-url-spaces/) | Disallows spaces in `href` and `src` URLs |
| [htmlacademy/boolean-attr-no-value](rules/boolean-attr-no-value/) | Disallows assigning a value to HTML boolean attributes (`disabled`, `checked`, …) |
| [htmlacademy/charset-position](rules/charset-position/) | Requires `<meta charset="…">` as the first element of `<head>` |
| [htmlacademy/class-first](rules/class-first/) | Requires `class` to be the first attribute (subset of `attr-order`; prefer `attr-order` for full coverage) |
| [htmlacademy/form-action-attribute](rules/form-action-attribute/) | Requires `action` attribute on `<form>` |
| [htmlacademy/head-meta-charset](rules/head-meta-charset/) | Requires `<meta charset="utf-8">` in `<head>` |
| [htmlacademy/heading-level](rules/heading-level/) | Disallows heading-level skips (`<h1>` → `<h3>`); document must start with `<h1>` |
| [htmlacademy/icon-button-aria-label](rules/icon-button-aria-label/) | Requires an accessible name on icon-only `<button>` elements |
| [htmlacademy/id-no-dup](rules/id-no-dup/) | Disallows duplicate `id` values on page |
| [htmlacademy/input-name-unique](rules/input-name-unique/) | Requires `<input>` names to be unique inside a `<form>` (radio / checkbox groups exempt) |
| [htmlacademy/input-req-label](rules/input-req-label/) | Requires label for input fields, allows `aria-label` |
| [htmlacademy/label-req-for](rules/label-req-for/) | Requires `<label>` to be associated with a form control |
| [htmlacademy/label-req-text](rules/label-req-text/) | Requires `<label>` to contain visible text content (or `aria-label`) |
| [htmlacademy/link-req-content](rules/link-req-content/) | Requires text content in `<a>` elements |
| [htmlacademy/no-blocking-script](rules/no-blocking-script/) | Validates script placement in markup |
| [htmlacademy/no-class-in-container](rules/no-class-in-container/) | Validates `class` attribute on children inside specified container |
| [htmlacademy/no-double-br](rules/no-double-br/) | Disallows consecutive `<br>` elements |
| [htmlacademy/no-px-size](rules/no-px-size/) | Requires `width` and `height` on `<img>`, `<svg>`, `<video>`, `<iframe>` to be integers without units |
| [htmlacademy/replaced-elements-req-dimensions](rules/replaced-elements-req-dimensions/) | Requires `width` and `height` on `<img>`, `<svg>`, `<video>`, `<iframe>` |
| [htmlacademy/req-charset-utf](rules/req-charset-utf/) | Requires `UTF-8` for `<meta charset="">` |
| [htmlacademy/req-head-styles](rules/req-head-styles/) | Disallows stylesheets outside `<head>` |
| [htmlacademy/req-mailto](rules/req-mailto/) | Requires `mailto:` for links with email text |
| [htmlacademy/req-meta-viewport](rules/req-meta-viewport/) | Requires `<meta name="viewport">` in `<head>` |
| [htmlacademy/req-preload-font](rules/req-preload-font/) | Requires font preload in `<head>` |
| [htmlacademy/req-single-styles](rules/req-single-styles/) | Allows only one `<link rel="stylesheet">` in `<head>` |
| [htmlacademy/req-source-width-height](rules/req-source-width-height/) | Requires `width` and `height` on `<source>` inside `<picture>` |
| [htmlacademy/req-stylesheet-link](rules/req-stylesheet-link/) | Requires `<link rel="stylesheet">` with non-empty `href` |
| [htmlacademy/req-submit-button](rules/req-submit-button/) | Requires every `<form>` to contain a submit button |
| [htmlacademy/req-tags-presence](rules/req-tags-presence/) | Requires specified tags on page |
| [htmlacademy/req-webp-in-picture](rules/req-webp-in-picture/) | Requires `webp` or `avif` format in `<picture>` |
| [htmlacademy/section-has-heading](rules/section-has-heading/) | Requires heading element in `<section>` |
| [htmlacademy/space-between-comments](rules/space-between-comments/) | Validates spaces in comments `<!-- Comment -->` |
| [htmlacademy/svg-role-img](rules/svg-role-img/) | Content `<svg>` must have `role="img"` and an accessible name; decorative `<svg>` must use `aria-hidden="true"` |
| [htmlacademy/tag-forbid-attr](rules/tag-forbid-attr/) | Disallows specified attributes (optionally restricted by value) on specified tags |
| [htmlacademy/tag-name-lowercase](rules/tag-name-lowercase/) | Requires lowercase tag names |
| [htmlacademy/tag-req-attr](rules/tag-req-attr/) | Requires specified attributes on specified tags |
| [htmlacademy/tag-self-close](rules/tag-self-close/) | Disallows self-closing void elements (`<br>` not `<br/>`) |

## Links

- [HTML Academy](https://htmlacademy.ru)
- [HTML Academy Codeguide](https://codeguide.academy)
- [Codeguide Repository](https://github.com/htmlacademy/codeguide)
- [LintHTML Plugins Documentation](https://linthtml.vercel.app/developer-guide/plugins)
Loading