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:
parent
cefa0e14ef
commit
0021fb7a95
8 changed files with 421 additions and 145 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>';
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
};
|
||||
|
||||
}());
|
|
@ -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, '\\_');
|
||||
|
|
264
core/shared/vendor/underscore.js
vendored
264
core/shared/vendor/underscore.js
vendored
|
@ -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);
|
|
@ -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 =
|
||||
|
|
|
@ -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: /^<section.*?((?!<a href=\'http:\/\/google.co.uk\/kitten.jpg\').)*<\/section>$/
|
||||
// },
|
||||
// {
|
||||
// input: "",
|
||||
// output: /^<section.*?((?!<a href=\'http:\/\/dsurl.stuff\/something\').)*<\/section>$/
|
||||
|
||||
// });
|
||||
//
|
||||
*/
|
||||
|
||||
it("should correctly output link and image markdown without autolinks", function () {
|
||||
var testPhrases = [
|
||||
|
|
|
@ -213,7 +213,7 @@ describe("Github showdown extensions", function () {
|
|||
},
|
||||
{
|
||||
input: "",
|
||||
output: /^!\[1\]\(http:\/\/google.co.uk\/kitten.jpg\)$/
|
||||
output: /^<img src=\"http:\/\/google.co.uk\/kitten.jpg\" alt=\"1\" \/>$/
|
||||
},
|
||||
{
|
||||
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: /^<img src=\"http:\/\/google.co.uk\/kitten.jpg\" alt=\"http:\/\/google.co.uk\/kitten.jpg\" \/>$/
|
||||
}
|
||||
],
|
||||
processedMarkup;
|
||||
|
|
Loading…
Add table
Reference in a new issue