- D: Django 专用
- H: HTML 适用
- J: Jinja 专用
- M: Handlebars 专用
- N: Nunjucks 专用
- T: 各模板语言通用
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 | <!DOCTYPE ... > 应在 HTML 标签之前。 |
✔️ |
| H008 | 属性应使用双引号。 | ✔️ |
| H009 | 标签名应小写。 | ✔️ |
| H010 | 属性名应小写。 | ✔️ |
| H011 | 属性值应加引号。 | ✔️ |
| H012 | 属性 = 周围不应有空格。 |
✔️ |
| H013 | img 标签应有 alt 属性。 |
✔️ |
| H014 | 空行多于配置保留的数量。 | ✔️ |
| H015 | h 标签后换行。 |
✔️ |
| H016 | HTML 中可缺少 title 标签。 |
✔️ |
| H017 | 空标签应为自闭合标签(与 H018 冲突)。 | - |
| D018 | (Django) 内部链接应使用 {% url ... %} 形式。 |
✔️ |
| H018 | 空标签本质上是自闭合的,必须以“>”结尾,而非“/>”(与 H017 冲突)。 | - |
| J018 | (Jinja) 内部链接应使用 {% url ... %} 形式。 |
✔️ |
| H019 | 将 javascript:abc() 替换为 on_ 事件和实际 URL。 |
✔️ |
| H020 | 发现空标签对时考虑移除。 | ✔️ |
| H021 | 应避免使用内联样式。 | ✔️ |
| H022 | 外部链接应使用 HTTPS。 | ✔️ |
| H023 | 不使用实体引用。 | ✔️ |
| H024 | 脚本和样式标签中可省略 type 属性。 |
✔️ |
| H025 | 标签可孤立存在。 | ✔️ |
| H026 | 空的 id 和 class 标签应被移除。 | ✔️ |
| T027 | 模板语法中发现未闭合的字符串。. | ✔️ |
| T028 | 建议在属性值中使用无空格标签,例如:{%- if/for -%} |
- |
| H029 | 建议使用小写表单方法值。 | ✔️ |
| H030 | 建议添加 meta 描述。 | ✔️ |
| T032 | 模板标签中发现多余空格。 | ✔️ |
| H033 | 表单 action 发现多余空格。 | ✔️ |
| T034 | 使用 {% … %} 代替 {% … }%? 。 | ✔️ |
| H036 | 避免使用 <br> 标签。 |
✔️ |
| H037 | 发现重复属性。 | ✔️ |
| T038 | 块标签没有匹配的结束标签。 | ✔️ |
| T039 | 发现未闭合的模板标签。 | ✔️ |
| T040 | extends 或 include 标签中缺少模板名或模板名为空。 | ✔️ |
| H041 | 标签在与打开它不同的模板块中关闭。 | ✔️ |
| T041 | extends 标签应是模板中的第一个标签。 | ✔️ |
| H042 | label 的 for 属性在此文件中没有匹配的元素 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 | 该 role 不在 ARIA 为标记定义的角色之列。 | ✔️ |
| H052 | meta refresh 不应按计时重新加载或跳转页面。 | ✔️ |
| H053 | id 在文件中被使用了不止一次。 | ✔️ |
| H054 | 交互元素不应嵌套在另一个交互元素之内。 | ✔️ |
| H055 | lang 属性应是语言标签,例如 en 或 pt-BR。 | ✔️ |
| H056 | src 不应为空。 | ✔️ |
| H057 | video 应有字幕轨道。 | ✔️ |
规则编码的第一个字母遵循如下规则。
变量外包含空格。示例:{{ this }}
像 {{user.name}} 这样内部不加空格的模板语法更难阅读和比对差异,而且整个代码库中空格风格不一致会让基于 grep 的重构(搜索某个变量或标签)变得不可靠,因为同一个表达式存在多种写法。Django 和 Jinja 的风格指南都采用带单个空格的 {{ var }} 和 {% tag %} 写法。
不适用于 handlebars 和 golang 配置文件。
错误示例:
{{user.name}}
正确示例:
{{ user.name }}
标签中应使用双引号。示例:{% extends "this.html" %}
在模板标签({% extends %}、{% include %}、{% with %}、{% trans %}、{% now %})中混用单引号和双引号,会让同一个模板名出现两种写法,导致搜索和批量重命名漏掉一半。统一使用双引号还能让标签参数与文件中其余 HTML 属性的引号风格保持一致。
HTML 属性值内部的单引号(例如 <span title="{% trans 'x' %}">)不会被标记,因为属性本身的双引号迫使那里只能使用单引号。
设置 quote_style = "single" 后,该规则反过来要求单引号,格式化器也会写成单引号。
--reformat 会替你改写这些引号,因此报告从不需要手工修改。
错误示例:
{% extends 'base.html' %}
正确示例:
{% extends "base.html" %}
Endblock 应包含名称。示例:{% endblock body %}.
当 {% block %} 跨越多行或存在嵌套时,不带名称的 {% endblock %} 无法说明它闭合的是哪个块,编辑时很容易结束错误的块,子模板随之覆盖错误的内容。为 endblock 命名可以标明配对关系,让 djLint 和 Django(endblock 名称不匹配时会抛出 TemplateSyntaxError)都能发现闭合位置错误的块。配对错误(未闭合的块、孤立的 endblock 以及名称不匹配)属于正确性检查,由 T038 负责。
默认禁用;使用 --include=T003 启用。--name-endblocks 会替你写入名称,因此报告从不需要手工修改。
当块在同一行内开始并结束时不要求命名,例如 {% block title %}``{% endblock %}。
错误示例:
{% block content %}
<p>hello</p>
{% endblock %}
正确示例:
{% block content %}
<p>hello</p>
{% endblock content %}
(Django) 静态 URL 应遵循 {% static path/to/file %} 形式。
硬编码 /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' %}">
(Jinja) 静态 URL 应遵循 {{ url_for('static'..) }} 形式。
硬编码 /static/ 路径会绕过 Flask/Jinja 的 url_for(‘static’, …),当应用挂载在某个 URL 前缀下、或静态目录/主机发生变化时,资源就会 404,框架附加的缓存清除(cache-busting)查询串也会丢失。该规则只查找字面上的 /static/ 前缀,从其他路径提供静态文件的项目不在其覆盖范围内。
错误示例:
<link rel="stylesheet" href="/static/css/style.css">
正确示例:
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
HTML 标签应包含非空的 lang 属性。
如果 <html> 上没有 lang 属性,屏幕阅读器只能猜测发音规则,可能用错误的语言朗读页面,浏览器也无法正确提供翻译、断字或符合区域习惯的引号。声明页面语言是 WCAG 2.1 成功标准 3.1.1(A 级)的要求。
lang="" 和不带值的 lang 都表示语言未知,因此与缺少该属性一样会被报告。
错误示例:
<!DOCTYPE html>
<html>
</html>
正确示例:
<!DOCTYPE html>
<html lang="en">
</html>
img 标签应包含 height 和 width 属性。
默认禁用;使用 --include=H006 启用。
当 <img> 没有 width 和 height 时,浏览器无法在图片下载前预留空间,图片加载过程中周围内容会随之跳动。这种布局偏移会恶化 Cumulative Layout Shift(Core Web Vitals 指标之一),还可能让用户在页面稳定前误点。
错误示例:
<img src="cat.png" alt="Cat">
正确示例:
<img src="cat.png" alt="Cat" width="120" height="80">
<!DOCTYPE ... > 应在 HTML 标签之前。
如果 <html> 标签之前没有 <!DOCTYPE>,浏览器会以怪异模式(quirks mode)渲染页面,模拟遗留的盒模型和布局行为,导致 CSS 在不同浏览器中渲染不一致。位于 doctype 之前的模板标签和注释没有问题;只要求 <html> 标签本身位于其后。
错误示例:
<html lang="en">
</html>
正确示例:
<!DOCTYPE html>
<html lang="en">
</html>
属性应使用双引号。
混用引号风格让属性值更难阅读和检索,而且内容一旦含有撇号,单引号包裹的值就会出错。双引号是 HTML 规范、格式化工具和多数风格指南采用的惯例,统一使用双引号能让模板与更广泛的生态保持一致。
错误示例:
<div class='content'></div>
正确示例:
<div class="content"></div>
标签名应小写。
HTML 解析器接受大写标签名,但 XHTML 和 XML 序列化是大小写敏感的,会拒绝它们;大小写混用还会让文本搜索和 diff 评审变得不可靠(grep <h1> 会漏掉 <H1>)。小写标签名让模板保持可移植且风格一致。
错误示例:
<H1>Welcome</H1>
正确示例:
<h1>Welcome</h1>
属性名应小写。
大写属性名在 XHTML/XML 序列化中是非法的,还会让跨模板的文本搜索失效(grep src= 会漏掉 SRC=)。DOM 本来就会把 HTML 属性名规范化为小写,因此大写写法只带来不一致,没有任何好处。
错误示例:
<img SRC="cat.png" alt="Cat" width="120" height="80">
正确示例:
<img src="cat.png" alt="Cat" width="120" height="80">
属性值应加引号。
不加引号的属性值到第一个空白字符就结束了,因此像 class=btn primary 这样的值会悄悄丢掉空格之后的所有内容(浏览器把 “primary” 当作一个独立的布尔属性)。来自模板变量的值尤其脆弱:渲染出的任何空格、“=” 或 “>” 都会破坏标签。加引号让值的边界明确而安全。
错误示例:
<div class=test></div>
正确示例:
<div class="test"></div>
属性 = 周围不应有空格。
在 “=” 周围加空格后,标签会被读成三个独立的部分,而且离出错只有一步之遥:中间的一次换行或截断就会留下一个孤立的布尔属性加上零散文本。让 name=“value” 保持连续也是简单文本工具(grep、查找替换)的默认假设,混杂的空格会让属性难以可靠地查找和重构。
错误示例:
<div class = "test"></div>
正确示例:
<div class="test"></div>
img 标签应有 alt 属性。
没有 alt 属性时,屏幕阅读器会读出图片的文件名,或者什么都不读,违反 WCAG 1.1.1(非文本内容)。alt 文本也是图片加载失败时用户看到的内容。装饰性图片应显式写上空的 alt=“”,让辅助技术知道跳过它们,这样写同样满足此规则。
不带值的 alt 等同于 alt="",即装饰性图片的情形,因此会被接受。
错误示例:
<img src="cat.jpg" height="200" width="300">
正确示例:
<img src="cat.jpg" height="200" width="300" alt="A sleeping cat">
存在超过两行的空行。
连续的空行对渲染后的页面没有任何影响(HTML 会折叠空白),却让模板变得臃肿,并在相邻行改动时产生嘈杂的 diff。djLint 的格式化工具默认会将其完全移除(最多保留 max_blank_lines 个空行,默认为 0),因此残留的连续空行说明代码未经格式化。
错误示例:
<div>one</div>
<p>two</p>
正确示例:
<div>one</div>
<p>two</p>
h 标签后换行。
标题是定义文档大纲的块级地标;把下一个元素挤在闭合的 h 标签同一行会在源码中掩盖这种结构,还会让对其中任一元素的编辑在 diff 中表现为对两者的改动。每个标题后换行能让模板的视觉结构与渲染后的大纲保持一致。
错误示例:
<h1>Heading</h1><p>Intro text.</p>
正确示例:
<h1>Heading</h1>
<p>Intro text.</p>
HTML 中可缺少 title 标签。
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>
空标签应为自闭合标签(与 H018 冲突)。
必须同时能按 XML/XHTML 解析(或供 XML 类工具消费)的模板会拒绝不带闭合斜杠的空元素,而在代码库中混用 <br> 和 <br /> 会产生不一致的 diff。该规则强制采用 XHTML 风格的约定,让所有空元素以同一种方式闭合。
默认禁用;使用 --include=H017 启用。与 H018 互斥,两种约定只能启用其一。
错误示例:
<br>
<meta charset="utf-8">
正确示例:
<br />
<meta charset="utf-8" />
(Django) 内部链接应使用 {% url ... %} 形式。
硬编码的内部 URL 会在 urls.py 中某个路由的路径变更时悄然失效,产生失效链接和提交到不存在地址的表单 action,而针对 URLconf 的任何测试都发现不了这一点。{% url %} 通过路由名称解析路径,因此重命名路径时所有链接会一并更新。
错误示例:
<a href="/accounts/login">Login</a>
正确示例:
<a href="{% url 'login' %}">Login</a>
空标签本质上是自闭合的,必须以“>”结尾,而非“/>”(与 H017 冲突)。
在 HTML 现行标准中,空元素末尾的斜杠没有任何含义(解析器会忽略它),因此写 <br /> 暗示了 HTML 并不具备的 XML 式自闭合行为,还可能误导读者给非空元素也加上斜杠,而那里多余的 / 会被悄悄丢弃,掩盖未闭合标签的 bug。该规则强制空元素以纯 > 结尾。
默认禁用;使用 --include=H018 启用。与 H017 互斥,两种约定只能启用其一。SVG 的 <path /> 不受约束,因为 SVG 是 XML,必须带斜杠。
错误示例:
<br />
<meta charset="utf-8" />
正确示例:
<br>
<meta charset="utf-8">
(Jinja) 内部链接应使用 {% url ... %} 形式。
硬编码的内部 URL 会在路由路径变更或应用挂载在前缀下时悄然失效,留下死链接和提交到 404 的表单 action。url_for() 根据端点名称构建 URL,因此路由变更会自动同步到所有模板。
错误示例:
<a href="/accounts/login">Login</a>
正确示例:
<a href="{{ url_for('login') }}">Login</a>
将 javascript:abc() 替换为 on_ 事件和实际 URL。
javascript: URL 会破坏鼠标中键点击和在新标签页打开,在 JavaScript 被禁用或加载失败时毫无作用,会被严格的内容安全策略(CSP)拦截,还是经典的 XSS 注入入口。href 应使用真实 URL,并改用事件处理器来附加行为。在严格 CSP 下,内联的 on* 处理器同样会被拦截:示例中的 onclick 只是模板内的最小修复;更好的做法是在脚本文件中用 addEventListener 绑定监听器。
错误示例:
<a href="javascript:openPopup()">Open popup</a>
正确示例:
<a href="{% url 'popup' %}" onclick="openPopup(event)">Open popup</a>
发现空标签对时考虑移除。
空标签对不渲染任何内容,却仍会创建一个 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>
应避免使用内联样式。
内联样式的优先级高于任何样式表选择器,之后想覆盖它就得用 !important;在 style-src 不含 ‘unsafe-inline’ 的内容安全策略下它们会被拦截;它们还把表现层散落在各个模板中,换主题或改设计意味着修改标记而非一份样式表。应把声明移到 CSS 类里。一个合理的例外:HTML 邮件模板(许多邮件客户端会剥离 <style> 块,内联样式是那里的标准做法),请排除你的邮件模板目录,或为其禁用此规则。
错误示例:
<div style="color: red;">Wrong username or password.</div>
正确示例:
<div class="error">Wrong username or password.</div>
外部链接应使用 HTTPS。
在通过 HTTPS 提供的页面上,纯 http:// 子资源属于混合内容:浏览器会直接拦截脚本、样式表和 iframe,并对图片自动升级或发出警告。指向 http:// 页面的 <a> 链接虽不算混合内容,但仍会把访问者送上易被窃听和篡改的未加密连接。指向确实没有 TLS 的内部主机的引用也会被标记;请用 {# djlint:off H022 #} 块局部静默这些位置,而不是禁用整条规则。
错误示例:
<a href="http://example.com">Example</a>
正确示例:
<a href="https://example.com">Example</a>
不使用实体引用。
HTML5 文档采用 UTF-8,因此字面字符在任何地方都可用,也是评审者真正读到的内容;实体引用中的笔误(例如 &mdsah;)浏览器不会报错,而是原样渲染成一段乱码。djLint 允许承载语法的实体(<、>、&、"、',以及大括号 {、} 和 %、#、$,它们若按字面书写会拼成模板分隔符),以及指代不可见字符、因而无法以字面形式审阅的实体:各类空格( 、 、 )、连接符与方向标记(‌、‍、‎、‏)和 ­,具名、十进制与十六进制形式皆可。
--reformat 会替你把实体改写成字符,因此报告从不需要手工修改。写在模板标签内部的实体属于标签而非页面,规则和格式化器都不会改动它。
错误示例:
<p>Dates 1900 — 2000</p>
正确示例:
<p>Dates 1900 — 2000</p>
脚本和样式标签中可省略 type 属性。
text/javascript 和 text/css 是 HTML5 中 <script> 和 <style> 的默认值,这个属性只是浏览器会忽略的累赘,WHATWG 规范明确建议省略它。去掉它还能避免过时的 MIME 字符串在被复制到模块脚本(type=“module” 真正起作用的地方)时破坏该元素。
错误示例:
<script type="text/javascript" src="app.js">
正确示例:
<script src="app.js"></script>
标签可孤立存在。
缺少对应开始或闭合标签的标签会迫使浏览器的容错机制去猜测元素在哪里结束,后续标记就会被吞进错误的元素。布局、CSS 选择器和 JavaScript DOM 查询随之悄悄出错,而且在不同浏览器中表现各异。H025 还会报告在 <p> 内打开的 <ol> 或 <ul>:HTML 解析器会在列表之前先闭合段落,标记永远不会按书写的方式嵌套。
错误示例:
<div>
<p>Hello</p>
正确示例:
<div>
<p>Hello</p>
</div>
空的 id 和 class 标签应被移除。
类选择器和 id 选择器都不会匹配空属性,而且空的 id 是非法 HTML(id 值不得为空字符串)。但属性存在选择器(如 div[class])仍会匹配它,因此删除它对这样写的样式表是可见的。它通常预示着一个模板 bug:本该在此插入某个变量,因此移除或补全它可以避免这个 bug 一直藏在眼皮底下。
错误示例:
<div id="" class="">content</div>
正确示例:
<div>content</div>
模板语法中发现未闭合的字符串。.
在 {% ... %} 或 {{ ... }} 中打开却未闭合的引号会让模板引擎错误解析该标签:Django 和 Jinja 要么在渲染时抛出 TemplateSyntaxError,要么悄悄把标签剩余的参数当作字符串内容吞掉,结果页面报 500,或在缺失参数的情况下渲染。
错误示例:
{% trans "Welcome %}
正确示例:
{% trans "Welcome" %}
建议在属性值中使用无空格标签,例如:{%- if/for -%}
默认禁用;使用 --include=T028 启用。
无空格标签去掉的空白是会显示出来的空白,因此只在属性没有空白可失时才这样写。alt="{%- if brand -%}Acme{%- endif -%} logo" 会渲染成 Acmelogo,而 svg 的 d="M12 {%- if big -%}20{%- endif -%} 4Z" 会变成另一条路径。这也是该规则默认关闭的原因。
属性值内部的模板标签会把标签周围的空白和换行原样输出到渲染后的属性中,因此用普通的 {% if %}/{% for %} 标签拼出来的 href 或 src 可能包含多余空格,产生损坏的 URL。Jinja/Nunjucks 的空白控制标签({%- ... -%})会去掉这些周围空白,让属性渲染为一个干净的值。class 属性不受此规则约束,因为类名之间的多余空白无伤大雅。
不适用于 django 配置文件:Django 模板标签不支持 {%- -%} 空白控制。
错误示例:
<a href="{% if x %}/home{% endif %}"></a>
正确示例:
<a href="{%- if x -%}/home{%- endif -%}"></a>
建议使用小写表单方法值。
HTML 规范将表单 method 的关键字定义为小写(get、post);浏览器只是通过大小写不敏感的回退匹配才接受大写变体。保持规范的小写形式让模板一致、便于 grep,也避免严格的校验器和基于 XHTML 的工具链报错。
错误示例:
<form method="POST"></form>
正确示例:
<form method="post"></form>
建议添加 meta 描述。
搜索引擎用 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>
模板标签中发现多余空格。
模板标签参数之间连续的空格或制表符是看不见的噪音:它们会在 diff 中掩盖真正的差异,让人难以发现缺失的参数,还会偏离 djLint 格式化工具输出的单空格风格,造成无谓的重复格式化。引号字符串内部的空白会被保留,不会被标记。
错误示例:
{% static 'css/style.css' %}
正确示例:
{% static 'css/style.css' %}
表单 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>
使用 {% ... %} 代替 {% ... }%? 。
}% 几乎总是 %} 的笔误。模板引擎不会把 }% 识别为标签定界符,因此该标签根本不会被解析:原始的 {% … }% 文本会泄漏到渲染后的 HTML 中,或者引擎在遇到这个未闭合的标签时抛出语法错误。
错误示例:
{% include "footer.html" }%
正确示例:
{% include "footer.html" %}
不要用 br 标签制造间距。
html 规范只允许把 <br> 用于内容本身自带的换行,例如邮政地址或诗歌,这类用法不会被标记。被标记的是规范排除的表现性用法:连续两个及以上的换行,也就是垂直间距;以及紧贴块级元素内边缘的换行,它渲染出的东西并不比该元素自身的外边距更多。两者都会破坏窄屏下的文本回流,而屏幕阅读器会在无可播报之处播报一次强制换行。
错误示例:
<p>Shipping is free.<br><br>Delivery takes 3 days.</p>
正确示例:
<p>Shipping is free.</p>
<p>Delivery takes 3 days.</p>
发现重复属性。
重复的属性是非法 HTML,浏览器只保留第一次出现的值并悄悄丢弃其余的,于是第二个 class 或 style 值永远不会生效,从而掩盖真正的 bug。该检查是模板感知的:在互斥分支({% if %}/{% else %})中重复出现的属性不会被标记,因为最终只会渲染其中一份。
错误示例:
<div class="card" id="profile" class="active">...</div>
正确示例:
<div class="card active" id="profile">...</div>
块标签没有匹配的结束标签。
像 {% if %}、{% for %} 或 {% macro %} 这样的块标签如果缺少对应的结束标签,在 Django 和 Jinja 中会直接导致 TemplateSyntaxError:页面在请求时无法渲染,而该规则能在部署前发现这一问题。它还会标记没有对应开始标签的孤立结束标签,以及嵌套交错错误的块(例如 {% 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 %}
发现未闭合的模板标签。
以 {{ 或 {% 开头、却没有用对应的 }} 或 %} 闭合的模板标签不会被当作标签解析:Django/Jinja 要么抛出 TemplateSyntaxError,要么把原始的大括号字符渲染进页面,而直到下一个定界符之前的内容都可能被悄悄吞掉。这类笔误(少写一个大括号、定界符不匹配)在代码评审中很容易被忽略,因为模板可能仍能部分渲染。
错误示例:
<p>{{ user.name }</p>
正确示例:
<p>{{ user.name }}</p>
extends 或 include 标签中缺少模板名或模板名为空。
{% extends %} 或 {% include %} 标签的模板名缺失、为空或只含空白时,就没有任何可加载的内容:名称完全缺失时 Django 会抛出 TemplateSyntaxError,名称为空时会在渲染时抛出 TemplateDoesNotExist,因此即使模板文件本身看起来语法上说得过去,页面也会在生产环境中报 500。
错误示例:
{% extends "" %}
正确示例:
{% extends "base.html" %}
标签在与打开它不同的模板块中关闭。
当一个 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 %}
extends 标签应是模板中的第一个标签。
Django 拒绝编译 {% extends %} 之前还有其他标签的模板,而写在它之前的文本会被渲染,于是这些文本会先于父模板产出的一切泄漏到页面中。Jinja 同样会渲染这段文本,nunjucks 则会将其丢弃,因此无论在哪种引擎中,模板的行为都与它看上去的不一样。
{# #} 注释不渲染任何内容,不算在内;djLint 不检查的块内的任何内容同样不算,例如 {% comment %}、{% raw %} 或 {% verbatim %} 块,或 {# djlint:off #} 区域。只检查第一个 {% extends %};第二个则是另一个独立的错误。 它之前的分支标签同样不算在内,因为 jinja 将 {% if x %}{% extends "a.html" %}{% else %}{% extends "b.html" %}{% endif %} 记载为选择父模板的方式。
不适用于 handlebars、golang、liquid 和 angular 配置文件。
错误示例:
{% load static %}
{% extends "base.html" %}
正确示例:
{% extends "base.html" %}
{% load static %}
label 的 for 属性在此文件中没有匹配的元素 id。
该检查只在可以可靠分析的文件上运行:如果文件中包含任何可能渲染出本文件看不到的 id 的内容(如表单控件等 {{ ... }} 输出、{% include %} 或 {% extends %}、或无法识别的模板标签),该规则对此文件保持沉默。凡是它运行到的地方,报告都是真实的关联断裂。
错误示例:
<label for="email">Email</label>
<input id="username">
正确示例:
<label for="email">Email</label>
<input id="email">
继承其他模板的模板中,块外的内容不会被渲染。
模板一旦继承了另一个模板,输出什么就由父模板决定,子模板只负责填充父模板的块。写在 {% extends %} 之后、又不在任何 {% block %} 之内的文本或 html 会在渲染时被悄悄丢弃,因此源码中看起来没问题的一个段落永远不会出现在页面上。
放在那里的模板标签仍会执行,因此 {% load %}、{% set %} 以及包在块外面的 {% if %} 会被跳过;{# #} 和 {% comment %} 注释、{% raw %} 和 {% verbatim %} 块,以及 {% macro %} 或块形式 {% set %} 的主体也一样,后者的内容是被捕获而非输出的。html 注释会像其他文本一样被输出,因此块外的 html 注释会被报告;{% blocktrans %} 中的文本也会,因为它并不是 {% block %}。只考虑 extends 标签之后的内容,每一段连续的这类内容只在其起始处报告一次。
不适用于 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 %}
button 标签应有 type 属性。
没有 type 的 <button> 默认为 submit,因此本意是调用脚本的按钮还会提交外层表单并使页面重新加载。写明类型即可避免。
错误示例:
<form>
<button onclick="preview()">预览</button>
</form>
正确示例:
<form>
<button type="button" onclick="preview()">预览</button>
</form>
块名在模板中被使用了不止一次。
Django、Jinja 和 Nunjucks 都拒绝解析两个块同名的模板,因此页面根本无法加载。这些引擎并不在乎两个块是否位于 {% if %} 的不同分支中,因此每个块名在整个文件中都必须唯一,无论这些块是并列的还是一个嵌套在另一个之中。
只有 {% block %} 算数:{% blocktrans %} 不是块,{% endblock name %} 只是指明它所关闭的块,而写在注释中的块永远到不了解析器。名称按原样比较,因为这些引擎把 Content 和 content 视为两个块。
不适用于 handlebars、golang、liquid 和 angular 配置文件。
错误示例:
{% block content %}{% endblock %}
{% block content %}{% endblock %}
正确示例:
{% block content %}{% endblock %}
{% block sidebar %}{% endblock %}
thead 中不应混用 th 和 td 单元格。
每一行单独判断,因此 html 规范放在 thead 中、与表头行并列的那一行说明性 td 不算混用。行首的空 td 是首列带表头的表格的角单元格,正是 W3C 无障碍教程推荐的写法,会被跳过。
th 与 td 对屏幕阅读器含义不同,通常样式也不一样,因此表头行里混进的一个单元格会被读作数据,并与相邻列显示得不一致。这种混用在 html 中是合法的,也正因如此难以发现。
错误示例:
<thead>
<tr>
<th>名称</th>
<td>大小</td>
</tr>
</thead>
正确示例:
<thead>
<tr>
<th>名称</th>
<th>大小</th>
</tr>
</thead>
输出标签中含有语句关键字;应改用块标签。
不适用于 golang、handlebars 和 angular 配置文件。
输出标签用于打印一个值,而 if、for、url、include 等都是语句,属于块标签。Django、Jinja 和 Nunjucks 都会以语法错误拒绝 {{ if x }};单独出现的结束关键字,例如 {{ endif }},会被读作一个什么都不渲染的变量,而它本应关闭的块仍然开着,于是页面要么无法加载,要么显示出条件本应隐藏的内容。
单独的关键字是普通的变量名,因此 {{ url }}、{{ url|default:"/" }} 和 {{ set.name }} 不会被报告。只有后面跟着参数的关键字才会,以及单独出现的结束或分支关键字,例如 {{ endif }} 或 {{ else }}。碰巧以这些名称开头的 jinja 表达式,例如 {{ url ~ "/x" }} 或 {{ url if url else "#" }},会被跳过。
错误示例:
{{ if user.is_active }}
正确示例:
{% if user.is_active %}
iframe 标签应有 title 属性。
屏幕阅读器通过可访问名称播报 iframe。没有名称时,它会读出框架的 url,或者什么都不读,用户在进入之前无从判断嵌入的是哪个页面。WCAG 将其归入 4.1.2「名称、角色、值」,axe 与 html-validate 也默认包含这项检查。
名称可以来自 title、aria-label 或 aria-labelledby,三者有其一即可。由模板标签写入的名称同样算数,因此逐页命名的框架不会被报告。
错误示例:
<iframe src="/report/"></iframe>
正确示例:
<iframe src="/report/" title="季度报告"></iframe>
html 注释中的模板标签仍会执行;应使用模板注释来禁用它。
html 注释只对浏览器隐藏标记,对模板引擎则不然。在 Django、Jinja、Nunjucks、Handlebars 和 Go 中都一样,<!-- {% include "debug.html" %} --> 仍会渲染该文件,<!-- {% if debug %}...{% endif %} --> 仍会求值,因此以这种方式注释掉的标签会继续执行,它写出的任何内容都会落在注释之内,或者,如果其中含有 -->,就会冲出注释。只有模板注释,即 Django 和 Jinja 中的 {# #}、Handlebars 中的 {{! }} 或 Go 中的 {{/* */}},才能阻止标签执行。
只有语句标签会被报告:{% %}、handlebars 的区块、闭合或局部标签,以及 Go 的关键字,例如 {{if}} 或 {{end}}。打印进注释中的值,例如 <!-- built {{ version }} -->,是有意为之的用法,会被跳过;模板注释或 {% comment %} 块中的标签同样如此,面向 Internet Explorer 的条件注释 <!--[if IE]> ... <![endif]--> 也一样,其主体是写给它所指名的浏览器的标记。
错误示例:
<!-- {% include "banner.html" %} -->
正确示例:
{# {% include "banner.html" %} #}
tabindex 不应为正数。
正的 tabindex 会把元素提到 Tab 顺序最前面,排在所有没有该属性的元素之前。只要有一个,键盘用户看到的整页顺序就被重排,此后每个新增控件的模板都得手工维护这个顺序。WCAG 在 2.4.3「焦点顺序」中讨论了这一点。
0 让元素按文档中的位置进入 Tab 顺序,-1 把它移出顺序但仍可由脚本聚焦,两者都不会被报告;由模板标签写入的值也不会,因为这里并不知道它的数值。
错误示例:
<input tabindex="1">
正确示例:
<input tabindex="0">
不应在可获得焦点的元素上设置 aria-hidden。
aria-hidden="true" 把元素移出可访问性树,却仍留在 Tab 顺序里,于是键盘用户依旧会停在它上面,而屏幕阅读器什么也不播报。隐藏装饰性图标是该属性的常规用法,不会被报告;只有本身可获得焦点的元素才会。
以下元素被视为可获得焦点:button、select、textarea、iframe、summary,带 href 的 a 或 area,非隐藏的 input,带 controls 的 audio 或 video,以及任何带 contenteditable 或 tabindex 大于等于 0 的元素。带 disabled 或 tabindex="-1" 的控件已在 Tab 顺序之外,会被跳过。
错误示例:
<button aria-hidden="true">Close</button>
正确示例:
<button type="button" aria-label="Close"><span aria-hidden="true">x</span></button>
该 aria 属性不是规范定义的属性。
拼错的 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>
viewport 不应禁止页面缩放。
user-scalable=no 以及小于 2 的 maximum-scale 会让页面在手机上无法放大,而放大往往是许多人能够阅读它的唯一方式。WCAG 在 1.4.4「调整文本大小」中要求支持 200%。浏览器越来越倾向于忽略该限制,但在仍然遵守它的地方,这个标签依旧关闭了缩放。
无论这两个属性以何种顺序书写,名称与内容都会被找到。
错误示例:
<meta name="viewport" content="width=device-width, user-scalable=no">
正确示例:
<meta name="viewport" content="width=device-width, initial-scale=1">
元素已废弃,应予替换。
<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>
该 role 不在 ARIA 为标记定义的角色之列。
任何规范都未命名的角色会被直接丢弃,元素仍保持它原有的含义: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>
meta refresh 不应按计时重新加载或跳转页面。
定时刷新会在读者眼前把页面换掉。阅读较慢的人、使用屏幕阅读器的人,或者只是被打断了的人,都会毫无预警地失去自己的位置,也无法阻止,这不符合 WCAG 2.2.1「可调节时间」;而刷新同一页面还会丢弃已经填入其中的内容。
延时为零是即时跳转而非计时器,在无法使用服务器跳转之处 WCAG 允许这样做,因此不会被报告。无论这两个属性以何种顺序书写,都能被找到。
错误示例:
<meta http-equiv="refresh" content="30">
正确示例:
<meta http-equiv="refresh" content="0; url=/next-page">
id 在文件中被使用了不止一次。
一个 id 指名一个元素。带有相同 id 的第二个元素会破坏 getElementById、<label for>、片段链接和 aria-labelledby:浏览器取第一个并悄悄忽略其余的,于是 label、链接或脚本会毫无警告地落在错误的元素上。
同一个 {% if %}...{% else %}...{% endif %} 的互斥分支中的两个 id 永远不会同时渲染,因此不会被报告。{% for %} 循环或 {% block %} 不是分支:其中的一个 id 与其外面的同一个 id 都会渲染,靠后的那个会被报告。由模板标签写入的值无从得知,会被跳过;空值也一样。id 按原样精确比较,与浏览器一致,因此 save 和 Save 是两个 id。
错误示例:
<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>
交互元素不应嵌套在另一个交互元素之内。
html 禁止在 <a> 和 <button> 内放置交互内容。链接里的按钮或按钮里的链接都是无效标记,各个浏览器会以各自的方式修复它,而屏幕阅读器或键盘用户拿到的是一个表现得像两个的控件。axe 以“nested-interactive”报告同样的问题。
所监视的容器是带 href 的 <a> 和 <button>,在其中会被报告的控件是带 href 的链接、button、input、select 和 textarea。不带 href 的 <a> 不是交互元素,无论作为哪一方都会被跳过;隐藏的 input 或类型由模板标签写入的 input 同样如此。未闭合的链接或按钮会随包围它的元素一起结束,与浏览器中的行为一致,因此一处笔误不会让文件的其余部分都被报告。
错误示例:
<a href="/cart"><button>Add</button></a>
正确示例:
<a href="/cart" class="button">Add</a>
lang 属性应是语言标签,例如 en 或 pt-BR。
H005 要求 <html> 上有 lang,但像 lang="english" 或 lang="en_US" 这样的值虽能满足它,却没有指出任何浏览器认识的语言。屏幕阅读器于是退回到默认语音,翻译和断字则会选错规则或者根本不选。该值必须是 BCP 47 标签:两个或三个字母,之后是任意数量、各由一到八个字母或数字组成的子标签,每个子标签前有一个连字符,例如 en、pt-BR 或 zh-Hant-TW。
只检查 <html> 标签,与 H005 一致,空值交由该规则处理。由模板标签写入的值,例如 lang="{{ LANGUAGE_CODE }}",无从得知,会被跳过;xml:lang 或 data-lang 不会被当作 lang 读取。
错误示例:
<html lang="english">
正确示例:
<html lang="en">
src 不应为空。
html 规范指出空的 src 是无效的,并警告浏览器会以文档自身的 url 来解析它,因此 <img src=""> 会把页面再当作图片取一次,<script src=""></script> 会把它当作脚本取一次。它通常是一个本该由脚本填入的占位符,修复方法是去掉该属性,或者把值放在 data 属性中,直到有真正的值为止。完全不带值的 src,例如 <img src>,同样是空的,会被报告。
只检查 img、script、iframe、embed、source、track、audio 和 video,因为只有这些元素会去获取 src 所指的资源。由模板标签写入的值会被跳过,只含空白的值也一样;srcset 和 data-src 是不同的属性,该规则不作判断。
错误示例:
<img src="" alt="Logo">
正确示例:
<img src="{% static 'logo.png' %}" alt="Logo">
video 应有字幕轨道。
有声视频的语音只存在于音频中,因此没有字幕时,失聪或听力障碍的观众从中什么也得不到,而 WCAG 1.2.2「字幕(预录)」要求提供字幕。<video> 内 kind 为 captions 或 subtitles 的 <track> 满足该规则,完全不带 kind 的 <track> 也满足,因为 subtitles 是默认值。
muted 的视频没有可加字幕的音频,不会被报告。开始标签或主体中含有模板标签的视频也不会,因为轨道或 muted 属性可能由模板在 djLint 看不到的地方写入。
错误示例:
<video controls src="talk.mp4"></video>
正确示例:
<video controls src="talk.mp4">
<track kind="captions" src="talk.vtt" srclang="en">
</video>
{% endraw %}
欢迎前来 PR 新规则!
优雅的规则应该包含
请包含一个测试样例以验证该规则。
你可以在 pyproject.toml 相同路径下创建一个 .djlint_rules.yaml 来添加自定义规则。
也可以使用 CLI 选项 --rules 指定其他位置的规则文件。
你可以将规则添加到这个文件中,djLint 将会识别并应用这些规则。
您可以添加规则,当其中一个正则表达式模式匹配成功时,该规则将判定为失败:
- rule:
name: T001
message: 发现 Trichotillomania
flags: re.DOTALL|re.I
patterns:
- Trichotillomania
您可以导入并执行自定义的 Python 函数来添加规则:
- rule:
name: T001
message: 发现 `bad` 单词
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 时的失败风险。该函数将返回一个字典列表,每个字典对应一个错误,并包含以下键:
code: 报告错误的规则的代码名称(通常为 rule['name'])。line: 行号和该行的字符号,以字符串形式表示,中间用冒号 : 分隔。例如 "2:3" 表示错误出现在第 2 行第 3 个字符处。match: 包含错误的内容部分。message: 用于提示错误的消息(通常为 rule['message'])。from typing import Any, Dict, List
from djlint.settings import Config
from djlint.lint import get_line
import re
def run(
rule: Dict[str, Any],
config: Config,
html: str,
filepath: str,
line_ends: List[Dict[str, int]],
*args: Any,
**kwargs: Any,
) -> List[Dict[str, str]]:
"""
如果 HTML 文件中包含 'bad',则规则判定为失败。这只是一个示例,
实际上,使用 `形式规则` 来实现会更简单。
"""
errors: List[Dict[str, str]] = []
for match in re.finditer(r"bad", html):
errors.append({
"code": rule["name"],
"line": get_line(match.start(), line_ends),
"match": match.group().strip()[:20],
"message": rule["message"],
})
return errors