Desktop GUI for remark β Markdown β Structured JSON, plugin-driven, batch-capable.
A monorepo that ships two complete desktop applications on top of the remark markdown processor β built and shipped from the same source tree, sharing one React UI, one feature set, and one converter (the structured document tree).
- Why remark-desk?
- Lineage / ηΌθ΅·
- Two builds, one product
- The structured document tree
- Design system
- Features
- Quick start
- Architecture
- Project layout
- Building
- Development workflow
- Testing
- Performance
- Limitations
- Roadmap (v0.2.0+)
- FAQ
- Changelog
- Third-party licenses
- License & acknowledgements
remark is the most popular markdown
processor (used in Next.js, VitePress, Astro, MDX, etc.). It already has
a CLI (remark-cli) for batch processing markdown files, but for day-to-day
authoring, you need a GUI β one that can:
- Show a live preview as you type
- Convert markdown to a structured JSON document that downstream tools can consume without re-parsing
- Manage your plugin chain visually
- Batch-process an entire folder of
.mdfiles with progress, retry, and a report - Work offline, single-file portable, no Electron/Tauri version decision needed at install time
remark-desk does all of the above, with the structured-document-tree
converter as its main differentiator β the right-pane shows a JSON tree
that's both human-readable (collapsible, with type labels) and
machine-consumable (round-trip-stable, drop into any other tool).
This project is a fork + extension of remarkjs/remark, not an independent product. Understanding this lineage explains most of the design decisions in the codebase.
remark-desk started as a monorepo clone of remarkjs/remark, then
added a desktop GUI layer on top. The upstream packages under
packages/ are vendored unmodified:
| Path | Upstream | Role |
|---|---|---|
packages/remark/ |
remarkjs/remark |
the unified pipeline |
packages/remark-parse/ |
remarkjs/remark-parse |
mdast parser |
packages/remark-stringify/ |
remarkjs/remark-stringify |
mdast serializer |
packages/remark-cli/ |
remarkjs/remark-cli |
CLI wrapper |
packages/remark-gfm/ |
remarkjs/remark-gfm |
GFM extension |
| β¦and more | (the full monorepo) | (parser, rehype, lint, etc.) |
The root license is the original Titus Wormer MIT
license (remark's original author). The package.json workspace list
still points at the upstream packages verbatim, with the new
packages/remark-gui/ entry appended on top.
| New code | Where | What |
|---|---|---|
| Desktop GUI (Electron) | packages/remark-gui/ |
~75 MB portable .exe |
| Desktop GUI (Tauri 2) | remark_Rust/ |
~16 MB portable .exe |
| Structured doc tree | electron/ipc/processor.ts, src-tauri/src/ast.rs |
markdownToDocument / jsonToMarkdown (the main differentiator) |
| Brand mark v5 | build/icon.svg, src/components/Brand.tsx |
{ R } with 12% safe margin |
| Batch processing | electron/ipc/batch.ts, src-tauri/src/commands/batch.rs |
recursive folder scan + report |
| Plugin manager UI | src/components/PluginManager.tsx |
visual .remarkrc editor + marketplace |
| Command palette | src/components/CommandPalette.tsx |
19 commands via Ctrl+Shift+P |
remark-desk stands on the shoulders of a number of widely-used open-source projects. We didn't reinvent any of these β we used them, and we list them here so the lineage is clear.
| Borrowed | From | How |
|---|---|---|
| Markdown engine (Tauri build) | kivikakk/comrak |
Pure-Rust GFM parser, drop-in for unified |
| Tauri shell | tauri-apps/tauri |
v2.11, the webview-based Rust desktop framework |
| React UI | react |
18, the renderer in both builds |
| Vite dev server | vite |
the renderer dev server for both builds |
| CodeMirror 6 editor | codemirror.net |
markdown syntax highlighting |
| Tailwind CSS | tailwindcss.com |
utility-first CSS, dark mode built-in |
| highlight.js | highlightjs.org |
code-block syntax highlighting (180+ languages) |
.remarkrc convention |
remarkjs/remark |
inherited verbatim β same format, same loader |
| GitHub Actions CI | remarkjs/.github |
the upstream CI scripts, lightly adapted for dual builds |
- The structured document tree shape (headings as keys, tables as
{headers, rows}, lists asstring[], etc.). The closest similar concept ispandoc's JSON AST, but our shape is application-facing (round-trip-clean, hand-editable), not a full representation of the parser state. - The dual-build system (Electron + Tauri from one source tree).
- The direction toggle (MDβJSON) with
looksLikeJsonauto-detect (the toggle is a hint, not a hard switch). - The race-condition-safe state management for rapid direction
toggles (verified by 27-case test in
scripts/test-direction-toggle-fix.cjs).
| Electron (default) | Tauri 2 (alternative) | |
|---|---|---|
| Folder | packages/remark-gui/ |
remark_Rust/ |
| Runtime | Node 20 + Chromium | Native WebView + Rust |
| Portable size | ~75 MB (single .exe) | ~16 MB (single .exe) |
| Cold start | ~1.5 s | ~0.3 s |
| Markdown engine | unified + remark + rehype (Node) |
comrak 0.31 (pure-Rust) |
| Plugin loading | β
dynamic import() of npm packages (150+) |
|
| Config file | .remarkrc (active) |
.remarkrc (read-only β same format) |
| Frontend | React 18 + Vite + CodeMirror 6 + Tailwind | same (shared via copy) |
| IPC | ipcRenderer.invoke (Electron) |
invoke() (Tauri) β same interface |
.remarkrc portability |
β bidirectional with Tauri | β bidirectional with Electron |
The two builds ship from the same React UI, the same feature set, and
the same .remarkrc format β switching is a no-op (just use the other
binary). They differ in size / startup / plugin loading, so:
- Use Electron if you need user-defined remark plugins (the
full 150+ plugin ecosystem) or you have a custom
.remarkrc. - Use Tauri if you're fine with standard CommonMark + GFM and want a smaller, faster binary.
The right pane shows a JSON tree that's both human-readable and
machine-consumable. Headings become object keys; tables, lists, code
blocks, and quotes each get a canonical shape; and the whole thing
round-trips β md β json β md β json' produces json == json'
byte-equivalent (or data-equivalent for the array-of-objects case).
Input markdown:
# Introduction
remark is a markdown processor.
## Setup
```bash
npm install remark| Name | Stars |
|---|---|
| remark-gfm | 2.1k |
| remark-math | 1.4k |
> Note: all of remark's tooling is built on [unified](https://unifiedjs.com).
Output JSON:
{
"Introduction": [
"remark is a markdown processor."
],
"Setup": [
{ "code": "bash", "content": "npm install remark" }
],
"Plugins": {
"headers": ["Name", "Stars"],
"rows": [
{ "Name": "remark-gfm", "Stars": "2.1k" },
{ "Name": "remark-math", "Stars": "1.4k" }
]
},
"quotes": [
"Note: all of remark's tooling is built on [unified](https://unifiedjs.com)."
]
}| Markdown element | JSON shape |
|---|---|
| Heading | Object key (the heading text) |
| Paragraph | string |
| Multiple paragraphs | string[] (or string if there's only one) |
| List (one level) | string[] (items) |
| Nested list | string[][] (each top-level item is a list of its sub-items) |
| Table | { "headers": string[], "rows": Array<{[header]: cell}> } |
| Code block | { "code": string, "content": string } (or string if no language) |
| Blockquote | string (collected into quotes: string[] when there are several) |
| Content before any heading | parked under the content key so it's not lost |
Multiple element types under the same heading are joined:
{ "heading": { "paragraphs": [...], "list": [...], "table": {...} } }.
Bonus: lenient input. Hand-rolled JSON arrays of objects are also
accepted as table input β [{name: "Alice"}, {name: "Bob"}] is rendered
back as a GFM table (header = union of keys in first-appearance order).
A few design choices are worth calling out:
- Headings as object keys, not nested children. This makes the JSON
a flat namespace (one key per heading) that you can grep, index, or
hand-edit without walking a tree. Downstream tools can read a heading
with a simple
doc["Setup"]lookup. - Tables as
{ headers, rows }with rows as object arrays, not arrays of arrays. Object rows are self-describing (row.Name,row.Stars) and survive column reordering when round-tripping. Array rows would lose column labels after one round-trip. - Lists as flattened
string[]. A one-level list is just["a", "b", "c"]; a nested list isstring[][]. We intentionally don't model the tree oful β linodes because the user's mental model is "list of items", not "tree of list nodes". - Code as
{ code, content }so the language tag is preserved. Without it,```bashand```shwould both round-trip to a language-less block.
The rejected alternative β a flat key-value like
{ "h1_0": "...", "table_1_headers": [...], "table_1_rows": [...] } β
was simpler but lost too much structure; headings collided, tables
became two separate keys, and the order was implicit.
remark-desk has its own brand mark that lives in two places, kept visually identical so the desktop icon and the in-app header always match.
A rounded square (1024Γ1024, 22.5% corner radius) with a blue β indigo β
violet gradient, a single letter R in white, framed by a pair of
curly braces β { R }. The braces are drawn as SVG paths (not font
characters) so they don't depend on the host platform's font set, and
the tips are pulled in to a 12% safe margin so the mark stays
legible at 16Γ16 (favicon) and at 256Γ256 (high-DPI desktop).
| Property | Value |
|---|---|
| Canvas | 1024Γ1024, rx=230 (22.5% of 1024) |
| Gradient stops | #3b82f6 (blue 500) β #6366f1 (indigo 500) β #8b5cf6 (violet 500) |
| R font-size | 660 px, weight 900, letter-spacing -30, all-white |
| Brace stroke | 72 px, all-white, drawn as <path> with cubic BΓ©zier |
| Brace tips x | 130 / 894 (12% margin from each side) |
| Content bounds | x β [130, 894], y β [200, 820] |
The same SVG feeds both the Windows .ico (electron-builder input)
and the Tauri icon set (via npx tauri icon).
The same mark renders inline in the app's top toolbar, via a shared
Brand.tsx component (in both packages/remark-gui/src/components/
and remark_Rust/src/components/). It uses useId() for unique
gradient IDs so multiple instances on one page don't collide.
<svg viewBox="0 0 1024 1024" aria-label="remark-desk">
<defs>
<linearGradient id={gradId} x1="0" y1="0" x2="1024" y2="1024">
<stop offset="0%" stopColor="#3b82f6" />
<stop offset="50%" stopColor="#6366f1" />
<stop offset="100%" stopColor="#8b5cf6" />
</linearGradient>
</defs>
<rect width="1024" height="1024" rx="230" fill={`url(#${gradId})`} />
<path d="M 290 200 C 220 200 190 240 190 320 ..." stroke="white" strokeWidth={72} fill="none" />
<text x="512" y="750" textAnchor="middle" fontSize={660} fontWeight={900} fill="white">R</text>
</svg>The branding was iterated through 5 versions to land on this β earlier attempts had the braces too close to the edge (6.8% margin β 12% margin in v5) and the R letter too thin. v5 is the first version that is fully legible at 16Γ16, the smallest size Windows uses for taskbar icons.
- Open / save
.md/.markdown/.mdxvia native dialog - CodeMirror 6 with markdown syntax highlighting
- Live preview β debounced 300 ms; renders the structured tree on the right (collapsible nodes, raw JSON toggle, copy button)
- Auto-load
.remarkrcfrom the file's directory (walk-up to root) - Direction toggle in the top toolbar β
MD β JSONorJSON β MD(the app auto-detects what your content looks like, so the toggle is a hint, not a hard switch) - Light / dark theme with smooth transition
- Three layouts: editor-only / split / preview-only
-
Ctrl+Sto save,Ctrl+Kto clear content,Ctrl+Shift+Pfor command palette
- Visual editor for
.remarkrc.json(enable / disable / options) - Curated marketplace of 10 popular remark / rehype plugins
- Add by name for any of the 150+ plugins not in the curated list
- Live-reload after save (next preview uses the new plugin set)
- Drop a folder into the window, or pick one
- Recursive scan for
.md/.markdown/.mdx - Bounded concurrency (1β16, default 4)
- Real-time progress (done / succeeded / failed / current file)
- Cancel mid-run
- Export report as JSON / CSV / Markdown
- Command Palette (
Ctrl+Shift+P) with 19 commands - Global shortcuts:
Ctrl+B(plugin manager),Ctrl+J(mode),Ctrl+S(save),Ctrl+K(clear),Ctrl+1/2/3(layout) - Dark mode with highlight.js dark theme
- Friendly errors with
π‘ hintfor common failures (plugin not installed, parse errors, file I/O) - Drag & drop files / folders
- Full Chinese (zh-CN) localization
git clone https://github.com/Dannykey/remark-desk.git
cd remark-desk
npm installcd packages/remark-gui
npm startThis builds the renderer + main, then launches Electron with hot-reload.
cd remark_Rust
npm install
npm run tauri:devThis starts Vite on localhost:1420 and launches the Tauri window pointing
at it. Edits in src/ hot-reload; edits in src-tauri/ trigger a Rust
rebuild.
See Building below.
βββββββββββββββββββββββββββββββββββ Renderer (React) βββββββββββββββββββββββββββββββββββ
β App.tsx β
β ββ Toolbar (open / save / clear / mode / layout / direction toggle / ) β
β ββ Editor (CodeMirror 6, markdown) β
β ββ Preview (structured JSON tree + raw JSON toggle) β
β ββ PluginManager (enable / disable / marketplace) β
β ββ BatchView (folder drop, progress, report export) β
β ββ StatusBar (line / char / config source / theme) β
β β
β Calls: remarkApi.remark.process(md) / processJson(json) / batch.* etc. β
βββββββββββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββ
β (Electron IPC or Tauri invoke)
βββββββββββββββββββ Main (Electron: Node 20) βββββββββββββββββββ
β electron/main.ts: window + tray + menu β
β electron/preload.ts: contextBridge β window.remarkApi β
β electron/ipc/remark.ts: unified pipeline (load .remarkrc) β
β electron/ipc/batch.ts: p-limit parallel scan β
β electron/ipc/files.ts: dialog + fs β
β electron/ipc/plugins.ts: .remarkrc visual editor β
β electron/ipc/processor.ts: the structured-doc-tree converter β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββ Backend (Tauri: Rust) ββββββββββββββββββββββ
β src-tauri/src/main.rs β 4-line stub β
β src-tauri/src/lib.rs β tauri::Builder + invoke_handler β
β src-tauri/src/state.rs β AppState (active batch jobs) β
β src-tauri/src/error.rs β AppError + AppResult<T> β
β src-tauri/src/commands/ β
β ββ files.rs open / save / read-dir / pick-dir β
β ββ remark.rs process_markdown / process_batch (comrak) β
β ββ batch.rs scan / start / cancel / results / export β
β ββ plugins.rs list / read / readPath / write / pickDir β
β src-tauri/src/ast.rs the structured-doc-tree converter β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The two backends produce byte-equivalent JSON for the same input
(verified by scripts/roundtrip-verify.cjs for Electron and 31
#[test] cases in src-tauri/src/ast.rs for Rust).
remark-desk/
βββ packages/
β βββ remark/ β (upstream) unified markdown processor
β βββ remark-cli/ β (upstream) CLI wrapper
β βββ remark-parse/ β (upstream) mdast parser
β βββ remark-stringify/ β (upstream) mdast serializer
β βββ remark-gui/ β **Electron desktop app** (the main work)
β βββ electron/ main + preload + IPC handlers
β βββ src/ React UI (App, Editor, Preview, β¦)
β βββ scripts/ build, test, generate-icons
β βββ locales/ zh-CN strings
β βββ build/ icon.svg β .ico / .icns / .png
β βββ styles/ Tailwind + custom CSS
β βββ release/ built .exe (gitignored)
βββ remark_Rust/ β **Tauri 2 desktop port** (alternative build)
β βββ src/ React UI (mirrors packages/remark-gui/src)
β βββ src-tauri/ Rust backend
β β βββ src/ main + lib + state + error + commands/
β β βββ icons/ generated from remark-gui's icon.svg
β β βββ tauri.conf.json productName, identifier, nsis target
β βββ target/ built .exe (gitignored)
βββ progress.md β session-by-session working notes
βββ package.json β pnpm workspace root
βββ .gitignore β ignores node_modules / release / target / dist
βββ readme.md β this file
The Electron build outputs a single .exe (~75 MB) into
packages/remark-gui/release/0.1.0/Remark 0.1.0.exe.
# from the monorepo root
npm install
npm run --workspace=remark-gui build # vite + tsc
npm run --workspace=remark-gui dist:win:portable
# β packages/remark-gui/release/0.1.0/Remark 0.1.0.exeFor a dev iteration loop, use npm start (or npm run --workspace=remark-gui start).
The Tauri build outputs a single .exe (~16 MB) into
remark_Rust/src-tauri/target/release/remark-rust.exe.
Prerequisites (one-time, only on Windows):
- Rust toolchain (
rustupβcargo+rustc) - MSVC C++ build tools (Visual Studio 2022 "Desktop development with C++" or Build Tools for Visual Studio)
# from the monorepo root
npm install
cd remark_Rust
# generate platform icons from src-tauri/icons/icon.svg (one-time)
npx tauri icon src-tauri/icons/icon.png
# dev mode (hot reload)
npm run tauri:dev
# production build
npm run tauri:build
# β remark_Rust/src-tauri/target/release/remark-rust.exeTip β icon cache gotcha: Tauri 2's
tauri-winrescaches the icon resource by hashingresource.rc. If you regenerate icons withnpx tauri iconbut the.rccontent (absolute path to the.ico) doesn't change, the.exemay still embed the old icon. If that happens, runcargo clean --release -p remark-rustto force re-linking, then re-build.
# Workspace-level commands
npm run --workspace=remark-gui start # Electron dev (hot reload)
npm run --workspace=remark-gui build # type-check + Vite build
npm run --workspace=remark-gui dist:win # Windows portable
cd remark_Rust
npm run tauri:dev # Tauri dev (hot reload)
npm run tauri:build # Tauri portableThe two builds share src/ patterns: components, stores, hooks,
locales. If you change a component, copy it from remark-gui/src/ to
remark_Rust/src/ (or write a tiny script to do it). See
remark_Rust/README.md for the file-by-file map.
# Round-trip converter tests (md β json β md β json == json)
node scripts/roundtrip-verify.cjs # 20 cases
node scripts/test-direction-toggle-fix.cjs # 27 cases (rapid toggle)
node scripts/test-content-format-fix.cjs # lenient input
# Tauri-side: cargo test (in remark_Rust/src-tauri/)
cd remark_Rust/src-tauri
cargo test --lib # 31 casesAll test scripts test production code paths, not parallel implementations.
Both builds target sub-second interactive feedback on typical
documents. Numbers below are measured on a Windows 11 / i5-12400
machine, single-file .md around 5 KB, 30+ headings, 1 table, 1
code block, 1 list.
| Metric | Electron | Tauri 2 |
|---|---|---|
| Cold start (window β ready) | ~1.5 s | ~0.3 s |
| Single-file conversion (md β json) | ~40 ms | ~30 ms |
| 100-file batch (concurrency 4) | ~6 s | ~5 s |
| Idle RAM (after window open) | ~180 MB | ~30 MB |
| Portable binary size | ~75 MB | ~16 MB |
| Renderer main bundle | ~1.1 MB (gz 366 KB) | ~1.1 MB (gz 366 KB) |
The Tauri build wins on every dimension that depends on the runtime
(the shell, the embedded webview, the IPC bridge) because the
heavy lifting (parsing markdown) is already pure-Rust via comrak.
The renderer bundle is the same in both builds.
These are intentional in v0.1.0, not bugs:
- No real-time collaboration. Single-user, single-device.
- No remote sync. Fully offline. Your markdown never leaves the machine.
- Tauri build: plugin loading is display-only. The Tauri backend
uses
comrak's built-in extensions (tables, strikethrough, task lists, autolinks, footnotes). For 150+ remark plugins, use the Electron build. - Tables with merged cells are not preserved. GFM tables with
colspan/rowspanare rendered cell-by-cell after a single round-trip; the merge is lost. .remarkrcwalking is per-file-directory only. No per-folder config switch in the GUI yet (the loader walks up to the repo root on its own).
Tentative plans, in priority order:
- Mermaid diagram preview (Electron build; via
remark-mermaidplugin) - LaTeX math rendering (via
remark-math+ KaTeX) - Outline pane (left sidebar TOC, click to jump)
- Custom CSS injection for the preview (theme override)
- Per-folder
.remarkrcswitcher in the GUI - Find & replace across headings
- Word/character count per heading (reuses the
markdownToDocumentAST) - Plugin search in the marketplace (currently category-bucketed only)
- i18n: ja-JP, en-US (currently only zh-CN)
If you have a request, please open an issue on github.com/Dannykey/remark-desk/issues.
Q: Is this a fork of remarkjs/remark?
A: It's a monorepo that contains the upstream remark packages
(packages/remark/, packages/remark-cli/, etc., unmodified) plus
two new directories on top: packages/remark-gui/ (Electron) and
remark_Rust/ (Tauri 2). The upstream packages are vendored, not
forked, and you can pull new versions of remark at any time.
Q: Can I use the converter without the GUI?
A: Yes. The two functions markdownToDocument(md: string): DocTree
and jsonToMarkdown(doc: DocTree): string are the public API. They
live in:
packages/remark-gui/electron/ipc/processor.ts(TypeScript)remark_Rust/src-tauri/src/ast.rs(Rust)
Both implementations are byte-equivalent on the supported input shapes (verified by 20 + 31 round-trip test cases).
Q: Why two builds?
A: Electron gives you the full 150+ plugin ecosystem at the cost
of a 75 MB binary. Tauri gives you a 16 MB binary with
sub-second startup but only CommonMark + GFM built-ins. Pick the
one that matches your use case; switching is a no-op (just use the
other .exe).
Q: Why "structured document tree" and not "AST"? A: The unified/remark mdast is rich but noisy β it carries source positions, formatting info, and node-type-specific metadata that downstream apps don't need. The structured doc tree is the application-facing shape: stable, round-trip-clean, and easy to hand-edit.
Q: Does it phone home? A: No. Both builds are fully offline. The only network calls are optional GitHub-Actions-CI fetches when you set up CI yourself.
See progress.md for the full session-by-session
working notes. Highlights:
| Version | Date | Highlights |
|---|---|---|
| v0.1.0 | 2026-08-02 | Initial release: two builds, structured-doc-tree converter, full plugin + batch + command palette |
This project is MIT-licensed β see ./license.
This is a fork/derivative of remarkjs/remark, not an independent product. The upstream packages under packages/ are vendored unmodified (see the Lineage section above for the full list). The package.json workspace list in the root still references the upstream packages verbatim; the packages/remark-gui/ entry is the new addition.
For a complete list of bundled third-party software (Electron, Tauri, comrak, React, Vite, etc.) and their licenses, see THIRD_PARTY_LICENSES.md.
Maintainer: Dannykey (921203783@qq.com).
Upstream authors: Titus Wormer (original remark), the unified collective, and the 100+ contributors of the remark ecosystem.
remark-desk is a desktop GUI layer on top of remark. The core markdown processing is upstream's work; what this repo adds is the desktop shell (Electron + Tauri), the React UI, the IPC layer, the plugin manager, the batch processor, and the structured document tree converter (the markdownToDocument / jsonToMarkdown pair in electron/ipc/processor.ts and its Rust mirror in src-tauri/src/ast.rs).
If you find this useful, please consider sponsoring the unified collective β they maintain the markdown ecosystem this project is built on.
remark-desk is a derivative of the remark monorepo, customised with two side-by-side desktop implementations.