Jintrick.netagenda2003年10月アーカイブ → 2003年10月11日

RELAX NGスキーマ文書をフラット版に変換してみる

xhtml-strict.rng等をみると、このスキーマ文書は「hub」になっており、様々な別文書から構成されています。Jing等で実際に検証する際に時間がかかるので、これを一つに纏めるスタイルシートを書いてみました。

最大の問題点

当初、include要素のhref属性で参照されるスキーマ文書の「中身」をそのまま書き出せば終わりかと思いました。しかし、include要素には子要素を置く事ができ、その子要素によってインクルード側の要素(同じname属性を持ったdefine要素とstart要素)が上書きされるという仕様になっています。これに悩まされました。

コンポーネントのノード集合を得る

上書きする側の要素をコンポーネントと呼ぶことにします。具体的にはinclude要素の子孫であるdefine要素とstart要素です。まず、このコンポーネント全てを含むノード集合を変数「component.nodeset」に入れることから始めました。

<xsl:template match="rng:include">
<xsl:variable name="components.nodeset" select="
  descendant::rng:*
  [boolean(self::rng:define) or boolean(self::rng:start)]
" />
</xsl:template>

include要素はdiv要素を子供に持つことが出来ます。このdiv要素は定義のグループ化に使用されるだけで、コンポーネントは、このdiv要素を、というか、divタグを全て取っ払ったときの、include要素直下の子要素であるということになるのです。

include要素は div要素に置き換え

include要素には、ns属性等をつけることが出来ます。これは場合によってはインクルードした要素に適用される為、インクルードした要素達に、親要素を一つ設けなくてはなりません。そのため、div要素で置き換え、include要素の属性をコピーしてみました。div要素はスキーマの定義のある集合を再利用する為のグループ化をしたり、ns属性等を子孫へ継承させる為の要素で、ある程度自由に使用できます。

<xsl:template match="rng:include">
<xsl:variable name="components.nodeset" select="
  descendant::rng:*
  [boolean(self::rng:define) or boolean(self::rng:start)]
" />

<div>
  <xsl:apply-templates select="@*" />
</div>

</xsl:template>

@* にはinclude要素のhref属性も含まれてしまいますが、これは削除します。この属性は外部参照のためのもので、フラットにした後のスキーマ文書には全く不要ですから、次のようなテンプレートを予め定義してあります。

<xsl:template match="@href" />

include要素の子孫ノードを書き出し

ここでinclude要素の子孫ノードをそのまま書き出します。但し、変数component.nodesetに収めたコンポーネントはここでは使いません。これは、外部からインクルードした要素との比較にのみ使用します。

<xsl:template match="rng:include">

<xsl:variable name="components.nodeset" select="
  descendant::rng:*
  [boolean(self::rng:define) or boolean(self::rng:start)]
" />

<div>
  <xsl:apply-templates select="@*" />
  <xsl:apply-tempaltes select="child::node()" />
</div>

</xsl:template>

インクルードする外部のgrammar要素を参照

次にインクルードする外部のgrammar要素を参照します。条件分岐等で何度も参照することになるので、変数grammar.elementに入れておきます。

<xsl:template match="rng:include">
<xsl:variable name="grammar.element" select="document(string(@href), /)/child::rng:grammar" />
<xsl:variable name="components.nodeset" select="
  descendant::rng:*
  [boolean(self::rng:define) or boolean(self::rng:start)]
" />

<div>
  <xsl:apply-templates select="@*" />
  <xsl:apply-tempaltes select="child::node()" />
</div>

</xsl:template>

外部のgrammar要素をdiv要素に置き換え

このgrammar要素にもやはりns属性等をつけることが出来、それは子孫の要素に継承される場合があります。そのため、include要素同様にdiv要素で置き換えて、属性をコピーします。

<xsl:template match="rng:include">
<xsl:variable name="grammar.element" select="document(string(@href), /)/child::rng:grammar" />
<xsl:variable name="components.nodeset" select="
  descendant::rng:*
  [boolean(self::rng:define) or boolean(self::rng:start)]
" />

<div>
  <xsl:apply-templates select="@*" />
  <xsl:apply-tempaltes select="child::node()" />
  <div>
    <xsl:apply-templates select="@*" />
  </div>
</div>

</xsl:template>

grammar要素の子孫を書き出す

いよいよ、外部のgrammar要素の子孫を書き出す部分です。コンポーネントと重複する要素(同じname属性を持つdefine要素と、start要素)は、書き出さずにパスします。

<xsl:template match="rng:include">
<xsl:variable name="grammar.element" select="document(string(@href), /)/child::rng:grammar" />
<xsl:variable name="components.nodeset" select="
  descendant::rng:*
  [boolean(self::rng:define) or boolean(self::rng:start)]
" />

<div>
  <xsl:apply-templates select="@*" />
  <xsl:apply-tempaltes select="child::node()" />
  <div>
    <xsl:apply-templates select="@*" />
    <xsl:for-each select="$grammar.element/child::node()">
      <xsl:choose>
        <xsl:when test="boolean(self::rng:define)"> <!-- 1 -->
          <xsl:if test="not($components.nodeset/self::rng:define[@name = current()/@name])">
            <xsl:apply-templates select="self::node()" />
          </xsl:if>
        </xsl:when>
        <xsl:when test="boolean(self::rng:start)">  <!-- 2 -->
          <xsl:if test="not($component.nodeset/self::rng:start)">
            <xsl:apply-templates select="self::node()" />
          </xsl:if>
        </xsl:when>
        <xsl:otherwise>                             <!-- 3 -->
          <xsl:apply-templates select="self::node()" />
        </xsl:otherwise>
      </xsl:choose>
    </xsl:for-each>
  </div>
</div>

</xsl:template>

まず、外部のgrammar要素の子ノード達($grammar.element/child::node())をカレントノードリストにして、それぞれのノードについて処理を行います。

1. 自分がdefine要素である場合(boolean(self::rng:define))、コンポーネントの中に自分と同じname属性を持つdefine要素が無かったなら(not($components.nodeset/self::rng:define[@name = current()/@name]))、そのままコピーします。あったなら、何もしません。<xsl:apply-templates select="self::node" />は、コピー用のテンプレートが適用されます。

2. 自分がstart要素である場合(boolean(self::rng:start))、コンポーネントの中にstart要素が無かったなら(not($components.nodeset/self::rng:start))やはりそのままコピーします。あったなら、何もしません。

3. それ以外のノードであるなら、それぞれに定義されたテンプレートを適用します。(<xsl:apply-templates select="self::node" />

終わり?

これでほぼ終了ですが、後は細かい調整になので面白くない話です(少なくとも私にとって)。

その他の調整

div要素によるグループ化に対応

条件分岐が分かりやすいのでxsl:for-each要素を使用しましたが、これでうまく行くのは外部のスキーマ文書がgrammar要素をルート要素としてその子供がフラットに並んでいるような場合だけです。例:

<grammar>
  <start />
  <define name="foo" />
  <define name="bar" />
</gramar>

しかし、次のようにdiv要素でグループ化されているかもしれません。

<grammar>
  <start />
  <div>
    <define name="foo" />
    <div>
      <define name="bar" />
    </div>
  </div>
</grammar>

このような不確定な構造に対応することはXSLTの得意とするところで、条件分岐の各項目をテンプレート化してしまえば良いだけです。まず、xsl:for-eachにて、テンプレート内でカレントノードリストを変更していた部分を、xsl:apply-templatesで行うように変更を加えます。内部のノード用のテンプレートと重複しないよう、mode属性を付けました。

<xsl:template match="rng:include">
<xsl:variable name="grammar.element" select="
  document(string(@href), /)/child::rng:grammar
" />
<xsl:variable name="components.nodeset" select="
  descendant::rng:*
  [boolean(self::rng:define) or boolean(self::rng:start)]
" />

<div>
  <xsl:apply-templates select="@*" />
  <xsl:apply-tempaltes select="child::node()" />
  <div>
    <xsl:apply-templates select="@*" />
    <xsl:apply-templates select="$grammar.element/child::node()" mode="exclusive">
      <xsl:with-param name="components.nodeset" select="$components.nodeset"
    </xsl:apply-tempalates>
  </div>
</div>

</xsl:template>

重複を確認する為に使用するコンポーネントをパラメータとして渡します。

次に、define要素、start要素、div要素、そしてそれ以外のノード用のテンプレートを同じmode属性にして作成します。コンポーネントとの比較は、xsl:for-each要素で回した時と全く同じ要領で行っています。

<xsl:template match="rng:start" mode="exclusive">
  <xsl:param name="components.nodeset" />
  <xsl:if test="
    not(
      $components.nodeset/self::rng:start
    )
  ">
    <xsl:apply-templates select="self::node()" />
  </xsl:if>
</xsl:template>

<xsl:template match="rng:define" mode="exclusive">
  <xsl:param name="components.nodeset" />
  <xsl:if test="
    not(
      $components.nodeset/self::rng:define
      [string(@name) = string(current()/@name)]
    )
  ">
    <xsl:apply-templates select="self::node()" />
  </xsl:if>
</xsl:template>

<xsl:template match="rng:div" mode="exclusive">
  <xsl:param name="components.nodeset" />
  <div>
    <xsl:apply-templates select="@*" />
    <xsl:apply-templates select="child::node()" mode="exclusive">
      <xsl:with-param name="components.nodeset" select="$components.nodeset" />
    </xsl:apply-templates>
  </div>
</xsl:template>

<xsl:template match="*|comment()|processing-instruction()" mode="exclusive">
  <xsl:apply-templates select="self::node()" />
</xsl:template>

div要素用のテンプレートでは、再び子ノードにカレントノードリストを変更しつつ、それを同じmodeでテンプレートに適用します。

datatypeLibrary属性の特殊性に対処

include要素のdatatypeLibrary属性は、そのinclude要素の子要素には継承されますが、インクルードした要素には継承されないそうです。これに対処します。

<xsl:template match="rng:include">
<xsl:variable name="grammar.element" select="
  document(string(@href), /)/child::rng:grammar
" />
<xsl:variable name="components.nodeset" select="
  descendant::rng:*
  [boolean(self::rng:define) or boolean(self::rng:start)]
" />

<div>
  <xsl:apply-templates select="@*" />
  <xsl:apply-tempaltes select="child::node()" />
  <div>
    <xsl:if test="boolean($grammar.element/@datatypeLibrary)">
      <xsl:attribute name="datatypeLibrary" />
    </xsl:if>
    <xsl:apply-templates select="@*" />
    <xsl:apply-templates select="$grammar.element/child::node()" mode="exclusive">
      <xsl:with-param name="components.nodeset" select="$components.nodeset"
    </xsl:apply-tempalates>
  </div>
</div>

</xsl:template>

終わり

というわけでダラダラ書きましたが、問題を切り分けたりしているととても時間がかかってしまうのでご勘弁を。

変換後、スキーマ文書の意味論が異なる等の誤りを含む可能性がありますが、発見されたなら宜しければお知らせ下さい。


webmaster@jintrick.net
公開: 2003年10月11日
カテゴリ: XPath ,XSLT ,スキーマ