👀

[CSS組版]目次の見出し番号を本文の見出しの`::before`から取ってくる

2024/12/02に公開

本記事はCSS組版アドベントカレンダー2024 2日目の記事です。

https://adventar.org/calendars/10448

CSS組版では、ページ決定時に決定する内容の記述は処理系に任せられる一方、事前に決定可能な箇所は記述しておく必要があります。言い換えると、目次の表示内容は本文から取ってこれるが、目次項目のリスト自体は書く必要があるということです(その目次項目を本文見出しからピックアップしてリストとして配置する事前処理が手書きだろうがJavaScriptだろうがXSLTだろうが、CSS組版の責務ではありません)。

扨、見出しは次のパーツで構成されます:

  • ラベル(見出し番号など)
  • ボディ(見出し項目)

例として第2章 CSS組版という見出しがあったとき、第2章がラベル、CSS組版がボディとなります。

ラベルについて、見出し番号を本文に直接記述するのはCSS組版的には旨味がありません。具体的には「ラベルの部分だけ小さく表示させたい!」「ボディと別行にしたい」などの要望が後から湧いたとき、マークアップをやり直しすことになったりします。スタイルシートだけ弄って解決する、これを可能にする記述が肝要です。

CSSに易しくない記述
<main>
  <section>
  <h2 id="chap2">第2章 CSS組版</h2>
<!-- `第2章`だけ表示を弄りたい -->
<!-- <h2 id="chap2"><span class="custom">第2章</span> CSS組版</h2> -->
 </section>
</main>

先ず、見出し番号は自動で付与するとします。するとHTMLは次。

<main>
  <section>
  <h2 id="chap2">CSS組版</h2>
  </section>
</main>

このときラベル内容をHTMLにそう書いてあるが如く扱えるのが::before疑似要素です。疑似要素というだけあって、アクセシビリティ的な話を除けば、スタイルシート側に記述されながら要素のように扱えます。

:root {
  counter-set: chap 0; /*章番号用カウンタの宣言。 処理系によってcounter-setに対応していなくてもcounter-resetは使えたりする */
}
main > section >h2::before {
            font-size: smaller; /*見出し番号はフォントサイズを落とす*/
            text-decoration: underline; /*見出し番号に下線*/
            content: "第" counter(chap) "章"; /*章番号カウンタの使用*/
            counter-increment: chap 1; /*カウンタを回す*/
            margin-right: 1em; /*見出し番号と項目間に1emのアキ*/
}

組版すると次のように。

本題:目次の見出し番号に反映する

「目次項目はCSS組版の責務では~」のような話をしました。上の箇所だけを読んだ素直な皆さんは素直にこんな感じに書き始めるのではないでしょうか。

    <header>
        <section><h2>目次</h2>
        <nav>
            <ol>
                <li><a href="#chap1">第1章 CSS</a></li>
                <li><a href="#chap2">第2章 CSS組版</a></li>
                <li><a href="#chap3">第3章 ページネーション</a></li>
            </ol>
        </nav>
    </section>
    </header>

しかしこれ、本文の見出しを変更したら一々記述を変更する感じですよね。JavaScriptなどを使って簡略化するにしても、「これがCSS組版です!」とお出しされたらげんなりしてしまう人もいるでしょう。

ではどうするのか。(先ずは)こうです。先程と同じようにラベル記述をHTMLから削除します。

「第○章」の表記を削除
    <header>
        <section><h2>目次</h2>
        <nav>
            <ol>
                <li><a href="#chap1">CSS</a></li>
                <li><a href="#chap2">CSS組版</a></li>
                <li><a href="#chap3">ページネーション</a></li>
            </ol>
        </nav>
    </section>
    </header>

序でに、一般的な目次用のCSSを書いておきます。

/* ol要素で書くが、::markerは使わない */
 body > header > section  > nav  ol {list-style:none;}

/*項目のページ番号を表示させる*/
header >section > nav a::after {
  content: leader('.') target-counter(attr(href), page);
  text-align-last: justify;
} 

目次項目とそのページ番号間の「...」を作るのがleader()です。

ahrefで示したidが対象の見出し項目なので、そのターゲットでのpageカウンタの値を示せ、というのがtarget-counter(attr(href), page)の指定です。

では、本文の見出しから、見出し番号を取って来ましょう。

header > section > nav a::before {
  content: target-text(attr(href),before);
  margin-right: 1em;
}

はい。ahrefattr(href)で引っぱって来ているのは先程と変わりません。しかし指定はtarget-text();ズバリ該当箇所のテキストを持って来る函数です。そして第2引数にbeforeとついています。そう、before疑似要素内のテキストも持って来れてしまうんですね! 勝った! 第3部、完!

まだもうちょっとだけ続くんじゃよ。
実はcontentですが、::before::afterではなく要素のセレクタにも書くことが可能です。その場合、既にHTMLで記述されている内容を消し飛ばしてcontentに置き換えます。つまり目次の最小記述はこう!

  <header>
        <section><h2>目次</h2>
        <nav>
            <ol>
                <li><a href="#chap1"></a></li>      
                <li><a href="#chap2"></a></li>
                <li><a href="#chap3"></a></li>
            </ol>
        </nav>
    </section>
</header>
header >section > nav a {
  text-decoration: none;
  color: inherit;
  content: target-text(attr(href));
}

空のaなんて書いていいのか、でもCSS組版用にJSで自動生成するのがコレなら別にいいんじゃない?

終わり

で、組版してみましょう。

3日目はhidarumaさんです! 今日もギリギリなので間に合わないかもしれんね。

セクショニングと見出しのid

セクションのid属性をsectionのようにセクショニングのコンテナが持つかh2のように見出しの構造が持つかはちょっと悩みますが、殊CSS組版の場合はidから近くの構造ををCSSから辿る方法が限られるため、見出し構造が持つことをオススメします。

組版・ドキュメンテーション勉強会

Discussion