Trong bài trước tôi show cách generate subnavigation links cho mỗi H2 trong trang Jekyll. Bài này, chúng ta xây dựng trên foundation đó và show cách thêm arbitrary levels của subnavigation dựa trên H3, H4, v.v.
Tổng quan
Tôi chia project thành vài bước:
- Thứ nhất, dùng nokogiri pull out sections defined by H3 tags "bên trong" H2 tags
- Tiếp theo, dùng trick cool render arbitrary levels sub navigation. Chúng ta sẽ làm recursive template.
Trước khi bắt đầu, làm rõ một điều. Khi tôi nói H3 tag bên trong H2, tôi KHÔNG có nghĩa nó nested literally. Thay vào đó, tôi nói về situation như dưới:
<h2>Animals</h2> <p>Here are some kinds of animals.</p> <h3>Giraffe</h3> <p>Section này về giraffes logically thuộc về section animals, dù DOM structure không define nó nested</p> <h3>Zebra</h3> <p>Section khác logically thuộc "Animals"</p>
Chia Document thành Sections
Vấn đề rõ ràng khi chia HTML document như trên thành sections, là không có gì nested. Hầu hết tool parse HTML built để work với nesting.
Đây không phải deal breaker, nhưng nghĩa là phải làm thêm chút việc. Ví dụ dưới tìm mỗi H2 tag rồi manually scan siblings cho H3 tags.
Tôi hơi fancy dùng custom enumerator. Nếu có câu hỏi về chúng, xem bài blog của tôi về chúng.
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 # Convert heading thành hash info cho link def to_nav_item(page, heading) { "title" => heading.text, "url" => [page.url, heading['id']].join("#") } end # Trả về enumerator của tất cả H3s "thuộc" 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
Tôi nhận ra đây là blob code lớn ném bạn, nhưng nó build off work bài trước. Nếu có câu hỏi về structure Jekyll plugins, hoặc cách dùng nokogiri, xem bài đó.
Khi chạy code này trên documentation site, tôi nhận hash như sau:
[{"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"}]}, ...
Bây giờ chỉ còn render cái này dùng liquid templates.
Render Subnav
Thực sự không khó render arbitrarily deep sub navigation dùng liquid templates. Trick là dùng partial render chính nó.
Trong layout, tôi render partial và pass collection navigation items.
{% include navigation_item.html collection=page.subnav level=0 %}
Partial tạo links cho level navigation này, rồi render chính nó, pass list children. Giống recursive function, lý thuyết có thể mãi mãi. Chỉ cho vui, tôi thêm code cho mỗi level subnav class như level-1 hoặc level-2. Rất hữu ích cho styling.
{% 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 %}
Đó là xong!
Đây kết thúc brief foray vào Jekyll wonderful world. Trong vài ngày tới sẽ publish series articles về Ruby internals, stay tuned!