Hugo 的巢狀 shortcode 處理
TL;DR
幾個重點如下
- 內層先渲染,才渲染外層
- 外層完全不知道內層的存在
- 內層可以用 .Parent 存取外層
- 如果要讓 nested 穩定運行,外層用 markdown notation
{{% %}},內層用 standard notation{{< >}}
不用想去網路上找第二來源查證這段資訊了,因為文檔沒寫,網路上也幾乎沒有相關資訊。
正文
這是老生常談的問題,巢狀 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 比較當然還是慢,但是這個方案本來就是要解決時間的問題。
抱怨
當你想做到 step 裡面包更多 shortcode、被 shortcode 包、支援 nesting shortcode 內容支援和 GoldMark 交互(支援 footnote),這就有得你搞,請注意這不是什麼過分的需求,比如說在 steps 裡面放 alert,或是 tabs/tab 組合時,一個 tab 放源碼,一個 tab 放 result,這些在 Hugo 裡面都很困難。
作為用戶也很麻煩,要用 {{< >}} 還是 {{% %}} 完全沒有任何記憶點,你只能純靠死背或是每次都翻文檔。撰寫也很麻煩,nesting 很多時候需要 indent 讓你好閱讀和編輯,但是 {{% %}} 會把內容丟給 GoldMark,因此 indent 還不能超過兩層(四個空隔),.InnerDeindent 對 {{% %}} 實測完全沒用,這一切都要用背的或是踩過才知道。
更糟糕的是 nesting 一定要開啟 markup.goldmark.renderer.unsafe = true,因為內部 shortcode 通常是被渲染成 HTML 交給外部 shortcode 處理,這代表 unsafe 這個避免寫手注入 HTML 內容的限制一定會被關掉。開發者麻煩,用戶麻煩,寫手也麻煩。
甚至 AI 對於兩種語法是毫無概念的,AI 只知道照本宣科沒有思考能力,你特別問他他會知道然後把概念唸一遍給你聽,但是不特別問他他就依照機率推斷概率永遠是 {{< >}} 遠大於 {{% %}},永遠都讓你用 {{< >}} 語法,因此就算問 AI 也沒用。
最糟糕的就是這些東西如果文檔清楚說明大家可以 link 一個頁面所有人要用到的時候去看就好了,但是沒有,Hugo 的文檔就是一坨,這代表所有 Hugo 知識都只能作為個人碰過才知道而不是看完文檔就知道可以避免。
反過來說因為 JS 生態系很多人用,AI 看的多相關的問題大部分也都能解決,西瓜偎大邊這句話的含金量還在提高。
- https://discourse.gohugo.io/t/nested-shortcode-rendering/56012/
- https://gohugo.io/content-management/shortcodes/#notation
- https://discourse.gohugo.io/t/arbitrarily-nested-shortcodes/30424
- https://discourse.gohugo.io/t/html-shortcodes-are-rendered-as-markdown-when-nested-inside-markdown-shortcodes-is-there-a-workaround/38573
- https://discourse.gohugo.io/t/nested-shortcodes-render-markdown-in-html-content-file/45536
- https://discourse.gohugo.io/t/how-to-render-both-shortcode-and-markdown-in-shortcode/47740