Skip to content
Open
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
15 changes: 14 additions & 1 deletion packages/@d-zero/markuplint-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,18 @@ npm install -D @d-zero/markuplint-config

- **`html`要素の`prefix`属性**: Open Graph Protocolのため許可

### 6. Markuplint標準ルールの追加有効化

recommendedプリセットに含まれない以下の標準ルールを有効化しています:

- **`attr-order`**: 属性順序を`id > class > role > aria-* > data-* > 要素固有属性`に統一
- **`no-boolean-attr-value`**: boolean属性の冗長な値を禁止(例: `disabled="disabled"`)
- **`no-default-value`**: デフォルト値と同一の属性値指定を禁止(例: `type="text"`)
- **`no-unsupported-browser-features`**: `browserslist`設定に基づくブラウザ未サポート要素・属性の検出(`browserslist`設定がないプロジェクトでは影響なし)
- **`performance/img-aspect-ratio`**: 無効化。`img[src]`の`width`/`height`必須ルールはビルド時に自動付与されるため不要

なお`head-element-order`(`<head>`内要素の順序)・`no-event-handler-attr`(インラインイベントハンドラ属性の禁止)は`markuplint:recommended-static-html`が拡張する`performance`/`security`プリセットで既定有効のため、このconfigでは重複指定していません。

### 拡張

プロジェクトに合わせて設定を追加します。
Expand Down Expand Up @@ -90,7 +102,8 @@ export default {
}),
// 他の設定
rules: {
'character-reference': false,
'no-malformed-character-reference': false,
'no-unescaped-char': false,
},
};
```
55 changes: 38 additions & 17 deletions packages/@d-zero/markuplint-config/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,40 @@
export default {
extends: ['markuplint:recommended-static-html'],
rules: {
'disallowed-element': {
value: ['br'],
reason:
'br要素は原則使用しません。代わりにCSSでスタイルを調整してください。使用する場合は理由が必要です。(D-ZERO独自ルール)',
// markuplint:recommended-static-html (performance preset) の
// img[src] に対する width, height 必須ルールを無効化
// width, height はビルド時に自動的に付与されるため問題なしとする
'performance/img-aspect-ratio': false,
'attr-order': ['id', 'class', 'role', { group: 'aria' }, { group: 'data' }],
// head-element-order, no-event-handler-attr は
// markuplint:recommended-static-html (performance/security preset) で既定有効のため指定しない
'no-boolean-attr-value': true,
'no-default-value': true,
// browserslist設定がある場合にブラウザ未サポート要素・属性を検出
'no-unsupported-browser-features': true,
'd-zero/no-br': {
rules: {
'no-restricted-element': {
value: ['br'],
reason:
'br要素は原則使用しません。代わりにCSSでスタイルを調整してください。使用する場合は理由が必要です。(D-ZERO独自ルール)',
},
},
},
},
nodeRules: [
{
selector: "script[src^='https://'], script[src^='https://']",
selector: "script[src^='http://'], script[src^='https://']",
rules: {
'required-attr': false,
'require-attr': false,
},
},
{
name: 'd-zero/html-allow-prefix-attr',
selector: 'html',
rules: {
// <html prefix="og: http://ogp.me/ns#">
'invalid-attr': {
'no-unknown-attr': {
options: {
allowAttrs: [
{
Expand All @@ -34,22 +50,22 @@ export default {
},
},
{
name: 'd-zero/img-require-alt',
selector: 'img',
rules: {
// https://github.com/markuplint/markuplint/blob/c35e0beb5e14093a41cee7634221dbe7f7d577f9/packages/%40markuplint/config-presets/src/preset.performance.json#L25-L35 の設定を上書き
// width, height の指定は上書きされるため、省略可能になるが、ビルド時に自動的に付与されるため問題なしとする
'required-attr': {
'require-attr': {
value: 'alt',
reason:
'省略可能なケースがほとんど想定されないため、原則禁止としています。省略する場合は明確な理由が必要です。(D-ZERO独自ルール)',
},
},
},
{
name: 'd-zero/img-src-kebab-case',
selector:
'img:not([src^="data:"], [src^="blob:"], [src^="https://"], [src^="http://"], [src^="//"])',
rules: {
'invalid-attr': {
'no-restricted-attr': {
options: {
disallowAttrs: [
{
Expand All @@ -64,9 +80,10 @@ export default {
},
},
{
name: 'd-zero/media-src-kebab-case',
selector: 'video, audio, source',
rules: {
'invalid-attr': {
'no-restricted-attr': {
options: {
disallowAttrs: [
{
Expand All @@ -85,14 +102,15 @@ export default {
},
},
{
name: 'd-zero/a-href-convention',
selector: 'a',
rules: {
'required-attr': {
'require-attr': {
value: 'href',
reason:
'省略可能なケースがほとんど想定されないため、原則禁止としています。省略する場合は明確な理由が必要です。(D-ZERO独自ルール)',
},
'invalid-attr': {
'no-restricted-attr': {
options: {
disallowAttrs: [
{
Expand All @@ -107,29 +125,32 @@ export default {
},
},
{
name: 'd-zero/button-require-command',
selector: 'button[type=button]:not([role]):not([popovertarget])',
rules: {
'required-attr': {
'require-attr': {
value: 'command',
reason:
'button要素には原則としてcommand属性が必要です。Invoker Commands APIを使用してアクセシブルなUIを実装してください。role属性を持つボタン(role="tab"など)やtype="submit"/type="reset"/typeなしのボタンは例外として許可されます。(D-ZERO独自ルール)',
},
},
},
{
name: 'd-zero/button-prefer-commandfor',
selector: 'button[popovertarget]',
rules: {
'required-attr': {
'require-attr': {
value: 'commandfor',
reason:
'popovertarget属性の代わりにcommandfor属性を使用してください。popovertarget属性は将来的に非推奨となる予定です。(D-ZERO独自ルール)',
},
},
},
{
name: 'd-zero/button-prefer-command-action',
selector: 'button[popovertargetaction]',
rules: {
'required-attr': {
'require-attr': {
value: 'command',
reason:
'popovertargetaction属性(show/hide/toggle)の代わりにcommand属性(show-popover/hide-popover/toggle-popover)を使用してください。(D-ZERO独自ルール)',
Expand Down
6 changes: 3 additions & 3 deletions packages/@d-zero/markuplint-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"access": "public"
},
"engines": {
"node": ">=22.0.0"
"node": ">=24.0.0"
},
"type": "module",
"exports": {
Expand All @@ -22,7 +22,7 @@
"*.js"
],
"dependencies": {
"@markuplint/pug-parser": "4.18.3",
"markuplint": "4.18.3"
"@markuplint/pug-parser": "5.0.0-rc.7",
"markuplint": "5.0.0-rc.7"
}
}
3 changes: 2 additions & 1 deletion packages/@d-zero/markuplint-config/pug.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export default {
overrides: {
[path.resolve(process.cwd(), '**', '*.pug')]: {
rules: {
'character-reference': false,
'no-malformed-character-reference': false,
'no-unescaped-char': false,
},
},
},
Expand Down
72 changes: 59 additions & 13 deletions test/cli.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -139,16 +139,15 @@ describe('markuplint', () => {
expect(violations).toStrictEqual([
'test/fixtures/markuplint/test.pug:14:6 The "c-component__invalid-element-nesting" class name is unmatched with the below patterns: "/^c-component2__[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/", "/^c-(?!component2)[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/", "/^c-component2[a-z0-9]*(?:-[a-z0-9]+)*$/"',
'test/fixtures/markuplint/test.pug:9:4 The "div" element is not allowed in the "span" element in this context',
'test/fixtures/markuplint/test.html:17:66 Illegal characters must escape in character reference',
'test/fixtures/markuplint/test.html:14:18 The "c-component__invalid-element-nesting" class name is unmatched with the below patterns: "/^c-component2__[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/", "/^c-(?!component2)[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/", "/^c-component2[a-z0-9]*(?:-[a-z0-9]+)*$/"',
'test/fixtures/markuplint/test.html:26:3 The "br" element is disallowed',
'test/fixtures/markuplint/test.html:25:12 The "href" attribute is matched with the below disallowed patterns: /^javascript:/i',
'test/fixtures/markuplint/test.html:9:9 The "div" element is not allowed in the "span" element in this context',
'test/fixtures/markuplint/test.html:23:3 Require accessible name',
'test/fixtures/markuplint/test.html:25:3 Require accessible name',
'test/fixtures/markuplint/test.html:1:1 Require the "h1" element',
'test/fixtures/markuplint/test.html:26:3 The "br" element is disallowed',
'test/fixtures/markuplint/test.html:23:3 The "img" element expects the "alt" attribute',
'test/fixtures/markuplint/test.html:24:3 The "a" element expects the "href" attribute',
'test/fixtures/markuplint/test.html:1:1 Require the "h1" element',
'test/fixtures/markuplint/test.html:25:12 The "href" attribute is matched with the below disallowed patterns: /^javascript:/i',
]);
});

Expand Down Expand Up @@ -184,10 +183,11 @@ describe('markuplint', () => {
'packages/@d-zero/markuplint-config/base.js',
);
expect(invalidNaming).toStrictEqual([
'test/fixtures/markuplint/image-naming-test.html:21:15 The "src" attribute is matched with the below disallowed patterns: /[A-Z\\s_]/',
'test/fixtures/markuplint/image-naming-test.html:22:15 The "src" attribute is matched with the below disallowed patterns: /[A-Z\\s_]/',
'test/fixtures/markuplint/image-naming-test.html:23:15 The "src" attribute is matched with the below disallowed patterns: /[A-Z\\s_]/',
'test/fixtures/markuplint/image-naming-test.html:24:15 The "src" attribute is matched with the below disallowed patterns: /[A-Z\\s_]/',
'test/fixtures/markuplint/image-naming-test.html:21:45 It includes unexpected characters (https://html.spec.whatwg.org/multipage/urls-and-fetching.html#valid-non-empty-url-potentially-surrounded-by-spaces)',
'test/fixtures/markuplint/image-naming-test.html:21:45 The "src" attribute is matched with the below disallowed patterns: /[A-Z\\s_]/',
'test/fixtures/markuplint/image-naming-test.html:22:39 The "src" attribute is matched with the below disallowed patterns: /[A-Z\\s_]/',
'test/fixtures/markuplint/image-naming-test.html:23:44 The "src" attribute is matched with the below disallowed patterns: /[A-Z\\s_]/',
'test/fixtures/markuplint/image-naming-test.html:24:41 The "src" attribute is matched with the below disallowed patterns: /[A-Z\\s_]/',
]);

const validNaming = await markuplint(
Expand All @@ -203,23 +203,69 @@ describe('markuplint', () => {
'packages/@d-zero/markuplint-config/base.js',
);
expect(violations).toStrictEqual([
'test/fixtures/markuplint/button-command.html:27:17 The "btn" class name is unmatched with the below patterns: "/^c-(?<ComponentName>[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/"',
'test/fixtures/markuplint/button-command.html:45:58 The "btn" class name is unmatched with the below patterns: "/^c-(?<ComponentName>[a-z][a-z0-9]*(?:-[a-z0-9]+)*)$/"',
'test/fixtures/markuplint/button-command.html:41:2 Detected perceptible nodes between the trigger and corresponding target',
'test/fixtures/markuplint/button-command.html:42:2 Detected perceptible nodes between the trigger and corresponding target',
'test/fixtures/markuplint/button-command.html:102:2 Detected perceptible nodes between the trigger and corresponding target',
'test/fixtures/markuplint/button-command.html:103:2 Detected perceptible nodes between the trigger and corresponding target',
'test/fixtures/markuplint/button-command.html:104:2 Detected perceptible nodes between the trigger and corresponding target',
'test/fixtures/markuplint/button-command.html:111:6 Detected perceptible nodes between the trigger and corresponding target',
'test/fixtures/markuplint/button-command.html:110:2 Require accessible name',
'test/fixtures/markuplint/button-command.html:24:2 The accessible name from "aria-label" overrides "content"',
'test/fixtures/markuplint/button-command.html:47:2 The accessible name from "aria-label" overrides "content"',
'test/fixtures/markuplint/button-command.html:129:3 Require accessible name',
'test/fixtures/markuplint/button-command.html:110:2 The "dialog" element referenced by a "show-modal" command requires an element with the "autofocus" attribute',
'test/fixtures/markuplint/button-command.html:63:16 The "button" role is the implicit role of the "button" element',
'test/fixtures/markuplint/button-command.html:54:16 The "tab" role requires an accessibility parent with the "tablist" role',
'test/fixtures/markuplint/button-command.html:55:16 The "tab" role requires an accessibility parent with the "tablist" role',
'test/fixtures/markuplint/button-command.html:57:16 The "menuitem" role requires an accessibility parent with one of the roles: "menu", "menubar", "menu > group", "menubar > group"',
'test/fixtures/markuplint/button-command.html:58:16 The "option" role requires an accessibility parent with one of the roles: "listbox", "listbox > group"',
'test/fixtures/markuplint/button-command.html:66:16 The "tab" role requires an accessibility parent with the "tablist" role',
'test/fixtures/markuplint/button-command.html:88:16 The "tab" role requires an accessibility parent with the "tablist" role',
'test/fixtures/markuplint/button-command.html:55:21 The "aria-selected" ARIA state is not global state',
'test/fixtures/markuplint/button-command.html:18:2 The "button" element expects the "command" attribute',
'test/fixtures/markuplint/button-command.html:101:2 The "button" element expects the "commandfor" attribute',
'test/fixtures/markuplint/button-command.html:102:2 The "button" element expects the "commandfor" attribute',
'test/fixtures/markuplint/button-command.html:103:2 The "button" element expects the "commandfor" attribute',
'test/fixtures/markuplint/button-command.html:104:2 The "button" element expects the "commandfor" attribute',
'test/fixtures/markuplint/button-command.html:102:2 The "button" element expects the "command" attribute',
'test/fixtures/markuplint/button-command.html:103:2 The "button" element expects the "command" attribute',
'test/fixtures/markuplint/button-command.html:104:2 The "button" element expects the "command" attribute',
'test/fixtures/markuplint/button-command.html:55:21 The "aria-selected" ARIA state is not global state',
'test/fixtures/markuplint/button-command.html:63:16 The "button" role is the implicit role of the "button" element',
]);
});

test('Attr Order', async () => {
const violations = await markuplint(
'test/fixtures/markuplint/attr-order-test.html',
'packages/@d-zero/markuplint-config/base.js',
);
expect(violations).toStrictEqual([
'test/fixtures/markuplint/attr-order-test.html:17:36 "id" should be before "type"',
'test/fixtures/markuplint/attr-order-test.html:18:23 "class" should be before "src"',
'test/fixtures/markuplint/attr-order-test.html:19:41 "class" should be before "name"',
'test/fixtures/markuplint/attr-order-test.html:20:26 "id" should be before "href"',
'test/fixtures/markuplint/attr-order-test.html:21:53 "id" should be before "data-value"',
'test/fixtures/markuplint/attr-order-test.html:11:38 The "tab" role requires an accessibility parent with the "tablist" role',
'test/fixtures/markuplint/attr-order-test.html:14:33 The "menuitem" role requires an accessibility parent with one of the roles: "menu", "menubar", "menu > group", "menubar > group"',
'test/fixtures/markuplint/attr-order-test.html:20:43 The "menuitem" role requires an accessibility parent with one of the roles: "menu", "menubar", "menu > group", "menubar > group"',
'test/fixtures/markuplint/attr-order-test.html:11:43 The "aria-selected" ARIA state is not global state',
'test/fixtures/markuplint/attr-order-test.html:17:2 The "button" element expects the "command" attribute',
]);
});

test('V5 New Rules (head-element-order, no-boolean-attr-value, no-default-value, no-event-handler-attr)', async () => {
const violations = await markuplint(
'test/fixtures/markuplint/v5-rules-test.html',
'packages/@d-zero/markuplint-config/base.js',
);
expect(violations).toStrictEqual([
'test/fixtures/markuplint/v5-rules-test.html:10:31 "onclick" should be before "type"',
'test/fixtures/markuplint/v5-rules-test.html:9:24 The "disabled" attribute is a boolean attribute. It doesn\'t need the value',
'test/fixtures/markuplint/v5-rules-test.html:11:22 It is the default value',
'test/fixtures/markuplint/v5-rules-test.html:9:2 Require accessible name',
'test/fixtures/markuplint/v5-rules-test.html:11:2 Require accessible name',
'test/fixtures/markuplint/v5-rules-test.html:4:2 The "meta" element should be before the "title" element',
'test/fixtures/markuplint/v5-rules-test.html:5:2 The "title" element should be before the "meta" element',
'test/fixtures/markuplint/v5-rules-test.html:10:31 The "onclick" attribute is disallowed',
'test/fixtures/markuplint/v5-rules-test.html:10:2 The "button" element expects the "command" attribute',
]);
});
});
Expand Down
23 changes: 23 additions & 0 deletions test/fixtures/markuplint/attr-order-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Attr Order Test</title>
</head>
<body>
<h1>Attribute Order Tests</h1>

<!-- Should PASS: correct order (id > class > role > aria > data > specific) -->
<button id="btn1" class="btn" role="tab" aria-selected="true" data-index="0" type="button">Correct</button>
<img id="hero" class="img" alt="Hero" src="hero.jpg" />
<input id="search" class="input" aria-label="Search" data-type="search" name="q" type="search">
<a id="link" class="nav" role="menuitem" aria-current="page" href="/">Home</a>

<!-- Should ERROR: wrong order -->
<button type="button" class="btn" id="btn2">type before class and id</button>
<img src="photo.jpg" class="img" alt="Photo" />
<input name="email" aria-label="Email" class="input" type="email">
<a href="/" class="nav" id="link2" role="menuitem">href before class and id</a>
<div data-value="1" aria-hidden="true" class="box" id="box1">data before aria, class, id</div>
</body>
</html>
Loading
Loading