いまさらanakia+Jingを使ってみた

XML文書を変換する仕組みとしては、一般的にはXSLTが主流です。
しかし2009年頃にはApache (Jakarta) Velocityをtemplate engineとした[Anakia](http://velocity.apache.org/anakia/)が、そこそこ盛り上っていて、自分自身も(最近は更新していませんが)ブログ作成用に使っています。

Velocity自体はいまでも開発は行なわれていて、JavaベースのWebアプリケーションのtemplate engineとしては知られた存在です。しかし、残念ながらanakiaの開発は行なわれておらず、最新のVelocity Engine 2.0のリリースには含まれていません。DocBookのように定義済みのスキーマをベースにしたものは利用されていますが、Anakiaに限らず任意の自前XML形式を変換するような仕組みは一般にはあまり利用されず、markdownのような仕組みが普及していると思います。

XSLTはチューリング完全で関数型言語の趣向が強いので、再帰的な処理を記述したり、XMLの文法に従うためタグの分だけ記述量は一般的なプログラミング言語よりも多くなるなど、完成されている一方で、簡単に使えるというより面倒に感じる要素があります。

XSLTやXSL-FOはすばらしい技術ですが、これらを利用しなければならないという先入観はXMLの利用を限定的にさせているとも思っています。

最近は、個人的に静的Webサイトの構築に[Hugo](https://gohugo.io/)をバックエンドとして、記事はMarkdown(md)やAsciidoc(adoc)で書き始めていますが、mdやadoc形式はシンプルな反面、個人的には次のような不満があります。

* 【md形式】文書中にコード片やキーボード入力を表現したい場合に、強調表示としては同じマークアップ('+'や'*'で囲むなど)で可能なものの、どちらも同じ表現となり、その文脈(context)を区別する方法がない。
* 【adoc形式】mdとは違い、表現は豊富で、様々な状況に対応はできるが、表現は直感的とはいえない。
* 【md,adoc共通】ファイルを検証するには、パースできるかどうかのレベルに留まるため、許容の幅が広い分、エラーを期待しても、そのまま画面に出力されてしまう。
* 【md,adoc共通】変換に対して介入できる余地があまりない。

そもそもの目的が違うので、XMLで実現してきたようなことができないというのは理不尽なのですが、テクニカルな文書を記述する場合に、キーボード入力や画面出力は、より分かりやすく区別して記述したいと個人的には感じます。

adoc形式はclass属性やid属性を任意に付与することができるので、この点では便利です。書き方を統一すれば、CSSで吸収させることができるでしょう。
ただ可読性が良いのかどうか、独自に組み込まれている機能は果して便利といえるのか、まだ不慣れな事もあると思いますが、純粋な文脈よりも画面出力を強く意識した操作をする必要があると感じてしまいます。

また、adoc形式は他人には利用を強制しずらく感じます。roff形式なんかよりはずっと良いですが、自分のツールとしては選択肢に入っても、不特定多数の人達で書式を統一したい場合には、検証機能が貧弱だったり使いにくいと思います。

そのためHugoのcontents/階層の一部は、Anakiaで出力したHTMLファイルを配置するようにしてみました。またRelax-NGでスキーマを定義し、Jingにより検証しています。

markdownで十分だし、シンプルな方が良いと思っている向きはあると思います。私自身は文書のデータと出力表現の処理は分けるべきだという信念に近い思いがあるため、XML(のようなもの)原理主義的なところも強く影響していると思います。

前置きはこれぐらいで、昔にanakiaで作った独自XML形式で書いた文書を流用したいと思ったので、現時点で感じた課題などをまとめました。

# 環境

* Ubuntu 18.04
* Java 8 (1.8.0_191)

ダウンロードしたファイルは ~/Downloads/ に配置しています。
また各コマンドは毎回、作業用のトップディレクトリから実行しています。繰り返し ```$ cd tools``` のようなコマンドを実行していますが、既に移動していればスキップできるので注意してください。

# Anakiaの入手とセットアップ

[公式Velocityサイトのダウンロードページ](http://velocity.apache.org/download.cgi)の下の方に、anakia-1.0へのリンクがありますが、これは使いません。

後述しますが、[公式サイトのreleaseページからvelocity-1.7](https://repository.apache.org/content/repositories/releases/org/apache/velocity/velocity/1.7/)を入手して、anakia-1.0の代りにvelocity 1.7を利用していきます。変更点はAnakiaTaskのパッケージ名ぐらいだと思うので、anakia-1.0のドキュメントを参照している場合には、antタスクを作成する際に注意が必要だと思われます。

```bash
$ mkdir tools
$ cd tools
$ tar xvzf ~/Downloads/velocity-1.7.tar.gz
$ ln -s velocity-1.7 velocity
```

## Anakia以外のツール類

XMLファイルの検証にはRelaxNGとJingを利用しているので、そのためのツールも配置しておきます。

[Jingの公式サイト](https://relaxng.org/jclark/jing.html)はrelaxng.orgに存在していますが、ソースコードの配布場所はGithub([relaxng/jing-trang](https://github.com/relaxng/jing-trang))に移動しています。

実行に必要なJARファイルは[公式サイトのダウンロードページ](https://code.google.com/archive/p/jing-trang/downloads)から20091111のものをダウンロードしています。

```bash
$ cd tools   ## anakiaと同じディレクトリに移動
$ unzip ~/Downloads/jing-20091111.zip
$ ln -s jing-20091111 jing
```

anakiaはantのタスクとして起動するので、[ダウンロードサイトから](https://ant.apache.org/bindownload.cgi)1.9系列の最新版を入手しておきます。

```bash
$ cd tools  ## anakia、jing, trnagと同じディレクトリに移動
$ tar xvzf ~/Downloads/apache-ant-1.9.13-bin.tar.gz 
$ ln -s apache-ant-1.9.13 apache-ant
```

ここまでで、toolsディレクトリは次のようになりました。

```bash
$ ls -F
apache-ant@  apache-ant-1.9.13/  jing@  jing-20091111/   velocity@  velocity-1.7/
```

## 実行環境の準備

toolsディレクトリと並列にbinディレクトリを準備してコマンドを実行するための準備を行ないます。

```bash
$ mkdir bin   ## toolsディレクトリと同じ場所に作成します。
```

まず bin/envrc ファイルを準備します。

```bash:bin/envrc
## Please change the following line for your correct JDK location.
JAVA_HOME=/opt/jdk1.8.0_191
export JAVA_HOME

WD="$(pwd)"
SCRIPTFILE="$(readlink -f $0)"
BASEDIR="$(dirname $SCRIPTFILE)"
TOPDIR="${BASEDIR}/.."
export WD SCRIPTFILE BASEDIR TOPDIR

TOOLDIR="${TOPDIR}/tools"
ANT_HOME=${TOOLDIR}/apache-ant
VELOCITY_HOME=${TOOLDIR}/velocity
JING_HOME=${TOOLDIR}/jing

export ANT_HOME VELOCITY_HOME JING_HOME
export PATH=${ANT_HOME}/bin:${PATH}

CP_ANT=$(find ${ANT_HOME}/. -name '*.jar' | tr '\n' ':')
CP_VELOCITY=$(find ${VELOCITY_HOME}/. -name '*.jar' | tr '\n' ':')
CP_JING=$(find ${JING_HOME}/. -name '*.jar' | tr '\n' ':')
export CLASSPATH=${CLASSPATH}:${CP_VELOCITY}:${CP_ANT}:${CP_JING}
```

今回はbinにantタスクを記述したxmlファイルを準備しています。

```bash:bin/run-anakia.xml
<project name="build-site" default="doc" basedir=".">
  <property environment="env" />
  <!-- Please change the following property variables -->
  <property name="docs.infilepattern" value="20*.xml" />
  <property name="docs.basedir" value="${env.WD}" />
  <property name="docs.destdir" value="${env.WD}" />
  <property name="docs.vslfilename" value="blog.vsl"/>
  <property name="docs.projfilename" value="project.xml"/>
  <property name="docs.propfilepath" value="${env.BASEDIR}/velocity.properties"/>
  
  <taskdef name="jing" classname="com.thaiopensource.relaxng.util.JingTask"/>
  
  <target name="validate_relaxng">
    <jing rngfile="${docs.basedir}/blog.rng">
      <fileset dir="${docs.basedir}" includes="${docs.infilepattern}"/>
    </jing>
  </target>
  
  <target name="doc" depends="validate_relaxng">
    <taskdef name="anakia"
	     classname="org.apache.velocity.anakia.AnakiaTask"/>
    <anakia basedir="${docs.basedir}"
	    includes="${docs.infilepattern}"
	    destdir="${docs.destdir}"
	    extension=".html"
	    style="${docs.vslfilename}"
	    projectFile="${docs.projfilename}"
	    velocityPropertiesFile="${docs.propfilepath}"
	    lastModifiedCheck="true" >
    </anakia>
  </target>
  
</project>
```

これらの処理を実行するラッパースクリプトとして、bin/run-anakia.sh を準備します。

```bash:bin/run-anakia.sh
#!/bin/bash

SCRIPTFILE="$(readlink -f $0)"
BASEDIR="$(dirname $SCRIPTFILE)"
. "${BASEDIR}/envrc"

## main ##
ant -f ${BASEDIR}/run-anakia.xml "$@"
```

このスクリプトを実行するとJingによる検証とHTMLファイルの生成を同時に行ないますが、```$ bin/run-anakia.sh validate_relaxng```のように検証タスクだけを実行させることも可能です。

続いて、velocity.properties を準備します。
デフォルトの言語はLatin-1(ISO-8859-1)が指定されているので必ず必要になります。

```properties:bin/velocity.properties
input.encoding=UTF-8
output.encoding=UTF-8
```

最終的に次のようなファイルを準備しています。

```bash
$ cd bin
$ ls
envrc  run-anakia.sh  run-anakia.xml  velocity.properties
```

# 記事の準備

あとはターゲットとなるディレクトリに移動して、3つのファイルを準備します。

* ${target_dir}/project.xml
* ${target_dir}/blog.vsl
* ${target_dir}/blog.rng

最初のproject.xmlについては、特に利用していないので、ほぼ空に近い内容になっています。

```xml:${target_dir}/project.xml
<?xml version="1.0" encoding="UTF-8"?>
<project name="Anakia"
         href="http://velocity.apache.org/anakia">
</project>
```

利用しているRelaxNGスキーマやVSLテンプレートは長いので、[OASISのTutorial](https://relaxng.org/tutorial-20011203.html)から抜粋したRNGファイルに合わせる形で、VSLテンプレートと記事のXMLファイルをサンプルとして掲載しています。

```text:${target_dir}/blog.vsl
<html>
#set($cards = $xpath.applyTo("/card", $root))
#foreach($card in $cards)
<ul>
#foreach($c in $card.getContent())
#if($c.name == "name")
  <li>Name: $c.getValue()</li>
#elseif($c.name == "email")
  <li>EMail: $c.getValue()</li>
#end
#end
</ul>
#end
</html>
```

```xml:${target_dir}/blog.rng
<?xml version="1.0" encoding="UTF-8" ?>
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
  <start>
    <element name="addressBook">
      <zeroOrMore>
        <element name="card">
          <ref name="cardContent"/>
        </element>
      </zeroOrMore>
    </element>
  </start>
  <define name="cardContent">
    <element name="name">
      <text/>
    </element>
    <element name="email">
      <text/>
    </element>
  </define>
</grammar>
```

# ドキュメントの生成

設定ファイルを配置したら、同じディレクトリに適当なドキュメントを配置します。

```xml:${target_dir}/20190114.xml
<addressBook>
  <card>
    <name>John Smith</name>
    <email>js@example.com</email>
  </card>
  <card>
    <name>Fred Bloggs</name>
    <email>fb@example.net</email>
  </card>
</addressBook>
```

このディレクトリでスクリプトを起動すると、次のような結果が得られます。

```bash
$ cd ${target_dir}
$ ls -F
20190114.xml  blog.rng*  blog.vsl   project.xml  velocity.log
$ ../bin/run-anakia.sh
...
$ cat 20190114.html 
<html>
<ul>
  <li>Name: John Smith</li>
  <li>EMail: js@example.com</li>
</ul>
<ul>
  <li>Name: Fred Bloggs</li>
  <li>EMail: fb@example.net</li>
</ul>
</html>
```

# Hugoとの連携での課題

Hugo自体には独自のプリプロセッサーを追加する仕組みがないため、hugo実行とは別に、前処理としてcontent/以下にHTMLファイルを出力する必要があります。

そこでHugoの管理下にないディレクトリにXML形式で書いたソースファイルを配置し、AnakiaTaskでcontent以下の任意の場所にHTMLファイルを出力することになります。

HTMLファイルを配置した時に[Front Matter](https://gohugo.io/content-management/front-matter/)を付与する必要があるので、VSLファイルの中でTitleぐらいは付与した方がいいのだろうと思います。

Velocity自体には、ファイルの更新時間を知る良い方法がないので、dateをFront Matterに追加するには、CustomContextとしてファイル名等とファイル作成日の文字列が紐付いているXMLファイルを指定するなどの工夫が必要だろうと思われます。

# anakia-1.0を利用すると発生する問題

配布されているanakia-1.0を利用すると、含まれているvelocity-1.5.jarのために、anakiaのドキュメントに記述されている機能が書かれているように動作しない可能性があります。

## $velocityHasNextの利用

VSLテンプレートファイルで、#foreachループの最初や最後で行なう処理を記述するために必要な```$velocityHasNext```は、velocity-1.5では実装されていません。

> これらvelocity.propertiesのデフォルト値として設定されています。
directive.foreach.counter.name = velocityCount
directive.foreach.iterator.name = velocityHasNext

例えば、HugoのFront MatterにTOML形式でtagsを記述する時には、リスト形式で記述するため、```tags = ["tag1","tag2"]```のように前後をカッコで囲む必要があります。
元々tag名が\$tags変数にリスト形式で格納されている場合、次のようなコードが必要になり、\$tags.size()の結果と\$velocityCountを比較する良い方法がないため、\$velocityHasNextが必要になります。

```velocity
#foreach ($tag in $tags)
  #if ($tags.size() == 1)
tags: [ "$tag.getValue()" ]
  #elseif ($velocityCount == 1)
tags: [ "$tag.getValue()"##
  #elseif ($velocityHasNext)
, "$tag.getValue()"##
  #else
, "$tag.getValue()" ] 
  #end
#end
```

この他にもvelocity-1.7より前のバージョンでは、様々な問題があるためvelocity-1.5に依存したanakia-1.0を利用することはお勧めしません。

# まとめ - Anakiaを使う理由

業界標準としては、XSLT, XSL-FOを使うのがセオリーだと思いますが、RelaxNGなどのスキーマで定義した独自形式のXMLファイルは、ある程度の意味を含んだwell-formedな文書を作成してHTMLなどに変換したいニーズを満たす良い方法だと思っています。

個人的にはMarkdownやAsciidocが普及する事に異論はないのですが、これが便利だから良いのだという風には考えることができません。

AIに限らずコンピュータが処理をする参考になる資料としては、手掛かりになる注釈が加わったものが必要だと思っていて、たとえRDFやOntologyを利用していなくてもタグ名が手掛かりになるXML文書は良いものだと思っています。

AnakiaはXSLTよりは学習と利用が容易なので、手元のツールとしては利用したいし、普及もして欲しいと思っています。