markdown-editor
Markdown editor integration — django-markdownx, markdown editors, preview rendering
You are a markdown editor specialist. Integrate and configure markdown editors for web applications.
Editor Options
| Editor | Type | Framework | Features | |--------|------|-----------|----------| | django-markdownx | Django app | Django | Upload, preview, admin integration | | EasyMDE | JS widget | Any | Simple, toolbar, preview | | CodeMirror | JS editor | Any | Extensible, syntax highlighting | | Monaco Editor | JS editor | Any | VS Code engine | | ProseMirror | JS editor | Any | Customizable WYSIWYG | | Toast UI Editor | JS editor | Any | Markdown + WYSIWYG modes | | MDX Editor | React | Next/React | MDX support | | Milkdown | JS editor | Any | Plugin-based, Prosemirror |
django-markdownx
Installation
pip install django-markdownx
settings.py
INSTALLED_APPS = [
'markdownx',
# ...
]
# Markdownx config
MARKDOWNX_MARKDOWN_EXTENSIONS = [
'markdown.extensions.extra',
'markdown.extensions.codehilite',
'markdown.extensions.toc',
'markdown.extensions.sane_lists',
'markdown.extensions.nl2br',
]
MARKDOWNX_MARKDOWN_EXTENSION_CONFIGS = {
'markdown.extensions.codehilite': {
'css_class': 'highlight',
'guess_lang': True,
},
}
MARKDOWNX_IMAGE_MAX_SIZE = {
'size': (1200, 1200),
'quality': 90,
}
MARKDOWNX_MEDIA_PATH = 'uploads/markdown/' # date formatted
MARKDOWNX_UPLOAD_CONTENT_TYPES = ['image/jpeg', 'image/png', 'image/webp']
MARKDOWNX_UPLOAD_MAX_SIZE = 5 * 1024 * 1024 # 5MB
urls.py
from django.urls import path, include
urlpatterns = [
path('markdownx/', include('markdownx.urls')),
]
models.py
from django.db import models
from markdownx.models import MarkdownxField
from markdownx.utils import markdownify
class Article(models.Model):
title = models.CharField(max_length=200)
content = MarkdownxField()
@property
def rendered_content(self):
return markdownify(self.content)
admin.py
from django.contrib import admin
from markdownx.admin import MarkdownxModelAdmin
from .models import Article
admin.site.register(Article, MarkdownxModelAdmin)
template.html
{% load markdownx %}
<form method="post">
{% csrf_token %}
{{ form.content }}
</form>
{# Include editor CSS + JS #}
{% block extra_head %}
{{ form.media }}
{% endblock %}
{# Render markdown in template #}
{{ article.content|markdownify }}
Custom preview template
# settings.py
MARKDOWNX_CONTENT_TEMPLATE = 'markdownx/custom_preview.html'
{# markdownx/custom_preview.html #}
{% load markdownx %}
<div class="markdown-preview markdownx-preview">
{{ html }}
</div>
EasyMDE
<!-- EasyMDE standalone -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
<textarea id="editor"></textarea>
<script>
const easyMDE = new EasyMDE({
element: document.getElementById('editor'),
autoDownloadFontAwesome: true,
spellChecker: false,
sideBySideFullscreen: false,
toolbar: ['bold', 'italic', 'heading', '|', 'quote',
'unordered-list', 'ordered-list', '|',
'link', 'image', '|', 'preview', 'guide'],
previewRender: function(plainText) {
// Custom renderer via API
return marked.parse(plainText);
},
renderingConfig: {
codeSyntaxHighlighting: true,
hljs: hljs
}
});
</script>
Markdown + Django Admin
# Custom admin form with markdown preview
from django import forms
from markdownx.widgets import MarkdownxWidget
class ArticleAdminForm(forms.ModelForm):
class Meta:
model = Article
fields = '__all__'
widgets = {
'content': MarkdownxWidget(attrs={
'rows': 20,
'class': 'markdown-editor',
'data-image-upload': True
})
}
class ArticleAdmin(admin.ModelAdmin):
form = ArticleAdminForm
readonly_fields = ['rendered_preview']
def rendered_preview(self, obj):
if obj.pk:
from markdownx.utils import markdownify
return markdownify(obj.content)
return ''
rendered_preview.short_description = 'Preview'
rendered_preview.allow_tags = True
API-based Rendering
# DRF endpoint for preview
from rest_framework.decorators import api_view
from rest_framework.response import Response
import markdown
@api_view(['POST'])
def render_preview(request):
text = request.data.get('text', '')
html = markdown.markdown(text, extensions=[
'extra', 'codehilite', 'toc', 'sane_lists'
])
return Response({'html': html})
// Client-side preview (debounced)
const preview = document.getElementById('preview');
let debounceTimer;
editor.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
const response = await fetch('/api/preview/', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: editor.value })
});
const data = await response.json();
preview.innerHTML = data.html;
}, 300);
});
Custom Widget
from django import forms
class MarkdownEditorWidget(forms.Textarea):
template_name = 'widgets/markdown_editor.html'
class Media:
css = { 'all': ['css/editor.css'] }
js = ['js/editor.js', 'js/marked.min.js']
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context['widget']['rows'] = 20
return context
{# widgets/markdown_editor.html #}
<div class="markdown-editor-wrapper">
<div class="editor-toolbar">
<button type="button" data-cmd="bold"><b>B</b></button>
<button type="button" data-cmd="italic"><i>I</i></button>
<button type="button" data-cmd="code"></></button>
<button type="button" data-cmd="preview">👁 Preview</button>
</div>
<div class="editor-main">
<textarea name="{{ widget.name }}"
class="markdown-textarea"
{% include "django/forms/widgets/attrs.html" %}>
{{ widget.value }}
</textarea>
<div class="markdown-preview" id="preview-{{ widget.name }}"></div>
</div>
</div>
Security
# Sanitize rendered HTML (prevent XSS)
import bleach
ALLOWED_TAGS = ['p', 'br', 'strong', 'em', 'a', 'code', 'pre',
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'blockquote', 'hr', 'table', 'thead', 'tbody', 'tr', 'th', 'td',
'img', 'sup', 'sub', 'del', 'ins']
ALLOWED_ATTRIBUTES = {
'a': ['href', 'title', 'rel'],
'img': ['src', 'alt', 'title', 'width', 'height'],
'*': ['class', 'id'],
}
def safe_markdown(text: str) -> str:
html = markdown.markdown(text, extensions=['extra', 'codehilite'])
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)