From e05e0e5fd22cc9ffaccc58a511651719937ef26f Mon Sep 17 00:00:00 2001 From: rr- Date: Sat, 10 Sep 2016 11:09:24 +0200 Subject: [PATCH] client/util: refactor Markdown formatter code --- client/js/util/markdown.js | 137 +++++++++++++++++++++++++++++++++++++ client/js/util/misc.js | 66 +----------------- 2 files changed, 139 insertions(+), 64 deletions(-) create mode 100644 client/js/util/markdown.js 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) {