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.