Использование линтера

В djLint много правил для проверки стиля и корректности ваших шаблонов. Чтобы использовать линтер в полную силу, настройте его на готовый профиль для выбранного вами языка шаблонов.

djlint /path/to/templates --lint

# с нестандартными расширениями
djlint /path/to/templates -e html.dj --profile=django

# или для отдельного файла
djlint /path/to/this.html.j2  --profile=jinja

Включение и отключение правил

Большинство правил включено по умолчанию. В командной строке правила можно отключить флагом --ignore, а включить — флагом --include.

Например:

djlint . --lint --include=H006,H017 --ignore=H013,H015

То же самое можно сделать в файле конфигурации.

Правила

Код Значение По умолчанию
T001 Переменные должны быть окружены пробелами. Пример: {{ this }} ✔️
T002 В тегах следует использовать двойные кавычки. Пример: {% extends "this.html" %} ✔️
T003 У endblock должно быть имя. Пример: {% endblock body %}. -
D004 (Django) Статические URL следует записывать в виде {% static path/to/file %}. ✔️
J004 (Jinja) Статические URL следует записывать в виде {{ url_for('static'..) }}. ✔️
H005 У тега html должен быть непустой атрибут lang. ✔️
H006 У тега img должны быть атрибуты height и width. -
H007 Перед тегом html должен стоять <!DOCTYPE ... >. ✔️
H008 Значения атрибутов должны быть в двойных кавычках. ✔️
H009 Имена тегов должны быть в нижнем регистре. ✔️
H010 Имена атрибутов должны быть в нижнем регистре. ✔️
H011 Значения атрибутов должны быть в кавычках. ✔️
H012 Вокруг = в атрибутах не должно быть пробелов. ✔️
H013 У тега img должен быть атрибут alt. ✔️
H014 Пустых строк больше, чем допускает конфигурация. ✔️
H015 Ставьте перенос строки после тегов h. ✔️
H016 В html нет тега title. ✔️
H017 Пустые теги должны быть самозакрывающимися (конфликтует с H018). -
D018 (Django) Для внутренних ссылок следует использовать {% url ... %}. ✔️
H018 Пустые теги самозакрывающиеся по своей природе и должны заканчиваться на «>», а не на «/>» (конфликтует с H017). -
J018 (Jinja) Для внутренних ссылок следует использовать {{ url_for() ... }}. ✔️
H019 Замените javascript:abc() на обработчик события on_ и настоящий URL. ✔️
H020 Найдена пустая пара тегов. Возможно, её стоит удалить. ✔️
H021 Следует избегать встроенных стилей. ✔️
H022 Используйте HTTPS для внешних ссылок. ✔️
H023 Не используйте мнемоники. ✔️
H024 Не указывайте type у скриптов и стилей. ✔️
H025 Похоже, у тега нет пары. ✔️
H026 Пустые id и class можно удалить. ✔️
T027 В синтаксисе шаблона найдена незакрытая строка. ✔️
T028 Стоит использовать теги с удалением пробелов внутри значений атрибутов. {%- if/for -%} -
H029 Стоит записывать значения method у form в нижнем регистре. ✔️
H030 Стоит добавить meta description. ✔️
T032 В тегах шаблона найдены лишние пробелы. ✔️
H033 В action формы найдены лишние пробелы. ✔️
T034 Возможно, вы имели в виду {% … %} вместо {% … }%? ✔️
H036 Не используйте теги br для отступов. ✔️
H037 Найден повторяющийся атрибут. ✔️
T038 У блочного тега нет парного закрывающего тега. ✔️
T039 Найден незакрытый тег шаблона. ✔️
T040 В теге extends или include нет имени шаблона или оно пустое. ✔️
H041 Тег закрывается не в том блоке шаблона, в котором открыт. ✔️
T041 Тег extends должен быть первым тегом в шаблоне. ✔️
H042 Для атрибута for у label в этом файле нет элемента с таким id. ✔️
T042 Содержимое вне блоков не выводится в шаблоне, который расширяет другой. ✔️
H043 У тега button должен быть атрибут type. ✔️
T043 Имя блока используется в шаблоне больше одного раза. ✔️
H044 В thead не следует смешивать ячейки th и td. ✔️
T044 Тег вывода содержит ключевое слово инструкции; используйте блочный тег. ✔️
H045 У тега iframe должен быть атрибут title. ✔️
T045 Тег шаблона внутри html-комментария всё равно выполняется; чтобы отключить его, используйте комментарий шаблона. ✔️
H046 Значение tabindex не должно быть положительным. ✔️
H047 Атрибут aria-hidden не следует задавать фокусируемому элементу. ✔️
H048 Такого атрибута aria нет в спецификации. ✔️
H049 Viewport не должен запрещать масштабирование страницы. ✔️
H050 Элемент устарел, и его следует заменить. ✔️
H051 Такой роли нет среди ролей, которые ARIA определяет для разметки. ✔️
H052 Meta refresh не должен перезагружать страницу или перенаправлять с неё по таймеру. ✔️
H053 Id используется в файле больше одного раза. ✔️
H054 Интерактивный элемент не должен быть вложен в другой интерактивный элемент. ✔️
H055 Атрибут lang должен содержать языковой тег, например en или pt-BR. ✔️
H056 Атрибут src не должен быть пустым. ✔️
H057 У видео должна быть дорожка с субтитрами. ✔️

Структура кодов

Первая буква кода означает следующее:

  • D: относится только к Django
  • H: относится к html
  • J: относится только к Jinja
  • M: относится только к Handlebars
  • N: относится только к Nunjucks
  • T: относится к шаблонам в целом

Описание правил

T001

Variables should be wrapped in a whitespace.

Синтаксис шаблона вроде {{user.name}} без внутренних пробелов труднее читать и сравнивать, а разнобой в пробелах по всей кодовой базе делает ненадёжным рефакторинг через grep (поиск переменной или тега), потому что одно и то же выражение встречается в нескольких написаниях. В руководствах по стилю и Django, и Jinja пишут {{ var }} и {% tag %} с одиночными пробелами.

Не применяется в профилях handlebars и golang.

Неправильно:

{{user.name}}

Правильно:

{{ user.name }}

T002

Double quotes should be used in tags.

Если в тегах шаблона ({% extends %}, {% include %}, {% with %}, {% trans %}, {% now %}) смешивать одинарные и двойные кавычки, одно и то же имя шаблона встречается в двух написаниях, и поиск или массовое переименование пропускают половину вхождений. Единообразные двойные кавычки согласуют аргументы тегов с кавычками HTML-атрибутов в остальной части файла.

Одинарные кавычки внутри значений HTML-атрибутов (например, <span title="{% trans 'x' %}">) ошибкой не считаются, поскольку двойные кавычки атрибута вынуждают использовать там одинарные.

С quote_style = "single" правило, наоборот, требует одинарные кавычки, и форматер расставляет именно их.

--reformat сам исправляет эти кавычки.

Неправильно:

{% extends 'base.html' %}

Правильно:

{% extends "base.html" %}

T003

Endblock should have name. Ex: {% endblock body %}.

Когда {% block %} занимает много строк или блоки вложены друг в друга, по {% endblock %} без имени не понять, какой блок он закрывает, поэтому при правке легко закрыть не тот; тогда дочерние шаблоны переопределяют не то содержимое. Имя в endblock документирует пару и позволяет и djLint, и Django (который выбрасывает TemplateSyntaxError при несовпадении имени в endblock) заметить блок, закрытый не в том месте. Ошибки парности (незакрытые блоки, endblock без пары и несовпадающие имена) — это проверки корректности, которые выполняет T038.

По умолчанию отключено; включается через --include=T003. --name-endblocks сам дописывает имя.

Имя не требуется, если блок открывается и закрывается на одной строке, например {% block title %}``{% endblock %}. Форматер переносит содержимое блока, в котором есть другие теги, на отдельные строки, поэтому блоку, записанному в одну строку, после форматирования может понадобиться имя; с --name-endblocks форматер допишет и его.

Неправильно:

{% block content %}
<p>hello</p>
{% endblock %}

Правильно:

{% block content %}
<p>hello</p>
{% endblock content %}

D004

(Django) Static urls should follow {% static path/to/file %} pattern.

Жёстко прописанные пути /static/ обходят тег Django {% static %}, поэтому шаблоны ломаются при изменении STATIC_URL (например, при переносе ресурсов на CDN или развёртывании в подкаталоге) и никогда не получают хешированные имена файлов от ManifestStaticFilesStorage, что приводит к ошибкам 404 или устаревшим закешированным ресурсам в рабочей среде. Правило ищет буквальный префикс /static/, поэтому проекты, раздающие статические файлы по другому пути, им не охватываются.

Неправильно:

<link rel="stylesheet" href="/static/css/style.css">

Правильно:

<link rel="stylesheet" href="{% static 'css/style.css' %}">

J004

(Jinja) Static urls should follow {{ url_for('static'..) }} pattern.

Жёстко прописанные пути /static/ обходят url_for(‘static’, …) из Flask/Jinja, поэтому ресурсы отдают 404, когда приложение смонтировано под префиксом URL или меняется папка либо хост статических файлов, а добавляемые фреймворком параметры запроса для сброса кеша теряются. Правило ищет буквальный префикс /static/, поэтому проекты, раздающие статические файлы по другому пути, им не охватываются.

Неправильно:

<link rel="stylesheet" href="/static/css/style.css">

Правильно:

<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">

H005

Html tag should have a non-empty lang attribute.

Без атрибута lang у <html> скринридеры угадывают правила произношения и могут прочитать страницу не на том языке, а браузеры не могут правильно предложить перевод, расставить переносы или выбрать кавычки, принятые в нужной локали. Указание языка страницы — критерий успешности WCAG 2.1 3.1.1 (уровень A).

И lang="", и lang без значения означают, что язык неизвестен, поэтому о них сообщается так же, как об отсутствующем атрибуте.

Неправильно:

<!DOCTYPE html>
<html>
</html>

Правильно:

<!DOCTYPE html>
<html lang="en">
</html>

H006

Img tag should have height and width attributes.

По умолчанию отключено; включается через --include=H006.

Если у <img> нет ширины и высоты, браузер не может зарезервировать место до загрузки изображения, и окружающее содержимое скачет по мере загрузки картинок. Такие сдвиги макета ухудшают показатель Cumulative Layout Shift (одну из метрик Core Web Vitals) и могут заставить пользователя промахнуться при клике, пока страница не устоится.

Неправильно:

<img src="cat.png" alt="Cat">

Правильно:

<img src="cat.png" alt="Cat" width="120" height="80">

H007

<!DOCTYPE ... > should be present before the html tag.

Без <!DOCTYPE> перед тегом <html> браузеры отображают страницу в режиме совместимости (quirks mode), имитируя устаревшую блочную модель и поведение раскладки, поэтому CSS в разных браузерах отображается по-разному. Теги шаблона и комментарии перед doctype допустимы; он должен предшествовать только самому тегу <html>.

Неправильно:

<html lang="en">
</html>

Правильно:

<!DOCTYPE html>
<html lang="en">
</html>

H008

Attributes should be double quoted.

Из-за разных кавычек значения атрибутов труднее просматривать и искать через grep, а значения в одинарных кавычках ломаются, как только в содержимом появляется апостроф. Двойные кавычки — соглашение, принятое в спецификациях HTML, форматерах и большинстве руководств по стилю, поэтому, придерживаясь их, вы согласуете шаблоны с остальной экосистемой.

Неправильно:

<div class='content'></div>

Правильно:

<div class="content"></div>

H009

Tag names should be lowercase.

HTML-парсеры принимают имена тегов в верхнем регистре, но сериализации XHTML и XML чувствительны к регистру и отвергают их, а смешанный регистр делает ненадёжными текстовый поиск и просмотр изменений (поиск <h1> через grep пропустит <H1>). Имена тегов в нижнем регистре делают шаблоны переносимыми и единообразными.

При --ignore-case правило не срабатывает: этот флаг велит форматеру не трогать регистр.

Неправильно:

<H1>Welcome</H1>

Правильно:

<h1>Welcome</h1>

H010

Attribute names should be lowercase.

Имена атрибутов в верхнем регистре недопустимы в сериализациях XHTML/XML и мешают текстовому поиску по шаблонам (поиск src= через grep пропустит SRC=). DOM всё равно приводит имена HTML-атрибутов к нижнему регистру, так что написание в верхнем регистре лишь добавляет разнобой без всякой пользы. Сообщается только об именах, которые форматер переводит в нижний регистр: class, id, src, width, height, alt, style, lang, title, media и srcset. --reformat исправляет их сам, а привязка фреймворка вроде :itemID или более длинное имя вроде data-ID сохраняют свой регистр.

При --ignore-case правило не срабатывает: этот флаг велит форматеру не трогать регистр.

Неправильно:

<img SRC="cat.png" alt="Cat" width="120" height="80">

Правильно:

<img src="cat.png" alt="Cat" width="120" height="80">

H011

Attribute values should be quoted.

Значение атрибута без кавычек заканчивается на первом пробельном символе, поэтому у значения вроде class=btn primary всё после пробела молча теряется (браузер считает «primary» отдельным логическим атрибутом). Особенно хрупки значения из переменных шаблона: любой выведенный пробел, «=» или «>» портит тег. Кавычки делают границу значения явной и безопасной.

Неправильно:

<div class=test></div>

Правильно:

<div class="test"></div>

H012

There should be no spaces around attribute =.

HTML допускает пробелы вокруг «=», так что это правило о стиле. Обычно name=“value” пишут без пробелов, и именно на такую запись рассчитаны простые текстовые инструменты вроде grep или поиска с заменой, поэтому из-за разнобоя в пробелах атрибуты труднее находить и рефакторить. --reformat сам удаляет эти пробелы.

Неправильно:

<div class = "test"></div>

Правильно:

<div class="test"></div>

H013

Img tag should have an alt attribute.

Без атрибута alt скринридеры зачитывают имя файла изображения или вообще ничего, что нарушает WCAG 1.1.1 (нетекстовое содержимое). Кроме того, текст alt пользователи видят, когда изображение не загрузилось. У декоративных изображений должен быть явный пустой alt=“”, чтобы вспомогательные технологии знали, что их нужно пропустить; такой alt это правило тоже устраивает.

alt без значения — то же самое, что alt="", то есть случай декоративного изображения, поэтому он допускается.

Неправильно:

<img src="cat.jpg" height="200" width="300">

Правильно:

<img src="cat.jpg" height="200" width="300" alt="A sleeping cat">

H014

Found extra blank lines.

Серии пустых строк никак не влияют на итоговую страницу (HTML схлопывает пробельные символы), но раздувают шаблоны и засоряют diff при изменении соседних строк. Форматер djLint по умолчанию удаляет их полностью (оставляя не более max_blank_lines пустых строк, по умолчанию 0), поэтому оставшиеся серии говорят о неотформатированном коде.

Неправильно:

<div>one</div>


<p>two</p>

Правильно:

<div>one</div>

<p>two</p>

H015

Follow h tags with a line break.

Заголовки — блочные ориентиры, задающие структуру документа; если втиснуть следующий элемент на одну строку с закрывающим тегом h, эта структура скрывается в исходном коде, а правка любого из элементов выглядит в diff как изменение обоих. Перенос строки после каждого заголовка сохраняет визуальную структуру шаблона в соответствии со структурой итоговой страницы.

Неправильно:

<h1>Heading</h1><p>Intro text.</p>

Правильно:

<h1>Heading</h1>
<p>Intro text.</p>

H016

Missing title tag in html.

Спецификация HTML требует элемент title в каждом документе. Без него вкладки браузера, закладки и история показывают голый URL вместо названия страницы, поисковые системы теряют главную подпись страницы, а пользователи скринридеров — первое, что зачитывается при загрузке, что нарушает WCAG 2.4.2 (заголовок страницы, уровень A).

Срабатывает только на файлах, содержащих полный документ <html>…</html>, поэтому фрагменты и дочерние шаблоны, расширяющие базовый, никогда не попадают под правило. Оболочкам SPA, которые задают заголовок на стороне клиента, всё равно нужен статический <title>: именно он виден при первой отрисовке, поисковым роботам и при сбое JavaScript.

Неправильно:

<html lang="en">
<body>Content</body>
</html>

Правильно:

<html lang="en">
<head>
<title>My page</title>
</head>
<body>Content</body>
</html>

H017

Void tags should be self closing.

Шаблоны, которые должны также разбираться как XML/XHTML (или передаваться инструментам на основе XML), ломаются на пустых элементах без закрывающей косой черты, а смесь <br> и <br /> в кодовой базе даёт непоследовательные diff. Это правило требует соглашения в стиле XHTML, чтобы все пустые элементы закрывались одинаково.

По умолчанию отключено; включается через --include=H017. Несовместимо с H018: включайте только одно из двух соглашений. --close-void-tags сам добавляет косую черту к пустым тегам html. svg-элемент <path> остаётся как написан, поэтому закройте его вручную.

Неправильно:

<br>
<meta charset="utf-8">

Правильно:

<br />
<meta charset="utf-8" />

D018

(Django) Internal links should use the {% url ... %} pattern.

Жёстко прописанные внутренние URL молча устаревают при изменении пути маршрута в urls.py, и появляются битые ссылки и нерабочие action форм, которые не поймает ни один тест URLconf. {% url %} получает путь по имени маршрута, поэтому при переименовании пути все ссылки обновляются разом.

Неправильно:

<a href="/accounts/login">Login</a>

Правильно:

<a href="{% url 'login' %}">Login</a>

H018

Void tags are self closing by nature and must end with ">", not "/>"

В живом стандарте HTML косая черта в конце пустого элемента ничего не значит (парсер её игнорирует), поэтому запись <br /> намекает на самозакрытие в стиле XML, которого в HTML нет, и может подтолкнуть читателей к добавлению косой черты в непустые теги, где лишняя / молча отбрасывается и маскирует ошибки с незакрытыми тегами. Это правило требует, чтобы пустые элементы заканчивались просто на >.

По умолчанию отключено; включается через --include=H018. Несовместимо с H017: включайте только одно из двух соглашений. Исключение — SVG <path />: SVG — это XML, и косая черта там обязательна. При --close-void-tags правило не срабатывает, поскольку тогда форматер сам пишет косую черту, на которую оно указывает.

Неправильно:

<br />
<meta charset="utf-8" />

Правильно:

<br>
<meta charset="utf-8">

J018

(Jinja) Internal links should use the {{ url_for() ... }} pattern.

Жёстко прописанные внутренние URL молча ломаются, когда меняется путь маршрута или приложение монтируется под префиксом: остаются битые ссылки и формы, отправляющие данные на 404. url_for() строит URL по имени конечной точки, поэтому изменения маршрутов автоматически распространяются на все шаблоны.

Неправильно:

<a href="/accounts/login">Login</a>

Правильно:

<a href="{{ url_for('login') }}">Login</a>

H019

Replace 'javascript:abc()' with on_ event and real url.

URL вида javascript: ломают клик средней кнопкой и открытие в новой вкладке, ничего не делают, если JavaScript отключён или не загрузился, блокируются строгими политиками Content Security Policy и служат классической точкой для XSS-инъекций. Вместо этого укажите в href настоящий URL, а поведение назначьте через обработчик события. При строгой CSP встроенные обработчики on* тоже блокируются: показанный onclick — минимальное исправление внутри шаблона; лучше назначать обработчик через addEventListener из файла скрипта.

Неправильно:

<a href="javascript:openPopup()">Open popup</a>

Правильно:

<a href="{% url 'popup' %}" onclick="openPopup(event)">Open popup</a>

H020

Empty tag pair found. Consider removing.

Пустая пара тегов ничего не выводит, но всё равно создаёт узел DOM, к которому таблицы стилей могут применить внешние отступы, рамки или промежутки flex/grid, и появляются призрачные отступы, источник которых трудно найти; обычно это остатки разметки после прежней правки. Теги, которые в обычной разметке вполне могут быть пустыми (td, th, li, dt, dd, slot), исключены. Теги с любым атрибутом (точки монтирования JS вроде <div id="app">``</div>, элементы иконочных шрифтов вроде <i class="fa fa-user">``</i>) тоже не попадают под правило; срабатывают только пустые пары совсем без атрибутов.

Неправильно:

<p>Saved.</p>
<span> </span>

Правильно:

<p>Saved.</p>

H021

Inline styles should be avoided.

Встроенные стили имеют более высокую специфичность, чем любой селектор из таблицы стилей, поэтому для их последующего переопределения нужен !important; их блокируют политики Content Security Policy без ‘unsafe-inline’ в style-src; кроме того, они разбрасывают оформление по шаблонам, и смена темы или дизайна превращается в правку разметки вместо одной таблицы стилей. Перенесите объявление в CSS-класс. Одно обоснованное исключение — HTML-шаблоны писем: многие почтовые клиенты вырезают блоки <style>, и встроенные стили там — стандартный приём; исключите каталоги с шаблонами писем или отключите для них это правило.

Неправильно:

<div style="color: red;">Wrong username or password.</div>

Правильно:

<div class="error">Wrong username or password.</div>

H022

Use HTTPS for external links.

Подресурсы по обычному http:// на странице, отданной по HTTPS, — это смешанное содержимое: скрипты, таблицы стилей и iframe браузеры блокируют сразу, а для изображений автоматически переходят на HTTPS или выводят предупреждение. Ссылка <a> на страницу по http:// смешанным содержимым не является, но всё равно отправляет посетителей по незашифрованному соединению, открытому для перехвата и подмены. Ссылки на внутренние хосты, где TLS действительно нет, тоже будут отмечены; заглушите такие места блоком {# djlint:off H022 #}, а не отключайте правило целиком.

Неправильно:

<a href="http://example.com">Example</a>

Правильно:

<a href="https://example.com">Example</a>

H023

Do not use entity references.

Документы HTML5 используют UTF-8, поэтому сам символ работает везде, и именно его на самом деле видит тот, кто проверяет код; опечатку в мнемонике (например, &mdsah;) браузер не заметит и выведет как есть, в виде испорченного текста. djLint допускает мнемоники, несущие синтаксическую роль (&lt;, &gt;, &amp;, &quot;, &apos;, а также фигурные скобки &lbrace; и &rbrace; вместе с &percnt;, &num; и &dollar;, которые, будучи записаны символами, завершили бы разделитель шаблона), и мнемоники невидимых символов, которые нельзя проверить на глаз в виде самого символа: пробелы (&nbsp;, &thinsp;, &hairsp;), соединители и метки направления (&zwnj;, &zwj;, &lrm;, &rlm;) и &shy;, причём в именованной, десятичной и шестнадцатеричной форме одинаково.

--reformat сам заменяет мнемонику символом. Мнемоника внутри тега шаблона — часть тега, а не страницы, поэтому её не трогают ни правило, ни форматер. --no-entity-formatting отключает замену, но оставляет правило включённым, так что о мнемонике всё равно будет сообщено. Чтобы сохранить мнемоники, добавьте H023 в ignore.

Неправильно:

<p>Dates 1900 &mdash; 2000</p>

Правильно:

<p>Dates 1900 — 2000</p>

H024

Omit type on scripts and styles.

text/javascript и text/css — значения по умолчанию в HTML5 для <script> и <style>, так что этот атрибут — бесполезный груз, который браузер игнорирует; спецификация WHATWG прямо предписывает его опускать. Кроме того, без него не остаётся устаревших MIME-строк, которые ломают элемент, если их скопировать в скрипты-модули (где type=“module” действительно важен).

Неправильно:

<script type="text/javascript" src="app.js">

Правильно:

<script src="app.js"></script>

H025

Tag seems to be an orphan.

Если у тега нет парного открывающего или закрывающего тега, механизм восстановления после ошибок в браузере вынужден угадывать, где кончается элемент, и последующая разметка попадает не в тот элемент; после этого вёрстка, CSS-селекторы и запросы к DOM из JavaScript молча ломаются, причём в разных браузерах по-разному. H025 также сообщает об <ol> или <ul>, открытых внутри <p>: HTML-парсер закрывает абзац перед списком, поэтому вложенность никогда не получается такой, как написано.

Неправильно:

<div>
  <p>Hello</p>

Правильно:

<div>
  <p>Hello</p>
</div>

H026

Empty id and class tags can be removed.

Ни один селектор класса или id не соответствует пустому атрибуту, а пустой id — недопустимый HTML (значение id не может быть пустой строкой). Селектор наличия атрибута вроде div[class] всё же ему соответствует, поэтому удаление такого атрибута заметно для таблицы стилей, написанной подобным образом. Обычно пустой атрибут говорит об ошибке в шаблоне, где должна была подставляться переменная, поэтому, удалив или заполнив его, вы не дадите этой ошибке прятаться у всех на виду.

Неправильно:

<div id="" class="">content</div>

Правильно:

<div>content</div>

T027

Unclosed string found in template syntax.

Открытая, но не закрытая кавычка внутри {% ... %} или {{ ... }} заставляет шаблонизатор неверно разобрать тег: Django и Jinja либо выбрасывают TemplateSyntaxError при отрисовке, либо молча поглощают остальные аргументы тега как содержимое строки, и страница либо падает с ошибкой 500, либо отрисовывается без части аргументов.

Неправильно:

{% trans "Welcome %}

Правильно:

{% trans "Welcome" %}

T028

Consider using spaceless tags inside attribute values. {%- if/for -%}

По умолчанию отключено; включается через --include=T028.

Пробельные символы, которые удаляет тег с удалением пробелов, попадают в вывод, поэтому применяйте его только там, где атрибуту нечего терять. alt="{%- if brand -%}Acme{%- endif -%} logo" отрисуется как Acmelogo, а svg-атрибут d="M12 {%- if big -%}20{%- endif -%} 4Z" превратится в другой контур. Именно поэтому правило нужно включать явно.

Теги шаблона внутри значения атрибута выводят окружающие их пробелы и переводы строк в итоговый атрибут, поэтому href или src, собранный обычными тегами {% if %}/{% for %}, может содержать лишние пробелы и давать битые URL. Теги управления пробелами Jinja/Nunjucks ({%- ... -%}) удаляют эти окружающие пробелы, и атрибут выводится одним чистым значением. Атрибут class исключён, поскольку лишние пробелы между именами классов безвредны.

Не применяется в профиле django: теги шаблонов Django не поддерживают управление пробелами через {%- -%}.

Неправильно:

<a href="{% if x %}/home{% endif %}"></a>

Правильно:

<a href="{%- if x -%}/home{%- endif -%}"></a>

H029

Consider using lowercase form method values.

Спецификация HTML определяет ключевые слова method формы в нижнем регистре (get, post); варианты в верхнем регистре браузеры принимают лишь благодаря запасному сравнению без учёта регистра. Каноническая форма в нижнем регистре делает шаблоны единообразными и удобными для поиска через grep и избавляет от претензий строгих валидаторов и инструментов на основе XHTML.

Неправильно:

<form method="POST"></form>

Правильно:

<form method="post"></form>

H030

Consider adding a meta description.

Поисковые системы показывают meta description как сниппет под заголовком страницы в результатах поиска; без него они составляют сниппет из произвольного текста страницы, что снижает кликабельность и ухудшает превью ссылки, когда страницей делятся.

Срабатывает только на файлах, содержащих полный документ <html>…</html>. Довод про сниппет относится к публично индексируемым страницам; для приложений, закрытых авторизацией, и приложений во внутренней сети это правило обычно отключают.

Неправильно:

<html lang="en">
  <head><title>Home</title></head>
  <body>Welcome</body>
</html>

Правильно:

<html lang="en">
  <head>
    <title>Home</title>
    <meta name="description" content="A short summary of this page.">
  </head>
  <body>Welcome</body>
</html>

T032

Extra whitespace found in template tags.

Серии пробелов или табуляций между аргументами тега шаблона — невидимый шум: они скрывают настоящие различия в diff, мешают заметить пропущенный аргумент и расходятся со стилем в один пробел, который выдаёт форматер djLint, вызывая лишние переформатирования. Пробелы внутри строк в кавычках сохраняются и ошибкой не считаются.

Неправильно:

{% static  'css/style.css' %}

Правильно:

{% static 'css/style.css' %}

H033

Extra whitespace found in form action.

Пробелы в начале или конце значения action формы становятся частью итогового URL. Браузеры удаляют их при разборе, но другие клиенты и тесты, обращающиеся по буквальному значению, могут этого не делать, а рядом с тегом {% url %} лишний пробел почти всегда означает опечатку, из-за которой URL отправки формы не совпадёт ни с одним маршрутом на сервере.

Неправильно:

<form action="{% url 'search' %} " method="get">
    <button>Search</button>
</form>

Правильно:

<form action="{% url 'search' %}" method="get">
    <button>Search</button>
</form>

T034

Did you intend to use {% ... %} instead of {% ... }%?

}% — почти всегда опечатка вместо %}. Шаблонизатор не распознаёт }% как разделитель тега, поэтому тег так и не разбирается: необработанный текст {% … }% просачивается в итоговый HTML, или шаблонизатор выдаёт синтаксическую ошибку, натолкнувшись на незакрытый тег.

Неправильно:

{% include "footer.html" }%

Правильно:

{% include "footer.html" %}

H036

Do not use br tags for spacing.

Спецификация html допускает <br> только для переноса строки, который сам является частью содержимого, как в почтовом адресе или стихотворении, и такое использование правило не трогает. Сообщается об оформительском использовании, которое спецификация запрещает: о серии из двух и более переносов, то есть о вертикальном отступе, и о переносе у внутреннего края блочного элемента, который не даёт ничего такого, чего не дал бы внешний отступ самого элемента. И то и другое ломает перетекание текста на узких экранах, а скринридер объявляет принудительный перенос там, где объявлять нечего.

Неправильно:

<p>Shipping is free.<br><br>Delivery takes 3 days.</p>

Правильно:

<p>Shipping is free.</p>
<p>Delivery takes 3 days.</p>

H037

Duplicate attribute found.

Повторяющиеся атрибуты — недопустимый HTML: браузеры оставляют только первое вхождение и молча отбрасывают остальные, поэтому второе значение class или style так и не применяется, что скрывает настоящие ошибки. Проверка учитывает синтаксис шаблонов: атрибут, повторённый во взаимоисключающих ветках ({% if %}/{% else %}), ошибкой не считается, поскольку выведена может быть только одна копия.

Неправильно:

<div class="card" id="profile" class="active">...</div>

Правильно:

<div class="card active" id="profile">...</div>

T038

Block tag has no matching end tag.

Блочный тег вроде {% if %}, {% for %} или {% macro %} без парного закрывающего тега — однозначная TemplateSyntaxError в Django и Jinja: страница не отрисуется в момент запроса, а это правило ловит ошибку ещё до развёртывания. Оно также отмечает закрывающие теги без открывающего и неправильно перемежающиеся блоки (например, {% if %}``{% for %}``{% endif %}).

Парность {% block %}/{% endblock %} и несовпадение имён в endblock проверяет это правило; T003 (по умолчанию отключено) дополнительно требует имя в каждом многострочном {% endblock %}. Проверяются и пользовательские блочные теги, зарегистрированные через custom_blocks, включая их самозакрывающуюся форму / %}.

Неправильно:

{% if user.is_authenticated %}
<p>Welcome back!</p>

Правильно:

{% if user.is_authenticated %}
<p>Welcome back!</p>
{% endif %}

T039

Unclosed template tag found.

Тег шаблона, открытый через {{ или {%, но не закрытый парным }} или %}, не разбирается как тег: Django/Jinja либо выбрасывают TemplateSyntaxError, либо выводят на страницу сами фигурные скобки, а всё до следующего разделителя может быть молча поглощено. Такие опечатки (одна пропущенная скобка, неподходящий разделитель) легко пропустить при проверке кода, потому что шаблон может всё же частично отрисоваться.

Неправильно:

<p>{{ user.name }</p>

Правильно:

<p>{{ user.name }}</p>

T040

Missing or empty template name in extends or include tag.

Тегу {% extends %} или {% include %} с отсутствующим, пустым или состоящим из одних пробелов именем шаблона нечего загружать: Django выбрасывает TemplateSyntaxError, если имени нет совсем, и TemplateDoesNotExist при отрисовке, если оно пустое, поэтому в рабочей среде страница падает с ошибкой 500, хотя сам файл шаблона выглядит синтаксически правдоподобно.

Неправильно:

{% extends "" %}

Правильно:

{% extends "base.html" %}

H041

Tag is closed in a different template block than it was opened.

Если HTML-тег открывается в одном {% block %}, а закрывается в другом, дочерний шаблон, переопределяющий только один из этих блоков, наследует половину элемента, и на итоговой странице получается несбалансированная разметка; браузеры затем непредсказуемо закрывают элементы сами или перестраивают их вложенность, ломая вёрстку и CSS-селекторы далеко от шаблона, который на самом деле правили. Если каждый элемент открывается и закрывается в одном и том же блоке, любой блок можно безопасно переопределять независимо от других.

Неправильно:

{% block content %}
<div class="wrapper">
{% endblock content %}
{% block footer %}
</div>
{% endblock footer %}

Правильно:

{% block content %}
<div class="wrapper">
</div>
{% endblock content %}
{% block footer %}
{% endblock footer %}

T041

Extends tag should be the first tag in the template.

Django отказывается компилировать шаблон, в котором перед {% extends %} стоит другой тег, а текст, написанный перед ним, выводится и попадает на страницу раньше всего, что выводит родительский шаблон. Jinja тоже выводит этот текст, а nunjucks его отбрасывает, так что ни в одном шаблонизаторе шаблон не делает того, что от него можно ожидать по виду.

Комментарий {# #} ничего не выводит и не учитывается, как и всё, что находится внутри блока {% comment %}, {% raw %} или {% verbatim %}, с именем или без, внутри области {# djlint:off #} в любом из трёх вариантов записи, а также front matter на yaml. Html-комментарий учитывается: шаблонизатор выводит его на страницу раньше doctype родительского шаблона, а именно о такой утечке это правило. Учитываются и блоки {% blocktrans %}, {% filter %} и <?php ?>, поскольку каждый из них что-то выводит. Проверяется только первый {% extends %}; второй — ошибка сама по себе. В профилях jinja и nunjucks тег ветвления перед ним не учитывается, поскольку в документации jinja {% if x %}{% extends "a.html" %}{% else %}{% extends "b.html" %}{% endif %} описан как способ выбрать родительский шаблон; django же считывает остаток шаблона в {% extends %} и затем отвергает {% endif %}, поэтому в этом профиле тег ветвления учитывается.

Не применяется в профилях handlebars, golang, liquid и angular.

Неправильно:

{% load static %}
{% extends "base.html" %}

Правильно:

{% extends "base.html" %}
{% load static %}

H042

Label for attribute has no matching element id in this file.

Проверка выполняется только для файлов, которые она может надёжно проанализировать: если в файле есть что-либо, что может вывести id, которого в самом файле не видно (вывод {{ ... }}, например виджет формы, {% include %} или {% extends %} либо нераспознанный тег шаблона), для этого файла правило молчит. Там же, где оно работает, каждое сообщение означает действительно нарушенную связь.

Неправильно:

<label for="email">Email</label>
<input id="username">

Правильно:

<label for="email">Email</label>
<input id="email">

T042

Content outside a block is not rendered in a template that extends another.

Если шаблон расширяет другой, то, что выводится, определяет родительский шаблон, а дочерний лишь заполняет его блоки. Текст или html, написанные после {% extends %} вне всех {% block %}, при отрисовке молча отбрасываются, так что абзац, который в исходном коде выглядит нормально, на страницу так и не попадает.

Тег шаблона в таком месте всё равно выполняется, поэтому {% load %}, {% set %} и {% if %}, обёрнутый вокруг блока, не трогаются; не трогаются и комментарии {# #} и {% comment %}, html-комментарии, блоки {% raw %} и {% verbatim %}, а также тело {% macro %}, блочной формы {% set %}, {% partialdef %} или {% addtoblock %}, которое захватывается, а не выводится. О тексте внутри {% blocktrans %} сообщается, поскольку {% blocktrans %} — это не {% block %}. Учитывается только содержимое после тега extends, и о каждом его непрерывном фрагменте сообщается один раз, в его начале.

Шаблонизатор читает теги шаблона раньше html, поэтому {% extends %} внутри html-комментария или <pre> всё равно выполняется и всё равно делает файл дочерним; по-настоящему скрывают его только {# #}, {% comment %}, {% raw %} и {% verbatim %}. Текст внутри html-комментария в любом случае не доходит до читателя, и о нём не сообщается, но теги, записанные там, всё равно читаются.

Не применяется в профилях handlebars, golang, liquid и angular.

Неправильно:

{% extends "base.html" %}
<p>This paragraph is never shown.</p>
{% block content %}
<h1>Welcome</h1>
{% endblock %}

Правильно:

{% extends "base.html" %}
{% block content %}
<h1>Welcome</h1>
<p>This paragraph is shown.</p>
{% endblock %}

H043

Button tag should have a type attribute.

<button> без type по умолчанию получает тип submit, поэтому кнопка, предназначенная для запуска скрипта, заодно отправляет окружающую её форму, и страница перезагружается. Явно указанный тип это предотвращает.

Неправильно:

<form>
  <button onclick="preview()">Preview</button>
</form>

Правильно:

<form>
  <button type="button" onclick="preview()">Preview</button>
</form>

T043

Block name is used more than once in the template.

Django, Jinja и Nunjucks отказываются разбирать шаблон, в котором два блока названы одинаково, поэтому страница вообще не загружается. Шаблонизаторам неважно, что два блока находятся в разных ветках {% if %}, поэтому имя каждого блока должно быть уникальным во всём шаблоне, стоят ли блоки рядом или один вложен в другой.

Учитывается только {% block %}: {% blocktrans %} — не блок, а {% endblock name %} лишь называет закрываемый блок. Имена сравниваются в точности как написаны, поскольку шаблонизаторы считают Content и content двумя разными блоками.

Пропускается только то, что шаблонизатор сам никогда не разбирает: комментарий шаблона, блок {% comment %}, тело {% raw %} или {% verbatim %} и область djlint:off. Блок внутри html-комментария, тела <script>, <style>, <pre> или <textarea> либо тела {% filter %} всё равно учитывается, поскольку шаблонизатор читает всё это и точно так же выдаёт ошибку на повторяющееся имя.

{% embed %} открывает собственную область видимости: блоки в нём заполняют встраиваемый шаблон, а не текущий, поэтому в двух встраиваниях одного и того же фрагмента можно в каждом написать {% block body %}. Об имени, повторённом внутри одного встраивания, по-прежнему сообщается.

Не применяется в профилях handlebars, golang, liquid и angular.

Неправильно:

{% block content %}{% endblock %}
{% block content %}{% endblock %}

Правильно:

{% block content %}{% endblock %}
{% block sidebar %}{% endblock %}

H044

Thead should not mix th and td cells.

Каждая строка оценивается отдельно, поэтому строка пояснений из td, которую спецификация html помещает в thead рядом со строкой заголовков, смесью не считается. Пустая td в начале строки — угловая ячейка таблицы с заголовками в первом столбце, и именно такую разметку требует руководство W3C по доступности, поэтому она пропускается.

Для скринридера th и td имеют разный смысл, и обычно у них разный css, поэтому одна случайная ячейка в строке заголовков читается как данные и оформлена не так, как соседние столбцы. Такая смесь — допустимый html, потому её и трудно заметить.

Неправильно:

<thead>
  <tr>
    <th>Name</th>
    <td>Size</td>
  </tr>
</thead>

Правильно:

<thead>
  <tr>
    <th>Name</th>
    <th>Size</th>
  </tr>
</thead>

T044

Output tag holds a statement keyword; use a block tag.

Не применяется в профилях golang, handlebars и angular.

Тег вывода печатает значение, а if, for, url, include и прочие — инструкции, место которых в блочном теге. Django, Jinja и Nunjucks отвергают {{ if x }} с синтаксической ошибкой, а закрывающее ключевое слово само по себе, например {{ endif }}, читается как переменная, которая ничего не выводит, тогда как блок, который оно должно было закрыть, остаётся открытым, поэтому страница либо не загружается, либо показывает то, что условие должно было скрыть.

Ключевое слово без аргументов является обычным именем переменной, поэтому о {{ url }}, {{ url|default:"/" }} и {{ set.name }} не сообщается. Сообщается только о ключевом слове с аргументом, а также о закрывающем ключевом слове или ключевом слове ветвления самом по себе, например {{ endif }} или {{ else }}. Управляющее ключевое слово вроде if или for не может начинать выражение, поэтому всё, что идёт после него, — аргумент, включая оператор, и о {{ if -1 > count }} сообщается. Остальные имена служат также переменными и функциями, и для них аргументом считается только строка в кавычках или слово, поэтому выражение, которое просто начинается с одного из них, не трогается: {{ url ~ "/x" }}, {{ url if url else "#" }}, {{ url ? url : '#' }}, {{ block ('title') }}, {{ block .super }} и {{ filter [0] }} проходят без замечаний.

Тело блока {% raw %} или {% verbatim %} — это текст, а не синтаксис шаблона, и оно пропускается, включая именованную форму {% verbatim vueapp %}...{% endverbatim vueapp %}, поэтому о защищённых таким образом {{ }} из Vue или Handlebars не сообщается. {{ }} внутри аргумента блочного тега в кавычках, как в {% trans "Write {{ if x }} instead" %}, — это текст, который шаблонизатор печатает, и он тоже не трогается.

Неправильно:

{{ if user.is_active }}

Правильно:

{% if user.is_active %}

H045

Iframe tag should have a title attribute.

Скринридер объявляет iframe по его доступному имени. Без имени он зачитывает url фрейма или вообще ничего, и до входа во фрейм невозможно понять, что за страница в него встроена. В WCAG это относится к критерию 4.1.2 «Имя, роль, значение», а axe и html-validate включают такую проверку по умолчанию.

Имя может задаваться через title, aria-label или aria-labelledby, и любого из трёх достаточно для правила. Имя, выводимое тегом шаблона, тоже засчитывается, поэтому о фрейме, заголовок которого задаётся для каждой страницы отдельно, не сообщается.

Неправильно:

<iframe src="/report/"></iframe>

Правильно:

<iframe src="/report/" title="Quarterly report"></iframe>

T045

Template tag inside an html comment still runs; use a template comment to disable it.

Html-комментарий скрывает разметку от браузера, но не от шаблонизатора. <!-- {% include "debug.html" %} --> всё равно выводит файл, а <!-- {% if debug %}...{% endif %} --> всё равно вычисляется, причём одинаково в Django, Jinja, Nunjucks, Handlebars и Go, поэтому закомментированный так тег продолжает выполняться, а всё, что он выводит, попадает внутрь комментария или, если там есть -->, вырывается за его пределы. Остановить выполнение тега может только комментарий шаблона: {# #} в Django и Jinja, {{! }} в Handlebars или {{/* */}} в Go.

Сообщается только о тегах-инструкциях: {% %} в профилях, где он есть, секциях, закрывающих тегах и частичных шаблонах handlebars, а также о ключевых словах Go вроде {{if}} или {{end}}. Значение, выводимое в комментарий, как в <!-- built {{ version }} -->, — намеренное использование, и оно не трогается, как и тег внутри комментария шаблона, блока {% comment %} или raw-блока, а также условный комментарий для Internet Explorer, <!--[if IE]> ... <![endif]-->, тело которого — разметка для указанного в нём браузера.

Ключевое слово Go без операнда во всех остальных шаблонизаторах является обычным именем переменной, поэтому <!-- period {{ start }} to {{ end }} --> и <!-- Template: {{ template }} --> в Django, Jinja, Nunjucks, Handlebars и Liquid — это значения, а инструкциями они считаются только при --profile golang. Ключевое слово с операндом Go, как в {{ if .X }} и {{ template "footer" . }}, — инструкция, которую не выводит ни один другой шаблонизатор, и о нём сообщается в любом профиле. {% тоже распознаётся только там, где он есть в шаблонизаторе, поэтому при --profile handlebars и --profile golang это текст, а открывающая последовательность, за которой не следует закрывающая скобка, как в <!-- battery at 50{% charge -->, — обычный текст, каким она и выглядит. Условным считается только комментарий, записанный как условный: начинающийся с <!--[if и закрытый своим <![endif]--> в любом регистре; незакрытый — это обычный комментарий, каким его и читает браузер, и о тегах в нём сообщается.

Неправильно:

<!-- {% include "banner.html" %} -->

Правильно:

{# {% include "banner.html" %} #}

H046

Tabindex should not be positive.

Положительный tabindex переносит элемент в начало порядка табуляции, раньше всех элементов, у которых его нет. Один такой элемент перестраивает всю страницу для пользователя клавиатуры, и после этого порядок приходится поддерживать вручную в каждом шаблоне, который добавляет элемент управления. В WCAG это относится к критерию 2.4.3 «Порядок фокуса».

0 включает элемент в порядок табуляции на месте, которое ему отводит документ, а -1 исключает его оттуда, оставляя возможность передать ему фокус из скрипта. Ни о том, ни о другом не сообщается, как и о значении, выводимом тегом шаблона, — его число здесь неизвестно.

Неправильно:

<input tabindex="1">

Правильно:

<input tabindex="0">

H047

Aria-hidden should not be set on a focusable element.

aria-hidden="true" убирает элемент из дерева доступности, но оставляет его в порядке табуляции, поэтому пользователь клавиатуры всё равно попадает на него и ничего не слышит. Скрытие декоративной иконки — обычное применение атрибута, и о нём не сообщается; сообщается только об элементе, который сам по себе получает фокус.

Фокусируемым считается элемент button, select, textarea, iframe или summary, элемент a или area с href, не скрытый input, audio или video с controls, а также любой элемент с contenteditable или tabindex от 0 и выше. Элемент управления с disabled или с tabindex="-1" уже исключён из порядка табуляции и не трогается.

Неправильно:

<button aria-hidden="true">Close</button>

Правильно:

<button type="button" aria-label="Close"><span aria-hidden="true">x</span></button>

H048

Aria attribute is not one the specification defines.

Атрибут aria с опечаткой не делает вообще ничего. Ни один браузер не предупредит о нём, ни один скринридер о нём не сообщит, а разметка при этом выглядит так, будто о доступности позаботились, поэтому aria-lable может годами оставаться в шаблоне, а элемент управления, которому он должен был дать имя, так и остаётся безымянным.

Правилу известны имена, определённые в ARIA. Привязка, записанная фреймворком, — не обычное имя aria, поэтому :aria-label, v-bind:aria-label и [attr.aria-label] не трогаются.

Неправильно:

<button type="button" aria-lable="Close">x</button>

Правильно:

<button type="button" aria-label="Close">x</button>

H049

Viewport should not stop the page being zoomed.

user-scalable=no, а также maximum-scale меньше 2 не дают увеличить страницу на телефоне, а для многих это единственный способ её прочитать. WCAG требует 200% в критерии 1.4.4 «Изменение размера текста». Браузеры всё чаще игнорируют это ограничение, но там, где оно соблюдается, тег по-прежнему отключает масштабирование.

Атрибуты name и content распознаются в любом порядке записи.

Неправильно:

<meta name="viewport" content="width=device-width, user-scalable=no">

Правильно:

<meta name="viewport" content="width=device-width, initial-scale=1">

H050

Element is obsolete and should be replaced.

<center>, <font>, <big>, <strike> и <tt> были исключены, когда оформление перешло к css, а <marquee>, <blink>, <nobr> и <spacer> вообще никогда не входили в стандарт. Html больше не определяет ни один из них, поэтому ничто не гарантирует, как браузер их расположит, и таблица стилей не может нацелиться на них так, как на класс.

Правилу известны элементы acronym, applet, basefont, bgsound, big, blink, center, dir, font, frame, frameset, isindex, keygen, marquee, menuitem, nobr, noembed, noframes, plaintext, spacer, strike, tt и xmp. Сообщается только об открывающем теге, так что каждый элемент упоминается один раз, а пользовательский элемент, имя которого просто начинается с одного из них, например <font-picker>, не трогается.

Неправильно:

<center><font color="red">Warning</font></center>

Правильно:

<p class="warning">Warning</p>

H051

Role is not one ARIA defines for markup.

Роль, которой нет ни в одной спецификации, просто отбрасывается, и элемент сохраняет тот смысл, который у него уже был, поэтому с role="buton" <div> так и остаётся <div>: для скринридера это ничто, хотя по разметке кажется, что элементу задали назначение. Ничто об этом не предупреждает, поэтому такую опечатку и стоит ловить.

Правилу известны роли, которые ARIA определяет для авторов, а также роли, добавленные DPUB-ARIA (doc-chapter и другие) и GRAPHICS-ARIA (graphics-symbol и другие). Об абстрактных ролях ARIA, таких как landmark и sectionhead, сообщается: по спецификации они нужны для построения её онтологии и не должны использоваться в разметке.

role может содержать несколько имён в виде списка запасных вариантов, и проверяется каждое. Значение с синтаксисом шаблона заранее неизвестно и не трогается, как и привязка, записанная фреймворком, например :role или [attr.role].

Неправильно:

<div role="buton">Save</div>

Правильно:

<button type="button">Save</button>

H052

Meta refresh should not reload or redirect the page on a timer.

Обновление по таймеру меняет страницу прямо во время чтения. Тот, кто читает медленно, пользуется скринридером или просто отвлёкся, без предупреждения и без возможности это остановить теряет место, где остановился, что нарушает WCAG 2.2.1 «Регулируемое время», а обновление, которое перезагружает ту же страницу, уничтожает всё, что в неё было введено.

Нулевая задержка — это немедленное перенаправление, а не таймер, и WCAG допускает его там, где нельзя использовать серверное перенаправление, поэтому о нём не сообщается. Оба атрибута распознаются в любом порядке записи.

Неправильно:

<meta http-equiv="refresh" content="30">

Правильно:

<meta http-equiv="refresh" content="0; url=/next-page">

H053

Id is used more than once in the file.

id именует один элемент. Второй элемент с тем же id ломает getElementById, <label for>, ссылки на фрагменты и aria-labelledby: браузер берёт первый и молча игнорирует остальные, поэтому подпись, ссылка или скрипт без всякого предупреждения попадают не на тот элемент.

Два id во взаимоисключающих ветках одного {% if %}...{% else %}...{% endif %} никогда не выводятся вместе, поэтому о них не сообщается. Цикл {% for %} или {% block %} — не ветка: id внутри него и такой же id снаружи выводятся оба, и сообщается о втором из них. Значение, выводимое тегом шаблона, заранее неизвестно и не трогается, как и пустое значение. id сравниваются точно, как это делает браузер, поэтому save и Save — два разных id.

Ветки распознаются на том языке, на котором написан файл, и с любыми пробелами, которые язык допускает между разделителями и именем тега: {% elif %} и {% else %}, {% elsif %} и {% when %} из liquid, {{ else }} из go, строковая инструкция mako % else:, а также {{else}} из handlebars, его цепочка {{else if x}} и обратная секция любого блока {{#helper}}, включая пользовательские хелперы. Содержимое <template> — отдельный фрагмент документа, до которого getElementById никогда не добирается, поэтому id в нём сравнивается только с id того же <template>: в двух элементах <template> может быть по строке с одинаковым id, а о двух таких id внутри одного <template> всё равно сообщается. id внутри комментария, тела {% raw %} или {% verbatim %} либо области djlint:off вообще не читается.

Неправильно:

<button type="submit" id="submit">Save</button>
<button type="submit" id="submit">Save and continue</button>

Правильно:

<button type="submit" id="submit">Save</button>
<button type="button" id="cancel">Cancel</button>

H054

Interactive element should not be nested inside another.

Html запрещает интерактивное содержимое внутри <a> и <button>. Кнопка внутри ссылки или ссылка внутри кнопки — недопустимая разметка, которую каждый браузер исправляет по-своему, а пользователь скринридера или клавиатуры получает один элемент управления, который ведёт себя как два. axe сообщает о том же как о «nested-interactive».

Отслеживаемые контейнеры — <a> с href и <button>, а сообщается о таких элементах управления внутри них: ссылка с href, button, input, select и textarea. <a> без href не интерактивен и не трогается ни снаружи, ни внутри; то же касается скрытого input или input, тип которого выводит тег шаблона. Содержимое <template> не отрисовывается там, где записано, поэтому элемент управления внутри него не считается вложенным в окружающую ссылку, хотя о вложенности, записанной внутри самого <template>, всё равно сообщается.

Атрибут распознаётся только там, где его записывает тег, и больше нигде: href в {% if not href %} — имя переменной, href в {# href="{{ url }}" #} закомментирован, а href в data-attr=href относится к другому атрибуту, тогда как href, который {% if %} выводит между своими тегами, — такой же атрибут, как любой другой. Незакрытые ссылка или кнопка заканчиваются вместе с окружающим элементом, как это было бы в браузере, поэтому из-за одной опечатки не появляются сообщения по всему остальному файлу; незакрытому элементу на верхнем уровне нечем закончиться, и он действительно тянется до конца файла, где о непарном теге сообщит ещё и H025.

Неправильно:

<a href="/cart"><button>Add</button></a>

Правильно:

<a href="/cart" class="button">Add</a>

H055

Lang attribute should be a language tag such as en or pt-BR.

H005 требует lang у <html>, но значение вроде lang="english" или lang="en_US" удовлетворяет ему, не называя ни одного языка, известного браузеру. Тогда скринридер переходит на голос по умолчанию, а перевод и расстановка переносов выбирают не те правила или никакие. Значение должно быть тегом BCP 47: две или три буквы, затем любое количество подтегов из одного-восьми букв или цифр, каждый после дефиса, как в en, pt-BR или zh-Hant-TW.

Как и в H005, проверяется только тег <html>. Пустое значение оставлено тому правилу, а о значении из одних пробелов сообщается здесь, поскольку H005 считает его значением. Значение, выводимое тегом шаблона, как в lang="{{ LANGUAGE_CODE }}", или коротким echo php, как в lang="<?= $lang ?>", заранее неизвестно и не трогается, а значение, которое просто начинается с $ или {, как в lang="$LANG", читается как написано. xml:lang или data-lang не считаются lang, а > внутри тега шаблона перед атрибутом, как в {% if a > b %}, не скрывает его.

Неправильно:

<html lang="english">

Правильно:

<html lang="en">

H056

Src should not be empty.

Спецификация html называет пустой src недопустимым и предупреждает, что браузер разрешает его относительно url самого документа, поэтому <img src=""> повторно загружает страницу как изображение, а <script src=""></script> — как скрипт. Обычно это заглушка, которую должен был заполнить скрипт, и исправление — убрать атрибут или хранить значение в data-атрибуте, пока не появится настоящее. src вообще без значения, как в <img src>, тоже пуст, и о нём сообщается.

Проверяются только img, script, iframe, embed, source, track, audio и video, поскольку именно эти элементы загружают то, на что указывает src. Сообщается только о значении, которое браузер считает пустым, поэтому значение без кавычек вроде <img src=/static/logo.png> не трогается, как и значение из одних пробелов и значение, выводимое тегом шаблона, а srcset и data-src — другие атрибуты, которые правило не оценивает.

Неправильно:

<img src="" alt="Logo">

Правильно:

<img src="{% static 'logo.png' %}" alt="Logo">

H057

Video should have a captions track.

В видео со звуком речь передаётся только звуковой дорожкой, поэтому глухой или слабослышащий зритель без субтитров ничего из него не получит. Субтитры требует критерий WCAG 1.2.2 «Субтитры (в записи)». Правило устраивает <track> с kind, равным captions или subtitles, внутри <video>, а также <track> вообще без kind, поскольку по умолчанию используется subtitles.

У видео с muted нет звука, для которого нужны субтитры, и о нём не сообщается. Не сообщается и о видео, в открывающем теге или теле которого есть тег шаблона, поскольку дорожки или атрибут muted может вывести шаблон там, где djLint их не видит. Под тегом шаблона понимается то, что открывается синтаксисом языка шаблонов: {{, {%, {# или ${, поэтому обработчик jQuery вроде onclick="$(this).play()", $refs из Alpine, фигурная скобка css и цена в резервном тексте — это обычный текст, каким они и выглядят, и видео всё равно проверяется.

muted должен быть атрибутом, поэтому <video class=muted> — это класс с именем muted, и о таком видео сообщается, а закомментированный <track> или <track> внутри значения атрибута — это текст, а не дорожка. </video> внутри значения ничего не закрывает, и элемент оценивается там, где его закрывает настоящий закрывающий тег, поэтому незакрытый <video> оставлен правилу H025.

Неправильно:

<video controls src="talk.mp4"></video>

Правильно:

<video controls src="talk.mp4">
  <track kind="captions" src="talk.vtt" srclang="en">
</video>

Добавление правил

Мы будем рады пул-реквестам с новыми правилами!

Хорошее правило включает

  • Имя
  • Код
  • Сообщение: сообщение, которое выводится при обнаружении ошибки.
  • Флаги: флаги регулярного выражения, например re.I|re.M. По умолчанию re.DOTALL.
  • Шаблоны поиска: регулярные выражения, которые находят ошибку.
  • Исключения: необязательный список профилей, к которым правило не применяется.

Пожалуйста, добавьте тест, проверяющий правило.

Пользовательские правила

Чтобы добавить пользовательские правила только для своего проекта, создайте файл .djlint_rules.yaml рядом с pyproject.toml. Правила, добавленные в этот файл, djLint подхватит сам. Файл правил в другом месте можно указать параметром командной строки --rules.

Правила с регулярными выражениями

Можно добавить правила, которые срабатывают, если найдено совпадение с одним из регулярных выражений:

- rule:
    name: T001
    message: Find Trichotillomania
    flags: re.DOTALL|re.I
    patterns:
      - Trichotillomania

Правила в виде модулей Python

Можно добавить правила, которые импортируют и выполняют пользовательскую функцию на Python:

- rule:
    name: T001
    message: Found the 'bad' word
    python_module: your_package.your_module

Указанный python_module должен содержать функцию run(), которая будет выполняться для каждого проверяемого файла. Она должна принимать следующие аргументы:

  • rule: словарь, представляющий ваше правило в .djlint_rules.yaml. Обычно через эту переменную получают имя и сообщение правила.
  • config: объект конфигурации DJLint.
  • html: всё html-содержимое файла.
  • filepath: путь к файлу, который проверяется в данный момент.
  • line_ends: список позиций символов start и end для каждой строки, которые можно передать в djlint.lint.get_line(), чтобы получить номер строки по позиции символа. См. пример.
  • *args, **kwargs: в будущем могут добавиться другие аргументы, поэтому укажите и эти два, чтобы снизить риск сбоя при обновлении djLint.

Модуль может находиться в корне проекта. Если он вернёт line или end, которые не являются позицией, djLint остановится с ошибкой, в которой указано имя правила.

Функция должна возвращать список словарей, по одному на каждую ошибку, со следующими ключами:

  • code: код правила, сообщающего об ошибке (обычно rule['name'])
  • line: номер строки и номер символа в этой строке, разделённые ‘:’, в виде строки.
    Например, "2:3" означает, что ошибка найдена в строке 2, символ 3
  • end: необязательный. Где заканчивается ошибка, в той же записи, что и line: позиция сразу за её последним символом. Без него ошибка заканчивается там же, где match.
  • match: часть содержимого, в которой находится ошибка
  • message: сообщение, которое будет выведено для обозначения ошибки (обычно rule['message'])
from __future__ import annotations

import re
from typing import TYPE_CHECKING

from djlint.helpers import inside_ignored_linter_block, inside_ignored_rule
from djlint.lint import get_line

if TYPE_CHECKING:
    from typing import Any

    from djlint.settings import Config
    from djlint.types import LintError


def run(
    rule: dict[str, Any],
    config: Config,
    html: str,
    filepath: str,
    line_ends: list[dict[str, int]],
    *args: Any,
    **kwargs: Any,
) -> list[LintError]:
    """Rule that fails if the html file contains 'bad'.

    This is just an example, in reality it's much simpler to do that with a
    "pattern rule".
    """
    return [
        {
            "code": rule["name"],
            "line": get_line(match.start(), line_ends),
            "end": get_line(match.end(), line_ends),
            "match": match.group().strip()[:20],
            "message": rule["message"],
        }
        for match in re.finditer(r"bad", html)
        if not inside_ignored_rule(config, html, match, rule["name"])
        and not inside_ignored_linter_block(config, html, match)
    ]