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">"Corpus-DB"</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">"https://github.com/JonathanReeve/corpus-db"</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">"JonathanReeve/corpus-db"</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">"A database and API for plain text archives, for digital humanities research."</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">"Corpus-DB: a Scriptable Textual Corpus Database for Cultural Analytics"</span></span> 379<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a> <span class="st">"https://dh2020.adho.org/wp-content/uploads/2020/07/604_CorpusDBaScriptableTextualCorpusDatabaseforCulturalAnalytics.html"</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">"Digital Humanities 2020"</span></span> 381<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a> <span class="st">"https://dh2020.adho.org/"</span></span> 382<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a> <span class="st">"Ottawa, CA"</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">"Department of English and Comparative Literature"</span> <span class="st">"https://english.columbia.edu/"</span> (uni <span class="st">"cu"</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><script src="script1.js"/> 414<script src="script2.js"/> 415<script src="script3.js"/> 416<script src="script4.js"/> 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">"script"</span> <span class="op">++</span> n <span class="op">++</span> <span class="st">".js"</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.