フィルタ・ライクなXSLTプロセッサを自作する

色々あって簡単なXSLTプロセッサを作った。

HDDの中にはXSLTを使ってXMLを変換する類の仕組みが3つもあるのに、どれも変換するドキュメントとXSLTプロセッサ等のツールをセットにして環境を構築してある。なので手元でちょこちょことXSLTを書いてXMLを変換する時に使えるようになっていない。

また使用しているXSLTプロセッサはxalan-javaとネット上で見つけたJavaで書かれた無茶苦茶シンプルな自作プロセッサなのだけど、どちらもJavaによる実装なのでコンソールで実行する時のコマンドがシンプルじゃないし且つあちこちのディレクトリから使うには少々不便だ。まあその辺はシェルスクリプトやバッチファイル等のラッパーを用意すれば回避できるけど、ちょっと面倒。

ただ、どうにもならない点がUnixのテキストフィルタのように使えないこと。変換対象のXMLファイルを標準入力から読み込んだり、変換した結果を標準出力に垂れ流したりするのが無理なようだ。

少し調べた限りXalan-C++ならフィルタっぽく使えそうだけど、最終更新から随分経過しているのが気になる。暫く前からMSXMLを使ってXMLファイルを取り扱う方法を試していたので、MSXMLを使ってXSLTプロセッサを自作してみた。

<?xml version="1.0" standalone="yes" encoding="Shift_JIS" ?>

<!--
  @brief   MSXMLを使ったシンプルなフィルタ型XSLTプロセッサ
  @author  eel3 @ TRASH BOX
  @date    2013/02/04
  @note    JScript on WSH 5.7 @ Windows XP Professional SP3
-->

<package>

<job id="transxml">
<runtime>
  <description>Simple UNIX filter style XSLT processor, using MSXML.</description>
  <named name="xml" type="string" required="false"
         helpstring="(Optional) Source XML document file you want to be transformed." />
  <named name="xsl" type="string" required="true"
         helpstring="XSLT stylesheet file you want apply to XML document file."/>
  <named name="out" type="string" required="false"
         helpstring="(Optional) Output file. Use this if output is XML." />
  <example><![CDATA[
EXAMPLE : 
    cscript transxml.wsf //Nologo /xml:foo.xml /xsl:bar.xsl /out:foo.html
    cscript transxml.wsf //Nologo /xml:foo.xml /xsl:bar.xsl > foo.html
    cscript transxml.wsf //Nologo /xsl:bar.xsl /out:foo.html < foo.xml
    cscript transxml.wsf //Nologo /xsl:bar.xsl < foo.xml > foo.html
]]></example>
</runtime>

<script language="JScript"><![CDATA[
/*jslint indent: 2, maxerr: 50, windows: true */
(function () {
  'use strict';

  /// このスクリプトを実行しているホストの名前を取得する。
  /// cscript, wscript, WScript の何れかが返ってくるはず。
  var scriptHost = (function () {
      var fsys = WScript.CreateObject("Scripting.FileSystemObject"),
        name = fsys.GetBaseName(WScript.FullName);
      return function () { return name; };
    }()),

    /// コンソールで実行中ならtrueを返す
    isConsole = function () {
      return (scriptHost() === "cscript");
    },

    // wscript で実行する場合、使用可能なメッセージ表示関数は Echo のみ。
    // cscript なら標準出力と標準エラー出力を分けて使用できる。

    /// メッセージを表示する
    print = function (s) { WScript.Echo(s); },

    /// エラーメッセージを表示する
    eprint = (function () {
      if (isConsole()) {
        return function (s) { WScript.StdErr.WriteLine(s); };
      } else {
        return function (s) { WScript.Echo(s); };
      }
    }()),

    /// path を絶対パスに変換して返す
    absolutePath = function (path) {
      return new ActiveXObject("Scripting.FileSystemObject").GetAbsolutePathName(path);
    },

    /// XMLファイルを読み込み、XMLドキュメントオブジェクトを返す
    loadXMLFile = function (url) {
      var doc = new ActiveXObject("MSXML2.DOMDocument");
      doc.async = false;
      doc.load(url);
      return doc;
    },

    /// XMLドキュメントの文字列を解析し、XMLドキュメントオブジェクトを返す
    parseXML = function (str) {
      var doc = new ActiveXObject("MSXML2.DOMDocument");
      doc.loadXML(str);
      return doc;
    },

    /// ドキュメント xml にスタイルシート xsl を適用した結果を文字列で返す
    transform = function (xml, xsl) {
      return xml.transformNode(xsl);
    },

    /// ドキュメント xml にスタイルシート xsl を適用して、
    /// 結果をファイル out に書き出す
    transformAndSave = function (xml, xsl, out) {
      var doc = new ActiveXObject("MSXML2.DOMDocument");
      doc.async = false;
      xml.transformNodeToObject(xsl, doc);
      doc.save(out);
    };

  /// メインルーチン
  WScript.Quit((function (progname, args) {
    var EXIT_SUCCESS = 0,
      EXIT_FAILURE = 1,
      opts = args.Named,
      xmldoc;

    if (!isConsole()) {
      eprint("Please use cscript.exe");
      return EXIT_FAILURE;
    }

    if ((opts.length < 1) || (opts.length > 3)) {
      eprint("invalid arguments");
      return EXIT_FAILURE;
    }
    if (!opts.Exists("xsl")) {
      eprint("need option \"/xsl:filename\"");
      return EXIT_FAILURE;
    }
    if (opts.Exists("xml")) {
      xmldoc = loadXMLFile(absolutePath(opts("xml")));
    } else {
      xmldoc = parseXML(WScript.StdIn.ReadAll());
    }

    if (opts.Exists("out")) {
      transformAndSave(xmldoc,
                       loadXMLFile(absolutePath(opts("xsl"))),
                       absolutePath(opts("out")));
    } else {
      print(transform(xmldoc, loadXMLFile(absolutePath(opts("xsl")))));
    }

    return EXIT_SUCCESS;

  }(WScript.ScriptName, WScript.Arguments)));
}());
]]></script>
</job>

</package>

使用言語はJScriptで、WSF(Windowsスクリプトファイル)に直接記述してみた。素のJScriptのファイル(*.js)で実装するよりもヘルプ周りの記述が楽だ。

実装としては特に大したことはしていない。変換するXMLファイルが指定されている場合はそのファイルを直接ロードし、指定されていない場合は標準入力を全て文字列として取得してXMLとしてパースする。標準入力から読み込む方法では読み込み可能なテキストのエンコーディングが限定されるかもしれない。

出力先がファイルの場合と標準出力の場合とでは変換処理が異なる。これはXMLからXMLに変換する際に出力されるXSL宣言のencoding指定が意図しないものになってしまう問題を回避する為だ。MSXML2.DOMDocument::saveでファイルに出力する場合は正しいencodingになるのだけど、それ以外の方法ではencodingが必ずUTF-16になってしまうようだ(しかも実際の文字コードUTF-16ではない……)。

結局、フィルタとしては限定的にしか使えないツールになってしまった。手元で自分が使う分には問題ないのだけど、他人に使ってもらっても大丈夫なレベルではないだろう。

それでも標準出力経由でページャで表示できるので、試行錯誤しながら変換用のXSLTを書く時に重宝するかも。