Conrefの先のxref

私は(自分としては)開発者のつもりでいて、実際に日本ではサポート業務はやらないんですが、USのサポートにDITA関連の質問が来てしまった場合、「オマエがやれ!」とお鉢がまわってくることがあります.
今回はそんな感じで常連になってしまったインドの会社の方から次のような質問が来ました.

conref(conkeyref)で参照しているブロックの中にxrefがあって、その宛先をhref=".#/[要素の@id]で参照していると、PDFに変換した場合、xrefがハイパーリンクになってくれない.

というものでした.実際に受け取ったデータからサンプルを作り直してみると次のような具合です.

[conref元: cConrefSourceConcept.dita]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE concept PUBLIC "-//OASIS//DTD DITA Concept//EN" "concept.dtd">
<concept id="cConrefSourceConcept">
    <title>Conref Source Concept</title>
    <shortdesc></shortdesc>
    <conbody>
        <div conref="cConrefTargetConcept.dita#cConrefTargetConcept/div-001"/>
    </conbody>
</concept>

[conref先: cConrefTargetConcept.dita]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE concept PUBLIC "-//OASIS//DTD DITA Concept//EN" "concept.dtd">
<concept id="cConrefTargetConcept">
    <title>Conref Target Concept</title>
    <shortdesc>
        Contains conref target elements
    </shortdesc>
    <conbody>
        <div id="div-001">
            <p>This paragraph is contained in <xmlelement>div</xmlelement> element and contains xref element that refers paragraph in parent div. <xref href="#./p-999" format="dita">The reference to paragraph authored using <xmlatt>href=#./element-id</xmlatt> notation.</xref></p>
            <p>Dummy paragraph</p>
            <p>Dummy paragraph</p>
            ...
            <p id="p-999">The referenced paragraph.</p>
        </div>
    </conbody>
</concept>

DITA-OTの初期処理(preprocess)を通した結果は次のように、xrefで参照している先の@id="p-999"がconrefの処理で@id="d7e148"書き換わってしまっています.

[conrefが展開された結果: cConrefSourceConcept.ditaの中間ファイル(見やすいように修正済み)]

<concept id="cConrefSourceConcept">
    <title>Conref Source Concept</title>
    <conbody>
        <div>
            <p>This paragraph is contained in <xmlelement>div</xmlelement> element and 
                   contains xref element that refers paragraph in parent div. 
                   <xref href="#cConrefSourceConcept/p-999"
                    format="dita" ><?ditaot usertext?>The reference to paragraph authored
                    using <xmlatt>href=#./element-id</xmlatt> notation.</xref>
            </p>
            <p>Dummy paragraph</p>
            ...
            <p id="d7e148">The referenced paragraph.</p>
        </div>
    </conbody>
</concept>

この"#./"という記法はDITTA 1.3から取り入れられたもので、いちいちトピックの@idを書かなくとも良いのでお客さんは採用したようです.でも何故かconrefで参照した先にあると動いてくれません.
実はこの"#./"は、conrefされる先にあった場合は期待したようには動きません.上記の例は

conref先が次のようになっていると何の問題もなく動いてくれます.

[conref先: cConrefTargetConcept.dita]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE concept PUBLIC "-//OASIS//DTD DITA Concept//EN" "concept.dtd">
<concept id="cConrefTargetConcept">
    <title>Conref Target Concept</title>
    <shortdesc>
        Contains conref target elements
    </shortdesc>
    <conbody>
        <div id="div-001">
            <p>This paragraph is contained in <xmlelement>div</xmlelement> element and contains xref element that refers paragraph in parent div. <xref href="#cConrefTargetConcept/p-999" format="dita">The reference to paragraph authored using <xmlatt>href=#topic-id/element-id</xmlatt> notation.</xref></p>
            <p>Dummy paragraph</p>
            <p>Dummy paragraph</p>
            ...
            <p id="p-999">The referenced paragraph.</p>
        </div>
    </conbody>
</concept>

実際のcConrefTargetConcept.ditaの中間ファイルを見ると次のように展開されています.

<concept id="cConrefSourceConcept">
    <title>Conref Source Concept</title>
    <conbody>
        <div>
            <p>This paragraph is contained in <xmlelement>div</xmlelement> element and contains xref element that refers paragraph in parent div. <xref href="#cConrefSourceConcept/d7e148" format="dita"><?ditaot usertext?>The reference to paragraph authored using <xmlatt>href=#topic-id/element-id</xmlatt> notation.</xref></p>
            <p>Dummy paragraph</p>
            ...
            <p id="d7e148">The referenced paragraph.</p>
        </div>
    </conbody>
</concept>

DITA 1.3の仕様を見ると、次のように解説されています.

2.4.2.4 Processing xrefs and conrefs within a conref
http://docs.oasis-open.org/dita/dita/v1.3/errata02/os/complete/part3-all-inclusive/archSpec/base/handling-xref-and-conref-within-topics.html#handling-xref-and-conref-within-topics

つまるところ、"#./"と書けば、参照元トピックの中で参照が解決されます.逆を言えば、DITA-OTは参照先トピックの中での@idの変化に合わせてくれる処理をしてくれないのです.

"#./"はconrefされる側で書くのと、conrefされることはない側で書くのでは違うという事を意識しなければならないようです.

しかしこれはちょっと不便にも思えます.再利用を考えると、自分が書いているトピックはいつ何時他のtopicからconrefされるか、オーサリングする人はわからないのですから.

コードとはてな記法

昔はYahoo! ブログに書いていたので、コードのハイライトなんて夢のようでしたが、HatenaBlogでは「はてな記法」を使えば自動的にコードをハイライトしてくれます.

 

なのですが、私の見た感じからするとちょっとXMLの表示はどぎつすぎます.もう少しVSCodeのような洗練された色にできないものか?(もちろんCSSを触れば出来るんでしょうけれども)

 

あと、最近のXSLTではXPathの部分がやたらと大きくなって、これは大抵@selectの中身、つまり属性なのですべて真っ赤な表示です.あまりに芸がありません.他の方のブログを見ているととてもきれいなコードのハイライトを行っていてうらやましく思えます.

 

そんなときxsllistのメーリングリストを見ていたら、DeltaXMLのPhil FearonさんがVSCodeで使用できる拡張機能をポストしてくれました.

 

https://www.biglist.com/lists/lists.mulberrytech.com/xsl-list/archives/202004/msg00029.html

 

これは!と思い、早速GitHubから xpath-embedded-0.0.1.vsix

を落として最新のVSCodeに組み込んでみたのが以下のスナップです.

 

f:id:toshi_xt500:20200414234430p:plain

VSCodeXSLTスタイルシート表示画面

ちょっと小さいのですが、中を見ていただくとXPathの中まで解析してコードのハイライトをしていてくれます.スゴイ!

 

ソースコードを落として勉強がてら見させていただいたのですが、TypeScriptでした.ああそういうことなんですね.VSCodeはHelpからToggle Developer Toolsを選ぶとブラウザのEdgeと同じようなデバッグ画面が表示されます.つまりVSCodeの表示はHTMLそのものなんでした.どおりでいろんな拡張パックがあって、いろいろな言語に対応できている訳です.

 

さて、このように表示させたVSCodeはHTMLなので、Copy/Pasteすれば、このブログにも貼り付けられます.なんですが、やはりVSCodeの表示を張り付けているので、そのままでははみ出してしまったり、改行幅がちょっと大きすぎるなど、いろいろ触りたくなります.しかし、スタイルは@style="..."にすべて展開されてしまっているので、HTMLモードにして直すのも結構大変です.例えば以下のような具合:

 

    <xsl:template match="/">
        <!-- 最も長い文字数の名前を出力する.複数あったら最初のものを出す. -->
        <xsl:variable as="xs:string?" name="aLongName" select=" $friends => fold-left( '', function($name1, $name2) { if (string-length($name1) ge string-length($name2)) then $name1 else $name2 } )" />
        <xsl:sequence select="if ($aLongName => exists()) then ('A longest name: ' || $aLongName) => ahf:system.out.println() else ()"/>
    </xsl:template>

 

なかなか自分の思う通りには行かないものです.XPathの部分は自分で解析できれば、これと同じような結果も得られると思うので、少し勉強しようと考えています.

 

 

XSLT 4.0 Proposal (3) Union Types

今回はUnion Typesの話題です.Union Typeつまり共用体という型というと昔のC言語のunionを思い出します.XMLではどうなのでしょうか?

XMLでも共用体はXML Schemaで表すことができます.ある要素のコンテンツや属性が複数の型から成ることは十分ありえるのです.例えばXML Schemaのunion型の例でWebによく出てくるのはジーンズのサイズを表す以下のようなスキーマです.

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" 
    elementFormDefault="qualified"
    attributeFormDefault="unqualified">
    
    <xs:element name="jeans_size" type="jeansType">
    </xs:element>
    
    <xs:simpleType name="jeansType">
        <xs:union memberTypes="sizebyno sizebystring" />
    </xs:simpleType>
    
    <xs:simpleType name="sizebyno">
        <xs:restriction base="xs:positiveInteger">
            <xs:maxInclusive value="42"/>
        </xs:restriction>
    </xs:simpleType>
    
    <xs:simpleType name="sizebystring">
        <xs:restriction base="xs:string">
            <xs:enumeration value="small"/>
            <xs:enumeration value="medium"/>
            <xs:enumeration value="large"/>
        </xs:restriction>
    </xs:simpleType>    
</xs:schema>

これにより、ジーンズのサイズは42(インチ?)までの正の整数、もしくは"small", "medium", "large"のいずれかの文字列と表現できます.このXML Schemeを参照するXMLインスタンスは例えば次のように書けるでしょう.

<?xml version="1.0" encoding="UTF-8"?>
<jeans_size
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="./xsd/union-sample.xsd">42</jeans_size>

これを処理するXSLTスタイルシートの例は以下のようになります.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:math="http://www.w3.org/2005/xpath-functions/math"
    xmlns:ahf="http://www.antennahouse.com/names/function"
    xmlns:saxon="http://saxon.sf.net/" 
    exclude-result-prefixes="xs math"
    version="3.0">
    
    <xsl:template match="jeans_size">
        <xsl:choose>
            <xsl:when test="data() instance of xs:positiveInteger">
                <xsl:message select="'Numeric jeans size = ' || (data() treat as xs:positiveInteger) => string()"/>
            </xsl:when>
            <xsl:when test="data() instance of xs:string">
                <xsl:message select="'Enumerated jeans size = ' || data()"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:message select="'Invalid jeans size type'"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

data()関数はSchema Awareな変換をやるときでないとめったに使用することはありません.引数に一般的にはノードを取って、それに型付けられた値を返してくれます.省略すればカレントコンテキストになります.(この場合はjeans_size要素)

これで先ほどのXMLを処理すれば

Numeric jeans size = 42

と表示されます.

でもこのように複数の型の値を取りうるデータを処理するときにいちいちXML Schemeを書かなければならないのは骨の折れる仕事です.つまりコストが高いです.もっと簡単にできないか?というのがUnion Typesの提案の趣旨です.

最新のSaxon 10.0ではEEバージョンでないとUnion Typesは使えないのですが、これより以前のバージョンならsaxon:asで表現できます.以下のスタイルシートが入力データにXML Schemeを関連付けなくとも動いてくれるフレキシブルなバージョンです.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:math="http://www.w3.org/2005/xpath-functions/math"
    xmlns:ahf="http://www.antennahouse.com/names/function"
    xmlns:saxon="http://saxon.sf.net/" 
    exclude-result-prefixes="xs math"
    version="3.0">
    
    <xsl:template match="jeans_size">
        <xsl:variable name="jeansSize" saxon:as="union(xs:positiveInteger,xs:string)" select="."/>
        <xsl:choose>
            <xsl:when test="data($jeansSize) instance of xs:positiveInteger">
                <xsl:message select="'Numeric jeans size = ' || ($jeansSize treat as xs:positiveInteger) => string()"/>
            </xsl:when>
            <xsl:when test="data($jeansSize) instance of xs:string">
                <xsl:message select="'Enumerated jeans size = ' || ($jeansSize treat as xs:string)"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:message select="'Invalid jeans_size type'"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
    
</xsl:stylesheet>

このスタイルシートだと、入力XMLファイルが

<?xml version="1.0" encoding="UTF-8"?>
<jeans_size>42</jeans_size>

のようにwell-formedだけのものであっても、ちゃんと

Numeric jeans size = 42

と出てくれます.いちいちスキーマを作る手間がないので実に簡単です.

またUnion Typesは入力データから導かれた変数ばかりに使う必要はありません.次のような例で変数をそれに結び付けて使うこともできます.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:math="http://www.w3.org/2005/xpath-functions/math"
    xmlns:ahf="http://www.antennahouse.com/names/function"
    xmlns:saxon="http://saxon.sf.net/" 
    exclude-result-prefixes="xs math"
    version="3.0">
    
    <xsl:variable name="gUnionVar" saxon:as="union(xs:date, xs:dateTime, xs:time)" select="current-date()"/>

    <xsl:template match="/">
        <xsl:choose>
            <xsl:when test="$gUnionVar instance of xs:date">
                <xsl:message select="'$gUnionVar = ' || format-date($gUnionVar, 'Today is [Y,1,4]-[M,1,2]-[D,1,2]')"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:message select="'$gUnionVar is not instance of xs:date'"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
    
</xsl:stylesheet>

この場合、ちゃんと

$gUnionVar = Today is 2020-04-07

と出てくれます.

Union TypesはSaxon 10ではEEでないと使えないのがちょっと残念ですが、これはたぶんschme-awareな変換が主な用途、つまり商用での使用が主な使い道と考えてのことなのでしょう.

XSLT3.0への道(34) "=>" と" !" と高階関数でチャチャッとやってしまう.

普段Javaなんてめったに使わないのですが、たまたまコーディングをせざるを得なくなり、同業の女房の持っている本を見たら「Javaによる関数プログラミング」というのがあったので読んでみました.
Java8になって追加されたメソッドを使い、コレクションをスマートに処理する例が載っています.例えば

final List<String> friends = Arrays.asList("Brian", "Nate", "Neal", "Raju", "Sara", "Scotto");

とあったとき、これを1行毎に列挙して出力するのを昔はfor文で

for(int i=0; i < friends.size(); i++){
  System.out.println(friends.get(i));
}

なんてベタでやっていたのを、IterableインタフェースにあるforEach()というメソッドを使って、

friends.forEach((name) -> System.out.println(name));

とするのが今のやり方.

また、大文字に変換して1行に列挙するのはStreamインタフェースに追加されたmap()メソッドで

friends.stream()
           .map(name -> name.toUppercase())
           .forEach(name -> System.out.print(name + " "));
System.out.println();

とやってしまいます.昔のfor文で回すイメージとは随分変わったものです.

ではXSLTではどうなのか?ちょっと試してみました.

まず次のような準備を整えます.ahf:system.out.printlnという関数でメッセージを出力します.XSLTではXPath式からは間接に関数を介さないと標準出力に出せないからです.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:math="http://www.w3.org/2005/xpath-functions/math"
    xmlns:ahf="http://www.antennahouse.com/names/XSLT/Functions/Document"
    exclude-result-prefixes="#all"
    version="3.0">
    
    <xsl:variable name="friends" as="xs:string+" select="('Brian','Nate','Neal','Raju','Sara','Scott')"/>
    
    <xsl:function name="ahf:system.out.println" as="empty-sequence()">
        <xsl:param name="prmStr" as="xs:string"/>
        <xsl:message select="$prmStr"/>
    </xsl:function>

    <xsl:template match="/">
        <!-- ここにfriendに対してやりたい処理を書く-->
    </xsl:template>

</xsl:stylesheet>

名前を1行毎に列挙して出力する.

これはSimple Map Operator !を使えばいとも簡単です.決してxsl:for-eachなんて使ってはいけません.

<xsl:sequence select="$friends ! ahf:system.out.println(.)"/>
[結果:oXygenのログ]
[xslt] Brian
[xslt] Nate
[xslt] Neal
[xslt] Raju
[xslt] Sara
[xslt] Scott

名前を大文字に変換して1行に列挙する.

これも"!"とArrow Operator "=>" にupper-case()を使えば簡単です.

<xsl:sequence select="$friends ! upper-case(.) => string-join(' ') => ahf:system.out.println()"/>
[結果]
[xslt] BRIAN NATE NEAL RAJU SARA SCOTT

名前の文字数をカウントして1行に出力する.

Javaでは

friends.stream()
           .map(name -> name.length())
           .forEach(name -> System.out.print(name + ""));

XSLTでは

<xsl:sequence select="$friends ! string-length(.) => string-join(' ') => ahf:system.out.println()"/>

[結果]

[xslt] 5  4  4  4  4  5 

"N"から始まる名前の数をカウントして出力する.

Javaでは、

final List<string> startsWithN = friends
        .stream()
        .filter(name -> name.startsWith("N"))
        .collect(Collections.toList());
System.out.println(String.format("Found %d names", startsWithN.size()));

XSLTではもう少し頑張って、filter()なんてifで自前で書きます.

<xsl:variable name="times" select="$friends ! (if (starts-with(.,'N')) then . else ()) 
                                => count() 
                                => string()"/>
<xsl:sequence select="('Found '|| $times ||' times') => ahf:system.out.println()"/>
[結果]
 [xslt] Found 2 times

最も長い文字数の名前を出力する.複数あったら最初のものを出す.

Javaでは、

final Option<String> aLongName =
        friends.stream()
                   .reduce((name1, name2) ->
                      name1.length() >= name2.length()? name1 : name2);
aLongname.ifPresent(name -> System.out.println(String.format("A longest name: %s", name)));

XSLTではreduceなんていうのはないので、高階関数を使えば次のように書けます.(もっとスマートな方法あったら教えてください)

<xsl:variable name="aLongName" as="xs:string?"
        select="$friends 
               => (let $getLength := function ($strings as xs:string*){for-each($strings,function ($a as xs:string) {string-length($a)})},
                           $getMaxLength := function ($strings as xs:string*, $getLength as function (xs:string*) as xs:integer*){max($getLength($strings))},
                           $maxLength := $getMaxLength(?, $getLength),
                           $maxString := function ($strings as xs:string*){$strings[. => string-length() eq $maxLength($strings)]}
                      return $maxString(?))()    
               => head()"/>
<xsl:sequence select="if ($aLongName => exists()) then ('A longest name:' || $aLongName) => ahf:system.out.println() else ()"/>
[結果]
 [xslt] A longest name:Brian

XSLT3.0では、Simple Map Operator "!"、Arrow Operator "=>"、それに高階関数を使うとXSLT2.0では考えも及ばなかったようなコーディングができるようになります.本当に知っていて損はありません.

※ もっぷさんからコメントのありましたようにXPath 3.1ではfold-left()という関数があって、以下のように一発で最も長い文字数の名前を出すことができます.

<xsl:variable as="xs:string?" name="aLongName" select=" $friends => fold-left( '', function($name1, $name2) { if (string-length($name1) ge string-length($name2)) then $name1 else $name2 } )" />
<xsl:sequence select="if ($aLongName => exists()) then ('A longest name: ' || $aLongName) => ahf:system.out.println() else ()"/>

fold-left()関数は以下に仕様があります.

https://www.w3.org/TR/xpath-functions-31/#func-fold-left

XSLT 4.0 Proposal (2) Saxon 10.0が公開されました.

XSLT 4.0のプロポーザルが出たと思ったら、もうSaxon 10.0が出てしまいました.「もう」と書いたのは、10.0は単なるバグフィクッスのためのバージョンアップでなく、XSLT 4.0のプロポーザルに沿って実験的に盛り込んだ各種の機能があることです.

sourceforge.net

Saxon 10.0 includes experimental implementations of a number of powerful new XSLT and XPath features, which Saxonica has put forward for inclusion in a new version 4.0 of the standards.

これらの機能を使用するにはコンフィグレーションファイルに、拡張機能を使う旨指定してやらねばなりません.以下がその例です.

<?xml version="1.0" encoding="UTF-8"?>
<sc:configuration xmlns:sc="http://saxon.sf.net/ns/configuration" edition="PE">
     <sc:global allowSyntaxExtensions="true"/>
     ...
</sc:configuration>

そして、SaxonをJAXPでantから起動している場合は、コンフィグレーションファイルを使う旨、指定してやる必要があります.以下のような感じになるでしょう.

<?xml version="1.0" encoding="UTF-8"?>
...
<target name="build.project.class.path">
	<path id="project.class.path">
		<athelement path="${java.class.path}"/>
		<pathelement path="D:/My_Documents/Java/SaxonPE10-0J/saxon-pe-10.0.jar"/>
	</path>
</target>

<property name="sylesheet.file" value="xsl/conv-pinyin-saxon10.xsl"/>

<target name="transform.template.common.jaxp" depends="build.project.class.path">
	<echo message="Input File=${input.file}"/>
	<echo message="Output File=${output.file}"/>
	<echo message="Stylesheet=${sylesheet.file}"/>
	<property name="classpath" refid="project.class.path"/>
	<xslt processor="trax" in="${dummy.input.file}" out="${output.file}" 
		style="${sylesheet.file}" classpathref="project.class.path" force="true">
		<factory name="net.sf.saxon.TransformerFactoryImpl">
			<attribute name="http://saxon.sf.net/feature/timing" value="true"/>
			<!-- Suppress namespace warning -->
			<attribute name="http://saxon.sf.net/feature/suppressXsltNamespaceCheck" value="true"/>
			<!--Assert-->
			<attribute name="http://saxon.sf.net/feature/enableAssertions" value="true"/>
			<!-- Trace -->
			<attribute name="http://saxon.sf.net/feature/traceListenerClass" value="net.sf.saxon.trace.XSLTTraceListener"/>
			<!-- Configuration File -->
			<attribute name="http://saxon.sf.net/feature/configuration-file" value="${basedir}${file.separator}saxon_config-10.xml"/>
			<!-- Initial Template -->
			<attribute name="http://saxon.sf.net/feature/initialTemplate" value="main"/>
		</factory>
		<param name="PRM_INPUT_FILE_URL" expression="${input.file.url}" if="input.file.url"/>
	</xslt>
</target>

オープンソース版のSaxon-HEを使用している人に朗報なのは、高階関数(High order function)やxsl:evaluate(XPath式の動的評価)も10.0からは使えるようになったことでしょう.

なにはともあれ新しい機能を紹介するのに動いてくれるSaxonがあるのは大変ありがたいものです.ちなみに前回紹介したタプルはさっそく言語仕様が変わっていて、以下のように":"でなくas=でフィールドの型を記述する必要があります.

[Saxon 9.xの書き方]

<xsl:function name="ahf:normalizePinyin" as="tuple(resultPinyin: xs:string, resultTone: xs:string)">

[Saxon 10.0の書き方]

<xsl:function name="ahf:normalizePinyin" as="tuple(resultPinyin as xs:string, resultTone as xs:string)">

XSLT 4.0 Proposal:タプル

非常に複雑な処理を行うテンプレート、関数を作っていて、戻り値として複数の値を呼び出し側に返したいと考えたことはないでしょうか?私は大いにありました.これをXSLT 2.0まででやっていた時代には次のような方法がありました.

 

  • テンポラリツリーを作って、要素/属性の組み合わせで返す.
  • as="item()*"のような戻り値の定義をしてシーケンスとして返す.

 

両方ともエレガントではありません.XSLT 1.0で必死にやろうとすれば前者.XSLT 2.0でやろうとすれば後者ではないかと思います.

 

例えば以下の関数は、要素を与えて、要素名、@id属性、ツリーの中での階層を返します.(手間なのでXSLT 3.0でやっています.)

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:math="http://www.w3.org/2005/xpath-functions/math"
xmlns:ahf="http://www.antennahouse.com/names/function"
exclude-result-prefixes="xs math"
version="3.0">

<xsl:function name="ahf:getElementInfo" as="item()*">
<xsl:param name="prmElem" as="element()"/>
<xsl:sequence select="name($prmElem), $prmElem/@id, $prmElem/ancestor-or-self::* => count()"/>
</xsl:function>

<xsl:template match="*">
<xsl:variable name="elementInfo" as="item()*" select=". => ahf:getElementInfo()"/>
<xsl:message select="'name=' || $elementInfo[1] || ' @id=' || $elementInfo[2] => string() || ' level=' || $elementInfo[3] => string()"/>
<xsl:apply-templates select="*"/>
</xsl:template>

</xsl:stylesheet>

これで十分動いてくれるのですが、呼び出し側と関数との間には暗黙に戻り値のシーケンスの1番目は要素名(xs:string)、2番目は@id属性(attribute())、3番目はその要素の階層レベル(xs:integer)でやり取りするという約束事を作っておく必要があります.

 

このスタイルシートに次のような入力を与えると

<?xml version="1.0" encoding="UTF-8"?>
<doc id="id0001">
<chapter id="id0002">
<section id="id0003">
<p id="id0004">A paragraph</p>
</section>
</chapter>
</doc>

結果は

 

name=doc @id=id0001 level=1

name=chapter @id=id0002 level=2

name=section @id=id0003 level=3

name=p @id=id0004 level=4

 

と表示してくれます.しかし戻り値を返すのはもっとスマートにやりたいものです.これを実現してくれるのがタプル(tuple)です.

 

A Proposal for XSLT 4.0では、このタプルを新しい型として追加する提案がなされています.実はSaxonはすでに独自実装でタプルを提供しています.タプルを使えば、[n]のような参照の仕方でなく、タプルのフィールド名で参照することができます.(タプル型の変数?フィールド名)

 

次のようになるでしょう.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:math="http://www.w3.org/2005/xpath-functions/math"
xmlns:ahf="http://www.antennahouse.com/names/function"
xmlns:saxon="http://saxon.sf.net/"
exclude-result-prefixes="xs math"
version="3.0">

<xsl:function name="ahf:getElementInfo" saxon:as="tuple(name: xs:string, id: attribute()?, level: xs:integer)">
<xsl:param name="prmElem" as="element()"/>
<xsl:sequence select="map{'name': name($prmElem), 'id': $prmElem/@id, 'level': $prmElem/ancestor-or-self::* => count()}"/>
</xsl:function>

<xsl:template match="*">
<xsl:variable name="elementInfo" as="item()*" select="ahf:getElementInfo(.)"/>
<xsl:message select="'name=' || $elementInfo?name || ' @id=' || $elementInfo?id => string() || ' level=' || $elementInfo?level => string()"/>
<xsl:apply-templates select="*"/>
</xsl:template>

</xsl:stylesheet>
 

ここで注目される点はタプルの実体は実はマップ(map)であるということです.ですのでKay博士はProposalの中で次のように述べています.

Note that I'm not introducing tuples as a new kind of object here. The values

are still maps, and the set of operations that apply to tuples are exactly the same as the operations that apply to maps. I'm only introducing a new way of describing and constraining the type.

今作っているスタイルシートgrepしてみたら、すでに20か所以上でタプルを使っていました.実績は十分というよりもはやタプルはXSLTスタイルシート開発に必須の型に思えます.

XSLT 4.0 Proposal

私は次のXSLT勧告は3.1になるんじゃないか?と思っていました.でもXSLT 3.0EditorMichael Kayが考えているのはなんとXSLT 4.0でした.

先月2月13日~15日に開かれたXMLプラハXMLPrague)でのMichael Kayは「A Proposal for XSLT 4.0」題して講演を行いました.

 

https://www.xmlprague.cz/day2-2020/#xslt4

 

スライドは以下から見られます

https://archive.xmlprague.cz/2020/files/presentations/xslt4.pdf

 

YouTubeのビデオは以下のURLの7時間43分くらいから見ることができます.

https://www.youtube.com/watch?v=pBDBRTyjArk&t=22381s

 

その内容は以下のSaxonicaのサイトからPDFで落として見られます.

http://www.saxonica.com/papers/xmlprague-2020mhk.pdf

 

XSLT 3.0は

 

  • 大規模データ処理のためのストリーミングのサポート
  • map, array, JSONなどのサポート
  • 高階関数

 

などXSLT 2.0に比べると凄い機能アップがされて、今でもその機能はとてもじゃないですが全部は理解しきれないし使いこなせません.でも時代はXSLT 4.0に向かってどんどん進んでゆくようです.このPDFは25ページに及ぶ内容ですが、勉強がてらに紹介してゆきたいと思います.