HackMD/public/js/extra.js

1151 lines
38 KiB
JavaScript
Raw Normal View History

require('prismjs/themes/prism.css');
require('prismjs/components/prism-wiki');
require('prismjs/components/prism-haskell');
require('prismjs/components/prism-go');
require('prismjs/components/prism-typescript');
require('prismjs/components/prism-jsx');
2017-01-05 09:52:32 +00:00
import Prism from 'prismjs';
import hljs from 'highlight.js';
import PDFObject from 'pdfobject';
import S from 'string';
import { saveAs } from 'file-saver';
2017-01-13 14:51:44 +00:00
require('./lib/common/login');
2016-10-13 00:56:56 +00:00
require('../vendor/md-toc');
var Viz = require("viz.js");
2016-10-09 02:51:39 +00:00
2015-07-01 16:10:20 +00:00
//auto update last change
2016-10-13 07:13:03 +00:00
window.createtime = null;
window.lastchangetime = null;
window.lastchangeui = {
status: $(".ui-status-lastchange"),
time: $(".ui-lastchange"),
user: $(".ui-lastchangeuser"),
nouser: $(".ui-no-lastchangeuser")
}
2015-07-01 16:10:20 +00:00
2017-01-05 09:52:32 +00:00
const ownerui = $(".ui-owner");
export function updateLastChange() {
if (!lastchangeui) return;
if (createtime) {
if (createtime && !lastchangetime) {
lastchangeui.status.text('created');
} else {
lastchangeui.status.text('changed');
}
2017-01-05 09:52:32 +00:00
const time = lastchangetime || createtime;
lastchangeui.time.html(moment(time).fromNow());
lastchangeui.time.attr('title', moment(time).format('llll'));
2015-07-01 16:10:20 +00:00
}
}
setInterval(updateLastChange, 60000);
2016-10-13 07:13:03 +00:00
window.lastchangeuser = null;
window.lastchangeuserprofile = null;
2017-01-05 09:52:32 +00:00
export function updateLastChangeUser() {
if (lastchangeui) {
if (lastchangeuser && lastchangeuserprofile) {
2017-01-05 09:52:32 +00:00
const icon = lastchangeui.user.children('i');
icon.attr('title', lastchangeuserprofile.name).tooltip('fixTitle');
if (lastchangeuserprofile.photo)
2017-01-05 09:52:32 +00:00
icon.attr('style', `background-image:url(${lastchangeuserprofile.photo})`);
lastchangeui.user.show();
lastchangeui.nouser.hide();
} else {
lastchangeui.user.hide();
lastchangeui.nouser.show();
}
}
}
2016-10-13 15:29:25 +00:00
window.owner = null;
window.ownerprofile = null;
2017-01-05 09:52:32 +00:00
export function updateOwner() {
if (ownerui) {
if (owner && ownerprofile && owner !== lastchangeuser) {
2017-01-05 09:52:32 +00:00
const icon = ownerui.children('i');
icon.attr('title', ownerprofile.name).tooltip('fixTitle');
2017-01-05 09:52:32 +00:00
const styleString = `background-image:url(${ownerprofile.photo})`;
if (ownerprofile.photo && icon.attr('style') !== styleString)
icon.attr('style', styleString);
ownerui.show();
} else {
ownerui.hide();
}
}
}
2015-05-04 07:53:29 +00:00
//get title
function getTitle(view) {
2017-01-05 09:52:32 +00:00
let title = "";
if (md && md.meta && md.meta.title && (typeof md.meta.title == "string" || typeof md.meta.title == "number")) {
title = md.meta.title;
2015-05-04 07:53:29 +00:00
} else {
2017-01-05 09:52:32 +00:00
const h1s = view.find("h1");
if (h1s.length > 0) {
title = h1s.first().text();
} else {
title = null;
}
2015-05-04 07:53:29 +00:00
}
return title;
}
2015-07-01 16:10:20 +00:00
2015-05-04 07:53:29 +00:00
//render title
2017-01-05 09:52:32 +00:00
export function renderTitle(view) {
let title = getTitle(view);
2015-07-01 16:10:20 +00:00
if (title) {
2015-05-04 07:53:29 +00:00
title += ' - HackMD';
} else {
2016-05-27 17:51:45 +00:00
title = 'HackMD - Collaborative markdown notes';
2015-05-04 07:53:29 +00:00
}
return title;
}
2015-07-01 16:10:20 +00:00
2015-05-04 07:53:29 +00:00
//render filename
2017-01-05 09:52:32 +00:00
export function renderFilename(view) {
let filename = getTitle(view);
2015-07-01 16:10:20 +00:00
if (!filename) {
2015-05-04 07:53:29 +00:00
filename = 'Untitled';
}
return filename;
}
// render tags
2017-01-05 09:52:32 +00:00
export function renderTags(view) {
const tags = [];
const rawtags = [];
if (md && md.meta && md.meta.tags && (typeof md.meta.tags == "string" || typeof md.meta.tags == "number")) {
2017-01-05 09:52:32 +00:00
const metaTags = (`${md.meta.tags}`).split(',');
for (var i = 0; i < metaTags.length; i++) {
2017-01-05 09:52:32 +00:00
const text = metaTags[i].trim();
if (text) rawtags.push(text);
}
} else {
2017-01-05 09:52:32 +00:00
view.find('h6').each((key, value) => {
if (/^tags/gmi.test($(value).text())) {
2017-01-05 09:52:32 +00:00
const codes = $(value).find("code");
for (let i = 0; i < codes.length; i++) {
const text = codes[i].innerHTML.trim();
if (text) rawtags.push(text);
}
}
});
}
for (var i = 0; i < rawtags.length; i++) {
2017-01-05 09:52:32 +00:00
let found = false;
for (let j = 0; j < tags.length; j++) {
if (tags[j] == rawtags[i]) {
found = true;
break;
}
}
if (!found)
tags.push(rawtags[i]);
}
return tags;
}
2015-07-01 16:10:20 +00:00
function slugifyWithUTF8(text) {
2017-01-05 09:52:32 +00:00
let newText = S(text.toLowerCase()).trim().stripTags().dasherize().s;
2015-07-01 16:10:20 +00:00
newText = newText.replace(/([\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\[\\\]\^\`\{\|\}\~])/g, '');
return newText;
}
2017-01-05 09:52:32 +00:00
export function isValidURL(str) {
const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
2016-06-21 13:44:06 +00:00
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator
if (!pattern.test(str)) {
return false;
} else {
return true;
}
}
//parse meta
2017-01-05 09:52:32 +00:00
export function parseMeta(md, edit, view, toc, tocAffix) {
let lang = null;
let dir = null;
let breaks = true;
if (md && md.meta) {
2017-01-05 09:52:32 +00:00
const meta = md.meta;
lang = meta.lang;
dir = meta.dir;
breaks = meta.breaks;
}
//text language
if (lang && typeof lang == "string") {
view.attr('lang', lang);
toc.attr('lang', lang);
tocAffix.attr('lang', lang);
if (edit)
edit.attr('lang', lang);
} else {
view.removeAttr('lang');
toc.removeAttr('lang');
tocAffix.removeAttr('lang');
if (edit)
edit.removeAttr('lang', lang);
}
//text direction
if (dir && typeof dir == "string") {
view.attr('dir', dir);
toc.attr('dir', dir);
tocAffix.attr('dir', dir);
} else {
view.removeAttr('dir');
toc.removeAttr('dir');
tocAffix.removeAttr('dir');
}
//breaks
if (typeof breaks === 'boolean' && !breaks) {
md.options.breaks = false;
} else {
md.options.breaks = true;
}
}
window.viewAjaxCallback = null;
2015-05-15 04:58:13 +00:00
//regex for extra tags
2017-01-05 09:52:32 +00:00
const spaceregex = /\s*/;
const notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/;
let coloregex = /\[color=([#|\(|\)|\s|\,|\w]*?)\]/;
2015-07-01 16:10:20 +00:00
coloregex = new RegExp(coloregex.source + notinhtmltagregex.source, "g");
2017-01-05 09:52:32 +00:00
let nameregex = /\[name=(.*?)\]/;
let timeregex = /\[time=([:|,|+|-|\(|\)|\s|\w]*?)\]/;
const nameandtimeregex = new RegExp(nameregex.source + spaceregex.source + timeregex.source + notinhtmltagregex.source, "g");
2015-07-01 16:10:20 +00:00
nameregex = new RegExp(nameregex.source + notinhtmltagregex.source, "g");
timeregex = new RegExp(timeregex.source + notinhtmltagregex.source, "g");
function replaceExtraTags(html) {
html = html.replace(coloregex, '<span class="color" data-color="$1"></span>');
html = html.replace(nameandtimeregex, '<small><i class="fa fa-user"></i> $1 <i class="fa fa-clock-o"></i> $2</small>');
html = html.replace(nameregex, '<small><i class="fa fa-user"></i> $1</small>');
html = html.replace(timeregex, '<small><i class="fa fa-clock-o"></i> $1</small>');
return html;
}
if (typeof mermaid !== 'undefined' && mermaid) mermaid.startOnLoad = false;
2015-05-04 07:53:29 +00:00
//dynamic event or object binding here
2017-01-05 09:52:32 +00:00
export function finishView(view) {
//todo list
2017-01-05 09:52:32 +00:00
const lis = view.find('li.raw').removeClass("raw").sortByDepth().toArray();
for (let li of lis) {
let html = $(li).clone()[0].innerHTML;
const p = $(li).children('p');
if (p.length == 1) {
html = p.html();
li = p[0];
}
html = replaceExtraTags(html);
li.innerHTML = html;
2017-01-05 09:52:32 +00:00
let disabled = 'disabled';
if(typeof editor !== 'undefined' && havePermission())
disabled = '';
if (/^\s*\[[x ]\]\s*/.test(html)) {
2017-01-05 09:52:32 +00:00
li.innerHTML = html.replace(/^\s*\[ \]\s*/, `<input type="checkbox" class="task-list-item-checkbox "${disabled}><label></label>`)
.replace(/^\s*\[x\]\s*/, `<input type="checkbox" class="task-list-item-checkbox" checked ${disabled}><label></label>`);
li.setAttribute('class', 'task-list-item');
}
if (typeof editor !== 'undefined' && havePermission())
$(li).find('input').change(toggleTodoEvent);
//color tag in list will convert it to tag icon with color
2017-01-05 09:52:32 +00:00
const tag_color = $(li).closest('ul').find(".color");
tag_color.each((key, value) => {
$(value).addClass('fa fa-tag').css('color', $(value).attr('data-color'));
});
}
2017-01-05 09:52:32 +00:00
2015-05-04 07:53:29 +00:00
//youtube
view.find("div.youtube.raw").removeClass("raw")
2015-07-01 16:10:20 +00:00
.click(function () {
imgPlayiframe(this, '//www.youtube.com/embed/');
});
2015-05-04 07:53:29 +00:00
//vimeo
view.find("div.vimeo.raw").removeClass("raw")
2015-05-04 07:53:29 +00:00
.click(function () {
imgPlayiframe(this, '//player.vimeo.com/video/');
})
2017-01-05 09:52:32 +00:00
.each((key, value) => {
2015-05-04 07:53:29 +00:00
$.ajax({
type: 'GET',
2017-01-05 09:52:32 +00:00
url: `//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`,
2015-05-04 07:53:29 +00:00
jsonp: 'callback',
dataType: 'jsonp',
2017-01-05 09:52:32 +00:00
success(data) {
const thumbnail_src = data[0].thumbnail_large;
const image = `<img src="${thumbnail_src}" />`;
$(value).prepend(image);
if(viewAjaxCallback) viewAjaxCallback();
2015-05-04 07:53:29 +00:00
}
});
});
//gist
2017-01-05 09:52:32 +00:00
view.find("code[data-gist-id]").each((key, value) => {
2015-07-01 16:10:20 +00:00
if ($(value).children().length == 0)
2015-05-15 04:58:13 +00:00
$(value).gist(viewAjaxCallback);
2015-05-04 07:53:29 +00:00
});
//sequence diagram
2017-01-05 09:52:32 +00:00
const sequences = view.find("div.sequence-diagram.raw").removeClass("raw");
sequences.each((key, value) => {
try {
var $value = $(value);
2017-01-05 09:52:32 +00:00
const $ele = $(value).parent().parent();
2017-01-05 09:52:32 +00:00
const sequence = $value;
sequence.sequenceDiagram({
theme: 'simple'
});
$ele.addClass('sequence-diagram');
$value.children().unwrap().unwrap();
2017-01-05 09:52:32 +00:00
const svg = $ele.find('> svg');
svg[0].setAttribute('viewBox', `0 0 ${svg.attr('width')} ${svg.attr('height')}`);
svg[0].setAttribute('preserveAspectRatio', 'xMidYMid meet');
} catch (err) {
$value.unwrap();
$value.parent().append('<div class="alert alert-warning">' + err + '</div>');
console.warn(err);
}
});
//flowchart
2017-01-05 09:52:32 +00:00
const flow = view.find("div.flow-chart.raw").removeClass("raw");
flow.each((key, value) => {
try {
var $value = $(value);
2017-01-05 09:52:32 +00:00
const $ele = $(value).parent().parent();
2017-01-05 09:52:32 +00:00
const chart = flowchart.parse($value.text());
$value.html('');
chart.drawSVG(value, {
'line-width': 2,
'fill': 'none',
'font-size': '16px',
'font-family': "'Andale Mono', monospace"
});
$ele.addClass('flow-chart');
$value.children().unwrap().unwrap();
} catch (err) {
$value.unwrap();
$value.parent().append('<div class="alert alert-warning">' + err + '</div>');
console.warn(err);
}
});
2016-01-12 13:56:29 +00:00
//graphviz
var graphvizs = view.find("div.graphviz.raw").removeClass("raw");
graphvizs.each(function (key, value) {
try {
var $value = $(value);
var $ele = $(value).parent().parent();
var graphviz = Viz($value.text());
if (!graphviz) throw Error('viz.js output empty graph');
$value.html(graphviz);
$ele.addClass('graphviz');
$value.children().unwrap().unwrap();
} catch (err) {
$value.unwrap();
$value.parent().append('<div class="alert alert-warning">' + err + '</div>');
console.warn(err);
}
});
//mermaid
2017-01-05 09:52:32 +00:00
const mermaids = view.find("div.mermaid.raw").removeClass("raw");
mermaids.each((key, value) => {
try {
var $value = $(value);
2017-01-05 09:52:32 +00:00
const $ele = $(value).closest('pre');
2017-01-05 09:52:32 +00:00
let mermaidError = null;
mermaid.parseError = (err, hash) => {
mermaidError = err;
};
if (mermaidAPI.parse($value.text())) {
$ele.addClass('mermaid');
$ele.html($value.text());
mermaid.init(undefined, $ele);
} else {
throw new Error(mermaidError);
}
2016-01-12 13:56:29 +00:00
} catch (err) {
$value.unwrap();
$value.parent().append('<div class="alert alert-warning">' + err + '</div>');
console.warn(err);
2016-01-12 13:56:29 +00:00
}
});
2015-07-01 16:10:20 +00:00
//image href new window(emoji not included)
2017-01-05 09:52:32 +00:00
const images = view.find("img.raw[src]").removeClass("raw");
images.each((key, value) => {
// if it's already wrapped by link, then ignore
2017-01-05 09:52:32 +00:00
const $value = $(value);
$value[0].onload = e => {
if(viewAjaxCallback) viewAjaxCallback();
};
2015-07-01 16:10:20 +00:00
});
//blockquote
2017-01-05 09:52:32 +00:00
const blockquote = view.find("blockquote.raw").removeClass("raw");
const blockquote_p = blockquote.find("p");
blockquote_p.each((key, value) => {
let html = $(value).html();
html = replaceExtraTags(html);
2015-07-01 16:10:20 +00:00
$(value).html(html);
});
//color tag in blockquote will change its left border color
2017-01-05 09:52:32 +00:00
const blockquote_color = blockquote.find(".color");
blockquote_color.each((key, value) => {
2015-07-01 16:10:20 +00:00
$(value).closest("blockquote").css('border-left-color', $(value).attr('data-color'));
});
//slideshare
view.find("div.slideshare.raw").removeClass("raw")
2017-01-05 09:52:32 +00:00
.each((key, value) => {
$.ajax({
type: 'GET',
2017-01-05 09:52:32 +00:00
url: `//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`,
jsonp: 'callback',
dataType: 'jsonp',
2017-01-05 09:52:32 +00:00
success(data) {
const $html = $(data.html);
const iframe = $html.closest('iframe');
const caption = $html.closest('div');
const inner = $('<div class="inner"></div>').append(iframe);
const height = iframe.attr('height');
const width = iframe.attr('width');
const ratio = (height / width) * 100;
inner.css('padding-bottom', `${ratio}%`);
$(value).html(inner).append(caption);
if(viewAjaxCallback) viewAjaxCallback();
}
});
});
//speakerdeck
view.find("div.speakerdeck.raw").removeClass("raw")
2017-01-05 09:52:32 +00:00
.each((key, value) => {
const url = `https://speakerdeck.com/oembed.json?url=https%3A%2F%2Fspeakerdeck.com%2F${encodeURIComponent($(value).attr('data-speakerdeckid'))}`;
//use yql because speakerdeck not support jsonp
$.ajax({
url: 'https://query.yahooapis.com/v1/public/yql',
data: {
2017-01-05 09:52:32 +00:00
q: `select * from json where url ='${url}'`,
format: "json"
},
dataType: "jsonp",
2017-01-05 09:52:32 +00:00
success(data) {
2016-03-16 04:46:29 +00:00
if (!data.query || !data.query.results) return;
2017-01-05 09:52:32 +00:00
const json = data.query.results.json;
const html = json.html;
var ratio = json.height / json.width;
$(value).html(html);
2017-01-05 09:52:32 +00:00
const iframe = $(value).children('iframe');
const src = iframe.attr('src');
if (src.indexOf('//') == 0)
2017-01-05 09:52:32 +00:00
iframe.attr('src', `https:${src}`);
const inner = $('<div class="inner"></div>').append(iframe);
const height = iframe.attr('height');
const width = iframe.attr('width');
var ratio = (height / width) * 100;
2017-01-05 09:52:32 +00:00
inner.css('padding-bottom', `${ratio}%`);
$(value).html(inner);
if(viewAjaxCallback) viewAjaxCallback();
}
});
});
2016-06-21 13:44:06 +00:00
//pdf
view.find("div.pdf.raw").removeClass("raw")
2016-10-13 00:56:56 +00:00
.each(function (key, value) {
2017-01-05 09:52:32 +00:00
const url = $(value).attr('data-pdfurl');
const inner = $('<div></div>');
2016-10-13 00:56:56 +00:00
$(this).append(inner);
PDFObject.embed(url, inner, {
height: '400px'
});
2016-06-21 13:44:06 +00:00
});
//syntax highlighting
view.find("code.raw").removeClass("raw")
2017-01-05 09:52:32 +00:00
.each((key, value) => {
const langDiv = $(value);
if (langDiv.length > 0) {
2017-01-05 09:52:32 +00:00
const reallang = langDiv[0].className.replace(/hljs|wrap/g, '').trim();
const codeDiv = langDiv.find('.code');
let code = "";
if (codeDiv.length > 0) code = codeDiv.html();
else code = langDiv.html();
if (!reallang) {
var result = {
value: code
};
} else if (reallang == "haskell" || reallang == "go" || reallang == "typescript" || reallang == "jsx") {
code = S(code).unescapeHTML().s;
var result = {
value: Prism.highlight(code, Prism.languages[reallang])
};
} else if (reallang == "tiddlywiki" || reallang == "mediawiki") {
code = S(code).unescapeHTML().s;
var result = {
value: Prism.highlight(code, Prism.languages.wiki)
};
} else {
code = S(code).unescapeHTML().s;
2017-01-05 09:52:32 +00:00
const languages = hljs.listLanguages();
if (!languages.includes(reallang)) {
var result = hljs.highlightAuto(code);
} else {
var result = hljs.highlight(reallang, code);
}
}
if (codeDiv.length > 0) codeDiv.html(result.value);
else langDiv.html(result.value);
}
});
//mathjax
2017-01-05 09:52:32 +00:00
const mathjaxdivs = view.find('span.mathjax.raw').removeClass("raw").toArray();
try {
if (mathjaxdivs.length > 1) {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, mathjaxdivs]);
MathJax.Hub.Queue(viewAjaxCallback);
} else if (mathjaxdivs.length > 0) {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, mathjaxdivs[0]]);
MathJax.Hub.Queue(viewAjaxCallback);
}
} catch (err) {
console.warn(err);
}
2015-05-04 07:53:29 +00:00
//render title
document.title = renderTitle(view);
}
2015-06-01 10:04:25 +00:00
2015-05-04 07:53:29 +00:00
//only static transform should be here
2017-01-05 09:52:32 +00:00
export function postProcess(code) {
const result = $(`<div>${code}</div>`);
//link should open in new window or tab
2016-01-31 21:28:52 +00:00
result.find('a:not([href^="#"]):not([target])').attr('target', '_blank');
//update continue line numbers
2017-01-05 09:52:32 +00:00
const linenumberdivs = result.find('.gutter.linenumber').toArray();
for (let i = 0; i < linenumberdivs.length; i++) {
if ($(linenumberdivs[i]).hasClass('continue')) {
2017-01-05 09:52:32 +00:00
const startnumber = linenumberdivs[i - 1] ? parseInt($(linenumberdivs[i - 1]).find('> span').last().attr('data-linenumber')) : 0;
$(linenumberdivs[i]).find('> span').each((key, value) => {
$(value).attr('data-linenumber', startnumber + key + 1);
});
}
}
// show yaml meta paring error
if (md.metaError) {
var warning = result.find('div#meta-error');
if (warning && warning.length > 0) {
warning.text(md.metaError)
} else {
warning = $('<div id="meta-error" class="alert alert-warning">' + md.metaError + '</div>')
result.prepend(warning);
}
}
return result;
}
window.postProcess = postProcess;
2016-06-17 08:17:37 +00:00
function generateCleanHTML(view) {
2017-01-05 09:52:32 +00:00
const src = view.clone();
const eles = src.find('*');
//remove syncscroll parts
eles.removeClass('part');
src.find('*[class=""]').removeAttr('class');
eles.removeAttr('data-startline data-endline');
src.find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll');
//remove gist content
src.find("code[data-gist-id]").children().remove();
//disable todo list
src.find("input.task-list-item-checkbox").attr('disabled', '');
//replace emoji image path
2017-01-05 09:52:32 +00:00
src.find("img.emoji").each((key, value) => {
let name = $(value).attr('alt');
name = name.substr(1);
name = name.slice(0, name.length - 1);
2017-01-05 09:52:32 +00:00
$(value).attr('src', `https://www.tortue.me/emoji/${name}.png`);
});
//replace video to iframe
2017-01-05 09:52:32 +00:00
src.find("div[data-videoid]").each((key, value) => {
const id = $(value).attr('data-videoid');
const style = $(value).attr('style');
let url = null;
if ($(value).hasClass('youtube')) {
url = 'https://www.youtube.com/embed/';
} else if ($(value).hasClass('vimeo')) {
url = 'https://player.vimeo.com/video/';
2015-07-01 16:10:20 +00:00
}
if (url) {
2017-01-05 09:52:32 +00:00
const iframe = $('<iframe frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>');
iframe.attr('src', url + id);
iframe.attr('style', style);
$(value).html(iframe);
2015-05-04 07:53:29 +00:00
}
});
2016-06-17 08:17:37 +00:00
return src;
}
2017-01-05 09:52:32 +00:00
export function exportToRawHTML(view) {
const filename = `${renderFilename(ui.area.markdown)}.html`;
const src = generateCleanHTML(view);
2016-06-17 08:17:37 +00:00
$(src).find('a.anchor').remove();
2017-01-05 09:52:32 +00:00
const html = src[0].outerHTML;
const blob = new Blob([html], {
2016-06-17 08:17:37 +00:00
type: "text/html;charset=utf-8"
});
2017-02-03 08:35:16 +00:00
saveAs(blob, filename, true);
2016-06-17 08:17:37 +00:00
}
//extract markdown body to html and compile to template
2017-01-05 09:52:32 +00:00
export function exportToHTML(view) {
const title = renderTitle(ui.area.markdown);
const filename = `${renderFilename(ui.area.markdown)}.html`;
const src = generateCleanHTML(view);
//generate toc
2017-01-05 09:52:32 +00:00
const toc = $('#ui-toc').clone();
toc.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll');
2017-01-05 09:52:32 +00:00
const tocAffix = $('#ui-toc-affix').clone();
tocAffix.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll');
//generate html via template
2017-01-05 09:52:32 +00:00
$.get(`${serverurl}/build/html.min.css`, css => {
$.get(`${serverurl}/views/html.hbs`, data => {
const template = Handlebars.compile(data);
const context = {
url: serverurl,
2017-01-05 09:52:32 +00:00
title,
css,
html: src[0].outerHTML,
'ui-toc': toc.html(),
'ui-toc-affix': tocAffix.html(),
2017-01-05 09:52:32 +00:00
lang: (md && md.meta && md.meta.lang) ? `lang="${md.meta.lang}"` : null,
dir: (md && md.meta && md.meta.dir) ? `dir="${md.meta.dir}"` : null
};
2017-01-05 09:52:32 +00:00
const html = template(context);
// console.log(html);
2017-01-05 09:52:32 +00:00
const blob = new Blob([html], {
type: "text/html;charset=utf-8"
});
2017-02-03 08:35:16 +00:00
saveAs(blob, filename, true);
});
});
2015-07-01 16:10:20 +00:00
}
//jQuery sortByDepth
$.fn.sortByDepth = function () {
2017-01-05 09:52:32 +00:00
const ar = this.map(function () {
2015-07-01 16:10:20 +00:00
return {
length: $(this).parents().length,
elt: this
}
2017-01-05 09:52:32 +00:00
}).get();
const result = [];
let i = ar.length;
ar.sort((a, b) => a.length - b.length);
2015-07-01 16:10:20 +00:00
while (i--) {
result.push(ar[i].elt);
}
return $(result);
};
function toggleTodoEvent(e) {
2017-01-05 09:52:32 +00:00
const startline = $(this).closest('li').attr('data-startline') - 1;
const line = editor.getLine(startline);
const matches = line.match(/^[>\s]*[\-\+\*]\s\[([x ])\]/);
if (matches && matches.length >= 2) {
2017-01-05 09:52:32 +00:00
let checked = null;
if (matches[1] == 'x')
checked = true;
else if (matches[1] == ' ')
checked = false;
2017-01-05 09:52:32 +00:00
const replacements = matches[0].match(/(^[>\s]*[\-\+\*]\s\[)([x ])(\])/);
editor.replaceRange(checked ? ' ' : 'x', {
line: startline,
ch: replacements[1].length
}, {
line: startline,
ch: replacements[1].length + 1
}, '+input');
}
}
2015-07-01 16:10:20 +00:00
//remove hash
function removeHash() {
history.pushState("", document.title, window.location.pathname + window.location.search);
}
2017-01-05 09:52:32 +00:00
let tocExpand = false;
function checkExpandToggle() {
2017-01-05 09:52:32 +00:00
const toc = $('.ui-toc-dropdown .toc');
const toggle = $('.expand-toggle');
if (!tocExpand) {
toc.removeClass('expand');
toggle.text('Expand all');
} else {
toc.addClass('expand');
toggle.text('Collapse all');
}
}
2015-07-01 16:10:20 +00:00
//toc
2017-01-05 09:52:32 +00:00
export function generateToc(id) {
const target = $(`#${id}`);
2015-07-01 16:10:20 +00:00
target.html('');
new Toc('doc', {
'level': 3,
'top': -1,
'class': 'toc',
2015-12-20 17:28:54 +00:00
'ulClass': 'nav',
'targetId': id,
'process': getHeaderContent
2015-06-01 10:04:25 +00:00
});
2015-09-25 06:22:45 +00:00
if (target.text() == 'undefined')
2015-07-01 16:10:20 +00:00
target.html('');
2017-01-05 09:52:32 +00:00
const tocMenu = $('<div class="toc-menu"></div');
const toggle = $('<a class="expand-toggle" href="#">Expand all</a>');
const backtotop = $('<a class="back-to-top" href="#">Back to top</a>');
const gotobottom = $('<a class="go-to-bottom" href="#">Go to bottom</a>');
checkExpandToggle();
2017-01-05 09:52:32 +00:00
toggle.click(e => {
e.preventDefault();
e.stopPropagation();
tocExpand = !tocExpand;
checkExpandToggle();
});
2017-01-05 09:52:32 +00:00
backtotop.click(e => {
2015-07-01 16:10:20 +00:00
e.preventDefault();
e.stopPropagation();
if (scrollToTop)
scrollToTop();
removeHash();
});
2017-01-05 09:52:32 +00:00
gotobottom.click(e => {
2015-07-01 16:10:20 +00:00
e.preventDefault();
e.stopPropagation();
if (scrollToBottom)
scrollToBottom();
removeHash();
});
tocMenu.append(toggle).append(backtotop).append(gotobottom);
target.append(tocMenu);
2015-07-01 16:10:20 +00:00
}
//smooth all hash trigger scrolling
2017-01-05 09:52:32 +00:00
export function smoothHashScroll() {
const hashElements = $("a[href^='#']:not([smoothhashscroll])").toArray();
for (const element of hashElements) {
const $element = $(element);
const hash = element.hash;
2015-07-01 16:10:20 +00:00
if (hash) {
$element.on('click', function (e) {
// store hash
2017-01-05 09:52:32 +00:00
const hash = decodeURIComponent(this.hash);
// escape special characters in jquery selector
2017-01-05 09:52:32 +00:00
const $hash = $(hash.replace(/(:|\.|\[|\]|,)/g, "\\$1"));
// return if no element been selected
if ($hash.length <= 0) return;
2015-07-01 16:10:20 +00:00
// prevent default anchor click behavior
e.preventDefault();
// animate
2015-09-25 06:22:45 +00:00
$('body, html').stop(true, true).animate({
scrollTop: $hash.offset().top
2017-01-05 09:52:32 +00:00
}, 100, "linear", () => {
2015-07-01 16:10:20 +00:00
// when done, add hash to url
// (default click behaviour)
window.location.hash = hash;
});
});
$element.attr('smoothhashscroll', '');
}
}
2015-05-04 07:53:29 +00:00
}
function imgPlayiframe(element, src) {
if (!$(element).attr("data-videoid")) return;
2017-01-05 09:52:32 +00:00
const iframe = $("<iframe frameborder='0' webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>");
$(iframe).attr("src", `${src + $(element).attr("data-videoid")}?autoplay=1`);
$(element).find('img').css('visibility', 'hidden');
$(element).append(iframe);
2015-05-04 07:53:29 +00:00
}
2017-01-05 09:52:32 +00:00
const anchorForId = id => {
const anchor = document.createElement("a");
anchor.className = "anchor hidden-xs";
2017-01-05 09:52:32 +00:00
anchor.href = `#${id}`;
anchor.innerHTML = "<span class=\"octicon octicon-link\"></span>";
2015-07-01 16:10:20 +00:00
anchor.title = id;
2015-05-04 07:53:29 +00:00
return anchor;
};
2017-01-05 09:52:32 +00:00
const linkifyAnchors = (level, containingElement) => {
const headers = containingElement.getElementsByTagName(`h${level}`);
for (let i = 0, l = headers.length; i < l; i++) {
let header = headers[i];
if (header.getElementsByClassName("anchor").length == 0) {
2015-07-01 16:10:20 +00:00
if (typeof header.id == "undefined" || header.id == "") {
//to escape characters not allow in css and humanize
2017-01-05 09:52:32 +00:00
const id = slugifyWithUTF8(getHeaderContent(header));
2015-07-01 16:10:20 +00:00
header.id = id;
}
header.insertBefore(anchorForId(header.id), header.firstChild);
2015-05-04 07:53:29 +00:00
}
}
};
2017-01-05 09:52:32 +00:00
export function autoLinkify(view) {
const contentBlock = view[0];
2015-05-04 07:53:29 +00:00
if (!contentBlock) {
return;
}
2017-01-05 09:52:32 +00:00
for (let level = 1; level <= 6; level++) {
2015-05-04 07:53:29 +00:00
linkifyAnchors(level, contentBlock);
}
}
function getHeaderContent(header) {
2017-01-05 09:52:32 +00:00
const headerHTML = $(header).clone();
headerHTML.find('.MathJax_Preview').remove();
headerHTML.find('.MathJax').remove();
return headerHTML[0].innerHTML;
}
2017-01-05 09:52:32 +00:00
export function deduplicatedHeaderId(view) {
const headers = view.find(':header.raw').removeClass('raw').toArray();
for (let i = 0; i < headers.length; i++) {
const id = $(headers[i]).attr('id');
if (!id) continue;
2017-01-05 09:52:32 +00:00
const duplicatedHeaders = view.find(`:header[id="${id}"]`).toArray();
for (let j = 0; j < duplicatedHeaders.length; j++) {
if (duplicatedHeaders[j] != headers[i]) {
2017-01-05 09:52:32 +00:00
const newId = id + j;
const $duplicatedHeader = $(duplicatedHeaders[j]);
$duplicatedHeader.attr('id', newId);
const $headerLink = $duplicatedHeader.find(`> a.anchor[href="#${id}"]`);
2017-01-05 09:52:32 +00:00
$headerLink.attr('href', `#${newId}`);
$headerLink.attr('title', newId);
}
}
}
}
2015-05-04 07:53:29 +00:00
2017-01-05 09:52:32 +00:00
export function renderTOC(view) {
const tocs = view.find('.toc').toArray();
for (let i = 0; i < tocs.length; i++) {
const toc = $(tocs[i]);
const id = `toc${i}`;
2015-12-20 17:28:54 +00:00
toc.attr('id', id);
2017-01-05 09:52:32 +00:00
const target = $(`#${id}`);
2015-12-20 17:28:54 +00:00
target.html('');
new Toc('doc', {
'level': 3,
'top': -1,
'class': 'toc',
'targetId': id,
'process': getHeaderContent
2015-12-20 17:28:54 +00:00
});
if (target.text() == 'undefined')
target.html('');
target.replaceWith(target.html());
}
}
2017-01-05 09:52:32 +00:00
export function scrollToHash() {
const hash = location.hash;
2015-05-04 07:53:29 +00:00
location.hash = "";
location.hash = hash;
}
function highlightRender(code, lang) {
if (!lang || /no(-?)highlight|plain|text/.test(lang))
return;
code = S(code).escapeHTML().s
2015-07-01 16:10:20 +00:00
if (lang == 'sequence') {
2017-01-05 09:52:32 +00:00
return `<div class="sequence-diagram raw">${code}</div>`;
2015-07-01 16:10:20 +00:00
} else if (lang == 'flow') {
2017-01-05 09:52:32 +00:00
return `<div class="flow-chart raw">${code}</div>`;
2016-01-12 13:56:29 +00:00
} else if (lang == 'graphviz') {
2017-01-05 09:52:32 +00:00
return `<div class="graphviz raw">${code}</div>`;
} else if (lang == 'mermaid') {
2017-01-05 09:52:32 +00:00
return `<div class="mermaid raw">${code}</div>`;
2015-05-04 07:53:29 +00:00
}
2017-01-05 09:52:32 +00:00
const result = {
value: code
};
2017-01-05 09:52:32 +00:00
const showlinenumbers = /\=$|\=\d+$|\=\+$/.test(lang);
if (showlinenumbers) {
2017-01-05 09:52:32 +00:00
let startnumber = 1;
const matches = lang.match(/\=(\d+)$/);
if (matches)
startnumber = parseInt(matches[1]);
2017-01-05 09:52:32 +00:00
const lines = result.value.split('\n');
const linenumbers = [];
for (let i = 0; i < lines.length - 1; i++) {
linenumbers[i] = `<span data-linenumber='${startnumber + i}'></span>`;
2015-05-04 07:53:29 +00:00
}
2017-01-05 09:52:32 +00:00
const continuelinenumber = /\=\+$/.test(lang);
const linegutter = `<div class='gutter linenumber${continuelinenumber ? " continue" : ""}'>${linenumbers.join('\n')}</div>`;
result.value = `<div class='wrapper'>${linegutter}<div class='code'>${result.value}</div></div>`;
2015-05-04 07:53:29 +00:00
}
return result.value;
}
2017-01-05 09:52:32 +00:00
import markdownit from 'markdown-it';
import markdownitContainer from 'markdown-it-container';
2017-01-05 09:52:32 +00:00
export let md = markdownit('default', {
2015-05-04 07:53:29 +00:00
html: true,
2015-07-01 16:10:20 +00:00
breaks: true,
langPrefix: "",
2015-05-04 07:53:29 +00:00
linkify: true,
typographer: true,
highlight: highlightRender
});
window.md = md;
md.use(require('markdown-it-abbr'));
md.use(require('markdown-it-footnote'));
md.use(require('markdown-it-deflist'));
md.use(require('markdown-it-mark'));
md.use(require('markdown-it-ins'));
md.use(require('markdown-it-sub'));
md.use(require('markdown-it-sup'));
2016-12-19 19:05:59 +00:00
md.use(require('markdown-it-mathjax')({
beforeMath: '<span class="mathjax raw">',
afterMath: '</span>',
beforeInlineMath: '<span class="mathjax raw">\\(',
afterInlineMath: '\\)</span>',
beforeDisplayMath: '<span class="mathjax raw">\\[',
afterDisplayMath: '\\]</span>'
}));
md.use(require('markdown-it-imsize'));
md.use(require('markdown-it-emoji'), {
shortcuts: {}
});
emojify.setConfig({
blacklist: {
elements: ['script', 'textarea', 'a', 'pre', 'code', 'svg'],
classes: ['no-emojify']
},
2017-01-05 09:52:32 +00:00
img_dir: `${serverurl}/build/emojify.js/dist/images/basic`,
ignore_emoticons: true
});
2017-01-05 09:52:32 +00:00
md.renderer.rules.emoji = (token, idx) => emojify.replace(`:${token[idx].markup}:`);
function renderContainer(tokens, idx, options, env, self) {
tokens[idx].attrJoin('role', 'alert');
tokens[idx].attrJoin('class', 'alert');
2017-01-05 09:52:32 +00:00
tokens[idx].attrJoin('class', `alert-${tokens[idx].info.trim()}`);
return self.renderToken(...arguments);
}
md.use(markdownitContainer, 'success', { render: renderContainer });
md.use(markdownitContainer, 'info', { render: renderContainer });
md.use(markdownitContainer, 'warning', { render: renderContainer });
md.use(markdownitContainer, 'danger', { render: renderContainer });
md.renderer.rules.image = function (tokens, idx, options, env, self) {
tokens[idx].attrJoin('class', 'raw');
2017-01-05 09:52:32 +00:00
return self.renderToken(...arguments);
};
md.renderer.rules.list_item_open = function (tokens, idx, options, env, self) {
tokens[idx].attrJoin('class', 'raw');
2017-01-05 09:52:32 +00:00
return self.renderToken(...arguments);
2015-07-01 16:10:20 +00:00
};
md.renderer.rules.blockquote_open = function (tokens, idx, options, env, self) {
tokens[idx].attrJoin('class', 'raw');
2017-01-05 09:52:32 +00:00
return self.renderToken(...arguments);
2015-07-01 16:10:20 +00:00
};
md.renderer.rules.heading_open = function (tokens, idx, options, env, self) {
tokens[idx].attrJoin('class', 'raw');
2017-01-05 09:52:32 +00:00
return self.renderToken(...arguments);
};
2017-01-05 09:52:32 +00:00
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx];
const info = token.info ? md.utils.unescapeAll(token.info).trim() : '';
let langName = '';
let highlighted;
if (info) {
langName = info.split(/\s+/g)[0];
if (/\!$/.test(info)) token.attrJoin('class', 'wrap');
token.attrJoin('class', options.langPrefix + langName.replace(/\=$|\=\d+$|\=\+$|\!$|\=\!$/, ''));
token.attrJoin('class', 'hljs');
token.attrJoin('class', 'raw');
2015-07-01 16:10:20 +00:00
}
if (options.highlight) {
highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content);
2015-07-01 16:10:20 +00:00
} else {
highlighted = md.utils.escapeHtml(token.content);
2015-07-01 16:10:20 +00:00
}
if (highlighted.indexOf('<pre') === 0) {
2017-01-05 09:52:32 +00:00
return `${highlighted}\n`;
}
2017-01-05 09:52:32 +00:00
return `<pre><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`;
2015-07-01 16:10:20 +00:00
};
/* Defined regex markdown it plugins */
2017-01-05 09:52:32 +00:00
import Plugin from 'markdown-it-regexp';
2015-05-04 07:53:29 +00:00
//youtube
2017-01-05 09:52:32 +00:00
const youtubePlugin = new Plugin(
2015-05-04 07:53:29 +00:00
// regexp to match
/{%youtube\s*([\d\D]*?)\s*%}/,
2017-01-05 09:52:32 +00:00
(match, utils) => {
const videoid = match[1];
2015-05-04 07:53:29 +00:00
if (!videoid) return;
2017-01-05 09:52:32 +00:00
const div = $('<div class="youtube raw"></div>');
div.attr('data-videoid', videoid);
2017-01-05 09:52:32 +00:00
const thumbnail_src = `//img.youtube.com/vi/${videoid}/hqdefault.jpg`;
const image = `<img src="${thumbnail_src}" />`;
div.append(image);
2017-01-05 09:52:32 +00:00
const icon = '<i class="icon fa fa-youtube-play fa-5x"></i>';
2015-05-04 07:53:29 +00:00
div.append(icon);
return div[0].outerHTML;
}
);
//vimeo
2017-01-05 09:52:32 +00:00
const vimeoPlugin = new Plugin(
2015-05-04 07:53:29 +00:00
// regexp to match
/{%vimeo\s*([\d\D]*?)\s*%}/,
2017-01-05 09:52:32 +00:00
(match, utils) => {
const videoid = match[1];
2015-05-04 07:53:29 +00:00
if (!videoid) return;
2017-01-05 09:52:32 +00:00
const div = $('<div class="vimeo raw"></div>');
div.attr('data-videoid', videoid);
2017-01-05 09:52:32 +00:00
const icon = '<i class="icon fa fa-vimeo-square fa-5x"></i>';
2015-05-04 07:53:29 +00:00
div.append(icon);
return div[0].outerHTML;
}
);
//gist
2017-01-05 09:52:32 +00:00
const gistPlugin = new Plugin(
2015-05-04 07:53:29 +00:00
// regexp to match
/{%gist\s*([\d\D]*?)\s*%}/,
2017-01-05 09:52:32 +00:00
(match, utils) => {
const gistid = match[1];
const code = `<code data-gist-id="${gistid}"/>`;
2015-05-04 07:53:29 +00:00
return code;
}
);
2015-12-20 17:28:54 +00:00
//TOC
2017-01-05 09:52:32 +00:00
const tocPlugin = new Plugin(
2015-12-20 17:28:54 +00:00
// regexp to match
/^\[TOC\]$/i,
2015-12-20 17:28:54 +00:00
2017-01-05 09:52:32 +00:00
(match, utils) => '<div class="toc"></div>'
2015-12-20 17:28:54 +00:00
);
//slideshare
2017-01-05 09:52:32 +00:00
const slidesharePlugin = new Plugin(
// regexp to match
/{%slideshare\s*([\d\D]*?)\s*%}/,
2017-01-05 09:52:32 +00:00
(match, utils) => {
const slideshareid = match[1];
const div = $('<div class="slideshare raw"></div>');
div.attr('data-slideshareid', slideshareid);
return div[0].outerHTML;
}
);
//speakerdeck
2017-01-05 09:52:32 +00:00
const speakerdeckPlugin = new Plugin(
// regexp to match
/{%speakerdeck\s*([\d\D]*?)\s*%}/,
2017-01-05 09:52:32 +00:00
(match, utils) => {
const speakerdeckid = match[1];
const div = $('<div class="speakerdeck raw"></div>');
div.attr('data-speakerdeckid', speakerdeckid);
return div[0].outerHTML;
}
);
2016-06-21 13:44:06 +00:00
//pdf
2017-01-05 09:52:32 +00:00
const pdfPlugin = new Plugin(
2016-06-21 13:44:06 +00:00
// regexp to match
/{%pdf\s*([\d\D]*?)\s*%}/,
2017-01-05 09:52:32 +00:00
(match, utils) => {
const pdfurl = match[1];
2016-06-21 13:44:06 +00:00
if (!isValidURL(pdfurl)) return match[0];
2017-01-05 09:52:32 +00:00
const div = $('<div class="pdf raw"></div>');
2016-06-21 13:44:06 +00:00
div.attr('data-pdfurl', pdfurl);
return div[0].outerHTML;
}
);
//yaml meta, from https://github.com/eugeneware/remarkable-meta
function get(state, line) {
2017-01-05 09:52:32 +00:00
const pos = state.bMarks[line];
const max = state.eMarks[line];
return state.src.substr(pos, max - pos);
}
function meta(state, start, end, silent) {
if (start !== 0 || state.blkIndent !== 0) return false;
if (state.tShift[start] < 0) return false;
if (!get(state, start).match(/^---$/)) return false;
2017-01-05 09:52:32 +00:00
const data = [];
for (var line = start + 1; line < end; line++) {
2017-01-05 09:52:32 +00:00
const str = get(state, line);
if (str.match(/^(\.{3}|-{3})$/)) break;
if (state.tShift[line] < 0) break;
data.push(str);
}
if (line >= end) return false;
try {
md.meta = jsyaml.safeLoad(data.join('\n')) || {};
delete md.metaError;
} catch(err) {
md.metaError = err;
console.warn(err);
return false;
}
state.line = line + 1;
return true;
}
function metaPlugin(md) {
md.meta = md.meta || {};
md.block.ruler.before('code', 'meta', meta, {
alt: []
});
}
md.use(metaPlugin);
2015-05-04 07:53:29 +00:00
md.use(youtubePlugin);
md.use(vimeoPlugin);
md.use(gistPlugin);
md.use(tocPlugin);
md.use(slidesharePlugin);
2016-06-21 13:44:06 +00:00
md.use(speakerdeckPlugin);
md.use(pdfPlugin);
2017-01-05 09:52:32 +00:00
export default {
md
};