diff --git a/client/js/util/markdown.js b/client/js/util/markdown.js
new file mode 100644
index 00000000..3b79c473
--- /dev/null
+++ b/client/js/util/markdown.js
@@ -0,0 +1,137 @@
+'use strict';
+
+const marked = require('marked');
+
+class BaseMarkdownWrapper {
+ preprocess(text) {
+ return text;
+ }
+
+ postprocess(text) {
+ return text;
+ }
+}
+
+class SjisWrapper extends BaseMarkdownWrapper {
+ constructor() {
+ super();
+ this.buf = [];
+ }
+
+ preprocess(text) {
+ return text.replace(
+ /\[sjis\]((?:[^\[]|\[(?!\/?sjis\]))+)\[\/sjis\]/ig,
+ (match, capture) => {
+ var ret = '%%%SJIS' + this.buf.length;
+ this.buf.push(capture);
+ return ret;
+ });
+ }
+
+ postprocess(text) {
+ return text.replace(
+ /(?:
)?%%%SJIS(\d+)(?:<\/p>)?/,
+ (match, capture) => {
+ return '
' + this.buf[capture] + '
';
+ });
+ }
+}
+
+// fix \ before ~ being stripped away
+class TildeWrapper extends BaseMarkdownWrapper {
+ preprocess(text) {
+ return text.replace(/\\~/g, '%%%T');
+ }
+
+ postprocess(text) {
+ return text.replace(/%%%T/g, '\\~');
+ }
+}
+
+//prevent ^#... from being treated as headers, due to tag permalinks
+class TagPermalinkFixWrapper extends BaseMarkdownWrapper {
+ preprocess(text) {
+ return text.replace(/^#/g, '%%%#');
+ }
+
+ postprocess(text) {
+ return text.replace(/%%%#/g, '#');
+ }
+}
+
+//post, user and tags permalinks
+class EntityPermalinkWrapper extends BaseMarkdownWrapper {
+ preprocess(text) {
+ text = text.replace(
+ /(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,
+ '$1[$2]($2)');
+ text = text.replace(/\]\(@(\d+)\)/g, '](/post/$1)');
+ text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, '](/user/$1)');
+ text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, '](/posts/query=$1)');
+ return text;
+ }
+}
+
+class SearchPermalinkWrapper extends BaseMarkdownWrapper {
+ postprocess(text) {
+ return text.replace(
+ /\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig,
+ '$1
');
+ }
+}
+
+class SpoilersWrapper extends BaseMarkdownWrapper {
+ postprocess(text) {
+ return text.replace(
+ /\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/ig,
+ '$1');
+ }
+}
+
+class SmallWrapper extends BaseMarkdownWrapper {
+ postprocess(text) {
+ return text.replace(
+ /\[small\]((?:[^\[]|\[(?!\/?small\]))+)\[\/small\]/ig,
+ '$1');
+ }
+}
+
+class StrikeThroughWrapper extends BaseMarkdownWrapper {
+ postprocess(text) {
+ text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, '$1$3');
+ return text.replace(/\\~/g, '~');
+ }
+}
+
+function formatMarkdown(text) {
+ const renderer = new marked.Renderer();
+ const options = {
+ renderer: renderer,
+ breaks: true,
+ sanitize: true,
+ smartypants: true,
+ };
+ let wrappers = [
+ new SjisWrapper(),
+ new TildeWrapper(),
+ new TagPermalinkFixWrapper(),
+ new EntityPermalinkWrapper(),
+ new SearchPermalinkWrapper(),
+ new SpoilersWrapper(),
+ new SmallWrapper(),
+ new StrikeThroughWrapper(),
+ ];
+ for (let wrapper of wrappers) {
+ text = wrapper.preprocess(text);
+ }
+ text = marked(text, options);
+ wrappers.reverse();
+ for (let wrapper of wrappers) {
+ text = wrapper.postprocess(text);
+ }
+ return text;
+}
+
+module.exports = {
+ formatMarkdown: formatMarkdown,
+};
diff --git a/client/js/util/misc.js b/client/js/util/misc.js
index 83e286df..256fae64 100644
--- a/client/js/util/misc.js
+++ b/client/js/util/misc.js
@@ -1,6 +1,6 @@
'use strict';
-const marked = require('marked');
+const markdown = require('./markdown.js');
function decamelize(str, sep) {
sep = sep === undefined ? '-' : sep;
@@ -92,69 +92,7 @@ function formatRelativeTime(timeString) {
}
function formatMarkdown(text) {
- const renderer = new marked.Renderer();
-
- const options = {
- renderer: renderer,
- breaks: true,
- sanitize: true,
- smartypants: true,
- };
-
- const sjis = [];
-
- const preDecorator = text => {
- text = text.replace(
- /\[sjis\]((?:[^\[]|\[(?!\/?sjis\]))+)\[\/sjis\]/ig,
- (match, capture) => {
- var ret = '%%%SJIS' + sjis.length;
- sjis.push(capture);
- return ret;
- });
- //prevent ^#... from being treated as headers, due to tag permalinks
- text = text.replace(/^#/g, '%%%#');
- //fix \ before ~ being stripped away
- text = text.replace(/\\~/g, '%%%T');
- //post, user and tags premalinks
- text = text.replace(
- /(^|^\(|(?:[^\]])\(|[\s<>\[\]\)])([+#@][a-zA-Z0-9_-]+)/g,
- '$1[$2]($2)');
- text = text.replace(/\]\(@(\d+)\)/g, '](/post/$1)');
- text = text.replace(/\]\(\+([a-zA-Z0-9_-]+)\)/g, '](/user/$1)');
- text = text.replace(/\]\(#([a-zA-Z0-9_-]+)\)/g, '](/posts/query=$1)');
- return text;
- };
-
- const postDecorator = text => {
- //restore fixes
- text = text.replace(/%%%T/g, '\\~');
- text = text.replace(/%%%#/g, '#');
-
- text = text.replace(
- /(?:)?%%%SJIS(\d+)(?:<\/p>)?/,
- (match, capture) => {
- return '
' + sjis[capture] + '
';
- });
-
- //search permalinks
- text = text.replace(
- /\[search\]((?:[^\[]|\[(?!\/?search\]))+)\[\/search\]/ig,
- '$1
');
- //spoilers
- text = text.replace(
- /\[spoiler\]((?:[^\[]|\[(?!\/?spoiler\]))+)\[\/spoiler\]/ig,
- '$1');
- //[small]
- text = text.replace(
- /\[small\]((?:[^\[]|\[(?!\/?small\]))+)\[\/small\]/ig,
- '$1');
- //strike-through
- text = text.replace(/(^|[^\\])(~~|~)([^~]+)\2/g, '$1$3');
- text = text.replace(/\\~/g, '~');
- return text;
- };
-
- return postDecorator(marked(preDecorator(text), options));
+ return markdown.formatMarkdown(text);
}
function formatUrlParameters(dict) {