Creating a TOC with mustache
I am working on porting mdbook to nim. It is called nimiBook. This is motivated by the work being done in sciNim/getting-started.
An issue is how to generate the TOC. Below I show a way to do it using mustache.
import
mustache, json, tables
var
context: Context
text: string
partials: Table[string, string]
data: JsonNode
An example result:
<ol class="chapter">
<li class="chapter-item expanded ">
<a href="./index.html" tabindex="0" class="active">
<strong aria-hidden="true">1.</strong> Introduction
</a>
</li>
<li class="chapter-item expanded ">
<a href="./basics/index.html" tabindex="0">
<strong aria-hidden="true">2.</strong> Basics
</a>
</li>
<li>
<ol class="section">
<li class="chapter-item expanded ">
<a href="./basics/plotting.html" tabindex="0">
<strong aria-hidden="true">2.1.</strong> Plotting
</a>
</li>
<li class="chapter-item expanded ">
<a href="./basics/data_manipulation.html" tabindex="0">
<strong aria-hidden="true">2.2.</strong> Data Manipulation
</a>
</li>
<li class="chapter-item expanded ">
<a href="./basics/models.html" tabindex="0">
<strong aria-hidden="true">2.3.</strong> Models
</a>
</li>
</ol>
<li class="chapter-item expanded ">
<a href="./misc/but/very/far/contributors.html" tabindex="0">Contributors</a>
</li>
</ol>
The basic element is the list item which is this template:
var listItem = """<li class="chapter-item expanded ">
<a href="{{path_to_root}}{{path_to_page}}" tabindex="0"{{#is_active}} class="active"{{/is_active}}>
{{#id}}<strong aria-hidden="true">{{id}}</strong> {{/id}}{{label}}
</a>
</li>
"""
<li class="chapter-item expanded ">
<a href="{{path_to_root}}{{path_to_page}}" tabindex="0"{{#is_active}} class="active"{{/is_active}}>
{{#id}}<strong aria-hidden="true">{{id}}</strong> {{/id}}{{label}}
</a>
</li>
And given some data we can render listItem
template:
var dataItem = %*{"path_to_root": "basics/", "path_to_page": "plotting.html",
"is_active": false, "id": "2.1", "label": "Plotting"}
context = newContext(values = dataItem.toValues)
echo listItem.render(context)
<li class="chapter-item expanded "> <a href="basics/plotting.html" tabindex="0"> <strong aria-hidden="true">2.1</strong> Plotting </a> </li>
context["is_active"] = true
echo listItem.render(context)
<li class="chapter-item expanded "> <a href="basics/plotting.html" tabindex="0" class="active"> <strong aria-hidden="true">2.1</strong> Plotting </a> </li>
context["id"] = false
echo listItem.render(context)
<li class="chapter-item expanded "> <a href="basics/plotting.html" tabindex="0" class="active"> Plotting </a> </li>
But a list item can also contain a section:
let section = """<ol class="section">
{{#items}}
{{> list_item }}
{{/items}}
</ol>
"""
<ol class="section">
{{#items}}
{{> list_item }}
{{/items}}
</ol>
Let's test it with some data:
var dataSection = %*{"is_section": true, "items": [{"path_to_root": "basics/",
"path_to_page": "plotting.html", "is_active": true, "id": "2.1",
"label": "Plotting"}, {"path_to_root": "basics/",
"path_to_page": "data_manipulation.html",
"is_active": false, "id": "2.2",
"label": "Data Manipulation"}, {
"path_to_root": "basics/", "path_to_page": "models.html",
"is_active": false, "id": "2.3", "label": "Models"}]}
context = newContext(partials = {"list_item": listItem}.toTable,
values = dataSection.toValues)
echo section.render(context)
<ol class="section"> <li class="chapter-item expanded "> <a href="basics/plotting.html" tabindex="0" class="active"> <strong aria-hidden="true">2.1</strong> Plotting </a> </li> <li class="chapter-item expanded "> <a href="basics/data_manipulation.html" tabindex="0"> <strong aria-hidden="true">2.2</strong> Data Manipulation </a> </li> <li class="chapter-item expanded "> <a href="basics/models.html" tabindex="0"> <strong aria-hidden="true">2.3</strong> Models </a> </li> </ol>
And the idea is that a list_item
is either a single item or a section:
listItem = """{{#is_section}}
<li>
<ol class="section">
{{#items}}
{{> list_item }}
{{/items}}
</ol>
</li>
{{/is_section}}
{{#label}}
<li class="chapter-item expanded ">
<a href="{{path_to_root}}{{path_to_page}}" tabindex="0"{{#is_active}} class="active"{{/is_active}}>
{{#id}}<strong aria-hidden="true">{{id}}</strong> {{/id}}{{label}}
</a>
</li>
{{/label}}
"""
Note that the partial is recursive!
We only need the final toc template:
let toc = """<ol class="chapter">
{{#chapters}}
{{> list_item}}
{{/chapters}}
</ol>
"""
data = %*{"chapters": [{"is_section": false, "path_to_root": "./",
"path_to_page": "introduction.html", "is_active": true,
"id": "1.", "label": "Introduction"}, {
"is_section": false, "path_to_root": "./",
"path_to_page": "basics/index.html", "is_active": false, "id": "2.",
"label": "Basics"}, {"is_section": true, "items": [{"is_section": false,
"path_to_root": "./", "path_to_page": "basics/plotting.html",
"is_active": true, "id": "2.1", "label": "Plotting"}, {"is_section": false,
"path_to_root": "./", "path_to_page": "basics/data_manipulation.html",
"is_active": false, "id": "2.2", "label": "Data Manipulation"}, {
"is_section": false, "path_to_root": "./",
"path_to_page": "basics/models.html", "is_active": false, "id": "2.3",
"label": "Models"}]}, {"is_section": false, "path_to_root": "./", "path_to_page": "misc/but/very/far/contributors.html",
"is_active": false, "label": "Contributors"}]}
context = newContext(partials = {"list_item": listItem, "toc": toc}.toTable,
values = data.toValues)
echo "{{>toc}}".render(context)
<ol class="chapter"> <li class="chapter-item expanded "> <a href="./introduction.html" tabindex="0" class="active"> <strong aria-hidden="true">1.</strong> Introduction </a> </li> <li class="chapter-item expanded "> <a href="./basics/index.html" tabindex="0"> <strong aria-hidden="true">2.</strong> Basics </a> </li> <li> <ol class="section"> <li class="chapter-item expanded "> <a href="./basics/plotting.html" tabindex="0" class="active"> <strong aria-hidden="true">2.1</strong> Plotting </a> </li> <li class="chapter-item expanded "> <a href="./basics/data_manipulation.html" tabindex="0"> <strong aria-hidden="true">2.2</strong> Data Manipulation </a> </li> <li class="chapter-item expanded "> <a href="./basics/models.html" tabindex="0"> <strong aria-hidden="true">2.3</strong> Models </a> </li> </ol> </li> <li class="chapter-item expanded "> <a href="./misc/but/very/far/contributors.html" tabindex="0"> Contributors </a> </li> </ol>
It works 🎉! I have to be very careful. At first I did not put is_section: false
in the single items, and the rendering went into a loop.
How does this help in generating a toc for nimiBook?
Well, the idea is that from a user-defined file, every page is
able to create the data
json above and put the is_active
in the correct place.
The two partials toc
and list_item
are then fixed mustache templates.
The user-defined file could be a SUMMARY.md
like
the one found in mdbook.