🎨 改用后端渲染Markdown

pull/56/head
ruibaby 2018-11-14 22:45:13 +08:00
parent 294ae693b1
commit 2ac0dda82b
15 changed files with 108 additions and 58 deletions

View File

@ -115,7 +115,7 @@ Let's start: http://localhost:8090
Halo的诞生离不开下面这些项目 Halo的诞生离不开下面这些项目
- [IntelliJ IDEA](https://www.jetbrains.com/idea/)个人认为最强大的Java IDE没有之一 - [IntelliJ IDEA](https://www.jetbrains.com/idea/)个人认为最强大的Java IDE没有之一
- [Spring Boot](https://github.com/spring-projects/spring-boot)Spring的微服务框架 - [Spring Boot](https://github.com/spring-projects/spring-boot)Spring的快速开发框架
- [Freemarker](https://freemarker.apache.org/):模板引擎,使页面静态化 - [Freemarker](https://freemarker.apache.org/):模板引擎,使页面静态化
- [H2 Database](https://github.com/h2database/h2database):嵌入式数据库,无需安装 - [H2 Database](https://github.com/h2database/h2database):嵌入式数据库,无需安装
- [Druid](https://github.com/alibaba/druid):阿里开发的连接池 - [Druid](https://github.com/alibaba/druid):阿里开发的连接池

18
pom.xml
View File

@ -40,6 +40,7 @@
<qiniu-java-sdk.version>7.2.14</qiniu-java-sdk.version> <qiniu-java-sdk.version>7.2.14</qiniu-java-sdk.version>
<thumbnailator.version>0.4.8</thumbnailator.version> <thumbnailator.version>0.4.8</thumbnailator.version>
<jaxb-api.version>2.3.0</jaxb-api.version> <jaxb-api.version>2.3.0</jaxb-api.version>
<commonmark.version>0.12.1</commonmark.version>
</properties> </properties>
<dependencies> <dependencies>
@ -174,6 +175,23 @@
<version>${jaxb-api.version}</version> <version>${jaxb-api.version}</version>
</dependency> </dependency>
<!-- Markdown渲染 -->
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>${commonmark.version}</version>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-gfm-tables</artifactId>
<version>${commonmark.version}</version>
</dependency>
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-yaml-front-matter</artifactId>
<version>${commonmark.version}</version>
</dependency>
</dependencies> </dependencies>
<profiles> <profiles>

View File

@ -5,6 +5,7 @@ import cc.ryanc.halo.repository.OptionsRepository;
import cc.ryanc.halo.service.OptionsService; import cc.ryanc.halo.service.OptionsService;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.HashMap; import java.util.HashMap;
@ -25,12 +26,15 @@ public class OptionsServiceImpl implements OptionsService {
@Autowired @Autowired
private OptionsRepository optionsRepository; private OptionsRepository optionsRepository;
private static final String POSTS_CACHE_NAME = "posts";
/** /**
* *
* *
* @param options options * @param options options
*/ */
@Override @Override
@CacheEvict(value = POSTS_CACHE_NAME, allEntries = true, beforeInvocation = true)
public void saveOptions(Map<String, String> options) { public void saveOptions(Map<String, String> options) {
if (null != options && !options.isEmpty()) { if (null != options && !options.isEmpty()) {
options.forEach((k, v) -> saveOption(k, v)); options.forEach((k, v) -> saveOption(k, v));

View File

@ -104,7 +104,7 @@ public class PostServiceImpl implements PostService {
public void updateAllSummary(Integer postSummary) { public void updateAllSummary(Integer postSummary) {
List<Post> posts = this.findAllPosts(PostTypeEnum.POST_TYPE_POST.getDesc()); List<Post> posts = this.findAllPosts(PostTypeEnum.POST_TYPE_POST.getDesc());
for (Post post : posts) { for (Post post : posts) {
String text = StrUtil.trim(HtmlUtil.cleanHtmlTag(post.getPostContent())); String text = StrUtil.cleanBlank(HtmlUtil.cleanHtmlTag(post.getPostContent()));
if (text.length() > postSummary) { if (text.length() > postSummary) {
post.setPostSummary(text.substring(0, postSummary)); post.setPostSummary(text.substring(0, postSummary));
} else { } else {

View File

@ -84,17 +84,17 @@ public class HaloUtils {
* @return String * @return String
*/ */
public static String parseSize(long size) { public static String parseSize(long size) {
if (size < CommonParamsEnum.NOT_FOUND.getValue()) { if (size < CommonParamsEnum.BYTE.getValue()) {
return String.valueOf(size) + "B"; return String.valueOf(size) + "B";
} else { } else {
size = size / 1024; size = size / 1024;
} }
if (size < CommonParamsEnum.NOT_FOUND.getValue()) { if (size < CommonParamsEnum.BYTE.getValue()) {
return String.valueOf(size) + "KB"; return String.valueOf(size) + "KB";
} else { } else {
size = size / 1024; size = size / 1024;
} }
if (size < CommonParamsEnum.NOT_FOUND.getValue()) { if (size < CommonParamsEnum.BYTE.getValue()) {
size = size * 100; size = size * 100;
return String.valueOf((size / 100)) + "." + String.valueOf((size % 100)) + "MB"; return String.valueOf((size / 100)) + "." + String.valueOf((size % 100)) + "MB";
} else { } else {

View File

@ -0,0 +1,65 @@
package cc.ryanc.halo.utils;
import org.commonmark.Extension;
import org.commonmark.ext.front.matter.YamlFrontMatterExtension;
import org.commonmark.ext.front.matter.YamlFrontMatterVisitor;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author : RYAN0UP
* @date : 2018/11/14
*/
public class MarkdownUtils {
/**
* Front-matter
*/
private static final Set<Extension> EXTENSIONS_YAML = Collections.singleton(YamlFrontMatterExtension.create());
/**
* Table
*/
private static final Set<Extension> EXTENSIONS_TABLE = Collections.singleton(TablesExtension.create());
/**
* Markdown
*/
private static final Parser PARSER = Parser.builder().extensions(EXTENSIONS_YAML).extensions(EXTENSIONS_TABLE).build();
/**
* HTML
*/
private static final HtmlRenderer RENDERER = HtmlRenderer.builder().extensions(EXTENSIONS_YAML).extensions(EXTENSIONS_TABLE).build();
/**
* Markdown
*
* @param content content
* @return String
*/
public static String renderMarkdown(String content) {
Node document = PARSER.parse(content);
return RENDERER.render(document);
}
/**
*
*
* @param content content
* @return Map
*/
public static Map<String, List<String>> getFrontMatter(String content) {
YamlFrontMatterVisitor visitor = new YamlFrontMatterVisitor();
Node document = PARSER.parse(content);
document.accept(visitor);
return visitor.getData();
}
}

View File

@ -13,6 +13,7 @@ import cc.ryanc.halo.service.LogsService;
import cc.ryanc.halo.service.PostService; import cc.ryanc.halo.service.PostService;
import cc.ryanc.halo.utils.HaloUtils; import cc.ryanc.halo.utils.HaloUtils;
import cc.ryanc.halo.utils.LocaleMessageUtil; import cc.ryanc.halo.utils.LocaleMessageUtil;
import cc.ryanc.halo.utils.MarkdownUtils;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@ -235,6 +236,7 @@ public class PageController {
post.setPostDate(DateUtil.date()); post.setPostDate(DateUtil.date());
post.setPostUpdate(DateUtil.date()); post.setPostUpdate(DateUtil.date());
} }
post.setPostContent(MarkdownUtils.renderMarkdown(post.getPostContentMd()));
//当没有选择文章缩略图的时候,自动分配一张内置的缩略图 //当没有选择文章缩略图的时候,自动分配一张内置的缩略图
if (StrUtil.equals(post.getPostThumbnail(), BlogPropertiesEnum.DEFAULT_THUMBNAIL.getProp())) { if (StrUtil.equals(post.getPostThumbnail(), BlogPropertiesEnum.DEFAULT_THUMBNAIL.getProp())) {
post.setPostThumbnail("/static/images/thumbnail/thumbnail-" + RandomUtil.randomInt(1, 10) + ".jpg"); post.setPostThumbnail("/static/images/thumbnail/thumbnail-" + RandomUtil.randomInt(1, 10) + ".jpg");

View File

@ -14,6 +14,7 @@ import cc.ryanc.halo.service.PostService;
import cc.ryanc.halo.service.TagService; import cc.ryanc.halo.service.TagService;
import cc.ryanc.halo.utils.HaloUtils; import cc.ryanc.halo.utils.HaloUtils;
import cc.ryanc.halo.utils.LocaleMessageUtil; import cc.ryanc.halo.utils.LocaleMessageUtil;
import cc.ryanc.halo.utils.MarkdownUtils;
import cc.ryanc.halo.web.controller.core.BaseController; import cc.ryanc.halo.web.controller.core.BaseController;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
@ -169,13 +170,14 @@ public class PostController extends BaseController {
User user = (User) session.getAttribute(HaloConst.USER_SESSION_KEY); User user = (User) session.getAttribute(HaloConst.USER_SESSION_KEY);
String msg = localeMessageUtil.getMessage("code.admin.common.save-success"); String msg = localeMessageUtil.getMessage("code.admin.common.save-success");
try { try {
post.setPostContent(MarkdownUtils.renderMarkdown(post.getPostContentMd()));
//提取摘要 //提取摘要
int postSummary = 50; int postSummary = 50;
if (StrUtil.isNotEmpty(HaloConst.OPTIONS.get(BlogPropertiesEnum.POST_SUMMARY.getProp()))) { if (StrUtil.isNotEmpty(HaloConst.OPTIONS.get(BlogPropertiesEnum.POST_SUMMARY.getProp()))) {
postSummary = Integer.parseInt(HaloConst.OPTIONS.get(BlogPropertiesEnum.POST_SUMMARY.getProp())); postSummary = Integer.parseInt(HaloConst.OPTIONS.get(BlogPropertiesEnum.POST_SUMMARY.getProp()));
} }
//文章摘要 //文章摘要
String summaryText = StrUtil.trim(HtmlUtil.cleanHtmlTag(post.getPostContent())); String summaryText = StrUtil.cleanBlank(HtmlUtil.cleanHtmlTag(post.getPostContent()));
if (summaryText.length() > postSummary) { if (summaryText.length() > postSummary) {
String summary = summaryText.substring(0, postSummary); String summary = summaryText.substring(0, postSummary);
post.setPostSummary(summary); post.setPostSummary(summary);
@ -210,6 +212,7 @@ public class PostController extends BaseController {
return new JsonResult(ResultCodeEnum.SUCCESS.getCode(), msg); return new JsonResult(ResultCodeEnum.SUCCESS.getCode(), msg);
} catch (Exception e) { } catch (Exception e) {
log.error("Save article failed: {}", e.getMessage()); log.error("Save article failed: {}", e.getMessage());
e.printStackTrace();
return new JsonResult(ResultCodeEnum.FAIL.getCode(), localeMessageUtil.getMessage("code.admin.common.save-failed")); return new JsonResult(ResultCodeEnum.FAIL.getCode(), localeMessageUtil.getMessage("code.admin.common.save-failed"));
} }
} }

View File

@ -7,6 +7,7 @@ import cc.ryanc.halo.model.enums.AllowCommentEnum;
import cc.ryanc.halo.model.enums.BlogPropertiesEnum; import cc.ryanc.halo.model.enums.BlogPropertiesEnum;
import cc.ryanc.halo.model.enums.TrueFalseEnum; import cc.ryanc.halo.model.enums.TrueFalseEnum;
import cc.ryanc.halo.service.*; import cc.ryanc.halo.service.*;
import cc.ryanc.halo.utils.MarkdownUtils;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil; import cn.hutool.crypto.SecureUtil;
@ -131,7 +132,7 @@ public class InstallController {
post.setPostTitle("Hello Halo!"); post.setPostTitle("Hello Halo!");
post.setPostContentMd("# Hello Halo!\n" + post.setPostContentMd("# Hello Halo!\n" +
"欢迎使用Halo进行创作删除这篇文章后赶紧开始吧。"); "欢迎使用Halo进行创作删除这篇文章后赶紧开始吧。");
post.setPostContent("<h1 id=\"h1-hello-halo-\"><a name=\"Hello Halo!\" class=\"reference-link\"></a><span class=\"header-link octicon octicon-link\"></span>Hello Halo!</h1><p>欢迎使用Halo进行创作删除这篇文章后赶紧开始吧。</p>\n"); post.setPostContent(MarkdownUtils.renderMarkdown(post.getPostContentMd()));
post.setPostSummary("欢迎使用Halo进行创作删除这篇文章后赶紧开始吧。"); post.setPostSummary("欢迎使用Halo进行创作删除这篇文章后赶紧开始吧。");
post.setPostStatus(0); post.setPostStatus(0);
post.setPostDate(DateUtil.date()); post.setPostDate(DateUtil.date());

View File

@ -23,10 +23,10 @@
<div class="login-body animated"> <div class="login-body animated">
<form> <form>
<div class="form-group animated fadeInUp" style="animation-delay: 0.1s"> <div class="form-group animated fadeInUp" style="animation-delay: 0.1s">
<input type="text" class="form-control" name="loginName" id="login-name" placeholder="<@spring.message code='login.form.loginName' />"> <input type="text" class="form-control" name="loginName" id="login-name" placeholder="<@spring.message code='login.form.loginName' />" autocomplete="username">
</div> </div>
<div class="form-group animated fadeInUp" style="animation-delay: 0.2s"> <div class="form-group animated fadeInUp" style="animation-delay: 0.2s">
<input type="password" class="form-control" name="loginPwd" id="login-pwd" placeholder="<@spring.message code='login.form.loginPwd' />"> <input type="password" class="form-control" name="loginPwd" id="login-pwd" placeholder="<@spring.message code='login.form.loginPwd' />" autocomplete="current-password">
</div> </div>
<#--<div class="row control animated fadeInUp" style="animation-delay: 0.3s">--> <#--<div class="row control animated fadeInUp" style="animation-delay: 0.3s">-->
<#--<div class="col-xs-6">--> <#--<div class="col-xs-6">-->

View File

@ -475,23 +475,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group">
<label class="col-lg-2 col-sm-4 control-label"><@spring.message code='admin.setting.form.admin-loading' /></label>
<div class="col-lg-4 col-sm-8 control-radio">
<div class="pretty p-default p-round">
<input type="radio" name="admin_loading" value="true" ${((options.admin_loading!)=='true')?string('checked','')}>
<div class="state p-primary">
<label><@spring.message code='common.radio.enable' /></label>
</div>
</div>
<div class="pretty p-default p-round">
<input type="radio" name="admin_loading" value="false" ${((options.admin_loading!'false')=='false')?string('checked','')}>
<div class="state p-primary">
<label><@spring.message code='common.radio.disable' /></label>
</div>
</div>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-lg-2 col-sm-4 control-label"><@spring.message code='admin.setting.form.admin-layout' /></label> <label class="col-lg-2 col-sm-4 control-label"><@spring.message code='admin.setting.form.admin-layout' /></label>
<div class="col-lg-4 col-sm-8 control-radio"> <div class="col-lg-4 col-sm-8 control-radio">
@ -588,7 +571,7 @@
<div class="form-group"> <div class="form-group">
<label for="emailSmtpPassword" class="col-lg-2 col-sm-4 control-label"><@spring.message code='admin.setting.form.email-smtp-password' /></label> <label for="emailSmtpPassword" class="col-lg-2 col-sm-4 control-label"><@spring.message code='admin.setting.form.email-smtp-password' /></label>
<div class="col-lg-4 col-sm-8"> <div class="col-lg-4 col-sm-8">
<input type="password" class="form-control" id="emailSmtpPassword" name="mail_smtp_password" value="${options.mail_smtp_password!}" current-password> <input type="password" class="form-control" id="emailSmtpPassword" name="mail_smtp_password" value="${options.mail_smtp_password!}" autocomplete="current-password">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@ -219,7 +219,6 @@
'postTitle': Title, 'postTitle': Title,
'postUrl' : $('#postUrl').html().toString(), 'postUrl' : $('#postUrl').html().toString(),
'postContentMd': simplemde.value(), 'postContentMd': simplemde.value(),
'postContent': simplemde.markdown(simplemde.value()),
'postThumbnail': $('#selectImg').attr('src'), 'postThumbnail': $('#selectImg').attr('src'),
'allowComment' : $('#allowComment').val(), 'allowComment' : $('#allowComment').val(),
'customTpl' : $("#customTpl").val() 'customTpl' : $("#customTpl").val()

View File

@ -310,7 +310,6 @@
'postTitle': Title, 'postTitle': Title,
'postUrl' : $('#postUrl').html().toString(), 'postUrl' : $('#postUrl').html().toString(),
'postContentMd': simplemde.value(), 'postContentMd': simplemde.value(),
'postContent': simplemde.markdown(simplemde.value()),
'postThumbnail': $('#selectImg').attr('src'), 'postThumbnail': $('#selectImg').attr('src'),
'cateList' : cateList.toString(), 'cateList' : cateList.toString(),
'tagList' : $('#tagList').tagEditor('getTags')[0].tags.toString(), 'tagList' : $('#tagList').tagEditor('getTags')[0].tags.toString(),

View File

@ -95,19 +95,19 @@
<div class="form-group"> <div class="form-group">
<label for="beforePass" class="col-sm-2 control-label"><@spring.message code='admin.user.profile.form.old-password' /></label> <label for="beforePass" class="col-sm-2 control-label"><@spring.message code='admin.user.profile.form.old-password' /></label>
<div class="col-sm-4"> <div class="col-sm-4">
<input type="password" class="form-control" id="beforePass" name="beforePass"> <input type="password" class="form-control" id="beforePass" name="beforePass" autocomplete="current-password">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="newPass" class="col-sm-2 control-label"><@spring.message code='admin.user.profile.form.new-password' /></label> <label for="newPass" class="col-sm-2 control-label"><@spring.message code='admin.user.profile.form.new-password' /></label>
<div class="col-sm-4"> <div class="col-sm-4">
<input type="password" class="form-control" id="newPass" name="newPass"> <input type="password" class="form-control" id="newPass" name="newPass" autocomplete="new-password">
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="reNewPass" class="col-sm-2 control-label"><@spring.message code='admin.user.profile.form.confirm-password' /></label> <label for="reNewPass" class="col-sm-2 control-label"><@spring.message code='admin.user.profile.form.confirm-password' /></label>
<div class="col-sm-4"> <div class="col-sm-4">
<input type="password" class="form-control" id="reNewPass" name="reNewPass"> <input type="password" class="form-control" id="reNewPass" name="reNewPass" autocomplete="new-password">
</div> </div>
</div> </div>
</div> </div>

View File

@ -29,19 +29,6 @@
<script src="/static/plugins/OwO/OwO.min.js"></script> <script src="/static/plugins/OwO/OwO.min.js"></script>
</head> </head>
<body class="hold-transition sidebar-mini ${options.admin_theme!'skin-blue'} ${options.admin_layout!''} ${options.sidebar_style!''}"> <body class="hold-transition sidebar-mini ${options.admin_theme!'skin-blue'} ${options.admin_layout!''} ${options.sidebar_style!''}">
<#if (options.admin_loading!'false') == 'true'>
<#-- 页面加载动画 -->
<div id="loading">
<div id="loading-center">
<div id="loading-center-absolute">
<div class="object" id="object_one"></div>
<div class="object" id="object_two"></div>
<div class="object" id="object_three"></div>
<div class="object" id="object_four"></div>
</div>
</div>
</div>
</#if>
<div class="wrapper"> <div class="wrapper">
<#-- 顶部栏模块 --> <#-- 顶部栏模块 -->
<#include "_header.ftl"> <#include "_header.ftl">
@ -53,7 +40,7 @@
<#include "_footer.ftl"> <#include "_footer.ftl">
</div> </div>
<#if (options.admin_pjax!'true') == 'true'> <#if (options.admin_pjax!'true') == 'true'>
<script src="/static/plugins/pjax/jquery.pjax.js"></script> <script src="/static/plugins/pjax/jquery.pjax.js"></script>
</#if> </#if>
<script src="/static/plugins/pace/pace.min.js"></script> <script src="/static/plugins/pace/pace.min.js"></script>
<script src="/static/js/adminlte.min.js"></script> <script src="/static/js/adminlte.min.js"></script>
@ -61,27 +48,16 @@
<script src="/static/plugins/layer/layer.js"></script> <script src="/static/plugins/layer/layer.js"></script>
<script src="/static/plugins/fileinput/fileinput.min.js"></script> <script src="/static/plugins/fileinput/fileinput.min.js"></script>
<#if (options.blog_locale!'zh_CN') == 'zh_CN'> <#if (options.blog_locale!'zh_CN') == 'zh_CN'>
<script src="/static/plugins/fileinput/zh.min.js"></script> <script src="/static/plugins/fileinput/zh.min.js"></script>
</#if> </#if>
<script src="/static/js/halo.min.js"></script> <script src="/static/js/halo.min.js"></script>
<@compress single_line=true> <@compress single_line=true>
<script> <script>
var halo = new $.halo(); var halo = new $.halo();
Pace.options = {
restartOnRequestAfter: false
};
$(document).ajaxStart(function() {Pace.restart();}); $(document).ajaxStart(function() {Pace.restart();});
<#if (options.admin_pjax!'true') == 'true'> <#if (options.admin_pjax!'true') == 'true'>
$(document).pjax('a[data-pjax=true]', '.content-wrapper', {fragment: '.content-wrapper',timeout: 8000}); $(document).pjax('a[data-pjax=true]', '.content-wrapper', {fragment: '.content-wrapper',timeout: 8000});
</#if> </#if>
<#if (options.admin_loading!'false') == 'true'>
$(window).on('load', function(){
$('body').addClass('loaded');
setTimeout(function () {
$('#loading').remove();
},500);
});
</#if>
var heading = "<@spring.message code='common.text.tips' />"; var heading = "<@spring.message code='common.text.tips' />";
</script> </script>
</@compress> </@compress>