Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

panel-mde

CI conda-forge pypi-version python-version

A rich text editor for Panel whose value is markdown, powered by EasyMDE.

pn.widgets.TextEditor is Quill-based and gives you HTML, with no way to write a table. MarkdownEditor reads and writes markdown source instead, so the editor, the rendered output and anything else that touches the text all agree on one format.

Features

  • Markdown in, markdown out - value is the markdown source, synced on every keystroke
  • A toolbar with tables - bold, italic, headings, lists, links, images, tables, undo/redo and more
  • Caret-safe programmatic writes - appending to value from Python keeps the caret, selection, scroll position and undo history intact
  • Live preview - optional, rendered by Panel's own markdown-it pane so it matches every other markdown surface in your app
  • Self-contained - the script, stylesheet and icons ship with the package, so nothing is fetched from a third-party CDN at runtime
  • Works in shadow roots and dialogs - inline SVG icons need no @font-face, and the editor re-measures itself when it is attached after render

Installation

Install via pip:

pip install panel-mde

Or via conda:

conda install -c conda-forge panel-mde

Quick Start

import panel as pn
from panel_mde import MarkdownEditor

pn.extension()

editor = MarkdownEditor(
    value="# Sprint notes\n\nMarkdown **in**, markdown **out**.",
    preview=True,
    status_bar=True,
    sizing_mode="stretch_width",
    height=430,
)

editor.servable()

Quick Start Example

Usage Examples

Two-way value sync

value updates on every keystroke, so a watcher (or a reactive expression) sees the text as it is typed:

import panel as pn
from panel_mde import MarkdownEditor

pn.extension()

editor = MarkdownEditor(value="# Title", height=300)
words = pn.pane.Markdown(editor.param.value.rx.pipe(lambda md: f"{len(md.split())} words"))

pn.Row(editor, words).servable()

Pass on_keyup=False to defer value until the editor loses focus or the user presses Ctrl/Cmd+Enter, the same contract as pn.widgets.TextInput. value_input always tracks the live text.

Appending while the user types

Writing value from Python applies the smallest edit that produces the new text, so an append from an upload flow does not disturb the person editing:

import panel as pn
from panel_mde import MarkdownEditor

pn.extension()

editor = MarkdownEditor(value="# Trip report\n\nWe left early.", height=300)
upload = pn.widgets.FileInput(accept=".png,.jpg")


def add_image(event):
    url = store(event.new)  # your own storage
    editor.value += f"\n\n![{upload.filename}]({url})\n"


upload.param.watch(add_image, "value")

pn.Row(editor, upload).servable()

The caret stays where it was, a selection survives, the view does not scroll and undo still walks back through what the user typed.

Choosing the toolbar

from panel_mde import MarkdownEditor

MarkdownEditor(
    toolbar=["bold", "italic", "heading", "|",
             "unordered-list", "ordered-list", "table", "|",
             "link", "image", "|", "undo", "redo"],
)

toolbar=True renders the set above, toolbar=False hides the toolbar, and "|" inserts a separator. panel_mde.TOOLBAR_ACTIONS lists every available action:

bold, italic, strikethrough, heading, heading-smaller, heading-bigger, heading-1, heading-2, heading-3, code, quote, unordered-list, ordered-list, check-list, clean-block, link, image, table, horizontal-rule, undo, redo, preview.

An unknown action raises ValueError rather than silently disappearing.

Live preview

from panel_mde import MarkdownEditor

MarkdownEditor(value="# Title", preview=True, preview_location="bottom", height=500)

The preview renders through pn.pane.Markdown, so it uses markdown-it with your app's own extensions. EasyMDE's bundled marked.js preview is never used, because it renders subtly differently from every other markdown surface in a Panel app. Add "preview" to the toolbar to let the user toggle it, or restyle it by assigning your own pane to preview_pane.

Inside a dialog

CodeMirror measures a detached element as zero-size, which is why some editors come up blank inside a dialog. MarkdownEditor re-measures on render, layout and resize, so it needs no help from the surrounding app:

import panel as pn
from panel_mde import MarkdownEditor

pn.extension()

editor = MarkdownEditor(value="# Notes", height=300)
card = pn.Card(editor, title="Notes", collapsed=True)

card.servable()

API Reference

MarkdownEditor

Parameter Type Default Description
value str "" The markdown source. Updates per keystroke unless on_keyup is disabled
value_input str "" The markdown source, always updated per keystroke
on_keyup bool True Whether value updates per keystroke or on blur
toolbar bool | list True The formatting toolbar: True, False or a list of actions
preview bool False Show the live preview
preview_location str "right" "right" or "bottom"
preview_pane Markdown None The pane rendering the preview, created on demand
autofocus bool False Focus the editor on initial render
disabled bool False Make the editor read-only
placeholder str "" Text shown while the editor is empty
line_numbers bool False Show line numbers in the gutter
line_wrapping bool True Wrap long lines instead of scrolling horizontally
indent_with_tabs bool False Indent with a tab character rather than spaces
tab_size int 2 Spaces per indent level
spellcheck bool True Use the browser's native spell checker
status_bar bool False Show the line, word and cursor counts
unordered_list_style str "-" Bullet marker: "-", "*" or "+"

Assets

Everything the editor needs is served by the Panel server itself. The EasyMDE bundle is compiled into the package, its stylesheet is vendored, and the toolbar icons are inline SVG. EasyMDE's two runtime downloads are both disabled: the Font Awesome icon font (replaced by the inline SVG) and the spell-check dictionaries (replaced by the browser's own checker). Inline SVG inherits currentColor, so the icons also render correctly inside the shadow root Panel gives every component, with no document-level @font-face registration.

Development

This project is managed by pixi.

Setup

git clone https://github.com/panel-extensions/panel-mde
cd panel-mde

pixi run pre-commit-install
pixi run postinstall
pixi run compile
pixi run test

Compiling the bundle

The ESM in src/panel_mde/models/ imports EasyMDE, which panel compile bundles into src/panel_mde/dist/:

pixi run compile        # one-off build
pixi run compile-dev    # rebuild on change

The vendored EasyMDE stylesheet is generated from the pinned npm version, with its @font-face rules and icon-font references stripped:

pixi run vendor-css

Testing

pixi run test                        # unit tests
pixi run -e test-ui test-ui          # Playwright UI tests

Examples

panel serve examples/apps/notes.py examples/apps/image_upload.py --dev

Documentation

The documentation is built with Zensical:

pixi run -e docs docs-serve    # live-reloading preview
pixi run -e docs docs-build    # build into builtdocs/

Pre-commit

Before committing the first time please install pre-commit:

pip install pre-commit
pre-commit install

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

See LICENSE file for details.

Release files for panel-mde 0.1.0a1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for panel-mde 0.1.0a1
File Size Uploaded
panel_mde-0.1.0a1.tar.gz 387.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for panel-mde 0.1.0a1
File Interpreter ABI Platform
panel_mde-0.1.0a1-py3-none-any.whl Python 3 none any Details

Total release size: 521.8 kB

Release files / panel_mde-0.1.0a1.tar.gz

Download URL panel_mde-0.1.0a1.tar.gz
Size 387.1 kB
Tags Source
SHA-256 checksum
How to use checksums
5f1a85ed7bab9a9d4110f0261deb05e2911e05d167f8912eaa96443be392fca2
BLAKE2b-256 checksum
How to use checksums
f607f8917923a05fc3486c9a1ff82498c02392af89855d4eb9aa7a4546a3d536
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log

Release files / panel_mde-0.1.0a1-py3-none-any.whl

Download URL panel_mde-0.1.0a1-py3-none-any.whl
Size 134.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
80cc9f9a37d0163fa60a1655db3754ccb5c7b5e7ff7c24aecd6b240905726e23
BLAKE2b-256 checksum
How to use checksums
df114feb2c2975979551251bfa6b2750c8f8ca0811e9157211b665dc3de7b87b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 21, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page