如何自定义Django TextField实现无第三方依赖的内嵌编辑器
Django 自定义 TextField 实现轻量富文本(无第三方编辑器依赖)
以下是完整的实现方案,基于类 Markdown 标记语法,从模型字段、前端输入到后端渲染全流程覆盖:
1. 自定义富文本字段(模型层)
无需修改存储逻辑,只需给 TextField 添加标记转 HTML 的渲染方法,同时指定前端输入组件:
from django.db import models from django.utils.html import escape import re class MarkdownTextField(models.TextField): def formfield(self, **kwargs): # 指定前端自定义输入框组件 kwargs['widget'] = MarkdownTextareaWidget return super().formfield(**kwargs) def render(self, value): if not value: return "" # 先转义特殊字符,防止XSS攻击 value = escape(value) # 标题转换:# 一级标题 → <h1>一级标题</h1> value = re.sub(r'^# (.*)$', r'<h1>\1</h1>', value, flags=re.MULTILINE) value = re.sub(r'^## (.*)$', r'<h2>\1</h2>', value, flags=re.MULTILINE) value = re.sub(r'^### (.*)$', r'<h3>\1</h3>', value, flags=re.MULTILINE) # 粗体/斜体转换 value = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', value) value = re.sub(r'\*(.*?)\*', r'<em>\1</em>', value) # 代码片段转换:```python\n代码``` → 带语法类的代码块 value = re.sub(r'```(\w+)\n(.*?)```', r'<pre><code class="language-\1">\2</code></pre>', value, flags=re.DOTALL) # 链接转换:[文本](url) → <a>标签 value = re.sub(r'\[(.*?)\]\((.*?)\)', r'<a href="\2" target="_blank" rel="noopener">\1</a>', value) # 图片转换: → <img>标签 value = re.sub(r'!\[(.*?)\]\((.*?)\)', r'<img src="\2" alt="\1" style="max-width:100%; height:auto;">', value) # 表格转换:解析|分隔的表格语法 def parse_table(match): rows = [row.strip() for row in match.group(0).split('\n') if row.strip()] if len(rows) < 2: return match.group(0) html = '<table border="1" cellpadding="8" cellspacing="0" style="border-collapse: collapse; margin: 16px 0;">' # 表头行 html += '<thead><tr>' + ''.join([f'<th>{cell.strip()}</th>' for cell in rows[0].split('|') if cell.strip()]) + '</tr></thead>' # 内容行(跳过分隔线行) html += '<tbody>' for row in rows[2:]: html += '<tr>' + ''.join([f'<td>{cell.strip()}</td>' for cell in row.split('|') if cell.strip()]) + '</tr>' html += '</tbody></table>' return html value = re.sub(r'\|.*?\|(\n\|.*?\|)+', parse_table, value, flags=re.DOTALL) # 换行与段落处理:空行分隔段落,普通换行转<br> paragraphs = re.split(r'\n\n+', value) return ''.join([f'<p>{p.replace("\n", "<br>")}</p>' for p in paragraphs if p.strip()])
2. 前端输入 Widget
给用户提供带语法提示的输入框,降低使用门槛:
from django.forms import widgets class MarkdownTextareaWidget(widgets.Textarea): def render(self, name, value, attrs=None, renderer=None): # 默认 attrs 优化:加大输入框高度 default_attrs = {'rows': 15, 'style': 'width:100%; font-family: monospace;'} if attrs: default_attrs.update(attrs) attrs = default_attrs # 语法提示说明 help_html = """ <div style="margin-top:8px; padding:12px; background:#f8f9fa; border-radius:4px; font-size:14px;"> <strong>使用说明:</strong><br> - 标题:# 一级标题 / ## 二级标题 / ### 三级标题<br> - 强调:**粗体文本** / *斜体文本*<br> - 代码:```python\n你的代码```(支持指定语言)<br> - 链接:[显示文本](https://example.com)<br> - 图片:<br> - 表格:<br> | 表头1 | 表头2 |<br> |-------|-------|<br> | 内容1 | 内容2 |<br> - 段落:空行分隔不同段落 </div> """ # 渲染输入框 + 提示 return super().render(name, value, attrs, renderer) + help_html
3. 模型使用示例
直接在模型中替换普通 TextField:
class ForumPost(models.Model): title = models.CharField(max_length=200, verbose_name='帖子标题') content = MarkdownTextField(verbose_name='帖子内容') created_at = models.DateTimeField(auto_now_add=True, verbose_name='发布时间') class Meta: verbose_name = '论坛帖子' verbose_name_plural = verbose_name
4. 视图与模板渲染
在视图中调用字段的render方法转换内容,模板中使用safe过滤器输出 HTML:
# views.py from django.shortcuts import render from .models import ForumPost def post_detail(request, post_id): post = ForumPost.objects.get(id=post_id) # 渲染富文本内容 rendered_content = post.content.render(post.content) return render(request, 'forum/post_detail.html', { 'post': post, 'rendered_content': rendered_content })
模板文件:
<!-- forum/post_detail.html --> <h1>{{ post.title }}</h1> <div class="post-meta">发布于 {{ post.created_at|date:"Y-m-d H:i" }}</div> <div class="post-content"> {{ rendered_content|safe }} </div>
5. 生产环境优化建议
- XSS 防护:已在
render方法开头加入escape处理,确保用户输入的恶意 HTML 被转义。 - 代码高亮:前端引入轻量高亮库(如 Prism.js),只需给页面添加对应脚本即可自动高亮
<code>标签内容。 - 实时预览:可通过前端 JS 实现输入时同步预览,复制后端的正则转换逻辑到 JS 中,监听输入框
input事件实时更新预览区。 - 扩展标记:如需支持列表、引用等,只需在
render方法中添加对应的正则替换规则,比如:# 无序列表:- 列表项 → <ul><li>列表项</li></ul> value = re.sub(r'^- (.*)$', r'<li>\1</li>', value, flags=re.MULTILINE) value = re.sub(r'(<li>.*?</li>)(\n<li>.*?</li>)+', r'<ul>\1\2</ul>', value, flags=re.DOTALL) # 引用:> 引用内容 → <blockquote>引用内容</blockquote> value = re.sub(r'^> (.*)$', r'<blockquote>\1</blockquote>', value, flags=re.MULTILINE)
内容的提问来源于stack exchange,提问作者Bubble
相关产品推荐
相关产品推荐

