テキスト出力にはまる.

少し前に書いたDITAからのテキスト出力にはまっています.
 
こんな書き込みをすると、「な~んだ、またテキストか!」と思う方もいらっしゃると思います.だってやっぱりDITAからの出力といえばPDFやHTMLの方が本筋だからです.でもやってみてわかったのですが、テキスト出力は実にごまかしがききません.
 
できることといえば、<xsl:value-of select="..."/>で文字を出力すること、スペースコードでインデントを調整すること、改行コードを出して行を次に進めることだけです.PDFへ出力するのにはXSL-FOを作りますが、PDFへのレンダリングはFormatterがやります.HTMLへ落としてもレンダリングはブラウザです.両方ともリーディングスペースやトレーリングスペースは取ってくれるは、禁則処理してくれるわと至れり尽くせりです.
 
でもテキスト出力は、文字の出力=レンダリングです.ちょっとでも処理を間違えると、あっという間にレイアウトが崩れます.逆にホワイトスペースの処理など、XMLの普段は見落としがちがちなところをちゃんと処理しなければなりません.何かXSLTスタイルシートでは一番プログラマーの実力が試されているような気がしています.

ところで、DITAといってもいろいろ要素があり、お客様が必ず実装して欲しいと言って来たのは、p、ol、ulなどです.これらはまあなんとかなりました.tableは表題だけ出しますので楽勝です.最後の難関はsimpletableでした.テキスト化したいメッセージのインスタンスは、説明に山ほどsimpletableを使っているのです.例えば次のような具合です.
 
<simpletable>
  <sthead><stentry>エラーコード</stentry><stentry>説明</stentry></sthead>
  <strow><stentry>0xA990:</stentry><stentry>入力コマンドが間違っています。</stentry></strow>
  <strow><stentry>0xA980:</stentry><stentry>パラメータ(-param)が不足しています。</stentry></strow>
</simpletable>
 
これを1カラム目は全部文字を出して、1スペース空け、2カラム目は78カラムになったら折り返して表示しなかればなりません.あげくの果てに、日本語のインスタンスでも英語の単語は途中で改行しない、日本語の"。"、"」"、"("など、英語は"."、","などは行頭禁則せよというおたっしです.こうなってくるとXSLTスタイルシートは本来のtransformation(変換)どころではなくformatting(組版)をやらねばなりません.
 
さてsimpletableのフォーマッティングはJavaにstringを渡してやらせようかと相当考えましたが、XSLTでやることにしました.簡単にいうと、simpletableのテンプレートで、最初のカラムの最大バイト数(Shift_JISか、US-ASCIIに落とすので文字数ではない)を求めます.それを元に<sthead>または<strow>のテンプレートで、次のようなグローバル変数を作ります.$colStringは行の文字を保持します.$colEndはカラムの終了位置を保持します.$currentColは現在のカラム番号を保持します.$currentColは現在出力した文字位置を保持します・
 
<xs:variable name="colString" as="xs:string*" select="('エラーコード','説明')"/>
<xs:variable name="colEnd" as="xs:integer*" select="(22,78)"/>
<xs:variable name="currentCol" as="xs:integer" select="1"/>
<xs:variable name="currentPos" as="xs:integer" select="10"/>

もちろんSaxonでこれらを更新できるようにsaxon:assignable="yes"にしておきます.こうしてこれをパラメータにして、フォーマットするテンプレートを呼びます.このテンプレートでは、$colString[$currentCol]からトークンを切り出し、$colend[$currentCol]を越さないように文字を置いて行きます.余りが出たら空白を出力して$currentColを更新します(つまりカラムチェンジです)カラムに入りきらずに終了位置に来たときもカラムチェンジです.カラムチェンジのときは、そこまで出力した文字の分を$colStringから削除します.
これを$colString=('','')になるまで続けて終了します.そうしたら次のstrowの処理に移りまた同じことの繰り返しです.作る前は相当悩んだのですが、データ構造さえ決まってSaxonでxs:string*の変数が更新可能か確認してしまえば早いものですぐ出来ました.
 
今回はお客様にお願いして、strowの中はブロック要素はなしにしてもらいました.実際もほとんど使われていなかったようです.でも、よくよくがんばれば、上記の$colStringを

 
<xsl:variable name="colElements" as="element()*"/>
 
にして、<stentry>をルートにして各行の要素を保持すれば、ブロック要素もいけるのでは?と思います.いずれにしてもスタイルシートでこんな芸当ができるようになったのもXSLT2.0のシーケンスとsaxon:assignのおかげです.感謝!