summaryrefslogtreecommitdiff
path: root/blog
diff options
context:
space:
mode:
Diffstat (limited to 'blog')
-rw-r--r--blog/8seg/index.html19
-rw-r--r--blog/css-only-code-blocks/index.html259
-rw-r--r--blog/hsm-basics/index.html19
-rw-r--r--blog/ihsm-worlds-first-diy-hsm/index.html19
-rw-r--r--blog/index.html43
-rw-r--r--blog/index.xml16
-rw-r--r--blog/jupyterlab-notebook-file-oneliner/index.html19
-rw-r--r--blog/kicad-mesh-plugin/index.html19
-rw-r--r--blog/led-characterization/index.html19
-rw-r--r--blog/multichannel-led-driver/index.html19
-rw-r--r--blog/private-contact-discovery/index.html19
-rw-r--r--blog/serial-protocols/index.html19
-rw-r--r--blog/telekom-gpon-sfp/index.html19
-rw-r--r--blog/thors-hammer/index.html19
-rw-r--r--blog/wifi-led-driver/index.html19
-rw-r--r--blog/wsdiff-static-html-diffs/index.html169
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">&lt;span&gt;</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">&lt;</span><span class="nt">pre</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;code [language] literal-block&quot;</span><span class="p">&gt;</span></span>
+<span class="lineno"></span><span class="line"> <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;lineno&quot;</span><span class="p">&gt;&lt;/</span><span class="nt">span</span><span class="p">&gt;</span></span>
+<span class="lineno"></span><span class="line"> <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;line&quot;</span><span class="p">&gt;</span></span>
+<span class="lineno"></span><span class="line"> <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;[syntax highlight token]&quot;</span><span class="p">&gt;</span>The <span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&quot;[other syntax highlight token]&quot;</span><span class="p">&gt;</span>code!<span class="p">&lt;</span><span class="nt">span</span><span class="p">&gt;</span></span>
+<span class="lineno"></span><span class="line"> <span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span></span>
+<span class="lineno"></span><span class="line"> <span class="cm">&lt;!-- ... repeat once for each source line. --&gt;</span></span>
+<span class="lineno"></span><span class="line"><span class="p">&lt;/</span><span class="nt">pre</span><span class="p">&gt;</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">&quot;Fira Code&quot;</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 &quot;justify&quot; 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">&gt;</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">&gt;</span><span class="w"> </span><span class="p">.</span><span class="nc">line</span><span class="w"> </span><span class="o">&gt;</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">&gt;</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">&gt;</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">&quot;\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳&quot;</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">&gt;</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">&lt;pre&gt;</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">&lt;span <span class="pre">class=&quot;lineno&quot;&gt;</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">&quot;\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳&quot;</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">&lt;span <span class="pre">class=&quot;lineno&quot;&gt;</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">&lt;span
+<span class="pre">class=&quot;lineno&quot;&gt;</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">&lt;pre&gt;</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">&lt;span <span class="pre">class=&quot;line&quot;&gt;</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">&lt;pre&gt;</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 &quot;lineno&quot; 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">'&lt;span class=&quot;lineno&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line&quot;&gt;'</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">&lt;/span&gt;&lt;/pre&gt;</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">'&lt;/span&gt;</span><span class="se">\n</span><span class="s1">&lt;span class=&quot;lineno&quot;&gt;&lt;/span&gt;&lt;span class=&quot;line&quot;&gt;'</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>&lt;div class=&#34;document&#34;&gt;&#xA;&#xA;&#xA;&lt;p&gt;There&#39;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.&lt;/p&gt;&#xA;&lt;/div&gt;</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>&lt;div class=&#34;document&#34;&gt;&#xA;&#xA;&#xA;&lt;p&gt;Code listings in web pages are often a bit of a pain to use. Usually, they don&#39;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.&lt;/p&gt;&#xA;&lt;/div&gt;</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 (&quot;old&quot;)
+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">&quot;\a↳\a↳\a↳\a↳\a↳...&quot;</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">&lt;span <span class="pre">class=&quot;lineno&quot;&gt;</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">&lt;span
+<span class="pre">class=&quot;lineno&quot;&gt;</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=&quot;checkbox&quot;]: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">&lt;div&gt;</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">&#64;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>