Saxon-JS2でXSLT 3.0とXPath 3.1 on Node.js
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を紹介します。
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スタイルシートは何をしているか
...
<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)
では
paramが無い場合は<otherwise>
の箇所が実行されます。
paramの有無によって、1つのスタイルシートから2種類のHTML文書が出力されるわけですね。
コンパイル済みスタイルシート
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側の話。
/ *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, World
、localhost:3002/json
ではHello, JSON
のように別のページが得られます。
次のステップ
helloWorldNodeJSサンプルには登場しませんでしたが、Saxon-JS2では対話的処理用の拡張があります。xmlns:ixsl="http://saxonica.com/ns/interactiveXSLT"
の名前空間で、幾つかの関数と要素があります。
単純にXSLTが使えるだけだとWeb Componentsなどを見てちょっと虚しくなるので、モチベーション維持のためにも、このあたりを次に学ぶのがよいかもしれません。
-
Saxon-EEのsefは以前はXMLだったと思いますが、今はJSON版も吐ける ↩︎
Discussion