← Writing
Parsing

Three things I decided not to parse

What I keep running into isn't parsing. It's deciding what not to parse, and being honest about what the app can't do.

The editor in Margin is CodeMirror 6 on @lezer/markdown, with a few inline extensions of my own. There's no HTML render step anywhere: the document is always the Markdown source, and the rendered look is decorations painted over it, revealed back to raw markup when the cursor enters a node. So "which implementation do you render with" has a slightly odd answer, which is none.

What I keep running into isn't parsing. It's deciding what not to parse. Three cases, and I'm least sure about the third.

Embeds I can't honour

I parse [[wikilinks]] as a single opaque inline node and render a page chip. I deliberately don't match ![[embed]].

Transclusion isn't supported, and in practice most embeds point at images rather than notes, so claiming the token would turn a working image embed into a page chip that goes nowhere. Left unmatched it stays plain text, which is at least honest about what the app does.

A precedence note for anyone doing the same: the wikilink parser has to run before: "Link", or [[Name]] is taken as a bracketed shortcut reference before you ever see it.

src/editor/markdownExt.tsts
parse(cx, next, pos) {
  if (next !== OPEN_BRACKET || cx.char(pos + 1) !== OPEN_BRACKET) return -1;
  // `![[…]]` is an embed, not a page link — leave it alone.
  if (pos > cx.offset && cx.char(pos - 1) === BANG) return -1;
  ...
}

Tables that aren't quite tables

My table parser returns null for anything that isn't a well-formed GFM pipe table, and the caller falls back to rendering the source as plain text.

The alternative is guessing at a repair, and a guessed repair means a malformed table silently becomes a different table the first time it round-trips, with the original gone.

Refusing to parse is the safer failure.

Display options with nowhere to live

This is the one I'd still like opinions on.

A table can be striped, or have its header row hidden. That's presentation, GFM has no way to express it, and I didn't want to invent syntax that every other tool renders as garbage. What I do now is put an HTML comment on the line above:

<!-- margin-table-noheader -->
| Item | Qty |
| ---- | --- |

A comment renders as nothing everywhere else, so the file stays valid and readable in any other editor. But it's positional, it's invisible to anyone reading the raw file who doesn't know the convention, and it is plainly not what HTML comments are for.

The generic directives discussion has been running in the CommonMark community for years, and ::: fenced directives would be the principled answer, except almost nothing else renders those either. I'd be trading an invisible comment for a visible block of syntax that degrades worse in the tools my users actually open these files in.

If you've shipped an app with presentation state that Markdown can't express, I'd genuinely like to know where you put it: HTML comments, directives, a frontmatter key, or did you land on it not belonging in the file at all?

Margin is the app this came out of
Local-first Markdown notes for the Mac. Free while it is alpha.
Download the alpha
More writing
On disk
The watcher that cried delete

Some editors save by replacing the whole file, so a file watcher briefly sees a deletion. Believe that event and your app tells someone their note is gone.

8 Aug 2026·3 min·/blog/atomic-saves
Building
16 lessons from building a productivity app on weekends

Notes from the workshop rather than wisdom from the mountain. What a few months of building a notes app has changed about how I look at every app I use.

2 Aug 2026·4 min·/blog/sixteen-lessons
On disk
Margin vs Bear: Markdown notes in a database or in a folder

Bear keeps Markdown in one SQLite database. Margin keeps each note as a .md file in a folder, so it has folders and Bear doesn't. Where each is better, and how to move.

5 Oct 2026·9 min·/blog/margin-vs-bear