NodeJS and ES Module

香港 2016

今天看了 TC39 一月會議的 Agenda 後才注意到,nodejs 用的 CommonJS ModuleECMAScript Module(ES Module) 在特定情況下會有混淆的情形發生,所謂的特定情形就是沒有import/require也沒export/exports的模組,例如寫東西在 root 物件上,只產生 side effect 的模組:

(function (root) {

  root.lib = {};

}(this));

像這樣的檔案,Parser 就無法判斷他是 CommonJS Module 還是 ES Module,這樣會產生什麼問題呢,其實 ES Module 有一些特色,例如它必須要使用 strict mode 來解析並執行,而光是這個差異,就會讓相同的程式碼有不一樣的執行結果了,而需要同時支援 CommonJS Module 和 ES Module 的主要是 NodeJS 環境,當然它目前還沒有兩種都支援,但是勢必需要支援 ES Module 的,所以 NodeJS 需要能夠百分之百正確的判斷每個 JavaScript 程式碼是屬於 CommonJS Module 還是 ES Module,這在目前是辦不到的,也因為這個問題所以 NodeJS 雖然已經支援大部分的 ES2015 的新功能,但卻遲遲還無法支援 ES Module,相關的討論至少也半年有了,當時還提出了新的副檔名.mjs這種解法,多一種副檔名聽起來有點不可思議,也引此還有個 Twitter 帳號專門在關注相關情報的,不過目前最新的解決方法,則是 ES Spec 修改 Module 的 Grammer 來解決這個問題,修改的方式是就是以後 ES Module 一定要至少有一個import或是exportstatement,如果是上面那種沒有需要 import 也沒有 export 的模組,那就要加上export {},變成:

(function (root) {

  root.lib = {};

}(this));

export {};

語意上剛好等於沒有匯出任何東西,所以不會和現在的 ES2015 版的 Module 有衝突,這份提案已經是 accept 狀態了,所以沒意外應該今年的 ES2017 就會包含進去了,當然這會影響到以前寫出這種 ES Module 的程式碼,不過目前也還沒有那個環境有直接使用 ES Module 的能力,都還是先過 bundler 轉成現在環境可以使用的形式,Web 的<script type="module">也才正要有瀏覽器支援,所以這個時間點做出這個修改影響還算是很小,之後大概就是有記得應該就沒問題了,我自己是比較期待 nodejs 能快點原生支援 ES Module 啦。


smartypants.js

大阪新年

SmartyPants 這個東西也是很久了,和 Markdown 差不多時期,都是 John Gruber 幫當時的網路文字出版軟體(ex: MovableType)所做的,而 SmartyPants 是用來處理一些標點符號的,其中,最容易被人注意到的就是引號「"」的轉換了。

雖然鍵盤上的引號按鍵只有一個,但是傳統的文書寫作上,引號是有分左邊(開始)的「“」和右邊(結束)的「”」,只不過早期為了減少鍵盤按鍵數,還有字元編碼上的限制,所以合併成為只有一個,不過隨著計算機的發展,可以使用的字元編碼資料量增加後,就還是有定義了開始和結束的兩種引號,並且有單引號和雙引號兩種:

  • 左單引號 ‘
  • 右單引號 ’
  • 左雙引號 “
  • 右雙引號 ”

理想上,寫作文章時也應該正確的使用這些引號,不過其實因為輸入上比較麻煩,一般人打字也不會特別注意,所以常常被忽略,比較常見的是軟體本身在使用者輸入文字時自動做轉換,例如 Apple 的 Pages、Keynote、微軟的 Skype 等等,講到 Keynote 自動轉換引號這點就要另外岔題一下,就是偶爾都會看到頭影片裡面的程式碼,其中的引號也被轉換過,其實就是因為貼上 code 到 Keynote 的時候被轉換了,如果作者沒有注意到的話就直接釋出,然後讀者 copy 程式碼出來試試看時,就會編譯失敗而無法執行。回到標點符號上,這類標點符號其實不止有引號,SmartyPants 可以處理的還包括:

  • --轉成 en-dash –
  • ---轉成 em-dash —
  • ...轉成 ellipsis …

而且它對於引號的轉換還算是聰明,會判斷是不是真的用來包起文字的,還支援用 backtick 來模擬的雙引號,看起來像:

``quoted string''

另為也會針對年代的特殊寫法做處理,例如'80s會轉成 ’80s,並且會避開 HTML 標籤的部分,不會把 HTML 標籤裡面的屬性值的引號也做轉換,像是<html lang="en">這種。前陣子因為工作上要處理翻譯字串,想要順便好好的處理這些符號,所以就想到了這個工具,因為我一般寫小 script 做事情是用 JavaScript 然後用 node 來執行,所以要用 SmartyPants 就沒辦法那麼直接了,一開始先找 JavaScript 的 solution,找到一個簡單的,只用幾條 regular epxression 的版本,其實運作的也還不錯,但是無法避開不處理 HTML 標籤,所以產生出來的結果也不能用,接著改成用 STDIO 丟給 John Gruber 的 Perl 版,結果這效率實在太差,所以又繼續研究一番,發現還有一套叫 typogr 的文字處理工具有實做,不過不想要太多功能,所以最後我決定自己把 Perl 版 SmartyPants 移植成 NPM module,也因此有了 smartypants.js

這次開發我選擇的語言是 TypeScript,原因可以參考我上一篇文章 20k-for-of,然後用 Makefile 加上一些指令轉成類似 jQuery UMD 形式的 JavaScript 檔案發佈到 NPM 上,目前的 smartypants.js 是完完全全把 Perl 版的邏輯翻過來,不過只有實作轉換編碼的部分加上 CLI 的介面,另外 Perl 版的只能轉換成 HTML entity 的格式,不過現在 UTF-8 已經算是很廣泛應用的文字編碼了,所以我還加上了一個轉成 UTF-8 字元的版本,用 UTF-8 編碼的雙引號其實很不錯,在 JSON 或是 csv 裡面也不用 escape,肉眼看起來也美觀許多。

實做過程比較緊張的地方大概就是要把 Perl 的 regular expression 轉成 JavaScript 的版本了,一度看到沒看過的用法都覺得會不會沒辦法用 JavaScript 做起來,還好沒用到很神奇的語法。另外它處理 HTML 語法的部分,其實是先過一個簡單的 lexer 轉成 token list,裡面兩種 token 分別就是一般文字和 HTML 標籤兩種,接著只針對一般文字 token 來做標點符號轉換,不過其實這個 tokenize 的部分有 bug,例如:

<span title=">">HAHA</span>

就會被分解成如下的 token:

  • <span title=">
  • ">HAHA
  • </span>

而且這種方法就也無法處理到一些文字內容的 HTML 屬性,例如 title 和 alt 等,所以接下來還可以做的改進,一個就是改進這部分的 lexer,然後也要對這些屬性值轉換標點符號,大概需要建立一個白名單,另外還有一個想加強的地方就是補上 test case 了,目前完全沒有相關的 test case 可以用,所以其實也沒很正式的驗證過,不過總之目前是都還運作正常,如果有相似需求的人可以參考一下。


更之前的文章