PageSourceSearch

https://jonreeve.com/2020/09/type-safe-blog/

html jonreeve.com collected 2026-09-24 22:49:35 UTC 22,566 bytes, 484 lines download raw bytes

1<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"><title>My Type-Safe Blog in Haskell
2</title><base href="/"><link rel="stylesheet" href="/assets/css/spectre.min.css"><link rel="stylesheet" href="//fonts.googleapis.com/css?family=Montserrat|Raleway"><link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/styles/default.min.css"><link rel="icon" href="/images/favicon.svg" sizes="any" type="image/svg+xml"><style type="text/css">
3
4@media screen and (min-width: 1200px)
5{
6html
7{
8  font-size : 23px;
9}
10
11}
12@media screen and (min-width: 992px)
13{
14html
15{
16  font-size : 24px;
17}
18
19}
20@media screen and (min-width: 768px)
21{
22html
23{
24  font-size : 25px;
25}
26
27}
28@media screen and (min-width: 576px)
29{
30html
31{
32  font-size : 27px;
33}
34
35}
36body
37{
38  font-family : "Raleway", sans-serif;
39}
40
41body.has-docked-nav #main-header
42{
43  width    : 100%;
44  opacity  : 0.94999;
45  z-index  : 2;
46  position : fixed;
47}
48
49#headerWrapper
50{
51  background-color : #ffffff;
52  border-top       : 2px solid #494e8e;
53  border-bottom    : 2px solid #494e8e;
54}
55
56#headerWrapper header
57{
58  padding-top    : 0;
59  padding-bottom : 0;
60  padding-left   : 1em;
61  padding-right  : 2em;
62  min-height     : 4em;
63  margin         : auto auto auto auto;
64  max-width      : 65em;
65}
66
67#headerWrapper header .nav
68{
69  flex-direction : row;
70}
71
72#headerWrapper header .nav li
73{
74  margin-top : 0;
75}
76
77.container
78{
79  max-width : 55em;
80  padding   : 3em 3em 3em 3em;
81}
82
83.header
84{
85  margin-bottom : 2em;
86}
87
88li.pages
89{
90  list-style-type : none;
91  margin-top      : 1em;
92}
93
94h1,
95h2,
96h3,
97h4
98{
99  margin-top : 1em;
100}
101
102b
103{
104  font-size : 1.19999em;
105}
106
107main
108{
109  font-family : "Raleway", sans-serif;
110}
111
112main li
113{
114  list-style-position : outside;
115}
116
117.csl-entry
118{
119  margin-left : 2em;
120  text-indent : -2em;
121}
122
123figure
124{
125  text-align : center;
126}
127
128figure figcaption
129{
130  text-align : center;
131}
132
133figure img
134{
135  max-width : 80%;
136}
137
138
139article img
140{
141  max-width : 80%;
142}
143
144span.update p
145{
146  display : inline;
147}
148
149.chip
150{
151  margin           : 0.1rem 0.29999rem 0.1rem 0.29999rem;
152  background-color : rgba(87,85,221,0.2588);
153}
154
155footer
156{
157  background-color : #494e8e;
158  color            : #fafafa;
159}
160
161footer a
162{
163  color : #ddddec;
164}
165
166footer a.icon img
167{
168  width : 3em;
169}
170
171footer .column
172{
173  display         : flex;
174  justify-content : center;
175  align-content   : center;
176}
177
178footer p
179{
180  margin-bottom : 0;
181}
182
183footer .icons
184{
185  color      : #fafafa;
186  font-size  : 2em;
187  display    : flex;
188  align-self : center;
189}
190
191footer .icons svg:hover
192{
193  opacity : 0.8;
194}
195
196@media screen and (min-width: 576px)
197{
198footer .icons
199{
200  flex-direction : column;
201}
202
203}
204@media screen and (min-width: 992px)
205{
206footer .icons
207{
208  flex-direction : row;
209}
210
211}
212section#greeting
213{
214  margin-top    : 4em;
215  margin-bottom : 2em;
216  font-size     : 2em;
217}
218
219section#greeting .icons
220{
221  display         : flex;
222  justify-content : space-between;
223}
224
225section#greeting .icons img
226{
227  height     : 4em;
228  margin-top : 2em;
229}
230
231
232section#postList li.post
233{
234  margin-top      : 5em;
235  list-style-type : none;
236}
237
238section#postList .postTitle
239{
240  font-size : 2em;
241}
242
243@media print
244{
245
246html
247{
248  font-size : 0.9rem;
249}
250
251button
252{
253  display : none;
254}
255
256#headerWrapper
257{
258  display : none;
259}
260
261footer
262{
263  display : none;
264}
265
266.container
267{
268  max-width : none;
269  padding   : 0em 0em 0em 0em;
270}
271
272table
273{
274  font-size : 0.8rem;
275}
276
277}
278
279/* Generated with Clay, http://fvisser.nl/clay */</style><style type="text/css">@page{margin: 3cm;}</style></head><body><div id="headerWrapper"><header class="navbar"><section class="navbar-section"><a class="navbar-brand" href>Jonathan Reeve: <span>Computational Literary Analysis</span></a></section><section class="navbar-section"><ul class="nav"><li class="nav-item"><a href>posts</a></li><li class="nav-item"><a href="cv">cv</a></li><li class="nav-item"><a href="tags.html">tags</a></li><li class="nav-item"><a href="feed.xml">feed</a></li></ul></section></header></div><div class="container"><h1>My Type-Safe Blog in Haskell
280</h1><article><p>Posted 2020-09-13</p><p>Like most, my first step into the blogging world was through
281WordPress. But after a few years of wrangling plugins, handing endless
282updates, dealing with custom CSS issues, and paying for shared web
283hosting, the idea of using a static site generator seemed more
284attractive. Whereas WordPress sites are dynamically generated on a web
285server, static sites are just made up of good old HTML pages, and are
286therefore much simpler, more secure, and easier to maintain. Plus, you
287can serve those pages using a free service like GitHub pages. I switched
288to Jekyll.</p>
289<p>But <a
290href="https://github.com/JonathanReeve/jonreeve.com/tree/816a19ff4454313a27f14a5ba9c7f5a5a5fc2d11">my
291first step</a> into the world of static site generators quickly grew in
292complexity. Jekyll allows you to avoid writing HTML by writing in
293Markdown instead, which is a real improvement. And to avoid the tedium
294of writing CSS, you can write in Sass instead. Then there’s
295Coffeescript, which compiles to JavaScript. You can template together
296all your pages using the templating language Liquid. But for each of
297these conveniences, there is some technical overhead. First, all of
298these micro-languages are compiled using a stack of Ruby gems. To manage
299those gems, you need Bundler, which puts them all together, and probably
300also a manager which juggles all your versions of Ruby. And in the end,
301you have to remember lots of syntax: what does a variable look like in
302Sass, again? What about Coffeescript? Liquid?</p>
303<p>For my <a
304href="https://github.com/JonathanReeve/jonreeve.com/tree/36b7520fb9c71ffc09b6eec3007994b1c32c3e01">second
305time around</a>, I wanted to solve a problem with my CV, which was that
306I wanted to avoid repeating myself in situations where I had a
307publication related to a project that was listed in both the
308<em>Projects</em> section and the <em>Publications</em> section. So I
309decided to put all that data in a data format, YAML, and then display it
310using templates. To do all that, I chose the simplest tool I could find.
311<a href="https://metalsmith.io/">Metalsmith</a> bills itself as “an
312extremely simple, pluggable static site generator.” And it is.
313Everything in Metalsmith is a plugin, and so getting the perfect blog
314set up was just a matter of assembling the right plugins. I even <a
315href="https://github.com/JonathanReeve/metalsmith-markdown-metadata">wrote
316one myself, to handle parsing Markdown within Yaml data</a>. But in the
317end, I found the JavaScript and node.js world to be really fickle. My
318dozen Metalsmith plugins had hundreds of dependencies, in total, and
319updating one would often break another. I kept getting Dependabot
320security alerts on GitHub for dependencies I didn’t even know I had. I
321<em>was</em> able to rewrite <a
322href="https://github.com/JonathanReeve/jonreeve.com/blob/36b7520fb9c71ffc09b6eec3007994b1c32c3e01/src/cv.yaml">my
323CV as YAML data</a>, though, and <a
324href="https://github.com/JonathanReeve/jonreeve.com/blob/36b7520fb9c71ffc09b6eec3007994b1c32c3e01/layouts/cv.pug">format
325it using the satisfyingly minimalist templating language Pug</a>. But I
326was still really in the same situation as before.</p>
327<p>So this most recent time I thought hard about semantic data. YAML had
328allowed me to write my CV like this:</p>
329<pre class="example"><code>projects:
330  - title: Corpus-DB
331    url: https://github.com/JonathanReeve/corpus-db
332    github: JonathanReeve/corpus-db
333    start: 2017-03
334    description: A database and API for plain text archives, for digital humanities research.
335    updates:
336      - date: 2017-10
337        type: award
338        description: Winner, [2017 NYCDH Graduate Student Project Award](https://nycdh.org/groups/nycdh-announcements-71439400/forum/topic/2017-nycdh-graduate-student-project-award-recipients/) 
339</code></pre>
340<p>This was a big step forward for my deduplication problem, since I
341could now write a template that could extract all <code
342class="verbatim">publication</code> types and display those in a
343different section, without having to maintain those in two different
344places in the data. There is an <em>implicit</em> schema here: every
345project has a title, a url, and so on. But there was no way to keep this
346from breaking in an unexpected way, since I just had to remember that my
347template expects there to be certain fields in the YAML data.</p>
348<p>This is where Haskell comes in. Haskell allows me to define algebraic
349data types, like this:</p>
350<div class="sourceCode" id="cb2"><pre
351class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="kw">data</span> <span class="dt">Project</span> <span class="ot">=</span> <span class="dt">Project</span> {</span>
352<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="ot">  title ::</span> <span class="dt">Text</span>,</span>
353<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="ot">  role ::</span> <span class="dt">ProjectRole</span>,</span>
354<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="ot">  homepage ::</span> <span class="dt">URI</span>,</span>
355<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="ot">  github ::</span> <span class="dt">Maybe</span> <span class="dt">Text</span>,</span>
356<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="ot">  pypi ::</span> <span class="dt">Maybe</span> <span class="dt">Text</span>,</span>
357<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="ot">  dateRange ::</span> <span class="dt">DateRange</span>,</span>
358<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a><span class="ot">
358  desc ::</span> <span class="dt">Markdown</span>,</span>
359<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="ot">  updates ::</span> [ <span class="dt">Update</span> ]</span>
360<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a>  } <span class="kw">deriving</span> <span class="dt">Show</span></span>
361<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a></span>
362<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a><span class="kw">data</span> <span class="dt">ProjectRole</span> <span class="ot">=</span> <span class="dt">Creator</span> <span class="op">|</span> <span class="dt">CoCreator</span> <span class="op">|</span> <span class="dt">Developer</span> <span class="op">|</span> <span class="dt">Collaborator</span> <span class="op">|</span> <span class="dt">ResearchAssistant</span> <span class="kw">deriving</span> <span class="dt">Show</span></span></code></pre></div>
363<p>What this says is: every project has a title, which is text, but the
364role I have in the project must come from a set list. Similarly, the
365URIs for the project must be URI types, which I can then validate and
366extract in predictable ways. This way, I can write an entry like
367this:</p>
368<div class="sourceCode" id="cb3"><pre
369class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a></span>
370<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="dt">Project</span> { title <span class="ot">=</span> <span class="st">&quot;Corpus-DB&quot;</span>,</span>
371<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>          role <span class="ot">=</span> <span class="dt">Creator</span>,</span>
372<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>          homepage <span class="ot">=</span> <span class="st">&quot;https://github.com/JonathanReeve/corpus-db&quot;</span>,</span>
373<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a>          github <span class="ot">=</span> <span class="dt">Just</span> <span class="st">&quot;JonathanReeve/corpus-db&quot;</span>,</span>
374<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a>          dateRange <span class="ot">=</span> <span class="dt">DateRange</span> (date <span class="dv">2017</span> <span class="dv">03</span>) <span class="dt">Present</span>,</span>
375<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a>          desc  <span class="ot">=</span> <span class="st">&quot;A database and API for plain text archives, for digital humanities research.&quot;</span>,</span>
376<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a>          updates <span class="ot">=</span> [</span>
377<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>            <span class="dt">Update</span> (date <span class="dv">2020</span> <span class="dv">08</span>) (<span class="dt">Publication</span> <span class="dt">Abstract</span></span>
378<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a>              <span class="st">&quot;Corpus-DB: a Scriptable Textual Corpus Database for Cultural Analytics&quot;</span></span>
379<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a>              <span class="st">&quot;https://dh2020.adho.org/wp-content/uploads/2020/07/604_CorpusDBaScriptableTextualCorpusDatabaseforCulturalAnalytics.html&quot;</span></span>
380<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a>              (<span class="dt">Venue</span> <span class="st">&quot;Digital Humanities 2020&quot;</span></span>
381<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a>                     <span class="st">&quot;https://dh2020.adho.org/&quot;</span></span>
382<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a>                     <span class="st">&quot;Ottawa, CA&quot;</span>)),</span></code></pre></div>
383<p>At first glance, this may seem like an abuse of a programming
384language. Programming languages are for logic, the argument goes, and
385configuration belongs to configuration files, in formats like JSON,
386TOML, or YAML. We must separate logic and data, right? Actually, it
387turns out there are tons of advantages to writing data in Haskell.</p>
388<p>For one, it is compile-time type safe, meaning that my text editor
389actually checks that this compiles, as I’m writing it. If it were just
390plain text in YAML, there’s nothing stopping a <code
391class="verbatim">date</code> from being something else entirely, like
392<code class="verbatim">foo</code>. But in Haskell, if I write a <code
393class="verbatim">date</code> that isn’t two integers, (a year followed
394by a month), it’ll raise an error immediately.</p>
395<p>Another advantage is composability. If I find myself repeating a
396given entry, I can just abstract it away into its own function. For
397instance, I have a <code class="verbatim">Venue</code> entry that looks
398like this:</p>
399<div class="sourceCode" id="cb4"><pre
400class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a>cuEng <span class="ot">=</span> <span class="dt">Venue</span> <span class="st">&quot;Department of English and Comparative Literature&quot;</span> <span class="st">&quot;https://english.columbia.edu/&quot;</span> (uni <span class="st">&quot;cu&quot;</span>)</span></code></pre></div>
401<p>And whenever I want to use that, I can just call <code
402class="verbatim">venue = cuEng</code>, and save myself some typing. That
403also means that, should the name or URL of any of these entries change,
404I can change them in one place, without having to change them in
405seven.</p>
406<p>In addition to making everything type-safe, though, I greatly reduce
407the technologies involved. I’m still writing posts in Markdown, but have
408collapsed all the other templating languages to just Haskell. The HTML,
409CSS, and data, instead of being written in Liquid, Sass, YAML, and so
410forth, are all just in Haskell, which allows me to use the full power of
411the programming language to do whatever I need it to. For instance, if
412I’m doing something repeatedly, like adding a bunch of scripts:</p>
413<pre class="example"><code>&lt;script src=&quot;script1.js&quot;/&gt;
414&lt;script src=&quot;script2.js&quot;/&gt;
415&lt;script src=&quot;script3.js&quot;/&gt;
416&lt;script src=&quot;script4.js&quot;/&gt;
417</code></pre>
418<p>I can write that in Haskell like this:</p>
419<div class="sourceCode" id="cb6"><pre
420class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a>script n <span class="ot">=</span> script_ [src_ <span class="st">&quot;script&quot;</span> <span class="op">++</span> n <span class="op">++</span> <span class="st">&quot;.js&quot;</span>]</span>
421<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="fu">map</span> script [<span class="dv">1</span><span class="op">..</span><span class="dv">4</span>]</span></code></pre></div>
422<p>And it’s all validated at compile time, so when I press save in my
423text editor, it checks to make sure everything works. Because Haskell is
424a purely functional language, there are no runtime exceptions.</p>
425<p>All of this I achieve using the wonderful static site generator <a
426href="https://github.com/srid/rib">Rib</a>, which built on the Haskell
427build tool <a
428href="https://hackage.haskell.org/package/shake">Shake</a>. So if
429there’s anything else I need to do, which Rib doesn’t provide out of the
430box, I don’t need a plugin for it; I can just write a Shake action.</p>
431<p>There’s still some complexity left over, though, in that I still have
432to manage Haskell packages. But with <a
433href="https://nixos.org/">Nix</a> that’s trivial. Nix is great at making
434reproducible builds possible.</p>
435<p><a
436href="https://github.com/JonathanReeve/jonreeve.com/blob/master/src/Main.hs">Browse
437the source code for this site here.</a></p></article><hr><p>I welcome your comments and annotations in the Hypothes.is sidebar to the right. →</p>
437<script src="https://hypothes.is/embed.js"></script>
437</div><footer><div class="container"><div class="columns"><div class="column col-8"><p>I believe in openness. This work is licensed under a <a
438href="https://creativecommons.org/licenses/by-nc-sa/4.0/">Creative
439Commons Attribution-NonCommercial-ShareAlike 4.0 International
440License</a>, unless otherwise stated. All content ©Jonathan Reeve 2023.
441Hand-coded with love, using exclusively free and open-source software,
442including <a href="https://github.com/srid/ema">Ema</a> and <a
443href="https://haskell.org/">Haskell</a>. Hosted on <a
444href="https://github.com">GitHub</a> and served with <a
445href="https://netlify.com">Netlify</a>. Icons by Nhor, via <a
446href="https://thenounproject.com">The Noun Project</a>. <a
447href="https://www.buymeacoffee.com/vaIVQZH">Buy me a coffee</a> or
448support me <a href="https://liberapay.com/JonathanReeve/donate">via
449Libera Pay</a>.</p></div><div class="column col-4"><div class="icons"><a href="http://github.com/JonathanReeve"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="#fafafa" width="3em" height="3em"><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48
45048h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zM277.3 415.7c-8.4
4511.5-11.5-3.7-11.5-8 0-5.4.2-33 .2-55.3 0-15.6-5.2-25.5-11.3-30.7 37-4.1 76-9.2
45276-73.1 0-18.2-6.5-27.3-17.1-39 1.7-4.3 7.4-22-1.7-45-13.9-4.3-45.7 17.9-45.7
45317.9-13.2-3.7-27.5-5.6-41.6-5.6-14.1 0-28.4 1.9-41.6 5.6 0
4540-31.8-22.2-45.7-17.9-9.1 22.9-3.5 40.6-1.7 45-10.6 11.7-15.6 20.8-15.6 39 0
45563.6 37.3 69 74.3 73.1-4.8 4.3-9.1 11.7-10.6 22.3-9.5 4.3-33.8
45611.7-48.3-13.9-9.1-15.8-25.5-17.1-25.5-17.1-16.2-.2-1.1 10.2-1.1 10.2 10.8 5
45718.4 24.2 18.4 24.2 9.7 29.7 56.1 19.7 56.1 19.7 0 13.9.2 36.5.2 40.6 0 4.3-3
4589.5-11.5 8-66-22.1-112.2-84.9-112.2-158.3 0-91.8 70.2-161.5 162-161.5S388 165.6
459388 257.4c.1 73.4-44.7 136.3-110.7
460158.3zm-98.1-61.1c-1.9.4-3.7-.4-3.9-1.7-.2-1.5 1.1-2.8 3-3.2 1.9-.2 3.7.6 3.9
4611.9.3 1.3-1 2.6-3 3zm-9.5-.9c0 1.3-1.5 2.4-3.5 2.4-2.2.2-3.7-.9-3.7-2.4 0-1.3
4621.5-2.4 3.5-2.4 1.9-.2 3.7.9 3.7 2.4zm-13.7-1.1c-.4 1.3-2.4 1.9-4.1
4631.3-1.9-.4-3.2-1.9-2.8-3.2.4-1.3 2.4-1.9 4.1-1.5 2 .6 3.3 2.1 2.8
4643.4zm-12.3-5.4c-.9 1.1-2.8.9-4.3-.6-1.5-1.3-1.9-3.2-.9-4.1.9-1.1 2.8-.9 4.3.6
4651.3 1.3 1.8 3.3.9 4.1zm-9.1-9.1c-.9.6-2.6 0-3.7-1.5s-1.1-3.2 0-3.9c1.1-.9 2.8-.2
4663.7 1.3 1.1 1.5 1.1 3.3 0
4674.1zm-6.5-9.7c-.9.9-2.4.4-3.5-.6-1.1-1.3-1.3-2.8-.4-3.5.9-.9 2.4-.4 3.5.6 1.1
4681.3 1.3 2.8.4 3.5zm-6.7-7.4c-.4.9-1.7 1.1-2.8.4-1.3-.6-1.9-1.7-1.5-2.6.4-.6
4691.5-.9 2.8-.4 1.3.7 1.9 1.8 1.5 2.6z"></svg></a><a href="http://twitter.com/j0_0n"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="#fafafa" width="3em" height="3em"><path d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48
47048 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48zm-48.9 158.8c.2 2.8.2
4715.7.2 8.5 0 86.7-66 186.6-186.6 186.6-37.2 0-71.7-10.8-100.7-29.4 5.3.6 10.4.8
47215.8.8 30.7 0 58.9-10.4 81.4-28-28.8-.6-53-19.5-61.3-45.5 10.1 1.5 19.2 1.5
47329.6-1.2-30-6.1-52.5-32.5-52.5-64.4v-.8c8.7 4.9 18.9 7.9 29.6 8.3a65.447 65.447
4740 0 1-29.2-54.6c0-12.2 3.2-23.4 8.9-33.1 32.3 39.8 80.8 65.8 135.2 68.6-9.3-44.5
47524-80.6 64-80.6 18.9 0 35.9 7.9 47.9 20.7 14.8-2.8 29-8.3 41.6-15.8-4.9
47615.2-15.2 28-28.8 36.1 13.2-1.4 26-5.1 37.8-10.2-8.9 13.1-20.1 24.7-32.9 34z"></svg></a><a href="mailto:[email protected]"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" fill="#fafafa" width="3em" height="3em"><path d="M400 32H48C21.49 32 0 53.49 0 80v352c0 26.51 21.49 48
47748 48h352c26.51 0 48-21.49 48-48V80c0-26.51-21.49-48-48-48zM178.117
478262.104C87.429 196.287 88.353 196.121 64 177.167V152c0-13.255 10.745-24
47924-24h272c13.255 0 24 10.745 24 24v25.167c-24.371 18.969-23.434 19.124-114.117
48084.938-10.5 7.655-31.392 26.12-45.883
48125.894-14.503.218-35.367-18.227-45.883-25.895zM384 217.775V360c0 13.255-10.745
48224-24 24H88c-13.255 0-24-10.745-24-24V217.775c13.958 10.794 33.329 25.236 95.303
48370.214 14.162 10.341 37.975 32.145 64.694 32.01 26.887.134 51.037-22.041
48464.72-32.025 61.958-44.965 81.325-59.406 95.283-70.199z"></svg></a></div></div></div>
484<script data-goatcounter="https://jonreeve.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
484<script src="/assets/js/jquery-3.5.1.min.js"></script>
484<script src="/assets/js/main.js"></script>
484<script src="//cdn.jsdelivr.net/gh/highlightjs/[email protected]/build/highlight.min.js"></script>
484<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
484<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
484<script>hljs.initHighlightingOnLoad();</script>
484</div></footer></body></html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.