Monday, December 7, 2020

CSS Within XSLT - a way to simplify simple HTML generation from XSLT

It can be a challenge to keep CSS files up to date as you change XSLT transformations to produce different HTML output. You've got to remember what you changed and then work out which CSS rules are affected and update them. Removing out-of-date rules is important, and can even affect your site’s ranking in search results, but it’s hard to do and requires attention to detail.

Recently I’ve been taking a different approach and I’m finding it much more productive.

I call the method CSS Within (OK, CSS Inside sounded too much like a trademark). It's a result of thinking about the single largest flaw I see in Knuth’s Literate  Programming: it assumes that programmers like writing and will keep documentation up to date. I don’t know about you but I just jump straight to the function and change it in place without taking time to read a bunch of stuff that doesn’t affect how it works.

So, for XSLT, I wanted to move the CSS into the stylesheet, but putting it before each template was a non-starter because I knew I wouldn't edit it. I wanted the CSS within the template itself.

Here’s an example from a stylesheet I was editing just now:

<xsl:template name="mknav">
 
<p class="prevnext">
    <css:rule match="div.index p.prevnext">
      margin-top: -18pt; /* allow for the height of the uparrow */
      margin-bottom: 1px; /* difference between arrow height and ls */

    </css:rule>

    <xsl:call-template name="prevnextlinks">
      <xsl:with-param name="small" select="true()" />
    </xsl:call-template>

  </p>

</xsl:template>

So now i get the CSS styles right where I need them. I can put them anywhere, but if i put them right after the element constructor,  it's super obvious what's going on. I can find them when i need to and i can edit them, and if i decide to delete that p element, and prevnext isn't used anywhere else in the XSLT stylesheet, hey, i just delete the CSS block too. Done.

But what’s really going on here? That css:rule element is actually, of course, an XSLT direct element constructor. It makes an element of that name in the output. Which is not what i want to end up with at all.

Instead, what i want is to gather up all of the CSS rules at the end of processing and write them out to a file. So I have two ways to handle them to achieve this.

The first way is to declare css:rule as an XSLT extension element; i wrote a Java class for Saxon which simply ignores the css:* elements, turning them into an empty sequence when the stylesheet is compiled. That way they don’t even slow down processing.

Then, i have a stylesheet that reads the XSLT stylesheet itself and fishes out the CSS elements and puts them in a CSS file. Note that i want all of the CSS elements, not just the ones in templates that were reached in any one particular document. It’s as easy as adding

    "post-process" : css:writefile#2,

to fn:transform().

If extensions are not supported, i also have an XSLT stylesheet that can load the main stylesheet, strip out the CSS elements, and then continue processing.

Notice also that this markup doesn’t need curly braces, so the CSS doesn’t interfere with text value templates (expand-text=yes) in XSLT. There are CSS elements for media queries, too, which can contain css:rule elements. It’s simpler to do than to describe.

What do you think? Would you like to try it?


10 comments:

Unknown said...

Sounds rather complex?
Why not output it then generate your cSS file in a second pass?

yamahito said...

Generating in a second pass is what he's doing. He's not outputting it because he doesn't want css:* junk in his HTML.

Unknown said...

Understood. Pity XSLT can't output two streams?

yamahito said...

It can, these days.

Liam Quin said...

Yes, XSLT can output multiple streams. But i need all of the CSS in the result, not only the CSS that is in templates that were actually visited - for example because i might have media queries, global styles, or styles for content generated dynamically through JavaScript.

It would be possible for my Java extension to write out the CSS,but i wanted a solution that worked with or without the extension.

Note, although it's two separate steps, it's a single run of XSLT.

Thanks for commenting!

Unknown said...

My goal was no java extension Liam?
Can include from both html and CSS I believe?
Options seem to be two XSLT runs or a java extension?
I guess for a server based transform java would win,
for a static site (I do like the 'closely associated' CSS)
I'd suggest two pass situation?

Liam Quin said...

Yes, what i have works fine without a Java extension. There are two XSLT runs, but both done from a single styleseet, using fn:transform() to run the 2nd from the first. (i'm using xslt 3 - for xslt 1 i'd use two separate stylesheets, one that writes the css and writes out a temp file that's the XSLT without the CSS elements, and then the original transform.

Norman Walsh said...

It's an interesting idea. Do you attempt to generate the whole CSS stylesheet this way, including sections in media queries, or just the "core" bits.

Ari N said...

This is a very cool idea. Where were you six months ago when I was cursing at multiple CSS files lagging behind after updating my XSLTs?

Liam Quin said...

@Norm yes, i generate the entire CSS - there's a css:media element that can contain rules (it has a when attribute) and can go anywhere.

@Ari sorry :) i should have published sooner!

Fonts Across the Libre Desktop: Design and Graphics Focus

Today, each application, and each toolkit, offers font selection. But fonts have become gradualy more complex, and none of the interfaces se...