Asciidocで仕様書の「開発環境」を整える (1)

## 概要

モダンな仕様書の「開発環境」を構築した。
結果として、以下のような雛形の環境が出来上がった。

https://github.com/ynitto/asciidoc-template

## モチベーション

システム系のお仕事では、多かれ少なかれ仕様書を書かなければいけない。また、この仕様書を共有したり、管理したりしなくてはいけない。長年、WordやExcelで書いて、ネットワークフォルダで共有・管理するとしていたが、次のようなツラさを感じたことはないだろうか。

- 複数人で分担して書いた後、マージする作業がつらい。マージをミスって肝心な部分が入っていない。
- 記載内容のレビューを任されたが、どこが変更点かわからない。しょうがないから、適当にスルーする。
- 後から見返すと日本語としてめちゃくちゃ。これが原因で誤解を生み、後々問題になる。
- フォルダやファイル名に頼ったバージョン管理をする。が、徐々に形骸化して必要な時に意味を成していない。

ソフトウェア開発、特にコーディングの世界では、昔から同じような問題に直面し、構成管理、自動テスト、CI/CDなど様々な仕組みの導入と改良がなされてきている。なのに、ドキュメントの世界では、なぜ進まないのか。なぜ未だに Word/Excel をネットワークフォルダで管理するのか。

この原因の一つは、慣習を捨てられないことだと思う。今まで Word/Excel で出来ていたことが出来ないとなると途端に「あれができない」「これができない」となり「やっぱり前のやり方に戻そう」となる。

なので、極力「Word/Excel+ネットワークフォルダで出来ていたこと」に応えられるモダンな仕様書開発環境を構築しようと思った。

実際にやってみると、先人の知恵はあるものの、情報が古かったり、まとまっていなかったりと、調査と検証に結構な時間がかかった。2022年8月現在の情報ではあるが、この記事によって、興味を持ち、同じような環境を構築してくれる人が増えたら嬉しい。

## 前提

仕様書の開発環境を整える上で事前に用意しておくものは以下の通り。

- Git
- VSCode
- Node.js (+npm等)

当たり前すぎるので、解説は省略。インストールもさほどはまらない。

## Asciidoc + draw.io で仕様書を書く

エンジニアなら簡単なドキュメントを作成する際に markdown を使うだろう。しかし、仕様書となると、一つの本としての体裁を整えたくなる。とくに表やリスト、文字レイアウトなどは、markdown では表現力として物足りなさを感じる。そこで導入したいのが Asciidoc だ。

https://asciidoc.org/

Asciidoc は markdown に似たマークアップ言語であり、markdown よりも様々な表現が可能である。詳細な文法は以下を参考にするとわかりやすい。

https://takumon.github.io/asciidoc-syntax-quick-reference-japanese-translation/

日頃 markdown を書いていれば、特に違和感なく使えると思う。markdown に不慣れな非エンジニアであれば、少し学習が必要であるかもしれない。だが、思った通りにレイアウトできず Word や Excel と格闘することを考えれば、そこまで大した学習コストではないだろう。

### Asciidoc を書く環境を整える

Asciidoc で仕様書を書くには、最終的にどのようなレイアウトになるのか確認しながら執筆できる環境が必要になる。そこで、VSCode の拡張機能で Asciidoc のライブプレビューやシンタックスハイライトが可能になる`asciidoctor-vscode`をインストールする。

https://marketplace.visualstudio.com/items?itemName=asciidoctor.asciidoctor-vscode

### フォルダ構成を整える

仕様書ファイルは以下のようなフォルダ構成で配置する。

```
.
　├ src/       ・・・ 仕様書ファイルを置くフォルダ
　│　　├ docs/    ・・・ Asciidoc　フォルダ
　│　　│ 　├ 100_backend.adoc
　│　　│　　　└ 200_architecture.adoc
　│　　│
　│　　├ images/   ・・・ 図フォルダ
　│　　│ 　├ 210_hardware.drawio.svg
　│　　│　　　└ 210_software.drawio.svg
　│　　│　　
　│　　└ index.adoc ・・・ Asciidoc　トップページ
　│
　├ ...
```

- `src`フォルダに仕様書を構成するファイルを置く
    - `src/docs`フォルダに文章ファイル (Asciidoc) を置く。ファイル名には通し番号をプレフィックスとしてつける。 (例えば`100_`)
    - `src/images`フォルダに図ファイル (Draw.io) を置く。ファイル名には使っている adoc ファイルの通し番号をプレフィックスとしてつける。
    - 最上位ファイルとして`index.adoc`ファイルを作成する
- `resource`フォルダに仕様書をコンパイルするためのファイルを置く

### Asciidoc を分割して書く

せっかく仕様書を構成管理し、ソースコードと同じように PR ベースの開発ができるのだから、チームで分担することを念頭に入れた書き方を意識したい。

おすすめは、Asciidoc ファイルを章や節で分割することである。分担しやすいだけでなく、なにより Asciidoc ファイルの見通しが良くなる。Asciidoc では`include`構文を用いて別ファイルの内容を取り込むことができるので、結合も簡単だ。

ただし、セクションのレベルをすべてのファイルで意識する必要があって、仕様書の章・節の構成を変えるたびにメンテする手間がでてくる。そこで、以下のように、```include 時にレベルのインクリメントをつける (leveloffset=+1)```、```adoc ファイル内はトップレベルのセクションから始める```ようにして、セクションのレベル調整を自動で行わせるようにする。

```index.adoc
= サンプルシステム外部仕様書

== 背景と目的
include::docs/100_background.adoc[leveloffset=+1]

...
```

```docs/100_background.adoc
== 背景
本システムの背景を以下に述べる。
...

== 目的
本システムの目的を以下に述べる。
...
```

### draw.io で図を描く

仕様書では当然、図や画像も埋め込む必要が出てくる。これらのファイルも構成管理やシームレスな編集をできるようにするため draw.io を導入する。

https://drawio-app.com/

draw.io は、ざっくり言うと、ドローイングツールで、作成した成果物に編集情報を埋め込むことができる。つまり、文章中に draw.io ファイルのリンクを張れば、エクスポート操作などなしに文章へ埋め込んだ形で確認できる。
VSCode上で編集できるプラグインもあるので、文章と図のシームレスな編集作業も可能だ。

https://qiita.com/riku-shiru/items/5ab7c5aecdfea323ec4e

今回 Git で管理するため、svg 形式の `.drawio.svg` 拡張子で作成する。フォーマットが svg なので差分は xml の記述として表示されると共に、GitHub 上ではプレビュー表示されるので確認できるのも魅力だ。
ただし、draw.io の svg に画像を埋め込む場合、サイズ制限があるため、事前にリサイズ・クロップする、JPEG 化するなどしてサイズを落とす必要がある。

また、asciidoc で draw.io の図を参照する場合には、

- 図フォルダへのパス``imagesdir``属性を設定しておく。``ifndef::imagesdir``で全ての adoc ファイルで図フォルダへの相対パスを設定しておくことで、VSCode 上でどのファイルをプレビューしても表示することができる。
- テキストを挿入した図は``image``記述には``options=inline``をつける。これは後述の PDF 生成時にフォントを指定するため。
- 文章上での図のサイズは``width=``で設定する

```docs/200_architecture.adoc
// VSCode プレビューのため図フォルダへのパスを設定する
ifndef::imagesdir[:imagesdir: ../images]

== ハードウェア構成
本システムにおけるハードウェア構成を説明する。

// テキストが入る図は options=inline をつける
// 図のサイズは width= で調整する
image::210_hardware.drawio.svg[options=inline,width=100%]

...
```

## textlint で仕様書をチェックする

Typo や文法上のミスだけでなく、読みにくい表現・誤解を生む表現は、仕様書としてできる限りなくしたい。そこで、ソースコードの静的解析のように自動でチェックする仕組みとして textlint を導入する。

https://textlint.github.io/

### textlint を準備する

まず textlint をインストールする。

```sh
$ npm install textlint --save-dev
```

textlint はデフォルトでテキストと markdown に対応しているが、 asciidoc を扱うには別途プラグインを導入する必要がある。

https://memo.laughk.org/articles/2021-01-18-textlint-for-asciidoc/

しかし、既存のプラグインは、対応できる Asciidoc の表現が限定的あり、表などを使う仕様書のチェックには都合が悪い。そこで、[seikichi/textlint-plugin-asciidoctor](https://github.com/seikichi/textlint-plugin-asciidoctor)を参考にプラグインを作成した。これをインストールして、textlint へ設定する。

https://github.com/ynitto/textlint-plugin-asciidoc

```sh
$ npm install @ynitto/textlint-plugin-asciidoc --save-dev
```

```json:.textlintrc
{
  "plugins": [
    "@ynitto/asciidoc"
  ]
}
```

### 文章をライブチェックする

textlint をインストールしただけでは、毎回コマンドを打たなければチェックされない。文章を書いている最中にライブでチェックをかけて、早くミスに気づいて直せるようにしおたい。そのために VSCode の拡張機能`vscode-textlint`をインストールする。

https://marketplace.visualstudio.com/items?itemName=taichi.vscode-textlint

この拡張機能を Asciidoc のライブチェックとして動かすために以下の設定をする。

```json:.vscode/settings.json
{
    "textlint.run": "onType",
    "textlint.languages": [
        "asciidoc"
    ]
}
```

### ルールセットを準備する

textlint は、適用したいルールセットをインストールして設定することでチェックを行う。
ルールセットは以下を参考にインストールした。

https://qiita.com/k8uwall/items/3c29ef2dce153e13bd1d

```bash
$ npm install @textlint-ja/textlint-rule-no-insert-dropping-sa \
              @textlint-ja/textlint-rule-no-synonyms sudachi-synonyms-dictionary \
              textlint-filter-rule-allowlist \
              textlint-rule-ja-hiragana-fukushi \
              textlint-rule-ja-hiragana-hojodoushi \
              textlint-rule-ja-hiragana-keishikimeishi \
              textlint-rule-ja-no-inappropriate-words \
              textlint-rule-ja-no-orthographic-variants \
              textlint-rule-jis-charset \
              textlint-rule-no-hoso-kinshi-yogo \
              textlint-rule-no-mixed-zenkaku-and-hankaku-alphabet \
              textlint-rule-no-start-duplicated-conjunction \
              textlint-rule-no-surrogate-pair \
              textlint-rule-prefer-tari-tari \
              textlint-rule-preset-ja-spacing \
              textlint-rule-preset-ja-technical-writing \
              textlint-rule-preset-japanese \
              textlint-rule-prh \
              textlint-rule-use-si-units --save-dev
```

`textlint-rule-prh`は、用語の表記揺れなどをチェックするルールであり、別途辞書ファイルが必要になる。今回は [WEB+DB PRESS のルール](https://github.com/prh/rules/blob/master/media/WEB%2BDB_PRESS.yml)をベースにMicrosoftも採用している[内閣告示の外来語長音表記](https://www.bunka.go.jp/kokugo_nihongo/sisaku/joho/joho/kijun/naikaku/gairai/honbun06.html)に対応したものを作成し、設定する。

```yaml:WEB+DB_PRESS+JCA.yml
...
- expected: エディター
  pattern:  /エディタ(?!ー)/
- expected: エミッター
  pattern:  /エミッタ(?!ー)/
- expected: エンコーダー
  pattern:  /エンコーダ(?!ー)/
- expected: デコーダー
  pattern:  /デコーダ(?!ー)/
- expected: エミュレーター
  pattern:  /エミュレータ(?!ー)/
...
```

```json:.textlintrc
{
  "rules": {
    "preset-ja-technical-writing": true,
    "preset-ja-spacing": true,
    "no-start-duplicated-conjunction": true,
    "no-surrogate-pair": true,
    "no-mixed-zenkaku-and-hankaku-alphabet": true,
    "ja-hiragana-fukushi": true,
    "ja-hiragana-hojodoushi": true,
    "@textlint-ja/textlint-rule-no-insert-dropping-sa": true,
    "prefer-tari-tari": true,
    "@textlint-ja/no-synonyms": true,
    "ja-no-orthographic-variants": true,
    "use-si-units": true,
    "no-hoso-kinshi-yogo": true,
    "ja-no-inappropriate-words": true,
    "prh": {
      "rulePaths": [
        "WEB+DB_PRESS+JCA.yml"
      ]
    }
  },
  "plugins": [
    "@ynitto/asciidoc"
  ]
}
```

適用するルールは結構多いと思う。だが、実際に運用した感覚からすると、十分守れるレベルである。
まずは指摘された通りに修正をしてみて、どうにも直せそうにない場合は、文章表現自体を改めてみる。こうして大概の場合は指摘に対応することができる。ただし、例外もあって、誤検知や専門用語・定義した用語など明らかに修正すべきでないものは、都度を`allowlist`に加えて、抑制していく。

以下が最終的な textlint の設定ファイル例。

```json:.textlintrc
{
  "filters": {
    "allowlist": {
      "allow": [
        "いいえ",
        "フラッシュ"
      ]
    }
  },
  "rules": {
    "preset-ja-technical-writing": true,
    "preset-ja-spacing": true,
    "no-start-duplicated-conjunction": true,
    "no-surrogate-pair": true,
    "no-mixed-zenkaku-and-hankaku-alphabet": true,
    "ja-hiragana-fukushi": true,
    "ja-hiragana-hojodoushi": true,
    "@textlint-ja/textlint-rule-no-insert-dropping-sa": true,
    "prefer-tari-tari": true,
    "@textlint-ja/no-synonyms": true,
    "ja-no-orthographic-variants": true,
    "use-si-units": true,
    "no-hoso-kinshi-yogo": true,
    "ja-no-inappropriate-words": true,
    "prh": {
      "rulePaths": [
        "WEB+DB_PRESS+JCA.yml"
      ]
    }
  },
  "plugins": [
    "@ynitto/asciidoc"
  ]
}
```

## Asciidoctor で仕様書を出力する

Asciidoc で記述した文章を配布するには、いわゆる、ビルド作業を行わなくてはいけない。

### HTML で出力する

Asciidoctor を用いると簡単に HTML に出力できる。

https://asciidoctor.org/

今回他のツール類を Node で揃えているため、Asciidoctor も Node 版を用いる。以下のコマンドでインストールする。

```bash
$ npm install asciidoctor --save-dev
```

これで、以下のコマンドを実行することで Asciidoc を一つの `index.html` として出力することができる。

```bash
$ asciidoctor ./src/index.adoc
```

しかし、このままでは、adoc ファイルを作成した時と同じ相対パスに svg ファイルが存在しないため表示されない。そこで、出力フォルダ`dist`へ HTML を出力すると共に、同フォルダへ svg　ファイルをコピーするコマンドを npm script へ登録する。

```json:package.json
{
...
  "scripts": {
    "build": "asciidoctor ./src/index.adoc --destination-dir ./dist/ && cp -rf ./src/images/ ./dist/images/"
  },
...
}
```

:::note info
2022/8/24 追記
``image``記述に``options=inline``をつけた場合、SVG が HTML に埋め込まれるため`images/`フォルダのコピーは必要ない。しかし、HTML ファイルが肥大化するのも避けたいので、以下のように「テキストを挿入した図のみ」``options=inline``を付けるとして、SVG のコピーは全て行うことにした。

> - テキストを挿入した図は``image``記述には``options=inline``をつける。これは後述の PDF 生成時にフォントを指定するため。
:::

これで`npm run build`コマンドを実行するのみで HTML を出力できる。

### HTML 出力の体裁を整える

出力された HTML は Asciidoctor デフォルトのスタイルが適用されるが、これをカスタマイズすることもできる。
docinfo ファイル (実態は断片的な HTML) を用意し、ビルドコマンドで引き渡せば、スタイルを上書きすることができる。ただし、docinfo ファイルにはパスの命名規則があるため、resources フォルダに用意しておき、ビルド時にコピー、ビルド後に削除するようにする。

```html:resources/index-docinfo.html
<style>
...
// ページマージンを設定
@page {
  margin: 20mm 20mm 20mm 20mm;
}

// フォントを設定
html {
  font-family: 'Noto Serif JP', serif !important;
  font-size: 12pt;
}
...
</style>
```

```json:package.json
{
...
  "scripts": {
    "prebuild": "cp ./resources/index-docinfo.html ./src/index-docinfo.html",
    "build": "npm run build:html",
    "build:html": "asciidoctor ./src/index.adoc -a docinfo=private-head --destination-dir ./dist/ && cp -rf ./src/images/ ./dist/images/",
    "postbuild": "rimraf ./src/index-docinfo.html"
  },
...
}
```

なお、上のコマンドを実行するには以下のパッケージのインストールが必要。

```bash
$ npm install rimraf --save-dev
```

ここまでで出力された HTML は[こちら](https://ynitto.github.io/asciidoc-template/index.html)で確認できる。

### PDF で出力する

情報共有のためだけであれば、HTML 出力だけで十分である。むしろリンクや検索性を考えると、HTML ファイルを社内サーバーや IP 制限をかけてクラウド配信すれば良い。しかし、Word/Excel の代替として考えると、ファイルとしてのポータビリティーを考慮しないといけない。そこで、今回は PDF で出力する方法を検討した。

Asciidoc を PDF にするには、Asciidoctor-pdf を使う方法と Asciidoctor-web-pdf を使う方法がある。それぞれの特徴は以下の通り。

- Asciidoctor-pdf
    - Ruby ネイティブで Asciidoc を PDF に変換するコンバーター
    - yaml で theme を設定して、フォントが細かく制御できる
    - 表紙ページなどレイアウトをカスタマイズするのは難しい
- Asciidoctor-web-pdf
    - Node 製。Asciidoctor でHTML出力し、ヘッドレスブラウザの PDF 印刷機能で出力する。
    - Asciidoctor の機能が使えるため、レイアウトのカスタマイズが柔軟
    - PDF　がブラウザ出力のため、特にフォントなどの細かな制御が難しい

どちらも一長一短であるが、今回はカスタマイズ性を重視して Asciidoctor-web-pdf を使う。

#### レイアウトをカスタマイズする

1ファイルとして出力できるようになったとはいえ、「Word 仕様書で出来ること」に対して満たせていない。その一つが、表紙・ヘッダー・フッターなどのページレイアウトだ。おそらく、Word では、ページレイアウトを施した、その組織固有のテンプレートがあるだろう。これを Asciidoctor-web-pdf でも再現しなければならない。

まず表紙ページを作成する。表紙ページは HTML 出力時に用いた docinfo ファイルを用いて設定する。表紙ページの docinfo ファイルでは、出力したい項目のみ列挙して、記述内容は attribute で置換するようにする。 なお、今回 attribute は`src/inex.adoc`で設定することにした。

```html:resources/index-docinfo-header-pdf.html
<div>
  <p class="cover-title">{docmaintitle}</p>
  <p class="cover-subtitle">{docsubtitle}</p>
  <p class="cover-version">Ver. {revnumber}</p>
  <p class="cover-date">{revdate}</p>
  <p class="cover-org">{orgname}</p>
  <p class="cover-author">{author}</p>
</div>
```

スタイルは CSS で設定する。Asciidoctor-web-pdf ではデフォルトのスタイルを残しつつ、CSS を追加することがコマンドラインからできる。今回は、各ページのヘッダーに Confidential マーク、フッター右にページ番号を出すようにレイアウトした。デフォルトのスタイルを残しているため、cssとして `!important` で優先度を上げる表記が増えることに注意。

```css:resources/style-pdf.css
...
@page {
  margin: 20mm 20mm 20mm 20mm;
}

@page :left {
  @top-right {
    content: "Confidential";
    font-size: 12pt;
    color: #ff4b00;
    margin: 10pt 0pt 0pt 0pt;
  }
  @bottom-left {
    width: 0 !important;
    content: "" !important;
    padding: 0 !important;
    margin: 0 !important;
    border-top: 0 !important;
  }
  @bottom-right {
    content: counter(page);
    font-size: 0.8rem;
    padding: 0 !important;
    margin: 0 !important;
    border-top: .1pt solid #d2d2d2;
  }
}

@page :right {
  @top-right {
    content: "Confidential";
    font-size: 12pt;
    color: #ff4b00;
    margin: 10pt 0pt 0pt 0pt;
  }
  @bottom-left {
    width: 0 !important;
    content: "" !important;
    padding: unset !important;
    margin: unset !important;
    border-top: unset !important;
  }
  @bottom-right {
    content: counter(page);
    font-size: 0.8rem;
    padding: 0 !important;
    margin: 0 !important;
    border-top: .1pt solid #d2d2d2;
  }
}
...
```

なお、表紙ページの docinfo とスタイル css は、HTML のときと同様、resources フォルダに用意しておき、ビルド時にコピー、ビルド後に削除するようにする。

```json:package.json
{
...
  "scripts": {
    "prebuild": "cp ./resources/index-docinfo.html ./src/index-docinfo.html && cp ./resources/index-docinfo-header-pdf.html ./src/index-docinfo-header-pdf.html && cp ./resources/style-pdf.css ./src/style-pdf.css",
    "build": "npm run build:html && npm run build:pdf",
    "build:html": "asciidoctor ./src/index.adoc -a docinfo=private-head --destination-dir ./dist/ && cp -rf ./src/images/ ./dist/images/",
    "build:pdf": "asciidoctor-web-pdf ./src/index.adoc -a stylesheet=\"+style-pdf.css\" --destination-dir ./dist/",
    "postbuild": "rimraf ./src/index-docinfo.html ./src/index-docinfo-header-pdf.html ./src/style-pdf.css ./src/index.html"
  },
...
}
```

今回の表紙ページやレイアウトは、以下のサイトを参考にした。

https://zenn.dev/tatsuyasusukida/articles/asciidoctor-requirement

https://devblog.lac.co.jp/entry/20220627

#### フォントを埋め込む

PDF で一番やっかいなのは、フォントの問題である。何も意識せずに Asciidoctor-web-pdf で PDF を作成すると、`Type3 フォント`になってしまうことがあり、環境によっては表示が崩れてしまう。また、意図せぬフォントが埋め込まれることもあり、ライセンスが心配だ。

> 参考: https://blog.shikoan.com/pdf-font-embed/

これを防ぐためには、指定したフォントをPDFに埋め込む必要がある。Asciidoctor-web-pdf　の[issue#293](https://github.com/Mogztter/asciidoctor-web-pdf/issues/293)によると「リポジトリー内のツールを参考にフォント埋め込み css を自作せよ」とのことなので、resourcesフォルダ内に作成する。

まず、[本家のコード](https://github.com/Mogztter/asciidoctor-web-pdf/blob/main/tasks/update-font-face.js)を参考に「フォント埋め込み css 作成コード」を実装する。

```javascript:resources/gen_fonts_css.js
const fs = require('fs');
const ospath = require('path');

let fontStylePath = 'fonts.css';
let fontsDirectoryPath = 'fonts';
switch (process.argv.length) {
case 0:
case 1:
case 2:
  break;
case 3:
  fontsDirectoryPath = process.argv[2];
  break;
default:
  fontStylePath = process.argv[2];
  fontsDirectoryPath = process.argv[3];
  break;
}

const fonts = fs.readdirSync(fontsDirectoryPath);

if (fonts.length == 0) {
  throw new Error('Not found fonts. Aborting...');
}

// generate the @font-face definitions from the fonts directory
const startTag = '/* start:font-face */';
const endTag = '/* end:font-face */';

const data = [];
data.push(startTag);

for (const font of fonts) {
  const fontPath = ospath.join(fontsDirectoryPath, font);
  const buff = fs.readFileSync(fontPath);

  let dataUriPrefix;
  let fontFormat;
  if (font.endsWith('.ttf')) {
    dataUriPrefix = 'data:font/truetype;charset=utf-8;base64,';
    fontFormat = 'truetype';
  } else if (font.endsWith('.otf')) {
    dataUriPrefix = 'data:font/opentype;charset=utf-8;base64,';
    fontFormat = 'opentype';
  } else if (font.endsWith('.woff2')) {
    dataUriPrefix = 'data:application/font-woff2;charset=utf-8;base64,';
    fontFormat = 'woff2';
  } else {
    continue;
  }

  const basename = ospath.basename(font, ospath.extname(font));
  const parts = basename.split('-');
  const fontType = parts[1];

  let fontWeight;
  if (fontType.startsWith('Thin')) {
    fontWeight = 100;
  } else if (fontType.startsWith('ExtraLight')) {
    fontWeight = 200;
  } else if (fontType.startsWith('Light')) {
    fontWeight = 300;
  } else if (fontType === 'Regular' || fontType === 'Italic') {
    fontWeight = 400;
  } else if (fontType.startsWith('Medium')) {
    fontWeight = 500;
  } else if (fontType.startsWith('SemiBold')) {
    fontWeight = 600;
  } else if (fontType.startsWith('Bold')) {
    fontWeight = 700;
  } else if (fontType.startsWith('ExtraBold')) {
    fontWeight = 800;
  } else if (fontType.startsWith('Black') || fontType.startsWith('Heavy')) {
    fontWeight = 900;
  } else {
    throw new Error('Unable to determine the font weight from the name. Aborting...');
  }

  let fontStyle = 'normal';
  if (fontType.includes('Italic')) {
    fontStyle = 'italic';
  }

  let unicodeRange = '';
// 文字コード範囲の特定は非対応
//   if (font === 'DroidSansMono-Regular.woff2') {
//     unicodeRange = `  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
// `;
//   }

  const fontFamily = parts[0].split("_").join(' ');
  const fontBase64 = buff.toString('base64');
  const template = `@font-face {
  font-family: '${fontFamily}';
  font-style: ${fontStyle};
  font-weight: ${fontWeight};
  font-display: block;
  src: url(${dataUriPrefix}${fontBase64}) format('${fontFormat}');
${unicodeRange}}`;

  data.push(template);
}

data.push(endTag)

// Write fonts css file
fs.writeFileSync(fontStylePath, data.join('\n'), 'utf8');
```

resoures フォルダに fonts フォルダを作成し、フォントファイルを配置する。フォントファイルは「(フォントファミリー名・スペースは"_"に置換)-(ウエイト)」というファイル名にする。

ただし、フォントファイルには、注意が必要である。埋め込むフォントファイル(正確には使用するフォントファイル)のサイズが大きいと、PDF 変換時にタイムアウトが発生する。このタイムアウトは設定で伸ばすこと可能であるが、ファイルのポータビリティを考える上で、PDF　のサイズはできるだけ小さくしたいため、ファイルサイズの小さいフォントを選択することが重要である。

:::note warn
検証が不十分ではあるが、まだ後述の `M PLUS 1` しか埋め込みが成功していない。`M PLUS 2`や`Noto Serif JP`と何が違うのか、わかる方がいれば、コメントいただきたい。
:::

今回は PDF 埋め込み配布可能なライセンスである [M PLUS 1](https://fonts.google.com/specimen/M+PLUS+1)と[M PLUS 1 Code](https://fonts.google.com/specimen/M+PLUS+1+Code) を使わせていただいた。公式ページは以下。

https://mplusfonts.github.io/

準備したコードとフォントからフォント埋め込み css を実行する。`npm scripts`化して`npm run pack:fonts`で実行できるようにした。

```json:package.json
...
  "scripts": {
    ...
    "pack:fonts": "node ./resources/gen_fonts_css.js ./resources/fonts.css ./resources/fonts"
  },
...
}
```

最後に、カスタムレイアウトで作成した`style-pdf.css`にフォント埋め込み css に埋め込まれたフォントを使用する記述を追加する。

```css:resources/style-pdf.css
...
html {
  font-family: 'M PLUS 1' !important;
  font-size: 12pt;
}

body, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 {
  font-family: 'M PLUS 1' !important;
}

code, kbd, pre, samp, pre {
  font-family: 'M PLUS 1 Code' !important;
}

#toc ul {
  font-family: 'M PLUS 1' !important;
}

.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title {
  font-family: 'M PLUS 1' !important;
}

.admonitionblock > table td.icon .title {
  font-family: 'M PLUS 1' !important;
}

.conum[data-value] {
  font-family: 'M PLUS 1' !important;
}

svg * {
  font-family: 'M PLUS 1' !important;
}
...
```

Asciidoctor-web-pdf のデフォルトスタイルからフォント設定の箇所を抜き出し`!important`で上書きする。また、draw.io 図内のテキストのフォント設定を上書きするため、`svg *`のルールを追加する。ここでは、前述のAsciidoc image タグの`options=inline`設定と相まって、フォント 'M PLUS 1' に設定される。

> \/\/ テキストが入る図は options=inline をつける
> image::210_hardware.drawio.svg[options=inline,width=100%]

なお、フォント埋め込み css も同様に、resources フォルダからビルド時にコピー、ビルド後に削除するようにするため、`npm script`化した。これらをすべて組み込んだ最終系の`npm script`は以下の通り。`npm run build`を実行すると、`dist/`以下に`index.html`、`images/`と`index.pdf`が出力される。 

```json:package.json
{
...
  "scripts": {
    "prebuild": "cp ./resources/index-docinfo.html ./src/index-docinfo.html && cp ./resources/index-docinfo-header-pdf.html ./src/index-docinfo-header-pdf.html && cp ./resources/fonts.css ./src/fonts.css && cp ./resources/style-pdf.css ./src/style-pdf.css",
    "build": "npm run build:html && npm run build:pdf",
    "build:html": "asciidoctor ./src/index.adoc -a docinfo=private-head --destination-dir ./dist/ && cp -rf ./src/images/ ./dist/images/",
    "build:pdf": "asciidoctor-web-pdf ./src/index.adoc -a stylesheet=\"+fonts.css,style-pdf.css\" --destination-dir ./dist/",
    "postbuild": "rimraf ./src/index-docinfo.html ./src/index-docinfo-header-pdf.html ./src/fonts.css ./src/style-pdf.css ./src/index.html",
    "pack:fonts": "node ./resources/gen_fonts_css.js ./resources/fonts.css ./resources/fonts"
  },
...
}
```

ここまでで出力された PDF は[こちら](https://ynitto.github.io/asciidoc-template/index.pdf)で確認できる。

## 仕様書執筆に関する Tips

最後に Asciidoc や draw.io を使う上での Tips を書いておく。これらの中には、過去のバージョンや今回採用しなかったツールで起きていた現象の回避策も含まれるため、詳細な検証はしていないが、気をつけておいて損はないと思う。

- 文章中に特殊文字を含めない。環境依存文字等は明らかであるが、◯や×などに注意が必要。PDF作成時に対応フォントを埋め込むなどの対策もあるが、ファイルサイズが大きくなるので、できれば避けたほうが良い。使いたい場合 draw.io で作成してインラインで埋め込む方法などがある。
- Asciidoc 中に無意味な改行を含めないほうがよい。レイアウトが崩れることがあった。文法上必要な改行は無論入れるが、文章中の改行は` +`を使うこと。`+`前に` `が必要なことにも注意。
- draw.io のテキスト設定から`ワードラップ`と`フォーマットされたテキスト`のチェックを OFF に設定すること。デフォルトは ON。これらが ON だと、full SVG 1.1 のフォールバックメッセージが埋め込まれてしまい、コンバータによってはこちらが出力されてしまう。不要なデータを入れないためにも OFF がおすすめ。

## 次回
ここまでで仕様書の開発環境は構築できた。[次回](https://qiita.com/ynitto/items/569de7073f476d588d36)は仕様書の CI/CD 環境を整える。