Metadata-Version: 2.4
Name: panel-mde
Version: 0.1.0a3
Summary: Markdown editor widget for Panel.
Project-URL: Homepage, https://github.com/panel-extensions/panel-mde
Project-URL: Source, https://github.com/panel-extensions/panel-mde
Author-email: Philipp Rudiger <philipp.jfr@gmail.com>
Maintainer-email: Philipp Rudiger <philipp.jfr@gmail.com>
License: BSD
License-File: LICENSE.txt
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: BSD License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Requires-Dist: packaging
Requires-Dist: panel>=1.9.0
Provides-Extra: dev
Requires-Dist: mkdocstrings[python]; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: pytest-asyncio; extra == 'dev'
Requires-Dist: pytest-rerunfailures; extra == 'dev'
Requires-Dist: pytest-xdist; extra == 'dev'
Requires-Dist: watchfiles; extra == 'dev'
Requires-Dist: zensical; extra == 'dev'
Provides-Extra: mypy
Requires-Dist: mypy; extra == 'mypy'
Requires-Dist: types-requests; extra == 'mypy'
Requires-Dist: typing-extensions; extra == 'mypy'
Description-Content-Type: text/markdown

# panel-mde

[![CI](https://img.shields.io/github/actions/workflow/status/panel-extensions/panel-mde/test.yml?style=flat-square&branch=main)](https://github.com/panel-extensions/panel-mde/actions/workflows/test.yml)
[![conda-forge](https://img.shields.io/conda/vn/conda-forge/panel-mde?logoColor=white&logo=conda-forge&style=flat-square)](https://prefix.dev/channels/conda-forge/packages/panel-mde)
[![pypi-version](https://img.shields.io/pypi/v/panel-mde.svg?logo=pypi&logoColor=white&style=flat-square)](https://pypi.org/project/panel-mde)
[![python-version](https://img.shields.io/pypi/pyversions/panel-mde?logoColor=white&logo=python&style=flat-square)](https://pypi.org/project/panel-mde)

A rich text editor for [Panel](https://panel.holoviz.org) whose value **is** markdown, powered by [EasyMDE](https://github.com/Ionaru/easy-markdown-editor).

`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
- **Paste and drop uploads** - hand pasted or dropped files to a Python handler, return a URL and it lands in the document as an image, a media tag or a link
- **Live preview** - optional, rendered by Panel's own markdown-it pane so it matches every other markdown surface in your app, with a draggable divider
- **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:

```bash
pip install panel-mde
```

Or via conda:

```bash
conda install -c conda-forge panel-mde
```

## Quick Start

```python
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](https://raw.githubusercontent.com/panel-extensions/panel-mde/main/docs/assets/images/quick-start.png)

## 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:

```python
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 <kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Enter</kbd>, 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:

```python
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.

### Pasting and dropping files

An `upload_handler` receives every file pasted or dropped into the editor, stores it wherever you like and returns the URL it is served from:

```python
from pathlib import Path

import panel as pn

from panel_mde import MarkdownEditor

pn.extension()

MEDIA = Path("media")
MEDIA.mkdir(exist_ok=True)


def store(file):
    # file.name, file.mime_type, file.size and file.data (bytes)
    (MEDIA / file.name).write_bytes(file.data)
    return f"/media/{file.name}"


editor = MarkdownEditor(
    upload_handler=store,
    accepted_filetypes=["image/*", "video/*", "audio/*"],
    height=400,
)

pn.serve(editor, static_dirs={"media": str(MEDIA)})
```

The URL is formatted by the type of the file: `![name](url)` for an image, `<video src="url" controls></video>` or `<audio src="url" controls></audio>` for media, and `[name](url)` for anything else. Return a markdown or HTML snippet instead to control the insertion yourself, or `None` to reject the file. The handler may be a coroutine function, and `value` only changes once it returns: until then the editor marks the spot the file will land in, and the marker follows the text the user keeps typing.

`accepted_filetypes` and `max_upload_size` (10MB by default) are applied in the browser and re-applied on the server, so a rejected file never reaches the handler.

### Choosing the toolbar

```python
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

```python
from panel_mde import MarkdownEditor

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

The editor and the preview split the space evenly, and the divider between them can be dragged to give either side more room (double-click resets it). 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:

```python
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 |
| `upload_handler` | callable | `None` | Called with an `UploadedFile` when a file is pasted or dropped; returns a URL, a snippet or `None` |
| `accepted_filetypes` | list | `[]` | MIME types, wildcards or extensions an upload accepts; empty accepts everything |
| `max_upload_size` | int | `10_000_000` | Largest pasted or dropped file in bytes; `None` disables the check |
| `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](https://pixi.sh).

### Setup

```bash
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/`:

```bash
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:

```bash
pixi run vendor-css
```

### Testing

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

### Examples

```bash
panel serve examples/apps/notes.py examples/apps/uploads.py \
    --static-dirs media=examples/apps/media --dev
```

`uploads.py` stores what you paste or drop in `examples/apps/media`, which is why it needs the static directory.

### Documentation

The documentation is built with [Zensical](https://zensical.org):

```bash
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`:

```bash
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.
