diff --git a/core/client/assets/lib/uploader.js b/core/client/assets/lib/uploader.js
index 8391b7b68b..728fd6930e 100644
--- a/core/client/assets/lib/uploader.js
+++ b/core/client/assets/lib/uploader.js
@@ -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('
');
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);
}
diff --git a/core/client/assets/vendor/showdown/extensions/ghostdown.js b/core/client/assets/vendor/showdown/extensions/ghostdown.js
index 242d337b90..621c1a3f15 100644
--- a/core/client/assets/vendor/showdown/extensions/ghostdown.js
+++ b/core/client/assets/vendor/showdown/extensions/ghostdown.js
@@ -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 = '
';
- } else if (id && defUrls.hasOwnProperty(id)) {
- result = '
';
}
- return '' + result +
+ return '';
diff --git a/core/client/views/editor.js b/core/client/views/editor.js
index 18401b9d3e..18938065bb 100644
--- a/core/client/views/editor.js
+++ b/core/client/views/editor.js
@@ -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();
+ });
+ };
+
+}());
\ No newline at end of file
diff --git a/core/shared/vendor/showdown/extensions/github.js b/core/shared/vendor/showdown/extensions/github.js
index 85898862d7..07db047f10 100644
--- a/core/shared/vendor/showdown/extensions/github.js
+++ b/core/shared/vendor/showdown/extensions/github.js
@@ -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 '
';
+ });
+
//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, '\\_');
diff --git a/core/shared/vendor/underscore.js b/core/shared/vendor/underscore.js
index 32ca0c1b14..3446c795a6 100644
--- a/core/shared/vendor/underscore.js
+++ b/core/shared/vendor/underscore.js
@@ -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/Fisher–Yates_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 @@
'<': '<',
'>': '>',
'"': '"',
- "'": ''',
- '/': '/'
+ "'": '''
}
};
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);
\ No newline at end of file
diff --git a/core/test/unit/client_ghostdown_spec.js b/core/test/unit/client_ghostdown_spec.js
index 7b732cd47f..d2bffcd2ad 100644
--- a/core/test/unit/client_ghostdown_spec.js
+++ b/core/test/unit/client_ghostdown_spec.js
@@ -36,11 +36,13 @@ describe("Ghostdown showdown extensions", function () {
"![image and another,/ image]",
"![image and another,/ image]()",
"",
- "",
+ ""
+ /* 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 () {
[
"",
- "",
+ ""
+ /* 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 =
diff --git a/core/test/unit/client_showdown_int_spec.js b/core/test/unit/client_showdown_int_spec.js
index 538e13ec58..1b427c2f2b 100644
--- a/core/test/unit/client_showdown_int_spec.js
+++ b/core/test/unit/client_showdown_int_spec.js
@@ -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: "",
-// output: /^$/
-// },
-// {
-// input: "",
-// output: /^$/
-
-// });
-//
+ */
it("should correctly output link and image markdown without autolinks", function () {
var testPhrases = [
diff --git a/core/test/unit/shared_gfm_spec.js b/core/test/unit/shared_gfm_spec.js
index 987c46adaa..f657f2abf3 100644
--- a/core/test/unit/shared_gfm_spec.js
+++ b/core/test/unit/shared_gfm_spec.js
@@ -213,7 +213,7 @@ describe("Github showdown extensions", function () {
},
{
input: "",
- output: /^!\[1\]\(http:\/\/google.co.uk\/kitten.jpg\)$/
+ output: /^
$/
},
{
input: " ",
@@ -221,7 +221,7 @@ describe("Github showdown extensions", function () {
},
{
input: "",
- output: /^!\[http:\/\/google.co.uk\/kitten.jpg\]\(http:\/\/google.co.uk\/kitten.jpg\)$/
+ output: /^
$/
}
],
processedMarkup;