XSLT3.0への道(36) 変わるDITAのテンプレート(その2)

この前XPath 3.1で使えるようになったcontains-tokenを使ったDITAのマッチングテンプレートを紹介させていただきました.でもDITAのスタイルシートを作っていると、いろいろな@class属性にマッチするか、テストしたい場合が出てきます.例えばfigのテンプレートで、下位にtitleかdesc要素が存在するか調べたいとします.これは実際意味があって、DITAのfigをHTML5のfigureに変換するときに、titleかdescが有ったらfigcaptionという要素を生成してやる必要があったからです.contains-tokenを使えば次のようになるでしょう.

<xsl:function name="ahf:hasFigTitle" as="xs:boolean">
<xsl:param name="prmFig" as="element()"/>
<xsl:sequence select="$prmFig/*[contains-token(@class, 'topic/title') and contains-token(@class,'topic/desc')] => exists()"/>
</xsl:function>

 確かにそのとおりこれで動いてくれるのではありますが、あんまりおもしろくありません.contains-tokenを単に2つ記述してandで結んでいるだけだからです.こういうのを私の住んでいる長野県のお年寄りの言葉では「げーもねぇ」(あまりよろしくない状態を指す)と言います.

では、欲しいのはどんなものでしょうか?簡単に言えば、'topic/title'か'topic/desc'が含まれることを一発でチェックできる関数がほしいです.でもこれって考えてみるとすごく簡単なのです.私がやってみた実装は以下のとおりです.

<xsl:function name="ahf:seqContainsToken" as="xs:boolean">
  <xsl:param name="prmDstStr" as="xs:string*"/>
  <xsl:param name="prmTokens" as="xs:string*"/>
  <xsl:choose>
    <xsl:when test="empty($prmDstStr) or empty($prmTokens)">
      <xsl:sequence select="false()"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:variable name="dstStrs" as="xs:string*" select="$prmDstStr ! tokenize(.,'\s')"/>
      <xsl:sequence select="$dstStrs = $prmTokens"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:function>

こういう関数を作っておけば、元の ahf:hasFigTitleも次のように簡素化できます.

<xsl:function name="ahf:hasFigTitle" as="xs:boolean">
  <xsl:param name="prmFig" as="element()"/>
  <xsl:sequence select="$prmFig/*[ahf:seqContainsToken(@class, ('topic/title','topic/desc'))] => exists()"/>
</xsl:function>

とっても簡単になりました. ahf:seqContainsTokenのミソは、$dstStrs = $prmTokensと一般比較演算子で左辺と右辺のシーケンスで「一つでも同じものがある?」を調べられることです.XSLT 3.0でやろうと決意していろいろ探してみると便利なのがいっぱいあります.ここで使われている=>もその一つ.また一例ですが、このようなahf:hasFigTitleの関数を作ると私はたいていこの逆の関数も作ります.

<xsl:function name="ahf:hasNoFigTitle" as="xs:boolean">
  <xsl:param name="prmFig" as="element()"/>
  <xsl:sequence select="ahf:hasFigTitle($prmFig) => not()"/>
</xsl:function>

 これもまた便利です.=> not() なんて動くの?ハイ、バッチリ動きます.XSLT 3.0で、この=>演算子と!演算子を使うと、本当にコーディングが楽しく、しかも楽にできるようになります.ご参考にしていただければ幸いです.

XSLT3.0への道(35) 変わるDITAのテンプレート

XSLT 3.0を使用するようになると、DITAのテンプレートも変わります.DITA-OT( DITA Open Toolkit)は3.xになってからついてくるSaxon HEのバージョンを9.8に変えました.なのでXSLT 3.0で自由にプラグインスタイルシートがかけるようになります.

と言っても、DITA-OT本体のコードはまだXSLT 2.0のままですが...

今回HTMLのプラグインを新規作成する機会があったので、これぞとばかりにすべてXSLTスタイルシートのバージョンを3.0に上げてみました.

例えば、今まででしたらDITAの要素にマッチングさせるテンプレートは次のように書いていたはずです.

<xsl:template match="*[contains(@class, ' topic/title ')]">

でも考えてもわかるんですが、containsというのは文字列をなめて一致を判定しなければならないのでトータルで見ると遅いですよね.DITAのコミュニティからの要望をうけてXSLT 3.0のエディタのMichael Kay博士はcontains-tokenという関数を追加してくれました.

5.3.9 fn:contains-token

https://www.w3.org/TR/xpath-functions-31/#func-contains-token

 これを使用すると、次のようにテンプレートの書き方が変わります.

<xsl:template match="*[contains-token(@class, 'topic/title')]">

 ご覧いただくとわかりますが、' topic/title 'の指定は、'topic/title'に代わります.これは、@classを文字列に置き換えて、ホワイトスペースを区切り文字としてトークンを切り出すためです.

たかだかこれだけとバカにすべきではありません.膨大なDITAの文書を処理する時、逐次文字検索のcontainsと、トークンに切り出して判定するcontains-tokenとでは、スピードに差が出ること必至です.

このコーディングでもいいのですが、私はもうちょっとカッコをつけて次のコーディングスタイルにしています.

<xsl:template match="*[@class => contains-token('topic/title')]">

これだと「@class属性に」「'topic/title'がトークンとして含まれているか?」という日本語の順序にあった書き方になるからです.

ここで使われている"=>"は以前紹介した演算子で、左辺を右辺の関数の第1引数として渡してくれるものです.

Arrow operator

https://www.w3.org/TR/xpath-31/#id-arrow-operator

あとよくあるパターンとして、@class属性がAかBに一致というマッチングパターンが良く出てきます.これもXSLT 3.0で書くとずっと簡単になります.次回に紹介させてもらいます. 

 

秋の一日

この前の台風は大変でしたが、私のところは天竜川の一つ上の河岸段丘のにあるので、警告が出てもまだなんとか安心でいられました.段丘の下にお住いの方は近くの中学校に皆さん避難されたそうです.

東北の方ではまだ稲刈りが済んでいないところもあるのですね.私のところはおかげさまで刈り取りも終わり、田んぼの土手には曼珠沙華がニョキニョキ生えてきてきれいでした.

f:id:toshi_xt500:20191020205035j:plain

刈り取り後の田んぼの土手の曼珠沙華

日曜の今日、地区の文化祭で農事関係の役員になっている私は朝8時に公民館に行って蕎麦をゆでる鍋と寸胴に水をいっぱい入れ、借りてきたコンロに火を入れました.あとはひたすら外のテントで蕎麦の販売、約200食分販売できて一日を終えることができました.あしたからはまた仕事に戻ります.

 

空の要素か判定する

たまにある要素が空要素か判定がしなければならない場合があります.ある要素が子要素を持っていないで、文書中に残されているときスタイルシートの処理をスキップしたいような場合がそれにあたります.ユーザーがオーサリングしたDITAのようなXML文書は、例えばXMLエディタで自動的にひな型の要素が挿入されて残存している場合があるからです.例えばoXygenでDITAのコンセプトを書こうとすると、こんな感じでプロトタイプが自動生成されます.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE concept PUBLIC "-//OASIS//DTD DITA Concept//EN" "concept.dtd">
<concept id="cConceptSample">
<title>コンセプトのタイトル</title>
<shortdesc></shortdesc>
<conbody>
<p></p>
</conbody>
</concept>

この例だとshortdescなどがよくそのまま残ります.で、フツーにスタイルシートを処理すると、PDFにするんだったらfo:block に、HTMLだったらpに変換するので見かけ上無駄な空行が出来てしまいます.お客さんによっては「無駄な空行つくらないで!」と言われます.

でもそんなの簡単じゃあない?と言われるかもしれません.そう関数のempty(shortdesc)で判定すれば一番簡単です.確かにそのとおりですが、これだと、

<shortdesc>
</shortdesc>

 と改行を入れられるともうヒットしなくなります.テキストノードがshortdescの子ノードとして存在してしまうからです.

もっと言えばこんないやらしいのも不要な空要素と判定しなければなりません.

<shortdesc><!--コメントです-->
<?pi 処理命令です ?>
</shortdesc>

 なので厳格にスタイルシートで判定するとなると、empty()では役不足になります.まじめにやったらどうなるか関数を作ってみました.名付けてahf:isEmptyElement()です.

<xsl:function name="ahf:isEmptyElement" as="xs:boolean">
    <xsl:param name="prmElem" as="element()"/>
    <xsl:choose>
        <xsl:when test="empty($prmElem/node())">
            <xsl:sequence select="true()"/>
        </xsl:when>
        <xsl:otherwise>
            <xsl:sequence select="every $node in $prmElem/node() satisfies ahf:isRedundantNode($node)"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:function>

<xsl:function name="ahf:isRedundantNode" as="xs:boolean">
    <xsl:param name="prmNode" as="node()"/>
    <xsl:choose>
        <xsl:when test="$prmNode/self::comment()">
            <xsl:sequence select="true()"/>
        </xsl:when>
        <xsl:when test="$prmNode/self::processing-instruction()">
            <xsl:sequence select="true()"/>
        </xsl:when>
        <xsl:when test="$prmNode/self::text()">
            <xsl:choose>
                <xsl:when test="string(normalize-space($prmNode)) eq ''">
                    <xsl:sequence select="true()"/>
                </xsl:when&gt
                <xsl:otherwise>
                    <xsl:sequence select="false()"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:when>
        <xsl:when test="$prmNode/self::element()">
            <xsl:sequence select="false()"/>
        </xsl:when>
        <xsl:otherwise>
            <xsl:sequence select="true()"/>
        </xsl:otherwise>
    </xsl:choose>
</xsl:function>

 こんな重装備の関数、本当に要るのか?と思うかもしれませんが、XMLエディタによっては、編集状態を処理命令で残してしまうようなものもありますので意味があります.

この関数は実際にDITAから Word文書(.docx)への変換スタイルシートで重宝しています.いやXMLって奥が深い、というかなかなかややこしいですね.

 

 

 

HTMLとXMLの処理命令

XMLからHTMLへの変換スタイルシートを書いています.「そんなのありふれてるじゃん」と言われそうなんですが、なにせ変換元がDITAなのでそう甘くはないです.

で、HTMLと言っても拡張子は.phpです.つまりサーバー側のPHPエンジンで変換されクライアントのブラウザに送られます.このため、XMLから生成する.phpファイルにはHTML構文だけでなく、"<?php ~ ?>" のPHPの命令を埋め込まねばなりません.

こんなの当たり前のことと思って<xsl:processing-instruction name="php">コンテンツ</xsl:processing-instructuin>で吐き出していました.出力がXML形式の場合はなんの問題もありませんが、HTML形式を指定すると、上手くいってくれません.出力は次のようになってしまい、"?>"で閉じてくれないのです.

 <?php ~ >

 これは使っているXSLTプロセッサのSaxonのバグではないかと思いSaxonicaのバグ報告にポストしたのが以下です.

https://saxonica.plan.io/issues/4321

 Kay博士の言うには、HTMLでは`>`で閉じるのが仕様とのこと!確かにHTML 4.0.1の仕様の該当箇所を見ると以下のように書いてあります.

B.3.6 Processing Instructions

Processing instructions are a mechanism to capture platform-specific idioms. A processing instruction begins with <? and ends with >

 エッ!全然知らなかった!XMLでは次のように書かれていて、間違いなく`?>`で終わるのです.

2.6 Processing Instructions

https://www.w3.org/TR/xml/#sec-pi

[16] PI ::= '<?' PITarget (S (Char* - (Char* '?' Char*)))? '?>'
[17] PITarget ::= Name - (('X' | 'x') ('M' | 'm') ('L' | 'l'))

 

XMLもHTMLも元はSGMLから生まれてきたものですが、こんなところが違うんですね.恥ずかしながら10数年間XMLをやってきて初めて知りました.それにしてもこの世界はどんどん新しいものが出てくるようで、その実昔の仕様を引きずっていたり、奥の深いものがあります.まだまだ勉強しなければならないと痛感しました. 

※ この件、Stckoverflowに質問して、回答者の方にはお世話になりました.この場を借りてお礼いたします

HTMLにおける処理命令

Access 2019 の VBA

私はもう子供は大きくなってしまったので実感がないですが、10月から消費税値上げと引き換えに保育の無償化(と言ってもタダにはならない)が始まるそうです、私がお世話になっていた園でも延長保育に関わるお金を集金するのに自治体の定める基準にのっとって事務を今年から変えねばならず大変とのこと.ついにお鉢がまわって集金事務のシステムの仕様変更が園長先生から私のところまで連絡が来てしまいました.
園ではもう20年前に私が作ったMicrosoft Accessの集金システムをずっと使い続けているのですが、市の徴収基準の区分にのっとって一部を修正しなければならず、7月は手修正でしのいでいたのですが、9月の請求はプログラムを改修して対応したい意向です.で、早速毎度のこと園のPCからAccessのデータベースをコピーして持ち帰り、家で仕様変更のためのコードの見直しに着手しました.やはりこのような時に大変なのが「自分が20年前の瞬間に何を考えてこのようなコードを書いていたか?」を理解するのに時間がかかることです.もう20年もメンテにメンテを重ねてきたシステムなら止むをえないことです.ともかく自分の書いたコードでも思い出す境地にいたるまで必ず一定の時間が必要です.

コードは次のようなVBAVisual Basic For Application)です.最初はAceess 2003の.mdbでした.VBAは今どきはAccessのコードかExcelのマクロを作る人くらいしか使っていないんじゃないかと思います.世の中いろんな言語が山ほど出てきていますから、これからはもうVBなんてやる人はそういないかもしれません.でも私はAccessMicrosoftが最初に日本語版を市場に投入したバージョン1.1の頃から使っているのでとても懐かしいものです.

f:id:toshi_xt500:20190909003716p:plain

Access 2019のVBAのコード

ご覧いただくとわかりますが、Visual Studioなんかだとインテリセンスによるコード補完をバンバンやってくれるので楽ですが、VBAIDEはそこまでは行ってくれません.Access 2019でも、せいぜいユーザ定義型のメンバーを表示してくれることくらい.昔からですが1行の文法が誤っているとすぐにエラーのダイアログが出て怒られますし、毎回毎回「デバッグ」-「コンパイル」で変数参照のエラーを取らねばなりません.今時にしてみれば遅れていますが、もうMicrosoftVBAに投資することはしてくれないように感じました.

しかし、大切なことはこんな私の作ったポンコツのシステムでも、現実に数十人規模の保育園の徴収事務をまかなえているということです.年に6回、数十万弱の金額が動きます.園長先生言うには、もしこのシステムがなければ、いわゆる事務処理パッケージを買ってカスタマイズにお金をかけねばならず、それはもう大変なこと間違いないということです.私に頼めば(ボランティアなので)ほぼ無料です.ほぼと書いたのはだいたい毎回Accessの最新版に上げねばならず、その分はお金がかかることくらいでしょう.

さて金曜日の夜からコードを見始めて、2日ほどでおよそ新しい方式で請求ができるところまでたどり着きました.もちろんそう甘いものではなく一晩はほぼ徹夜です.また明日からはDITAとXSLTの普段の仕事に戻らねばなりません.ともかく毎日忙しいです.

千畳敷カール

8月のお盆前、父も母もディサービスに行ってくれ、かつ休日だったので思い切って昼から女房と中央アルプス千畳敷カールまで行ってきました.自家用車で駒ケ根高原の先にある黒川のバス停まで行き、そこから専用バスに乗ります.と言ってももう駒ケ根高原で乗客はいっぱいのため、立ち乗りです.狭い1車線しかない山道を途中で何度も下りのバスと行き違いをしながら、やっとロープウェイの乗り場のしらび平に着きましたが、そこで降車時に整理券を受け取りました.ロープウェイは1時間30分待ちとのこと.昼食のおにぎりを食べてしばし待ってからいよいよ番号が呼ばれてロープウェイの乗り場に並びます.一回で60人ほどが乗れるみたい.待った甲斐があって、進行方向の窓側に乗ることができました.千畳敷駅までは10分にも満たない時間ですが一気に900mほどの高低差を登ってくれます.ついた千畳敷は観光客でいっぱい.散策路をぐるっと一回りしてきましたが、やはり標高2600mともなれば、ちゃんと装備をしてきた方が良かったです.

千畳敷カールからは、前岳、宝剣岳、極楽平など望めましたが、反対の南アルプス側はガスっていて見えなかったのがちょっと残念.考えてみれば東京からこちらに引っ越してきてから、もう20年たつのに一度も女房をつれてきてあげていませんでした.紅葉の時期にはまた一緒に来て見せてあげたいと思います.

f:id:toshi_xt500:20190901211814j:plain

千畳敷から見た宝剣岳