Hexo 博客無(wú)法復(fù)制 Markdown 本地圖片?我寫了一個(gè)插件

不知道現(xiàn)在大家寫博客、文章還多不多,我一直在用 Obsidian + Markdown 寫文章,然后用 Hexo 生成靜態(tài)站點(diǎn)發(fā)布到 GitHub Pages,綁定到域名 xiaoming.io。

幾年前我寫過(guò)一篇文章,分享我是怎么構(gòu)建筆記和博客系統(tǒng)的。

構(gòu)建自己的筆記博客系統(tǒng)(程序員版) | 搬磚的小明

[圖片上傳失敗...(image-972663-1776605940103)]

遇到問(wèn)題

最近我在 Obsidian 里面用了一個(gè)新插件,它會(huì)把我本地的圖片轉(zhuǎn)成 WebP 格式保存。這樣能大大節(jié)省存儲(chǔ)空間,圖片畫質(zhì)也還不錯(cuò)。

用了 WebP 之后,我發(fā)現(xiàn)原先那套發(fā)博客的代碼只支持JPG、PNG、GIF,沒(méi)有正常把 WebP 圖片復(fù)制到發(fā)布目錄里。

我的第一反應(yīng)是讓 Codex 直接幫我解決這個(gè)問(wèn)題,它 Debug 了半天,沒(méi)有什么進(jìn)展,看來(lái)還是需要我人工參與一下。

我又重新研究了一下 Hexo,已經(jīng)很多年沒(méi)研究它了。之前它有個(gè)問(wèn)題,在本地寫 Markdown 時(shí),如果用相對(duì)路徑引用本地圖片,在有些情況不會(huì)把圖片復(fù)制到 public 目錄。

幾年前,為了解決這個(gè)問(wèn)題,我魔改了 Markdown 的渲染器,也就是 hexo-renderer-markdown-it。在它解析完圖片以后,我會(huì)把每一張圖片都復(fù)制到固定目錄,同時(shí)修改渲染的圖片 URL 讓其對(duì)應(yīng)上。

當(dāng)時(shí)之所以這么做,是因?yàn)槲野l(fā)現(xiàn) Hexo 本身的 API 不會(huì)把每個(gè) post 里的圖片都暴露給我。為了拿到一篇 Markdown 里所有圖片的路徑,我就不得不在渲染器里插一段自己的代碼,來(lái)實(shí)現(xiàn)這個(gè)效果。

但這種實(shí)現(xiàn)思路非常不好,耦合性太強(qiáng)。我改了現(xiàn)有渲染器以后,只要渲染器一更新,我又得重新改。這么多年來(lái),我一直用的都是舊版本渲染器,沒(méi)有更新。

最近我又看了一下,時(shí)隔多年,Hexo 還是沒(méi)有原生很好的解決這個(gè)問(wèn)題。盡管它有一個(gè) post_asset_folder 參數(shù),但這個(gè)參數(shù)只支持和 Markdown 文件同名的文件夾里的圖片上傳,也就是推薦在 Hexo 里面創(chuàng)建 Markdown,這太局限了。

https://hexo.io/zh-cn/docs/asset-folders

我也嘗試在網(wǎng)上找現(xiàn)成插件,確實(shí)有一些插件支持這個(gè)功能,例如 hexo-asset-image,但這個(gè)項(xiàng)目已經(jīng)無(wú)效并且不維護(hù)了。有人Fork了一個(gè)版本修復(fù),但是看起來(lái)和我想要的效果不太一樣。

決定重新開(kāi)發(fā)(第一版)

所以我決定讓 Codex 幫我開(kāi)發(fā)一個(gè)。我本來(lái)不知道這件事會(huì)不會(huì)很復(fù)雜,沒(méi)想到 Codex 直接幫我寫了一個(gè)不到 100 行的 script,放進(jìn)項(xiàng)目里以后,圖片確實(shí)就能復(fù)制了,比我原先的實(shí)現(xiàn)優(yōu)雅很多。

Codex 的實(shí)現(xiàn)思路很直接,就是拿到 Markdown 源文件,然后用幾個(gè)正則表達(dá)式去找里面所有圖片,再把它們復(fù)制到目標(biāo)目錄。

其實(shí)當(dāng)年我一開(kāi)始想到的也是這種方式。正則表達(dá)式比較難寫,擔(dān)心寫錯(cuò),識(shí)別出錯(cuò)誤圖片或者漏掉,所以當(dāng)時(shí)就直接走了魔改 Markdown 渲染器這條路,快速把問(wèn)題解決了。

但在今天,AI 寫代碼已經(jīng)這么強(qiáng)了,讓 Codex 直接實(shí)現(xiàn)一個(gè)新的插件,反而是一個(gè)更快、更簡(jiǎn)單的辦法。

于是我把這個(gè)插件發(fā)布到了 NPM 上,也把我原來(lái)那個(gè)魔改插件去掉了,重新改回原版的 Markdown 渲染器,并且升級(jí)到了最新版。

后來(lái)我又去 Hexo 的 GitHub 主頁(yè)看了一下,到現(xiàn)在還是一直有人提這個(gè)問(wèn)題。我把這個(gè)插件網(wǎng)址回復(fù)在了那個(gè) GitHub Issue 里。不知道這些提 Issue 的人現(xiàn)在還在不在用 Hexo 寫博客,還是已經(jīng)遷移到別的工具上了。

發(fā)現(xiàn)更多問(wèn)題(第二版)

當(dāng)我實(shí)際用這個(gè)插件時(shí),發(fā)現(xiàn)它對(duì)圖片的判斷還是太簡(jiǎn)單了。Markdown 里引用圖片有很多種情況。

大概有這么幾類:

  1. 相對(duì)路徑,比如當(dāng)前目錄、子目錄,或者父目錄里的圖片

  2. 絕對(duì)路徑,比如以斜線開(kāi)頭的 Linux 路徑,或者 Windows 上帶盤符的路徑

  3. 不是本地文件路徑的情況,比如 data: 這種 Base64 形式、# 開(kāi)頭的路徑,或者 http、https 這種鏈接

我去和 Codex 討論,它只處理了 https 鏈接等場(chǎng)景,還有一些場(chǎng)景都沒(méi)有處理。

于是我就和 Codex 繼續(xù)討論,把我想到的場(chǎng)景都補(bǔ)充進(jìn)去了,也寫了針對(duì)這些場(chǎng)景的測(cè)試代碼。

寫 test 也有一些細(xì)節(jié)。因?yàn)檫@是一個(gè) Hexo 插件,理論上最徹底的測(cè)試方式,應(yīng)該是端到端測(cè)試:準(zhǔn)備一個(gè)真實(shí)的 Hexo 環(huán)境,放一些測(cè)試用的 Markdown 文件,運(yùn)行插件和編譯過(guò)程,再判斷處理結(jié)果對(duì)不對(duì)。

但 Codex 認(rèn)為這樣做會(huì)依賴 Hexo 的版本,太重了,比較容易出現(xiàn)兼容問(wèn)題。所以它建議我用集成測(cè)試:Mock 一個(gè)假的 Hexo 環(huán)境調(diào)用整個(gè)插件,判斷它是不是按照設(shè)計(jì)的方式工作,有沒(méi)有準(zhǔn)確復(fù)制 Markdown 文件里引用的圖片。

當(dāng)時(shí)我覺(jué)得 Codex 說(shuō)的有道理,集成測(cè)試也就夠了,因?yàn)橹饕菧y(cè)試這些格式的解析邏輯。

于是第二版就發(fā)出來(lái)了。

更全面系統(tǒng)的設(shè)計(jì)(第三版)

在我寫這篇文章的時(shí)候,又去網(wǎng)上搜了一些相關(guān)的資料,包括 Hexo 官方文檔,我發(fā)現(xiàn)事情沒(méi)有這么簡(jiǎn)單。

例如 Hexo 里 relative_link 的配置選項(xiàng)可能會(huì)導(dǎo)致渲染不一樣,還有些用戶可能會(huì)在圖片文件名里有特殊字符等。

我覺(jué)得有必要用更系統(tǒng)化的方式去實(shí)現(xiàn)這個(gè)工具。

在之前,我用 AI 做過(guò)好多個(gè)小工具,說(shuō)實(shí)話,這個(gè)應(yīng)該是真實(shí)解決現(xiàn)實(shí)問(wèn)題當(dāng)中功能最簡(jiǎn)單的一個(gè)項(xiàng)目。

但是正因?yàn)樗亲詈?jiǎn)單的一個(gè),我反而可以更清晰地學(xué)習(xí)如何用 AI 開(kāi)發(fā)項(xiàng)目。因?yàn)樗恳粋€(gè)步驟都相對(duì)簡(jiǎn)單,我可以把重點(diǎn)放在思考開(kāi)發(fā)流程上,而不是讓注意力陷入過(guò)多設(shè)計(jì)實(shí)現(xiàn)細(xì)節(jié)里出不來(lái)。

同時(shí)它也不是簡(jiǎn)單到完全不需要思考的程度,它也在解決一個(gè)有多種可能分支的現(xiàn)實(shí)問(wèn)題,有一定的研究?jī)r(jià)值。

1、確定核心目標(biāo)

首先是理清這個(gè)項(xiàng)目的核心目標(biāo)。核心目標(biāo)的重點(diǎn)不只是這個(gè)項(xiàng)目要做什么,還要關(guān)注這個(gè)項(xiàng)目不做什么。

這個(gè)項(xiàng)目的名字叫 hexo-relative-post-image,顧名思義,它的核心目標(biāo)應(yīng)該是只處理相對(duì)路徑引用的圖片文件。如果是絕對(duì)路徑,我們直接不處理,并且在 log 里面報(bào)一個(gè) warning,通知用戶自行處理。

我們的設(shè)計(jì)思路是,只做圖片復(fù)制,不去干涉 Markdown 渲染成 HTML 的過(guò)程。

2、確定調(diào)研方向并調(diào)研

確定了這個(gè)核心目標(biāo)以后,我們需要對(duì) Markdown 有關(guān)的東西進(jìn)行足夠深入、系統(tǒng)的調(diào)研。

這里面包括 CommonMark 對(duì) Markdown 語(yǔ)法的規(guī)定是什么樣的、支持什么樣的圖片引用方式。

現(xiàn)有的 marked 和 markdown-it 這兩個(gè)渲染器是怎么實(shí)現(xiàn) Markdown 圖片渲染的,這兩個(gè)渲染器在 Hexo 上又有什么樣的圖片相關(guān)的配置參數(shù)。

目前已有的 Hexo 渲染器,用戶都報(bào)了什么樣的 Issue,能從這個(gè) Issue 里面學(xué)習(xí)到哪些兼容性問(wèn)題。

我讓 AI 把這些調(diào)研來(lái)源和結(jié)果都放在了 Reference 文件中。

3、Spec 開(kāi)發(fā)

在這個(gè)調(diào)研完成以后,就需要做一個(gè)詳細(xì)的需求文檔,一般稱為 Spec (Specification,產(chǎn)品規(guī)格)。這個(gè) Spec 里面除了有項(xiàng)目的核心目標(biāo),還有從調(diào)研結(jié)果學(xué)來(lái)的東西,有哪些語(yǔ)法格式需要考慮,有哪些可能的兼容性問(wèn)題需要處理。

根據(jù)我們前面定的核心目標(biāo),有些兼容問(wèn)題是可以在插件里處理的,而有些兼容問(wèn)題則是和我們的設(shè)計(jì)目標(biāo)沖突的,應(yīng)該不做處理,而是直接報(bào)錯(cuò)拋給用戶,例如絕對(duì)路徑引用圖片。

一開(kāi)始我并沒(méi)有把這個(gè)項(xiàng)目當(dāng)成一個(gè)正經(jīng)項(xiàng)目去做,只是從一個(gè) 100 行的小腳本慢慢補(bǔ)全了,所以前面的版本也沒(méi)有 Spec。但是現(xiàn)在既然要更系統(tǒng)地實(shí)現(xiàn)這個(gè)項(xiàng)目,Spec 就是必要的。

一開(kāi)始都是跟 AI 用幾句話來(lái)描述需求,但是當(dāng)需求的邊界條件、各種分支越來(lái)越多,幾句話就描述不清楚了。如果下次需要 AI 修改維護(hù),Spec 就是最重要的依據(jù)。聊天結(jié)束了,上下文就沒(méi)了,而 Spec 是以文件形式保存在項(xiàng)目里的,一直在。

4、代碼實(shí)現(xiàn)

有了這個(gè) Spec 以后才是寫代碼,代碼分為插件本身的實(shí)現(xiàn)代碼和測(cè)試代碼兩塊。

實(shí)際上這個(gè)插件本身的代碼量并不是很大,只有一個(gè)幾百行的文件。但它的文檔以及它的測(cè)試代碼反而比實(shí)現(xiàn)代碼更復(fù)雜。

5、測(cè)試

在測(cè)試代碼的實(shí)現(xiàn)上,之前的集成測(cè)試也滿足不了需求了,因?yàn)楝F(xiàn)在已經(jīng)明確發(fā)現(xiàn)了不同的渲染插件可能也會(huì)有兼容性問(wèn)題。

所以我讓 AI 又加了真正的端到端測(cè)試,直接調(diào)用 Hexo 去驗(yàn)證兼容性問(wèn)題。

6、README 文檔

最后,還有一個(gè)精簡(jiǎn)的 README,是中英雙語(yǔ)版的,為了更加國(guó)際化,默認(rèn)英文版。README 分為兩塊。

1、給普通用戶看的,要告訴用戶怎么配置這個(gè)項(xiàng)目,支持什么場(chǎng)景,什么是不支持的。

2、給想要參與開(kāi)發(fā)項(xiàng)目的人和 AI 看的。會(huì)介紹環(huán)境配置、開(kāi)發(fā)約定、具體的發(fā)布流程。

實(shí)際上我的開(kāi)發(fā)和發(fā)布都是讓 AI 完成的,這個(gè)時(shí)候文檔就非常重要了,如果沒(méi)有文檔,AI 沒(méi)辦法每次都按照固定的流程開(kāi)發(fā)和發(fā)布項(xiàng)目,到時(shí)候就亂了。

一點(diǎn)思考

這是一個(gè)很簡(jiǎn)單的小項(xiàng)目,用 AI 幫忙寫也很輕松。但你會(huì)發(fā)現(xiàn),即使是這樣的小項(xiàng)目,也還是需要人工參與,而且人工參與的程度并不低。

這不是我不會(huì)用 AI,而是很多實(shí)際問(wèn)題本身就帶著一堆隱性的背景知識(shí)。

比如 Markdown 引用圖片這件事,由于Markdown編輯器五花八門,用戶真的會(huì)用各種奇奇怪怪的方式去寫,不只是標(biāo)準(zhǔn)語(yǔ)法,還可能有 HTML 標(biāo)簽、文件名有特殊字符、不同語(yǔ)法格式、網(wǎng)絡(luò)圖片,甚至不同渲染器還有自己的擴(kuò)展。這些東西本身就沒(méi)有一個(gè)固定范圍。

AI 默認(rèn)不會(huì)把這些情況全都想一遍,它一般只會(huì)給你一個(gè)“常見(jiàn)寫法”的方案,而不會(huì)一上來(lái)就幫你把所有邊界情況都覆蓋掉。

如果要讓 AI 自主探索所有可能的情況,復(fù)雜度可能會(huì)指數(shù)級(jí)增加。

這時(shí)候人的作用就體現(xiàn)出來(lái)了。我會(huì)用自己的經(jīng)驗(yàn)去引導(dǎo) AI,比如我會(huì)想到要看現(xiàn)有生態(tài),比如 CommonMark 語(yǔ)法規(guī)范,Marked、Markdown-it 這些主流渲染庫(kù),相似插件以及它們的 Issue 里別人踩過(guò)的坑。

這些東西 AI 不是查不到,而是如果你不說(shuō),它通常不會(huì)主動(dòng)意識(shí)到“這一步是必要的”,也不會(huì)自己把這些上下文串起來(lái)。

本質(zhì)上就是,有些問(wèn)題需要很大的上下文才能想清楚,而這些上下文很多是隱性的,不是直接搜就能出來(lái)的。

人能做的,是主動(dòng)把這些上下文補(bǔ)上,確定問(wèn)題范圍;AI 更擅長(zhǎng)的是,在這個(gè)范圍里面把事情做快、做全。

那么利用 AI 開(kāi)發(fā)項(xiàng)目的過(guò)程就是這樣的:

1、我會(huì)利用我的知識(shí)經(jīng)驗(yàn),引導(dǎo) AI 去做調(diào)研,確定問(wèn)題范圍,讓 AI 寫 Spec,寫完了我再核查,再讓它修改,直到 Spec 達(dá)到我比較滿意的程度。

2、讓 AI 根據(jù) Spec 把代碼實(shí)現(xiàn)出來(lái)。理論上只要 Spec 寫得足夠完備,代碼實(shí)現(xiàn)就已經(jīng)不重要了,不需要太多人工參與就能被 AI 自動(dòng)完成。甚至可以把 AI 當(dāng)成編譯器來(lái)看待,AI 把 Spec 編譯成代碼,就像過(guò)去編譯器把高級(jí)語(yǔ)言編譯成機(jī)器碼一樣。

3、在編譯過(guò)程中,如果由于 Spec 寫的有硬性的邏輯問(wèn)題導(dǎo)致編譯報(bào)錯(cuò),或者最終運(yùn)行出來(lái)的效果和設(shè)想的不一樣,我不會(huì)單獨(dú)去干預(yù)代碼,而是去完善 Spec,然后重新編譯成代碼,可以是增量編譯的過(guò)程,不用全部重寫。

整個(gè)項(xiàng)目的維護(hù)核心,從過(guò)去的代碼,開(kāi)始轉(zhuǎn)向 Spec。用 AI 的話來(lái)說(shuō),Spec 是 Single Source of Truth。Spec 可能需要花很長(zhǎng)的時(shí)間去開(kāi)發(fā),并且它是人工參與程度最高的文件。

之后無(wú)論是實(shí)現(xiàn)代碼,還是測(cè)試代碼,還是 README,只要有不對(duì)的,就應(yīng)該從 Spec 里面找參考依據(jù)。每次修改項(xiàng)目的時(shí)候,都應(yīng)該先改 Spec,然后才是代碼和 README。

項(xiàng)目鏈接

如果你也在用 Hexo,遇到類似問(wèn)題沒(méi)解決,可以留意一下這個(gè)插件。

我發(fā)現(xiàn)似乎還有人在用我以前開(kāi)發(fā)的那個(gè)版本的 Markdown 渲染器,因?yàn)槲铱吹?NPM 上還有下載量。我把原先那個(gè)項(xiàng)目標(biāo)記成了 Deprecated,也鏈接到了這個(gè)新項(xiàng)目。

如果你是單純想了解這個(gè)項(xiàng)目的 Reference、Spec 和 Test 是怎么寫的,也可以在項(xiàng)目中看到。

這個(gè)項(xiàng)目的 GitHub 主頁(yè)在這里:

https://github.com/jzj1993/hexo-relative-post-images

如果覺(jué)得文章有幫助,歡迎分享轉(zhuǎn)發(fā),也歡迎關(guān)注我的公眾號(hào)“搬磚的小明”,及時(shí)獲取更新

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容