Import files from disk

This commit is contained in:
benweet 2013-06-16 11:47:45 +01:00
parent d334dfe2f8
commit e5cc6e907b
4 changed files with 636 additions and 0 deletions

192
js/classes/AsyncTask.js Normal file
View File

@ -0,0 +1,192 @@
define([
"underscore",
"core",
"utils",
"extensionMgr",
"config",
"libs/stacktrace",
], function(_, core, utils, extensionMgr) {
var taskQueue = [];
function AsyncTask() {
this.finished = false;
this.timeout = ASYNC_TASK_DEFAULT_TIMEOUT;
this.retryCounter = 0;
this.callPath = [];
this.runCallbacks = [];
this.successCallbacks = [];
this.errorCallbacks = [];
}
/**
* onRun callbacks are called by chain(). These callbacks have to call
* chain() themselves to chain with next onRun callback or error() to
* throw an exception or retry() to restart the task.
*/
AsyncTask.prototype.onRun = function(callback) {
this.runCallbacks.push(callback);
};
/**
* onSuccess callbacks are called when every onRun callbacks have
* succeed.
*/
AsyncTask.prototype.onSuccess = function(callback) {
this.successCallbacks.push(callback);
};
/**
* onError callbacks are called when error() is called in a onRun
* callback.
*/
AsyncTask.prototype.onError = function(callback) {
this.errorCallbacks.push(callback);
};
/**
* chain() calls the next onRun callback or the onSuccess callbacks when
* finished. The optional callback parameter can be used to pass an
* onRun callback during execution, bypassing the onRun queue.
*/
AsyncTask.prototype.chain = function(callback) {
this.callPath.unshift(printStackTrace()[5]);
if(this.finished === true) {
return;
}
// If first execution
if(this.queue === undefined) {
// Create a copy of the onRun callbacks
this.queue = this.runCallbacks.slice();
}
// If a callback is passed as a parameter
if(callback !== undefined) {
callback();
return;
}
// If all callbacks have been run
if(this.queue.length === 0) {
// Run the onSuccess callbacks
runSafe(this, this.successCallbacks);
return;
}
// Run the next callback
var runCallback = this.queue.shift();
runCallback();
};
/**
* error() calls the onError callbacks passing the error parameter and
* ends the task by throwing an exception.
*/
AsyncTask.prototype.error = function(error) {
this.callPath.unshift(printStackTrace()[5]);
if(this.finished === true) {
return;
}
error = error || new Error("Unknown error|\n" + this.callPath.join("\n"));
if(error.message) {
extensionMgr.onError(error);
}
runSafe(this, this.errorCallbacks, error);
// Exit the current call stack
throw error;
};
/**
* retry() can be called in an onRun callback to restart the task
*/
AsyncTask.prototype.retry = function(error, maxRetryCounter) {
if(this.finished === true) {
return;
}
maxRetryCounter = maxRetryCounter || 5;
this.queue = undefined;
if(this.retryCounter >= maxRetryCounter) {
this.error(error);
return;
}
// Implement an exponential backoff
var delay = Math.pow(2, this.retryCounter++) * 1000;
currentTaskStartTime = utils.currentTime + delay;
currentTaskRunning = false;
this.callPath = [];
runTask();
};
/**
* enqueue() has to be called to add the task in the running task queue
*/
AsyncTask.prototype.enqueue = function() {
taskQueue.push(this);
runTask();
};
var asyncRunning = false;
var currentTask = undefined;
var currentTaskRunning = false;
var currentTaskStartTime = 0;
// Run the next task in the queue if any and no other running
function runTask() {
// Use defer to avoid stack overflow
_.defer(function() {
// If there is a task currently running
if(currentTaskRunning === true) {
// If the current task takes too long
if(currentTaskStartTime + currentTask.timeout < utils.currentTime) {
currentTask.error(new Error("A timeout occurred.|\n" + currentTask.callPath.join("\n")));
}
return;
}
if(currentTask === undefined) {
// If no task in the queue
if(taskQueue.length === 0) {
return;
}
// Dequeue an enqueued task
currentTask = taskQueue.shift();
currentTaskStartTime = utils.currentTime;
if(asyncRunning === false) {
asyncRunning = true;
extensionMgr.onAsyncRunning(true);
}
}
// Run the task
if(currentTaskStartTime <= utils.currentTime) {
currentTaskRunning = true;
currentTask.chain();
}
});
}
// Run runTask function periodically
core.addPeriodicCallback(runTask);
function runSafe(task, callbacks, param) {
try {
_.each(callbacks, function(callback) {
callback(param);
});
}
finally {
task.finished = true;
if(currentTask === task) {
currentTask = undefined;
currentTaskRunning = false;
}
if(taskQueue.length === 0) {
asyncRunning = false;
extensionMgr.onAsyncRunning(false);
}
else {
runTask();
}
}
}
return AsyncTask;
});

View File

@ -0,0 +1,102 @@
define(["utils"], function(utils) {
function FileDescriptor(fileIndex, title, syncLocations, publishLocations) {
this.fileIndex = fileIndex;
this._title = title;
this._editorScrollTop = parseInt(localStorage[fileIndex + ".editorScrollTop"]) || 0;
this._editorStart = parseInt(localStorage[fileIndex + ".editorStart"]) || 0;
this._editorEnd = parseInt(localStorage[fileIndex + ".editorEnd"]) || 0;
this._previewScrollTop = parseInt(localStorage[fileIndex + ".previewScrollTop"]) || 0;
this._selectTime = parseInt(localStorage[fileIndex + ".selectTime"]) || 0;
this.syncLocations = syncLocations || {};
this.publishLocations = publishLocations || {};
Object.defineProperty(this, 'title', {
get: function() {
return this._title;
},
set: function(title) {
this._title = title;
localStorage[this.fileIndex + ".title"] = title;
}
});
Object.defineProperty(this, 'content', {
get: function() {
return localStorage[this.fileIndex + ".content"];
},
set: function(content) {
localStorage[this.fileIndex + ".content"] = content;
}
});
Object.defineProperty(this, 'editorScrollTop', {
get: function() {
return this._editorScrollTop;
},
set: function(editorScrollTop) {
this._editorScrollTop = editorScrollTop;
localStorage[this.fileIndex + ".editorScrollTop"] = editorScrollTop;
}
});
Object.defineProperty(this, 'editorStart', {
get: function() {
return this._editorStart;
},
set: function(editorStart) {
this._editorStart = editorStart;
localStorage[this.fileIndex + ".editorStart"] = editorStart;
}
});
Object.defineProperty(this, 'editorEnd', {
get: function() {
return this._editorEnd;
},
set: function(editorEnd) {
this._editorEnd = editorEnd;
localStorage[this.fileIndex + ".editorEnd"] = editorEnd;
}
});
Object.defineProperty(this, 'previewScrollTop', {
get: function() {
return this._previewScrollTop;
},
set: function(previewScrollTop) {
this._previewScrollTop = previewScrollTop;
localStorage[this.fileIndex + ".previewScrollTop"] = previewScrollTop;
}
});
Object.defineProperty(this, 'selectTime', {
get: function() {
return this._selectTime;
},
set: function(selectTime) {
this._selectTime = selectTime;
localStorage[this.fileIndex + ".selectTime"] = selectTime;
}
});
}
FileDescriptor.prototype.addSyncLocation = function(syncAttributes) {
utils.storeAttributes(syncAttributes);
utils.appendIndexToArray(this.fileIndex + ".sync", syncAttributes.syncIndex);
this.syncLocations[syncAttributes.syncIndex] = syncAttributes;
};
FileDescriptor.prototype.removeSyncLocation = function(syncAttributes) {
utils.removeIndexFromArray(this.fileIndex + ".sync", syncAttributes.syncIndex);
delete this.syncLocations[syncAttributes.syncIndex];
localStorage.removeItem(syncAttributes.syncIndex);
};
FileDescriptor.prototype.addPublishLocation = function(publishAttributes) {
utils.storeAttributes(publishAttributes);
utils.appendIndexToArray(this.fileIndex + ".publish", publishAttributes.publishIndex);
this.publishLocations[publishAttributes.publishIndex] = publishAttributes;
};
FileDescriptor.prototype.removePublishLocation = function(publishAttributes) {
utils.removeIndexFromArray(this.fileIndex + ".publish", publishAttributes.publishIndex);
delete this.publishLocations[publishAttributes.publishIndex];
localStorage.removeItem(publishAttributes.publishIndex);
};
return FileDescriptor;
});

View File

@ -0,0 +1,94 @@
define([
"jquery",
"underscore",
"toMarkdown",
"config",
], function($, _, toMarkdown) {
var dialogOpenHarddrive = {
extensionId: "dialogOpenHarddrive",
extensionName: 'Dialog "Open from"',
settingsBloc: '<p>Handles the "Open from hard drive" and the "Convert HTML to Markdown" dialog boxes.</p>'
};
var fileMgr = undefined;
dialogOpenHarddrive.onFileMgrCreated = function(fileMgrParameter) {
fileMgr = fileMgrParameter;
};
var extensionMgr = undefined;
dialogOpenHarddrive.onExtensionMgrCreated = function(extensionMgrParameter) {
extensionMgr = extensionMgrParameter;
};
var contentWrapper = undefined;
var converter = undefined;
var htmlContentWrapper = function(content) {
return converter.makeMd(content);
};
function handleFileImport(evt) {
evt.stopPropagation();
evt.preventDefault();
var files = (evt.dataTransfer || evt.target).files;
$("#modal-import-harddrive-markdown, #modal-import-harddrive-html").modal("hide");
_.each(files, function(file) {
var reader = new FileReader();
reader.onload = (function(importedFile) {
return function(e) {
var content = e.target.result;
if(content.match(/\uFFFD/)) {
extensionMgr.onError(importedFile.name + " is a binary file.");
return;
}
content = contentWrapper ? contentWrapper(content) : content;
if(content === undefined) {
extensionMgr.onError(importedFile.name + " is not a valid HTML file.");
return;
}
var title = importedFile.name;
var dotPosition = title.lastIndexOf(".");
title = dotPosition !== -1 ? title.substring(0, dotPosition) : title;
var fileDesc = fileMgr.createFile(title, content);
fileMgr.selectFile(fileDesc);
};
})(file);
var blob = file.slice(0, IMPORT_FILE_MAX_CONTENT_SIZE);
reader.readAsText(blob);
});
}
function handleMarkdownImport(evt) {
contentWrapper = undefined;
handleFileImport(evt);
}
function handleHtmlImport(evt) {
contentWrapper = htmlContentWrapper;
handleFileImport(evt);
}
function handleDragOver(evt) {
evt.stopPropagation();
evt.preventDefault();
evt.dataTransfer.dropEffect = 'copy';
}
dialogOpenHarddrive.onReady = function() {
// Create toMarkdown converter
converter = new toMarkdown.converter();
$("#input-file-import-harddrive-markdown").change(handleMarkdownImport);
$('#dropzone-import-harddrive-markdown').each(function() {
this.addEventListener('dragover', handleDragOver, false);
this.addEventListener('drop', handleMarkdownImport, false);
});
$("#input-file-import-harddrive-html").change(handleHtmlImport);
$('#dropzone-import-harddrive-html').each(function() {
this.addEventListener('dragover', handleDragOver, false);
this.addEventListener('drop', handleHtmlImport, false);
});
};
return dialogOpenHarddrive;
});

248
js/libs/to-markdown.js Normal file
View File

@ -0,0 +1,248 @@
/*
* to-markdown - an HTML to Markdown converter
*
* Copyright 2011-2012, Dom Christie
* Licenced under the MIT licence
*
*/
(function() {
var root = this;
var toMarkdown = {};
var isNode = false;
if(typeof module !== 'undefined' && module.exports) {
module.exports = toMarkdown;
root.toMarkdown = toMarkdown;
isNode = true;
}
else {
root.toMarkdown = toMarkdown;
}
toMarkdown.converter = function(options) {
if(options && options.elements && $.isArray(options.elements)) {
ELEMENTS = ELEMENTS.concat(options.elements);
}
this.makeMd = function(input, callback) {
var result;
if(isNode) {
var jsdom = require('jsdom');
jsdom.env({
html: input,
scripts: [
'http://code.jquery.com/jquery-1.6.4.min.js'
],
done: function(errors, window) {
if(typeof callback === 'function') {
callback(process(input, window.$));
}
}
});
}
else {
result = process(input, $);
}
return result;
};
};
var process = function(input, $) {
// Escape potential ol triggers
// see bottom of lists section: http://daringfireball.net/projects/markdown/syntax#list
input = input.replace(/(\d+)\. /g, '$1\\. ');
// Wrap in containing div
var $container = $('<div/>');
var $input = $container.html(input);
// Remove whitespace
$input.find('*:not(pre, code)').contents().filter(function() {
return this.nodeType === 3 && (/^\s+$/.test(this.nodeValue));
}).remove();
var selectors = [];
for(var i = 0, len = ELEMENTS.length; i < len; i++) {
selectors.push(ELEMENTS[i].selector);
}
selectors = selectors.join(',');
while($input.find(selectors).length) {
for(var i = 0, len = ELEMENTS.length; i < len; i++) {
// Find the innermost elements containing NO children that convert to markdown
$matches = $input.find(ELEMENTS[i].selector + ':not(:has("' + selectors + '"))');
$matches.each(function(j, el) {
var $el = $(el);
$el.before(ELEMENTS[i].replacement($el.html(), $el)).remove();
});
}
}
return cleanUp($input.html());
};
// =============
// = Utilities =
// =============
var trimNewLines = function(str) {
return str.replace(/^[\n\r\f]+|[\n\r\f]+$/g, '');
};
var decodeHtmlEntities = function(str) {
return String(str).replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"');
};
var cleanUp = function(string) {
string = string.replace(/^[\t\r\n]+|[\t\r\n]+$/g, ''); // trim leading/trailing whitespace
string = string.replace(/\n\s+\n/g, '\n\n');
string = string.replace(/\n{3,}/g, '\n\n'); // limit consecutive linebreaks to 2
string = decodeHtmlEntities(string);
return string;
};
var strongReplacement = function(innerHTML) {
innerHTML = trimNewLines(innerHTML);
return innerHTML ? '**' + innerHTML + '**' : '';
};
var emReplacement = function(innerHTML) {
innerHTML = trimNewLines(innerHTML);
return innerHTML ? '_' + innerHTML + '_' : '';
};
// ============
// = Elements =
// ============
var ELEMENTS = [
{
selector: 'p',
replacement: function(innerHTML, el) {
innerHTML = $.trim(innerHTML);
return innerHTML ? '\n\n' + innerHTML + '\n\n' : '';
}
},
{
selector: 'br',
replacement: function(innerHTML, el) {
return '\n';
}
},
{
selector: 'h1,h2,h3,h4,h5,h6',
replacement: function(innerHTML, $el) {
innerHTML = $.trim(innerHTML);
var hLevel = $el.prop("nodeName").charAt(1),
prefix = '';
for(var i = 0; i < hLevel; i++) {
prefix += '#';
}
return innerHTML ? '\n\n' + prefix + ' ' + innerHTML + '\n\n' : '';
}
},
{
selector: 'hr',
replacement: function(innerHTML, el) {
return '\n\n* * *\n\n';
}
},
{
selector: 'a[href]',
replacement: function(innerHTML, $el) {
if(innerHTML) {
innerHTML = trimNewLines(innerHTML);
var href = $el.attr('href'),
title = $el.attr('title') || '';
return '[' + innerHTML + ']' + '(' + href + (title ? ' "' + title + '"' : '') + ')';
}
else {
return false;
}
}
},
{
selector: 'b',
replacement: strongReplacement
},
{
selector: 'strong',
replacement: strongReplacement
},
{
selector: 'i',
replacement: emReplacement
},
{
selector: 'em',
replacement: emReplacement
},
{
selector: 'code',
replacement: function(innerHTML, el) {
innerHTML = trimNewLines(innerHTML);
return innerHTML ? '`' + innerHTML + '`' : '';
}
},
{
selector: 'img',
replacement: function(innerHTML, $el) {
var alt = $el.attr('alt') || '',
src = $el.attr('src') || '',
title = $el.attr('title') || '';
return '![' + alt + ']' + '(' + src + (title ? ' "' + title + '"' : '') + ')';
}
},
{
selector: 'pre',
replacement: function(innerHTML, el) {
if(/^\s*\`/.test(innerHTML)) {
innerHTML = innerHTML.replace(/\`/g, '');
return ' ' + innerHTML.replace(/\n/g, '\n ');
}
else {
return '';
}
}
},
{
selector: 'li',
replacement: function(innerHTML, $el) {
innerHTML = innerHTML.replace(/^\s+|\s+$/, '').replace(/\n/gm, '\n ');
var prefix = '* ';
var suffix = '';
var $parent = $el.parent();
var $children = $parent.contents().filter(function() {
return (this.nodeType === 1 && this.nodeName === 'LI') || (this.nodeType === 3);
});
var index = $children.index($el) + 1;
prefix = $parent.is('ol') ? index + '. ' : '* ';
if(index == $children.length) {
if(!$el.parents('li').length) {
suffix = '\n';
}
innerHTML = innerHTML.replace(/\s+$/, ''); // Trim
$el.unwrap();
}
return prefix + innerHTML + suffix + '\n';
}
},
{
selector: 'blockquote',
replacement: function(innerHTML, el) {
innerHTML = innerHTML = $.trim(innerHTML).replace(/\n{3,}/g, '\n\n');
innerHTML = innerHTML.replace(/\n/g, '\n&gt; ');
return "&gt; " + innerHTML;
}
}
];
var NON_MD_BLOCK_ELEMENTS = ['address', 'article', 'aside', 'audio', 'canvas', 'div', 'dl', 'dd', 'dt',
'fieldset', 'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'output',
'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'section', 'video'];
})();