概要

XSL Transformations (XSLT) Version 2.0関連のメモ書きです。

XSL Transformations (XSLT) Version 2.0 とは

関数など

fn:doc-available()

@echo off
setlocal

set SRC=C:/tmp/aaa/word/document.xml
set DST=C:/output/aaa/xxx.xml
set LIB=C:/lib/saxon-9.1.0.8.jar
set SAXON=net.sf.saxon.Transform
set XSLT=C:/xslt/docx2xxx.xsl

"%JAVA_HOME%\bin\java" -cp "%LIB%" %SAXON% -s:"%SRC%" -xsl:"%XSLT%" -o:"%DST%"
<!-- C:/tmp/aaa/word/numbering.xml が存在するかをチェックしたい -->
<xsl:if test="fn:doc-available(fn:replace(fn:base-uri(),'document\.xml$','numbering.xml'))">
  <xsl:variable name="numbering" select="fn:document('numbering.xml',/)/w:numbering" />
</xsl:if>
  • fn:document('numbering.xml',/)のように第二引数を指定して入力ファイル相対パスでfn:doc-availableを使用することは出来ない
  • fn:base-uri() -> C:/tmp/aaa/word/document.xml
  • fn:resolve-uri('') -> C:/xslt/docx2xxx.xsl

xsl:analyze-string

docxw:lvlTextのフォーマット(例えば%1.%2.%3)に従って、実際の段落番号文字列(例えば、1.5.10)に変換する場合、xsl:analyze-stringが便利。

<!-- w:lvlRestartの処理などは省略 -->
<xsl:variable name="numberText">
  <xsl:analyze-string select="$lvl/w:lvlText/@w:val" regex="%\d">
    <xsl:matching-substring>
      <xsl:variable name="pos" select="number(replace(.,'%',''))" />
      <xsl:variable name="i" select="$pos - 1" />
      <xsl:variable name="ii" select="$i - 1" />
      <xsl:variable name="parentList" select="$list[(w:pPr/w:numPr/w:ilvl and w:pPr/w:numPr/w:ilvl/@w:val=$ii) or ($ii=0 and not(w:pPr/w:numPr/w:ilvl))]" />
      <xsl:variable name="pstl" select="if(empty($parentList)) then '' else $parentList[1]/w:pPr/w:pStyle/@w:val" />
      <xsl:variable name="next" select="if(empty($psList) or empty($pstl)) then () else index-of($psList,$pstl)" />
      <xsl:variable name="list2" select="if(empty($next)) then $list else subsequence($list,$next[last()])" />
      <xsl:value-of select="fn:count($list2[(w:pPr/w:numPr/w:ilvl and w:pPr/w:numPr/w:ilvl/@w:val=$i) or ($i=0 and w:pPr/w:numPr and not(w:pPr/w:numPr/w:ilvl))])" />
    </xsl:matching-substring>
    <xsl:non-matching-substring>
      <xsl:value-of select="." />
    </xsl:non-matching-substring>
  </xsl:analyze-string>
</xsl:variable>
  • Excelの日付書式(YYYY/MM/DD)をXSLT 2.0の日付書式([Y0001]/[M01]/[D01])に変換
    • 条件付き書式や\エスケープなどには未対応
<!-- 関数: Excelの日付書式をXSLTの日付書式に変換 -->
<xsl:function as="xs:string" name="kjxf:convertDateFormat">
  <xsl:param name="formatCode" />
  <xsl:variable name="ret">
    <xsl:analyze-string select="$formatCode" regex="([Y|y]+)|([M|m]+)|([D|d])+">
      <xsl:matching-substring>
        <xsl:variable name="len" select="fn:string-length(.)" />
        <xsl:choose>
          <xsl:when test="regex-group(1)"><!-- Y -->
            <xsl:value-of select="'[Y',substring('0000',1,$len - 1),'1]'" separator="" />
          </xsl:when>
          <xsl:when test="regex-group(2)"><!-- M -->
            <xsl:choose>
              <xsl:when test="$len = 5">
                <xsl:value-of select="'[MN,*-1]'" />
              </xsl:when>
              <xsl:when test="$len = 4">
                <xsl:value-of select="'[MNn]'" />
              </xsl:when>
              <xsl:when test="$len = 3">
                <xsl:value-of select="'[MNn,*-3]'" />
              </xsl:when>
              <xsl:when test="$len = 2">
                <xsl:value-of select="'[M01]'" />
              </xsl:when>
              <xsl:when test="$len = 1">
                <xsl:value-of select="'[M]'" />
              </xsl:when>
            </xsl:choose>
          </xsl:when>
          <xsl:when test="regex-group(3)"><!-- D -->
            <xsl:choose>
              <xsl:when test="$len = 4">
                <xsl:value-of select="'[FNn]'" />
              </xsl:when>
              <xsl:when test="$len = 3">
                <xsl:value-of select="'[FNn,*-3]'" />
              </xsl:when>
              <xsl:when test="$len = 2">
                <xsl:value-of select="'[D01]'" />
              </xsl:when>
              <xsl:when test="$len = 1">
                <xsl:value-of select="'[D]'" />
              </xsl:when>
            </xsl:choose>
          </xsl:when>
        </xsl:choose>
      </xsl:matching-substring>
      <xsl:non-matching-substring>
        <xsl:value-of select="." />
      </xsl:non-matching-substring>
    </xsl:analyze-string>
  </xsl:variable>
  <xsl:value-of select="$ret" />
</xsl:function>

to演算子

<xsl:for-each select="1 to $gridBefore">のように、指定した回数でのループが可能で、以下の例ではw:trPr/w:gridBeforeで指定された数だけ空のtdを作成している。

  <xsl:template match="w:tr">
    <tr>
      <xsl:variable name="gridBefore" as="xs:integer" select="if(w:trPr/w:gridBefore) then w:trPr/w:gridBefore/@w:val else 0" />
      <xsl:if test="$gridBefore &gt; 0">
        <xsl:for-each select="1 to $gridBefore">
          <td></td>
        </xsl:for-each>
      </xsl:if>
      <xsl:apply-templates select="w:tc[not(w:tcPr/w:vMerge) or w:tcPr/w:vMerge/@w:val='restart']" />
    <tr>
  </xsl:template>
<xsl:variable name="aaa" select="for $i in (1 to 9) return $i" />
<xsl:variable name="bbb" as="xs:integer*" select="(1,2,3,4,5,6,7,8,9)" />
<xsl:value-of select="deep-equal($aaa,$bbb)" /> <!-- true -->
  • <xsl:value-of select="for $i in (0 to 2) return $i" />は、(0,1,2)
  • <xsl:value-of select="for $i in (3 to 1) return $i" />は、()
  • <xsl:value-of select="for $i in reverse(1 to 3) return $i" />は、(3,2,1)
  • <xsl:value-of select="for $i in (1 to 1) return $i" />は、(1)
  • <xsl:value-of select="for $i in (-1 to 1) return $i" />は、(-1,0,1)
  • <xsl:value-of select="for $i in (1, 3 to 5, 7) return $i" />は、(1,3,4,5,7)

for~in~return 式

for~in~return式は、二重ループなども可能。

<xsl:function as="xs:string*" name="myfn:get-merge-cells">
  <xsl:param name="ref" as="xs:string" />
  <xsl:variable name="a" as="xs:integer*">
    <xsl:analyze-string select="$ref" regex="([A-Z]+)([0-9]+):([A-Z]+)([0-9]+)">
      <xsl:matching-substring>
        <xsl:sequence select="myfn:column-index(regex-group(1))" />
        <xsl:sequence select="xs:integer(regex-group(2))" />
        <xsl:sequence select="myfn:column-index(regex-group(3))" />
        <xsl:sequence select="xs:integer(regex-group(4))" />
      </xsl:matching-substring>
    </xsl:analyze-string>
  </xsl:variable>
  <!-- xsl:sequence select="remove(for $ri in ($a[2] to $a[4]) return for $ci in ($a[1] to $a[3]) return concat(myfn:column-label($ci),$ri), 1)" / -->
  <xsl:sequence select="remove(for $ri in ($a[2] to $a[4]), $ci in ($a[1] to $a[3]) return concat(myfn:column-label($ci),$ri), 1)" />
</xsl:function>
  • myfn:get-merge-cells('A18:D20')は、(B18, C18, D18, A19, B19, C19, D19, A20, B20, C20, D20)

fn:codepoints-to-string, fn:string-to-codepoints

  • Excelなどの列名(アルファベット26進数bijective base-26の文字列)を10進数の列番号に変換する
    • <xsl:value-of select="myfn:column-index('AA')" />は、27
    • <xsl:value-of select="myfn:column-index('XFD')" />は、16384
<!-- 関数: アルファベット26進数(bijective base-26)の文字列を10進数のxs:integerに変換 -->
<!-- べき乗が計算できない?ので、あらかじめ用意する。最大列はXFD(16384)なので、26^3(17576)は不要 -->
<xsl:variable name="power" as="xs:integer+" select="(1, 26, 676)" />
<!-- xsl:variable name="AtoZ" as="xs:string+" select="('A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z')" / -->
<xsl:variable name="AtoZ" as="xs:string+" select="for $i in (0 to 26 - 1) return codepoints-to-string(string-to-codepoints('A') + $i)" />

<xsl:function as="xs:integer" name="myfn:column-index">
  <xsl:param name="str" as="xs:string" />
  <xsl:variable name="len" select="fn:string-length($str)" /> <!-- 最大3桁 -->
  <xsl:value-of select="sum(for $i in (1 to $len) return index-of($AtoZ, substring($str, $i, 1)) * $power[$len - $i + 1])" />
</xsl:function>
  • 列番号の数値を列名に変換
    • <xsl:value-of select="myfn:column-label(702)" />は、ZZ
    • <xsl:value-of select="myfn:column-label(703)" />は、AAA
  <xsl:function as="xs:string" name="myfn:column-label">
    <xsl:param name="num" as="xs:integer" />
    <xsl:variable name="cpa" as="xs:integer" select="string-to-codepoints('A')" />
    <xsl:variable name="dv3" as="xs:integer" select="($num - 26 - 1) idiv 676" />
    <xsl:variable name="st3" as="xs:string"  select="if($dv3 != 0) then codepoints-to-string($cpa + $dv3 - 1) else ''" />
    <xsl:variable name="md3" as="xs:integer" select="$num - ($dv3 * 676)" />
    <xsl:variable name="dv2" as="xs:integer" select="($md3 - 1) idiv 26" />
    <xsl:variable name="st2" as="xs:string"  select="if($dv2 != 0) then codepoints-to-string($cpa + $dv2 - 1) else ''" />
    <xsl:variable name="md2" as="xs:integer" select="$md3 - ($dv2 * 26)" />
    <xsl:variable name="st1" as="xs:string"  select="codepoints-to-string($cpa + $md2 - 1)" />
    <xsl:value-of select="concat($st3,$st2,$st1)" />
  </xsl:function>

fn:format-date関数、xs:dayTimeDurationデータタイプ

  • Excelの日付のシリアル値を指定された書式で日付に変換する
    • シリアル値は、1900-01-00からの日数
    • 1900-01-01: 1
    • 1900-02-29: 60(うるう年計算のバグ、Leap year bug)
    • 2016-07-27: 42577
  • xs:date('1899-12-31') + (シリアル値 - 1) * xs:dayTimeDuration('P1D')
  • Excelの日付書式(例:YYYY/MM/DD)をfn:format-date関数に与える書式(例:[Y0001]/[M01]/[D01])に変換する必要がある(以下のサンプルテンプレートでは省略)
<xsl:variable name="styleSheetNumFmts" select="document('styles.xml',/)/styleSheet/numFmts/numFmt" />
<xsl:template match="c">
  <xsl:variable name="xf" select="..." />
  <xsl:variable name="numFmt" select="$styleSheetNumFmts[@numFmtId = $xf/@numFmtId]" />
  <xsl:variable name="formatCode" select="if(fn:exists($numFmt)) then fn:replace(fn:tokenize($numFmt/@formatCode,';')[1], '\[.+\]|_\)|\\(.)', '$1') else ''" />
  <td>
    <xsl:value-of select="fn:format-date(xs:date('1900-01-01') + xs:integer(v - 2) * xs:dayTimeDuration('P1D'), $formatCode)" />
  </td>
</xsl:template>

fn:tokenize関数

  • 文字列を分割
<xsl:value-of select="fn:tokenize('aaa;bbb;ccc', ';')[1]" /> <!-- aaa -->
<xsl:value-of select="fn:tokenize('aaa&quot;bbb&quot;;ccc', ';')[1]" /> <!-- aaa&#34;bbb&#34; -->
<xsl:value-of select="fn:tokenize('aaa&#34;', ';')[1]" /> <!-- aaa&#34; -->

separator属性

  • xsl:value-of要素で、separator属性が使用できる
  • separator=""で区切り文字なしで連結(fn:concatの代わりになる)
<xsl:variable name="hex" select="'0123456789ABCDEF'" />
<xsl:function as="xs:string?" name="myfn:decimalToHex">
  <xsl:param as="xs:integer" name="dec" />
  <xsl:if test="$dec gt 0">
    <xsl:value-of select="myfn:decimalToHex($dec idiv 16), substring($hex, ($dec mod 16) + 1, 1)" separator="" />
  </xsl:if>
</xsl:function>

シーケンスに要素が含まれているかをチェックする

  • XSLT 1.0では一旦文字列にしてcontains関数などを使用する必要があったが、XSLT 2.0では、=でシーケンス内に要素が存在するかをチェックできる
<!-- 1~3番目のシートのみ処理する -->
<xsl:variable name="output" as="xs:integer+" select="1 to 3" />
<xsl:for-each select="sheets/sheet[position() = $output]">
  <xsl:variable name="num" as="xs:integer" select="position()" />
  <xsl:variable name="sheet" as="xs:string" select="concat('worksheets/sheet',$num,'.xml')" />
  <xsl:variable name="worksheet" select="document($sheet,/)/worksheet" />
  ...

xsl:for-each-group

Office Open XML

DrawingML

English Metric Units

1in = 914400emu = 127 * 3 * 2400emu
1pt = 12700emu = 127 * 100emu
1mm = 36000emu
1級 = 9000emu

コメント