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

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

## はじめに

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

 1. 原稿を書く (Asciidoc, Atom, Visual Studio Code)
 2. 共有する (GitHub, Slack)
 3. HTML/PDF形式に変換する (Rakefile, CircleCI)
 4. 限定公開する (docker, nginx)

![Ruby超入門を支える技術](https://qiita-image-store.s3.amazonaws.com/0/15802/f6b5ba9c-43f3-116e-0b07-b355a2ba0fad.png)

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

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

Asciidocを扱うツールである[Asciidoctor](https://asciidoctor.org)が、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に変換すると、このようになります。

<img width="917" alt="スクリーンショット 2018-11-23 17.05.45.e5e7b8af4b8144f19cce38cd35dec308.png" src="https://qiita-image-store.s3.amazonaws.com/0/15802/e0e5bbe2-f78b-b405-614b-2b19c03e485c.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形式でのリアルタイムプレビューが使えます。

<img width="1557" alt="スクリーンショット 2018-11-17 14.37.23.4fc2d2bcc531456986a592c21d8c4be3.png" src="https://qiita-image-store.s3.amazonaws.com/0/15802/736871e8-03d7-6fd5-6894-86edc00f0030.png">

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

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

## Step 2 共有する (GitHub, Slack)

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

<img width="640" alt="スクリーンショット 2018-11-23 13.39.50.54a97eac3d4f4e61a719175754766d2c.png" src="https://qiita-image-store.s3.amazonaws.com/0/15802/479dbe11-0b20-e18f-14b9-23618cfb7487.png">

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

<img width="640" alt="スクリーンショット 2018-11-23 14.20.25.75f08dee150b4e04a72ee547fba916fc.png" src="https://qiita-image-store.s3.amazonaws.com/0/15802/c97fdc4d-8179-12d1-e044-02a1ef680113.png">

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

<img width="640" alt="スクリーンショット 2018-11-23 14.21.56.d29b4e7a48c2464cbf83598a4b930d78.png" src="https://qiita-image-store.s3.amazonaws.com/0/15802/7f353b7a-962c-e574-cfb7-1f04bf8d3fbf.png">

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

<img width="480" alt="スクリーンショット 2018-11-23 17.24.16.2c8e6ad8005d4685afd58022df3399d6.png" src="https://qiita-image-store.s3.amazonaws.com/0/15802/8990b3ab-4860-01a0-4a17-cedb50e1b53e.png">

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

執筆の中盤は、GitHubにアップロードした原稿をいつでもHTMLとPDF形式で読めるようにしていました。具体的にはCIツールを使って、Asciidoc形式をHTML形式とPDF形式に変換します。今回はCircleCIを使いました。
また、同時にサンプルプログラムをテストできるようテストコードも作りました。今回はやりませんでしたが、校正チェックツールも一緒に回しても良いかもしれません。

### asciidocからHTMLとPDFへの変換

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

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

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

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

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

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

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

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

```ruby:Rakefile
# toc.adocを生成
sh "grep -E '^={1,3}\s' all.adoc | sed -e 's/=/*/g' > toc.adoc"
```

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

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

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

```ruby:codes/chapter4/4-1/array1.rb
p ["カフェラテ", "モカ", "コーヒー"]
```

```ruby:codes/chapter4/4-1/array2.rb
p ["カフェラテ", 400, 1.08] # 文字列オブジェクト、　整数オブジェクト、小数オブジェクトが入った配列
p [300] # 要素が1つの配列
p [] # 要素が1つもない空の配列
```

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

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

<img width="640" alt="スクリーンショット 2018-11-23 17.47.21.903e1b865c244e77abb45d88d989ee54.png" src="https://qiita-image-store.s3.amazonaws.com/0/15802/255ba002-2f7a-9463-c141-d7cb5c23c85a.png">

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

```ruby
  def test_**name**
    stdout = <<-EOS

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

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

```ruby: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]([https://www.netlify.com])が最適です。netlifyはGitHubと連携したビルドと公開サーバの機能を備えています。しかし、原稿をレビュワーさんに限定公開するためにBasic認証を使おうとすると$45/月となり、今回の用途ではやや割高との判断でした。

そこで、いつも使っているさくらのVPSサーバ上に、dockerを使ってnginxでホスティングすることにしました。

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

```yaml:.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](https://workflowy.com)はオンラインで使えるアウトラインエディターです。アイデアを書き出してまとめるときに使いました。

### draw.io

[draw.io](https://www.draw.io)はベクターの図を書けるサービスです。図の下書きに利用しました。最終的にはデザイナーさんにイラストレーターで書き直してもらっています。（ので、ここまで丁寧に書かなくてよかった）

<img width="320" alt="draw.io" src="https://qiita-image-store.s3.amazonaws.com/0/15802/dd043489-58c7-89b5-3bf7-510bc8aa41f4.png">

### PlantUML

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

<img width="640" alt="PlantUML" src="https://qiita-image-store.s3.amazonaws.com/0/15802/8724ada3-76e0-84a2-d8ec-7ec52cdb8dc6.png">

```exception.pu
@startuml
class Exception
note left: 全ての例外の祖先のクラス
（中略）
class ZeroDivisionError

Exception <|-- NoMemoryError
Exception <|-- ScriptError
ScriptError <|-- SyntaxError
（中略）
StandardError <|-- ZeroDivisionError
@enduml
```

## おわりに

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

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

 * [#Ruby超入門 のかわいいキャラクターたちが生まれるまで](https://note.mu/becolomochi/n/nbfac9ad3a8d0)

## 参考リンク

AsciiDoc

 * [AsciiDoc Syntax Quick Reference](https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/)
 * [Asciidoctor 文法クイックリファレンス(日本語訳)](https://takumon.github.io/asciidoc-syntax-quick-reference-japanese-translation/ "Asciidoctor 文法クイックリファレンス(日本語訳)")
 * [AsciiDoc入門](https://qiita.com/xmeta/items/de667a8b8a0f982e123a)
 * [AsciiDocを全力で勧める 4つの理由](https://ryuta46.com/359)
 * [脱Word、脱Markdown、asciidocでドキュメント作成する際のアレコレ](https://qiita.com/tamikura@github/items/5d3f62dae55617ee42bb)

Visual Studio Code

 * [AsciiDoc - Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=joaompinto.asciidoctor-vscode)

minitest

 * [GitHub - seattlerb/minitest](https://github.com/seattlerb/minitest)
 * [Ruby minitest で標準出力をテストする](https://qiita.com/YutakakINJO/items/80a229dbfbaf11004b77)
 * [Ruby で標準入出力のテスト](https://qiita.com/key-amb/items/a134e2c3bea172b3deeb)

Dash (スニペット)

 * [あっという間に仕事効率化。ブログにも使えるMac定型文ツール「Dash」の設定方法と使い方](https://requlog.com/self-branding/management/dash/)
 * [Dashのスニペット機能をちゃんと使う](https://qiita.com/hachi8833/items/5c195f1164ce7c5e65ab)

CircleCI

 * [Circle CIでwebサイトを自動デプロイ - esnetk6.com](https://esnetk6.com/2018/04/circle-ci-deployment/)

ゼロからわかるRuby超入門

 * [ゼロからわかる Ruby 超入門：書籍案内｜技術評論社 - Gihyo](https://gihyo.jp/book/2018/978-4-297-10123-7)
 * [#Ruby超入門 のかわいいキャラクターたちが生まれるまで](https://note.mu/becolomochi/n/nbfac9ad3a8d0)