From 0836dec4a63fb5bb36d9a94c7c2d699ec9bed0a4 Mon Sep 17 00:00:00 2001
From: yuri
Date: Thu, 14 Dec 2017 15:06:08 +0200
Subject: [PATCH] markdown support
---
.../Espo/Resources/i18n/en_US/Global.json | 2 +-
client/lib/marked.min.js | 6 ++
client/res/templates/about.tpl | 3 +-
client/src/view-helper.js | 93 ++++++-------------
frontend/less/espo/custom.less | 52 +++++++++++
5 files changed, 87 insertions(+), 69 deletions(-)
create mode 100644 client/lib/marked.min.js
diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json
index 32d79057f8..e852c379b2 100644
--- a/application/Espo/Resources/i18n/en_US/Global.json
+++ b/application/Espo/Resources/i18n/en_US/Global.json
@@ -260,7 +260,7 @@
"massRemoveResultSingle": "{count} record has been removed",
"noRecordsRemoved": "No records were removed",
"clickToRefresh": "Click to refresh",
- "streamPostInfo": "Type @username to mention users in the post.\n\nAvailable markdown syntax:\n`code`\n**strong text**\n*emphasized text*\n~deleted text~\n> blockquote\n[link text](url)",
+ "streamPostInfo": "Type @username to mention users in the post.\n\nAvailable markdown syntax:\n`code`\n```multiline code```\n**strong text**\n*emphasized text*\n~~deleted text~~\n> blockquote\n[link text](url)",
"writeYourCommentHere": "Write your comment here",
"writeMessageToUser": "Write a message to {user}",
"writeMessageToSelf": "Write a message on your stream",
diff --git a/client/lib/marked.min.js b/client/lib/marked.min.js
new file mode 100644
index 0000000000..e4c3205560
--- /dev/null
+++ b/client/lib/marked.min.js
@@ -0,0 +1,6 @@
+/**
+ * marked - a markdown parser
+ * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)
+ * https://github.com/chjj/marked
+ */
+(function(){function e(e){this.tokens=[],this.tokens.links={},this.options=e||a.defaults,this.rules=p.normal,this.options.gfm&&(this.options.tables?this.rules=p.tables:this.rules=p.gfm)}function t(e,t){if(this.options=t||a.defaults,this.links=e,this.rules=u.normal,this.renderer=this.options.renderer||new n,this.renderer.options=this.options,!this.links)throw new Error("Tokens array requires a `links` property.");this.options.gfm?this.options.breaks?this.rules=u.breaks:this.rules=u.gfm:this.options.pedantic&&(this.rules=u.pedantic)}function n(e){this.options=e||{}}function r(e){this.tokens=[],this.token=null,this.options=e||a.defaults,this.options.renderer=this.options.renderer||new n,this.renderer=this.options.renderer,this.renderer.options=this.options}function s(e,t){return e.replace(t?/&/g:/&(?!#?\w+;)/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function i(e){return e.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g,function(e,t){return t=t.toLowerCase(),"colon"===t?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}function l(e,t){return e=e.source,t=t||"",function n(r,s){return r?(s=s.source||s,s=s.replace(/(^|[^\[])\^/g,"$1"),e=e.replace(r,s),n):new RegExp(e,t)}}function o(){}function h(e){for(var t,n,r=1;rAn error occured:
"+s(c.message+"",!0)+"
";throw c}}var p={newline:/^\n+/,code:/^( {4}[^\n]+\n*)+/,fences:o,hr:/^( *[-*_]){3,} *(?:\n+|$)/,heading:/^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,nptable:o,lheading:/^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,blockquote:/^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,list:/^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,html:/^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,def:/^ *\[([^\]]+)\]: *([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,table:o,paragraph:/^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,text:/^[^\n]+/};p.bullet=/(?:[*+-]|\d+\.)/,p.item=/^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/,p.item=l(p.item,"gm")(/bull/g,p.bullet)(),p.list=l(p.list)(/bull/g,p.bullet)("hr","\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))")("def","\\n+(?="+p.def.source+")")(),p.blockquote=l(p.blockquote)("def",p.def)(),p._tag="(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b",p.html=l(p.html)("comment",//)("closed",/<(tag)[\s\S]+?<\/\1>/)("closing",/])*?>/)(/tag/g,p._tag)(),p.paragraph=l(p.paragraph)("hr",p.hr)("heading",p.heading)("lheading",p.lheading)("blockquote",p.blockquote)("tag","<"+p._tag)("def",p.def)(),p.normal=h({},p),p.gfm=h({},p.normal,{fences:/^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,paragraph:/^/,heading:/^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/}),p.gfm.paragraph=l(p.paragraph)("(?!","(?!"+p.gfm.fences.source.replace("\\1","\\2")+"|"+p.list.source.replace("\\1","\\3")+"|")(),p.tables=h({},p.gfm,{nptable:/^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,table:/^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/}),e.rules=p,e.lex=function(t,n){var r=new e(n);return r.lex(t)},e.prototype.lex=function(e){return e=e.replace(/\r\n|\r/g,"\n").replace(/\t/g," ").replace(/\u00a0/g," ").replace(/\u2424/g,"\n"),this.token(e,!0)},e.prototype.token=function(e,t,n){for(var r,s,i,l,o,h,a,u,c,e=e.replace(/^ +$/gm,"");e;)if((i=this.rules.newline.exec(e))&&(e=e.substring(i[0].length),i[0].length>1&&this.tokens.push({type:"space"})),i=this.rules.code.exec(e))e=e.substring(i[0].length),i=i[0].replace(/^ {4}/gm,""),this.tokens.push({type:"code",text:this.options.pedantic?i:i.replace(/\n+$/,"")});else if(i=this.rules.fences.exec(e))e=e.substring(i[0].length),this.tokens.push({type:"code",lang:i[2],text:i[3]||""});else if(i=this.rules.heading.exec(e))e=e.substring(i[0].length),this.tokens.push({type:"heading",depth:i[1].length,text:i[2]});else if(t&&(i=this.rules.nptable.exec(e))){for(e=e.substring(i[0].length),h={type:"table",header:i[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:i[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:i[3].replace(/\n$/,"").split("\n")},u=0;u ?/gm,""),this.token(i,t,!0),this.tokens.push({type:"blockquote_end"});else if(i=this.rules.list.exec(e)){for(e=e.substring(i[0].length),l=i[2],this.tokens.push({type:"list_start",ordered:l.length>1}),i=i[0].match(this.rules.item),r=!1,c=i.length,u=0;u1&&o.length>1||(e=i.slice(u+1).join("\n")+e,u=c-1)),s=r||/\n\n(?!\s*$)/.test(h),u!==c-1&&(r="\n"===h.charAt(h.length-1),s||(s=r)),this.tokens.push({type:s?"loose_item_start":"list_item_start"}),this.token(h,!1,n),this.tokens.push({type:"list_item_end"});this.tokens.push({type:"list_end"})}else if(i=this.rules.html.exec(e))e=e.substring(i[0].length),this.tokens.push({type:this.options.sanitize?"paragraph":"html",pre:!this.options.sanitizer&&("pre"===i[1]||"script"===i[1]||"style"===i[1]),text:i[0]});else if(!n&&t&&(i=this.rules.def.exec(e)))e=e.substring(i[0].length),this.tokens.links[i[1].toLowerCase()]={href:i[2],title:i[3]};else if(t&&(i=this.rules.table.exec(e))){for(e=e.substring(i[0].length),h={type:"table",header:i[1].replace(/^ *| *\| *$/g,"").split(/ *\| */),align:i[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:i[3].replace(/(?: *\| *)?\n$/,"").split("\n")},u=0;u])/,autolink:/^<([^ >]+(@|:\/)[^ >]+)>/,url:o,tag:/^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,link:/^!?\[(inside)\]\(href\)/,reflink:/^!?\[(inside)\]\s*\[([^\]]*)\]/,nolink:/^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,strong:/^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,em:/^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,code:/^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,br:/^ {2,}\n(?!\s*$)/,del:o,text:/^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/,u.link=l(u.link)("inside",u._inside)("href",u._href)(),u.reflink=l(u.reflink)("inside",u._inside)(),u.normal=h({},u),u.pedantic=h({},u.normal,{strong:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,em:/^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/}),u.gfm=h({},u.normal,{escape:l(u.escape)("])","~|])")(),url:/^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,del:/^~~(?=\S)([\s\S]*?\S)~~/,text:l(u.text)("]|","~]|")("|","|https?://|")()}),u.breaks=h({},u.gfm,{br:l(u.br)("{2,}","*")(),text:l(u.gfm.text)("{2,}","*")()}),t.rules=u,t.output=function(e,n,r){var s=new t(n,r);return s.output(e)},t.prototype.output=function(e){for(var t,n,r,i,l="";e;)if(i=this.rules.escape.exec(e))e=e.substring(i[0].length),l+=i[1];else if(i=this.rules.autolink.exec(e))e=e.substring(i[0].length),"@"===i[2]?(n=":"===i[1].charAt(6)?this.mangle(i[1].substring(7)):this.mangle(i[1]),r=this.mangle("mailto:")+n):(n=s(i[1]),r=n),l+=this.renderer.link(r,null,n);else if(this.inLink||!(i=this.rules.url.exec(e))){if(i=this.rules.tag.exec(e))!this.inLink&&/^/i.test(i[0])&&(this.inLink=!1),e=e.substring(i[0].length),l+=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(i[0]):s(i[0]):i[0];else if(i=this.rules.link.exec(e))e=e.substring(i[0].length),this.inLink=!0,l+=this.outputLink(i,{href:i[2],title:i[3]}),this.inLink=!1;else if((i=this.rules.reflink.exec(e))||(i=this.rules.nolink.exec(e))){if(e=e.substring(i[0].length),t=(i[2]||i[1]).replace(/\s+/g," "),t=this.links[t.toLowerCase()],!t||!t.href){l+=i[0].charAt(0),e=i[0].substring(1)+e;continue}this.inLink=!0,l+=this.outputLink(i,t),this.inLink=!1}else if(i=this.rules.strong.exec(e))e=e.substring(i[0].length),l+=this.renderer.strong(this.output(i[2]||i[1]));else if(i=this.rules.em.exec(e))e=e.substring(i[0].length),l+=this.renderer.em(this.output(i[2]||i[1]));else if(i=this.rules.code.exec(e))e=e.substring(i[0].length),l+=this.renderer.codespan(s(i[2],!0));else if(i=this.rules.br.exec(e))e=e.substring(i[0].length),l+=this.renderer.br();else if(i=this.rules.del.exec(e))e=e.substring(i[0].length),l+=this.renderer.del(this.output(i[1]));else if(i=this.rules.text.exec(e))e=e.substring(i[0].length),l+=this.renderer.text(s(this.smartypants(i[0])));else if(e)throw new Error("Infinite loop on byte: "+e.charCodeAt(0))}else e=e.substring(i[0].length),n=s(i[1]),r=n,l+=this.renderer.link(r,null,n);return l},t.prototype.outputLink=function(e,t){var n=s(t.href),r=t.title?s(t.title):null;return"!"!==e[0].charAt(0)?this.renderer.link(n,r,this.output(e[1])):this.renderer.image(n,r,s(e[1]))},t.prototype.smartypants=function(e){return this.options.smartypants?e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014\/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014\/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…"):e},t.prototype.mangle=function(e){if(!this.options.mangle)return e;for(var t,n="",r=e.length,s=0;s.5&&(t="x"+t.toString(16)),n+=""+t+";";return n},n.prototype.code=function(e,t,n){if(this.options.highlight){var r=this.options.highlight(e,t);null!=r&&r!==e&&(n=!0,e=r)}return t?''+(n?e:s(e,!0))+"\n
\n":""+(n?e:s(e,!0))+"\n
"},n.prototype.blockquote=function(e){return"\n"+e+"
\n"},n.prototype.html=function(e){return e},n.prototype.heading=function(e,t,n){return"\n"},n.prototype.hr=function(){return this.options.xhtml?"
\n":"
\n"},n.prototype.list=function(e,t){var n=t?"ol":"ul";return"<"+n+">\n"+e+""+n+">\n"},n.prototype.listitem=function(e){return""+e+"\n"},n.prototype.paragraph=function(e){return""+e+"
\n"},n.prototype.table=function(e,t){return"\n"},n.prototype.tablerow=function(e){return"\n"+e+"
\n"},n.prototype.tablecell=function(e,t){var n=t.header?"th":"td",r=t.align?"<"+n+' style="text-align:'+t.align+'">':"<"+n+">";return r+e+""+n+">\n"},n.prototype.strong=function(e){return""+e+""},n.prototype.em=function(e){return""+e+""},n.prototype.codespan=function(e){return""+e+""},n.prototype.br=function(){return this.options.xhtml?"
":"
"},n.prototype.del=function(e){return""+e+""},n.prototype.link=function(e,t,n){if(this.options.sanitize){try{var r=decodeURIComponent(i(e)).replace(/[^\w:]/g,"").toLowerCase()}catch(s){return""}if(0===r.indexOf("javascript:")||0===r.indexOf("vbscript:")||0===r.indexOf("data:"))return""}var l='"+n+""},n.prototype.image=function(e,t,n){var r='
":">"},n.prototype.text=function(e){return e},r.parse=function(e,t,n){var s=new r(t,n);return s.parse(e)},r.prototype.parse=function(e){this.inline=new t(e.links,this.options,this.renderer),this.tokens=e.reverse();for(var n="";this.next();)n+=this.tok();return n},r.prototype.next=function(){return this.token=this.tokens.pop()},r.prototype.peek=function(){return this.tokens[this.tokens.length-1]||0},r.prototype.parseText=function(){for(var e=this.token.text;"text"===this.peek().type;)e+="\n"+this.next().text;return this.inline.output(e)},r.prototype.tok=function(){switch(this.token.type){case"space":return"";case"hr":return this.renderer.hr();case"heading":return this.renderer.heading(this.inline.output(this.token.text),this.token.depth,this.token.text);case"code":return this.renderer.code(this.token.text,this.token.lang,this.token.escaped);case"table":var e,t,n,r,s,i="",l="";for(n="",e=0;egridstack.js by Pavel Reznikov
vis.js by Almende B.V.
Ace
+ Marked by Christopher Jeffrey
@@ -81,7 +82,7 @@
Doctrine (DBAL)
Slim
Cron Expression Parser by Michael Dowling
- Zendframework (Validator, Mail, Ldap)
+ Zendframework (Mail, Ldap)
Monolog
Identicon by Don Park
php-semver by Lars Vierbergen
diff --git a/client/src/view-helper.js b/client/src/view-helper.js
index d1c56787f1..79d5f75004 100644
--- a/client/src/view-helper.js
+++ b/client/src/view-helper.js
@@ -26,32 +26,34 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
-Espo.define('view-helper', [], function () {
+Espo.define('view-helper', ['lib!client/lib/marked.min.js'], function () {
var ViewHelper = function (options) {
this.urlRegex = /(^|[^\(])(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
this._registerHandlebarsHelpers();
- this.mdSearch = [
- /\["?(.*?)"?\]\((.*?)\)/g,
- /\&\#x60;\&\#x60;\&\#x60;\n?([\s\S]*?)\&\#x60;\&\#x60;\&\#x60;/g,
- /\&\#x60;([\s\S]*?)\&\#x60;/g,
- /(\*\*)(.*?)\1/g,
- /(\*)(.*?)\1/g,
- /\~\~(.*?)\~\~/g
- ];
- this.mdReplace = [
- '$1',
- function (s, string) {
- return '' + string.replace(/\*/g, '*').replace(/\~/g, '~') + '
';
+ this.mdBeforeList = [
+ {
+ regex: /\["?(.*?)"?\]\((.*?)\)/g,
+ value: '$1'
},
- function (s, string) {
- return '' + string.replace(/\*/g, '*').replace(/\~/g, '~') + '';
+ {
+ regex: /\&\#x60;\&\#x60;\&\#x60;\n?([\s\S]*?)\&\#x60;\&\#x60;\&\#x60;/g,
+ value: function (s, string) {
+ return '' + string.replace(/\*/g, '*').replace(/\~/g, '~') + '
';
+ }
},
- '$2',
- '$2',
- '$1'
+ {
+ regex: /\&\#x60;([\s\S]*?)\&\#x60;/g,
+ value: function (s, string) {
+ return '' + string.replace(/\*/g, '*').replace(/\~/g, '~') + '';
+ }
+ }
];
+
+ marked.setOptions({
+ breaks: true
+ });
}
_.extend(ViewHelper.prototype, {
@@ -78,47 +80,6 @@ Espo.define('view-helper', [], function () {
return text;
},
- tranformTextMarkdown: function (text) {
- var newline = text.indexOf('\r\n') != -1 ? '\r\n' : text.indexOf('\n') != -1 ? '\n' : '';
-
- if (newline != '') {
- var d = [];
- var r = [];
-
- var p = text.split(newline);
- p.push('');
-
- var isBlockLevel = false;
- p.forEach(function (item, i) {
- if (item.length >= 5 && item.indexOf('> ') === 0) {
- if (!isBlockLevel) {
- d = [];
- isBlockLevel = true;
- }
- d.push(item.replace(/> /gm, ''));
-
- } else {
- if (isBlockLevel) {
- r.push('' + d.join(newline) + '
' + item)
- } else {
- r.push(item);
- }
- isBlockLevel = false;
- }
- }, this);
-
- if (r.slice(-1)[0] == '') {
- r.pop();
- }
-
- var t = r.join(newline);
-
- return t;
- }
-
- return text;
- },
-
_registerHandlebarsHelpers: function () {
var self = this;
@@ -219,21 +180,19 @@ Espo.define('view-helper', [], function () {
Handlebars.registerHelper('complexText', function (text) {
text = text || ''
- text = text.replace(self.urlRegex, '$1[$2]($2)');
+ text = text.replace(this.urlRegex, '$1[$2]($2)');
- text = Handlebars.Utils.escapeExpression(text);
+ text = Handlebars.Utils.escapeExpression(text).replace(/>+/g, '>');
- self.mdSearch.forEach(function (re, i) {
- text = text.replace(re, self.mdReplace[i]);
+ this.mdBeforeList.forEach(function (item) {
+ text = text.replace(item.regex, item.value);
});
- text = self.tranformTextMarkdown(text);
-
- text = text.replace(/(\r\n|\n|\r)/gm, '
');
+ text = marked(text);
text = text.replace('[#see-more-text]', ' ' + self.language.translate('See more')) + '';
return new Handlebars.SafeString(text);
- });
+ }.bind(this));
Handlebars.registerHelper('translateOption', function (name, options) {
var scope = options.hash.scope || null;
diff --git a/frontend/less/espo/custom.less b/frontend/less/espo/custom.less
index b1a1b6a6f0..8d843ec97c 100644
--- a/frontend/less/espo/custom.less
+++ b/frontend/less/espo/custom.less
@@ -1175,6 +1175,58 @@ table.table td.cell .complex-text {
white-space: normal;
}
+.complex-text {
+ h1:first-child,
+ h2:first-child,
+ h3:first-child,
+ h4:first-child,
+ h5:first-child,
+ h6:first-child,
+ p:first-child,
+ ul:first-child,
+ ol:first-child,
+ blockquote:first-child {
+ margin-top: 0 !important;
+ }
+ h1 {
+ font-weight: bold;
+ margin-top: @line-height-computed;
+ margin-bottom: (@line-height-computed / 2);
+ }
+ h2, h3 {
+ font-weight: bold;
+ margin-top: (@line-height-computed / 2);
+ margin-bottom: (@line-height-computed / 2);
+ }
+ h4, h5, h6 {
+ font-weight: normal;
+ margin-top: (@line-height-computed / 2);
+ margin-bottom: (@line-height-computed / 2);
+ }
+ h1 {
+ font-size: floor(@font-size-base * 1.1);
+ }
+ h2, h3, h4, h5, h6 {
+ font-size: floor(@font-size-base);
+ }
+
+ p,
+ ul,
+ ol,
+ pre,
+ blockquote {
+ margin-top: @line-height-computed / 2;
+ }
+
+ ul > li {
+ list-style-type: disc;
+ }
+
+ ul, ol {
+ padding-left: 30px;
+ }
+}
+
#popup-notifications-container {
overflow-y: auto;
overflow-x: hidden;