XSLTでフラットな文書を構造化

このページの目次
  1. 概要
  2. ソースコード
  3. 簡単な説明
  4. 主な変更履歴
  5. 関連文書

概要

見出し要素(hn要素)に基づき、次のようにdiv要素を追加するXSLTスタイルシートです。

変換前:

<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>構造化サンプル</title>
  </head>
  <body>
    <h1>見出し1</h1>
    <h2>見出し2-1</h2>
    <p>本文a</p>
    <h3>見出し3-1</h3>
    <p>本文b</p>
    <h3>見出し3-2</h3>
    <p>本文c</p>
    <h2>見出し2-2</h2>
    <p>本文d</p>
  </body>
</html>

変換後:

<?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>構造化サンプル</title>
  </head>
  <body>
    <div class="section">
      <h1>見出し1</h1>
      <div class="section">
        <h2>見出し2-1</h2>
        <p>本文a</p>
        <div class="section">
          <h3>見出し3-1</h3>
          <p>本文b</p>
        </div>
        <div class="section">
          <h3>見出し3-2</h3>
          <p>本文c</p>
        </div>
      </div>
      <div class="section">
        <h2>見出し2-2</h2>
        <p>本文d</p>
      </div>
    </div>
  </body>
</html>

ソースコード

<?xml version="1.0"?>
<xsl:stylesheet
  xmlns="http://www.w3.org/1999/xhtml"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:rfs="http://refluxflow.net/2014/XSLT/Structuring"
  exclude-result-prefixes="xsl rfs"
  version="1.0">

  <xsl:output
    method="xml"
    version="1.0"
    encoding="utf-8"
    indent="yes"/>

  <xsl:template match="*">
    <xsl:copy>
      <xsl:apply-templates select="attribute::node()|child::node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="attribute::node()|text()">
    <xsl:copy/>
  </xsl:template>

  <xsl:template match="*[local-name() = 'body']">
    <!-- body要素を複製し、その子要素を構造化する -->
    <xsl:element name="{name()}">
      <xsl:apply-templates select="attribute::node()"/>
      <xsl:for-each select="child::node()[1]">
        <xsl:call-template name="rfs:structuring">
          <xsl:with-param name="level" select="number('0')"/>
        </xsl:call-template>
      </xsl:for-each>
    </xsl:element>
  </xsl:template>

  <xsl:template name="rfs:section">
    <xsl:param name="level"/>
    <div class="section">
      <xsl:apply-templates select="self::node()"/>
      <xsl:for-each select="following-sibling::node()[1]">
        <xsl:call-template name="rfs:structuring">
          <xsl:with-param name="level" select="$level"/>
        </xsl:call-template>
      </xsl:for-each>
    </div>
  </xsl:template>

  <xsl:template name="rfs:structuring">
    <!-- 見出しが見つかるまでノードにテンプレートを適用する -->
    <xsl:param name="level"/>
    <xsl:variable name="headingLevel" select="number(substring(local-name(), 2))" />
    <xsl:variable name="isHeading" select="starts-with(local-name(), 'h') and boolean($headingLevel)"/>

    <xsl:choose>
      <xsl:when test="$isHeading">
        <xsl:if test="$headingLevel &gt; $level">
          <xsl:call-template name="rfs:sameLevelSections">
            <xsl:with-param name="level" select="$headingLevel"/>
          </xsl:call-template>
        </xsl:if>
      </xsl:when>
      <xsl:otherwise>
        <xsl:apply-templates select="self::node()"/>
        <!-- 次のノードに移動 -->
        <xsl:for-each select="following-sibling::node()[1]">
          <xsl:call-template name="rfs:structuring">
            <xsl:with-param name="level" select="$level"/>
          </xsl:call-template>
        </xsl:for-each>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

  <xsl:template name="rfs:sameLevelSections">
    <!-- より上位のレベルの見出しが見つかるまで、同レベルの見出しを探してsectionテンプレートを呼び出す -->
    <xsl:param name="level"/>
    <xsl:variable name="headingLevel" select="number(substring(local-name(), 2))"/>
    <xsl:variable name="isHeading" select="starts-with(local-name(), 'h') and boolean($headingLevel)"/>

    <xsl:if test="$isHeading and ($headingLevel = $level)">
      <xsl:call-template name="rfs:section">
        <xsl:with-param name="level" select="$headingLevel"/>
      </xsl:call-template>
    </xsl:if>

    <xsl:if test="not($isHeading) or ($headingLevel &gt;= $level)">
      <!-- 次の要素ノードに移動 -->
      <xsl:for-each select="following-sibling::*[1]">
        <xsl:call-template name="rfs:sameLevelSections">
          <xsl:with-param name="level" select="$level"/>
        </xsl:call-template>
      </xsl:for-each>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>

簡単な説明

構造化に必要なテンプレートは、rfs:section、rfs:structuring、rfs:sameLevelSectionsの3つです。他のテンプレートとの名前の衝突を避けるために接頭辞としてrtsを付与しています。

構造化するには、対象となる見出し要素(hn要素)の親ノード(上記のコードではbody要素)の最初の子ノードをカレントノードにした状態でrfs:structuringテンプレートを呼び出します(rfs:section要素の1つ上のテンプレートを参照)。

補足

  • 変換前のXMLでは、見出し要素はすべて同じ要素の子要素である必要があります。
  • 要素名に名前空間接頭辞がついていても大丈夫です(例えばxht:h1でも可)。
  • h7やh8があっても処理できます。
  • Saxon HE 9.5.1.6, Xalan 2.7.2, MSXSL 3.0で動作することを確認しました。

自由に書き換えて再利用して構いませんが、ご使用は自己責任でお願いします。

主な変更履歴

2014年7月26日
構造化スタイルシートのコードを見直し、大幅に変更しました。

関連文書

外部リンク

フラットな文書を構造化 その2(agenda)
構造化スタイルシートの元祖?