SATySFiを使ってみた話
PDFで文書を書く必要性に直面したので、最近話題のSATySFiを使ってみました。
前提として、The SATySFibookは持っていません。
電子書籍で売って欲しいのですが、電子版は無償公開するという話なのでひとまず待っています。
OCamlは全く触ったことがないので、実装はよくわかりません。 印象としては、TeXレベルの実装は完了していてLaTeXレベルのマクロライブラリやドキュメントクラスのほうがまだまだなのかなといった感じでした。
LaTeXの方は、もう6年くらい触っていないので最近の動向がよくわかりませんが、当時のLaTeXと現在のSATySFiを比べると、以下のような感じかなと思います。
- 当然UTF-8(LaTeXもupLaTeXとかあるらしいですね)
- 型の不一致によるエラー検出が明確でわかりやすい。
section
等が構造化されていて、文書構造をネストで表現できる(HTMLの感覚に近い)。- 使いたい機能の不足。
- そして機能が追加しやすい。
基本的には良いことだらけという印象ですが、機能の不足はやはり否めません。「機能」と、雑に書いていますが、実際のところ基本機能として絶対的に足りていないものがあるわけではなく、マクロレベルのショートハンドが足りない、といった印象です。 実際に使う際には、付属の stdjareport.satyh をコピーして改造し、 myjareport.satyh というものを作って、必要な機能を足しながらやっていくということになりました。
そんなに複雑な文書を作成したわけではないので、追加した機能は4つだけです。
+pn
の移植
字下げの無い段落を作るやつです。 stdjabook.satyh にはあるのに、 stdjareport.satyh には無かったので、必要そうな部分をコピペしました。それだけです。
即時改行
これが無いというのは驚きました。とはいえ、基本的には無いなら無いで+pn
を使えばよいという話なのですが、論理構造が崩れるのが嫌だったので導入しました。
なぜ即時改行が必要になったかというと、禁則処理の問題です。CamelCaseのような単語が和文中に出現すると、右側の余白にはみ出してしまう場合があり、私の意図としては次の行に送ってほしかったために、その前に即時改行を入れるようにした、ということです。 本来的には禁則処理をそのように動かすように改造する(はみ出す語を次の行へ送る+元の行を均等割付)のが筋ですが、難易度が高そうに思えたので、手抜きをしました。
実装としては以下のようにしました。
let-inline ctx \br = inline-fil ++ embed-block-breakable ctx (block-skip (get-natural-width (read-inline ctx {m})))
使うときは \br;
です。
block-skip
の引数はlength
型なのですが、length
型のリテラルが見当たらず、適当に型合わせをしました。適当に型を合わせればなんとなく動くというのは静的型付け関数型言語一般の強みだと思います。お世辞にもきれいとは言えませんが、動けばいいのです。((get-font-size ctx) *' 0.7)
でもわりとそれっぽくはなりました。
ページ番号を表示しないオプションを追加
今回の要件ではページ番号は必要なかったため、このようなものを追加しました。document
にわたす無名レコード型にwith-pageno : bool;
という行を足して、let
でwith-pageno
という名前を束縛し、
{— #it-pageno; — }
と書かれている部分を
(if with-pageno then {— #it-pageno; — } else {})
に置き換えただけです。document
にwith-pageno = false;
を渡してやれば、ページ番号が表示されなくなります。
+subsubsection
の追加
stdjabook.satyh の場合、セクション構造として以下の3つが用意されています。
- section
- subsection
- subsubsection
一方 stdjareport.satyh はこうです。
- chapter
- section
- subsection
これは確かLaTeXのjarticle系でもこのような感じだったと思いますし、不備というわけではありません。
そもそも文書構造が悪いという話なんですが、書いている途中、どうしても4段目のセクション構造が欲しくなってしまったので、軽率に足しました。つまり、以下の4段階構造となります。
- chapter
- section
- subsection
- subsubsection
やったことといえば、+subsubsection
用のカウンタとフォントサイズ指定を足し、上位のセクションヘッダ出力時にカウンタをリセットするようにし、セクション出力コードは上位のものをコピペして適当に書き換えるだけ、という単純なものです。
わからなかった点
.satyhファイルを触る上で、わからなかった点を挙げます。上記とも重複します。
- 禁則処理のカスタマイズ方法
length
型の値を生成するうまいやり方document
に与えるレコード型のデフォルト値の設定方法+section
等にラベルを与える方法
最終的にはわかったんですが、Stringリテラルが最初は見つけられず、\code
に渡したい引数をどう書けばいいのか悩んでいた時期もありました。
こう↓書けば動くようでした。
+p{ \code(`answer`);メソッドの定義。 } +code(` def answer 42 end `);
以下の資料が参考になりました。
- SATySFiに関すること - Qiita
- satysfi-grammar.md · GitHub
- SATySFi/memo-ja-how-to-use.md at 66fdd27e2ba65e4b4c4c0612834e6c547905ab11 · gfngfn/SATySFi · GitHub
- 本体付属のdoc(ディレクトリに入って
make
するとできるprimitives.pdf)
The SATySFibookも読みたいものですが、可能な限り物理書籍の所持数を減らしたいので、電子版待ちです。
参考までに、改造した myjareport.satyh を貼っておきます。
% -*- coding: utf-8 -*- @require: pervasives @require: gr @require: list @require: math @require: color @require: footnote-scheme module MyJaReport : sig val document : 'a -> block-text -> document constraint 'a :: (| title : inline-text; author : inline-text; with-pageno : bool; |) val font-latin-roman : string * float * float val font-latin-italic : string * float * float val font-latin-sans : string * float * float val font-latin-mono : string * float * float val font-cjk-mincho : string * float * float val font-cjk-gothic : string * float * float val set-latin-font : (string * float * float) -> context -> context val set-cjk-font : (string * float * float) -> context -> context direct \ref : [string] inline-cmd direct \ref-page : [string] inline-cmd direct \figure : [string?; inline-text; block-text] inline-cmd direct +p : [inline-text] block-cmd direct +pn : [inline-text] block-cmd direct +chapter : [string?; inline-text; block-text] block-cmd direct +section : [string?; inline-text; block-text] block-cmd direct +subsection : [string?; inline-text; block-text] block-cmd direct +subsubsection : [string?; inline-text; block-text] block-cmd direct \emph : [inline-text] inline-cmd direct \dfn : [inline-text] inline-cmd direct \footnote : [inline-text] inline-cmd end = struct % type toc-element = % | TOCElementChapter of string * inline-text % | TOCElementSection of string * inline-text % | TOCElementSubsection of string * inline-text let generate-fresh-label = let-mutable count <- 0 in (fun () -> ( let () = count <- !count + 1 in `generated:` ^ (arabic (!count)) )) let-inline ctx \ref key = let opt = get-cross-reference (key ^ `:num`) in let it = match opt with | None -> {?} | Some(s) -> embed-string s in read-inline ctx it let-inline ctx \ref-page key = let opt = get-cross-reference (key ^ `:page`) in let it = match opt with | None -> {?} | Some(s) -> embed-string s in read-inline ctx it let font-size-normal = 12pt let font-size-title = 18pt let font-size-author = 16pt let font-size-chapter = 22pt let font-size-section = 18pt let font-size-subsection = 16pt let font-size-subsubsection = 14pt let section-top-margin = 20pt let section-bottom-margin = 12pt let chapter-top-margin = 30pt let chapter-bottom-margin = 18pt let font-ratio-latin = 1. let font-ratio-cjk = 0.88 let font-latin-roman = (`Junicode` , font-ratio-latin, 0.) let font-latin-italic = (`Junicode-it`, font-ratio-latin, 0.) let font-latin-sans = (`lmsans` , font-ratio-latin, 0.) let font-latin-mono = (`lmmono` , font-ratio-latin, 0.) let font-cjk-mincho = (`ipaexm` , font-ratio-cjk , 0.) let font-cjk-gothic = (`ipaexg` , font-ratio-cjk , 0.) let set-latin-font font ctx = ctx |> set-font Latin font let set-cjk-font font ctx = ctx |> set-font HanIdeographic font |> set-font Kana font let get-standard-context wid = get-initial-context wid (command \math) |> set-dominant-wide-script Kana |> set-language Kana Japanese |> set-language HanIdeographic Japanese |> set-dominant-narrow-script Latin |> set-language Latin English |> set-font Kana font-cjk-mincho |> set-font HanIdeographic font-cjk-mincho |> set-font Latin font-latin-roman |> set-math-font `lmodern` |> set-hyphen-penalty 100 let-mutable ref-float-boxes <- [] let height-of-float-boxes pageno = % let () = display-message `get height` in (!ref-float-boxes) |> List.fold-left (fun h (pn, bb) -> ( if pn < pageno then h +' (get-natural-length bb) else h )) 0pt let-mutable ref-figure <- 0 let-inline ctx \figure ?:labelopt caption inner = let () = ref-figure <- !ref-figure + 1 in let s-num = arabic (!ref-figure) in let () = match labelopt with | Some(label) -> register-cross-reference (label ^ `:num`) s-num | None -> () in let it-num = embed-string s-num in let bb-inner = let d (_, _) _ _ _ = [] in block-frame-breakable ctx (2pt, 2pt, 2pt, 2pt) (d, d, d, d) (fun ctx -> ( read-block ctx inner +++ line-break true true ctx (inline-fil ++ read-inline ctx {図#it-num; #caption;} ++ inline-fil) )) in hook-page-break (fun pbinfo _ -> ( % let () = display-message (`register` ^ (arabic pbinfo#page-number)) in ref-float-boxes <- (pbinfo#page-number, bb-inner) :: !ref-float-boxes )) let make-chapter-title ctx = ctx |> set-font-size font-size-chapter |> set-font Latin font-latin-sans |> set-cjk-font font-cjk-gothic let make-section-title ctx = ctx |> set-font-size font-size-section |> set-font Latin font-latin-sans |> set-cjk-font font-cjk-gothic let make-subsection-title ctx = ctx |> set-font-size font-size-subsection |> set-font Latin font-latin-sans |> set-cjk-font font-cjk-gothic let make-subsubsection-title ctx = ctx |> set-font-size font-size-subsubsection |> set-font Latin font-latin-sans |> set-cjk-font font-cjk-gothic % let-mutable toc-acc-ref <- [] let get-cross-reference-number label = match get-cross-reference (label ^ `:num`) with | None -> `?` | Some(s) -> s let get-cross-reference-page label = match get-cross-reference (label ^ `:page`) with | None -> `?` | Some(s) -> s let chapter-heading ctx ib-heading = line-break true false (ctx |> set-paragraph-margin chapter-top-margin chapter-bottom-margin) ib-heading let section-heading ctx ib-heading = line-break true false (ctx |> set-paragraph-margin section-top-margin section-bottom-margin) ib-heading let-inline ctx \dummy it = let ib = read-inline (ctx |> set-text-color Color.white) it in let w = get-natural-width ib in ib ++ inline-skip (0pt -' w) let document record inner = % -- constants -- let title = record#title in let author = record#author in let with-pageno = record#with-pageno in let page = A4Paper in let txtorg = (80pt, 100pt) in let txtwid = 440pt in let txthgt = 630pt in let hdrorg = (40pt, 30pt) in let ftrorg = (40pt, 780pt) in let hdrwid = 520pt in let ftrwid = 520pt in let () = register-cross-reference `changed` `F` in let ctx-doc = get-standard-context txtwid in % -- title -- let bb-title = let bb-title-main = let ctx = ctx-doc |> set-font-size font-size-title in line-break false false ctx (inline-fil ++ read-inline ctx title ++ inline-fil) in let bb-author = let ctx = ctx-doc |> set-font-size font-size-author in line-break false false ctx (inline-fil ++ read-inline ctx author ++ inline-fil) in bb-title-main +++ bb-author in % -- main -- let bb-main = read-block ctx-doc inner in % -- page settings -- let pagecontf pbinfo = let () = FootnoteScheme.start-page () in let hgtfb = height-of-float-boxes pbinfo#page-number in let (txtorgx, txtorgy) = txtorg in (| text-origin = (txtorgx, txtorgy +' hgtfb); text-height = txthgt -' hgtfb; |) in let pagepartsf pbinfo = let pageno = pbinfo#page-number in let header = let ctx = get-standard-context hdrwid |> set-paragraph-margin 0pt 0pt in let ib-text = if pageno mod 2 == 0 then (inline-fil ++ read-inline ctx title) else (read-inline ctx title ++ inline-fil) in % let () = display-message `insert` in let (bb-float-boxes, acc) = (!ref-float-boxes) |> List.fold-left (fun (bbacc, acc) elem -> ( let (pn, bb) = elem in if pn < pageno then let bbs = line-break true true (ctx |> set-paragraph-margin 0pt 12pt) (inline-fil ++ embed-block-top ctx txtwid (fun _ -> bb) ++ inline-fil) % 'ctx' is a dummy context in (bbacc +++ bbs, acc) else (bbacc, elem :: acc) )) (block-nil, []) in let () = ref-float-boxes <- acc in bb-float-boxes in let footer = let ctx = get-standard-context ftrwid in let it-pageno = embed-string (arabic pbinfo#page-number) in line-break true true ctx (inline-fil ++ (read-inline ctx (if with-pageno then {— #it-pageno; —} else {})) ++ inline-fil) in (| header-origin = hdrorg; header-content = header; footer-origin = ftrorg; footer-content = footer; |) in page-break page pagecontf pagepartsf (bb-title +++ bb-main) let-mutable num-chapter <- 0 let-mutable num-section <- 0 let-mutable num-subsection <- 0 let-mutable num-subsubsection <- 0 let quad-indent ctx = inline-skip (get-font-size ctx *' font-ratio-cjk) let-block ctx +p inner = let ib-inner = read-inline ctx inner in let br-parag = (quad-indent ctx) ++ ib-inner ++ inline-fil in form-paragraph ctx br-parag let-block ctx +pn inner = let ib-inner = read-inline ctx inner in form-paragraph ctx (ib-inner ++ inline-fil) let chapter-scheme ctx label title inner = let ctx-title = make-chapter-title ctx in let () = increment num-chapter in let () = num-section <- 0 in let () = num-subsection <- 0 in let () = num-subsubsection <- 0 in let s-num = arabic (!num-chapter) in let () = register-cross-reference (`chapter:` ^ label ^ `:num`) s-num in % let () = toc-acc-ref <- (TOCElementChapter(label, title)) :: !toc-acc-ref in let ib-num = read-inline ctx-title (embed-string (s-num ^ `.`)) ++ hook-page-break (fun pbinfo _ -> ( let pageno = pbinfo#page-number in register-cross-reference (`chapter:` ^ label ^ `:page`) (arabic pageno))) in let ib-title = read-inline ctx-title title in let bb-title = chapter-heading ctx (ib-num ++ (inline-skip 10pt) ++ ib-title ++ (inline-fil)) in let bb-inner = read-block ctx inner in bb-title +++ bb-inner let section-scheme ctx label title inner = let ctx-title = make-section-title ctx in let () = increment num-section in let () = num-subsection <- 0 in let () = num-subsubsection <- 0 in let s-num = arabic (!num-chapter) ^ `.` ^ arabic (!num-section) in let () = register-cross-reference (`section:` ^ label ^ `:num`) s-num in % let () = toc-acc-ref <- (TOCElementSection(label, title)) :: !toc-acc-ref in let ib-num = read-inline ctx-title (embed-string (s-num ^ `.`)) ++ hook-page-break (fun pbinfo _ -> ( let pageno = pbinfo#page-number in register-cross-reference (`section:` ^ label ^ `:page`) (arabic pageno))) in let ib-title = read-inline ctx-title title in let bb-title = section-heading ctx (ib-num ++ (inline-skip 10pt) ++ ib-title ++ (inline-fil)) in let bb-inner = read-block ctx inner in bb-title +++ bb-inner let subsection-scheme ctx label title inner = let () = num-subsubsection <- 0 in let () = increment num-subsection in let s-num = arabic (!num-chapter) ^ `.` ^ arabic (!num-section) ^ `.` ^ arabic (!num-subsection) in let () = register-cross-reference (label ^ `:num`) s-num in % let () = toc-acc-ref <- (TOCElementSubsection(label, title)) :: !toc-acc-ref in let ctx-title = make-subsection-title ctx in let ib-num = read-inline ctx-title (embed-string (s-num ^ `.`)) ++ hook-page-break (fun pbinfo _ -> register-cross-reference (label ^ `:page`) (arabic pbinfo#page-number)) in let ib-title = read-inline ctx-title title in let bb-title = line-break true false (ctx |> set-paragraph-margin section-top-margin section-bottom-margin) (ib-num ++ (inline-skip 10pt) ++ ib-title ++ (inline-fil)) in let bb-inner = read-block ctx inner in bb-title +++ bb-inner let subsubsection-scheme ctx label title inner = let () = increment num-subsubsection in let s-num = arabic (!num-chapter) ^ `.` ^ arabic (!num-section) ^ `.` ^ arabic (!num-subsection) ^ `.` ^ arabic (!num-subsubsection) in let () = register-cross-reference (label ^ `:num`) s-num in % let () = toc-acc-ref <- (TOCElementSubsection(label, title)) :: !toc-acc-ref in let ctx-title = make-subsubsection-title ctx in let ib-num = read-inline ctx-title (embed-string (s-num ^ `.`)) ++ hook-page-break (fun pbinfo _ -> register-cross-reference (label ^ `:page`) (arabic pbinfo#page-number)) in let ib-title = read-inline ctx-title title in let bb-title = line-break true false (ctx |> set-paragraph-margin section-top-margin section-bottom-margin) (ib-num ++ (inline-skip 10pt) ++ ib-title ++ (inline-fil)) in let bb-inner = read-block ctx inner in bb-title +++ bb-inner let-block ctx +chapter ?:labelopt title inner = let label = match labelopt with | None -> generate-fresh-label () | Some(label) -> label in chapter-scheme ctx label title inner let-block ctx +section ?:labelopt title inner = let label = match labelopt with | None -> generate-fresh-label () | Some(label) -> label in section-scheme ctx label title inner let-block ctx +subsection ?:labelopt title inner = let label = match labelopt with | None -> generate-fresh-label () | Some(label) -> label in subsection-scheme ctx label title inner let-block ctx +subsubsection ?:labelopt title inner = let label = match labelopt with | None -> generate-fresh-label () | Some(label) -> label in subsubsection-scheme ctx label title inner let-inline ctx \emph inner = let ctx = ctx |> set-font Latin font-latin-sans |> set-cjk-font font-cjk-gothic in read-inline ctx inner let-inline \dfn inner = {\emph{#inner;}} let-inline ctx \footnote it = let size = get-font-size ctx in let ibf num = let it-num = embed-string (arabic num) in let ctx = ctx |> set-font-size (size *' 0.75) |> set-manual-rising (size *' 0.25) in read-inline ctx {\*#it-num;} in let bbf num = let it-num = embed-string (arabic num) in let ctx = ctx |> set-font-size (size *' 0.9) |> set-leading (size *' 1.2) |> set-paragraph-margin (size *' 0.5) (size *' 0.5) %temporary in line-break false false ctx (read-inline ctx {#it-num; #it;} ++ inline-fil) in FootnoteScheme.main ctx ibf bbf end let-inline ctx \br = inline-fil ++ embed-block-breakable ctx (block-skip (get-natural-width (read-inline ctx {m}))) let document = MyJaReport.document % ad-hoc