You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何自定义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)
        
        # 图片转换:![描述](url) → <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>
          - 图片:![图片描述](https://example.com/img.jpg)<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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 17:03:30