XSLT3.0への道(33) 楽しいXPath 3.1

よくDITAの導入時にお客様と議論することがあります.

「どの要素・属性を使用されるのですか?それがわからないとプラグインスタイルシートなんて書けません!」

でも、必死にExcelでDITA 1.3の要素/属性一覧を作ってお客様に記入してもらっても、結果は白黒つけがたいものになります.何故かというと、DITA 1.3では要素数が多すぎて、「Excelのシートに使用の有無を記入する」==「DITA 1.3の要素/属性をすべて理解する」になってしまうからです.これからDITAを導入するお客様に求めるのはそもそも無理があります.

そんなときに役に立ってくれるスタイルシートを作ってみました.お客様が最初のトライでDITAでオーサリングをしてくれて、一定程度コンテンツが作れるようになったときです.このマップとトピックをいただきます.そしてDITA-OTのPDFのプラグインを通して、そのテンポラリフォルダを残すようにし、[マップ名]_MERGED.xmlという「マージ後中間ファイル」をスタイルシートの入力とします.

実際のコードは次の様なものになります.これで使用しているDITAの要素のドメインでグルーピングして要素名をソーティングしてテキストファイルに落とせます.

<?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="xs math"
    version="3.0">
    
    <xsl:output method="text" encoding="UTF-8" byte-order-mark="no"/>
    
    <xsl:template match="/">
        <xsl:variable name="domainAndElementNames" as="xs:string+" select="descendant::* ! ahf:getDomainAndElementName(.)"/>
        <xsl:variable name="uniqueDomainAndElementNames" as="xs:string+" select="distinct-values($domainAndElementNames)"/>
        <xsl:variable name="uniqueDomainAndElementNamesSorted" as="xs:string+">
            <xsl:perform-sort select="$uniqueDomainAndElementNames">
                <xsl:sort select="." case-order="lower-first" order="ascending" data-type="text"/>
            </xsl:perform-sort>
        </xsl:variable>
        <xsl:for-each select="$uniqueDomainAndElementNamesSorted">
            <xsl:value-of select="."/>
            <xsl:value-of select="'
'"/>
        </xsl:for-each>
    </xsl:template>
    
    <!-- 
     function:  get domain name and element name
     param:     prmElem
     return:	[domain-name]-[element-name]
     note:      
     -->
    <xsl:function name="ahf:getDomainAndElementName" as="xs:string">
        <xsl:param name="prmElem" as="element()"/>
        <xsl:variable name="class" as="xs:string" select="$prmElem/@class => string() => normalize-space()"/>
        <xsl:variable name="domains" as="xs:string*" select="tokenize($class,'[\s]+')"/>
        <xsl:variable name="domain" as="xs:string" select="$domains[last()] => substring-before('/')"/>
        <xsl:variable name="name" as="xs:string" select="$prmElem => name()"/>
        <xsl:sequence select="'[' || $domain || ']-[' || $name || ']'"/>
    </xsl:function>
    
</xsl:stylesheet>

出てくるテキストファイルはこんな感じになります.

[]-[dita-merge]
[bookmap]-[backmatter]
[bookmap]-[booklists]
[bookmap]-[bookmap]
[bookmap]-[frontmatter]
[bookmap]-[indexlist]
[bookmap]-[toc]
[bookmap]-[topicref]
[hi-d]-[sup]
[map]-[linktext]
[map]-[topicmeta]
[map]-[topicref]
[topic]-[body]
[topic]-[colspec]
[topic]-[dd]
[topic]-[desc]
[topic]-[div]
[topic]-[dl]
[topic]-[dlentry]
[topic]-[dt]
[topic]-[entry]
[topic]-[fig]
[topic]-[fn]
[topic]-[image]
[topic]-[indexterm]
[topic]-[keywords]
[topic]-[li]
[topic]-[link]
[topic]-[linkpool]
[topic]-[linktext]
[topic]-[metadata]
[topic]-[navtitle]
[topic]-[note]
[topic]-[ol]
[topic]-[p]
[topic]-[ph]
[topic]-[prolog]
[topic]-[related-links]
[topic]-[row]
[topic]-[section]
[topic]-[table]
[topic]-[tbody]
[topic]-[tgroup]
[topic]-[thead]
[topic]-[title]
[topic]-[titlealts]
[topic]-[topic]
[topic]-[ul]
[topic]-[xref]

こうすればお客様がどのような要素を使っているかが簡単にわかります.

このスタイルシートXSLT 3.0で書いています.XSLT 3.0に対応するXPathは3.1なんですが、XPath 2.0になかった特徴が結構あり便利です.簡単に紹介します.

パイプ演算子

最初は=>のパイプ演算子です.左辺のシーケンスをひとまとめにして、右辺の関数の第一引数に渡してくれます.例えばselect="$prmElem/@class => string() => normalize-space()"と書けば、@classstring()に渡って文字列となり、normalize-space()に渡って冗長なホワイトスペースが削除されます.左から右へ流れるように書けるのでとても自然です.日本語のように最後に結論を持ってくる書き方に合っています.

XPath 3.1では本当はパイプ演算子ではなくて「Arrow operator」として定義されています

3.16 Arrow operator (=>)

このような演算子関数型言語で良く使用例がありますよね.F#とかScalaとか.使いだすと病みつきになること請け合いです.

マップ演算子

次はちょっと奇抜な書き方の!のマップ演算子です.左辺のシーケンス一つ一つに対して、それがカレントコンテキストになって、右辺のXPath式が適用されます.なので<xsl:variable name="domainAndElementNames" as="xs:string+" select="descendant::* ! ahf:getDomainAndElementName(.)"/> と書けば、子孫のすべての要素に対してそれがカレントコンテキストになってahf:getDomainAndElementName(.)という関数の結果が返されます.

ここで間違って<xsl:variable name="domainAndElementNames" as="xs:string+" select="descendant::* >= ahf:getDomainAndElementName()"/>と書くと一発でこけます.ahf:getDomainAndElementNameelement()つまり一個の要素を引数に取ります.ところが>=と書くと、すべての子孫要素のシーケンスがまとめて引数に送られてしまうからです.

3.15 Simple map operator (!)

文字列の連結演算子

最後は文字列の連結演算子||です.select="'[' || $domain || ']-[' || $name || ']'" と書けば、concat('[', $domain, ']-[', $name, ']')と書くのと同じです.でも||があることを知ってしまえば、まずconcat()は二度と使わなくなるでしょう.||の方が簡単だからです.また<xsl:message>デバッグログを出すときも、いちいちconcat()を書かずに済みますので重宝します.

3.6 String Concatenation Expressions

XSLT 3.0というととかくXSLTの仕様の方に目が行きがちですが、XPath 3.1も見ておいて損はありません.と言いますか特にパイプ演算子なんかは、コーディングスタイルがXSLT 2.0の頃とは全然変わるようになります.