<commit_before>/* ***** BEGIN LICENSE BLOCK *****
 * Distributed under the BSD license:
 *
 * Copyright (c) 2012, Ajax.org B.V.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Ajax.org B.V. nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ***** END LICENSE BLOCK ***** */

define(function(require, exports, module) {
"use strict";

var HashHandler = require("./keyboard/hash_handler").HashHandler;
var AcePopup = require("./autocomplete/popup").AcePopup;
var util = require("./autocomplete/util");
var event = require("./lib/event");
var lang = require("./lib/lang");
var snippetManager = require("./snippets").snippetManager;

var Autocomplete = function() {
    this.autoInsert = true;
    this.autoSelect = true;
    this.keyboardHandler = new HashHandler();
    this.keyboardHandler.bindKeys(this.commands);

    this.blurListener = this.blurListener.bind(this);
    this.changeListener = this.changeListener.bind(this);
    this.mousedownListener = this.mousedownListener.bind(this);
    this.mousewheelListener = this.mousewheelListener.bind(this);

    this.changeTimer = lang.delayedCall(function() {
        this.updateCompletions(true);
    }.bind(this));
};

(function() {
    this.gatherCompletionsId = 0;

    this.$init = function() {
        this.popup = new AcePopup(document.body || document.documentElement);
        this.popup.on("click", function(e) {
            this.insertMatch();
            e.stop();
        }.bind(this));
        this.popup.focus = this.editor.focus.bind(this.editor);
    };

    this.openPopup = function(editor, prefix, keepPopupPosition) {
        if (!this.popup)
            this.$init();

        this.popup.setData(this.completions.filtered);

        var renderer = editor.renderer;
        this.popup.setRow(this.autoSelect ? 0 : -1);
        if (!keepPopupPosition) {
            this.popup.setTheme(editor.getTheme());
            this.popup.setFontSize(editor.getFontSize());

            var lineHeight = renderer.layerConfig.lineHeight;

            var pos = renderer.$cursorLayer.getPixelPosition(this.base, true);
            pos.left -= this.popup.getTextLeftOffset();

            var rect = editor.container.getBoundingClientRect();
            pos.top += rect.top - renderer.layerConfig.offset;
            pos.left += rect.left - editor.renderer.scrollLeft;
            pos.left += renderer.$gutterLayer.gutterWidth;

            this.popup.show(pos, lineHeight);
        }
    };

    this.detach = function() {
        this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler);
        this.editor.off("changeSelection", this.changeListener);
        this.editor.off("blur", this.blurListener);
        this.editor.off("mousedown", this.mousedownListener);
        this.editor.off("mousewheel", this.mousewheelListener);
        this.changeTimer.cancel();

        if (this.popup && this.popup.isOpen) {
            this.gatherCompletionsId += 1;
            this.popup.hide();
        }

        if (this.base)
            this.base.detach();
        this.activated = false;
        this.completions = this.base = null;
    };

    this.changeListener = function(e) {
        var cursor = this.editor.selection.lead;
        if (cursor.row != this.base.row || cursor.column < this.base.column) {
            this.detach();
        }
        if (this.activated)
            this.changeTimer.schedule();
        else
            this.detach();
    };

    this.blurListener = function() {
        // we have to check if activeElement is a child of popup because
        // on IE preventDefault doesn't stop scrollbar from being focussed
        var el = document.activeElement;
        if (el != this.editor.textInput.getElement() && el.parentNode != this.popup.container)
            this.detach();
    };

    this.mousedownListener = function(e) {
        this.detach();
    };

    this.mousewheelListener = function(e) {
        this.detach();
    };

    this.goTo = function(where) {
        var row = this.popup.getRow();
        var max = this.popup.session.getLength() - 1;

        switch(where) {
            case "up": row = row <= 0 ? max : row - 1; break;
            case "down": row = row >= max ? -1 : row + 1; break;
            case "start": row = 0; break;
            case "end": row = max; break;
        }

        this.popup.setRow(row);
    };

    this.insertMatch = function(data) {
        if (!data)
            data = this.popup.getData(this.popup.getRow());
        if (!data)
            return false;

        if (data.completer && data.completer.insertMatch) {
            data.completer.insertMatch(this.editor);
        } else {
            if (this.completions.filterText) {
                var ranges = this.editor.selection.getAllRanges();
                for (var i = 0, range; range = ranges[i]; i++) {
                    range.start.column -= this.completions.filterText.length;
                    this.editor.session.remove(range);
                }
            }
            if (data.snippet)
                snippetManager.insertSnippet(this.editor, data.snippet);
            else
                this.editor.execCommand("insertstring", data.value || data);
        }
        this.detach();
    };

    this.commands = {
        "Up": function(editor) { editor.completer.goTo("up"); },
        "Down": function(editor) { editor.completer.goTo("down"); },
        "Ctrl-Up|Ctrl-Home": function(editor) { editor.completer.goTo("start"); },
        "Ctrl-Down|Ctrl-End": function(editor) { editor.completer.goTo("end"); },

        "Esc": function(editor) { editor.completer.detach(); },
        "Space": function(editor) { editor.completer.detach(); editor.insert(" ");},
        "Return": function(editor) { return editor.completer.insertMatch(); },
        "Shift-Return": function(editor) { editor.completer.insertMatch(true); },
        "Tab": function(editor) {
            var result = editor.completer.insertMatch();
            if (!result && !editor.tabstopManager)
                editor.completer.goTo("down");
            else
                return result;
        },

        "PageUp": function(editor) { editor.completer.popup.gotoPageUp(); },
        "PageDown": function(editor) { editor.completer.popup.gotoPageDown(); }
    };

    this.gatherCompletions = function(editor, callback) {
        var session = editor.getSession();
        var pos = editor.getCursorPosition();

        var line = session.getLine(pos.row);
        var prefix = util.retrievePrecedingIdentifier(line, pos.column);

        this.base = session.doc.createAnchor(pos.row, pos.column - prefix.length);

        var matches = [];
        var total = editor.completers.length;
        editor.completers.forEach(function(completer, i) {
            completer.getCompletions(editor, session, pos, prefix, function(err, results) {
                if (!err)
                    matches = matches.concat(results);
                // Fetch prefix again, because they may have changed by now
                var pos = editor.getCursorPosition();
                var line = session.getLine(pos.row);
                callback(null, {
                    prefix: util.retrievePrecedingIdentifier(line, pos.column, results[0] && results[0].identifierRegex),
                    matches: matches,
                    finished: (--total === 0)
                });
            });
        });
        return true;
    };

    this.showPopup = function(editor) {
        if (this.editor)
            this.detach();

        this.activated = true;

        this.editor = editor;
        if (editor.completer != this) {
            if (editor.completer)
                editor.completer.detach();
            editor.completer = this;
        }

        editor.keyBinding.addKeyboardHandler(this.keyboardHandler);
        editor.on("changeSelection", this.changeListener);
        editor.on("blur", this.blurListener);
        editor.on("mousedown", this.mousedownListener);
        editor.on("mousewheel", this.mousewheelListener);

        this.updateCompletions();
    };

    this.updateCompletions = function(keepPopupPosition) {
        if (keepPopupPosition && this.base && this.completions) {
            var pos = this.editor.getCursorPosition();
            var prefix = this.editor.session.getTextRange({start: this.base, end: pos});
            if (prefix == this.completions.filterText)
                return;
            this.completions.setFilter(prefix);
            if (!this.completions.filtered.length)
                return this.detach();
            if (this.completions.filtered.length == 1
            && this.completions.filtered[0].value == prefix
            && !this.completions.filtered[0].snippet)
                return this.detach();
            this.openPopup(this.editor, prefix, keepPopupPosition);
            return;
        }

        // Save current gatherCompletions session, session is close when a match is insert
        var _id = this.gatherCompletionsId;
        this.gatherCompletions(this.editor, function(err, results) {
            // Only detach if result gathering is finished
            var detachIfFinished = function() {
                if (!results.finished) return;
                return this.detach();
            }.bind(this);

            var prefix = results.prefix;
            var matches = results && results.matches;

            if (!matches || !matches.length)
                return detachIfFinished();

            // Wrong prefix or wrong session -> ignore
            if (prefix.indexOf(results.prefix) !== 0 || _id != this.gatherCompletionsId)
                return;

            this.completions = new FilteredList(matches);
            this.completions.setFilter(prefix);
            var filtered = this.completions.filtered;

            // No results
            if (!filtered.length)
                return detachIfFinished();

            // One result equals to the prefix
            if (filtered.length == 1 && filtered[0].value == prefix && !filtered[0].snippet)
                return detachIfFinished();

            // Autoinsert if one result
            if (this.autoInsert && filtered.length == 1)
                return this.insertMatch(filtered[0]);

            this.openPopup(this.editor, prefix, keepPopupPosition);
        }.bind(this));
    };

    this.cancelContextMenu = function() {
        this.editor.$mouseHandler.cancelContextMenu();
    };

}).call(Autocomplete.prototype);

Autocomplete.startCommand = {
    name: "startAutocomplete",
    exec: function(editor) {
        if (!editor.completer)
            editor.completer = new Autocomplete();
        editor.completer.autoInsert =
        editor.completer.autoSelect = true;
        editor.completer.showPopup(editor);
        // needed for firefox on mac
        editor.completer.cancelContextMenu();
    },
    bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space"
};

var FilteredList = function(array, filterText, mutateData) {
    this.all = array;
    this.filtered = array;
    this.filterText = filterText || "";
};
(function(){
    this.setFilter = function(str) {
        if (str.length > this.filterText && str.lastIndexOf(this.filterText, 0) === 0)
            var matches = this.filtered;
        else
            var matches = this.all;

        this.filterText = str;
        matches = this.filterCompletions(matches, this.filterText);
        matches = matches.sort(function(a, b) {
            return b.exactMatch - a.exactMatch || b.score - a.score;
        });

        // make unique
        var prev = null;
        matches = matches.filter(function(item){
            var caption = item.value || item.caption || item.snippet;
            if (caption === prev) return false;
            prev = caption;
            return true;
        });

        this.filtered = matches;
    };
    this.filterCompletions = function(items, needle) {
        var results = [];
        var upper = needle.toUpperCase();
        var lower = needle.toLowerCase();
        loop: for (var i = 0, item; item = items[i]; i++) {
            var caption = item.value || item.caption || item.snippet;
            if (!caption) continue;
            var lastIndex = -1;
            var matchMask = 0;
            var penalty = 0;
            var index, distance;
            // caption char iteration is faster in Chrome but slower in Firefox, so lets use indexOf
            for (var j = 0; j < needle.length; j++) {
                // TODO add penalty on case mismatch
                var i1 = caption.indexOf(lower[j], lastIndex + 1);
                var i2 = caption.indexOf(upper[j], lastIndex + 1);
                index = (i1 >= 0) ? ((i2 < 0 || i1 < i2) ? i1 : i2) : i2;
                if (index < 0)
                    continue loop;
                distance = index - lastIndex - 1;
                if (distance > 0) {
                    // first char mismatch should be more sensitive
                    if (lastIndex === -1)
                        penalty += 10;
                    penalty += distance;
                }
                matchMask = matchMask | (1 << index);
                lastIndex = index;
            }
            item.matchMask = matchMask;
            item.exactMatch = penalty ? 0 : 1;
            item.score = (item.score || 0) - penalty;
            results.push(item);
        }
        return results;
    };
}).call(FilteredList.prototype);

exports.Autocomplete = Autocomplete;
exports.FilteredList = FilteredList;

});
<commit_msg>changed FilteredList.setFilter unique matches computing<commit_after>/* ***** BEGIN LICENSE BLOCK *****
 * Distributed under the BSD license:
 *
 * Copyright (c) 2012, Ajax.org B.V.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Ajax.org B.V. nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ***** END LICENSE BLOCK ***** */

define(function(require, exports, module) {
"use strict";

var HashHandler = require("./keyboard/hash_handler").HashHandler;
var AcePopup = require("./autocomplete/popup").AcePopup;
var util = require("./autocomplete/util");
var event = require("./lib/event");
var lang = require("./lib/lang");
var snippetManager = require("./snippets").snippetManager;

var Autocomplete = function() {
    this.autoInsert = true;
    this.autoSelect = true;
    this.keyboardHandler = new HashHandler();
    this.keyboardHandler.bindKeys(this.commands);

    this.blurListener = this.blurListener.bind(this);
    this.changeListener = this.changeListener.bind(this);
    this.mousedownListener = this.mousedownListener.bind(this);
    this.mousewheelListener = this.mousewheelListener.bind(this);

    this.changeTimer = lang.delayedCall(function() {
        this.updateCompletions(true);
    }.bind(this));
};

(function() {
    this.gatherCompletionsId = 0;

    this.$init = function() {
        this.popup = new AcePopup(document.body || document.documentElement);
        this.popup.on("click", function(e) {
            this.insertMatch();
            e.stop();
        }.bind(this));
        this.popup.focus = this.editor.focus.bind(this.editor);
    };

    this.openPopup = function(editor, prefix, keepPopupPosition) {
        if (!this.popup)
            this.$init();

        this.popup.setData(this.completions.filtered);

        var renderer = editor.renderer;
        this.popup.setRow(this.autoSelect ? 0 : -1);
        if (!keepPopupPosition) {
            this.popup.setTheme(editor.getTheme());
            this.popup.setFontSize(editor.getFontSize());

            var lineHeight = renderer.layerConfig.lineHeight;

            var pos = renderer.$cursorLayer.getPixelPosition(this.base, true);
            pos.left -= this.popup.getTextLeftOffset();

            var rect = editor.container.getBoundingClientRect();
            pos.top += rect.top - renderer.layerConfig.offset;
            pos.left += rect.left - editor.renderer.scrollLeft;
            pos.left += renderer.$gutterLayer.gutterWidth;

            this.popup.show(pos, lineHeight);
        }
    };

    this.detach = function() {
        this.editor.keyBinding.removeKeyboardHandler(this.keyboardHandler);
        this.editor.off("changeSelection", this.changeListener);
        this.editor.off("blur", this.blurListener);
        this.editor.off("mousedown", this.mousedownListener);
        this.editor.off("mousewheel", this.mousewheelListener);
        this.changeTimer.cancel();

        if (this.popup && this.popup.isOpen) {
            this.gatherCompletionsId += 1;
            this.popup.hide();
        }

        if (this.base)
            this.base.detach();
        this.activated = false;
        this.completions = this.base = null;
    };

    this.changeListener = function(e) {
        var cursor = this.editor.selection.lead;
        if (cursor.row != this.base.row || cursor.column < this.base.column) {
            this.detach();
        }
        if (this.activated)
            this.changeTimer.schedule();
        else
            this.detach();
    };

    this.blurListener = function() {
        // we have to check if activeElement is a child of popup because
        // on IE preventDefault doesn't stop scrollbar from being focussed
        var el = document.activeElement;
        if (el != this.editor.textInput.getElement() && el.parentNode != this.popup.container)
            this.detach();
    };

    this.mousedownListener = function(e) {
        this.detach();
    };

    this.mousewheelListener = function(e) {
        this.detach();
    };

    this.goTo = function(where) {
        var row = this.popup.getRow();
        var max = this.popup.session.getLength() - 1;

        switch(where) {
            case "up": row = row <= 0 ? max : row - 1; break;
            case "down": row = row >= max ? -1 : row + 1; break;
            case "start": row = 0; break;
            case "end": row = max; break;
        }

        this.popup.setRow(row);
    };

    this.insertMatch = function(data) {
        if (!data)
            data = this.popup.getData(this.popup.getRow());
        if (!data)
            return false;

        if (data.completer && data.completer.insertMatch) {
            data.completer.insertMatch(this.editor);
        } else {
            if (this.completions.filterText) {
                var ranges = this.editor.selection.getAllRanges();
                for (var i = 0, range; range = ranges[i]; i++) {
                    range.start.column -= this.completions.filterText.length;
                    this.editor.session.remove(range);
                }
            }
            if (data.snippet)
                snippetManager.insertSnippet(this.editor, data.snippet);
            else
                this.editor.execCommand("insertstring", data.value || data);
        }
        this.detach();
    };

    this.commands = {
        "Up": function(editor) { editor.completer.goTo("up"); },
        "Down": function(editor) { editor.completer.goTo("down"); },
        "Ctrl-Up|Ctrl-Home": function(editor) { editor.completer.goTo("start"); },
        "Ctrl-Down|Ctrl-End": function(editor) { editor.completer.goTo("end"); },

        "Esc": function(editor) { editor.completer.detach(); },
        "Space": function(editor) { editor.completer.detach(); editor.insert(" ");},
        "Return": function(editor) { return editor.completer.insertMatch(); },
        "Shift-Return": function(editor) { editor.completer.insertMatch(true); },
        "Tab": function(editor) {
            var result = editor.completer.insertMatch();
            if (!result && !editor.tabstopManager)
                editor.completer.goTo("down");
            else
                return result;
        },

        "PageUp": function(editor) { editor.completer.popup.gotoPageUp(); },
        "PageDown": function(editor) { editor.completer.popup.gotoPageDown(); }
    };

    this.gatherCompletions = function(editor, callback) {
        var session = editor.getSession();
        var pos = editor.getCursorPosition();

        var line = session.getLine(pos.row);
        var prefix = util.retrievePrecedingIdentifier(line, pos.column);

        this.base = session.doc.createAnchor(pos.row, pos.column - prefix.length);

        var matches = [];
        var total = editor.completers.length;
        editor.completers.forEach(function(completer, i) {
            completer.getCompletions(editor, session, pos, prefix, function(err, results) {
                if (!err)
                    matches = matches.concat(results);
                // Fetch prefix again, because they may have changed by now
                var pos = editor.getCursorPosition();
                var line = session.getLine(pos.row);
                callback(null, {
                    prefix: util.retrievePrecedingIdentifier(line, pos.column, results[0] && results[0].identifierRegex),
                    matches: matches,
                    finished: (--total === 0)
                });
            });
        });
        return true;
    };

    this.showPopup = function(editor) {
        if (this.editor)
            this.detach();

        this.activated = true;

        this.editor = editor;
        if (editor.completer != this) {
            if (editor.completer)
                editor.completer.detach();
            editor.completer = this;
        }

        editor.keyBinding.addKeyboardHandler(this.keyboardHandler);
        editor.on("changeSelection", this.changeListener);
        editor.on("blur", this.blurListener);
        editor.on("mousedown", this.mousedownListener);
        editor.on("mousewheel", this.mousewheelListener);

        this.updateCompletions();
    };

    this.updateCompletions = function(keepPopupPosition) {
        if (keepPopupPosition && this.base && this.completions) {
            var pos = this.editor.getCursorPosition();
            var prefix = this.editor.session.getTextRange({start: this.base, end: pos});
            if (prefix == this.completions.filterText)
                return;
            this.completions.setFilter(prefix);
            if (!this.completions.filtered.length)
                return this.detach();
            if (this.completions.filtered.length == 1
            && this.completions.filtered[0].value == prefix
            && !this.completions.filtered[0].snippet)
                return this.detach();
            this.openPopup(this.editor, prefix, keepPopupPosition);
            return;
        }

        // Save current gatherCompletions session, session is close when a match is insert
        var _id = this.gatherCompletionsId;
        this.gatherCompletions(this.editor, function(err, results) {
            // Only detach if result gathering is finished
            var detachIfFinished = function() {
                if (!results.finished) return;
                return this.detach();
            }.bind(this);

            var prefix = results.prefix;
            var matches = results && results.matches;

            if (!matches || !matches.length)
                return detachIfFinished();

            // Wrong prefix or wrong session -> ignore
            if (prefix.indexOf(results.prefix) !== 0 || _id != this.gatherCompletionsId)
                return;

            this.completions = new FilteredList(matches);
            this.completions.setFilter(prefix);
            var filtered = this.completions.filtered;

            // No results
            if (!filtered.length)
                return detachIfFinished();

            // One result equals to the prefix
            if (filtered.length == 1 && filtered[0].value == prefix && !filtered[0].snippet)
                return detachIfFinished();

            // Autoinsert if one result
            if (this.autoInsert && filtered.length == 1)
                return this.insertMatch(filtered[0]);

            this.openPopup(this.editor, prefix, keepPopupPosition);
        }.bind(this));
    };

    this.cancelContextMenu = function() {
        this.editor.$mouseHandler.cancelContextMenu();
    };

}).call(Autocomplete.prototype);

Autocomplete.startCommand = {
    name: "startAutocomplete",
    exec: function(editor) {
        if (!editor.completer)
            editor.completer = new Autocomplete();
        editor.completer.autoInsert =
        editor.completer.autoSelect = true;
        editor.completer.showPopup(editor);
        // needed for firefox on mac
        editor.completer.cancelContextMenu();
    },
    bindKey: "Ctrl-Space|Ctrl-Shift-Space|Alt-Space"
};

var FilteredList = function(array, filterText, mutateData) {
    this.all = array;
    this.filtered = array;
    this.filterText = filterText || "";
};
(function(){
    this.setFilter = function(str) {
        if (str.length > this.filterText && str.lastIndexOf(this.filterText, 0) === 0)
            var matches = this.filtered;
        else
            var matches = this.all;

        this.filterText = str;
        matches = this.filterCompletions(matches, this.filterText);
        matches = matches.sort(function(a, b) {
            return b.exactMatch - a.exactMatch || b.score - a.score;
        });

        // make unique
        var prev = null;
        matches = matches.filter(function(item){
            var caption = item.snippet || item.caption || item.value;
            if (caption === prev) return false;
            prev = caption;
            return true;
        });

        this.filtered = matches;
    };
    this.filterCompletions = function(items, needle) {
        var results = [];
        var upper = needle.toUpperCase();
        var lower = needle.toLowerCase();
        loop: for (var i = 0, item; item = items[i]; i++) {
            var caption = item.value || item.caption || item.snippet;
            if (!caption) continue;
            var lastIndex = -1;
            var matchMask = 0;
            var penalty = 0;
            var index, distance;
            // caption char iteration is faster in Chrome but slower in Firefox, so lets use indexOf
            for (var j = 0; j < needle.length; j++) {
                // TODO add penalty on case mismatch
                var i1 = caption.indexOf(lower[j], lastIndex + 1);
                var i2 = caption.indexOf(upper[j], lastIndex + 1);
                index = (i1 >= 0) ? ((i2 < 0 || i1 < i2) ? i1 : i2) : i2;
                if (index < 0)
                    continue loop;
                distance = index - lastIndex - 1;
                if (distance > 0) {
                    // first char mismatch should be more sensitive
                    if (lastIndex === -1)
                        penalty += 10;
                    penalty += distance;
                }
                matchMask = matchMask | (1 << index);
                lastIndex = index;
            }
            item.matchMask = matchMask;
            item.exactMatch = penalty ? 0 : 1;
            item.score = (item.score || 0) - penalty;
            results.push(item);
        }
        return results;
    };
}).call(FilteredList.prototype);

exports.Autocomplete = Autocomplete;
exports.FilteredList = FilteredList;

});
<|endoftext|>


<commit_before>function get(url) {
  return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest()
    req.open('GET', url)

    req.onload = function() {
      if (req.status == 200) {
        resolve(req.response)
      }
      else {
        reject(Error(req.statusText))
      }
    }

    req.onerror = function() {
      reject(Error("Network Error"))
    }

    req.send()
  })
}

function getJSON(url) {
  return get(url).then(JSON.parse)
}

function sortByKey(key, d) {
  return d.slice().sort( function (a, b) {
    return a[key] - b[key]
  }).reverse()
}

function limit(key, m, d) {
  return d.filter( function (d) {
    return d[key].isAfter(m)
  })
}

function sum(a) {
  return a.reduce( function (a, b) {
    return a + b
  }, 0)
}

function one() {
  return 1
}

function trueDefault(d) {
  return d === undefined ? true : d
}

function dictGet(dict, key) {
  var k = key.shift()

  if (!(k in dict))
    return null

  if (key.length == 0)
    return dict[k]

  return dictGet(dict[k], key)
}

function localStorageTest() {
  var test = 'test'
  try {
    localStorage.setItem(test, test)
    localStorage.removeItem(test)
    return true
  } catch(e) {
    return false
  }
}

function listReplace(s, subst) {
  for (key in subst) {
    var re = new RegExp(key, 'g')
    s = s.replace(re, subst[key])
  }
  return s
}

/* Helpers working with nodes */

function offline(d) {
  return !d.flags.online
}

function online(d) {
  return d.flags.online
}

function has_location(d) {
  return "location" in d.nodeinfo &&
         Math.abs(d.nodeinfo.location.latitude) < 90 &&
         Math.abs(d.nodeinfo.location.longitude) < 180
}

function subtract(a, b) {
  var ids = {}

  b.forEach( function (d) {
    ids[d.nodeinfo.node_id] = true
  })

  return a.filter( function (d) {
    return !(d.nodeinfo.node_id in ids)
  })
}

/* Helpers working with links */

function showDistance(d) {
  if (isNaN(d.distance))
    return

  return numeral(d.distance).format("0,0") + " m"
}

function showTq(d) {
  return numeral(1/d.tq).format("0%")
}

/* Infobox stuff (XXX: move to module) */

function attributeEntry(el, label, value) {
  if (value === null || value == undefined)
    return

  var tr = document.createElement("tr")
  var th = document.createElement("th")
  if (typeof label === "string")
    th.textContent = label

  else
    th.appendChild(label)

  tr.appendChild(th)

  var td = document.createElement("td")

  if (typeof value == "function")
    value(td)
  else
    td.appendChild(document.createTextNode(value))

  tr.appendChild(td)

  el.appendChild(tr)

  return td
}

function createIframe(opt, width, height) {
  el = document.createElement("iframe")
  width = typeof width !== 'undefined' ? width : '525px'
  height = typeof height !== 'undefined' ? height : '350px'

  if (opt.src)
    el.src = opt.src
  else
    el.src = opt

  if (opt.frameBorder)
    el.frameBorder = opt.frameBorder
  else
    el.frameBorder = 1

  if (opt.width)
    el.width = opt.width
  else
    el.width = width

  if (opt.height)
    el.height = opt.height
  else
    el.height = height

  el.scrolling = "no"
  el.seamless = "seamless"

  return el
}

function showStat(o, subst) {
  var content, caption
  subst = typeof subst !== 'undefined' ? subst : {}

  if (o.thumbnail) {
    content = document.createElement("img")
    content.src = listReplace(o.thumbnail, subst)
  }

  if (o.caption) {
    caption = listReplace(o.caption, subst)

    if (!content)
    content = document.createTextNode(caption)
  }

  if (o.iframe) {
    content = createIframe(o.iframe, o.width, o.height)
    if (o.iframe.src)
    content.src = listReplace(o.iframe.src, subst)
    else
    content.src = listReplace(o.iframe, subst)
  }

  var p = document.createElement("p")

  if (o.href) {
    var link = document.createElement("a")
    link.target = "_blank"
    link.href = listReplace(o.href, subst)
    link.appendChild(content)

    if (caption && o.thumbnail)
    link.title = caption

    p.appendChild(link)
  } else
    p.appendChild(content)

  return p
}

<commit_msg>iframe width relates to sidebar width (#44)<commit_after>function get(url) {
  return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest()
    req.open('GET', url)

    req.onload = function() {
      if (req.status == 200) {
        resolve(req.response)
      }
      else {
        reject(Error(req.statusText))
      }
    }

    req.onerror = function() {
      reject(Error("Network Error"))
    }

    req.send()
  })
}

function getJSON(url) {
  return get(url).then(JSON.parse)
}

function sortByKey(key, d) {
  return d.slice().sort( function (a, b) {
    return a[key] - b[key]
  }).reverse()
}

function limit(key, m, d) {
  return d.filter( function (d) {
    return d[key].isAfter(m)
  })
}

function sum(a) {
  return a.reduce( function (a, b) {
    return a + b
  }, 0)
}

function one() {
  return 1
}

function trueDefault(d) {
  return d === undefined ? true : d
}

function dictGet(dict, key) {
  var k = key.shift()

  if (!(k in dict))
    return null

  if (key.length == 0)
    return dict[k]

  return dictGet(dict[k], key)
}

function localStorageTest() {
  var test = 'test'
  try {
    localStorage.setItem(test, test)
    localStorage.removeItem(test)
    return true
  } catch(e) {
    return false
  }
}

function listReplace(s, subst) {
  for (key in subst) {
    var re = new RegExp(key, 'g')
    s = s.replace(re, subst[key])
  }
  return s
}

/* Helpers working with nodes */

function offline(d) {
  return !d.flags.online
}

function online(d) {
  return d.flags.online
}

function has_location(d) {
  return "location" in d.nodeinfo &&
         Math.abs(d.nodeinfo.location.latitude) < 90 &&
         Math.abs(d.nodeinfo.location.longitude) < 180
}

function subtract(a, b) {
  var ids = {}

  b.forEach( function (d) {
    ids[d.nodeinfo.node_id] = true
  })

  return a.filter( function (d) {
    return !(d.nodeinfo.node_id in ids)
  })
}

/* Helpers working with links */

function showDistance(d) {
  if (isNaN(d.distance))
    return

  return numeral(d.distance).format("0,0") + " m"
}

function showTq(d) {
  return numeral(1/d.tq).format("0%")
}

/* Infobox stuff (XXX: move to module) */

function attributeEntry(el, label, value) {
  if (value === null || value == undefined)
    return

  var tr = document.createElement("tr")
  var th = document.createElement("th")
  if (typeof label === "string")
    th.textContent = label

  else
    th.appendChild(label)

  tr.appendChild(th)

  var td = document.createElement("td")

  if (typeof value == "function")
    value(td)
  else
    td.appendChild(document.createTextNode(value))

  tr.appendChild(td)

  el.appendChild(tr)

  return td
}

function createIframe(opt, width, height) {
  el = document.createElement("iframe")
  width = typeof width !== 'undefined' ? width : '100%'
  height = typeof height !== 'undefined' ? height : '350px'

  if (opt.src)
    el.src = opt.src
  else
    el.src = opt

  if (opt.frameBorder)
    el.frameBorder = opt.frameBorder
  else
    el.frameBorder = 1

  if (opt.width)
    el.width = opt.width
  else
    el.width = width

  if (opt.height)
    el.height = opt.height
  else
    el.height = height

  el.scrolling = "no"
  el.seamless = "seamless"

  return el
}

function showStat(o, subst) {
  var content, caption
  subst = typeof subst !== 'undefined' ? subst : {}

  if (o.thumbnail) {
    content = document.createElement("img")
    content.src = listReplace(o.thumbnail, subst)
  }

  if (o.caption) {
    caption = listReplace(o.caption, subst)

    if (!content)
    content = document.createTextNode(caption)
  }

  if (o.iframe) {
    content = createIframe(o.iframe, o.width, o.height)
    if (o.iframe.src)
    content.src = listReplace(o.iframe.src, subst)
    else
    content.src = listReplace(o.iframe, subst)
  }

  var p = document.createElement("p")

  if (o.href) {
    var link = document.createElement("a")
    link.target = "_blank"
    link.href = listReplace(o.href, subst)
    link.appendChild(content)

    if (caption && o.thumbnail)
    link.title = caption

    p.appendChild(link)
  } else
    p.appendChild(content)

  return p
}

<|endoftext|>


<commit_before>/* testing pagination of timbles tables */

QUnit.test( 'Correct Number of Table Rows Showing on Page', function( assert ) {
  var numRows = $('table').find('tr').length;
  console.log('# rows altogether', numRows);
  assert.ok( numRows === 4, 'Passed!' );
});

QUnit.test( 'Detect single Header Row', function( assert ) {
  var numRows = $('table').find('tr.header-row').length;
  console.log('# header rows', numRows);
  assert.ok( numRows === 1, 'Passed!' );
});

QUnit.test( 'Correct Number of Non-header Record Rows', function( assert ) {
  var numRows = $('table').find('tr').not('.header-row').length;
  console.log('# records', numRows);
  assert.ok( numRows === 3, 'Passed!' );
});<commit_msg>Refactored pagination tests to use .equal() instead of .ok().<commit_after>/* testing pagination of timbles tables */

QUnit.test('Correct number of table rows showing on page', function(assert) {
  var numRows = $('table').find('tr').length;
  assert.equal(numRows, 4);
});

QUnit.test( 'Detect single header row', function( assert ) {
  var numRows = $('table').find('tr.header-row').length;
  assert.equal(numRows, 1);
});

QUnit.test('Correct number of non-header record rows', function( assert ) {
  var numRows = $('table').find('tr').not('.header-row').length;
  assert.ok(numRows, 3);
});
<|endoftext|>


<commit_before>/* ***** BEGIN LICENSE BLOCK *****
 * Distributed under the BSD license:
 *
 * Copyright (c) 2010, Ajax.org B.V.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Ajax.org B.V. nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ***** END LICENSE BLOCK ***** */

define(function(require, exports, module) {
"use strict";

var oop = require("../lib/oop");
var TextMode = require("./text").Mode;
var ClojureHighlightRules = require("./clojure_highlight_rules").ClojureHighlightRules;
var MatchingParensOutdent = require("./matching_parens_outdent").MatchingParensOutdent;
var Range = require("../range").Range;

var Mode = function() {
    this.HighlightRules = ClojureHighlightRules;
    this.$outdent = new MatchingParensOutdent();
};
oop.inherits(Mode, TextMode);

(function() {

    this.lineCommentStart = ";";

    this.getNextLineIndent = function(state, line, tab) {
        var indent = this.$getIndent(line);

        var tokenizedLine = this.getTokenizer().getLineTokens(line, state);
        var tokens = tokenizedLine.tokens;

        if (tokens.length && tokens[tokens.length-1].type == "comment") {
            return indent;
        }

        if (state == "start") {
            var match = line.match(/[\(\[]/);
            if (match) {
                indent += "  ";
            }
            match = line.match(/[\)]/);
            if (match) {
              indent = "";
            }
        }

        return indent;
    };

    this.checkOutdent = function(state, line, input) {
        return this.$outdent.checkOutdent(line, input);
    };

    this.autoOutdent = function(state, doc, row) {
        this.$outdent.autoOutdent(doc, row);
    };

    this.$id = "ace/mode/clojure";
}).call(Mode.prototype);

exports.Mode = Mode;
});
<commit_msg>Slightly improved auto indent for Clojure<commit_after>/* ***** BEGIN LICENSE BLOCK *****
 * Distributed under the BSD license:
 *
 * Copyright (c) 2010, Ajax.org B.V.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Ajax.org B.V. nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * ***** END LICENSE BLOCK ***** */

define(function(require, exports, module) {
"use strict";

var oop = require("../lib/oop");
var TextMode = require("./text").Mode;
var ClojureHighlightRules = require("./clojure_highlight_rules").ClojureHighlightRules;
var MatchingParensOutdent = require("./matching_parens_outdent").MatchingParensOutdent;
var lang = require("../lib/lang");

var Mode = function() {
    this.HighlightRules = ClojureHighlightRules;
    this.$outdent = new MatchingParensOutdent();
};
oop.inherits(Mode, TextMode);

(function() {

    this.lineCommentStart = ";";

    this.$checkBracketDelta = function (line) {
        var delta = 0;
        for(var i = 0; i < line.length; i++) {
            var ch = line[i];
            if(ch === '(' || ch === '[' || ch === '{') {
                delta++;
            } else if(ch === ')' || ch === ']' || ch === '}') {
                delta--;
            }
        }
        return delta;
    };

    this.getNextLineIndent = function(state, line, tab) {
        var indent = this.$getIndent(line);

        var tokenizedLine = this.getTokenizer().getLineTokens(line, state);
        var tokens = tokenizedLine.tokens;

        if (tokens.length && tokens[tokens.length-1].type == "comment") {
            return indent;
        }

        if (state == "start") {
            var delta = this.$checkBracketDelta(line);
            if (delta > 0) {
                indent += tab;
            } else if (delta < 0) {
                indent = indent.substring(0, indent.length - tab.length);
            }
        }

        return indent;
    };

    this.checkOutdent = function(state, line, input) {
        return this.$outdent.checkOutdent(line, input);
    };

    this.autoOutdent = function(state, doc, row) {
        this.$outdent.autoOutdent(doc, row);
    };

    this.$id = "ace/mode/clojure";
}).call(Mode.prototype);

exports.Mode = Mode;
});
<|endoftext|>


<commit_before>/** internal
 * class ActionContainer
 *
 * Action container. Parent for [[ArgumentParser]] and [[ArgumentGroup]]
 **/

'use strict';

var _ = require('underscore');
_.str = require('underscore.string');

// Constants
var $$ = require('./const');

//Actions
var ActionHelp = require('./action/help');
var ActionAppend = require('./action/append');
var ActionAppendConstant = require('./action/append/constant');
var ActionCount = require('./action/count');
var ActionStore = require('./action/store');
var ActionStoreConstant = require('./action/store/constant');
var ActionStoreTrue = require('./action/store/true');
var ActionStoreFalse = require('./action/store/false');
var ActionVersion = require('./action/version');
var ActionSubparsers = require('./action/subparsers');

// Errors
var argumentErrorHelper = require('./argument/error');

/**
 * new ActionContainer(options)
 *
 * Action container. Parent for [[ArgumentParser]] and [[ArgumentGroup]]
 *
 * ##### Options:
 *
 * - `description` -- A description of what the program does
 * - `prefixChars`  -- Characters that prefix optional arguments
 * - `argumentDefault`  -- The default value for all arguments
 **/
var ActionContainer = module.exports = function ActionContainer(options) {
  options = options || {};

  this.description = options.description;
  this.argumentDefault = options.argumentDefault;
  this.prefixChars = options.prefixChars || '';

  // ToDo conflict handler

  // set up registries
  this._registries = {};

  // register actions
  this.register('action', null, ActionStore);
  this.register('action', 'store', ActionStore);
  this.register('action', 'storeConst', ActionStoreConstant);
  this.register('action', 'storeTrue', ActionStoreTrue);
  this.register('action', 'storeFalse', ActionStoreFalse);
  this.register('action', 'append', ActionAppend);
  this.register('action', 'appendConst', ActionAppendConstant);
  this.register('action', 'count', ActionCount);
  this.register('action', 'help', ActionHelp);
  this.register('action', 'version', ActionVersion);
  this.register('action', 'parsers', ActionSubparsers);

  // action storage
  this._actions = [];
  this._optionStringActions = {};

  // groups
  this._actionGroups = [];

  // defaults storage
  this._defaults = {};

  // determines whether an "option" looks like a negative number
  this._regexpNegativeNumber = new RegExp('^-\\d+$|^-\\d*\\.\\d+$');

  // whether or not there are any optionals that look like negative
  // numbers -- uses a list so it can be shared and edited
  this._hasNegativeNumberOptionals = [];
};

// Groups must be required, then ActionContainer already defined
var ArgumentGroup = require('./argument/group');

//
// Registration methods
//

/**
 * ActionContainer#register(registryName, value, object) -> Void
 * - registryName (String) : object type action|type
 * - value (string) : keyword
 * - object (Object|Function) : handler
 *
 *  Register handlers
 **/
ActionContainer.prototype.register = function (registryName, value, object) {
  this._registries[registryName] = this._registries[registryName] || {};
  this._registries[registryName][value] = object;
};

ActionContainer.prototype._registryGet = function (registryName, value, defaultValue) {
  defaultValue = defaultValue || null;
  return this._registries[registryName][value] || defaultValue;
};

//
// Namespace default accessor methods
//

/**
 * ActionContainer#setDefaults(options) -> Void
 * - options (object):hash of options see [[Action.new]]
 *
 * Set defaults
 **/
ActionContainer.prototype.setDefaults = function (options) {
  options = options || {};
  for (var property in options) {
    this._defaults[property] = options[property];
  }

  // if these defaults match any existing arguments, replace the previous
  // default on the object with the new one
  this._actions.forEach(function (action) {
    if (action.dest in options) {
      action.defaultValue = options[action.dest];
    }
  });
};

/**
 * ActionContainer#getDefault(dest) -> Mixed
 * - dest (string): action destination
 *
 * Return action default value
 **/
ActionContainer.prototype.getDefault = function (dest) {
  var result;
  result = (_.has(this._defaults, dest)) ? this._defaults[dest] : null;
  this._actions.forEach(function (action) {
    if (action.dest === dest && action.defaultValue !== null) {
      result = action.defaultValue;
    }
  });
  return result;
};
//
// Adding argument actions
//

/**
 * ActionContainer#addArgument(args, options) -> Object
 * - args (Array): array of argument keys
 * - options (Object): action objects see [[Action.new]]
 *
 * #### Examples
 * - addArgument([-f, --foo], {action:'store', defaultValue=1, ...})
 * - addArgument(['bar'], action: 'store', nargs:1, ...})
 **/
ActionContainer.prototype.addArgument = function (args, options) {
  args = args;
  options = options || {};

  if (!_.isArray(args)) {
    throw new TypeError('addArgument first argument should be an array');
  }
  if (!_.isObject(options) || _.isArray(options)) {
    throw new TypeError('addArgument second argument should be a hash');
  }

  // if no positional args are supplied or only one is supplied and
  // it doesn't look like an option string, parse a positional argument
  if (!args || args.length === 1 && this.prefixChars.indexOf(args[0][0]) < 0) {
    if (args && !!options.dest) {
      throw new Error('dest supplied twice for positional argument');
    }
    options = this._getPositional(args, options);

    // otherwise, we're adding an optional argument
  } else {
    options = this._getOptional(args, options);
  }

  // if no default was supplied, use the parser-level default
  if (_.isUndefined(options.defaultValue)) {
    var dest = options.dest;
    if (_.has(this._defaults, dest)) {
      options.defaultValue = this._defaults[dest];
    } else if (!_.isUndefined(this.argumentDefault)) {
      options.defaultValue = this.argumentDefault;
    }
  }

  // create the action object, and add it to the parser
  var ActionClass = this._popActionClass(options);
  if (! _.isFunction(ActionClass)) {
    throw new Error(_.str.sprintf(
          'Unknown action "%(action)s".',
          {action: ActionClass}
    ));
  }
  var action = new ActionClass(options);

  // throw an error if the action type is not callable
  var typeFunction = this._registryGet('type', action.type, action.type);
  if (!_.isFunction(typeFunction)) {
    throw new Error(_.str.sprintf(
          '"%(function)s" is not callable',
          {'function': typeFunction}
    ));
  }

  return this._addAction(action);
};

/**
 * ActionContainer#addArgumentGroup(options) -> ArgumentGroup
 * - options (Object): hash of options see [[ArgumentGroup.new]]
 *
 * Create new arguments groups
 **/
ActionContainer.prototype.addArgumentGroup = function (options) {
  var group = new ArgumentGroup(this, options);
  this._actionGroups.push(group);
  return group;
};

ActionContainer.prototype._addAction = function (action) {
  var self = this;

  // resolve any conflicts
  this._checkConflict(action);

  // add to actions list
  this._actions.push(action);
  action.container = this;

  // index the action by any option strings it has
  action.optionStrings.forEach(function (optionString) {
    self._optionStringActions[optionString] = action;
  });

  // set the flag if any option strings look like negative numbers
  action.optionStrings.forEach(function (optionString) {
    if (optionString.match(self._regexpNegativeNumber)) {
      if (!self._hasNegativeNumberOptionals) {
        self._hasNegativeNumberOptionals.push(true);
      }
    }
  });

  // return the created action
  return action;
};

ActionContainer.prototype._removeAction = function (action) {
  var actionIndex = this._actions.indexOf(action);
  if (actionIndex >= 0) {
    this._actions.splice(actionIndex);
  }
};

ActionContainer.prototype._addContainerActions = function (container) {
  // collect groups by titles
  var titleGroupMap = {};
  this._actionGroups.forEach(function (group) {
    if (titleGroupMap[group.title]) {
      throw new Error(_.str.sprintf('Cannot merge actions - two groups are named "%(group.title)s".', group));
    }
    titleGroupMap[group.title] = group;
  });

  // map each action to its group
  var groupMap = {};

  container._actionGroups.forEach(function (group) {
    // if a group with the title exists, use that, otherwise
    // create a new group matching the container's group
    if (!titleGroupMap[group.title]) {
      titleGroupMap[group.title] = this.addArgumentGroup({
        title: group.title,
        description: group.description
      });
    }

    // map the actions to their new group
    group._groupActions.forEach(function (action) {
      groupMap[action] = titleGroupMap[group.title];
    });
  });

  // add container's mutually exclusive groups
  // NOTE: if add_mutually_exclusive_group ever gains title= and
  // description= then this code will need to be expanded as above
  // ToDo mutually_exclusive_group

  // add all actions to this container or their group
  container._actions.forEach(function (action) {
    if (!!groupMap[action]) {
      groupMap[action]._addAction(action);
    }
    else
    {
      this._addAction(action);
    }
  });
};

ActionContainer.prototype._getPositional = function (dest, options) {
  if (_.isArray(dest)) {
    dest = _.first(dest);
  }
  // make sure required is not specified
  if (options.required) {
    throw new Error('"required" is an invalid argument for positionals.');
  }

  // mark positional arguments as required if at least one is
  // always required
  if (options.nargs !== $$.OPTIONAL && options.nargs !== $$.ZERO_OR_MORE) {
    options.required = true;
  }
  if (options.nargs === $$.ZERO_OR_MORE && options.defaultValue === undefined) {
    options.required = true;
  }

  // return the keyword arguments with no option strings
  options.dest = dest;
  options.optionStrings = [];
  return options;
};

ActionContainer.prototype._getOptional = function (args, options) {
  var prefixChars = this.prefixChars;
  var optionStrings = [];
  var optionStringsLong = [];

  // determine short and long option strings
  args.forEach(function (optionString) {
    // error on strings that don't start with an appropriate prefix
    if (prefixChars.indexOf(optionString[0]) < 0) {
      throw new Error(_.str.sprintf('Invalid option string "%(option)s": must start with a "%(prefix)s".', {
        option: optionString,
        prefix: prefixChars
      }));
    }

    // strings starting with two prefix characters are long options
    optionStrings.push(optionString);
    if (optionString.length > 1 && prefixChars.indexOf(optionString[1]) >= 0) {
      optionStringsLong.push(optionString);
    }
  });

  // infer dest, '--foo-bar' -> 'foo_bar' and '-x' -> 'x'
  var dest = options.dest || null;
  delete options.dest;

  if (!dest) {
    var optionStringDest = optionStringsLong.length ? optionStringsLong[0] :optionStrings[0];
    dest = _.str.strip(optionStringDest, this.prefixChars);

    if (dest.length === 0) {
      throw new Error(_.str.sprintf(
        'dest= is required for options like "%(option)s"',
        {option: optionStrings.join(', ')}
      ));
    }
    dest = dest.replace('-', '_');
  }

  // return the updated keyword arguments
  options.dest = dest;
  options.optionStrings = optionStrings;

  return options;
};

ActionContainer.prototype._popActionClass = function (options, defaultValue) {
  defaultValue = defaultValue || null;

  var action = (options.action || defaultValue);
  delete options.action;

  var actionClass = this._registryGet('action', action, action);
  return actionClass;
};

ActionContainer.prototype._checkConflict = function (action) {
  var optionStringActions = this._optionStringActions;
  var conflictOptionals = [];

  // find all options that conflict with this option
  action.optionStrings.forEach(function (optionString) {
    if (!!optionStringActions[optionString]) {
      conflictOptionals.push([optionString]);
    }
  });

  if (conflictOptionals.length > 0) {
    throw argumentErrorHelper(
      action,
      _.str.sprintf('Conflicting option string(s): %(conflict)s', {
        conflict: conflictOptionals.join(', ')
      })
    );
  }
};
<commit_msg>Small improvement<commit_after>/** internal
 * class ActionContainer
 *
 * Action container. Parent for [[ArgumentParser]] and [[ArgumentGroup]]
 **/

'use strict';

var _ = require('underscore');
_.str = require('underscore.string');

// Constants
var $$ = require('./const');

//Actions
var ActionHelp = require('./action/help');
var ActionAppend = require('./action/append');
var ActionAppendConstant = require('./action/append/constant');
var ActionCount = require('./action/count');
var ActionStore = require('./action/store');
var ActionStoreConstant = require('./action/store/constant');
var ActionStoreTrue = require('./action/store/true');
var ActionStoreFalse = require('./action/store/false');
var ActionVersion = require('./action/version');
var ActionSubparsers = require('./action/subparsers');

// Errors
var argumentErrorHelper = require('./argument/error');

/**
 * new ActionContainer(options)
 *
 * Action container. Parent for [[ArgumentParser]] and [[ArgumentGroup]]
 *
 * ##### Options:
 *
 * - `description` -- A description of what the program does
 * - `prefixChars`  -- Characters that prefix optional arguments
 * - `argumentDefault`  -- The default value for all arguments
 **/
var ActionContainer = module.exports = function ActionContainer(options) {
  options = options || {};

  this.description = options.description;
  this.argumentDefault = options.argumentDefault;
  this.prefixChars = options.prefixChars || '';

  // ToDo conflict handler

  // set up registries
  this._registries = {};

  // register actions
  this.register('action', null, ActionStore);
  this.register('action', 'store', ActionStore);
  this.register('action', 'storeConst', ActionStoreConstant);
  this.register('action', 'storeTrue', ActionStoreTrue);
  this.register('action', 'storeFalse', ActionStoreFalse);
  this.register('action', 'append', ActionAppend);
  this.register('action', 'appendConst', ActionAppendConstant);
  this.register('action', 'count', ActionCount);
  this.register('action', 'help', ActionHelp);
  this.register('action', 'version', ActionVersion);
  this.register('action', 'parsers', ActionSubparsers);

  // action storage
  this._actions = [];
  this._optionStringActions = {};

  // groups
  this._actionGroups = [];

  // defaults storage
  this._defaults = {};

  // determines whether an "option" looks like a negative number
  this._regexpNegativeNumber = new RegExp('^-\\d+$|^-\\d*\\.\\d+$');

  // whether or not there are any optionals that look like negative
  // numbers -- uses a list so it can be shared and edited
  this._hasNegativeNumberOptionals = [];
};

// Groups must be required, then ActionContainer already defined
var ArgumentGroup = require('./argument/group');

//
// Registration methods
//

/**
 * ActionContainer#register(registryName, value, object) -> Void
 * - registryName (String) : object type action|type
 * - value (string) : keyword
 * - object (Object|Function) : handler
 *
 *  Register handlers
 **/
ActionContainer.prototype.register = function (registryName, value, object) {
  this._registries[registryName] = this._registries[registryName] || {};
  this._registries[registryName][value] = object;
};

ActionContainer.prototype._registryGet = function (registryName, value, defaultValue) {
  defaultValue = defaultValue || null;
  return this._registries[registryName][value] || defaultValue;
};

//
// Namespace default accessor methods
//

/**
 * ActionContainer#setDefaults(options) -> Void
 * - options (object):hash of options see [[Action.new]]
 *
 * Set defaults
 **/
ActionContainer.prototype.setDefaults = function (options) {
  options = options || {};
  for (var property in options) {
    this._defaults[property] = options[property];
  }

  // if these defaults match any existing arguments, replace the previous
  // default on the object with the new one
  this._actions.forEach(function (action) {
    if (action.dest in options) {
      action.defaultValue = options[action.dest];
    }
  });
};

/**
 * ActionContainer#getDefault(dest) -> Mixed
 * - dest (string): action destination
 *
 * Return action default value
 **/
ActionContainer.prototype.getDefault = function (dest) {
  var result = (_.has(this._defaults, dest)) ? this._defaults[dest] : null;

  this._actions.forEach(function (action) {
    if (action.dest === dest && _.has(action, 'defaultValue')) {
      result = action.defaultValue;
    }
  });

  return result;
};
//
// Adding argument actions
//

/**
 * ActionContainer#addArgument(args, options) -> Object
 * - args (Array): array of argument keys
 * - options (Object): action objects see [[Action.new]]
 *
 * #### Examples
 * - addArgument([-f, --foo], {action:'store', defaultValue=1, ...})
 * - addArgument(['bar'], action: 'store', nargs:1, ...})
 **/
ActionContainer.prototype.addArgument = function (args, options) {
  args = args;
  options = options || {};

  if (!_.isArray(args)) {
    throw new TypeError('addArgument first argument should be an array');
  }
  if (!_.isObject(options) || _.isArray(options)) {
    throw new TypeError('addArgument second argument should be a hash');
  }

  // if no positional args are supplied or only one is supplied and
  // it doesn't look like an option string, parse a positional argument
  if (!args || args.length === 1 && this.prefixChars.indexOf(args[0][0]) < 0) {
    if (args && !!options.dest) {
      throw new Error('dest supplied twice for positional argument');
    }
    options = this._getPositional(args, options);

    // otherwise, we're adding an optional argument
  } else {
    options = this._getOptional(args, options);
  }

  // if no default was supplied, use the parser-level default
  if (_.isUndefined(options.defaultValue)) {
    var dest = options.dest;
    if (_.has(this._defaults, dest)) {
      options.defaultValue = this._defaults[dest];
    } else if (!_.isUndefined(this.argumentDefault)) {
      options.defaultValue = this.argumentDefault;
    }
  }

  // create the action object, and add it to the parser
  var ActionClass = this._popActionClass(options);
  if (! _.isFunction(ActionClass)) {
    throw new Error(_.str.sprintf(
          'Unknown action "%(action)s".',
          {action: ActionClass}
    ));
  }
  var action = new ActionClass(options);

  // throw an error if the action type is not callable
  var typeFunction = this._registryGet('type', action.type, action.type);
  if (!_.isFunction(typeFunction)) {
    throw new Error(_.str.sprintf(
          '"%(function)s" is not callable',
          {'function': typeFunction}
    ));
  }

  return this._addAction(action);
};

/**
 * ActionContainer#addArgumentGroup(options) -> ArgumentGroup
 * - options (Object): hash of options see [[ArgumentGroup.new]]
 *
 * Create new arguments groups
 **/
ActionContainer.prototype.addArgumentGroup = function (options) {
  var group = new ArgumentGroup(this, options);
  this._actionGroups.push(group);
  return group;
};

ActionContainer.prototype._addAction = function (action) {
  var self = this;

  // resolve any conflicts
  this._checkConflict(action);

  // add to actions list
  this._actions.push(action);
  action.container = this;

  // index the action by any option strings it has
  action.optionStrings.forEach(function (optionString) {
    self._optionStringActions[optionString] = action;
  });

  // set the flag if any option strings look like negative numbers
  action.optionStrings.forEach(function (optionString) {
    if (optionString.match(self._regexpNegativeNumber)) {
      if (!self._hasNegativeNumberOptionals) {
        self._hasNegativeNumberOptionals.push(true);
      }
    }
  });

  // return the created action
  return action;
};

ActionContainer.prototype._removeAction = function (action) {
  var actionIndex = this._actions.indexOf(action);
  if (actionIndex >= 0) {
    this._actions.splice(actionIndex);
  }
};

ActionContainer.prototype._addContainerActions = function (container) {
  // collect groups by titles
  var titleGroupMap = {};
  this._actionGroups.forEach(function (group) {
    if (titleGroupMap[group.title]) {
      throw new Error(_.str.sprintf('Cannot merge actions - two groups are named "%(group.title)s".', group));
    }
    titleGroupMap[group.title] = group;
  });

  // map each action to its group
  var groupMap = {};

  container._actionGroups.forEach(function (group) {
    // if a group with the title exists, use that, otherwise
    // create a new group matching the container's group
    if (!titleGroupMap[group.title]) {
      titleGroupMap[group.title] = this.addArgumentGroup({
        title: group.title,
        description: group.description
      });
    }

    // map the actions to their new group
    group._groupActions.forEach(function (action) {
      groupMap[action] = titleGroupMap[group.title];
    });
  });

  // add container's mutually exclusive groups
  // NOTE: if add_mutually_exclusive_group ever gains title= and
  // description= then this code will need to be expanded as above
  // ToDo mutually_exclusive_group

  // add all actions to this container or their group
  container._actions.forEach(function (action) {
    if (!!groupMap[action]) {
      groupMap[action]._addAction(action);
    }
    else
    {
      this._addAction(action);
    }
  });
};

ActionContainer.prototype._getPositional = function (dest, options) {
  if (_.isArray(dest)) {
    dest = _.first(dest);
  }
  // make sure required is not specified
  if (options.required) {
    throw new Error('"required" is an invalid argument for positionals.');
  }

  // mark positional arguments as required if at least one is
  // always required
  if (options.nargs !== $$.OPTIONAL && options.nargs !== $$.ZERO_OR_MORE) {
    options.required = true;
  }
  if (options.nargs === $$.ZERO_OR_MORE && options.defaultValue === undefined) {
    options.required = true;
  }

  // return the keyword arguments with no option strings
  options.dest = dest;
  options.optionStrings = [];
  return options;
};

ActionContainer.prototype._getOptional = function (args, options) {
  var prefixChars = this.prefixChars;
  var optionStrings = [];
  var optionStringsLong = [];

  // determine short and long option strings
  args.forEach(function (optionString) {
    // error on strings that don't start with an appropriate prefix
    if (prefixChars.indexOf(optionString[0]) < 0) {
      throw new Error(_.str.sprintf('Invalid option string "%(option)s": must start with a "%(prefix)s".', {
        option: optionString,
        prefix: prefixChars
      }));
    }

    // strings starting with two prefix characters are long options
    optionStrings.push(optionString);
    if (optionString.length > 1 && prefixChars.indexOf(optionString[1]) >= 0) {
      optionStringsLong.push(optionString);
    }
  });

  // infer dest, '--foo-bar' -> 'foo_bar' and '-x' -> 'x'
  var dest = options.dest || null;
  delete options.dest;

  if (!dest) {
    var optionStringDest = optionStringsLong.length ? optionStringsLong[0] :optionStrings[0];
    dest = _.str.strip(optionStringDest, this.prefixChars);

    if (dest.length === 0) {
      throw new Error(_.str.sprintf(
        'dest= is required for options like "%(option)s"',
        {option: optionStrings.join(', ')}
      ));
    }
    dest = dest.replace('-', '_');
  }

  // return the updated keyword arguments
  options.dest = dest;
  options.optionStrings = optionStrings;

  return options;
};

ActionContainer.prototype._popActionClass = function (options, defaultValue) {
  defaultValue = defaultValue || null;

  var action = (options.action || defaultValue);
  delete options.action;

  var actionClass = this._registryGet('action', action, action);
  return actionClass;
};

ActionContainer.prototype._checkConflict = function (action) {
  var optionStringActions = this._optionStringActions;
  var conflictOptionals = [];

  // find all options that conflict with this option
  action.optionStrings.forEach(function (optionString) {
    if (!!optionStringActions[optionString]) {
      conflictOptionals.push([optionString]);
    }
  });

  if (conflictOptionals.length > 0) {
    throw argumentErrorHelper(
      action,
      _.str.sprintf('Conflicting option string(s): %(conflict)s', {
        conflict: conflictOptionals.join(', ')
      })
    );
  }
};
<|endoftext|>


<commit_before>var extended = require('./extended')
  , elementary = require('./elementary')
  , traverser = require('traverser')
  , render = require('render')
  , fail = require('./failure').fail

module.exports = {
  every:    every,
  has:      has,
  apply:    apply,
  throws:   throws,
  path:     path,
  noop:     noop,
  atIndex:  atIndex,
  property: property
}
function property (actual, property, value, message){

    function explain(err, template) {
      throw fail(err).explain(
                'property:'+template
              , { actual: actual
                , property: property
                , path: [property]
                , assertion: value
                }
              , message
              )
    }

    if(!actual[property] && value == null)
    //checks that property is defined on actual, even if it is undefined (but not deleted)
      explain(new Error(), 'property: {actual:render}{path:path} must exist')
    //if value is a function, assume it is an assertion... apply it to actual[property]
    if('function' == typeof value) {
      try { value(actual[property]) } catch (err) {
        explain(err, 'property: ({actual:render}){path:path} must pass {assertion:function}')
      }
    } else if (value != null) //else if value is exiting, check it's equal to actual[property]
      try { elementary.equal(actual[property], value) } catch (err) {
        explain(err, 'property: ({actual:render}){path:path} must equal {assertion:function}')
      }

    //if you want to assert a value is null or undefined,
    //use .property(name,it.equal(null|undefined))
  }

function path (actual, expected, assertion, message) {
  var current = actual
    , soFar = []
  if('string' == typeof expected) expected = [expected] //allow a singe key as a string
  if('function' != typeof assertion) message = assertion, assertion = noop

  for ( var i in expected) {
    var key = expected[i]
    current = current[key]
    soFar.push(key)
    if(!(typeof current !== 'undefined')) // check if there actually is a property
      throw fail(new Error()).explain(
          'path: ({actual:render}){soFar:path} must exist, (is undefined), expected path: {path:path}'
        , { actual: actual, soFar: soFar, path:expected}
        , message
        )
  }

  try {
    assertion(current)
  } catch (err) {
    throw fail(err).explain(
        'path: expected ({actual:render}){path:path} to pass {assertion:function}'
      , { actual: actual
        , path: expected
        , assertion: assertion
        , current: current  }
      , message
    )
  }
  return current
}

function apply (actual, assertion, message) {

  //catch and wrap in message
  try {
    assertion (actual)
  } catch (err) {
    throw fail(err).explain('apply: {actual:render} did not pass {assertion:function}', {
      actual: actual,
      expected: assertion,
      assertion: assertion
    }, message)
  }

}

function noop () {}

function throws (actual, assertion, message) {
  if('string' == typeof assertion) message = assertion, assertion= noop

  try {
    actual()
  } catch (err) {
    try {
      return apply (err, assertion, message)
    } catch (f) {
      throw f.explain(
          'throws: {actual} threw an exception that did not pass {assertion:function}'
        , {actual: actual, assertion: assertion}
        , message
      )
    }
  }
  throw fail().explain('throws: {actual} did not throw', {actual: actual, expected: assertion}, message)

}

function every (array, assertion, message){

  try{
    extended.isArray(array)
  }catch(err){
    throw fail(err).explain('every: {actual:render} must be an Array')
  }
  for(var i in array){
    try {
      assertion.call(null,array[i])
    } catch (err) {
      throw fail(err).explain(
          'every: every[{index}] (== {actual:render}) must pass {assertion:function}, \n  ({index} out of {every.length} have passed)'
        , { index: i
          , every: array
          , assertion: assertion
          , actual: array[i]
          }
        , message
        )
    }
  }
}

function has(obj, props, message) {
  var pathTo = []
  //traverser has lots of functions, so it needs a longer stack trace.
  var orig = Error.stackTraceLimit
  Error.stackTraceLimit = orig + 20

  if('object' !== typeof props)
    return extended.equal(obj, props)

  try{
    traverser(props, {leaf:leaf, branch: branch})
  } catch (err){
      err = fail(err).explain(
          'has: ({actual:render}){path:path} must match {expected:render}){path:path}'
        , { actual: obj
          , expected: props
          , path: pathTo
          }
        , message
        )
      Error.stackTraceLimit = orig

      throw err
  }
  function leaf(p){
    pathTo = p.path
    var other = path(obj,p.path)
    if('function' == typeof p.value){
      p.value.call(p.value.parent,other)
    }
    else {
    //since this is the leaf function, it cannot be an object.
    elementary.equal(other,p.value)
    }
  }
  function branch (p){
    pathTo = p.path

    var other = path(obj,p.path)
    if('function' !== typeof p.value)
      extended.complex(other)
    p.each()
  }
}

function atIndex (actual, relativeIndex, assertion, message) {

  var index = relativeIndex < 0 ? actual.length + relativeIndex : relativeIndex
    , minLength = Math.abs(relativeIndex)
    ;

  function explain(err, template) {
    throw fail(err).explain('atIndex: ' + template
      , {actual: actual
        , index: index
        , relativeIndex: relativeIndex
        , minLength: minLength
        , assertion: assertion
        }
      , message)
  }

  if(!(actual))
    explain(new Error(), '{actual:render} must not be null')
  if(!(actual.length))
    explain(new Error(), '{actual:render} must have length property')
  if(!(actual.length > minLength))
    explain(new Error(), '{actual:render}.length (== {actual.length}) must be greater than {minLength}')

  if(!('function' == typeof assertion))
    message = assertion, assertion == noop
  try {
    assertion(actual[index])
  } catch (err) {
    explain(err, '({actual:render})[{index}] must pass {assertion:function}')
  }
}

/*
higher level assections that could be implemented:

  * all(actual, assertions..., message)         //all assertions must pass
  * any(actual, assertions..., message)         //at least one assertion must pass
                                                //if index is negative, take index as length + index

  refactor so that whereever it says 'assertion', if it's not a function : use equal.
*/
<commit_msg>fix indentations<commit_after>var extended = require('./extended')
  , elementary = require('./elementary')
  , traverser = require('traverser')
  , render = require('render')
  , fail = require('./failure').fail

module.exports = {
  every:    every,
  has:      has,
  apply:    apply,
  throws:   throws,
  path:     path,
  noop:     noop,
  atIndex:  atIndex,
  property: property
}
function property (actual, property, value, message){

    function explain(err, template) {
      throw fail(err).explain(
                'property:'+template
              , { actual: actual
                , property: property
                , path: [property]
                , assertion: value
                }
              , message
              )
    }

    if(!actual[property] && value == null)
    //checks that property is defined on actual, even if it is undefined (but not deleted)
      explain(new Error(), 'property: {actual:render}{path:path} must exist')
    //if value is a function, assume it is an assertion... apply it to actual[property]
    if('function' == typeof value) {
      try { value(actual[property]) } catch (err) {
        explain(err, 'property: ({actual:render}){path:path} must pass {assertion:function}')
      }
    } else if (value != null) //else if value is exiting, check it's equal to actual[property]
      try { elementary.equal(actual[property], value) } catch (err) {
        explain(err, 'property: ({actual:render}){path:path} must equal {assertion:function}')
      }

    //if you want to assert a value is null or undefined,
    //use .property(name,it.equal(null|undefined))
  }

function path (actual, expected, assertion, message) {
  var current = actual
    , soFar = []
  if('string' == typeof expected) expected = [expected] //allow a singe key as a string
  if('function' != typeof assertion) message = assertion, assertion = noop

  for ( var i in expected) {
    var key = expected[i]
    current = current[key]
    soFar.push(key)
    if(!(typeof current !== 'undefined')) // check if there actually is a property
      throw fail(new Error()).explain(
          'path: ({actual:render}){soFar:path} must exist, (is undefined), expected path: {path:path}'
        , { actual: actual, soFar: soFar, path:expected}
        , message
        )
  }

  try {
    assertion(current)
  } catch (err) {
    throw fail(err).explain(
        'path: expected ({actual:render}){path:path} to pass {assertion:function}'
      , { actual: actual
        , path: expected
        , assertion: assertion
        , current: current  }
      , message
    )
  }
    return current
}

function apply (actual, assertion, message) {

  //catch and wrap in message
  try {
    assertion (actual)
  } catch (err) {
    throw fail(err).explain('apply: {actual:render} did not pass {assertion:function}', {
      actual: actual,
      expected: assertion,
      assertion: assertion
    }, message)
  }

}

function noop () {}

function throws (actual, assertion, message) {
  if('string' == typeof assertion) message = assertion, assertion= noop

  try {
    actual()
  } catch (err) {
    try {
      return apply (err, assertion, message)
    } catch (f) {
      throw f.explain(
          'throws: {actual} threw an exception that did not pass {assertion:function}'
        , {actual: actual, assertion: assertion}
        , message
      )
    }
  }
  throw fail().explain('throws: {actual} did not throw', {actual: actual, expected: assertion}, message)

}

function every (array, assertion, message){

  try{
    extended.isArray(array)
  }catch(err){
    throw fail(err).explain('every: {actual:render} must be an Array')
  }
  for(var i in array){
    try {
      assertion.call(null,array[i])
    } catch (err) {
      throw fail(err).explain(
          'every: every[{index}] (== {actual:render}) must pass {assertion:function}, \n  ({index} out of {every.length} have passed)'
        , { index: i
          , every: array
          , assertion: assertion
          , actual: array[i]
          }
        , message
        )
    }
  }
}

function has(obj, props, message) {
  var pathTo = []
  //traverser has lots of functions, so it needs a longer stack trace.
  var orig = Error.stackTraceLimit
  Error.stackTraceLimit = orig + 20

  if('object' !== typeof props)
    return extended.equal(obj, props)

  try{
    traverser(props, {leaf:leaf, branch: branch})
  } catch (err){
      err = fail(err).explain(
          'has: ({actual:render}){path:path} must match {expected:render}){path:path}'
        , { actual: obj
          , expected: props
          , path: pathTo
          }
        , message
        )
      Error.stackTraceLimit = orig

      throw err
  }
  function leaf(p){
    pathTo = p.path
    var other = path(obj,p.path)
    if('function' == typeof p.value){
      p.value.call(p.value.parent,other)
    }
    else {
    //since this is the leaf function, it cannot be an object.
    elementary.equal(other,p.value)
    }
  }
  function branch (p){
    pathTo = p.path

    var other = path(obj,p.path)
    if('function' !== typeof p.value)
      extended.complex(other)
    p.each()
  }
}

function atIndex (actual, relativeIndex, assertion, message) {

  var index = relativeIndex < 0 ? actual.length + relativeIndex : relativeIndex
    , minLength = Math.abs(relativeIndex)
    ;

  function explain(err, template) {
    throw fail(err).explain('atIndex: ' + template
      , {actual: actual
        , index: index
        , relativeIndex: relativeIndex
        , minLength: minLength
        , assertion: assertion
        }
      , message)
  }

  if(!(actual))
    explain(new Error(), '{actual:render} must not be null')
  if(!(actual.length))
    explain(new Error(), '{actual:render} must have length property')
  if(!(actual.length > minLength))
    explain(new Error(), '{actual:render}.length (== {actual.length}) must be greater than {minLength}')

  if(!('function' == typeof assertion))
    message = assertion, assertion == noop
  try {
    assertion(actual[index])
  } catch (err) {
    explain(err, '({actual:render})[{index}] must pass {assertion:function}')
  }
}

/*
higher level assections that could be implemented:

  * all(actual, assertions..., message)         //all assertions must pass
  * any(actual, assertions..., message)         //at least one assertion must pass
                                                //if index is negative, take index as length + index

  refactor so that whereever it says 'assertion', if it's not a function : use equal.
*/
<|endoftext|>