AsciiDoc のテンプレートコンバーターを使ってHTML出力をカスタマイズする

create
2020年11月28日
update
2020年11月28日

動機🤔

AsciiDocprism.jsCommand Line プラグイン を使ってみたかった。
上記プラグインではカスタムデータ属性の指定が必要だが、標準の Asciidoctor.js では任意のカスタムデータ属性に対応していない。

よって、テンプレートコンバーターを使ってカスタムデータ属性に対応したHTML出力をさせてみる。

……やってみた結果としては、カスタムコンバーターを使うほうがよさそうだった🥺。

Table 1. 開発環境
ツール バージョン

Node

12.19.0

@asciidoctor/core

2.2.0

本記事でやったこと

AsciidocTemplate
JavaScript の関数でもテンプレート作成できるが、今回は割愛。

テンプレート🥪

HTMLコードを生成するJSテンプレートエンジン用のソースファイルを指す。
asciidoctor.js の各ノード(paragraph ブロックとか table ブロックとか)ごとのHTML出力を、このテンプレートから生成されるHTMLコードで置き換える。

checkメリット
  • 各ノードの出力をファイル単位で独立して記述できるので、上書きしたいノードに対してのみテンプレートを作成すればよい

  • 適用するファイルまたはディレクトリを変更するだけでHTML出力を変更できる

closeデメリット
  • テンプレートエンジンの使い方を学習する必要がある

  • ブラウザ環境では使えない(はず)

JSテンプレートエンジン

Asciidoctor.js がサポートするテンプレートエンジンには pugEJS などいくつかある
今回はそのうちの nunjucks を使ってみた。

nunjucksインストール
yarn add nunjucks

ライブラリをインストールすれば、あとは自動的に Asciidoctor.js の方で読み込まれる。

テンプレートの作成

  1. テンプレートを配置するディレクトリを作成(出力時に template_dirs で指定)

  2. 置き換えたいノード名と一致する名前でファイル作成
    ノード名の一覧は こちら を参照。

    たとえば置き換えたいノード名が listing ならば、作成するテンプレートは listing.njk というように同じ名前にする。
  3. テンプレートのコードを書く


ノード名とテンプレート名を同じにすることだけ注意すれば、あとはがんばってテンプレートを書くだけ。

以下はその一例で、prism.jsCommand Line プラグインに対応させるためにカスタムデータ属性の出力などを行っている。

テンプレートのサンプル
listing.njk
{%- import "macros/sourcecode.njk" as func -%}

<div {{ func.roles(node) }}>
{% if node.getTitle() -%}
  <div class="title">{{ node.getTitle() }}</div>
{%- endif %}
  <div class="content">
  {% if node.getStyle() === 'source' -%}
    <pre class="highlight {{ 'command-line' if node.getRoles().includes('command-line') -}}" {{ func.customdata(node.getAttributes()) }}><code {{ func.language(node) }}>{{ node.getContent() | safe }}</code></pre>
  {% else -%}
    <pre {{- func.customdata(node.getAttributes()) }}>{{ node.getContent() | safe }}</pre>
  {%- endif -%}
  </div>
</div>
macros/sourcecode.njk (マクロ用ソース)
{# set id and class attributes #}
{%- macro roles(node) -%}
  {% if node.getId() -%}
    id="{{ node.getId() }}"
  {%- endif %}
    class="{{ ['listingblock', node.getRole()] | join(' ') | trim }}"
{%- endmacro -%}

{# <code> language class and attribute #}
{%- macro language(node) -%}
  {%- set lang = node.getAttribute('language') -%}
  {%- if lang -%}
    class="language-{{ lang }}"
    data-lang="{{ lang }}"
  {%- endif -%}
{%- endmacro -%}

{# custom data attributes #}
{% macro customdata(attrs) -%}
  {%- set regExp = r/^data-.*/ -%}
  {% for key, value in attrs -%}
    {% if regExp.test(key) -%}
      {{ key }}="{{ value }}"
    {% endif %}
  {%- endfor %}
{%- endmacro %}
safe フィルター
nunjucks では文字列を自動エスケープするが、エスケープ処理は asciidoctor で行いたいので停止しておく。
node オブジェクト
テンプレートには AbstractNode 型の node オブジェクトが引数として渡される。
テンプレート内ではこの node からノードのクラスや属性、テキストなどを参照する。

テンプレートを適用して出力💻

CLIまたはAPIにおいて、先ほど作成したテンプレートファイルのあるディレクトリを template_dirs オプションで指定して呼び出す。

main.ts
import Processor from '@asciidoctor/core'

const processor = Processor()
const doc = processor.loadFile(
  'path/to/adoc_file', { template_dirs: ['path/to/template/dir'] })

console.log(doc.convert())
templateフローチャート

おわりに😎

テンプレートコンバーターの使い方についてあまり情報がなくて戦々恐々としてたけど、やってみたら簡単だった。

ただテンプレートエンジンの使い方も学習する必要があるのが難点。
JavaScript コードで完結するカスタムコンバーターのほうを使ったほうがいいかもしれない。