Ruby
GitHub
asciidoctor
asciidoc

執筆活動を支える技術 #ruby超入門

ゼロからわかる Ruby🔰超入門」という本を書きました。共同執筆での作業を効率的に進められるように、編集者・レビュワーさんを含め、みんながいつでも最新の原稿を確認できるように環境を整えました。ここでは、技術面・環境面で工夫したこと、得た知見を共有します。書籍に限らず、技術文書の作成にも使えます。


はじめに

この本は、 @igaiga さんと共同で執筆しました。イラストを描いてくれた @becolomochi さんを含めて、3人での共同作業でした。原稿を書いてから公開するまでのフローはこんな感じです。プログラミングでの開発に似ています。


  1. 原稿を書く (Asciidoc, Atom, Visual Studio Code)

  2. 共有する (GitHub, Slack)

  3. HTML/PDF形式に変換する (Rakefile, CircleCI)

  4. 限定公開する (docker, nginx)

Ruby超入門を支える技術


Step 1 原稿を書く (Asciidoc, Atom, Visual Studio Code)

執筆フォーマットについて、出版社からの指定は特にありませんでした。例えば、Microsoft Wordでもなんでも良いです。しかし、GitHubで履歴を管理できるように、テキストプレーンで書けることと、HTML形式でプレビューできることから、Asciidocというフォーマットを使いました。

Asciidocを扱うツールであるAsciidoctorが、Ruby製のツールだったことも、採用した大きな理由です😃


Asciidoc

AsciidocはMarkdownのように書ける記法です。こちらは、Asciidocで書いた原稿の一部です。


setup/00_about_ruby.adoc

== Rubyとは

ここでは、本書で学習するRubyがどのようなプログラミング言語なのかを解説します。

=== Rubyの特徴

世の中にはJavaやPythonなどの、たくさんのプログラミング言語があります。その中でもRubyは、楽しくプログラムを書くことにこだわった言語です。Rubyの公式サイト( https://www.ruby-lang.org/ja/ )では、Rubyの特徴をこのように紹介しています。

> オープンソースの動的なプログラミング言語で、 シンプルさと高い生産性を備えています。 エレガントな文法を持ち、自然に読み書きができます。

.Rubyの公式サイト
image::../images/setup/ruby_website.png[]


このAsciidocの原稿をHTMLに変換すると、このようになります。

スクリーンショット 2018-11-23 17.05.45.e5e7b8af4b8144f19cce38cd35dec308.png

Asciidocは高度なMarkdownという感じで、標準で表や注釈をサポートしています。Markdownも広く普及していて使い勝手も良いのですが、本格的な文章を書くときには少し物足りなくもあります。僕は(この文章のように)単一ページであればMarkdown、複数ページにわたるものはAsciidocという使い分けをしています。

Asciidocには外部ファイルをインクルードする機能があります。執筆時には節単位でファイルを分割し、章単位のファイルでインクルードするようにしました。書籍ともなるとページ数が多くなるので、このようにファイルを分割して管理すると便利です。


setup.adoc

= 環境をつくる

: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形式でのリアルタイムプレビューが使えます。

スクリーンショット 2018-11-17 14.37.23.4fc2d2bcc531456986a592c21d8c4be3.png

Visual Studio CodeにはGit機能も統合されています。修正箇所を確認し、コミットログを書いてGitHubにプッシュするまでの一連の作業をエディター内で完結できるので便利でした。

エディターのメリット・デメリットは宗教戦争になるので割愛します。使いやすいものを使えばよいです。


Step 2 共有する (GitHub, Slack)

ソースコードはGitHubのプライベートリポジトリで管理しました。GitHubはAsciidocに対応しており、Web上でプレビューを見ることができます。

スクリーンショット 2018-11-23 13.39.50.54a97eac3d4f4e61a719175754766d2c.png

執筆段階ではmasterブランチだけのシンプルな運用でした。後半のレビューフェイズに入ってからは、初稿以降の差分管理のために、修正箇所を別ブランチとしてPull Requestベースで管理していきました。

スクリーンショット 2018-11-23 14.20.25.75f08dee150b4e04a72ee547fba916fc.png

修正箇所については、Pull Requestのコメント欄やSlackを使って議論しました。原稿がテキストベースだと、修正箇所のdiffを取りやすかったり、リンクとして該当箇所を示すことができて便利でした。

スクリーンショット 2018-11-23 14.21.56.d29b4e7a48c2464cbf83598a4b930d78.png

厳密に決めていた訳ではありませんが、自然と大きな確認はGitHubのコメント欄で、より細かく話したいときはSlackでという使い分けになっていました。Slackで話した後に、GitHub側にSlackスレッドのURLを貼っておけば、あとから議論を追跡できます。

スクリーンショット 2018-11-23 17.24.16.2c8e6ad8005d4685afd58022df3399d6.png


Step 3 HTML/PDF形式に変換する (Rakefile, CircleCI)

執筆の中盤は、GitHubにアップロードした原稿をいつでもHTMLとPDF形式で読めるようにしていました。具体的にはCIツールを使って、Asciidoc形式をHTML形式とPDF形式に変換します。今回はCircleCIを使いました。

また、同時にサンプルプログラムをテストできるようテストコードも作りました。今回はやりませんでしたが、校正チェックツールも一緒に回しても良いかもしれません。


asciidocからHTMLとPDFへの変換

Rakefileにて、asciidoc形式をHTMLとPDFに変換するタスクを作ります。


Rakefile

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行だけです。


Rakefile

sh "asciidoctor -d book -o #{OUTDIR}/index.html -n #{SOURCE}"



Rakefile

sh "asciidoctor-pdf -r asciidoctor-pdf-cjk drafts/index.adoc -o #{OUTDIR}/rbbook.pdf"


ここでは外部コマンドとして asciidoctorasciidoctor-pdf を呼び出しています。asciidoctorはRuby製のツールなので、本当は外部コマンドでなくもう少しスマートな呼び出し方もありそうです。


目次を自動で作る & 原稿を1ファイルに連結する

前述のとおり、原稿は節ごとにファイルを分割していました。編集者さんから全ファイルを結合したasciidoc形式のファイルが欲しいと言われたので、Rakefileに原稿を結合するタスクを作りました。ここだけasciidoctorのライブラリをRubyで直接読み込んでいます。


Rakefile

# 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で置き換えているだけです。


Rakefile

# toc.adocを生成

sh "grep -E '^={1,3}\s' all.adoc | sed -e 's/=/*/g' > toc.adoc"


サンプルプログラムをテストする

執筆後半ではサンプルプログラムが増えてきて、動作チェックもWindowsとMacの両方で実施します。量が増えると手動テストも大変になるので、テストを自動化しました。テストツールにはminitestを採用しました。

今回は入門書のため、putsでコンソールに出力するサンプルプログラムが主でした。


codes/chapter4/4-1/array1.rb

p ["カフェラテ", "モカ", "コーヒー"]



codes/chapter4/4-1/array2.rb

p ["カフェラテ", 400, 1.08] # 文字列オブジェクト、 整数オブジェクト、小数オブジェクトが入った配列

p [300] # 要素が1つの配列
p [] # 要素が1つもない空の配列

そこで、minitestの assert_output を使って標準出力に期待する結果が出力されているかをチェックするようにしました。以下のテストコードのうち、 stdout 変数に格納している文字列が期待する出力です。


test/chapter4_test.rb

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のスニペットにテンプレートを作ってテストコードの雛形を簡単に貼り付けられるようにしました。

スクリーンショット 2018-11-23 17.47.21.903e1b865c244e77abb45d88d989ee54.png

エディターで ;test と入力すると、スニペットの画面が開きます。 **name** にサンプルコードのファイル名、 **chapter** にフォルダー名を入力するだけで、テストコードの雛形が貼り付けられます。あとはEOSのところに期待する出力結果を貼り付ければ、テストの完成です。

  def test_**name**

stdout = <<-EOS

EOS
assert_output(stdout) {
load "#{SOURCE_DIR}/**chapter**/**name**.rb"
}
end

一部のサンプルコードは、標準入力(キーボード)から値を入力します。そのようなケースでは、 StringIO を使って標準入力を切り替えたテストコードを書きました。以下のテストコードでは、キーボードから2と3を入力すると、画面に5が表示されることを期待しています。


test/chapter2_test.rb

  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でホスティングすることにしました。


docker-compose.yml

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認証を設定します。


nginx.conf

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サーバに登録されるようになります。


.circleci/config.yml

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はベクターの図を書けるサービスです。図の下書きに利用しました。最終的にはデザイナーさんにイラストレーターで書き直してもらっています。(ので、ここまで丁寧に書かなくてよかった)

draw.io


PlantUML

PlantUMLはクラス図の下書きに利用しました。asciidocとも親和性が高いです。

PlantUML


exception.pu

@startuml

class Exception
note left: 全ての例外の祖先のクラス
(中略)
class ZeroDivisionError

Exception <|-- NoMemoryError
Exception <|-- ScriptError
ScriptError <|-- SyntaxError
(中略)
StandardError <|-- ZeroDivisionError
@enduml



おわりに

共同執筆の環境では、GitHubを中心としテキストプレインで原稿を管理するスタイルはやりやすかったです。使っている技術も、Webサービス開発で使う技術と同じなので、導入しやすいでしょう。この記事があなたの執筆活動の参考になれば幸いです。

また、イラスト面での裏話を @becolomochi さんが書かれています。あわせてご覧ください。


参考リンク

AsciiDoc

Visual Studio Code

minitest

Dash (スニペット)

CircleCI

ゼロからわかるRuby超入門