🗺️

XPath 3で、複数トークンをORでfn:contains-token()判定したい

2022/07/14に公開
5

2024-08-24追記。

fn:contains-token()はDITAやHTMLの@classの値を判定するときに使える便利関数です。この文脈での@classの値というのは次のようなものです。

<div class="col-1 col-2-md col-4-lg" ...>...</div>

fn:contains()が単純な一致判定のために入力全体を走査するのに対し、fn:contains-token()は入力をトークンに分けた上で文字列の一致を見てくれます。

今回の問題は「幾つかの候補文字列のうち、どれかに合致するかを判定したい」というものです。どういうときに使うかというと、たとえば「トークンAまたはトークンBが入力@classに含まれるときtemplatelABを呼ぶ」という処理を行うときですね。XSLT 3.0のパッケージ使わなければこんなん書かない気がしますが。

<xsl:template match="*[トークンAまたはトークンBを含む@classにいい感じにマッチする述部]">
	<xsl:call-template name="templateAB"/>
</xsl:template>

単純には「入力に対しトークンAの有無を判定し、trueならその時点で終了し、falseならトークンBの有無を判定し……」という処理ですね。

fn:contains-token()はどんな関数か

さて、トークンに対し文字列の一致を判定するfn:contains-token()の引数と返り値の型をみてみましょう。

fn:contains-token($input as xs:string*, $token as xs:string) as xs:boolean

入力は1個以上の(*は付いていても省略はできない)xs:stringでよいのに対し、
判定するトークンを示す引数は単一の文字列となっています。複数のトークンを一度に判定はできない、ということですね。

ところで、XPath 3系には便利演算子!があります。

マップ演算子!

マップ演算子!は左辺のシーケンスに対し、右辺をマップで適用します。左辺のシーケンスには複数のitem()が想定されます。
これはパス指定での返り値としてのノードセットなどが分かりやすい対象ですが、XPathで直接、(A,B,...)のようにしても記述できます。

マップされるそれぞれの対象item()は右辺中ではコンテキストアイテム.として記述できます。
左辺がxs:string*であれば、!を挟んだ右辺ではそれぞれのxs:string.となるわけですね。

式 2023-10-20 追記

先の話を踏まえ、次のようなパターンを書くとよいわけです。

*[let $class := @class return ('トークンA','トークンB')!fn:contains-token($class, .) => fn:fold-left(false(),function($head,$rest){$head or $rest})]

let構文はlet $variable := return ...といった記法で、XSLTで<xsl:variable>を使わずともXPathだけで変数を用意可能にします。XPath 3.0からのlet構文で@classを取得しておき、コンテキストアイテムがxs:stringになっても判定したい値を読めるようにしておきます。

2023-10-23 よく考えたらfold-left内で判定すればいいよね。これなら素で書いてもいいかもしれない。

*[let $class := @class return 
('tokenA','tokenB','tokenC'...) => fold-left(false(),
	function($head, $rest){
		$head or fn:contains-token($class, $rest)
	})
]

おまけ AND条件の場合

*[@class => fn:contains-token('トークンA')][@class => fn:contains-token('トークンB')]

述部は左から評価して残った結果を次の述部で……という挙動なので、ANDの場合これでOK。同じ述部内でAND取るのとどっちが速いかはよく分かっていません。=>演算子は左のitem()右の第一引数として渡す、はず。

2024-08-24 someでまあまあ解決ということにする

ふとsomeのことを思い出した(あるいは記憶していなかった)ので追記。
some $item in $sequence satisfies $function then(A) else(B)$sequenceの中のある$item$functiontrue()を返すようなときAを、まったく無ければBを返すという構文。$sequence中の全$item$functiontrue()を返すケースでAを返すような操作ではsomeではなくeveryがある。XSLT 2.0からある文。
some .. in ... satisfies .. then..と、かなりテキスト的な並びなのでシーケンスや函数部分が長くならなければそれなりに見易さを保てるのがポイント。

で、真偽値のみが必要な場合、then() else()はなくとも条件判定部分は機能するので、次のように書ける。

*[some $tk in ('トークンA','トークンB') satisfies fn:contains-token(@class, $tk)]

参考資料

Discussion

AQAQ

fn:contains-token() 便利ですよね。わたしもそれを or したい場面に多々出くわします。

*[('トークンA','トークンB')!fn:contains-token(@class, .)]

これですと、fn:contains-token() の第 1 パラメーター評価時も 'トークンA''トークンB' が各コンテキスト アイテムであるがゆえに、@class の評価でエラー (err:XPTY0020) となるのではないでしょうか。

hidarumahidaruma

これですと、fn:contains-token() の第 1 パラメーター評価時も 'トークンA' と 'トークンB' が各コンテキスト アイテムであるがゆえに、@class の評価でエラー (err:XPTY0020) となるのではないでしょうか。
ご指摘の通りです。ありがとうございます。

別の処理で使っていたものを「@class処理したい」となってガッと書いたものでした。

let文によって元の./@classを保持するように修正。まだ期待の結果になっていないので検討します。

hidarumahidaruma

これはboolean()*で述部としてfalseになっている気がする

AQAQ

当初の式のように ! を適用しただけだと、結果が複数の値になり、最終的な effective boolean value を得られなかったということではないかと。

@class の内容にある程度の行儀良さを期待できる場面では、tokenize(@class) = ('トークンA', 'トークンB')xs:NMTOKENS(@class) = ('トークンA', 'トークンB') でもよいかもしれませんね。

hidarumahidaruma

はい。
当初の方針をやりつつ修正したのでfold-left()が登場しました。ここまで長くなると!の手軽さはないですね。