A question that was raised by a Fonto developer, how it was resolved and why Fonto behaved like that in the first place. This week, a partner got into a fight with XPath when trying to target a specific element!
A support issue came in with the following description:
Hello, I am trying to select the
<p class="my-class"/>
inside the second column of a table. My selector looks like this:configureProperties(sxModule, xq`self::p[@class="my-class" and ancestor::div[@class="my-other-class"]/descendant::tr/td[2]]`)
. But this XPath selects all the<p class="my-class"/>
in all the columns.In the XPath playground, the selector
//div[@class="my-other-class"]//tr/td[2]/p[@class="my-class"]
works like I expect it, but in Fonto that selects almost all<div/>
elements.What am I doing wrong?
These selectors are not working because they are looking around a bit too much.
Let’s go over the parts:
- Usually the
self::
part gives a good hint to which XML elements the XPath is matching against. In this case thep
element. - From that
<p/>
, the XPath is looking for the nearest<div/>
ancestor, - From the div, we start looking at the descendants for any
<tr/>
element with at least two<td/>
children.- Ignoring any relation between the original
<p/>
element and the<tr/>
. As long as there is any row with two cells, this part of the selector will match!
- Ignoring any relation between the original
To make the selector work like we want, we should split it up in a few parts:
self::p[@class="my-class"]
to cover the class requirement of the paragraph,ancestor::div[@class="my-other-class"]
to cover the test we had in the original query,parent::td[count(preceding-sibling::td) = 1]
to test whether we are in the second cell of a row (our parenttd
has exactly onetd
preceding sibling.)
In total, it’s this: self::p and ancestor::div[@class="m-other-class"] and parent::td[count(preceding-sibling::td) = 1]
. We tested this using the selector testing mode of the XPath playground: Fonto XPath | Playground.
But why?
Fonto selectors are a lot like XSLT patterns, but with some caveats. We often get the question why we did not ‘just go’ for XSLT patterns instead, but invent our own style?
There is no single reason why Fonto selectors are not like XSLT. Primarily it is because the API organically grew over the years. When we started out, there was no (usable) well-maintained XPath or CSS engine we could plug into Fonto, so we started with a fluent API. This API is still available in the earliest commits of our XPath 3.1 engine: matchNodeName.js, last changed in 2015.
// The old fluent API equivalent to `self::p[parent::div]: matchNodeName('p').requireParent('div')
Because of the verbosity of the fluent API and the increasingly complex selectors, we started development on FontoXPath. We were not able to redesign the whole infrastructure of matching nodes to selectors so we chose to keep with the self::p
style selectors, which match nodes.
I hope this explained how Fonto works and why it works like that. During the years we built quite the product and we are aware some parts work in unexpected ways for those who have not been with it from the start. If you have any points of Fonto you would like some focus on, we are always ready and willing to share! Reach out on Twitter to Martin Middel (@dr_rataplan) or file a support issue!
Developer advocate / Evangelist. Has been with Fonto since it all began in 2013. He’s currently designing the next steps in Fonto Developer APIs with the input of our valuable partners.
In his spare time, Martin is an avid home brewer.
Receive updates on new Fonto Why & How blog posts by email