🦁

markuplint で HTML の構文チェックを始めよう

2022/04/27に公開

ブラウザが HTML を解釈する方法はその他のプログラミング言語と比べてはるかに寛容です。ブラウザは HTML 内に構文エラーを発見しても大抵の場合は問題なくページに表示されます。ブラウザには、誤って書かれたマークアップを解釈する方法を決定するための組み込みのルールがあるためです。

例として以下のコード例を確認してみましょう。<ul> タグは仕様でその子要素には 0 個以上の <li> タグまたは script supporting elements (<script><template>)のみが許可されています。従って、<ul> タグの子要素に <a> タグを配置している以下のコードは構文エラーとなります。

<ul>
  <a href="/home">Home</a>
  <a href="/about">About</a>
  <a href="/blog">Blog</a>
  <a href="#top">Top</a>
</ul>

しかしながら、上記コードをブラウザで表示してもブラウザはエラーを報告することなく問題なく表示します。

スクリーンショット 2022-04-25 21.42.28

このことは良い点と悪い点があります。良い点としては構文エラーに対して寛容になることにより幅広い人々が Web を利用して情報を発信できます。これは Web の基本理念である普遍性に沿っていると言えるでしょう。問題点としては Web サイトの作成者が構文エラーに気が付きづらいということです。HTML の構文エラーを放置しているとブラウザによっては意図しないレンダリングとなったり、正しい意味を解釈できない可能性があります。

実際にエラーの報告されない HTML をデバッグするのはひどく難しいです。VS Code のような IDE を利用していても構文エラーをうまく検出できません。

HTML の構文チェックを実施するには適切なツールを導入するのがよいでしょう。この記事では markuplint と呼ばれる HTML の静的解析ツールを紹介します。markuplint は JSX(React),Vue,Svlete のようなテンプレートエンジンにも対応しています。

https://markuplint.dev/

markuplint を始める

markuplint は npm でインストールできます。

$ npm install --save-dev markuplint

もし VS Code を使用している場合には markuplint の拡張機能をインストールすると良いでしょう。

https://marketplace.visualstudio.com/items?itemName=yusukehirao.vscode-markuplint

続いて、以下コマンドで .marklintntrc という名前の設定ファイルをプロジェクトルートに配置します。

$ npx markuplint --init

package.json に静的解析を実行する npm-scripts を追加しましょう。

{
  "scripts": {
    "html:lint": "markuplint index.html"
  }
}

それでは実際にコマンドを実行してみましょう。

$ npm run html:lint

> html-check@1.0.0 html:lint
> markuplint index.html

<markuplint> error: HTMLの仕様において、要素「ul」の内容は妥当ではありません (permitted-contents) /work/html-check/index.html:12:3
  11: <body>
  12: ••<ul>
  13: ••••<a•href="/home">Home</a>
<markuplint> error: HTMLの仕様において、要素「a」の内容は妥当ではありません (permitted-contents) /work/html-check/index.html:13:5
  12: ••<ul>
  13: ••••<a href="/home">Home</a>
  14: ••••<a•href="/about">About</a>
<markuplint> error: HTMLの仕様において、要素「a」の内容は妥当ではありません (permitted-contents) /work/html-check/index.html:14:5
  13: ••••<a•href="/home">Home</a>
  14: ••••<a href="/about">About</a>
  15: ••••<a•href="/blog">Blog</a>
<markuplint> error: HTMLの仕様において、要素「a」の内容は妥当ではありません (permitted-contents) /work/html-check/index.html:15:5
  14: ••••<a•href="/about">About</a>
  15: ••••<a href="/blog">Blog</a>
  16: ••••<a•href="#top">Top</a>
<markuplint> error: HTMLの仕様において、要素「a」の内容は妥当ではありません (permitted-contents) /work/html-check/index.html:16:5
  15: ••••<a•href="/blog">Blog</a>
  16: ••••<a href="#top">Top</a>
  17: ••</ul>

複数のエラーが報告されました。エラーの内容と行数が表示されています。エラーの対象となったルールはすべて Permitted contents によるもので、これは子要素が許可されていない要素またはテキストノードを持つ場合に警告を出します。

コードを次のように修正しましょう。<ul> タグの子要素には許可されている <li> タグを配置するようにします。

<ul>
  <li>
    <a href="/home">Home</a>
  </li>
  <li>
    <a href="/about">About</a>
  </li>
  <li>
    <a href="/blog">Blog</a>
  </li>
  <li>
    <a href="#top">Top</a>
  </li>
</ul>

再度コマンドを実行するとエラーがすべて解消されていることが確認できます。

$ npm run html:lint

<markuplint> passed /work/html-check/index.html

特定の状況でのルールの無効化

Lint ツールを使用しているとき特定の行のみでルールを無効化したいような状況が存在します。例えば ESLint であれば // eslint-disable-next-line というコメントで次の行の ESLint のルールを無効にできます。

markuplint ではセレクタによるルールを上書きして無効化できます。例として以下のような HTML があるとします。

<table class="foo">
  <td>1</td>
  <td>2</td>
</table>

<table> の子要素に <td> 要素を直接配置できないのでこれは Permitted contents ルールによってエラーが検出されます。

$ npm run html:lint

<markuplint> error: HTMLの仕様において、要素「table」の内容は妥当ではありません (permitted-contents) /work/html-check/index.html:14:3
  13:
  14: ••<table class="foo">
  15: ••••<td>1</td>

例として不適切ではあるのですが、このエラーを無効化したいと考えたとしましょう。.marklintntrc ファイルに nodeRules を追加します。

{
  "nodeRules": [
    {
      "selector": ".foo",
      "rules": {
        "permitted-contents": false
      }
    }
  ]
}

nodeRules を使用することで特定要素のみに対してルールを上書きできます。selector には CSS のセレクターの記法を利用できます。上記例では foo というクラスが付与されているクラスに対してルールを上書きします。rules において marmitted-contentsfalse に設定しているので、リントを実行してもエラーが報告されないようになります。

$ npm run html:lint

<markuplint> passed /work/html-check/index.html

nodeRules と似た設定に childNodeRules が存在します。これはルールを上書きするのは nodeRules と同様ですが、selector で指定した要素の子要素に対してもルールを上書きします。inheritance プロパティを true にすることで子孫要素まで範囲を広げることができます。

カスタム要素のエラーを検出する

React や Vue のようなフレームワークを使用している場合独自のコンポーネントを作成して利用します。ここで問題になるのはカスタム要素を利用している場合には HTML の構文エラーが明らかな場合にもエラーを検出できないというところです。以下の例を見てみましょう。

const MyList = ({ children }) => {
  return <ul>{children}</ul>;
};

const MyListItem = ({ children }) => {
  return <li>{children}</li>;
};

const App = () => {
  return (
    <div>
      <MyList>
        <div>
          <MyListItem>1</MyListItem>
          <MyListItem>2</MyListItem>
          <MyListItem>3</MyListItem>
        </div>
      </MyList>
    </div>
  );
};

MyList コンポーネントは <ul> タグをラップしたコンポーネントです。子要素には <li> タグまたは <li> タグをルートに使用しているコンポーネントを配置するべきです。ここでは <MyList> の直下に <div> を配置してしまっており、最終的なレンダリング結果では <ul> タグの子要素に <div> タグが配置されてしまうことがわかっているのですが、この構文エラーを検出できません。

このような場合には .marklintntrc ファイルを修正して対応できます。rules プロパティの permitted-contents を次のように修正します。

{
  "rules": {
    "permitted-contents": [
      {
        "tag": "MyList",
        "contents": [
          {
            "zeroOrMore": "MyListItem"
          }
        ]
      }
    ]
  }
}

tag でどのタグをルールの対象とするか選択します。contents には配列形式で tag において指定した要素に配置できる要素のルールを記述します。上記例では <MyList> タグの子要素には 0 個以上の <MyListItem> タグが必要だと設定しています。

ルールとして設定できるキーワードは以下のとおりです。

  • require:常にひとつ
  • optional:0 または 1
  • oneOrMore:1 個以上
  • zeroOrMore:0 個以上
  • choice:配列で指定するルールの中からどれか 1 つ
  • interleave:配列で指定するルールを順序関係なく適用

リントを実行すると以下のようにエラーが検出されます。

$ npm run html:lint

<markuplint> error: 要素「MyList」の内容は妥当ではありません (permitted-contents) /work/html-check/src/components/App.jsx:12:7
  11: ••••<div>
  12: ••••••<MyList>
  13: ••••••••<div>

アクセシビリティ

markuplint はアクセシビリティ上の問題を検知できます。例えば required-h1 はページ内に必ず <h1> タグが存在するように警告します。

wai-aria ルールは誤った role 属性や aria-* 属性の使用を検出します。誤った role の使用例として以下のようなコードがあげられます。

<nav role="navigation">
  <ul>
    <li role="button">list 1</li>
  </ul>
</nav>

リントを実行すると以下のように表示されます。

$ npm run html:lint

<markuplint> error: ロール「navigation」は要素「nav」の暗黙のロールです (wai-aria) /work/html-check/index.html:13:8
  12: ••<h1>test</h1>
  13: ••<nav•role="navigation">
  14: ••••<ul>
<markuplint> error: ARIA in HTMLの仕様において、ロール「button」を要素「li」に上書きすることはできません (wai-aria) /work/html-check/index.html:15:11
  14: ••••<ul>
  15: ••••••<li•role="button">list•1</li>
  16: ••••</ul>

1 つ目のエラーでは <nav> タグに navigation ロールを付与しています。これは一見正しいように思えますが、<nav> タグは暗黙のロールとしてすでに navigation ロールを持っているため冗長な指定となっています。このように暗黙的にロールを持っている属性をあえて指定することは避けるべきです。

2 つ目のエラーでは <li> タグに button ロールを付与しています。しかし、要素によっては許可できる要素が決まっている場合があり、<li> タグには以下のロールのみが許可されています。

  • menuitem
  • menuitemcheckbox
  • menuitemradio
  • option
  • none
  • presentation
  • radio
  • separator
  • tab
  • treeitem

https://developer.mozilla.org/ja/docs/Web/HTML/Element/li

上記のコードは以下のとおりに修正できます。

<nav>
  <ul>
    <li><button>list 1</button></li>
  </ul>
</nav>

その他興味深いルールに use-list があります。これは以下のように先頭に を付与して文字列を列挙している場合には <li> タグを使うように警告をします。

<div>
  <div>•Alice</div>
  <div>•Bob</div>
  <div>•charlie</div>
</div>
<markuplint> warning: Use li element (use-list) /work/html-check/index.html:14:10
  13: ••<div>
  14: ••••<div>•Alice</div>
  15: ••••<div>•Bob</div>
<markuplint> warning: Use li element (use-list) /work/html-check/index.html:15:10
  14: ••••<div>•Alice</div>
  15: ••••<div>•Bob</div>
  16: ••••<div>•charlie</div>
<markuplint> warning: Use li element (use-list) /work/html-check/index.html:16:10
  15: ••••<div>•Bob</div>
  16: ••••<div>•charlie</div>
  17: ••</div>

感想

markuplint は厳格に HTML 構文をチェックしてくれたり、カスタム要素などにもルールを追加可能であるなどの特徴を持ちます。HTML の構文エラーに対して素早いフィードバックを受けられるのは魅力的ですね。

markuplint には playground もあるので普段 HTML の構文にあまり気を使っていなかったような場合には、一度試してみるとよいでしょう。

https://playground.markuplint.dev/

GitHubで編集を提案

Discussion