Markdown blocks
StableTurns a note’s Markdown into flat blocks (headings, lists, checklists, quotes) and back, so notes stay plain Markdown in storage.
The note editor works on a flat list of blocks, but notes are stored as plain Markdown so they survive export, sync and other apps. parseMarkdown reads Markdown into blocks; serializeBlocks writes them out. For Markdown in the shape it writes, the round trip gives back exactly the same text, blank lines included. Bold, italics and links stay literal inside a block’s text.
Updated View source
Round trip
import { useId, useMemo, useState } from 'react'import { parseMarkdown, serializeBlocks } from '@rchursin/product/notes'import { Textarea } from '@rchursin/product/ui'
/** * parseMarkdown turns a note into the flat blocks the editor works on; serializeBlocks writes * them back. For Markdown in the shape it writes, the round trip gives back the same text, * byte for byte. Edit the note and watch the blocks and the check below. */const initial = `# Release checklist
Ship **v2.4** on Friday.
- [x] Freeze the branch- [ ] Run the eval suite - [ ] Nightly set- [ ] Write the changelog
> Keep the rollback command handy.`
Usage
import { parseMarkdown, serializeBlocks, type Block, type BlockKind } from '@rchursin/product/notes'const blocks = parseMarkdown(note.body)const next = blocks.map((b) => (b.id === id ? { ...b, checked: !b.checked } : b))save(serializeBlocks(next))Examples
Edit blocks, save Markdown
Tick an item: one block changes and the Markdown on the right is written again.
import { useState } from 'react'import { parseMarkdown, serializeBlocks, type Block } from '@rchursin/product/notes'import { cx } from '@rchursin/components/cx'
/** * Work on blocks, store Markdown. Checking an item changes one block; serializeBlocks writes * the note out again, and the Markdown on the right is what would be saved. */const note = `## Monday standup
- [x] Merge the retry fix- [ ] Review the pricing page copy- [ ] Book the design review
1. Demo the new eval runner2. Agree on the release date`
export default function MarkdownChecklist() { const [blocks, setBlocks] = useState<Block[]>(() => parseMarkdown(note))
API
parseMarkdown(md)
| Prop | Type | Default | Description |
|---|---|---|---|
mdrequired | string | – | Markdown. Windows line endings are fine; tabs count as four spaces. |
returns | Block[] | – | At least one block (an empty paragraph for empty text). Headings #### and deeper become h3; list items indent at most two levels. |
serializeBlocks(blocks)
| Prop | Type | Default | Description |
|---|---|---|---|
blocksrequired | Block[] | – | Any blocks, including ones you made or edited. |
returns | string | – | Markdown. Numbered items are renumbered from 1, nested items get four spaces a level, and text that would read as a marker is escaped with a backslash. |
Block
| Prop | Type | Default | Description |
|---|---|---|---|
idrequired | string | – | Made fresh on every parse (for React keys); not stored in the Markdown. |
kindrequired | 'p' | 'h1' | 'h2' | 'h3' | 'bullet' | 'number' | 'todo' | 'quote' | – | What the block is. |
textrequired | string | – | Without its marker. May hold newlines. |
indentrequired | number | – | List nesting, 0 to 2. |
checkedrequired | boolean | – | Checklist items only. |
spaced | boolean | – | Whether a blank line came before it in the source, so notes written elsewhere come back unchanged. |
Accessibility
- Render checklist blocks as real checkboxes with the text as their label, as the second example does.
- Render heading blocks at a level that fits the page around them, not always h1.
Guidelines
Do
- Keep notes as Markdown in storage and blocks in memory.
- Use the ids for keys only; match blocks by position or content when merging.
Don’t
- Don’t feed it tables, code fences or HTML: they are read as plain paragraphs, and a line inside them that looks like a list item becomes one.
- Don’t rely on list numbers in the text; they are renumbered on save.
In use
Source
The component’s own files, exactly as they run on this site.
/** * The note body as blocks, stored as Markdown. * * The editor works on a flat list of blocks (paragraph, heading, bullet, numbered item, checklist * item, quote). Notes are stored as plain Markdown, so they survive export, sync and other apps: * * parseMarkdown(serializeBlocks(blocks)) gives the same blocks back (ids aside), and * serializeBlocks(parseMarkdown(md)) === md for the Markdown this file writes. * * Pure functions, no imports, so Node can test them directly. * Bold, italic and links stay literal Markdown inside the text (**like this**): the editor is * plain text with block structure, which is what makes textareas possible. */
export type BlockKind = "p" | "h1" | "h2" | "h3" | "bullet" | "number" | "todo" | "quote"
export interface Block { id: string kind: BlockKind /** The block's text, without its Markdown marker. May hold newlines (Shift+Enter). */