<commit_before>'use strict';

var archiveType = require('archive-type');
var Download = require('../');
var nock = require('nock');
var path = require('path');
var fixture = path.join.bind(path, __dirname, 'fixtures');
var server = require('./test-server');
var tar = require('gulp-tar');
var test = require('ava');

test('expose a constructor', function (t) {
	t.plan(1);
	t.assert(typeof Download === 'function');
});

test('add a plugin to the middleware stack', function (t) {
	t.plan(1);

	var download = new Download()
		.use(function () {});

	t.assert(download.ware.fns.length === 1);
});

test('set a file to get', function (t) {
	t.plan(1);

	var download = new Download()
		.get('http://example.com/test.jpg');

	t.assert(download._get[0] === 'http://example.com/test.jpg');
});

test('download a file', function (t) {
	t.plan(4);

	var download = new Download()
		.get('http://foo.com/file.zip');

	var scope = nock('http://foo.com')
		.get('/file.zip')
		.replyWithFile(200, fixture('file.zip'));

	download.run(function (err, files) {
		t.assert(!err);
		t.assert(scope.isDone());
		t.assert(files[0].path === 'file.zip');
		t.assert(archiveType(files[0].contents) === 'zip');
	});
});

test('download a file and rename it', function (t) {
	t.plan(4);

	var download = new Download()
		.get('http://foo.com/file.zip')
		.rename('foobar.zip');

	var scope = nock('http://foo.com')
		.get('/file.zip')
		.replyWithFile(200, fixture('file.zip'));

	download.run(function (err, files) {
		t.assert(!err);
		t.assert(scope.isDone());
		t.assert(path.basename(files[0].path) === 'foobar.zip');
		t.assert(archiveType(files[0].contents) === 'zip');
	});
});

test('download and extract a file', function (t) {
	t.plan(3);

	var download = new Download({ extract: true })
		.get('http://foo.com/file.zip');

	var scope = nock('http://foo.com')
		.get('/file.zip')
		.replyWithFile(200, fixture('file.zip'));

	download.run(function (err, files) {
		t.assert(!err);
		t.assert(scope.isDone());
		t.assert(files[0].path === 'file.txt');
	});
});

test('download and perform task on it', function (t) {
	t.plan(3);

	var download = new Download()
		.get('http://foo.com/file.zip')
		.pipe(tar('file.tar'));

	var scope = nock('http://foo.com')
		.get('/file.zip')
		.replyWithFile(200, fixture('file.zip'));

	download.run(function (err, files) {
		t.assert(!err);
		t.assert(scope.isDone());
		t.assert(path.basename(files[0].path) === 'file.tar');
	});
});

test('proxy google.com', function (t) {
	t.plan(1);

	var srv = server();
	var download = new Download({
		headers: { 'proxy-authorization': 'Foo Bar' },
		proxy: 'http://localhost:9001'
	});

	srv.listen(9001, function () {
		srv.on('http://google.com/', function (req, res) {
			t.assert(req.headers.host === 'google.com');
			t.assert(req.headers['proxy-authorization'] === 'Foo Bar');
			res.end();
		});
	});

	download
		.get('http://google.com/')
		.run(function (err) {
			t.assert(!err, err);
			srv.close();
		});
});

test('error on 404', function (t) {
	t.plan(3);

	var download = new Download()
		.get('http://foo.com/error');
	var scope = nock('http://foo.com')
		.get('/error')
		.reply(404);

	download.run(function (err) {
		t.assert(err.message === 'Couldn\'t connect to http://foo.com/error (404)');
		t.assert(err.code === 404);
		t.assert(scope.isDone());
	});
});
<commit_msg>Fix `t.plan()`<commit_after>'use strict';

var archiveType = require('archive-type');
var Download = require('../');
var nock = require('nock');
var path = require('path');
var fixture = path.join.bind(path, __dirname, 'fixtures');
var server = require('./test-server');
var tar = require('gulp-tar');
var test = require('ava');

test('expose a constructor', function (t) {
	t.plan(1);
	t.assert(typeof Download === 'function');
});

test('add a plugin to the middleware stack', function (t) {
	t.plan(1);

	var download = new Download()
		.use(function () {});

	t.assert(download.ware.fns.length === 1);
});

test('set a file to get', function (t) {
	t.plan(1);

	var download = new Download()
		.get('http://example.com/test.jpg');

	t.assert(download._get[0] === 'http://example.com/test.jpg');
});

test('download a file', function (t) {
	t.plan(4);

	var download = new Download()
		.get('http://foo.com/file.zip');

	var scope = nock('http://foo.com')
		.get('/file.zip')
		.replyWithFile(200, fixture('file.zip'));

	download.run(function (err, files) {
		t.assert(!err);
		t.assert(scope.isDone());
		t.assert(files[0].path === 'file.zip');
		t.assert(archiveType(files[0].contents) === 'zip');
	});
});

test('download a file and rename it', function (t) {
	t.plan(4);

	var download = new Download()
		.get('http://foo.com/file.zip')
		.rename('foobar.zip');

	var scope = nock('http://foo.com')
		.get('/file.zip')
		.replyWithFile(200, fixture('file.zip'));

	download.run(function (err, files) {
		t.assert(!err);
		t.assert(scope.isDone());
		t.assert(path.basename(files[0].path) === 'foobar.zip');
		t.assert(archiveType(files[0].contents) === 'zip');
	});
});

test('download and extract a file', function (t) {
	t.plan(3);

	var download = new Download({ extract: true })
		.get('http://foo.com/file.zip');

	var scope = nock('http://foo.com')
		.get('/file.zip')
		.replyWithFile(200, fixture('file.zip'));

	download.run(function (err, files) {
		t.assert(!err);
		t.assert(scope.isDone());
		t.assert(files[0].path === 'file.txt');
	});
});

test('download and perform task on it', function (t) {
	t.plan(3);

	var download = new Download()
		.get('http://foo.com/file.zip')
		.pipe(tar('file.tar'));

	var scope = nock('http://foo.com')
		.get('/file.zip')
		.replyWithFile(200, fixture('file.zip'));

	download.run(function (err, files) {
		t.assert(!err);
		t.assert(scope.isDone());
		t.assert(path.basename(files[0].path) === 'file.tar');
	});
});

test('proxy google.com', function (t) {
	t.plan(3);

	var srv = server();
	var download = new Download({
		headers: { 'proxy-authorization': 'Foo Bar' },
		proxy: 'http://localhost:9001'
	});

	srv.listen(9001, function () {
		srv.on('http://google.com/', function (req, res) {
			t.assert(req.headers.host === 'google.com');
			t.assert(req.headers['proxy-authorization'] === 'Foo Bar');
			res.end();
		});
	});

	download
		.get('http://google.com/')
		.run(function (err) {
			t.assert(!err, err);
			srv.close();
		});
});

test('error on 404', function (t) {
	t.plan(3);

	var download = new Download()
		.get('http://foo.com/error');
	var scope = nock('http://foo.com')
		.get('/error')
		.reply(404);

	download.run(function (err) {
		t.assert(err.message === 'Couldn\'t connect to http://foo.com/error (404)');
		t.assert(err.code === 404);
		t.assert(scope.isDone());
	});
});
<|endoftext|>


<commit_before>'use strict';

var expect = require('expect.js');
var commandExists = require('..');
var commandExistsSync = commandExists.sync;
var resolve = require('path').resolve;
var isUsingWindows = process.platform == 'win32'

describe('commandExists', function(){
    describe('async - callback', function() {
        it('it should find a command named ls or xcopy', function(done){
            var commandToUse = 'ls'
            if (isUsingWindows) {
                commandToUse = 'xcopy'
            }

            commandExists(commandToUse, function(err, exists) {
                expect(err).to.be(null);
                expect(exists).to.be(true);
                done();
            });
        });

        it('it should not find a command named fdsafdsafdsafdsafdsa', function(done){
            commandExists('fdsafdsafdsafdsafdsa', function(err, exists) {
                expect(err).to.be(null);
                expect(exists).to.be(false);
                done();
            });
        });
    });

    describe('async - promise', function() {
        it('it should find a command named ls or xcopy', function(done){
            var commandToUse = 'ls'
            if (isUsingWindows) {
                commandToUse = 'xcopy'
            }

            commandExists(commandToUse)
            .then(function(command) {
                expect(command).to.be(commandToUse);
                done();
            });
        });

        it('it should not find a command named fdsafdsafdsafdsafdsa', function(done){
            commandExists('fdsafdsafdsafdsafdsa')
            .then(function() {
                // We should not execute this line.
                expect(true).to.be(false);
            })
            .catch(function() {
                done();
            });
        });
    });

    describe('sync', function() {
        it('it should find a command named ls or xcopy', function(){
            var commandToUse = 'ls'
            if (isUsingWindows) {
                commandToUse = 'xcopy'
            }
            expect(commandExistsSync(commandToUse)).to.be(true);
        });

        it('it should not find a command named fdsafdsafdsafdsafdsa', function(){
            expect(commandExistsSync('fdsafdsafdsafdsafdsa')).to.be(false);
        });

        it('it should not find a command named ls or xcopy prefixed with some nonsense', function(){
            var commandToUse = 'fdsafdsa ls'
            if (isUsingWindows) {
                commandToUse = 'fdsafdsaf xcopy'
            }
            expect(commandExistsSync(commandToUse)).to.be(false);
        });

        it('it should not execute some nefarious code', function(){
            expect(commandExistsSync('ls; touch /tmp/foo0')).to.be(false);
        });

        it('it should not execute some nefarious code', function(){
            expect(commandExistsSync('ls touch /tmp/foo0')).to.be(false);
        });
    });

    describe('local file', function() {
        it('it should report false if there is a non-executable file with that name', function(done) {
            var commandToUse = 'test/non-executable-script.js'
            commandExists(commandToUse)
                .then(function(command){
                    // We should not execute this line.
                    expect(true).to.be(false);
                }).catch(function(err){
                    expect(err).to.be(null);
                    done();
                });
        });

        if (!isUsingWindows) {
            it('it should report true if there is an executable file with that name', function(done) {
                var commandToUse = 'test/executable-script.js'
                commandExists(commandToUse)
                    .then(function(command){
                        // We should not execute this line.
                        expect(command).to.be(commandToUse);
                        done();
                    });
            });
        }

        if (isUsingWindows) {
            it('it should report true if there is an executable file with that name', function(done) {
                var commandToUse = 'test\\executable-script.cmd'
                commandExists(commandToUse)
                    .then(function(command){
                        expect(command).to.be(commandToUse);
                        done();
                    });
            });

            it('it should report false if there is a double quotation mark in the file path', function() {
                var commandToUse = 'test\\"executable-script.cmd'
                expect(commandExists.sync(commandToUse)).to.be(false);
            });
        }
    });

    describe('absolute path', function() {
        if (!isUsingWindows) {
            it('it should report true if there is a command with that name in absolute path', function(done) {
                var commandToUse = resolve('./test/executable-script.cmd')
                commandExists(commandToUse)
                .then(function(command){
                    expect(command).to.be(commandToUse);
                    done();
                });
            });

            it('it should report false if there is not a command with that name in absolute path', function() {
                var commandToUse = resolve('./executable-script.cmd')
                expect(commandExists.sync(commandToUse)).to.be(false);
            });
        }
        if (isUsingWindows) {
            it('it should report true if there is a command with that name in absolute path', function(done) {
                var commandToUse = resolve('.\\test\\executable-script.cmd')
                commandExists(commandToUse)
                .then(function(command){
                    expect(command).to.be(commandToUse);
                    done();
                });
            });

            it('it should report false if there is not a command with that name in absolute path', function() {
                var commandToUse = resolve('.\\executable-script.cmd')
                expect(commandExists.sync(commandToUse)).to.be(false);
            });
        }
    });
});
<commit_msg>Fixed non-windows absolute path test<commit_after>'use strict';

var expect = require('expect.js');
var commandExists = require('..');
var commandExistsSync = commandExists.sync;
var resolve = require('path').resolve;
var isUsingWindows = process.platform == 'win32'

describe('commandExists', function(){
    describe('async - callback', function() {
        it('it should find a command named ls or xcopy', function(done){
            var commandToUse = 'ls'
            if (isUsingWindows) {
                commandToUse = 'xcopy'
            }

            commandExists(commandToUse, function(err, exists) {
                expect(err).to.be(null);
                expect(exists).to.be(true);
                done();
            });
        });

        it('it should not find a command named fdsafdsafdsafdsafdsa', function(done){
            commandExists('fdsafdsafdsafdsafdsa', function(err, exists) {
                expect(err).to.be(null);
                expect(exists).to.be(false);
                done();
            });
        });
    });

    describe('async - promise', function() {
        it('it should find a command named ls or xcopy', function(done){
            var commandToUse = 'ls'
            if (isUsingWindows) {
                commandToUse = 'xcopy'
            }

            commandExists(commandToUse)
            .then(function(command) {
                expect(command).to.be(commandToUse);
                done();
            });
        });

        it('it should not find a command named fdsafdsafdsafdsafdsa', function(done){
            commandExists('fdsafdsafdsafdsafdsa')
            .then(function() {
                // We should not execute this line.
                expect(true).to.be(false);
            })
            .catch(function() {
                done();
            });
        });
    });

    describe('sync', function() {
        it('it should find a command named ls or xcopy', function(){
            var commandToUse = 'ls'
            if (isUsingWindows) {
                commandToUse = 'xcopy'
            }
            expect(commandExistsSync(commandToUse)).to.be(true);
        });

        it('it should not find a command named fdsafdsafdsafdsafdsa', function(){
            expect(commandExistsSync('fdsafdsafdsafdsafdsa')).to.be(false);
        });

        it('it should not find a command named ls or xcopy prefixed with some nonsense', function(){
            var commandToUse = 'fdsafdsa ls'
            if (isUsingWindows) {
                commandToUse = 'fdsafdsaf xcopy'
            }
            expect(commandExistsSync(commandToUse)).to.be(false);
        });

        it('it should not execute some nefarious code', function(){
            expect(commandExistsSync('ls; touch /tmp/foo0')).to.be(false);
        });

        it('it should not execute some nefarious code', function(){
            expect(commandExistsSync('ls touch /tmp/foo0')).to.be(false);
        });
    });

    describe('local file', function() {
        it('it should report false if there is a non-executable file with that name', function(done) {
            var commandToUse = 'test/non-executable-script.js'
            commandExists(commandToUse)
                .then(function(command){
                    // We should not execute this line.
                    expect(true).to.be(false);
                }).catch(function(err){
                    expect(err).to.be(null);
                    done();
                });
        });

        if (!isUsingWindows) {
            it('it should report true if there is an executable file with that name', function(done) {
                var commandToUse = 'test/executable-script.js'
                commandExists(commandToUse)
                    .then(function(command){
                        // We should not execute this line.
                        expect(command).to.be(commandToUse);
                        done();
                    });
            });
        }

        if (isUsingWindows) {
            it('it should report true if there is an executable file with that name', function(done) {
                var commandToUse = 'test\\executable-script.cmd'
                commandExists(commandToUse)
                    .then(function(command){
                        expect(command).to.be(commandToUse);
                        done();
                    });
            });

            it('it should report false if there is a double quotation mark in the file path', function() {
                var commandToUse = 'test\\"executable-script.cmd'
                expect(commandExists.sync(commandToUse)).to.be(false);
            });
        }
    });

    describe('absolute path', function() {
        if (!isUsingWindows) {
            it('it should report true if there is a command with that name in absolute path', function(done) {
                var commandToUse = resolve('test/executable-script.js');
                commandExists(commandToUse)
                .then(function(command){
                    expect(command).to.be(commandToUse);
                    done();
                });
            });

            it('it should report false if there is not a command with that name in absolute path', function() {
                var commandToUse = resolve('executable-script.js');
                expect(commandExists.sync(commandToUse)).to.be(false);
            });
        }
        if (isUsingWindows) {
            it('it should report true if there is a command with that name in absolute path', function(done) {
                var commandToUse = resolve('test\\executable-script.cmd');
                commandExists(commandToUse)
                .then(function(command){
                    expect(command).to.be(commandToUse);
                    done();
                });
            });

            it('it should report false if there is not a command with that name in absolute path', function() {
                var commandToUse = resolve('executable-script.cmd');
                expect(commandExists.sync(commandToUse)).to.be(false);
            });
        }
    });
});
<|endoftext|>


<commit_before>(function (window, document) {

var slice = [].slice, dToR = Math.PI / 180, define;

function makeFn(args, fn) {
    return Function(
        args.concat('__canvas,__canvasElement,__turtle,__args,__extras').join(','),
        'return (' + fn + ').apply(this, __extras.concat(__args))'
    );
}

function turtleGraphics(canvas, draw) {
    if (typeof canvas == 'string')
        canvas = document.querySelector(canvas);
    var turtle = {
        x: 0,
        y: 0,
        heading: 0,
        drawing: true
    },  context = canvas.getContext('2d'),
        keys = Object.keys(turtleGraphics.functions),
        values = keys.map(function (key) { return turtleGraphics.functions[key]; }).map(function (fn) {
            var fun = makeFn(keys, fn.fn);
            return function () {
                return fun.apply(this, args(arguments, fn.args));
            };
        }), returnVal;
    function args(args, extras) {
        return values.concat(context, canvas, turtle, [slice.call(args)], [extras]);
    }
    context.moveTo(turtle.x, turtle.y);
    returnVal = makeFn(keys, draw).apply(this, args(arguments, []));
    return returnVal;
}

turtleGraphics.functions = {};
define = turtleGraphics.define = function (name, fn, args) {
    turtleGraphics.functions[name] = {fn: fn, args: (args || []).concat(fn.args || [])};
};

define('penup', function () {
    __turtle.drawing = false;
});

define('pendown', function () {
    __turtle.drawing = true;
});

define('color', function () {
    return __canvas.strokeStyle;
});

define('setcolor', function (color) {
    __canvas.strokeStyle = color;
});

define('width', function () {
    return __canvas.lineWidth;
});

define('setwidth', function (width) {
    __canvas.lineWidth = width;
});

define('pos', function () {
    return {x: __turtle.x, y: __turtle.y};
});

define('setpos', function (x, y) {
    __turtle.x = x;
    __turtle.y = y;
});

define('heading', function () {
    return __turtle.heading;
});

define('setheading', function (val) {
    __turtle.heading = val;
});

define('clearscreen', function () {
    __turtle.x = 0;
    __turtle.y = 0;
    __turtle.heading = 0;
    __turtle.drawing = true;
    __canvas.save();
    __canvas.lineWidth = 1;
    __canvas.strokeStyle = '#000000';
    __canvas.setTransform(1, 0, 0, 1, 0, 0);
    __canvas.clearRect(0, 0, __canvasElement.width, __canvasElement.height);
    __canvas.restore();
});

function newCoord(old, dist, heading, add, trig) {
    return Math.round(add(old, dist * trig(heading * dToR)));
}

function mover(add) {
    function fn(add, newCoord, dist) {
        var newX = newCoord(__turtle.x, dist, __turtle.heading, add, Math.sin),
            newY = newCoord(__turtle.y, dist, __turtle.heading, add, Math.cos);
        __turtle.x = newX;
        __turtle.y = newY;
        __canvas.beginPath();
        if (__turtle.drawing)
            __canvas.lineTo(newX, newY);
        else
            __canvas.moveTo(newX, newY);
        __canvas.stroke();
        __canvas.closePath();
    }
    fn.args = [add, newCoord];
    return fn;
}

function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }

define('forward', mover(add));

define('back', mover(subtract));

function rotator(add) {
    function fn(add, deg) {
        return __turtle.heading = add(__turtle.heading, deg) % 360;
    }
    fn.args = [add];
    return fn;
}

define('left', rotator(subtract));

define('right', rotator(add));

window.turtleGraphics = turtleGraphics;

})(this, document);
<commit_msg>move beginPath and closePath back to the main function<commit_after>(function (window, document) {

var slice = [].slice, dToR = Math.PI / 180, define;

function makeFn(args, fn) {
    return Function(
        args.concat('__canvas,__canvasElement,__turtle,__args,__extras').join(','),
        'return (' + fn + ').apply(this, __extras.concat(__args))'
    );
}

function turtleGraphics(canvas, draw) {
    if (typeof canvas == 'string')
        canvas = document.querySelector(canvas);
    var turtle = {
        x: 0,
        y: 0,
        heading: 0,
        drawing: true
    },  context = canvas.getContext('2d'),
        keys = Object.keys(turtleGraphics.functions),
        values = keys.map(function (key) { return turtleGraphics.functions[key]; }).map(function (fn) {
            var fun = makeFn(keys, fn.fn);
            return function () {
                return fun.apply(this, args(arguments, fn.args));
            };
        }), returnVal;
    function args(args, extras) {
        return values.concat(context, canvas, turtle, [slice.call(args)], [extras]);
    }
    context.beginPath();
    context.moveTo(turtle.x, turtle.y);
    returnVal = makeFn(keys, draw).apply(this, args(arguments, []));
    context.closePath();
    return returnVal;
}

turtleGraphics.functions = {};
define = turtleGraphics.define = function (name, fn, args) {
    turtleGraphics.functions[name] = {fn: fn, args: (args || []).concat(fn.args || [])};
};

define('penup', function () {
    __turtle.drawing = false;
});

define('pendown', function () {
    __turtle.drawing = true;
});

define('color', function () {
    return __canvas.strokeStyle;
});

define('setcolor', function (color) {
    __canvas.strokeStyle = color;
});

define('width', function () {
    return __canvas.lineWidth;
});

define('setwidth', function (width) {
    __canvas.lineWidth = width;
});

define('pos', function () {
    return {x: __turtle.x, y: __turtle.y};
});

define('setpos', function (x, y) {
    __turtle.x = x;
    __turtle.y = y;
});

define('heading', function () {
    return __turtle.heading;
});

define('setheading', function (val) {
    __turtle.heading = val;
});

define('clearscreen', function () {
    __turtle.x = 0;
    __turtle.y = 0;
    __turtle.heading = 0;
    __turtle.drawing = true;
    __canvas.save();
    __canvas.lineWidth = 1;
    __canvas.strokeStyle = '#000000';
    __canvas.setTransform(1, 0, 0, 1, 0, 0);
    __canvas.clearRect(0, 0, __canvasElement.width, __canvasElement.height);
    __canvas.restore();
});

function newCoord(old, dist, heading, add, trig) {
    return Math.round(add(old, dist * trig(heading * dToR)));
}

function mover(add) {
    function fn(add, newCoord, dist) {
        var newX = newCoord(__turtle.x, dist, __turtle.heading, add, Math.sin),
            newY = newCoord(__turtle.y, dist, __turtle.heading, add, Math.cos);
        __turtle.x = newX;
        __turtle.y = newY;
        if (__turtle.drawing)
            __canvas.lineTo(newX, newY);
        else
            __canvas.moveTo(newX, newY);
        __canvas.stroke();
    }
    fn.args = [add, newCoord];
    return fn;
}

function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }

define('forward', mover(add));

define('back', mover(subtract));

function rotator(add) {
    function fn(add, deg) {
        return __turtle.heading = add(__turtle.heading, deg) % 360;
    }
    fn.args = [add];
    return fn;
}

define('left', rotator(subtract));

define('right', rotator(add));

window.turtleGraphics = turtleGraphics;

})(this, document);
<|endoftext|>


<commit_before>/*
* jQuery listnav plugin
*
* Add a slick "letter-based" navigation bar to all of your lists.
* Click a letter to quickly filter the list to items that match that letter.
*
* Dual licensed under the MIT and GPL licenses:
*   http://www.opensource.org/licenses/mit-license.php
*   http://www.gnu.org/licenses/gpl.html
*
* Version 3.0.0 (11/22/2017)
* Author: Eric Steinborn
* Compatibility: jQuery 2.0+
*
*/
(function ($) {
  $.fn.listnav = function (options) {
    var opts = $.extend({}, $.fn.listnav.defaults, options),
        letters = ['_', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '-'],
        firstClick = false,
        clickEventType = '';

    //detect if you are on a touch device easily.
    if (document.ontouchstart !== null) {
      clickEventType = 'click';
    } else {
      // We added 'click' to this becuase on touchscreen computers, when you click with a mouse, it will not fire the click function on a letter, hence disabling the entire plugin functionality. Thanks to my homeboy @clechner77 for pointing this out to me:
      // https://github.com/esteinborn/jquery-listnav/issues/34
      clickEventType = 'touchend click';
    }

    opts.prefixes = $.map(opts.prefixes, function (n) {
      return n.toLowerCase();
    });

    return this.each(function () {
      var $wrapper, $letters, $letterCount, left, width, count,
          id = this.id,
          $list = $(this),
          counts = {},
          allCount = 0, fullCount = 0,
          isAll = true,
          prevLetter = '';

      if ( !$('#' + id + '-nav').length ) {
        $('
').insertBefore($list);
        // Insert the nav if its not been inserted already (preferred method)
        // Legacy method was to add the nav yourself in HTML, I didn't like that requirement
      }

      $wrapper = $('#' + id + '-nav');
      //
 for list and
 for nav wrapper

      function init() {
        $wrapper.append(createLettersHtml());

        $letters = $('.ln-letters', $wrapper).slice(0, 1);

        if ( opts.showCounts ) {
          $letterCount = $('.ln-letter-count', $wrapper).slice(0, 1);
        }

        addClasses();

        addNoMatchLI();

        bindHandlers();

        if (opts.flagDisabled) {
          addDisabledClass();
        }

        // remove nav items we don't need

        if ( !opts.includeAll ) {
          $('.all', $letters).remove();
        }

        if ( !opts.includeNums ) {
          $('._', $letters).remove();
        }

        if ( !opts.includeOther ) {
          $('.-', $letters).remove();
        }

        if ( opts.removeDisabled ) {
          $('.ln-disabled', $letters).remove();
        }

        $(':last', $letters).addClass('ln-last');

        if ( $.cookie && (opts.cookieName !== null) ) {
          var cookieLetter = $.cookie(opts.cookieName);

          if ( cookieLetter !== null && typeof cookieLetter !== "undefined" ) {
            opts.initLetter = cookieLetter;
          }
        }

        // decide what to show first
        // Is there an initLetter set, if so, show that letter first
        if ( opts.initLetter !== '' ) {
          firstClick = true;

          // click the initLetter if there was one
          $('.' + opts.initLetter.toLowerCase(), $letters).slice(0, 1).trigger('click');
        } else {
          // If you want to Hide all options until a user clicks
          if ( opts.initHidden ) {

            addInitHiddenLI();

            $list.children().addClass("listNavHide");

            $list.children('.ln-init-hidden').removeClass('listNavHide');
          }
          // If no init letter is set, and you included All, then show it
          else if ( opts.includeAll ) {
            // make the All link look clicked, but don't actually click it
            $('.all', $letters).addClass('ln-selected');
          } else {
            // All was not included, lets find the first letter with a count and show it
            for ( var i = ((opts.includeNums) ? 0 : 1); i < letters.length; i++) {
              if ( counts[letters[i]] > 0 ) {
                firstClick = true;

                $('.' + letters[i], $letters).slice(0, 1).trigger('click');

                break;
              }
            }
          }
        }
      }

      // position the letter count above the letter links
      function setLetterCountTop() {
        // we're going to need to subtract this from the top value of the wrapper to accomodate changes in font-size in CSS.
        var letterCountHeight = $letterCount.outerHeight();

        $letterCount.css({
          top: $('a:first', $wrapper).slice(0, 1).position().top - letterCountHeight
          // we're going to grab the first anchor in the list
          // We can no longer guarantee that a specific letter will be present
          // since adding the "removeDisabled" option
        });
      }

      // adds a class to each LI that has text content inside of it (ie, inside an , a
, nested DOM nodes, etc)
      function addClasses() {
        var str, spl, $this,
          firstChar = '',
          hasPrefixes = (opts.prefixes.length > 0),
          hasFilterSelector = (opts.filterSelector.length > 0);

        // Iterate over the list and set a class on each one and use that to filter by
        $($list).children().each(function () {
          $this = $(this);

          // I'm assuming you didn't choose a filterSelector, hopefully saving some cycles
          if ( !hasFilterSelector ) {
            //Grab the first text content of the LI, we'll use this to filter by
            str = $.trim($this.text()).toLowerCase();
          } else {
            // You set a filterSelector so lets find it and use that to search by instead
            str = $.trim($this.find(opts.filterSelector).text()).toLowerCase();
          }

          // This will run only if there is something to filter by, skipping over images and non-filterable content.
          if (str !== '') {
            // Apply the non-prefix class to LIs that have prefixed content in them
            if (hasPrefixes) {
              var prefixes = $.map(opts.prefixes, function(value) {
                return value.indexOf(' ') <= 0 ? value + ' ' : value;
              });
              var matches = $.grep(prefixes, function(value) {
                return str.indexOf(value) === 0;
              });
              if (matches.length > 0) {
                var afterMatch = str.toLowerCase().split(matches[0])[1];

                if(afterMatch !== null) {
                  firstChar = $.trim(afterMatch).charAt(0);
                } else {
                  firstChar = str.charAt(0);
                }
                addLetterClass(firstChar, $this, true);
                return;
              }
            }
            // Find the first letter in the LI, including prefixes
            firstChar = str.charAt(0);

            // Doesn't send true to function, which will ++ the All count on prefixed items
            addLetterClass(firstChar, $this);
          }
        });
      }

      // Add the appropriate letter class to the current element
      function addLetterClass(firstChar, $el, isPrefix) {
        if ( /\W/.test(firstChar) ) {
          firstChar = '-'; // not A-Z, a-z or 0-9, so considered "other"
        }

        if ( !isNaN(firstChar) ) {
          firstChar = '_'; // use '_' if the first char is a number
        }

        $el.addClass('ln-' + firstChar);

        if ( counts[firstChar] === undefined ) {
          counts[firstChar] = 0;
        }

        counts[firstChar]++;

        if (!isPrefix) {
          allCount++;
        }

      }

      function addDisabledClass() {
        for ( var i = 0; i < letters.length; i++ ) {
          if ( counts[letters[i]] === undefined ) {
            $('.' + letters[i], $letters).addClass('ln-disabled');
          }
        }
      }

      function addNoMatchLI() {
        $list.append('
' + opts.noMatchText + '
');
      }

      function addInitHiddenLI() {
        $list.append('
' + opts.initHiddenText + '
');
      }

      function getLetterCount(el) {
        if ($(el).hasClass('all')) {
          if (opts.dontCount) {
            fullCount = allCount - $list.find(opts.dontCount).length;
          } else {
            fullCount = allCount;
          }

          return fullCount;
        } else {
          el = '.ln-' + $(el).attr('class').split(' ')[0];

          if (opts.dontCount) {
            count = $list.find(el).not(opts.dontCount).length;
          } else {
            count = $list.find(el).length;
          }

          return (count !== undefined) ? count : 0; // some letters may not have a count in the hash
        }
      }

      function bindHandlers() {
        if (opts.showCounts) {
          // sets the top position of the count div in case something above it on the page has resized
          $wrapper.mouseover(function () {
            setLetterCountTop();
          });

          //shows the count above the letter
          //
          $('.ln-letters a', $wrapper).mouseover(function () {
            left = $(this).position().left;
            width = ($(this).outerWidth()) + 'px';
            count = getLetterCount(this);

            $letterCount.css({
              left: left,
              width: width
            }).text(count).addClass("letterCountShow").removeClass("listNavHide"); // set left position and width of letter count, set count text and show it
          }).mouseout(function () { // mouseout for each letter: hide the count
            $letterCount.addClass("listNavHide").removeClass("letterCountShow");
          });
        }

        // click handler for letters: shows/hides relevant LI's
        //
        $('a', $letters).on(clickEventType, function (e) {
          e.preventDefault();
          var $this = $(this),
            letter = $this.attr('class').split(' ')[0],
            noMatches = $list.children('.ln-no-match');

          if ( opts.initHidden ) {
            $list.children('.ln-init-hidden').remove();
          }

          if ( prevLetter !== letter ) {
          // Only to run this once for each click, won't double up if they clicked the same letter
          // Won't hinder firstRun
            $('a.ln-selected', $letters).removeClass('ln-selected');

            if ( letter === 'all' ) {
              // If ALL button is clicked:
              $list.children().addClass("listNavShow").removeClass("listNavHide"); // Show ALL

              noMatches.addClass("listNavHide").removeClass("listNavShow"); // Hide the list item for no matches

              isAll = true; // set this to quickly check later
            } else {
              // If you didn't click ALL
              if ( isAll ) {
                // since you clicked ALL last time:
                $list.children().addClass("listNavHide").removeClass("listNavShow");

                isAll = false;
              } else if (prevLetter !== '') {
                $list.children('.ln-' + prevLetter).addClass("listNavHide").removeClass("listNavShow");
              }

              var count = getLetterCount(this);

              if (count > 0) {
                $list.children('.ln-' + letter).addClass("listNavShow").removeClass("listNavHide");
                noMatches.addClass("listNavHide").removeClass("listNavShow"); // in case it's showing
              } else {
                noMatches.addClass("listNavShow").removeClass("listNavHide");
              }
            }

            prevLetter = letter;

            if ($.cookie && (opts.cookieName !== null)) {
              $.cookie(opts.cookieName, letter, {
                expires: 999
              });
            }

            $this.addClass('ln-selected');

            $this.blur();

            if (!firstClick && (opts.onClick !== null)) {
              opts.onClick(letter);
            } else {
              firstClick = false; //return false;
            }
          } // end if prevLetter !== letter
        }); // end click()
      } // end BindHandlers()

      // creates the HTML for the letter links
      //
      function createLettersHtml() {
        var html = [];
        for (var i = 1; i < letters.length; i++) {
          if (html.length === 0) {
            html.push(''+ opts.allText + '0-9');
          }
          html.push('' + ((letters[i] === '-') ? '...' : letters[i].toUpperCase()) + '');
        }
        return '
' + html.join('') + '
' + ((opts.showCounts) ? '
0
' : '');
        // Remove inline styles, replace with css class
        // Element will be repositioned when made visible
      }
      init();
    });
  };

  $.fn.listnav.defaults = {
    initHidden: false,
    initHiddenText: 'Tap a letter above to view matching items',
    initLetter: '',
    includeAll: true,
    allText: 'All',
    includeOther: false,
    includeNums: true,
    flagDisabled: true,
    removeDisabled: false,
    noMatchText: 'No matching entries',
    showCounts: true,
    dontCount: '',
    cookieName: null,
    onClick: null,
    prefixes: [],
    filterSelector: ''
  };
})(jQuery);
<commit_msg>small update to trigger push<commit_after>/*
* jQuery listnav plugin
*
* Add a slick "letter-based" navigation bar to all of your lists.
* Click a letter to quickly filter the list to items that match that letter.
*
* Dual licensed under the MIT and GPL licenses:
*   http://www.opensource.org/licenses/mit-license.php
*   http://www.gnu.org/licenses/gpl.html
*
* Version 3.0.0 (11/22/2017)
* Author: Eric Steinborn
* Compatibility: jQuery 2.0+
*
*/
(function ($) {
  $.fn.listnav = function (options) {
    var opts = $.extend({}, $.fn.listnav.defaults, options),
        letters = ['_', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '-'],
        firstClick = false,
        clickEventType = '';

    //detect if you are on a touch device easily.
    if (document.ontouchstart !== null) {
      clickEventType = 'click';
    } else {
      // We added 'click' to this becuase on touchscreen computers, when you click with a mouse, it will not fire the click function on a letter, hence disabling the entire plugin functionality. Thanks to my homeboy @clechner77 for pointing this out to me:
      // https://github.com/esteinborn/jquery-listnav/issues/34
      clickEventType = 'touchend click';
    }

    opts.prefixes = $.map(opts.prefixes, function (n) {
      return n.toLowerCase();
    });

    return this.each(function () {
      var $wrapper, $letters, $letterCount, left, width, count,
          id = this.id,
          $list = $(this),
          counts = {},
          allCount = 0, fullCount = 0,
          isAll = true,
          prevLetter = '';

      if ( !$('#' + id + '-nav').length ) {
        $('
').insertBefore($list);
        // Insert the nav if its not been inserted already (preferred method)
        // Legacy method was to add the nav yourself in HTML, I didn't like that requirement
      }

      $wrapper = $('#' + id + '-nav');
      //
 for list and
 for nav wrapper

      function init() {
        $wrapper.append(createLettersHtml());

        $letters = $('.ln-letters', $wrapper).slice(0, 1);

        if ( opts.showCounts ) {
          $letterCount = $('.ln-letter-count', $wrapper).slice(0, 1);
        }

        addClasses();

        addNoMatchLI();

        bindHandlers();

        if (opts.flagDisabled) {
          addDisabledClass();
        }

        // remove nav items we don't need

        if ( !opts.includeAll ) {
          $('.all', $letters).remove();
        }

        if ( !opts.includeNums ) {
          $('._', $letters).remove();
        }

        if ( !opts.includeOther ) {
          $('.-', $letters).remove();
        }

        if ( opts.removeDisabled ) {
          $('.ln-disabled', $letters).remove();
        }

        $(':last', $letters).addClass('ln-last');

        if ( $.cookie && (opts.cookieName !== null) ) {
          var cookieLetter = $.cookie(opts.cookieName);

          if ( cookieLetter !== null && typeof cookieLetter !== "undefined" ) {
            opts.initLetter = cookieLetter;
          }
        }

        // decide what to show first
        // Is there an initLetter set, if so, show that letter first
        if ( opts.initLetter !== '' ) {
          firstClick = true;

          // click the initLetter if there was one
          $('.' + opts.initLetter.toLowerCase(), $letters).slice(0, 1).trigger('click');
        } else {
          // If you want to Hide all options until a user clicks
          if ( opts.initHidden ) {

            addInitHiddenLI();

            $list.children().addClass("listNavHide");

            $list.children('.ln-init-hidden').removeClass('listNavHide');
          }
          // If no init letter is set, and you included All, then show it
          else if ( opts.includeAll ) {
            // make the All link look clicked, but don't actually click it
            $('.all', $letters).addClass('ln-selected');
          } else {
            // All was not included, lets find the first letter with a count and show it
            for ( var i = ((opts.includeNums) ? 0 : 1); i < letters.length; i++) {
              if ( counts[letters[i]] > 0 ) {
                firstClick = true;

                $('.' + letters[i], $letters).slice(0, 1).trigger('click');

                break;
              }
            }
          }
        }
      }

      // position the letter count above the letter links
      function setLetterCountTop() {
        // we're going to need to subtract this from the top value of the wrapper to accomodate changes in font-size in CSS.
        var letterCountHeight = $letterCount.outerHeight();

        $letterCount.css({
          top: $('a:first', $wrapper).slice(0, 1).position().top - letterCountHeight
          // we're going to grab the first anchor in the list
          // We can no longer guarantee that a specific letter will be present
          // since adding the "removeDisabled" option
        });
      }

      // adds a class to each LI that has text content inside of it (ie, inside an , a
, nested DOM nodes, etc)
      function addClasses() {
        var str, spl, $this,
          firstChar = '',
          hasPrefixes = (opts.prefixes.length > 0),
          hasFilterSelector = (opts.filterSelector.length > 0);

        // Iterate over the list and set a class on each one and use that to filter by
        $($list).children().each(function () {
          $this = $(this);

          // I'm assuming you didn't choose a filterSelector, hopefully saving some cycles
          if ( !hasFilterSelector ) {
            //Grab the first text content of the LI, we'll use this to filter by
            str = $.trim($this.text()).toLowerCase();
          } else {
            // You set a filterSelector so lets find it and use that to search by instead
            str = $.trim($this.find(opts.filterSelector).text()).toLowerCase();
          }

          // This will run only if there is something to filter by, skipping over images and non-filterable content.
          if (str !== '') {
            // Apply the non-prefix class to LIs that have prefixed content in them
            if (hasPrefixes) {
              var prefixes = $.map(opts.prefixes, function(value) {
                return value.indexOf(' ') <= 0 ? value + ' ' : value;
              });
              var matches = $.grep(prefixes, function(value) {
                return str.indexOf(value) === 0;
              });
              if (matches.length > 0) {
                var afterMatch = str.toLowerCase().split(matches[0])[1];

                if(afterMatch !== null) {
                  firstChar = $.trim(afterMatch).charAt(0);
                } else {
                  firstChar = str.charAt(0);
                }
                addLetterClass(firstChar, $this, true);
                return;
              }
            }
            // Find the first letter in the LI, including prefixes
            firstChar = str.charAt(0);

            // Doesn't send true to function, which will ++ the All count on prefixed items
            addLetterClass(firstChar, $this);
          }
        });
      }

      // Add the appropriate letter class to the current element
      function addLetterClass(firstChar, $el, isPrefix) {
        if ( /\W/.test(firstChar) ) {
          firstChar = '-'; // not A-Z, a-z or 0-9, so considered "other"
        }

        if ( !isNaN(firstChar) ) {
          firstChar = '_'; // use '_' if the first char is a number
        }

        $el.addClass('ln-' + firstChar);

        if ( counts[firstChar] === undefined ) {
          counts[firstChar] = 0;
        }

        counts[firstChar]++;

        if (!isPrefix) {
          allCount++;
        }
      }

      function addDisabledClass() {
        for ( var i = 0; i < letters.length; i++ ) {
          if ( counts[letters[i]] === undefined ) {
            $('.' + letters[i], $letters).addClass('ln-disabled');
          }
        }
      }

      function addNoMatchLI() {
        $list.append('
' + opts.noMatchText + '
');
      }

      function addInitHiddenLI() {
        $list.append('
' + opts.initHiddenText + '
');
      }

      function getLetterCount(el) {
        if ($(el).hasClass('all')) {
          if (opts.dontCount) {
            fullCount = allCount - $list.find(opts.dontCount).length;
          } else {
            fullCount = allCount;
          }

          return fullCount;
        } else {
          el = '.ln-' + $(el).attr('class').split(' ')[0];

          if (opts.dontCount) {
            count = $list.find(el).not(opts.dontCount).length;
          } else {
            count = $list.find(el).length;
          }

          return (count !== undefined) ? count : 0; // some letters may not have a count in the hash
        }
      }

      function bindHandlers() {
        if (opts.showCounts) {
          // sets the top position of the count div in case something above it on the page has resized
          $wrapper.mouseover(function () {
            setLetterCountTop();
          });

          //shows the count above the letter
          //
          $('.ln-letters a', $wrapper).mouseover(function () {
            left = $(this).position().left;
            width = ($(this).outerWidth()) + 'px';
            count = getLetterCount(this);

            $letterCount.css({
              left: left,
              width: width
            }).text(count).addClass("letterCountShow").removeClass("listNavHide"); // set left position and width of letter count, set count text and show it
          }).mouseout(function () { // mouseout for each letter: hide the count
            $letterCount.addClass("listNavHide").removeClass("letterCountShow");
          });
        }

        // click handler for letters: shows/hides relevant LI's
        //
        $('a', $letters).on(clickEventType, function (e) {
          e.preventDefault();
          var $this = $(this),
            letter = $this.attr('class').split(' ')[0],
            noMatches = $list.children('.ln-no-match');

          if ( opts.initHidden ) {
            $list.children('.ln-init-hidden').remove();
          }

          if ( prevLetter !== letter ) {
          // Only to run this once for each click, won't double up if they clicked the same letter
          // Won't hinder firstRun
            $('a.ln-selected', $letters).removeClass('ln-selected');

            if ( letter === 'all' ) {
              // If ALL button is clicked:
              $list.children().addClass("listNavShow").removeClass("listNavHide"); // Show ALL

              noMatches.addClass("listNavHide").removeClass("listNavShow"); // Hide the list item for no matches

              isAll = true; // set this to quickly check later
            } else {
              // If you didn't click ALL
              if ( isAll ) {
                // since you clicked ALL last time:
                $list.children().addClass("listNavHide").removeClass("listNavShow");

                isAll = false;
              } else if (prevLetter !== '') {
                $list.children('.ln-' + prevLetter).addClass("listNavHide").removeClass("listNavShow");
              }

              var count = getLetterCount(this);

              if (count > 0) {
                $list.children('.ln-' + letter).addClass("listNavShow").removeClass("listNavHide");
                noMatches.addClass("listNavHide").removeClass("listNavShow"); // in case it's showing
              } else {
                noMatches.addClass("listNavShow").removeClass("listNavHide");
              }
            }

            prevLetter = letter;

            if ($.cookie && (opts.cookieName !== null)) {
              $.cookie(opts.cookieName, letter, {
                expires: 999
              });
            }

            $this.addClass('ln-selected');

            $this.blur();

            if (!firstClick && (opts.onClick !== null)) {
              opts.onClick(letter);
            } else {
              firstClick = false; //return false;
            }
          } // end if prevLetter !== letter
        }); // end click()
      } // end BindHandlers()

      // creates the HTML for the letter links
      //
      function createLettersHtml() {
        var html = [];
        for (var i = 1; i < letters.length; i++) {
          if (html.length === 0) {
            html.push(''+ opts.allText + '0-9');
          }
          html.push('' + ((letters[i] === '-') ? '...' : letters[i].toUpperCase()) + '');
        }
        return '
' + html.join('') + '
' + ((opts.showCounts) ? '
0
' : '');
        // Remove inline styles, replace with css class
        // Element will be repositioned when made visible
      }
      init();
    });
  };

  $.fn.listnav.defaults = {
    initHidden: false,
    initHiddenText: 'Tap a letter above to view matching items',
    initLetter: '',
    includeAll: true,
    allText: 'All',
    includeOther: false,
    includeNums: true,
    flagDisabled: true,
    removeDisabled: false,
    noMatchText: 'No matching entries',
    showCounts: true,
    dontCount: '',
    cookieName: null,
    onClick: null,
    prefixes: [],
    filterSelector: ''
  };
})(jQuery);
<|endoftext|>


<commit_before>'use strict';

var chai = require('chai');
chai.use(require('chai-shallow-deep-equal'));
var assert = chai.assert;
var equal = chai.assert.equal;
var dEqual = chai.assert.deepEqual;
var sdEqual = chai.assert.shallowDeepEqual;
function treeEquals(x, y) {return sdEqual(x.tree, y);}

var clubExpr = require('../index');

var parse = clubExpr.parse;

describe('#parse', function() {
    it('should fail if the source contains bad characters', function() {
        assert.throw(function () {parse('"');},
                     Error, "Invalid char: \"");
    });

    it('should fail if the source is empty', function() {
        assert.throw(function () {parse('');},
                     Error, "Empty expr");
    });

    it('should fail if a ( is the only char', function() {
        assert.throw(function () {parse('(');},
                     Error, "Missing cmd");
    });

    it('should parse an expression with no arg', function() {
        treeEquals(parse('(a)'), ['a']);
    });

    it('should parse an expression with no arg (longer command)', function() {
        treeEquals(parse('(ab)'), ['ab']);
    });

    it('should parse a single expression with one arg', function() {
        treeEquals(parse('(a b)'), ['a', 'b']);
    });

    it('should parse a single expression with two args', function() {
        treeEquals(parse('(a b c)'), ['a', 'b', 'c']);
    });

    it('should parse a nested expression', function() {
        treeEquals(parse('(a (b c) d)'), ['a', ['b', 'c'], 'd']);
    });

    it('should parse an expression with creative whitespace', function() {
        treeEquals(parse(' ( a \n b \t c ) '), ['a', 'b', 'c']);
    });

    it('should fail if the starting ( is missing (1 token)', function() {
        assert.throw(function () {parse('a');},
                     Error, "Missing starting (");
    });

    it('should fail if the starting ( is missing (2 tokens)', function() {
        assert.throw(function () {parse('a b');},
                     Error, "Missing starting (");
    });

    it('should fail if a double ( is found', function() {
        assert.throw(function () {parse('((');},
                     Error, "Double (");
    });

    it('should warn us if a ( is the last significant char', function() {
        var result = parse('(a b (');
        dEqual(result.warnings, ["Missing cmd", "Missing )"]);
        assert.deepEqual(result.tree, ['a', 'b']);
    });

    it('should warn us if a ) is missing', function() {
        var result = parse('(a b');
        dEqual(result.warnings, ["Missing )"]);
        dEqual(result.tree, ['a', 'b']);
    });

    it('should warn us if a ) is missing in a nested expression', function() {
        var result = parse('(a (b 2)');
        dEqual(result.warnings, ["Missing )"]);
        dEqual(result.tree, ['a', ['b', '2']]);
    });

    it('should warn us if the expr is already closed', function() {
        var result = parse('(a b) c');
        dEqual(result.warnings, ["Already closed"]);
        dEqual(result.tree, ['a', 'b']);
    });

    it('should warn us if a ) is trailing', function() {
        var result = parse('(a b))');
        dEqual(result.warnings, ["Already closed"]);
        dEqual(result.tree, ['a', 'b']);
    });

    it('should warn us if a ( is trailing', function() {
        var result = parse('(a b) (');
        dEqual(result.warnings, ["Already closed"]);
        dEqual(result.tree, ['a', 'b']);
    });

    it('should fail if a command is missing', function() {
        assert.throw(function () {parse('()');},
                     Error, "Missing cmd");
    });
});

var natureFromLisp = clubExpr.natureFromLisp;

describe('#natureFromLisp', function() {
    it('should fail silently for some malformed expressions', function() {
        var expressions = ['', 'b', ')', '(S'];
        expressions.forEach(function (src) {
            equal(natureFromLisp(src), '');
        });
    });

    it('should fail silently for some wellformed expressions', function() {
        var expressions = ['(S 1 1)'];
        expressions.forEach(function (src) {
            equal(natureFromLisp(src), '');
        });
    });

    it('should return the correct nature of some expressions', function() {
        var exprsAndNatures = [
            ['Somme 1 1)', 'Somme'],
            [' Somme 1 1)', 'Somme'],
            ['  Somme 1 1)', 'Somme'],
            ['Somme(', 'Somme'],
            ['(Somme 1 1)', 'Somme'],
            [' (Somme 1 1)', 'Somme'],
            ['( Somme 1 1)', 'Somme'],
            ['(Somme(', 'Somme'],
            [' ( Somme 1 1)', 'Somme']
        ];
        exprsAndNatures.forEach(function (exprAndNature) {
            var src =    exprAndNature[0];
            var nature = exprAndNature[1];
            equal(natureFromLisp(src), nature);
        });
    });
});

var renderExprAsLisp = clubExpr.renderExprAsLisp;

describe('#renderExprAsLisp', function() {
    it('should render a single expression with one arg', function() {
        equal(renderExprAsLisp(['a', 'b']), '(a b)');
    });

    it('should render a single expression with two args', function() {
        equal(renderExprAsLisp(['a', 'b', 'c']), '(a b c)');
    });

    it('should render a nested expression', function() {
        equal(renderExprAsLisp(['a', ['b', 'c'], 'd']), '(a (b c) d)');
    });
});

var expressions = clubExpr.expressions;

describe('#renderingParsingRoundTrip', function() {
    it('should round trip on all official expressions', function() {
        expressions.forEach(function (exprObj) {
            var expr = exprObj.expr;
            // Here [ 'Somme', '1', '2' ] equals [ 'Somme', 1, 2 ]
            treeEquals(parse(renderExprAsLisp(expr)), expr);
        });
    });
});

var renderExprAsLaTeX = clubExpr.renderExprAsLaTeX;
var exprsRenderedAsLaTeX = [
            '1+2',
            'a+1',
            '1+a',
            'a+b',
            '1-2',
            'a-1',
            '1-a',
            'a-b',
            '1 \\times 2',
            '1 a',
            'a \\times 1',
            'a b',
            'a \\times a',
            '\\frac{1}{2}',
            '\\frac{a}{1}',
            '\\frac{1}{a}',
            '\\frac{a}{b}',
            '{1}^{2}',
            '{a}^{2}',
            '{a}^{1}',
            '\\frac{1}{a}',
            '-a',
            '1 \\left(a+2\\right)',
            '1 \\left(a-2\\right)',
            '1+2 a',
            '1+\\left(-2\\right)',
            '1+\\left(-a\\right)',
            '1-\\left(-2\\right)',
            '1-\\left(-a\\right)',
            '1 a-2',
            '1+\\frac{a}{2}',
            '-a-1',
            '-\\left(a-1\\right)',
            '1-\\left(2+a\\right)',
            'a-1+2',
            '\\frac{a+b}{1}',
            '\\frac{1}{a+1}',
            '{a}^{b+c}',
            '{a}^{{b}^{c}}',
            '{\\left({a}^{b}\\right)}^{c}',
            '\\frac{1 a}{2}',
            '-1 a',
            '-\\frac{1}{a}',
            '-\\frac{a}{b}',
            '-{a}^{2}',
            '{\\left(-a\\right)}^{2}',
            '{\\left(1+a\\right)}^{2}',
            '{\\left(a+b\\right)}^{2}',
            '{\\left(a-b\\right)}^{2}',
            '1+{a}^{2}',
            '1 {a}^{2}',
            '{\\left(1 a\\right)}^{2}',
            '\\frac{{a}^{2}}{1}',
            '{\\left(\\frac{a}{1}\\right)}^{2}',
            'a \\times 1 a',
            '{\\left(\\frac{1}{a}\\right)}^{2}',
            '\\frac{1}{{a}^{2}}',
            '\\sqrt{{a}^{2}}',
            '{a}^{2}+{b}^{2}',
            '\\sqrt{{a}^{2}+{b}^{2}}',
            'a \\left(1+2 a\\right)',
            '1 a-\\left(2+a\\right)',
            '1 a-2 a',
            '1 \\left(a-2\\right)-3',
            '1-2 \\left(a+3\\right)',
            '1 a-a+2',
            'a-\\frac{a}{1}+2',
            '{a}^{2}-a+1',
            '{\\left(a+1\\right)}^{2}-2',
            '\\left(a+1\\right) \\left(a-2\\right)',
            '-{a}^{2}+1',
            '1 {a}^{2}+a',
            '-a-a+1',
            'a-\\left(-a\\right)+1',
            '{a}^{2}-a+1',
            'a-1 \\left(a-2\\right)+3',
            '\\frac{a+1}{a-2}',
            '1+\\frac{2}{a+3}',
            '\\sqrt{{\\left(1-2\\right)}^{2}+{\\left(3+4\\right)}^{2}}',
            '1 \\left(2+3\\right) \\left(4+5\\right)',
            '{a}^{2}+1 a+2',
            '1 {\\left(a+2\\right)}^{2}-3',
            '1 \\left(2+3\\right) \\left(4-5\\right)',
            '{\\left(a+1\\right)}^{2}-{a}^{2}',
            '1 {a}^{2}+2 a+3',
            '\\frac{1 a+2}{3 a+4}',
            '1+\\frac{2}{3 a+4}'];

describe('#renderExprAsLaTeX', function() {
    it('should render the official expressions correctly', function() {
        expressions.forEach(function (exprObj, idx) {
            var expr = exprObj.expr;
            equal(renderExprAsLaTeX(expr).latex, exprsRenderedAsLaTeX[idx]);
        });
    });
});

var renderLispAsLaTeX = clubExpr.renderLispAsLaTeX;

describe('#renderLispAsLaTeX', function() {
    it('should render a single expression with one arg', function() {
        var result = renderLispAsLaTeX('(Racine b)');
        equal(result.latex, '\\sqrt{b}');
        dEqual(result.warnings, []);
    });

    it('should render a single expression with two args', function() {
        var result = renderLispAsLaTeX('(Somme a b)');
        equal(result.latex, 'a+b');
        dEqual(result.warnings, []);
    });

    it('should render a nested expression', function() {
        var result = renderLispAsLaTeX('(Somme a (Produit b c))');
        equal(result.latex, 'a+b c');
        dEqual(result.warnings, []);
    });

    it('should render an expression with a greek letter', function() {
        var result = renderLispAsLaTeX('(Produit 2 pi)');
        equal(result.latex, '2 \\pi');
        dEqual(result.warnings, []);
    });

    it('should render an expression and warn us if a ) is missing', function() {
        var result = renderLispAsLaTeX('(Somme 1 2');
        equal(result.latex, '1+2');
        dEqual(result.warnings, ["Missing )"]);
    });

    it('should warn us if the command is unknown', function() {
        var result = renderLispAsLaTeX('(Unk a b)');
        equal(result.latex, '?');
        dEqual(result.warnings, ["Unknown cmd: Unk"]);
    });

    it('should render an expr with an incomplete cmd name and warn us', function() {
        var result = renderLispAsLaTeX('(Somm)');
        equal(result.latex, '?');
        dEqual(result.warnings, ["Unknown cmd: Somm"]);
    });

    it('should render a Somme with no arg and warn us', function() {
        var result = renderLispAsLaTeX('(Somme)');
        equal(result.latex, '?+?');
        dEqual(result.warnings, ["Somme: nb args < 2"]);
    });

    it('should render a Somme with one arg and warn us', function() {
        var result = renderLispAsLaTeX('(Somme 1)');
        equal(result.latex, '1+?');
        dEqual(result.warnings, ["Somme: nb args < 2"]);
    });

    it('should warn us if too few args for Diff', function() {
        var result = renderLispAsLaTeX('(Diff)');
        equal(result.latex, '?-?');
        dEqual(result.warnings, ["Diff: nb args < 2"]);
    });

    it('should warn us if too few args for Diff', function() {
        var result = renderLispAsLaTeX('(Diff 1)');
        equal(result.latex, '1-?');
        dEqual(result.warnings, ["Diff: nb args < 2"]);
    });

    it('should warn us if too many args for Diff', function() {
        var result = renderLispAsLaTeX('(Diff a b c)');
        equal(result.latex, 'a-b\\bigotimes');
        dEqual(result.warnings, ["Diff: nb args > 2"]);
    });

    it('should warn us if too few args for Produit', function() {
        var result = renderLispAsLaTeX('(Produit)');
        equal(result.latex, '? \\times ?');
        dEqual(result.warnings, ["Produit: nb args < 2"]);
    });

    it('should fail if a leaf is not allowed', function() {
        assert.throw(function () {renderLispAsLaTeX('(a bc)');},
                     Error, "Bad leaf: bc");
    });
});

var properties = clubExpr.properties;

describe('#properties', function() {
    it('should detect all the properties of a flat expr', function() {
        dEqual(properties(['Somme', 'a', 1]),
                {conventions: [],
                 depth: 1,
                 leaves: 2,
                 letters: 1,
                 nature: "Somme",
                 nbOps: 1,
                 numbers: 1,
                 ops: ["Somme"],
                 uniqueOps: ["Somme"]});
    });

    it('should not error on the official expressions', function() {
        expressions.forEach(function (exprObj) {
            properties(exprObj.expr);
        });
    });
});

var replace = clubExpr.replaceValuesWith;

describe('#replace', function() {
    it('should replace "a" with "x" and 1 with 2 in a flat expr', function() {
        dEqual(replace(['Somme', 'a', 1], {'a':'x', 1:2}),
                       ['Somme', 'x', 2]);
    });

    it('should replace "a" with "x" and conversely in a flat expr', function() {
        dEqual(replace(['Somme', 'a', 'x'], {'a':'x', 'x':'a'}),
                       ['Somme', 'x', 'a']);
    });

    it('should replace "a" with "x" and 1 with 2 in a nested expr', function() {
        dEqual(replace(['Produit', ['Somme', 1, 'a'], 3], {'a':'x', 1:2}),
                       ['Produit', ['Somme', 2, 'x'], 3]);
    });
});
<commit_msg>Add a test for two warnings<commit_after>'use strict';

var chai = require('chai');
chai.use(require('chai-shallow-deep-equal'));
var assert = chai.assert;
var equal = chai.assert.equal;
var dEqual = chai.assert.deepEqual;
var sdEqual = chai.assert.shallowDeepEqual;
function treeEquals(x, y) {return sdEqual(x.tree, y);}

var clubExpr = require('../index');

var parse = clubExpr.parse;

describe('#parse', function() {
    it('should fail if the source contains bad characters', function() {
        assert.throw(function () {parse('"');},
                     Error, "Invalid char: \"");
    });

    it('should fail if the source is empty', function() {
        assert.throw(function () {parse('');},
                     Error, "Empty expr");
    });

    it('should fail if a ( is the only char', function() {
        assert.throw(function () {parse('(');},
                     Error, "Missing cmd");
    });

    it('should parse an expression with no arg', function() {
        treeEquals(parse('(a)'), ['a']);
    });

    it('should parse an expression with no arg (longer command)', function() {
        treeEquals(parse('(ab)'), ['ab']);
    });

    it('should parse a single expression with one arg', function() {
        treeEquals(parse('(a b)'), ['a', 'b']);
    });

    it('should parse a single expression with two args', function() {
        treeEquals(parse('(a b c)'), ['a', 'b', 'c']);
    });

    it('should parse a nested expression', function() {
        treeEquals(parse('(a (b c) d)'), ['a', ['b', 'c'], 'd']);
    });

    it('should parse an expression with creative whitespace', function() {
        treeEquals(parse(' ( a \n b \t c ) '), ['a', 'b', 'c']);
    });

    it('should fail if the starting ( is missing (1 token)', function() {
        assert.throw(function () {parse('a');},
                     Error, "Missing starting (");
    });

    it('should fail if the starting ( is missing (2 tokens)', function() {
        assert.throw(function () {parse('a b');},
                     Error, "Missing starting (");
    });

    it('should fail if a double ( is found', function() {
        assert.throw(function () {parse('((');},
                     Error, "Double (");
    });

    it('should warn us if a ( is the last significant char', function() {
        var result = parse('(a b (');
        dEqual(result.warnings, ["Missing cmd", "Missing )"]);
        assert.deepEqual(result.tree, ['a', 'b']);
    });

    it('should warn us if a ) is missing', function() {
        var result = parse('(a b');
        dEqual(result.warnings, ["Missing )"]);
        dEqual(result.tree, ['a', 'b']);
    });

    it('should warn us if a ) is missing in a nested expression', function() {
        var result = parse('(a (b 2)');
        dEqual(result.warnings, ["Missing )"]);
        dEqual(result.tree, ['a', ['b', '2']]);
    });

    it('should warn us if the expr is already closed', function() {
        var result = parse('(a b) c');
        dEqual(result.warnings, ["Already closed"]);
        dEqual(result.tree, ['a', 'b']);
    });

    it('should warn us if a ) is trailing', function() {
        var result = parse('(a b))');
        dEqual(result.warnings, ["Already closed"]);
        dEqual(result.tree, ['a', 'b']);
    });

    it('should warn us if a ( is trailing', function() {
        var result = parse('(a b) (');
        dEqual(result.warnings, ["Already closed"]);
        dEqual(result.tree, ['a', 'b']);
    });

    it('should fail if a command is missing', function() {
        assert.throw(function () {parse('()');},
                     Error, "Missing cmd");
    });
});

var natureFromLisp = clubExpr.natureFromLisp;

describe('#natureFromLisp', function() {
    it('should fail silently for some malformed expressions', function() {
        var expressions = ['', 'b', ')', '(S'];
        expressions.forEach(function (src) {
            equal(natureFromLisp(src), '');
        });
    });

    it('should fail silently for some wellformed expressions', function() {
        var expressions = ['(S 1 1)'];
        expressions.forEach(function (src) {
            equal(natureFromLisp(src), '');
        });
    });

    it('should return the correct nature of some expressions', function() {
        var exprsAndNatures = [
            ['Somme 1 1)', 'Somme'],
            [' Somme 1 1)', 'Somme'],
            ['  Somme 1 1)', 'Somme'],
            ['Somme(', 'Somme'],
            ['(Somme 1 1)', 'Somme'],
            [' (Somme 1 1)', 'Somme'],
            ['( Somme 1 1)', 'Somme'],
            ['(Somme(', 'Somme'],
            [' ( Somme 1 1)', 'Somme']
        ];
        exprsAndNatures.forEach(function (exprAndNature) {
            var src =    exprAndNature[0];
            var nature = exprAndNature[1];
            equal(natureFromLisp(src), nature);
        });
    });
});

var renderExprAsLisp = clubExpr.renderExprAsLisp;

describe('#renderExprAsLisp', function() {
    it('should render a single expression with one arg', function() {
        equal(renderExprAsLisp(['a', 'b']), '(a b)');
    });

    it('should render a single expression with two args', function() {
        equal(renderExprAsLisp(['a', 'b', 'c']), '(a b c)');
    });

    it('should render a nested expression', function() {
        equal(renderExprAsLisp(['a', ['b', 'c'], 'd']), '(a (b c) d)');
    });
});

var expressions = clubExpr.expressions;

describe('#renderingParsingRoundTrip', function() {
    it('should round trip on all official expressions', function() {
        expressions.forEach(function (exprObj) {
            var expr = exprObj.expr;
            // Here [ 'Somme', '1', '2' ] equals [ 'Somme', 1, 2 ]
            treeEquals(parse(renderExprAsLisp(expr)), expr);
        });
    });
});

var renderExprAsLaTeX = clubExpr.renderExprAsLaTeX;
var exprsRenderedAsLaTeX = [
            '1+2',
            'a+1',
            '1+a',
            'a+b',
            '1-2',
            'a-1',
            '1-a',
            'a-b',
            '1 \\times 2',
            '1 a',
            'a \\times 1',
            'a b',
            'a \\times a',
            '\\frac{1}{2}',
            '\\frac{a}{1}',
            '\\frac{1}{a}',
            '\\frac{a}{b}',
            '{1}^{2}',
            '{a}^{2}',
            '{a}^{1}',
            '\\frac{1}{a}',
            '-a',
            '1 \\left(a+2\\right)',
            '1 \\left(a-2\\right)',
            '1+2 a',
            '1+\\left(-2\\right)',
            '1+\\left(-a\\right)',
            '1-\\left(-2\\right)',
            '1-\\left(-a\\right)',
            '1 a-2',
            '1+\\frac{a}{2}',
            '-a-1',
            '-\\left(a-1\\right)',
            '1-\\left(2+a\\right)',
            'a-1+2',
            '\\frac{a+b}{1}',
            '\\frac{1}{a+1}',
            '{a}^{b+c}',
            '{a}^{{b}^{c}}',
            '{\\left({a}^{b}\\right)}^{c}',
            '\\frac{1 a}{2}',
            '-1 a',
            '-\\frac{1}{a}',
            '-\\frac{a}{b}',
            '-{a}^{2}',
            '{\\left(-a\\right)}^{2}',
            '{\\left(1+a\\right)}^{2}',
            '{\\left(a+b\\right)}^{2}',
            '{\\left(a-b\\right)}^{2}',
            '1+{a}^{2}',
            '1 {a}^{2}',
            '{\\left(1 a\\right)}^{2}',
            '\\frac{{a}^{2}}{1}',
            '{\\left(\\frac{a}{1}\\right)}^{2}',
            'a \\times 1 a',
            '{\\left(\\frac{1}{a}\\right)}^{2}',
            '\\frac{1}{{a}^{2}}',
            '\\sqrt{{a}^{2}}',
            '{a}^{2}+{b}^{2}',
            '\\sqrt{{a}^{2}+{b}^{2}}',
            'a \\left(1+2 a\\right)',
            '1 a-\\left(2+a\\right)',
            '1 a-2 a',
            '1 \\left(a-2\\right)-3',
            '1-2 \\left(a+3\\right)',
            '1 a-a+2',
            'a-\\frac{a}{1}+2',
            '{a}^{2}-a+1',
            '{\\left(a+1\\right)}^{2}-2',
            '\\left(a+1\\right) \\left(a-2\\right)',
            '-{a}^{2}+1',
            '1 {a}^{2}+a',
            '-a-a+1',
            'a-\\left(-a\\right)+1',
            '{a}^{2}-a+1',
            'a-1 \\left(a-2\\right)+3',
            '\\frac{a+1}{a-2}',
            '1+\\frac{2}{a+3}',
            '\\sqrt{{\\left(1-2\\right)}^{2}+{\\left(3+4\\right)}^{2}}',
            '1 \\left(2+3\\right) \\left(4+5\\right)',
            '{a}^{2}+1 a+2',
            '1 {\\left(a+2\\right)}^{2}-3',
            '1 \\left(2+3\\right) \\left(4-5\\right)',
            '{\\left(a+1\\right)}^{2}-{a}^{2}',
            '1 {a}^{2}+2 a+3',
            '\\frac{1 a+2}{3 a+4}',
            '1+\\frac{2}{3 a+4}'];

describe('#renderExprAsLaTeX', function() {
    it('should render the official expressions correctly', function() {
        expressions.forEach(function (exprObj, idx) {
            var expr = exprObj.expr;
            equal(renderExprAsLaTeX(expr).latex, exprsRenderedAsLaTeX[idx]);
        });
    });
});

var renderLispAsLaTeX = clubExpr.renderLispAsLaTeX;

describe('#renderLispAsLaTeX', function() {
    it('should render a single expression with one arg', function() {
        var result = renderLispAsLaTeX('(Racine b)');
        equal(result.latex, '\\sqrt{b}');
        dEqual(result.warnings, []);
    });

    it('should render a single expression with two args', function() {
        var result = renderLispAsLaTeX('(Somme a b)');
        equal(result.latex, 'a+b');
        dEqual(result.warnings, []);
    });

    it('should render a nested expression', function() {
        var result = renderLispAsLaTeX('(Somme a (Produit b c))');
        equal(result.latex, 'a+b c');
        dEqual(result.warnings, []);
    });

    it('should render an expression with a greek letter', function() {
        var result = renderLispAsLaTeX('(Produit 2 pi)');
        equal(result.latex, '2 \\pi');
        dEqual(result.warnings, []);
    });

    it('should render an expression and warn us if a ) is missing', function() {
        var result = renderLispAsLaTeX('(Somme 1 2');
        equal(result.latex, '1+2');
        dEqual(result.warnings, ["Missing )"]);
    });

    it('should warn us if the command is unknown', function() {
        var result = renderLispAsLaTeX('(Unk a b)');
        equal(result.latex, '?');
        dEqual(result.warnings, ["Unknown cmd: Unk"]);
    });

    it('should render an expr with an incomplete cmd name and warn us', function() {
        var result = renderLispAsLaTeX('(Somm)');
        equal(result.latex, '?');
        dEqual(result.warnings, ["Unknown cmd: Somm"]);
    });

    it('should render a Somme with no arg and warn us', function() {
        var result = renderLispAsLaTeX('(Somme)');
        equal(result.latex, '?+?');
        dEqual(result.warnings, ["Somme: nb args < 2"]);
    });

    it('should render a Somme with one arg and warn us', function() {
        var result = renderLispAsLaTeX('(Somme 1)');
        equal(result.latex, '1+?');
        dEqual(result.warnings, ["Somme: nb args < 2"]);
    });

    it('should warn us if too few args for Diff', function() {
        var result = renderLispAsLaTeX('(Diff)');
        equal(result.latex, '?-?');
        dEqual(result.warnings, ["Diff: nb args < 2"]);
    });

    it('should warn us if too few args for Diff', function() {
        var result = renderLispAsLaTeX('(Diff 1)');
        equal(result.latex, '1-?');
        dEqual(result.warnings, ["Diff: nb args < 2"]);
    });

    it('should warn us if too many args for Diff', function() {
        var result = renderLispAsLaTeX('(Diff a b c)');
        equal(result.latex, 'a-b\\bigotimes');
        dEqual(result.warnings, ["Diff: nb args > 2"]);
    });

    it('should warn us if too many args for Diff and paren missing', function() {
        var result = renderLispAsLaTeX('(Diff a b c');
        equal(result.latex, 'a-b\\bigotimes');
        dEqual(result.warnings, ["Missing )", "Diff: nb args > 2"]);
    });

    it('should warn us if too few args for Produit', function() {
        var result = renderLispAsLaTeX('(Produit)');
        equal(result.latex, '? \\times ?');
        dEqual(result.warnings, ["Produit: nb args < 2"]);
    });

    it('should fail if a leaf is not allowed', function() {
        assert.throw(function () {renderLispAsLaTeX('(a bc)');},
                     Error, "Bad leaf: bc");
    });
});

var properties = clubExpr.properties;

describe('#properties', function() {
    it('should detect all the properties of a flat expr', function() {
        dEqual(properties(['Somme', 'a', 1]),
                {conventions: [],
                 depth: 1,
                 leaves: 2,
                 letters: 1,
                 nature: "Somme",
                 nbOps: 1,
                 numbers: 1,
                 ops: ["Somme"],
                 uniqueOps: ["Somme"]});
    });

    it('should not error on the official expressions', function() {
        expressions.forEach(function (exprObj) {
            properties(exprObj.expr);
        });
    });
});

var replace = clubExpr.replaceValuesWith;

describe('#replace', function() {
    it('should replace "a" with "x" and 1 with 2 in a flat expr', function() {
        dEqual(replace(['Somme', 'a', 1], {'a':'x', 1:2}),
                       ['Somme', 'x', 2]);
    });

    it('should replace "a" with "x" and conversely in a flat expr', function() {
        dEqual(replace(['Somme', 'a', 'x'], {'a':'x', 'x':'a'}),
                       ['Somme', 'x', 'a']);
    });

    it('should replace "a" with "x" and 1 with 2 in a nested expr', function() {
        dEqual(replace(['Produit', ['Somme', 1, 'a'], 3], {'a':'x', 1:2}),
                       ['Produit', ['Somme', 2, 'x'], 3]);
    });
});
<|endoftext|>