diff options
Diffstat (limited to 'blog')
-rw-r--r-- | blog/8seg/index.html | 19 | ||||
-rw-r--r-- | blog/css-only-code-blocks/index.html | 259 | ||||
-rw-r--r-- | blog/hsm-basics/index.html | 19 | ||||
-rw-r--r-- | blog/ihsm-worlds-first-diy-hsm/index.html | 19 | ||||
-rw-r--r-- | blog/index.html | 43 | ||||
-rw-r--r-- | blog/index.xml | 16 | ||||
-rw-r--r-- | blog/jupyterlab-notebook-file-oneliner/index.html | 19 | ||||
-rw-r--r-- | blog/kicad-mesh-plugin/index.html | 19 | ||||
-rw-r--r-- | blog/led-characterization/index.html | 19 | ||||
-rw-r--r-- | blog/multichannel-led-driver/index.html | 19 | ||||
-rw-r--r-- | blog/private-contact-discovery/index.html | 19 | ||||
-rw-r--r-- | blog/serial-protocols/index.html | 19 | ||||
-rw-r--r-- | blog/telekom-gpon-sfp/index.html | 19 | ||||
-rw-r--r-- | blog/thors-hammer/index.html | 19 | ||||
-rw-r--r-- | blog/wifi-led-driver/index.html | 19 | ||||
-rw-r--r-- | blog/wsdiff-static-html-diffs/index.html | 169 |
16 files changed, 688 insertions, 27 deletions
diff --git a/blog/8seg/index.html b/blog/8seg/index.html index 2be0896..793b338 100644 --- a/blog/8seg/index.html +++ b/blog/8seg/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -216,11 +216,26 @@ set of pre-programmed waveform transitions.</p> / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/css-only-code-blocks/index.html b/blog/css-only-code-blocks/index.html new file mode 100644 index 0000000..5b15937 --- /dev/null +++ b/blog/css-only-code-blocks/index.html @@ -0,0 +1,259 @@ +<!DOCTYPE html> +<html><head> + <meta charset="utf-8"> + <title>Code listings with nice line wrapping and line numbers from plain CSS | Home</title> + <meta name="description" content=""> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="mobile-web-app-capable" content="yes"> + <meta name="color-scheme" content="dark light"> + <link rel="stylesheet" href="/style.css"> + + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> + <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> +</head> +<body><nav> + <div class="internal"> + + <a href="/" title="Home">Home</a> + <a href="/blog/" title="Blog">Blog</a> + <a href="/projects/" title="Projects">Projects</a> + <a href="/about/" title="About">About</a> + </div> + <div class="search"> + <div id="search"></div> + </div> + <div class="external"> + <a href="https://git.jaseg.de/" title="cgit">cgit</a> + <a href="https://github.com/jaseg" title="Github">Github</a> + <a href="https://gitlab.com/neinseg" title="Gitlab">Gitlab</a> + <a href="https://chaos.social/@jaseg" title="Mastodon">Mastodon</a> + </span> +</nav> + + <header> + <h1>Code listings with nice line wrapping and line numbers from plain CSS</h1> +<ul class="breadcrumbs"> + <li><a href="/">jaseg.de</a></li> + <li><a href="/blog/">Blog</a></li><li><a href="/blog/css-only-code-blocks/">Code listings with nice line wrapping and line numbers from plain CSS</a></li> +</ul> + <strong>2025-07-23</strong> + </header> + <main data-pagefind-body> + <div class="document"> + + +<p>Code listings in web pages are often a bit of a pain to use. Often, they don't wrap on small screens. Also, copy-pasting +code from a code listing often copies the line numbers along with the code. Finally, many implementations use +heavyweight HTML and/or javascript, making them slow to render (looking at you, gitlab).</p> +<p>For this blog, I wrote an implementation that renders HTML code listings entirely without JavaScript, renders line +numbers using plain CSS such that they don't get selected with the code, and that works with the browser to wrap in a +natural way while still supporting the little line continuation arrows that are used to show that a line was soft +wrapped in text editors.</p> +<p>This blog is rendered as a static site using <a class="reference external" href="https://gohugo.io/">Hugo</a> from a pile of <a class="reference external" href="https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html">RestructuredText</a> documents. RestructuredText renders +code listings using <a class="reference external" href="https://pygments.org/">Pygments</a> by default. Pygments hard-bakes the line numbers into the generated HTML, so I am using a +<a class="reference external" href="https://en.wikipedia.org/wiki/Monkey_patch">monkey-patched</a> hook that changes the line number rendering to just a bunch of empty <tt class="docutils literal"><span></tt> elements. The resulting +HTML for a code block then looks like this:</p> +<pre class="code html literal-block"> +<span class="lineno"></span><span class="line"><span class="p"><</span><span class="nt">pre</span> <span class="na">class</span><span class="o">=</span><span class="s">"code [language] literal-block"</span><span class="p">></span></span> +<span class="lineno"></span><span class="line"> <span class="p"><</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">"lineno"</span><span class="p">></</span><span class="nt">span</span><span class="p">></span></span> +<span class="lineno"></span><span class="line"> <span class="p"><</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">"line"</span><span class="p">></span></span> +<span class="lineno"></span><span class="line"> <span class="p"><</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">"[syntax highlight token]"</span><span class="p">></span>The <span class="p"></</span><span class="nt">span</span><span class="p">><</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">"[other syntax highlight token]"</span><span class="p">></span>code!<span class="p"><</span><span class="nt">span</span><span class="p">></span></span> +<span class="lineno"></span><span class="line"> <span class="p"></</span><span class="nt">span</span><span class="p">></span></span> +<span class="lineno"></span><span class="line"> <span class="cm"><!-- ... repeat once for each source line. --></span></span> +<span class="lineno"></span><span class="line"><span class="p"></</span><span class="nt">pre</span><span class="p">></span> +</span></pre> +<p>You can find the (rather short) source of the <tt class="docutils literal">rst2html</tt> wrapper <a class="reference external" href="#rst2html-wrapper">below</a>.</p> +<div class="section" id="the-css"> +<h2>The CSS</h2> +<p>This modified HTML structure of the code listing gets accompanied by some CSS to make it flow nicely. Here is a listing +of the complete CSS controlling the listing. The only bit that isn't included here is the actual syntax styling rules +for the pygments tokens.</p> +<pre class="code css literal-block"> +<span class="lineno"></span><span class="line"><span class="c">/*****************************************************/</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="c">/* Code block formatting / syntax highlighting rules */</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="c">/*****************************************************/</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">.</span><span class="nc">code</span><span class="w"> </span><span class="p">{</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">font-family</span><span class="p">:</span><span class="w"> </span><span class="s2">"Fira Code"</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mi">13</span><span class="kt">px</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">text-align</span><span class="p">:</span><span class="w"> </span><span class="kc">left</span><span class="p">;</span><span class="w"> </span><span class="c">/* Override default content "justify" alignment */</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">white-space</span><span class="p">:</span><span class="w"> </span><span class="kc">pre-wrap</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">word-wrap</span><span class="p">:</span><span class="w"> </span><span class="kc">break-word</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">overflow-x</span><span class="p">:</span><span class="w"> </span><span class="kc">auto</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">display</span><span class="p">:</span><span class="w"> </span><span class="k">grid</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">align-items</span><span class="p">:</span><span class="w"> </span><span class="kc">start</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">grid-template-columns</span><span class="p">:</span><span class="w"> </span><span class="n">min-content</span><span class="w"> </span><span class="mi">1</span><span class="n">fr</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">}</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">.</span><span class="nc">code</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="p">.</span><span class="nc">line</span><span class="w"> </span><span class="p">{</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">padding-left</span><span class="p">:</span><span class="w"> </span><span class="nb">calc</span><span class="p">(</span><span class="mi">2</span><span class="kt">em</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">5</span><span class="kt">px</span><span class="p">);</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">text-indent</span><span class="p">:</span><span class="w"> </span><span class="mi">-2</span><span class="kt">em</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">padding-top</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="kt">px</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">min-width</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span><span class="kt">em</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">}</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="c">/* Make individual syntax tokens wrap anywhere */</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">.</span><span class="nc">code</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="p">.</span><span class="nc">line</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="nt">span</span><span class="w"> </span><span class="p">{</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">overflow-wrap</span><span class="p">:</span><span class="w"> </span><span class="n">anywhere</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">white-space</span><span class="p">:</span><span class="w"> </span><span class="kc">pre-wrap</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">}</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="c">/* We render line numbers in CSS! */</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">.</span><span class="nc">code</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="p">.</span><span class="nc">lineno</span><span class="w"> </span><span class="p">{</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">counter-increment</span><span class="p">:</span><span class="w"> </span><span class="n">lineno</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">word-break</span><span class="p">:</span><span class="w"> </span><span class="n">keep-all</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">margin</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">padding-left</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span><span class="kt">px</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">padding-right</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="kt">px</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">overflow</span><span class="p">:</span><span class="w"> </span><span class="kc">clip</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">position</span><span class="p">:</span><span class="w"> </span><span class="kc">relative</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">text-align</span><span class="p">:</span><span class="w"> </span><span class="kc">right</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--c-text-muted</span><span class="p">);</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">border-right</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="kt">px</span><span class="w"> </span><span class="kc">solid</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--c-fg-highlight</span><span class="p">);</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">align-self</span><span class="p">:</span><span class="w"> </span><span class="kc">stretch</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">}</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="c">/* We also handle line continuation markers in CSS. */</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">.</span><span class="nc">code</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="p">.</span><span class="nc">lineno</span><span class="p">::</span><span class="nd">after</span><span class="w"> </span><span class="p">{</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">position</span><span class="p">:</span><span class="w"> </span><span class="kc">absolute</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">right</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="kt">px</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">content</span><span class="p">:</span><span class="w"> </span><span class="s2">"\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳"</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">white-space</span><span class="p">:</span><span class="w"> </span><span class="kc">pre</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--c-text-muted</span><span class="p">);</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">}</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="c">/* Insert the actual line number */</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">.</span><span class="nc">code</span><span class="w"> </span><span class="o">></span><span class="w"> </span><span class="p">.</span><span class="nc">lineno</span><span class="p">::</span><span class="nd">before</span><span class="w"> </span><span class="p">{</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">content</span><span class="p">:</span><span class="w"> </span><span class="nb">counter</span><span class="p">(</span><span class="n">lineno</span><span class="p">);</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">}</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">.</span><span class="nc">code</span><span class="p">::</span><span class="nd">before</span><span class="w"> </span><span class="p">{</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"> </span><span class="k">counter-reset</span><span class="p">:</span><span class="w"> </span><span class="n">lineno</span><span class="p">;</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">}</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">.</span><span class="nc">code</span><span class="w"> </span><span class="p">.</span><span class="nc">hll</span><span class="w"> </span><span class="p">{}</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="c">/* Following are about 50 lines that define the styling of each kind of pygments syntax highlight token. These lines</span></span> +<span class="lineno"></span><span class="line"><span class="c"> all look like the following: */</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="p">.</span><span class="nc">code</span><span class="w"> </span><span class="p">.</span><span class="nc">c</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">color</span><span class="p">:</span><span class="w"> </span><span class="nf">var</span><span class="p">(</span><span class="nv">--c-text</span><span class="p">);</span><span class="w"> </span><span class="k">font-weight</span><span class="p">:</span><span class="w"> </span><span class="mi">400</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="c">/* Comment */</span> +</span></pre> +<p>This CSS does a few things:</p> +<blockquote> +<ol class="arabic simple"> +<li>It renders the <tt class="docutils literal"><pre></tt> code listing element using a two-column CSS <tt class="docutils literal">display: grid</tt> layout. The left column is +used for the line numbers, and the right column is used for the code lines.</li> +<li>It numbers the lines using a <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/CSS/counter">CSS Counter</a>. CSS counters are meant for things like numbering headings and such, but +they are a perfect fit for our purpose.</li> +<li>It inserts the counter value as the line number into the <tt class="docutils literal"><span <span class="pre">class="lineno"></span></tt> element's <tt class="docutils literal">::before</tt> +pseudo-element. A side effect of using the <tt class="docutils literal">::before</tt> pseudo-element is that without doing anything extra, the +line numbers will remain outside of the normal text selection so they will neither be highlighted when selecting +listing content, nor will they be copied when copy/pasting the listing content.</li> +<li>It inserts a string of <tt class="docutils literal"><span class="pre">"\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳"</span></tt> into the line number span's +<tt class="docutils literal">::after</tt> pseudo-element. This string evaluates to a sequence of unicode arrows separated by line breaks, and +starting with an empty line. The <tt class="docutils literal">::after</tt> pseudo-element is positioned using <tt class="docutils literal">position: absolute</tt>, and the +parent <tt class="docutils literal"><span <span class="pre">class="lineno"></span></tt> has <tt class="docutils literal">position: relative</tt> set. This way, the arrow pseudo-element gets placed on +top of the lineno span without affecting the layout at all. By setting <tt class="docutils literal">overflow: clip</tt> on the parent <tt class="docutils literal"><span +<span class="pre">class="lineno"></span></tt>, the arrow pseudo-element gets cut off vertically wherever the parent lineno element naturally +ends.</li> +</ol> +</blockquote> +<p>The line number span is inserted into the parent <tt class="docutils literal"><pre></tt> element's CSS grid using <tt class="docutils literal"><span class="pre">align-self:</span> stretch</tt>, which +causes it to vertically stretch to fill the available space. Since the line number span only contains the line number, +its minimum height is a single line. As a result, it will stretch higher only when the corresponding code line in the +right grid column stretches vertically because of line wrapping. When that happens, part of the arrow pseudo-element +starts showing through from behind the <tt class="docutils literal">overflow: clip</tt> of the line number span, and one arrow gets rendered for each +wrapped listing line.</p> +<p>When the page is too narrow, we don't want the code listing's lines to wrapp into a column of single characters. To +prevent that, we simply set a <tt class="docutils literal"><span class="pre">min-width</span></tt> on the <tt class="docutils literal"><span <span class="pre">class="line"></span></tt> in the right column, and set <tt class="docutils literal"><span class="pre">overflow-x:</span> +auto</tt> on the listing <tt class="docutils literal"><pre></tt>. This results in a horizontal scroll bar appearing whenever the listing gets too narrow.</p> +<p>You can try out the line wrapping by resizing this page!</p> +</div> +<div class="section" id="rst2html-wrapper"> +<h2>rst2html wrapper</h2> +<p>Here is the python <tt class="docutils literal">rst2html</tt> wrapper that monkey-patches code rendering. I made hugo invoke this while building the +page by simply overriding the <tt class="docutils literal">PATH</tt> environment variable.</p> +<pre class="code python literal-block"> +<span class="lineno"></span><span class="line"><span class="ch">#!/usr/bin/env python3</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="c1"># Based on https://gist.github.com/mastbaum/2655700 for the basic plugin scaffolding</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">sys</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">re</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="kn">import</span><span class="w"> </span><span class="nn">docutils.core</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="kn">from</span><span class="w"> </span><span class="nn">docutils.transforms</span><span class="w"> </span><span class="kn">import</span> <span class="n">Transform</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="kn">from</span><span class="w"> </span><span class="nn">docutils.nodes</span><span class="w"> </span><span class="kn">import</span> <span class="n">TextElement</span><span class="p">,</span> <span class="n">Inline</span><span class="p">,</span> <span class="n">Text</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="kn">from</span><span class="w"> </span><span class="nn">docutils.parsers.rst</span><span class="w"> </span><span class="kn">import</span> <span class="n">Directive</span><span class="p">,</span> <span class="n">directives</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="kn">from</span><span class="w"> </span><span class="nn">docutils.writers.html4css1</span><span class="w"> </span><span class="kn">import</span> <span class="n">Writer</span><span class="p">,</span> <span class="n">HTMLTranslator</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="k">class</span><span class="w"> </span><span class="nc">UnfuckedHTMLTranslator</span><span class="p">(</span><span class="n">HTMLTranslator</span><span class="p">):</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="k">def</span><span class="w"> </span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="bp">self</span><span class="o">.</span><span class="n">in_literal_block</span> <span class="o">=</span> <span class="kc">False</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="k">def</span><span class="w"> </span><span class="nf">visit_literal_block</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">node</span><span class="p">):</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="c1"># Insert an empty "lineno" span before each line. We insert the line numbers using pure CSS in a ::before</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="c1"># pseudo-element. This has the added advantage that the line numbers don't get included in text selection.</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="c1"># These line number spans are also used to show line continuation markers when a line is wrapped.</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="bp">self</span><span class="o">.</span><span class="n">in_literal_block</span> <span class="o">=</span> <span class="kc">True</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="bp">self</span><span class="o">.</span><span class="n">body</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">starttag</span><span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="s1">'pre'</span><span class="p">,</span> <span class="n">CLASS</span><span class="o">=</span><span class="s1">'literal-block'</span><span class="p">))</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="bp">self</span><span class="o">.</span><span class="n">body</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s1">'<span class="lineno"></span><span class="line">'</span><span class="p">)</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="k">def</span><span class="w"> </span><span class="nf">depart_literal_block</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">node</span><span class="p">):</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="bp">self</span><span class="o">.</span><span class="n">in_literal_block</span> <span class="o">=</span> <span class="kc">False</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="bp">self</span><span class="o">.</span><span class="n">body</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s1">'</span><span class="se">\n</span><span class="s1"></span></pre></span><span class="se">\n</span><span class="s1">'</span><span class="p">)</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="k">def</span><span class="w"> </span><span class="nf">visit_Text</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">node</span><span class="p">):</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">in_literal_block</span><span class="p">:</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="k">for</span> <span class="n">match</span> <span class="ow">in</span> <span class="n">re</span><span class="o">.</span><span class="n">finditer</span><span class="p">(</span><span class="s1">'([^</span><span class="se">\n</span><span class="s1">]*)(</span><span class="se">\n</span><span class="s1">|$)'</span><span class="p">,</span> <span class="n">node</span><span class="o">.</span><span class="n">astext</span><span class="p">()):</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="n">text</span><span class="p">,</span> <span class="n">end</span> <span class="o">=</span> <span class="n">match</span><span class="o">.</span><span class="n">groups</span><span class="p">()</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="k">if</span> <span class="n">text</span><span class="p">:</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">visit_Text</span><span class="p">(</span><span class="n">Text</span><span class="p">(</span><span class="n">text</span><span class="p">))</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="k">if</span> <span class="n">end</span> <span class="o">==</span> <span class="s1">'</span><span class="se">\n</span><span class="s1">'</span><span class="p">:</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">node</span><span class="o">.</span><span class="n">parent</span><span class="p">,</span> <span class="n">Inline</span><span class="p">):</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="bp">self</span><span class="o">.</span><span class="n">depart_inline</span><span class="p">(</span><span class="n">node</span><span class="o">.</span><span class="n">parent</span><span class="p">)</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="bp">self</span><span class="o">.</span><span class="n">body</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s1">'</span></span><span class="se">\n</span><span class="s1"><span class="lineno"></span><span class="line">'</span><span class="p">)</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">node</span><span class="o">.</span><span class="n">parent</span><span class="p">,</span> <span class="n">Inline</span><span class="p">):</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="bp">self</span><span class="o">.</span><span class="n">visit_inline</span><span class="p">(</span><span class="n">node</span><span class="o">.</span><span class="n">parent</span><span class="p">)</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="k">else</span><span class="p">:</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">visit_Text</span><span class="p">(</span><span class="n">node</span><span class="p">)</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="n">html_writer</span> <span class="o">=</span> <span class="n">Writer</span><span class="p">()</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="n">html_writer</span><span class="o">.</span><span class="n">translator_class</span> <span class="o">=</span> <span class="n">UnfuckedHTMLTranslator</span><span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span><span class="n">docutils</span><span class="o">.</span><span class="n">core</span><span class="o">.</span><span class="n">publish_cmdline</span><span class="p">(</span><span class="n">writer</span><span class="o">=</span><span class="n">html_writer</span><span class="p">)</span> +</span></pre> +</div> +</div> + </main><footer> + Copyright © 2025 Jan Sebastian Götte + / <a href="/about/">About</a> + / <a href="/imprint/">Imprint</a> +</footer> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> + <script> + window.addEventListener('DOMContentLoaded', (event) => { + new PagefindUI({element: "#search", showSubResults: true}); + }); + </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> + </body> +</html> diff --git a/blog/hsm-basics/index.html b/blog/hsm-basics/index.html index 66ad297..cfa72b9 100644 --- a/blog/hsm-basics/index.html +++ b/blog/hsm-basics/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -233,11 +233,26 @@ while not providing better sensitivity.</p> / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/ihsm-worlds-first-diy-hsm/index.html b/blog/ihsm-worlds-first-diy-hsm/index.html index c61a01b..b393484 100644 --- a/blog/ihsm-worlds-first-diy-hsm/index.html +++ b/blog/ihsm-worlds-first-diy-hsm/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -77,11 +77,26 @@ focusing our effort on the next iteration of the design instead. Stay tuned for / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/index.html b/blog/index.html index 51e985c..f50aa1b 100644 --- a/blog/index.html +++ b/blog/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -40,6 +40,30 @@ </header> <main class="cards"> + <div class="card"><h3><a href="/blog/wsdiff-static-html-diffs/">wsdiff: Responsive diffs in plain HTML</a></h3><strong>2025-07-25</strong> + + <div class="summary"> + <div class="document"> + + +<p>There's many tools that render diffs on the web, but almost none that work well on small screens such as phones. I fixed this by publishing wsdiff, a diffing tool written in Python that produces diffs as beautiful, responsive, static, self-contained HTML pages. wsdiffs wrap text to fit the window, and dynamically switch between unified and split diffs based on screen size using only CSS.</p> +</div> + <a href="http://jaseg.de/blog/wsdiff-static-html-diffs/">Read more</a> + </div> +</div> + + <div class="card"><h3><a href="/blog/css-only-code-blocks/">Code listings with nice line wrapping and line numbers from plain CSS</a></h3><strong>2025-07-23</strong> + + <div class="summary"> + <div class="document"> + + +<p>Code listings in web pages are often a bit of a pain to use. Usually, they don't wrap on small screens. Also, copy-pasting code from a code listing often copies the line numbers along with the code. Finally, many implementations use heavyweight HTML and/or javascript, making them slow to render. For this blog, I wrote a little CSS hack that renders nice, wrapping code blocks with line continuation markers in plain CSS without any JS.</p> +</div> + <a href="http://jaseg.de/blog/css-only-code-blocks/">Read more</a> + </div> +</div> + <div class="card"><h3><a href="/blog/jupyterlab-notebook-file-oneliner/">Getting the .ipynb Notebook File Location From a Running Jupyter Lab Notebook</a></h3><strong>2025-06-29</strong> <div class="summary"> @@ -196,11 +220,26 @@ set up your UART to talk to an external hardware USB to serial converter is a ma / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/index.xml b/blog/index.xml index 285b3f8..718b0d0 100644 --- a/blog/index.xml +++ b/blog/index.xml @@ -7,9 +7,23 @@ <generator>Hugo</generator> <language>en-us</language> <copyright>Jan Sebastian Götte</copyright> - <lastBuildDate>Sun, 29 Jun 2025 23:42:00 +0100</lastBuildDate> + <lastBuildDate>Fri, 25 Jul 2025 23:42:00 +0100</lastBuildDate> <atom:link href="http://jaseg.de/blog/index.xml" rel="self" type="application/rss+xml" /> <item> + <title>wsdiff: Responsive diffs in plain HTML</title> + <link>http://jaseg.de/blog/wsdiff-static-html-diffs/</link> + <pubDate>Fri, 25 Jul 2025 23:42:00 +0100</pubDate> + <guid>http://jaseg.de/blog/wsdiff-static-html-diffs/</guid> + <description><div class="document">


<p>There's many tools that render diffs on the web, but almost none that work well on small screens such as phones. I fixed this by publishing wsdiff, a diffing tool written in Python that produces diffs as beautiful, responsive, static, self-contained HTML pages. wsdiffs wrap text to fit the window, and dynamically switch between unified and split diffs based on screen size using only CSS.</p>
</div></description> + </item> + <item> + <title>Code listings with nice line wrapping and line numbers from plain CSS</title> + <link>http://jaseg.de/blog/css-only-code-blocks/</link> + <pubDate>Wed, 23 Jul 2025 23:42:00 +0100</pubDate> + <guid>http://jaseg.de/blog/css-only-code-blocks/</guid> + <description><div class="document">


<p>Code listings in web pages are often a bit of a pain to use. Usually, they don't wrap on small screens. Also, copy-pasting code from a code listing often copies the line numbers along with the code. Finally, many implementations use heavyweight HTML and/or javascript, making them slow to render. For this blog, I wrote a little CSS hack that renders nice, wrapping code blocks with line continuation markers in plain CSS without any JS.</p>
</div></description> + </item> + <item> <title>Getting the .ipynb Notebook File Location From a Running Jupyter Lab Notebook</title> <link>http://jaseg.de/blog/jupyterlab-notebook-file-oneliner/</link> <pubDate>Sun, 29 Jun 2025 23:42:00 +0100</pubDate> diff --git a/blog/jupyterlab-notebook-file-oneliner/index.html b/blog/jupyterlab-notebook-file-oneliner/index.html index 9071c12..9eb93a2 100644 --- a/blog/jupyterlab-notebook-file-oneliner/index.html +++ b/blog/jupyterlab-notebook-file-oneliner/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -60,11 +60,26 @@ ourselves.</p> / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/kicad-mesh-plugin/index.html b/blog/kicad-mesh-plugin/index.html index 68762b0..2119817 100644 --- a/blog/kicad-mesh-plugin/index.html +++ b/blog/kicad-mesh-plugin/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -213,11 +213,26 @@ making a copy of the board file first and treating mesh generation as a non-reve / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/led-characterization/index.html b/blog/led-characterization/index.html index 8803537..96fe4ea 100644 --- a/blog/led-characterization/index.html +++ b/blog/led-characterization/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -427,11 +427,26 @@ can view the Jupyter notebook most of the analysis above <a class="reference ext / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/multichannel-led-driver/index.html b/blog/multichannel-led-driver/index.html index 14981e5..0077335 100644 --- a/blog/multichannel-led-driver/index.html +++ b/blog/multichannel-led-driver/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -380,11 +380,26 @@ analyze the brightness measurement data <a class="reference external" href="http / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/private-contact-discovery/index.html b/blog/private-contact-discovery/index.html index 8a15c4b..0ad3659 100644 --- a/blog/private-contact-discovery/index.html +++ b/blog/private-contact-discovery/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -76,11 +76,26 @@ accountability issues by simply not producing as much sensitive data in the firs / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/serial-protocols/index.html b/blog/serial-protocols/index.html index 1fb7aba..562b451 100644 --- a/blog/serial-protocols/index.html +++ b/blog/serial-protocols/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -247,11 +247,26 @@ want to set a large framebuffer in pieces, do it in a <a class="reference extern / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/telekom-gpon-sfp/index.html b/blog/telekom-gpon-sfp/index.html index d83f73e..1351707 100644 --- a/blog/telekom-gpon-sfp/index.html +++ b/blog/telekom-gpon-sfp/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -222,11 +222,26 @@ collected <a class="reference external" href="https://github.com/xvzf/zyxel-gpon / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/thors-hammer/index.html b/blog/thors-hammer/index.html index 8520fa0..366194d 100644 --- a/blog/thors-hammer/index.html +++ b/blog/thors-hammer/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -84,11 +84,26 @@ board as shims between the plunger and the case to limit the plunger's travel in / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/wifi-led-driver/index.html b/blog/wifi-led-driver/index.html index 81713f5..88a4b62 100644 --- a/blog/wifi-led-driver/index.html +++ b/blog/wifi-led-driver/index.html @@ -8,7 +8,7 @@ <meta name="color-scheme" content="dark light"> <link rel="stylesheet" href="/style.css"> - <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> @@ -142,11 +142,26 @@ violence.</p> / <a href="/about/">About</a> / <a href="/imprint/">Imprint</a> </footer> -<script src="/pagefind/pagefind-ui.js" defer></script> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> <script> window.addEventListener('DOMContentLoaded', (event) => { new PagefindUI({element: "#search", showSubResults: true}); }); </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> </body> </html> diff --git a/blog/wsdiff-static-html-diffs/index.html b/blog/wsdiff-static-html-diffs/index.html new file mode 100644 index 0000000..5bccd82 --- /dev/null +++ b/blog/wsdiff-static-html-diffs/index.html @@ -0,0 +1,169 @@ +<!DOCTYPE html> +<html><head> + <meta charset="utf-8"> + <title>wsdiff: Responsive diffs in plain HTML | Home</title> + <meta name="description" content=""> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="mobile-web-app-capable" content="yes"> + <meta name="color-scheme" content="dark light"> + <link rel="stylesheet" href="/style.css"> + + <link rel="preload" href="/fonts/roboto_slab/RobotoSlab-VariableFont_wght.ttf" as="font" type="font/ttf" crossorigin /> + <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Regular.woff2" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-Bold.woff2" as="font" type="font/woff2" crossorigin /> + <link rel="preload" href="/fonts/nyght-serif-main/fonts/WEB/NyghtSerif-BoldItalic.woff2" as="font" type="font/woff2" crossorigin /> +</head> +<body><nav> + <div class="internal"> + + <a href="/" title="Home">Home</a> + <a href="/blog/" title="Blog">Blog</a> + <a href="/projects/" title="Projects">Projects</a> + <a href="/about/" title="About">About</a> + </div> + <div class="search"> + <div id="search"></div> + </div> + <div class="external"> + <a href="https://git.jaseg.de/" title="cgit">cgit</a> + <a href="https://github.com/jaseg" title="Github">Github</a> + <a href="https://gitlab.com/neinseg" title="Gitlab">Gitlab</a> + <a href="https://chaos.social/@jaseg" title="Mastodon">Mastodon</a> + </span> +</nav> + + <header> + <h1>wsdiff: Responsive diffs in plain HTML</h1> +<ul class="breadcrumbs"> + <li><a href="/">jaseg.de</a></li> + <li><a href="/blog/">Blog</a></li><li><a href="/blog/wsdiff-static-html-diffs/">wsdiff: Responsive diffs in plain HTML</a></li> +</ul> + <strong>2025-07-25</strong> + </header> + <main data-pagefind-body> + <div class="document"> + + +<div class="section" id="demo"> +<h2>Demo</h2> +<p>First off, have a demo. Because of the width of this page, the output will show an unified diff. To try out the split +diff layout, make sure your browser window is wide enough and open the demo in a separate tab using <a class="reference external" href="/wsdiff-example.html">this link</a>.</p> +<p>wsdiff supports dark mode, try it out by toggling dark mode in your operating system!</p> +<iframe src="/wsdiff-example.html" style="width: 100%; height: 30em; border: 1px #d0d0d0 solid" id="wsdiff example diff"></iframe></div> +<div class="section" id="core-features"> +<h2>Core Features</h2> +<p>There's many tools that render diffs on the web, but almost none that work well on small screens such as phones. I fixed +this by publishing <a class="reference external" href="https://pypi.org/project/wsdiff/">wsdiff</a>, a diffing tool written in Python that produces diffs +as beautiful, responsive, static, self-contained HTML pages. wsdiffs wrap text to fit the window, and dynamically switch +between unified and split diffs based on screen size using only CSS.</p> +<div class="section" id="responsive-line-wrapping"> +<h3>Responsive Line Wrapping</h3> +<p>The first challenge I solved was wrapping source code lines to match the available screen space. Other tools often just +show horizontal scroll bars, which is an okay workaround when you're mostly working with hard-wrapped source code on a +laptop or desktop screen, but which results in catastrophic UX on any phone.</p> +<p>I solved line breaking with a combination of CSS-controlled, web-standard word breaking rules: <tt class="docutils literal"><span class="pre">overflow-wrap:</span> +anywhere</tt> for source code (<a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-wrap">MDN link</a>) and +<tt class="docutils literal"><span class="pre">white-space:</span> <span class="pre">pre-wrap</span></tt> to preserve whitespace accurately (<a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/CSS/white-space">MDN link</a>). To make both sides of the split diff align, and to +align line numbers with wrapped source code lines, the diff is laid out using a <a class="reference external" href="https://css-tricks.com/snippets/css/complete-guide-grid/">CSS grid layout</a>. In side-by-side +view, the layout has four columns: two for line numbers and two for the content. In unified view, the left ("old") +content column is dropped, and the deleted or modified lines that are highlighted in it in side-by-side view are slotted +into the remaining right column.</p> +<p>When soft-wrapping source code, text editors will often display a little curved arrow marker to indicate that a line was +soft-wrapped, and that there is not actually a newline character in the file at that location. wsdiff solves this +using the same technique I used for the soft-wrapping code blocks in this blog, described <a class="reference external" href="http://jaseg.de/blog/css-only-code-blocks/">here</a>. It inserts a string of <tt class="docutils literal"><span class="pre">"\a↳\a↳\a↳\a↳\a↳..."</span></tt> into the line number +element's <tt class="docutils literal">::after</tt> pseudo-element. This string evaluates to a sequence of unicode arrows separated by line breaks, +and starting with an empty line. The <tt class="docutils literal">::after</tt> pseudo-element is positioned using <tt class="docutils literal">position: absolute</tt>, and the +parent <tt class="docutils literal"><span <span class="pre">class="lineno"></span></tt> has <tt class="docutils literal">position: relative</tt> set. This way, the arrow pseudo-element gets placed on top +of the lineno span without affecting the layout at all. By setting <tt class="docutils literal">overflow: clip</tt> on the parent <tt class="docutils literal"><span +<span class="pre">class="lineno"></span></tt>, the arrow pseudo-element gets cut off vertically wherever the parent line number element naturally +ends. Since both the line and the line number element share a grid row, the line number element always matches the +height of the soft-wrapped line.</p> +</div> +<div class="section" id="responsive-split-unified-layout-selection"> +<h3>Responsive Split/Unified Layout Selection</h3> +<p>To dynamically change between unified and side-by-side views, wsdiff uses a web-standard <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries">Media Query</a>. By default, the +page is laid out for side-by-side view. In the HTML source, the diff is listed as it is displayed in side-by-side view, +with the old and new lines along with their line numbers interleaved.</p> +<p>The magic happens when the media query gets triggered by a narrow screen width. The media query re-adjusts the layout in +four core steps:</p> +<blockquote> +<ol class="arabic simple"> +<li>All unchanged lines in the left (old) column are hidden.</li> +<li>The left content column of the grid layout is hidden, so that now there are three columns: old line number, new line +number, and unified content.</li> +<li>All deleted or changed lines from the left (old) column are re-located to the right column. They naturally slot in +in the right spot because they already appear in the right order in the HTML source.</li> +<li>By slotting in the old lines in the right column, we have created gaps in the line number columns. Every deleted +line has an empty cell in the new line number column, and every inserted line has one in the old line number column. +The CSS adjusts the layout of these empty cells such that the border lines align nicely, and it overrides the +newline markers so that they only show in the right (new) line number column, not both.</li> +</ol> +</blockquote> +<p>Since this is all CSS, it happens automatically and near-instantly. Since it is using only web standard features, it +works across browsers and platforms.</p> +</div> +<div class="section" id="unchanged-line-folding-in-css"> +<h3>Unchanged Line Folding in CSS</h3> +<p>When showing the diff of a large file, it is usually best to hide large runs of unchanged lines. wsdiff does this +similar to code folding in text editors. When a long run of unchanged lines is detected, a marker is placed spanning the +diff. This marker contains a checkbox that can be toggled to hide the unchanged lines. This feature is done completely +in CSS using a <tt class="docutils literal"><span class="pre">:has(input[type="checkbox"]:checked)</span></tt> selector.</p> +<p>The actual mechanics are quite simple. To cleanly hide the lines, they must be placed in a container <tt class="docutils literal"><div></tt>. That div +has a CSS subgrid layout using <tt class="docutils literal">display: grid; <span class="pre">grid-template-columns:</span> subgrid;</tt>, meaning that its contents align to +the surrounding diff grid.</p> +</div> +<div class="section" id="dark-mode"> +<h3>Dark Mode</h3> +<p>Integrating a website with the OS-level dark mode is surprisingly easy. All you need is a <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries">Media Query</a> that selects +for <tt class="docutils literal">@media <span class="pre">(prefers-color-scheme:</span> dark)</tt> and you're good. wsdiff uses named colors using <a class="reference external" href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties">CSS Custom Properties</a>, so +the actual dark mode media query only needs to override these color properties, and the rest of the CSS will be +re-computed automatically.</p> +</div> +<div class="section" id="limitations-text-selection"> +<h3>Limitations: Text selection</h3> +<p>A limitation in having a combined, single HTML source for both side-by-side and unified diffs is that text selection +only works naturally in either mode. You can't make text selection work in both simultaneously without re-sorting the +lines in the HTML source, since there is no way to override the text selection order from pure CSS. In wsdiff, I worked +around this issue by just disabling text selection on the unchanged lines in the left (old) column, so selecting text in +the right column copies the unified diff as one would expect.</p> +</div> +</div> +<div class="section" id="try-it-yourself"> +<h2>Try it yourself!</h2> +<p>You can find the demo from above at <a class="reference external" href="/wsdiff-example.html">this link</a>.</p> +<p>You can install wsdiff yourself <a class="reference external" href="https://pypi.org/project/wsdiff/">from PyPI</a>:</p> +<pre class="code sh literal-block"> +<span class="lineno"></span><span class="line">$<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span>-U<span class="w"> </span>wsdiff<span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span>Successfully<span class="w"> </span>installed<span class="w"> </span>wsdiff-0.3.1<span class="w"></span></span> +<span class="lineno"></span><span class="line"><span class="w"></span>$<span class="w"> </span>wsdiff<span class="w"> </span>old.py<span class="w"> </span>new.py<span class="w"> </span>-o<span class="w"> </span>diff.html +</span></pre> +</div> +</div> + </main><footer> + Copyright © 2025 Jan Sebastian Götte + / <a href="/about/">About</a> + / <a href="/imprint/">Imprint</a> +</footer> +<script type="text/javascript" src="/pagefind/pagefind-ui.js" defer></script> + <script> + window.addEventListener('DOMContentLoaded', (event) => { + new PagefindUI({element: "#search", showSubResults: true}); + }); + </script> + <script type="speculationrules"> + { + "prerender": [ + { + "source": "document", + "where": { + "and": [ + {"href_matches": "/*"} + ] + }, + "eagerness": "moderate" + } + ] + } + </script> + </body> +</html> |