#set heading(numbering: "1.1.1.1.") #show raw: set text(font: "Noto Sans Mono CJK TC") #set page("a5") #set text( font: ("New Computer Modern", "AR PL UMing TW"), size: 11pt ) #show heading: it => [ #set text(font: "Noto Serif CJK TC", weight: "black") #it.body ] #set par( justify: true,leading: 1em, ) #align(center)[#set text( font: ("EB Garamond 08"), weight:"medium", size: 20pt, ) Clo: another typesetter] #align(center)[#box([#set text(size: 15pt, font: "Noto Serif CJK TC", weight:"medium") 一個排版器的實作心得])] #box( height:0.5em ) #align(center)[#box([#set text(size: 11pt, font: "AR PL UMing TW", weight:"light") 陳建町])] #pagebreak() #set page( margin: (top: 60pt, bottom: 20pt), header: locate( loc => if (calc.odd(loc.page()) == true){ [#set align(right) #numbering("i", loc.page()) ] } else { [#set align(left) #numbering("i", loc.page())] } )); #heading(level:2, "版權聲明",outlined:false) (c) 2023 陳建町 (Tan, Kian-ting) 本書內容非經許可,禁止複製、分發、商業使用等違反著作權法之行為。 然書中之程式碼,採用 #link("https://opensource.org/license/mit/")[MIT許可證]授權。 #pagebreak() #outline( title: align(left, [目#box(width:1em)錄 #box( height:1.5em)]), target: heading.where(outlined: true), ) #pagebreak() #set page( numbering: "i", number-align: top+right) #heading(numbering: none, "序言") 以前從國中時候試用Linux以及架站以後,就開始想用LaTeX排版些自己所寫的東西,其中包含覺得LaTeX的語法不好想要重造輪子。就算後來大學沒有走上資訊工程這條路,還是希望有天至少能夠完成個能用的雛形。 但是這是涉及字體檔案的處理、PDF的處理、語法分析,後來自己因為不知道如何開發,所以一直停擺。不是遇到很多蟲,就是效能問題有缺失。因為時間繁忙很少更不消說了。甚至買了Knuth教授的 _Digital Typography_,想要瞭解斷行演算法,結果粗估五、六十頁,所以幾乎沒有讀。 另外筆者一個分支興趣是編譯器的相關知識,所以開始讀和王垠的編譯器思想系出同門的Jeremy G. Siek所著作之_Essential of Complication: An Incremental Approach in Racket_(編譯之要素:Racket語言的遞增的方法)。我想到:既然編譯器這種複雜的軟體,可以一層一層的用pass來遞增功能,就像水彩從背景、大物體一直由少漸多的完成。而排版軟體也是把使用者輸入的排版之領域特定語言(DSL)轉換成文字、圖形和二維座標對應關係(最後匯出成PDF或SVG等等)的編譯器,若是能夠用層層遞增的方法來完成,相信也能夠避免結構的複雜化導致錯誤容易發生的挫折。 然而排版語言不只是輸入文字轉圖形而已,更重要的是還要有因應美觀的自動斷行(justification)和斷字(hyphenation)等等的演算法、還有PDF的基本知識、字型函式庫的取用、排版要求(多欄)、甚至還牽涉到語言特有的特性:比如東亞全形文字(漢字、諺文、日文假名、注音符號)和非全形文字中間要加空白,以及從左寫到右的文字(希伯來字母和阿拉伯字母等)的排版方法,不一而足。 為了簡化起見,且目標讀者是臺灣的受眾,本書僅涉及到ASCII英文字母——頂多加些一些附加符號(diacritics)和漢字的排版。其他的功能希望讀者可以漸次由少漸多的附加。另外這邊會使用到一些LISP的表達式來表達抽象語法樹,若是不懂的話,可以看一點教 Lisp或是Scheme的書,如SICP。另外這本書不是編譯原理和描述PDF規格的書,不涉獵底層的知識,有需要的可以參考相關領域的書。 #heading(numbering: none, "致謝") 感謝Donald Knuth教授開發出這麼一套排版系統以及排版的演算法,除了造福科學排版的諸多用戶外,也間接鼓舞我想要研究排版軟體如何實作;感謝Jeremy G.Siek老師的_Essential of Complication: An Incremental Approach in Racket_,讓我獲得排版語言編譯器設計的啟發。感謝王垠讓我對編譯器相關的技術有興趣,從而斷斷續續學習相關的資訊。 感謝愛爾蘭語,除了讓我對語言和語言復興的知識打開新的世界以外,這個軟體的名字Clo也是從這裡來的(cló有「活字」的意思,因為技術限制抱歉沒辦法輸入長音符號)。 感謝我的父母,雖然專長不是電腦資訊科技,但是要感謝他們讓我讓我有餘力能夠在中學的時候研究這種興趣,這條路才能走下去。 感謝這本書閱讀的人們,讓我知道筆者不是孤單的。 Siōng-āu Kám-siā góa ê Siōng Chú, nā-bô i ê hû-chhî kap pó-siú, chit-pún chheh iā bô-hó oân-sêng.(最後感謝上主,若無扶持保守,這本書也很難完成) #pagebreak() #set page( margin: (top: 60pt, bottom: 20pt), header: locate( loc =>{ let chapter_query = query(selector(heading.where(level: 1)).after(loc),loc) let section_query = query(selector(heading.where(level: 2)).after(loc),loc) let chapter = ""; let section = ""; if chapter_query == (){chapter = ""} else{chapter = chapter_query.at(0).body}; if section_query == (){section = ""} else{section = section_query.at(0).body} if (calc.odd(loc.page()) == true){ grid( columns: (0.333333fr, 0.333333fr, 0.333333fr), text(style: "italic")[ ], [#set align(center) #chapter], [ #h(1fr) #loc.page-numbering()]) } else { grid( columns: (0.333333fr, 0.333333fr, 0.333333fr), text(style: "italic")[#loc.page-numbering()], [#set align(center) #section], [ ]) }} )); #show heading: it => [ #set text(font: ("New Computer Modern", "Noto Serif CJK TC"), weight: "black") #counter(heading).display() #it ] #set page(numbering: "1") #counter(page).update(1) #set heading(numbering: "1.1.1.1.1") = 先備知識 這不是教一位入門使用者如從零知識撰寫排版軟體的書,讀者應該有知道如何使用靜態型別語言的經驗,比如一點C、或是Rust等等。另外抽象語法樹為求方便,使用LISP撰寫,所以需要會LISP和Scheme的知識(知名教科書SICP的開頭可以讀一讀)。 這本書也不教編譯理論和tokenizing、parsing、狀態機等等的,頂多只會帶到一些很基礎的知識,有需要的請另外再讀。所以使用者需要會有使用正規表達式(regex)的能力。 == 抽象語法樹 C語言、Python語言就算有許多的關鍵字、操作符、符號或是常數變數,在編譯器分析語法以後,最後會轉成編譯器可以操作的樹結構,然後再轉成我們想要的另一個語言的樹,最後輸出另一個語言的程式碼。 但是什麼叫做抽象語法樹呢?我們先從一點句法知識來談。 學過中學國文文法的課程,會背一堆類似「主詞+動詞+受詞」、「主詞+(有/無)+受詞」的結構。可以換個說法,是句子=「主詞+動詞+受詞」或是「主詞+(有/無)+賓詞」的形式。我們將「=」寫成「::=」,「/」(或是)寫成「|」,動詞擴充變成「動詞片語」,就變成: ``` 句子 ::= (主詞 動詞片語 受詞) | (主詞 (有 | 無) 受詞)... ``` 用這種形式表示的語言句法,叫做「BNF文法」。這種句法看起來很語言學,但是我們想:受詞和主詞可以為名詞、專有名詞或是「形容詞+名詞」;動詞片語可以為動詞或是「副詞+動詞」。因此這樣之規則,就可以生成許多句子,比如「我有筆」、「張三養貓」、「小芳慢慢移動檯燈」等等的句子。然後句子可以用上述規則,分析成語法的樹狀結構,如圖1把「我曾旅居新竹」寫成語法樹。 #figure( image("syntaxtree.svg", width: 40%), caption: [ 「我曾旅居新竹」的語法樹 ], supplement: [圖], ) 同理,程式語言通常也有更嚴謹的這樣生成文法,可以用幾個簡單規則生出繁多的程式碼,而且合乎語法規定。這種生成文法也可檢查輸入的程式碼有沒有符合句法的規定。而這種語法生成的程式碼,去掉不需要的逗號等等符號,當然也可以做成語法樹,就是抽象語法樹 (abstract syntax tree, AST),如圖2所示。 #figure( image("syntaxtree2.svg", width: 30%), caption: [ `(2+2) == 4`的語法樹。注意括號已經刪除。 ], supplement: [圖], ) 而上文的抽象語法樹,可以是我們把程式經過編譯器分析之後,用「樹」儲存的資料結構。而樹形結構我們可以使用Lisp語言的S表達式(S-expressiom; S-exp)來表示,本文採用這樣的表示方法。所以上文的`(2+2)==4`即`(== (+ 2 2) 4)`;`let baz = foo("bar")`,若是把foo("bar")這種函數套用(apply)寫成`(APPLY foo "bar")`,則其S-exp語法樹可寫為`(let baz(APPLY foo "bar"))`。