技術書典14の新刊として『オブジェクト・ウォーズ』を書いた。
このとき、組版システムとしてVivliostyleを使ってみたので、その感想とか。
Vivliostyleとは
本を作ろうとすると文章のレイアウト(組版)が必要になって、そのために使われるのが組版システム。 やInDesignなどいろいろなソフトがあって、自分も独自のシステムを作ってみたりしてるんだけど、Vivliostyleもそんな組版システムの一つ。
Vivliostyleの特徴はレイアウトの指定をWebページと同じようにCSSでできるようにしていること。 これによりWebページを作るときのテクニックが流用できるというメリットがある。 また、VFMというMarkdownの拡張文法が用意されていて、原稿を(VFMの仕様に従った)Markdownで書くこともできる。 さらにはCreate Bookというツールも用意されていて、原稿を書くためのプロジェクトを簡単に作れるようになっている。 コマンドラインからのPDF出力も可能。
元々はCSSで組版するというコアな機能だけあってちょっと取っ付きづらく、また行末処理が弱くて日本語組版に使うには苦労が多いかなという感じだったんだけど、VFMなどで周辺ツールも揃ってきて触りやすくなり、行末処理も改善されたので、かなり使えるツールになってきている。
商業誌も出たのでますます広がっていくんじゃないかな。
そんな感じで、だいぶ良くなってきているように感じたので、実際に触って感触を確かめたいなというのが今回Vivliostyleを使ってみたモチベーション。
よかったこと
使ってみてまず思ったのは、Create Book便利だなということ。 あとチュートリアルが思ったよりもしっかりしてた。
こういうツールって「で、最初に何やればいいの?」っていう最初の一歩が難しい。 Vivliostyleの存在は以前から知ってて触ってみたいとも思いつつ、けど何から始めたらいいのかを調べるのが面倒という気持ちがあった。 でもチュートリアルとCreate Bookのおかげで「あぁ、こんな感じね」と割とさっくり理解できたと思う。
思い返せば『Vivliostyleで本を作ろう Vol. 1』を見たときは、最初の項目が「EPUB Adaptive Layout による段組み混在レイアウトへの挑戦」ってなってて「???」ってなったり、そのあとの初心者向けっぽい「CSS 組版やってみた!」という項目を見ても「・・・これは何から手をつければいいんだ?」って絶望したもんだった。
でも今はちゃんとしたチュートリアルになっていたのでホッとした。 Create Bookもコマンド一発でプロジェクト作れて、プレビューやビルドもコマンドで簡単にできたし。
あと原稿をMarkdownで書けるのはやっぱり便利。 差分をGitで管理できるし。
そしていろいろ試行錯誤は必要だったものの、CSSでレイアウトを指定できるのはやっぱりいいなと思った。 デザインはやっぱり宣言的であるべきよねぇ。 また、CSSを使うことでなかなかいい感じのデザインにもできたと思う。
脚注とかコードブロックもちゃんと使えてよかった。
それとページや章などの番号を自動で振ったり、それを参照したりもちゃんとできた。 組版システムとしてはこのあたりがしっかり自動でできるのはいいよね。
あとトンボの出力もCSSで指定するだけでできたのでよかった。
大変だったこと
そんな感じで使い始めるまでのハードルがかなり下がっていて基本的な機能もしっかりあるんだけど、ちょっと大変だったことも。
まずはCSSをいじってカスタムテーマを用意する手順がちょっと大変だった。
Create Bookだと指定されたテーマの一連のCSSがローカルにコピーされてくるんだけど、これを適当な場所にコピペしてそちらを参照しにいく感じだった(このあたり、書いている途中でテーマの仕様が変わったので今は違うかも)。 感覚としてはCSSなんだし差分を上書きするだけで済む方が嬉しいかなと思うけど、コピペして直接書き換えるのかぁというのが。
また、このテーマのディレクトリ自体が1つのnpmパッケージになっているので、SCSSのビルドをするには依存してるパッケージのインストールが必要で、テーマのディレクトリに入ってnpm install
を実行しておかないとnpm run build:scss
を呼んでもエラーになったり(チュートリアルの手順で書かれてなかった;普段npmを使ってないのでそのあたりの常識がない)。
CSSも少しは齧ってたけど最新のは全然キャッチアップできてなかったので、いろいろ調べるのがそれなりに大変だった。 とくにflex?あたりは全然分からなくて、目次のレイアウトで苦労した。 とりあえずそれっぽいデザインにはできたけど。
目次に関連して、章番号も振りたい章とそうでない章があって、それが少し困った。 「はじめに」や「目次」「あとがき」とかは番号を振りたくないんだけど、何もしないとそういった章でもカウンターが増えてしまう。
対処としては、VFMのフロントマターでCSSを指定するとそれが追加で読み込まれるので、それを使うようにした。 ただ、そうやって個別に指定するCSSは全体のCSSを上書きしたいはずなので、共通で読み込まれるCSSの後ろに追加されるとよかったんだけど、残念ながら前に追加されてしまうようになっていた。 なので、理想としては「全体のCSSでカウントアップを定義」「カウントを増やしたくない章についてはカウントアップしないようにするCSSを追加で読み込む」としたかったけど、「全体のCSSではカウントアップを定義しない」「カウントを増やす通常の章ではカウントアップするCSSも追加で読み込む」とする必要があった。 このあたりは設計をちょっと考え直してほしいなぁ。
他、バグかな?と思ったこととして、脚注とコードブロックが同じページにあってコードブロックで改ページが必要になったときに、コードブロックが丸々次のページに送られている気がした。 CSSの設定のせいの可能性もあるけど。 これに関しては仕方ないのでコードブロックを手動で適当なサイズに分割して次のページに送られないようにした。
CSS以外の話で大変だったのは、Markdownでの改行で半角スペースが入ってしまうこと。
HTMLに変換されるとき、改行はそのまま改行となるので、その改行が半角スペースになるのはまぁ自然な仕様。 でもVimで原稿を書いていたりGitでバージョン管理している都合、適当に改行は入れたくて、 preとかでないなら半角スペースが入らないようになっていてほしいところ。
そのあたり、設定で半角スペースが入らないようにもできると嬉しい。
今回はしょうがないのでRubyで簡単なコードを書いて前処理をするようにした:
# 改行で半角スペースが入ってしまうので # 無駄な改行を取り除く # # 改行を取り除いてはいけないケースもあって、 # 1. コードブロック内 # 2. 箇条書きの直前 # 3. ?や!で終わる場合 # 4. 英単語で終わる場合 # 5. 空行自身 # 6. フロントマター # # 標準入力を読んで標準出力へ出す(適当にリダイレクトする) def preprocess(infile, outfile) begin_front_matter = false end_front_matter = false in_code_block = false lazy_linefeed = false infile.each_line do |line| # フロントマターの間はそのまま出力 if !begin_front_matter begin_front_matter = true if line =~ /^---/ outfile.print line next else end_front_matter = true end elsif !end_front_matter if line =~ /^---/ end_front_matter = true end outfile.print line next end # 改行が遅延されているとき、 # 空行や箇条書きが来たら遅延させていた改行を出力する #if (not in_code_block) && lazy_linefeed if lazy_linefeed if (line =~ /^$/) || (line =~ /^\s*([0-9]+.|[\-+*])\s+/) outfile.print "\n" lazy_linefeed = false end end if line =~ /^```/ outfile.print line in_code_block = !in_code_block elsif in_code_block || (line =~ /^$/) outfile.print line elsif line =~ /[?!]$/ outfile.print line lazy_linefeed = false else outfile.print line.chomp lazy_linefeed = true end end end if __FILE__ == $0 preprocess STDIN, STDOUT end
いやぁ、かなりナイーブでめっちゃ汚いコードだけど、とりあえず動きはする(^^;
そして、前処理が必要な関係で、単にnpm run build
するだけだとビルドとしては不十分で、それはRakefileを書いて補完する必要があった。
この依存関係に関しては追加で弱い部分があるなと思ったり。
原稿を書いてるプロジェクトとテーマをビルドするプロジェクトは別物で依存関係が構築されてないので、CSSを修正して見た目を確認したいとなったときのビルドが手間だった。 CSSが変更されていればCSSのビルドも行ってPDFのビルドもやってほしいわけだけど、それができない感じ。 おそらく、想定している使い方がビルドプロセスをずっと動かし続けて、更新があればライブでビルドするという感じなんだと思う。 でもずっとビルドプロセスを走らせているのも微妙な気はする。
もう一つ、大きく困ったのが、バリアントをどう作るかという話。
技術同人誌を作る場合、電子書籍として頒布するためのPDFと印刷所に回して印刷してもらうためのPDFを用意する必要があったりする。 このとき、原稿は同じなんだけど、地味に細かい違いがあったり:
- 電子版
- カラーでOK
- 表紙、裏表紙のページも追加
- トンボは不要
- 印刷版
- グレースケース
- 表紙、裏表紙は不要
- トンボが必要
すると、参照するCSSや画像ファイルが違ってくるんだけど、このバリアントを切り替えてビルドする仕組みがない。。。
ということで、そのあたりを解決するためにガッツリとRakefileを用意する必要があった。
まず、ファイル構成は次のような感じ:
- ObjWarsBook/ - Rakefile - preprocess.rb - *.md # 原稿 - vivliostyle.config.js - images_ebook/ # 電子用の画像(カラー) - images_print/ # 印刷用の画像(グレースケール) - settings/ - ebook.vivliostyle.config.js # 電子用の設定ファイル - print.vivliostyle.config.js # 印刷用の設定ファイル - theme-techbook-custom/ # テーマ - scss/ - *.scss # ビルド前のSCSS - theme_ebook.css # 電子用のビルド済みCSS - theme_print.css # 印刷用のビルド済みCSS - .vivliostyle_ebook/ # 電子用のビルドディレクトリ - .vivliostyle_print/ # 印刷用のビルドディレクトリ
電子用、印刷用をそれぞれ用意して、ビルド時にはそれぞれのビルド用ディレクトリにコピーしたりすることでバリアントを作れるようにした。
Rakefile自体は次のような感じ:
require 'rake/clean' require 'pathname' require_relative 'preprocess' # 設定 ======================================== md_files = %w( cover1.md opening.md toc.md chap1_rule.md chap2_flow.md chap3_object.md chap4_class.md chap5_test.md chap6_refactor.md chap7_enhance.md chap8_inherit.md chap9_further.md closing.md cover4.md ) scss_files = %w( _variables_ebook.scss _variables_print.scss _base.scss _head.scss _code.scss _footnote.scss _page.scss _toc.scss theme_ebook.scss theme_print.scss ) image_files = %w( bgimage_ul.png bgimage_br.png bgimage_chapter.png logo.png rule.png flow_vs_object.png inside_vs_outside.png interface.png cover1.png cover4.png ) css_files = %w( countup.css reset_page.css ) settings_dir = Pathname.new "settings" theme_dir = Pathname.new "theme-techbook-custom" targets = [:ebook, :print] default_target = :ebook work_dir = {} images_dir = {} pdf = {} setting = {} theme = {} targets.each do |target| work_dir[target] = Pathname.new ".vivliostyle_#{target}" images_dir[target] = Pathname.new "images_#{target}" pdf[target] = "ObjectWars.#{target}.pdf" setting[target] = settings_dir / "#{target}.vivliostyle.config.js" theme[target] = theme_dir / "theme_#{target}.css" end task :default => :all # ファイル ======================================== pp_md_paths = {} css_paths = {} work_images_dir = {} image_paths = {} targets.each do |target| pp_md_paths[target] = md_files.map do |md_file| work_dir[target] / md_file end css_paths[target] = css_files.map do |css_file| work_dir[target] / css_file end work_images_dir[target] = work_dir[target] / "images" image_paths[target] = image_files.map do |image_file| work_images_dir[target] / image_file end end scss_paths = scss_files.map do |scss_file| theme_dir / "scss" / scss_file end # タスク定義 ======================================== task :all => targets task :build, [:target] do |task, args| target = args[:target]&.to_sym || default_target Rake::Task[target].invoke end task :open, [:target] do |task, args| build_target = args[:target]&.to_sym || default_target open_target = "open_#{build_target}".to_sym Rake::Task[open_target].invoke end task :copy_setting, [:target] do |task, args| use_setting_file = "#{args[:target]}.vivliostyle.config.js" use_setting_path = settings_dir / use_setting_file sh "cp", use_setting_path.to_s, "vivliostyle.config.js" end # preview用の設定にする task :previewmode do Rake::Task[:copy_setting].invoke(:preview) end task :build_css do Dir.chdir(theme_dir) do sh "npm", "run", "build:scss" end end targets.each do |target| task target => pdf[target] # build -------------------- file pdf[target] => [*pp_md_paths[target], setting[target], theme[target], *css_paths[target], *image_paths[target]] do Rake::Task[:copy_setting].invoke(target) sh "npm", "run", "build" end file theme[target] => [*scss_paths] do Rake::Task[:build_css].invoke end pp_md_paths[target].each do |pp_md_path| md_path = Pathname.new pp_md_path.basename file pp_md_path => [work_dir[target], md_path] do puts "preprocess #{md_path} to #{pp_md_path}..." md_path.open do |input| pp_md_path.open("w") do |output| preprocess input, output end end end end image_paths[target].each do |image_path| src_image_path = images_dir[target] / image_path.basename file image_path => [work_images_dir[target], src_image_path] do sh "cp", src_image_path.to_s, image_path.to_s end end directory work_dir[target] directory work_images_dir[target] css_paths[target].each do |css_path| css_file = css_path.basename file css_path => [work_dir[target], css_file] do sh "cp", css_file.to_s, css_path.to_s end end # open -------------------- task "open_#{target}".to_sym => target do sh "open", pdf[target] end # clean -------------------- CLEAN.include(pdf[target]) CLEAN.include(work_dir[target]) end
これで
$ rake build[ebook]
とすれば電子用が、
$ rake build[print]
とすれば印刷用が、依存関係をチェックして必要な前処理やコピーなどを行いながらビルドされる。
(rake open
というのも使えて、ビルド後にプレビューアプリで開かれる)
このあたりのバリアントを作ったり依存関係をチェックしながらビルドする機能が最初からあるともっと便利かなと思った。 自分はTypeScript分からないので貢献しようとするとちょっと大変だけど。
最後にちょっと宣伝で、そんな感じでVivliostyleで作った新刊は技術書典のサイトから購入可能なので、気になる方はぜひ。
今日はここまで!