變更記錄
- 2019-06-08
- 修正「解决 jekyll 中文换行变成空格的问题」這篇文章的連結。
想寫 Blog 很久了,一直覺得該找個地方記錄一下腦子裡的想法,不然我記性超級差,隔天就忘了自己到底在忙什麼。
那要用什麼寫?
一般的 Blog 服務當然是不考慮,對時常要放程式碼的人來說完全不適合。
在很潮的 Medium 上寫過一篇文章,沒有原生支援 Code Syntax Highlighting 讓人非常消火,每次都得用 GitHub Gist 放程式碼實在有點麻煩。而且用 Vim + Markup Language 寫文件寫習慣了,對必須用滑鼠改格式這件事覺得不太順手。
Google 了一下,身為 Python 工程師及愛好者,用 Pelican 寫然後架在 GitHub Pages 上似乎是最好的選擇:
- 用 Python 寫的,可以用 Python 擴充和修改功能
- 支援 reStructuredText 及 Markdown
- 支援 Disqus 和 Google Analytic 等其他好用的服務
- 支援許多主題: Pelican Themes
讚,那就開始寫吧!
當 Pelican 遇上中文
平常文件都是用英文在寫,當開始用 reStructuredText 寫起中文立刻覺得不太對勁…好像出現了很多不該出現的空格?
換行變成了空格
我是龜毛人,原始碼的行數不超過 80 個字元是基本,最多也不能超過 100,所以很長的一個段落會用多行表示:
我是龜毛人,原始碼的行數不超過 80 個字元是基本,最多也不能超過 100,
所以很長的一個段落會用多行表示:
reStructuredText 和 Markdown 都會保留這個換行字元到轉換後的 HTML 中:
<p>我是龜毛人,原始碼的行數不超過 80 個字元是基本,最多也不能超過 100,
所以很長的一個段落會用多行表示:</p>
而瀏覽器在遇到這樣的換行字元時會將他轉換為空格,因為 Spec 就是這樣規定:
An HTML user agent should treat end of line in any of its variations as a word space in all contexts except preformatted text.
這件事在英文很合理,但到了中文就不合理了,因為我們不會用空格把文字隔開。於是乎,上面的例子瀏覽器會顯示為:
我是龜毛人,原始檔的行數不超過 80 個字元是基本,100 則是最大值, 所以很長的一個段落會用多行表示:
注意到「所以」前面多了一個空格。如果你習慣很好,只有在使用標點符號之後才會換行,那看起來影響不大。但如果換行是介於兩個中文字之間,那就會 像這樣在文字間出現詭異的空格。
Inline Markup 的空格
不像 Markdown,reStructuredText 要求必須用空格 (或其他類似功能的字元)將 Inline Markup 與其他的文字區隔開來:
This is **inline markup** bold.
這樣的空格會被保留到 HTML:
<p>This is <strong>inline markup</strong> bold.</p>
跟上面提到的一樣,這空格在英文沒差,中文就不行了。不過江湖在走,Workaround 要有,最簡單的方法是自己用 \ 來「跳脫」這個空格:
This is\ **inline markup**\ bold.
但每次都要手動加入這反斜線實在有點麻煩。如果這空格能自己消失,那該有多好。
Bonus:中英文間的空格
有研究顯示,打字的時候不喜歡在中文和英文之間加空格的人,感情路都走得很辛苦,有七成的比例會在 34 歲的時候跟自己不愛的人結婚,而其餘三成的人最後只能把遺產留給自己的貓。畢竟愛情跟書寫都需要適時地留白。 —— vinta/pangu.js
…這種空格我個人是覺得還可以接受啦,不過如果 Pelican 能自動幫我加上這些空格,那我就不用擔心未來會跟不愛的人結婚了。寫程式真是份偉大的工作。
寫個 Pelican Plugin 吧!
原本想說可以從處理 reStructuredText 的函式庫 docutils 下手,無奈功力不夠高深,看不出來到底該怎麼修改他的行為,只好從 Pelican 下手。
之前提到 Pelican 能夠用 Python 自己擴充功能,而在官方的 pelican-plugins 列表中搜尋了一下只有 cjk-auto-spacing 能夠自動調整中英文間的空格,但還是沒有解決所有的問題。Google 了一下找到這篇「解决 jekyll 中文换行变成空格的问题」,但他是用 Jekyll 而不是 Pelican。安捏…不如自己寫一個吧!
Pelican Plugin 的運作方式
Pelican 定義了各種「信號」(Signal),代表了從原始碼到最後生出 HTML 的各個階段。你可以將自己寫的 Python 函式註冊到這些信號上,Pelican 就會在那些信號對應的階段發生時呼叫你的函式,並將當下的狀態或處理的物件傳進這個函式,讓你的函式能夠調整 Pelican 的行為。細節和信號列表請參考 Pelican Plugin Document 。
前面提到了 cjk-auto-spacing ,理所當然拿他來參考一下。它處理的方式是使用信號 content_object_init 來取得 content_object 物件,而這個物件的 _content 屬性存放了從 reStructuredText 及 Markdown 原始碼轉換而來的 HTML ,以 str 儲存。我們可以根據需求來調整這個 HTML,調整完後再 assign 回 _content ,Pelican 就會用這份新的 HTML 繼續之後的工作。
舉例來說,如果我們想把 HTML 裡的所有 <p> Tag 換成 <foo> ,可以很快的用 Regular Expression 來達成:
import re
from pelican import signals
def process(content):
new_content = re.sub(r'<(/)?p>', r'<\1foo>', content._content)
content._content = new_content
def register():
signals.content_object_init.connect(process)
Pelican 規定每個 Plugin 都必須要有 register 函式,目的在指定你需要哪些信號以及他們要觸發的函式。
Pelican-CJK
花了些時間用 Regular Expression 刻了一個能夠自動處理以上問題的 Plugin: pelican-cjk 。它能夠自動根據你寫的內容調整 HTML,解決上述那些小毛病。
在開發這個 Plugin 的時候考慮了以下幾點:
- 必須支援 reStructuredText 及 Markdown
- 不想依賴其他第三方模組
如果要從原始碼( .md 與 .rst )或 Parser 下手,就還得考慮 reStructuredText 和 Markdown 的差異,所以如果兩個都得支援,直接從 HTML 下手會好處理很多。
而基於第二點, Beautiful Soup 等等能夠幫助處理 HTML 的模組也就不考慮了,而 Python 內建的 HTML Parser 又太陽春,所以最後我直接用 Regex 來處理。但這不免有些小問題:
- 無法判斷目前要調整的文字屬於那種區塊。reStructuredText 和 Markdown 都有所謂的「Literal Block」,在這個區塊內是不會處理任何標記的。 但因為程式無法根據 HTML 判斷區塊,它一樣會調整這個區塊內的文字。 不過 Literal Block 通常是用來放範例程式碼的,比較不會出現中英混用的情況,所以就我認為影響不大。
- 透過上述信號拿到的 HTML 不包含文章的標題,所以標題無法調整,得自己加入中英文間的空格。這應該可以透過其他信號取得,但我還沒研究。
- 為了簡單起見,我寫的 Regex 不會針對以下情況調整空格:
- 巢狀 Inline Markup:reStructuredText 不允許這種情況,也就是說 HTML 中不會出現 English<em><strong>斜體又粗體</strong></em> 這樣的東西。但 Markdown 允許,所以這是有機會出現的。以這個例子來說,「English」與「斜體又粗體」間就不會自動加空格。
- 連續 Inline Markup: <em>English</em><strong>很強</strong> 連續的兩個 Inline Markup 也需要額外判斷,而且使用情況也不多,所以在此也不考慮。
希望這個 Plugin 能夠幫助更多跟我一樣毛很多的人,如果大家有什麼更好的方法也歡迎一起討論。