Occasionally someone will want to print one of my web pages, even though it means losing access to all the interactive parts. There are a few things I do to make this work better:

  1. I try to start all my interactive diagrams in a state where it's informative without interaction. This is not only helpful for printing, but also for people skimming the page. I haven't always done this in the past so I've been trying to go back through my older pages and change them to work this way.
  2. I have a "print stylesheet" using @media print { … } that changes the page style when printing. It changes the font, removes background colors, removes text shadows, and instructs the browser to avoid breaking the page inside a diagram.
  3. (Added today) When printing, I display the URLs for the links on the page. Since you can't click the links, it's useful to display the URLs.

I use this CSS for printing:

@media print {
    @page { margin: 1in; }
    body {
        font-size: 13pt;
        font-family: "Book Antiqua", "Times New Roman", serif;
    }
    header, footer, h2 {
        text-shadow: none;
        color: #000;
        background-image: unset;
        background-color: unset;
    }
    h2, h3 { page-break-after: avoid; }
    figure { page-break-inside: avoid; }
}

I think it might be simpler to use @media screen { } to set some of the colors instead of trying to undo them with @media print { }.

To display URLs, I first tried this CSS rule:

@media print {
    a:before { content: "["; }
    a:after  { content: "] (" attr(href) ")"; }
}

This makes the links display in Markdown format, as [text](url).

Unfortunately, many URLs are quite long. Markdown has a footnote/endnote-style format which works better for long links: [text][1] followed by [1]: url. It's not something I can do in pure CSS. I would have to edit the HTML to make this work. But I'm not actually writing HTML directly. I write XHTML that is transformed into HTML using XSLT. Can XSLT do this transformation for me? Yes! I was able to adapt this XSLT technique to work on my pages.

First, create an XSLT rule that applies to all links:

<xsl:template match="//a">
  <xsl:copy>
    <xsl:apply-templates select="node() | @*"/>
  </xsl:copy>
  <sup class="print-endnote">
    <xsl:number level="any" count="//a" format="[1]"/>
  </sup>
</xsl:template>

This will find links of the form <a href="url">test</a> and turn them into <a href="url">test</a><sup class="print-endnote">[3]</sup>. The count="//a" parameter to xsl:number will generate a counter for all <a> elements, and format it with brackets: the third link will be [3].

Then, at the bottom of the page, make a list of all the links:

<ul class="print-endnote">
  <xsl:apply-templates select="//a" mode="endnote"/>
</ul>

This will loop over all elements that match //a, and then apply this template to them:

<xsl:template match="a" mode="endnote">
  <li>
    <xsl:number level="any" count="//a" format="[1]"/>:
    <xsl:value-of select="@href"/>
  </li>
</xsl:template>

The output will look like this:

<ul class="print-endnote">
  <li>[1]: https://…</li>
  <li>[2]: http://…</li>
  <li>[3]: http2://…</li>
  …
</ul>

I don't want these to show up when viewing the page on the screen, so I use CSS to hide them by default, and then show them again when printing:

.print-endnote { display: none; }
@media print { .print-endnote { display: unset; } }

This works! Throughout the page, links are annotated with a number like link[3]. Then at the bottom, it displays [3]: url.

However, as usual, there are details that make things more complicated in practice.

  1. I want to exclude relative links (which are often to the same page) so I changed the pattern to match a[starts-with(@href,'http')] instead of a. This is in five places; it would be nice to abstract this somehow. [Update 2019-01-01: looks like XLST 2 might let me abstract over this, but XSLT 1 does not.]
  2. I want to exclude links in the nav bar and table of contents. Due to the page structure I use, these are outside of a <section> element, so I changed a to section//a. Combined with the previous rule, it's now the ugly pattern //x:section//a[starts-with(@href,'http')].
  3. I want to exclude links in the footer, which is inside <section> on some of my pages. I did this by adding a test, <xsl:if test="count(ancestor::x:footer) = 0"> for both the endnote marker and the list of urls. These links still receive a number with xsl:number though; I wasn't able to find a way to avoid that. However, since they're at the end of the page, it's not a problem in practice.
  4. All of these rules messed up the weird whitespace handling rules I have in place. I ended up having to make two passes over all the elements, once to expand <a>, and once to apply the whitespace rules. Even then, it is now applying the whitespace in slightly the wrong place. The printed page has link [3] instead of link[3]. [Update 2019-01-01: I was able to fix this.]

I'm a newbie with XSLT so there's some cargo cult involved. I'm simultaneously impressed that XSLT is able to do this, and horrified by how ugly it is. Someday I hope to revisit all of this, either improving the XSLT or moving away from it, but for now, it works reasonably well, and I'm happy with it.

See screenshots I posted on twitter, or try printing one of my pages to see how it looks. If you run into glitches, please let me know!

Update Here's a slightly different implementation:

Instead of adding a new element in XSLT, add an attribute:

  <xsl:template match="//a">
    <xsl:copy>
      <xsl:attribute name="data-endnote">
        <xsl:number level="any" count="//a" format="1"/>
      </xsl:attribute>
      <xsl:apply-templates select="node() | @*"/>
    </xsl:copy>
  </xsl:template>

Then during printing, display that attribute using CSS:

    *[data-endnote]:after {
        color: #000;
        content: "[" attr(data-endnote) "]";
        text-decoration: none;
        font-size: 75%;
        position: relative;
        top: -0.5em;
        vertical-align: baseline;
    }

The advantage of this approach is that I don't have to add a new element. The downside is that the underlining of links will apply to the :after element so these superscripts will be underlined. Another downside is that the superscript is purely visual instead of using a semantic tag like <sup>. More CSS, less HTML.

Update: [2021-11-22] I added QR codes to the endnotes so that you can scan them instead of typing them in. I added <img><xsl:attribute name="src">https://chart.googleapis.com/chart?chs=120x120&amp;cht=qr&amp;chl=<xsl:value-of select="str:encode-uri(@href, true())"/></xsl:attribute></img> just after the xsl:number. [Edit: I removed this, as it didn't work well]

Labels: ,

0 comments: