<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sven Müller</title>
    <description>The latest articles on DEV Community by Sven Müller (@sventmueller).</description>
    <link>https://dev.to/sventmueller</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4216015%2F8d4b70d3-1ae9-41bc-9d17-b7bbfce3ccf9.jpg</url>
      <title>DEV Community: Sven Müller</title>
      <link>https://dev.to/sventmueller</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sventmueller"/>
    <language>en</language>
    <item>
      <title>Splat – the native macOS Markdown editor for vibecoders, written in Rust</title>
      <dc:creator>Sven Müller</dc:creator>
      <pubDate>Thu, 29 Oct 2026 20:22:52 +0000</pubDate>
      <link>https://dev.to/sventmueller/splat-the-native-macos-markdown-editor-for-vibecoders-written-in-rust-h60</link>
      <guid>https://dev.to/sventmueller/splat-the-native-macos-markdown-editor-for-vibecoders-written-in-rust-h60</guid>
      <description>&lt;p&gt;I spend a good part of every day in Markdown files: CLAUDE.md and AGENTS.md, specs, plans, READMEs. Most of the time I read them rendered, and that is exactly where I spot the typo or the sentence I want to change. Switching to the source, finding the same spot among the asterisks and brackets, fixing it and switching back gets old quickly.&lt;/p&gt;

&lt;p&gt;Editors that let you type into the rendered view exist, but many of them work the same way under the hood: parse the file into a tree, apply your edit to the tree, and print the tree back out as Markdown. That last step is where things go wrong. The printer has its own opinions. &lt;code&gt;*emphasis*&lt;/code&gt; turns into &lt;code&gt;_emphasis_&lt;/code&gt;, list numbers get renumbered, tables get realigned, line breaks move. You fix one word and your Git diff shows forty changed lines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/sventmueller/splat" rel="noopener noreferrer"&gt;Splat&lt;/a&gt; is a native macOS Markdown editor I wrote in Rust, with the source and a live preview side by side. Both panes are editable, and the rule for the preview is strict: &lt;strong&gt;an edit in the preview may only change the bytes it has to&lt;/strong&gt;. Every line you did not touch stays byte-for-byte identical. This post is about how that works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never print the tree
&lt;/h2&gt;

&lt;p&gt;The trick is to never serialize anything. The Markdown text is the only truth. The preview is a projection of it, and every edit in the preview is translated into one plain text edit on the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Edit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Range&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// bytes of the source to replace&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;replacement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// what goes there&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;selection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Range&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// where the caret ends up&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typing, deleting, pasting, pressing Return, ticking a checkbox: all of them end up as one &lt;code&gt;Edit&lt;/code&gt;. The source is patched, the affected part is parsed again, and the preview is updated from the new source. Undo works on the source too, so there is one undo history for both panes, and every preview edit is a single undo step.&lt;/p&gt;

&lt;p&gt;For that to work, the preview has to know, for every character it shows, where that character came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every rendered character knows its source byte
&lt;/h2&gt;

&lt;p&gt;Splat parses with &lt;a href="https://github.com/kivikakk/comrak" rel="noopener noreferrer"&gt;comrak&lt;/a&gt; and lays the document out into paragraphs of styled runs. Each run carries a map back into the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;RunMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// Source byte offset of every char of the run text, plus the end of the last char.&lt;/span&gt;
    &lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;chars&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CharMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="cd"&gt;/// Something that renders as a unit: a list marker, a formula, a diagram, an image...&lt;/span&gt;
    &lt;span class="n"&gt;Atom&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;atom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Atom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Range&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take &lt;code&gt;Some **bold** text&lt;/code&gt;. The preview shows &lt;code&gt;Some bold text&lt;/code&gt; in three runs. The run &lt;code&gt;bold&lt;/code&gt; maps its four characters to bytes 7 to 10 of the source; the asterisks are not in the preview at all, so they are simply not referenced by any character.&lt;/p&gt;

&lt;p&gt;Storing an offset for every character would be expensive for a 10 MB file, and it is almost never needed: most text is copied verbatim from the source, so its offsets are consecutive. &lt;code&gt;CharMap&lt;/code&gt; therefore has two shapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;CharMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// The run is a verbatim copy of the source starting here.&lt;/span&gt;
    &lt;span class="nf"&gt;Verbatim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="cd"&gt;/// Offsets relative to the first char's: a run never spans gigabytes.&lt;/span&gt;
    &lt;span class="nf"&gt;Offsets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only runs where the rendered text differs from the source pay for a table: escaped characters, typographic quotes and dashes, HTML entities, soft line breaks. Even then it is four bytes per character, relative to the run's first one.&lt;/p&gt;

&lt;p&gt;Things that do not render as text, such as list bullets, task checkboxes, formulas, Mermaid diagrams or footnote references, become atoms. An atom knows the source range it stands for, but you cannot place the caret inside it. You can delete it as a whole, and a click on a formula or a diagram opens its editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typing: where does the character go?
&lt;/h2&gt;

&lt;p&gt;When you type at a position in the preview, the question is which source byte the new text goes in front of. Splat answers it the way macOS answers "which font do I type in": the character left of the caret decides. Type right after &lt;strong&gt;bold&lt;/strong&gt; and you extend the bold text; type after a link and you continue the link text.&lt;/p&gt;

&lt;p&gt;In code, &lt;code&gt;insert_point&lt;/code&gt; walks the runs of the paragraph, finds the one that ends at or contains the caret, and asks its &lt;code&gt;CharMap&lt;/code&gt; for the source offset. After that, the typed text has to stay literal. If you type an asterisk into the preview, you mean an asterisk, not the start of emphasis, so it is escaped before it goes into the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sc"&gt;'\\'&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="sc"&gt;'`'&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="sc"&gt;'*'&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="sc"&gt;'['&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="sc"&gt;']'&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="sc"&gt;'&amp;lt;'&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="sc"&gt;'$'&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="sc"&gt;'~'&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="sc"&gt;'^'&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'\\'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// more rules: `_` next to non-letters, doubled `=`, `%` and `|`,&lt;/span&gt;
    &lt;span class="c1"&gt;// and `#`, `&amp;gt;`, `-` or `1.` at the start of a paragraph&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside code spans and code blocks nothing is escaped, because there everything is literal anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting: only the characters you can see
&lt;/h2&gt;

&lt;p&gt;Deleting a selection in the preview deletes the source bytes behind the selected characters, and nothing else. The markup between them stays. Select &lt;code&gt;ld te&lt;/code&gt; across &lt;code&gt;**bold** text&lt;/code&gt; and Splat removes &lt;code&gt;ld&lt;/code&gt; and &lt;code&gt;te&lt;/code&gt; from the source, but keeps the closing &lt;code&gt;**&lt;/code&gt; between them, so the result is &lt;code&gt;**bo**xt&lt;/code&gt;. That is exactly what the preview showed you would get.&lt;/p&gt;

&lt;p&gt;Two cases need more than that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Emptied containers.&lt;/strong&gt; Select all of &lt;code&gt;bold&lt;/code&gt; and delete it, and the naive result is &lt;code&gt;****&lt;/code&gt;, which renders as a horizontal rule or as literal asterisks depending on context. So after computing the deletions, Splat checks every inline container (emphasis, links, strikethrough, inline code) whose content would now be completely gone, and adds the container itself to the deletion. This repeats until nothing changes, because removing a link can empty the bold around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Joining paragraphs.&lt;/strong&gt; Deleting across the boundary between two paragraphs means the paragraphs should become one. In that case Splat also deletes the source between the end of the first paragraph's content and the start of the second's: the blank line and, if the second one is a heading, its &lt;code&gt;#&lt;/code&gt; marks.&lt;/p&gt;

&lt;p&gt;All deletions and the insertion are then folded into one &lt;code&gt;Edit&lt;/code&gt; by a small &lt;code&gt;compose&lt;/code&gt; function, so a paste over a multi-paragraph selection is still one change and one undo step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Return, checkboxes and other structure
&lt;/h2&gt;

&lt;p&gt;Pressing Return in the preview is structural, so it is handled separately, again as one small text edit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a paragraph it inserts a blank line and keeps any quote prefix. If the caret sits inside bold or italic text, the markup is closed before the break and opened again after it, so both halves stay valid.&lt;/li&gt;
&lt;li&gt;In a list item it starts the next item with the same indentation and marker. In an ordered list the number goes up by one, and in a task list the new item gets an empty &lt;code&gt;[ ]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In an empty list item it removes the marker, which ends the list.&lt;/li&gt;
&lt;li&gt;In a table cell it moves to the cell below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some edits touch a single byte. Ticking a task checkbox in the preview replaces the space in &lt;code&gt;[ ]&lt;/code&gt; with &lt;code&gt;x&lt;/code&gt;. Folding an Obsidian callout flips its &lt;code&gt;-&lt;/code&gt; to &lt;code&gt;+&lt;/code&gt;. That is the whole change in the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it fast: parse only what changed
&lt;/h2&gt;

&lt;p&gt;After every edit the source has to be parsed again, and parsing 10 MB on every keystroke is too slow. Markdown helps here, because it is mostly local: a blank line between two top-level blocks ends everything that came before. So Splat finds the changed byte range, takes the top-level blocks around it plus one unchanged block on each side, parses only that window and splices the result into the existing tree, shifting the offsets of everything behind it.&lt;/p&gt;

&lt;p&gt;There are a few things that are not local, and they are checked for explicitly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;footnotes, because comrak numbers references across the whole document,&lt;/li&gt;
&lt;li&gt;link reference definitions, because &lt;code&gt;[ref]: url&lt;/code&gt; anywhere can turn brackets anywhere into a link,&lt;/li&gt;
&lt;li&gt;front matter, because an opening &lt;code&gt;---&lt;/code&gt; at the top is closed by the next &lt;code&gt;---&lt;/code&gt; anywhere,&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;%%&lt;/code&gt; comments, which can span blank lines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When one of those is involved, or when the window grows past half of a large document, Splat simply parses everything. The preview then replaces only the blocks that actually changed. A keystroke in a 10 MB document takes about 25 ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing it with random documents
&lt;/h2&gt;

&lt;p&gt;Code like this is full of edge cases, so most of the guarantees are checked with randomised tests rather than hand-written examples. Two of them carry most of the weight.&lt;/p&gt;

&lt;p&gt;The first types a random character at a random position in the preview of a random document, applies the resulting edit to the source, lays the new source out again, and checks that the preview shows exactly the old preview plus that character:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;edit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bridge&lt;/span&gt;&lt;span class="nf"&gt;.replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;typed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"typing in a paragraph"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"typing"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shown&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="nf"&gt;.splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;typed&lt;/span&gt;&lt;span class="nf"&gt;.encode_utf16&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;new_text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The documents include bold, italic, code, links, strikethrough and HTML, and the typed characters include &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt; and &lt;code&gt;_&lt;/code&gt;, the ones most likely to change the meaning of the source.&lt;/p&gt;

&lt;p&gt;The second makes random edits to random multi-block documents and checks that the partial re-parse produces exactly the tree a full parse produces, byte offsets and syntax colours included. If the "Markdown is mostly local" assumption ever breaks for some construct, this test is where it shows up. Both tests take a seed and a round count from environment variables, so a longer search before a release is one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native, not a web view
&lt;/h2&gt;

&lt;p&gt;One more design decision makes all of this simpler. Both panes are real AppKit text views, written in Rust via &lt;a href="https://github.com/madsmtm/objc2" rel="noopener noreferrer"&gt;objc2&lt;/a&gt;, not HTML in a web view. The preview text lives in the same text system as the source, with native selection, spell checking, dictation and VoiceOver. WebKit is only used, without network access, to turn formulas and diagrams into graphics, which then appear in the preview as atoms, and for the visual formula editor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Splat is free and open source (MIT or Apache-2.0), signed and notarized, for macOS 14 and later on Apple Silicon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; sventmueller/tap/splat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is on GitHub: &lt;a href="https://github.com/sventmueller/splat" rel="noopener noreferrer"&gt;github.com/sventmueller/splat&lt;/a&gt;. The mapping lives in &lt;code&gt;src/preview/map.rs&lt;/code&gt; and &lt;code&gt;src/preview/bridge.rs&lt;/code&gt;, the partial re-parse in &lt;code&gt;src/markdown/incremental.rs&lt;/code&gt;, and the randomised tests in &lt;code&gt;tests/robustness.rs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you find a document where an edit in the preview changes more than it should, I would love to see it. Please open an issue with the Markdown.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Drafted with AI assistance from the Splat source code; every code excerpt and claim was checked against the repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>markdown</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
