一覧WB-06-2 · HTMLの文書構造

リンク・画像・リスト・表

勉強会の案内へリンク・画像・一覧・参加費の表を加えます。同じ文書を更新しながら、URLと参照先、画像の役割、項目順、見出しとセルの対応をDOMと表示で確認します。

説明

HTMLでは、リンクの行き先、画像の代替文、一覧の並び、表の見出しと値の対応を明示できます。見た目がそれらしくても、要素や属性が不足すると、文書を機械的に扱ったり読み上げたりする際に意味が伝わりにくくなります。

勉強会の案内へリンク・画像・一覧・参加費の表を加えます。同じ文書を更新しながら、URLと参照先、画像の役割、項目順、見出しとセルの対応をDOMと表示で確認します。

1案内文書を用意

見出しと三つの節を持つ文書から始めます。

(globalThis.doc=new DOMParser().parseFromString独立した案内文書を解析('<!doctype html><html lang="ja"><head><title>勉強会の参加案内</title></head><body><h1>勉強会の参加案内</h1><section id="place"><h2>会場</h2><p>中央図書館の図書室で開催します。</p></section><section id="prepare"><h2>準備</h2></section><section id="fee"><h2>参加費</h2></section></body></html>',"text/html"),[...doc.querySelectorAll("section")].map(e=>e.id)各節の参照名)

各節のidはplace、prepare、feeです。idは文書内の参照先として使うため、同じ文書で重複しないようにします。

2文書内へのリンク

冒頭へ参加費の節を指すリンクを追加します。

(globalThis.a=doc.createElement("a"),a.href="#fee"文書内の参照先,a.textContent="参加費を確認"内容が分かるリンク文言,doc.querySelector("h1").after(a),{href:a.getAttribute("href"),target:doc.getElementById参照先が存在するか確認(a.getAttribute("href").slice(1)).querySelector("h2").textContent})

href="#fee"はこの文書内のid=feeを指します。出力のtargetは「参加費」で、リンク先の意味とリンク文言が対応しています。「こちら」だけの文言より、何を参照するリンクかが分かります。

3相対URLを基準URLへ結び付ける

会場案内への相対リンクを追加し、基準URLからの解決結果を計算します。

(globalThis.guide=doc.createElement("a"),guide.setAttribute("href","../access/map.html")相対リンクを設定,guide.textContent="会場への道順",doc.querySelector("#place").append(guide),{written:guide.getAttribute("href"),resolved:new URL指定した基準URLで解決(guide.getAttribute("href"),"https://guide.example.test/events/session.html").href})

書いた値は../access/map.htmlで、解決先はhttps://guide.example.test/access/map.htmlです。相対URLは文書の基準URLと組み合わせて解釈します。DOMの属性文字列と、絶対URLへ解決した結果を分けて確認します。

4画像へ代替文と寸法を指定

会場までの矢印を画像として追加します。画像データはこの文書に埋め込んだSVGです。

(globalThis.img=doc.createElement("img"),img.src="data:image/svg+xml,"文書内に埋め込んだ画像+encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="280" height="60"><rect width="280" height="60" fill="#1b333b"/><text x="12" y="38" fill="white" font-size="18">Entrance → Room A</text></svg>'),img.alt=画像が伝える内容"入口から右へ進むと図書室Aがあります",img.width=280,img.height=60寸法を指定,doc.querySelector("#place").append(img),{alt:img.alt,width:img.getAttribute("width"),height:img.getAttribute("height")})

altは画像の内容・役割を文脈に合わせて文字で伝えます。widthとheightは表示領域の比率を事前に確保する手掛かりにもなります。ここでは入口から会場へ進む方向が重要なので、その情報を代替文に含めています。

5装飾画像は代替文を空にする

同じ画像を装飾用として複製し、空のaltを指定します。

(globalThis.decoration=img.cloneNode()画像を複製,decoration.alt=""装飾として代替文を空にする,decoration.width=140,decoration.height=30,doc.querySelector("#place").append(decoration),[...doc.images].map(e=>({hasAlt:e.hasAttribute("alt")属性の有無を確認,alt:e.getAttribute("alt")})))

二枚目にもalt属性はありますが、値は空です。周囲の説明と重複する装飾なら、読み上げる内容を持たせない選択ができます。altを省略することと、装飾として空を指定することは同じではありません。

6順序のない一覧

準備の節に持ち物を列挙します。

(doc.querySelector("#prepare").insertAdjacentHTML("beforeend"節の末尾へ追加,"<ul><li>筆記用具</li><li>ノートPC</li><li>飲み物</li></ul>"),[...doc.querySelectorAll("#prepare ul > li一覧の直接の項目")].map(e=>e.textContent))

ulの各項目がliです。持ち物は順序を入れ替えても意味が変わらない一覧です。見た目の行頭記号を文字で打つのではなく、一覧の構造を要素で表します。

7順番のある手順

受付から着席までを順序付き一覧で追加します。

(doc.querySelector("#prepare").insertAdjacentHTML("beforeend","<ol>順序付き一覧<li>受付で名前を伝える</li><li>名札を受け取る</li><li>席に着く</li></ol>"),[...doc.querySelectorAll("#prepare ol > li")].map((e,i)=>({step:i+1手順の順番,text:e.textContent})))

olは並びに意味がある一覧です。この手順では受付、名札、着席の順序を保ちます。単に数字の見た目が欲しいかではなく、項目の順序が内容の意味に関わるかで選びます。

8名称と説明を対応付ける

参加区分と説明をdlへ追加します。

(doc.querySelector("#prepare").insertAdjacentHTML("beforeend","<dl>名称と説明の一覧<dt>一般</dt><dd>学生以外の参加者</dd><dt>学生</dt><dd>学生証を提示する参加者</dd></dl>"),[...doc.querySelectorAll("dt")].map(e=>({term:e.textContent名称,description:e.nextElementSibling.textContent対応する説明})))

dtが名称、ddが説明です。dlは用語集だけでなく、名前と値の組合せなどにも使えます。ここでは一つのdtに一つのddという構造なのでnextElementSiblingで取得していますが、一般には複数の名称や説明が対応する構造もあります。

9参加費を表にする

区分・人数・料金の表を作り、行見出しと列見出しを指定します。

(doc.querySelector("#fee").insertAdjacentHTML("beforeend",'<table>表を追加<caption>一人あたりの参加費</caption><thead><tr><th scope="col">区分</th><th scope="col">人数</th><th scope="col">料金</th></tr></thead><tbody><tr><th scope="row">一般</th><td>1人</td><td>500円</td></tr><tr><th scope="row">学生</th><td>1人</td><td>300円</td></tr></tbody></table>'),[...doc.querySelectorAll("th")].map(e=>({text:e.textContent,scope:e.scope見出しが対応する方向})))

captionは表が何を示すか、列見出しのscope=colは列、行見出しのscope=rowは行との対応を示します。500円という値だけでは誰の何の料金か分からないため、見出しとの関係をHTMLにも持たせます。

10特定のセルの見出しを明示

一般の料金セルに、行・列の二つの見出しidを対応付けます。

(doc.querySelectorAll("thead th")[2].id="price",doc.querySelector("tbody th").id="general",globalThis.cell=doc.querySelector("tbody tr").lastElementChild,cell.setAttribute("headers","general price")行・列の見出しidを指定,{value:cell.textContent,headers:cell.getAttribute("headers").split(" ").map(id=>doc.getElementById(id).textContent)参照する見出しを取得})

500円の見出しは「一般」と「料金」です。headers属性は見出しセルのidを空白区切りで参照します。複雑な表で関連を明示する方法ですが、参照先idが存在し、意図した見出しを指していることが必要です。

11参照と一覧・表をまとめて点検

idの重複、リンクの参照先、表のセル数を確認します。

({uniqueIds:idの重複new Set([...doc.querySelectorAll("[id]")].map(e=>e.id)).size===doc.querySelectorAll("[id]").length,fragmentTarget:参照先の有無!!doc.getElementById("fee"),tableRows:各行のセル数[...doc.querySelector("table").rows].map(r=>r.cells.length),imagesWithAlt:alt属性の有無[...doc.images].every(e=>e.hasAttribute("alt"))})

idは重複せず、文書内リンクの参照先があり、表は各行三セル、画像にはaltがあります。これらは構造の点検です。代替文が画像の目的に合っているか、表の内容が正確かは、値と文脈を読んで確かめます。

まとめ

リンク・画像・一覧は、行き先・役割・項目の関係を要素と属性で表します。

表では値だけでなく、captionや見出しとの対応を指定します。

属性の有無の検査と、内容や文脈に合っているかの確認は別々に行います。

操作履歴と解説を振り返れます。

ブラウザ / HTML・DOM

1
(globalThis.doc=new DOMParser().parseFromString('<!doctype html><html lang="ja"><head><title>勉強会の参加案内</title></head><body><h1>勉強会の参加案内</h1><section id="place"><h2>会場</h2><p>中央図書館の図書室で開催します。</p></section><section id="prepare"><h2>準備</h2></section><section id="fee"><h2>参加費</h2></section></body></html>',"text/html"),[...doc.querySelectorAll("section")].map(e=>e.id))
[
  "place",
  "prepare",
  "fee"
]
2
(globalThis.a=doc.createElement("a"),a.href="#fee",a.textContent="参加費を確認",doc.querySelector("h1").after(a),{href:a.getAttribute("href"),target:doc.getElementById(a.getAttribute("href").slice(1)).querySelector("h2").textContent})
{
  "href": "#fee",
  "target": "参加費"
}
3
(globalThis.guide=doc.createElement("a"),guide.setAttribute("href","../access/map.html"),guide.textContent="会場への道順",doc.querySelector("#place").append(guide),{written:guide.getAttribute("href"),resolved:new URL(guide.getAttribute("href"),"https://guide.example.test/events/session.html").href})
{
  "written": "../access/map.html",
  "resolved": "https://guide.example.test/access/map.html"
}
4
(globalThis.img=doc.createElement("img"),img.src="data:image/svg+xml,"+encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="280" height="60"><rect width="280" height="60" fill="#1b333b"/><text x="12" y="38" fill="white" font-size="18">Entrance → Room A</text></svg>'),img.alt="入口から右へ進むと図書室Aがあります",img.width=280,img.height=60,doc.querySelector("#place").append(img),{alt:img.alt,width:img.getAttribute("width"),height:img.getAttribute("height")})
{
  "alt": "入口から右へ進むと図書室Aがあります",
  "width": "280",
  "height": "60"
}
5
(globalThis.decoration=img.cloneNode(),decoration.alt="",decoration.width=140,decoration.height=30,doc.querySelector("#place").append(decoration),[...doc.images].map(e=>({hasAlt:e.hasAttribute("alt"),alt:e.getAttribute("alt")})))
[
  {
    "hasAlt": true,
    "alt": "入口から右へ進むと図書室Aがあります"
  },
  {
    "hasAlt": true,
    "alt": ""
  }
]
6
(doc.querySelector("#prepare").insertAdjacentHTML("beforeend","<ul><li>筆記用具</li><li>ノートPC</li><li>飲み物</li></ul>"),[...doc.querySelectorAll("#prepare ul > li")].map(e=>e.textContent))
[
  "筆記用具",
  "ノートPC",
  "飲み物"
]
7
(doc.querySelector("#prepare").insertAdjacentHTML("beforeend","<ol><li>受付で名前を伝える</li><li>名札を受け取る</li><li>席に着く</li></ol>"),[...doc.querySelectorAll("#prepare ol > li")].map((e,i)=>({step:i+1,text:e.textContent})))
[
  {
    "step": 1,
    "text": "受付で名前を伝える"
  },
  {
    "step": 2,
    "text": "名札を受け取る"
  },
  {
    "step": 3,
    "text": "席に着く"
  }
]
8
(doc.querySelector("#prepare").insertAdjacentHTML("beforeend","<dl><dt>一般</dt><dd>学生以外の参加者</dd><dt>学生</dt><dd>学生証を提示する参加者</dd></dl>"),[...doc.querySelectorAll("dt")].map(e=>({term:e.textContent,description:e.nextElementSibling.textContent})))
[
  {
    "term": "一般",
    "description": "学生以外の参加者"
  },
  {
    "term": "学生",
    "description": "学生証を提示する参加者"
  }
]
9
(doc.querySelector("#fee").insertAdjacentHTML("beforeend",'<table><caption>一人あたりの参加費</caption><thead><tr><th scope="col">区分</th><th scope="col">人数</th><th scope="col">料金</th></tr></thead><tbody><tr><th scope="row">一般</th><td>1人</td><td>500円</td></tr><tr><th scope="row">学生</th><td>1人</td><td>300円</td></tr></tbody></table>'),[...doc.querySelectorAll("th")].map(e=>({text:e.textContent,scope:e.scope})))
[
  {
    "text": "区分",
    "scope": "col"
  },
  {
    "text": "人数",
    "scope": "col"
  },
  {
    "text": "料金",
    "scope": "col"
  },
  {
    "text": "一般",
    "scope": "row"
  },
  {
    "text": "学生",
    "scope": "row"
  }
]
10
(doc.querySelectorAll("thead th")[2].id="price",doc.querySelector("tbody th").id="general",globalThis.cell=doc.querySelector("tbody tr").lastElementChild,cell.setAttribute("headers","general price"),{value:cell.textContent,headers:cell.getAttribute("headers").split(" ").map(id=>doc.getElementById(id).textContent)})
{
  "value": "500円",
  "headers": [
    "一般",
    "料金"
  ]
}
11
({uniqueIds:new Set([...doc.querySelectorAll("[id]")].map(e=>e.id)).size===doc.querySelectorAll("[id]").length,fragmentTarget:!!doc.getElementById("fee"),tableRows:[...doc.querySelector("table").rows].map(r=>r.cells.length),imagesWithAlt:[...doc.images].every(e=>e.hasAttribute("alt"))})
{
  "uniqueIds": true,
  "fragmentTarget": true,
  "tableRows": [
    3,
    3,
    3
  ],
  "imagesWithAlt": true
}

演習完了