0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-25 02:31:59 -05:00

Save image uploads in the editor

closes #295

- Maintain a list of markers for CodeMirror which reference image codes
- Upload start triggers a selection
- Upload success replaces the selection
- No ref-style image markdown handling
- Showdown image URL handling improved at the expense of titles
- Tests updated
This commit is contained in:
Hannah Wolfe 2013-09-01 22:03:01 +01:00
parent cefa0e14ef
commit 0021fb7a95
8 changed files with 421 additions and 145 deletions

View file

@ -26,7 +26,7 @@
url: '/ghost/upload',
add: function (e, data) {
$progress.find('.js-upload-progress-bar').removeClass('fail');
$dropzone.trigger('uploadstart');
$dropzone.trigger('uploadstart', [$dropzone.attr('id')]);
$dropzone.find('span.media, div.description, a.image-url, a.image-webcam')
.animate({opacity: 0}, 250, function () {
$dropzone.find('div.description').hide().css({"opacity": 100});
@ -47,6 +47,7 @@
}
},
fail: function (e, data) {
$dropzone.trigger("uploadfailure", [data.result]);
$dropzone.find('.js-upload-progress-bar').addClass('fail');
$dropzone.find('div.js-fail, button.js-fail').fadeIn(1500);
$dropzone.find('button.js-fail').on('click', function () {
@ -57,6 +58,8 @@
});
},
done: function (e, data) {
$dropzone.trigger("uploadsuccess", [data.result, $dropzone.attr('id')]);
function showImage(width, height) {
$dropzone.find('img.js-upload-target').attr({"width": width, "height": height}).css({"display": "block"});
$dropzone.find('.fileupload-loading').remove();
@ -85,7 +88,6 @@
$dropzone.find('span.media').after('<img class="fileupload-loading" src="/public/img/loadingcat.gif" />');
if (!settings.editor) {$progress.find('.fileupload-loading').css({"top": "56px"}); }
});
$dropzone.trigger("uploadsuccess", [data.result]);
$img.one('load', function () { animateDropzone($img); })
.attr('src', data.result);
}

View file

@ -5,24 +5,18 @@
{
type: 'lang',
filter: function (text) {
var defRegex = /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/gim,
match,
defUrls = {};
var imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
/* regex from isURL in node-validator. Yum! */
uriRegex = /^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i,
pathRegex = /^(\/)?([^\/\0]+(\/)?)+$/i;
while ((match = defRegex.exec(text)) !== null) {
defUrls[match[1]] = match;
}
return text.replace(/^!(?:\[([^\n\]]*)\])(?:\[([^\n\]]*)\]|\(([^\n\]]*)\))?$/gim, function (match, alt, id, src) {
return text.replace(imageMarkdownRegex, function (match, key, alt, src) {
var result = "";
/* regex from isURL in node-validator. Yum! */
if (src && src.match(/^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i)) {
if (src && (src.match(uriRegex) || src.match(pathRegex))) {
result = '<img class="js-upload-target" src="' + src + '"/>';
} else if (id && defUrls.hasOwnProperty(id)) {
result = '<img class="js-upload-target" src="' + defUrls[id][2] + '"/>';
}
return '<section class="js-drop-zone image-uploader">' + result +
return '<section id="image_upload_' + key + '" class="js-drop-zone image-uploader">' + result +
'<div class="description">Add image of <strong>' + alt + '</strong></div>' +
'<input data-url="upload" class="js-fileupload fileupload" type="file" name="uploadimage">' +
'</section>';

View file

@ -4,8 +4,11 @@
(function () {
"use strict";
/*jslint regexp: true, bitwise: true */
var PublishBar,
ActionsWidget,
UploadManager,
MarkerManager,
MarkdownShortcuts = [
{'key': 'Ctrl+B', 'style': 'bold'},
{'key': 'Meta+B', 'style': 'bold'},
@ -31,7 +34,10 @@
{'key': 'Ctrl+L', 'style': 'list'},
{'key': 'Ctrl+Alt+C', 'style': 'copyHTML'},
{'key': 'Meta+Alt+C', 'style': 'copyHTML'}
];
],
imageMarkdownRegex = /^(?:\{<(.*?)>\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
markerRegex = /\{<([\w\W]*?)>\}/;
/*jslint regexp: false, bitwise: false */
// The publish bar associated with a post, which has the TagWidget and
// Save button and options and such.
@ -197,14 +203,16 @@
},
savePost: function (data) {
// TODO: The markdown getter here isn't great, shouldn't rely on currentView.
_.each(this.model.blacklist, function (item) {
this.model.unset(item);
}, this);
var saved = this.model.save(_.extend({
title: $('#entry-title').val(),
markdown: Ghost.currentView.editor.getValue()
// TODO: The content_raw getter here isn't great, shouldn't rely on currentView.
markdown: Ghost.currentView.getEditorValue()
}, data));
// TODO: Take this out if #2489 gets merged in Backbone. Or patch Backbone
@ -260,7 +268,7 @@
});
// The entire /editor page's route (TODO: move all views to client side templates)
// The entire /editor page's route
// ----------------------------------------
Ghost.Views.Editor = Ghost.View.extend({
@ -371,7 +379,9 @@
var self = this,
preview = document.getElementsByClassName('rendered-markdown')[0];
preview.innerHTML = this.converter.makeHtml(this.editor.getValue());
this.$('.js-drop-zone').upload({editor: true});
this.initUploads();
Countable.once(preview, function (counter) {
self.$('.entry-word-count').text($.pluralize(counter.words, 'word'));
self.$('.entry-character-count').text($.pluralize(counter.characters, 'character'));
@ -391,6 +401,7 @@
lineWrapping: true,
dragDrop: false
});
this.uploadMgr = new UploadManager(this.editor);
// Inject modal for HTML to be viewed in
shortcut.add("Ctrl+Alt+C", function () {
@ -406,11 +417,42 @@
});
});
this.enableEditor();
},
options: {
markers: {}
},
getEditorValue: function () {
return this.uploadMgr.getEditorValue();
},
initUploads: function () {
this.$('.js-drop-zone').upload({editor: true});
this.$('.js-drop-zone').on('uploadstart', $.proxy(this.disableEditor, this));
this.$('.js-drop-zone').on('uploadstart', this.uploadMgr.handleDownloadStart);
this.$('.js-drop-zone').on('uploadfailure', $.proxy(this.enableEditor, this));
this.$('.js-drop-zone').on('uploadsuccess', $.proxy(this.enableEditor, this));
this.$('.js-drop-zone').on('uploadsuccess', this.uploadMgr.handleDownloadSuccess);
},
enableEditor: function () {
var self = this;
this.editor.setOption("readOnly", false);
this.editor.on('change', function () {
self.renderPreview();
});
},
disableEditor: function () {
var self = this;
this.editor.setOption("readOnly", "nocursor");
this.editor.off('change', function () {
self.renderPreview();
});
},
showHTML: function () {
this.addSubview(new Ghost.Views.Modal({
model: {
@ -431,4 +473,191 @@
render: function () { return this; }
});
}());
MarkerManager = function (editor) {
var markers = {},
uploadPrefix = 'image_upload',
uploadId = 1;
function addMarker(line, ln) {
var marker,
magicId = '{<' + uploadId + '>}';
editor.setLine(ln, magicId + line.text);
marker = editor.markText(
{line: ln, ch: 0},
{line: ln, ch: (magicId.length)},
{collapsed: true}
);
markers[uploadPrefix + '_' + uploadId] = marker;
uploadId += 1;
}
function getMarkerRegexForId(id) {
id = id.replace('image_upload_', '');
return new RegExp('\\{<' + id + '>\\}', 'gmi');
}
function stripMarkerFromLine(line) {
var markerText = line.text.match(markerRegex),
ln = editor.getLineNumber(line);
if (markerText) {
editor.replaceRange('', {line: ln, ch: markerText.index}, {line: ln, ch: markerText.index + markerText[0].length});
}
}
function findAndStripMarker(id) {
editor.eachLine(function (line) {
var markerText = getMarkerRegexForId(id).exec(line.text),
ln;
if (markerText) {
ln = editor.getLineNumber(line);
editor.replaceRange('', {line: ln, ch: markerText.index}, {line: ln, ch: markerText.index + markerText[0].length});
}
});
}
function removeMarker(id, marker, line) {
delete markers[id];
marker.clear();
if (line) {
stripMarkerFromLine(line);
} else {
findAndStripMarker(id);
}
}
function checkMarkers() {
_.each(markers, function (marker, id) {
var line;
marker = markers[id];
if (marker.find()) {
line = editor.getLineHandle(marker.find().from.line);
if (!line.text.match(imageMarkdownRegex)) {
removeMarker(id, marker, line);
}
} else {
removeMarker(id, marker);
}
});
}
function initMarkers(line) {
var isImage = line.text.match(imageMarkdownRegex),
hasMarker = line.text.match(markerRegex);
if (isImage && !hasMarker) {
addMarker(line, editor.getLineNumber(line));
}
}
// public api
_.extend(this, {
markers: markers,
checkMarkers: checkMarkers,
addMarker: addMarker,
stripMarkerFromLine: stripMarkerFromLine,
getMarkerRegexForId: getMarkerRegexForId
});
// Initialise
editor.eachLine(initMarkers);
};
UploadManager = function (editor) {
var markerMgr = new MarkerManager(editor);
function findLine(result_id) {
// try to find the right line to replace
if (markerMgr.markers.hasOwnProperty(result_id) && markerMgr.markers[result_id].find()) {
return editor.getLineHandle(markerMgr.markers[result_id].find().from.line);
}
return false;
}
function checkLine(ln, mode) {
var line = editor.getLineHandle(ln),
isImage = line.text.match(imageMarkdownRegex),
hasMarker;
// We care if it is an image
if (isImage) {
hasMarker = line.text.match(markerRegex);
if (hasMarker && mode === 'paste') {
// this could be a duplicate, and won't be a real marker
markerMgr.stripMarkerFromLine(line);
}
if (!hasMarker) {
markerMgr.addMarker(line, ln);
}
}
// TODO: hasMarker but no image?
}
function handleDownloadStart(e) {
/*jslint regexp: true, bitwise: true */
var line = findLine($(e.currentTarget).attr('id')),
lineNumber = editor.getLineNumber(line),
match = line.text.match(/\([^\n]*\)?/),
replacement = '(http://)';
/*jslint regexp: false, bitwise: false */
if (match) {
// simple case, we have the parenthesis
editor.setSelection({line: lineNumber, ch: match.index + 1}, {line: lineNumber, ch: match.index + match[0].length - 1});
} else {
match = line.text.match(/\]/);
if (match) {
editor.replaceRange(
replacement,
{line: lineNumber, ch: match.index + 1},
{line: lineNumber, ch: match.index + 1}
);
editor.setSelection(
{line: lineNumber, ch: match.index + 2},
{line: lineNumber, ch: match.index + replacement.length }
);
}
}
}
function handleDownloadSuccess(e, result_src) {
editor.replaceSelection(result_src);
}
function getEditorValue() {
var value = editor.getValue();
_.each(markerMgr.markers, function (marker, id) {
value = value.replace(markerMgr.getMarkerRegexForId(id), '');
});
return value;
}
// Public API
_.extend(this, {
getEditorValue: getEditorValue,
handleDownloadStart: handleDownloadStart,
handleDownloadSuccess: handleDownloadSuccess
});
// initialise
editor.on('change', function (cm, changeObj) {
var linesChanged = _.range(changeObj.from.line, changeObj.from.line + changeObj.text.length);
_.each(linesChanged, function (ln) {
checkLine(ln, changeObj.origin);
});
// Is this a line which may have had a marker on it?
markerMgr.checkMarkers();
});
};
}());

View file

@ -20,6 +20,7 @@
type : 'lang',
filter : function (text) {
var extractions = {},
imageMarkdownRegex = /^(?:\{(.*?)\})?!(?:\[([^\n\]]*)\])(?:\(([^\n\]]*)\))?$/gim,
hashID = 0;
function hashId() {
@ -33,6 +34,11 @@
return "{gfm-js-extract-pre-" + hash + "}";
}, 'm');
// better URL support, but no title support
text = text.replace(imageMarkdownRegex, function (match, key, alt, src) {
return '<img src="' + src + '" alt="' + alt + '" />';
});
//prevent foo_bar and foo_bar_baz from ending up with an italic word in the middle
text = text.replace(/(^(?! {4}|\t)\w+_\w+_\w[\w_]*)/gm, function (x) {
return x.replace(/_/gm, '\\_');

View file

@ -1,15 +1,14 @@
// Underscore.js 1.4.4
// ===================
// Underscore.js 1.5.2
// http://underscorejs.org
// (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Underscore may be freely distributed under the MIT license.
// > http://underscorejs.org
// > (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc.
// > Underscore may be freely distributed under the MIT license.
// Baseline setup
// --------------
(function() {
// Establish the root object, `window` in the browser, or `global` on the server.
// Baseline setup
// --------------
// Establish the root object, `window` in the browser, or `exports` on the server.
var root = this;
// Save the previous value of the `_` variable.
@ -22,11 +21,12 @@
var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
// Create quick reference variables for speed access to core prototypes.
var push = ArrayProto.push,
slice = ArrayProto.slice,
concat = ArrayProto.concat,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
var
push = ArrayProto.push,
slice = ArrayProto.slice,
concat = ArrayProto.concat,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// All **ECMAScript 5** native function implementations that we hope to use
// are declared here.
@ -65,7 +65,7 @@
}
// Current version.
_.VERSION = '1.4.4';
_.VERSION = '1.5.2';
// Collection Functions
// --------------------
@ -78,14 +78,13 @@
if (nativeForEach && obj.forEach === nativeForEach) {
obj.forEach(iterator, context);
} else if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
for (var i = 0, length = obj.length; i < length; i++) {
if (iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (_.has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
var keys = _.keys(obj);
for (var i = 0, length = keys.length; i < length; i++) {
if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
}
}
};
@ -97,7 +96,7 @@
if (obj == null) return results;
if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
results.push(iterator.call(context, value, index, list));
});
return results;
};
@ -172,7 +171,7 @@
if (obj == null) return results;
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
each(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) results[results.length] = value;
if (iterator.call(context, value, index, list)) results.push(value);
});
return results;
};
@ -239,7 +238,7 @@
// Convenience version of a common use case of `filter`: selecting only objects
// containing specific `key:value` pairs.
_.where = function(obj, attrs, first) {
if (_.isEmpty(attrs)) return first ? null : [];
if (_.isEmpty(attrs)) return first ? void 0 : [];
return _[first ? 'find' : 'filter'](obj, function(value) {
for (var key in attrs) {
if (attrs[key] !== value[key]) return false;
@ -256,7 +255,7 @@
// Return the maximum element or (element-based computation).
// Can't optimize arrays of integers longer than 65,535 elements.
// See: https://bugs.webkit.org/show_bug.cgi?id=80797
// See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797)
_.max = function(obj, iterator, context) {
if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
return Math.max.apply(Math, obj);
@ -265,7 +264,7 @@
var result = {computed : -Infinity, value: -Infinity};
each(obj, function(value, index, list) {
var computed = iterator ? iterator.call(context, value, index, list) : value;
computed >= result.computed && (result = {value : value, computed : computed});
computed > result.computed && (result = {value : value, computed : computed});
});
return result.value;
};
@ -284,7 +283,8 @@
return result.value;
};
// Shuffle an array.
// Shuffle an array, using the modern version of the
// [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/FisherYates_shuffle).
_.shuffle = function(obj) {
var rand;
var index = 0;
@ -297,6 +297,17 @@
return shuffled;
};
// Sample **n** random values from a collection.
// If **n** is not specified, returns a single random element.
// The internal `guard` argument allows it to work with `map`.
_.sample = function(obj, n, guard) {
if (n == null || guard) {
if (obj.length !== +obj.length) obj = _.values(obj);
return obj[_.random(obj.length - 1)];
}
return _.shuffle(obj).slice(0, Math.max(0, n));
};
// An internal function to generate lookup iterators.
var lookupIterator = function(value) {
return _.isFunction(value) ? value : function(obj){ return obj[value]; };
@ -307,9 +318,9 @@
var iterator = lookupIterator(value);
return _.pluck(_.map(obj, function(value, index, list) {
return {
value : value,
index : index,
criteria : iterator.call(context, value, index, list)
value: value,
index: index,
criteria: iterator.call(context, value, index, list)
};
}).sort(function(left, right) {
var a = left.criteria;
@ -318,38 +329,41 @@
if (a > b || a === void 0) return 1;
if (a < b || b === void 0) return -1;
}
return left.index < right.index ? -1 : 1;
return left.index - right.index;
}), 'value');
};
// An internal function used for aggregate "group by" operations.
var group = function(obj, value, context, behavior) {
var result = {};
var iterator = lookupIterator(value || _.identity);
each(obj, function(value, index) {
var key = iterator.call(context, value, index, obj);
behavior(result, key, value);
});
return result;
var group = function(behavior) {
return function(obj, value, context) {
var result = {};
var iterator = value == null ? _.identity : lookupIterator(value);
each(obj, function(value, index) {
var key = iterator.call(context, value, index, obj);
behavior(result, key, value);
});
return result;
};
};
// Groups the object's values by a criterion. Pass either a string attribute
// to group by, or a function that returns the criterion.
_.groupBy = function(obj, value, context) {
return group(obj, value, context, function(result, key, value) {
(_.has(result, key) ? result[key] : (result[key] = [])).push(value);
});
};
_.groupBy = group(function(result, key, value) {
(_.has(result, key) ? result[key] : (result[key] = [])).push(value);
});
// Indexes the object's values by a criterion, similar to `groupBy`, but for
// when you know that your index values will be unique.
_.indexBy = group(function(result, key, value) {
result[key] = value;
});
// Counts instances of an object that group by a certain criterion. Pass
// either a string attribute to count by, or a function that returns the
// criterion.
_.countBy = function(obj, value, context) {
return group(obj, value, context, function(result, key) {
if (!_.has(result, key)) result[key] = 0;
result[key]++;
});
};
_.countBy = group(function(result, key) {
_.has(result, key) ? result[key]++ : result[key] = 1;
});
// Use a comparator function to figure out the smallest index at which
// an object should be inserted so as to maintain order. Uses binary search.
@ -364,7 +378,7 @@
return low;
};
// Safely convert anything iterable into a real, live array.
// Safely create a real, live array from anything iterable.
_.toArray = function(obj) {
if (!obj) return [];
if (_.isArray(obj)) return slice.call(obj);
@ -386,7 +400,7 @@
// allows it to work with `_.map`.
_.first = _.head = _.take = function(array, n, guard) {
if (array == null) return void 0;
return (n != null) && !guard ? slice.call(array, 0, n) : array[0];
return (n == null) || guard ? array[0] : slice.call(array, 0, n);
};
// Returns everything but the last entry of the array. Especially useful on
@ -401,10 +415,10 @@
// values in the array. The **guard** check allows it to work with `_.map`.
_.last = function(array, n, guard) {
if (array == null) return void 0;
if ((n != null) && !guard) {
return slice.call(array, Math.max(array.length - n, 0));
} else {
if ((n == null) || guard) {
return array[array.length - 1];
} else {
return slice.call(array, Math.max(array.length - n, 0));
}
};
@ -423,8 +437,11 @@
// Internal implementation of a recursive `flatten` function.
var flatten = function(input, shallow, output) {
if (shallow && _.every(input, _.isArray)) {
return concat.apply(output, input);
}
each(input, function(value) {
if (_.isArray(value)) {
if (_.isArray(value) || _.isArguments(value)) {
shallow ? push.apply(output, value) : flatten(value, shallow, output);
} else {
output.push(value);
@ -433,7 +450,7 @@
return output;
};
// Return a completely flattened version of an array.
// Flatten out an array, either recursively (by default), or just one level.
_.flatten = function(array, shallow) {
return flatten(array, shallow, []);
};
@ -467,7 +484,7 @@
// Produce an array that contains the union: each distinct element from all of
// the passed-in arrays.
_.union = function() {
return _.uniq(concat.apply(ArrayProto, arguments));
return _.uniq(_.flatten(arguments, true));
};
// Produce an array that contains every item shared between all the
@ -491,11 +508,10 @@
// Zip together multiple lists into a single array -- elements that share
// an index go together.
_.zip = function() {
var args = slice.call(arguments);
var length = _.max(_.pluck(args, 'length'));
var length = _.max(_.pluck(arguments, "length").concat(0));
var results = new Array(length);
for (var i = 0; i < length; i++) {
results[i] = _.pluck(args, "" + i);
results[i] = _.pluck(arguments, '' + i);
}
return results;
};
@ -506,7 +522,7 @@
_.object = function(list, values) {
if (list == null) return {};
var result = {};
for (var i = 0, l = list.length; i < l; i++) {
for (var i = 0, length = list.length; i < length; i++) {
if (values) {
result[list[i]] = values[i];
} else {
@ -524,17 +540,17 @@
// for **isSorted** to use binary search.
_.indexOf = function(array, item, isSorted) {
if (array == null) return -1;
var i = 0, l = array.length;
var i = 0, length = array.length;
if (isSorted) {
if (typeof isSorted == 'number') {
i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted);
i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted);
} else {
i = _.sortedIndex(array, item);
return array[i] === item ? i : -1;
}
}
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
for (; i < l; i++) if (array[i] === item) return i;
for (; i < length; i++) if (array[i] === item) return i;
return -1;
};
@ -560,11 +576,11 @@
}
step = arguments[2] || 1;
var len = Math.max(Math.ceil((stop - start) / step), 0);
var length = Math.max(Math.ceil((stop - start) / step), 0);
var idx = 0;
var range = new Array(len);
var range = new Array(length);
while(idx < len) {
while(idx < length) {
range[idx++] = start;
start += step;
}
@ -575,14 +591,25 @@
// Function (ahem) Functions
// ------------------
// Reusable constructor function for prototype setting.
var ctor = function(){};
// Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
// available.
_.bind = function(func, context) {
if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
var args = slice.call(arguments, 2);
return function() {
return func.apply(context, args.concat(slice.call(arguments)));
var args, bound;
if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
if (!_.isFunction(func)) throw new TypeError;
args = slice.call(arguments, 2);
return bound = function() {
if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
ctor.prototype = func.prototype;
var self = new ctor;
ctor.prototype = null;
var result = func.apply(self, args.concat(slice.call(arguments)));
if (Object(result) === result) return result;
return self;
};
};
@ -599,7 +626,7 @@
// all callbacks defined on an object belong to it.
_.bindAll = function(obj) {
var funcs = slice.call(arguments, 1);
if (funcs.length === 0) funcs = _.functions(obj);
if (funcs.length === 0) throw new Error("bindAll must be passed function names");
each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
return obj;
};
@ -628,17 +655,23 @@
};
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time.
_.throttle = function(func, wait) {
var context, args, timeout, result;
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
options || (options = {});
var later = function() {
previous = new Date;
previous = options.leading === false ? 0 : new Date;
timeout = null;
result = func.apply(context, args);
};
return function() {
var now = new Date;
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
@ -647,7 +680,7 @@
timeout = null;
previous = now;
result = func.apply(context, args);
} else if (!timeout) {
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
@ -659,16 +692,24 @@
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
_.debounce = function(func, wait, immediate) {
var timeout, result;
var timeout, args, context, timestamp, result;
return function() {
var context = this, args = arguments;
context = this;
args = arguments;
timestamp = new Date();
var later = function() {
timeout = null;
if (!immediate) result = func.apply(context, args);
var last = (new Date()) - timestamp;
if (last < wait) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (!immediate) result = func.apply(context, args);
}
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (!timeout) {
timeout = setTimeout(later, wait);
}
if (callNow) result = func.apply(context, args);
return result;
};
@ -713,7 +754,6 @@
// Returns a function that will only be executed after being called N times.
_.after = function(times, func) {
if (times <= 0) return func();
return function() {
if (--times < 1) {
return func.apply(this, arguments);
@ -729,28 +769,39 @@
_.keys = nativeKeys || function(obj) {
if (obj !== Object(obj)) throw new TypeError('Invalid object');
var keys = [];
for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key;
for (var key in obj) if (_.has(obj, key)) keys.push(key);
return keys;
};
// Retrieve the values of an object's properties.
_.values = function(obj) {
var values = [];
for (var key in obj) if (_.has(obj, key)) values.push(obj[key]);
var keys = _.keys(obj);
var length = keys.length;
var values = new Array(length);
for (var i = 0; i < length; i++) {
values[i] = obj[keys[i]];
}
return values;
};
// Convert an object into a list of `[key, value]` pairs.
_.pairs = function(obj) {
var pairs = [];
for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]);
var keys = _.keys(obj);
var length = keys.length;
var pairs = new Array(length);
for (var i = 0; i < length; i++) {
pairs[i] = [keys[i], obj[keys[i]]];
}
return pairs;
};
// Invert the keys and values of an object. The values must be serializable.
_.invert = function(obj) {
var result = {};
for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key;
var keys = _.keys(obj);
for (var i = 0, length = keys.length; i < length; i++) {
result[obj[keys[i]]] = keys[i];
}
return result;
};
@ -801,7 +852,7 @@
each(slice.call(arguments, 1), function(source) {
if (source) {
for (var prop in source) {
if (obj[prop] == null) obj[prop] = source[prop];
if (obj[prop] === void 0) obj[prop] = source[prop];
}
}
});
@ -825,7 +876,7 @@
// Internal recursive comparison function for `isEqual`.
var eq = function(a, b, aStack, bStack) {
// Identical objects are equal. `0 === -0`, but they aren't identical.
// See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.
// See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
if (a === b) return a !== 0 || 1 / a == 1 / b;
// A strict comparison is necessary because `null == undefined`.
if (a == null || b == null) return a === b;
@ -867,6 +918,13 @@
// unique nested structures.
if (aStack[length] == a) return bStack[length] == b;
}
// Objects with different constructors are not equivalent, but `Object`s
// from different frames are.
var aCtor = a.constructor, bCtor = b.constructor;
if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
_.isFunction(bCtor) && (bCtor instanceof bCtor))) {
return false;
}
// Add the first object to the stack of traversed objects.
aStack.push(a);
bStack.push(b);
@ -883,13 +941,6 @@
}
}
} else {
// Objects with different constructors are not equivalent, but `Object`s
// from different frames are.
var aCtor = a.constructor, bCtor = b.constructor;
if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
_.isFunction(bCtor) && (bCtor instanceof bCtor))) {
return false;
}
// Deep compare objects.
for (var key in a) {
if (_.has(a, key)) {
@ -1013,7 +1064,7 @@
// Run a function **n** times.
_.times = function(n, iterator, context) {
var accum = Array(n);
var accum = Array(Math.max(0, n));
for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
return accum;
};
@ -1034,8 +1085,7 @@
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;'
"'": '&#x27;'
}
};
entityMap.unescape = _.invert(entityMap.escape);
@ -1056,17 +1106,17 @@
};
});
// If the value of the named property is a function then invoke it;
// otherwise, return it.
// If the value of the named `property` is a function then invoke it with the
// `object` as context; otherwise, return it.
_.result = function(object, property) {
if (object == null) return null;
if (object == null) return void 0;
var value = object[property];
return _.isFunction(value) ? value.call(object) : value;
};
// Add your own custom functions to the Underscore object.
_.mixin = function(obj) {
each(_.functions(obj), function(name){
each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
@ -1224,4 +1274,4 @@
});
}).call(this);
}).call(this);

View file

@ -36,11 +36,13 @@ describe("Ghostdown showdown extensions", function () {
"![image and another,/ image]",
"![image and another,/ image]()",
"![image and another,/ image](http://dsurl.stuff)",
"![](http://dsurl.stuff)",
"![](http://dsurl.stuff)"
/* No ref-style for now
"![][]",
"![image and another,/ image][stuff]",
"![][stuff]",
"![image and another,/ image][]"
*/
]
.forEach(function (imageMarkup) {
var processedMarkup =
@ -56,9 +58,11 @@ describe("Ghostdown showdown extensions", function () {
it("should correctly include an image", function () {
[
"![image and another,/ image](http://dsurl.stuff)",
"![](http://dsurl.stuff)",
"![](http://dsurl.stuff)"
/* No ref-style for now
"![image and another,/ image][test]\n\n[test]: http://dsurl.stuff",
"![][test]\n\n[test]: http://dsurl.stuff"
*/
]
.forEach(function (imageMarkup) {
var processedMarkup =

View file

@ -223,6 +223,7 @@ describe("Showdown client side converter", function () {
});
});
/* No ref-style for now
it("should convert reference format image", function () {
var testPhrases = [
{
@ -245,6 +246,7 @@ describe("Showdown client side converter", function () {
processedMarkup.should.match(testPhrase.output);
});
});
*/
it("should NOT auto-link URL in HTML", function () {
var testPhrases = [
@ -352,6 +354,7 @@ describe("Showdown client side converter", function () {
});
});
/* No ref-style for now
it("should have placeholder with image if image reference is present", function () {
var testPhrases = [
{
@ -374,19 +377,7 @@ describe("Showdown client side converter", function () {
}
});
});
// it("should NOT auto-link URL in image", function () {
// var testPhrases = [
// {
// input: "![http://google.co.uk/kitten.jpg](http://google.co.uk/kitten.jpg)",
// output: /^<section.*?((?!<a href=\'http:\/\/google.co.uk\/kitten.jpg\').)*<\/section>$/
// },
// {
// input: "![image stuff](http://dsurl.stuff/something)",
// output: /^<section.*?((?!<a href=\'http:\/\/dsurl.stuff\/something\').)*<\/section>$/
// });
//
*/
it("should correctly output link and image markdown without autolinks", function () {
var testPhrases = [

View file

@ -213,7 +213,7 @@ describe("Github showdown extensions", function () {
},
{
input: "![1](http://google.co.uk/kitten.jpg)",
output: /^!\[1\]\(http:\/\/google.co.uk\/kitten.jpg\)$/
output: /^<img src=\"http:\/\/google.co.uk\/kitten.jpg\" alt=\"1\" \/>$/
},
{
input: " ![1](http://google.co.uk/kitten.jpg)",
@ -221,7 +221,7 @@ describe("Github showdown extensions", function () {
},
{
input: "![http://google.co.uk/kitten.jpg](http://google.co.uk/kitten.jpg)",
output: /^!\[http:\/\/google.co.uk\/kitten.jpg\]\(http:\/\/google.co.uk\/kitten.jpg\)$/
output: /^<img src=\"http:\/\/google.co.uk\/kitten.jpg\" alt=\"http:\/\/google.co.uk\/kitten.jpg\" \/>$/
}
],
processedMarkup;