🗺️

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

2022/07/14に公開
5

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()右の第一引数として渡す、はず。

参考資料

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()が登場しました。ここまで長くなると!の手軽さはないですね。