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 = /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\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 '
' + result + '
Add image of ' + alt + '
' + '' + '
'; 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 '' + 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, '\\_'); 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]()", "![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 = 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: "![http://google.co.uk/kitten.jpg](http://google.co.uk/kitten.jpg)", -// output: /^$/ -// }, -// { -// input: "![image stuff](http://dsurl.stuff/something)", -// 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: "![1](http://google.co.uk/kitten.jpg)", - output: /^!\[1\]\(http:\/\/google.co.uk\/kitten.jpg\)$/ + output: /^\"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: /^\"http:\/\/google.co.uk\/kitten.jpg\"$/ } ], processedMarkup;