There's a brute-force solution. I'll demonstrate for two attributes instead of three.
(
//web:link[@text != 'Login' and @href != 'Login.php'
and not(//web:link[@text = 'Login' or @href = 'Login.php'])]
| //web:link[@text != 'Login' and @href = 'Login.php'
and not(//web:link[@text = 'Login'])]
| //web:link[@text = 'Login' and @href != 'Login.php'
and not(//web:link[@text = 'Login' and @href = 'Login.php'])]
| //web:link[@text = 'Login' and @href = 'Login.php']
)[1]
That is, select all the links where neither attribute matches, but only if there's no link that has a better match. Then select all the links that have the lesser attribute match, but only when there are no links with the superior attribute matching. The select links where only the first attribute matches, but only if there are no links where both attributes match. Then select links where both attributes match. Only one of those four conjuncts will be non-empty, so the "|
" operator never actually combines anything. Finally, select the first link in document order, in case any of those node-sets had more than one element.
The reason I only did two attributes instead of three is because I didn't want to type out all eight cases. You can omit the first case if you're not interested in any links unless at least one of the attributes matches.
This is a situation where you might be better off just selecting all the candidates in the much simpler query Jeff showed, and then using other code to rank the results afterward, where you can more readily use iteration and variables to choose the best candidate.
If you can use XPath 2, then you can use the comma operator (or the concat
function) to join node sequences (which supersede node-sets). Try this, for example:
(
//web:link[@text = 'Login' and @href = 'Login.php' and @index = 0]
, //web:link[@text = 'Login' and @href = 'Login.php' and @index != 0]
, //web:link[@text = 'Login' and @href != 'Login.php' and @index = 0]
, //web:link[@text = 'Login' and @href != 'Login.php' and @index != 0]
, //web:link[@text != 'Login' and @href = 'Login.php' and @index = 0]
, //web:link[@text != 'Login' and @href = 'Login.php' and @index != 0]
, //web:link[@text != 'Login' and @href != 'Login.php' and @index = 0]
, //web:link[@text != 'Login' and @href != 'Login.php' and @index != 0]
)[1]
As an aside, here's an easy way to assign a rank to each link, which makes comparing them pretty straightforward. Imagine a bit field, one bit for each attribute you want to check. If the first attribute matches, set the left-most bit, else leave it unset. If the second attribute matches, set the next most significant bit, etc. So for your example, you get the following bit values:
011 link A: text='Sign In', href='Login.php', index=0
100 link B: text='Login', href='Signin.php', index=15
110 link C: text='Login', href='Login.php', index=22
To select the best match, treat the bit fields as binary numbers. Link A has a score of 3, link B a score of 4, and link C a score of 6. (This is a little reminiscent of how the specificity of CSS selectors is determined.) This is a way of modeling the ordering criteria, but now that I've typed it all out, I don't quite see that it leads to any concise solution in XPath.