ノードを、いくつかの固まりごとに処理する

たとえば、XML形式の住所録データがあるとする。そこから宛名ラベル印刷用のデータを作る(住所録データ→XSL-FO→PDFといった形で印刷可能なデータにするとか)。宛名ラベル印刷用紙は用紙1枚につき10宛先をできるとする。そうなると、住所録データ内の住所データ(氏名、住所などなどを子としてもつ要素とする)を、10ずつ扱うとかしたい。
これは手続き型言語のように単純な繰り返し構文(C言語のforみたいなの)があれば話は簡単なのだけれども、そういうのはない。xsl:for-eachの使い方を工夫して実現する必要がある。10ずつに区切ったノードの先頭(Character[position() mod 10 = 1] または Character[(position() - 1) mod 10 = 0])をxsl:for-eachで処理するようにして、そのノード自身と、あとに続く兄弟ノード(最大9個)を処理するといったふうにやるとよい。
住所録データじゃないけど、下にマンガの登場人物データベース(しごく、単純なもの)の登場人物要素(Character要素)を、いくつかの固まりで処理するサンプルを書いた。HTMLとして出力し、各データは表形式で出力される。
表の列数(あとのサンプルだと列数も)を無理にあわせるために、ちょっとめんどうな処理を入れている部分があり、サンプルとしては冗長だけど、そのへんはご容赦のほどを。

もっと良いやり方があれば、コメント欄などでお教えください。

"sample.xml"
元データとなる登場人物データベース。
"sample1.xsl"
変数 range の大きさをひとかたまりとしてCharacter要素を扱う。
"sample2.xsl"
"sample1.xsl"を、ちょっと複雑にしたもの。変数 columnSize x 変数 rowSize の表を作る(= 変数 columnSize x 変数 rowSize の大きさをひとかたまりとしてCharacter要素を扱う。)

XSLTは、.NET Framework(System.Xml.Xsl.XslCompiledTransform クラス。PowerShellから利用する)とSAXONで処理した。

こちらの動作環境は以下のもの。

OS
Windows XP SP3 x86
PowerShell
PowerShell Ver. 2.0
Java
Java 6 Update 22
SAXON
SAXON-HE 9.3.0.4 (公式サイト:The SAXON XSLT and XQuery Processor

各種ファイル

"sample.xml"
<?xml version="1.0" encoding="UTF-8"?>
<Characters>
	<!-- けいおん! -->
	<Character>
		<FamilyName>平沢</FamilyName>
		<FirstName></FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>
	<Character>
		<FamilyName>秋山</FamilyName>
		<FirstName></FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>
	<Character>
		<FamilyName>田井中</FamilyName>
		<FirstName></FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>
	<Character>
		<FamilyName>琴吹</FamilyName>
		<FirstName></FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>
	<Character>
		<FamilyName>中野</FamilyName>
		<FirstName></FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>
	<Character>
		<FamilyName>山中</FamilyName>
		<FirstName>さわ子</FirstName>
		<Race>人間</Race>
		<Sex></Sex>
		<Title>けいおん!</Title>
	</Character>

	<!-- セキレイ -->
	<Character>
		<FamilyName>佐橋</FamilyName>
		<FirstName>皆人</FirstName>
		<Sex></Sex>
		<Race>人間</Race>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName></FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName>草野</FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName></FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName>月海</FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName>風花</FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
	<Character>
		<FirstName></FirstName>
		<Race>セキレイ</Race>
		<Sex></Sex>
		<Title>セキレイ</Title>
	</Character>
</Characters>
"sample1.xsl"
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
	<xsl:output method="html"
		encoding="UTF-8"
		indent="yes"
		doctype-public="-//W3C//DTD HTML 4.01 Transitional//EN"
		doctype-system="http://www.w3.org/TR/html4/loose.dtd" />

	<xsl:variable name="range" select="4"/>

	<xsl:template match="/">
		<html>
			<head>
				<title>いろいろな作品の登場人物</title>
				<style>
					<![CDATA[div { margin-bottom: 8px; }
					table { margin: 0px; padding: 0px; border-collapse: collapse; }
					tr { margin: 0px; padding: 0px; }
					td { margin: 0px; padding: 0px; width: 180px; height: 180px; border: solid 1px black; }
					td div { margin: 0px; padding: 4px; }
					td dl { margin: 0; }
					td dl dt { margin: 0 0 2px 0; width: 5em; float:left; }
					td dl dd { margin: 0 0 2px 5em; border-bottom: solid 1px black; }
					td.pattern1 { background: #FFFFFF;}
					td.pattern2 { background: #DDDDFF;}]]>
				</style>
			</head>
			<body>
				<xsl:apply-templates />
			</body>
		</html>
	</xsl:template>

	<xsl:template match="Characters">
		<xsl:for-each select="Character[(position() - 1) mod $range = 0]">
			<div>
				<table>
					<tr>
						<!-- カレントノードと、カレントノードに続く兄弟ノード。 -->
						<xsl:apply-templates select=".|following-sibling::Character[position() &lt; $range]" />
						<!-- テーブル内のセル数が $range になるようにする。足りない文、空のセルを追加する。 -->
						<xsl:call-template name="emptyCell">
							<!-- 0〜($range-1) -->
							<xsl:with-param name="restNodeCount" select="$range - count(following-sibling::Character[position() &lt; $range]) - 1" />
						</xsl:call-template>
					</tr>
				</table>
			</div>
		</xsl:for-each>
	</xsl:template>

	<!-- Character 要素をテーブルのセルに。 -->
	<!-- 最大 $range 個の Character 要素のノードセットを処理する。 -->
	<xsl:template match="Character">
		<td>
			<xsl:attribute name="class">
			<xsl:choose>
				<xsl:when test="position() mod 2 = 1">pattern1</xsl:when>
				<xsl:otherwise>pattern2</xsl:otherwise>
			</xsl:choose>
			</xsl:attribute>
			<div>
				<dl>
					<dt>氏名</dt>
					<dd><xsl:if test="count(FamilyName)>0"><xsl:value-of select="FamilyName" /><xsl:text> </xsl:text></xsl:if><xsl:value-of select="FirstName" /></dd>
					<dt>種族</dt>
					<dd><xsl:value-of select="Race" /></dd>
					<dt>性別</dt>
					<dd><xsl:value-of select="Sex" /></dd>
					<dt>作品</dt>
					<dd><xsl:value-of select="Title" /></dd>
				</dl>
			</div>
		</td>
	</xsl:template>

	<!-- テーブルのセル数を $range の数にあわせるために、空のセルを追加する。 -->
	<xsl:template name="emptyCell">
		<xsl:param name="restNodeCount" />

		<xsl:if test="$restNodeCount &gt;= 1">
			<td>
				<xsl:attribute name="class">
				<xsl:choose>
					<xsl:when test="$restNodeCount mod 2 = ($range mod 2)">pattern1</xsl:when>
					<xsl:otherwise>pattern2</xsl:otherwise>
				</xsl:choose>
				</xsl:attribute>
				<div></div>
			</td>

			<!-- 再帰呼び出し。 -->
			<xsl:call-template name="emptyCell">
				<xsl:with-param name="restNodeCount" select="$restNodeCount - 1" />
			</xsl:call-template>
		</xsl:if>
	</xsl:template>
</xsl:stylesheet>

"sample1.xsl"を処理するPowerShellスクリプト

try {
	$xslPath = ".\sample1.xsl";
	$xmlPath = ".\sample.xml";
	$htmlPath = ".\sample1result_a.html";

	$xslt = new-object System.Xml.Xsl.XslCompiledTransform;
	$xslt.Load($xslPath);
	$xslt.Transform($xmlPath, $htmlPath);
	write "無事終了した。";
	exit 0;
} catch {
	write-error $_;
	exit -1;
}

"sample1.xsl"をSAXONで処理するコマンド。SAXON-HEのjarファイルの位置は c:\opt\saxon\saxon9he.jar と想定している。

java -cp c:\opt\saxon\saxon9he.jar net.sf.saxon.Transform -t -strip:all "-s:.\sample.xml" "-xsl:.\sample1.xsl" "-o:.\sample1result_b.html"
"sample2.xsl"
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
	<xsl:output method="html"
		encoding="UTF-8"
		indent="yes"
		doctype-public="-//W3C//DTD HTML 4.01 Transitional//EN"
		doctype-system="http://www.w3.org/TR/html4/loose.dtd" />

	<xsl:variable name="rowSize" select="2" />
	<xsl:variable name="columnSize" select="3" />
	<xsl:variable name="range" select="$rowSize * $columnSize" />

	<xsl:template match="/">
		<html>
			<head>
				<title>いろいろな作品の登場人物</title>
				<style>
					<![CDATA[div { margin-bottom: 8px; }
					table { margin: 0px; padding: 0px; border-collapse: collapse; }
					tr { margin: 0px; padding: 0px; }
					td { margin: 0px; padding: 0px; width: 180px; height: 180px; border: solid 1px black; }
					td div { margin: 0px; padding: 4px; }
					td dl { margin: 0; }
					td dl dt { margin: 0 0 2px 0; width: 5em; float:left; }
					td dl dd { margin: 0 0 2px 5em; border-bottom: solid 1px black; }
					td.pattern1 { background: #FFFFFF;}
					td.pattern2 { background: #DDDDFF;}]]>
				</style>
			</head>
			<body>
				<xsl:apply-templates />
			</body>
		</html>
	</xsl:template>

	<xsl:template match="Characters">
		<!-- 表。rowSize x columnSize -->
		<xsl:for-each select="Character[(position() - 1) mod $range = 0]">
			<div>
				<table>
					<!-- 行。columnSize -->
					<xsl:for-each select=".|following-sibling::Character[(position() &lt; $range) and (position() mod $columnSize = 0)]">
						<tr>
							<!-- カレントノードと、カレントノードに続く兄弟ノード。 -->
							<xsl:apply-templates select=".|following-sibling::Character[position() &lt; $columnSize]">
								<xsl:with-param name="rowNum" select="position()" />
							</xsl:apply-templates>
							<!-- テーブル内のセル数が $columnSize になるようにする。足りない文、空のセルを追加する。 -->
							<xsl:call-template name="emptyCell">
								<!-- 0〜$columnSize -->
								<xsl:with-param name="rowNum" select="position()" />
								<xsl:with-param name="restNodeCount" select="$columnSize - count(following-sibling::Character[position() &lt; $columnSize]) - 1" />
							</xsl:call-template>
						</tr>
					</xsl:for-each>
					<!-- 必要なら数あわせの行を追加する。 -->
					<xsl:call-template name="emptyRow">
						<!-- 0〜($rowSize - 1) -->
						<xsl:with-param name="restRowCount" select="$rowSize - ceiling((count(following-sibling::Character[position() &lt; $range]) + 1) div $columnSize)" />
					</xsl:call-template>
				</table>
			</div>
		</xsl:for-each>
	</xsl:template>

	<!-- Character 要素をテーブルのセルに。 -->
	<!-- 最大 $columnSize 個の Character 要素のノードセットを処理する。 -->
	<xsl:template match="Character">
		<xsl:param name="rowNum" />

		<xsl:comment><xsl:value-of select="$rowNum"/></xsl:comment>
		<td>
			<xsl:attribute name="class">
			<xsl:choose>
				<xsl:when test="position() mod 2 = ($rowNum mod 2)">pattern1</xsl:when>
				<xsl:otherwise>pattern2</xsl:otherwise>
			</xsl:choose>
			</xsl:attribute>
			<div>
				<dl>
					<dt>氏名</dt>
					<dd><xsl:if test="count(FamilyName)>0"><xsl:value-of select="FamilyName" /><xsl:text> </xsl:text></xsl:if><xsl:value-of select="FirstName" /></dd>
					<dt>種族</dt>
					<dd><xsl:value-of select="Race" /></dd>
					<dt>性別</dt>
					<dd><xsl:value-of select="Sex" /></dd>
					<dt>作品</dt>
					<dd><xsl:value-of select="Title" /></dd>
				</dl>
			</div>
		</td>
	</xsl:template>

	<!-- テーブルの列数を $rowSize の数にあわせるために、空の行を追加する。 -->
	<xsl:template name="emptyRow">
		<xsl:param name="restRowCount" />

		<xsl:if test="$restRowCount &gt;= 1">
			<tr>
				<xsl:call-template name="emptyCell">
					<xsl:with-param name="rowNum" select="$rowSize - $restRowCount + 1" />
					<xsl:with-param name="restNodeCount" select="$columnSize" />
				</xsl:call-template>
			</tr>

			<!-- 再帰呼び出し。 -->
			<xsl:call-template name="emptyRow">
				<xsl:with-param name="restRowCount" select="$restRowCount - 1" />
			</xsl:call-template>
		</xsl:if>
	</xsl:template>

	<!-- テーブルのセル数を $columnSize の数にあわせるために、空のセルを追加する。 -->
	<xsl:template name="emptyCell">
		<xsl:param name="rowNum" />
		<xsl:param name="restNodeCount" />

		<xsl:if test="$restNodeCount &gt;= 1">
			<td>
				<xsl:attribute name="class">
				<xsl:choose>
					<xsl:when test="$restNodeCount mod 2 = (((($rowNum - 1) mod 2) + $columnSize) mod 2)">pattern1</xsl:when>
					<xsl:otherwise>pattern2</xsl:otherwise>
				</xsl:choose>
				</xsl:attribute>
				<div></div>
			</td>

			<!-- 再帰呼び出し。 -->
			<xsl:call-template name="emptyCell">
				<xsl:with-param name="rowNum" select="$rowNum" />
				<xsl:with-param name="restNodeCount" select="$restNodeCount - 1" />
			</xsl:call-template>
		</xsl:if>
	</xsl:template>
</xsl:stylesheet>

"sample2.xsl"を処理するPowerShellスクリプト

try {
	$xslPath = ".\sample2.xsl";
	$xmlPath = ".\sample.xml";
	$htmlPath = ".\sample2result_a.html";

	$xslt = new-object System.Xml.Xsl.XslCompiledTransform;
	$xslt.Load($xslPath);
	$xslt.Transform($xmlPath, $htmlPath);
	write "無事終了した。";
	exit 0;
} catch {
	write-error $_;
	exit -1;
}

"sample2.xsl"をSAXONで処理するコマンド。SAXON-HEのjarファイルの位置は c:\opt\saxon\saxon9he.jar と想定している。

java -cp c:\opt\saxon\saxon9he.jar net.sf.saxon.Transform -t -strip:all "-s:.\sample.xml" "-xsl:.\sample2.xsl" "-o:.\sample2result_b.html"