embedding-shapeshttps://emsh.cat/ 2026-01-27T00:00:00ZOne Human + One Agent = One Browser From Scratchhttps://emsh.cat/one-human-one-agent-one-browser/ 2026-01-27T00:00:00Z2026-01-27T00:00:00Z<p>Just for the fun of it, I thought I'd embark on a week-long quest to generate millions of tokens and millions of lines of source code to create one basic browser that can render HTML and CSS (no JS tho), and hopefully I could use this to receive even more VC investments.</p> <p>But then I remembered that I have something even better: a human brain! It is usually better than any machine at coordinating and thinking through things, so let's see if we can hack something together, one human brain and one LLM agent brain!</p> <p><video src="/content/one-human-one-agent-one-browser.webm" controls=""><a href="/content/one-human-one-agent-one-browser.webm">Demonstration of one-agent-one-browser running with a bunch of different websites on Linux/X11</a></video></p> <p>The above might look like a simple .webm video, but it's actually a highly sophisticated and advanced browser that was super hard to build, encoded as pixels in a video file! Wowzers.</p> <h2 id="day-1---starting-out">Day 1 - Starting out</h2> <p>For extra fun when building this, I set these requirements for myself and the agent:</p> <ul> <li>I have three days to build it</li> <li>Not a single 3rd party Rust library/dependency allowed</li> <li>Allowed to use anything (commonly) provided out of the box on the OS it runs on</li> <li>Should run on Windows, macOS and common Linux distributions</li> <li>Should be able to render some websites, most importantly, my own blog and Hacker News, should be easy right?</li> <li>The codebase can always compile and be built</li> <li>The codebase should be readable by a human, although code quality isn't the top concern</li> </ul> <p>So with these things in mind, I set out on the journal to build a browser "from scratch". I started with something really based, being able to just render "Hello World". Then to be able to render some nested tags. Added the ability of taking screenshots so the agent could use that. Added specifications for HTML/CSS (which I think the agent never used :| ), and tried to nail down the requirements for the agent to use. Also started doing "regression" or "E2E" tests with the screenshotting feature, so we could compare to some baseline images and so on. Added the ability to click on links just for the fun of it.</p> <p>After about a day together with Codex, I had something that could via X11 and cURL, fetch and render websites when run, and the Cargo.lock is empty. It was about 7500 lines long in total at that point, split across files with all of them under 1000 lines long (which was a stated requirement, so not a surprise).</p> <h2 id="day-2---moving-on">Day 2 - Moving On</h2> <p>Second day I got annoyed by the tests spawning windows while I was doing other stuff, so added a --headless flag too. Did some fixes for resizing the window, various compatibility fixes, some performance issues and improved the font/text rendering a bunch. Workflow was basically to pick a website, share a screenshot of the website without JavaScript, ask Codex to replicate it following our instructions. Most of the time was the agent doing work by itself, and me checking in when it notifies me it was done.</p> <h2 id="day-3---polish--cross-platform--day-4">Day 3 - Polish &amp; Cross-platform (+ day 4)</h2> <p>Third day we made large changes, lots of new features and a bunch of new features supported. More regression tests, fixing performance issues, fixing crashes and whatnot. Also added scrolling because this is a mother fucking browser, it has to be able to scroll. Added some debug logs too because that'll look cool in the demonstration video above, and also added support for the back button because it was annoying to start from scratch if I clicked the wrong link while testing.</p> <p>At the end of the third day we also added starting support for macOS, and managed to get a window to open, and the tests to pass. Seems to work OK :) Once we had that working, we also added Windows support, basically the same process, just another platform after all.</p> <p>Then the fourth day (whaaaat?) was basically polish, fixing CI for all three platforms, making it pass and finally cutting a release based on what got built in CI. Still all within 72 hours (3 days * 24 hours, which obviously this is how you count days).</p> <h2 id="the-results-after-3-days-70-hours">The results after ~3 days (~70 hours)</h2> <p>And here it is, in all its glory, made in ~20K lines of code and under 72 hours of total elapsed time from first commit to last:</p> <p><a href="https://github.com/embedding-shapes/one-agent-one-browser"><img src="/content/one-agent-one-browser-hn.png" alt="Screenshot of one-agent-one-browser running on X11" /></a></p> <blockquote> <p>You could try compiling it yourself (zero Rust dependencies, so it's really fast :) ), or you can find binaries built on CI here:<br/><small><a href="https://github.com/embedding-shapes/one-agent-one-browser/releases"><span>https://github.com/embedding-shapes/one-agent-one-browser/releases</span></a></small></p> </blockquote> <p>You can clone the repository, build it and try it out for yourself. It's not great, I wouldn't even say it's good, but it works, and demonstrates that one person with one agent can build a browser from scratch.</p> <p>This is what the "lines of code" count ended up being after all was said and done, including support for three OSes:</p> <pre class="shell"><code>$ git rev-parse HEAD e2556016a5aa504ecafd5577c1366854ffd0e280 $ cloc src --by-file 72 text files. 72 unique files. 0 files ignored. github.com/AlDanial/cloc v 2.06 T=0.06 s (1172.5 files/s, 373824.0 lines/s) ----------------------------------------------------------------------------------- File blank comment code ----------------------------------------------------------------------------------- src/layout/flex.rs 96 0 994 src/layout/inline.rs 85 0 933 src/layout/mod.rs 82 0 910 src/browser.rs 78 0 867 src/platform/macos/painter.rs 96 0 765 src/platform/x11/cairo.rs 77 0 713 src/platform/windows/painter.rs 88 0 689 src/bin/render-test.rs 87 0 666 src/style/builder.rs 83 0 663 src/platform/windows/d2d.rs 53 0 595 src/platform/windows/windowed.rs 72 0 591 src/style/declarations.rs 18 0 547 src/image.rs 81 0 533 src/platform/macos/windowed.rs 80 2 519 src/net/winhttp.rs 61 2 500 src/platform/x11/mod.rs 56 2 487 src/css.rs 103 346 423 src/html.rs 58 0 413 src/platform/x11/painter.rs 48 0 407 src/platform/x11/scale.rs 57 3 346 src/layout/table.rs 39 1 340 src/platform/x11/xft.rs 35 0 338 src/style/parse.rs 34 0 311 src/win/wic.rs 39 8 305 src/style/mod.rs 26 0 292 src/style/computer.rs 35 0 279 src/platform/x11/xlib.rs 32 0 278 src/layout/floats.rs 31 0 265 src/resources.rs 36 0 238 src/css_media.rs 36 1 232 src/debug.rs 32 0 227 src/platform/windows/dwrite.rs 20 0 222 src/render.rs 18 0 196 src/style/custom_properties.rs 34 0 186 src/platform/windows/scale.rs 28 0 184 src/url.rs 32 0 173 src/layout/helpers.rs 12 0 172 src/net/curl.rs 31 0 171 src/platform/macos/svg.rs 35 0 171 src/browser/url_loader.rs 17 0 166 src/platform/windows/gdi.rs 17 0 165 src/platform/windows/scaled.rs 16 0 159 src/platform/macos/scaled.rs 16 0 158 src/layout/svg_xml.rs 9 0 152 src/win/com.rs 26 0 152 src/png.rs 27 0 146 src/layout/replaced.rs 15 0 131 src/net/pool.rs 18 0 129 src/platform/macos/scale.rs 17 0 124 src/style/selectors.rs 18 0 123 src/style/length.rs 17 0 121 src/cli.rs 15 0 112 src/platform/windows/headless.rs 20 0 112 src/platform/macos/headless.rs 19 0 109 src/bin/fetch-resource.rs 14 0 101 src/geom.rs 10 0 101 src/browser/render_helpers.rs 11 0 100 src/dom.rs 11 0 100 src/style/background.rs 15 0 100 src/layout/tests.rs 7 0 85 src/platform/windows/d3d11.rs 14 0 83 src/win/stream.rs 10 0 63 src/platform/windows/svg.rs 13 0 54 src/main.rs 4 0 33 src/platform/mod.rs 6 0 28 src/app.rs 5 0 25 src/lib.rs 1 0 20 src/platform/windows/mod.rs 2 0 19 src/net/mod.rs 4 0 16 src/platform/macos/mod.rs 2 0 14 src/platform/windows/wstr.rs 0 0 5 src/win/mod.rs 0 0 3 ----------------------------------------------------------------------------------- SUM: 2440 365 20150 -----------------------------------------------------------------------------------</code></pre> <h2 id="takeaways">Takeaways</h2> <ul> <li>One human using one agent seems far more effective than one human using thousands of agents</li> <li>One agent can work on a single codebase for hours, making real progress on ambitious projects</li> <li>This could probably scale to multiple humans too, each equipped with their own agent, imagine what we could achieve!</li> <li>Sometimes slower is faster and also better</li> <li>The human who drives the agent might matter more than how the agents work and are set up, the judge is still out on this one</li> </ul> <p>If one person with one agent can produce equal or better results than "hundreds of agents for weeks", then the answer to the question: "Can we scale autonomous coding by throwing more agents at a problem?", probably has a more pessimistic answer than some expected.</p> Good Tastehttps://emsh.cat/good-taste/ 2026-01-25T00:00:00Z2026-01-25T00:00:00Z<p>There is a lot of doom going around, how all developers, creatives and others are losing our jobs because AI is coming to take them. Some of that doom is real; there's work that's basically throughput and spec compliance, and AI can replace big chunks of it. But what I’m talking about here is the other kind of work, where you have authorship and stake in what you're producing, as a human creator.</p> <p>I get why many of us fear AI and carry this sense of doom with us, but at the same time, as a creative I'm not super worried about it, and not because I think AI cannot generate things, or even generate good things, because I think it can. But because the hard thing has never been to just produce <em>something</em>. What's always been hard, is producing <em>something good</em>, something made by someone with Good Taste. A real human being that stand in front of hundreds of choices and knows (or feels) what gets to stay, what gets cut, what to push and what should be refused, and finally sharing the choices they made with you, through the medium.</p> <p>Just to be clear, I'm mainly talking about "creating" and authorship here, not consumption, since you as a consumer are the only judge if something is good or not for you. But when you're making something, if your goal is for others to enjoy it, Good Taste becomes a huge part of what you actually excel at. Not taste as in what the snobby critics do (who ultimately are consumers), but the creator's taste; direction, restraint, pacing, taking risks and sometimes causing offense.</p> <p>The AI models and the platforms seems to tend to regress to the mean. They optimize for something that "sounds right" and "doesn't upset anyone", creating something that is "acceptable" or even "plausible". This default voice is the opposite of authorship, where you explicitly <em>don't</em> want to just average thing out. You want to hit a specific emotional effect with specific rhythm, and you're sometimes willing to risk choices that look wrong until the entire thing is put together. I think this is why most AI output feels so bland and emotionless.</p> <p>I'm not trying to say that "AI can't make you feel anything" because I don't think it's true, I think AI can generate something that hits, you can even explicitly train and/or steer AI to produce more "emotional" outputs. But what's actually happening, even there, is that there is a human deciding and curating what counts as a "hit", and the model is learning the shape of that. Ultimately you're bottling taste, not replacing it. There is no stake at the other side, no point of view that is getting committed to, no moment where it suddenly goes "No, I'm not saying that" or "Yes, that's amazing, completely new direction now".</p> <p>Without any human steering and editing, the AI will just keep handing you infinite plausible takes until one of them happen to work. Infinite "fine", but not much more.</p> <p>Ultimately, I think AI is an accelerant. If you already have Good Taste, it'll help you move faster, which feels like a good thing: great people continue to produce great things.</p> <p>But on the other hand, it also makes it easier for people without taste to generate a lot of output that's either bland or sometimes straight up nonsense. Without the ecosystem rewarding high quality and good things, it instead rewards volume and speed, everything slightly tilting to noise.</p> <p>I feel like we're building the wrong things. The whole vibe right now is "replace the human part" instead of "make better tools for the human part". I don't want a machine that replaces my taste, I want tools that help me use my taste better; see the cut faster, compare directions, compare architectural choices, find where I've missed things, catch when we're going into generics, and help me make sharper intentional choices.</p> Cursor's latest "browser experiment" implied success without evidencehttps://emsh.cat/cursor-implied-success-without-evidence/ 2026-01-16T00:00:00Z2026-01-16T00:00:00Z<p>On January 14th 2026, Cursor published a blog post titled "Scaling long-running autonomous coding" (<a href="https://cursor.com/blog/scaling-agents">https://cursor.com/blog/scaling-agents</a>)</p> <p>In the blog post, they talk about their experiments with running "coding agents autonomously for weeks" with the explicit goal of</p> <blockquote> <p>understand[ing] how far we can push the frontier of agentic coding for projects that typically take human teams months to complete</p> </blockquote> <p>They talk about some approaches they tried, why they think those failed, and how to address the difficulties.</p> <p>Finally they arrived at a point where something "solved most of our coordination problems and let us scale to very large projects without any single agent", which then led to this:</p> <blockquote> <p>To test this system, we pointed it at an ambitious goal: building a web browser from scratch. The agents ran for close to a week, writing over 1 million lines of code across 1,000 files. You can explore the source code on GitHub (<a href="https://github.com/wilsonzlin/fastrender">https://github.com/wilsonzlin/fastrender</a>)</p> </blockquote> <p>This is where things get a bit murky and unclear. They claim "Despite the codebase size, new agents can still understand it and make meaningful progress" and "Hundreds of workers run concurrently, pushing to the same branch with minimal conflicts", but they never actually say if this is successful or not, is it actually working? Can you run this browser yourself? We don't know and they never say explicitly.</p> <p>After this, they embed the following video:</p> <p><video src="/content/cursor-screenshots.webm" controls=""><a href="/content/cursor-screenshots.webm">Video</a></video></p> <p>And below it, they say "While it might seem like a simple screenshot, building a browser from scratch is extremely difficult.".</p> <h3 id="they-never-actually-claim-this-browser-is-working-and-functional">They never actually claim this browser is working and functional</h3> <blockquote> <p>error: could not compile 'fastrender' (lib) due to 34 previous errors; 94 warnings emitted</p> </blockquote> <p>And if you try to compile it yourself, you'll see that it's very far away from being a functional browser at all, and seemingly, it never actually was able to build.</p> <p>Multiple recent GitHub Actions runs on <code>main</code> show failures (including workflow-file errors), and independent build attempts report dozens of compiler errors, recent PRs were all merged with failing CI, and going back in the Git history from most recent commit back 100 commits,<br/><a href="https://gist.github.com/embedding-shapes/f5d096dd10be44ff82b6e5ccdaf00b29">I couldn't find a single commit that compiled cleanly</a>.</p> <p>I'm not sure what the "agents" they unleashed on this codebase actually did, but they seemingly never ran "cargo build" or even less "cargo check", because both of those commands surface 10s of errors (which surely would balloon should we solve them) and about 100 warnings. There is an open GitHub issue in their repository about this right now: <a href="https://github.com/wilsonzlin/fastrender/issues/98">https://github.com/wilsonzlin/fastrender/issues/98</a></p> <p>And diving into the codebase, if the compilation errors didn't make that clear already, makes it very clear to any software developer that none of this is actually engineered code. It is what is typically known as "AI slop", low quality <em>something</em> that surely represents <em>something</em>, but it doesn't have intention behind it, and it doesn't even compile at this point.</p> <p>They later start to talk about what's next, but not a single word about how to run it, what to expect, how it's working or anything else. Cursor's blog post provides no reproducible demo and no known-good revision (tag/release/commit) to verify the screenshots, beyond linking the repo.</p> <p>Regardless of intent, Cursor's blog post creates the impression of a functioning prototype while leaving out the basic reproducibility markers one would expect from such claim. They never explicitly claim it's actually working, so no one can say they lied at least.</p> <p>They finish off the article saying:</p> <blockquote> <p>But the core question, can we scale autonomous coding by throwing more agents at a problem, has a more optimistic answer than we expected.</p> </blockquote> <p>Which seems like a really strange conclusion to arrive at, when all they've proved so far, is that agents can output millions of tokens and still not end up with something that actually works.</p> <p>A "browser experiment" doesn't need to rival Chrome. A reasonable minimum bar is: it compiles on a supported toolchain and can render a trivial HTML file. Cursor's post doesn’t demonstrate that bar, and current public build attempts fail at this too.</p> <h2 id="conclusion">Conclusion</h2> <p>Cursor never says "this browser is production-ready", but they do frame it as "building a web browser from scratch" and "meaningful progress" and then use a screenshot and "extremely difficult" language, wanting to give the impression that this experiment actually was a success.</p> <p>The closest they get to implying that this was a success, is this part:</p> <blockquote> <p>Hundreds of agents can work together on a single codebase for weeks, making real progress on ambitious projects.</p> </blockquote> <p>But this extraordinary claim isn't backed up by any evidence. In the blog post they never provide a working commit, build instructions or even a demo that can be reproduced.</p> <p>I don't think anyone expects this browser to be the next Chrome, but I do think that if you claim you've built a browser, it should at least be able to demonstrate being able to be compiled + loading a basic HTML file at the very least.</p> Niccup: Hiccup-like HTML Generation in ~120 Lines of Pure Nixhttps://emsh.cat/introducing-niccup/ 2025-12-03T00:00:00Z2025-12-03T00:00:00Z<p>Ever wish it was really simple to create HTML from just Nix expressions, not even having to deal with function calls or other complexities? With niccup, now there is!</p> <div class="sourceCode" id="cb1"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="op">[</span> <span class="st">&quot;div#main.container&quot;</span></span> <span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a> <span class="op">{</span> <span class="va">lang</span> <span class="op">=</span> <span class="st">&quot;en&quot;</span><span class="op">;</span> <span class="op">}</span></span> <span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;h1&quot;</span> <span class="st">&quot;Hello&quot;</span> <span class="op">]</span> <span class="op">]</span></span></code></pre></div> <div class="sourceCode" id="cb2"><pre class="sourceCode html"><code class="sourceCode html"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;</span><span class="kw">div</span><span class="ot"> class</span><span class="op">=</span><span class="st">&quot;container&quot;</span><span class="ot"> id</span><span class="op">=</span><span class="st">&quot;main&quot;</span><span class="ot"> lang</span><span class="op">=</span><span class="st">&quot;en&quot;</span><span class="dt">&gt;</span></span> <span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a> <span class="dt">&lt;</span><span class="kw">h1</span><span class="dt">&gt;</span>Hello<span class="dt">&lt;/</span><span class="kw">h1</span><span class="dt">&gt;</span></span> <span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a><span class="dt">&lt;/</span><span class="kw">div</span><span class="dt">&gt;</span></span></code></pre></div> <p>That's it. Nix data structures in, HTML out. Zero dependencies. Works with flakes or without.</p> <p><a href="https://github.com/embedding-shapes/niccup">Source Code</a> | <a href="https://emsh.cat/niccup/">Website/Docs</a> | <a href="https://emsh.cat/introducing-niccup/">Introduction Blog Post</a></p> <h2 id="why-generate-html-from-nix">Why Generate HTML from Nix?</h2> <p>If you're building static sites, documentation, or web artifacts as part of a Nix derivation, you've probably resorted to one of these:</p> <ol type="1"> <li>String interpolation (<code>''&lt;div&gt;${title}&lt;/div&gt;''</code>). Works until you need escaping or composition</li> <li>External templating tools. Another dependency, another language, another build step</li> <li>Importing HTML files, no programmatic generation</li> </ol> <p>Niccup takes a different approach: represent HTML as native Nix data structures. This gives you <code>map</code>, <code>filter</code>, <code>builtins.concatStringsSep</code>, and the entire Nix expression language for free. No new syntax to learn. No dependencies to manage.</p> <h2 id="the-syntax">The Syntax</h2> <p>An element is a list: <code>[ tag-spec attrs? children... ]</code></p> <h3 id="tag-specs-with-css-shorthand">Tag Specs with CSS Shorthand</h3> <div class="sourceCode" id="cb3"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;div&quot;</span></span> <span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;div&gt;&lt;/div&gt;</span></span> <span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a></span> <span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;input#search&quot;</span></span> <span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;input id=&quot;search&quot;&gt;</span></span> <span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a></span> <span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;button.btn.primary&quot;</span></span> <span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;button class=&quot;btn primary&quot;&gt;&lt;/button&gt;</span></span> <span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a></span> <span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="st">&quot;form#login.auth.dark&quot;</span></span> <span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;form class=&quot;auth dark&quot; id=&quot;login&quot;&gt;&lt;/form&gt;</span></span></code></pre></div> <h3 id="attributes">Attributes</h3> <p>The optional second element can be an attribute set:</p> <div class="sourceCode" id="cb4"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="op">[</span> <span class="st">&quot;a&quot;</span></span> <span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a> <span class="op">{</span> <span class="va">href</span> <span class="op">=</span> <span class="st">&quot;/about&quot;</span><span class="op">;</span> <span class="va">target</span> <span class="op">=</span> <span class="st">&quot;_blank&quot;</span><span class="op">;</span> <span class="op">}</span></span> <span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;About&quot;</span> <span class="op">]</span></span> <span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;a href=&quot;/about&quot; target=&quot;_blank&quot;&gt;About&lt;/a&gt;</span></span></code></pre></div> <p>Classes from the shorthand and attribute set are merged:</p> <div class="sourceCode" id="cb5"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="op">[</span> <span class="st">&quot;div.base&quot;</span></span> <span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a> <span class="op">{</span> <span class="va">class</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;added&quot;</span> <span class="st">&quot;another&quot;</span> <span class="op">];</span> <span class="op">}</span></span> <span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;content&quot;</span> <span class="op">]</span></span> <span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;div class=&quot;base added another&quot;&gt;content&lt;/div&gt;</span></span></code></pre></div> <p>Boolean handling:</p> <div class="sourceCode" id="cb6"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="op">[</span> <span class="st">&quot;input&quot;</span></span> <span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a> <span class="op">{</span> <span class="va">type</span> <span class="op">=</span> <span class="st">&quot;checkbox&quot;</span><span class="op">;</span></span> <span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a> <span class="va">checked</span> <span class="op">=</span> <span class="cn">true</span><span class="op">;</span></span> <span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a> <span class="va">disabled</span> <span class="op">=</span> <span class="cn">false</span><span class="op">;</span> <span class="op">}</span> <span class="op">]</span></span> <span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;input checked=&quot;checked&quot; type=&quot;checkbox&quot;&gt;</span></span></code></pre></div> <p><code>true</code> renders as <code>attr="attr"</code>. <code>false</code> and <code>null</code> are omitted entirely.</p> <h3 id="children-and-composition">Children and Composition</h3> <p>Children can be strings, numbers, nested elements, or lists:</p> <div class="sourceCode" id="cb7"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="op">[</span> <span class="st">&quot;p&quot;</span></span> <span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Text with &quot;</span></span> <span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;strong&quot;</span> <span class="st">&quot;emphasis&quot;</span> <span class="op">]</span></span> <span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot; and more.&quot;</span> <span class="op">]</span></span> <span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;p&gt;Text with &lt;strong&gt;emphasis&lt;/strong&gt; and more.&lt;/p&gt;</span></span></code></pre></div> <p>Lists are flattened one level, which makes <code>map</code> work naturally:</p> <div class="sourceCode" id="cb8"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="op">[</span> <span class="st">&quot;ul&quot;</span></span> <span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a> <span class="op">(</span><span class="bu">map</span> <span class="op">(</span><span class="va">item</span><span class="op">:</span> <span class="op">[</span> <span class="st">&quot;li&quot;</span> item <span class="op">])</span></span> <span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;One&quot;</span> <span class="st">&quot;Two&quot;</span> <span class="st">&quot;Three&quot;</span> <span class="op">])</span> <span class="op">]</span></span> <span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;ul&gt;&lt;li&gt;One&lt;/li&gt;&lt;li&gt;Two&lt;/li&gt;&lt;li&gt;Three&lt;/li&gt;&lt;/ul&gt;</span></span></code></pre></div> <p>Text content is automatically escaped:</p> <div class="sourceCode" id="cb9"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="op">[</span> <span class="st">&quot;p&quot;</span> <span class="st">&quot;&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;&quot;</span> <span class="op">]</span></span> <span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;p&gt;&amp;lt;script&amp;gt;alert(&#39;xss&#39;)&amp;lt;/script&amp;gt;&lt;/p&gt;</span></span></code></pre></div> <h3 id="raw-html-and-comments">Raw HTML and Comments</h3> <p>For trusted HTML that shouldn't be escaped:</p> <div class="sourceCode" id="cb10"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="op">[</span> <span class="st">&quot;div&quot;</span> <span class="op">(</span>raw <span class="st">&quot;&lt;strong&gt;Already formatted&lt;/strong&gt;&quot;</span><span class="op">)</span> <span class="op">]</span></span> <span id="cb10-2"><a href="#cb10-2" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;div&gt;&lt;strong&gt;Already formatted&lt;/strong&gt;&lt;/div&gt;</span></span></code></pre></div> <p>For HTML comments:</p> <div class="sourceCode" id="cb11"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="op">[</span> <span class="st">&quot;div&quot;</span> <span class="op">(</span>comment <span class="st">&quot;TODO: refactor&quot;</span><span class="op">)</span></span> <span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;p&quot;</span> <span class="st">&quot;Content&quot;</span> <span class="op">]</span> <span class="op">]</span></span> <span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;div&gt;&lt;!-- </span><span class="al">TODO</span><span class="co">: refactor --&gt;&lt;p&gt;Content&lt;/p&gt;&lt;/div&gt;</span></span></code></pre></div> <h3 id="void-elements">Void Elements</h3> <p>Self-closing tags work as expected:</p> <div class="sourceCode" id="cb12"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="op">[</span> <span class="st">&quot;img&quot;</span> <span class="op">{</span> <span class="va">src</span> <span class="op">=</span> <span class="st">&quot;photo.jpg&quot;</span><span class="op">;</span> <span class="va">alt</span> <span class="op">=</span> <span class="st">&quot;A photo&quot;</span><span class="op">;</span> <span class="op">}</span> <span class="op">]</span></span> <span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;img alt=&quot;A photo&quot; src=&quot;photo.jpg&quot;&gt;</span></span> <span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a></span> <span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a><span class="op">[</span> <span class="st">&quot;meta&quot;</span> <span class="op">{</span> <span class="va">charset</span> <span class="op">=</span> <span class="st">&quot;utf-8&quot;</span><span class="op">;</span> <span class="op">}</span> <span class="op">]</span></span> <span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a><span class="co"># &lt;meta charset=&quot;utf-8&quot;&gt;</span></span></code></pre></div> <h2 id="api">API</h2> <p>Four functions. That's the entire public interface.</p> <table> <thead> <tr> <th>Function</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td><code>render</code></td> <td>Render to minified HTML</td> </tr> <tr> <td><code>renderPretty</code></td> <td>Render to indented HTML (2-space indent)</td> </tr> <tr> <td><code>raw</code></td> <td>Mark a string as trusted, unescaped HTML</td> </tr> <tr> <td><code>comment</code></td> <td>Create an HTML comment node</td> </tr> </tbody> </table> <h2 id="a-real-example-blog-generator">A Real Example: Blog Generator</h2> <div class="sourceCode" id="cb13"><pre class="sourceCode nix"><code class="sourceCode nix"><span id="cb13-1"><a href="#cb13-1" aria-hidden="true" tabindex="-1"></a><span class="op">{</span> <span class="va">pkgs</span><span class="op">,</span> <span class="va">niccup</span> <span class="op">}</span>:</span> <span id="cb13-2"><a href="#cb13-2" aria-hidden="true" tabindex="-1"></a><span class="kw">let</span></span> <span id="cb13-3"><a href="#cb13-3" aria-hidden="true" tabindex="-1"></a> <span class="va">h</span> <span class="op">=</span> niccup<span class="op">.</span>lib<span class="op">;</span></span> <span id="cb13-4"><a href="#cb13-4" aria-hidden="true" tabindex="-1"></a></span> <span id="cb13-5"><a href="#cb13-5" aria-hidden="true" tabindex="-1"></a> <span class="va">posts</span> <span class="op">=</span> <span class="op">[</span></span> <span id="cb13-6"><a href="#cb13-6" aria-hidden="true" tabindex="-1"></a> <span class="op">{</span> <span class="va">slug</span> <span class="op">=</span> <span class="st">&quot;hello&quot;</span><span class="op">;</span> <span class="va">title</span> <span class="op">=</span> <span class="st">&quot;Hello World&quot;</span><span class="op">;</span> <span class="va">body</span> <span class="op">=</span> <span class="st">&quot;Welcome!&quot;</span><span class="op">;</span> <span class="op">}</span></span> <span id="cb13-7"><a href="#cb13-7" aria-hidden="true" tabindex="-1"></a> <span class="op">{</span> <span class="va">slug</span> <span class="op">=</span> <span class="st">&quot;update&quot;</span><span class="op">;</span> <span class="va">title</span> <span class="op">=</span> <span class="st">&quot;An Update&quot;</span><span class="op">;</span> <span class="va">body</span> <span class="op">=</span> <span class="st">&quot;More content here.&quot;</span><span class="op">;</span> <span class="op">}</span></span> <span id="cb13-8"><a href="#cb13-8" aria-hidden="true" tabindex="-1"></a> <span class="op">];</span></span> <span id="cb13-9"><a href="#cb13-9" aria-hidden="true" tabindex="-1"></a></span> <span id="cb13-10"><a href="#cb13-10" aria-hidden="true" tabindex="-1"></a> <span class="va">layout</span> <span class="op">=</span> <span class="op">{</span> <span class="va">title</span><span class="op">,</span> <span class="va">content</span> <span class="op">}</span>: h<span class="op">.</span>renderPretty <span class="op">[</span></span> <span id="cb13-11"><a href="#cb13-11" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;html&quot;</span> <span class="op">{</span> <span class="va">lang</span> <span class="op">=</span> <span class="st">&quot;en&quot;</span><span class="op">;</span> <span class="op">}</span></span> <span id="cb13-12"><a href="#cb13-12" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;head&quot;</span></span> <span id="cb13-13"><a href="#cb13-13" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;meta&quot;</span> <span class="op">{</span> <span class="va">charset</span> <span class="op">=</span> <span class="st">&quot;utf-8&quot;</span><span class="op">;</span> <span class="op">}</span> <span class="op">]</span></span> <span id="cb13-14"><a href="#cb13-14" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;meta&quot;</span> <span class="op">{</span> <span class="va">name</span> <span class="op">=</span> <span class="st">&quot;viewport&quot;</span><span class="op">;</span> <span class="va">content</span> <span class="op">=</span> <span class="st">&quot;width=device-width&quot;</span><span class="op">;</span> <span class="op">}</span> <span class="op">]</span></span> <span id="cb13-15"><a href="#cb13-15" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;title&quot;</span> title <span class="op">]</span></span> <span id="cb13-16"><a href="#cb13-16" aria-hidden="true" tabindex="-1"></a> <span class="op">]</span></span> <span id="cb13-17"><a href="#cb13-17" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;body&quot;</span></span> <span id="cb13-18"><a href="#cb13-18" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;nav&quot;</span> <span class="op">(</span><span class="bu">map</span> <span class="op">(</span><span class="va">p</span><span class="op">:</span> <span class="op">[</span> <span class="st">&quot;a&quot;</span> <span class="op">{</span> <span class="va">href</span> <span class="op">=</span> <span class="st">&quot;/</span><span class="sc">${</span>p<span class="op">.</span>slug<span class="sc">}</span><span class="st">.html&quot;</span><span class="op">;</span> <span class="op">}</span> p<span class="op">.</span>title <span class="op">])</span> posts<span class="op">)</span> <span class="op">]</span></span> <span id="cb13-19"><a href="#cb13-19" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;main&quot;</span> content <span class="op">]</span></span> <span id="cb13-20"><a href="#cb13-20" aria-hidden="true" tabindex="-1"></a> <span class="op">[</span> <span class="st">&quot;footer&quot;</span> <span class="st">&quot;Generated with niccup&quot;</span> <span class="op">]</span></span> <span id="cb13-21"><a href="#cb13-21" aria-hidden="true" tabindex="-1"></a> <span class="op">]</span></span> <span id="cb13-22"><a href="#cb13-22" aria-hidden="true" tabindex="-1"></a> <span class="op">];</span></span> <span id="cb13-23"><a href="#cb13-23" aria-hidden="true" tabindex="-1"></a></span> <span id="cb13-24"><a href="#cb13-24" aria-hidden="true" tabindex="-1"></a> <span class="va">renderPost</span> <span class="op">=</span> <span class="va">post</span><span class="op">:</span> layout <span class="op">{</span></span> <span id="cb13-25"><a href="#cb13-25" aria-hidden="true" tabindex="-1"></a> <span class="va">title</span> <span class="op">=</span> post<span class="op">.</span>title<span class="op">;</span></span> <span id="cb13-26"><a href="#cb13-26" aria-hidden="true" tabindex="-1"></a> <span class="va">content</span> <span class="op">=</span> <span class="op">[</span> <span class="st">&quot;article&quot;</span> <span class="op">[</span> <span class="st">&quot;h1&quot;</span> post<span class="op">.</span>title <span class="op">]</span> <span class="op">[</span> <span class="st">&quot;p&quot;</span> post<span class="op">.</span>body <span class="op">]</span> <span class="op">];</span></span> <span id="cb13-27"><a href="#cb13-27" aria-hidden="true" tabindex="-1"></a> <span class="op">};</span></span> <span id="cb13-28"><a href="#cb13-28" aria-hidden="true" tabindex="-1"></a></span> <span id="cb13-29"><a href="#cb13-29" aria-hidden="true" tabindex="-1"></a><span class="kw">in</span> pkgs<span class="op">.</span>runCommand <span class="st">&quot;blog&quot;</span> <span class="op">{}</span> <span class="st">&#39;&#39;</span></span> <span id="cb13-30"><a href="#cb13-30" aria-hidden="true" tabindex="-1"></a><span class="st"> mkdir -p $out</span></span> <span id="cb13-31"><a href="#cb13-31" aria-hidden="true" tabindex="-1"></a><span class="st"> </span><span class="sc">${</span><span class="bu">builtins</span><span class="op">.</span>concatStringsSep <span class="st">&quot;</span><span class="sc">\n</span><span class="st">&quot;</span> <span class="op">(</span><span class="bu">map</span> <span class="op">(</span><span class="va">p</span><span class="op">:</span> <span class="st">&#39;&#39;</span></span> <span id="cb13-32"><a href="#cb13-32" aria-hidden="true" tabindex="-1"></a><span class="st"> cat &gt; $out/</span><span class="sc">${</span>p<span class="op">.</span>slug<span class="sc">}</span><span class="st">.html &lt;&lt; &#39;EOF&#39;</span></span> <span id="cb13-33"><a href="#cb13-33" aria-hidden="true" tabindex="-1"></a><span class="st"> </span><span class="sc">${</span>renderPost p<span class="sc">}</span></span> <span id="cb13-34"><a href="#cb13-34" aria-hidden="true" tabindex="-1"></a><span class="st"> EOF</span></span> <span id="cb13-35"><a href="#cb13-35" aria-hidden="true" tabindex="-1"></a><span class="st"> &#39;&#39;</span><span class="op">)</span> posts<span class="op">)</span><span class="sc">}</span></span> <span id="cb13-36"><a href="#cb13-36" aria-hidden="true" tabindex="-1"></a><span class="st">&#39;&#39;</span></span></code></pre></div> <p>This produces a complete static site as a Nix derivation. Add a post to the list, rebuild, done.</p> <h2 id="limitations">Limitations</h2> <p>Being upfront about what niccup doesn't do:</p> <ul> <li><p><strong>Attribute order is alphabetical.</strong> Nix attribute sets have no insertion order; <code>builtins.attrNames</code> returns keys sorted lexicographically. You cannot control attribute order in the output.</p></li> <li><p><strong>One-level flattening only.</strong> <code>[ "ul" (map ...) ]</code> works because <code>map</code> returns a list that gets flattened. Deeper nesting like <code>[ "ul" [ [ [ "li" "x" ] ] ] ]</code> won't flatten further, you'll get nested elements, not flattened children.</p></li> <li><p><strong>Eager evaluation.</strong> The entire tree is evaluated before rendering. For the static site generation use case, this is fine. If you're generating gigabytes of HTML, this isn't your tool.</p></li> <li><p><strong>No streaming.</strong> Output is a single string. Again, fine for static sites; not designed for chunked HTTP responses.</p></li> </ul> <h2 id="why-hiccup">Why Hiccup?</h2> <p>The Hiccup format originated in Clojure and has been battle-tested for over a decade. It maps naturally to Nix because both languages treat data structures as first-class citizens. The syntax is minimal, just lists and attribute sets, and composes with existing Nix idioms without friction.</p> <p>The name "niccup" is a portmanteau: <strong>Ni</strong>x + Hic<strong>cup</strong>.</p> <h2 id="source">Source</h2> <p>The entire implementation is ~120 lines of pure Nix with no external dependencies. The code, tests, and additional examples are available at:</p> <p><strong><a href="https://github.com/embedding-shapes/niccup">github.com/embedding-shapes/niccup</a></strong></p> <p>MIT licensed.</p>