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-1やlevel-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の内部構造に関する一連の記事を公開予定ですので、ぜひお楽しみに!
-
AndroidでWebViewの複数ウィンドウ(マルチスクリーン)をサポートする方法
この記事では、Androidアプリ内のWebViewで複数のウィンドウ(マルチスクリーン)表示をサポートする方法を、サンプルコードとともに段階的に解説します。WebViewは標準設定では単一ウィンドウしか扱えませんが、setSupportMultipleWindows(true)を有効化し、WebChromeClientを適切に設定することで、新しいウィンドウやタブの起動に対応できるようになります。ステップ1:新規プロジェクトを作成するAndroid Studioを開き、「File」→「New Project」から新しいプロジェクトを作成します。必要な項目をすべて入力してプロジェクトの雛形を生
-
CSS3で複数の背景画像を追加する方法
CSS3では、background-imageプロパティを使うことで、1つの要素に複数の背景画像を簡単に追加できます。各背景画像はカンマ(,)で区切って指定し、表示順序は先に書いたものが前面に、後ろに書いたものが背面に配置されます。 さらに、background-positionやbackground-repeatなどの関連プロパティも、背景画像と同じ順番でカンマ区切りで指定することで、それぞれの画像に対して個別の設定が可能です。 サンプルコード 以下は、CSS3で複数の背景を追加する具体例です。 <!DOCTYPE html> <html> <head>