XSLTでフラットな文書を構造化
概要
見出し要素(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 > $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 >= $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)
- 構造化スタイルシートの元祖?