本小节将对输入文章的多行文本输入框升级,让其支持MarkDown语法,并使用MarkDown和Flask-PageDown支持富文本文章,此处需要安装的几个新包如下:
- PageDown:使用JS实现的客户端MarkDown到HTML的转换程序;
- Flask-PageDown:Flask包装的PageDown,把PageDown集成到Flask-WTF表单中;
- MarkDown:使用Python实现的服务器端MarkDown到HTML的转换程序;
- Bleach:使用Python实现的HTML清理器。
安装:
pip install flask-pagedown markdown bleach
一. 使用FLask-PageDown
Flask-PageDown定义了一个PageDownField类,作为MarkDown富文本编辑器。这个类和WTForms中的TextAreaField接口一致。使用PageDownField字段之前,先要初始化扩展。
app/__init__.py:初始化Flask-PageDown
...
from flask_pagedown import PageDown
pagedown = PageDown()
def create_app(config_name):
app = Flask(__name__)
...
pagedown.init_app(app)
...
return app
若想把首页中的多行文本输入框转换成MarkDown富文本输入框,PostForm表单中的body字段要进行扩展:
app/main/forms.py:启用MarkDown的文本表单:
class PostForm(FlaskForm):
body = PageDownField("What's on your mind?", validators=[DataRequired()])
submit = SubmitField('Submit')
MarkDown预览使用PageDown库生成,因此需要修改模板。Flask-PageDown简化了这一过程,提供了一个模板宏,从CDN中加载所需文件。
app/index.html:Flask-PageDown模板声明
{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock %}
注: 做了上述修改后,多行文本字段中输入MarkDown格式的文本会立即被渲染成HTML并显示在输入框的下方。
二. 在服务端处理富文本
提交表单后,POST请求只会发送纯MarkDown文本,页面中显示的HTML预览会被丢掉。和表单一起发送生成的HTML预览会有安全隐患,因为攻击者能轻易修改HTML代码,使其和MarkDown源不匹配,然后再提交表单。安全起见,只提交MarkDown源文本,在服务器上使用MarkDown将其转换成HTML。得到HTML后使用Bleach进行清洗,确保其中只包含几个允许使用的HTML标签。
把MarkDown文本转换成HTML的过程可以在_posts.html模板中完成,但这么做效率不高,因为每次渲染模板都要转换一次。为了避免重复工作,我们可以在建博客文章时做一次转换,转换后的博客文章HTML字段代码缓存在POST模型的一个新字段中,在模板中可以直接调用。文章的MarkDown源文本还要保存在数据库中,以防要编辑。
app/models.py:在Post模型中处理MarkDown文本
from markdown import markdown
import bleach
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
body_html = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
@staticmethod
def on_changed_body(target, value, oldvalue, initiator):
allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',
'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',
'h1', 'h2', 'h3', 'p']
target.body_html = bleach.linkify(bleach.clean(
markdown(value, output_format='html'),
tags=allowed_tags, strip=True))
db.event.listen(Post.body, 'set', Post.on_changed_body)
on_changed_body()函数注册在body字段上,是SQLAlchemy “set”事件的监听程序,这意味着只要这个类实例的body字段设了新值,函数就会自动被调用。注:该回调函数的4个参数是缺一不可的,在程序运行时,依次打印4个参数的值如下:
on_changed_body()将MarkDown文本转换成HTML格式的过程分三步完成:
- markdown()函数初步把MarkDown文本转换成HTML;
- 把得到的结果和允许使用的HTML标签列表传给clean()函数,clean函数删除所有不再白名单中的标签;
- linkify函数完成转换的最后一步,该函数有Bleach提供,把纯文本中的URL转换成适当的<a>链接;
注:linkify函数的使用是很有必要的,因为MarkDown规范没有为自动生成链接提供官方支持。
app/templates/_posts.html:在模板中使用文章内容的 HTML 格式
...
<div class="post-body">
{% if post.body_html %}
{{ post.body_html | safe }}
{% else %}
{{ post.body }}
{% endif %}
</div>
...
渲染 HTML 格式内容时使用 | safe 后缀,其目的是告诉 Jinja2 不要转义 HTML 元素。出于安全考虑,默认情况下 Jinja2 会转义所有模板变量。Markdown 转换成的 HTML 在服务 器上生成,因此可以放心渲染。