🔰

Saxon-JS2でXSLT 3.0とXPath 3.1 on Node.js

2023/05/25に公開

Saxon-JS2とは

Saxonicaが提供する、無償利用可能なXSLT 3.0の処理系です。無償利用可能ですが、オープンソースではありません。

Saxonica製のJavaScriptによるXSLT処理系については、Saxon-JS2以前にもSaxon-CEというものがありました。Saxon-CEはXSLT 2.0の処理系・ソースコード公開ということもあり、Saxon-JS系は仕切り直しのようです。

Saxon-JS2は2023年5月時点で2.5.0が最新です。

本記事では、NodeJS版のサンプルとして、ミニマルなサンプルであるhelloWorldNodeJSを紹介します。

https://github.com/Saxonica/helloWorldNodeJS

Webブラウザ(ランタイム)版の方は、サンプルが凝りすぎていて説明が難しいのでとりあえず割愛します。

xslt3とsaxon-jsパッケージ

Saxon-JS2はNode.jsとWebブラウザで利用で可能です。つまり、ウェブブラウザに残る負の遺産と化したXSLT 1.0ではなくXSLT 3.0を使えるようになるということ……!ではあるのですが、ちょっとした制約があります。

  • 処理時にデータ型がJavaScriptのもので扱われる
  • xsl:package非対応
  • コンパイル済みスタイルシートだけに対応

コンパイル済みスタイルシートの説明の前に、Saxon-JS2パッケージについて。
Saxon-JS2はnodeパッケージとしてxslt3とsaxon-jsに分かれています。
簡単には、xslt3はコマンドライン実行で、saxon-jsはライブラリ利用です。xslt3では普通にスタイルシートを読み込み、実行可能です。-it(Initial Template)や、jsonによってJSONを入力に使うことも可能です。そしてxslt3では、通常の実行の他に、スタイルシートのコンパイルが可能です。

準備

yarnでもnpmでもpnpmについては適宜読み換えてください。

$ git clone https://github.com/Saxonica/helloWorldNodeJS.git
$ cd helloWorldNodeJS
$ yarn install

helloWorldNodeJSスタイルシートは何をしているか

xslt/main.xsl
...
<xsl:param name="payload" as="map(*)?" select="()"/>

<xsl:template name="xsl:initial-template">
  <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    ...
    </head>
    <body>
      <xsl:choose>
        <xsl:when test="exists($payload)">
          <h1>Hello, JSON</h1>
          <dl>
            <xsl:for-each select="map:keys($payload)">
              <dt>
                <xsl:sequence select="."/>
              </dt>
              <dd>
                <xsl:sequence select="map:get($payload, .)"/>
              </dd>
            </xsl:for-each>
          </dl>
        </xsl:when>
        <xsl:otherwise>
          <h1>Hello, World</h1>
          <p>This text comes from the stylesheet.</p>
        </xsl:otherwise>
      </xsl:choose>
    </body>
  </html>
</xsl:template>

先ず、paramとしてpayloadをmap型で読み込みます。これがJS側からJSONを突っ込む所です。

そして、<when>の条件として、スタイルシートを呼び出すときにこのparamを使っているかどうかで分岐します。$payloadが存在する場合、<for-each>でJSONの中身をdlのアイテムとして並べる、となります。map:keys($payload)ではpayload内のkey-valueのキーがシーケンスの形で返りますから、これをfor-eachで回すため、内部のコンテキストノード`.`は個別のkey名となります。`map:get()`は第一引数に対象のmap、第二引数にkey名を取るため、`map:get(payload,.)`では、$payload mapの、現在のkeyに対応するvalueが返ります。

paramが無い場合は<otherwise>の箇所が実行されます。

paramの有無によって、1つのスタイルシートから2種類のHTML文書が出力されるわけですね。

コンパイル済みスタイルシート

https://github.com/Saxonica/helloWorldNodeJS

node node_modules/.../xslt3.js ...を適当に読み換えたのが次。npmでもpnpmでもそれぞれで。

$ yarn run xslt3  -t -xsl:xslt/main.xsl -export:main.sef.json -nogo "-ns:##html5"

README.mdで、-tでスタイルシートを指定し、-exportというオプションでjsonを吐き出しています。saxon-jsパッケージから利用するためスタイルシートをコンパイルしています。

実行形式へのコンパイルは、JavaのSaxon-EEには以前からある機能です[1]。sefは、多分saxon execution formatあたりの頭字でしょう。 saxon-jsでは、JSON形式として、ちょっとした亜種となっています。まあ、後は編集も無く機械処理するだけならXML形式は冗長ですからね。さもありなん。

-nsは、名前空間の明示されていないXML語彙が来たときの名前空間を指定しています。##html5は、次の特徴があります。

  • xhtml名前空間または名前空間の宣言無しのときに対応
  • HTML5 DOMに対応(通常、HTMLは妥当なXMLでないためXSLTの対象外)

-nogoはコンパイル時のスタイルシート実行を避けるためのオプションです。

Node.js側コード

スタイルシートの挙動が分かったところでJS側の話。

index.js
/ *https://github.com/Saxonica/helloWorldNodeJS/blob/main/index.js */
const express = require('express');
const cors = require('cors');
const dotenv = require('dotenv').config();
const SaxonJS = require('saxon-js');
const stylesheet = require('./main.sef.json');
const app = express();

const homepage = (request, response) => {
  const options = { "stylesheetInternal": stylesheet,
                    "destination": "serialized" };
  SaxonJS.transform(options, "async")
  .then(output => {
    response.status(200).send(output.principalResult);
  }).catch(error => {
    response.status(400).send(error);
  });
};

const jsonpage = (request, response) => {
  const options = { "stylesheetInternal": stylesheet,
                    "stylesheetParams": { "payload": { "hello": "world" } },
                    "destination": "serialized" };
  SaxonJS.transform(options, "async")
  .then(output => {
    response.status(200).send(output.principalResult);
  }).catch(error => {
    response.status(400).send(error);
  });
};

app.use(cors());

app.route("/").get(homepage);
app.route("/json").get(jsonpage);

// Start server
app.listen(process.env.PORT, () => {
  console.log(`Server listening`);
});

express.jsな箇所については説明を割愛します。
大事なのはsaxon-js関連です。

const SaxonJS = require('saxon-js');
const stylesheet = require('./main.sef.json');

先程説明したように、コンパイル済みスタイルシートを使います。const homepage const jsonpageはそれぞれ、スタイスシートを実行した結果を格納します。この際、homepageには、SaxonJS.Transform()に渡すoptionsに "stylesheetParams"がなく、jsonpageの同様の箇所には "stylesheetParams": { "payload": { "hello": "world" } }とありますね。
このparamによって出力結果が変わり、それぞれの出力を別のパスにルーティングします。結果、localhost:3002/ではHello, Worldlocalhost:3002/jsonではHello, JSONのように別のページが得られます。

次のステップ

helloWorldNodeJSサンプルには登場しませんでしたが、Saxon-JS2では対話的処理用の拡張があります。xmlns:ixsl="http://saxonica.com/ns/interactiveXSLT"の名前空間で、幾つかの関数と要素があります。

https://www.saxonica.com/saxon-js/documentation2/index.html#!ixsl-extension

単純にXSLTが使えるだけだとWeb Componentsなどを見てちょっと虚しくなるので、モチベーション維持のためにも、このあたりを次に学ぶのがよいかもしれません。

脚注
  1. Saxon-EEのsefは以前はXMLだったと思いますが、今はJSON版も吐ける ↩︎

Discussion