Webのトホホ(その1)

昨年来ずっとHTML(.php)ばかりやっていて、XSL-FOの世界はほとほと忘れてしまいました.それでもオープンソースで出しているリポジトリに、時たま障害報告や改善要望が寄せられるので、ちょこちょことメンテしています.さて今は仕事の主になってしまったXMLHTML5(.php)ですが、実際はトホホの連続でした.

まあみんなWebを使っているけれど、その裏方でいかほどのWeb技術者が苦労しているか、なかなかわからないものです.そしていざ自分がやってみると、Webの世界のあまりの技術の進展の速さについてゆけないのが正直です.たぶん自分もその一人.

そして、私にとってはHTMLとCSSはずっとトラウマです.本当を言えばダイキライ!元はまだNetscapeが健在だったり、MicrosoftIEを出し始めたり、そのうち悪乗りして、Officeの文書をIEでしか解釈できないようなHTMLに出したり、ブラウザ大戦争があったりとWebの世界はまあ思い出すだけでもろくなことがありませんでした.こんな時代をすべてはたから見てきたので、使いはすれども、自分で開発する立場からは遠く離れていました.

しかし自分のことを考えても、システム開発上で沸いた疑念や問題点、わからないことは今ではWebの検索で調べるのがあたりまえとなりました.昔は専門書を仕入れて、いつもわからない点のキーワードを索引から拾い上げてしみじみ本で調べるのがあたりまえでした.しかし、検索の発達した今は、いちいち重い専門書を開くのがおっくうでたまりません.Webの検索の方がはるかに多くの情報を入手できるし、同じことで悩んでいる人は世界には居るもので、例えばはるか彼方の海外のStackoverflowのやりとりから解決策を得ることもしばしばです.

やっぱり、Webから身を遠ざけている訳にはゆきませんね.という訳で、やりはじめた仕事ですが、経験した「トホホ」を紹介したいと思います.

最初は「Quote " in attribute name. Probable cause: Matching quote missing somewhere earlier.」というエラーメッセージです.こんなメッセージに出会った人はまずいないかもしれません.出会ったのは、必死に開発を重ねて、.phpを生成するところまでやっとたどり着き、自分のPCでXAMPPを立ち上げてローカルでその.phpファイルを読み込んだ時です.

意図した画面とはまったく違った崩れるだけ崩れはてたWebページ.しかも(私には)訳のわからない、JavaScript満載のものでした.一瞬「やっぱり私の実力じゃダメか」とあきらめかけたんですが、ChromeでF12を押してデバッグコンソールを立ち上げて、ブラウザが解釈したDOMを見ると、見事に破壊されつくしているのがわかりました.

ブラウザがDOMに展開された結果を見ると、なんとlink要素でCSSを参照している箇所が分断されていています.ある箇所まではhtml/headに、それから下はなんとhtml/bodyに(しかも入れ子に)展開されています.こんな状態ではまともに表示されるはずがありません.

f:id:toshi_xt500:20200922212956p:plain
ボコボコのHTMLのDOM

しかしChromeの「ページのソースを表示(Ctrl + U)」で見るとPHPが出しているのは意図通りのHTMLです.決して間違っていない.

悩みに悩んだんですが、別のブラウザではどうかとFireFoxを試すとやはり同じように展開されたHTMLのDOMは散々な状態です.ここでFireFoxのHTMLソース表示をやってみると、link要素の箇所にカーソルを持って行ったとき、「Quote " in attribute name. Probable cause: Matching quote missing somewhere earlier.」というエラーが表示されます.

f:id:toshi_xt500:20200922213107p:plain
Firefoxの出してくれるエラー

しかし、ダブルクォートをいくら追っても、決して間違ってはいません.そこでもう一度、html/headとhtml/bodyで分断されたCSSへの参照のlink要素をタイプし直して実行すると、今度はエラーが出る箇所が下へ下へとずれます.そう原因は実は要素名と最初の属性名の間の空白でした.これが ではなく (NO BREAK SPACE)になっていました.こんなの意図的に入れる訳がないので、どこかでコピー/ペーストする際に混入したみたいです.

ブラウザのHTMLのリーダーは、こんなコードが入っていても、どんどんHTMLを解釈してとんでもないDOMを作り上げます.ブラウザなのでどんなHTMLでもエラーを出さずにそれなりに出してしまうところがスゴク恐ろしい.

しかしこんなエラーでも丹念に出してくれたFireFoxには感謝です.普段Chromeしか使っていないですが、初心者のエラーでもちゃんと表示してくれるのには頭が下がりました.

VB: 2次元配列を行の単位で処理する

XSLTとはまるで縁のないVisual Basicの話です.数年前会社が新しい社員を募集したとき、面接に参加したら、来ていた人は「Visual Basicできます」でした.「VBってC#C++やっている人からするとディスられますよね?」って聞いたら「まあそういうこともありますね」だったと思います.今の会社はC++一辺倒で、他に有ってもJavaくらいなので、(たぶん)私が「Visual Basicでできます!」っていったら間違いなくMLでボロクソに言われるか、スリッパが飛んでくる感じです.

私事で恐縮ですが、Visual Basicどうこう以前のMS-DOSの時代にQuick Basic 4.5というのがありました.初めてフリーになったときに仕事を探しにいったら「一本5万円で組んでみますか?」と言われて、自分で購入して使い始めました.Quick Basicだったのは、その相手先の会社さんが、その前は「スパゲティプログラム量産間違いなし」のN88 BASICでやってたからなんじゃないかと思います.しかしQB45は廉価版ゆえコンパイラの品質も良くなく、よくこけました.そこでもうちょっと高価版のコンパイラを搭載したMicrosoft Basic Professional Development Systemというのを買って乗り換え、仕事はほぼBasic一辺倒でした.DOSからWindowsに代わったときにMSから発売されたVisual Basicはお試し英語版だったと思いますが、DOSでは味わえなかったカッコイイフォントでGUIでコードが表示されて感激したものでした.
そのVisual Basicも今年の3月にMSの.NETのチームのブログで

We Do Not Plan to Evolve Visual Basic as a Language

「言語としてVisual Basicを進化させる予定はありません」

と発表されてからは、もうVBも終わりなのか?と悲しくなったりしますが、VB自体は2016年には25年の誕生日を迎えて久しいので、言語自体はもうそれほど進化する必要もないのでは?なんて思ったりします.

さて本題、実は次のようなExcelのワークシートから、VSTOで作ったアドインでシートの内容を配列に読み取り、意味のあるデータに加工しようと思います.

f:id:toshi_xt500:20200823000701p:plain
取り出してデータに加工したいExcelのシート

そうするとかならず使わねばならないのが2次元の配列です.実は配列苦手です.あたりまえなのですが、そもそも行と列のインデックスを指定しないと要素にはアクセスできません.あながちでてくるのが2段のForループです.

例えば、データ行をmDataRowという二次元配列に無事格納した後、空の行を調べるとすると

        Dim IsEmpty As Boolean
        For i = 0 To mDataRow.GetLength(0) - 1
            IsEmpty = True
            For j = 0 To mDataRow.GetLength(1) - 1
                If mDataRow(i, j).value <> "" Then
                    IsEmpty = False
                End If
            Next
            If IsEmpty Then
                Debug.WriteLine("Empty row no=" & i)
            End If
        Next

なんていうForループが出てくるに決まっているからです.(そしてこういうコードを平気で書いていると永久にディスられると思うんですが...)

考えたのは、こんな昔の書き方とは違って

  • 行や列単位で2次元配列にアクセスできるようにし
  • LINQを使って、もっと「スマート」に書けないか?

ということです.贅沢ですかね?

で普段はVBなんて数年間やっていないのでWebを探したら、「拡張メソッド」というのがありました.実は上記のmDataRowの要素はCellInfoというクラスのインスタンスなんですが、これを一般化して、array As T(,)の行をIEnumrableとして取り出せる拡張メソッドを書いてみました.以下のような感じです.

Imports System.Runtime.CompilerServices

''' <summary>
'''  Extend two dimensional array : get IEnumerble or list of row 
''' </summary>
Public Module ArrayEx

    <Extension()>
    Public Iterator Function GetRow(Of T)(ByVal array As T(,), ByVal row As Integer) As IEnumerable(Of T)
        For i As Integer = 0 To array.GetLength(1) - 1
            Yield array(row, i)
        Next
    End Function

    <Extension()>
    Public Iterator Function GetRows(Of T)(ByVal array As T(,)) As IEnumerable(Of IEnumerable(Of T))
        For i As Integer = 0 To array.GetLength(0) - 1
            Yield array.GetRow(i)
        Next
    End Function

    <Extension()>
    Public Function GetRowsAsList(Of T)(ByVal array As T(,)) As List(Of IEnumerable(Of T))
        Dim list As List(Of IEnumerable(Of T)) = New List(Of IEnumerable(Of T))
        For i As Integer = 0 To array.GetLength(0) - 1
            list.Add(array.GetRow(i))
        Next
        Return list
    End Function

End Module

こんな風に定義しておくと、空の行を調べるプログラムはLINQ+拡張メソッドを使って次のように書けます.

        Dim rows As IEnumerable(Of IEnumerable(Of ATLCellInfo)) = DirectCast(mDataRow, CellInfo(,)).GetRows
        Dim emptyRowIndex2 = rows _
                             .Select(Function(row, i) New With {.Row = row, .Index = i}) _
                             .Where(Function(aci) aci.Row.All(Function(cell) cell.value = "")) _
                             .Select(Function(aci) aci.Index)
        For Each i As Integer In emptyRowIndex2
            Debug.WriteLine("Empty row no=" & i)
        Next

例えば結果は

Empty row no=5
Empty row no=29

なんて出てくれます.

エッ?Forループで回した方が簡単で早いだって?確かにそうかもしれません.ただ拡張メソッドを定義しておくだけで、LINQで一発で問い合わせのように書いて結果が得られるところがいいと思います.この例だと、「何番目」というのを出さねばならなかったので、クェリ構文でなくメソッド構文しか書き方がわかりませんでした.だからちょっと冗長.でももし拡張メソッドがなければ、いくら望む処理関数や手続きにカプセル化してもその中で毎回のようにForループで回すオンパレードになってしまいますよね.これではウン十年前から進化がありません.

XSLT 3.0への道(35) xsl:accumulatorでスタックを作る

「XMLの鬼門:処理命令」で、oXygenなどのエディタが生成する処理命令は通常のXSLTスタイルシートでは処理が難しいことを説明しました.
ではこれを解決するのは何か?恥ずかしながら、以下のSaxonのドキュメントを見るまで知りませんでした.
saxon:assign
https://www.saxonica.com/html/documentation/extensions/instructions/assign.html

あたりまえですがXSLTでは変数は一回初期化したら、そのあとは値を変更できませんでした.つまりimmutableの性質を持ちます.当然、再代入する命令は提供されていません.でもsaxon:assignはこれを可能にしちゃうのです.

例えば

<xsl:variable name="stack" as="processing-instruction()*" saxon:assignable="yes" select="()"/>

グローバル変数でやっておいて、

<xslt:template match="processing-instruction()">

で処理命令を見張っていて、もし開始の処理命令にマッチしたら

<saxon:assign name="stack" select="(., $stack)"/>

とやってスタックにpushし、もし終了の処理命令にマッチしたら、

<saxon:assign name="stack" select="remove($stack,1)"/>

とスタックからpopしてやるというものです.こうして、node()の処理でこのスタックを参照して、先頭を拾って(peek)、空でなければそのnode()は「処理命令の中に存在する」とみなせば良いからです.

でもsaxon:assignは有償のSaxon PE/EEがないと使えません.そして上記のURLのドキュメントには、

This instruction works only with global variables. It should be regarded as deprecated, and may be withdrawn completely at some time in the future, since it is incompatible with many of the optimizations that Saxon now performs.
There are better ways of achieving the same effect. Consider constructs such as tunnel parameters, xsl:iterate, higher-order functions, or xsl:accumulator.

とあったからです.将来のバージョンではなくなっちゃうのでは困ります.そこで見つけたのがxsl:accumulator でした.xsl:accumulatorはSaxon-HE(つまりオープンソース版)でも動作してくれます.これでスタックを実装するのは実はごく簡単で、次のようにコーディングしてやれば自動的にやってくれるのです.

    <xsl:variable name="cCommentStartPiName" as="xs:string" static="yes"  select="'oxy_comment_start'"/>
    <xsl:variable name="cCommentEndPiName" as="xs:string" static="yes"  select="'oxy_comment_end'"/>
    <!--コメント開始の処理命令を判定する-->
    <xsl:function name="ahf:isCommentStartPi" as="xs:boolean">
        <xsl:param name="prmNode" as="node()"/>
        <xsl:sequence select="$prmNode/self::processing-instruction()[name() eq $cCommentStartPiName] => exists()"/>
    </xsl:function>
    <!--コメント終了の処理命令を判定する-->
    <xsl:function name="ahf:isCommentEndPi" as="xs:boolean">
        <xsl:param name="prmNode" as="node()"/>
        <xsl:sequence select="$prmNode/self::processing-instruction()[name() eq $cCommentEndPiName] => exists()"/>
    </xsl:function>

    <xsl:accumulator name="glCommentPi" as="processing-instruction()*" initial-value="()">
        <!-- 開始の処理命令にマッチしたらpushする-->
        <xsl:accumulator-rule match="processing-instruction()[ahf:isCommentStartPi(.)]" select="(., $value)"/>
        <!--終了の処理命令にマッチしたらpopする-->
        <xsl:accumulator-rule match="processing-instruction()[ahf:isCommentEndPi(.)]" select="remove($value,1)"/>
    </xsl:accumulator>

この宣言だけでスタックのpush/popを自動化できてしまいます.あとはテキストノードの処理で、スタックの状態を見に行けば良いだけです.このxsl:accumulatorの中を覗くのが以下の変数です.

<xsl:variable name="commentStartPi" as="processing-instruction()?" select="accumulator-before('glCommentPi') => head()"/>

accumulator-before('glCommentPi') => head()は、そのtext()がマッチした直前のスタックの先頭を拾ってきてくれます.もし$commentStartPi => empty()なら、コメントの処理命令に囲まれておらず、$commentStartPi =>exists()なら、コメントの処理命令に囲まれています.

次のサンプルトピックを使って

<?xml version="1.0"?>
<!DOCTYPE topic PUBLIC "-//OASIS//DTD DITA Topic//EN" "topic.dtd">
<topic id="topic_55762342DAC09974FDC8" xml:lang="en-US">
	<title>Typographic elements testing</title>
	<shortdesc>The typographic elements are used to highlight text with styles (such as bold, italic, and monospace). Never use these elements when a semantically specific element is available. These elements are not
intended for use by specializers, and are intended solely for use by authors when no semantically
appropriate element is available and a formatting effect is required.</shortdesc>
	<body>
    <section>
            <title>sup/sub element</title>
            <p>The superscript (<xmlelement>sup</xmlelement>) element indicates that text should be
                superscripted, or vertically raised in relationship to the surrounding text.
                Superscripts are usually a smaller font than the surrounding text. Use this element
                only when there is not some other more proper tag. This element is part of the
                <?oxy_comment_start author="toshi" timestamp="20200719T210652+0900" comment="&lt;p&gt;要素をまたがったコメントが書けてしまいます.
これは通常のXSLTスタイルシートでは処理しようがありません."?>DITA
                highlighting domain.</p>
            <p>A subscript (<xmlelement>s<?oxy_comment_end?>ub</xmlelement>) indicates that text
                should be subscripted, or placed lower in relationship to the surrounding text.
                Subscripted text is often a smaller font than the surrounding text. Formatting may
                vary depending on your output process. This element is part of the DITA highlighting
                domain.</p>
            <p>The power produced by the electrohydraulic dam was 10<sup>10</sup> more than the
                older electric plant. The difference was H<sub id="sub_99876530FDA634CE2A34"
                >2</sub>O.</p>
        </section>
    
    </body>
</topic>
<?oxy_options track_changes="on"?>

次のようなテンプレートで検証してみると

    <xsl:template match="text()>
        <xsl:variable name="commentStartPi" as="processing-instruction()?" select="accumulator-before('glCommentPi') => head()"/>
        <xsl:variable name="isCommented" as="xs:boolean" select="$commentStartPi => exists()"/>
        <!-- ahf:getHistoryXpathStr()はノードに対応するXPathを表示-->
        <xsl:variable name="xpath" as="xs:string" select="ahf:getHistoryXpathStr(.)"/>
        <xsl:choose>
            <xsl:when test="$isCommented">
                <xsl:message select="'XPath='|| $xpath||' comment PI=',$commentStartPi"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:message select="'XPath='|| $xpath||' comment PI: none'"/>
            </xsl:otherwise>
        </xsl:choose>
        ...
    </xsl:template>

次のようなデバッグ出力を得ることができます.つまり、処理命令の始まりと終わりを捕捉できています

     [xslt] XPath=/topic/text()[1] comment PI: none
     [xslt] XPath=/topic/title[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/text()[2] comment PI: none
     [xslt] XPath=/topic/shortdesc[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/text()[3] comment PI: none
     [xslt] XPath=/topic/body[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/title[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/text()[2] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[1]/xmlelement[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[1]/text()[2] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[1]/text()[3] comment PI=<?oxy_comment_start author="toshi" timestamp="20200719T210652+0900" comment="&lt;p&gt;要素をまたがったコメントが書けてしまいます.
     [xslt] これは通常のXSLTスタイルシートでは処理しようがありません."?>
     [xslt] XPath=/topic/body[1]/section[1]/text()[3] comment PI=<?oxy_comment_start author="toshi" timestamp="20200719T210652+0900" comment="&lt;p&gt;要素をまたがったコメントが書けてしまいます.
     [xslt] これは通常のXSLTスタイルシートでは処理しようがありません."?>
     [xslt] XPath=/topic/body[1]/section[1]/p[2]/text()[1] comment PI=<?oxy_comment_start author="toshi" timestamp="20200719T210652+0900" comment="&lt;p&gt;要素をまたがったコメントが書けてしまいます.
     [xslt] これは通常のXSLTスタイルシートでは処理しようがありません."?>
     [xslt] XPath=/topic/body[1]/section[1]/p[2]/xmlelement[1]/text()[1] comment PI=<?oxy_comment_start author="toshi" timestamp="20200719T210652+0900" comment="&lt;p&gt;要素をまたがったコメントが書けてしまいます.
     [xslt] これは通常のXSLTスタイルシートでは処理しようがありません."?>
     [xslt] XPath=/topic/body[1]/section[1]/p[2]/xmlelement[1]/text()[2] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[2]/text()[2] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/text()[4] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[3]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[3]/sup[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[3]/text()[2] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[3]/sub[1]/text()[1] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/p[3]/text()[3] comment PI: none
     [xslt] XPath=/topic/body[1]/section[1]/text()[5] comment PI: none
     [xslt] XPath=/topic/body[1]/text()[2] comment PI: none
     [xslt] XPath=/topic/text()[4] comment PI: none

これを応用すれば、PDFに以下のように出力することも可能です.

f:id:toshi_xt500:20200723205515p:plain
処理命令によるコメントをPDFで表す(コメントアイコンは見やすいように移動してあります)

XSLT 3.0は痒い所に手が届くようなxsl:accumulatorを用意してくれました.使わなければ損ですね.

※ ちなみにoXygenのコメント機能は、さらに複雑で次のような複数のauthorによるコメントの「範囲の入れ子」「コメントに対するコメント」ができちゃいます.このような場合、スタックの処理は上記で紹介したものよりずっと複雑になります.

f:id:toshi_xt500:20200723210849p:plain
入れ子のコメント、コメントに対するコメント

XMLの鬼門:処理命令

XMLを処理する上で普段めったにお目にかからないのが処理命令です.
普通オーサリングで意図的に処理命令を書く人はまずいらっしゃらないでしょう?ではどのような場面で処理命令は使われるのでしょうか?
OxygenやXMetaLなんかのXMLエディタを使うとTracking Change(変更履歴の記録)の機能があります.実はここで挿入や削除を記録しするのに処理命令を使うのです.例えばOxygenだと、Trackin Changeの機能をONにしておくと次のように挿入/削除が表示されます.また自由に文書中にコメントを入れることができます.まるで履歴を管理するMicrosoft Wordみたいですね.

f:id:toshi_xt500:20200719211925p:plain
OxygenのTracking Change機能

挿入されたところはこんな感じで処理命令に囲まれます.

<?oxy_insert_start author="toshi" timestamp="20200719T192312+0900"?>Use this element
only when there is not some other more proper element. For example, for specific items such as GUI
controls, use the <xmlelement>uicontrol</xmlelement> element. This element is part of the DITA highlighting domain.<?oxy_insert_end?>

削除された内容は処理命令の中に保存されます.

<?oxy_delete author="toshi" timestamp="20200719T203739+0900" content="&lt;b&gt;STOP!&lt;/b&gt; This is &lt;b&gt;very&lt;/b&gt; important!"?>

コメントも次のように範囲が囲まれます.

<?oxy_comment_start author="toshi" timestamp="20200719T204110+0900" comment="重要な指摘!Typographic elementは出力の体裁を指定するので、論理的な意味は持っていない."?>
Use this element only when there is not some other more proper element.
<?oxy_comment_end?>

これをXMLエディタの表示と同じように出したいお話があります.企業や組織が文書を作るときは、必ずレビューするからです.レビューはやっぱりPDFに出して行いますので必要なのです.

このような処理命令をXSLTスタイルシートで処理するにはどうしたらよいでしょうか?上記のような例だと、ある要素の下のノードを、

  • 挿入の開始の処理命令~挿入の終了の処理命令
  • 削除の処理命令(これは単独)
  • コメントの開始の処理命令~コメントの終了の処理命令

でグルーピングすれば良いように見えます.

ところがこの目論見(もくろみ)は無残にも打ち砕かれます.それは次のような、要素をまたがったコメントが出てきてしまうからです.

f:id:toshi_xt500:20200719212548p:plain
要素をまたがって書けてしまうコメント

この箇所のXMLはつぎのようになっています.

<p>The superscript (<xmlelement>sup</xmlelement>) element indicates that text should be
       superscripted, or vertically raised in relationship to the surrounding text.
        Superscripts are usually a smaller font than the surrounding text. Use this element
      only when there is not some other more proper tag. This element is part of the
      <?oxy_comment_start author="toshi" timestamp="20200719T210652+0900" comment="&lt;p&gt;要素をまたがったコメントが書けてしまいます.
これは通常のXSLTスタイルシートでは処理しようがありません."?>DITA
       highlighting domain.</p>
<p>A subscript (<xmlelement>s<?oxy_comment_end?>ub</xmlelement>) indicates that text
       should be subscripted, or placed lower in relationship to the surrounding text.
       Subscripted text is often a smaller font than the surrounding text. Formatting may
       vary depending on your output process. This element is part of the DITA highlighting
       domain.</p>

つまり処理命令はXMLの要素の開始タグ/終了タグなど無視して、どこの位置にも書けてしまうということです.これはノードのグルーピングでは太刀打ちができません.これが処理命令が「XMLの鬼門」である所以(ゆえん)です.

ところが、こんなどうにもなりそうにない処理命令をバッチリ捕捉できるXSLTの手段があります.それはxsl:accumulatorです.以下でどのように捕捉できるかを紹介します.

大型特殊免許

うちは農家ですので、耕起、代かき、田植えも無事終わり、除草剤も撒いて梅雨になれば一息です.代かきにつかうロータリーのハローをそろそろと思い点検にメーカーに出したところ、いきなり電話がかかってきて、

  • ~さんのロータリーは、全幅が180cmあります.道交法の改正で、170cmまでは小型特殊でOKなんですが、180cmだと大型特殊免許が要ります.
  • できる手段は、大型特殊免許を取ってもらうか、折り畳み式のハローで新規購入して道路走行時の幅を170cmに納めるか?です.

と言われてしまいました.ちなみに折り畳み式のハローを買うだけで最低で50数万円要るとの事.たった10cmのためにまさかこんな羽目に合うとは思いませんでしたが、決めない限りディラーに出した代かきロータリーはどうにも行き場を失ってしまい、来年の代かきも出来なくなります.

なので、この歳で教習所に通うことにしました.幸い従妹が地元の農業法人のオペレータを頼まれていて大型特殊を数年前に取っていたので話を聞くことができましたが、大型特殊はホイールローダーを操作して実地試験に合格しなければなりません.ホイールローダーは、丁度雪かきなんかでも使ったり、建設現場で土砂を運搬トラックに積み下ろしする重機です.

ラクタだけで、それにたった年に1回代かきをするだけなんですが、免許を取るのにそんな言い訳は通用しません.今4時間乗りました.以下が教習所にあるKOMATSUのWA100というホイールローダーです.

f:id:toshi_xt500:20200620222005j:plain
KOMATSU WA-100

ともかく私の体力では運転席によじ登って、座席に座るだけで、息がつきます.ハンドルは左手だけで、ノブを回して操作します.右手は、バケットの操作なんですが、発着地点で教官に指示されるのを聞くのがせいぜいで、あとで思い出そうとしても忘れてしまっています.ちなみに、大型特殊の検定コースは2つあって、もちろん暗記していなければなりません.

ここにKOMATSUのWA100のYouTubeのビデオがありますね.

www.youtube.com


ああ、こんな状態であさっては最後の教習で、明々後日は検定試験です.こんな受かってくれるものなんでしょうか?

CCMSのプラットフォームとXSLTスタイルシート

今まで山ほどDITAのXSLTスタイルシートを書いてきましたが、その主なプラットフォームはWindowsでした.もちろんお客様の中にはCCMSを導入していない場合もありますが、導入している場合はそのプラットフォームはWindowsでした.
つまり、手元のWindows 10でスタイルシートを書いて試験して、それがお客様のWindowsマシンかCCMSのパブリッシングサーバーでもすぐ動いてくれたのです.
でもこの頃はCCMSは競争が大変ですね.Windows だけではなく、この頃はLinuxプラットフォームのCCMSも増えてきました.でもWindowsにしてもLinuxにしても、XSLTスタイルシートは同じではないのでしょうか?
実は違ったのです.結構痛い目に遭いました.こんなストーリーです.

  1. DITAのマップの中にトピックだけでなく、様々なXMLのリソースを記述してスタイルシートで使用します.例えばターゲットの国特有の共通言語リソースなど.
  2. このようなものはDITAではないXMLに記述するので、で記述します.
  3. でもこのようなtopicrefはPDFを作る前のマージ後中間ファイルでは綺麗に消え去ってしまいます.リソースなので要らないと言う訳です.
  4. このため、リソースファイルを使用するには、自前でマップを読んで、その在りかを記憶しておく必要があります(ANTのプロパティにです)これをスタイルシートのパラメータとして@hrefを渡してやります.

DITAのマップ@hrefはURIです.でもCMSの場合、パブリッシュの時はフラットな空間になんでもチェックアウトされてしまうので、ファイルURIではなくパスですね.
そして多くの場合、スタイルシートではこれらのリソースをdoc()で読み込もうとします.doc()のパラメータはURIなので、XSLTのresolve-uri()関数でURIを作ってやるんですが、実はこの結果がWindowsLinuxで異なります.

  • Windowsは"file:/"で始まります.
  • Linuxは"file://"で始まります.

そして、結果をdoc()に渡すと、Windowsはそのdocument-nodeを返してくれますが、Linuxはエラーになってしまいます.止む無く、"file://"をなんとか"file:/"にスタイルシートで直すとちゃんと通ります.

XSLTなのでプラットフォーム共通と思っていたのですが、思わぬところにトラップがありました.覚えておいて損はありません.

Conrefの先のxref

私は(自分としては)開発者のつもりでいて、実際に日本ではサポート業務はやらないんですが、USのサポートにDITA関連の質問が来てしまった場合、「オマエがやれ!」とお鉢がまわってくることがあります.
今回はそんな感じで常連になってしまったインドの会社の方から次のような質問が来ました.

conref(conkeyref)で参照しているブロックの中にxrefがあって、その宛先をhref=".#/[要素の@id]で参照していると、PDFに変換した場合、xrefがハイパーリンクになってくれない.

というものでした.実際に受け取ったデータからサンプルを作り直してみると次のような具合です.

[conref元: cConrefSourceConcept.dita]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE concept PUBLIC "-//OASIS//DTD DITA Concept//EN" "concept.dtd">
<concept id="cConrefSourceConcept">
    <title>Conref Source Concept</title>
    <shortdesc></shortdesc>
    <conbody>
        <div conref="cConrefTargetConcept.dita#cConrefTargetConcept/div-001"/>
    </conbody>
</concept>

[conref先: cConrefTargetConcept.dita]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE concept PUBLIC "-//OASIS//DTD DITA Concept//EN" "concept.dtd">
<concept id="cConrefTargetConcept">
    <title>Conref Target Concept</title>
    <shortdesc>
        Contains conref target elements
    </shortdesc>
    <conbody>
        <div id="div-001">
            <p>This paragraph is contained in <xmlelement>div</xmlelement> element and contains xref element that refers paragraph in parent div. <xref href="#./p-999" format="dita">The reference to paragraph authored using <xmlatt>href=#./element-id</xmlatt> notation.</xref></p>
            <p>Dummy paragraph</p>
            <p>Dummy paragraph</p>
            ...
            <p id="p-999">The referenced paragraph.</p>
        </div>
    </conbody>
</concept>

DITA-OTの初期処理(preprocess)を通した結果は次のように、xrefで参照している先の@id="p-999"がconrefの処理で@id="d7e148"書き換わってしまっています.

[conrefが展開された結果: cConrefSourceConcept.ditaの中間ファイル(見やすいように修正済み)]

<concept id="cConrefSourceConcept">
    <title>Conref Source Concept</title>
    <conbody>
        <div>
            <p>This paragraph is contained in <xmlelement>div</xmlelement> element and 
                   contains xref element that refers paragraph in parent div. 
                   <xref href="#cConrefSourceConcept/p-999"
                    format="dita" ><?ditaot usertext?>The reference to paragraph authored
                    using <xmlatt>href=#./element-id</xmlatt> notation.</xref>
            </p>
            <p>Dummy paragraph</p>
            ...
            <p id="d7e148">The referenced paragraph.</p>
        </div>
    </conbody>
</concept>

この"#./"という記法はDITTA 1.3から取り入れられたもので、いちいちトピックの@idを書かなくとも良いのでお客さんは採用したようです.でも何故かconrefで参照した先にあると動いてくれません.
実はこの"#./"は、conrefされる先にあった場合は期待したようには動きません.上記の例は

conref先が次のようになっていると何の問題もなく動いてくれます.

[conref先: cConrefTargetConcept.dita]

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE concept PUBLIC "-//OASIS//DTD DITA Concept//EN" "concept.dtd">
<concept id="cConrefTargetConcept">
    <title>Conref Target Concept</title>
    <shortdesc>
        Contains conref target elements
    </shortdesc>
    <conbody>
        <div id="div-001">
            <p>This paragraph is contained in <xmlelement>div</xmlelement> element and contains xref element that refers paragraph in parent div. <xref href="#cConrefTargetConcept/p-999" format="dita">The reference to paragraph authored using <xmlatt>href=#topic-id/element-id</xmlatt> notation.</xref></p>
            <p>Dummy paragraph</p>
            <p>Dummy paragraph</p>
            ...
            <p id="p-999">The referenced paragraph.</p>
        </div>
    </conbody>
</concept>

実際のcConrefTargetConcept.ditaの中間ファイルを見ると次のように展開されています.

<concept id="cConrefSourceConcept">
    <title>Conref Source Concept</title>
    <conbody>
        <div>
            <p>This paragraph is contained in <xmlelement>div</xmlelement> element and contains xref element that refers paragraph in parent div. <xref href="#cConrefSourceConcept/d7e148" format="dita"><?ditaot usertext?>The reference to paragraph authored using <xmlatt>href=#topic-id/element-id</xmlatt> notation.</xref></p>
            <p>Dummy paragraph</p>
            ...
            <p id="d7e148">The referenced paragraph.</p>
        </div>
    </conbody>
</concept>

DITA 1.3の仕様を見ると、次のように解説されています.

2.4.2.4 Processing xrefs and conrefs within a conref
http://docs.oasis-open.org/dita/dita/v1.3/errata02/os/complete/part3-all-inclusive/archSpec/base/handling-xref-and-conref-within-topics.html#handling-xref-and-conref-within-topics

つまるところ、"#./"と書けば、参照元トピックの中で参照が解決されます.逆を言えば、DITA-OTは参照先トピックの中での@idの変化に合わせてくれる処理をしてくれないのです.

"#./"はconrefされる側で書くのと、conrefされることはない側で書くのでは違うという事を意識しなければならないようです.

しかしこれはちょっと不便にも思えます.再利用を考えると、自分が書いているトピックはいつ何時他のtopicからconrefされるか、オーサリングする人はわからないのですから.