Comment by ndriscoll
3 months ago
You're sort of exaggerating the boilerplate there; a more idiomatic, complete template might be:
<xsl:variable name="nav-menu-items">
<item href="foo.xhtml"><strong>Foo</strong> Page</item>
<item href="bar.xhtml"><em>Bar</em> Page</item>
<item href="baz.xhtml">Baz <span>Page</span></item>
</xsl:variable>
<xsl:template match="nav-menu">
<nav>
<ul>
<xsl:apply-templates select="$nav-menu-items/item">
<xsl:with-param name="current" select="@current-page"/>
</xsl:apply-templates>
</ul>
</nav>
</xsl:template>
<xsl:template match="item">
<xsl:param name="current"/>
<li>
<xsl:choose>
<xsl:when test="@href=$current">
<a class="selected"><xsl:apply-templates/></a>
</xsl:when>
<xsl:otherwise>
<a href="{@href}"><xsl:apply-templates/></a>
</xsl:otherwise>
</xsl:choose>
</li>
</xsl:template>
One nice thing about XSLT is that if you start with a passthrough template:
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
You have basically your entire "framework" with no need to figure out how to set up a build environment because there is no build environment; it's just baked into the browser. Apparently in XSLT 3.0, the passthrough template is shortened to just `<xsl:mode on-no-match="shallow-copy"/>`. In XSLT 2.0+ you could also check against `base-uri(/)` instead of needing to pass in the current page with `<nav-menu current-page="foo.xhtml"/> and there's no `param` and `with-param` stuff needed. In modern XSLT 3.0, it should be able to be something more straightforward like:
<xsl:mode on-no-match="shallow-copy"/>
<xsl:variable name="menu-items">
<item href="foo.xhtml"><strong>Foo</strong> Page</item>
<item href="bar.xhtml"><em>Bar</em> Page</item>
<item href="baz.xhtml">Baz <span>Page</span></item>
</xsl:variable>
<xsl:template match="nav-menu">
<nav>
<ul>
<xsl:apply-templates select="$menu-items/item"/>
</ul>
</nav>
</xsl:template>
<xsl:template match="item">
<li>
<xsl:variable name="current-page" select="tokenize(base-uri(/),'/')[last()]"/>
<a href="{if (@href = $current-page) then '' else @href}"
class="{if (@href = $current-page) then 'selected' else ''}">
<xsl:apply-templates/>
</a>
</li>
</xsl:template>
The other nice thing is that it's something that's easy to grow into. If you don't want to get fancy with your menu, you can just do:
<xsl:template match="nav-menu">
<nav>
<ul>
<li><a href="foo.xhtml">Foo</a></li>
<li><a href="bar.xhtml">Bar</a></li>
<li><a href="baz.xhtml">Baz</a></li>
</ul>
</nav>
</xsl:template>
And now you have a `<nav-menu/>` component that you can add to any page. So to the extent that you're using it to create simple website templates but you're not a "web dev", it works really well for people that don't want to go through all of the hoops that professional programmers deal with. Asking people to figure out react to make a static website is absurd.
wow, thank you. your first example is actually what i have been trying to do but i could not get it to work. i did search for examples or explanations for hours (spread over a week or so). i found the documentation of each of the parts and directives used, but i just could not figure out how to pull it together.
your last example is what i started out with, including the pass through template. you may remember this message from almost two months ago: https://news.ycombinator.com/item?id=44398626
one comment for the xslt 3 example: href="" doesn't disable the link. it's just turns into a link to self (which it would be anyways if the value was present). the href attribute needs to be gone completely to disable the link.
unfortunately i hit another snag: https://stackoverflow.com/questions/3884927/how-to-use-xsl-v...
nodes you output don't have type "node-set" - instead, they're what is called a "result tree fragment". You can store that to a variable, and you can use that variable to insert the fragment into output (or another variable) later on, but you cannot use XPath to query over it.
the xsl documentation https://www.w3.org/TR/xslt-10/#variables says:
Variables introduce an additional data-type into the expression language. This additional data type is called result tree fragment. A variable may be bound to a result tree fragment instead of one of the four basic XPath data-types (string, number, boolean, node-set). A result tree fragment represents a fragment of the result tree. A result tree fragment is treated equivalently to a node-set that contains just a single root node. However, the operations permitted on a result tree fragment are a subset of those permitted on a node-set. An operation is permitted on a result tree fragment only if that operation would be permitted on a string (the operation on the string may involve first converting the string to a number or boolean). In particular, it is not permitted to use the /, //, and [] operators on result tree fragments.
so using apply-templates on a variable doesn't work. this is actually where i got stuck before. i just was not sure because i could not verify that everything else was correct.
i wonder if it is possible to load the menu from a second document: https://www.w3.org/TR/xslt-10/#document
edit: it is!
now i just need to finetune this because somehow the $current param fails now.
Ah, I could've sworn that it worked in some version of the page that I tried as I iterated on things, but it could be that the browser just froze on my previously working page and I fooled myself.
Adding xmlns:exsl="http://exslt.org/common" to your xsl:stylesheet and doing select="exsl:node-set($nav-menu-items)/item" seems to work on both Chrome and Librewolf.
4 replies →
Yeah, unfortunately the one criticism of XSLT that you can't really deny is that there's no information out there about how to use it, so beyond the tiny amount of documentation on MDN, you kind of have to just figure out your own patterns. It feels a little unfair though that it basically comes down to "this doesn't have a mega-corporation marketing it". That and the devtools for it are utterly broken/left in the early 00s for similar reasons. You could imagine something could exist like the Godbolt compiler explorer for template expansion showing the input document on the left and output on the right with color highlighting for how things expanded, but instead we get devtools that barely work at all.
You're right on the href; maybe there's not a slick/more "HTML beginner friendly" way to get rid of the <xsl:choose> stuff even in 3.0. I have no experience with 3.0 though since it doesn't work.
I get a little fired up about the XSLT stuff because I remember being introduced to HTML in an intersession school class when I was like... 6? XSLT wasn't around at that time, but I think I maybe learned about it when I was ~12-13, and it made sense to me then. The design of all of the old stuff was all very normal-human approachable and made it very easy to bite a little bit more off at a time to make your own personal web pages. "Use React and JSON APIs" or "use SSR" seems to just be giving up on the idea that non-programmers should be able to participate in the web too. Should we do away with top level HTML/CSS while we're at it and just use DOM APIs?
There were lots of things in the XML ecosystem I didn't understand at the time (what in the world was the point of XSDs and what was a schema and how do you use them to make web pages? I later came to appreciate those as well after having to work as a programmer with APIs that didn't have schema files), but the template expansion thing to make new tags was easy to latch onto.
devtools for it are utterly broken
right, that's a big issue too. when the xsl breaks (in this case when i use <xsl:apply-templates select="$nav-menu-items/item">) i get an empty page and nothing telling me what could be wrong. if i remove the $ the page works, and the apply-templates directive is just left out.