Commit c92d116b authored by Liang Ding's avatar Liang Ding

Merge remote-tracking branch 'refs/remotes/origin/2.2.0-dev'

parents b71be4b8 15618f0f
...@@ -94,6 +94,7 @@ Solo 的诞生离不开以下开源项目: ...@@ -94,6 +94,7 @@ Solo 的诞生离不开以下开源项目:
* [emojify.js](https://github.com/Ranks/emojify.js):前端 Emoji 处理库 * [emojify.js](https://github.com/Ranks/emojify.js):前端 Emoji 处理库
* [jsoup](https://github.com/jhy/jsoup):Java HTML 解析器 * [jsoup](https://github.com/jhy/jsoup):Java HTML 解析器
* [flexmark](https://github.com/vsch/flexmark-java):Java Markdown 处理库 * [flexmark](https://github.com/vsch/flexmark-java):Java Markdown 处理库
* [marked](https://github.com/chjj/marked):NodeJS Markdown 处理库
* [Apache Commons](http://commons.apache.org):Java 工具库集 * [Apache Commons](http://commons.apache.org):Java 工具库集
* [emoji-java](https://github.com/vdurmont/emoji-java):Java Emoji 处理库 * [emoji-java](https://github.com/vdurmont/emoji-java):Java Emoji 处理库
* [FreeMarker](http://freemarker.org):好用的 Java 模版引擎 * [FreeMarker](http://freemarker.org):好用的 Java 模版引擎
......
{ {
"name": "Symphony", "name": "Solo",
"version": "1.3.0", "version": "2.2.0",
"description": "A real-time community forum written in Java. Java 实时社区论坛。 https://hacpai.com ", "description": "A blogging system written in Java, feel free to create your or your team own blog. 一个用 Java 实现的博客系统,为你或你的团队创建个博客吧!",
"homepage": "https://github.com/b3log/symphony", "homepage": "https://github.com/b3log/solo",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/b3log/symphony.git" "url": "git://github.com/b3log/solo.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/b3log/symphony/issues" "url": "https://github.com/b3log/solo/issues"
}, },
"license": "Apache License", "license": "GPLv3",
"private": true, "private": true,
"author": "Daniel <dl882509@gmail.com> (http://88250.b3log.org) & Vanessa <lly219@gmail.com> (http://vanessa.b3log.org)", "author": "Daniel <dl882509@gmail.com> (http://88250.b3log.org) & Vanessa <lly219@gmail.com> (http://vanessa.b3log.org)",
"maintainers": [ "maintainers": [
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-concat": "2.6.0", "gulp-concat": "2.6.0",
"gulp-uglify": "^2.0.0", "gulp-uglify": "^2.0.0",
"gulp-clean-css": "^2.0.13" "gulp-clean-css": "^2.0.13",
"marked": "^0.3.6"
} }
} }
...@@ -306,6 +306,7 @@ ...@@ -306,6 +306,7 @@
<includes> <includes>
<include>**/src/*/java/**/*.java</include> <include>**/src/*/java/**/*.java</include>
<include>**/src/*/webapp/js/*.js</include> <include>**/src/*/webapp/js/*.js</include>
<include>**/src/*/webapp/js/marked/*.js</include>
<include>**/src/*/webapp/css/*.css</include> <include>**/src/*/webapp/css/*.css</include>
<include>**/src/*/webapp/skins/*/js/*.js</include> <include>**/src/*/webapp/skins/*/js/*.js</include>
<include>**/src/*/webapp/skins/*/css/*.css</include> <include>**/src/*/webapp/skins/*/css/*.css</include>
......
...@@ -37,7 +37,7 @@ import java.util.*; ...@@ -37,7 +37,7 @@ import java.util.*;
* Import service. * Import service.
* *
* @author <a href="http://88250.b3log.org">Liang Ding</a> * @author <a href="http://88250.b3log.org">Liang Ding</a>
* @version 1.0.0.1, Jun 26, 2017 * @version 1.0.0.2, Jul 3, 2017
* @since 2.2.0 * @since 2.2.0
*/ */
@Service @Service
...@@ -91,6 +91,10 @@ public class ImportService { ...@@ -91,6 +91,10 @@ public class ImportService {
int succCnt = 0, failCnt = 0; int succCnt = 0, failCnt = 0;
final Set<String> failSet = new TreeSet<>(); final Set<String> failSet = new TreeSet<>();
final Collection<File> mds = FileUtils.listFiles(new File(markdownsPath), new String[]{"md"}, true); final Collection<File> mds = FileUtils.listFiles(new File(markdownsPath), new String[]{"md"}, true);
if (null == mds || mds.isEmpty()) {
return;
}
for (final File md : mds) { for (final File md : mds) {
final String fileName = md.getName(); final String fileName = md.getName();
if (StringUtils.equalsIgnoreCase(fileName, "README.md")) { if (StringUtils.equalsIgnoreCase(fileName, "README.md")) {
...@@ -117,6 +121,10 @@ public class ImportService { ...@@ -117,6 +121,10 @@ public class ImportService {
} }
} }
if (0 == succCnt && 0 == failCnt) {
return;
}
final StringBuilder logBuilder = new StringBuilder(); final StringBuilder logBuilder = new StringBuilder();
logBuilder.append("[").append(succCnt).append("] imported, [").append(failCnt).append("] failed"); logBuilder.append("[").append(succCnt).append("] imported, [").append(failCnt).append("] failed");
if (failCnt > 0) { if (failCnt > 0) {
......
...@@ -15,23 +15,42 @@ ...@@ -15,23 +15,42 @@
*/ */
package org.b3log.solo.util; package org.b3log.solo.util;
import com.vladsch.flexmark.ast.Node;
import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.profiles.pegdown.Extensions; import com.vladsch.flexmark.profiles.pegdown.Extensions;
import com.vladsch.flexmark.profiles.pegdown.PegdownOptionsAdapter; import com.vladsch.flexmark.profiles.pegdown.PegdownOptionsAdapter;
import com.vladsch.flexmark.util.options.DataHolder; import com.vladsch.flexmark.util.options.DataHolder;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.b3log.latke.ioc.LatkeBeanManagerImpl;
import org.b3log.latke.logging.Level;
import org.b3log.latke.logging.Logger; import org.b3log.latke.logging.Logger;
import org.b3log.latke.service.LangPropsService;
import org.b3log.latke.service.LangPropsServiceImpl;
import org.b3log.latke.util.Callstacks;
import org.b3log.latke.util.Stopwatchs;
import org.b3log.latke.util.Strings; import org.b3log.latke.util.Strings;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.parser.Parser;
import org.jsoup.select.Elements;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Set;
import java.util.concurrent.*;
/** /**
* <a href="http://en.wikipedia.org/wiki/Markdown">Markdown</a> utilities. * <a href="http://en.wikipedia.org/wiki/Markdown">Markdown</a> utilities.
* <p> * <p>
* Uses the <a href="https://github.com/vsch/flexmark-java">flexmark</a> as the converter. * Uses the <a href="https://github.com/chjj/marked">marked</a> as the processor, if not found this command, try
* built-in <a href="https://github.com/vsch/flexmark-java">flexmark</a> instead.
* </p> * </p>
* *
* @author <a href="http://88250.b3log.org">Liang Ding</a> * @author <a href="http://88250.b3log.org">Liang Ding</a>
* @version 2.1.0.2, Jun 3, 2017 * @version 2.2.0.2, Jul 3, 2017
* @since 0.4.5 * @since 0.4.5
*/ */
public final class Markdowns { public final class Markdowns {
...@@ -41,6 +60,17 @@ public final class Markdowns { ...@@ -41,6 +60,17 @@ public final class Markdowns {
*/ */
private static final Logger LOGGER = Logger.getLogger(Markdowns.class); private static final Logger LOGGER = Logger.getLogger(Markdowns.class);
/**
* Language service.
*/
private static final LangPropsService LANG_PROPS_SERVICE
= LatkeBeanManagerImpl.getInstance().getReference(LangPropsServiceImpl.class);
/**
* Markdown to HTML timeout.
*/
private static final int MD_TIMEOUT = 2000;
/** /**
* Built-in MD engine options. * Built-in MD engine options.
*/ */
...@@ -60,6 +90,44 @@ public final class Markdowns { ...@@ -60,6 +90,44 @@ public final class Markdowns {
*/ */
private static final HtmlRenderer RENDERER = HtmlRenderer.builder(OPTIONS).build(); private static final HtmlRenderer RENDERER = HtmlRenderer.builder(OPTIONS).build();
/**
* Marked engine serve path.
*/
private static final String MARKED_ENGINE_URL = "http://localhost:8250";
/**
* Whether marked is available.
*/
public static boolean MARKED_AVAILABLE;
static {
try {
final URL url = new URL(MARKED_ENGINE_URL);
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
final OutputStream outputStream = conn.getOutputStream();
IOUtils.write("Solo 大法好", outputStream, "UTF-8");
IOUtils.closeQuietly(outputStream);
final InputStream inputStream = conn.getInputStream();
final String html = IOUtils.toString(inputStream, "UTF-8");
IOUtils.closeQuietly(inputStream);
conn.disconnect();
MARKED_AVAILABLE = StringUtils.contains(html, "<p>Solo 大法好</p>");
if (MARKED_AVAILABLE) {
LOGGER.log(Level.INFO, "[marked] is available, uses it for markdown processing");
} else {
LOGGER.log(Level.INFO, "[marked] is not available, uses built-in [flexmark] for markdown processing");
}
} catch (final Exception e) {
LOGGER.log(Level.INFO, "[marked] is not available caused by [" + e.getMessage() + "], uses built-in [flexmark] for markdown processing");
}
}
/** /**
* Private constructor. * Private constructor.
*/ */
...@@ -70,20 +138,145 @@ public final class Markdowns { ...@@ -70,20 +138,145 @@ public final class Markdowns {
* Converts the specified markdown text to HTML. * Converts the specified markdown text to HTML.
* *
* @param markdownText the specified markdown text * @param markdownText the specified markdown text
* @return converted HTML, returns {@code null} if the specified markdown text is "" or {@code null}, returns * @return converted HTML, returns an empty string "" if the specified markdown text is "" or {@code null}, returns
* "Markdown error" if exception * 'markdownErrorLabel' if exception
*/ */
public static String toHTML(final String markdownText) { public static String toHTML(final String markdownText) {
if (Strings.isEmptyOrNull(markdownText)) { if (Strings.isEmptyOrNull(markdownText)) {
return ""; return "";
} }
final Node document = PARSER.parse(markdownText); final ExecutorService pool = Executors.newSingleThreadExecutor();
String ret = RENDERER.render(document); final long[] threadId = new long[1];
if (!StringUtils.startsWith(ret, "<p>")) {
ret = "<p>" + ret + "</p>"; final Callable<String> call = () -> {
threadId[0] = Thread.currentThread().getId();
String html = LANG_PROPS_SERVICE.get("contentRenderFailedLabel");
if (MARKED_AVAILABLE) {
html = toHtmlByMarked(markdownText);
if (!StringUtils.startsWith(html, "<p>")) {
html = "<p>" + html + "</p>";
}
} else {
com.vladsch.flexmark.ast.Node document = PARSER.parse(markdownText);
html = RENDERER.render(document);
if (!StringUtils.startsWith(html, "<p>")) {
html = "<p>" + html + "</p>";
}
html = formatMarkdown(html);
}
return html;
};
Stopwatchs.start("Md to HTML");
try {
final Future<String> future = pool.submit(call);
return future.get(MD_TIMEOUT, TimeUnit.MILLISECONDS);
} catch (final TimeoutException e) {
LOGGER.log(Level.ERROR, "Markdown timeout [md=" + markdownText + "]");
Callstacks.printCallstack(Level.ERROR, new String[]{"org.b3log"}, null);
final Set<Thread> threads = Thread.getAllStackTraces().keySet();
for (final Thread thread : threads) {
if (thread.getId() == threadId[0]) {
thread.stop();
break;
}
}
} catch (final Exception e) {
LOGGER.log(Level.ERROR, "Markdown failed [md=" + markdownText + "]", e);
} finally {
pool.shutdownNow();
Stopwatchs.end();
}
return LANG_PROPS_SERVICE.get("contentRenderFailedLabel");
}
private static String toHtmlByMarked(final String markdownText) throws Exception {
final URL url = new URL(MARKED_ENGINE_URL);
final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
final OutputStream outputStream = conn.getOutputStream();
IOUtils.write(markdownText, outputStream, "UTF-8");
IOUtils.closeQuietly(outputStream);
final InputStream inputStream = conn.getInputStream();
final String html = IOUtils.toString(inputStream, "UTF-8");
IOUtils.closeQuietly(inputStream);
//conn.disconnect();
return html;
}
/**
* See https://github.com/b3log/symphony/issues/306.
*
* @param markdownText
* @return
*/
private static String formatMarkdown(final String markdownText) {
String ret = markdownText;
final Document doc = Jsoup.parse(markdownText, "", Parser.htmlParser());
final Elements tagA = doc.select("a");
for (final Element aTagA : tagA) {
final String search = aTagA.attr("href");
final String replace = StringUtils.replace(search, "_", "[downline]");
ret = StringUtils.replace(ret, search, replace);
}
final Elements tagImg = doc.select("img");
for (final Element aTagImg : tagImg) {
final String search = aTagImg.attr("src");
final String replace = StringUtils.replace(search, "_", "[downline]");
ret = StringUtils.replace(ret, search, replace);
} }
final Elements tagCode = doc.select("code");
for (final Element aTagCode : tagCode) {
final String search = aTagCode.html();
final String replace = StringUtils.replace(search, "_", "[downline]");
ret = StringUtils.replace(ret, search, replace);
}
String[] rets = ret.split("\n");
for (final String temp : rets) {
final String[] toStrong = StringUtils.substringsBetween(temp, "**", "**");
final String[] toEm = StringUtils.substringsBetween(temp, "_", "_");
if (toStrong != null && toStrong.length > 0) {
for (final String strong : toStrong) {
final String search = "**" + strong + "**";
final String replace = "<strong>" + strong + "</strong>";
ret = StringUtils.replace(ret, search, replace);
}
}
if (toEm != null && toEm.length > 0) {
for (final String em : toEm) {
final String search = "_" + em + "_";
final String replace = "<em>" + em + "<em>";
ret = StringUtils.replace(ret, search, replace);
}
}
}
ret = StringUtils.replace(ret, "[downline]", "_");
return ret; return ret;
} }
} }
...@@ -16,12 +16,13 @@ ...@@ -16,12 +16,13 @@
# #
# Description: Solo language configurations(en_US). # Description: Solo language configurations(en_US).
# Version: 2.15.0.0, May 25, 2017 # Version: 2.16.0.0, Jul 3, 2017
# Author: Liang Ding # Author: Liang Ding
# Author: Liyuan Li # Author: Liyuan Li
# Author: Dongxu Wang # Author: Dongxu Wang
# #
contentRenderFailedLabel=Content render failed, please <a href="https://hacpai.com/article/1438049659432">report</a> this problem to help us enhance it, thank you &hearts;
userNameInvalidLabel=Username only allow alphabet or number! userNameInvalidLabel=Username only allow alphabet or number!
sponsorLabel=Become a Sponsor sponsorLabel=Become a Sponsor
addBoldLabel=Add bold text addBoldLabel=Add bold text
......
...@@ -16,12 +16,13 @@ ...@@ -16,12 +16,13 @@
# #
# Description: Solo default language configurations(zh_CN). # Description: Solo default language configurations(zh_CN).
# Version: 2.15.0.0, May 25, 2017 # Version: 2.16.0.0, Jul 3, 2017
# Author: Liang Ding # Author: Liang Ding
# Author: Liyuan Li # Author: Liyuan Li
# Author: Dongxu Wang # Author: Dongxu Wang
# #
contentRenderFailedLabel=\u5185\u5BB9\u6E32\u67D3\u51FA\u73B0\u4E86\u4E00\u4E9B\u95EE\u9898\uFF0C\u8BF7\u5230<a href="https://hacpai.com/article/1438049659432">\u8FD9\u91CC</a>\u53CD\u9988\u95EE\u9898\u4EE5\u5E2E\u52A9\u6211\u4EEC\u8FDB\u884C\u6539\u8FDB\uFF0C\u975E\u5E38\u611F\u8C22 &hearts;
userNameInvalidLabel=\u7528\u6237\u540D\u53EA\u80FD\u662F\u5B57\u6BCD\u6216\u6570\u5B57\uFF01 userNameInvalidLabel=\u7528\u6237\u540D\u53EA\u80FD\u662F\u5B57\u6BCD\u6216\u6570\u5B57\uFF01
sponsorLabel=\u6210\u4E3A\u8D5E\u52A9\u8005 sponsorLabel=\u6210\u4E3A\u8D5E\u52A9\u8005
addBoldLabel=\u6DFB\u52A0\u7C97\u4F53 addBoldLabel=\u6DFB\u52A0\u7C97\u4F53
......
/*
* Copyright (c) 2010-2017, b3log.org & hacpai.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview marked HTTP server.
*
* @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
* @author <a href="http://88250.b3log.org">Liang Ding</a>
* @version 1.1.0.0, May 20, 2017
* @since 1.7.0
*/
var PORT = 8250;
var http = require('http');
var marked = require('marked');
var renderer = new marked.Renderer();
renderer.listitem = function (text, level) {
if (text.indexOf('[ ] ') === 0 && text.replace(/\s/g, '') !== '[]') {
text = '<input type="checkbox" disabled>' + text.replace('[ ]', '');
return `<li class="task-item">${text}</li>`;
} else if (text.indexOf('[x] ') === 0 && text.replace(/\s/g, '') !== '[x]') {
text = '<input type="checkbox" checked disabled>' + text.replace('[x]', '');
return `<li class="task-item">${text}</li>`;
}
return `<li>${text}</li>`;
};
marked.setOptions({
renderer: renderer,
gfm: true,
tables: true,
breaks: true,
smartLists: true
});
process.on('uncaughtException', function (err) {
console.log(err);
});
var server = http.createServer(function (request, response) {
var mdContent = '';
request.on('data', function (data) {
mdContent += data;
});
request.on('end', function () {
response.write(marked(mdContent));
response.end();
});
});
server.listen(PORT);
console.log("Marked engine is running at port: " + PORT);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment