wysiwyg-markdown-editor
v0.1.1
Published
Markdown-first WYSIWYG React editor with WYSIWYG/Markdown/HTML view modes
Maintainers
Readme
wysiwyg-markdown-editor
A production-quality, Markdown-first WYSIWYG editor for React. Markdown is the canonical storage format — every edit round-trips cleanly through Markdown.
Features
- Three view modes: WYSIWYG (rich text), Markdown source, and HTML output
- Strictly Markdown-compatible formatting: bold, italic, strikethrough, headings, lists, blockquotes, code blocks, links, images, horizontal rules
- Controlled & uncontrolled API modes
- Image upload callback
- HTML sanitization (XSS-safe by default via
sanitize-html) - Clean, accessible toolbar with keyboard shortcut hints
- Full TypeScript types
- ESM + CJS builds, separate CSS file
Installation
npm install wysiwyg-markdown-editorPeer dependencies (must be installed separately):
npm install react react-domQuick Start
import { useState } from 'react'
import { MarkdownMailerEditor } from 'wysiwyg-markdown-editor'
import 'wysiwyg-markdown-editor/dist/index.css'
function App() {
const [markdown, setMarkdown] = useState('# Hello world')
return (
<MarkdownMailerEditor
valueMarkdown={markdown}
onChangeMarkdown={setMarkdown}
/>
)
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| valueMarkdown | string | — | Controlled mode: the current Markdown value |
| defaultMarkdown | string | '' | Uncontrolled mode: the initial Markdown value |
| onChangeMarkdown | (md: string) => void | required | Called on every content change |
| onChangeHtml | (html: string) => void | — | Also receive the sanitized HTML on change |
| placeholder | string | — | Placeholder shown in empty editor |
| readOnly | boolean | false | Disables editing |
| className | string | — | Extra class on the root element |
| toolbar | 'basic' \| 'full' | 'full' | 'basic' shows fewer toolbar options |
| viewMode | 'wysiwyg' \| 'markdown' \| 'html' | — | Controlled view mode |
| onViewModeChange | (mode: ViewMode) => void | — | Called when user switches view |
| sanitizeHtmlOutput | boolean | true | Sanitize HTML output via sanitize-html |
| sanitizerConfig | SanitizerConfig | — | Custom sanitizer config (merged with defaults) |
| onImageUpload | (file: File) => Promise<{ url: string; alt?: string }> | — | Image upload handler |
View Modes
WYSIWYG (default)
Full rich-text editing experience with the toolbar. All formatting is constrained to what Markdown supports.
Markdown
Editable monospace textarea showing the raw Markdown. Changes here sync back to the WYSIWYG editor.
HTML
Read-only view of the sanitized HTML output with a copy button.
Image Upload
async function handleImageUpload(file: File) {
const formData = new FormData()
formData.append('image', file)
const res = await fetch('/api/upload', { method: 'POST', body: formData })
const { url } = await res.json()
return { url, alt: file.name }
}
<MarkdownMailerEditor
onChangeMarkdown={setMarkdown}
onImageUpload={handleImageUpload}
/>Utility Exports
The package also exports the underlying utility functions:
import { markdownToHtml, htmlToMarkdown, sanitizeHtml } from 'wysiwyg-markdown-editor'
// Convert Markdown to HTML (with XSS sanitization by default)
const html = markdownToHtml('**bold** and _italic_')
// Convert HTML to Markdown (best-effort, via turndown)
const md = htmlToMarkdown('<strong>bold</strong>')
// Sanitize HTML with default allowed tags
const safe = sanitizeHtml('<script>alert(1)</script><p>safe</p>')Styling
Import the CSS once in your app entry point:
import 'wysiwyg-markdown-editor/dist/index.css'All styles use the .wmme- prefix and CSS custom properties for easy theming:
.wmme-root {
--wmme-border: #e0e0e0;
--wmme-active-bg: #e8f0fe;
--wmme-active-color: #1a73e8;
--wmme-hover-bg: #f5f5f5;
}Architecture
| Layer | Technology |
|---|---|
| Rich editor | TipTap v2 (ProseMirror) |
| MD → editor | markdown-it → HTML → editor.commands.setContent() |
| Editor → MD | Custom JSON walker (serializer.ts) |
| MD → HTML display | markdown-it + sanitize-html |
| HTML → MD import | turndown |
| Build | tsup (ESM + CJS + types + CSS) |
| Tests | vitest |
Markdown is the canonical format. The editor never stores HTML or ProseMirror JSON as the source of truth — only Markdown.
Development
# Install dependencies
npm install
# Run tests
npm test
# Watch mode
npm run test:watch
# Build library
npm run build
# Run demo
cd demo && npm install && npm run devReleasing
This package uses release-it with Keep a Changelog format.
Prerequisites (one-time)
# Authenticate with npm
npm login
# Set a GitHub personal access token (repo scope) for GitHub Releases
export GITHUB_TOKEN=ghp_...Workflow
1. Document your changes in CHANGELOG.md under ## [Unreleased]:
## [Unreleased]
### Added
- Support for tables
### Fixed
- Bold escaping in nested lists2. Preview the release (no side effects):
npm run release:dry3. Publish:
npm run release
# Prompts for: patch / minor / major
# Then automatically:
# → runs tests + build
# → moves [Unreleased] → [x.y.z] in CHANGELOG.md
# → git commit + tag vx.y.z
# → git push --follow-tags
# → creates GitHub Release
# → npm publishVersioning rules (semver)
| Change | Version bump | Example |
|---|---|---|
| Bug fixes, doc updates | patch | 0.1.0 → 0.1.1 |
| New features, new props | minor | 0.1.0 → 0.2.0 |
| Breaking API changes | major | 0.1.0 → 1.0.0 |
License
MIT
