Hugo 的巢狀 shortcode 處理
TL;DR
幾個重點如下
- 內層先渲染,才渲染外層
- 外層完全不知道內層的存在
- 內層可以用 .Parent 存取外層
- 如果要讓 nested 穩定運行,外層用 markdown notation
{{% %}},內層用 standard notation{{< >}} - 一定要啟用
markup.goldmark.renderer.unsafe = true,因為內層是 HTML
不用想去網路上找第二來源查證這段資訊了,因為文檔沒寫,網路上也幾乎沒有相關資訊。
正文
這是老生常談的問題,巢狀 shortcode 的困難點在於不知道哪個要用 Markdown 哪個要當作純 HTML,關鍵是要注意 {{< >}} 和 {{% %}} 語法選用,並且 shortcode 內部最好都要 trim spaces 避免空白被當成 markdown 渲染,而且即使做到這樣也不見得都順利可用。
比如說要建立 steps + step 組合型 shortcode,steps 是外部容器, step 是每個步驟,對於開發者來說,他有這些組合可能
- 兩種shortcode notation
{{< >}}{{% %}} - 是否使用 .Page.RenderString
如果要支援 shortcode 可渲染出 footnote/TOC,正確答案是外部使用 {{% %}},內部使用 {{< >}},並且都不要使用 .Page.RenderString,原理是內部 shortcode 先渲染變成 HTML,然後外部 shortcode 把內容放到 Markdown 裡面由 Goldmark 解析,這在文檔隻字不提,我是所有排列組合測試一次才知道該怎麼做。
Ordinal
Ordinal 用於排出 shortcode 序數,通常用於生成 uid。
nested shortcode 內部自己的 ordinal 是從零開始重新計算的。
UID
建立 shortcode 的 UID 以用於產生唯一的編號。這和本文無關,但是我不想為了這個資訊寫一篇獨立文章,因此先放在這裡。
直接給我認為最好的方案:
{{/* generate a unique id
Usage 1: pass the shortcode context only
{{ partial "uid.html" . }\}
Usage 2: pass a dict to include extra values in the hash source
{{ partial "uid.html" (dict "page" . "extra" (slice "foo" "bar")) }\}
*/}}
{{- $page := .Page -}}
{{- $extra := slice -}}
{{- if reflect.IsMap . -}}
{{- $page = .page -}}
{{- $extra = .extra | default slice -}}
{{- end -}}
{{- $count := $page.Store.Get "uid_count" | default 0 -}}
{{- $page.Store.Set "uid_count" (add $count 1) -}}
{{- $parts := slice $page.RelPermalink $count -}}
{{- $parts = $parts | append $extra -}}
{{- $raw := delimit $parts "-" -}}
{{- return hash.XxHash $raw -}}
這個 UID 實現最大的特色就是不依賴時間,確保多次構建只要內容相同,輸出就相同。
.Page 之所以能成功是因為 shortcode context 本身就可以再呼叫 .Page。這個方案用 $page.Store.Get "uid_count" 遞增以保證同一頁面永遠不碰撞,而且不依賴時間,不會因為不同的構建時間就讓你的 HTML 內容變來變去造成除錯時不必要的負擔。同時還支援 extra 輸入,在一些奇怪情境下就可以新增更多參數作為 hash source。
關於效能問題,reflect, append, delimit 也都是很輕量的原生 Go 操作。和 UnixNano 比較當然還是慢,但是這個方案本來就是要解決時間的問題。