Extension pattern
This commit is contained in:
parent
a271abc2bc
commit
8879d327dd
BIN
img/icons.png
BIN
img/icons.png
Binary file not shown.
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.5 KiB |
@ -81,7 +81,7 @@ define([ "core", "underscore" ], function(core) {
|
||||
}
|
||||
error = error || new Error("Unknown error");
|
||||
if(error.message) {
|
||||
core.showError(error.message);
|
||||
core.showError(error);
|
||||
}
|
||||
runSafe(task, task.errorCallbacks, error);
|
||||
// Exit the current call stack
|
||||
|
272
js/core.js
272
js/core.js
@ -1,7 +1,7 @@
|
||||
define(
|
||||
[ "jquery", "mathjax-editing", "bootstrap", "jgrowl", "layout", "Markdown.Editor", "storage", "config",
|
||||
[ "jquery", "extension-manager", "bootstrap", "layout", "Markdown.Editor", "storage", "config",
|
||||
"underscore", "FileSaver", "css_browser_selector" ],
|
||||
function($, mathjaxEditing) {
|
||||
function($, extensionManager) {
|
||||
|
||||
var core = {};
|
||||
|
||||
@ -118,6 +118,7 @@ define(
|
||||
return url;
|
||||
};
|
||||
|
||||
// Export data on disk
|
||||
core.saveFile = function(content, filename) {
|
||||
if(saveAs !== undefined) {
|
||||
var blob = new Blob([content], {type: "text/plain;charset=utf-8"});
|
||||
@ -141,26 +142,21 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
// Used to show a notification message
|
||||
core.showMessage = function(msg, iconClass, options) {
|
||||
if(!msg) {
|
||||
return;
|
||||
}
|
||||
var endOfMsg = msg.indexOf("|");
|
||||
if(endOfMsg !== -1) {
|
||||
msg = msg.substring(0, endOfMsg);
|
||||
if(!msg) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
options = options || {};
|
||||
iconClass = iconClass || "icon-info-sign";
|
||||
$.jGrowl("<i class='icon-white " + iconClass + "'></i> " + _.escape(msg), options);
|
||||
// Log a message
|
||||
core.showMessage = function(message) {
|
||||
console.log(message);
|
||||
extensionManager.onMessage(message);
|
||||
};
|
||||
|
||||
// Used to show an error message
|
||||
core.showError = function(msg) {
|
||||
core.showMessage(msg, "icon-warning-sign");
|
||||
// Log an error
|
||||
core.showError = function(error) {
|
||||
console.error(error);
|
||||
if(_.isString(error)) {
|
||||
extensionManager.onMessage(error);
|
||||
}
|
||||
else if(_.isObject(error)) {
|
||||
extensionManager.onMessage(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Offline management
|
||||
@ -174,25 +170,19 @@ define(
|
||||
offlineTime = core.currentTime;
|
||||
if(core.isOffline === false) {
|
||||
core.isOffline = true;
|
||||
core.showMessage("You are offline.", "icon-exclamation-sign msg-offline", {
|
||||
sticky : true,
|
||||
close : function() {
|
||||
core.showMessage("You are back online!", "icon-signal");
|
||||
}
|
||||
extensionManager.onOfflineChanged(true);
|
||||
_.each(offlineListeners, function(listener) {
|
||||
listener();
|
||||
});
|
||||
for(var i=0; i<offlineListeners.length; i++) {
|
||||
offlineListeners[i]();
|
||||
}
|
||||
}
|
||||
};
|
||||
core.setOnline = function() {
|
||||
if(core.isOffline === true) {
|
||||
$(".msg-offline").parents(".jGrowl-notification").trigger(
|
||||
'jGrowl.beforeClose');
|
||||
core.isOffline = false;
|
||||
for(var i=0; i<offlineListeners.length; i++) {
|
||||
offlineListeners[i]();
|
||||
}
|
||||
extensionManager.onOfflineChanged(false);
|
||||
_.each(offlineListeners, function(listener) {
|
||||
listener();
|
||||
});
|
||||
}
|
||||
};
|
||||
function checkOnline() {
|
||||
@ -295,140 +285,6 @@ define(
|
||||
}
|
||||
};
|
||||
|
||||
// Used by Scroll Link feature
|
||||
var mdSectionList = [];
|
||||
var htmlSectionList = [];
|
||||
function pxToFloat(px) {
|
||||
return parseFloat(px.substring(0, px.length-2));
|
||||
}
|
||||
var buildSections = _.debounce(function() {
|
||||
|
||||
// Try to find Markdown sections by looking for titles
|
||||
var editorElt = $("#wmd-input");
|
||||
mdSectionList = [];
|
||||
// This textarea is used to measure sections height
|
||||
var textareaElt = $("#md-section-helper");
|
||||
// It has to be the same width than wmd-input
|
||||
textareaElt.width(editorElt.width());
|
||||
// Consider wmd-input top padding
|
||||
var padding = pxToFloat(editorElt.css('padding-top'));
|
||||
var offset = 0, mdSectionOffset = 0;
|
||||
function addMdSection(sectionText) {
|
||||
var sectionHeight = padding;
|
||||
if(sectionText !== undefined) {
|
||||
textareaElt.val(sectionText);
|
||||
sectionHeight += textareaElt.prop('scrollHeight');
|
||||
}
|
||||
var newSectionOffset = mdSectionOffset + sectionHeight;
|
||||
mdSectionList.push({
|
||||
startOffset: mdSectionOffset,
|
||||
endOffset: newSectionOffset,
|
||||
height: sectionHeight
|
||||
});
|
||||
mdSectionOffset = newSectionOffset;
|
||||
padding = 0;
|
||||
}
|
||||
// Create MD sections by finding title patterns (excluding gfm blocs)
|
||||
var text = editorElt.val() + "\n\n";
|
||||
text.replace(/^```.*\n[\s\S]*?\n```|(^.+[ \t]*\n=+[ \t]*\n+|^.+[ \t]*\n-+[ \t]*\n+|^\#{1,6}[ \t]*.+?[ \t]*\#*\n+)/gm,
|
||||
function(match, title, matchOffset) {
|
||||
if(title) {
|
||||
// We just found a title which means end of the previous section
|
||||
// Exclude last \n of the section
|
||||
var sectionText = undefined;
|
||||
if(matchOffset > offset) {
|
||||
sectionText = text.substring(offset, matchOffset-1);
|
||||
}
|
||||
addMdSection(sectionText);
|
||||
offset = matchOffset;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
);
|
||||
// Last section
|
||||
// Consider wmd-input bottom padding and exclude \n\n previously added
|
||||
padding += pxToFloat(editorElt.css('padding-bottom'));
|
||||
addMdSection(text.substring(offset, text.length-2));
|
||||
|
||||
// Try to find corresponding sections in the preview
|
||||
var previewElt = $("#wmd-preview");
|
||||
htmlSectionList = [];
|
||||
var htmlSectionOffset = 0;
|
||||
var previewScrollTop = previewElt.scrollTop();
|
||||
// Each title element is a section separator
|
||||
previewElt.children("h1,h2,h3,h4,h5,h6").each(function() {
|
||||
// Consider div scroll position and header element top margin
|
||||
var newSectionOffset = $(this).position().top + previewScrollTop + pxToFloat($(this).css('margin-top'));
|
||||
htmlSectionList.push({
|
||||
startOffset: htmlSectionOffset,
|
||||
endOffset: newSectionOffset,
|
||||
height: newSectionOffset - htmlSectionOffset
|
||||
});
|
||||
htmlSectionOffset = newSectionOffset;
|
||||
});
|
||||
// Last section
|
||||
var scrollHeight = previewElt.prop('scrollHeight');
|
||||
htmlSectionList.push({
|
||||
startOffset: htmlSectionOffset,
|
||||
endOffset: scrollHeight,
|
||||
height: scrollHeight - htmlSectionOffset
|
||||
});
|
||||
|
||||
// apply Scroll Link
|
||||
lastEditorScrollTop = -9;
|
||||
skipScrollLink = false;
|
||||
scrollLink();
|
||||
}, 500);
|
||||
|
||||
// -9 is less than -5
|
||||
var lastEditorScrollTop = -9;
|
||||
var lastPreviewScrollTop = -9;
|
||||
var skipScrollLink = false;
|
||||
var scrollLink = _.debounce(function() {
|
||||
if(skipScrollLink === true || mdSectionList.length === 0 || mdSectionList.length !== htmlSectionList.length) {
|
||||
return;
|
||||
}
|
||||
var editorElt = $("#wmd-input");
|
||||
var editorScrollTop = editorElt.scrollTop();
|
||||
var previewElt = $("#wmd-preview");
|
||||
var previewScrollTop = previewElt.scrollTop();
|
||||
function animate(srcScrollTop, srcSectionList, destElt, destSectionList, lastDestScrollTop, callback) {
|
||||
// Find the section corresponding to the offset
|
||||
var sectionIndex = undefined;
|
||||
var srcSection = _.find(srcSectionList, function(section, index) {
|
||||
sectionIndex = index;
|
||||
return srcScrollTop < section.endOffset;
|
||||
});
|
||||
if(srcSection === undefined) {
|
||||
// Something wrong in the algorithm...
|
||||
return -9;
|
||||
}
|
||||
var posInSection = (srcScrollTop - srcSection.startOffset) / srcSection.height;
|
||||
var destSection = destSectionList[sectionIndex];
|
||||
var destScrollTop = destSection.startOffset + destSection.height * posInSection;
|
||||
destScrollTop = _.min([destScrollTop, destElt.prop('scrollHeight') - destElt.outerHeight()]);
|
||||
if(Math.abs(destScrollTop - lastDestScrollTop) < 5) {
|
||||
// Skip the animation in case it's not necessary
|
||||
return;
|
||||
}
|
||||
destElt.animate({scrollTop: destScrollTop}, 600, function() {
|
||||
callback(destScrollTop);
|
||||
});
|
||||
}
|
||||
if(Math.abs(editorScrollTop - lastEditorScrollTop) > 5) {
|
||||
lastEditorScrollTop = editorScrollTop;
|
||||
animate(editorScrollTop, mdSectionList, previewElt, htmlSectionList, lastPreviewScrollTop, function(destScrollTop) {
|
||||
lastPreviewScrollTop = destScrollTop;
|
||||
});
|
||||
}
|
||||
else if(Math.abs(previewScrollTop - lastPreviewScrollTop) > 5) {
|
||||
lastPreviewScrollTop = previewScrollTop;
|
||||
animate(previewScrollTop, htmlSectionList, editorElt, mdSectionList, lastEditorScrollTop, function(destScrollTop) {
|
||||
lastEditorScrollTop = destScrollTop;
|
||||
});
|
||||
}
|
||||
}, 600);
|
||||
|
||||
// Create the layout
|
||||
var layout = undefined;
|
||||
core.createLayout = function() {
|
||||
@ -449,9 +305,7 @@ define(
|
||||
center__minWidth : 200,
|
||||
center__minHeight : 200
|
||||
};
|
||||
if(core.settings.scrollLink === true) {
|
||||
layoutGlobalConfig.onresize = buildSections;
|
||||
}
|
||||
extensionManager.onLayoutConfigure(layoutGlobalConfig);
|
||||
if (core.settings.layoutOrientation == "horizontal") {
|
||||
$(".ui-layout-south").remove();
|
||||
$(".ui-layout-east").addClass("well").prop("id", "wmd-preview");
|
||||
@ -483,10 +337,7 @@ define(
|
||||
layout.allowOverflow('north');
|
||||
});
|
||||
|
||||
// ScrollLink
|
||||
if(core.settings.scrollLink === true) {
|
||||
$("#wmd-input, #wmd-preview").scroll(scrollLink);
|
||||
}
|
||||
extensionManager.onLayoutCreated();
|
||||
};
|
||||
core.layoutRefresh = function() {
|
||||
if(layout !== undefined) {
|
||||
@ -498,57 +349,8 @@ define(
|
||||
// Create the PageDown editor
|
||||
var insertLinkCallback = undefined;
|
||||
core.createEditor = function(onTextChange) {
|
||||
var firstChange = true;
|
||||
skipScrollLink = true;
|
||||
lastPreviewScrollTop = -9;
|
||||
$("#wmd-input, #wmd-preview").scrollTop(0);
|
||||
$("#wmd-button-bar").empty();
|
||||
var converter = new Markdown.Converter();
|
||||
if(core.settings.converterType.indexOf("markdown-extra") === 0) {
|
||||
// Markdown extra customized converter
|
||||
var options = {};
|
||||
if(core.settings.converterType == "markdown-extra-prettify") {
|
||||
options.highlighter = "prettify";
|
||||
}
|
||||
Markdown.Extra.init(converter, options);
|
||||
}
|
||||
// Convert email addresses (not managed by pagedown)
|
||||
converter.hooks.chain("postConversion", function(text) {
|
||||
return text.replace(/<(mailto\:)?([^\s>]+@[^\s>]+\.\S+?)>/g, function(match, mailto, email) {
|
||||
return '<a href="mailto:' + email + '">' + email + '</a>';
|
||||
});
|
||||
});
|
||||
var editor = new Markdown.Editor(converter);
|
||||
// Prettify
|
||||
if(core.settings.converterType == "markdown-extra-prettify") {
|
||||
editor.hooks.chain("onPreviewRefresh", prettyPrint);
|
||||
}
|
||||
var previewRefreshFinished = function() {
|
||||
if(viewerMode === false && core.settings.scrollLink === true) {
|
||||
function updateScrollLinkSections() {
|
||||
// Modify scroll position of the preview not the editor
|
||||
lastEditorScrollTop = -9;
|
||||
buildSections();
|
||||
// Preview may change if images are loading
|
||||
$("#wmd-preview img").load(function() {
|
||||
lastEditorScrollTop = -9;
|
||||
buildSections();
|
||||
});
|
||||
}
|
||||
// MathJax may have change the scroll position. Restore it
|
||||
$("#wmd-preview").scrollTop(lastPreviewScrollTop);
|
||||
_.defer(updateScrollLinkSections);
|
||||
}
|
||||
};
|
||||
// MathJax
|
||||
if(core.settings.enableMathJax === true) {
|
||||
mathjaxEditing.prepareWmdForMathJax(editor, [["$", "$"], ["\\\\(", "\\\\)"]], function() {
|
||||
skipScrollLink = true;
|
||||
}, previewRefreshFinished);
|
||||
}
|
||||
else {
|
||||
editor.hooks.chain("onPreviewRefresh", previewRefreshFinished);
|
||||
}
|
||||
// Custom insert link dialog
|
||||
editor.hooks.set("insertLinkDialog", function (callback) {
|
||||
insertLinkCallback = callback;
|
||||
@ -570,6 +372,7 @@ define(
|
||||
return true;
|
||||
});
|
||||
|
||||
var firstChange = true;
|
||||
var previewWrapper = function(makePreview) {
|
||||
return function() {
|
||||
if(firstChange !== true) {
|
||||
@ -592,6 +395,18 @@ define(
|
||||
};
|
||||
};
|
||||
}
|
||||
extensionManager.onEditorConfigure(editor);
|
||||
editor.hooks.chain("onPreviewRefresh", extensionManager.onAsyncPreview);
|
||||
|
||||
// Convert email addresses (not managed by pagedown)
|
||||
converter.hooks.chain("postConversion", function(text) {
|
||||
return text.replace(/<(mailto\:)?([^\s>]+@[^\s>]+\.\S+?)>/g, function(match, mailto, email) {
|
||||
return '<a href="mailto:' + email + '">' + email + '</a>';
|
||||
});
|
||||
});
|
||||
|
||||
$("#wmd-input, #wmd-preview").scrollTop(0);
|
||||
$("#wmd-button-bar").empty();
|
||||
editor.run(previewWrapper);
|
||||
firstChange = false;
|
||||
|
||||
@ -793,13 +608,12 @@ define(
|
||||
documentLoaded = true;
|
||||
runReadyCallbacks();
|
||||
});
|
||||
|
||||
|
||||
core.onReady(function() {
|
||||
extensionManager.init(core.settings.extensionConfig);
|
||||
});
|
||||
core.onReady(extensionManager.onReady);
|
||||
core.onReady(function() {
|
||||
// jGrowl configuration
|
||||
$.jGrowl.defaults.life = 5000;
|
||||
$.jGrowl.defaults.closer = false;
|
||||
$.jGrowl.defaults.closeTemplate = '';
|
||||
$.jGrowl.defaults.position = 'bottom-right';
|
||||
|
||||
// Load theme list
|
||||
_.each(THEME_LIST, function(name, value) {
|
||||
|
77
js/extension-manager.js
Normal file
77
js/extension-manager.js
Normal file
@ -0,0 +1,77 @@
|
||||
define( [
|
||||
"underscore",
|
||||
"extensions/notifications",
|
||||
"extensions/markdown-extra",
|
||||
"extensions/math-jax",
|
||||
"extensions/scroll-link"
|
||||
], function() {
|
||||
|
||||
var extensionManager = {};
|
||||
|
||||
// Create a map with providerId: providerObject
|
||||
var extensionList = _.chain(arguments)
|
||||
.map(function(argument) {
|
||||
return argument && argument.extensionId && argument;
|
||||
}).compact().value();
|
||||
|
||||
// Return every named callbacks implemented in extensions
|
||||
function getExtensionCallbackList(callbackName) {
|
||||
return _.chain(extensionList)
|
||||
.map(function(extension) {
|
||||
return extension[callbackName];
|
||||
}).compact().value();
|
||||
}
|
||||
|
||||
// Return a function that calls every callbacks from extensions
|
||||
function createCallback(callbackName) {
|
||||
var callbackList = getExtensionCallbackList(callbackName);
|
||||
return function() {
|
||||
var callbackArguments = arguments;
|
||||
_.each(callbackList, function(callback) {
|
||||
callback.apply(null, callbackArguments);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Add a callback to the extensionManager
|
||||
function addCallback(callbackName) {
|
||||
extensionManager[callbackName] = createCallback(callbackName);
|
||||
}
|
||||
|
||||
extensionManager.init = function(extensionConfigMap) {
|
||||
// Set the extension configuration
|
||||
extensionConfigMap = extensionConfigMap || {};
|
||||
_.each(extensionList, function(extension) {
|
||||
extension.config = _.extend({}, extension.defaultConfig, extensionConfigMap[extension.extensionId]);
|
||||
});
|
||||
};
|
||||
|
||||
addCallback("onReady");
|
||||
addCallback("onMessage");
|
||||
addCallback("onError");
|
||||
addCallback("onOfflineChanged");
|
||||
addCallback("onLayoutConfigure");
|
||||
addCallback("onLayoutCreated");
|
||||
addCallback("onEditorConfigure");
|
||||
|
||||
var onPreviewFinished = createCallback("onPreviewFinished");
|
||||
var onAsyncPreviewCallbackList = getExtensionCallbackList("onAsyncPreview");
|
||||
extensionManager["onAsyncPreview"] = function() {
|
||||
// Call onPreviewFinished callbacks when all async preview are finished
|
||||
var counter = 0;
|
||||
function tryFinished() {
|
||||
if(counter === onAsyncPreviewCallbackList.length) {
|
||||
onPreviewFinished();
|
||||
}
|
||||
}
|
||||
_.each(onAsyncPreviewCallbackList, function(asyncPreviewCallback) {
|
||||
asyncPreviewCallback(function() {
|
||||
counter++;
|
||||
tryFinished();
|
||||
});
|
||||
});
|
||||
tryFinished();
|
||||
};
|
||||
|
||||
return extensionManager;
|
||||
});
|
22
js/extensions/markdown-extra.js
Normal file
22
js/extensions/markdown-extra.js
Normal file
@ -0,0 +1,22 @@
|
||||
define( [ "Markdown.Extra" ], function() {
|
||||
|
||||
var markdownExtra = {
|
||||
extensionId: "markdownExtra",
|
||||
extensionName: "Markdown Extra",
|
||||
defaultConfig: {
|
||||
prettify: true
|
||||
}
|
||||
};
|
||||
|
||||
markdownExtra.onEditorConfigure = function(editor) {
|
||||
var converter = editor.getConverter();
|
||||
var options = {};
|
||||
if(markdownExtra.config.prettify === true) {
|
||||
options.highlighter = "prettify";
|
||||
editor.hooks.chain("onPreviewRefresh", prettyPrint);
|
||||
}
|
||||
Markdown.Extra.init(converter, options);
|
||||
};
|
||||
|
||||
return markdownExtra;
|
||||
});
|
259
js/extensions/math-jax.js
Normal file
259
js/extensions/math-jax.js
Normal file
@ -0,0 +1,259 @@
|
||||
define( [ "MathJax" ], function($) {
|
||||
|
||||
var mathJax = {
|
||||
extensionId: "mathJax",
|
||||
extensionName: "MathJax"
|
||||
};
|
||||
|
||||
MathJax.Hub.Config({"HTML-CSS": {preferredFont: "TeX",availableFonts: ["STIX", "TeX"],linebreaks: {automatic: true},EqnChunk: (MathJax.Hub.Browser.isMobile ? 10 : 50), imageFont: null},
|
||||
tex2jax: {inlineMath: [["$", "$"], ["\\\\(", "\\\\)"]],displayMath: [["$$", "$$"], ["\\[", "\\]"]],processEscapes: true,ignoreClass: "tex2jax_ignore|dno"},
|
||||
TeX: {noUndefined: {attributes: {mathcolor: "red",mathbackground: "#FFEEEE",mathsize: "90%"}},
|
||||
Safe: {allow: {URLs: "safe",classes: "safe",cssIDs: "safe",styles: "safe",fontsize: "all"}}},
|
||||
messageStyle: "none"
|
||||
});
|
||||
|
||||
var ready = false; // true after initial typeset is complete
|
||||
var pending = false; // true when MathJax has been requested
|
||||
var preview = null; // the preview container
|
||||
var inline = "$"; // the inline math delimiter
|
||||
|
||||
var blocks, start, end, last, braces; // used in searching for math
|
||||
var math; // stores math until markdone is done
|
||||
|
||||
var HUB = MathJax.Hub;
|
||||
|
||||
//
|
||||
// Runs after initial typeset
|
||||
//
|
||||
HUB.Queue(function() {
|
||||
ready = true;
|
||||
HUB.processUpdateTime = 50; // reduce update time so that we can cancel
|
||||
// easier
|
||||
HUB.Config({ "HTML-CSS" : { EqnChunk : 10, EqnChunkFactor : 1 }, // reduce
|
||||
// chunk
|
||||
// for
|
||||
// more
|
||||
// frequent
|
||||
// updates
|
||||
SVG : { EqnChunk : 10, EqnChunkFactor : 1 } });
|
||||
});
|
||||
|
||||
//
|
||||
// The pattern for math delimiters and special symbols
|
||||
// needed for searching for math in the page.
|
||||
//
|
||||
var SPLIT = /(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[\\{}$]|[{}]|(?:\n\s*)+|@@\d+@@)/i;
|
||||
|
||||
//
|
||||
// The math is in blocks i through j, so
|
||||
// collect it into one block and clear the others.
|
||||
// Replace &, <, and > by named entities.
|
||||
// For IE, put <br> at the ends of comments since IE removes \n.
|
||||
// Clear the current math positions and store the index of the
|
||||
// math, then push the math string onto the storage array.
|
||||
//
|
||||
function processMath(i, j) {
|
||||
var block = blocks.slice(i, j + 1).join("").replace(/&/g, "&") // use
|
||||
// HTML
|
||||
// entity
|
||||
// for
|
||||
// &
|
||||
.replace(/</g, "<") // use HTML entity for <
|
||||
.replace(/>/g, ">") // use HTML entity for >
|
||||
;
|
||||
if (HUB.Browser.isMSIE) {
|
||||
block = block.replace(/(%[^\n]*)\n/g, "$1<br/>\n")
|
||||
}
|
||||
while (j > i) {
|
||||
blocks[j] = "";
|
||||
j--
|
||||
}
|
||||
blocks[i] = "@@" + math.length + "@@";
|
||||
math.push(block);
|
||||
start = end = last = null;
|
||||
}
|
||||
|
||||
//
|
||||
// Break up the text into its component parts and search
|
||||
// through them for math delimiters, braces, linebreaks, etc.
|
||||
// Math delimiters must match and braces must balance.
|
||||
// Don't allow math to pass through a double linebreak
|
||||
// (which will be a paragraph).
|
||||
//
|
||||
function removeMath(text) {
|
||||
start = end = last = null; // for tracking math delimiters
|
||||
math = []; // stores math strings for latter
|
||||
|
||||
blocks = text.replace(/\r\n?/g, "\n").split(SPLIT);
|
||||
for ( var i = 1, m = blocks.length; i < m; i += 2) {
|
||||
var block = blocks[i];
|
||||
if (block.charAt(0) === "@") {
|
||||
//
|
||||
// Things that look like our math markers will get
|
||||
// stored and then retrieved along with the math.
|
||||
//
|
||||
blocks[i] = "@@" + math.length + "@@";
|
||||
math.push(block);
|
||||
} else if (start) {
|
||||
//
|
||||
// If we are in math, look for the end delimiter,
|
||||
// but don't go past double line breaks, and
|
||||
// and balance braces within the math.
|
||||
//
|
||||
if (block === end) {
|
||||
if (braces) {
|
||||
last = i
|
||||
} else {
|
||||
processMath(start, i)
|
||||
}
|
||||
} else if (block.match(/\n.*\n/)) {
|
||||
if (last) {
|
||||
i = last;
|
||||
processMath(start, i)
|
||||
}
|
||||
start = end = last = null;
|
||||
braces = 0;
|
||||
} else if (block === "{") {
|
||||
braces++
|
||||
} else if (block === "}" && braces) {
|
||||
braces--
|
||||
}
|
||||
} else {
|
||||
//
|
||||
// Look for math start delimiters and when
|
||||
// found, set up the end delimiter.
|
||||
//
|
||||
if (block === inline || block === "$$") {
|
||||
start = i;
|
||||
end = block;
|
||||
braces = 0;
|
||||
} else if (block.substr(1, 5) === "begin") {
|
||||
start = i;
|
||||
end = "\\end" + block.substr(6);
|
||||
braces = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (last) {
|
||||
processMath(start, last)
|
||||
}
|
||||
return blocks.join("");
|
||||
}
|
||||
|
||||
//
|
||||
// Put back the math strings that were saved,
|
||||
// and clear the math array (no need to keep it around).
|
||||
//
|
||||
function replaceMath(text) {
|
||||
text = text.replace(/@@(\d+)@@/g, function(match, n) {
|
||||
return math[n]
|
||||
});
|
||||
math = null;
|
||||
return text;
|
||||
}
|
||||
|
||||
//
|
||||
// This is run to restart MathJax after it has finished
|
||||
// the previous run (that may have been canceled)
|
||||
//
|
||||
var afterRefreshCallback = undefined;
|
||||
function RestartMJ() {
|
||||
pending = false;
|
||||
HUB.cancelTypeset = false; // won't need to do this in the future
|
||||
HUB.Queue([ "Typeset", HUB, preview ]);
|
||||
HUB.Queue(afterRefreshCallback);
|
||||
}
|
||||
|
||||
//
|
||||
// When the preview changes, cancel MathJax and restart,
|
||||
// if we haven't done that already.
|
||||
//
|
||||
function UpdateMJ() {
|
||||
if (!pending && ready) {
|
||||
pending = true;
|
||||
HUB.Cancel();
|
||||
HUB.Queue(RestartMJ);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Save the preview ID and the inline math delimiter.
|
||||
// Create a converter for the editor and register a preConversion hook
|
||||
// to handle escaping the math.
|
||||
// Create a preview refresh hook to handle starting MathJax.
|
||||
//
|
||||
mathJax.onEditorConfigure = function(editorObject) {
|
||||
preview = document.getElementById("wmd-preview");
|
||||
|
||||
var converterObject = editorObject.getConverter();
|
||||
converterObject.hooks.chain("preConversion", removeMath);
|
||||
converterObject.hooks.chain("postConversion", replaceMath);
|
||||
editorObject.hooks.chain("onPreviewRefresh", UpdateMJ);
|
||||
};
|
||||
mathJax.onAsyncPreview = function(callback) {
|
||||
afterRefreshCallback = callback;
|
||||
};
|
||||
|
||||
//
|
||||
// Set up MathJax to allow canceling of typesetting, if it
|
||||
// doesn't already have that.
|
||||
//
|
||||
if (!HUB.Cancel) {
|
||||
|
||||
HUB.cancelTypeset = false;
|
||||
var CANCELMESSAGE = "MathJax Canceled";
|
||||
|
||||
HUB.Register
|
||||
.StartupHook(
|
||||
"HTML-CSS Jax Config",
|
||||
function() {
|
||||
var HTMLCSS = MathJax.OutputJax["HTML-CSS"], TRANSLATE = HTMLCSS.Translate;
|
||||
HTMLCSS.Augment({ Translate : function(script, state) {
|
||||
if (HUB.cancelTypeset || state.cancelled) {
|
||||
throw Error(CANCELMESSAGE)
|
||||
}
|
||||
return TRANSLATE.call(HTMLCSS, script, state);
|
||||
} });
|
||||
});
|
||||
|
||||
HUB.Register.StartupHook("SVG Jax Config", function() {
|
||||
var SVG = MathJax.OutputJax["SVG"], TRANSLATE = SVG.Translate;
|
||||
SVG.Augment({ Translate : function(script, state) {
|
||||
if (HUB.cancelTypeset || state.cancelled) {
|
||||
throw Error(CANCELMESSAGE)
|
||||
}
|
||||
return TRANSLATE.call(SVG, script, state);
|
||||
} });
|
||||
});
|
||||
|
||||
HUB.Register.StartupHook("TeX Jax Config", function() {
|
||||
var TEX = MathJax.InputJax.TeX, TRANSLATE = TEX.Translate;
|
||||
TEX.Augment({ Translate : function(script, state) {
|
||||
if (HUB.cancelTypeset || state.cancelled) {
|
||||
throw Error(CANCELMESSAGE)
|
||||
}
|
||||
return TRANSLATE.call(TEX, script, state);
|
||||
} });
|
||||
});
|
||||
|
||||
var PROCESSERROR = HUB.processError;
|
||||
HUB.processError = function(error, state, type) {
|
||||
if (error.message !== CANCELMESSAGE) {
|
||||
return PROCESSERROR.call(HUB, error, state, type)
|
||||
}
|
||||
MathJax.Message.Clear(0, 0);
|
||||
state.jaxIDs = [];
|
||||
state.jax = {};
|
||||
state.scripts = [];
|
||||
state.i = state.j = 0;
|
||||
state.cancelled = true;
|
||||
return null;
|
||||
};
|
||||
|
||||
HUB.Cancel = function() {
|
||||
this.cancelTypeset = true
|
||||
};
|
||||
}
|
||||
|
||||
return mathJax;
|
||||
});
|
58
js/extensions/notifications.js
Normal file
58
js/extensions/notifications.js
Normal file
@ -0,0 +1,58 @@
|
||||
define( [ "jquery", "jgrowl", "underscore" ], function($) {
|
||||
|
||||
var notifications = {
|
||||
extensionId: "notifications",
|
||||
extensionName: "Notifications",
|
||||
defaultConfig: {
|
||||
showingTime: 5000
|
||||
}
|
||||
};
|
||||
|
||||
notifications.onReady = function() {
|
||||
// jGrowl configuration
|
||||
$.jGrowl.defaults.life = notifications.config.showingTime;
|
||||
$.jGrowl.defaults.closer = false;
|
||||
$.jGrowl.defaults.closeTemplate = '';
|
||||
$.jGrowl.defaults.position = 'bottom-right';
|
||||
};
|
||||
|
||||
function showMessage(msg, iconClass, options) {
|
||||
if(!msg) {
|
||||
return;
|
||||
}
|
||||
var endOfMsg = msg.indexOf("|");
|
||||
if(endOfMsg !== -1) {
|
||||
msg = msg.substring(0, endOfMsg);
|
||||
if(!msg) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
options = options || {};
|
||||
iconClass = iconClass || "icon-info-sign";
|
||||
$.jGrowl("<i class='icon-white " + iconClass + "'></i> " + _.escape(msg), options);
|
||||
}
|
||||
|
||||
notifications.onMessage = function(message) {
|
||||
showMessage(message);
|
||||
};
|
||||
|
||||
notifications.onError = function(error) {
|
||||
showMessage(error, "icon-warning-sign");
|
||||
};
|
||||
|
||||
notifications.onOfflineChanged = function(isOffline) {
|
||||
if(isOffline === true) {
|
||||
showMessage("You are offline.", "icon-exclamation-sign msg-offline", {
|
||||
sticky : true,
|
||||
close : function() {
|
||||
showMessage("You are back online!", "icon-signal");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$(".msg-offline").parents(".jGrowl-notification").trigger(
|
||||
'jGrowl.beforeClose');
|
||||
}
|
||||
};
|
||||
|
||||
return notifications;
|
||||
});
|
187
js/extensions/scroll-link.js
Normal file
187
js/extensions/scroll-link.js
Normal file
@ -0,0 +1,187 @@
|
||||
define( [ "jquery", "underscore" ], function($) {
|
||||
|
||||
var scrollLink = {
|
||||
extensionId: "scrollLink",
|
||||
extensionName: "Scroll Link"
|
||||
};
|
||||
|
||||
var mdSectionList = [];
|
||||
var htmlSectionList = [];
|
||||
function pxToFloat(px) {
|
||||
return parseFloat(px.substring(0, px.length-2));
|
||||
}
|
||||
var buildSections = _.debounce(function() {
|
||||
|
||||
// Try to find Markdown sections by looking for titles
|
||||
var editorElt = $("#wmd-input");
|
||||
mdSectionList = [];
|
||||
// This textarea is used to measure sections height
|
||||
var textareaElt = $("#md-section-helper");
|
||||
// It has to be the same width than wmd-input
|
||||
textareaElt.width(editorElt.width());
|
||||
// Consider wmd-input top padding
|
||||
var padding = pxToFloat(editorElt.css('padding-top'));
|
||||
var offset = 0, mdSectionOffset = 0;
|
||||
function addMdSection(sectionText) {
|
||||
var sectionHeight = padding;
|
||||
if(sectionText !== undefined) {
|
||||
textareaElt.val(sectionText);
|
||||
sectionHeight += textareaElt.prop('scrollHeight');
|
||||
}
|
||||
var newSectionOffset = mdSectionOffset + sectionHeight;
|
||||
mdSectionList.push({
|
||||
startOffset: mdSectionOffset,
|
||||
endOffset: newSectionOffset,
|
||||
height: sectionHeight
|
||||
});
|
||||
mdSectionOffset = newSectionOffset;
|
||||
padding = 0;
|
||||
}
|
||||
// Create MD sections by finding title patterns (excluding gfm blocs)
|
||||
var text = editorElt.val() + "\n\n";
|
||||
text.replace(/^```.*\n[\s\S]*?\n```|(^.+[ \t]*\n=+[ \t]*\n+|^.+[ \t]*\n-+[ \t]*\n+|^\#{1,6}[ \t]*.+?[ \t]*\#*\n+)/gm,
|
||||
function(match, title, matchOffset) {
|
||||
if(title) {
|
||||
// We just found a title which means end of the previous section
|
||||
// Exclude last \n of the section
|
||||
var sectionText = undefined;
|
||||
if(matchOffset > offset) {
|
||||
sectionText = text.substring(offset, matchOffset-1);
|
||||
}
|
||||
addMdSection(sectionText);
|
||||
offset = matchOffset;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
);
|
||||
// Last section
|
||||
// Consider wmd-input bottom padding and exclude \n\n previously added
|
||||
padding += pxToFloat(editorElt.css('padding-bottom'));
|
||||
addMdSection(text.substring(offset, text.length-2));
|
||||
|
||||
// Try to find corresponding sections in the preview
|
||||
var previewElt = $("#wmd-preview");
|
||||
htmlSectionList = [];
|
||||
var htmlSectionOffset = 0;
|
||||
var previewScrollTop = previewElt.scrollTop();
|
||||
// Each title element is a section separator
|
||||
previewElt.children("h1,h2,h3,h4,h5,h6").each(function() {
|
||||
// Consider div scroll position and header element top margin
|
||||
var newSectionOffset = $(this).position().top + previewScrollTop + pxToFloat($(this).css('margin-top'));
|
||||
htmlSectionList.push({
|
||||
startOffset: htmlSectionOffset,
|
||||
endOffset: newSectionOffset,
|
||||
height: newSectionOffset - htmlSectionOffset
|
||||
});
|
||||
htmlSectionOffset = newSectionOffset;
|
||||
});
|
||||
// Last section
|
||||
var scrollHeight = previewElt.prop('scrollHeight');
|
||||
htmlSectionList.push({
|
||||
startOffset: htmlSectionOffset,
|
||||
endOffset: scrollHeight,
|
||||
height: scrollHeight - htmlSectionOffset
|
||||
});
|
||||
|
||||
// apply Scroll Link
|
||||
lastEditorScrollTop = -9;
|
||||
skipScrollLink = false;
|
||||
isScrollPreview = false;
|
||||
runScrollLink();
|
||||
}, 500);
|
||||
|
||||
// -9 is less than -5
|
||||
var lastEditorScrollTop = -9;
|
||||
var lastPreviewScrollTop = -9;
|
||||
var skipScrollLink = false;
|
||||
var isScrollPreview = false;
|
||||
var runScrollLink = _.debounce(function() {
|
||||
if(skipScrollLink === true || mdSectionList.length === 0 || mdSectionList.length !== htmlSectionList.length) {
|
||||
return;
|
||||
}
|
||||
var editorElt = $("#wmd-input");
|
||||
var editorScrollTop = editorElt.scrollTop();
|
||||
var previewElt = $("#wmd-preview");
|
||||
var previewScrollTop = previewElt.scrollTop();
|
||||
function animate(srcScrollTop, srcSectionList, destElt, destSectionList, lastDestScrollTop, callback) {
|
||||
// Find the section corresponding to the offset
|
||||
var sectionIndex = undefined;
|
||||
var srcSection = _.find(srcSectionList, function(section, index) {
|
||||
sectionIndex = index;
|
||||
return srcScrollTop < section.endOffset;
|
||||
});
|
||||
if(srcSection === undefined) {
|
||||
// Something wrong in the algorithm...
|
||||
return -9;
|
||||
}
|
||||
var posInSection = (srcScrollTop - srcSection.startOffset) / srcSection.height;
|
||||
var destSection = destSectionList[sectionIndex];
|
||||
var destScrollTop = destSection.startOffset + destSection.height * posInSection;
|
||||
destScrollTop = _.min([destScrollTop, destElt.prop('scrollHeight') - destElt.outerHeight()]);
|
||||
if(Math.abs(destScrollTop - lastDestScrollTop) < 5) {
|
||||
// Skip the animation in case it's not necessary
|
||||
return;
|
||||
}
|
||||
destElt.animate({scrollTop: destScrollTop}, 600, function() {
|
||||
callback(destScrollTop);
|
||||
});
|
||||
}
|
||||
// Perform the animation if diff > 5px
|
||||
if(isScrollPreview === false && Math.abs(editorScrollTop - lastEditorScrollTop) > 5) {
|
||||
// Animate the preview
|
||||
lastEditorScrollTop = editorScrollTop;
|
||||
animate(editorScrollTop, mdSectionList, previewElt, htmlSectionList, lastPreviewScrollTop, function(destScrollTop) {
|
||||
lastPreviewScrollTop = destScrollTop;
|
||||
});
|
||||
}
|
||||
else if(Math.abs(previewScrollTop - lastPreviewScrollTop) > 5) {
|
||||
// Animate the editor
|
||||
lastPreviewScrollTop = previewScrollTop;
|
||||
animate(previewScrollTop, htmlSectionList, editorElt, mdSectionList, lastEditorScrollTop, function(destScrollTop) {
|
||||
lastEditorScrollTop = destScrollTop;
|
||||
});
|
||||
}
|
||||
}, 600);
|
||||
|
||||
scrollLink.onLayoutConfigure = function(layoutConfig) {
|
||||
layoutConfig.onresize = buildSections;
|
||||
};
|
||||
|
||||
scrollLink.onLayoutCreated = function() {
|
||||
$("#wmd-preview").scroll(function() {
|
||||
isScrollPreview = true;
|
||||
runScrollLink();
|
||||
});
|
||||
$("#wmd-input").scroll(function() {
|
||||
isScrollPreview = false;
|
||||
runScrollLink();
|
||||
});
|
||||
};
|
||||
|
||||
scrollLink.onEditorConfigure = function(editor) {
|
||||
skipScrollLink = true;
|
||||
lastPreviewScrollTop = 0;
|
||||
editor.hooks.chain("onPreviewRefresh", function() {
|
||||
skipScrollLink = true;
|
||||
});
|
||||
};
|
||||
|
||||
scrollLink.onPreviewFinished = function() {
|
||||
// MathJax may have change the scrolling position. Restore it.
|
||||
if(lastPreviewScrollTop >= 0) {
|
||||
$("#wmd-preview").scrollTop(lastPreviewScrollTop);
|
||||
}
|
||||
_.defer(function() {
|
||||
// Modify scroll position of the preview not the editor
|
||||
lastEditorScrollTop = -9;
|
||||
buildSections();
|
||||
// Preview may change if images are loading
|
||||
$("#wmd-preview img").load(function() {
|
||||
lastEditorScrollTop = -9;
|
||||
buildSections();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return scrollLink;
|
||||
});
|
@ -10,7 +10,7 @@ requirejs.config({
|
||||
'jgrowl': ['jquery'],
|
||||
'layout': ['jquery-ui'],
|
||||
'Markdown.Extra': ['Markdown.Converter', 'prettify'],
|
||||
'Markdown.Editor': ['Markdown.Extra']
|
||||
'Markdown.Editor': ['Markdown.Converter']
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -162,7 +162,6 @@ define(["jquery", "core", "dropbox-provider", "gdrive-provider", "underscore"],
|
||||
|
||||
function isError(error) {
|
||||
if(error !== undefined) {
|
||||
console.error(error);
|
||||
syncRunning = false;
|
||||
synchronizer.updateSyncButton();
|
||||
return true;
|
||||
|
Loading…
Reference in New Issue
Block a user