Editor.md 是一款开源的、可嵌入的 Markdown 在线编辑器(组件),基于 CodeMirror、jQuery 和 Marked 构建
本文为springboot与editor.md的集成例子,详细说明如何在springboot工程中使用editor.md进行markdown文档的编写,包含前端页面与后台数据库及接口,例子很完整,建好库表后,启动工程即可测试。
建立一张文章表用来存储markdown文章:
DROP TABLE IF EXISTS `article`;
CREATE TABLE `article` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`title` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '文章名称',
`content` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '文章md的内容',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
略,可详参考本文最后的源码里面查看
public interface ArticleService {
//查看所有文章列表
public List<Article> getArticleList();
//添加文章
public int addArticle(Article article);
//更新文章
public int updateArticle(Article article);
//删除文章
public int deleteArticle(Article article);
//根据文章id查询文章
public Article getArticleById(Integer id);
}
另外ArticleController类中增加一个文件上传接口用来进行图片的上传,上传至临时目录
还有两个查看图片的接口(查看临时目录下的图片,和查看正式目录下的图片)
//临时照片展示
@GetMapping("/getTempImage")
//正式照片展示,md上传的新照片,是先以/getTempImage?fileName=xxx的方式临时存放
//发表文章或修改文章时,通过以文章Id为目录将文章的图片独立出去管理,
//正式的照片访问url如:/getImage?id=10&fileName=xxxxxxxxxxxx.jpg
@GetMapping("/getImage")
//上传图片并回显
@ResponseBody
@RequestMapping("/article/uploadImg")
注:
md上传的新照片,是先以/getTempImage?fileName=xxx的方式临时存放,等发表文章或修改文章时,通过以文章Id为目录将文章的图片独立出去管理,l如:/getImage?id=10&fileName=xxxxxxxxxxxx.jpg,此逻辑在ArticleServiceImpl类的addArticle和updateArticle方法中都有相应的实现代码,如:
@Override
@Transactional
public int addArticle(Article article) {
try {
//先插入数据库
articleMapper.addArticle(article);
//取出自增id
int id = article.getId();
//将文章内容中的临时图片转换成正式的图片访问地址
String articleContentTemp = article.getContent();
//重新更新进去,articleContent.replaceAll后不会改变articleContent的值,在后面会继续进行临时图片的查询并替换
article.setContent(article.getContent().replaceAll("/getTempImage\\?fileName=", "/getImage\\?id="+id+"&fileName="));
articleMapper.updateArticle(article);
//============================
//查找文章内容中临时的图片,在文章保存(新增或修改)到数据库时,同时将临时目录下的图片迁移到以文章Id为目录的图片文件夹
String tempImagePattern = "(getTempImage\\?fileName=)(\\w+-\\w+-\\w+-\\w+-\\w+\\.\\w+)(.*)";
Pattern r = Pattern.compile(tempImagePattern);
Matcher m = r.matcher(articleContentTemp);
boolean imageDirCreted = false;
while(m.find()){
if(!imageDirCreted){
Path newRealPath = Paths.get(imageAbsoluteFilePath+"/"+imageUploadPathPrex+"/"+id);
//创建文件夹(不存在则创建)
Files.createDirectories(newRealPath);
imageDirCreted = true;
}
String tempImageFileName = m.group(2);
try {
Path fileRealPath = Paths.get(imageAbsoluteFilePath+"/" + imageUploadPathPrex + "/" + tempPrex + "/"+tempImageFileName);
Path newRealPathOfFile = Paths.get(imageAbsoluteFilePath+"/" + imageUploadPathPrex + "/"+id+"/"+tempImageFileName);
//覆盖式地复制文件
//Files.copy(fileRealPath, newRealPathOfFile, StandardCopyOption.REPLACE_EXISTING);
//将临时图片移至正式目录(以文章id命名的目录)
logger.info("将临时图片移至正式目录,{},fileName:{}",id,tempImageFileName);
Files.move(fileRealPath, newRealPathOfFile);
} catch (IOException e) {
throw new RuntimeException("重命名文件失败");
}
}
//最后,比对正式目录下的所有图片,将一些垃极图片从正式目录中也删除
String tempImagePattern2 = "(getImage\\?id=\\d+&fileName=)(\\w+-\\w+-\\w+-\\w+-\\w+\\.\\w+)(.*)";
Pattern r2 = Pattern.compile(tempImagePattern2);
Matcher m2 = r2.matcher(article.getContent());
HashSet<String> imageFileNameSet = new HashSet<String>();
while(m2.find()){
imageFileNameSet.add(m2.group(2));
}
File dir = new File(imageAbsoluteFilePath+"/"+imageUploadPathPrex+"/"+id);
if(dir.exists()){
String[] fileNames = dir.list();
List<String> needDeleteFiles = new ArrayList<String>();
for(String fileName : fileNames) {
if(!imageFileNameSet.contains(fileName)){
needDeleteFiles.add(fileName);
}
}
for(String needDeleteFileName : needDeleteFiles){
File file = new File(dir, needDeleteFileName);
//从正式目录删除无效文件
logger.info("从正式目录删除无效文件,{},fileName:{}",id,needDeleteFileName);
boolean success = file.delete();
if(!success){
throw new RuntimeException("删除无效文件失败:"+ needDeleteFileName);
}
}
}
} catch (Exception e) {
logger.error("error",e);
throw new RuntimeException(e.getMessage());
} finally {
}
return article.getId();
}
官网地址:https://pandao.github.io/editor.md/en.html
或我的网盘分享:
链接:https://pan.baidu.com/s/1Dl62ld1u6p5Z8ZNTijWhxA
提取码:2zan
在springboot工程中新建static文件目录(如果没有的话)然后将下载压缩包中的相应文件复制进去。
另外editormd.js相关的我新建了一个新的js目录来存放,
这里我写了三个页面articleList.html(文章列表)
,previewArticle.html(文章查看)
,showEditor.html(markdown编辑)
编辑器的构造代码如:
testEditor = editormd("test-editormd", {
width : "100%",
height : 640,
syncScrolling : "single",
path : contextPath + "/lib/",
imageUpload: true, //同意图片上传
imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
imageUploadURL: contextPath +"/article/uploadImg", //图片上传URL 即后台的图片上传接口URL
onload : function() {
}
});
浏览文章代码如:
var testEditormdView2 = editormd.markdownToHTML("test-editormd-view2", {
htmlDecode : "style,script,iframe", // you can filter tags decode
emoji : true,
taskList : true,
tex : true, // 默认不解析
flowChart : true, // 默认不解析
sequenceDiagram : true, // 默认不解析
});
测试地址:http://localhost:8080/articleList.html
文章列表(没什么样式)
编辑文章或发表新文章
发布后浏览的示例效果如
:
动态图:
由于他引用了国外的cdn导致加载变慢甚至不能加载的情况,我们需要修改到自己的服务器路径
http://micuer.com/static/js/katex.min.css
http://micuer.com/static/js/katex.min.js
下载好上述2个文件后,修改文件editormd.js大约4181行
// 使用国外的CDN,加载速度有时会很慢,或者自定义URL
// You can custom KaTeX load url.
editormd.katexURL = {
css : "css/katex.min",
js : "js/katex.min"
};
无法手动控制图片大小,默认是最大的,有时需要想自己设置大小比如 ![图片描述](url =300x150)
首先找到lib目录中的marked.min.js这个文件
然后定位Renderer.prototype.image 这个就是生成图片html的位置,原代码如:
Renderer.prototype.image = function(href, title, text) {
var out = '+ href + '" alt="' + text + '"';
if (title) {
out += ' title="' + title + '"'
}
out += this.options.xhtml ? "/>" : ">";
return out
}
修改后:
Renderer.prototype.image = function(href, title, text) {
var array = href.split("=");
var width;
var height;
if(array.length == 2){
href = array[0];
var resolution = array[1].split("x");
if (resolution.length == 2){
width = resolution[0]
height = resolution[1];
}
}
var out = '+ href + '" alt="' + text + '"';
if (title) {
out += ' title="' + title + '"'
}
if(width){
out += ' width="' + width + '"'
}
if(height){
out += ' height="' + height + '"'
}
out += this.options.xhtml ? "/>" : ">";
return out
};
修改后,通过在线的Js压缩工具进行js压缩后,替换到marked.min.js即可
需要支持截图并粘贴的话,可采用插件扩展的方式
新建一个uploadImage.js,将以下代码内容如下复制进去:
function initPasteDragImg(Editor){
var doc = document.getElementById(Editor.id);
doc.addEventListener('paste', function (event) {
var items = (event.clipboardData || window.clipboardData).items;
var file = null;
if (items && items.length) {
// 搜索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile();
break;
}
}
} else {
//console.log("当前浏览器不支持");
return;
}
if (!file) {
//console.log("粘贴内容非图片");
return;
}
//console.log(file);
uploadImg(file,Editor);
});
var dashboard = document.getElementById(Editor.id)
dashboard.addEventListener("dragover", function (e) {
e.preventDefault()
e.stopPropagation()
})
dashboard.addEventListener("dragenter", function (e) {
e.preventDefault()
e.stopPropagation()
})
dashboard.addEventListener("drop", function (e) {
e.preventDefault()
e.stopPropagation()
var files = this.files || e.dataTransfer.files;
uploadImg(files[0],Editor);
})
}
function uploadImg(file,Editor){
var formData = new FormData();
var fileName=new Date().getTime()+"."+file.name.split(".").pop();
formData.append('editormd-image-file', file, fileName);
$.ajax({
url: Editor.settings.imageUploadURL,
type: 'post',
data: formData,
processData: false,
contentType: false,
dataType: 'json',
success: function (msg) {
var success=msg['success'];
if(success==1){
var url=msg["url"];
if(/\.(png|jpg|jpeg|gif|bmp|ico)$/.test(url)){
Editor.insertValue("![]("+msg["url"]+")");//
}else{
//Editor.insertValue("[]("+msg["url"]+")");//下载文件
}
}else{
//console.log(msg);
alert("上传失败");
}
}
});
}
在showEditor.html页面引入后,在editormd加载载的回调函数中进行调用即可
<script src="js/uploadImage.js"></script>
将initPasteDragImg(this);
代码在onload
回调中进行调用
testEditor = editormd("test-editormd", {
width : "100%",
height : 640,
syncScrolling : "single",
path : contextPath + "/lib/",
imageUpload: true, //同意图片上传
imageFormats: ["jpg", "jpeg", "gif", "png", "bmp", "webp"],
imageUploadURL: contextPath +"/article/uploadImg", //图片上传URL 即后台的图片上传接口URL
onload : function() {
initPasteDragImg(this); //支持复制图片直接粘贴
}
});
github: https://github.com/jxlhljh/springbootmarkdowndemotest.git
gitee: https://gitee.com/jxlhljh/springbootmarkdowndemotest.git