<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" media="screen" href="/~files/atom-premium.xsl"?>
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:feedpress="https://feed.press/xmlns" xmlns:media="http://search.yahoo.com/mrss/" xmlns:podcast="https://podcastindex.org/namespace/1.0">
  <feedpress:locale>en</feedpress:locale>
  <feedpress:newsletterId>brettterpstra</feedpress:newsletterId>
  <link rel="hub" href="https://feedpress.superfeedr.com/"/>
  <logo>https://static.feedpress.com/logo/brettterpstra-5d7926fa5332c.png</logo>
  <link rel="alternate" type="text/html" href="https://brettterpstra.com"/>
  <subtitle>Welcome to The Lab, detailing the coding and automation exploits of Brett Terpstra.</subtitle>
  <title>BrettTerpstra.com - The Mad Science of Brett Terpstra</title>
  <link href="https://brett.trpstra.net/" rel="self"/>
  <link href="https://brettterpstra.com/"/>
  <updated>2026-03-05T12:00:38-06:00</updated>
  <id>https://brettterpstra.com/</id>
  <author>
    <name><![CDATA[Brett Terpstra]]></name>
  </author>
  <generator uri="https://brettterpstra.com">BrettTerpstra</generator>
  <entry>
    <title type="html"><![CDATA[Web Excursions for March 5th, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17294032/web-excursions-for-march-5th-2026"/>
    <updated>2026-03-05T12:00:00-06:00</updated>
    <published>2026-03-05T12:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/03/05/web-excursions-for-march-5th-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with Setapp. <a href="https://go.setapp.com/stp44">Get access to hundreds of Mac and iOS apps for one low monthly subscription fee.</a></p>

<dl>
  <dt><a href="https://github.com/julienXX/terminal-notifier">julienXX/terminal-notifier</a></dt>
  <dd>Send User Notifications on macOS from the command-line. A great shell script companion that doesn&rsquo;t require making your own calls to <code class="language-plaintext highlighter-rouge">osascript</code>.</dd>
  <dt><a href="https://github.com/giladdarshan/gdialog">giladdarshan/gdialog: Display macOS dialogs from terminal and scripts</a></dt>
  <dd>Another handy scripting tool to display macOS dialogs from terminal and scripts. There was a more involved version of this that I used to use years ago, but this fits the bill nicely for my current needs.</dd>
  <dt><a href="https://github.com/steveyegge/beads">steveyegge/beads</a></dt>
  <dd>
    <blockquote>
      <p>Beads - A memory upgrade for your coding agent.</p>
    </blockquote>
  </dd>
  <dd>I haven&rsquo;t quite gotten the hang of this yet, but I think it has a lot of potential. A persistent, structured memory for coding agents, replacing markdown plans with a dependency-aware graph to handle long-horizon tasks without losing context.</dd>
  <dt><a href="https://github.com/gsd-build/get-shit-done">Get Sh*t Done</a></dt>
  <dd>
    <blockquote>
      <p>A light-weight and powerful meta-prompting, context engineering and spec-driven development system for Claude Code and OpenCode.</p>
    </blockquote>
  </dd>
  <dd>I&rsquo;m not a Claude Code user, but for non-masochistic developers, this looks like an excellent tool.</dd>
</dl>

<p><a href="https://go.setapp.com/stp44">Check out Setapp</a> today and get access to the best Mac and iOS apps out there.</p>

<p>Like or share this post <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F05%2Fweb-excursions-for-march-5th-2026%2F&text=Web+Excursions+for+March+5th%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F05%2Fweb-excursions-for-march-5th-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17294032.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[A (slightly better?) linkding extension for Firefox and Chrome]]></title>
    <link href="https://brett.trpstra.net/link/535/17293138/a-slightly-better-linkding-extension-for-firefox-and-chrome"/>
    <updated>2026-03-04T08:00:00-06:00</updated>
    <published>2026-03-04T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/03/04/a-slightly-better-linkding-extension-for-firefox-and-chrome</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/03/bt-linkding-header-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I&rsquo;ve been using <a href="https://github.com/sissbruecker/linkding-extension/">Sascha Ißbrücker&rsquo;s extension</a> for <a href="https://github.com/sissbruecker/linkding">linkding</a> with my <a href="https://bookmarks.scoffb.in">own instance</a> for a while now, and while it&rsquo;s simple and works great, there&rsquo;s one thing that always frustrated me: the popover window closes the moment you click outside of it, erasing what you&rsquo;ve entered.</p>

<p>Obviously, this will only be of use to people who:</p>

<ul>
  <li>Have a linkding server</li>
  <li>Use Firefox or Chrome for general browsing</li>
</ul>

<p>If you fit the target audience, read on!</p>

<p>Picture: you&rsquo;re adding a bookmark, you&rsquo;ve filled in the title and tags, maybe added some notes, and then you realize you need to copy something from the current page. Or maybe you want to paste some text from another window. The moment you click away to grab that text, <em>poof</em> &mdash; the popover closes and you lose everything you&rsquo;ve typed.</p>

<h3 id="the-persistent-popup-solution">The persistent popup solution</h3>

<p>So I created <a href="https://github.com/ttscoff/bt-linkding">bt-linkding</a>, a fork that opens the bookmark panel in a <strong>persistent popup window</strong> instead of the default toolbar popover. The window stays open until you explicitly click <strong>Save</strong>, <strong>Update</strong>, or <strong>Cancel</strong>. No more accidentally dismissing the window and losing your changes.</p>

<p>This means you can freely navigate away from the bookmark panel to copy text from the current page, paste from another window, or do whatever else you need to do. Your work stays put until you&rsquo;re ready to save it.</p>

<p>The extension keeps all the features from the original like tag autocomplete, automatic page description detection, keyboard shortcuts (<code class="language-plaintext highlighter-rouge">Alt+Shift+L</code> to bookmark the current tab), and Omnibox search (type <code class="language-plaintext highlighter-rouge">ld</code> in the address bar). It just fixes that one annoying behavior.</p>

<h3 id="building-and-installing">Building and installing</h3>

<p>Right now, the extension isn&rsquo;t available in the browser stores (though I&rsquo;ve submitted it to both Firefox and Chrome, but this is my first time submitting to either, so no promises about when or if it&rsquo;ll be available there). For now, you&rsquo;ll need to build and install it manually.</p>

<p>The process is pretty straightforward. First, clone the repo and install dependencies:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>npm <span class="nb">install</span></code></pre></div></div>

<p>Then build for your target browser. Firefox and Chrome require different background script formats, so you need to use the right build command:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c"># For Firefox</span>
npm run build:firefox

<span class="c"># For Chrome</span>
npm run build:chrome</code></pre></div></div>

<p>This updates the <code class="language-plaintext highlighter-rouge">manifest.json</code> file for the target browser. Once that&rsquo;s done, you can load it as an unpacked extension.</p>

<h3 id="installing-in-firefox">Installing in Firefox</h3>

<p>After running <code class="language-plaintext highlighter-rouge">npm run build:firefox</code>, open Firefox and navigate to <code class="language-plaintext highlighter-rouge">about:debugging</code>. Click &ldquo;This Firefox&rdquo; in the sidebar, then click &ldquo;Load Temporary Add-on&hellip;&rdquo;. Navigate to the project root directory and select any file in the extension (or just the <code class="language-plaintext highlighter-rouge">manifest.json</code> file). The extension will load and you&rsquo;re good to go.</p>

<h3 id="installing-in-chrome">Installing in Chrome</h3>

<p>After running <code class="language-plaintext highlighter-rouge">npm run build:chrome</code>, open Chrome and navigate to <code class="language-plaintext highlighter-rouge">chrome://extensions/</code>. Enable &ldquo;Developer mode&rdquo; (toggle in the top right), then click &ldquo;Load unpacked&rdquo;. Navigate to the project root directory and select it. The extension will load immediately.</p>

<h3 id="whats-next">What&rsquo;s next</h3>

<p>I&rsquo;ve packaged the extension for both stores (<code class="language-plaintext highlighter-rouge">npm run package:firefox</code> and <code class="language-plaintext highlighter-rouge">npm run package:chrome</code> create the zip files), and I&rsquo;ve submitted it to both. But since this is my first time going through the submission process for either store, I&rsquo;m not making any promises about availability or timing. For now, building from source is the way to go.</p>

<p>If the popover closing unexpectedly is a nit for you as well, give this fork a try. The persistent window makes a world of difference when you&rsquo;re trying to add bookmarks with content from multiple sources.</p>

<p>Full instructions and code are <a href="https://github.com/ttscoff/bt-linkding">on GitHub</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116171557755669087">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=A+%28slightly+better%3F%29+linkding+extension+for+Firefox+and+Chrome%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F04%2Fa-slightly-better-linkding-extension-for-firefox-and-chrome%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F04%2Fa-slightly-better-linkding-extension-for-firefox-and-chrome%2F&text=A+%28slightly+better%3F%29+linkding+extension+for+Firefox+and+Chrome&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F04%2Fa-slightly-better-linkding-extension-for-firefox-and-chrome%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17293138.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/03/bt-linkding-header-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/03/bt-linkding-header-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for March 3rd, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17293139/web-excursions-for-march-3rd-2026"/>
    <updated>2026-03-03T12:00:00-06:00</updated>
    <published>2026-03-03T12:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/03/03/web-excursions-for-march-3rd-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with Backblaze. <a href="https://secure.backblaze.com/r/00dszk">Back up everything.</a></p>

<dl>
  <dt><a href="https://docs.docmd.io/">docmd &mdash; Minimalist Markdown Docs Generator</a></dt>
  <dd>
    <blockquote>
      <p>Generate beautiful, lightweight static documentation sites directly from your Markdown files with docmd. Zero clutter, just content.</p>
    </blockquote>
  </dd>
  <dd>
    <p>I&rsquo;ve written multiple versions of this concept for myself, ranging from Jekyll plugin-based sites to wholly custom solutions using Apex. This is a very nice, plugin-capable solution that I look forward to trying out for my next documentation project.</p>
  </dd>
  <dt><a href="https://github.com/SourceDocs/SourceDocs">SourceDocs/SourceDocs: Generate Markdown documentation from source code</a></dt>
  <dd>As I get more into Swift development, automatic generation of documentation from source code comments is very nice.</dd>
  <dt><a href="https://github.com/Kapeli/cheatset">Kapeli/cheatset: Generate cheat sheets for Dash</a></dt>
  <dd>I recently had a pull request merged that fixed Cheatset for versions 2.7-4.x compatibility. If you want to make cheat sheets for Dash easily, this provides a DSL for doing so. I&rsquo;ve incorporated it into several workflows, including a new one that uses Apex to convert Markdown documents into Dash cheatsheets that I&rsquo;ll publish soon.</dd>
  <dt><a href="https://www.macbartender.com/Bartender6/blog/">Bartender 6</a></dt>
  <dd>The latest builds of Bartender 6 bring it once again to the top of my list of menu bar managers. Super smooth on Tahoe, and beats Ice and Barbee for me now.</dd>
</dl>

<p>Backblaze securely backs up your entire computer to the cloud, affordably and reliably. I trust it with all my data. <a href="https://secure.backblaze.com/r/00dszk">Check it out today.</a></p>

<p>Like or share this post <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F03%2Fweb-excursions-for-march-3rd-2026%2F&text=Web+Excursions+for+March+3rd%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F03%2Fweb-excursions-for-march-3rd-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17293139.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Generating Man Pages with Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17287905/generating-man-pages-with-apex"/>
    <updated>2026-03-02T08:00:00-06:00</updated>
    <published>2026-03-02T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/03/02/generating-man-pages-with-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>I&rsquo;ve been using a combination of Pandoc and other tools to generate the man page for Apex from Markdown. As Apex nears a 1.0 release, I figured it should be able to generate man pages on its own.</p>

<p>Man pages are what gets shown when you run <code class="language-plaintext highlighter-rouge">man COMMAND</code> in Terminal. They use <code class="language-plaintext highlighter-rouge">roff</code> formatting, which is a pain to write as a human. Writing documentation in Markdown is much easier. Apex can now turn your Markdown into roff man pages or styled HTML man docs. Two output formats cover both classic terminals and the web.</p>

<p><strong><code class="language-plaintext highlighter-rouge">-t man</code></strong> turns Markdown into roff (the traditional man-page source). Use it to generate <code class="language-plaintext highlighter-rouge">apex.1</code>, <code class="language-plaintext highlighter-rouge">my-tool.1</code>, and so on, then install them with <code class="language-plaintext highlighter-rouge">make install</code> or your package manager. No pandoc or go-md2man needed: after <code class="language-plaintext highlighter-rouge">make build</code>, <code class="language-plaintext highlighter-rouge">make man</code> runs the built <code class="language-plaintext highlighter-rouge">apex</code> binary with <code class="language-plaintext highlighter-rouge">-t man</code> to produce the man pages. Write your docs in Markdown and keep a single source for both the website and <code class="language-plaintext highlighter-rouge">man apex</code>.</p>

<p><strong><code class="language-plaintext highlighter-rouge">-t man-html</code></strong> turns the same Markdown into HTML. Without <code class="language-plaintext highlighter-rouge">-s</code> you get a content-only snippet (no wrapper, no nav), handy for embedding. With <code class="language-plaintext highlighter-rouge">-s</code> (standalone) you get a full page: fixed left sidebar TOC (NAME, SYNOPSIS, DESCRIPTION, etc.), a large headline from the NAME section, and optional custom CSS via <code class="language-plaintext highlighter-rouge">--css</code> or <code class="language-plaintext highlighter-rouge">--style</code>. The <code class="language-plaintext highlighter-rouge">document_title</code> metadata (e.g. for <code class="language-plaintext highlighter-rouge">APEX(1)</code>) is used when present. You can also pass <code class="language-plaintext highlighter-rouge">--code-highlight pygments</code> or <code class="language-plaintext highlighter-rouge">--code-highlight skylighting</code> to highlight code blocks in either snippet or standalone output.</p>

<h3 id="see-it-in-action">See it in action</h3>

<p>The Apex man page is available as HTML and as Markdown:</p>

<ul>
  <li><strong>HTML (standalone):</strong> <a href="https://apexmarkdown.org/man/apex.1.html">apex.1.html</a></li>
  <li><strong>Markdown source:</strong> <a href="https://apexmarkdown.org/man/apex.1.md">apex.1.md</a></li>
</ul>

<p>Both are generated from the same Markdown; the HTML is what you get with <code class="language-plaintext highlighter-rouge">apex -t man-html -s</code> (plus any site styling).</p>

<h3 id="what-else-is-new">What else is new</h3>

<p>Highlights from the changelog:</p>

<ul>
  <li><strong>Man page creation:</strong> <code class="language-plaintext highlighter-rouge">-t man</code> and <code class="language-plaintext highlighter-rouge">-t man-html</code> as above; Makefile uses <code class="language-plaintext highlighter-rouge">apex -t man</code> for man targets.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">-t</code> / <code class="language-plaintext highlighter-rouge">--to</code> output formats:</strong> html, json, json-filtered/ast-json/ast, markdown/md, mmd, commonmark/cmark, kramdown, gfm, terminal/cli, terminal256, and now man and man-html.</li>
  <li><strong>Terminal and terminal256 renderers</strong> with theme support, <code class="language-plaintext highlighter-rouge">--theme</code>, and user theme files in <code class="language-plaintext highlighter-rouge">~/.config/apex/terminal/themes/</code>. JSON and AST JSON output before and after filters for tooling.</li>
  <li><strong>Terminal options:</strong> <code class="language-plaintext highlighter-rouge">--width</code> for column wrap; <code class="language-plaintext highlighter-rouge">Terminal.width</code> and <code class="language-plaintext highlighter-rouge">Terminal.theme</code> metadata; theme <code class="language-plaintext highlighter-rouge">list_marker</code> style for bullets and numbers; <code class="language-plaintext highlighter-rouge">Span_classes</code> mapping for inline span classes.</li>
</ul>

<p>For the full list, see <a href="https://github.com/ApexMarkdown/apex/blob/main/CHANGELOG.md">CHANGELOG.md</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116160352356420789">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Generating+Man+Pages+with+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F02%2Fgenerating-man-pages-with-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F02%2Fgenerating-man-pages-with-apex%2F&text=Generating+Man+Pages+with+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F03%2F02%2Fgenerating-man-pages-with-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17287905.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for February 27th, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17287236/web-excursions-for-february-27th-2026"/>
    <updated>2026-02-27T12:00:00-06:00</updated>
    <published>2026-02-27T12:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/27/web-excursions-for-february-27th-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Whether you&rsquo;re a new user or a seasoned pro, ScreenCastsONLINE offers in-depth screencasts on a wide range of topics, from tutorials to app discovery. <a href="https://screencastsonline.com/members/aff/go/bterpstra">Check it out.</a></p>

<dl>
  <dt><a href="https://dockflow.appitstudio.com/">DockFlow</a></dt>
  <dd>
    <blockquote>
      <p>Boost your productivity with DockFlow. Instantly save, manage, and switch between multiple macOS Docks.</p>
    </blockquote>
  </dd>
  <dd>Interesting app. This is basically how Bunch started (albeit Bunch configuration is text-based). It lets you set up different Dock configurations for different contexts, which I like a lot. It is, in my opinion, overpriced for such a utility, but I also would never begrudge a developer charging what they think their work is worth. Who am I to say?</dd>
  <dt><a href="https://www.producthunt.com/products/openclaw-mac-mini-m4-enclosure">OpenClaw Mac mini M4 Enclosure: Every powerful little crustacean needs a proper shell! | Product Hunt</a></dt>
  <dd>Just for fun.</dd>
  <dd>
    <blockquote>
      <p>The OpenClaw Mac mini M4 Enclosure is a fun, display-worthy 3D printed case for your OpenClaw/Clawdbot/Moltbot device. It’s a perfect blend of cute character + clean desk setup, turning your Mac mini into a chunky little desk companion.</p>
    </blockquote>
  </dd>
  <dt><a href="https://dockey.publicspace.co/">Dockey - Make your Dock faster</a></dt>
  <dd>A simple utility that basically offers GUI access to the animation delay and speed of the macOS Dock hide/show, avoiding the necessity of Terminal commands. Donationware.</dd>
  <dt><a href="https://www.dockfix.app/">DockFix</a></dt>
  <dd>A reasonably-priced Dock enhancer with themes, custom icons, custom widgets, file shelf, and more.</dd>
</dl>

<p>Want more great tips and apps? Check out <a href="https://screencastsonline.com/members/aff/go/bterpstra">ScreenCastsOnline</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116154380405810039">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+February+27th%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F27%2Fweb-excursions-for-february-27th-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F27%2Fweb-excursions-for-february-27th-2026%2F&text=Web+Excursions+for+February+27th%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F27%2Fweb-excursions-for-february-27th-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17287236.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[A NYT Connections Helper]]></title>
    <link href="https://brett.trpstra.net/link/535/17284282/a-nyt-connections-helper"/>
    <updated>2026-02-26T06:00:00-06:00</updated>
    <published>2026-02-26T06:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/26/a-nyt-connections-helper</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/connections-header-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>I love playing the New York Times game Connections. Some days it&rsquo;s easy, occasionally it stumps me. I don&rsquo;t cheat at it &mdash; I&rsquo;ll take a loss when I have to.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/perfect-score-400.jpg"></p>
<p>What I <em>do</em> do, though, is check the built-in hints to find out what order the categories are in. My goal is always to get the purple (hardest) first, which often involves figuring out at least the other three categories first. Once I know all four categories, the guessing game is just which one Connections thinks is hardest.</p>

<div id="paywall_e5758c0b42401f51" class="paywall-container" data-paywall-id="paywall_e5758c0b42401f51" data-content-id="e5758c0b42401f51">
  <div class="paywall-content-placeholder" style="display: none;"></div>
  <div class="paywall-overlay">
    <div class="paywall-message">
      <div class="paywall-icon">🔒</div>
      <h3>Premium Content</h3>
      <p>Join to get the Connections script</p>
      <div class="paywall-actions">
        <a href="https://brettterpstra.com//support/" class="paywall-subscribe-btn">Subscribe Now</a>
        <button class="paywall-login-btn" onclick="paywallLogin()">I'm a Subscriber</button>
      </div>
    </div>
  </div>
</div>

<script>
  // Set up global paywall config and registration queue
  (function() {
    if (!window.paywallConfig) {
      window.paywallConfig = {
        serviceUrl: 'https://paywall.brettterpstra.com',
        cookieName: 'bt_member_token',
        instances: []
      };
    }

    // Register this instance (collected by bt.Paywall when it loads)
    window.paywallConfig.instances.push({
      id: 'paywall_e5758c0b42401f51',
      contentId: 'e5758c0b42401f51'
    });
  })();
</script>

<p>Cheat if you want to.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116137002913823683">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=A+NYT+Connections+Helper%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F26%2Fa-nyt-connections-helper%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F26%2Fa-nyt-connections-helper%2F&text=A+NYT+Connections+Helper&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F26%2Fa-nyt-connections-helper%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17284282.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/connections-header-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/connections-header-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for February 25th, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17283540/web-excursions-for-february-25th-2026"/>
    <updated>2026-02-25T06:36:00-06:00</updated>
    <published>2026-02-25T06:36:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/25/web-excursions-for-february-25th-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with <a href="https://macpaw.7eer.net/c/228914/82479/1733">CleanMyMac X</a>, all the tools to speed up your Mac, in one app.</p>

<dl>
  <dt><a href="https://github.com/rhsev/grubber">rhsev/grubber</a></dt>
  <dd>Another cool one from Ralf Hülsmann. Turn YAML front matter and YAML code blocks in a bunch of Markdown files into structured data you can search like a database. Like data view without Obsidian.</dd>
  <dt><a href="https://apps.apple.com/us/app/cursor-widget/id6758641320?mt=12">Cursor Widget App</a></dt>
  <dd>A cool (free) idea that lets you connect a macOS desktop widget to Cursor using an MCP. What I&rsquo;ll use it for is yet to be determined, but I like the idea. Right now I&rsquo;m just having it reflect its current todo list to the widget, which is kinda handy, but I think I can do cooler stuff with it.</dd>
  <dt><a href="https://mariozechner.at/posts/2025-11-30-pi-coding-agent/">What I learned building an opinionated and minimal coding agent</a></dt>
  <dd>
    <blockquote>
      <p>Lessons I learned while building my own coding agent from scratch.</p>
    </blockquote>
  </dd>
  <dt><a href="https://github.com/aryankashyap0/shorlabs">aryankashyap0/shorlabs</a></dt>
  <dd>Vercel for Backend. One-click deploy of Python and Node.js apps to AWS with no Docker knowledge needed. Mostly bookmarking this for my own future needs&hellip;</dd>
</dl>

<p><a href="https://brettterpstra.com///macpaw.7eer.net/c/228914/82479/1733"><img src="https://brettterpstra.com///a.impactradius-go.com/display-ad/1733-82479" border="0" alt="CleanMyMac X" width="728" height="90" /></a><img height="0" width="0" src="https://brettterpstra.com///macpaw.7eer.net/i/228914/82479/1733" style="position:absolute;visibility:hidden;" border="0" /></p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116131509887714990">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+February+25th%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F25%2Fweb-excursions-for-february-25th-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F25%2Fweb-excursions-for-february-25th-2026%2F&text=Web+Excursions+for+February+25th%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F25%2Fweb-excursions-for-february-25th-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17283540.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for February 20th, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17280025/web-excursions-for-february-20th-2026"/>
    <updated>2026-02-20T05:58:00-06:00</updated>
    <published>2026-02-20T05:58:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/20/web-excursions-for-february-20th-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with Backblaze. <a href="https://secure.backblaze.com/r/00dszk">Back up everything.</a></p>

<dl>
  <dt><a href="https://devly.techfixpro.net/">Devly — Developer Utilities</a></dt>
  <dd>The fastest way to encode, decode, format, hash, and convert — 50+ developer tools, one click from your menu bar. No subscriptions, no internet required. A nice companion to <a href="https://retina.studio/textbuddy/" title="TextBuddy">TextBuddy</a>⏎.</dd>
  <dt><a href="https://gwfh.mranftl.com/fonts">google webfonts helper</a></dt>
  <dd>Found this nifty tool for downloading very streamlined, compressed versions of Google fonts for self-hosting. Get eot, ttf, svg, woff and woff2 files + CSS snippets.</dd>
  <dt><a href="https://giscus.app/">giscus</a></dt>
  <dd>A comments widget built on GitHub Discussions. If I give up on running comments from <a href="https://forum.brettterpstra.com">my forum Discourse server</a> at some point, this is what I&rsquo;ll switch to.</dd>
  <dt><a href="https://gist.github.com/ttscoff/bbf5a04b25c5dd04d9658e728da26cd7">Cursor iThoughts integration</a></dt>
  <dd>One of my own design: teach Cursor to read iThoughts X mind maps and create implementation plans for them. Just place in .cursor/commands for the project and run <code class="language-plaintext highlighter-rouge">/ithoughts path/to/brainstorm.itmz</code>.</dd>
</dl>

<p>Backblaze securely backs up your entire computer to the cloud, affordably and reliably. I trust it with all my data. <a href="https://secure.backblaze.com/r/00dszk">Check it out today.</a></p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116102915676838157">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+February+20th%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F20%2Fweb-excursions-for-february-20th-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F20%2Fweb-excursions-for-february-20th-2026%2F&text=Web+Excursions+for+February+20th%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F20%2Fweb-excursions-for-february-20th-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17280025.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Apex and Multiple Image Formats]]></title>
    <link href="https://brett.trpstra.net/link/535/17278639/apex-and-multiple-image-formats"/>
    <updated>2026-02-18T10:05:00-06:00</updated>
    <published>2026-02-18T10:05:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/18/apex-and-multiple-image-formats</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>Here&rsquo;s part two of my Apex posts for the day: Multiple image formats from a single image syntax.</p>

<p>Modern browsers support a bunch of image formats, like WebP and AVIF, that are smaller and sharper than good old PNG and JPEG. The trick is serving them without breaking older browsers. Apex makes that easy with a few attributes on your image syntax.</p>

<p>Apex isn&rsquo;t going to generate the additional images themselves, of course, that&rsquo;s up to you. But if you have a series of images in the same directory, inserting them properly is super simple now. Take a directory with:</p>

<ul>
  <li>image.png</li>
  <li>image@2x.png</li>
  <li>image.webp</li>
  <li>image@2x.webp</li>
  <li>image.avif</li>
  <li>image@2x.avif</li>
</ul>

<p>Now you can generate a <code class="language-plaintext highlighter-rouge">&lt;picture&gt;</code> (and optionally a <code class="language-plaintext highlighter-rouge">&lt;figure&gt;</code> with captions) with a single line of Markdown.</p>

<h3 id="webp-and-avif">WebP and AVIF</h3>

<p>Add <code class="language-plaintext highlighter-rouge">webp</code> or <code class="language-plaintext highlighter-rouge">avif</code> after the URL and Apex wraps the image in a <code class="language-plaintext highlighter-rouge">&lt;picture&gt;</code> element with the right <code class="language-plaintext highlighter-rouge">&lt;source&gt;</code> tags. Browsers that support the format use it; everyone else falls back to the main image.</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Hero image</span><span class="p">](</span><span class="sx">img/hero.png</span> webp)
<span class="p">![</span><span class="nv">Hero AVIF</span><span class="p">](</span><span class="sx">img/hero.png</span> avif)</code></pre></div></div>

<p>You can combine both for maximum compatibility:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Modern formats</span><span class="p">](</span><span class="sx">img/banner.jpg</span> webp avif)</code></pre></div></div>

<p>Both attributes work with <code class="language-plaintext highlighter-rouge">@2x</code> for retina. So <code class="language-plaintext highlighter-rouge">![Retina](img/hero.png webp @2x)</code> produces a srcset with <code class="language-plaintext highlighter-rouge">img.webp</code> at 1x and <code class="language-plaintext highlighter-rouge">img@2x.webp</code> at 2x. Same deal for avif. Apex also recognizes @3x, if you need that.</p>

<h3 id="auto-discovery">Auto discovery</h3>

<p>If you&rsquo;d rather not list every format by hand, use the <code class="language-plaintext highlighter-rouge">auto</code> attribute:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Profile menu</span><span class="p">](</span><span class="sx">img/app-pass-1-profile-menu.jpg</span> auto)</code></pre></div></div>

<p>When <code class="language-plaintext highlighter-rouge">base_directory</code> is set (e.g. from the document path or <code class="language-plaintext highlighter-rouge">--base-directory</code>), Apex checks the filesystem for existing variants. For images it looks for 2x, 3x, webp, and avif. It only emits <code class="language-plaintext highlighter-rouge">&lt;source&gt;</code> elements for files that actually exist, so you can add formats over time without touching the Markdown.</p>

<p>You can also use <code class="language-plaintext highlighter-rouge">*</code> as the extension, which does the same thing:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Profile menu</span><span class="p">](</span><span class="sx">img/app-pass-1-profile-menu.*</span><span class="p">)</span></code></pre></div></div>

<p>That scans for jpg, png, gif, webp, and avif (plus 2x and 3x variants) for images, and mp4, webm, ogg, mov, and m4v for videos. <code class="language-plaintext highlighter-rouge">![](image.*)</code> is equivalent to <code class="language-plaintext highlighter-rouge">![](image.png auto)</code>.</p>

<h3 id="video-same-syntax-different-element">Video: same syntax, different element</h3>

<p>Video URLs in image syntax get special treatment. If the URL ends in mp4, mov, webm, ogg, ogv, or m4v, Apex emits a <code class="language-plaintext highlighter-rouge">&lt;video&gt;</code> tag instead of <code class="language-plaintext highlighter-rouge">&lt;img&gt;</code>:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Demo video</span><span class="p">](</span><span class="sx">media/demo.mp4</span><span class="p">)</span></code></pre></div></div>

<p>That becomes <code class="language-plaintext highlighter-rouge">&lt;video&gt;&lt;source src="https://brettterpstra.commedia/demo.mp4" type="video/mp4"&gt;&lt;/video&gt;</code>. No extra syntax needed.</p>

<p>To add alternative formats for broader browser support, tack on the format name:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">Demo with WebM</span><span class="p">](</span><span class="sx">media/demo.mp4</span> webm)
<span class="p">![</span><span class="nv">Demo with OGG</span><span class="p">](</span><span class="sx">media/intro.mp4</span> ogg)</code></pre></div></div>

<p>Apex adds <code class="language-plaintext highlighter-rouge">&lt;source&gt;</code> elements for each format you specify. The primary URL (e.g. <code class="language-plaintext highlighter-rouge">demo.mp4</code>) stays as the fallback; the attributes add webm, ogg, or whatever before it. So <code class="language-plaintext highlighter-rouge">![](video.mp4 ogg)</code> gives you an ogg source plus the mp4 fallback.</p>

<p>Like I said, using <code class="language-plaintext highlighter-rouge">auto</code> or <code class="language-plaintext highlighter-rouge">demo.*</code> as a video URL will generate markup for all the existing formats.</p>

<h3 id="what-this-means">What This Means</h3>

<p>It means proper, accessible markup, and optimized web pages. It won&rsquo;t mean much if your output goal is PDF or DOCX, but it&rsquo;s great for web production.</p>

<p>The <code class="language-plaintext highlighter-rouge">auto</code> and `*` syntaxes mean you can write the Markdown once, and then every time you add a new format or resolution, the correct markup will be generated the next time you render, without touching the Markdown.</p>

<h3 id="quick-reference">Quick reference</h3>

<table>
  <thead>
    <tr>
      <th>Syntax</th>
      <th>Result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](url webp)</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;picture&gt;</code> with WebP source</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](url avif)</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;picture&gt;</code> with AVIF source</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](url webp @2x)</code></td>
      <td>WebP with retina srcset</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](url auto)</code></td>
      <td>Discovers formats from disk (requires base_directory)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](url.*)</code></td>
      <td>Same as auto—scans jpg/png/gif/webp/avif and video formats</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](video.mp4)</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;video&gt;</code> with mp4 source</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">![alt](video.mp4 webm)</code></td>
      <td><code class="language-plaintext highlighter-rouge">&lt;video&gt;</code> with webm + mp4 fallback</td>
    </tr>
  </tbody>
</table>

<p>Same Markdown image syntax, better output. Check out the <a href="https://brettterpstra.com//projects/apex">Apex wiki</a> for more details.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116092579424853852">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Apex+and+Multiple+Image+Formats%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F18%2Fapex-and-multiple-image-formats%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F18%2Fapex-and-multiple-image-formats%2F&text=Apex+and+Multiple+Image+Formats&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F18%2Fapex-and-multiple-image-formats%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17278639.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Apex as Terminal Markdown Renderer]]></title>
    <link href="https://brett.trpstra.net/link/535/17278640/apex-as-terminal-markdown-renderer"/>
    <updated>2026-02-18T10:00:00-06:00</updated>
    <published>2026-02-18T10:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/18/apex-as-terminal-markdown-renderer</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>This is the first of two <a href="https://brettterpstra.com//projects/apex">Apex</a> posts today. In my opinion, the two things I&rsquo;ve been working on deserve their own headlines. So here&rsquo;s part one: terminal rendering.</p>

<p>If you&rsquo;ve ever wanted to read Markdown in the terminal with syntax highlighting and nice formatting, you&rsquo;ve probably reached for <strong>mdless</strong> or <strong>glow</strong>. They&rsquo;re great tools. But what if you want all of that <em>plus</em> Apex&rsquo;s extensions, filters, and plugins—tables with captions, footnotes, callouts, file includes, and whatever your custom filters do?</p>

<p>That&rsquo;s where Apex&rsquo;s new built-in terminal output comes in.</p>

<h2 id="triggering-terminal-output">Triggering Terminal Output</h2>

<p>Apex can now render Markdown directly to your terminal instead of HTML. You use <code class="language-plaintext highlighter-rouge">--to</code> flag with a special target:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex README.md <span class="nt">--to</span> terminal</code></pre></div></div>

<p>or using the short form:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex README.md <span class="nt">-t</span> cli</code></pre></div></div>

<p>Both produce colored, formatted output suitable for reading in an interactive terminal. No piping to mdless required—Apex handles it natively.
For terminals that support 256 colors, use:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex README.md <span class="nt">-t</span> terminal256</code></pre></div></div>

<p>This gives you a richer color palette for headings, code blocks, links, and other elements. If your terminal supports it, the difference is noticeable.</p>

<h2 id="what-you-get">What You Get</h2>

<p>Because this is Apex doing the rendering, you get the full pipeline:</p>

<ul>
  <li><strong>Extensions</strong>: Tables, footnotes, definition lists, task lists, callouts, wiki links, and all the rest</li>
  <li><strong>Filters</strong>: Any filters in your config run before render, so your title filter, delink filter, or custom Lua scripts all apply</li>
  <li><strong>Plugins</strong>: Pre-parse and post-render plugins run as usual &mdash; kbd for <code class="language-plaintext highlighter-rouge">&lt;kbd&gt;</code> tags, md-fixup, or whatever you&rsquo;ve installed</li>
</ul>

<p>So when you <code class="language-plaintext highlighter-rouge">apex doc.md -t terminal</code>, you&rsquo;re seeing the same processed document you&rsquo;d get as HTML, just rendered for the terminal instead.</p>

<p>It&rsquo;s not perfect for handling all elements yet, but the foundation is there and I&rsquo;ll tweak it as needed.</p>

<h2 id="theming-mdless-compatibility">Theming: mdless Compatibility</h2>

<p>Apex&rsquo;s terminal theming is compatible with <a href="https://brettterpstra.com//projects/mdless">mdless</a> theme files. If you already use mdless, you can point Apex at your existing theme:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex doc.md <span class="nt">-t</span> terminal256 <span class="nt">--theme</span> ~/.config/mdless/mdless.theme</code></pre></div></div>

<p>Or set a default in your Apex config. The theme format matches mdless, so your <code class="language-plaintext highlighter-rouge">~/.config/mdless/mdless.theme</code> (or any <code class="language-plaintext highlighter-rouge">*.theme</code> file) works out of the box.</p>

<p>Apex adds a few extra keys for elements mdless doesn&rsquo;t have—callouts, definition lists, table captions, and the like. If you don&rsquo;t define them, sensible defaults are used. You can override them in your theme file when you want finer control.</p>

<h2 id="json-output-build-your-own-renderer">JSON Output: Build Your Own Renderer</h2>

<p>Apex can also output a Pandoc-compatible JSON representation of the document:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex doc.md <span class="nt">--to</span> json</code></pre></div></div>

<p>This emits the full AST, including blocks, inlines, metadata, to stdout. From there you can pipe it into your own script to generate whatever you want: custom terminal formatting, a different HTML structure, plain text, or something else entirely.</p>

<p>The JSON format is the same one filters use. So if you&rsquo;ve written a filter, you already know the structure. A simple Python script can read it, walk the tree, and emit your own output format. No need to re-implement the parser.</p>

<h2 id="quick-reference">Quick Reference</h2>

<table>
  <thead>
    <tr>
      <th>Option</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-t cli</code> or <code class="language-plaintext highlighter-rouge">-t terminal</code></td>
      <td>Terminal output with basic ANSI colors</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-t terminal256</code></td>
      <td>Terminal output with 256-color palette</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">-t json</code></td>
      <td>Raw Pandoc JSON AST for custom processing</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--theme FILE</code></td>
      <td>Use mdless-compatible theme file</td>
    </tr>
  </tbody>
</table>

<p>So next time you&rsquo;re browsing docs in the terminal, skip the Apex &amp;rarr mdless/glow pipeline and let <a href="https://brettterpstra.com//projects/apex">Apex</a> do it all in one shot.</p>

<p>Like or share this post <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F18%2Fapex-as-terminal-markdown-renderer%2F&text=Apex+as+Terminal+Markdown+Renderer&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F18%2Fapex-as-terminal-markdown-renderer%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17278640.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[NiftyMenu Tahoe Edition]]></title>
    <link href="https://brett.trpstra.net/link/535/17276851/niftymenu-tahoe-edition"/>
    <updated>2026-02-15T12:57:00-06:00</updated>
    <published>2026-02-15T12:57:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/15/niftymenu-tahoe-edition</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/niftymenutahoe-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p><a href="https://brettterpstra.com/projects/niftymenu">NiftyMenu</a> is the little tool I use to dump a macOS app&rsquo;s menu bar into an HTML page so I can search it, click through to items, and add callouts for screencasts and product documentation. The latest round of updates brings the UI in line with macOS Tahoe and cleans up a bunch of behavior and script logic.</p>

<figure class="bt-video-container" style="padding-bottom:56.25%"><a class="vimeo" href="https://player.vimeo.com/video/1165172881" data-videoid="1165172881" data-width="640" data-height="360">Vimeo Video</a></figure>

<p>I wrote NiftyMenu <a href="https://brettterpstra.com/2019/06/12/niftymenu-mac-menu-madness/">years ago</a>, and did a major update <a href="https://brettterpstra.com/2023/05/04/niftymenu-update-for-ventura/">for Ventura</a>. But it was way behind on Liquid Glass and basically wasn&rsquo;t working anymore.</p>

<h3 id="what-it-is">What it is</h3>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/shortcut-callout.jpg"></p>
<p>Since it&rsquo;s been a while, let me explain NiftyMenu a bit. It&rsquo;s a tool specifically for people documenting or blogging about apps (though anybody might get a kick out of it). When you want a screenshot of a menu item, highlighted (maybe with callouts), getting a menu to stick in place and maintain highlight state while you take a screenshot can be tricky, and doing a bunch of screenshots like this can be a pain.</p>

<p>NiftyMenu creates an HTML version of any app&rsquo;s menu bar. You can click any item to lock it in place, navigate submenus, select items, highlight them, add callouts, and focus the shortcut key. It has dark and light modes, quick search for finding menu items with just the keyboard, customizable desktop images (including random images), and more.</p>

<p>I recommend viewing the pages it produces in Chrome. The styling should work well in any browser, but with Chrome you can just hit <span class="keycombo separated" title="Shift-S"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">S</kbd></span> to save a screenshot of the currently-focused menu item. It&rsquo;s super handy. Screenshotting works in Firefox, but the text gets screwy. And it doesn&rsquo;t work at all in Safari. So use Chrome/Chromium.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/settings-menu.jpg"></p>
<p>At the bottom right of a generated menu page there&rsquo;s a settings box that appears on hover, and you can change things like light/dark mode, background image, callout type, etc.</p>

<p>NiftyMenu is a Ruby script that generates a web app of sorts. Just one page with all the markup necessary for the menu, and some CSS and JavaScript to enable all of its functionality.</p>

<h3 id="styling-to-match-tahoe">Styling to match Tahoe</h3>

<p>The new layout and colors are tuned to feel like Tahoe&rsquo;s menu bar. The font stack uses SF Pro when available (Tahoe and recent macOS), with sensible fallbacks. Top-level menu items get a soft white overlay on hover with rounded corners instead of a flat accent block; the solid accent is reserved for the selected item (<code class="language-plaintext highlighter-rouge">.last</code>). Submenus use the same idea: light grey overlay on hover, accent only on the selected row. Toolbar height, padding, and submenu positioning are adjusted so the overall proportions match the system menus.</p>

<p>Dark mode gets the same treatment: overlay-based hover states and accent only on the selected item. Submenu and divider colors use the new variables so they stay consistent with the rest of the theme.</p>

<h3 id="backgrounds-and-overlays">Backgrounds and overlays</h3>

<p>Unsplash deprecated their simpler API for image embeds, so I switched the random wallpaper generator to use <a href="https://picsum.photos/">Picsum.photos</a>. You can&rsquo;t do a search, but you can get a random image and apply blur and grayscale to it (NiftyMenu gives you the options when getting a random wallpaper). You can also add a &ldquo;seed&rdquo; word, which will stick the image so that it can be retrieved reliably. The seed you enter is stored in browser storage and will automatically be used next time the dialog is opened.</p>

<p>The page background (gradient or desktop image) is now fixed to the viewport, so when the menu list is taller than the window you scroll the content, not the background.</p>

<p>If you use a desktop image, there&rsquo;s a brightening overlay: a semi-transparent layer with <code class="language-plaintext highlighter-rouge">background-blend-mode: overlay</code> so the image reads a bit lighter. You can tweak it (or turn it off) via <code class="language-plaintext highlighter-rouge">$desktop-image-overlay</code> and <code class="language-plaintext highlighter-rouge">$dark-desktop-image-overlay</code> in your variables. The toolbar strip at the top uses the same overlay color and blend so it sits nicely over the gradient or image.</p>

<p>In light mode, when submenus overlap each other or the parent, the overlap darkens instead of lightening. Submenu <code class="language-plaintext highlighter-rouge">ul</code>s use <code class="language-plaintext highlighter-rouge">mix-blend-mode: multiply</code> and an opaque background so you get a clear light bluish-grey panel that darkens where it stacks.</p>

<h3 id="a-few-fixes-while-i-was-at-it">A few fixes while I was at it</h3>

<p>Divider rows no longer highlight on hover or accept clicks. They use <code class="language-plaintext highlighter-rouge">pointer-events: none</code> and the click handler bails out for divider targets and list items that only contain a divider.</p>

<p>The script also fixes the case where a bogus shortcut was attached to a divider line and produced blockquotes in the markdown; that combo is now normalized to a plain divider span before processing.</p>

<p>The globe key in shortcuts is no longer an emoji. The script outputs the entity, then swaps it for a <code class="language-plaintext highlighter-rouge">&lt;span class="globe-icon"&gt;</code> so CSS can draw the icon with a mask and <code class="language-plaintext highlighter-rouge">currentColor</code>, including when the shortcut is in the callout (inverted) state.</p>

<p>There&rsquo;s a bit more progress reporting now, going to STDERR in Terminal while processing, with a simple bar and step labels: gather, process, convert, write.</p>

<h3 id="whats-missing">What&rsquo;s missing</h3>

<p>In macOS, there are often menu items that change to an alternate option when you hold down the <span class="keycombo separated" title="Option"><kbd class="mod symbol">&#8997;</kbd></span> key. With the way NiftyMenu gathers items, these all appear in the same menu, not hidden behind a modifier. I spent an hour this morning trying to get that to work, but it doesn&rsquo;t seem feasible.</p>

<p>I also struggled with overlapping transparencies. In Tahoe, when a submenu&rsquo;s left edge overlaps the parent menu, the edge appears opaque, or at least at uniform transparency with the submenu. Because the HTML version uses CSS colors with alpha channels, the overlap instead becomes a brighter version of the base color. I played with blend modes and other tricks but couldn&rsquo;t find a way to replicate this. It&rsquo;s a very small issue, but one that bugged me. If anyone has suggestions, I&rsquo;d love to hear them.</p>

<p>There&rsquo;s a <a href="https://ttscoff.github.io/niftymenu/jsapi/index.html">JavaScript API</a> for automating the process of selecting menu items using fuzzy search and even saving the screenshots, so you can create a workflow that generates new screenshots even as menu item names and positions change. It&rsquo;s not a perfect solution &mdash; being able to do it with AppleScript would be better, but not an option here.</p>

<p>I also could have, but didn&rsquo;t, set it up so you could choose the OS styling to use. It&rsquo;s always going to use the latest macOS (that it&rsquo;s been updated for) and not be backward compatible. This seems like a reasonable approach to me.</p>

<h3 id="give-it-a-shot">Give it a shot</h3>

<p>If you&rsquo;re writing documentation or are a blogger who often talks about apps&rsquo; menu items and shortcuts, give <a href="https://brettterpstra.com/projects/niftymenu">NiftyMenu</a> a shot. If nothing else, it&rsquo;s a pretty cool proof of concept.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116076286219123700">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=NiftyMenu+Tahoe+Edition%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F15%2Fniftymenu-tahoe-edition%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F15%2Fniftymenu-tahoe-edition%2F&text=NiftyMenu+Tahoe+Edition&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F15%2Fniftymenu-tahoe-edition%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17276851.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/niftymenutahoe-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/niftymenutahoe-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Pandoc-style filters for Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17272052/pandoc-style-filters-for-apex"/>
    <updated>2026-02-07T09:36:00-06:00</updated>
    <published>2026-02-07T09:36:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/07/pandoc-style-filters-for-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>In my quest to make <a href="https://github.com/ApexMarkdown/apex/">Apex</a> as complete as possible before I integrate it into Marked, I&rsquo;ve added Pandoc-compatible filters, which can be written in Lua for native execution, or in any language using a Pandoc JSON pipeline.</p>

<p>The filter system runs your code on the document <em>after</em> parsing and <em>before</em> rendering. The pipeline is: Markdown → AST → <strong>Pandoc-style JSON</strong> → your filters → JSON → HTML. That means you can transform the document in any language that speaks JSON &mdash; Ruby, Python, Lua, Node, whatever &mdash; using the same <a href="https://pandoc.org/filters.html">Pandoc JSON AST</a> that Pandoc uses. If you&rsquo;ve written a Pandoc filter, the proecess is the same: read one JSON document from stdin, write one JSON document to stdout.</p>

<h3 id="running-filters-from-the-cli">Running filters from the CLI</h3>

<p>Three main options:</p>

<ul>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--filter NAME</code></strong> — Run a single filter. Apex looks for an executable named <code class="language-plaintext highlighter-rouge">NAME</code> in your user filters directory (<code class="language-plaintext highlighter-rouge">~/.config/apex/filters</code> or <code class="language-plaintext highlighter-rouge">$XDG_CONFIG_HOME/apex/filters</code>). So <code class="language-plaintext highlighter-rouge">apex --filter title input.md</code> runs <code class="language-plaintext highlighter-rouge">~/.config/apex/filters/title</code>. These can exist inside of subdirectories.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--filters</code></strong> — Run <em>all</em> executables in that directory, in sorted order. Handy if you keep a fixed pipeline (e.g. <code class="language-plaintext highlighter-rouge">01-title</code>, <code class="language-plaintext highlighter-rouge">10-delink</code>) and just want one flag.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--lua-filter FILE</code></strong> — Run a Lua script as a filter. Apex calls <code class="language-plaintext highlighter-rouge">lua FILE</code>; the script reads Pandoc JSON from stdin and writes JSON to stdout. You need a JSON library (e.g. <strong>dkjson</strong>: <code class="language-plaintext highlighter-rouge">luarocks install dkjson</code>). No Pandoc Lua runtime required.</p>
  </li>
</ul>

<p>Filters run in sequence. If any filter exits non-zero or outputs invalid JSON, Apex aborts unless you pass <code class="language-plaintext highlighter-rouge">--no-strict-filters</code> (then it skips the bad filter and continues).</p>

<h3 id="the-central-filter-list-install-and-list">The central filter list: install and list</h3>

<p>There&rsquo;s a small index of filters at <a href="https://github.com/ApexMarkdown/apex-filters">ApexMarkdown/apex-filters</a>. It&rsquo;s a single JSON file listing filter id, title, description, author, repo, etc. This will grow as I and others create new filters.</p>

<ul>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">apex --list-filters</code></strong> — Prints &ldquo;Installed Filters&rdquo; (what&rsquo;s in your <code class="language-plaintext highlighter-rouge">~/.config/apex/filters</code> dir) and &ldquo;Available Filters&rdquo; from that index, with titles and descriptions.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">apex --install-filter ID</code></strong> — Installs a filter by id from the index (e.g. <code class="language-plaintext highlighter-rouge">apex --install-filter unwrap</code>). It clones the repo into your filters directory. You can also install by Git URL or GitHub shorthand (user/repo).</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">apex --uninstall-filter ID</code></strong> — Removes that filter (with a confirmation prompt).</p>
  </li>
</ul>

<p>To add your own filter to the list, open a pull request on <a href="https://github.com/ApexMarkdown/apex-filters">github.com/ApexMarkdown/apex-filters</a>. Once it&rsquo;s merged, everyone can <code class="language-plaintext highlighter-rouge">--list-filters</code> and <code class="language-plaintext highlighter-rouge">--install-filter your-id</code>. <a href="https://github.com/ApexMarkdown/apex-filters?tab=readme-ov-file#how-to-request-adding-a-filter">See the docs</a> for more info on contributing.</p>

<h3 id="example-filters-in-the-wild">Example filters in the wild</h3>

<p>Two good references:</p>

<ul>
  <li>
    <p><strong><a href="https://github.com/ApexMarkdown/apex-filter-uppercase">ApexMarkdown/apex-filter-uppercase</a></strong> — Lua filter that uppercases every <code class="language-plaintext highlighter-rouge">Str</code> inline. Shows how to walk the AST with dkjson and mutate in place.</p>
  </li>
  <li>
    <p><strong><a href="https://github.com/ApexMarkdown/apex-filter-unwrap">ApexMarkdown/apex-filter-unwrap</a></strong> — Lua filter that unwraps elements starting with <code class="language-plaintext highlighter-rouge">&lt; </code> (e.g. custom block markers). More involved AST walking.</p>
  </li>
</ul>

<p>Install them with <code class="language-plaintext highlighter-rouge">apex --install-filter uppercase</code> and <code class="language-plaintext highlighter-rouge">apex --install-filter unwrap</code>, then use <code class="language-plaintext highlighter-rouge">--filter uppercase</code> or <code class="language-plaintext highlighter-rouge">--filter unwrap</code> in your pipeline.</p>

<h3 id="short-ruby-example">Short Ruby example</h3>

<p>A minimal filter that reads Pandoc JSON, does one thing (e.g. prepend an H1 from metadata), and writes JSON back. Ruby&rsquo;s stdlib <code class="language-plaintext highlighter-rouge">json</code> is enough.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c1">#!/usr/bin/env ruby</span>
<span class="nb">require</span> <span class="s2">"json"</span>

<span class="n">doc</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="vg">$stdin</span><span class="p">.</span><span class="nf">read</span><span class="p">)</span>
<span class="n">blocks</span> <span class="o">=</span> <span class="n">doc</span><span class="p">[</span><span class="s2">"blocks"</span><span class="p">]</span> <span class="o">||</span> <span class="p">[]</span>
<span class="n">meta</span>   <span class="o">=</span> <span class="n">doc</span><span class="p">[</span><span class="s2">"meta"</span><span class="p">]</span> <span class="o">||</span> <span class="p">{}</span>

<span class="c1"># Get title from meta if present (simplified)</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">meta</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="s2">"title"</span><span class="p">,</span> <span class="s2">"c"</span><span class="p">)</span> <span class="o">||</span> <span class="s2">"Untitled"</span>
<span class="n">title</span> <span class="o">=</span> <span class="n">title</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">String</span><span class="p">)</span> <span class="p">?</span> <span class="n">title</span> <span class="p">:</span> <span class="n">title</span><span class="p">.</span><span class="nf">to_s</span>

<span class="n">header</span> <span class="o">=</span> <span class="p">{</span>
  <span class="s2">"t"</span> <span class="o">=&gt;</span> <span class="s2">"Header"</span><span class="p">,</span>
  <span class="s2">"c"</span> <span class="o">=&gt;</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="p">[</span><span class="s2">""</span><span class="p">,</span> <span class="p">[],</span> <span class="p">[]],</span> <span class="p">[{</span> <span class="s2">"t"</span> <span class="o">=&gt;</span> <span class="s2">"Str"</span><span class="p">,</span> <span class="s2">"c"</span> <span class="o">=&gt;</span> <span class="n">title</span> <span class="p">}]]</span>
<span class="p">}</span>
<span class="n">doc</span><span class="p">[</span><span class="s2">"blocks"</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">header</span><span class="p">]</span> <span class="o">+</span> <span class="n">blocks</span>

<span class="nb">puts</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span></code></pre></div></div>

<p>Save as <code class="language-plaintext highlighter-rouge">~/.config/apex/filters/title</code>, <code class="language-plaintext highlighter-rouge">chmod +x</code>, then <code class="language-plaintext highlighter-rouge">apex --filter title doc.md &gt; out.html</code>.</p>

<h3 id="short-lua-example">Short Lua example</h3>

<p>Same idea in Lua: read JSON, tweak <code class="language-plaintext highlighter-rouge">doc.blocks</code> (or <code class="language-plaintext highlighter-rouge">doc.meta</code>), write JSON. Requires <code class="language-plaintext highlighter-rouge">dkjson</code>.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="kd">local</span> <span class="n">json</span> <span class="o">=</span> <span class="nb">require</span><span class="p">(</span><span class="s2">"dkjson"</span><span class="p">)</span>

<span class="kd">local</span> <span class="n">input</span> <span class="o">=</span> <span class="nb">io.read</span><span class="p">(</span><span class="s2">"*a"</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">doc</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="n">input</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">doc</span> <span class="k">then</span>
  <span class="nb">io.stderr</span><span class="p">:</span><span class="n">write</span><span class="p">(</span><span class="s2">"Invalid JSON</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
  <span class="nb">os.exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1">-- Optional: transform doc.blocks or doc.meta</span>
<span class="c1">-- doc.blocks = ...</span>

<span class="nb">io.write</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">doc</span><span class="p">))</span></code></pre></div></div>

<p>Run it with <code class="language-plaintext highlighter-rouge">apex --lua-filter myfilter.lua input.md &gt; output.html</code>.</p>

<h3 id="is-this-feature-complete">Is This Feature Complete?</h3>

<p>No, but close. I can&rsquo;t get to 100% Pandoc compatibility at this point
without some restructuring of the AST generated by cmark-gfm. I&rsquo;m not
sure 100% parity is the goal at this point. So some existing Pandoc Lua
filters require some updates to work with Apex. Additionally, this
feature is literally what I came up with in two days and has a lot of
testing ahead. If you&rsquo;re willing to help, especially if you have Pandoc
filters you&rsquo;d like to port, please keep me posted on
<a href="https://github.com/ApexMarkdown/apex/issues">GitHub</a>.</p>

<h3 id="am-i-trying-to-replace-pandoc">Am I Trying to Replace Pandoc?</h3>

<p>No, really I&rsquo;m not. I&rsquo;m not trying to replicate Pandoc&rsquo;s amazing export abilities, or many of its extensions.</p>

<p>However, Pandoc&rsquo;s relatively vast compatibility with various flavors of Markdown is similar to what I want to do in Apex, so supporting many of its extensions makes sense, and supporting filters means users who&rsquo;ve written their own pipelines can easily switch to using Apex as a primary renderer. That&rsquo;s going to be important if I want Marked to have Pandoc capabilities without using Pandoc as an external dependency.</p>

<h3 id="learn-more">Learn more</h3>

<ul>
  <li><a href="https://pandoc.org/filters.html">Pandoc filters</a> — The JSON AST format and filter contract Apex follows.</li>
  <li><a href="https://github.com/ApexMarkdown/apex.wiki/blob/master/Filters.md">Apex wiki: Filters</a> — Full protocol, block/inline types, Lua details, and more examples.</li>
</ul>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116030178973650794">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Pandoc-style+filters+for+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F07%2Fpandoc-style-filters-for-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F07%2Fpandoc-style-filters-for-apex%2F&text=Pandoc-style+filters+for+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F07%2Fpandoc-style-filters-for-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17272052.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[A trick for better naps]]></title>
    <link href="https://brett.trpstra.net/link/535/17270683/a-trick-for-better-naps"/>
    <updated>2026-02-05T12:51:00-06:00</updated>
    <published>2026-02-05T12:51:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/05/a-trick-for-better-naps</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/nap-routine-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I&rsquo;ve been sleeping anywhere between 4 and 6 hours a night, which isn&rsquo;t great because through trial and (mostly) error, I&rsquo;ve determined that at this point in my life, 8.5 hours of sleep is what I do best on. But naps help quite a bit.</p>

<blockquote class="tip">
  <p>By the way, if you want to follow my sleep journey as well as a couple of other neurodivergent people, along with fun pop culture and knowledge tech tidbits, you should listen to <a href="https://overtiredpod.com">Overtired</a>.</p>
</blockquote>

<p>As I&rsquo;ve <a href="https://brettterpstra.com/2026/02/04/project-updates-feb-4-2026-sleepless-edition/">mentioned before</a>, this sleep deprivation hasn&rsquo;t affected my coding productivity, but it&rsquo;s been a serious detriment to my social life, and affects my <a href="https://my.clevelandclinic.org/health/diseases/6004-dysautonomia">dysautonomia</a> (which I haven&rsquo;t talked about <a href="https://brettterpstra.com/2025/01/19/health-update-diagnoses/">for a while</a> but is still a major issue for me) and overall pain levels pretty drastically. My only saving grace has been daily naps.</p>

<h3 id="origin-of-my-routine">Origin of My Routine</h3>

<p>I&rsquo;ve always taken naps, even when sleeping well. I find it very rejuvenating and my afternoons are way more productive if I nap about an hour after lunch.</p>

<p>Once upon a time there was a startup called Napjitsu that sold a product appropriately called <em>Nap</em>. It was a combination of a chewable with mostly Valerian and 2 capsules containing mostly caffeine plus some other nootropics. You took them all at once. The chewable kicked in within 10 minutes, and the capsules metabolized about 30 minutes later, giving you a deep sleep for about 20 minutes and then waking up raring to go.</p>

<p>I loved those, but whatever else was in the capsules often made me wake up a bit &ldquo;shaky.&rdquo; Napjitsu shuttered a while back, but I found alternatives. This version of the routine doesn&rsquo;t leave me shaky. To the contrary, I wake up feeling quite energetic without side effects.</p>

<h3 id="scheduled-wakeup-call">Scheduled Wakeup Call</h3>

<p>For the caffeine, I&rsquo;m using <a href="https://amzn.to/4a0zzo9">capsules with 50mg of caffeine &amp; 100mg of L-Theanine</a> (these are Amazon affiliate links, which are helpful to me, but you can find these direct in other places, and I would support you <em>not supporting</em> Amazon). I only take one of those, as that&rsquo;s enough to get me going without making me jittery or affecting my sleep later that night.</p>

<p>You can really just drink a cup of coffee or a shot of espresso before laying down, as <a href="https://en.wikipedia.org/wiki/Caffeine">caffeine</a> in general takes 30 minutes to metabolize. But if you want the combination of caffeine and L-Theanine, that&rsquo;s basically tea. Just drink a cup of tea. (White or green tea will give you approximately the right amount of caffeine, with white tea capping out at 55mg and green tea capping out at 70mg.) The capsules aren&rsquo;t the key, just the caffeine.</p>

<p>By the way, if you ever see guaranine as an ingredient in energy drinks or supplements, it&rsquo;s essentially just caffeine. <a href="https://en.wikipedia.org/wiki/Guarana">Guarana</a> is a plant, and guaranine is derived from it&rsquo;s seeds. It includes some other compounds, but the only one of note is caffeine.</p>

<h3 id="sleep-phase">Sleep Phase</h3>

<p>For the sleep phase of the nap, I tried a few things, but valerian root gives me the best results.</p>

<p><a href="https://www.sleepfoundation.org/sleep-aids/valerian-root">Valerian root</a> is primarily indicated as a sleep aid. It&rsquo;s rumored to be good for stress, PMS, and anxiety, but there&rsquo;s no scientific evidence for those. It&rsquo;s also commonly used as a tea, so if you really want to drink a couple of cups of tea before laying down, that&rsquo;s an option, but you&rsquo;ll have to pee, what with liquids and diuretics and all. A need to urinate might help you get back up, too, but I personally dislike waking up like that.</p>

<p>Velerian root offers me fast sedation, but easy to overcome with stimulants like caffeine, unlike supplements like melatonin. I&rsquo;m using gummies with <a href="https://amzn.to/3OnYUQm">6000mg of valerian root and chamomile</a>. I take 2-4 of those, depending on how overtired I am. I don&rsquo;t know about the safety of doubling the recommended serving size (2 gummies), but 4 does the trick on days I feel wired despite (or because of) lack of sleep.</p>

<p>Weirdly, napping is easiest for me on days I actually <em>did</em> get enough sleep, which happens every 7-10 days. But on the days I most need a nap, a little supplemental cocktail is both necessary and effective.</p>

<p>I don&rsquo;t know if this routine is a good idea, or how well it will work for others. I can&rsquo;t guarantee anything. But I can anecdotally endorse it as a decent way to &ldquo;force&rdquo; a restful nap that doesn&rsquo;t leave me dragging for the afternoon. If you want to nap but have trouble dozing off, or have trouble waking up, or both, this might be worth a try.</p>

<h3 id="hows-your-sleep">How&rsquo;s Your Sleep?</h3>

<p>I hope that&rsquo;s helpful to others 💤. Let me know <a href="https://forum.brettterpstra.com">in the forum</a> if you have any of your own tips, I&rsquo;m always looking for new ideas in this area.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116019596506298906">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=A+trick+for+better+naps%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F05%2Fa-trick-for-better-naps%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F05%2Fa-trick-for-better-naps%2F&text=A+trick+for+better+naps&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F05%2Fa-trick-for-better-naps%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17270683.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/nap-routine-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/nap-routine-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Project Updates Feb 4, 2026 — Sleepless Edition]]></title>
    <link href="https://brett.trpstra.net/link/535/17269961/project-updates-feb-4-2026-sleepless-edition"/>
    <updated>2026-02-04T09:24:00-06:00</updated>
    <published>2026-02-04T09:24:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/04/project-updates-feb-4-2026-sleepless-edition</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/project-updates-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>I&rsquo;ve been hard at work on a bunch of projects at once, so this is partly just a personal check-in on how things are going. Thanks for being my therapist of sorts. So here&rsquo;s what&rsquo;s up with current projects, including Marked 3, Howzit, Apex, and more.</p>

<ul class="toc">
  <li>Table of Contents</li>
</ul>

<h3 id="marked-3">Marked 3</h3>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/marked-icon.png"></p>
<p>The <a href="https://markedapp.com/join-the-beta">Marked beta</a> continues. It&rsquo;s going really well, and I believe I&rsquo;ve fixed all crashes and made a bunch of improvements. One thing the next release will feature is better large document handling. This was important for dealing with BlogBook (see below) exports, but is also a vital quality of life improvement. Now the loading doesn&rsquo;t hang while reading 1MB+ files, and the rendering is faster for all processors.</p>

<p>I added full tab handling (finally), with the option to open new documents in tabs. Plus a <a href="https://markedapp.com/help/Quick_Open">Quick Open</a> feature that lets you hit <span class="keycombo separated" title="Shift-Command-O"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">O</kbd></span> and quickly type or use the arrow keys to jump to a window, tab, or recent document.</p>

<p>I also included <a href="https://markedapp.com/help/Command_Line_Utility">a CLI</a> that makes accessing all of Marked&rsquo;s URL scheme methods easy from the command line. It can&rsquo;t yet automate a full export, but it&rsquo;s still useful. I&rsquo;ve refactored the whole export system to eventually allow full automation, but that&rsquo;s going to be a 3.1 feature, not something that holds up the 3.0 release.</p>

<h3 id="blogbook">BlogBook</h3>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/blogbook-icon.png"></p>
<p>This is <a href="https://blogbook.my">a new app</a> I&rsquo;m publishing as a complement to Marked 3. It generates a &ldquo;Book&rdquo; from WordPress, Micro.blog, or Ghost blogs. You can filter posts by author, categories, tags, date ranges, and more, then export a Markdown document (or collection of documents with an index file) which you can open in any Markdown editor. If you open it in Marked 3, special syntax like Table of Contents and page breaks will be rendered, and you can output to PDF, HTML, EPUB, and more.</p>

<p>This will be a great tool for people who have been blogging for a few (or a lot of) years who want a more permanent record of their work. Websites are known to disappear, but a good EPUB on your backup drive means your content will never be lost. Store your content as text, PDF, or EPUB for posterity, and/or distribute (or sell) a PDF/Ebook version of your writings. The filtering means you can easily export just a certain type of post, export only a certain date range, and you can even have the export split by year or month for making smaller books.</p>

<p>Check out the documentation at <a href="https://blogbook.my">blogbook.my</a> to see what it looks like. If you&rsquo;re interested in helping me test, a TestFlight build will be available soon. It will be invite-only, so if you&rsquo;re interested and have a WordPress, Micro.blog, or Ghost blog, <a href="https://brettterpstra.com//contact">contact me directly</a> for an invite.</p>

<h3 id="apex">Apex</h3>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/apex-icon.png"></p>
<p>The last few releases of Apex have mostly addressed issues, but I added a couple new things.</p>

<p>First, I added support for Leanpub index syntax: <code class="language-plaintext highlighter-rouge">{i: term}</code>, <code class="language-plaintext highlighter-rouge">{i: "term"}</code>, and <code class="language-plaintext highlighter-rouge">{i: "Main!sub"}</code> for hierarchical entries under headings. This won&rsquo;t be of use to anyone but Leanpub authors, but adding it to the existing handling of mmark and TextIndex syntax was fairly trivial.</p>

<p>Second, a feature I really like: if you add a <code class="language-plaintext highlighter-rouge">@2x</code> attribute anywhere in image attributes, e.g. <code class="language-plaintext highlighter-rouge">![ALT](/image.png @2x)</code>, it will generate an image with a srcset containing the original image at 1x, and then the image with @2x appended before the extension for retina displays. I also added support for <code class="language-plaintext highlighter-rouge">@3x</code>, which will generate srcset entries for 1x, 2x, and 3x variants of the same path.</p>

<p>This srcset feature is one I&rsquo;ve always wanted, as I almost always provide @2x assets but Markdown never made it easy to generate the appropriate markup for these. I&rsquo;m doing my best not to add arbitrary syntax to Apex that doesn&rsquo;t exist anywhere else, but this one I really needed.</p>

<p>I also did my first real-world conversion of a build system to Apex. The Marked documentation has always used MultiMarkdown, with a build script that manually converts a bunch of Liquid-style tags (like my <code class="language-plaintext highlighter-rouge">kbd</code> plugin and one that generates menu item notations that open Settings panes in Marked). I converted the whole thing to use Apex, and wrote project-level plugins for all of the custom syntax I wanted to handle. It worked perfectly and required no changes to my existing documentation. So I can vouch that Apex is a great tool for this kind of thing.</p>

<p>I haven&rsquo;t pulled the trigger on integrating Apex into Marked yet, but thanks to some reported Issues, I&rsquo;ve got the Xcode integration working well. Apex is on the verge of a 1.0 release, at which point I <em>will</em> be converting Marked to use it. If you&rsquo;re a Mac or iOS developer of a Markdown app and are willing to try it out, I&rsquo;d love to help you integrate Apex and am very interested in getting some traction via inclusion in Mac/iOS apps. It&rsquo;s pure C, so if you&rsquo;re developing on other platforms, it will work just about anywhere, I just have the most interest in Apple platforms.</p>

<h4 id="apex-fixes">Apex Fixes</h4>

<ul>
  <li>TextIndex <code class="language-plaintext highlighter-rouge">[term]{^}</code> no longer includes the closing bracket in the index entry (e.g. &ldquo;fresh&rdquo; instead of &ldquo;fresh]&rdquo;).</li>
  <li>Reference-style image attributes (width, height, style, classes, id) are correctly applied in Unified, MultiMarkdown, and GFM modes, even when mixed with inline images and fenced div/figure blocks</li>
</ul>

<h3 id="howzit">Howzit</h3>

<p>I made a simple update to <a href="https://brettterpstra.com/projects/howzit/" title="howzit">Howzit</a> to allow some interesting shell integration.</p>

<p>The <code class="language-plaintext highlighter-rouge">--test-search</code> command just returns a zero exit code if:</p>

<ol>
  <li>A build note file is found (<code class="language-plaintext highlighter-rouge">build*.{md,txt}</code>)</li>
  <li>The search string given matches at least one topic, based on configured search type</li>
</ol>

<p>If either of those fails, then it returns a non-zero exit code.</p>

<p>This means you can have a <a href="https://github.com/ttscoff/howzit/wiki/Shell-integration#using-topics-as-shell-commands-fish-and-zsh">Fish or Zsh function for handling unknown commands</a> by testing for a matching topic and executing that if the command isn&rsquo;t found and a matching build note topic exists. So if I have a &ldquo;Run Tests&rdquo; topic in my build notes, and I run <code class="language-plaintext highlighter-rouge">runt</code> on the command line, the unknown command function will execute and will run <code class="language-plaintext highlighter-rouge">howzit -r runt</code>, which will match the &ldquo;Run Tests&rdquo; topic (if fuzzy matching is enabled).</p>

<h3 id="wordpress-plugins">WordPress Plugins</h3>

<p>I also added some localization to the <a href="https://brettterpstra.com/2026/02/02/a-couple-new-wordpress-plugins/">WordPress plugins</a> I shared the other day. No major changes to functionality, just language files for German, Spanish, French, and Italian. These were generated with AI, so I can&rsquo;t vouch for their accuracy. If any speakers of these languages care to look through the various PO files for <a href="https://github.com/ttscoff/bt-downloads/tree/main/bt-downloads/languages">bt-downloads</a> and <a href="https://github.com/ttscoff/bt-keyboard-shortcuts/tree/main/bt-keyboard-shortcuts/languages">bt-keyboard-shortcuts</a>, feel free to let me know (and I&rsquo;ll credit you in the documentation). Eventually when I get these published to the WordPress directory<sup id="fnref:wpdirectory"><a href="https://brettterpstra.com#fn:wpdirectory" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, this will be especially useful.</p>

<p>I should probably at least add Simplified Chinese, too. I&rsquo;ll think about that. Any other language suggestions?</p>

<h3 id="on-multitasking">On Multitasking</h3>

<p>That pretty well sums up what I&rsquo;m working on right now. My sleep cycle is still only allowing me about 5 hours of sleep per night, and I&rsquo;m usually up coding by 3:30AM. And when I&rsquo;m really tired I have this weird ability to focus better while simultaneously multitasking, so most mornings I have 3 or 4 projects open at once and switch between them while one compiles or run tests or I just get bored. That part is kind of cool, though I&rsquo;d rather be sleeping, were that an option.</p>

<p>The reason I keep adding to Howzit is because it&rsquo;s how I manage to jump between projects. With identically-named topics between the projects, when switching contexts I don&rsquo;t have to remember which build system I&rsquo;m supposed to use, I just use Howzit to develop, run tests, build/compile, and deploy. And taking copious notes in my build notes files means I can quickly query a topic and remember what exactly I was doing.</p>

<p>And then there&rsquo;s <a href="https://brettterpstra.com//projects/doing">Doing</a>, which I have attached to git hooks in every repo. Every time I make a commit, it adds a Doing entry for me, so if at 4am I suddenly blank on what I was doing at 3:30, I can just run <code class="language-plaintext highlighter-rouge">doing since 3:30am</code> to see what I&rsquo;ve been working on. It&rsquo;s <em>super</em> handy, and since I&rsquo;m constantly committing to my git repos, I don&rsquo;t even have to worry about making manual Doing entries.</p>

<p>Hopefully something in this update was of interest to you. Please keep me
posted on what you&rsquo;re using (<a href="https://forum.brettterpstra.com">join us in the forum!</a>), what questions/issues/requests you have,
and, as we we always say on Overtired, get some sleep, if you can.</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:wpdirectory">
      <p>My BT-SVG-Viewer plugin has been stuck in review for what feels like months, and you can only submit one plugin at a time.&nbsp;<a href="https://brettterpstra.com#fnref:wpdirectory" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116013154871074263">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Project+Updates+Feb+4%2C+2026+%E2%80%94+Sleepless+Edition%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F04%2Fproject-updates-feb-4-2026-sleepless-edition%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F04%2Fproject-updates-feb-4-2026-sleepless-edition%2F&text=Project+Updates+Feb+4%2C+2026+%E2%80%94+Sleepless+Edition&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F04%2Fproject-updates-feb-4-2026-sleepless-edition%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17269961.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/project-updates-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/project-updates-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Karabiner Home row app switcher]]></title>
    <link href="https://brett.trpstra.net/link/535/17269248/home-row-app-switcher"/>
    <updated>2026-02-03T09:23:00-06:00</updated>
    <published>2026-02-03T09:23:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/03/home-row-app-switcher</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/keyboard-header-green-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>Here&rsquo;s a new Karabiner-Elements trick that I&rsquo;ve been using to add home row app switching to my workflow. Tools like <a href="https://github.com/mikker/LeaderKey" title="mikker/LeaderKey:The *faster than your launcher* launcher">LeaderKey</a> are great, but sometimes you just want the App Switcher, and sometimes you just want to be lazy with your fingers.</p>

<p>My goal was a home-row app switcher that triggered without leaving the home row, and didn&rsquo;t steal keys for normal typing. The solution is a Karabiner-Elements complex modification that uses <strong>simultaneous</strong> key detection: the layer only activates when <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span> and <span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span> are pressed <em>together</em> within a very short window (basically at the exact same time).</p>

<p><strong>This modification is designed for the <a href="https://www.keyboardmaestro.com/">Keyboard Maestro</a> app switcher</strong>, not the macOS default <span class="keycombo separated" title="Command-Tab Key"><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">&#8677;</kbd></span> switcher. Keyboard Maestro&rsquo;s switcher uses Shift alone to move backward, whereas the system switcher expects <span class="keycombo separated" title="Shift-Command-Tab Key"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">&#8677;</kbd></span>; the key outputs below match KM&rsquo;s behavior, so you&rsquo;d need to adjust the modification (e.g. have <span class="keycombo separated" title="D"><kbd class="key symbol">D</kbd></span> send <span class="keycombo separated" title="Shift-Tab Key"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">&#8677;</kbd></span>) to use it with the built-in app switcher.</p>

<h2 id="what-it-does">What it does</h2>

<p>Hold <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span> and <span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span> at the same time. While both are held, you get a temporary layer:</p>

<ul>
  <li><strong><span class="keycombo separated" title="D"><kbd class="key symbol">D</kbd></span></strong> &rarr; previous app (Shift only; KM uses this for backward)</li>
  <li><strong><span class="keycombo separated" title="F"><kbd class="key symbol">F</kbd></span></strong> &rarr; next app (<span class="keycombo separated" title="Command-Tab Key"><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">&#8677;</kbd></span>)</li>
  <li><strong><span class="keycombo separated" title="G"><kbd class="key symbol">G</kbd></span></strong> &rarr; quit (<span class="keycombo separated" title="Command-Q"><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">Q</kbd></span>)</li>
</ul>

<p>So you chord <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span>+<span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span>, then tap <span class="keycombo separated" title="F"><kbd class="key symbol">F</kbd></span>, it loads the App Switcher, then tap <span class="keycombo separated" title="D"><kbd class="key symbol">D</kbd></span> or <span class="keycombo separated" title="F"><kbd class="key symbol">F</kbd></span> to move through the apps, or <span class="keycombo separated" title="G"><kbd class="key symbol">G</kbd></span> to quit the highlighted app. Release <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span> and/or <span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span> and switch to the highlighted app.</p>

<p>This works great with my <a href="https://brettterpstra.com/2025/03/24/keybindings-home-row-arrow-cluster/">home row arrow keys</a>, allowing easy up/down/left/right navigation through Keyboard Maestro&rsquo;s grid-based app switcher. Two-handed, but still all home row.</p>

<h2 id="how-it-avoids-hijacking-a-and-s-for-regular-typing">How it avoids hijacking A and S for regular typing</h2>

<p>The modification never intercepts a lone <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span> or <span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span>. It only triggers when both keys are pressed <strong>simultaneously</strong>, with a 50 ms threshold (<code class="language-plaintext highlighter-rouge">basic.simultaneous_threshold_milliseconds</code>). When you type normally, you press one key, release it, then press another. Your &ldquo;a&rdquo; and &ldquo;s&rdquo; key events are separated by hundreds of milliseconds, so Karabiner never sees them as a chord. Only an intentional A+S chord activates the layer. Single-key &ldquo;a&rdquo; and &ldquo;s&rdquo; pass through unchanged.</p>

<p>Under the hood, the rule uses Karabiner&rsquo;s <code class="language-plaintext highlighter-rouge">simultaneous</code> <code class="language-plaintext highlighter-rouge">from</code> condition: an array of key codes that must all be pressed together. On that chord it sets a variable (<code class="language-plaintext highlighter-rouge">as_layer</code>) and injects <span class="keycombo separated" title="Left_command"><kbd class="key symbol">left_command</kbd></span> so that the following D/F/G rules (which are gated on <code class="language-plaintext highlighter-rouge">as_layer</code>) produce the right modifiers. When either A or S is released, <code class="language-plaintext highlighter-rouge">to_after_key_up</code> clears the variable and the layer drops. So you keep full use of <span class="keycombo separated" title="A"><kbd class="key symbol">A</kbd></span> and <span class="keycombo separated" title="S"><kbd class="key symbol">S</kbd></span> for typing; only the deliberate two-finger chord turns them into a layer key.</p>

<h2 id="import-this-modification">Import this modification</h2>

<div class="biner-wrap" data-json-url="https://brettterpstra.com/karabiner/modifications/home-row-app-switcher.json">
  <div class="biner-btn-group">
    <a href="https://brettterpstra.comkarabiner://karabiner/assets/complex_modifications/import?url=https%3A%2F%2Fbrettterpstra.com%2Fkarabiner%2Fmodifications%2Fhome-row-app-switcher.json" class="biner-btn-main">Import to Karabiner</a>
    <button type="button" class="biner-btn-dropdown-toggle" aria-expanded="false" aria-haspopup="true" title="More options">
      <span class="biner-chevron" aria-hidden="true">&#9660;</span>
    </button>
  </div>
  <div class="biner-dropdown-menu" role="menu" hidden="">
    <button type="button" class="biner-dropdown-item biner-show-json" role="menuitem">Show JSON</button>
    <button type="button" class="biner-dropdown-item biner-copy-url" role="menuitem">Copy JSON URL</button>
  </div>
</div>
<script>
(function(){
  if (!window.BinerModal) {
    window.BinerModal = (function(){
      var overlay, pre, currentText;
      function closeModal(){ if (overlay) { overlay.setAttribute("hidden", ""); overlay.setAttribute("aria-hidden", "true"); document.removeEventListener("keydown", escHandler); } }
      function escHandler(e){ if (e.key === "Escape") closeModal(); }
      function createModal() {
        overlay = document.createElement('div');
        overlay.className = 'biner-modal-overlay';
        overlay.setAttribute('aria-hidden', 'true');
        overlay.innerHTML = '<div class="biner-modal-box" role="dialog" aria-modal="true" aria-labelledby="biner-modal-title">' +
          '<div class="biner-modal-header">' +
            '<h2 id="biner-modal-title" class="biner-modal-title">Modification JSON</h2>' +
            '<button type="button" class="biner-modal-close" aria-label="Close">&times;</button>' +
          '</div>' +
          '<div class="biner-modal-body">' +
            '<pre class="biner-modal-pre"></pre>' +
          '</div>' +
          '<div class="biner-modal-footer">' +
            '<button type="button" class="biner-modal-btn biner-modal-copy">Copy</button>' +
            '<button type="button" class="biner-modal-btn biner-modal-download">Download</button>' +
          '</div></div>';
        pre = overlay.querySelector('.biner-modal-pre');
        var style = document.createElement('style');
        style.textContent = '.biner-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:10000;padding:20px;}.biner-modal-overlay[hidden]{display:none!important}.biner-modal-box{background:#fff;border-radius:8px;box-shadow:0 8px 32px rgba(0,0,0,0.2);max-width:90vw;max-height:85vh;display:flex;flex-direction:column;overflow:hidden}.biner-modal-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #eee}.biner-modal-title{margin:0;font-size:1.1rem;font-weight:600}.biner-modal-close{background:none;border:none;font-size:24px;line-height:1;cursor:pointer;color:#666;padding:0 4px}.biner-modal-close:hover{color:#000}.biner-modal-body{flex:1;overflow:auto;padding:16px}.biner-modal-pre{margin:0;font-family:monospace;font-size:13px;line-height:1.5;white-space:pre-wrap;word-break:break-all}.biner-modal-footer{padding:12px 16px;border-top:1px solid #eee;display:flex;gap:8px;justify-content:flex-end}.biner-modal-btn{padding:8px 16px;border-radius:6px;border:1px solid #ccc;background:#f5f5f5;cursor:pointer;font:inherit}.biner-modal-btn:hover{background:#e5e5e5}.biner-modal-copy:focus,.biner-modal-download:focus{outline:2px solid #007aff;outline-offset:2px}';
        document.head.appendChild(style);
        overlay.querySelector('.biner-modal-close').addEventListener('click', closeModal);
        overlay.addEventListener('click', function(e){ if (e.target === overlay) closeModal(); });
        overlay.querySelector('.biner-modal-copy').addEventListener('click', function(){
          if (!currentText) return;
          if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(currentText);
          } else {
            var ta = document.createElement('textarea'); ta.value = currentText; ta.setAttribute('readonly', ''); ta.style.position = 'absolute'; ta.style.left = '-9999px';
            document.body.appendChild(ta); ta.select();
            try { document.execCommand('copy'); } finally { document.body.removeChild(ta); }
          }
        });
        overlay.querySelector('.biner-modal-download').addEventListener('click', function(){
          if (!currentText) return;
          var fn = (overlay.getAttribute('data-download-filename') || 'modification.json');
          var blob = new Blob([currentText], { type: 'application/json' });
          var url = URL.createObjectURL(blob);
          var a = document.createElement('a'); a.href = url; a.download = fn; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
        });
        document.body.appendChild(overlay);
      }
        return {
        show: function(jsonUrl) {
          if (!overlay) createModal();
          overlay.removeAttribute('hidden');
          overlay.setAttribute('aria-hidden', 'false');
          document.addEventListener('keydown', escHandler);
          pre.textContent = 'Loading…';
          currentText = null;
          overlay.setAttribute('data-download-filename', (jsonUrl.split('/').pop() || 'modification.json'));
          fetch(jsonUrl).then(function(r){ if (!r.ok) throw new Error(r.status); return r.text(); }).then(function(text){
            try { text = JSON.stringify(JSON.parse(text), null, 2); } catch (e) {}
            currentText = text;
            pre.textContent = text;
          }).catch(function(){
            pre.textContent = 'Failed to load JSON.';
          });
        }
      };
    })();
  }
  var wrap = document.currentScript.previousElementSibling;
  var toggle = wrap.querySelector('.biner-btn-dropdown-toggle');
  var menu = wrap.querySelector('.biner-dropdown-menu');
  var jsonUrl = wrap.getAttribute('data-json-url');
  function closeMenu(){ menu.hidden = true; toggle.setAttribute('aria-expanded', 'false'); }
  function openMenu(){ menu.hidden = false; toggle.setAttribute('aria-expanded', 'true'); }
  toggle.addEventListener('click', function(e){
    e.preventDefault();
    e.stopPropagation();
    menu.hidden ? openMenu() : closeMenu();
  });
  wrap.querySelector('.biner-show-json').addEventListener('click', function(){
    closeMenu();
    window.BinerModal.show(jsonUrl);
  });
  wrap.querySelector('.biner-copy-url').addEventListener('click', function(){
    if (navigator.clipboard && navigator.clipboard.writeText) {
      navigator.clipboard.writeText(jsonUrl).then(closeMenu);
    } else {
      var ta = document.createElement('textarea');
      ta.value = jsonUrl;
      ta.setAttribute('readonly', '');
      ta.style.position = 'absolute';
      ta.style.left = '-9999px';
      document.body.appendChild(ta);
      ta.select();
      try { document.execCommand('copy'); closeMenu(); } catch (err) {}
      document.body.removeChild(ta);
    }
  });
  document.addEventListener('click', function(e){
    if (!wrap.contains(e.target)) closeMenu();
  });
})();
</script>

<div id="paywall_8743d3287caa2902" class="paywall-container" data-paywall-id="paywall_8743d3287caa2902" data-content-id="8743d3287caa2902">
  <div class="paywall-content-placeholder" style="display: none;"></div>
  <div class="paywall-overlay">
    <div class="paywall-message">
      <div class="paywall-icon">🔒</div>
      <h3>Premium Content</h3>
      <p>And More</p>
      <div class="paywall-actions">
        <a href="https://brettterpstra.com//support/" class="paywall-subscribe-btn">Subscribe Now</a>
        <button class="paywall-login-btn" onclick="paywallLogin()">I'm a Subscriber</button>
      </div>
    </div>
  </div>
</div>

<script>
  // Set up global paywall config and registration queue
  (function() {
    if (!window.paywallConfig) {
      window.paywallConfig = {
        serviceUrl: 'https://paywall.brettterpstra.com',
        cookieName: 'bt_member_token',
        instances: []
      };
    }

    // Register this instance (collected by bt.Paywall when it loads)
    window.paywallConfig.instances.push({
      id: 'paywall_8743d3287caa2902',
      contentId: '8743d3287caa2902'
    });
  })();
</script>


<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116007497228558733">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Karabiner+Home+row+app+switcher%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F03%2Fhome-row-app-switcher%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F03%2Fhome-row-app-switcher%2F&text=Karabiner+Home+row+app+switcher&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F03%2Fhome-row-app-switcher%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17269248.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/keyboard-header-green-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/keyboard-header-green-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[A couple new WordPress plugins]]></title>
    <link href="https://brett.trpstra.net/link/535/17269249/a-couple-new-wordpress-plugins"/>
    <updated>2026-02-02T13:00:00-06:00</updated>
    <published>2026-02-02T13:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/02/a-couple-new-wordpress-plugins</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/wordpress-plugins-header-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>Just for fun, I&rsquo;ve been porting some of my Jekyll plugins to
WordPress, and two of them have turned into what I think are
really useful plugins.</p>

<p>I&rsquo;m not switching from Jekyll to WordPress, I&rsquo;ve just been
enjoying the challenge of recreating these tools as
WordPress extensions. It&rsquo;s somewhat funny because the Jekyll
plugins were originally created from WordPress plugins I&rsquo;d
built, and they developed over time. So this is kind of a
round trip.</p>

<h3 id="bt-downloads">BT Downloads</h3>

<p><a href="https://brettterpstra.com//projects/bt-downloads">Project page</a></p>

<p>A plugin for managing downloadables and dropping download
cards into posts. You get a custom post type for each
download (file URL, version, description, info link, icon,
changelog), upload buttons for the file and icon right on
the edit screen, and an editable HTML card template with
Mustache-style conditionals
(<code class="language-plaintext highlighter-rouge">{{#description}}...{{/description}}</code>) plus custom CSS with
a live preview.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/downloads-800.jpg"></p>

<p>Insert them via a TinyMCE button in the classic editor or a
<strong>Download</strong> block in the block editor. Pick from a dropdown
and the shortcode (or block) is inserted for you. On the
frontend you get a styled card: title, download link,
description, dates, and optional donate/info links. There&rsquo;s
also a WP-CLI command to import from CSV (the way I handle
downloads in Jekyll):
<code class="language-plaintext highlighter-rouge">wp btdl import_downloads --file=/path/to/downloads.csv</code> .
Full details, more screenshots, and the shortcode reference
are on the <a href="https://brettterpstra.com//projects/bt-downloads">BT Downloads project page</a>.</p>

<h3 id="bt-keyboard-shortcuts">BT Keyboard Shortcuts</h3>

<p><a href="https://brettterpstra.com//projects/bt-keyboard-shortcuts">Project page</a></p>

<p>This one is for writing keyboard shortcuts in posts without
hand-coding symbols. I&rsquo;ve created a few variations of this
over time: <a href="https://brettterpstra.com/2021/06/22/a-jekyll-plugin-for-documenting-mac-keyboard-shortcuts/">for this blog</a>, for Marked and Bunch
documentation, and probably others. A shortcode <code class="language-plaintext highlighter-rouge">[kbd]</code>
renders things like <span class="keycombo separated" title="Option-Shift-Command-A"><kbd class="mod symbol">&#8997;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">A</kbd></span> in the order Apple
recommends, with options for symbol vs text (e.g.
&ldquo;Command-Shift-P&rdquo;), a + separator, and Mac vs Windows
naming.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/kbd-shortcut-editor.jpg"></p>

<p>In the block or classic editor, use the formatting menu and
choose <strong>⌘ Insert keyboard shortcut</strong> to open a dialog:
check modifiers (Cmd/Opt/Shift/Ctrl/Fn), type the key, and
insert the shortcode. Under <strong>Settings → Keyboard
Shortcuts</strong> you can tweak display (symbols vs text, +
separator, key symbols) and add custom CSS for the <code class="language-plaintext highlighter-rouge">.btkbd</code>
keycaps, with a live preview. Examples: <code class="language-plaintext highlighter-rouge">[kbd cmd shift p]</code>
&rarr; <span class="keycombo separated" title="Shift-Command-P"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">P</kbd></span>, <code class="language-plaintext highlighter-rouge">[kbd$@P]</code> → <span class="keycombo separated" title="Shift-Command-P"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">P</kbd></span>. More
syntax and options are on the
<a href="https://brettterpstra.com//projects/bt-keyboard-shortcuts">BT Keyboard Shortcuts project page</a>.</p>

<p><img src="https://cdn3.brettterpstra.com/uploads/2026/02/kbd-post.jpg"></p>

<h3 id="repo-and-requirements">Repo and requirements</h3>

<p>Both require WordPress 5.8+ and PHP 7.4+, and are GPLv2 or
later. The source for these and any future WordPress plugins
from me is my <a href="https://github.com/ttscoff/wordpress-plugins">wordpress-plugins repo on GitHub</a>. Grab
the code there or follow the install steps on each project
page.</p>


<p>Like or share this post <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F02%2Fa-couple-new-wordpress-plugins%2F&text=A+couple+new+WordPress+plugins&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F02%2Fa-couple-new-wordpress-plugins%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17269249.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/02/wordpress-plugins-header-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/02/wordpress-plugins-header-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for February 2nd, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17268512/web-excursions-for-february-2nd-2026"/>
    <updated>2026-02-02T10:14:00-06:00</updated>
    <published>2026-02-02T10:14:00-06:00</published>
    <id>https://brettterpstra.com//2026/02/02/web-excursions-for-february-2nd-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Whether you&rsquo;re a new user or a seasoned pro, ScreenCastsONLINE offers in-depth screencasts on a wide range of topics, from tutorials to app discovery. <a href="https://screencastsonline.com/members/aff/go/bterpstra">Check it out.</a></p>

<dl>
  <dt><a href="https://iloveapiano.app/">I Love a Piano</a></dt>
  <dd>I met Max Mellman at Macstock and we became fast friends. His first app is a piano synth with multiple on-screen scrollable keyboards, different piano sounds, and a built in music player with fine scrubbing for learning along with your favorite music.</dd>
  <dt><a href="https://www.amazon.com/Lawyers-Guide-Podcasting-Tech-Savvy-Lawyer-Pages/dp/B0GGX32DZH#detailBullets_feature_div">The Lawyer&rsquo;s Guide to Podcasting</a></dt>
  <dd>A book from my friend Michael D.J. Eisenberg ((The Tech-Savvy Lawyer): The Lawyer&rsquo;s Guide to Podcasting: Building Your Brand, Audience, Tech Stack, and Expertise!</dd>
  <dt><a href="https://github.com/rhsev/mi.lan">rhsev/mi.lan:</a></dt>
  <dd>A lightweight URL bridge for macOS automation, and a companion for <a href="https://github.com/rhsev/dy.lan">dy.lan</a>, which I <a href="https://brettterpstra.com/2026/01/27/a-url-router-that-turns-your-local-network-into-a-workflow-engine/">wrote about last week</a>.</dd>
  <dt><a href="https://apps.apple.com/us/app/vimari/id1480933944?mt=12">‎Vimari App - App Store</a></dt>
  <dd>A port of Vimium for Safari. Vimium is one of my favorite extensions on Chrome and Firefox, and I&rsquo;d always missed it on Safari, not realizing someone had already ported it.</dd>
  <dt><a href="https://apps.apple.com/us/app/linkthing/id1666031776">‎LinkThing</a></dt>
  <dd>Not perfect, but if you need to view and search your linkding bookmarks, this runs on all Apple platforms and does the trick.</dd>
  <dt><a href="https://github.com/moltbot/moltbot">moltbot/moltbot: Your own personal AI assistant.</a></dt>
  <dd>A personal chatbot assistant for any OS and any platform. I think this is what I&rsquo;m going to do with one of the 2012 Mac minis I picked up for $25.</dd>
  <dt><a href="https://github.com/steveyegge/gastown">Gas Town - multi-agent workspace manager</a></dt>
  <dd>We talked about this at some length on the <a href="https://overtiredpod.com/ep/442/">last Overtired</a>. Run multiple AI coding agents simultaneously with lots of useful features. Not cheap to implement fully, but powerful.</dd>
  <dt><a href="https://updates.techforpalestine.org/upscrolled-is-live-the-instagram-alternative-thats-actually-on-your-side/">UpScrolled is live</a></dt>
  <dd>
    <blockquote>
      <p>If you&rsquo;ve been waiting for a real alternative to Instagram, this is it.</p>
    </blockquote>
  </dd>
  <dd>
    <p>From a Palestinian-Australian developer and supported by Tech for Palestine, a solid looking alternative to Instagram that allows text updates and no shadow banning/censorship. Right now it&rsquo;s mostly my source for news from Gaza, but I&rsquo;m curious to see how it grows. They&rsquo;ve already hit limitations of scale and are updating rapidly.</p>
  </dd>
</dl>

<p>Want more great tips and apps? Check out <a href="https://screencastsonline.com/members/aff/go/bterpstra">ScreenCastsOnline</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/116001986004813901">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+February+2nd%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F02%2Fweb-excursions-for-february-2nd-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F02%2Fweb-excursions-for-february-2nd-2026%2F&text=Web+Excursions+for+February+2nd%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F02%2F02%2Fweb-excursions-for-february-2nd-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17268512.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Project-level plugins and config for Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17265388/project-level-plugins-and-config-for-apex"/>
    <updated>2026-01-28T12:14:00-06:00</updated>
    <published>2026-01-28T12:14:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/28/project-level-plugins-and-config-for-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>If you use Apex for more than one project, you&rsquo;ve probably hit the point where a single global setup doesn&rsquo;t quite cut it. Maybe your book project wants a different plugin set than your docs site, or one repo has stricter defaults than everything else on your machine.</p>

<p>As a personal example, most of my app documentation has always been written in MultiMarkdown, where header ids get generated with no spaces or dashes, so all of my cross-references link to <code class="language-plaintext highlighter-rouge">#thissection</code> type of anchors. My blog and my Jekyll-based documentation sites have always used Kramdown, so header ids and cross references are <code class="language-plaintext highlighter-rouge">#this-section</code>. I needed an easy way to always have Apex use the right header format for the current project.</p>

<p>I also have special plugins for different destinations. For example, my Marked documentation has special Liquid-style tags like <code class="language-plaintext highlighter-rouge">prefpane</code> that generates nice HTML for referencing Preference panes with <code class="language-plaintext highlighter-rouge">x-marked</code> URLs that will open a preference pane directly in Marked. I don&rsquo;t need or want a plugin to do that universally, the output it generates is very specific to Marked.</p>

<p>So I added project-scoped plugins and configurations to Apex. This allows me to get the settings just right for a project, then save them into a local directory and be able to just run <code class="language-plaintext highlighter-rouge">apex</code> without a bunch of command line flags to remember.</p>

<p>You also get a cleaner way to &ldquo;shadow&rdquo; plugins you don&rsquo;t want with a local noop plugin.</p>

<blockquote>
  <p>I also added <code class="language-plaintext highlighter-rouge">++insert++</code> syntax for adding <code class="language-plaintext highlighter-rouge">&lt;ins&gt;insert&lt;/ins&gt;</code> tags, but that&rsquo;s just a little one-off addition.</p>
</blockquote>

<h3 id="project-scoped-plugins-in-apexplugins">Project-scoped plugins in <code class="language-plaintext highlighter-rouge">.apex/plugins</code></h3>

<p>Plugins used to be purely global: Apex would only look in your XDG config dir:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">$XDG_CONFIG_HOME/apex/plugins</code>, or</li>
  <li><code class="language-plaintext highlighter-rouge">~/.config/apex/plugins</code></li>
</ul>

<p>Now there&rsquo;s a proper <strong>project scope</strong>, searched in this order:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">./.apex/plugins</code> (current working directory)</li>
  <li><code class="language-plaintext highlighter-rouge">BASE/.apex/plugins</code> when you run with <code class="language-plaintext highlighter-rouge">--base-dir BASE</code></li>
  <li><code class="language-plaintext highlighter-rouge">&lt;git-root&gt;/.apex/plugins</code> when you&rsquo;re inside a Git work tree</li>
  <li>Global: <code class="language-plaintext highlighter-rouge">$XDG_CONFIG_HOME/apex/plugins</code> or <code class="language-plaintext highlighter-rouge">~/.config/apex/plugins</code></li>
</ol>

<p>Each of those directories can hold Apex plugins in the usual format:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>.apex/
  plugins/
    my-plugin/
      plugin.yml
      whatever-script-you-like</code></pre></div></div>

<p>When Apex builds the plugin list, <strong>earlier locations win by id</strong>. If a plugin with id <code class="language-plaintext highlighter-rouge">footnotes-plus</code> exists in both <code class="language-plaintext highlighter-rouge">.apex/plugins</code> and your global config dir, the project version is the one that runs.</p>

<h3 id="no-op-shadowing-turning-off-plugins-per-project">No-op shadowing: turning off plugins per project</h3>

<p>That id-based precedence also gives you a neat trick: <strong>no-op shadowing</strong>.</p>

<p>If there&rsquo;s a global plugin you usually like, but you don&rsquo;t want it in a specific project, you can &ldquo;shadow&rdquo; it by dropping an empty or no-op plugin with the same id into <code class="language-plaintext highlighter-rouge">.apex/plugins</code>. For example:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>.apex/
  plugins/
    kbd/
      plugin.yml
      noop.sh</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">plugin.yml</code> might look like:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">id</span><span class="pi">:</span> <span class="s">kbd</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">KBD Noop</span></code></pre></div></div>

<p>Because the project copy of <code class="language-plaintext highlighter-rouge">kbd</code> is discovered first, it <strong>shadows</strong> the global one. You can also do the same trick with purely declarative regex plugins: define a plugin with the same id that simply doesn&rsquo;t match anything meaningful, and the global behavior is effectively disabled for that project.</p>

<h3 id="--list-plugins-now-understands-projects"><code class="language-plaintext highlighter-rouge">--list-plugins</code> now understands projects</h3>

<p>To make this discoverable, <code class="language-plaintext highlighter-rouge">apex --list-plugins</code> was updated to use the <strong>same resolution rules</strong> as the runtime plugin loader.</p>

<p>When you run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--list-plugins</span></code></pre></div></div>

<p>you&rsquo;ll see:</p>

<ul>
  <li>An &ldquo;Installed Plugins&rdquo; section that includes plugins from:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">./.apex/plugins</code></li>
      <li><code class="language-plaintext highlighter-rouge">BASE/.apex/plugins</code> (if <code class="language-plaintext highlighter-rouge">--base-dir</code> was used)</li>
      <li><code class="language-plaintext highlighter-rouge">&lt;git-root&gt;/.apex/plugins</code></li>
      <li>global config plugins</li>
    </ul>
  </li>
  <li>An &ldquo;Available Plugins&rdquo; section from the remote directory, <strong>filtered</strong> so remote entries are hidden when you already have a plugin with the same id installed anywhere (project or global).</li>
</ul>

<p>If a project plugin shadows a global one, you&rsquo;ll only see the project entry in the installed list, and the remote listing won&rsquo;t try to &ldquo;helpfully&rdquo; re-offer the same id.</p>

<h3 id="project-level-config-in-apexconfigyml">Project-level config in <code class="language-plaintext highlighter-rouge">.apex/config.yml</code></h3>

<p>Plugins aren&rsquo;t the only thing that benefit from scoping. Apex&rsquo;s configuration system now has an explicit <strong>project layer</strong>, alongside the existing global and per-document metadata.</p>

<p>Config is now read from these places:</p>

<ol>
  <li><strong>Global config</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">$XDG_CONFIG_HOME/apex/config.yml</code>, or</li>
      <li><code class="language-plaintext highlighter-rouge">~/.config/apex/config.yml</code></li>
    </ul>
  </li>
  <li><strong>Project config</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">./.apex/config.yml</code></li>
      <li><code class="language-plaintext highlighter-rouge">BASE/.apex/config.yml</code> when using <code class="language-plaintext highlighter-rouge">--base-dir BASE</code></li>
      <li><code class="language-plaintext highlighter-rouge">&lt;git-root&gt;/.apex/config.yml</code> when inside a Git work tree</li>
    </ul>
  </li>
  <li><strong>Explicit metadata file</strong>
    <ul>
      <li>Any file you pass with <code class="language-plaintext highlighter-rouge">--meta-file FILE</code></li>
    </ul>
  </li>
  <li><strong>Per-document metadata</strong>
    <ul>
      <li>YAML front matter, MultiMarkdown metadata, or Pandoc title blocks</li>
    </ul>
  </li>
  <li><strong>Command-line metadata and flags</strong>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">--meta KEY=VALUE</code>, <code class="language-plaintext highlighter-rouge">--mode</code>, <code class="language-plaintext highlighter-rouge">--pretty</code>, <code class="language-plaintext highlighter-rouge">--no-tables</code>, and so on</li>
    </ul>
  </li>
</ol>

<p>The merge order matters:</p>

<ul>
  <li>Global <code class="language-plaintext highlighter-rouge">config.yml</code> (lowest file precedence)</li>
  <li>Project <code class="language-plaintext highlighter-rouge">.apex/config.yml</code></li>
  <li><code class="language-plaintext highlighter-rouge">--meta-file FILE</code></li>
  <li>Document metadata</li>
  <li><code class="language-plaintext highlighter-rouge">--meta</code> and CLI flags (highest precedence)</li>
</ul>

<p>So if you put this in your <strong>global</strong> config:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">mode</span><span class="pi">:</span> <span class="s">unified</span>
<span class="na">pretty</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">wikilinks</span><span class="pi">:</span> <span class="kc">false</span></code></pre></div></div>

<p>and then in your <strong>project</strong> <code class="language-plaintext highlighter-rouge">.apex/config.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">wikilinks</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">indices</span><span class="pi">:</span> <span class="kc">true</span></code></pre></div></div>

<p>you&rsquo;ll end up with:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">mode: unified</code></li>
  <li><code class="language-plaintext highlighter-rouge">pretty: true</code></li>
  <li><code class="language-plaintext highlighter-rouge">wikilinks: true</code>   # project overrides global</li>
  <li><code class="language-plaintext highlighter-rouge">indices: true</code>     # project-only addition</li>
</ul>

<p>Any <code class="language-plaintext highlighter-rouge">--meta-file</code> you pass on the command line layers on top of both, and document/CLI overrides still win last.</p>

<h3 id="a-quick-example-project-layout">A quick example project layout</h3>

<p>Here&rsquo;s what a repo might look like with all of this wired up:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>my-book/
  .git/
  .apex/
    config.yml
    plugins/
      figures/
        plugin.yml
        figures.py
      kbd/
        plugin.yml
  chapters/
    01-intro.md
    02-deep-dive.md</code></pre></div></div>

<p>Running:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="nb">cd </span>my-book
apex chapters/01-intro.md <span class="nt">--plugins</span></code></pre></div></div>

<p>will:</p>

<ul>
  <li>Load config from:
    <ul>
      <li>global <code class="language-plaintext highlighter-rouge">config.yml</code> (if any),</li>
      <li>then <code class="language-plaintext highlighter-rouge">./.apex/config.yml</code>,</li>
      <li>then any <code class="language-plaintext highlighter-rouge">--meta-file</code> you pass,</li>
    </ul>
  </li>
  <li>Run plugins from:
    <ul>
      <li><code class="language-plaintext highlighter-rouge">./.apex/plugins</code> first,</li>
      <li>then fall back to global plugins,</li>
    </ul>
  </li>
  <li>Apply per-document metadata and CLI overrides on top.</li>
</ul>

<p>You get per-repo behavior <strong>without</strong> having to constantly remember the right <code class="language-plaintext highlighter-rouge">--meta-file</code> or a long list of flags.</p>

<h3 id="a-quick-note-on-insert">A quick note on <code class="language-plaintext highlighter-rouge">++insert++</code></h3>

<p>While we were in here, another small but handy syntax has been added: <code class="language-plaintext highlighter-rouge">++insert++</code>.</p>

<p><code class="language-plaintext highlighter-rouge">++insert++</code> gives you a lightweight way to add an <code class="language-plaintext highlighter-rouge">&lt;ins&gt;text&lt;/ins&gt;</code> tag to your document. It&rsquo;s just a little shorter and easier than typing out the tags manually. I try not to add too much esoteric markup to the syntax, but I&rsquo;ve seen <code class="language-plaintext highlighter-rouge">++</code> a couple of places and thought it a worthwhil addition.</p>

<h3 id="wrapping-up">Wrapping up</h3>

<p>To recap:</p>

<ul>
  <li>Plugins can now live in <code class="language-plaintext highlighter-rouge">.apex/plugins</code> at the project level, and they override global plugins by id.</li>
  <li><code class="language-plaintext highlighter-rouge">--list-plugins</code> shows the <strong>actual</strong> set of plugins Apex will run for your current project, including overrides.</li>
  <li>Config can now live in <code class="language-plaintext highlighter-rouge">.apex/config.yml</code>, layered on top of your global <code class="language-plaintext highlighter-rouge">config.yml</code> and below any explicit <code class="language-plaintext highlighter-rouge">--meta-file</code>, document metadata, and flags.</li>
  <li><code class="language-plaintext highlighter-rouge">++insertion++</code> gives you <code class="language-plaintext highlighter-rouge">&lt;ins&gt;</code> tags.</li>
</ul>

<p>If you&rsquo;ve been juggling different shell aliases or wrapper scripts for each project, you can probably simplify a lot of that now by letting Apex&rsquo;s own project-aware behavior do the heavy lifting.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115974151781282518">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Project-level+plugins+and+config+for+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F28%2Fproject-level-plugins-and-config-for-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F28%2Fproject-level-plugins-and-config-for-apex%2F&text=Project-level+plugins+and+config+for+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F28%2Fproject-level-plugins-and-config-for-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17265388.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Dy.lan: turn your local network into a workflow engine]]></title>
    <link href="https://brett.trpstra.net/link/535/17264591/a-url-router-that-turns-your-local-network-into-a-workflow-engine"/>
    <updated>2026-01-27T08:00:00-06:00</updated>
    <published>2026-01-27T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/27/a-url-router-that-turns-your-local-network-into-a-workflow-engine</id>
    <content type="html"><![CDATA[
<p>Have you ever wished you could turn your local network into a smart routing system? Instead of remembering IP addresses and ports, what if you could use simple, memorable URLs that trigger workflows, redirect to services, or even search your notes?</p>

<p>That&rsquo;s exactly what <a href="https://github.com/rhsev/dy.lan">dy.lan</a> does. It&rsquo;s a self-hosted URL router with a plugin architecture that transforms your local network into a powerful automation platform. Built by Ralf Hülsmann, a Ruby rookie from Sevelen, Germany, dy.lan (pronounced &ldquo;Dylan&rdquo;) is a lightweight HTTP router designed specifically for local networks.</p>

<h2 id="the-basic-concept">The Basic Concept</h2>

<p>At its core, dy.lan acts as a central entry point that translates URLs into actions. Instead of remembering <code class="language-plaintext highlighter-rouge">192.168.1.73:8384</code> for Syncthing, you can access <code class="language-plaintext highlighter-rouge">http://sync.lan</code>. Instead of writing complex scripts, you can use <code class="language-plaintext highlighter-rouge">http://dy.lan/n/meeting</code> to search your Apple Notes.</p>

<p>The project solves several common problems:</p>

<ul>
  <li><strong>Infrastructure abstraction</strong>: When a service moves to a new IP or port, you update one config line instead of hunting down bookmarks and scripts</li>
  <li><strong>Workflow shortcuts</strong>: Turn URLs into actions with pattern-based routing</li>
  <li><strong>Clean local services</strong>: Route HTTP traffic without the complexity of full reverse proxies like Traefik or nginx for simple use cases</li>
  <li><strong>Extensibility</strong>: YAML configs for simple redirects, Ruby plugins for custom logic</li>
</ul>

<h2 id="plugin-architecture-and-extensibility">Plugin Architecture and Extensibility</h2>

<p>What makes dy.lan powerful is its plugin system. The architecture is built around numbered plugins (00-, 10-, 20-&hellip;) that follow a first-match-wins priority system. Each plugin can:</p>

<ul>
  <li>Match URLs using regex patterns with capture groups</li>
  <li>Filter by domain/host</li>
  <li>Execute custom Ruby logic</li>
  <li>Handle timeouts (default 500ms, configurable per plugin)</li>
  <li>Auto-disable after 5 errors (circuit breaker pattern)</li>
</ul>

<p>Plugins are hot-reloadable for YAML configs (after domain-match), and the system is resilient &mdash; syntax errors and loops won&rsquo;t crash the server.</p>

<p>The project includes 8 example plugins covering everything from simple redirects to API integrations, monitoring dashboards, and cron jobs. You can extend it with any feature you can implement in Ruby.</p>

<h2 id="real-world-examples">Real-World Examples</h2>

<p>Here are some practical ways dy.lan can be used:</p>

<p><strong>Google Search Shortcut</strong></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c1"># config/redirects.yaml</span>
<span class="na">redirects</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">pattern</span><span class="pi">:</span> <span class="s1">'</span><span class="s">^/g/(.+)$'</span>
    <span class="na">target</span><span class="pi">:</span> <span class="s1">'</span><span class="s">https://google.com/search?q=${1}'</span></code></pre></div></div>

<p>Access <code class="language-plaintext highlighter-rouge">http://dy.lan/g/ruby</code> and it redirects to a Google search for &ldquo;ruby&rdquo;. Simple, memorable, and no coding required.</p>

<p><strong>DEVONthink Search</strong>
For more complex workflows, you can use a Ruby plugin:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="k">class</span> <span class="nc">DevonthinkPlugin</span> <span class="o">&lt;</span> <span class="no">Dylan</span><span class="o">::</span><span class="no">Plugin</span>
  <span class="n">pattern</span> <span class="sr">%r{^/(</span><span class="se">\d</span><span class="sr">{8})}</span>

  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">request</span><span class="p">)</span>
    <span class="n">alias_id</span> <span class="o">=</span> <span class="n">path</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="n">pattern</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
    <span class="no">Dylan</span><span class="o">::</span><span class="no">Response</span><span class="p">.</span><span class="nf">redirect</span><span class="p">(</span><span class="s2">"x-devonthink-item://</span><span class="si">#{</span><span class="n">alias_id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre></div></div>

<p>Now <code class="language-plaintext highlighter-rouge">http://dy.lan/12345678</code> opens the document with that alias in DEVONthink.</p>

<p><strong>Apple Notes Search</strong></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="k">class</span> <span class="nc">NotesPlugin</span> <span class="o">&lt;</span> <span class="no">Dylan</span><span class="o">::</span><span class="no">Plugin</span>
  <span class="n">pattern</span> <span class="sr">%r{^/n/(.+)}</span>

  <span class="k">def</span> <span class="nf">call</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">request</span><span class="p">)</span>
    <span class="n">query</span> <span class="o">=</span> <span class="n">path</span><span class="p">.</span><span class="nf">match</span><span class="p">(</span><span class="n">pattern</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
    <span class="no">Dylan</span><span class="o">::</span><span class="no">Response</span><span class="p">.</span><span class="nf">redirect</span><span class="p">(</span><span class="s2">"shortcuts://run-shortcut?name=search_notes&amp;input=</span><span class="si">#{</span><span class="n">query</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre></div></div>

<p>Access <code class="language-plaintext highlighter-rouge">http://dy.lan/n/example</code> to search your Apple Notes for &ldquo;example&rdquo; via Shortcuts.</p>

<h2 id="deployment-options">Deployment Options</h2>

<p>One of the great things about dy.lan is its flexibility. It can run on your Mac for local development or on a Synology NAS for 24/7 operation.</p>

<p><strong>On Your Mac:</strong></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>git clone https://github.com/rhsev/dy.lan
<span class="nb">cd </span>dy.lan
docker-compose <span class="nt">-f</span> docker-compose.mac.yml up <span class="nt">-d</span></code></pre></div></div>

<p><strong>On Synology:</strong>
The project includes <code class="language-plaintext highlighter-rouge">docker-compose.synology.yml</code> for easy deployment on NAS devices. With macvlan networking, you can give dy.lan its own dedicated IP address, avoiding conflicts with other reverse proxies.</p>

<p>The performance is impressive: 6,000 requests per second on a Mac mini M4, and 2,500 requests per second on a Synology DS224+ (compared to ~200 req/s for YOURLS on similar hardware). All while using just 20-30 MB of RAM.</p>

<h2 id="technical-highlights">Technical Highlights</h2>

<p>Built with Ruby 4.0&rsquo;s async/fiber-based concurrency, dy.lan uses non-blocking I/O so slow plugins don&rsquo;t block fast ones. It&rsquo;s a pure Ruby implementation with no Rails, no middleware, and no database. Configuration is done through YAML files and Ruby plugins, with a browser-based dashboard for stats and container management.</p>

<p>The system includes modern Ruby syntax (using <code class="language-plaintext highlighter-rouge">it</code> parameter in core code while keeping plugins explicit), configurable timeouts, circuit breakers, and domain filtering per plugin. It&rsquo;s designed to be simple, transparent, and easy to extend.</p>

<h2 id="getting-started">Getting Started</h2>

<p>If you want to try dy.lan yourself, check out the <a href="https://github.com/rhsev/dy.lan">GitHub repository</a>. The project is shared as-is, built to solve specific friction in personal automation workflows. Ralf has been developing and sharing this with me for a while, and I&rsquo;m glad he decided to make it public so others can benefit.</p>

<p>Whether you need simple URL redirects, workflow automation, or a lightweight reverse proxy for local services, dy.lan provides a clean and extensible solution.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115968136522432127">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Dy.lan%3A+turn+your+local+network+into+a+workflow+engine%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F27%2Fa-url-router-that-turns-your-local-network-into-a-workflow-engine%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F27%2Fa-url-router-that-turns-your-local-network-into-a-workflow-engine%2F&text=Dy.lan%3A+turn+your+local+network+into+a+workflow+engine&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F27%2Fa-url-router-that-turns-your-local-network-into-a-workflow-engine%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17264591.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Tween: Fish function for outputting ranges of text]]></title>
    <link href="https://brett.trpstra.net/link/535/17260953/tween-fish-function-for-outputting-ranges-of-text"/>
    <updated>2026-01-22T10:51:00-06:00</updated>
    <published>2026-01-22T10:51:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/22/tween-fish-function-for-outputting-ranges-of-text</id>
    <content type="html"><![CDATA[
<p>I&rsquo;ve been developing a Fish function called <code class="language-plaintext highlighter-rouge">tween</code>. It&rsquo;s a simple, flexible utility for extracting ranges of lines from files or STDIN input, and it&rsquo;s flexible enough to handle just about any scenario you can throw at it, from numeric ranges, array style position/length ranges, or even string matching with regex capabilities.</p>

<p>You can find the source code in my <a href="https://github.com/ttscoff/fish_files">Fish functions repository</a>, specifically at <a href="https://github.com/ttscoff/fish_files/blob/master/functions/tween.fish">tween.fish</a>.</p>

<h2 id="what-it-does">What it does</h2>

<p><code class="language-plaintext highlighter-rouge">tween</code> displays lines between a start and end point. The start and end can be line numbers, string matches, or regex patterns. It supports multiple ranges, relative offsets, and even works with piped input.</p>

<h2 id="basic-usage">Basic usage</h2>

<p>The simplest form is specifying line numbers:</p>

<pre><code class="language-fish">tween file.txt 10 20
</code></pre>

<p>This displays lines 10 through 20 (inclusive). You can also use a dashed range format:</p>

<pre><code class="language-fish">tween file.txt 10-20
</code></pre>

<p>And here&rsquo;s the nice part: arguments can be in any order. Both of these work the same way:</p>

<pre><code class="language-fish">tween file.txt 10 20
tween 10 20 file.txt
</code></pre>

<p>If you only specify a single line number, <code class="language-plaintext highlighter-rouge">tween</code> will display from that line to the end of the file:</p>

<pre><code class="language-fish">tween file.txt 50
</code></pre>

<p>This shows lines 50 through the end of the file.</p>

<h2 id="multiple-ranges">Multiple ranges</h2>

<p>Need to extract several sections? Just separate them with commas:</p>

<pre><code class="language-fish">tween file.txt 10-20,30-40
tween file.txt 10 20, 30 40
</code></pre>

<p>Both formats work, so use whichever feels more natural.</p>

<h2 id="relative-offsets">Relative offsets</h2>

<p>Sometimes you know where you want to start but need to go a certain number of lines forward. Use <code class="language-plaintext highlighter-rouge">+N</code> for relative offsets:</p>

<pre><code class="language-fish">tween file.txt 10 +20
</code></pre>

<p>This displays lines 10 through 30 (10 plus 20 lines). You can also count from the end using <code class="language-plaintext highlighter-rouge">-N</code>:</p>

<pre><code class="language-fish">tween file.txt 50 -10
</code></pre>

<p>This shows lines 50 to 10 lines from the end of the file. There&rsquo;s a special case: <code class="language-plaintext highlighter-rouge">-1</code> means the end of the file (the last line):</p>

<pre><code class="language-fish">tween file.txt 50 -1
</code></pre>

<p>This displays lines 50 through the end of the file. Other negative numbers like <code class="language-plaintext highlighter-rouge">-2</code>, <code class="language-plaintext highlighter-rouge">-3</code>, etc. still mean &ldquo;N lines from the end&rdquo; (second-to-last, third-to-last, etc.).</p>

<h2 id="string-matching">String matching</h2>

<p>Instead of line numbers, you can match on strings. This is super useful when you know the content but not the exact line:</p>

<pre><code class="language-fish">tween file.txt 'START' +20
</code></pre>

<p>This finds the line containing &ldquo;START&rdquo; and displays it plus the next 20 lines. You can also use string matching for both start and end:</p>

<pre><code class="language-fish">tween file.txt 50-'END'
</code></pre>

<p>This shows lines 50 through the line containing &ldquo;END&rdquo;.</p>

<h2 id="regex-patterns">Regex patterns</h2>

<p>For more complex matching, use regex patterns wrapped in slashes:</p>

<pre><code class="language-fish">tween file.txt /START/ +20
</code></pre>

<p>This matches the regex pattern &ldquo;START&rdquo; and displays that line plus 20 more. You can also use the <code class="language-plaintext highlighter-rouge">-r</code> or <code class="language-plaintext highlighter-rouge">--regex</code> flag to treat all string arguments as regex:</p>

<pre><code class="language-fish">tween -r file.txt 'foo' 'bar'
</code></pre>

<p>Both patterns are treated as regex when using the <code class="language-plaintext highlighter-rouge">-r</code> flag.</p>

<h2 id="exclusive-mode">Exclusive mode</h2>

<p>Sometimes you want the lines <em>between</em> two markers but not the markers themselves. Use <code class="language-plaintext highlighter-rouge">-e</code> or <code class="language-plaintext highlighter-rouge">--exclusive</code>:</p>

<pre><code class="language-fish">tween -e file.txt 'BEGIN' 'END'
</code></pre>

<p>This displays all lines between (but not including) the lines containing &ldquo;BEGIN&rdquo; and &ldquo;END&rdquo;.</p>

<h2 id="syntax-highlighting-with-bat">Syntax highlighting with bat</h2>

<p>If you have <code class="language-plaintext highlighter-rouge">bat</code> installed, you can use it for syntax highlighting with the <code class="language-plaintext highlighter-rouge">-b</code> or <code class="language-plaintext highlighter-rouge">--bat</code> flag:</p>

<pre><code class="language-fish">tween -b file.txt 10-20
</code></pre>

<p>This displays the range with syntax highlighting, which is especially nice when viewing code.</p>

<h2 id="piped-input">Piped input</h2>

<p><code class="language-plaintext highlighter-rouge">tween</code> works with piped input too. Just use <code class="language-plaintext highlighter-rouge">-</code> as the file argument:</p>

<pre><code class="language-fish">cat file.txt | tween 10-20,30-40 -
</code></pre>

<p>This is handy when you&rsquo;re already working with a pipeline.</p>

<h2 id="options-summary">Options summary</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-e, --exclusive</code> - Exclude the start and end lines from output</li>
  <li><code class="language-plaintext highlighter-rouge">-b, --bat</code> - Use bat instead of sed for syntax highlighting</li>
  <li><code class="language-plaintext highlighter-rouge">-r, --regex</code> - Treat all string arguments as regular expressions</li>
  <li><code class="language-plaintext highlighter-rouge">-h, --help</code> - Show help message</li>
</ul>

<p>The function is flexible enough to handle most text extraction tasks, and the ability to mix line numbers, strings, and regex makes it incredibly versatile. Give it a try and see how it fits into your workflow!</p>


<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115939853563229265">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Tween%3A+Fish+function+for+outputting+ranges+of+text%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F22%2Ftween-fish-function-for-outputting-ranges-of-text%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F22%2Ftween-fish-function-for-outputting-ranges-of-text%2F&text=Tween%3A+Fish+function+for+outputting+ranges+of+text&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F22%2Ftween-fish-function-for-outputting-ranges-of-text%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17260953.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Markdown Fixup plugin for Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17256661/markdown-fixup-plugin-for-apex"/>
    <updated>2026-01-19T12:13:00-06:00</updated>
    <published>2026-01-19T12:13:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/19/markdown-fixup-plugin-for-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/mdfixup-apex-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>My two biggest projects over the last week have been <a href="https://github.com/ttscoff/md-fixup">Markdown Fixup</a> and <a href="https://github.com/ApexMarkdown/apex">Apex</a>. It seemed worthwhile to integrate the two in some useful way.</p>

<blockquote>
  <p>A quick update on Markdown Fixup: I fixed the regex replacement engine to handle multi-line replacements. This is kind of a big deal if you&rsquo;re trying to convert things like BBCode to Markdown as part of the pipeline.</p>
</blockquote>

<p>I&rsquo;ve added a new plugin that integrates <code class="language-plaintext highlighter-rouge">md-fixup</code> directly into Apex&rsquo;s processing pipeline. If you haven&rsquo;t been tracking md-fixup, it&rsquo;s an opinionated markdown linter and fixer that can normalize spacing, fix emphasis markers, and apply custom regex replacements to your markdown files. And if you haven&rsquo;t been following Apex, it&rsquo;s my &ldquo;universal markdown processor&rdquo; project that handles all kinds of Markdown extensions in one place.</p>

<h2 id="why-a-plugin">Why a plugin?</h2>

<p>You could always just pipe md-fixup into apex:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>md-fixup file.md | apex <span class="nt">--mode</span> kramdown</code></pre></div></div>

<p>That works fine for simple cases, but if you want to run md-fixup as part of a pipeline with other Apex plugins, the plugin version is the way to go. Plugins run in a deterministic order, and you can enable or disable them as needed without changing your workflow.</p>

<h2 id="when-it-runs">When it runs</h2>

<p>The md-fixup plugin runs in the <code class="language-plaintext highlighter-rouge">pre_parse</code> phase, which means it processes your raw markdown text before Apex parses it. This is important for a couple of reasons:</p>

<ul>
  <li><strong>Compatibility</strong>: md-fixup can normalize your markdown to ensure it&rsquo;s compatible with Apex&rsquo;s formatting expectations</li>
  <li><strong>Regex replacements</strong>: Most importantly, if you&rsquo;re using custom regex replacements in your <code class="language-plaintext highlighter-rouge">replacements.yml</code> file, those run before Apex processes the text. This means you can transform markdown syntax itself, not just the final HTML output</li>
</ul>

<h2 id="installation">Installation</h2>

<p>The plugin is available in the Apex plugin directory. You can see it listed with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--list-plugins</span></code></pre></div></div>

<p>Install it with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--install-plugin</span> md-fixup</code></pre></div></div>

<p>The plugin assumes you already have the <a href="https://github.com/ttscoff/md-fixup?tab=readme-ov-file#installation">md-fixup binary installed</a> and available in your PATH. (And it uses the Rust version, which is what <code class="language-plaintext highlighter-rouge">brew install md-fixup</code> will install.)</p>

<h2 id="configuring-replacements">Configuring replacements</h2>

<p>After installation, the plugin creates a support directory at <code class="language-plaintext highlighter-rouge">~/.config/apex/support/md-fixup/</code> with a template <code class="language-plaintext highlighter-rouge">replacements.yml</code> file. Edit this file to add your custom regex replacements.</p>

<p>The file uses YAML format with a <code class="language-plaintext highlighter-rouge">replacements:</code> array. Each replacement has:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">name</code>: A descriptive name for the replacement</li>
  <li><code class="language-plaintext highlighter-rouge">pattern</code>: A regex pattern to match</li>
  <li><code class="language-plaintext highlighter-rouge">replacement</code>: The replacement text (can use regex groups like <code class="language-plaintext highlighter-rouge">$1</code>, <code class="language-plaintext highlighter-rouge">$2</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">timing</code>: When to apply (<code class="language-plaintext highlighter-rouge">before</code> or <code class="language-plaintext highlighter-rouge">after</code> other processing)</li>
  <li><code class="language-plaintext highlighter-rouge">in_code_blocks</code>: Whether to apply inside code blocks (<code class="language-plaintext highlighter-rouge">true</code>/<code class="language-plaintext highlighter-rouge">false</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">in_frontmatter</code>: Whether to apply in frontmatter (<code class="language-plaintext highlighter-rouge">true</code>/<code class="language-plaintext highlighter-rouge">false</code>)</li>
</ul>

<p>Here&rsquo;s an example:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">replacements</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">normalize-http"</span>
    <span class="na">pattern</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http://"</span>
    <span class="na">replacement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://"</span>
    <span class="na">timing</span><span class="pi">:</span> <span class="s">after</span>
    <span class="na">in_code_blocks</span><span class="pi">:</span> <span class="kc">false</span>
    <span class="na">in_frontmatter</span><span class="pi">:</span> <span class="kc">false</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">fix-double-spaces"</span>
    <span class="na">pattern</span><span class="pi">:</span> <span class="s2">"</span><span class="nv">  </span><span class="s">+"</span>
    <span class="na">replacement</span><span class="pi">:</span> <span class="s2">"</span><span class="nv"> </span><span class="s">"</span>
    <span class="na">timing</span><span class="pi">:</span> <span class="s">after</span>
    <span class="na">in_code_blocks</span><span class="pi">:</span> <span class="kc">false</span>
    <span class="na">in_frontmatter</span><span class="pi">:</span> <span class="kc">false</span></code></pre></div></div>

<h2 id="running-with-plugins">Running with plugins</h2>

<p>To use the plugins on a file, you can enable it globally, or run them for a specific conversion:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--plugins</span> file.md</code></pre></div></div>

<p>The plugin runs automatically if you&rsquo;ve enabled plugins globally.</p>

<p>The plugin is hosted at <a href="https://github.com/ApexMarkdown/apex-plugin-md-fixup">https://github.com/ApexMarkdown/apex-plugin-md-fixup</a> if you want to check out the code or contribute improvements.</p>

<p>And if you haven&rsquo;t, <a href="https://github.com/ApexMarkdown/apex">check out Apex</a>.</p>

<p>Quick side note: it&rsquo;s been pointed out that md-fixup makes a great
Marked preprocessor. Marked 3 adds enough search and replace
capabilities that it might not be necessary, but for Marked 2, or if you
prefer to edit all your replacements in a dedicated YAML file, you can
just set <code class="language-plaintext highlighter-rouge">md-fixup</code> as your Custom Proprocessor.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115923183050762555">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Markdown+Fixup+plugin+for+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F19%2Fmarkdown-fixup-plugin-for-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F19%2Fmarkdown-fixup-plugin-for-apex%2F&text=Markdown+Fixup+plugin+for+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F19%2Fmarkdown-fixup-plugin-for-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17256661.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/mdfixup-apex-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/mdfixup-apex-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Markdown to Sendy with Image Stacks and File Uploads]]></title>
    <link href="https://brett.trpstra.net/link/535/17255313/markdown-to-sendy-with-image-stacks-and-file-uploads"/>
    <updated>2026-01-17T06:22:00-06:00</updated>
    <published>2026-01-17T06:22:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/17/markdown-to-sendy-with-image-stacks-and-file-uploads</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/mdtosendy-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I&rsquo;ve updated my Markdown to Sendy script with the ability to use &ldquo;sliced&rdquo; images with separate links, the ability to upload assets to a CDN automatically, and a &ldquo;test email&rdquo; mode that will actually send a test email to you without going through Sendy.</p>

<p>If you haven&rsquo;t heard of <a href="https://github.com/ttscoff/mdtosendy">mdtosendy</a> before, it&rsquo;s a Ruby script that converts Markdown files into email-ready HTML with automatic inline styling for maximum email client compatibility. While it can generate emails for any newsletter or email platform, it integrates best with Sendy, automatically creating and scheduling campaigns. You write your emails in Markdown, maintain your styles in CSS files, and mdtosendy handles all the messy email HTML details. And it can handle multiple templates, so you can have different styles for the same newsletter, and/or configure multiple instances with different styles/settings.</p>

<p>The latest version adds some powerful new features that make it even more useful for email workflows.</p>

<h3 id="test-email-sending">Test Email Sending</h3>

<p>The HTML preview mode is useful, but nothing beats being able to actually view the email in an email app so you can see exactly how it will render. Now you can send test emails directly without going through Sendy. The new <code class="language-plaintext highlighter-rouge">--test-send</code> option does exactly that:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>mdtosendy <span class="nt">--test-send</span> your-email@example.com your-email.md</code></pre></div></div>

<p>This sends a test email directly via SMTP, bypassing Sendy entirely. Perfect for quick previews or testing your email design before creating a campaign. You&rsquo;ll need to configure SMTP settings in your <code class="language-plaintext highlighter-rouge">config.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">smtp</span><span class="pi">:</span>
  <span class="na">host</span><span class="pi">:</span> <span class="s2">"</span><span class="s">smtp.gmail.com"</span>
  <span class="na">port</span><span class="pi">:</span> <span class="m">587</span>
  <span class="na">domain</span><span class="pi">:</span> <span class="s2">"</span><span class="s">gmail.com"</span>
  <span class="na">user</span><span class="pi">:</span> <span class="s2">"</span><span class="s">me@example.com"</span>
  <span class="na">password</span><span class="pi">:</span> <span class="s2">"</span><span class="s">your-app-password-here"</span>
  <span class="na">auth</span><span class="pi">:</span> <span class="s2">"</span><span class="s">plain"</span>
  <span class="na">starttls</span><span class="pi">:</span> <span class="kc">true</span></code></pre></div></div>

<p>For Gmail, you&rsquo;ll need to generate an App Password (not your regular password) at https://myaccount.google.com/apppasswords. Most other SMTP providers follow a similar pattern.</p>

<h3 id="cdn-image-upload-with-overwrite-control">CDN Image Upload with Overwrite Control</h3>

<p>The CDN image upload feature is a significant enhancement. You can add a <code class="language-plaintext highlighter-rouge">cdn</code> section to config (which can be inherited from the main config, or set per template), and when <code class="language-plaintext highlighter-rouge">mdtosendy</code> detects a local file path, it will be uploaded to the CDN and the url in the output HTML will be updated accordingly.</p>

<p>CDN uploads work with S3, SCP, or SFTP. See the example config in the repo for details.</p>

<p>You can configure how <code class="language-plaintext highlighter-rouge">mdtosendy</code> handles existing files on your CDN with the <code class="language-plaintext highlighter-rouge">overwrite</code> option:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">cdn</span><span class="pi">:</span>
  <span class="na">url</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://cdn.example.com"</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s2">"</span><span class="s">s3"</span>
  <span class="na">username</span><span class="pi">:</span> <span class="s2">"</span><span class="s">your-access-key-id"</span>
  <span class="na">password</span><span class="pi">:</span> <span class="s2">"</span><span class="s">your-secret-access-key"</span>
  <span class="na">path</span><span class="pi">:</span> <span class="s2">"</span><span class="s">your-bucket-name"</span>
  <span class="na">overwrite</span><span class="pi">:</span> <span class="s">ask</span>  <span class="c1"># Options: true, false, or ask/prompt</span></code></pre></div></div>

<p>By default, mdtosendy uses original filenames (no timestamps) and prompts you when a file already exists. The prompt defaults to &ldquo;Yes&rdquo;, so you can just press Enter or type <code class="language-plaintext highlighter-rouge">y</code> to overwrite, or <code class="language-plaintext highlighter-rouge">n</code> to skip. If you choose not to overwrite, a timestamp will be added to the filename to avoid conflicts.</p>

<p>The <code class="language-plaintext highlighter-rouge">overwrite</code> setting supports three modes:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">true</code> - Always overwrite without prompting</li>
  <li><code class="language-plaintext highlighter-rouge">false</code> - Never overwrite, automatically add timestamps when files exist</li>
  <li><code class="language-plaintext highlighter-rouge">ask</code> or <code class="language-plaintext highlighter-rouge">prompt</code> - Prompt for confirmation (default behavior)</li>
</ul>

<p>This works with all three CDN types: S3, SCP, and SFTP. The file existence checking is smart enough to detect existing files before uploading.</p>

<h3 id="image-stack-tag">Image Stack Tag</h3>

<p>The new <code class="language-plaintext highlighter-rouge">{% stack %}</code> tag is perfect for creating multi-part images that need to be displayed as a seamless vertical stack. This is especially useful for sliced images or creating visual sections in your emails.</p>

<p>You can use it with markdown image syntax:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% stack %}
<span class="p">[</span><span class="nv">![</span><span class="p">](</span><span class="sx">/images/image1.png</span><span class="p">)</span>](https://example.com/link1)
<span class="p">[</span><span class="nv">![</span><span class="p">](</span><span class="sx">/images/image2.png</span><span class="p">)</span>](https://example.com/link2)
<span class="p">[</span><span class="nv">![</span><span class="p">](</span><span class="sx">/images/image3.png</span><span class="p">)</span>](https://example.com/link3)
{% endstack %}</code></pre></div></div>

<p>Or with YAML for more structured definitions:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% stack type="yaml" %}
images:
<span class="p">  -</span> path: /images/image1.png
    url: https://example.com/link1
<span class="p">  -</span> path: /images/image2.png
    url: https://example.com/link2
<span class="p">  -</span> path: /images/image3.png
{% endstack %}</code></pre></div></div>

<p>The stack tag creates a vertical stack of images with zero spacing between them, all full width of the content area. Each image can optionally have a link. Local images in stacks are automatically uploaded to your CDN if you have it configured, and the tag uses a table-based layout for maximum email client compatibility.</p>

<h3 id="demo-please">Demo Please!</h3>

<p>Here&rsquo;s an email generated from the <code class="language-plaintext highlighter-rouge">demo.md</code> file in the repository,
using my own <code class="language-plaintext highlighter-rouge">marked</code> template, which uploads to my cdn.marked2app.com
bucket on S3. The demo shows a single image uploaded to the CDN from a
local file path, as well as 3 separate images combined into one stack,
with 3 separate links.</p>

<p><a href="https://stuff.brettterpstra.com/demo-2.html" class="btn--big" target="_blank">Demo email</a></p>

<h3 id="getting-started">Getting Started</h3>

<p>If you&rsquo;re new to <code class="language-plaintext highlighter-rouge">mdtosendy</code>, installation is a one-liner:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>curl <span class="nt">-s</span> https://github.com/ttscoff/mdtosendy/raw/main/bootstrap.sh | bash</code></pre></div></div>

<p>For more details on all the features, configuration options, and examples, check out the <a href="https://github.com/ttscoff/mdtosendy">mdtosendy repository on GitHub</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115910698917028512">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Markdown+to+Sendy+with+Image+Stacks+and+File+Uploads%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F17%2Fmarkdown-to-sendy-with-image-stacks-and-file-uploads%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F17%2Fmarkdown-to-sendy-with-image-stacks-and-file-uploads%2F&text=Markdown+to+Sendy+with+Image+Stacks+and+File+Uploads&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F17%2Fmarkdown-to-sendy-with-image-stacks-and-file-uploads%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17255313.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/mdtosendy-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/mdtosendy-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Marked features for Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17254761/marked-features-for-apex"/>
    <updated>2026-01-16T08:00:00-06:00</updated>
    <published>2026-01-16T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/16/marked-features-for-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>When <a href="https://github.com/ApexMarkdown/apex">Apex</a> reaches 1.0, I&rsquo;m planning to include it in Marked 3. I realized that Marked has a lot of preprocessing
features that were previously handled in Objective-C that
would make sense to have in the core processor for both
speed and accessibility from the command line.</p>

<p>So I&rsquo;ve added a bunch of new flags and C API definitions to
Apex that bring some of Marked&rsquo;s capabilities directly into
the processor. These are all available via command-line
flags, configuration options, and the C API.</p>

<h2 id="hashtags">Hashtags</h2>

<p>The <code class="language-plaintext highlighter-rouge">--hashtags</code> flag converts <code class="language-plaintext highlighter-rouge">#tags</code> in your Markdown into
span-wrapped hashtags. By default, it uses the <code class="language-plaintext highlighter-rouge">mkhashtag</code>
class, but you can use <code class="language-plaintext highlighter-rouge">--style-hashtags</code> to use
<code class="language-plaintext highlighter-rouge">mkstyledtag</code> instead.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex document.md <span class="nt">--hashtags</span>
apex document.md <span class="nt">--hashtags</span> <span class="nt">--style-hashtags</span></code></pre></div></div>

<p>This is smart enough to skip hashtags inside code blocks and
HTML attributes, so you won&rsquo;t get false matches in things
like <code class="language-plaintext highlighter-rouge">href="https://brettterpstra.com#anchor"</code> or code examples.</p>

<p>Hashtags are disabled by default because they would conflict
with headers if you&rsquo;re not in the habit of putting a space
after the <code class="language-plaintext highlighter-rouge">#</code> in an ATX header (e.g. <code class="language-plaintext highlighter-rouge">#Header 1</code>). This
option is only for people who want to convert things like
Bear notes with tag formatting. They can be enabled with
<code class="language-plaintext highlighter-rouge">--hashtags</code> on the command line, by including <code class="language-plaintext highlighter-rouge">hashtags: true</code>
in a config file, or by using the <code class="language-plaintext highlighter-rouge">enable_hashtags</code>
boolean when using the C API.</p>

<h2 id="random-footnote-ids">Random Footnote IDs</h2>

<p>When you&rsquo;re combining multiple documents, footnote ID
collisions can be a problem. The <code class="language-plaintext highlighter-rouge">--random-footnote-ids</code>
flag generates hash-based footnote IDs using an 8-character
hex prefix from the document content.</p>

<p>Instead of <code class="language-plaintext highlighter-rouge">fn-1</code> and <code class="language-plaintext highlighter-rouge">fnref-1</code>, you&rsquo;ll get <code class="language-plaintext highlighter-rouge">fn-a7b3c9d2-1</code>
and <code class="language-plaintext highlighter-rouge">fnref-a7b3c9d2-1</code>. Different documents get different
hash prefixes, so you can safely combine them without
conflicts.</p>

<h2 id="widont-for-headings">Widon&rsquo;t for Headings</h2>

<p>The <code class="language-plaintext highlighter-rouge">--widont</code> flag prevents short widows in headings by
inserting non-breaking spaces between trailing words. It
works backwards from the end of the heading, combining words
until the trailing portion exceeds 10 characters.</p>

<p>So a heading like &ldquo;introduction to the topic&rdquo; becomes
<code class="language-plaintext highlighter-rouge">introduction to&amp;nbsp;the&amp;nbsp;topic</code> &mdash; ensuring that if
the heading wraps, the trailing portion won&rsquo;t be a short,
lonely word on its own line.</p>

<p>Widon&rsquo;t is disabled by default in all modes, as it might
create potentially unexpected results if the user isn&rsquo;t
aware of it. It can be explicitly enabld with <code class="language-plaintext highlighter-rouge">--widont</code>,
<code class="language-plaintext highlighter-rouge">widont: true</code> in config, or with the <code class="language-plaintext highlighter-rouge">enable_widont</code>
boolean in the C API.</p>

<h2 id="code-is-poetry">Code is Poetry</h2>

<p>Code blocks without a programming language specified can be
treated as poetry with the <code class="language-plaintext highlighter-rouge">--code-is-poetry</code> flag. This
adds a <code class="language-plaintext highlighter-rouge">poetry</code> class to code blocks that don&rsquo;t have a
language specified, and automatically enables
<code class="language-plaintext highlighter-rouge">--highlight-language-only</code> so only code blocks with
languages get syntax highlighting.</p>

<p>Works for both fenced code blocks and indented code blocks.</p>

<p>Again, this is disabled by default as it has very specific
use cases.</p>

<h2 id="proofreader-mode">Proofreader Mode</h2>

<p>The <code class="language-plaintext highlighter-rouge">--proofreader</code> flag converts <code class="language-plaintext highlighter-rouge">==highlight==</code> and
<code class="language-plaintext highlighter-rouge">~~delete~~</code> syntax into CriticMarkup highlight and
deletion. It automatically enables CriticMarkup processing,
so you can use this simpler syntax and still get the full
CriticMarkup rendering.</p>

<h2 id="markdown-in-html-toggle">Markdown in HTML Toggle</h2>

<p>Marked has always processed markdown inside HTML blocks with
<code class="language-plaintext highlighter-rouge">markdown</code> attributes. Now you can control this behavior
with <code class="language-plaintext highlighter-rouge">--markdown-in-html</code> and <code class="language-plaintext highlighter-rouge">--no-markdown-in-html</code>. It&rsquo;s
enabled by default in unified mode, but you can toggle it
for other modes or when you need stricter behavior.</p>

<h2 id="page-breaks">Page Breaks</h2>

<p>Two new flags for page break handling:</p>

<ul>
  <li>
    <p><code class="language-plaintext highlighter-rouge">--hr-page-break</code> replaces <code class="language-plaintext highlighter-rouge">&lt;hr&gt;</code> elements with
Marked-style page break divs</p>
  </li>
  <li>
    <p><code class="language-plaintext highlighter-rouge">--page-break-before-footnotes</code> inserts a page break
before the footnotes section</p>
  </li>
</ul>

<p>Both use Marked&rsquo;s standard page break format with proper
attributes for styling and identification.</p>

<h2 id="title-from-h1">Title from H1</h2>

<p>When using <code class="language-plaintext highlighter-rouge">--standalone</code>, the <code class="language-plaintext highlighter-rouge">--title-from-h1</code> flag
extracts the text from the first H1 heading and uses it as
the document title if no title is specified via <code class="language-plaintext highlighter-rouge">--title</code> or
metadata. The H1 stays in the document body, but now it&rsquo;s
also in the <code class="language-plaintext highlighter-rouge">&lt;title&gt;</code> tag.</p>

<h2 id="all-available-everywhere">All Available Everywhere</h2>

<p>All of these features are available through:</p>

<ul>
  <li>Command-line flags (as shown above)</li>
  <li>Configuration files (<code class="language-plaintext highlighter-rouge">config.yml</code>, <code class="language-plaintext highlighter-rouge">--meta-file</code>)</li>
  <li>
    <p>Document metadata (YAML front matter, MultiMarkdown
metadata)</p>
  </li>
  <li>The C API (via the <code class="language-plaintext highlighter-rouge">apex_options</code> struct)</li>
</ul>

<p>This means you can set defaults in your config file,
override per-document in metadata, or use command-line flags
for one-off processing. The flexibility is there.</p>

<h2 id="why-build-these-into-apex">Why Build These Into Apex?</h2>

<p>These features were originally preprocessing steps in
Marked&rsquo;s Objective-C code. Moving them into Apex provides:</p>

<ol>
  <li>
    <p><strong>Speed</strong> &mdash; C-based processing is faster than
Objective-C string manipulation</p>
  </li>
  <li>
    <p><strong>Accessibility</strong> &mdash; Available from the command line
without needing Marked</p>
  </li>
  <li>
    <p><strong>Consistency</strong> &mdash; Same behavior whether called from
Marked or directly</p>
  </li>
  <li>
    <p><strong>Extensibility</strong> &mdash; Other tools can use these features
via the C API</p>
  </li>
</ol>

<p>As Apex approaches 1.0, these features will make the
integration with Marked a little easier while also making
some of Marked&rsquo;s capabilities available to anyone using Apex
directly. I know none of these recent changes are killer features for most people, so this is just serving as documentation of the development process.</p>

<p>As always, as Apex develops, I&rsquo;m very interested in what features you&rsquo;d like to see. The goal is to have one universal processor that, at least for HTML output<sup id="fnref:pandoc"><a href="https://brettterpstra.com#fn:pandoc" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>, can do everything that other Markdown flavors can do. If you have ideas or requests, or want to contribute to the development, please <a href="https://github.com/ApexMarkdown/apex">join me on GitHub</a>!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:pandoc">
      <p>I&rsquo;m not going to try to replicate all of Pandoc&rsquo;s powerful conversion features, as you can just pipe Apex HTML output into Pandoc for easy conversion to PDF, DOCX, etc. I&rsquo;m just focusing on making as many extensions for HTML output as possible work.&nbsp;<a href="https://brettterpstra.com#fnref:pandoc" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115905169266877317">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Marked+features+for+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F16%2Fmarked-features-for-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F16%2Fmarked-features-for-apex%2F&text=Marked+features+for+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F16%2Fmarked-features-for-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17254761.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Git-based changelogs for Rust projects and more]]></title>
    <link href="https://brett.trpstra.net/link/535/17253954/git-based-changelogs-for-rust-projects-and-more"/>
    <updated>2026-01-15T08:00:00-06:00</updated>
    <published>2026-01-15T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/15/git-based-changelogs-for-rust-projects-and-more</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/changelog-header-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I&rsquo;ve been using my <a href="https://github.com/ttscoff/changelog">changelog</a> script for years to generate release notes from git commit messages. It&rsquo;s saved me countless hours and helped me maintain complete, informative changelogs across all my projects.</p>

<p>I wrote <code class="language-plaintext highlighter-rouge">changelog</code> <a href="https://brettterpstra.com/2017/08/14/automatic-release-notes-from-git-commit-messages/">in 2017</a> and mentioned it <a href="https://brettterpstra.com/2024/08/31/updated-generate-slick-changelogs-from-git-commits/">back in 2024</a>. I&rsquo;ve used it for every project I&rsquo;ve worked on since then, and I&rsquo;ve been improving it and making it work with more and more project types over the years.</p>

<h3 id="rust-project-support">Rust project support</h3>

<p>The latest update adds support for Rust projects, which I&rsquo;m just dipping my toes into. The script now automatically detects Rust projects in two ways:</p>

<ol>
  <li>
    <p><strong>Cargo.toml detection</strong>: If your project has a <code class="language-plaintext highlighter-rouge">Cargo.toml</code> file, it reads the <code class="language-plaintext highlighter-rouge">name</code> and <code class="language-plaintext highlighter-rouge">version</code> fields directly from it.</p>
  </li>
  <li>
    <p><strong>Rust source file scanning</strong>: For projects without a <code class="language-plaintext highlighter-rouge">Cargo.toml</code> (or as a fallback), it scans <code class="language-plaintext highlighter-rouge">.rs</code> files looking for version constants like <code class="language-plaintext highlighter-rouge">const VERSION: &amp;str = "1.0.0"</code> and command names from <code class="language-plaintext highlighter-rouge">Command::new("appname")</code>.</p>
  </li>
</ol>

<p>This means you can use the changelog script with any Rust project just by running <code class="language-plaintext highlighter-rouge">changelog</code> or <code class="language-plaintext highlighter-rouge">changelog -u</code> to update your <code class="language-plaintext highlighter-rouge">CHANGELOG.md</code> file, and it will automatically pull the version from your <code class="language-plaintext highlighter-rouge">Cargo.toml</code> or source files.</p>

<h3 id="other-improvements">Other improvements</h3>

<p>Along with Rust support, I&rsquo;ve made a few other improvements:</p>

<ul>
  <li>
    <p><strong>VERSION constant</strong>: The script now includes its own <code class="language-plaintext highlighter-rouge">VERSION</code> constant that stays in sync with the <code class="language-plaintext highlighter-rouge">VERSION</code> file, making it easier to track the script&rsquo;s version.</p>
  </li>
  <li>
    <p><strong>Code refactoring</strong>: I&rsquo;ve extracted the version detection, git log parsing, and formatting logic into separate modules (<code class="language-plaintext highlighter-rouge">VersionDetector</code>, <code class="language-plaintext highlighter-rouge">GitLogParser</code>, and <code class="language-plaintext highlighter-rouge">ChangelogFormatter</code>) to make the codebase more maintainable.</p>
  </li>
  <li>
    <p><strong>Better version detection</strong>: The script now has a more robust fallback chain for detecting versions, with Rust projects taking priority when detected, followed by Ruby gems, Xcode projects, and plain <code class="language-plaintext highlighter-rouge">VERSION</code> files.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--since-version</code> flag</strong>: You can now generate changelogs starting from a specific version tag using <code class="language-plaintext highlighter-rouge">--since-version</code> (or <code class="language-plaintext highlighter-rouge">--sv</code> for short). This is useful when you want to see changes since a particular release, and it supports partial version matching. For example, <code class="language-plaintext highlighter-rouge">changelog --since-version 1.0</code> will find the most recent tag starting with &ldquo;1.0&rdquo; and generate a changelog from that point.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--select</code> flag</strong>: If you prefer an interactive approach, use <code class="language-plaintext highlighter-rouge">--select</code> (or <code class="language-plaintext highlighter-rouge">-s</code>) to pop up an <code class="language-plaintext highlighter-rouge">fzf</code> menu of all your git tags. This lets you visually choose which tag to use as the starting point for your changelog generation. Perfect for when you can&rsquo;t remember the exact version number but know roughly when you want to start from.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--only</code> flag</strong>: Filter changelog output to show only specific change types. Use it like <code class="language-plaintext highlighter-rouge">changelog --only new,fixed</code> to see only new features and bug fixes, or <code class="language-plaintext highlighter-rouge">changelog --only changed</code> to see only breaking changes and modifications.</p>
  </li>
  <li>
    <p><strong><code class="language-plaintext highlighter-rouge">--split</code> flag</strong>: When used with <code class="language-plaintext highlighter-rouge">--select</code> or <code class="language-plaintext highlighter-rouge">--since-version</code>, this splits the changelog output by version tag, showing what changed in each release separately. This is great for generating comprehensive release notes that span multiple versions. You can control the order with <code class="language-plaintext highlighter-rouge">--order asc</code> (oldest first) or <code class="language-plaintext highlighter-rouge">--order desc</code> (newest first, default).</p>
  </li>
</ul>

<h3 id="how-it-works">How it works</h3>

<p>The script works exactly the same way it always has &mdash; you format your commit messages with prefixes like <code class="language-plaintext highlighter-rouge">NEW:</code> or <code class="language-plaintext highlighter-rouge">FIXED:</code>, or tags like <code class="language-plaintext highlighter-rouge">@new</code>, <code class="language-plaintext highlighter-rouge">@fixed</code>, <code class="language-plaintext highlighter-rouge">@changed</code>, etc., and it generates a changelog from commits since your last git tag. The only difference now is that it works seamlessly with Rust projects without any additional configuration.</p>

<p>The script is still designed to fit my personal workflow (including some nvUltra, Marked, and Bunch-specific formatting), but it&rsquo;s open for anyone to hack on and adapt to their needs. All of my project-specific paths are gated by exact directory structures and won&rsquo;t affect operation by anyone else.</p>

<p>If you&rsquo;re working with a Rust, Xcode, Ruby, or any script project and want to automate your changelog generation, <a href="https://github.com/ttscoff/changelog">give it a try</a>. It makes it so easy to build a changelog and release notes as you develop, and have them ready to go when you hit release time.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115899507184402063">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Git-based+changelogs+for+Rust+projects+and+more%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F15%2Fgit-based-changelogs-for-rust-projects-and-more%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F15%2Fgit-based-changelogs-for-rust-projects-and-more%2F&text=Git-based+changelogs+for+Rust+projects+and+more&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F15%2Fgit-based-changelogs-for-rust-projects-and-more%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17253954.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/changelog-header-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/changelog-header-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Regex Replacements for Markdown Fixup]]></title>
    <link href="https://brett.trpstra.net/link/535/17253235/regex-replacements-for-markdown-fixup"/>
    <updated>2026-01-14T08:00:00-06:00</updated>
    <published>2026-01-14T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/14/regex-replacements-for-markdown-fixup</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p><a href="https://www.barebones.com/products/bbedit">BBEdit</a> has a cool feature called
<a href="https://www.barebones.com/products/bbedit/benefitsexercise.html">Text Factories</a>
for automating repetitive text transformations. When
Younghart
<a href="https://forum.brettterpstra.com/t/a-quick-markdown-fixup-update/4418/2">mentioned it on the forum</a>,
it got me thinking.</p>

<p>While I can&rsquo;t replicate the full power of Text Factories
(which can chain multiple transformations, with all of
BBEdit&rsquo;s power), I could at least add a flexible regex
search-and-replace system to md-fixup.</p>

<p>So that&rsquo;s what I built: a YAML-driven regex replacement
engine that lets you define custom patterns that run as part
of an <a href="https://github.com/ttscoff/md-fixup"><code class="language-plaintext highlighter-rouge">md-fixup</code></a> pass. It&rsquo;s not a replacement for Text
Factories &mdash; it only does regex search and replace &mdash; but
it offers a way to extend <code class="language-plaintext highlighter-rouge">md-fixup</code> with your own
transformations.</p>

<h2 id="how-it-works">How It Works</h2>

<p>Replacements are defined in a YAML file and can be scoped to
run before or after Markdown Fixup&rsquo;s built-in rules. Each
replacement can optionally run inside code blocks or YAML
frontmatter, giving you fine-grained control over where
transformations happen.</p>

<p>The replacement file lives in one of these locations
(checked in order):</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">.md-fixup-replacements</code> in your current directory</li>
  <li>
    <p>The path specified in your config file&rsquo;s
<code class="language-plaintext highlighter-rouge">replacements_file:</code> key</p>
  </li>
  <li><code class="language-plaintext highlighter-rouge">~/.config/md-fixup/replacements.yml</code> (or
<code class="language-plaintext highlighter-rouge">$XDG_CONFIG_HOME/md-fixup/replacements.yml</code>)</li>
</ul>

<h2 id="setting-up-replacements">Setting Up Replacements</h2>

<p>Here&rsquo;s a simple example that fixes double spaces and
normalizes HTTP links:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">replacements</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">fix-double-spaces"</span>
    <span class="na">pattern</span><span class="pi">:</span> <span class="s2">"</span><span class="nv">  </span><span class="s">+"</span>
    <span class="na">replacement</span><span class="pi">:</span> <span class="s2">"</span><span class="nv"> </span><span class="s">"</span>
    <span class="na">timing</span><span class="pi">:</span> <span class="s">after</span>
    <span class="na">in_code_blocks</span><span class="pi">:</span> <span class="kc">false</span>
    <span class="na">in_frontmatter</span><span class="pi">:</span> <span class="kc">false</span>

  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">normalize-http"</span>
    <span class="na">pattern</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http://"</span>
    <span class="na">replacement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://"</span>
    <span class="na">timing</span><span class="pi">:</span> <span class="s">after</span></code></pre></div></div>

<p>Each replacement has a few key properties:</p>

<ul>
  <li>
    <p><strong>name</strong>: A human-readable identifier (useful for
debugging)</p>
  </li>
  <li>
    <p><strong>pattern</strong>: A Rust <code class="language-plaintext highlighter-rouge">regex</code> pattern (supports capture
groups)</p>
  </li>
  <li>
    <p><strong>replacement</strong>: The replacement string (use <code class="language-plaintext highlighter-rouge">$1</code>, <code class="language-plaintext highlighter-rouge">$2</code>,
etc. for capture groups)</p>
  </li>
  <li>
    <p><strong>timing</strong>: When to run&mdash;<code class="language-plaintext highlighter-rouge">before</code> or <code class="language-plaintext highlighter-rouge">after</code> the built-in
rules</p>
  </li>
  <li>
    <p><strong>in_code_blocks</strong>: If <code class="language-plaintext highlighter-rouge">true</code>, the pattern runs inside
fenced code blocks</p>
  </li>
  <li>
    <p><strong>in_frontmatter</strong>: If <code class="language-plaintext highlighter-rouge">true</code>, the pattern runs inside
YAML frontmatter</p>
  </li>
</ul>

<h2 id="a-more-complex-example">A More Complex Example</h2>

<p>Here&rsquo;s one that swaps version numbers using capture groups:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">replacements</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">swap-version"</span>
    <span class="na">pattern</span><span class="pi">:</span> <span class="s2">"</span><span class="s">(</span><span class="se">\\</span><span class="s">d+)</span><span class="se">\\</span><span class="s">.(</span><span class="se">\\</span><span class="s">d+)"</span>
    <span class="na">replacement</span><span class="pi">:</span> <span class="s2">"</span><span class="s">$2.$1"</span>
    <span class="na">timing</span><span class="pi">:</span> <span class="s">before</span></code></pre></div></div>

<p>This would turn &ldquo;Version 1.2 is released&rdquo; into &ldquo;Version 2.1
is released&rdquo;. The pattern captures two groups of digits
separated by a dot, then swaps them in the replacement.
Stupid example, of course (why would you ever do that?), but
hopefully you get the idea.</p>

<h2 id="controlling-replacements">Controlling Replacements</h2>

<p>You can enable or disable replacements via your config file:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">width</span><span class="pi">:</span> <span class="m">80</span>
<span class="na">replacements</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">replacements_file</span><span class="pi">:</span> <span class="s">~/my-replacements.yml</span>
<span class="na">rules</span><span class="pi">:</span>
  <span class="na">skip</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">wrap</span></code></pre></div></div>

<p>Or use command-line flags:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c"># Force enable</span>
md-fixup <span class="nt">--replacements</span> file.md

<span class="c"># Force disable</span>
md-fixup <span class="nt">--no-replacements</span> file.md

<span class="c"># Use a specific file</span>
md-fixup <span class="nt">--replacement-file</span> ./custom.yml file.md</code></pre></div></div>

<p>This regex replacement system is intentionally limited, and
as mentioned, is no replacement for Text Factories. It only
does regex search and replace, but it runs automatically as
part of md-fixup&rsquo;s fixup pass, which means you can integrate
custom transformations into your existing workflow without
switching tools.</p>

<p>If you need the full power of Text Factories, by all means
use <a href="https://www.barebones.com/products/bbedit">BBEdit</a>. It&rsquo;s an amazing text editor. But if you just
need a few regex replacements to run alongside Markdown
Fixup&rsquo;s built-in rules, this might be exactly what you&rsquo;re
looking for.</p>

<p>Check out the latest version of <a href="https://github.com/ttscoff/md-fixup">Markdown Fixup on GitHub</a>.
Find installation and usage instructions there!</p>


<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115893841894623108">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Regex+Replacements+for+Markdown+Fixup%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F14%2Fregex-replacements-for-markdown-fixup%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F14%2Fregex-replacements-for-markdown-fixup%2F&text=Regex+Replacements+for+Markdown+Fixup&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F14%2Fregex-replacements-for-markdown-fixup%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17253235.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Built-in Syntax Highlighting for Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17252486/built-in-syntax-highlighting-for-apex"/>
    <updated>2026-01-13T08:42:00-06:00</updated>
    <published>2026-01-13T08:42:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/13/built-in-syntax-highlighting-for-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>I think a lot of people using Apex are going to want syntax
highlighting of code blocks. Including a script like
Highlight.js in your HTML output is <em>fine</em>, but I wanted
Apex to be able to directly output HTML with the necessary
spans and tables for highlighting. So, introducing the
<code class="language-plaintext highlighter-rouge">--code-highlight</code> flag.</p>

<h3 id="how-it-works">How It Works</h3>

<p>Rather than bundling a syntax highlighting engine (which
would bloat the binary and require constant updates for new
languages), I decided to leverage external tools that you
probably already have installed.</p>

<p>The new <code class="language-plaintext highlighter-rouge">--code-highlight</code> flag accepts either <code class="language-plaintext highlighter-rouge">pygments</code> or
<code class="language-plaintext highlighter-rouge">skylighting</code> as arguments:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c"># Using Pygments (Python-based)</span>
apex <span class="nt">--code-highlight</span> pygments input.md

<span class="c"># Using Skylighting (Haskell-based, used by Pandoc)</span>
apex <span class="nt">--code-highlight</span> skylighting input.md

<span class="c"># Short forms work too</span>
apex <span class="nt">--code-highlight</span> p input.md   <span class="c"># pygments</span>
apex <span class="nt">--code-highlight</span> s input.md   <span class="c"># skylighting</span></code></pre></div></div>

<p>When you specify a highlighter, Apex processes your Markdown
normally, then makes a second pass over the HTML output. It
finds all <code class="language-plaintext highlighter-rouge">&lt;pre&gt;&lt;code&gt;</code> blocks, extracts the raw code
content, pipes it through the external tool, and replaces
the original block with the colorized HTML output.</p>

<h3 id="language-detection">Language Detection</h3>

<p>Apex handles language specification in the standard way
you&rsquo;d expect. Just add the language identifier after the
opening fence:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">```</span><span class="nl">python
</span>
<span class="k">def</span> <span class="nf">hello</span><span class="p">():</span>
    <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Hello, world!</span><span class="sh">"</span><span class="p">)</span>

<span class="p">```</span></code></pre></div></div>

<p>The language is extracted from either the
<code class="language-plaintext highlighter-rouge">class="language-XXX"</code> attribute on the code tag or the
<code class="language-plaintext highlighter-rouge">lang="XXX"</code> attribute on the pre tag (depending on how
cmark-gfm formatted it). This language is then passed to the
external highlighter.</p>

<p>If you don&rsquo;t specify a language, both Pygments and
Skylighting will attempt auto-detection. Pygments is
particularly good at this thanks to its <code class="language-plaintext highlighter-rouge">-g</code> (guess) flag.
Skylighting will try its best but may fall back to plain
text for ambiguous code.</p>

<h3 id="line-numbers">Line Numbers</h3>

<p>Sometimes you want line numbers in your code blocks,
especially for tutorials or when referencing specific lines.
The new <code class="language-plaintext highlighter-rouge">--code-line-numbers</code> flag has you covered:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--code-highlight</span> pygments <span class="nt">--code-line-numbers</span> input.md</code></pre></div></div>

<p>This passes the appropriate options to the highlighter
(Pygments gets <code class="language-plaintext highlighter-rouge">linenos=1</code>, Skylighting gets the <code class="language-plaintext highlighter-rouge">-n</code> flag).
The result is nicely numbered code that&rsquo;s easy to reference.</p>

<h3 id="automatic-styling-in-standalone-mode">Automatic Styling in Standalone Mode</h3>

<p>When you use <code class="language-plaintext highlighter-rouge">--code-highlight</code> with <code class="language-plaintext highlighter-rouge">--standalone</code> and
don&rsquo;t specify a <code class="language-plaintext highlighter-rouge">--css FILE</code> option, Apex automatically
embeds GitHub-style syntax highlighting CSS in the document
head. This covers all the common class names used by both
Pygments and Skylighting, so your highlighted code looks
great out of the box.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex <span class="nt">--standalone</span> <span class="nt">--code-highlight</span> pygments input.md <span class="o">&gt;</span> output.html</code></pre></div></div>

<p>The embedded CSS uses a clean, GitHub-inspired color scheme
that works well with light backgrounds. Keywords are red,
strings are blue, comments are gray, and so on. If you want
different colors, you can always override with your own
stylesheet using the <code class="language-plaintext highlighter-rouge">--css</code> flag.</p>

<h3 id="multiple-stylesheets">Multiple Stylesheets</h3>

<p>Speaking of stylesheets, another improvement today is that
<code class="language-plaintext highlighter-rouge">--css</code> now accepts multiple files. You can either use the
flag multiple times or pass a comma-separated list:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c"># Multiple flags</span>
apex <span class="nt">--standalone</span> <span class="nt">--css</span> base.css <span class="nt">--css</span> syntax.css input.md

<span class="c"># Comma-separated</span>
apex <span class="nt">--standalone</span> <span class="nt">--css</span> base.css,syntax.css,custom.css input.md</code></pre></div></div>

<p>All specified stylesheets are linked in the document head in
the order provided. If you use <code class="language-plaintext highlighter-rouge">--embed-css</code>, all of them
get embedded as inline <code class="language-plaintext highlighter-rouge">&lt;style&gt;</code> blocks.</p>

<h3 id="requirements">Requirements</h3>

<p>You&rsquo;ll need one of the external tools installed and
available in your PATH:</p>

<ul>
  <li>
    <p><strong>Pygments</strong>: Install with <code class="language-plaintext highlighter-rouge">pip install Pygments</code>. The
  <code class="language-plaintext highlighter-rouge">pygmentize</code> binary needs to be accessible.</p>
  </li>
  <li>
    <p><strong>Skylighting</strong>: Install with <code class="language-plaintext highlighter-rouge">cabal install skylighting</code>
  or get it with Homebrew (<code class="language-plaintext highlighter-rouge">brew install skylighting</code>). The
  <code class="language-plaintext highlighter-rouge">skylighting</code> binary needs to be accessible.
If the specified tool isn&rsquo;t found, Apex will print a warning
and leave your code blocks unstyled (but otherwise
functional).</p>
  </li>
</ul>

<h3 id="other-updates">Other Updates</h3>

<p>A few other things landed alongside the syntax highlighting
feature:</p>

<ul>
  <li>
    <p><strong>ANSI art logo</strong> in <code class="language-plaintext highlighter-rouge">--version</code> output, because why not?</p>

    <p><img src="https://brettterpstra.com//uploads/2026/01/Apex-Version-400.jpg" alt="Apex Version" /></p>
  </li>
  <li>
    <p><strong>Test runner badge mode</strong> (<code class="language-plaintext highlighter-rouge">--badge</code>) that outputs just
  the pass/fail count for CI badge generation</p>
  </li>
  <li>
    <p><strong>Improved test output</strong> in errors-only mode now only
  shows suite titles for suites with failures</p>
  </li>
  <li>
    <p><strong>Test infrastructure refactoring</strong> with new suite
  tracking helpers for cleaner, more maintainable tests</p>
  </li>
</ul>

<p>I think the syntax highlighting will be a very useful
feature, at least for people publishing code. Integrating
with external tools means Apex handles the Markdown parsing
and document structure, while battle-tested highlighters
handle the colorization. Best of both worlds.</p>


<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115888368589343553">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Built-in+Syntax+Highlighting+for+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fbuilt-in-syntax-highlighting-for-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fbuilt-in-syntax-highlighting-for-apex%2F&text=Built-in+Syntax+Highlighting+for+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fbuilt-in-syntax-highlighting-for-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17252486.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Markdown to Sendy update: button tags, template fixes]]></title>
    <link href="https://brett.trpstra.net/link/535/17252432/markdown-to-sendy-update-button-tags-template-fixes"/>
    <updated>2026-01-13T08:00:00-06:00</updated>
    <published>2026-01-13T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/13/markdown-to-sendy-update-button-tags-template-fixes</id>
    <content type="html"><![CDATA[
<p>I&rsquo;ve been working on a few updates to my <a href="https://github.com/ttscoff/mdtosendy">Markdown to Sendy script</a> that add some nice quality-of-life improvements for creating email campaigns. The main additions are support for greeting/salutation customization and a new button liquid tag that makes it easier to create styled call-to-action buttons.</p>

<h2 id="greetings-and-salutations">Greetings and Salutations</h2>

<p>The script now supports customizable greetings that can be set in your template configuration. You can define a default greeting in your <code class="language-plaintext highlighter-rouge">config.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">template</span><span class="pi">:</span>
  <span class="na">greeting</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Hey</span><span class="nv"> </span><span class="s">[Name,fallback=there],'</span>
  <span class="c1"># or use the alias:</span>
  <span class="na">salutation</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Hey</span><span class="nv"> </span><span class="s">[Name,fallback=there],'</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">salutation</code> key takes precedence if both are defined, which is useful if you want to keep things semantic. The greeting will automatically appear after the header image and before the first paragraph in your email.</p>

<p>You can also override it in the YAML frontmatter of individual emails:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">title</span><span class="pi">:</span> <span class="s">My Email</span>
<span class="na">greeting</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Hello</span><span class="nv"> </span><span class="s">everyone,'</span></code></pre></div></div>

<p>Or use a liquid tag to place it anywhere in your markdown:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% greeting %}

This is the email content.</code></pre></div></div>

<p>You can even provide a custom greeting directly in the tag:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% greeting "Hi there" %}</code></pre></div></div>

<p>The greeting text can contain Markdown or HTML and will be processed accordingly. The <code class="language-plaintext highlighter-rouge">[Name,fallback=there]</code> syntax is a Sendy merge tag that gets replaced when the email is sent.</p>

<h2 id="button-liquid-tags">Button Liquid Tags</h2>

<p>One of the more tedious parts of creating email buttons has been remembering the exact syntax for applying classes. The new <code class="language-plaintext highlighter-rouge">{% button %}</code> liquid tag makes this much easier.</p>

<p>You can use it with named attributes:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% button class="primary" text="Click Here" url="https://example.com" %}
{% button text="Click Here" url="https://example.com" %}</code></pre></div></div>

<p>Or with positional arguments:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>{% button "Click Here" https://example.com %}
{% button alt "Click Here" https://example.com %}</code></pre></div></div>

<p>The second example uses the <code class="language-plaintext highlighter-rouge">alt</code> class, which maps to the secondary button style. You can also use <code class="language-plaintext highlighter-rouge">alt2</code> for tertiary buttons. These classes (<code class="language-plaintext highlighter-rouge">alt</code> and <code class="language-plaintext highlighter-rouge">alt2</code>) are aliases for <code class="language-plaintext highlighter-rouge">secondary</code> and <code class="language-plaintext highlighter-rouge">tertiary</code>, making it quick to type common variants.</p>

<h3 id="reference-style-links">Reference-Style Links</h3>

<p>Button tags also support markdown reference-style links, which is great for keeping your markdown clean:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">[</span><span class="ss">Forum</span><span class="p">]:</span> <span class="sx">https://forum.brettterpstra.com</span>
<span class="p">[</span><span class="ss">Markdown Web</span><span class="p">]:</span> <span class="sx">https://forum.brettterpstra.com/t/how-about-a-markdown-web/4412/12</span>

{% button "Join the Conversation!" [Forum] %}
{% button alt "How about a Markdown Web?" [Markdown Web] %}</code></pre></div></div>

<p>Reference matching is case-insensitive, so <code class="language-plaintext highlighter-rouge">[Forum]</code>, <code class="language-plaintext highlighter-rouge">[forum]</code>, and <code class="language-plaintext highlighter-rouge">[FORUM]</code> all work the same way.</p>

<blockquote class="warn">
  <p>Note that button creation and styling requires a Markdown processor that handles IALs, such as Kramdown or Apex. I recommend installing <a href="https://github.com/ApexMarkdown/apex">Apex</a> for this tool.</p>
</blockquote>

<p>I know this is a project with a niche audience, but I&rsquo;m aware of a few people are already getting some use out of this. I know I personally love it. If you publish a newsletter or send marketing emails, let me know what you&rsquo;d need to see before it was useful in your own situation.</p>

<p>Check the script out <a href="https://github.com/ttscoff/mdtosendy">on GitHub</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115888185336445211">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Markdown+to+Sendy+update%3A+button+tags%2C+template+fixes%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fmarkdown-to-sendy-update-button-tags-template-fixes%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fmarkdown-to-sendy-update-button-tags-template-fixes%2F&text=Markdown+to+Sendy+update%3A+button+tags%2C+template+fixes&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F13%2Fmarkdown-to-sendy-update-button-tags-template-fixes%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17252432.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for January 12nd, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17251867/web-excursions-for-january-12nd-2026"/>
    <updated>2026-01-12T12:00:00-06:00</updated>
    <published>2026-01-12T12:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/12/web-excursions-for-january-12nd-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with <a href="https://www.git-tower.com/?via=brett">Tower</a>, the absolute best Git GUI for macOS.</p>

<dl>
  <dt><a href="https://www.xmunch.com//?file=moss%3A%2F%2Fwww.xmunch.com%2Fcontent%2Fnodes%2Fmoss%2Findex.md">moss://xmunch.com</a></dt>
  <dd>Right in line with what I was thinking with my Markdown Web brainstorming. A protocol for content distribution that requires Markdown &mdash; portable, secure, and durable.</dd>
  <dt><a href="https://github.com/NSHipster/sosumi.ai">NSHipster/sosumi.ai: Making Apple docs AI-readable</a></dt>
  <dd>A web app and MCP server for converting Apple API docs into AI-compatible formats. Another way to save on tokens and get better results.</dd>
  <dt><a href="https://github.com/karol-broda/snitch">karol-broda/snitch: a prettier way to inspect network connections</a></dt>
  <dd>Great little CLI for inspecting network connections that&rsquo;s friendlier than <code class="language-plaintext highlighter-rouge">ss</code>/<code class="language-plaintext highlighter-rouge">netstat</code>.</dd>
  <dt><a href="https://justcontent.app/">JustContent</a></dt>
  <dd>Another (better?) take on markdownification. Very similar to <a href="https://markdownrules.om">Marky</a> but I think it might get even better results. Works well as a <code class="language-plaintext highlighter-rouge">curl</code> command, and while it can output markdown the same way Gather or Marky does, it&rsquo;s ostensibly designed as a better <code class="language-plaintext highlighter-rouge">curl</code> for your LLMs, reducing tokens and context.</dd>
  <dt><a href="https://www.anildash.com/2026/01/09/how-markdown-took-over-the-world/?__readwiseLocation=">How Markdown took over the world</a></dt>
  <dd>Excellent article from Anil Dash documenting the rise and rapid ubiquity of Markdown.</dd>
  <dd>
    <p>(The banner image on this post is pretty amazing as well.)</p>
  </dd>
</dl>

<p>If you&rsquo;re using Git, you need Tower. <a href="https://www.git-tower.com/?via=brett">Check it out today</a></p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115883477651073263">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+January+12nd%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fweb-excursions-for-january-12nd-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fweb-excursions-for-january-12nd-2026%2F&text=Web+Excursions+for+January+12nd%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fweb-excursions-for-january-12nd-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17251867.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Apex gets tables right]]></title>
    <link href="https://brett.trpstra.net/link/535/17251719/apex-gets-tables-right"/>
    <updated>2026-01-12T08:00:00-06:00</updated>
    <published>2026-01-12T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/12/apex-gets-tables-right</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>Tables in Markdown have always been a bit of a mess. Every processor handles them slightly differently, and when you start wanting advanced features like column spans or captions, you&rsquo;re usually out of luck. I&rsquo;ve been working on <a href="https://github.com/ApexMarkdown/apex">Apex</a>, my unified Markdown processor, and I&rsquo;m happy to say that tables are now pretty solid.</p>

<h3 id="the-problem-with-empty-cells">The Problem With Empty Cells</h3>

<p>One of the trickier issues I ran into was how to handle empty cells versus colspan syntax. In many table implementations, consecutive pipes (<code class="language-plaintext highlighter-rouge">||</code>) create a column span. But what about cells that are just&hellip; empty? You know, with some whitespace between the pipes for alignment?</p>

<p>Previously, Apex was a little too eager to merge cells. If you had <code class="language-plaintext highlighter-rouge">|    |</code> (an empty cell with spaces), it might get treated as part of a colspan. That&rsquo;s not what anyone wants.</p>

<p>Now the rule is simple and predictable:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">||</code> or <code class="language-plaintext highlighter-rouge">|||</code> (consecutive pipes with nothing between them) = colspan</li>
  <li><code class="language-plaintext highlighter-rouge">|    |</code> (pipes with whitespace) = empty cell</li>
</ul>

<p>This means your carefully aligned Markdown source stays readable without accidentally merging cells.</p>

<h3 id="the-marker-for-explicit-colspans">The &laquo;&nbsp;Marker for Explicit Colspans</h3>

<p>Sometimes you want a colspan but you also want your source Markdown to look nice and aligned. That&rsquo;s where the new <code class="language-plaintext highlighter-rouge">&lt;&lt;</code> marker comes in:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| A   | B   | C   |
| --- | --- | --- |
| D   | &lt;&lt;  | &lt;&lt;  |</code></pre></div></div>

<p>Each <code class="language-plaintext highlighter-rouge">&lt;&lt;</code> indicates &ldquo;this cell is part of a colspan from the left.&rdquo; In this example, <code class="language-plaintext highlighter-rouge">D</code> spans all three columns. Your source stays perfectly aligned, and you get the colspan you wanted.</p>

<h3 id="row-spans-with-">Row Spans with ^^</h3>

<p>Column spans get all the attention, but row spans are just as useful. Use <code class="language-plaintext highlighter-rouge">^^</code> in a cell to merge it with the cell above:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| Department  | Employee |
| ----------- | -------- |
| Engineering | Alice    |
| ^^          | Bob      |
| ^^          | Charlie  |</code></pre></div></div>

<p>Here, &ldquo;Engineering&rdquo; spans three rows. Stack multiple <code class="language-plaintext highlighter-rouge">^^</code> markers and the rowspan grows accordingly.</p>

<h3 id="relaxed-tables">Relaxed Tables</h3>

<p>In unified and kramdown modes, you don&rsquo;t even need a separator row:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>one | two | three
1 | 2 | 3
4 | 5 | 6</code></pre></div></div>

<p>Apex figures out it&rsquo;s a table and renders it. No <code class="language-plaintext highlighter-rouge">|---|---|---|</code> required.</p>

<h3 id="headerless-tables">Headerless Tables</h3>

<p>Sometimes you just want data without a header row. Start with an alignment row:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| --- | :---: | ---: |
| left | center | right |
| a | b | c |</code></pre></div></div>

<p>The alignment row sets the column alignment, and the rest is body data. No phantom header row cluttering up your HTML.</p>

<h3 id="row-header-columns">Row Header Columns</h3>

<p>Need semantic row headers? Leave the first header cell empty:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>|       | Q1  | Q2  |
| ----- | --- | --- |
| Sales | 100 | 120 |
| Costs | 80  | 90  |</code></pre></div></div>

<p>Apex renders the first column of each body row as <code class="language-plaintext highlighter-rouge">&lt;th scope="row"&gt;</code>, which is exactly what screen readers expect for accessible data tables.</p>

<h3 id="csv-and-tsv-tables">CSV and TSV Tables</h3>

<p>Don&rsquo;t feel like typing all those pipes? Use a fenced code block with the <code class="language-plaintext highlighter-rouge">table</code> info string:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">```</span><span class="nl">table
</span><span class="sb">header 1,header 2,header 3
data 1,data 2,data 3</span>
<span class="p">```</span></code></pre></div></div>

<p>Apex auto-detects whether it&rsquo;s CSV or TSV and converts it to a proper table. You can even include an alignment row using keywords like <code class="language-plaintext highlighter-rouge">left</code>, <code class="language-plaintext highlighter-rouge">center</code>, <code class="language-plaintext highlighter-rouge">right</code>.</p>

<h3 id="multiple-caption-formats">Multiple Caption Formats</h3>

<p>Captions can go before or after tables, and Apex supports several syntaxes:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>[My Table Caption]

| A | B |
|---|---|
| 1 | 2 |</code></pre></div></div>

<p>Or Pandoc-style:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| A | B |
|---|---|
| 1 | 2 |

Table: My Table Caption</code></pre></div></div>

<p>Or the Pandoc table attributes extension:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| A | B |
|---|---|
| 1 | 2 |

: My Caption {#table-id .my-class}</code></pre></div></div>

<p>That last one even lets you attach IAL attributes directly to the table through the caption. Use <code class="language-plaintext highlighter-rouge">--captions above</code> or <code class="language-plaintext highlighter-rouge">--captions below</code> to control placement.</p>

<h3 id="table-footers">Table Footers</h3>

<p>Need a footer section? Use a row of <code class="language-plaintext highlighter-rouge">===</code>:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| Item   | Price |
| ------ | ----: |
| Widget | 10.00 |
| Gadget | 25.00 |
| ====== | ===== |
| Total  | 35.00 |</code></pre></div></div>

<p>Everything after the <code class="language-plaintext highlighter-rouge">===</code> row goes into <code class="language-plaintext highlighter-rouge">&lt;tfoot&gt;</code>.</p>

<h3 id="per-cell-alignment">Per-Cell Alignment</h3>

<p>Override column alignment on individual cells using colons:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| A      | B       | C        |
| ------ | ------- | -------- |
| :Left  | Right:  | :Center: |</code></pre></div></div>

<p>The colons are stripped from the output and replaced with inline styles.</p>

<h3 id="wrapping-up">Wrapping Up</h3>

<p>Tables were one of the areas where I really wanted Apex to shine. Between relaxed parsing, multiple colspan/rowspan syntaxes, CSV support, captions, footers, and per-cell alignment, I think it covers pretty much every table scenario I&rsquo;ve ever needed.</p>

<p>Check out the <a href="https://github.com/ApexMarkdown/apex">Apex repository</a> and the <a href="https://github.com/ApexMarkdown/apex/wiki/Tables">Tables documentation</a> for more details. And if you find edge cases I haven&rsquo;t covered, <a href="https://github.com/ApexMarkdown/apex/issues">open an issue on GitHub</a>. I&rsquo;m always looking to make tables even better.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115882519497199452">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Apex+gets+tables+right%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fapex-gets-tables-right%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fapex-gets-tables-right%2F&text=Apex+gets+tables+right&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F12%2Fapex-gets-tables-right%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17251719.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[A quick Markdown Fixup update]]></title>
    <link href="https://brett.trpstra.net/link/535/17250368/a-quick-markdown-fixup-update"/>
    <updated>2026-01-09T15:02:00-06:00</updated>
    <published>2026-01-09T15:02:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/09/a-quick-markdown-fixup-update</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>I&rsquo;ve made a couple of improvements to <a href="https://github.com/ttscoff/md-fixup">md-fixup</a>, the opinionated Markdown formatting and linting tool I <a href="https://brettterpstra.com//2026/01/07/markdown-fixup-an-opinionated-markdown-linter/">shared this week</a>. The main additions are better emphasis handling and link conversion options.</p>

<h3 id="emphasis-handling">Emphasis Handling</h3>

<p>By default, md-fixup normalizes bold markers to <code class="language-plaintext highlighter-rouge">__</code> (double underscore) and italic markers to <code class="language-plaintext highlighter-rouge">*</code> (single asterisk). So <code class="language-plaintext highlighter-rouge">**bold**</code> becomes <code class="language-plaintext highlighter-rouge">__bold__</code> and <code class="language-plaintext highlighter-rouge">_italic_</code> becomes <code class="language-plaintext highlighter-rouge">*italic*</code>. This keeps everything consistent in your Markdown files.</p>

<p>But what if you prefer the other style? You can use the <code class="language-plaintext highlighter-rouge">--reverse-emphasis</code> flag to swap them:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>md-fixup <span class="nt">--reverse-emphasis</span> file.md</code></pre></div></div>

<p>This will convert <code class="language-plaintext highlighter-rouge">__bold__</code> to <code class="language-plaintext highlighter-rouge">**bold**</code> and <code class="language-plaintext highlighter-rouge">*italic*</code> to <code class="language-plaintext highlighter-rouge">_italic_</code>. Handy if you&rsquo;re working with a different Markdown flavor or just have a preference.</p>

<p>And if you want to skip emphasis normalization entirely, just skip rule 25:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>md-fixup <span class="nt">--skip</span> bold-italic file.md
<span class="c"># or</span>
md-fixup <span class="nt">--skip</span> 25 file.md</code></pre></div></div>

<h3 id="link-conversion">Link Conversion</h3>

<p>The new link conversion feature is one I should have had originally but kind of forgot. By default, md-fixup now converts all your links to numeric reference-style links, keeping your document body clean and all the URLs organized at the bottom.</p>

<p>Here&rsquo;s what happens:</p>

<p><strong>Before:</strong></p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>This is a <span class="p">[</span><span class="nv">link</span><span class="p">](</span><span class="sx">https://example.com</span><span class="p">)</span> in the text.
Another <span class="p">[</span><span class="nv">link</span><span class="p">](</span><span class="sx">https://example.com</span><span class="p">)</span> with the same URL.</code></pre></div></div>

<p><strong>After:</strong></p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>This is a <span class="p">[</span><span class="nv">link</span><span class="p">][</span><span class="ss">1</span><span class="p">]</span> in the text.
Another <span class="p">[</span><span class="nv">link</span><span class="p">][</span><span class="ss">1</span><span class="p">]</span> with the same URL.

<span class="p">[</span><span class="ss">1</span><span class="p">]:</span> <span class="sx">https://example.com</span></code></pre></div></div>

<p>Notice how the second link reuses the same reference number since it&rsquo;s the same URL. It de-duplicates for you automatically.</p>

<h3 id="reference-links-default">Reference Links (Default)</h3>

<p>Reference links are the default behavior (rule 28). Links get converted to <code class="language-plaintext highlighter-rouge">[text][number]</code> format, and all definitions are placed at the end of the document (rule 29, also default).</p>

<p>You can also put the link definitions at the beginning of the document instead. Just skip rule 29:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>md-fixup <span class="nt">--skip</span> links-at-end file.md
<span class="c"># or</span>
md-fixup <span class="nt">--skip</span> 29 file.md</code></pre></div></div>

<p>This is useful if you prefer your reference definitions up top, or if you&rsquo;re using a tool that reads links from the beginning.</p>

<h3 id="inline-links">Inline Links</h3>

<p>If you prefer inline links instead, you can enable rule 30. This converts everything to <code class="language-plaintext highlighter-rouge">[text](url)</code> format and automatically disables the reference link behavior. Since inline links are disabled by default, the cleanest way to enable them is through a config file. Create <code class="language-plaintext highlighter-rouge">~/.config/md-fixup/config.yml</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">rules</span><span class="pi">:</span>
  <span class="na">skip</span><span class="pi">:</span> <span class="s">all</span>
  <span class="na">include</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">inline-links</span></code></pre></div></div>

<p>Or if you&rsquo;re using the skip/include pattern already, just add <code class="language-plaintext highlighter-rouge">inline-links</code> to your include list. Inline links will override reference links when enabled.</p>

<h3 id="configuration-examples">Configuration Examples</h3>

<p>Here are some practical configuration setups:</p>

<p><strong>Default (reference links at end):</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">width</span><span class="pi">:</span> <span class="m">60</span>
<span class="na">overwrite</span><span class="pi">:</span> <span class="kc">false</span>
<span class="c1"># No rules section needed - reference links are default</span></code></pre></div></div>

<p><strong>Reference links at beginning:</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">rules</span><span class="pi">:</span>
  <span class="na">skip</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">links-at-end</span>  <span class="c1"># This puts links at the beginning</span></code></pre></div></div>

<p><strong>Inline links:</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">rules</span><span class="pi">:</span>
  <span class="na">skip</span><span class="pi">:</span> <span class="s">all</span>
  <span class="na">include</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">inline-links</span>
    <span class="c1"># Other rules to include</span></code></pre></div></div>

<p><strong>Skip emphasis conversion:</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">rules</span><span class="pi">:</span>
  <span class="na">skip</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">bold-italic</span></code></pre></div></div>

<p><strong>Reverse emphasis markers:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>md-fixup <span class="nt">--reverse-emphasis</span> file.md</code></pre></div></div>

<p>This can&rsquo;t be set in the config file yet—it&rsquo;s command-line only for now.</p>

<h3 id="what-gets-converted">What Gets Converted</h3>

<p>The link converter handles:</p>

<ul>
  <li>Inline links: <code class="language-plaintext highlighter-rouge">[text](url)</code> → <code class="language-plaintext highlighter-rouge">[text][1]</code></li>
  <li>Reference links: <code class="language-plaintext highlighter-rouge">[text][ref]</code> → <code class="language-plaintext highlighter-rouge">[text][1]</code> (renumbered)</li>
  <li>Implicit reference links: <code class="language-plaintext highlighter-rouge">[text]</code> → <code class="language-plaintext highlighter-rouge">[text][1]</code> (if definition exists)</li>
  <li>Links with titles: <code class="language-plaintext highlighter-rouge">[text](url "title")</code> → <code class="language-plaintext highlighter-rouge">[text][1]</code> with <code class="language-plaintext highlighter-rouge">[1]: url "title"</code></li>
</ul>

<p>It also smartly ignores links inside code blocks and code spans, so your examples don&rsquo;t get mangled.</p>

<blockquote>
  <p>I plan to continue modifying the link handling to maintain &ldquo;implicit links&rdquo; (links where there&rsquo;s just text surrounded by square brackets and the exact text is used in a link definition); I do actually use those frequently and I think they&rsquo;re great for readability. Stay tuned, I&rsquo;m sure I&rsquo;ll have another morning soon where I feel like hacking on that.</p>
</blockquote>

<p>The emphasis normalizer handles nested cases too, like <code class="language-plaintext highlighter-rouge">***bold italic***</code> becoming <code class="language-plaintext highlighter-rouge">__*bold italic*__</code> (or <code class="language-plaintext highlighter-rouge">_**bold italic**_</code> in reverse mode). It&rsquo;s aware of context and won&rsquo;t mess with code blocks or other protected regions.</p>

<h3 id="and-apex">And Apex</h3>

<p>Speaking of Markdown stuff, I also pushed <a href="https://github.com/ApexMarkdown/apex/releases/tag/v0.1.51">v0.1.51</a> of <a href="https://brettterpstra.com//projects/apex">Apex</a> today. It adds support for <code class="language-plaintext highlighter-rouge">: Caption</code> syntax before tables (previously only after), with or without IAL attributes, and handles blank lines between captions and tables.</p>

<p>It fixes table and other parsing when files don&rsquo;t end with a newline or use CR line endings. <code class="language-plaintext highlighter-rouge">cmark-gfm</code> already had great CRLF handling, but I broke some of it when building out the Apex extensions. Should be better now, and I&rsquo;ve added regression tests to ensure it stays that way.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115867225108269152">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=A+quick+Markdown+Fixup+update%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F09%2Fa-quick-markdown-fixup-update%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F09%2Fa-quick-markdown-fixup-update%2F&text=A+quick+Markdown+Fixup+update&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F09%2Fa-quick-markdown-fixup-update%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17250368.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Web Excursions for January 9th, 2026]]></title>
    <link href="https://brett.trpstra.net/link/535/17250092/web-excursions-for-january-9th-2026"/>
    <updated>2026-01-09T08:00:00-06:00</updated>
    <published>2026-01-09T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/09/web-excursions-for-january-9th-2026</id>
    <content type="html"><![CDATA[
<p><img src="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.jpg"></p>

<p>Web excursions brought to you in partnership with Backblaze. <a href="https://secure.backblaze.com/r/00dszk">Back up everything.</a></p>

<dl>
  <dt><a href="https://github.com/hamzafer/cursor-commands/tree/main">A Collection of Cursor custom slash commands</a></dt>
  <dd>A collection of custom slash commands for Cursor. All potentially useful, with some exhaustive examples.</dd>
  <dt><a href="https://github.com/zcaceres/markdownify-mcp">zcaceres/markdownify-mcp: A Model Context Protocol server for converting almost anything to Markdown</a></dt>
  <dd>A Model Context Protocol server for converting almost anything to Markdown. It can do YouTube videos to Markdown, which is worth trying out.</dd>
  <dt><a href="https://www.pfrazee.com/blog/atmospheric-computing">Atmospheric Computing</a></dt>
  <dd>
    <blockquote>
      <p>Cloud computing has been extremely successful, but it lost the values that drove personal computing. We can solve this by evolving forward.</p>
    </blockquote>
  </dd>
  <dt><a href="https://updatest.app/">Updatest</a></dt>
  <dd>Trying this out and I&rsquo;m really impressed. A worthy successor to MacUpdater (which ceased operations at the beginning of the month). Keeps all your Sparkle and Homebrew apps/utils up to date automatically, and can run in the background, menu bar only.</dd>
</dl>

<p>Backblaze securely backs up your entire computer to the cloud, affordably and reliably. I trust it with all my data. <a href="https://secure.backblaze.com/r/00dszk">Check it out today.</a></p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115865532424672982">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Web+Excursions+for+January+9th%2C+2026%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F09%2Fweb-excursions-for-january-9th-2026%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F09%2Fweb-excursions-for-january-9th-2026%2F&text=Web+Excursions+for+January+9th%2C+2026&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F09%2Fweb-excursions-for-january-9th-2026%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17250092.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2017/03/web-exc-map.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Markdown Fixup: An Opinionated Markdown Linter]]></title>
    <link href="https://brett.trpstra.net/link/535/17248787/markdown-fixup-an-opinionated-markdown-linter"/>
    <updated>2026-01-07T12:00:00-06:00</updated>
    <published>2026-01-07T12:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/07/markdown-fixup-an-opinionated-markdown-linter</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I have some strong opinions about how Markdown should look. Liberal line breaks everywhere. ATX headers with exactly one space after the <code class="language-plaintext highlighter-rouge">#</code>. Consistent list indentation using tabs. Tables that are properly aligned. And on and on. So I made <a href="https://github.com/ttscoff/md-fixup">Markdown Fixup</a> (md-fixup).</p>

<h3 id="building-my-own-tool">Building My Own Tool</h3>

<p>I just wanted a tool to fix my own Markdown files up to match these preferences. But then I thought, maybe someone else out there has the same opinions, or at least could be swayed to think they were good ideas? So I decided to make it available to the world, just in case anyone&rsquo;s tastes matched mine.</p>

<p><code class="language-plaintext highlighter-rouge">md-fixup</code> is a comprehensive markdown linter and formatter that performs 27 different normalization and formatting rules. It solves some common issues you might run into with &ldquo;it works in X.app but not in Y&rdquo; by standardizing on a consistent set of rules.</p>

<p>I considered building this functionality directly into my <a href="https://brettterpstra.com//projects/apex">Apex</a> project, but I determined it was feature bloat and made more sense as a standalone tool. Keeping it separate means it can be used independently, works great in pipelines, and doesn&rsquo;t add complexity to Apex itself. If you want to use it with Apex, you can always pipe it in (the Unix philosophy of doing one thing well).</p>

<h3 id="why-not-use-existing-tools">Why Not Use Existing Tools?</h3>

<p>There are a few good markdown linters available that can be configured to use similar rules &mdash; this one is just specific to my own preferences and easy to run. If your preferences happen to align with mine, you might find it useful.</p>

<h3 id="flexible-usage">Flexible Usage</h3>

<p>The tool takes STDIN and can be used in a pipeline, and can output to STDOUT or overwrite files. Here&rsquo;s a quick example:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c"># Process a file and output to stdout</span>
md-fixup file.md

<span class="c"># Overwrite files in place</span>
md-fixup <span class="nt">--overwrite</span> file.md

<span class="c"># Use in a pipeline</span>
find <span class="nb">.</span> <span class="nt">-name</span> <span class="s2">"*.md"</span> | md-fixup <span class="nt">--width</span> 100</code></pre></div></div>

<p>All linters can be enabled or disabled on the command line, via project config files, or via global config (in that order of precedence). So you can customize it to match your exact needs, even if they don&rsquo;t perfectly align with my defaults.</p>

<h3 id="configuration">Configuration</h3>

<p>You can skip specific rules if you don&rsquo;t want them:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c"># Skip wrapping and end-of-file newline rules</span>
md-fixup <span class="nt">--skip</span> wrap,end-newline file.md

<span class="c"># Or use a config file</span>
md-fixup <span class="nt">--init-config</span></code></pre></div></div>

<p>The config file lives at <code class="language-plaintext highlighter-rouge">~/.config/md-fixup/config.yml</code> and lets you set defaults for width, overwrite behavior, and which rules to skip.</p>

<h3 id="vs-code-extension">VS Code Extension</h3>

<p>There&rsquo;s a start of a VS Code extension in the repo, but I haven&rsquo;t fully developed that yet. It&rsquo;s on the roadmap, but for now the command-line tool works great.</p>

<h3 id="installation">Installation</h3>

<p>You can install it with Homebrew:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>brew tap ttscoff/thelab
brew <span class="nb">install </span>md-fixup</code></pre></div></div>

<p>The release action is generating macOS and Linux binaries (x86, arm) so you don&rsquo;t need any external tools to install via Homebrew.</p>

<p>You can also visit <a href="https://github.com/ttscoff/md-fixup">the GitHub repo</a> for details on installing the Python version or building the Rust version from source.</p>

<h3 id="all-the-fixes">All The Fixes</h3>

<p>Here&rsquo;s what <code class="language-plaintext highlighter-rouge">md-fixup</code> does:</p>

<ul>
  <li>Normalizes line endings to Unix</li>
  <li>Trims trailing whitespace (preserves exactly 2 spaces for line breaks)</li>
  <li>Collapses multiple blank lines (max 1 consecutive, except in code blocks)</li>
  <li>Normalizes headline spacing (exactly 1 space after #)</li>
  <li>Ensures blank line after headline</li>
  <li>Ensures blank line before/after code block</li>
  <li>Ensures blank line before/after list</li>
  <li>Ensures blank line before/after horizontal rule</li>
  <li>Converts list indentation spaces to tabs consistently</li>
  <li>Normalizes list marker spacing</li>
  <li>Wraps text at specified width (preserving links, code spans, fenced blocks)</li>
  <li>Ensures exactly one blank line at end of file</li>
  <li>Normalizes IAL (Inline Attribute List) spacing for both Kramdown and Pandoc styles</li>
  <li>Normalizes fenced code block language identifier spacing</li>
  <li>Normalizes reference-style link definition spacing</li>
  <li>Normalizes task list checkbox (lowercase x)</li>
  <li>Normalizes blockquote spacing</li>
  <li>Normalizes <code class="language-plaintext highlighter-rouge">$$</code> display and <code class="language-plaintext highlighter-rouge">$</code> inline math block spacing (handles multi-line, preserves currency)</li>
  <li>Normalizes table formatting (aligns columns, handles relaxed and headerless tables)</li>
  <li>Normalizes emoji names (spellcheck and correct typos using fuzzy matching)</li>
  <li>Normalizes typography (curly quotes to straight, en/em dashes, ellipses, guillemets)</li>
  <li>Normalizes bold/italic markers (bold: always <code class="language-plaintext highlighter-rouge">__</code>, italic: always <code class="language-plaintext highlighter-rouge">*</code>)</li>
  <li>Normalizes list markers (renumber ordered lists, standardize bullet markers by level)</li>
  <li>Resets ordered lists to start at 1 (if disabled, preserves starting number)</li>
</ul>

<h2 id="feedback-welcome">Feedback Welcome</h2>

<p>Feedback and code contributions are welcome! If you have opinions about Markdown formatting that differ from mine, I&rsquo;d love to hear about them. And if you want to help build out the VS Code extension or add new features, pull requests are always appreciated.</p>

<p>See details <a href="https://github.com/ttscoff/md-fixup">on GitHub</a> and take it for a spin!</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115855150044327809">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Markdown+Fixup%3A+An+Opinionated+Markdown+Linter%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F07%2Fmarkdown-fixup-an-opinionated-markdown-linter%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F07%2Fmarkdown-fixup-an-opinionated-markdown-linter%2F&text=Markdown+Fixup%3A+An+Opinionated+Markdown+Linter&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F07%2Fmarkdown-fixup-an-opinionated-markdown-linter%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17248787.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/md-fixup-header-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Howzit Script Helpers]]></title>
    <link href="https://brett.trpstra.net/link/535/17248028/howzit-script-helpers"/>
    <updated>2026-01-06T08:42:00-06:00</updated>
    <published>2026-01-06T08:42:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/06/howzit-script-helpers</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/howzit-conditionals-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>One of the things I love about <a href="https://brettterpstra.com//projects/howzit">Howzit</a> is how it bridges the gap between simple task lists and full automation. You can write quick (or complex) scripts right in your build notes and, with the latest updates, they can communicate back to Howzit in useful ways. The latest updates make this even easier with automatically-injected helper scripts and some powerful new directives.</p>

<p>The first thing I did was a major overhaul of the way Topics are
processed. It now &ldquo;streams&rdquo; the topics in a sequential order,
reevaluating conditions and variable substitutions after each
executable directive or block is run. That means that the variables you
pass from inside of run blocks can be used to control if/else logic in
the rest of the topic being run, and it makes variables immediately
available in the next task in the topic. Previously you would have had
to incorporate multiple topics to use the variables effectively. Now
they work the way you would expect in any scripting environment.
(Yes, I&rsquo;ll apply this idea to Bunch soon, too).</p>

<p>Here are couple of features made possible by this change:</p>

<h3 id="automatic-helper-script-injection">Automatic Helper Script Injection</h3>

<p>When you write a run block (those fenced code blocks with <code class="language-plaintext highlighter-rouge">run</code> as the language), Howzit automatically detects your script&rsquo;s interpreter from the hashbang and injects helper functions. No setup required &mdash; just start using them.</p>

<p>Here&rsquo;s what happens behind the scenes: Howzit sees your hashbang, figures out you&rsquo;re using bash (or Ruby, Python, Fish, etc.), and automatically loads the appropriate helper script. The helpers are installed in <code class="language-plaintext highlighter-rouge">~/.config/howzit/support/</code> and made available via the <code class="language-plaintext highlighter-rouge">HOWZIT_SUPPORT_DIR</code> environment variable.</p>

<blockquote class="warn">
  <p>By the way, Howzit now runs out of <code class="language-plaintext highlighter-rouge">~/.config</code> instead of <code class="language-plaintext highlighter-rouge">~/.local/share</code>. It will automatically migrate files as necessary, with confirmation. You can force a migration with <code class="language-plaintext highlighter-rouge">howzit --migrate</code>.</p>
</blockquote>

<p>For bash scripts, you get functions like <code class="language-plaintext highlighter-rouge">log</code> and <code class="language-plaintext highlighter-rouge">set_var</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="sb">```</span>run Build and Test
<span class="c">#!/bin/bash</span>
log info <span class="s2">"Starting build process..."</span>
set_var BUILD_STATUS <span class="s2">"in_progress"</span>

<span class="c"># Do your build stuff</span>
make build

<span class="k">if</span> <span class="o">[</span> <span class="nv">$?</span> <span class="nt">-eq</span> 0 <span class="o">]</span><span class="p">;</span> <span class="k">then
  </span>set_var BUILD_STATUS <span class="s2">"success"</span>
  log info <span class="s2">"Build completed successfully"</span>
<span class="k">else
  </span>set_var BUILD_STATUS <span class="s2">"failed"</span>
  log error <span class="s2">"Build failed"</span>
<span class="k">fi</span>
<span class="sb">```</span></code></pre></div></div>

<p>The same helpers work in Ruby, Python, Fish, and other languages—just with syntax that matches each language. In Ruby, it&rsquo;s <code class="language-plaintext highlighter-rouge">Howzit.logger.info()</code> and <code class="language-plaintext highlighter-rouge">Howzit.set_var()</code>. In Python, it&rsquo;s <code class="language-plaintext highlighter-rouge">howzit.Howzit.logger.info()</code> and <code class="language-plaintext highlighter-rouge">howzit.Howzit.set_var()</code>. You get the idea.</p>

<h3 id="controlling-log-output-with-log_level">Controlling Log Output with @log_level</h3>

<p>Sometimes you want verbose output during development, but quieter logs in production. The <code class="language-plaintext highlighter-rouge">@log_level</code> directive lets you control which messages from your scripts actually get displayed.</p>

<p>Log levels work like you&rsquo;d expect: <code class="language-plaintext highlighter-rouge">debug</code> shows everything, <code class="language-plaintext highlighter-rouge">info</code> shows informational messages and above, <code class="language-plaintext highlighter-rouge">warn</code> shows warnings and errors, and <code class="language-plaintext highlighter-rouge">error</code> only shows errors. You can change the log level multiple times throughout a topic:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>@log_level(debug)
@run(./verbose-script.sh) Development build with all messages

@log_level(warn)
@run(./production-script.sh) Production build, warnings only</code></pre></div></div>

<p>This is especially useful when you have scripts that output a lot of debug information. Set the log level to <code class="language-plaintext highlighter-rouge">warn</code> or <code class="language-plaintext highlighter-rouge">error</code> and you&rsquo;ll only see what matters.</p>

<h3 id="setting-variables-with-set_var">Setting Variables with @set_var</h3>

<p>Variables are super useful for making your build notes dynamic. You can set them in scripts using <code class="language-plaintext highlighter-rouge">set_var</code>, but now you can also set them directly in your build notes with the <code class="language-plaintext highlighter-rouge">@set_var</code> directive.</p>

<p>The basic syntax is straightforward:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>@set_var(VERSION, "1.2.3")
@set_var(STATUS, "success")</code></pre></div></div>

<p>Variables set this way work exactly like variables set in scripts &mdash; they&rsquo;re available as <code class="language-plaintext highlighter-rouge">${VAR}</code> in subsequent tasks and can be used in conditional blocks.</p>

<h3 id="command-substitution">Command Substitution</h3>

<p>Here&rsquo;s where it gets interesting: <code class="language-plaintext highlighter-rouge">@set_var</code> supports command substitution. You can execute a command and use its output as the variable value:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>@set_var(VERSION, `rake vver`)
@set_var(BUILD_DATE, $(date +%Y-%m-%d))</code></pre></div></div>

<p>Both backticks and <code class="language-plaintext highlighter-rouge">$()</code> syntax work. The command output (with whitespace stripped) becomes your variable value.</p>

<p>Even better, you can reference other variables in your commands:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>@set_var(PREV_VERSION, "1.2.2")
@set_var(NEXT_VERSION, `echo ${PREV_VERSION} | awk -F. '{print $1"."$2"."$3+1}'`)</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">${PREV_VERSION}</code> gets substituted before the command runs, so you can build up complex values from simpler ones.</p>

<h3 id="putting-it-all-together">Putting It All Together</h3>

<p>Here&rsquo;s a practical example that combines everything:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>## Deploy

@set_var(VERSION, `git describe --tags`)
@set_var(BRANCH, `git rev-parse --abbrev-ref HEAD`)

@log_level(info)
@if ${BRANCH} == "main"
  @run(./deploy-prod.sh ${VERSION}) Deploy to production
@else
  @run(./deploy-staging.sh ${VERSION}) Deploy to staging
@end

@log_level(debug)
```run Verify Deployment
#!/bin/bash
log debug "Checking deployment status..."
# More verbose logging here
```</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">@set_var</code> directives set up your variables from git commands, <code class="language-plaintext highlighter-rouge">@log_level</code> controls verbosity, and the conditional uses those variables to decide what to deploy. The run block at the end uses the helper functions to send messages back to Howzit.</p>

<h3 id="why-this-matters">Why This Matters</h3>

<p>These features make Howzit build notes feel more like a real scripting environment while keeping the simplicity of markdown, and now scripts within a topic can communicate with each other, and topics can cascade variables between them. You can write complex automation without leaving your build notes file, and the helper scripts mean you don&rsquo;t have to remember communication file formats or write boilerplate code.</p>

<p>The automatic injection means less friction &mdash; just write your script and use the helpers. The <code class="language-plaintext highlighter-rouge">@log_level</code> directive gives you control over noise. And <code class="language-plaintext highlighter-rouge">@set_var</code> with command substitution lets you build dynamic workflows that adapt to your project&rsquo;s current state.</p>

<p>This latest release, along with the recent <a href="https://brettterpstra.com/2026/01/01/howzit-with-conditional-blocks/">conditional blocks features</a>, makes Howzit even more useful for me. Hopefully you&rsquo;ll find it useful, too. See the <a href="https://brettterpstra.com//projects/howzit">project page</a> for an overview, and check out <a href="https://github.com/ttscoff/howzit/wiki">the wiki</a> for full documentation.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115848733143070420">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Howzit+Script+Helpers%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F06%2Fhowzit-script-helpers%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F06%2Fhowzit-script-helpers%2F&text=Howzit+Script+Helpers&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F06%2Fhowzit-script-helpers%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17248028.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/howzit-conditionals-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/howzit-conditionals-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Improved Apex Xcode integration]]></title>
    <link href="https://brett.trpstra.net/link/535/17247198/improved-apex-xcode-integration"/>
    <updated>2026-01-05T08:00:00-06:00</updated>
    <published>2026-01-05T08:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/05/improved-apex-xcode-integration</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I&rsquo;ve been working on making Apex (<a href="https://brettterpstra.com//projects/apex">my ultimate Markdown processor</a>) easier to integrate into Xcode projects, and I&rsquo;m excited to share what&rsquo;s new. The biggest change is full Swift Package Manager (SPM) support, which makes adding Apex to your project as simple as clicking a button in Xcode.</p>

<p>While Apex is still in a 0.x state, I don&rsquo;t suggest it be used in production, but if developers want to start testing it out in an application scenario, it should be easy to load in a Mac or iOS app. Once it gets to 1.0, it will be a lot less likely to have breaking changes, and the API won&rsquo;t change in the 1.x series. I don&rsquo;t foresee many, if any, changes to the API moving forward, but we&rsquo;ll see how reception goes and what feedback arises.</p>

<h3 id="swift-package-manager-support">Swift Package Manager Support</h3>

<p>The easiest way to add Apex to your Xcode project now is through Swift Package Manager. Just open your project, go to the Package Dependencies tab, and add the Apex repository. Xcode handles the rest.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="kd">import</span> <span class="kt">Apex</span>

<span class="k">let</span> <span class="nv">markdown</span> <span class="o">=</span> <span class="s">"# Hello World"</span>
<span class="k">let</span> <span class="nv">html</span> <span class="o">=</span> <span class="n">markdown</span><span class="o">.</span><span class="nf">apexHTML</span><span class="p">()</span></code></pre></div></div>

<p>That&rsquo;s it. No framework dragging, no manual linking, no build configuration tweaks. SPM takes care of everything.</p>

<p>The package supports both macOS 10.13+ and iOS 11+, so you can use Apex in your iOS apps too. I&rsquo;ve bundled libyaml as part of the package to make iOS support work seamlessly, with worrying about system library dependencies.</p>

<h3 id="framework-build-options">Framework Build Options</h3>

<p>If you prefer the traditional framework approach, that still works too. The CMake build now includes a module map in the framework, which means Swift can import the C API directly if you need it. The framework build is still the way to go if you&rsquo;re integrating with existing CMake projects or need more control over the build process.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>cmake <span class="nt">-DBUILD_FRAMEWORK</span><span class="o">=</span>ON ..
make</code></pre></div></div>

<p>The framework gets installed to <code class="language-plaintext highlighter-rouge">/Library/Frameworks</code> by default, or you can customize the install location.</p>

<h3 id="better-swift-integration">Better Swift Integration</h3>

<p>I&rsquo;ve improved the Swift API wrapper to make it more idiomatic. The <code class="language-plaintext highlighter-rouge">ApexOptions</code> struct now has a public initializer, so you can create options instances directly:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="k">var</span> <span class="nv">options</span> <span class="o">=</span> <span class="kt">ApexOptions</span><span class="p">()</span>
<span class="n">options</span><span class="o">.</span><span class="n">pretty</span> <span class="o">=</span> <span class="kc">true</span>
<span class="n">options</span><span class="o">.</span><span class="n">generateHeaderIDs</span> <span class="o">=</span> <span class="kc">true</span>
<span class="k">let</span> <span class="nv">html</span> <span class="o">=</span> <span class="n">markdown</span><span class="o">.</span><span class="nf">apexHTML</span><span class="p">(</span><span class="nv">mode</span><span class="p">:</span> <span class="o">.</span><span class="n">gfm</span><span class="p">,</span> <span class="nv">options</span><span class="p">:</span> <span class="n">options</span><span class="p">)</span></code></pre></div></div>

<p>The bridging between Swift and Objective-C has been cleaned up too. All the category methods on <code class="language-plaintext highlighter-rouge">NSString</code> are now properly accessible from Swift, so you get the full API surface without any awkward workarounds.</p>

<h3 id="module-map-for-direct-c-access">Module Map for Direct C Access</h3>

<p>If you need to use the C API directly from Swift, there&rsquo;s now a module map that makes it straightforward:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="kd">import</span> <span class="kt">Apex</span>

<span class="k">let</span> <span class="nv">opts</span> <span class="o">=</span> <span class="nf">apex_options_default</span><span class="p">()</span>
<span class="n">opts</span><span class="o">.</span><span class="n">pretty</span> <span class="o">=</span> <span class="kc">true</span>
<span class="c1">// ... configure options ...</span>
<span class="k">let</span> <span class="nv">html</span> <span class="o">=</span> <span class="nf">apex_markdown_to_html</span><span class="p">(</span><span class="n">markdown</span><span class="p">,</span> <span class="n">markdown</span><span class="o">.</span><span class="n">count</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">opts</span><span class="p">)</span></code></pre></div></div>

<p>This is useful if you need fine-grained control or want to use features that aren&rsquo;t exposed through the higher-level Swift API yet.</p>

<h3 id="testing-it-out">Testing It Out</h3>

<p>I&rsquo;ve included a test script (<code class="language-plaintext highlighter-rouge">test_spm.sh</code>) that creates a temporary test project and verifies everything works. It does a clean build each time, so you can be confident the package builds correctly from scratch.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>./test_spm.sh</code></pre></div></div>

<p>The script tests all the main integration points—basic conversion, different modes, options, and standalone document generation.</p>

<h3 id="one-more-thing">One More Thing</h3>

<p>While I was working on this, I also enabled emoji autocorrect by default in unified mode. So if you type <code class="language-plaintext highlighter-rouge">:rocket:</code> and it doesn&rsquo;t quite match, Apex will try to find the closest match. It&rsquo;s a small quality-of-life improvement that makes the emoji syntax a bit more forgiving.</p>

<p>The SPM package is ready to use in the <a href="https://github.com/ApexMarkdown/apex/releases/latest">latest release</a>, and I&rsquo;d love to hear how it works for you. If you run into any issues or have suggestions for improvements, let me know!</p>

<p>Learn more about Apex on the <a href="https://brettterpstra.com//projects/apex">project page</a> and in the <a href="https://github.com/ApexMarkdown/apex/wiki">Apex wiki</a>. If you&rsquo;re interested in Xcode integration, there&rsquo;s a <a href="https://github.com/ApexMarkdown/apex/wiki/Xcode-Integration">whole page for that</a>.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115842882370658044">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Improved+Apex+Xcode+integration%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F05%2Fimproved-apex-xcode-integration%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F05%2Fimproved-apex-xcode-integration%2F&text=Improved+Apex+Xcode+integration&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F05%2Fimproved-apex-xcode-integration%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17247198.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[How about a Markdown Web?]]></title>
    <link href="https://brett.trpstra.net/link/535/17245639/how-about-a-markdown-web"/>
    <updated>2026-01-02T12:00:00-06:00</updated>
    <published>2026-01-02T12:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/02/how-about-a-markdown-web</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/mdweb-header-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>I often come up with ideas in the hazy half-dream state before I wake up. Recently I was thinking about exactly how shitty the web has become. And also about how much I love plain text formats like Markdown. So what about a Markdown Web?</p>

<p>The idea is harkening back to the dawn of the web, but instead of basic HTML, everything is Markdown. The rendering is client side (with some plugins for extensibility) and the user gets to decide how the whole web looks simply by choosing a theme for their browser. All script and style tags are stripped, no hosted ads, no tracking. Sites can still run sponsors and use affiliate links, but banner ads are gone.</p>

<p>I know that a lot of the web depends on ads to survive. There have been a lot of micropayment options, and I think something like that could work, replacing the ad-based model entirely. Everything is paywalled, but at very affordable rates. That would require a lot more central processing than I&rsquo;ve brainstormed thus far, so I&rsquo;m leaving it as an open question.</p>

<p>I&rsquo;m envisioning this as running on the same servers the web currently exists on, not building some independent blockchain web or anything. Although I&rsquo;m open to brainstorming on that.</p>

<p>I&rsquo;m posting this mind map in the hopes that it will start a conversation. Let me know what projects you know already exist around this, what you think it would require, and what your own imagination for a better web looks like. Add a comment to this post to start a conversation <a href="https://forum.brettterpstra.com">on the forum</a>.</p>

<div id="svg-viewer-46fb404045dc" class="svg-viewer-wrapper controls-position-bottom controls-mode-both pan-mode-scroll zoom-mode-super_scroll" style="width: 100%; max-width: 100%; min-width: 0">
  <div class="svg-viewer-title">Markdown Web</div>
  <div class="svg-viewer-main controls-position-bottom controls-align-alignleft">
    <div class="svg-controls controls-mode-both controls-align-alignleft" data-viewer="svg-viewer-46fb404045dc">
  
  <button type="button" class="svg-viewer-btn zoom-in-btn" data-viewer="svg-viewer-46fb404045dc" title="Zoom In (Ctrl +)" aria-label="Zoom In">
  <span class="btn-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM272 176C258.7 176 248 186.7 248 200L248 248L200 248C186.7 248 176 258.7 176 272C176 285.3 186.7 296 200 296L248 296L248 344C248 357.3 258.7 368 272 368C285.3 368 296 357.3 296 344L296 296L344 296C357.3 296 368 285.3 368 272C368 258.7 357.3 248 344 248L296 248L296 200C296 186.7 285.3 176 272 176z" /></svg></span>
  <span class="btn-text">Zoom In</span>
</button>
<button type="button" class="svg-viewer-btn zoom-out-btn" data-viewer="svg-viewer-46fb404045dc" title="Zoom Out (Ctrl -)" aria-label="Zoom Out">
  <span class="btn-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM200 248C186.7 248 176 258.7 176 272C176 285.3 186.7 296 200 296L344 296C357.3 296 368 285.3 368 272C368 258.7 357.3 248 344 248L200 248z" /></svg></span>
  <span class="btn-text">Zoom Out</span>
</button>
<button type="button" class="svg-viewer-btn reset-zoom-btn" data-viewer="svg-viewer-46fb404045dc" title="Reset Zoom" aria-label="Reset Zoom">
  <span class="btn-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M480 272C480 317.9 465.1 360.3 440 394.7L566.6 521.4C579.1 533.9 579.1 554.2 566.6 566.7C554.1 579.2 533.8 579.2 521.3 566.7L394.7 440C360.3 465.1 317.9 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272zM272 416C351.5 416 416 351.5 416 272C416 192.5 351.5 128 272 128C192.5 128 128 192.5 128 272C128 351.5 192.5 416 272 416z" /></svg></span>
  <span class="btn-text">Reset Zoom</span>
</button>
<button type="button" class="svg-viewer-btn center-view-btn" data-viewer="svg-viewer-46fb404045dc" title="Center View" aria-label="Center View">
  <span class="btn-icon" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path fill="currentColor" d="M320 48C337.7 48 352 62.3 352 80L352 98.3C450.1 112.3 527.7 189.9 541.7 288L560 288C577.7 288 592 302.3 592 320C592 337.7 577.7 352 560 352L541.7 352C527.7 450.1 450.1 527.7 352 541.7L352 560C352 577.7 337.7 592 320 592C302.3 592 288 577.7 288 560L288 541.7C189.9 527.7 112.3 450.1 98.3 352L80 352C62.3 352 48 337.7 48 320C48 302.3 62.3 288 80 288L98.3 288C112.3 189.9 189.9 112.3 288 98.3L288 80C288 62.3 302.3 48 320 48zM163.2 352C175.9 414.7 225.3 464.1 288 476.8L288 464C288 446.3 302.3 432 320 432C337.7 432 352 446.3 352 464L352 476.8C414.7 464.1 464.1 414.7 476.8 352L464 352C446.3 352 432 337.7 432 320C432 302.3 446.3 288 464 288L476.8 288C464.1 225.3 414.7 175.9 352 163.2L352 176C352 193.7 337.7 208 320 208C302.3 208 288 193.7 288 176L288 163.2C225.3 175.9 175.9 225.3 163.2 288L176 288C193.7 288 208 302.3 208 320C208 337.7 193.7 352 176 352L163.2 352zM320 272C346.5 272 368 293.5 368 320C368 346.5 346.5 368 320 368C293.5 368 272 346.5 272 320C272 293.5 293.5 272 320 272z" /></svg></span>
  <span class="btn-text">Center View</span>
</button>

  
  <div class="divider"></div>
  <span class="zoom-display">
    <span class="zoom-percentage" data-viewer="svg-viewer-46fb404045dc">600</span>%
  </span>
</div>

    <div class="svg-container" style="height: 600px; width: 100%; max-width: 100%; min-width: 0;" data-viewer="svg-viewer-46fb404045dc">
      <div class="svg-viewport" data-viewer="svg-viewer-46fb404045dc"></div>
    </div>
  </div>
  
  <div class="svg-viewer-caption">Just a potential future for a better web</div>
</div>
<script>
  window.svgViewerInstances ||= {};
  window.__SVG_VIEWER_I18N__ = {"en":{"zoom_in":"Zoom In","zoom_in_title":"Zoom In (Ctrl +)","zoom_out":"Zoom Out","zoom_out_title":"Zoom Out (Ctrl -)","reset_zoom":"Reset Zoom","center_view":"Center View","copy_center":"Copy Center","copy_center_title":"Copy current center coordinates","instruction_click":"Cmd/Ctrl-click to zoom in, Option/Alt-click to zoom out.","instruction_scroll":"Scroll up to zoom in, scroll down to zoom out.","instruction_drag_scroll":"Drag to pan around the image while scrolling zooms.","instruction_drag":"Drag to pan around the image.","zoom_slider_label":"Zoom level"},"de":{"zoom_in":"Vergrößern","zoom_in_title":"Vergrößern (Strg +)","zoom_out":"Verkleinern","zoom_out_title":"Verkleinern (Strg -)","reset_zoom":"Zoom zurücksetzen","center_view":"Ansicht zentrieren","copy_center":"Zentrum kopieren","copy_center_title":"Aktuelle Mittelpunktkoordinaten kopieren","instruction_click":"Mit Cmd/Strg-Klick vergrößern, mit Wahltaste/Alt-Klick verkleinern.","instruction_scroll":"Nach oben scrollen zum Vergrößern, nach unten scrollen zum Verkleinern.","instruction_drag_scroll":"Ziehen zum Schwenken, während scrollen zoomt.","instruction_drag":"Ziehen zum Schwenken des Bildes.","zoom_slider_label":"Zoomstufe"},"es":{"zoom_in":"Acercar","zoom_in_title":"Acercar (Ctrl +)","zoom_out":"Alejar","zoom_out_title":"Alejar (Ctrl -)","reset_zoom":"Restablecer zoom","center_view":"Centrar vista","copy_center":"Copiar centro","copy_center_title":"Copiar las coordenadas del centro actual","instruction_click":"Cmd/Ctrl-clic para acercar, Opción/Alt-clic para alejar.","instruction_scroll":"Desplaza hacia arriba para acercar, hacia abajo para alejar.","instruction_drag_scroll":"Arrastra para desplazarte por la imagen mientras el desplazamiento hace zoom.","instruction_drag":"Arrastra para desplazarte por la imagen.","zoom_slider_label":"Nivel de zoom"},"fr":{"zoom_in":"Zoom avant","zoom_in_title":"Zoom avant (Ctrl +)","zoom_out":"Zoom arrière","zoom_out_title":"Zoom arrière (Ctrl -)","reset_zoom":"Réinitialiser le zoom","center_view":"Centrer la vue","copy_center":"Copier le centre","copy_center_title":"Copier les coordonnées du centre actuel","instruction_click":"Cliquer avec Cmd/Ctrl pour zoomer, cliquer avec Option/Alt pour dézoomer.","instruction_scroll":"Faites défiler vers le haut pour zoomer, vers le bas pour dézoomer.","instruction_drag_scroll":"Faites glisser pour vous déplacer dans l’image pendant que le défilement effectue un zoom.","instruction_drag":"Faites glisser pour vous déplacer dans l’image.","zoom_slider_label":"Niveau de zoom"},"it":{"zoom_in":"Zoom avanti","zoom_in_title":"Zoom avanti (Ctrl +)","zoom_out":"Zoom indietro","zoom_out_title":"Zoom indietro (Ctrl -)","reset_zoom":"Reimposta zoom","center_view":"Centra vista","copy_center":"Copia centro","copy_center_title":"Copia le coordinate del centro attuale","instruction_click":"Cmd/Ctrl-clic per ingrandire, Option/Alt-clic per ridurre.","instruction_scroll":"Scorri verso l’alto per ingrandire, verso il basso per ridurre.","instruction_drag_scroll":"Trascina per spostarti nell’immagine mentre lo scorrimento esegue lo zoom.","instruction_drag":"Trascina per spostarti nell’immagine.","zoom_slider_label":"Livello di zoom"}};
  (function init() {
    if (typeof SVGViewer === "undefined") {
      return setTimeout(init, 50);
    }
    var options = {"viewerId":"svg-viewer-46fb404045dc","svgUrl":"/uploads/2026/01/MarkdownWeb.svg","initialZoom":6.0,"minZoom":0.25,"maxZoom":8.0,"zoomStep":1.0,"centerX":1440.81,"centerY":2003.85,"showCoordinates":false,"panMode":"scroll","zoomMode":"super_scroll","controlsConfig":{"position":"bottom","mode":"both","styles":[],"alignment":"alignleft","buttons":["zoom_in","zoom_out","reset","center"],"has_slider":false},"buttonFill":"","buttonBorder":"","buttonForeground":"","locale":"en","i18n":{"en":{"zoom_in":"Zoom In","zoom_in_title":"Zoom In (Ctrl +)","zoom_out":"Zoom Out","zoom_out_title":"Zoom Out (Ctrl -)","reset_zoom":"Reset Zoom","center_view":"Center View","copy_center":"Copy Center","copy_center_title":"Copy current center coordinates","instruction_click":"Cmd/Ctrl-click to zoom in, Option/Alt-click to zoom out.","instruction_scroll":"Scroll up to zoom in, scroll down to zoom out.","instruction_drag_scroll":"Drag to pan around the image while scrolling zooms.","instruction_drag":"Drag to pan around the image.","zoom_slider_label":"Zoom level"},"de":{"zoom_in":"Vergrößern","zoom_in_title":"Vergrößern (Strg +)","zoom_out":"Verkleinern","zoom_out_title":"Verkleinern (Strg -)","reset_zoom":"Zoom zurücksetzen","center_view":"Ansicht zentrieren","copy_center":"Zentrum kopieren","copy_center_title":"Aktuelle Mittelpunktkoordinaten kopieren","instruction_click":"Mit Cmd/Strg-Klick vergrößern, mit Wahltaste/Alt-Klick verkleinern.","instruction_scroll":"Nach oben scrollen zum Vergrößern, nach unten scrollen zum Verkleinern.","instruction_drag_scroll":"Ziehen zum Schwenken, während scrollen zoomt.","instruction_drag":"Ziehen zum Schwenken des Bildes.","zoom_slider_label":"Zoomstufe"},"es":{"zoom_in":"Acercar","zoom_in_title":"Acercar (Ctrl +)","zoom_out":"Alejar","zoom_out_title":"Alejar (Ctrl -)","reset_zoom":"Restablecer zoom","center_view":"Centrar vista","copy_center":"Copiar centro","copy_center_title":"Copiar las coordenadas del centro actual","instruction_click":"Cmd/Ctrl-clic para acercar, Opción/Alt-clic para alejar.","instruction_scroll":"Desplaza hacia arriba para acercar, hacia abajo para alejar.","instruction_drag_scroll":"Arrastra para desplazarte por la imagen mientras el desplazamiento hace zoom.","instruction_drag":"Arrastra para desplazarte por la imagen.","zoom_slider_label":"Nivel de zoom"},"fr":{"zoom_in":"Zoom avant","zoom_in_title":"Zoom avant (Ctrl +)","zoom_out":"Zoom arrière","zoom_out_title":"Zoom arrière (Ctrl -)","reset_zoom":"Réinitialiser le zoom","center_view":"Centrer la vue","copy_center":"Copier le centre","copy_center_title":"Copier les coordonnées du centre actuel","instruction_click":"Cliquer avec Cmd/Ctrl pour zoomer, cliquer avec Option/Alt pour dézoomer.","instruction_scroll":"Faites défiler vers le haut pour zoomer, vers le bas pour dézoomer.","instruction_drag_scroll":"Faites glisser pour vous déplacer dans l’image pendant que le défilement effectue un zoom.","instruction_drag":"Faites glisser pour vous déplacer dans l’image.","zoom_slider_label":"Niveau de zoom"},"it":{"zoom_in":"Zoom avanti","zoom_in_title":"Zoom avanti (Ctrl +)","zoom_out":"Zoom indietro","zoom_out_title":"Zoom indietro (Ctrl -)","reset_zoom":"Reimposta zoom","center_view":"Centra vista","copy_center":"Copia centro","copy_center_title":"Copia le coordinate del centro attuale","instruction_click":"Cmd/Ctrl-clic per ingrandire, Option/Alt-clic per ridurre.","instruction_scroll":"Scorri verso l’alto per ingrandire, verso il basso per ridurre.","instruction_drag_scroll":"Trascina per spostarti nell’immagine mentre lo scorrimento esegue lo zoom.","instruction_drag":"Trascina per spostarti nell’immagine.","zoom_slider_label":"Livello di zoom"}}};
    window.svgViewerInstances["svg-viewer-46fb404045dc"] = new SVGViewer(options);
  })();
</script>

<p>Mind map courtesy of <a href="https://www.mindnode.com/" title="MindNode">MindNode Next</a> and my <a href="https://brettterpstra.com/2025/11/10/a-jekyll-plugin-for-viewing-large-svg-diagrams/" title="A Jekyll plugin for viewing large SVG diagrams">Jekyll SVG Viewer</a>.</p>

<p>Just thinking out loud here. Let me know what you think.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115826844208730623">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=How+about+a+Markdown+Web%3F%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F02%2Fhow-about-a-markdown-web%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F02%2Fhow-about-a-markdown-web%2F&text=How+about+a+Markdown+Web%3F&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F02%2Fhow-about-a-markdown-web%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17245639.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/mdweb-header-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/mdweb-header-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Additional Pandoc features for Apex]]></title>
    <link href="https://brett.trpstra.net/link/535/17245219/additional-pandoc-features-for-apex"/>
    <updated>2026-01-01T15:00:00-06:00</updated>
    <published>2026-01-01T15:00:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/01/additional-pandoc-features-for-apex</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p><a href="https://brettterpstra.com/projects/apex/">Apex</a> has always supported Kramdown-style IAL (Inline Attribute Lists), but I&rsquo;ve been steadily adding more Pandoc compatibility. The latest release brings several new features that make Apex work better with Pandoc-style markdown while maintaining backward compatibility.</p>

<p>Thanks to <a href="https://github.com/somelinguist">somelinguist</a> for the <a href="https://github.com/ApexMarkdown/apex/issues/3">suggestions</a>!</p>

<h3 id="table-captions-get-more-flexible">Table Captions Get More Flexible</h3>

<p>Table captions now support three different formats. You can use the existing <code class="language-plaintext highlighter-rouge">[Caption]</code> and <code class="language-plaintext highlighter-rouge">Table: Caption</code> syntax, plus the new Pandoc-style <code class="language-plaintext highlighter-rouge">: Caption</code> format:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| Col1 | Col2 |
|------|------|
| A    | B    |

: This is a Pandoc-style caption</code></pre></div></div>

<p>Even better, IAL attributes in table captions are now extracted and applied to the table element itself. So you can do things like:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>| Col1 | Col2 |
|------|------|
| A    | B    |

: My Table {#table-id .highlight}</code></pre></div></div>

<p>This applies both the <code class="language-plaintext highlighter-rouge">id="table-id"</code> and <code class="language-plaintext highlighter-rouge">class="highlight"</code> attributes directly to the <code class="language-plaintext highlighter-rouge">&lt;table&gt;</code> element, not just the caption.</p>

<h3 id="pandoc-style-ial-everywhere">Pandoc-Style IAL Everywhere</h3>

<p>One of the biggest changes is full support for Pandoc-style IAL syntax without the colon prefix. Where you used to need <code class="language-plaintext highlighter-rouge">{: #id .class}</code>, you can now use <code class="language-plaintext highlighter-rouge">{#id .class}</code> in all contexts:</p>

<ul>
  <li>Block-level IALs after headings, paragraphs, and other blocks</li>
  <li>Inline IALs on links, images, emphasis, and other inline elements</li>
  <li>Pure IAL paragraphs</li>
  <li>Table captions</li>
</ul>

<p>Both formats work, so your existing Kramdown documents continue to work while new Pandoc-style documents are fully supported.</p>

<h3 id="fenced-divs">Fenced Divs</h3>

<p>Pandoc&rsquo;s fenced divs are now supported in unified mode (enabled by default). You can create custom block containers with attributes:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>::::: {#warning .alert}
This is a warning box with custom styling.
:::::</code></pre></div></div>

<p>Fenced divs support nesting, attributes, and all the usual IAL features. You can control them with the <code class="language-plaintext highlighter-rouge">--divs</code> and <code class="language-plaintext highlighter-rouge">--no-divs</code> command-line flags.</p>

<h3 id="bracketed-spans">Bracketed Spans</h3>

<p>Bracketed spans let you create inline HTML spans with attributes using a simple syntax:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>This is [some text]{.highlight} with a span.</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">[text]{IAL}</code> syntax converts to <code class="language-plaintext highlighter-rouge">&lt;span class="highlight"&gt;some text&lt;/span&gt;</code>. It supports all IAL attribute types—IDs, classes, and key-value pairs—and processes markdown inside the spans.</p>

<p>Reference links take precedence, so if <code class="language-plaintext highlighter-rouge">[text]</code> matches a reference link definition, it stays a link. Otherwise, it becomes a span. Bracketed spans are enabled by default in unified mode, and you can control them with <code class="language-plaintext highlighter-rouge">--spans</code> and <code class="language-plaintext highlighter-rouge">--no-spans</code> flags.</p>

<h3 id="image-attributes-get-smarter">Image Attributes Get Smarter</h3>

<p>Image support has been significantly enhanced. You can now use IAL syntax on both inline and reference-style images:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="p">![</span><span class="nv">alt text</span><span class="p">](</span><span class="sx">image.jpg</span><span class="p">)</span>{#my-image .photo width=50%}

<span class="p">[</span><span class="ss">ref</span><span class="p">]:</span> <span class="sx">image.jpg</span> <span class="nn">"Title"</span> {#ref-image .thumbnail width=300px}
<span class="p">![</span><span class="nv">alt</span><span class="p">][</span><span class="ss">ref</span><span class="p">]</span></code></pre></div></div>

<p>The width and height conversion follows Pandoc&rsquo;s rules:</p>
<ul>
  <li>Percentages like <code class="language-plaintext highlighter-rouge">width=50%</code> convert to <code class="language-plaintext highlighter-rouge">style="width: 50%"</code></li>
  <li>Pixel values like <code class="language-plaintext highlighter-rouge">width=300px</code> convert to <code class="language-plaintext highlighter-rouge">width="300"</code> (the <code class="language-plaintext highlighter-rouge">px</code> suffix is stripped)</li>
  <li>Bare integers like <code class="language-plaintext highlighter-rouge">width=300</code> stay as <code class="language-plaintext highlighter-rouge">width="300"</code></li>
  <li>Other units like <code class="language-plaintext highlighter-rouge">5em</code> or <code class="language-plaintext highlighter-rouge">10rem</code> convert to style attributes</li>
  <li>Decimal pixel values like <code class="language-plaintext highlighter-rouge">100.5px</code> also go to style attributes</li>
</ul>

<p>This means you get clean, semantic HTML output that works well across different contexts. Reference image definitions also now preserve title attributes, so <code class="language-plaintext highlighter-rouge">[ref]: url "title" {#id}</code> includes the title in the final output.</p>

<h3 id="better-ial-detection">Better IAL Detection</h3>

<p>I&rsquo;ve improved IAL detection throughout the codebase. IAL syntax with spaces (like <code class="language-plaintext highlighter-rouge">{ width=50% }</code>) now works correctly, and IALs are properly stripped from output even when parsing fails, preventing raw IAL syntax from appearing in your HTML.</p>

<p>All of these features work together seamlessly. You can mix Kramdown and Pandoc syntax in the same document, use IALs everywhere, and get clean, semantic HTML output. The test suite has been expanded to cover all these new features, so everything should work reliably.</p>

<p>Check out the <a href="https://github.com/ApexMarkdown/apex/releases/latest">latest release</a> for full release notes. And if you haven&rsquo;t tried Apex yet, check out <a href="https://brettterpstra.com/projects/apex/">the Apex project</a> and help me build the ultimate Markdown processor!</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115821892265850449">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Additional+Pandoc+features+for+Apex%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F01%2Fadditional-pandoc-features-for-apex%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F01%2Fadditional-pandoc-features-for-apex%2F&text=Additional+Pandoc+features+for+Apex&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F01%2Fadditional-pandoc-features-for-apex%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17245219.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Howzit with conditional blocks]]></title>
    <link href="https://brett.trpstra.net/link/535/17245090/howzit-with-conditional-blocks"/>
    <updated>2026-01-01T09:34:00-06:00</updated>
    <published>2026-01-01T09:34:00-06:00</published>
    <id>https://brettterpstra.com//2026/01/01/howzit-with-conditional-blocks</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2026/01/howzit-conditionals-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>Happy New Year! I spent some time this morning developing some features for Howzit that I think will be really useful (at least for me).</p>

<p><a href="https://brettterpstra.com//projects/howzit">Howzit</a> is my tool for documenting my project structure/build/deploy/CI methods and instructions, with the ability to execute code from the Markdown documentation. I use it in every project I create, and it makes it really easy to come back to a project where I&rsquo;ve forgotten exactly how it works. For example, every project has a &ldquo;Deploy&rdquo; topic where I detail the steps for deploying/publishing. In any project, I can type <code class="language-plaintext highlighter-rouge">howzit deploy</code> and get the instructions, and <code class="language-plaintext highlighter-rouge">howzit -r deploy</code> will just execute my automated deploy scripts.</p>

<p>I added a couple of advanced features this morning.</p>

<h3 id="script-to-howzit-communication">Script-to-Howzit Communication</h3>

<p>Scripts can now communicate back to Howzit while they&rsquo;re running. This lets scripts send log messages and set variables that can be used in subsequent tasks or conditional blocks.</p>

<p>The communication happens through a temporary file. Howzit sets the <code class="language-plaintext highlighter-rouge">HOWZIT_COMM_FILE</code> environment variable to point to this file, and your scripts can write to it using a simple format:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>LOG:info:This is an info message
LOG:warn:This is a warning
LOG:error:Something went wrong
VAR:MY_VAR=some_value</code></pre></div></div>

<p>For example, in a bash script:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="c">#!/bin/bash</span>
<span class="k">if</span> <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$HOWZIT_COMM_FILE</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"LOG:info:Starting deployment process"</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$HOWZIT_COMM_FILE</span><span class="s2">"</span>
  <span class="nb">echo</span> <span class="s2">"VAR:DEPLOY_STATUS=in_progress"</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$HOWZIT_COMM_FILE</span><span class="s2">"</span>
<span class="k">fi</span>

<span class="c"># ... do your deployment ...</span>

<span class="k">if</span> <span class="o">[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$HOWZIT_COMM_FILE</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
  </span><span class="nb">echo</span> <span class="s2">"LOG:info:Deployment complete"</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$HOWZIT_COMM_FILE</span><span class="s2">"</span>
  <span class="nb">echo</span> <span class="s2">"VAR:DEPLOY_STATUS=complete"</span> <span class="o">&gt;&gt;</span> <span class="s2">"</span><span class="nv">$HOWZIT_COMM_FILE</span><span class="s2">"</span>
<span class="k">fi</span></code></pre></div></div>

<p>Variables set by scripts are merged into Howzit&rsquo;s named arguments, so they&rsquo;re available for variable substitution (<code class="language-plaintext highlighter-rouge">${VAR}</code>) in subsequent tasks and can be used in conditional blocks. Log messages are displayed through Howzit&rsquo;s console logger at the appropriate log level.</p>

<p>This is particularly useful for scripts that need to communicate their status or set flags that affect later tasks. See the <a href="https://github.com/ttscoff/howzit/wiki/Script-to-Howzit-Communication">Script-to-Howzit Communication wiki page</a> for complete details and examples.</p>

<h3 id="conditional-blocks">Conditional Blocks</h3>

<p>Howzit now supports conditional blocks, so you can include or exclude content and tasks based on conditions. Use <code class="language-plaintext highlighter-rouge">@if</code> and <code class="language-plaintext highlighter-rouge">@unless</code> directives to control what appears in your build notes.</p>

<h4 id="basic-conditional-blocks">Basic Conditional Blocks</h4>

<p>The syntax is straightforward:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>@if environment == "production"
@run(./deploy-prod.sh) Deploy to Production
@end

@unless $SKIP_TESTS == "1"
@run(./test.sh) Run Tests
@end</code></pre></div></div>

<p>You can also use <code class="language-plaintext highlighter-rouge">@elsif</code> and <code class="language-plaintext highlighter-rouge">@else</code> for multiple branches:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>@if env == "production"
Production setup
@elsif env == "staging"
Staging setup
@else
Development setup
@end</code></pre></div></div>

<h4 id="string-comparison-operators">String Comparison Operators</h4>

<p>Conditions support various string comparison operators. The newest addition is the fuzzy match operator <code class="language-plaintext highlighter-rouge">**=</code>, which matches if the search string&rsquo;s characters appear in order within the target string. For example, <code class="language-plaintext highlighter-rouge">"fluffy" **= "ffy"</code> matches because the characters f, f, and y appear in order.</p>

<p>Other operators include <code class="language-plaintext highlighter-rouge">==</code> for exact equality, <code class="language-plaintext highlighter-rouge">*=</code> for contains, <code class="language-plaintext highlighter-rouge">^=</code> for starts with, <code class="language-plaintext highlighter-rouge">$=</code> for ends with, and <code class="language-plaintext highlighter-rouge">=~</code> for regex matching. See the <a href="https://github.com/ttscoff/howzit/wiki/Conditional-Blocks">Conditional Blocks wiki page</a> for details on all comparison operators.</p>

<h4 id="file-contents-conditions">File Contents Conditions</h4>

<p>You can now check file contents directly in conditions:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>@if file contents VERSION.txt ^= 0.
Version starts with 0.
@end

@if file contents CHANGELOG.md *= "Breaking"
Changelog contains breaking changes
@end</code></pre></div></div>

<p>The file path can be a literal path or a variable from metadata, named arguments, or environment variables. This makes it easy to check version files, configuration files, or any other file content as part of your conditional logic.</p>

<h4 id="special-conditions">Special Conditions</h4>

<p>Howzit also supports special conditions like <code class="language-plaintext highlighter-rouge">git dirty</code>, <code class="language-plaintext highlighter-rouge">git clean</code>, <code class="language-plaintext highlighter-rouge">file exists</code>, <code class="language-plaintext highlighter-rouge">dir exists</code>, and <code class="language-plaintext highlighter-rouge">topic exists</code>. You can even check the current working directory with <code class="language-plaintext highlighter-rouge">cwd</code> using string comparison operators.</p>

<p>Check out the <a href="https://brettterpstra.com//projects/howzit">Howzit project page</a> for general details. For complete documentation on all condition types, operators, and examples, check out the <a href="https://github.com/ttscoff/howzit/wiki/Conditional-Blocks">Conditional Blocks wiki page</a>.</p>

<p>Please enjoy, and may Howzit make your 2026 project development smooth!</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115820691643634397">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Howzit+with+conditional+blocks%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F01%2Fhowzit-with-conditional-blocks%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F01%2Fhowzit-with-conditional-blocks%2F&text=Howzit+with+conditional+blocks&url=https%3A%2F%2Fbrettterpstra.com%2F2026%2F01%2F01%2Fhowzit-with-conditional-blocks%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17245090.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2026/01/howzit-conditionals-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2026/01/howzit-conditionals-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Markdown to HTML Emails with multiple templates]]></title>
    <link href="https://brett.trpstra.net/link/535/17244103/markdown-to-html-emails-with-multiple-templates"/>
    <updated>2025-12-30T14:00:00-06:00</updated>
    <published>2025-12-30T14:00:00-06:00</published>
    <id>https://brettterpstra.com//2025/12/30/markdown-to-html-emails-with-multiple-templates</id>
    <content type="html"><![CDATA[

<p>I&rsquo;ve been working a bit more on <a href="https://github.com/ttscoff/mdtosendy">mdtosendy</a>, my Ruby script for converting Markdown to email-ready HTML, and recently added a multi-template system that makes it much more flexible for managing different email designs.</p>

<p>To read more about the inspiration and initial development of mdtosendy,
see the <a href="https://brettterpstra.com/2025/12/26/create-email-campaigns-from-markdown/">blog post I wrote a couple of days
ago</a>.</p>

<h2 id="the-problem">The Problem</h2>

<p>Originally, <code class="language-plaintext highlighter-rouge">mdtosendy</code> used a single set of template files in <code class="language-plaintext highlighter-rouge">~/.config/mdtosendy/</code>. If you wanted different styling or configuration options for different types of emails, you&rsquo;d have to manually swap files or maintain multiple config directories. Not ideal.</p>

<h2 id="the-solution-template-directories">The Solution: Template Directories</h2>

<p>Now you can create multiple templates, each in its own directory under <code class="language-plaintext highlighter-rouge">~/.config/mdtosendy/templates/</code>. Each template can have its own HTML template, CSS file, and configuration.</p>

<h3 id="creating-a-template">Creating a Template</h3>

<p>Creating a new template is simple:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>mdtosendy.rb <span class="nt">--create-template</span> mytemplate</code></pre></div></div>

<p>This creates <code class="language-plaintext highlighter-rouge">~/.config/mdtosendy/templates/mytemplate/</code> with:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">email-template.html</code> - copied from the default template</li>
  <li><code class="language-plaintext highlighter-rouge">styles.css</code> - copied from the default styles</li>
  <li><code class="language-plaintext highlighter-rouge">config.yml</code> - blank, so it uses the base config</li>
</ul>

<p>Then use it with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>mdtosendy.rb <span class="nt">--template</span> mytemplate email.md</code></pre></div></div>

<h3 id="child-templates">Child Templates</h3>

<p>Even better, you can create child templates that inherit from a parent. This is perfect when you have a base design but want variations:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>mdtosendy.rb <span class="nt">--create-template</span> darktheme <span class="nt">--parent</span> mytemplate</code></pre></div></div>

<p>This creates a child template that:</p>
<ul>
  <li>Inherits all config from the parent</li>
  <li>Inherits HTML and CSS files if they don&rsquo;t exist in the child</li>
  <li>Lets you override just what you need</li>
</ul>

<p>The child&rsquo;s <code class="language-plaintext highlighter-rouge">config.yml</code> looks like:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="na">parent</span><span class="pi">:</span> <span class="s">mytemplate</span>

<span class="c1"># Parent template config (commented out):</span>
<span class="c1"># ... all the parent's config as comments for reference</span></code></pre></div></div>

<p>You can override specific settings in the child&rsquo;s config, and if you don&rsquo;t provide HTML or CSS files, it automatically uses the parent&rsquo;s.</p>

<h2 id="configuration-hierarchy">Configuration Hierarchy</h2>

<p>The configuration system works in layers:</p>

<ol>
  <li><strong>Base config</strong> (<code class="language-plaintext highlighter-rouge">~/.config/mdtosendy/config.yml</code>) - Your Sendy API settings, default list IDs, etc.</li>
  <li><strong>Template config</strong> (<code class="language-plaintext highlighter-rouge">~/.config/mdtosendy/templates/NAME/config.yml</code>) - Template-specific overrides</li>
  <li><strong>Child template config</strong> - Overrides parent template config</li>
</ol>

<p>Each layer can override the previous, so you can have base settings for Sendy, template-specific list IDs, and child-specific styling tweaks.</p>

<h2 id="development-mode">Development Mode</h2>

<p>I also added a <code class="language-plaintext highlighter-rouge">--dev</code> flag that generates an <code class="language-plaintext highlighter-rouge">email-dev.html</code> file for easier template development:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>mdtosendy.rb <span class="nt">--dev</span> <span class="nt">--template</span> mytemplate</code></pre></div></div>

<p>This creates <code class="language-plaintext highlighter-rouge">~/.config/mdtosendy/email-dev.html</code> with:</p>

<ul>
  <li>Linked CSS (not inlined) so you can edit styles and refresh</li>
  <li>Sample content showing all the elements</li>
  <li>A template info table showing which template and CSS file you&rsquo;re using</li>
  <li>Proper wrapper and content-wrapper classes so the styling matches the final email</li>
</ul>

<p>It&rsquo;s much easier to iterate on designs when you can edit CSS and see changes immediately in the browser.</p>

<h2 id="button-variants">Button Variants</h2>

<p>While building this, I also added support for button variants. You can now use:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">[Primary](url){:.button}</code> - The default button</li>
  <li><code class="language-plaintext highlighter-rouge">[Secondary](url){:.button .secondary}</code> - Alternative styling</li>
  <li><code class="language-plaintext highlighter-rouge">[Tertiary](url){:.button .tertiary}</code> - Another alternative</li>
</ul>

<p>Or use the aliases:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">.btn</code> for <code class="language-plaintext highlighter-rouge">.button</code></li>
  <li><code class="language-plaintext highlighter-rouge">.btn.alt</code> for <code class="language-plaintext highlighter-rouge">.button.secondary</code></li>
  <li><code class="language-plaintext highlighter-rouge">.btn.alt2</code> for <code class="language-plaintext highlighter-rouge">.button.tertiary</code></li>
</ul>

<p>The script automatically detects which variant to use and applies the correct CSS styles when converting to email HTML.</p>

<h2 id="backwards-compatibility">Backwards Compatibility</h2>

<p>If you&rsquo;re upgrading from an older version, the script automatically migrates your existing template files to <code class="language-plaintext highlighter-rouge">templates/default/</code> on the first run, so nothing breaks.</p>

<p>The multi-template system makes <code class="language-plaintext highlighter-rouge">mdtosendy</code> much more flexible for managing different email designs, whether you need completely different templates or just variations on a theme. Check out <a href="https://github.com/ttscoff/mdtosendy">the GitHub repo</a> for the latest script and details.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115810328279317120">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Markdown+to+HTML+Emails+with+multiple+templates%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2025%2F12%2F30%2Fmarkdown-to-html-emails-with-multiple-templates%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2025%2F12%2F30%2Fmarkdown-to-html-emails-with-multiple-templates%2F&text=Markdown+to+HTML+Emails+with+multiple+templates&url=https%3A%2F%2Fbrettterpstra.com%2F2025%2F12%2F30%2Fmarkdown-to-html-emails-with-multiple-templates%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17244103.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2024/09/default-thumb-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Apex Inline Attribute Lists Update]]></title>
    <link href="https://brett.trpstra.net/link/535/17243951/apex-inline-attribute-lists-update"/>
    <updated>2025-12-30T09:43:00-06:00</updated>
    <published>2025-12-30T09:43:00-06:00</published>
    <id>https://brettterpstra.com//2025/12/30/apex-inline-attribute-lists-update</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>

<p>I&rsquo;m excited to share that <a href="https://github.com/ApexMarkdown/apex/">Apex</a> version <a href="https://github.com/ApexMarkdown/apex/releases/tag/v0.1.41">0.1.41</a> has comprehensive support for Inline Attribute Lists (IALs), including inline IALs for span-level elements, key-value pairs, and Attribute List Definitions (ALDs). This brings Apex&rsquo;s IAL support to full feature parity with Kramdown.</p>

<p>In case you haven&rsquo;t been keeping up, Apex is my universal Markdown processor project. The goal is to create one Markdown processor that incorporates all the best features from other Markdown processors, like Pandooc, Kramdown, Maruku, MultiMarkdown, and more. One processor to rule them all. Learn more <a href="https://github.com/ApexMarkdown/apex/wiki/">on the wiki</a>.</p>

<h2 id="what-are-ials">What Are IALs?</h2>

<p>Inline Attribute Lists are a Kramdown extension (inspired by Maruku) that let you add HTML attributes (IDs, classes, and custom key-value pairs) directly to Markdown elements. Instead of dropping into raw HTML every time you need to add a class or ID, you can use a simple <code class="language-plaintext highlighter-rouge">{: attributes}</code> syntax.</p>

<p>For block-level elements, IALs go on the line immediately after:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="gh"># My Heading</span>
{: #section-1 .highlight}

This paragraph has custom attributes.
{: .important .note title="Important Note"}</code></pre></div></div>

<p>For inline elements, IALs appear right after the element within the paragraph:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>Here's a <span class="p">[</span><span class="nv">styled link</span><span class="p">](</span><span class="sx">https://example.com</span><span class="p">)</span>{:.button} with a class.

This has <span class="gs">**bold text**</span>{:.bold-style} and <span class="ge">*italic text*</span>{:.italic-style}.</code></pre></div></div>

<h2 id="whats-new">What&rsquo;s New</h2>

<h3 id="inline-ial-support">Inline IAL Support</h3>

<p>Previously, IALs only worked for block-level elements. Now you can apply attributes to links, images, emphasis, strong text, and code spans—all inline within your paragraphs.</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>Here's a <span class="p">[</span><span class="nv">button-style link</span><span class="p">](</span><span class="sx">url</span><span class="p">)</span>{:.btn .btn-primary} that looks great.

You can even style <span class="gs">**bold**</span>{:.highlight} and <span class="ge">*italic*</span>{:.accent} text separately.

Code spans like <span class="sb">`example`</span>{:.code-inline} can have classes too.</code></pre></div></div>

<h3 id="key-value-pairs">Key-Value Pairs</h3>

<p>Beyond IDs and classes, you can now add any custom HTML attributes using key-value pairs:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>Paragraph with custom attributes.
{: title="Tooltip Text" lang="en" data-id="123"}

<span class="p">[</span><span class="nv">External Link</span><span class="p">](</span><span class="sx">https://example.com</span><span class="p">)</span>{:rel="nofollow" target="_blank"}</code></pre></div></div>

<p>Attributes support quoted values (single or double quotes) and unquoted values for simple cases.</p>

<h3 id="attribute-list-definitions-alds">Attribute List Definitions (ALDs)</h3>

<p>ALDs let you define reusable attribute sets and reference them multiple times. This is perfect for maintaining consistent styling across your document:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="gh"># Header 1</span>
{:section-heading}

<span class="gh"># Header 2</span>
{:section-heading}

<span class="gh"># Header 3</span>
{:section-heading}

{:section-heading: #main .large .bold lang="en"}</code></pre></div></div>

<p>All three headers in the example above get the same set of attributes from the ALD definition on the last line. Much cleaner than repeating attributes everywhere!</p>

<h2 id="nested-elements">Nested Elements</h2>

<p>One of the trickier features I implemented is support for nested inline elements with IALs:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code><span class="ge">**</span>Bold text with <span class="ge">*italic*</span>{:.nested-italic} inside<span class="ge">**</span>{:.bold-wrapper}</code></pre></div></div>

<p>Both the italic and bold elements get their respective classes, even when nested.</p>

<h2 id="how-it-works">How It Works</h2>

<p>The implementation uses element indexing to correctly match attributes to elements, even when multiple elements share the same content (like two links with the same URL). This ensures that each element gets the right attributes based on its position in the document.</p>

<p>For inline elements, the processing is recursive, so it handles nested structures correctly. The IAL syntax is extracted from the text nodes and attributes are attached to the corresponding AST nodes before HTML rendering.</p>

<h2 id="usage">Usage</h2>

<p>IALs are available in <strong>Kramdown</strong> and <strong>unified</strong> modes. In unified mode (the default), they&rsquo;re enabled automatically. To use Kramdown mode:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight fixed"><code>apex document.md <span class="nt">--mode</span> kramdown</code></pre></div></div>

<h2 id="complete-documentation">Complete Documentation</h2>

<p>I&rsquo;ve created comprehensive documentation for IALs:</p>

<ul>
  <li><strong><a href="https://github.com/ApexMarkdown/apex/wiki/Inline-Attribute-Lists">Inline Attribute Lists Guide</a></strong> - Complete user guide with examples</li>
  <li><strong><a href="https://github.com/ApexMarkdown/apex/wiki/Syntax#inline-attribute-lists-ial">Syntax Reference</a></strong> - Quick reference in the syntax docs</li>
</ul>

<p>The documentation covers all the edge cases, shows examples for every element type, and explains both block-level and inline IAL syntax in detail.</p>

<h2 id="try-it-out">Try It Out</h2>

<p>Grab the latest version of <a href="https://github.com/ApexMarkdown/apex/">Apex</a> and start adding attributes to your Markdown! Homebrew is updated, just follow the <a href="https://github.com/ApexMarkdown/apex?tab=readme-ov-file#installation">Installation</a> instructions in the README. See the <a href="https://github.com/ApexMarkdown/apex/releases/tag/v0.1.41">release notes</a> for the latest details.</p>

<p>Whether you&rsquo;re styling links, adding IDs for anchor navigation, or using ALDs to maintain consistent styling, IALs make it easy to add HTML attributes without leaving the comfort of Markdown syntax.</p>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115809370861149246">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Apex+Inline+Attribute+Lists+Update%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2025%2F12%2F30%2Fapex-inline-attribute-lists-update%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2025%2F12%2F30%2Fapex-inline-attribute-lists-update%2F&text=Apex+Inline+Attribute+Lists+Update&url=https%3A%2F%2Fbrettterpstra.com%2F2025%2F12%2F30%2Fapex-inline-attribute-lists-update%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17243951.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/apex-header-2-rb_fb.7528.jpg"/>
  </entry>
  <entry>
    <title type="html"><![CDATA[Brett's Favorites 2025]]></title>
    <link href="https://brett.trpstra.net/link/535/17243396/bretts-favorites-2025"/>
    <updated>2025-12-29T09:24:00-06:00</updated>
    <published>2025-12-29T09:24:00-06:00</published>
    <id>https://brettterpstra.com//2025/12/29/bretts-favorites-2025</id>
    <content type="html"><![CDATA[<p><img src="https://cdn3.brettterpstra.com/uploads/2025/12/bretts-favorites-2025-rb.7528.jpg" width="800" height="226" style="margin:0 auto;clear:both;" class="header" /></p>
<p>It&rsquo;s time for my yearly roundup of my favorite tools, hardware, and projects. I&rsquo;m combining everything in one post this year. You might find something new, or you might just have your own preferences validated. Feel free to add your own suggestions in the comments!</p>

<p>It&rsquo;s been an interesting year. I lost my job in April, and have spent the remainder of the year furiously coding, mostly on <a href="https://markedapp.com">Marked 3</a>. A lot of my top picks for the year are a result of that.</p>

<h3 id="macos-apps">macOS Apps</h3>

<dl>
  <dt><a href="https://cursor.com/" title="Cursor">Cursor</a></dt>
  <dd>As skeptical as I am about AI in the world in general, I&rsquo;ve almost mastered getting great results from Copilot and Cursor, Cursor being my tool of choice. Claude is good, but I like the interface of Cursor better.</dd>
  <dd>I should also mention <a href="https://github.com/manaflow-ai/cmux">cmux</a>, which has been helpful, but a lot of what I originally liked about it has already been incorporated into Cursor.</dd>
  <dt><a href="https://cotypist.app/">Cotypist</a></dt>
  <dd>A little more AI. This one offers predictive text anywhere you&rsquo;re typing. It&rsquo;s very intelligent and generally completes my words and sentences like it&rsquo;s reading my mind. It&rsquo;s especially useful in Cursor, where it completes variable names and writes prompts basically magically for me.</dd>
  <dt><a href="https://www.git-tower.com/" title="The most powerful Git client for Mac and Windows">Tower</a></dt>
  <dd>More great improvements this year from my favorite Git client. The UI keeps getting better. They added support for Graphite (stacked PRs), plus other advanced functionality. Tower even released a rebuilt version of git-flow (<a href="https://git-flow.sh/" title="Your Favorite Git Workflow. Reimagined.">git-flow-next</a>)</dd>
  <dd>One thing I discovered is that if you paste a full commit message into the commit subject field, with the message on the first line, a blank line, and then the commit body, it will automatically format it for you, breaking the body into the notes field. I have Cursor write my specialized <a href="https://brettterpstra.com/2024/08/31/updated-generate-slick-changelogs-from-git-commits/?referrer=searchlink" title="Updated:generate slick changelogs from Git commits">changelog commits</a> for me, then just paste them into Tower, where I have full control over what gets staged and committed.</dd>
  <dt><a href="https://kaleidoscope.app/" title="Git Diff and Merge Tool—Kaleidoscope">Kaleidoscope</a></dt>
  <dd>Still my favorite diff tool. I love it for resolving merge conflicts, but also for things like directory comparison/merging and image diffing. The image diff in Kaleidoscope got a huge update this year and is pretty spectacular.</dd>
  <dt><a href="https://kindavim.app/" title="kindaVim">KindaVim</a></dt>
  <dd>This nifty utility lets you enter a limited &ldquo;Vim mode&rdquo; in any text field. I use it everywhere, allowing me to use Vim navigation when I&rsquo;m typing in places like nvUltra or iTerm.</dd>
  <dd>You can use <code class="language-plaintext highlighter-rouge">b</code> and <code class="language-plaintext highlighter-rouge">e</code> to move to word boundaries, <code class="language-plaintext highlighter-rouge">f</code> and <code class="language-plaintext highlighter-rouge">t</code> to jump to characters, and even <code class="language-plaintext highlighter-rouge">dd</code> or <code class="language-plaintext highlighter-rouge">d$</code> to delete text. <code class="language-plaintext highlighter-rouge">a</code> and <code class="language-plaintext highlighter-rouge">i</code> work for returning to &ldquo;insert mode&rdquo; (ending Vim navigation), as well as <code class="language-plaintext highlighter-rouge">o</code> and <code class="language-plaintext highlighter-rouge">O</code>.</dd>
  <dt><a href="https://github.com/mikker/LeaderKey" title="mikker/LeaderKey:The *faster than your launcher* launcher">LeaderKey</a></dt>
  <dd>Another really handy tool. I use LaunchBar for most app launching and navigation, but for some reason I&rsquo;ve really enjoyed assigning key sequences to specific apps and URLs. With LeaderKey I can just hit <code class="language-plaintext highlighter-rouge">Hyper-A, f, q</code> to launch MailMate (I know it&rsquo;s not mnemonically intuitive &mdash; long story). Once you have the muscle memory for it, it makes navigating and launching super fast.</dd>
  <dd>I believe you can achieve the same effect with <a href="https://www.keyboardmaestro.com/" title="Keyboard Maestro 11.0.4:Work Faster with Macros for macOS">Keyboard Maestro</a> conflict palettes, but LeaderKey is super easy to configure and, well, free<sup id="fnref:km"><a href="https://brettterpstra.com#fn:km" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>.</dd>
</dl>

<dl>
  <dt><a href="https://www.devontechnologies.com/apps/devonthink" title="DEVONthink, professional document and ...">DEVONthink</a></dt>
  <dd>I&rsquo;ve been a DEVONthink owner for over a decade, but never fully invested in it until this year. Researching my own health issues, <a href="https://brettterpstra.com/2025/02/08/readwise-highlights-to-devonthink/?referrer=searchlink" title="Readwise highlights to DEVONthink">saving my reading highlights</a>, and creating a knowledgebase of PDFs and web pages has been really useful. And holy moly DEVONthink has an extensive feature set for building your database exactly the way that works for you.</dd>
  <dd>The thing that really made me love it is that I can integrate everything with my plain text notes (in nvUltra), syncing between the two with zero effort.</dd>
  <dt>Mona</dt>
  <dd>I&rsquo;ve deleted my Twitter account (yes, I refuse to call it X) and my Threads account. I dug in entirely to Mastodon, and after trying a bunch of native apps, I found Mona and it blew me away. Great functionality. You can find me on Mastodon at <a href="https://hackyderm.io/@ttscoff/">ttscoff@hachyderm.io</a> and on Pixelfed (Instagram alternative) at <a href="https://pixelfed.social/ttscoff">ttscoff@pixelfed.social</a>. Come find me with whatever client you choose.</dd>
  <dd>I had 5x as many followers on Twitter than I do on Mastodon, but I get <em>way</em> more interaction on Mastodon than I ever did on Twitter. It&rsquo;s a vibrant and active community and you should join if you haven&rsquo;t.</dd>
  <dt><a href="https://www.taskpaper.com/" title="TaskPaper–Plain text to-do lists for Mac">TaskPaper</a></dt>
  <dd>I know it&rsquo;s not actively developed anymore, but I still rely heavily on TaskPaper for organizing my project todos. I keep a separate TaskPaper file in every project directory, which I interact with via Next Action, but when I want to manage the todo list, TaskPaper is still my go-to. I love <a href="https://www.omnigroup.com/omnifocus/" title="Task Management Software Built For Pros">OmniFocus</a> for the rest of my life, but for coding projects, TaskPaper rules.</dd>
  <dt><a href="https://typefaceapp.com/" title="Typeface App-Font Manager for Mac">Typeface</a></dt>
  <dd>I use the <a href="https://setapp.com/apps/typeface?irgwc=1&amp;clickid=Sx90RgTR0xyKU3Ixy3wLtQ%3AeUkCVBpy1p19D2s0&amp;iradid=343321&amp;irpid=228914&amp;sharedid=&amp;mpaid=3" title="Typeface">Setapp version of Typeface</a> for all my font management these days. I spent years paying for expensive font management software, but none of the major players have kept up, and they&rsquo;re mostly bloated apps these days. Typeface is a clean interface for viewing large font collections, with great preview and typography tools, and easy activation and deactivation of font families.</dd>
  <dt><a href="https://hookproductivity.com/" title="Links beat searching">Hookmark</a></dt>
  <dd>Hookmark is crazy useful, once you get in the habit of linking things. It lets you link anything to anything, and easily navigate between the current item and related documents, web pages, todo items, lines of code, and more. If you want to purchase, here&rsquo;s my <a href="https://a.paddle.com/v2/click/30443/127?link=2216">affiliate link</a>.</dd>
  <dd>If you&rsquo;re a developer, check out <a href="https://hookproductivity.com/blog/2025/12/developers-lose-time-to-lost-context-hookmark-fixes-that/">this blog post</a> for ways Hookmark is a vital tool for coding.</dd>
  <dt><a href="https://freron.com/" title="MailMate">MailMate</a></dt>
  <dd>I try pretty much every email client that comes out, but nothing has tempted me away from MailMate. It&rsquo;s so powerful and flexible that I pay twice for it.</dd>
  <dt><a href="https://iterm2.com/" title="macOS Terminal Replacement">iTerm</a></dt>
  <dd>Similar to email clients, I try every terminal emulator that comes out. There are a lot of cool options, but none of them have <em>all</em> the features that iTerm provides. It&rsquo;s another one that&rsquo;s free but I pay for anyway.</dd>
  <dt><a href="https://go.setapp.com/stp44?utm_medium=vendor_program&amp;utm_source=Brett+Terpstra&amp;utm_content=link">Setapp</a></dt>
  <dd>There are a bunch of apps on Setapp that I use &mdash; more than enough to justify the cost. Marked is on there, and v3 will be automatically available to subscribers. I won&rsquo;t list all of the apps, but check out the <a href="https://setapp.com/apps">apps list</a> to see what all you can get.</dd>
  <dd>One highlight is <a href="https://setapp.com/apps/cleanshot?irgwc=1&amp;clickid=Sx90RgTR0xyKU3Ixy3wLtQ%3AeUkCVBpy1p19D2s0&amp;iradid=343321&amp;irpid=228914&amp;sharedid=&amp;mpaid=3" title="CleanShot X">CleanShot X</a>, which is still the only screen capture app I need. It does everything so elegantly.</dd>
  <dd>Another indispensable tool is <a href="https://setapp.sjv.io/c/1198576/1011331/5114">Paletro</a>, which gives me a type-ahead search of all menu items for the current app. I hit <span class="keycombo separated" title="Shift-Command-P"><kbd class="mod symbol">&#8679;</kbd><span class="keycombo combiner">+</span><kbd class="mod symbol">&#8984;</kbd><span class="keycombo combiner">+</span><kbd class="key symbol">P</kbd></span> and access any function I need, in any app.</dd>
  <dd>I also use Setapp for: TaskPaper, Typeface, BetterTouchTool, Permute, Downie, AlDente Pro, Ulysses, Forklift, Timing, NotePlan, Dash, CodeRunner, Default Folder X, Soulver, HoudahSpot, In Your Face, Hookmark, BoltAI, IconJar, Unite, SSH Config Editor, Trickster, KeyCue, and MindNode Next.</dd>
  <dd>I own a lot of these apps outside of Setapp, but I use the Setapp version of those because it means the developers also gets a chunk of my subscription fee.</dd>
</dl>

<h3 id="web">Web</h3>

<dl>
  <dt><a href="https://kagi.com/" title="Kagi Search-A Premium Search Engine">Kagi</a></dt>
  <dd>I just switched to Kagi as my default search engine at end of 2025. No ads, better AI stuff, great keyboard navigation, and you can report and exclude AI slop sites from your search results (which was my primary reason for switching). It&rsquo;s a <a href="https://kagi.com/pricing?plan=individual">paid service</a>, but it&rsquo;s worth $5/mo (or $10 with unlimited searches), and you can also get a <a href="https://kagi.com/pricing?plan=family&amp;period=monthly">family plan</a> that gets 6 people professional access for even less per person.</dd>
  <dt><a href="https://readwise.io/" title="The first read-it-later app built for power readers.">Readwise</a></dt>
  <dd>I&rsquo;ve been paying for Readwise all year. It&rsquo;s an awesome way to handle read-it-later lists, with highlighting, tagging, and note-taking. I save all long-form articles I want to read to Readwise, and I use it as my RSS reader as well. I can sync it with other apps, turn highlights into flash cards, and I can access all of my reading on my Mac, iPhone, and iPad.</dd>
  <dd>The <a href="https://readwise.io/read">Reader</a> app works with your Readwise reading list, and offers a great reading experience with full keyboard control.</dd>
  <dd>The browser plugins make saving articles and highlighting text right on web pages a breeze.</dd>
  <dt><a href="https://answer.apache.org/" title="Apache Answer-Free Open-source Q&amp;A Platform">Apache Answer</a></dt>
  <dd>I&rsquo;ve switched Marked support over to Answer. I lost a little functionality like the ability to reply to email notifications and have the response go to the ticket instead of the user, but overall I&rsquo;m finding it better than my previous Tender setup. Tender hasn&rsquo;t been updated in like 10 years and their customer support is poor. Answer is free, and I run it on <a href="https://www.pikapods.com/">PikaPods</a> for a couple bucks a month.</dd>
  <dt><a href="https://github.com/sissbruecker/linkding" title="sissbruecker/linkding:Self-hosted bookmark manager that ...">linkding</a></dt>
  <dd>I&rsquo;ve completely replaced Pinboard with linkding, running on my Synology and <a href="https://bookmarks.scoffb.in/">publicly accessible</a>. With my <a href="https://brettterpstra.com/2024/11/18/a-card-based-layout-for-linkding/?referrer=searchlink" title="A card-based layout for linkding">card-based custom layout</a> and complete API, it&rsquo;s an excellent bookmark manager.</dd>
  <dt><a href="https://umami.is/" title="Umami">Umami</a></dt>
  <dd>I&rsquo;ve long used <a href="https://usefathom.com/ref/RKODVK" title="A Better Google Analytics Alternative">Fathom</a> for privacy-focused web analytics, and it&rsquo;s still an excellent option, but I&rsquo;m running Umami in parallel right now. It&rsquo;s a fantastic interface with lots of extra data, still completely privacy focused.</dd>
  <dt><a href="https://try.sanebox.com/ttscoff">SaneBox</a></dt>
  <dd>SaneBox has made my list for years. I&rsquo;m so used to it tidying my email workflow that I don&rsquo;t even think about it anymore, but would sincerely miss it if it were gone.</dd>
  <dd>SaneBox sorts your email into trainable folders, automatically cleaning out your inbox so it only contains important emails. You can scan your @SaneLater folder when you have time, and you can train it to sort things like newsletters or promos into folders you may or may not ever look at.</dd>
  <dd>Use <a href="https://try.sanebox.com/be2h3q">this link</a> to get a $25 credit.</dd>
</dl>

<h3 id="ios">iOS</h3>

<p>I mostly use my iPhone for texting and gaming. And I play mostly the same games I&rsquo;ve played for years, so I don&rsquo;t have a long list of new stuff to share here. I use a ton of apps, but not many new or exciting ones, aside from this short list.</p>

<dl>
  <dt><a href="https://apps.apple.com/us/app/drafts/id1435957248?mt=12&amp;uo=4&amp;at=10l4tL&amp;ct=searchlink" title="Drafts">Drafts</a></dt>
  <dd>I also really dug into using Drafts this year. When I wake up in the middle of the night with an idea, I just open Drafts and type it up to get it out of my head so I can fall back asleep. Then I can easily check it on my Mac when I&rsquo;m up, and use Drafts actions to send it where it needs to go.</dd>
  <dd>I use the Drafts Mac app as a scratchpad for what I&rsquo;m going to do next when working on a project. It&rsquo;s especially handy for sketching out AI prompts while another one is working.</dd>
  <dt><a href="https://apps.apple.com/us/app/impressia-for-pixelfed/id1663543216?uo=4&amp;at=10l4tL&amp;ct=searchlink" title="Impressia for Pixelfed">Impressia</a></dt>
  <dd>A great little client for Pixelfed. I do most of my Pixelfed interactions via Mona (it&rsquo;s just a Mastodon server), but Impressia and the <a href="https://apps.apple.com/us/app/pixelfed/id1632519816?uo=4&amp;at=10l4tL&amp;ct=searchlink" title="Pixelfed">Pixelfed</a> app are great for browsing.</dd>
  <dt><a href="https://apps.apple.com/us/app/paprika-recipe-manager-3/id1303222868?uo=4&amp;at=10l4tL&amp;ct=searchlink" title="Paprika Recipe Manager 3">Paprika</a></dt>
  <dd>Paprika is the best recipe manager I&rsquo;ve ever found. I&rsquo;ve used it for a decade and it&rsquo;s well-maintained and great for saving recipes, creating and sharing shopping lists, and for actually cooking in the kitchen with checklists and timers.</dd>
  <dt><a href="https://apps.apple.com/us/app/super-monsters-ate-my-condo/id6466547187?uo=4&amp;at=10l4tL&amp;ct=searchlink" title="Super Monsters Ate My Condo">Super Monsters Ate My Condo</a></dt>
  <dd>Still one of the best games I&rsquo;ve ever played. As of 2025 it&rsquo;s available in Apple Arcade.</dd>
  <dt><a href="https://apps.apple.com/us/app/threes/id779157948?uo=4&amp;at=10l4tL&amp;ct=searchlink" title="Threes!">Threes</a></dt>
  <dd>I spend more time in this app than I do in any other. It&rsquo;s a perfect puzzle game. It&rsquo;s been around for as long as I can remember. It&rsquo;s available on Arcade now, but the classic version is better for reasons I won&rsquo;t go into.</dd>
  <dt><a href="https://apps.apple.com/us/app/zen-pinball-party/id1536783591?uo=4&amp;at=10l4tL&amp;ct=searchlink" title="Zen Pinball Party">Zen Pinball Party</a></dt>
  <dd>This is OK on iPhone but great on iPad. A whole bunch of different pinball tables. The physics and gameplay differs between tables, so it&rsquo;s really a bunch of apps in one. I generally focus on one table at a time and get really good at it, moving on when I get 3 stars or feel like I&rsquo;ve reached my potential. I&rsquo;ve cycled through most of them repeatedly.</dd>
  <dt><a href="https://apps.apple.com/us/app/spark-ai-email-calendar/id997102246?uo=4&amp;at=10l4tL&amp;ct=searchlink" title="Spark AI Email &amp; Calendar">Spark</a></dt>
  <dd>Spark is my email client of choice on iPhone and iPad. It&rsquo;s quite powerful, easy to use with multiple layouts, and has great snooze and quick reply features.</dd>
  <dd>Spark incorporated more AI this year, but I don&rsquo;t use it. I don&rsquo;t think I&rsquo;ll ever want AI to write emails for me.</dd>
</dl>

<h3 id="hardware">Hardware</h3>

<p>Living on unemployment and not having Marked 3 or nvUltra out yet, I
haven&rsquo;t had a lot of extra cash for hardware. I invested in a few
things, though.</p>

<dl>
  <dt><a href="https://amzn.to/44FzEuy">Homepod Mini</a></dt>
  <dd>I&rsquo;m really late to this scene, but I&rsquo;m finally ridding my house of all Amazon products, and the Homepod was the least of all evils when it comes to home assistants. I&rsquo;ve only purchased the minis at this point, and they fulfill all my needs just fine. There&rsquo;s a little more latency than I had with Alexa, but far fewer privacy concerns.</dd>
  <dd>I&rsquo;ll be honest, my primary use for an assistant is controlling multiple timers in the kitchen hands-free. Siri is good at that. And the sound quality for playing music while cooking is exceptional.</dd>
  <dt><a href="https://amzn.to/4jjtt5s">M4 MacBook Pro</a></dt>
  <dd>This is the greatest machine I&rsquo;ve had since the 2012<sup id="fnref:year"><a href="https://brettterpstra.com#fn:year" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> MacBook Air, which I long considered Apple&rsquo;s best product. I still love my first-gen Studio, but the combination of speed, battery life, and portability make the M4 MacBook a killer machine.</dd>
  <dd>I have my Mac Studio paid off, but I&rsquo;ll be paying on this beast for a
while.</dd>
</dl>

<dl>
  <dt><a href="https://amzn.to/4jaUpEa">StarTech.com 4-Port USB-C Charging Station</a></dt>
  <dd>I needed a powerful charging hub that could charge my laptop, phone, and iPad all at the same time, and I needed it to be mountable. I used to use nano tape to attach a charging hub to my bedside table, but to get a hub powerful enough to handle all these new devices, you have to get one that weighs too much to stick with tape. So I needed one I could screw in. This is the best one I found, complete with VESA mounting plate.</dd>
</dl>

<h3 id="projects">Projects</h3>

<dl>
  <dt><a href="https://bearandglass.co">Bear &amp; Glass</a></dt>
  <dd>Christopher Gamblée Wallendjack and I launched a consulting firm this year. We&rsquo;re still getting a foothold, but I&rsquo;m excited about the possibilities. If you&rsquo;re looking for help with automation and streamlining your business, drop us a line for a free consultation.</dd>
  <dt><a href="https://brettterpstra.com/2025/02/08/readwise-highlights-to-devonthink/">Readwise to DEVONthink</a></dt>
  <dd>This script runs on my always-on Studio, and as I create highlights in <a href="https://readwise.io/" title="Readwise">Readwise</a>, they automatically show up as annotated Markdown documents in my DEVONthink database. It&rsquo;s been a really handy way to save articles.</dd>
  <dt><a href="https://brettterpstra.com/projects/searchlink/" title="SearchLink">SearchLink</a></dt>
  <dd>I added <a href="https://brettterpstra.com/2025/04/02/searchlink-with-url-previews/">URL Previews</a> and link shortening to SearchLink this year. It&rsquo;s still a tool I use every day and I think it might be the most useful thing I&rsquo;ve ever created.</dd>
  <dt><a href="https://brettterpstra.com/2025/04/15/off-white-i-made-a-stupid-thing-this-morning/">Off White</a></dt>
  <dd>My stupid color picker for barely off-white and -black colors, now with gradients.</dd>
  <dt><a href="https://rnkd.xyz">RNKD</a></dt>
  <dd>This was a project I built for my own needs but made public. It allows you to create polls with text and images, and then share them with ranked choice voting. There are other polling services, but you often can&rsquo;t use images (which is important for getting design feedback), and I couldn&rsquo;t find any that offered ranked choice, which offers more usable results than just simple voting.</dd>
  <dt>SVG Viewer</dt>
  <dd>I wrote the <a href="https://brettterpstra.com/projects/bt-svg-viewer/" title="BT SVG Viewer">BT SVG Viewer</a> WordPress plugin for Allison Sheridan. It lets you embed large SVG diagrams in your posts, with full navigation, zooming, and text search. It was a fun project.</dd>
  <dd>I also made <a href="https://brettterpstra.com/2025/11/10/a-jekyll-plugin-for-viewing-large-svg-diagrams/">a Jekyll plugin version</a> with an editor that generates Liquid tags for you.</dd>
  <dt><a href="https://brettterpstra.com/2025/10/04/markdown-lipsum-api-v4-and-the-random-words-gem/">md-lipsum</a></dt>
  <dd>I made a massive v4 update to my Markdown lipsum generator, and also published the <code class="language-plaintext highlighter-rouge">random-words</code> gem for generating your own tools.</dd>
  <dt><a href="https://brettterpstra.com/2025/06/30/ripple-an-indeterminate-progress-indicator/">Ripple</a></dt>
  <dd>I went down a rabbit hole creating indeterminate progress indicators. This Ruby library and accompanying CLI is the result.</dd>
  <dt><a href="https://github.com/ApexMarkdown/apex">Apex</a></dt>
  <dd>This in an ongoing big project: I started building my own Markdown processor late in the year. Apex is designed to be a permissive Markdown processor that incorporates syntax from man other tools into one unified processor. Pandoc, Kramdown, CommonMark, MultiMarkdown, plus advanced table syntax, index support for multiple syntaxes, and full bibliography support.</dd>
  <dt><a href="https://brettterpstra.com//projects/na">NA</a></dt>
  <dd>I made a lot of updates to Next Action, my tool for command-line management of TaskPaper files. Among other improvements, I added support for <a href="https://brettterpstra.com/2025/12/03/taskpaper-search-syntax-for-na/">TaskPaper search syntax</a>, and updated my  <a href="https://brettterpstra.com/2025/03/27/taskpaper-to-github-flavored-markdown/">TaskPaper to MD</a> script.</dd>
  <dt><a href="https://brettterpstra.com//projects/howzit">Howzit</a></dt>
  <dd>My project documentation and task runner got a ton of updates. This is another tool I use daily. I&rsquo;ve taught Cursor how to write Howzit-compatible build note files, so after I scaffold out a new project, it can automatically generate the build notes with runnable topics for me based on the project requirements.</dd>
  <dt><a href="https://markedapp.com">Marked 3</a></dt>
  <dd>As I mentioned, most of my time in 2025 has gone to Marked 3. It&rsquo;s an insanely huge update to Marked 2, with completely rewritten DOCX, Scrivener, CriticMarkup, PDF export, writing tools, and much, much more. Coming soon, and the <a href="https://brettterpstra.com/2025/12/26/join-the-marked-3-beta/">beta is public</a>!</dd>
  <dt><a href="https://nvultra.com">nvUltra</a></dt>
  <dd>Fletcher and I made a lot of progress on nvUltra this year. The final piece of the puzzle should be solved by some things I&rsquo;ve built for Marked, and as soon as that&rsquo;s public I&rsquo;ll start porting them to nvUltra, hopefully getting it out the door pretty quickly after the Marked release. The beta is basically public at this point, just contact me through the website for an invite.</dd>
</dl>

<p>Those are my highlights at the end of 2025. Not nearly a complete list of all the great apps and services I&rsquo;ve tried and used this year, of course. Share your own favorites <a href="https://forum.brettterpstra.com">on the forum</a>!</p>
<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:km">
      <p>Yes, of course I already own Keyboard Maestro. I&rsquo;m just saying, it&rsquo;s a free alternative.&nbsp;<a href="https://brettterpstra.com#fnref:km" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:year">
      <p>I forget exactly which year I bought that Air. 2011 or 2012, I think.&nbsp;<a href="https://brettterpstra.com#fnref:year" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>

<p>Like or share this post <a title="This post on Mastodon" target="_blank" href="https://hachyderm.io/users/ttscoff/statuses/115803609262598923">on Mastodon</a>, <a title="Share on Bluesky" target="_blank" href="https://bsky.app/intent/compose?text=Brett%27s+Favorites+2025%0A%0Ahttps%3A%2F%2Fbrettterpstra.com%2F2025%2F12%2F29%2Fbretts-favorites-2025%2F">Bluesky</a>, or <a class="twitter" target="_blank" rel="nofollow" href="https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fbrettterpstra.com%2F2025%2F12%2F29%2Fbretts-favorites-2025%2F&text=Brett%27s+Favorites+2025&url=https%3A%2F%2Fbrettterpstra.com%2F2025%2F12%2F29%2Fbretts-favorites-2025%2F&via=ttscoff" title="Tweet this post">Twitter</a>.</p>
<hr style="margin:40px 0">

<p>BrettTerpstra.com is supported by readers like you. <a href="https://brettterpstra.com/support/">Click here if you'd
        like to help out.</a></p>
<p>Find Brett on <a rel="me" href="https://hachyderm.io/@ttscoff">Mastodon</a>, <a
        href="https://bsky.app/profile/ttscoff.bsky.social">Bluesky</a>, <a
        href="https://github.com/ttscoff">GitHub</a>, and <a href="https://brettterpstra.com/elsewhere">everywhere
        else</a>.</p><img src="https://brett.trpstra.net/link/535/17243396.gif" height="1" width="1"/>]]></content>
    <media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://cdn3.brettterpstra.com/uploads/2025/12/bretts-favorites-2025-rb_fb.7528.jpg" height="300" width="300"/>
    <media:content xmlns:media="http://search.yahoo.com/mrss/" medium="image" url="https://cdn3.brettterpstra.com/uploads/2025/12/bretts-favorites-2025-rb_fb.7528.jpg"/>
  </entry>
</feed>
