Flask搭建博客内置第三方Markdown
一直有在赶自己的博客网站,然后发现文章编辑如果只是使用文本编辑的话会产生很多问题,因此就决定查找资料并且内置一个第三方开源的Markdown编辑器。
最终我选择的是editor.md。
image.png
展示一下最终的效果。
image.png
image.png
废话不多说,开始今天的学习之旅吧。
一、整理思路
仔细思考后,发现有几点技术方面的问题:
- 将源代码内置到flask中并且能在页面上显示
- 输入文章后,需要将文章文本格式转化成一种格式保存在数据库中
- 查看博客文章时,从数据库读取相应文本,并且转换成与编辑器右边相同的浏览格式。
- 解决文章中不能插入图片的问题。
以下我将以文章“新增”时作为介绍,并非“编辑”。
二、显示编辑器
首先编写此页面的form:
class PostForm(FlaskForm):
title = StringField('标题', [DataRequired(), length(max=255)])
body = TextAreaField('内容', [DataRequired()])
#categories = SelectMultipleField('Categories', coerce=int)
categories=SelectField('文章种类', choices=[],coerce=int )
body_html = HiddenField()
submit=SubmitField(render_kw={'value': "提交",'class': 'btn btn-success pull-right'})
file = FileField(label="文章封面",validators=[FileRequired(),FileAllowed(['png', 'jpg'], '只接收.png和.jpg的图片')])
#保证数据与数据库同步
def __init__(self):
super(PostForm, self).__init__()
self.categories.choices = [(c.id, c.name) for c in Category.query.order_by('id')]
其中categories要保证是灵活可变的并且能够和数据库中的“文章种类”表相同步,因此这里用了__init__
函数。
接着在html上:
<form class="am-form am-form-horizontal" method="post" action="" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="am-form-group am-form-file am-form-group-lg am-form-group-sm am-form-group-md">
<div class="am-u-sm-4 am-u-md-4 am-u-lg-4">
<button type="button" class="am-btn am-btn-dark am-btn-sm am-radius">
<i class="am-icon-cloud-upload"></i> 选择文章封面</button>
{{ form.file(class_='form-control',id='doc-form-file') }}
<div id="file-list"></div>
<script>
$(function() {
$('#doc-form-file').on('change', function() {
var fileNames = '';
$.each(this.files, function() {
fileNames += '<span class="am-badge">' + this.name + '</span> ';
});
$('#file-list').html(fileNames);
});
});
</script></div>
</div>
<div class="am-form-group am-form-group-lg am-form-group-sm am-form-group-md">
<div class="am-u-sm-12 am-u-md-12 am-u-lg-12">
{{ form.categories(class_="am-radius") }}
</div>
</div>
<div class="am-form-group am-form-group-lg am-form-group-sm am-form-group-md">
<div class="am-u-sm-12 am-u-md-12 am-u-lg-12">
{% if form.title.errors %}
{% for e in form.title.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
{{ form.title(class_="am-form-field am-radius",placeholder="请输入标题") }}
</div>
</div>
<div class="am-form-group am-form-group-lg am-form-group-sm am-form-group-md">
<div class="am-u-sm-12 am-u-md-12 am-u-lg-12">
{% if form.body.errors %}
{% for e in form.body.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
<div id="editormd" class="form-control">
{{ form.body(style="display:none;" ,class_="am-radius") }}
</div>
</div>
</div>
<div class="am-form-group">
<div class="am-u-sm-2 am-fr ">
<button type="submit" class="am-btn am-btn-default am-fr">提交</button>
</div>
</div>
</form>
其中id="editormd"是编辑器显示的关键所在:
image.png
并且需要在文件末加上脚本:
<script src="{{ url_for('static',filename='editormd/editormd.min.js') }}"></script>
<script type="text/javascript">
var testEditor;
$(function () {
testEditor = editormd("editormd", {
//width: "90%",#此处width不要设置,否则会显示不出
height: 640,
syncScrolling: "single",
path: "{{ url_for('static',filename='editormd/lib/') }}",
theme : "dark",
previewTheme : "dark",#背景颜色,还可以使用light
editorTheme : "pastel-on-dark",
//markdown : md,
codeFold : true,
//syncScrolling : false,
saveHTMLToTextarea : true, // 保存 HTML 到 Textarea
searchReplace : true,
//watch : false, // 关闭实时预览
htmlDecode : "style,script,iframe|on*", // 开启 HTML 标签解析,为了安全性,默认不开启
//toolbar : false, //关闭工具栏
//previewCodeHighlight : false, // 关闭预览 HTML 的代码块高亮,默认开启
emoji : true,
taskList : true,
tocm : true, // Using [TOCM]
tex : true, // 开启科学公式TeX语言支持,默认关闭
flowChart : true, // 开启流程图支持,默认关闭
sequenceDiagram : true, // 开启时序/序列图支持,默认关闭,
//dialogLockScreen : false, // 设置弹出层对话框不锁屏,全局通用,默认为true
//dialogShowMask : false, // 设置弹出层对话框显示透明遮罩层,全局通用,默认为true
//dialogDraggable : false, // 设置弹出层对话框不可拖动,全局通用,默认为true
//dialogMaskOpacity : 0.4, // 设置透明遮罩层的透明度,全局通用,默认值为0.1
//dialogMaskBgColor : "#000", // 设置透明遮罩层的背景颜色,全局通用,默认为#fff
imageUpload : true,
imageFormats : ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
imageUploadURL : "{{ url_for('.upload') }}",
onload : function() {
console.log('onload', this);
//this.fullscreen();
//this.unwatch();
//this.watch().fullscreen();
//this.setMarkdown("#PHP");
//this.width("100%");
//this.height(480);
//this.resize("100%", 640);
}
});
});
</script>
上述代码中其中对于图片上传的代码后面再解释:
image.png
三、设计数据库并且增加路由
大家也看到我博客的界面有哪些表单了,这里还要设计数据库来存储这些表单。
class Article(db.Model):
__tablename__ = 'article'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(128)) #标题
body=db.Column(db.Text) #原本text格式
body_html = db.Column(db.Text) #转化成html代码后的格式
create_time = db.Column(db.DateTime, index=True, default=datetime.utcnow) #创建时间
seo_link = db.Column(db.String(128)) #文章原链接
pic_path = db.Column(db.String(320)) #封面地址
category_id = db.Column(db.Integer, db.ForeignKey('categories.id')) #文章类别
posts = db.relationship('Post',backref='article',lazy='dynamic') #一个评论的外键
#将文本转化为html
@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', 'img', 'video', 'div', 'iframe', 'p', 'br', 'span', 'hr', 'src', 'class']
allowed_attrs = {'*': ['class'],
'a': ['href', 'rel'],
'img': ['src', 'alt']}
target.body_html = bleach.linkify(bleach.clean(
markdown(value, output_format='html'),
tags=allowed_tags, attributes=allowed_attrs, strip=True))
db.event.listen(Article.body, 'set', Article.on_changed_body)
解释一下,编辑器中的文本是你写的text格式,此处存储到数据库,使用html格式存储,可以记录文章的排版,方便在输出文章内容时展现出来相同的排版格式。
下图的代码是editor.md转化成html格式的关键代码。
image.png
接下来增加路由来处理逻辑
@app.route('/admin/post', methods=['GET', 'POST'])
@login_required
@csrf.exempt
def post():
title="写文章"
form = PostForm()
if form.validate_on_submit():
#basepath = os.path.dirname(__file__) # 当前文件所在路径
#fileGet='uploads/assignment{}'.format(HOMEWORK_TIME)
#upload_path = os.path.join(basepath,fileGet,secure_filename(fpy.filename))
savepic_path = 'app/static/assets/img/'+form.file.data.filename
form.file.data.save(savepic_path) #处理封面地址
cate=Category.query.filter_by(name=dict(form.categories.choices).get(form.categories.data)).first_or_404() #处理类别
cate.number=cate.number+1
article=Article(title=form.title.data,body = form.body.data,create_time = datetime.now(),pic_path='static/assets/img/'+form.file.data.filename,category_id=cate.id) #新建文章
db.session.add(article)
db.session.commit()
flash('上传成功!')
return redirect(url_for('index'))
#if request.method=='POST':
# fpic=request.files['editormd-image-file']
# bodypic_path='app/static/pic/'+fpic.filename
# fpic.save(bodypic_path)
return render_template('/admin/post.html',title=title, form=form,category=category)
四、无法插入图片问题解决
在我以为将要大功告成的时候发现编辑器的图片无法插入,于是查看源代码,并且查阅了资料后,最终找到问题所在。上面的html页面代码中
image.png
此处打开编辑器的插入图片功能,并且设置一个视图函数来处理上传的图片所到的位置以及可以上传图片的格式。
upload视图函数:
@app.route('/upload/',methods=['GET','POST'])
@login_required
@csrf.exempt
def upload():
file=request.files.get('editormd-image-file')
if not file:
res={
'success':0,
'message':'上传失败'
}
else:
ex=os.path.splitext(file.filename)[1]
filename=datetime.now().strftime('%Y%m%d%H%M%S')+ex
bodypic_path='app/static/pic/'+filename
file.save(bodypic_path)
res={
'success':1,
'message':'上传成功',
'url':url_for('.image',name=filename)
}
return jsonify(res)
在editor.md官方文档当中,图片插入的格式是一个json格式,我们要对此格式进行处理。那为什么file=request.files.get('editormd-image-file')呢?
插入图片的时候,会出现新框,我们打开开发者工具观察得到,上传图片的from的name,我们需要此id并且得到其中的数据,因此就有这么一个file变量了。
image.png
点击提交后即可。
最后秀一波我未完成的网站:
image.png image.png
image.png
这边排版还有有点问题后续再加以改进!
如有疑问或者需要壁纸的可以加我的微信公众号私聊我,我会给你最准确的答复,并且因为个人博客还在设计当中,域名也在备案,后续会公布并且公开源代码。欢迎大家参观我的博客鸭!你的支持就是我最大的动力!
君莫舞丶无念blog.jpg