✍🏻

Asciidoc centricな(Zenn含む)複数メディア向け電子書籍作成環境

2021/12/14に公開

本記事はQiita Advent Calendar "ドキュメンテーション Advent Calendar 2021" 12/15(水)分の記事になります。

https://qiita.com/advent-calendar/2021/documentation

はじめに

Twitterで活動をご覧の方や、今年の別なAdvent Calendarをご覧になった方はご存知かと思いますが、

https://dimeiza.hatenablog.com/entry/2021/12/11/080000

今年の5月、ちょっと縁があって本を書いたんですよ。

https://zenn.dev/dimeiza/books/professional_engineer_guide_book

最初に本を書いたのはZenn上だったんですが、その後で欲が出てきて色々なメディアに展開しようとした際、色々と手間が発生し、試行錯誤をそこそこたくさんこなした結果、何やら良さげな環境ができまして。

Advent Calendarにかこつけて共有してみたくなったので、この記事を書きました。

電子書籍の複数メディア展開

今私が記事や本をアップロードしている、このZennではmarkdownで電子書籍を書くことになります。

この書籍をAmazon Kindle向けに提供しようとすると、KDP(Kindle Desktop Publishing)というサイトに持ち込むことになるんですが、

https://kdp.amazon.co.jp/ja_JP/help/topic/G200634390

Amazon独自の形式を除くと、Wordやepubといったファイル形式を求められるわけです。

さらに、epubを配信できなかったり、電子書籍リーダを期待できない環境に対しては、pdfのようなポータビリティのあるフォーマットを提供しなければならなくなります[1]

となるとですよ、我々が電子書籍頒布する際、特に複数メディアに展開する際に要求されるであろうフォーマットとしては、

  • markdown(Zenn等Webコンテンツ向け)
  • epub(Kindle:KDP等電子書籍頒布サイト向け)
  • pdf(紙書籍作成や、電子書籍リーダ非対応用途向け)

この3つは必要になってくるわけです。

Single source化への切実な要求

今回の場合、私自身はまずZennに書籍を書いたので、ソース自体はmarkdownで構成していたんですよ。

その後、KindleやBoothでも配布してみたいなぁという欲が出てきて、epubやpdfファイルを用意しようとしたんですが、これが意外に大変で。後で説明しますが、markdownからepubやpdfをそのまま作ろうとすると、種々不都合があったりしました。

仕方がないので、(業務で仕様書等を書くのに使っている)Asciidocでほぼ同じ内容を書いてepubやpdfを生成し、頒布したんですが、これはこれで二重管理コンテンツのメンテナンスが大変で。

商用書籍のように、ガチガチに校正をかけて出版するわけではないので誤記、誤植は残りますし、そもそも後で内容を書き足したいと思うことも電子書籍ならではの欲求として発生するわけです。

実際に何度も改訂したんですが、同じ内容を2回追記する面倒に加え、修正漏れやら同期ミスやら、手作業由来の面倒事が色々出てきました。

そうなると、我々ITエンジニアとしては、コードの移植性を考えるときのように、Write once, run anywhereをやりたいわけですよ。

  • コンテンツの内容は同じなのだから、単一のソースから複数の電子書籍フォーマットへ自動展開できるようにできないか?

という話です。

考えておくべき事項

で、実際にいろいろ試行錯誤に入ったんですが、色々と障壁があり、私自身にもこだわりがありました。

markdownの表現力不足

最初はZennのmarkdownをベースにしてpandocとかで何とかならないかと思ったわけなんですが。

https://pandoc.org/

変換してみると、少なくとも2つ、markdownでは絶対に超えられない問題があったんですよ。

テーブル幅の個別設定

markdownのtable表現では(独自拡張してないかぎり)、テーブル内の各列の幅を個別に設定できません。

例えばこの表を表現するのに、

設問 コンピテンシー
必須科目I 1. 技術者視点での社会課題分析(専門的学識、問題解決)
2. 最重要課題の分析と解決策の提案(問題解決)
3. 解決策の評価(評価)
4. 倫理性、持続可能性からの記述(技術者倫理)
選択科目I 1. 知識、技術の説明(専門的学識)
選択科目II 1. 調査すべき事項の検討(専門的学識)
2. 業務手順と工夫の説明(マネジメント)
3. 関係者との円滑な調整(リーダーシップ、コミュニケーション)
選択科目III 1. 技術者視点での社会課題分析(専門的学識、問題解決)
2. 最重要課題の分析と解決策の提案(問題解決)
3. 解決後の状態、残存課題の抽出(評価)
|設問|コンピテンシー|
|:-:|:-|
|必須科目I|1. 技術者視点での社会課題分析(専門的学識、問題解決)<br>2. 最重要課題の分析と解決策の提案(問題解決)<br>3. 解決策の評価(評価)<br>4. 倫理性、持続可能性からの記述(技術者倫理)|
|選択科目I|1. 知識、技術の説明(専門的学識)|
|選択科目II|1. 調査すべき事項の検討(専門的学識)<br>2. 業務手順と工夫の説明(マネジメント)<br>3. 関係者との円滑な調整(リーダーシップ、コミュニケーション)
|選択科目III|1. 技術者視点での社会課題分析(専門的学識、問題解決)<br>2. 最重要課題の分析と解決策の提案(問題解決)<br>3. 解決後の状態、残存課題の抽出(評価)

こんな感じで書くことになると思うんですが、見ての通り、列内の配置は設定できても、列の幅を個別に設定できないんですよね。

markdownの場合はhtmlのレンダリング時にある程度よろしくやってくれますが、epubやpdfでこれをやると結構大変なことになります。

例えばepubにするとこんな感じで、左列に無駄な領域ができることもあります。

image

列幅をスマートに制御できないと、非Webコンテンツに展開した時に見苦しくなる、というのが1つ目の問題です。

ページ制御

もう一つはページ制御です。

markdownの世界では、基本的にページの概念がないので、ページ番号や改ページに対応する概念がまったくないんですね。

一応、見出しとCSSを併用すれば、見出し毎に改ページする制御を持ち込ませることもでき、改ページに対してはある程度対応させられるんですが、ページ番号の振り方の細かい制御はなかなか難しいです。

特にページ番号が問題になるのがpdfで、

  • 表紙やタイトルページ、目次ページにはページ番号を振りたくない。
  • それ以降のページには必ずページ番号を振らないと、目次が用をなさなくなる。

という点が大きく、ページ番号の柔軟な制御から絶対に逃げることができません。

元ファイルのフォーマットをどうする…?

こんな感じで、そもそもmarkdownは、電子書籍の各形式に変換するに当たって、十分な表現力を持っていないことが分かりました。

となると別な方式を中心に据える必要があって、色々検討はしたんですが、複数触ってみて、私が譲れないと思ったのはこの2つでした。

組版に関する知識を一切持ち込まずにコンテンツを記述したい。

我々の目的はあくまでもコンテンツの作成であって、組版は手段でしか無いわけです。我々は印刷屋ではないので。

ページ番号、目次作成、ヘッダ、フッタなど、Word等の日常文書作成で求められる知識だけで電子書籍や紙の書籍を世に出したいのです。

これを考えると、Re:VIEWとかは必要以上に組版に深入りしていて、私にとっては煩雑だったので採用しませんでした。

ソースファイルの可読性を最大化したい。

文書作成目的で複数のフォーマットに展開でき、かつ表現力が高い形式としては、20世紀から長い歴史を持つTexがあるわけです。

が、Texの可読性[2]は、こうして自然言語で書かれた文書と比較すると著しく低いです。

また、ちょっとした文字飾りをするにもいちいちタグを覚えなければならない、という意味で、学習曲線も利用時の脳内負荷も高いと考えています。

私は頭が悪いので、文書を書く時にタグの利用でいちいち頭を回したくないのです。タグのためではなく、文章のためだけに頭と手を動かしたいのです。

20世紀の未整備な環境の中であれだけの文書作成環境を実現したドナルド・クヌース先生にはただただリスペクトしか無いんですが、アカデミックな場でもなく、カジュアルに電子書籍を書きたい私にとって、Texは数式表現のためにやむなく使用する必要悪でしかありませんでした。

で、私が採用したのが…。

ということで、私は結局Asciidocを採用することにしました。

https://asciidoc-py.github.io/index.html

使用するツールは、いつも仕事で使い慣れているAsciidoctorです。

https://asciidoctor.org/

Asciidoctor出力の問題とmarkdownの取扱

ただ、基本的にAsciidoctorで出力可能なのは、

  • pdf
  • html
  • docbook
  • epub

といった形式で、markdownを出力することができないのです。

更に言うと、Asciidoctorのepub出力は現時点でαバージョンで、

https://docs.asciidoctor.org/epub3-converter/latest/

Asciidoctor EPUB3 is currently alpha software.

出力されたepubファイルをKDPに登録しようとするとエラーが出力されたりしていて、別なソフト(Calibre)で手動訂正しないと登録できない、という問題に悩まされたりもしました。

つまり、asciidoctor単体では、所望する複数フォーマット出力には不十分なのです。

一方で、一度markdown(GFM:GitHub Flavored Markdown)にしてしまえば、Zennへの展開はもちろん、pandocを使用してエラーなくepubを出力できることも確認できました。

https://pandoc.org/MANUAL.html#epubs

しかし、pandocは入力フォーマットとしてasciidocをサポートしていないのです。

markdownとpandocの間隙をどうやって埋めるか、という点が大きな関心事になったわけです。

docbook経由ツールチェーンの確立

で、調べつつ気づいたのが、両ツールともdocbookという形式をサポートしている、という事実でした。

https://docbook.org/

docbookはxmlで文書構成を表現できるフォーマットなんですが、docbookそのものを直接編集するのではなく、docbookを仲介にしてasciidocの出力とpandocの入力をつないだらどうか、と。

ということで、ようやく各フォーマットを出力するためのツールチェーンが固まりました。

出力形式 出力フロー
pdf asciidoctor-pdfから出力する。
epub asciidoctorでdocbookに変換し、docbookをpandocに入力してepub出力を得る。
Zenn(GFM) asciidoctorでdocbookに変換し、docbookをpandocに入力してmd出力を得る。

変換に当たっての問題群

ただ、実際にはそう簡単ではなく、変換中に種々の問題と解決を伴っています。例えばですね。

数式の表現

私が書いた電子書籍の中には、(使いたくないと言いつつも)Texを使って数式表現している書籍があったりします。

https://zenn.dev/dimeiza/books/professional_engineer_1st_exam_r1_r2

これなんですが、ZennがKatexをサポートしているのを良いことに、解答を解説する際にTexで数式を書いているんですよ。

  • 数式をどうやって各文書に違和感なく埋め込むか?

ということが結構重要な問題になりました。

epubやpdfの場合、Web上のAPIやJavaScriptを使ってTexをレンダリングするわけには行かないので、予め画像に数式をレンダリングした上で、その画像を埋め込むようにしたいところです。

不要なエスケープ文字の追加

Texに関してはもう一つ問題があって。

AsciidocからDocbook経由でpandocに渡すと、GFM出力する際、Tex表現のバックスラッシュが増えてしまう、という問題が観測されたんですよ。

例えばAsciidocでこういう数式表現を書いたとき、

[latexmath]
++++
\begin{aligned}
5 4 3 * + 2 1 + - 
&\text{→} 5 (4 3 *) + (2 1 +) - \\
&\text{→} ( 5 (4 3 *) +) (2 1 +) - \\
&\text{→} ((5 (4 3 *) +) (2 1 +) -) \\
\end{aligned}
++++

docbook上ではこうなんですが、

<informalequation>
<alt><![CDATA[\begin{aligned}
5 4 3 * + 2 1 + -
&\text{→} 5 (4 3 *) + (2 1 +) - \\
&\text{→} ( 5 (4 3 *) +) (2 1 +) - \\
&\text{→} ((5 (4 3 *) +) (2 1 +) -) \\
\end{aligned}]]></alt>
<mathphrase><![CDATA[\begin{aligned}
5 4 3 * + 2 1 + -
&\text{→} 5 (4 3 *) + (2 1 +) - \\
&\text{→} ( 5 (4 3 *) +) (2 1 +) - \\
&\text{→} ((5 (4 3 *) +) (2 1 +) -) \\
\end{aligned}]]></mathphrase>
</informalequation>

GFMに変換するとこうなってしまいまして。

$$\\begin{aligned}
5 4 3 \* + 2 1 + -
&\\text{→} 5 (4 3 \*) + (2 1 +) - \\\\
&\\text{→} ( 5 (4 3 \*) +) (2 1 +) - \\\\
&\\text{→} ((5 (4 3 \*) +) (2 1 +) -) \\\\
\\end{aligned}$$

どうもエスケープ文字が勝手に増えてしまうようなので、これを除去するための細工をしてやる必要がありそうでした。

Zennの画像格納フォルダとの対応

私はZennの書籍をGithub管理しているんですが、この場合、画像はZenn用リポジトリ内のimagesフォルダに一括して格納する必要があります。

https://zenn.dev/zenn/articles/deploy-github-images

  • まずimagesというフォルダを作って、
  • そのフォルダ内に書籍ごとのサブフォルダを作り
  • 書籍毎に画像ファイルを格納する、

という運用になっていて、markdownファイルと画像ファイルは、同一書籍のものであっても別のフォルダに保管されることになります。

が、コンテンツ管理上の都合としては、markdownファイルと画像ファイルを書籍単位でまとめて管理したいわけです。

実際に私は、各電子書籍毎にフォルダを切って、各フォルダ内に、その書籍が利用するmarkdownと画像をすべてまとめていました。

私の場合、このように書籍コンテンツ管理側とZennリポジトリで画像ファイルの管理ポリシーが異なるので、Zennに書籍をデプロイする際には、画像ファイルの配置を変更してやる必要があります。

Zennにそぐわない表記の訂正

あと、pandocからのGFM出力には、2つほどZennにそぐわない表現が出てきていました。

リンク

一つはリンクです。

ZennはURLだけを表記すると、埋め込みのカードを作ってくれる機能があって、Asciidoc側でもその機能を使う意図でURLを書いていたんですが、

 技術士になるための道のりは、日本技術士会の以下のページに載っています。

https://www.engineer.or.jp/contents/become_engineer.html

pandoc経由でgfmに変換すると、'<'と'>'がついてしまうんですよ。

 技術士になるための道のりは、日本技術士会の以下のページに載っています。

<https://www.engineer.or.jp/contents/become_engineer.html>

これを除去する必要があります。

取り消し線

もう一つは取り消し線の表現で、Asciidocではこう書いていた所、

 [line-through]#第一次試験の情報工学部門の専門科目に関する書籍って、ほとんどなくてですね。正攻法で挑むなら、過去問を取ってきて愚直に解きつつ、自分で調べることになるでしょう。#

pandoc経由で出力されたmdでは、spanタグを使ってしまっていたんですよ。

 <span class="line-through">第一次試験の情報工学部門の専門科目に関する書籍って、ほとんどなくてですね。正攻法で挑むなら、過去問を取ってきて愚直に解きつつ、自分で調べることになるでしょう。</span>

GFMには取り消し線の表現があるので、これを使って欲しい所で、ここも修正したいわけです。

 ~~第一次試験の情報工学部門の専門科目に関する書籍って、ほとんどなくてですね。正攻法で挑むなら、過去問を取ってきて愚直に解きつつ、自分で調べることになるでしょう。~~

章構成

Zennとそれ以外の形式で差になってくるのが章構成です。

章、markdownファイル、チャプターの対応関係

Zennで電子書籍を書く際、原則としてmarkdownファイル1つ毎にチャプターが1つ対応する関係になっています。

ただ、実際に書籍を書いてみると、各章の文書記述量は必ずしも均等にならないので、章ごとにmarkdownファイルを1つ用意し、Zennの1チャプターに割り当ててしまうと、

  • チャプター毎に記述分量の差が著しくなる。
  • 各チャプターが肥大化し、気軽に読むことができない分量になる。

と、読み手にとって優しくない構成になってしまいます。

このため、私はZenn版を書いた際に、章をチャプターに直接対応させず、複数の節に分割して、節をチャプターに配置するようにしています (1.1→1.2→2.1…)

一方で、pdfやepubの場合は普通に階層構成の章表現をする必要があります (1→1.1→1.2→2→2.1…)

Zennと他形式で表現している章構成それぞれに対して、対応可能なファイル構成にしておく必要もありました。

メタデータ

また、Zennでは各チャプターのタイトルをメタデータで表現しています。

---
title: "2.1 技術士(情報工学部門)の山を登ろうとする人々へ"
---

# 技術士(情報工学部門)の山を登ろうとする人々へ

## 技術士とは

 技術士が何者かは、日本技術士会のページか、

このメタデータは他形式では使用しておらず、Zennだけで必要なものなので、Asciidoc上に含めず、Zennに変換する場合だけに反映しておきたいところです。

実装

という、諸々の問題をなんとかする形で、実装を整備していきました。

フォルダ構成

フォルダ構成は、例えば技術士(情報工学部門)攻略ガイドブックを例に取るとこんな感じになっています。

  • ProfessionalEngineerGuideBook
    • cover
    • images
    • include
      • epub
        • book_epub.adoc
        • epubl_metadata.xml
        • github_epub.css
        • my_template.epub3
      • pdf
        • fonts
        • book_pdf.adoc
        • github.css
        • pdf_style.yml
      • zenn
        • chapter_header
          • chapter1_0_header.adoc
          • chapter1_1_header.adoc
          • chapter2_0_header.adoc
          • chapter2_1_header.adoc
          • chapter2_2_header.adoc\
    • out
      • epub
      • pdf
      • zenn
    • chapter1_0.adoc
    • chapter1_1.adoc
    • chapter2_0.adoc
    • chapter2_1.adoc
    • chapter2_2.adoc
    • epub.sh
    • pdf.sh
    • zenn.sh

フォルダ構成のコンセプト

コンセプトとしてはこんな感じです。

  • コンテンツフォルダ直下にasciidocファイルと変換スクリプトを置きつつ、
  • includeフォルダに各形式で使用する共通ファイルを配置し、
  • coverにカバー画像、imageにその他の画像ファイルを置き、
  • 出力結果はoutフォルダに出力
    • 必要に応じてその下にtempフォルダを作成。

章構成の構築について

これに加えて、pdfおよびepub生成時には、以下の機構を採用しています。

  • includeフォルダ内のルートとなるファイル(book_pdf.adoc,book_epub.adoc)から、各チャプターのファイルをインクルードする。
    • asciidoctorでは、このルートとなるファイルを指定することで、文書全体を構築する。

こうすることで、Zennとそれ以外の形式で、章構成の構築方法を変えています。

  • Zennはコンテンツフォルダ直下のasciidocファイルから直接変換。
    • 文書単位、章単位ではなく、節単位で変換する。
  • pdf、epubはコンテンツフォルダ直下のasciidocファイルをインクルードするルートファイルから変換。
    • インクルードするファイルを追加削除することで章構成をカスタマイズする。

pdf(pdf.sh)

これが一番シンプルかも知れませんね。

pdfはasciidoctor-pdfから直接pdf化しています。

current_folder=`pwd`

file_type="pdf"
output_file_name="professional_engineer_guide_book"

out_folder="${current_folder}/out/${file_type}"
include_folder="${current_folder}/include/${file_type}"

temp_folder="${out_folder}/temp"
fonts_folder="${include_folder}/fonts"

target_file="${out_folder}/${output_file_name}.${file_type}"
pdf_theme_file="${include_folder}/pdf_style.yml"
pdf_adoc_file="${include_folder}/book_pdf.adoc "

mkdir -p $out_folder
mkdir -p $temp_folder
mkdir -p $temp_folder/images
mkdir -p $temp_folder/cover

cp images/* $temp_folder/images
cp cover/*  $temp_folder/cover

asciidoctor-pdf -r asciidoctor-mathematical -a imagesdir=$temp_folder -a pdf-fontsdir=$fonts_folder -a scripts=cjk -a mathematical-format=svg -a pdf-theme=$pdf_theme_file -o $target_file $pdf_adoc_file 

基本的にはカレントフォルダを絶対パスで確定した後で、ファイル名、フォルダ名を定義してasciidoctorに入れているだけです。

書式とフォントの指定は一般的なasciidoctor-pdfのそれですが、数式とimagesdirについては説明が必要かも知れません。

数式の処理とimagedirの指定

asciidoctorでTex数式を処理する方法はいくつかあるんですが、今回はasciidoctor-mathematicalを使っています。

https://docs.asciidoctor.org/asciidoctor/latest/stem/mathematical/

asciidoctor-mathematicalは、asciidocで書かれた数式を画像にレンダリングしてくれるんですが、どうも出力先が、コマンドで指定したimagesdirになるっぽかったんですよ。

一方で、静的な画像ファイルはimagesフォルダにおいてあり、Asciidocはimagedirを通してこれらのファイルを参照することになるので、

  • tenpファイルをimagesdirに指定してasciidoctor-mathematical出力を受けつつ、
  • 静的な画像ファイルもtempフォルダにまとめ、全ての画像ファイルをtempから参照させてしまえ

という意図で画像のコピーとimagesdirの指定を行っています。

formatはsvgにしていますが、pngでも生成自体は可能なはず(pdfサイズが大きくなるかも)。

epub(epub.sh)

epubはasciidocをdocbookに変換した後、pandocから生成してます。

#!/bin/bash

current_folder=`pwd`

file_type="epub"
output_file_name="professional_engineer_guide_book"

out_folder="${current_folder}/out/${file_type}"
include_folder="${current_folder}/include/${file_type}"

temp_folder="${out_folder}/temp"

cover_file="./cover/cover.png"
css_file="${include_folder}/github_epub.css"
template_file="${include_folder}/my_template.epub3"
epub_metadata_file="${include_folder}/epub_metadata.xml"
docbook_adoc_file="${include_folder}/book_epub.adoc"

target_file="${out_folder}/${output_file_name}.${file_type}"
docbook_file="${temp_folder}/${output_file_name}.xml"

mkdir -p $out_folder
mkdir -p $temp_folder
mkdir -p $temp_folder/images
mkdir -p $temp_folder/cover

cp images/* $temp_folder/images
cp cover/*  $temp_folder/cover

asciidoctor -b docbook5 -r asciidoctor-mathematical -a imagesdir=$temp_folder -a scripts=cjk -a mathematical-format=svg -o $docbook_file $docbook_adoc_file 

pandoc -f docbook -t epub $docbook_file --css=$css_file --toc --epub-chapter-level=2 --epub-cover-image=$cover_file --epub-metadata=$epub_metadata_file --toc-depth=2 -N --template=$template_file -o $target_file

数式の生成とimagesdirの指定は先ほどのpdfと同じです。

pandocでepubを作成する際はcssファイル、メタデータ指定、テンプレートをそれぞれ指定する必要があります。

CSS

cssはgithub.cssを使いつつ、書籍毎に必要な階層でpage-break-beforeを入れて改ページを制御しています。

.level2 {
    page-break-before: always;
  }

メタデータ

メタデータはxmlで記述しましたが、

<dc:language>ja</dc:language> 
<dc:publisher>Dimeiza</dc:publisher>
<dc:date opf:event="publication">2021-12-04</dc:date>
<dc:rights>Copyright 2021 Dimeiza</dc:rights>

一つ注意点があって。

https://a244.hateblo.jp/entry/2018/08/12/193838

title.txtと--epub-metadataで指定しているファイルの両方でtitleを指定しているとkindlegenコマンドでエラーとなる。

タイトルを2回指定してしまうと、KDPがepubを受け付けなくなってしまうので、とりあえず上記サイトに書いてあるように、メタデータではタイトルを指定せずに生成しました。

テンプレート

テンプレートはデフォルトテンプレートを出力させたものを使おうとしたんですが、

pandoc -D epub

タイトルページの出力(著者や出版社が出力される)が邪魔だったので、これを削除して別ファイル化して使ってます。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="jp">
<head></head>
<body$if(coverpage)$ id="cover"$endif$$if(body-type)$ epub:type="$body-type$"$endif$>
$if(titlepage)$
<section epub:type="titlepage">
<!--
$for(title)$
$if(title.type)$
  <h1 class="$title.type$">$title.text$</h1>
$else$
  <h1 class="title">$title$</h1>
$endif$
$endfor$
$if(subtitle)$
  <p class="subtitle">$subtitle$</p>
$endif$
$for(author)$
  <p class="author">$author$</p>
$endfor$
$for(creator)$
  <p class="$creator.role$">$creator.text$</p>
$endfor$
$if(publisher)$
  <p class="publisher">$publisher$</p>
$endif$
$if(date)$
  <p class="date">$date$</p>
$endif$
$if(rights)$
  <div class="rights">$rights$</div>
$endif$
-->
</section>

Zenn

さて、おまちかねのZennですが、これが一番めんどくさいです。

#!/bin/bash

current_folder=`pwd`

out_folder="${current_folder}/out/zenn"
include_folder="${current_folder}/include/zenn"
zenn_contents_folder="/home/dimeiza/Documents/ZennContents"

temp_folder="${out_folder}/temp"

header_folder="${include_folder}/chapter_header"
zenn_contents_name="professional_engineer_guide_book"

zenn_repository_book="${zenn_contents_folder}/books/${zenn_contents_name}"
zenn_repository_image="${zenn_contents_folder}/images/${zenn_contents_name}"
cover_png=cover/cover.png

. $include_folder/zenn_convert_function.sh

mkdir -p $out_folder
mkdir -p $temp_folder

awk 1 chapter1_1.adoc version.adoc  > $temp_folder/chapter1_1.adoc 

adoc_to_zenn_md $temp_folder/chapter1_1.adoc chapter1_1_header.md chapter1_1.md $zenn_contents_name

adoc_to_zenn_md chapter2_1.adoc chapter2_1_header.md chapter2_1.md $zenn_contents_name
adoc_to_zenn_md chapter2_2.adoc chapter2_2_header.md chapter2_2.md $zenn_contents_name

adoc_to_zenn_md chapter3_1.adoc chapter3_1_header.md chapter3_1.md $zenn_contents_name
adoc_to_zenn_md chapter3_2.adoc chapter3_2_header.md chapter3_2.md $zenn_contents_name
adoc_to_zenn_md chapter3_3.adoc chapter3_3_header.md chapter3_3.md $zenn_contents_name

adoc_to_zenn_md chapter4_1.adoc chapter4_1_header.md chapter4_1.md $zenn_contents_name
adoc_to_zenn_md chapter4_2.adoc chapter4_2_header.md chapter4_2.md $zenn_contents_name

adoc_to_zenn_md chapter5_1.adoc chapter5_1_header.md chapter5_1.md $zenn_contents_name
adoc_to_zenn_md chapter5_2.adoc chapter5_2_header.md chapter5_2.md $zenn_contents_name
adoc_to_zenn_md chapter5_3.adoc chapter5_3_header.md chapter5_3.md $zenn_contents_name

adoc_to_zenn_md chapter6_1.adoc chapter6_1_header.md chapter6_1.md $zenn_contents_name
adoc_to_zenn_md chapter6_2.adoc chapter6_2_header.md chapter6_2.md $zenn_contents_name
adoc_to_zenn_md chapter6_3.adoc chapter6_3_header.md chapter6_3.md $zenn_contents_name

awk 1 chapter7_1_1.adoc ./include/zenn/chapter7_1_zenn.adoc chapter7_1_2.adoc > $temp_folder/chapter7_1.adoc  

adoc_to_zenn_md $temp_folder/chapter7_1.adoc chapter7_1_header.md chapter7_1.md $zenn_contents_name
adoc_to_zenn_md chapter8_1.adoc chapter8_1_header.md chapter8_1.md $zenn_contents_name

cp $out_folder/*.md $zenn_repository_book
cp ./images/* $zenn_repository_image

cp $cover_png $zenn_repository_book

Zennの場合も基本的にはasciidoc->docbook->GFMと変換していますが、具体的な変換工程はこのファイルからはまだ見えません。

それ以外の要素を先に説明してしまいます。

チャプター毎の変換

前述したように、ZennはMarkdown1つごとにチャプターファイル一つを対応させる、という構成になっているので、asciidocのincludeで文書全体をひと塊のファイルにすることはできません。

そこで、変換対象のasciidocファイルを個別にmarkdown化する、ということをしていて、その際に細かい処理をしています。

例えばバージョン番号の埋め込みや、Zenn固有の内容追記などを、awkを使ってasciidocの段階で実施しています。

awk 1 chapter1_1.adoc version.adoc  > $temp_folder/chapter1_1.adoc 
…
awk 1 chapter7_1_1.adoc ./include/zenn/chapter7_1_zenn.adoc chapter7_1_2.adoc > $temp_folder/chapter7_1.adoc  

Zennリポジトリフォルダへのコピー

markdownへの変換が完了したら、変換したmarkdownと画像ファイルをZennのリポジトリが格納されたフォルダにコピーするようにしています。

cp $out_folder/*.md $zenn_repository_book
cp ./images/* $zenn_repository_image

cp $cover_png $zenn_repository_book

Zennのリポジトリ側には書籍の管理情報など、書籍コンテンツそのものには無関係の、Zenn固有の設定情報が含まれているので、あくまでも両リポジトリは別にした上で、コンテンツ変換結果だけを上書きするようにしています。

asciidoc->markdown変換ロジック

ではそろそろ面倒な場所を。

asciidoc->markdown変換ロジック(adoc_to_zenn_md)は別シェルスクリプトにまとめていて、

. $include_folder/zenn_convert_function.sh

中はこうなっています。

#!/bin/bash

adoc_to_zenn_md () {
    asciidoctor -b docbook $1 -o - | pandoc -f docbook -t gfm --wrap=none -o $temp_folder/$3
    awk 1 $header_folder/$2 $temp_folder/$3 > $out_folder/$3

    fix_tex_expression $out_folder/$3
    fix_markdown_expression_for_zenn $out_folder/$3 $4
}

fix_tex_expression(){

    grep -l '\\\\[A-Za-z]*[ ]*{' $1 | xargs sed -i -e 's/\\\\\([A-Za-z]*\)[ ]*{\([^}]*\)}/\\\1{\2}/g'

    # for nesting
    grep -l '\\\\[A-Za-z]*[ ]*{' $1 | xargs sed -i -e 's/\\\\\([A-Za-z]*\)[ ]*{\([^}]*\)}/\\\1{\2}/g'

    grep -l '\\\\[A-Za-z]*' $1 | xargs sed -i -e 's/\\\\\([A-Za-z]*\)/\\\1/g'

    grep -l '\*' $1 | xargs sed -i -e 's/\\\*/*/g'
    grep -l '\[' $1 | xargs sed -i -e 's/\\\[/[/g'
    grep -l '\]' $1 | xargs sed -i -e 's/\\\]/]/g'
    grep -l '<'  $1 | xargs sed -i -e 's/\\</</g'
}

fix_markdown_expression_for_zenn(){

    # replace images folder for zenn
    grep -l '\](images/' $1 | xargs sed -i -e 's/\](images/\](\/images\/'$2'/g'

    # remove '<' '>' (inserted pandoc) from URL
    grep -l '^<http.*>' $1 | xargs sed -i -e 's/<\(http.*\)>/\1/g'

    # support line-through
    grep -l '<span class="line-through">.*<\/span>' $1 | xargs sed -i -e 's/<span class="line-through">\(.*\)<\/span>/~~\1~~/g'
}

adoc_to_zenn_mdでやっていることは大別して4つです。

asciidoc->markdownへの変換

これはそう難しくないですね。asciidoctorとpandocを順に読んでいるだけです。

asciidoctor -b docbook $1 -o - | pandoc -f docbook -t gfm --wrap=none -o $temp_folder/$3
メタデータの連結

Zennでしか使用しないメタデータは、includeフォルダ内にヘッダとしてまとめていて、これをawkで連結しています。

awk 1 $header_folder/$2 $temp_folder/$3 > $out_folder/$3
tex変換時のエスケープ修正

これはだいぶ力技なんですが、増えてしまったエスケープ用のバックスラッシュをsedで排除しています。

同じ表現を2回回してるんですが、

    grep -l '\\\\[A-Za-z]*[ ]*{' $1 | xargs sed -i -e 's/\\\\\([A-Za-z]*\)[ ]*{\([^}]*\)}/\\\1{\2}/g'

    # for nesting
    grep -l '\\\\[A-Za-z]*[ ]*{' $1 | xargs sed -i -e 's/\\\\\([A-Za-z]*\)[ ]*{\([^}]*\)}/\\\1{\2}/g'

1回だと、こんな感じで入れ子になっている場合に変換に失敗するので2回やっているという、極めて場当たり的対処です。

frac{19958400\text{[秒]}} {3600\text{[秒/時間]}} &= 5544\text{[時間]} 

Zenn向けの表現修正

最後に、Zenn向けに、

  • 画像ファイルの格納場所をZennルールに準拠するよう、markdown内のパスを修正
  • リンクの前後に挿入されてしまっている'<'、'>'を削除
  • 取り消し線表現をhtmlからmarkdownに修正

という修正をsedで行っています。

変換結果

いや…長かったですね…。

それでは、変換結果をとくとご覧あれ。

普通のページ

このページにしましょうか。

https://zenn.dev/dimeiza/books/professional_engineer_guide_book/viewer/chapter2_1

Zenn

image

epub

image

pdf

image

数式とか

これもとりあえず問題なく変換されてます。

Zenn

image

epub

image

pdf

image

完成

というわけで、これにて、Asciidoc centricな複数メディア向け電子書籍展開環境が私の前に爆誕いたしました。

変換してみると、まだいくつか変換がうまくいっていなかったりと粗があったりもするんですが、これでようやくメディア毎のソースファイル多重管理から脱し、一つのコンテンツを複数メディアに展開させることができるようになりました。

仕事で使っている知識と環境をそのまま使って、コマンド一発で電子書籍の各形式に展開できるので何かと楽です。

これから執筆したい書籍もありますし、既存書籍も改訂、修正していく予定なので、この環境が私自身の作業に大いに役立ってくれることを祈っております。

と同時に

実はこの記事を書こうと思った理由の一つは、以下の傾向性を業界に強く感じたからでもあります。

  • 組版作業というコンテンツの価値に直結しない作業の抽象化が不十分。
    • 書籍を書く際に、(エンジニアも含め)コンテンツ以上に組版の仕組みに注力、拘泥しすぎ。

というのも、KDPで電子書籍を出版した数カ月後、とうとう日本のAmazon(KDP)もこんな事を始めたんですよ。

https://www.kdpcommunity.com/s/article/Japanese-is-now-available-as-a-language-option-for-print-books?language=ja

カバーとか原稿のサイズ調整とかはまぁあるんですけど、このサービス、基本的にpdfファイルさえ作っちゃえば、誰でも紙の本が上梓できてしまう[3]んですよ。

私自身がTexとか組版とかを好きじゃない[4]、ということもあるんですが、Amazonのこの動きを見ていて、

  • 『組版を意識させないこと』が"出版の民主化"のために最も必要なことなのでは?

と強く感じたのが、この環境を共有しようと思った大きな動機です。

テキストであることの力

あとは、

  • テキストファイルで構造化コンテンツを作成している限り、大抵の書式変更はテキスト処理で何とかなる。

という気付きがあったのも面白かったですね。

今回はZennに合わせるという消極的な処理でしたが、可読性高めの構造化テキストしか勝たん! という思いを強くしました。

さいごに

というわけで、だいぶ長くなりましたが、お読み頂きありがとうございました。

もし各位になにかの縁があって、複数メディアに対して書籍を書く機会があるようでしたら、良ければ参考になさってください。

追伸

直近(2022/4)、Ubuntu22.04で環境を構築し直したんですが、ツール類のバージョンが古いと上記の連携がうまく行かなかったので、バージョン情報を残しておきます。

$ asciidoctor --version
Asciidoctor 2.0.16 [https://asciidoctor.org]
Runtime Environment (ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux-gnu]) (lc:UTF-8 fs:UTF-8 in:UTF-8 ex:UTF-8)
$ pandoc --version
pandoc 2.18
Compiled with pandoc-types 1.22.2, texmath 0.12.5, skylighting 0.12.3,
citeproc 0.7, ipynb 0.2, hslua 2.2.0
Scripting engine: Lua 5.4

という状況で動かし、直近書籍の更新をしたりしています。

https://zenn.dev/dimeiza/books/professional_engineer_1st_exam_r1_r2

OSデフォルトだとバージョンが古い可能性もあるので、必要に応じて公式から最新版を取ってきたほうが良いかもしれません。

脚注
  1. KDPでペーパーバックを出版するときにもpdfを要求されます。 ↩︎

  2. 自然言語の表記方法なのに『可読性』という言葉を意識する時点で、文書表現方法として問題があると私は考えています。 ↩︎

  3. 大げさかも知れませんが、このサービスが一般消費者に開放されている辺り、出版のDXと言えるのでは、と思いました。 ↩︎

  4. これは大学時代の恩師の影響でもあります。多くの研究室でTexしか文書作成環境がない中、いち早くWordを研究室に展開して論文作成速度を他研究室と差別化していました。 ↩︎

Discussion