pyfedi/app/static/js/markdown/editor.js

725 lines
31 KiB
JavaScript

// MIT license
var easyMarkdown = (function() {
'use strict';
var regexplink = new RegExp(
'^' +
// protocol identifier
'(?:(?:https?|ftp)://)' +
// user:pass authentication
'(?:\\S+(?::\\S*)?@)?' +
'(?:' +
// IP address exclusion
// private & local networks
'(?!(?:10|127)(?:\\.\\d{1,3}){3})' +
'(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' +
'(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
// IP address dotted notation octets
// excludes loopback network 0.0.0.0
// excludes reserved space >= 224.0.0.0
// excludes network & broacast addresses
// (first & last IP address of each class)
'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
'(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
'(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
'|' +
// host name
'(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' +
// domain name
'(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' +
// TLD identifier
'(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' +
')' +
// port number
'(?::\\d{2,5})?' +
// resource path
'(?:/\\S*)?' +
'$', 'i'
);
var regexppic = new RegExp(
'^' +
// protocol identifier
'(?:(?:https?|ftp)://)' +
// user:pass authentication
'(?:\\S+(?::\\S*)?@)?' +
'(?:' +
// IP address exclusion
// private & local networks
'(?!(?:10|127)(?:\\.\\d{1,3}){3})' +
'(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})' +
'(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})' +
// IP address dotted notation octets
// excludes loopback network 0.0.0.0
// excludes reserved space >= 224.0.0.0
// excludes network & broacast addresses
// (first & last IP address of each class)
'(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])' +
'(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}' +
'(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))' +
'|' +
// host name
'(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)' +
// domain name
'(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*' +
// TLD identifier
'(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))' +
')' +
// port number
'(?::\\d{2,5})?' +
// resource path
'(?:/\\S*)?' +
// image
'(?:jpg|gif|png)'+
'$', 'i'
);
function createDom(obj){
var nodeArray = [];
for ( var i in obj){
var node = document.createElement(obj[i].type);
for ( var j in obj[i].attrs)
node.setAttribute( j, obj[i].attrs[j]);
if (obj[i].text)
node.appendChild(document.createTextNode(obj[i].text));
nodeArray[i] = node;
if (typeof(obj[i].childrenOf) !== 'undefined')
nodeArray[obj[i].childrenOf].appendChild(node);
}
return nodeArray[0];
}
function createNode(el,attrs,text){
var node = document.createElement(el);
for(var key in attrs)
node.setAttribute(key, attrs[key]);
if (text)
node.appendChild(document.createTextNode(text));
return node;
}
function applyStyle(el,attrs){
for(var key in attrs)
el.style[key] = attrs[key];
}
function merge(obj) {
var i = 1,target, key;
for (; i < arguments.length; i += 1) {
target = arguments[i];
for (key in target)
if (Object.prototype.hasOwnProperty.call(target, key))
obj[key] = target[key];
}
return obj;
}
function getStyle (el,styleProp){
var y;
if (el.currentStyle)
y = el.currentStyle[styleProp];
else if (window.getComputedStyle)
y = document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp);
return y;
}
function easyMarkdown(node, options) {
return new Editor(node, options);
}
/*========== BUTTONS ==========*/
function Buttons(element,options,buttons) {
this.element = element;
this.options = options;
this.locale = merge({}, easyMarkdown.locale, easyMarkdown.locale[options.locale] || {});
this.buttons = {
header: {
title : this.locale.header.title,
text : 'header',
group : 0,
callback : function(e) {
// Append/remove ### surround the selection
var chunk, cursor, selected = e.getSelection(),
content = e.getContent();
if (selected.length === 0) {
// Give extra word
chunk = e.locale.header.description + '\n';
} else {
chunk = selected.text + '\n';
}
var key = 0,
hash='',
start = selected.start-1,
end = selected.start,
prevChr = content.substring(start,end);
while (/^\s+$|^#+$/.test(prevChr)){
if (/^#+$/.test(prevChr))
hash = hash+'#';
key +=1;
prevChr = content.substring(start-key,end-key);
}
if (hash.length > 0){
// already a title
var startLinePos,
endLinePos = content.indexOf('\n', selected.start);
// more than ### -> #
if (hash.length > 2){
hash = '#';
startLinePos = content.indexOf('\n', selected.start - 5);
e.setSelection(startLinePos, endLinePos+1);
e.replaceSelection('\n'+hash+' '+chunk);
cursor = startLinePos+3;
}else{
hash = hash +'#';
startLinePos = content.indexOf('\n', selected.start - (hash.length + 1));
e.setSelection(startLinePos, endLinePos+1);
e.replaceSelection('\n'+hash+' '+chunk);
cursor = selected.start + 1;
}
}else{
// new title
hash= '#';
e.replaceSelection('\n'+hash+' '+ chunk);
cursor = selected.start + 3;
}
e.setSelection(cursor, cursor + chunk.length-1);
return false;
}
},
bold: {
title : this.locale.bold.title,
text : 'bold',
group : 0,
callback : function(e) {
// Give/remove ** surround the selection
var chunk, cursor, selected = e.getSelection(),
content = e.getContent();
if (selected.length === 0) {
// Give extra word
chunk = e.locale.bold.description;
} else {
chunk = selected.text;
}
// transform selection and set the cursor into chunked text
if (content.substr(selected.start - 2, 2) === '**' && content.substr(selected.end, 2) === '**') {
e.setSelection(selected.start - 2, selected.end + 2);
e.replaceSelection(chunk);
cursor = selected.start - 2;
} else {
e.replaceSelection('**' + chunk + '**');
cursor = selected.start + 2;
}
// Set the cursor
e.setSelection(cursor, cursor + chunk.length);
}
},
italic: {
title : this.locale.italic.title,
text : 'italic',
group : 0,
callback : function(e) {
// Give/remove * surround the selection
var chunk, cursor, selected = e.getSelection(),
content = e.getContent();
if (selected.length === 0) {
// Give extra word
chunk = e.locale.italic.description;
} else {
chunk = selected.text;
}
// transform selection and set the cursor into chunked text
if (content.substr(selected.start - 1, 1) === '_' && content.substr(selected.end, 1) === '_') {
e.setSelection(selected.start - 1, selected.end + 1);
e.replaceSelection(chunk);
cursor = selected.start - 1;
} else {
e.replaceSelection('_' + chunk + '_');
cursor = selected.start + 1;
}
// Set the cursor
e.setSelection(cursor, cursor + chunk.length);
}
},
image: {
title : this.locale.image.title,
text : 'image',
group : 1,
callback : function(e) {
// Give ![] surround the selection and prepend the image link
var chunk, cursor, selected = e.getSelection(),
link;
if (selected.length === 0) {
// Give extra word
chunk = e.locale.image.description;
} else {
chunk = selected.text;
}
link = prompt(e.locale.image.title, 'http://');
if (regexppic.test(link)) {
e.replaceSelection('![' + chunk + '](' + link + ' "' + e.locale.image.description + '")');
cursor = selected.start + 2;
e.setSelection(cursor, cursor + chunk.length);
}
return false;
}
},
link: {
title : this.locale.link.title,
text : 'link',
group : 1,
callback : function(e) {
// Give [] surround the selection and prepend the link
var chunk, cursor, selected = e.getSelection(),
link;
if (selected.length === 0) {
// Give extra word
chunk = e.locale.link.description;
} else {
chunk = selected.text;
}
link = prompt(e.locale.link.title, 'http://');
if (regexplink.test(link)) {
e.replaceSelection('[' + chunk + '](' + link + ')');
cursor = selected.start + 1;
// Set the cursor
e.setSelection(cursor, cursor + chunk.length);
}
return false;
}
},
ol: {
title : this.locale.ol.title,
text : 'ol',
group : 2,
callback : function(e) {
// Prepend/Give - surround the selection
var chunk, cursor, selected = e.getSelection();
// transform selection and set the cursor into chunked text
if (selected.length === 0) {
// Give extra word
chunk = e.locale.ol.description;
e.replaceSelection('1. ' + chunk);
// Set the cursor
cursor = selected.start + 3;
} else {
if (selected.text.indexOf('\n') < 0) {
chunk = selected.text;
e.replaceSelection('1. ' + chunk);
// Set the cursor
cursor = selected.start + 3;
} else {
var list = [];
list = selected.text.split('\n');
chunk = list[0];
for (var key in list) {
var index = parseInt(key) + parseInt(1);
list[key] = index + '. ' + list[key];
}
e.replaceSelection('\n\n' + list.join('\n'));
// Set the cursor
cursor = selected.start + 5;
}
}
// Set the cursor
e.setSelection(cursor, cursor + chunk.length);
}
},
ul: {
title : this.locale.ul.title,
text : 'ul',
group : 2,
callback : function(e) {
// Prepend/Give - surround the selection
var chunk, cursor, selected = e.getSelection();
// transform selection and set the cursor into chunked text
if (selected.length === 0) {
// Give extra word
chunk = e.locale.ul.description;
e.replaceSelection('- ' + chunk);
// Set the cursor
cursor = selected.start + 2;
} else {
if (selected.text.indexOf('\n') < 0) {
chunk = selected.text;
e.replaceSelection('- ' + chunk);
// Set the cursor
cursor = selected.start + 2;
} else {
var list = [];
list = selected.text.split('\n');
chunk = list[0];
for (var key in list) {
list[key] = '- ' + list[key];
}
e.replaceSelection('\n\n' + list.join('\n'));
// Set the cursor
cursor = selected.start + 4;
}
}
// Set the cursor
e.setSelection(cursor, cursor + chunk.length);
}
},
comment: {
title : this.locale.comment.title,
text : 'comment',
group : 3,
callback : function(e) {
// Prepend/Give - surround the selection
var chunk, cursor, selected = e.getSelection(),
content = e.getContent();
// transform selection and set the cursor into chunked text
if (selected.length === 0) {
// Give extra word
chunk = e.locale.comment.description;
e.replaceSelection('> ' + chunk);
// Set the cursor
cursor = selected.start + 2;
} else {
if (selected.text.indexOf('\n') < 0) {
chunk = selected.text;
e.replaceSelection('> ' + chunk);
// Set the cursor
cursor = selected.start + 2;
} else {
var list = [];
list = selected.text.split('\n');
chunk = list[0];
for (var key in list)
list[key] = '> ' + list[key];
e.replaceSelection('\n\n' + list.join('\n'));
// Set the cursor
cursor = selected.start + 4;
}
}
// Set the cursor
e.setSelection(cursor, cursor + chunk.length);
}
},
code: {
title : this.locale.code.title,
text : 'code',
group : 3,
callback : function(e) {
// Give/remove ** surround the selection
var chunk, cursor, selected = e.getSelection(),
content = e.getContent();
if (selected.length === 0) {
// Give extra word
chunk = e.locale.code.description;
} else {
chunk = selected.text;
}
// transform selection and set the cursor into chunked text
if (content.substr(selected.start - 4, 4) === '```\n' && content.substr(selected.end, 4) === '\n```') {
e.setSelection(selected.start - 4, selected.end + 4);
e.replaceSelection(chunk);
cursor = selected.start - 4;
} else if (content.substr(selected.start - 1, 1) === '`' && content.substr(selected.end, 1) === '`') {
e.setSelection(selected.start - 1, selected.end + 1);
e.replaceSelection(chunk);
cursor = selected.start - 1;
} else if (content.indexOf('\n') > -1) {
e.replaceSelection('```\n' + chunk + '\n```');
cursor = selected.start + 4;
} else {
e.replaceSelection('`' + chunk + '`');
cursor = selected.start + 1;
}
// Set the cursor
e.setSelection(cursor, cursor + chunk.length);
}
},
preview: {
title : this.locale.preview.title,
text : 'preview',
group : 4,
callback : function(e) {
var txteditor = document.getElementById('easy-markdown');
var preview = document.getElementById('easy-preview');
var button = document.getElementById('easy-preview-close');
button.removeAttribute('disabled');
//preview.childNodes[1].childNodes[0].innerHTML = markdown.toHTML(e.element.value);
var md = window.markdownit();
preview.childNodes[1].innerHTML = md.render(e.element.value);
txteditor.classList.add('is-hidden');
preview.classList.add('is-visible');
}
}
};
if (this.options.framework === 'bootstrap' || this.options.framework === 'foundation'){
return this[this.options.framework]();
}else{
return this.none();
}
}
Buttons.prototype = {
getContent: function() {
return this.element.value;
},
findSelection: function(chunk) {
var content = this.getContent(),
startChunkPosition;
if (startChunkPosition = content.indexOf(chunk), startChunkPosition >= 0 && chunk.length > 0) {
var oldSelection = this.getSelection(),
selection;
this.setSelection(startChunkPosition, startChunkPosition + chunk.length);
selection = this.getSelection();
this.setSelection(oldSelection.start, oldSelection.end);
return selection;
} else {
return null;
}
},
getSelection: function() {
var e = this.element;
return (
('selectionStart' in e && function() {
var l = e.selectionEnd - e.selectionStart;
return {
start: e.selectionStart,
end: e.selectionEnd,
length: l,
text: e.value.substr(e.selectionStart, l)
};
}) ||
/* browser not supported */
function() {
return null;
}
)();
},
setIcons: function(element,button){
if (typeof(this.options.icons) === 'string'){
var t = document.createTextNode(element.title);
button.appendChild(t);
}else{
var i = document.createElement('I');
i.setAttribute('class', this.options.icons[element.text]);
button.appendChild(i);
}
},
setListener: function(node) {
var that = this;
node.addEventListener('click', function(e) {
var element = e.target,
target = (element.nodeName === 'I') ? element.parentNode : element;
that.buttons[target.getAttribute('data-md')].callback(that);
e.preventDefault();
}, false);
return node;
},
setSelection: function(start, end) {
var e = this.element;
return (
('selectionStart' in e && function() {
e.selectionStart = start;
e.selectionEnd = end;
return;
}) ||
/* browser not supported */
function() {
return null;
}
)();
},
replaceSelection: function(text) {
var e = this.element;
return (
('selectionStart' in e && function() {
e.value = e.value.substr(0, e.selectionStart) + text + e.value.substr(e.selectionEnd, e.value.length);
// Set cursor to the last replacement end
e.selectionStart = e.value.length;
return this;
})
)();
}
};
Buttons.prototype.bootstrap = function(){
var button_groups = createDom ({
0 : {'type':'div'},
1 : {'type':'div','attrs': {'class':'btn-group','role':'group','style':'margin:5px;'},'childrenOf': 0},
2 : {'type':'div','attrs': {'class':'btn-group','role':'group','style':'margin:5px;'},'childrenOf': 0},
3 : {'type':'div','attrs': {'class':'btn-group','role':'group','style':'margin:5px;'},'childrenOf': 0},
4 : {'type':'div','attrs': {'class':'btn-group','role':'group','style':'margin:5px;'},'childrenOf': 0},
5 : {'type':'div','attrs': {'class':'btn-group','role':'group','style':'margin:5px;'},'childrenOf': 0}
});
for (var i in this.buttons) {
var obj = this.buttons[i];
if (this.options.disabled[obj.text] !== true) {
var button = createNode('BUTTON',{'class':this.options.btnClass,'data-md':obj.text,'title':obj.title });
this.setIcons(obj,button);
button_groups.childNodes[obj.group].appendChild(button);
}
}
this.setListener(button_groups);
return button_groups;
};
Buttons.prototype.foundation = function(){
var container = createNode('UL',{'class': 'button-group','style':'margin: 0 0 10px 0;'});
for (var i in this.buttons) {
var obj = this.buttons[i];
if (this.options.disabled[obj.text] !== true) {
var li = createNode('LI');
var a = createNode('A',{'class':this.options.btnClass,'data-md':obj.text,'title':obj.title });
this.setIcons(obj,a);
container.appendChild(li).appendChild(a);
}
}
this.setListener(container);
return container;
};
Buttons.prototype.none = function(){
var container = document.createElement('DIV');
for (var key in this.buttons) {
var obj = this.buttons[key];
if (this.options.disabled[obj.text] !== true) {
var button = createNode('BUTTON',{'class':this.options.btnClass,'data-md':obj.text,'title':obj.title });
this.setIcons(obj,button);
container.appendChild(button);
}
}
this.setListener(container);
return container;
};
/*========== SKELETON ==========*/
function Skeleton(options,textarea,buttons) {
this.element = textarea;
this.options = options;
this.buttons = buttons;
return this.build();
}
Skeleton.prototype = {
build : function(){
var buttons = new Buttons(this.element,this.options,this.buttons);
var dom = createDom ({
0 : {'type':'div','attrs': {'class':'easy-markdown','id':'easy-markdown'}},
1 : {'type':'div','attrs': {'id':'easy-markdown-buttons'},'childrenOf': 0},
2 : {'type':'div','attrs': {'id':'easy-markdown-textarea'},'childrenOf': 0}
});
dom.childNodes[0].appendChild(buttons);
dom.childNodes[1].appendChild(this.element);
return dom;
}
};
/*========== PREVIEW ==========*/
function Preview(options,parent) {
this.parent = parent;
this.options = options;
this.locale = merge({}, easyMarkdown.locale, easyMarkdown.locale[options.locale] || {});
return this.build();
}
Preview.prototype = {
build: function() {
var dom = createDom ({
0 : {'type':'div','attrs': {'class':'easy-preview','id':'easy-preview','style':'height:'+this.parent.clientHeight +'px;'}},
1 : {'type':'div','attrs': {'id':'easy-preview-buttons'},'childrenOf': 0},
2 : {'type':'div','attrs': {'id':'easy-preview-html','style':'overflow:auto;'},'childrenOf': 0},
3 : {'type':'button','attrs':{'class':this.options.btnClass,'disabled':'disabled','id':'easy-preview-close'},'text':this.locale.getback.title,'childrenOf': 1},
4 : {'type':'HR','childrenOf':1}
});
this.setListener(dom.childNodes[0].childNodes[0]);
this.parent.appendChild(dom);
dom.childNodes[1].style.height = this.parent.clientHeight - dom.childNodes[0].clientHeight - 20 +'px';
},
setListener : function(node){
node.addEventListener('click', function(e) {
var txteditor = document.getElementById('easy-markdown');
var preview = document.getElementById('easy-preview');
var button = document.getElementById('easy-preview-close');
button.setAttribute('disabled','disabled');
txteditor.classList.remove('is-hidden');
preview.classList.remove('is-visible');
e.preventDefault();
}, false);
return node;
}
};
/*========== EDITOR ==========*/
function Editor(node, options) {
this.element = node;
this.parent = node.parentNode;
this.parent.innerHTML = '';
this.options = merge({}, Editor.defaults, options || {});
// test if markdown.js is missing
if (typeof(markdownit) === 'undefined')
this.options.disabled.preview = true;
this.preview = 'off';
var skeleton = new Skeleton(this.options,this.element);
node.style.width = this.options.width;
this.parent.appendChild(skeleton);
applyStyle(this.parent,{position:'relative',height:skeleton.clientHeight+'px',overflow:'hidden'});
new Preview(this.options,this.parent);
}
easyMarkdown.Preview = Preview;
easyMarkdown.Buttons = Buttons;
easyMarkdown.Skeleton = Skeleton;
easyMarkdown.Editor = Editor;
easyMarkdown.locale = {
bold: {
title:'Bold',
description:'Strong Text'
},
italic: {
title:'Italic' ,
description: 'Emphasized text'
},
header: {
title:'Header',
description: 'Heading text'
},
image: {
title:'Image',
description:'Image description'
},
link: {
title:'Link',
description:'Link description'
},
ol: {
title:'Numbered',
description:'Numbered list'
},
ul: {
title:'Bullet',
description:'Bulleted list'
},
comment: {
title:'Comment',
description: 'Comment'
},
code: {
title:'Code',
description: 'code text'
},
preview: {
title:'Preview'
},
getback: {
title: 'Get back'
}
};
Editor.defaults = {
width: '100%',
btnClass: '',
framework: 'none',
locale:'',
icons: '',
disabled: {
bold: false,
italic: false,
header: false,
image: false,
link: false,
ol: false,
ul: false,
comment: false,
code: false,
preview: false
}
};
return easyMarkdown;
})();