丁度良い事例なので鳩丸更新履歴から引用しますが:
ちなみに foo要素と foo要素の間にある CRLF も一つのノードとして扱われていて、position() はそれもカウントした値を返している模様。
「HTML鳩丸倶楽部」更新履歴(2001-10-04) より
position()関数が改行を数えてしまう原因は、「カレントノードリスト」に改行が含まれてしまっているからです。
<xsl:template match="foo">
<xsl:if test="position() mod 4 = 2">
……
</xsl:if>
……
</xsl:template>
このテンプレートが処理されている時、カレントノード(処理中のノード)は一つのfoo要素です。ではカレントノードリストはなんでしょうか。
実は、この例を見ただけでは分かりません。一般に、この「テンプレート」はxsl:apply-templates要素経由で処理が開始されます、その場合、そのxsl:apply-templates要素のselect属性が、カレントノードリストを決定します。例えば:
<xsl:template match="bar">
<xsl:apply-templates />
</xsl:template>
上のxsl:apply-templates要素のselect属性は指定されていないのでデフォルト値「child::node()」です。カレントノードはbar要素ですから、bar要素の子ノード全てを意味します。従ってカレントノードリストは、「bar要素の子ノード全て」となります。
よく見かける解説では、xsl:template要素のmatch属性に指定されたノード集合が、カレントノードリストになる、と説明されていますが、match属性値は単なるパターンであり、処理すべきノード集合を指定するものではありませんから、これは間違いです。(某書籍はXSLTの勘違いを撒き散らしているような気がします。)
プロセッサの処理順序を考えてみると分かります。 select="child::node()"という属性をもつxsl:apply-templates要素の処理に入った時点で、child::node()で指定された「リスト」を元に、そのリストに含まれるそれぞれのノードについて、マッチするテンプレートを見つけだし、そのテンプレートの処理に入ります。xsl:template要素のmatch属性は、マッチするか否かをプロセッサに判断させる基準に過ぎません。次の例を考えると、さらに明確になると思います。
XMLは以下の例を使います(断片)。
<document>
<foo></foo>
<bar></bar>
<foo></foo>
</document>
XSLTは以下の例を用います(断片)。
<xsl:template match="document">
<xsl:apply-templates select="child::*" />
<!-- select属性はデフォルト値 -->
</xsl:template>
<xsl:template match="foo">
some template
</xsl:template>
プロセッサが、document要素の処理に入っている段階から考えます。xsl:apply-templates要素が現われたので、select属性に指定されている通り、document要素の子要素ノード達(child::*)について、マッチするテンプレートを探し始めます。最初は、第一番目のfoo要素です。さてこのとき、カレントノードリストがmatch属性に指定されている「foo」になっているとしたら、次に処理するのは第三番目のfoo要素ということになってしまいます。なぜならカレントノードリストとは、現在処理中のノード(カレントノード)のリストを意味するからです。ところが実際には、第一番目のfoo要素の処理を終えたプロセッサは、第二番目のbaz要素の処理にかかります。これは多くの解説書等にもきちんと説明されているとおりです。match属性に指定された「foo」に、baz要素が含まれる余地はありませんから、これは矛盾していることになります。
従ってこの例からも、xsl:template のmatch属性がカレントノードリストを変更しないことが分かります。その辺の参考書に騙されないないよう、ご注意ください。
ちなみに現時点で私が知る限り、カレントノードリストを変更することができるのは、xsl:apply-templates要素のselect属性、それから、xsl:for-each要素のselect属性の二つです。
さて、position()関数は(XSLTにおいては)、カレントノードリスト内の、カレントノードの位置を返す関数です。従って改行を数えて欲しくない場合、カレントノードリストに、改行を含めなければ良いだけです。
鳩丸更新履歴で取り上げられていた例を(ちょっと変更しつつ)紹介します。
<?xml version="1.0" encoding="Shift_JIS"?>
<document>
<foo>1</foo>
<foo>2</foo>
<foo>3</foo>
<foo>4</foo>
<foo>5</foo>
</document>
このようなXMLを、次ようにグループ化するにはどうすれば良いか、という問題です。
<?xml version="1.0" encoding="Shift_JIS"?>
<document>
<bar>
<foo>1</foo>
<foo>2</foo>
</bar>
<bar>
<foo>3</foo>
<foo>4</foo>
</bar>
<bar>
<foo>5</foo>
</bar>
</document>
これは、次のようなXSLTで可能です。(一例に過ぎません)
<?xml version="1.0" encoding="Shift_JIS"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" />
<xsl:template match="/">
<document>
<xsl:apply-templates />
</document>
</xsl:template>
<xsl:template match="document">
<xsl:for-each select="foo">
<xsl:if test="(position() mod 2) != 0">
<bar>
<foo><xsl:value-of select="self::node()" /></foo>
<xsl:if test="following-sibling::foo[1]">
<foo><xsl:value-of select="following-sibling::foo[1]" /></foo>
</xsl:if>
</bar>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
ここに登場するposition()関数が扱っている、カレントノード及びカレントノードリストを考えてみます。
処理開始時点では、カレントノードリスト及びカレントノードは共にルートノードです。そしてカレントノードであるルートノードにマッチするテンプレートを探し始めます(デフォルトの挙動です)。これは7行目〜11行目の<xsl:template match="/"></xsl:templates>に最優先でマッチします。
次に、このテンプレートの中に再びxsl:apply-templates要素が登場します。この要素にはselect属性が指定されていないようですが、デフォルトで、select="child::node()"が指定されているものと見なされます(重要)。従ってこの時点で、カレントノードリストはルートノードの子ノード全てということになります。ここは勘違いポイントなのですが、ルートノードはdocument要素ではありません。document要素のさらに上に、親としてルートノードが存在しているものと見なされます。というわけでこの時点で、カレントノードリスト及びカレントノードはdocument要素(XPathでは、/child::document)ただ一つとういことになり、マッチするテンプレートを探し始めます。
さてそのdocument要素にマッチするテンプレート<xsl:template match="document"></xsl:template>内に、select属性を持ったxsl:for-each要素が現われます。これはカレントノードリストを変更します。select="foo"と指定されていますが、select="child::foo"の省略形です。従ってカレントノードリストは、XPathの絶対パスでいうところの、/child::document/child::fooに変更されました。これらのfoo要素達を順次処理してゆきますが、その時処理しているfoo要素がカレントノードです。
position()関数が登場するノードはxsl:if要素のtest属性内ですが、これは仕様書にも明示されているように、カレントノードを一切変更しません。従って、position()関数が返すのは、「/child::document/child::fooというノードリストの中で、現在処理しているfoo要素の位置」ということになります。
agendaを試験的に構造化。agenda.xmlをagenda.xslで(構造化されたdivだらけの)HTMLに変換。ただ、XSLファイルを書いていて面倒臭くなってしまったので、h6要素を脳内から抹殺。使ったことないし、これからも使わないだろう。たぶん。で、実利は……今のところほとんどないだろう。でもXSLTが面白くてしかたないんです(手段の目的化)。というのは半分冗談。
agenda.xmlは基本的にフラットでリニアであり、何の変哲もないテキストエディタで作成、編集する。脳内スキーマに基づいた、いい加減な代物だ。つまり構造を「暗示」しているXML文書だって「アリ」だと、そう言いたいわけで。「暗示」をタブーにしてしまったら、マークアップは際限がなくなる。そしてマークアップは、人間も、直接行うことができるように考えられたものだ。特別なツールがなくともリソースを作成できるのがメリットの一つではなかったか。
ともかく、しばらく試してみて問題がないようなら、サイト内の全ての文書を変換してみよう、か、な。ソースファイルがXHTMLでもちゃんと変換できることは確認済み。まあHTML4.01とかでも別の何かを経由すればいいだけで、見出しと本文の登場「順序」に構造が示されていれば一応何とかなる。つまり中身がきちんとしているかどうかが、重要だ。昔の一部の文書に自信がない。
まだあまり試していないけれども、XSLTにはmessage要素というものがある。これを利用することで例えば、h1要素の次にh4要素が出現したり、h2要素が空だったりした際に警告を発しつつ変換プロセスを中断させることが出来るので、文法チェッカを兼ねさせることができる(かも知れない)。ただ正規表現を使えないのが……かなり痛い。全角スペースのみのp要素とかを判別するのは、ちょっと現実的ではないかも。
また当然ながら、タグの閉じ忘れや入れ子構造の不正があれば、message要素など使わずともパースエラーとして処理は中断される。
現在は div 要素による階層の作り方に関しては、 W3C でははっきりと規定していないため、制作者によって記述がまちまちなのは事実です。ただ、「悪例」と同様の構造を有するサイトが複数あったのを確認しましたもので。特に私が不自然だと感じたのは、 h1 要素を含む div 要素の使い方。見栄えの為ならば仕方無いにせよ、「論理構造の明示」という観点から見れば、やや不自然かと思われます。
レナ姫のWeb研究室 より
ここで古林さんが問題視されているのは:
<div class="section1">
<h1>大見出し</h1>
<p>段落。</p>
</div>
<div class="section2">
<h2>中見出し</h2>
<p>段落。</p>
</div>
このような構造だと思うのですが、h1の兄弟としてグループ化されているp要素が、セクションの「本文」であったなら、明らかに不自然なマークアップです。しかし、このh1とpをひっくるめてそのセクションの「header」だとしたらどうでしょう。
<section>
<section-header> ……HTMLではdiv要素
<title>見出し</title> ……HTMLではh1要素
<summary>要約</summary>……HTMLではp要素等
</section-header>
<section-body>
<paragraph>段落。</paragraph>
<paragraph>段落。</paragraph>
……
</section-body>
</section>
というわけで私は、section-headerとsection-body要素もdivで明示することにしてみました。冗談半分ながら。
同じname属性をもったvariable要素が複数出現するとエラーになるのだけど、値を書き換えられないものを変数っていうの?
というか、for文が使えないということにならないかコレ。n回繰り返せって時にはどうすればいいんだろう。別に今のところ必要ないんだけど、なんか先々不安。
気になったので探してみたところ、ピッタリなリソースを見つけた。
for-eachがらみでばかり考えていたけど、call-templateで条件付のループをさせるんですね。
残る疑問は、pre要素内のみ結果ツリーのインデントを防ぐにはどうするか。第一番目の子をテキストノードとして扱わせれば良いのだけど、text要素内ではPCDATAしか扱えない(変数、式を使えない)のが悩みどころ。
次のようなXMLを考える:
<?xml version="1.0"?>
<document>
<h1>大見出し</h1>
<p>文章。</p>
<h2>中見出し1</h2>
<p>文章。</p>
<h3>小見出し1</h3>
<p>文章。</p>
<h2>中見出し2</h2>
<p>文章。</p>
<h3>小見出し2</h3>
<p>文章。</p>
</document>
これは、document要素の子孫が全て兄弟になっている(フラットな)構造である。
ここで、document要素を変換するテンプレート内で、h2要素の数だけ繰り返し処理を行い(for-each要素)、そのfor-each要素内において:
の2つを、このfor-each要素内で扱えるローカル変数($H2posおよび$nextH2pos)にそれぞれバインドするにはどうすれば良いか。
一応はできたが、もう、悪い夢のようだ。
<xsl:template match="document">
<xsl:for-each select="h2">
<xsl:vriable name="H2pos">
<xsl:number level="single" count="*" />
</xsl:variable>
<xsl:variable name="nextH2pos">
<xsl:for-each select="following-sibling::h2[1]">
<xsl:number level="single" count="*" />
</xsl:for-each>
</xsl:variable>
以下テンプレート
</xsl:for-each>
</xsl:template>
for-each要素直下においたvariable要素は、そのfor-each要素内でのみ有効なローカル変数になる。ところが、カレントノードを変更しないことには、number要素による「カレントノード以外のノード」のカウントができず、for-each要素内でカレントノードを変更するには、その中でもう一度for-each要素を作るしかない。つまり知る限り、template要素内でカレントノードを変更するには、for-each要素を使うしかないということ。for-eachは、指定したノードリストで繰り返しを行う為にあるのだが、ノードリストではなく特定のノードをselect属性に指定し、また、そのfor-each要素をvariable要素の子にすることで、ローカル変数が子のfor-each要素内に隠蔽されてしまうのを防ぐ。小汚いハックであるとしか思えないよ。
というわけで、この例では、<xsl:for-each select="h2"></xsl:for-each>内の式なら何処でも、$H2pos、$nextH2posを参照できる。
実際には「最後のh2要素」という例外を処理する為にwhen、otherwise要素を使って更に分岐しなければならないが、これでようやく内容と構造の分離を図ることができそうだ。簡単に言ってしまえば、フラットな文書にツリー構造を与えられる。問題は、h1からh6までをこの方法を使って実現しようとすると、ちと人間が読むようなものではなくなってしまうことだ。「冗長、ここに極まれり」
しかしどうも腑に落ちない。要素の位置を返すposition()関数が非力過ぎる。用途の違うnumber要素の方が使い勝手が良いというのは一体どういうことなのか。XPathで可能なら変数にバインドする必要もないので、コードがスッキリするのに。XSLTの要素、XPathの関数などの中で、何か重大なものを見落としているような気がしてならない。一応全部目を通したんだけどなあ。
教えてクンに変身2秒前?
「自分もuser stylesheet
とuser javascript
を書いています」という方からメールが。
後者はjavascript:スキーム経由で発射するタイプらしい。もしかして、Contextmenu Extensions とか fub_redとかでかなり幸せになれるのでは。
そういえば海外のタブブラウザって調べたことないなあ。ちょっと聞いてみようかな。親切そうな人だし。
どうやら、闇黒日記経由でhttp://randomfoo.net/ (英語)にウチのリファラを残した人がいて、このサイトのマスターが例によってWeblogにて「リンクされてるけど日本語読めねー」とぼやいて、その読者がウチを発見したらしい。リンクって凄いですね。
decendant-or-self軸がNGになっているためdecendant-or-self::node()は駄目だが、同じ意味の省略形「//」なら通る。その他、parent軸もNGらしい。というか、child軸とattribute軸以外は全部駄目。だから「//」が通るのは例外的と考えるべきかも。
XPointerで遊んでいた時には、XPathの関数は全然知らなかったが、XSLTがらみになると重要になってくる。position関数は、コンテキストノードの「位置」を返す。正しくは、カレントノードリスト内のカレントノードの位置です。本を鵜呑みにしてはいけませんな。
本当にそれだけの関数だ……。過剰な期待をした自分が悪いのだけど、親ノードの横の位置くらいは知りたい:
position(..)
もちろんこれは駄目。position関数に引数はない。
XSLTは、フラットな構造に厳しい。
ルートノードとルート要素は違うものだったという罠。
<xsl:template match="/"></xsl:template>内で(XPathの)count関数が使えないのには驚きだ。ルート要素直下に見出しをズラズラ並べていたのだけど、数えてくれない……。このためだけに階層を一つ増やしてみたのだけど、なんか気持ち悪い。
XMLで内容を記述して、XSLTで構造を定義して、CSSで見栄えを……と思っていたけど、XSLTが弱い、というよりそもそも用途が違うっぽいので無理。要素を自由に切り貼りできるようなものではないらしい。ソースのXMLで構造を明示しないと駄目だ。要するに、XMLパーサを意識してXMLを書かねばならない。
要素を自由に切り貼りできるようなものではないらしい。
と書いてしまいましたが、あれは嘘でした。template要素のmode属性、call-template要素をよく知らなかったのが勘違いの原因。
SAXONのサイトはテーブルレイアウトだけれども、ユーザビリティが高いので好感が持てた。とりあえずInstant SAXONを試してみたところ、Shift_JISがまるで駄目らしい。
しかし、UTF-8への「乗り換え」は、異常にコストが高いことを実感しつつある。メインのテキストエディタはUTF-8を扱えないし、send toでメモ帖も良く使うし。ということは、私に残された選択肢はiXSLT (英語)のみということになるのか(どうなのか)。
プロセッサはどれがベストかよく分からないので、ともかくXSLファイルを完成させてからゆっくり選ぶことに。XPathの関数が未サポートだったりして凹むのも嫌だし。
# id属性直しました。ありがとうございます。
XTを使ってXMLファイルをHTMLに変換したところ、全ての日本語が数値文字参照に置き換わってしまった。
<xsl:output method="html" encoding="Shift_JIS"/>
こうしてShift_JISで生成すると、今度は以下のような余計なゴミが強制的に挿入される。
<META http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
要素名大文字、終了タグ無しですか……。XTのバージョンが古い(XT Version 19991105 (英語))のが原因……のような気が。
やらなきゃならないことは多い:
まず、XTのバージョンアップ、それから、手持ちのエディタはUTF-8で保存するとゴミ(BOM)がくっついてきてXTが文字コードを認識してくれないから他のテキストエディタを探すか何かしなきゃならないし、XTのGUI操作を補助してくれるツールも探したいし(GUI世代故)……。
XSLTは趣味の範囲ギリギリだ。XPathに馴染みがあったので、まだ若干楽しめる程度。ただ、HTMLのcodingを変更するのは、もう、うんざりだし、XHTMLのテンプレート部(だんだん肥大してきた)が隠蔽されていないのも気味が悪いというか。
というか、見出し構造とツリー構造の「明示」についての「明示方法の悪例」って、モロにウチの構造でないの。古林さんは、ウチを「参考」にしたのかな(苦笑)。……ツリーの深さにある制約を課していただけなんだけど、視点を変えると何だか酷いマークアップに見えるものだ。
若干異論はあるものの、div イコール sectionと見なせば確かにその通りだし、その意味で統一した方がベターだとも思うので、これからは構造と見栄え、それから内容の3つを分離してフレキシブルにすることにした。初期投資がでかいけど、どうせなら徹底的に。
こういう記事をみると妙にやる気がでるのは一体なんなのだろう。