いものやま。

雑多な知識の寄せ集め

Vivliostyleで技術同人誌を書いてみた。

技術書典14の新刊として『オブジェクト・ウォーズ』を書いた。

このとき、組版システムとしてVivliostyleを使ってみたので、その感想とか。

Vivliostyleとは

本を作ろうとすると文章のレイアウト(組版)が必要になって、そのために使われるのが組版システム。  \TeXInDesignなどいろいろなソフトがあって、自分も独自のシステムを作ってみたりしてるんだけど、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で作った新刊は技術書典のサイトから購入可能なので、気になる方はぜひ。

今日はここまで!