「ゼロからわかる Ruby🔰超入門」という本を書きました。共同執筆での作業を効率的に進められるように、編集者・レビュワーさんを含め、みんながいつでも最新の原稿を確認できるように環境を整えました。ここでは、技術面・環境面で工夫したこと、得た知見を共有します。書籍に限らず、技術文書の作成にも使えます。
はじめに
この本は、 @igaiga さんと共同で執筆しました。イラストを描いてくれた @becolomochi さんを含めて、3人での共同作業でした。原稿を書いてから公開するまでのフローはこんな感じです。プログラミングでの開発に似ています。
- 原稿を書く (Asciidoc, Atom, Visual Studio Code)
- 共有する (GitHub, Slack)
- HTML/PDF形式に変換する (Rakefile, CircleCI)
- 限定公開する (docker, nginx)
Step 1 原稿を書く (Asciidoc, Atom, Visual Studio Code)
執筆フォーマットについて、出版社からの指定は特にありませんでした。例えば、Microsoft Wordでもなんでも良いです。しかし、GitHubで履歴を管理できるように、テキストプレーンで書けることと、HTML形式でプレビューできることから、Asciidocというフォーマットを使いました。
Asciidocを扱うツールであるAsciidoctorが、Ruby製のツールだったことも、採用した大きな理由です😃
Asciidoc
AsciidocはMarkdownのように書ける記法です。こちらは、Asciidocで書いた原稿の一部です。
== Rubyとは
ここでは、本書で学習するRubyがどのようなプログラミング言語なのかを解説します。
=== Rubyの特徴
世の中にはJavaやPythonなどの、たくさんのプログラミング言語があります。その中でもRubyは、楽しくプログラムを書くことにこだわった言語です。Rubyの公式サイト( https://www.ruby-lang.org/ja/ )では、Rubyの特徴をこのように紹介しています。
> オープンソースの動的なプログラミング言語で、 シンプルさと高い生産性を備えています。 エレガントな文法を持ち、自然に読み書きができます。
.Rubyの公式サイト
image::../images/setup/ruby_website.png[]
このAsciidocの原稿をHTMLに変換すると、このようになります。
Asciidocは高度なMarkdownという感じで、標準で表や注釈をサポートしています。Markdownも広く普及していて使い勝手も良いのですが、本格的な文章を書くときには少し物足りなくもあります。僕は(この文章のように)単一ページであればMarkdown、複数ページにわたるものはAsciidocという使い分けをしています。
Asciidocには外部ファイルをインクルードする機能があります。執筆時には節単位でファイルを分割し、章単位のファイルでインクルードするようにしました。書籍ともなるとページ数が多くなるので、このようにファイルを分割して管理すると便利です。
= 環境をつくる
:toc:
:toclevels: 2
:sectnums:
:imagesdir: images/
"創作するスキル。演繹力。推論力。知的な頷き。Rubyはあなたの心と世界とを結びつけるツールになる。"
-- _whyの(感動的)rubyガイド 第1章より
ようこそ、Rubyプログラミングの世界へ。プログラムを書くためには、Rubyやエディタなどのツールを使います。これらのツールはあなたのプログラミングの支えになってくれるでしょう。この章では、Rubyやエディタのインストール方法とRubyプログラムの動かし方を説明します。
include::setup/00_about_ruby.adoc[]
include::setup/01a_install_ruby_windows.adoc[]
include::setup/01b_install_ruby_mac.adoc[]
他にも、今回の執筆では使いませんでしたが、図表にタイトルをつけたり、Microsoft Wordにあるような相互参照の機能もあり、使いこなせば電子出版の原稿に最適です。
Asciidocのメリット
- plain textなのでGitHubで管理しやすい
- インクルード機能により原稿を複数ファイルに分割できる
- 注釈、表、キートップ表記に対応している
- HTML, PDFに変換できる(詳しくは後述します)
Asciidocのデメリット
- Markdownと比べて対応エディタが限られる
- 多機能な分だけ使いこなすのが難しい
Visual Studio Code
原稿を書くためのエディターは、書き始めはAtomを使っていました。途中からはVisual Studio Codeを使うようになりました。どちらのエディターも、プラグインによりAsciidocに対応します。エディター内のコードハイライトや、HTML形式でのリアルタイムプレビューが使えます。
Visual Studio CodeにはGit機能も統合されています。修正箇所を確認し、コミットログを書いてGitHubにプッシュするまでの一連の作業をエディター内で完結できるので便利でした。
エディターのメリット・デメリットは宗教戦争になるので割愛します。使いやすいものを使えばよいです。
Step 2 共有する (GitHub, Slack)
ソースコードはGitHubのプライベートリポジトリで管理しました。GitHubはAsciidocに対応しており、Web上でプレビューを見ることができます。
執筆段階ではmasterブランチだけのシンプルな運用でした。後半のレビューフェイズに入ってからは、初稿以降の差分管理のために、修正箇所を別ブランチとしてPull Requestベースで管理していきました。
修正箇所については、Pull Requestのコメント欄やSlackを使って議論しました。原稿がテキストベースだと、修正箇所のdiffを取りやすかったり、リンクとして該当箇所を示すことができて便利でした。
厳密に決めていた訳ではありませんが、自然と大きな確認はGitHubのコメント欄で、より細かく話したいときはSlackでという使い分けになっていました。Slackで話した後に、GitHub側にSlackスレッドのURLを貼っておけば、あとから議論を追跡できます。
Step 3 HTML/PDF形式に変換する (Rakefile, CircleCI)
執筆の中盤は、GitHubにアップロードした原稿をいつでもHTMLとPDF形式で読めるようにしていました。具体的にはCIツールを使って、Asciidoc形式をHTML形式とPDF形式に変換します。今回はCircleCIを使いました。
また、同時にサンプルプログラムをテストできるようテストコードも作りました。今回はやりませんでしたが、校正チェックツールも一緒に回しても良いかもしれません。
asciidocからHTMLとPDFへの変換
Rakefileにて、asciidoc形式をHTMLとPDFに変換するタスクを作ります。
require 'asciidoctor'
require 'rake/clean'
require 'rake/testtask'
OUTDIR = "public"
SOURCE = "drafts/index.adoc"
CLEAN.include(OUTDIR)
Rake::TestTask.new do |test|
test.test_files = Dir['test/**/*_test.rb']
test.verbose = true
end
desc 'Render the docs'
task :html do
# public/index.htmlを生成
sh "asciidoctor -d book -o #{OUTDIR}/index.html -n #{SOURCE}"
sh "cp -rp drafts/images #{OUTDIR}"
end
task :pdf do
sh "asciidoctor-pdf -r asciidoctor-pdf-cjk drafts/index.adoc -o #{OUTDIR}/rbbook.pdf"
end
task :default => [:test]
いろいろ書いていますが、ポイントは以下2行だけです。
sh "asciidoctor -d book -o #{OUTDIR}/index.html -n #{SOURCE}"
sh "asciidoctor-pdf -r asciidoctor-pdf-cjk drafts/index.adoc -o #{OUTDIR}/rbbook.pdf"
ここでは外部コマンドとして asciidoctor
と asciidoctor-pdf
を呼び出しています。asciidoctorはRuby製のツールなので、本当は外部コマンドでなくもう少しスマートな呼び出し方もありそうです。
目次を自動で作る & 原稿を1ファイルに連結する
前述のとおり、原稿は節ごとにファイルを分割していました。編集者さんから全ファイルを結合したasciidoc形式のファイルが欲しいと言われたので、Rakefileに原稿を結合するタスクを作りました。ここだけasciidoctorのライブラリをRubyで直接読み込んでいます。
# all.adocを生成
doc = Asciidoctor.load_file SOURCE, safe: :unsafe, parse: false
open("all.adoc", 'w') do |out|
out.puts doc.reader.read
end
また、結城さんの数学文章作法に、「目次を自動で作れるようにすること」と書いてあったので、以下のタスクで目次を自動生成するようにしました。こちらは単純にgrepとsedで置き換えているだけです。
# toc.adocを生成
sh "grep -E '^={1,3}\s' all.adoc | sed -e 's/=/*/g' > toc.adoc"
サンプルプログラムをテストする
執筆後半ではサンプルプログラムが増えてきて、動作チェックもWindowsとMacの両方で実施します。量が増えると手動テストも大変になるので、テストを自動化しました。テストツールにはminitestを採用しました。
今回は入門書のため、putsでコンソールに出力するサンプルプログラムが主でした。
p ["カフェラテ", "モカ", "コーヒー"]
p ["カフェラテ", 400, 1.08] # 文字列オブジェクト、 整数オブジェクト、小数オブジェクトが入った配列
p [300] # 要素が1つの配列
p [] # 要素が1つもない空の配列
そこで、minitestの assert_output
を使って標準出力に期待する結果が出力されているかをチェックするようにしました。以下のテストコードのうち、 stdout
変数に格納している文字列が期待する出力です。
require 'minitest/autorun'
BASE_DIR = File.expand_path(File.join(File.dirname(__FILE__), '..'))
class Chapter4Test < Minitest::Test
SOURCE_DIR = "#{BASE_DIR}/codes/chapter4"
def test_array1
stdout = <<-EOS
["カフェラテ", "モカ", "コーヒー"]
EOS
assert_output(stdout) {
load "#{SOURCE_DIR}/4-1/array1.rb"
}
end
def test_array2
stdout = <<-EOS
["カフェラテ", 400, 1.08]
[300]
[]
EOS
assert_output(stdout) {
load "#{SOURCE_DIR}/4-1/array2.rb"
}
end
end
テストケースは似たような構造の繰り返しでした。変わる部分は stdout
変数の中身と読み込むファイル名くらいです。そこで、力技ですがDashのスニペットにテンプレートを作ってテストコードの雛形を簡単に貼り付けられるようにしました。
エディターで ;test
と入力すると、スニペットの画面が開きます。 **name**
にサンプルコードのファイル名、 **chapter**
にフォルダー名を入力するだけで、テストコードの雛形が貼り付けられます。あとはEOSのところに期待する出力結果を貼り付ければ、テストの完成です。
def test_**name**
stdout = <<-EOS
EOS
assert_output(stdout) {
load "#{SOURCE_DIR}/**chapter**/**name**.rb"
}
end
一部のサンプルコードは、標準入力(キーボード)から値を入力します。そのようなケースでは、 StringIO
を使って標準入力を切り替えたテストコードを書きました。以下のテストコードでは、キーボードから2と3を入力すると、画面に5が表示されることを期待しています。
def test_gets2
input = <<-EOS
2
3
EOS
stdout = <<-EOS
5
EOS
$stdin = StringIO.new(input)
assert_output(stdout) {
load "#{SOURCE_DIR}/2-3/gets2.rb"
}
$stdin = STDIN
end
ちなみにテストコードを書いたのは、執筆の最後の最後でした。レビュー中にコードはどんどん書き換わるし、各コードは独立していてデグレの危険性もないので、最後にテストコードを書くという判断は正しかったと思っています。
本当は、サンプルコード、標準出力の結果を別ファイルにしておき、テストコードとasciidocの本文それぞれにインクルードする仕組みを作れれば、もう少し楽だったかもしれません。
Step 4 限定公開する (docker, nginx)
ビルドしたPDFとHTMLをレビュー用にWebサーバに公開します。こういう用途ではnetlifyが最適です。netlifyはGitHubと連携したビルドと公開サーバの機能を備えています。しかし、原稿をレビュワーさんに限定公開するためにBasic認証を使おうとすると$45/月となり、今回の用途ではやや割高との判断でした。
そこで、いつも使っているさくらのVPSサーバ上に、dockerを使ってnginxでホスティングすることにしました。
version: '2'
services:
web:
image: nginx
volumes:
- /home/core/var/rbbook-preview:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./mime.types:/etc/nginx/mime.types:ro
- ./.htpasswd:/etc/nginx/.htpasswd:ro
environment:
- VIRTUAL_HOST=example.com
- LETSENCRYPT_HOST=example.com
- LETSENCRYPT_EMAIL=example@example.com
- NGINX_HOST=example.com
- NGINX_PORT=80
restart: always
networks:
default:
external:
name: nginx-proxy
nginx.conf
では .htpasswd
を使ってBasic認証を設定します。
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
server {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
root /usr/share/nginx/html;
}
}
CircleCIにて、ビルド後にscpでWebサーバへデプロイしています。これで、原稿をGitHubにプッシュするたびに自動でHTMLとPDFが生成され、Webサーバに登録されるようになります。
version: 2
jobs:
build:
branches:
only:
- master
docker:
- image: circleci/ruby
working_directory: ~/repo
steps:
- checkout
- add_ssh_keys:
fingerprints:
- "*******************************"
- run:
name: Start ssh-keyscan
command: |
ssh-keyscan example.com >> ~/.ssh/known_hosts
- run: bundle install --path vendor/bundle
- run:
name: Create HTML
command: bundle exec rake html pdf
- run:
name: Deploy HTML to preview server
command: scp -rp public/* example@example.com:preview/
終盤:PDFベースでのやり取り
この環境を使ったのは、執筆序盤から初稿作成、第2稿作成くらいまででした。今回は入門者向けの書籍のため、最終的には編集部さんにて綿密にレイアウトいただきました。そのため、終盤は編集部さんに作っていただいたPDFをDropboxで共有し、iPadなどで赤入れする作業でした。編集さんが指定ページ内に原稿を収めていく超絶レイアウトは、まさしくプロの技でした。
それでも、GitHubのIssueは最後まで使っていました。
ここで紹介しなかったツールたち
WorkFlowy
WorkFlowyはオンラインで使えるアウトラインエディターです。アイデアを書き出してまとめるときに使いました。
draw.io
draw.ioはベクターの図を書けるサービスです。図の下書きに利用しました。最終的にはデザイナーさんにイラストレーターで書き直してもらっています。(ので、ここまで丁寧に書かなくてよかった)
PlantUML
PlantUMLはクラス図の下書きに利用しました。asciidocとも親和性が高いです。
@startuml
class Exception
note left: 全ての例外の祖先のクラス
(中略)
class ZeroDivisionError
Exception <|-- NoMemoryError
Exception <|-- ScriptError
ScriptError <|-- SyntaxError
(中略)
StandardError <|-- ZeroDivisionError
@enduml
おわりに
共同執筆の環境では、GitHubを中心としテキストプレインで原稿を管理するスタイルはやりやすかったです。使っている技術も、Webサービス開発で使う技術と同じなので、導入しやすいでしょう。この記事があなたの執筆活動の参考になれば幸いです。
また、イラスト面での裏話を @becolomochi さんが書かれています。あわせてご覧ください。
参考リンク
AsciiDoc
- AsciiDoc Syntax Quick Reference
- Asciidoctor 文法クイックリファレンス(日本語訳)
- AsciiDoc入門
- AsciiDocを全力で勧める 4つの理由
- 脱Word、脱Markdown、asciidocでドキュメント作成する際のアレコレ
Visual Studio Code
minitest
Dash (スニペット)
CircleCI
ゼロからわかるRuby超入門