🕶️

XMLの親の属性を子に移動する(XSLTで)

2022/07/28に公開
4

https://zenn.dev/k16/articles/cd2d9bfb84ac19

ふつうはどうやるのが一般的なんだろう。やはりXSLTかな。

ということでね、一般的な手段であるXSLTでやってみましょう。

元記事の課題は「table要素のid属性を、子のcaptionに移す」ですね。こんな簡単な課題、さぞ簡潔な処理になるに違いない

課題を達成するスタイルシート

<xsl:transform
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="3.0"
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:fn="http://www.w3.org/2005/xpath-functions"
  exclude-result-prefixes="fn">
  <xsl:output method="xhtml" html-version="5.0" />
  <xsl:mode on-no-match="shallow-copy"/>
 
  <xsl:template 
    match="table[fn:exists(@id)][
      fn:exists(
        child::caption[fn:not(fn:exists(@id))]
       )
      ]">
    <xsl:copy>
      <!--2022-07-28 修正 xsl:copy-of
      select="attribute::*[fn:not(fn:local-name() = 'id')]"/-->
       <xsl:copy-of
          select="attribute::* except @id"/>
      <xsl:apply-templates >
        <xsl:with-param name="tableId" 
        select="./@id"/>
      </xsl:apply-templates>
    </xsl:copy>
  </xsl:template>
  
  <xsl:template
      match="table[fn:exists(@id)]/caption[fn:not(fn:exists(@id))]" >
      <xsl:param name="tableId" as="attribute()"/>
      <xsl:copy>
        <xsl:copy-of select="attribute::*"/>
        <xsl:attribute name="id" select="$tableId"/>
        <xsl:apply-templates/>
      </xsl:copy>
    </xsl:template>
  </xsl:transform>

30行以上ある……どうして……。

スタイルシートの説明

<xsl:output>について。XSLT 3.0ではhtml5出力ができるというのは何度か別記事でも言及していますが、HTML5のXHTML記法も通っている、ようにみえますね。Saxon-HEで試しています。

<xsl:mode>。今回XSLT 3.0な理由の一番はこれ。@on-no-matchで、スタイルシート中でマッチしないノードに対する挙動をshallow-copy、つまりコピーして次のノード処理に移っています。2.0までは@match="*"のンプレートを書いたりがあったので結構大事な進化です。

テンプレート部分。一見複雑そうにも見えますが、起こりそうなパターンの大体に対応した上でのパターン指定です。<xsl:copy>は、例えば複数パターンの要素にマッチさせて、それぞれ要素名を保った処理をするときに重宝します。つまり今回はただのお洒落。<xsl:copy-of>はdeep-copyな処理ですのでこれはこれでattribute以外処理させるのは怖いやつです。

<table>に対しては、「id属性を持ち」「子にid属性の無い<caption>がいる」パターンでid属性以外の残りの属性コピーを行い、子の処理に対しパラメータとして自身の持っていたidの値を渡しています。

<caption>も、「id属性を持った<table>の子」で「自身はid属性を持たない」パターンです。先程の<table>パターンも「子の<caption>id属性が無い」パターンに限定したものだったので、id属性のロストは多分起こらない、はず。パラメータとしてtableIdを受けとるので、これを自身のidとして挿入します。<xsl:apply-templates/>にパラメータ渡せるのはXSLT 2.0からだったか。

Discussion

AQAQ
  <xsl:copy-of
  select="attribute::*[fn:not(fn:local-name() = 'id')]"/>

これだと、たとえば次のような table が来たときに

<table id="1" foo:id="2" xmlns:foo="bar">

@foo:id が消失するので、素直に except したほうが、より安全でもあるかなと思います:

      <xsl:copy-of
      select="attribute::* except @id"/>
hidarumahidaruma

確かに複数の名前空間でidを別で付けかねないXMLには憶えがあります。記事を修正します。

AQAQ

ありがとうございます。

あと、

<xsl:mode default-mode="yes" on-no-match="shallow-copy"/>

この @default-modeboolean ではないはずなので、何かの書き間違いではないかと思いました。(実害はありませんが)

hidarumahidaruma

ありがとうございます。修正しました。

xsl:modeは書きなれていとはいえ、そもそもどうしてここで@default-modeを書こうとしたのか……。