Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Jekyllで実現する多階層サブナビゲーションの作成方法

前回の記事では、Jekyllページ内の各H2見出しからサブナビゲーションのリンクを生成する方法をご紹介しました。今回はその基礎をさらに発展させ、H3やH4などの見出しに基づいて、任意の階層を持つサブナビゲーションを追加する方法を解説します。

概要

このプロジェクトは、大きく分けて以下のステップで構成されています。

  • まず、Nokogiriを使って、H2タグの「内部」に相当する位置にあるH3タグで定義されたセクションを抽出します
  • 続いて、再帰的なテンプレートを作成するテクニックを活用し、任意の階層のサブナビゲーションをレンダリングします

始める前に、一点だけ整理しておきましょう。ここで「H3タグがH2タグの中にある」と表現するのは、DOM上で文字通りネストしているという意味ではありません。以下のような状況を指しています。

<h2>Animals</h2>
<p>Here are some kinds of animals.</p>
<h3>Giraffe</h3>
<p>This section about giraffes logically belongs inside of the section about animals, even though the structure of the Dom doesn't define it as being nested</p>
<h3>Zebra</h3>
<p>Another section that logically belongs under "Animals"</p>

ドキュメントをセクションに分割する

上記のようなHTMLドキュメントをセクションに分割する際の課題は、「何もネストされていない」という点です。HTMLをパースするツールの多くは、ネスト構造を前提として設計されています。

致命的な問題ではありませんが、そのぶん少し工夫が必要になります。以下の例では、まず各H2タグを見つけ、その後に兄弟要素を走査してH3タグを探しています。

なお、この実装ではカスタムEnumerator(列挙子)を活用しています。Enumeratorについて詳しく知りたい方は、以前のブログ記事をご参照ください。

require "nokogiri"

class MySubnavGenerator < Jekyll::Generator
  def generate(site)
    parser = Jekyll::Converters::Markdown.new(site.config)

    site.pages.each do |page|
      if page.ext == ".md"
        doc = Nokogiri::HTML(parser.convert(page['content']))

        page.data["subnav"] = doc.css('h2').map do |h2|
          to_nav_item(page, h2).tap do |item|
            item["children"] = subheadings(h2).map { |h3| to_nav_item(page, h3) }
          end
        end
      end
    end
  end

  # Converts a heading into a hash of the info for a link
  def to_nav_item(page, heading)
    {
      "title" => heading.text,
      "url" => [page.url, heading['id']].join("#")
    }
  end

  # Returns an enumerator of all H3s "belonging" to an H2
  def subheadings(el)
    Enumerator.new do |y|
      next_el = el.next_sibling
      while next_el && next_el.name != "h2"
        if next_el.name == "h3"
          y << next_el
        end
        next_el = next_el.next_sibling
      end
    end
  end
end

いきなり大量のコードを提示することになりましたが、これは前回の記事で行った作業の続きにあたります。Jekyllプラグインの構造やNokogiriの使い方について不明な点がある場合は、ぜひ前回の記事をご覧ください。

このコードをドキュメントサイトに対して実行すると、次のようなハッシュが得られます。

[{"title"=>"Getting Started",
  "url"=>"/lib/java.html#getting-started",
  "sub_subnav"=>
   [{"title"=>"Download / Maven", "url"=>"/lib/java.html#download-maven"},
    {"title"=>"Stand Alone Usage", "url"=>"/lib/java.html#stand-alone-usage"},
    {"title"=>"Servlet Usage", "url"=>"/lib/java.html#servlet-usage"},
    {"title"=>"Play Usage", "url"=>"/lib/java.html#play-usage"},
    {"title"=>"API Usage", "url"=>"/lib/java.html#api-usage"}]},
    ...

あとは、このデータをLiquidテンプレートでどのようにレンダリングするかを考えるだけです。

サブナビゲーションのレンダリング

Liquidテンプレートを使えば、任意の深さのサブナビゲーションをレンダリングするのは、実はそれほど難しくありません。ポイントとなるのは、「自分自身をレンダリングするパーシャル(部分テンプレート)」を使うことです。

レイアウト側では、パーシャルを呼び出し、ナビゲーション項目のコレクションを渡します。

{% include navigation_item.html collection=page.subnav level=0 %}

パーシャルは、現在の階層のリンクを生成した後、子要素のリストを渡しながら自分自身を再度呼び出します。再帰関数と同じ仕組みなので、理論上は無限に階層を掘り下げることが可能です。おまけとして、サブナビの各階層にlevel-1level-2といったクラスを付与するコードも加えてみました。スタイリングの際にとても便利です。

{% if include.collection.size > 0 %}
<ul class="nav nav-list level-{{ include.level }}">
    {% for item in include.collection %}
      {% if item.url == page.url %}
      <li class="active">
      {% else %}
      <li>
      {% endif %}
        {% if item.subnav.size > 0 %}
          <a class="has-subnav" href="{{ item.url }}">
          <span class="glyphicon glyphicon-plus"></span>
          <span class="glyphicon glyphicon-minus"></span>
        {% else %}
          <a href="{{ item.url }}">
        {% endif %}
          {{ item.title }}
        </a>
        {% assign next_level = include.level | plus: 1 %}
        {% include navigation_item.html collection=item.children level=next_level %}
      </li>
    {% endfor %}
  </ul>
{% endif %}

まとめ

以上で、Jekyllの世界への短い旅は終わりです。今後数日にわたって、Rubyの内部構造に関する一連の記事を公開予定ですので、ぜひお楽しみに!

  1. AndroidでWebViewの複数ウィンドウ(マルチスクリーン)をサポートする方法

    この記事では、Androidアプリ内のWebViewで複数のウィンドウ(マルチスクリーン)表示をサポートする方法を、サンプルコードとともに段階的に解説します。WebViewは標準設定では単一ウィンドウしか扱えませんが、setSupportMultipleWindows(true)を有効化し、WebChromeClientを適切に設定することで、新しいウィンドウやタブの起動に対応できるようになります。ステップ1:新規プロジェクトを作成するAndroid Studioを開き、「File」→「New Project」から新しいプロジェクトを作成します。必要な項目をすべて入力してプロジェクトの雛形を生

  2. CSS3で複数の背景画像を追加する方法

    CSS3では、background-imageプロパティを使うことで、1つの要素に複数の背景画像を簡単に追加できます。各背景画像はカンマ(,)で区切って指定し、表示順序は先に書いたものが前面に、後ろに書いたものが背面に配置されます。 さらに、background-positionやbackground-repeatなどの関連プロパティも、背景画像と同じ順番でカンマ区切りで指定することで、それぞれの画像に対して個別の設定が可能です。 サンプルコード 以下は、CSS3で複数の背景を追加する具体例です。 <!DOCTYPE html> <html> <head>