A fast, minimal PyQt6 Markdown editor with live preview, HTML/PDF export, and a clean, SOLID-friendly architecture.
Designed to stay small, deterministic, and easy to package.
Owner-led governance; contributions welcome (see CONTRIBUTING).
- Debounced, side-by-side Markdown preview while you type.
- Dark-mode friendly styling.
- Optional Qt WebEngine rendering (automatically disabled in tests/headless runs).
Powered by:
python-markdown- Extensions:
extrafenced_codecodehilitetocsane_listssmarty
- Optional:
pymdown-extensions(e.g. math/LaTeX viaarithmatex)
- Open/Save
.mdfiles - Atomic writes via
QSaveFile - UTF-8 encoding
- Drag & drop support
- Recent files persisted via
QSettings
PyMarkdownEditor includes selection-aware behaviour for common Markdown actions.
When text is selected, Bold and Italic behave as toggles:
- If the selection is not already wrapped, it is wrapped with the correct Markdown.
- If the selection is already wrapped, the wrapping is removed.
Examples:
- Selecting
hellothen pressing Ctrl+B β**hello** - Selecting
**hello**then pressing Ctrl+B βhello - Selecting
hellothen pressing Ctrl+I β_hello_ - Selecting
_hello_then pressing Ctrl+I βhello
When you paste a URL (Ctrl+V) the editor detects whether you have selected text:
-
No selection + URL in clipboard
- Inserts:
[](https://example.com) - Cursor is placed inside the
[]so you can type the label immediately.
- Inserts:
-
Selection + URL in clipboard
- Converts to:
[selected text](https://example.com)
- Converts to:
This makes link creation fast without opening a dialog.
- Ctrl+B β toggle bold on selected text (
**...**) - Ctrl+I β toggle italic on selected text (
_..._) - Ctrl+E β insert a new fenced code block on a new line
Exporters are strategy-based and registered in an ExporterRegistry.
Saves the preview HTML as-is.
Uses QTextDocument + QPrinter (A4, 12.7mm margins).
Uses QWebEngineView for closer WYSIWYG output.
Automatically disabled in:
- pytest
- headless environments
- when
PYMD_DISABLE_WEBENGINE=1
PyMarkdownEditor includes a first-class plugin architecture.
Plugins are discovered via:
- Built-in plugins
- Python entry points
Discovery is deterministic.
Recommended host wiring:
plugin_manager.set_api(app_api)
plugin_manager.reload()
plugin_manager.on_app_ready()Hooks (optional):
on_load(api)β runs once per processactivate(api)β runs when enabledon_ready(api)β runs once per activation sessiondeactivate()β runs when disabled
Plugin state is persisted via IPluginStateStore.
Built-in plugins:
- Appear in the Plugins UI
- Can be enabled/disabled
- Never crash discovery if missing
SOLID-leaning, layered design:
- Domain layer (interfaces, models)
- Services layer (rendering, exporters, plugins, config)
- UI layer (thin Qt window + dialogs)
- Dependency injection container
- Plugin lifecycle manager
- Clear boundaries
- No UI in core
- Strategy-based exporters
- Explicit plugin lifecycle
- Deterministic startup
- Test-safe QtWebEngine behaviour
# 1) Create virtual environment
python -m venv .venv
# Windows
. .venv/Scripts/activate
# macOS / Linux
source .venv/bin/activate
# 2) Install dependencies
pip install -r requirements.txt
# 3) Run
python -m pymdPython 3.10+ recommended.
pip install py-markdown-editor
python -m pymdFuture console entry:
pymdRuntime:
PyQt6>=6.6Markdown>=3.5Pygments>=2.17pymdown-extensions
Optional (WebEngine PDF export):
PyQt6-WebEngine
- New / Open / Save / Save As
- Toggle wrap
- Toggle preview
- Quit
- Ctrl+B β Bold toggle (selection-aware)
- Ctrl+I β Italic toggle (selection-aware)
code(inline)# H1## H2- list
- Ctrl+E β Insert fenced code block on a new line
- Insert link (dialog)
- Insert image
- Insert table
- Find / Replace
- About dialog
All actions are exposed via toolbar + menus.
.
βββ pymd/
β βββ app.py
β βββ di/
β β βββ container.py
β βββ plugins/
β β βββ discovery.py
β β βββ manager.py
β β βββ state.py
β β βββ builtin/
β βββ services/
β β βββ config/
β β βββ exporters/
β β βββ file_service.py
β β βββ markdown_renderer.py
β β βββ settings_service.py
β β βββ ui/
β β βββ main_window.py
β β βββ dialogs
β β βββ adapters
β β βββ ports
β β βββ presenters
β β βββ commands
β βββ domain/
β βββ interfaces.py
β βββ models.py
βββ tests/
βββ pyproject.toml
βββ requirements.txt
βββ README.md
Install dev dependencies:
pip install -r dev-requirements.txtRun:
pytest --cov=pymd --cov-report=term-missing --timeout=120Includes:
- pytest
- pytest-qt
- pytest-cov
- pytest-timeout
- ruff
WebEngine is automatically disabled during pytest to prevent Chromium aborts.
pip install pyinstaller
pyinstaller -n PyMarkdownEditor --windowed --onefile \
-i NONE -s -y pymd/__main__.pyGitHub Actions:
- Windows / Linux / macOS builds
- Hidden imports collected
- Artifacts attached to tagged releases
- Runs on push/PR
- Ruff + pytest + coverage
- Fast path for dev branches
- Full OS matrix for master PRs
Triggered by semver tag:
vMAJOR.MINOR.PATCH
Builds cross-platform binaries and attaches to GitHub Release.
Triggered by version tags.
- Pre-releases β TestPyPI
- Final releases β PyPI
- Version verified against
pyproject.toml
- Ensure target folder writable
- Verify Qt Print/WebEngine libraries installed
-
Ensure matching Qt libraries
-
Or disable:
PYMD_DISABLE_WEBENGINE=1
pip install pymdown-extensionsWe welcome issues and PRs.
See:
- CONTRIBUTING.md
- LICENSE (Apache-2.0)
Owner-led governance means:
- Maintainer steers architecture
- Small, focused scope
- Clean, maintainable contributions
ruff format .
ruff check .
pytest --cov=pymd --cov-report=term-missing --timeout=120Apache-2.0 Β© 2025 clintonshane84 See LICENSE.